public inbox for [email protected]
help / color / mirror / Atom feedFrom: Dean Rasheed <[email protected]>
To: John Naylor <[email protected]>
Cc: Andrew Dunstan <[email protected]>
Cc: PostgreSQL Hackers <[email protected]>
Subject: Re: Global temporary tables
Date: Tue, 7 Jul 2026 23:37:11 +0100
Message-ID: <CAEZATCVjaZFa80S-V_+nvnzRK1ZiLUPx8jwdyAtRVHAmPZQQMg@mail.gmail.com> (raw)
In-Reply-To: <CANWCAZah-o=gGsKA2+Zr5JZ82D_aWSR1t22mh0WK7eDeoSw5Ag@mail.gmail.com>
References: <CAEZATCU_goy9GiN75Rhqwg8=AEHrujYdf+cF9ORMGg98Tc44YA@mail.gmail.com>
<[email protected]>
<CANWCAZbAFmULzmenAG3kLk68d2Vt+3k9pBUSrdxYxUDwUoaj0Q@mail.gmail.com>
<CAEZATCXz3TCmjH=KfehyKrXrJkCf8TsgeE5S4v7O+g6vb87KVg@mail.gmail.com>
<CAEZATCUhknD0kAkXVJeehvP10sEAwV8LCCNecrRvbBLm2vavCQ@mail.gmail.com>
<CANWCAZah-o=gGsKA2+Zr5JZ82D_aWSR1t22mh0WK7eDeoSw5Ag@mail.gmail.com>
On Tue, 7 Jul 2026 at 10:06, John Naylor <[email protected]> wrote:
>
> Okay, I followed up with various attempts to get the new sub XID
> tracking to do something dire, and couldn't find anything.
>
Unfortunately, it still wasn't working properly. In a fresh session,
doing VACUUM FREEZE on a GTT with ON COMMIT DELETE ROWS would fail
because the VACUUM FREEZE would update a new pg_temp_class tuple,
flushing it to the database, and then the ON COMMIT code would fail to
find it, because there was no intervening CommandCounterIncrement() to
make it visible.
I don't want to go round adding CommandCounterIncrement()'s to fix
things like this -- part of the reason for having the pending inserts
was to make that unnecessary. So instead, in v8, I have made it so
that the pending insert entries are kept up-to-date all the way to the
end of the transaction, even if they're flushed to the database before
that. This turns out to make the code far simpler, and easier to
reason about, as well as hopefully being more robust. So I feel much
better about this formulation of pending inserts.
> I did find something else, however:
>
> set statement_timeout = '3s';
> set debug_discard_caches = 1;
>
> create global temporary table g (a int);
>
> This enters an infinite loop
>
Doh! Fixed in v8.
Regards,
Dean
Attachments:
[text/x-patch] v8-0001-Save-temporary-table-ON-COMMIT-actions-to-pg_clas.patch (12.8K, ../CAEZATCVjaZFa80S-V_+nvnzRK1ZiLUPx8jwdyAtRVHAmPZQQMg@mail.gmail.com/2-v8-0001-Save-temporary-table-ON-COMMIT-actions-to-pg_clas.patch)
download | inline diff:
From 7bcdac5da6706f041c471d7d4e88e2743b937c98 Mon Sep 17 00:00:00 2001
From: Dean Rasheed <[email protected]>
Date: Fri, 26 Jun 2026 18:19:33 +0100
Subject: [PATCH v8 01/10] Save temporary table ON COMMIT actions to pg_class.
This adds a new column reloncommit to pg_class and uses it to save the
ON COMMIT actions of temporary tables. This is then used by psql's \d
meta-command to display the table's ON COMMIT action.
This will be required by global temporary tables in future commits.
---
doc/src/sgml/catalogs.sgml | 15 +++++++++++++
src/backend/bootstrap/bootparse.y | 1 +
src/backend/catalog/heap.c | 6 ++++-
src/backend/catalog/index.c | 1 +
src/backend/utils/cache/relcache.c | 19 +++++++++++++++-
src/bin/psql/describe.c | 35 +++++++++++++++++++++++++++++-
src/include/catalog/heap.h | 1 +
src/include/catalog/pg_class.h | 8 +++++++
src/include/utils/relcache.h | 4 +++-
src/test/regress/expected/temp.out | 18 +++++++++++++++
src/test/regress/sql/temp.sql | 6 +++++
11 files changed, 110 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 4b474c13917..cd2be3ea3f2 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2171,6 +2171,21 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>reloncommit</structfield> <type>char</type>
+ </para>
+ <para>
+ The <literal>ON COMMIT</literal> behavior of the table:
+ <literal>p</literal> = preserve rows,
+ <literal>d</literal> = delete rows,
+ <literal>D</literal> = drop;
+ see <link linkend="sql-createtable"><command>CREATE TABLE</command></link>.
+ Preserve rows is the default; the other options are only supported for
+ temporary tables.
+ </para></entry>
+ </row>
+
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>relnatts</structfield> <type>int2</type>
diff --git a/src/backend/bootstrap/bootparse.y b/src/backend/bootstrap/bootparse.y
index 943ff4733d3..305a5654ff3 100644
--- a/src/backend/bootstrap/bootparse.y
+++ b/src/backend/bootstrap/bootparse.y
@@ -214,6 +214,7 @@ Boot_CreateStmt:
RELPERSISTENCE_PERMANENT,
shared_relation,
mapped_relation,
+ ONCOMMIT_NOOP,
true,
&relfrozenxid,
&relminmxid,
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 88087654de9..684a44946fc 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -294,6 +294,7 @@ heap_create(const char *relname,
char relpersistence,
bool shared_relation,
bool mapped_relation,
+ OnCommitAction oncommit,
bool allow_system_table_mods,
TransactionId *relfrozenxid,
MultiXactId *relminmxid,
@@ -371,7 +372,8 @@ heap_create(const char *relname,
shared_relation,
mapped_relation,
relpersistence,
- relkind);
+ relkind,
+ oncommit);
/*
* Have the storage manager create the relation's disk file, if needed.
@@ -958,6 +960,7 @@ InsertPgClassTuple(Relation pg_class_desc,
values[Anum_pg_class_relisshared - 1] = BoolGetDatum(rd_rel->relisshared);
values[Anum_pg_class_relpersistence - 1] = CharGetDatum(rd_rel->relpersistence);
values[Anum_pg_class_relkind - 1] = CharGetDatum(rd_rel->relkind);
+ values[Anum_pg_class_reloncommit - 1] = CharGetDatum(rd_rel->reloncommit);
values[Anum_pg_class_relnatts - 1] = Int16GetDatum(rd_rel->relnatts);
values[Anum_pg_class_relchecks - 1] = Int16GetDatum(rd_rel->relchecks);
values[Anum_pg_class_relhasrules - 1] = BoolGetDatum(rd_rel->relhasrules);
@@ -1339,6 +1342,7 @@ heap_create_with_catalog(const char *relname,
relpersistence,
shared_relation,
mapped_relation,
+ oncommit,
allow_system_table_mods,
&relfrozenxid,
&relminmxid,
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 9407c357f27..ffc7f6c254e 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -989,6 +989,7 @@ index_create(Relation heapRelation,
relpersistence,
shared_relation,
mapped_relation,
+ ONCOMMIT_NOOP,
allow_system_table_mods,
&relfrozenxid,
&relminmxid,
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index fb4e042be8a..1b20f1f82e2 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -1952,6 +1952,7 @@ formrdesc(const char *relationName, Oid relationReltype,
relation->rd_rel->relallvisible = 0;
relation->rd_rel->relallfrozen = 0;
relation->rd_rel->relkind = RELKIND_RELATION;
+ relation->rd_rel->reloncommit = RELONCOMMIT_PRESERVE_ROWS;
relation->rd_rel->relnatts = (int16) natts;
/*
@@ -3524,7 +3525,8 @@ RelationBuildLocalRelation(const char *relname,
bool shared_relation,
bool mapped_relation,
char relpersistence,
- char relkind)
+ char relkind,
+ OnCommitAction oncommit)
{
Relation rel;
MemoryContext oldcxt;
@@ -3668,6 +3670,21 @@ RelationBuildLocalRelation(const char *relname,
break;
}
+ /* set up oncommit behavior */
+ switch (oncommit)
+ {
+ case ONCOMMIT_NOOP:
+ case ONCOMMIT_PRESERVE_ROWS:
+ rel->rd_rel->reloncommit = RELONCOMMIT_PRESERVE_ROWS;
+ break;
+ case ONCOMMIT_DELETE_ROWS:
+ rel->rd_rel->reloncommit = RELONCOMMIT_DELETE_ROWS;
+ break;
+ case ONCOMMIT_DROP:
+ rel->rd_rel->reloncommit = RELONCOMMIT_DROP;
+ break;
+ }
+
/* if it's a materialized view, it's not populated initially */
if (relkind == RELKIND_MATVIEW)
rel->rd_rel->relispopulated = false;
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index fef86b4cca3..e710db8e102 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1580,6 +1580,7 @@ describeOneTableDetails(const char *schemaname,
char relpersistence;
char relreplident;
char *relam;
+ char reloncommit;
} tableinfo;
bool show_column_details = false;
@@ -1594,7 +1595,25 @@ describeOneTableDetails(const char *schemaname,
/* Get general table info */
printfPQExpBuffer(&buf, "/* %s */\n",
_("Get general information about one relation"));
- if (pset.sversion >= 120000)
+ if (pset.sversion >= 200000)
+ {
+ appendPQExpBuffer(&buf,
+ "SELECT c.relchecks, c.relkind, c.relhasindex, c.relhasrules, "
+ "c.relhastriggers, c.relrowsecurity, c.relforcerowsecurity, "
+ "false AS relhasoids, c.relispartition, %s, c.reltablespace, "
+ "CASE WHEN c.reloftype = 0 THEN '' ELSE c.reloftype::pg_catalog.regtype::pg_catalog.text END, "
+ "c.relpersistence, c.relreplident, am.amname, c.reloncommit\n"
+ "FROM pg_catalog.pg_class c\n "
+ "LEFT JOIN pg_catalog.pg_class tc ON (c.reltoastrelid = tc.oid)\n"
+ "LEFT JOIN pg_catalog.pg_am am ON (c.relam = am.oid)\n"
+ "WHERE c.oid = '%s';",
+ (verbose ?
+ "pg_catalog.array_to_string(c.reloptions || "
+ "array(select 'toast.' || x from pg_catalog.unnest(tc.reloptions) x), ', ')\n"
+ : "''"),
+ oid);
+ }
+ else if (pset.sversion >= 120000)
{
appendPQExpBuffer(&buf,
"SELECT c.relchecks, c.relkind, c.relhasindex, c.relhasrules, "
@@ -1662,6 +1681,10 @@ describeOneTableDetails(const char *schemaname,
NULL : pg_strdup(PQgetvalue(res, 0, 14));
else
tableinfo.relam = NULL;
+ if (pset.sversion >= 200000)
+ tableinfo.reloncommit = *(PQgetvalue(res, 0, 15));
+ else
+ tableinfo.reloncommit = RELONCOMMIT_PRESERVE_ROWS;
PQclear(res);
res = NULL;
@@ -3670,6 +3693,16 @@ describeOneTableDetails(const char *schemaname,
printfPQExpBuffer(&buf, _("Access method: %s"), tableinfo.relam);
printTableAddFooter(&cont, buf.data);
}
+
+ /* On-commit action */
+ if (verbose && tableinfo.reloncommit != RELONCOMMIT_PRESERVE_ROWS)
+ {
+ printfPQExpBuffer(&buf, _("On-commit action: %s"),
+ tableinfo.reloncommit == RELONCOMMIT_DELETE_ROWS ? "ON COMMIT DELETE ROWS" :
+ tableinfo.reloncommit == RELONCOMMIT_DROP ? "ON COMMIT DROP" :
+ "???");
+ printTableAddFooter(&cont, buf.data);
+ }
}
/* reloptions, if verbose */
diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h
index 6c9ac812aa0..a622d703e97 100644
--- a/src/include/catalog/heap.h
+++ b/src/include/catalog/heap.h
@@ -59,6 +59,7 @@ extern Relation heap_create(const char *relname,
char relpersistence,
bool shared_relation,
bool mapped_relation,
+ OnCommitAction oncommit,
bool allow_system_table_mods,
TransactionId *relfrozenxid,
MultiXactId *relminmxid,
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index c4af599dc90..0e2c53591c4 100644
--- a/src/include/catalog/pg_class.h
+++ b/src/include/catalog/pg_class.h
@@ -88,6 +88,9 @@ CATALOG(pg_class,1259,RelationRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(83,Relat
/* see RELKIND_xxx constants below */
char relkind BKI_DEFAULT(r);
+ /* see RELONCOMMIT_xxx constants below */
+ char reloncommit BKI_DEFAULT(p);
+
/* number of user attributes */
int16 relnatts BKI_DEFAULT(0); /* genbki.pl will fill this in */
@@ -184,6 +187,11 @@ MAKE_SYSCACHE(RELNAMENSP, pg_class_relname_nsp_index, 128);
#define RELPERSISTENCE_UNLOGGED 'u' /* unlogged permanent table */
#define RELPERSISTENCE_TEMP 't' /* temporary table */
+/* on-commit action (only temporary tables support delete/drop) */
+#define RELONCOMMIT_PRESERVE_ROWS 'p' /* default: keep data on commit */
+#define RELONCOMMIT_DELETE_ROWS 'd' /* delete table rows on commit */
+#define RELONCOMMIT_DROP 'D' /* drop table on commit */
+
/* 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/utils/relcache.h b/src/include/utils/relcache.h
index 89c27aa1529..ed09a699e8b 100644
--- a/src/include/utils/relcache.h
+++ b/src/include/utils/relcache.h
@@ -17,6 +17,7 @@
#include "access/tupdesc.h"
#include "common/relpath.h"
#include "nodes/bitmapset.h"
+#include "nodes/primnodes.h"
/*
@@ -121,7 +122,8 @@ extern Relation RelationBuildLocalRelation(const char *relname,
bool shared_relation,
bool mapped_relation,
char relpersistence,
- char relkind);
+ char relkind,
+ OnCommitAction oncommit);
/*
* Routines to manage assignment of new relfilenumber to a relation
diff --git a/src/test/regress/expected/temp.out b/src/test/regress/expected/temp.out
index a50c7ae88a9..d3542a60242 100644
--- a/src/test/regress/expected/temp.out
+++ b/src/test/regress/expected/temp.out
@@ -42,6 +42,12 @@ SELECT * FROM temptest;
DROP TABLE temptest;
-- test temp table deletion
CREATE TEMP TABLE temptest(col int);
+SELECT reloncommit FROM pg_class WHERE oid = 'temptest'::regclass;
+ reloncommit
+-------------
+ p
+(1 row)
+
\c
SELECT * FROM temptest;
ERROR: relation "temptest" does not exist
@@ -49,6 +55,12 @@ LINE 1: SELECT * FROM temptest;
^
-- Test ON COMMIT DELETE ROWS
CREATE TEMP TABLE temptest(col int) ON COMMIT DELETE ROWS;
+SELECT reloncommit FROM pg_class WHERE oid = 'temptest'::regclass;
+ reloncommit
+-------------
+ d
+(1 row)
+
-- while we're here, verify successful truncation of index with SQL function
CREATE INDEX ON temptest(bit_length(''));
BEGIN;
@@ -86,6 +98,12 @@ DROP TABLE temptest;
-- Test ON COMMIT DROP
BEGIN;
CREATE TEMP TABLE temptest(col int) ON COMMIT DROP;
+SELECT reloncommit FROM pg_class WHERE oid = 'temptest'::regclass;
+ reloncommit
+-------------
+ D
+(1 row)
+
INSERT INTO temptest VALUES (1);
INSERT INTO temptest VALUES (2);
SELECT * FROM temptest;
diff --git a/src/test/regress/sql/temp.sql b/src/test/regress/sql/temp.sql
index d50472ddced..01a5399bc96 100644
--- a/src/test/regress/sql/temp.sql
+++ b/src/test/regress/sql/temp.sql
@@ -47,6 +47,8 @@ DROP TABLE temptest;
CREATE TEMP TABLE temptest(col int);
+SELECT reloncommit FROM pg_class WHERE oid = 'temptest'::regclass;
+
\c
SELECT * FROM temptest;
@@ -55,6 +57,8 @@ SELECT * FROM temptest;
CREATE TEMP TABLE temptest(col int) ON COMMIT DELETE ROWS;
+SELECT reloncommit FROM pg_class WHERE oid = 'temptest'::regclass;
+
-- while we're here, verify successful truncation of index with SQL function
CREATE INDEX ON temptest(bit_length(''));
@@ -85,6 +89,8 @@ BEGIN;
CREATE TEMP TABLE temptest(col int) ON COMMIT DROP;
+SELECT reloncommit FROM pg_class WHERE oid = 'temptest'::regclass;
+
INSERT INTO temptest VALUES (1);
INSERT INTO temptest VALUES (2);
--
2.51.0
[text/x-patch] v8-0002-Basic-support-for-global-temporary-tables.patch (141.8K, ../CAEZATCVjaZFa80S-V_+nvnzRK1ZiLUPx8jwdyAtRVHAmPZQQMg@mail.gmail.com/3-v8-0002-Basic-support-for-global-temporary-tables.patch)
download | inline diff:
From 9af286bf6bb4981a41ff47cad54ab682863c0eb2 Mon Sep 17 00:00:00 2001
From: Dean Rasheed <[email protected]>
Date: Tue, 9 Jun 2026 11:14:27 +0100
Subject: [PATCH v8 02/10] Basic support for global temporary tables.
This allows global temporary tables to be created. When a global
temporary table is first accessed in a session, it is initialised by
creating local storage for it. All such storage created is tracked, in
case of rollback (in which case the table needs to be reinitialised),
and deleted on backend end.
All usage of global temporary tables is recorded in a shared hash
table to prevent certain operations like an ALTER TABLE rewrite, if
the table is being used by another session.
Lots more to do, to make a fully-baked feature...
---
contrib/pg_prewarm/pg_prewarm.c | 18 +
src/backend/access/heap/heapam_handler.c | 5 +-
src/backend/access/transam/xact.c | 26 +
src/backend/catalog/Makefile | 1 +
src/backend/catalog/catalog.c | 1 +
src/backend/catalog/global_temp.c | 1225 +++++++++++++++++
src/backend/catalog/heap.c | 21 +-
src/backend/catalog/information_schema.sql | 1 +
src/backend/catalog/meson.build | 1 +
src/backend/catalog/namespace.c | 15 +-
src/backend/catalog/pg_publication.c | 3 +-
src/backend/catalog/storage.c | 21 +-
src/backend/commands/dbcommands.c | 9 +-
src/backend/commands/lockcmds.c | 3 +-
src/backend/commands/propgraphcmds.c | 5 +
src/backend/commands/repack.c | 10 +
src/backend/commands/tablecmds.c | 88 +-
src/backend/commands/tablespace.c | 3 +-
src/backend/commands/view.c | 9 +
src/backend/optimizer/path/allpaths.c | 6 +-
src/backend/parser/gram.y | 38 +-
src/backend/postmaster/autovacuum.c | 16 +-
src/backend/postmaster/datachecksum_state.c | 23 +
src/backend/storage/buffer/bufmgr.c | 36 +-
.../utils/activity/wait_event_names.txt | 3 +
src/backend/utils/adt/dbsize.c | 3 +
src/backend/utils/cache/relcache.c | 52 +-
src/backend/utils/cache/relfilenumbermap.c | 3 +-
src/bin/pg_amcheck/pg_amcheck.c | 19 +-
src/bin/pg_dump/pg_dump.c | 21 +-
src/bin/pg_dump/pg_dump.h | 1 +
src/bin/pg_upgrade/info.c | 3 +
src/bin/psql/describe.c | 22 +-
src/bin/scripts/vacuuming.c | 4 +-
src/include/catalog/global_temp.h | 45 +
src/include/catalog/pg_class.h | 3 +-
src/include/catalog/storage.h | 2 +-
src/include/storage/lwlocklist.h | 3 +
src/include/storage/subsystemlist.h | 3 +
src/include/utils/rel.h | 23 +-
src/test/isolation/expected/global-temp.out | 252 ++++
src/test/isolation/isolation_schedule | 1 +
src/test/isolation/specs/global-temp.spec | 60 +
.../test_checksums/t/010_global_temp.pl | 56 +
src/test/regress/expected/alter_table.out | 6 +-
.../expected/create_property_graph.out | 2 +
src/test/regress/expected/create_table.out | 10 +-
src/test/regress/expected/create_view.out | 2 +-
src/test/regress/expected/global_temp.out | 290 ++++
src/test/regress/expected/type_sanity.out | 2 +-
src/test/regress/parallel_schedule | 4 +-
src/test/regress/sql/alter_table.sql | 5 +-
.../regress/sql/create_property_graph.sql | 1 +
src/test/regress/sql/create_table.sql | 6 +
src/test/regress/sql/global_temp.sql | 160 +++
src/test/regress/sql/type_sanity.sql | 2 +-
src/tools/pgindent/typedefs.list | 5 +
57 files changed, 2555 insertions(+), 103 deletions(-)
create mode 100644 src/backend/catalog/global_temp.c
create mode 100644 src/include/catalog/global_temp.h
create mode 100644 src/test/isolation/expected/global-temp.out
create mode 100644 src/test/isolation/specs/global-temp.spec
create mode 100644 src/test/modules/test_checksums/t/010_global_temp.pl
create mode 100644 src/test/regress/expected/global_temp.out
create mode 100644 src/test/regress/sql/global_temp.sql
diff --git a/contrib/pg_prewarm/pg_prewarm.c b/contrib/pg_prewarm/pg_prewarm.c
index c2716086693..ec91c12589c 100644
--- a/contrib/pg_prewarm/pg_prewarm.c
+++ b/contrib/pg_prewarm/pg_prewarm.c
@@ -15,6 +15,7 @@
#include <sys/stat.h>
#include <unistd.h>
+#include "access/parallel.h"
#include "access/relation.h"
#include "catalog/index.h"
#include "fmgr.h"
@@ -160,10 +161,27 @@ pg_prewarm(PG_FUNCTION_ARGS)
/* Check that the fork exists. */
if (!smgrexists(RelationGetSmgr(rel), forkNumber))
+ {
+ /*
+ * Normally, we treat this as an error, but in a parallel worker, it
+ * can happen for a global temporary relation that hasn't been used,
+ * and so has not been initialized. Treat this as empty storage.
+ */
+ if (IsParallelWorker() && RELATION_IS_GLOBAL_TEMP(rel))
+ {
+ relation_close(rel, AccessShareLock);
+
+ if (privOid != relOid)
+ UnlockRelationOid(privOid, AccessShareLock);
+
+ PG_RETURN_INT64(0);
+ }
+
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("fork \"%s\" does not exist for this relation",
forkString)));
+ }
/* Validate block numbers, or handle nulls. */
nblocks = RelationGetNumberOfBlocksInFork(rel, forkNumber);
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index 2268cc277bc..d9f64287c59 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -514,7 +514,7 @@ heapam_relation_set_new_filelocator(Relation rel,
*/
*minmulti = GetOldestMultiXactId();
- srel = RelationCreateStorage(*newrlocator, persistence, true);
+ srel = RelationCreateStorage(rel->rd_id, *newrlocator, persistence, true);
/*
* If required, set up an init fork for an unlogged table so that it can
@@ -557,7 +557,8 @@ heapam_relation_copy_data(Relation rel, const RelFileLocator *newrlocator)
* NOTE: any conflict in relfilenumber value will be caught in
* RelationCreateStorage().
*/
- dstrel = RelationCreateStorage(*newrlocator, rel->rd_rel->relpersistence, true);
+ dstrel = RelationCreateStorage(rel->rd_id, *newrlocator,
+ rel->rd_rel->relpersistence, true);
/* copy main fork */
RelationCopyStorage(RelationGetSmgr(rel), dstrel, MAIN_FORKNUM,
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 3a89149016f..e1b092014e9 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -32,6 +32,7 @@
#include "access/xlogrecovery.h"
#include "access/xlogutils.h"
#include "access/xlogwait.h"
+#include "catalog/global_temp.h"
#include "catalog/index.h"
#include "catalog/namespace.h"
#include "catalog/pg_enum.h"
@@ -2346,6 +2347,13 @@ CommitTransaction(void)
/* Shut down the deferred-trigger manager */
AfterTriggerEndXact(true);
+ /*
+ * Process dropped global temporary relations, deleting their local
+ * storage. This must happen before ON COMMIT handling so that we remove
+ * any ON COMMIT actions for dropped relations.
+ */
+ PreCommit_GlobalTempRelation();
+
/*
* Let ON COMMIT management do its thing (must happen after closing
* cursors, to avoid dangling-reference problems)
@@ -2462,6 +2470,9 @@ CommitTransaction(void)
/* Clean up the relation cache */
AtEOXact_RelationCache(true);
+ /* Clean up storage and usage records for global temporary relations */
+ AtEOXact_GlobalTempRelation(true);
+
/* Clean up the type cache */
AtEOXact_TypeCache();
@@ -2609,6 +2620,13 @@ PrepareTransaction(void)
/* Shut down the deferred-trigger manager */
AfterTriggerEndXact(true);
+ /*
+ * Process dropped global temporary relations, deleting their local
+ * storage. This must happen before ON COMMIT handling so that we remove
+ * any ON COMMIT actions for dropped relations.
+ */
+ PreCommit_GlobalTempRelation();
+
/*
* Let ON COMMIT management do its thing (must happen after closing
* cursors, to avoid dangling-reference problems)
@@ -2771,6 +2789,9 @@ PrepareTransaction(void)
/* Clean up the relation cache */
AtEOXact_RelationCache(true);
+ /* Clean up storage and usage records for global temporary relations */
+ AtEOXact_GlobalTempRelation(true);
+
/* Clean up the type cache */
AtEOXact_TypeCache();
@@ -3021,6 +3042,7 @@ AbortTransaction(void)
AtEOXact_Aio(false);
AtEOXact_Buffers(false);
AtEOXact_RelationCache(false);
+ AtEOXact_GlobalTempRelation(false);
AtEOXact_TypeCache();
AtEOXact_Inval(false);
AtEOXact_MultiXact();
@@ -5214,6 +5236,8 @@ CommitSubTransaction(void)
true, false);
AtEOSubXact_RelationCache(true, s->subTransactionId,
s->parent->subTransactionId);
+ AtEOSubXact_GlobalTempRelation(true, s->subTransactionId,
+ s->parent->subTransactionId);
AtEOSubXact_TypeCache();
AtEOSubXact_Inval(true);
AtSubCommit_smgr();
@@ -5399,6 +5423,8 @@ AbortSubTransaction(void)
AtEOXact_Aio(false);
AtEOSubXact_RelationCache(false, s->subTransactionId,
s->parent->subTransactionId);
+ AtEOSubXact_GlobalTempRelation(false, s->subTransactionId,
+ s->parent->subTransactionId);
AtEOSubXact_TypeCache();
AtEOSubXact_Inval(false);
ResourceOwnerRelease(s->curTransactionOwner,
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index 26fa0c9b18c..0fb085fd8ee 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -17,6 +17,7 @@ OBJS = \
aclchk.o \
catalog.o \
dependency.o \
+ global_temp.o \
heap.o \
index.o \
indexing.o \
diff --git a/src/backend/catalog/catalog.c b/src/backend/catalog/catalog.c
index be8791af875..65d786685f2 100644
--- a/src/backend/catalog/catalog.c
+++ b/src/backend/catalog/catalog.c
@@ -598,6 +598,7 @@ GetNewRelFileNumber(Oid reltablespace, Relation pg_class, char relpersistence)
switch (relpersistence)
{
case RELPERSISTENCE_TEMP:
+ case RELPERSISTENCE_GLOBAL_TEMP:
procNumber = ProcNumberForTempRelations();
break;
case RELPERSISTENCE_UNLOGGED:
diff --git a/src/backend/catalog/global_temp.c b/src/backend/catalog/global_temp.c
new file mode 100644
index 00000000000..098a3f4de0e
--- /dev/null
+++ b/src/backend/catalog/global_temp.c
@@ -0,0 +1,1225 @@
+/*-------------------------------------------------------------------------
+ *
+ * global_temp.c
+ * Global temporary relation management.
+ *
+ * This tracks all global temporary relations in use across all backends,
+ * as well as any local storage created for global temporary relations used
+ * in this backend.
+ *
+ * When a global temporary relation is created or first opened, it is
+ * initialized for use, which (for most relkinds) includes creating local
+ * storage for it. All global temporary relations in use and all local
+ * storage created is tracked, taking into account (sub)transaction
+ * rollback --- a rollback can undo the effects of creating or opening a
+ * global temporary relation, including the creation of local storage. If
+ * a global temporary relation is first opened in a (sub)transaction which
+ * is then rolled back, it is reinitialized the next time it is opened.
+ * When the backend exits, all locally created storage is deleted.
+ *
+ * Relcache invalidation messages are passed on to code here so that it can
+ * deal with other backends dropping global temporary relations. If a
+ * global temporary relation in use by this backed is dropped by another
+ * backend, all local storage created for the relation in this backend is
+ * deleted the next time a transaction commits. (It could perhaps be done
+ * sooner, but it's not crucial, since such storage will be deleted on
+ * backend exit anyway.)
+ *
+ * A shared hash table is used to track all global temporary relations in
+ * use across all databases and all backends. A "usage count" is kept for
+ * each relation, which is a count of the number of backends using it.
+ * This is used to prevent operations like ALTER TABLE from altering a
+ * global temporary table in a way that would require rewriting its
+ * contents, if it's in use by other backends, which cannot be allowed,
+ * since there is no way to rewrite the local storage of other backends.
+ *
+ * A global temporary relation is regarded as "in use" by a backend from
+ * the time it is created or first opened until the time it is dropped or
+ * the backend exits (or a rollback undoes the creation or opening of the
+ * relation). This means that a backend that executes CREATE GLOBAL TEMP
+ * TABLE is counted as using it, even if it never opens it.
+ *
+ * When a global temporary relation is not in use by any backend, it has no
+ * physical storage. Thus a global temporary relation must have a shared
+ * dependency on its tablespace to prevent the tablespace from being
+ * dropped while the relation is not being used.
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * src/backend/catalog/global_temp.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/parallel.h"
+#include "access/tableam.h"
+#include "access/xact.h"
+#include "access/xlogutils.h"
+#include "catalog/global_temp.h"
+#include "catalog/storage.h"
+#include "commands/tablecmds.h"
+#include "lib/dshash.h"
+#include "miscadmin.h"
+#include "storage/ipc.h"
+#include "storage/lwlock.h"
+#include "storage/shmem.h"
+#include "storage/subsystems.h"
+#include "utils/memutils.h"
+#include "utils/syscache.h"
+#include "utils/tuplestore.h"
+
+/*
+ * gtr_local_storage
+ *
+ * Hash table to track local storage created by this backend for global
+ * temporary relations.
+ */
+typedef struct GtrStorageEntry
+{
+ RelFileLocator rlocator; /* lookup key: relfilelocator of storage */
+ Oid relid; /* OID of relation owning the storage */
+ SubTransactionId created_subid; /* storage was created in current xact */
+ SubTransactionId dropped_subid; /* dropped with another subid set */
+} GtrStorageEntry;
+
+static HTAB *gtr_local_storage;
+
+/*
+ * eoxact_storage_list[]
+ *
+ * List of relfilelocators of storage that (might) need AtEOXact cleanup
+ * work. As with the relcache's eoxact_list[], this list intentionally has
+ * limited size, and we switch to a full hash table traversal if the list
+ * overflows.
+ */
+#define MAX_EOXACT_STORAGE_LIST 32
+static RelFileLocator eoxact_storage_list[MAX_EOXACT_STORAGE_LIST];
+static int eoxact_storage_list_len = 0;
+static bool eoxact_storage_list_overflowed = false;
+
+#define EOXactStorageListAdd(rlocator) \
+ do { \
+ if (eoxact_storage_list_len < MAX_EOXACT_STORAGE_LIST) \
+ eoxact_storage_list[eoxact_storage_list_len++] = (rlocator); \
+ else \
+ eoxact_storage_list_overflowed = true; \
+ } while (0)
+
+/*
+ * gtr_local_usage
+ *
+ * Hash table to track global temporary relations in use in this backend.
+ */
+typedef struct GtrUsageEntry
+{
+ Oid relid; /* lookup key: OID of relation in use */
+ SubTransactionId started_subid; /* usage started in current xact */
+ SubTransactionId stopped_subid; /* usage ended with another subid set */
+} GtrUsageEntry;
+
+static HTAB *gtr_local_usage;
+
+/*
+ * eoxact_usage_list[]
+ *
+ * List of OIDs of global temporary relation usage entries that (might) need
+ * AtEOXact cleanup work. Cf. eoxact_storage_list[].
+ */
+#define MAX_EOXACT_USAGE_LIST 32
+static Oid eoxact_usage_list[MAX_EOXACT_USAGE_LIST];
+static int eoxact_usage_list_len = 0;
+static bool eoxact_usage_list_overflowed = false;
+
+#define EOXactUsageListAdd(relid) \
+ do { \
+ if (eoxact_usage_list_len < MAX_EOXACT_USAGE_LIST) \
+ eoxact_usage_list[eoxact_usage_list_len++] = (relid); \
+ else \
+ eoxact_usage_list_overflowed = true; \
+ } while (0)
+
+/*
+ * Invalidation message handling lists:
+ *
+ * gtrs_invalidated
+ * OIDs of global temporary relations that we are using, for which we
+ * have received an invalidation message.
+ *
+ * gtrs_dropped
+ * OIDs of global temporary relations that we were using, which have been
+ * dropped (by us or another backend).
+ */
+static List *gtrs_invalidated;
+static List *gtrs_dropped;
+
+/*
+ * gtr_shared_usage
+ *
+ * Shared hash table recording all global temporary relation usage across all
+ * databases and backends. For each relation, "usage_count" records the
+ * number of backends (including us) using the relation. Entries are
+ * removed when the usage count hits zero.
+ */
+typedef struct GtrSharedUsageKey
+{
+ Oid dbid; /* DB containing global temporary relation */
+ Oid relid; /* OID of global temporary relation */
+} GtrSharedUsageKey;
+
+typedef struct GtrSharedUsageEntry
+{
+ GtrSharedUsageKey key; /* lookup key: (dbid, relid) of relation */
+ int usage_count; /* number of backends accessing relation */
+} GtrSharedUsageEntry;
+
+static const dshash_parameters gtr_shared_usage_params = {
+ sizeof(GtrSharedUsageKey),
+ sizeof(GtrSharedUsageEntry),
+ dshash_memcmp,
+ dshash_memhash,
+ dshash_memcpy,
+ LWTRANCHE_GLOBAL_TEMP_REL_HASH
+};
+
+static dsa_area *gtr_shared_usage_dsa;
+static dshash_table *gtr_shared_usage;
+
+/*
+ * gtr_shmem_control
+ *
+ * Shared memory state for the global temporary relation shared usage table.
+ */
+typedef struct GlobalTempRelShmemControl
+{
+ dsa_handle dsa_handle; /* usage table's DSA handle */
+ dshash_table_handle dshash_handle; /* usage table's dshash handle */
+} GlobalTempRelShmemControl;
+
+static GlobalTempRelShmemControl *gtr_shmem_control;
+
+/*
+ * GlobalTempRelShmemCallbacks
+ *
+ * Callbacks to register our shared memory requirements and initialize it.
+ */
+static void
+gtr_shmem_request(void *arg)
+{
+ ShmemRequestStruct(.name = "Global Temporary Relation Usage Table",
+ .size = sizeof(GlobalTempRelShmemControl),
+ .ptr = (void **) >r_shmem_control,
+ );
+}
+
+static void
+gtr_shmem_init(void *arg)
+{
+ gtr_shmem_control->dsa_handle = DSA_HANDLE_INVALID;
+ gtr_shmem_control->dshash_handle = DSHASH_HANDLE_INVALID;
+}
+
+const ShmemCallbacks GlobalTempRelShmemCallbacks = {
+ .request_fn = gtr_shmem_request,
+ .init_fn = gtr_shmem_init,
+};
+
+/*
+ * gtr_delete_all_storage_on_exit
+ *
+ * Backend exit callback to delete all local storage created for global
+ * temporary relations in this backend.
+ */
+static void
+gtr_delete_all_storage_on_exit(int code, Datum arg)
+{
+ ProcNumber backend;
+ HASH_SEQ_STATUS status;
+ GtrStorageEntry *entry;
+ SMgrRelation srel;
+
+ /* Loop over all storage created and delete it */
+ backend = ProcNumberForTempRelations();
+ hash_seq_init(&status, gtr_local_storage);
+ while ((entry = hash_seq_search(&status)) != NULL)
+ {
+ srel = smgropen(entry->rlocator, backend);
+ smgrdounlinkall(&srel, 1, false);
+ smgrclose(srel);
+ }
+}
+
+/*
+ * gtr_init_storage_table
+ *
+ * Initialize the hash table recording local storage created for global
+ * temporary relations, if not already done.
+ */
+static void
+gtr_init_storage_table(void)
+{
+ if (gtr_local_storage == NULL)
+ {
+ HASHCTL ctl;
+
+ ctl.keysize = sizeof(RelFileLocator);
+ ctl.entrysize = sizeof(GtrStorageEntry);
+
+ gtr_local_storage =
+ hash_create("Global temporary relation storage table",
+ 128, &ctl, HASH_ELEM | HASH_BLOBS);
+
+ /* Register callback to delete all local storage on exit */
+ before_shmem_exit(gtr_delete_all_storage_on_exit, 0);
+ }
+}
+
+/*
+ * gtr_storage_dropped
+ *
+ * Invalidate a global temporary relation whose storage has been dropped.
+ *
+ * This is called as part of AtEO(Sub)Xact cleanup if storage creation is
+ * rolled back, or storage deletion is committed. This can happen several
+ * different ways:
+ *
+ * - The relation was initialized in a transaction which was then rolled
+ * back, causing the local storage created during initialization to be
+ * dropped.
+ *
+ * - An operation like REPACK or TRUNCATE committed and the old storage was
+ * dropped.
+ *
+ * - An operation like REPACK or TRUNCATE was rolled back and the new storage
+ * was dropped. The old storage may or may not still exist, depending on
+ * when it was created.
+ *
+ * - The table itself was dropped.
+ *
+ * Here, we have no way to distinguish between these cases, so we just mark
+ * the relation's relcache entry as invalid (if it still has one), forcing it
+ * to be reloaded and reinitialized when it is next opened. New storage for
+ * the relation will then be created, if needed.
+ */
+static void
+gtr_storage_dropped(Oid relid, RelFileLocator rlocator)
+{
+ /* If the relation is still in the relcache mark it as invalid */
+ RelationMarkInvalid(relid);
+
+ /*
+ * Remove the hash entry for the dropped storage, forcing the relation to
+ * create new storage if its relfilenode points to this storage after it
+ * is reloaded.
+ */
+ (void) hash_search(gtr_local_storage, &rlocator, HASH_REMOVE, NULL);
+}
+
+/*
+ * AtEOXact_StorageCleanup
+ *
+ * Clean up storage records for a single global temporary relation at
+ * main-transaction commit or abort.
+ *
+ * NB: this processing must be idempotent, because EOXactStorageListAdd()
+ * doesn't bother to prevent duplicate entries in eoxact_storage_list[].
+ */
+static void
+AtEOXact_StorageCleanup(GtrStorageEntry *entry, bool isCommit)
+{
+ /*
+ * If the storage no longer exists after this transaction ends, the global
+ * temporary relation that was using it may no longer have any storage.
+ * Mark the relation as invalid and remove the storage hash entry, forcing
+ * the relation to be reinitialized and have new storage created, if
+ * necessary, when it is next loaded. Otherwise, reset the hash entry's
+ * subids to zero.
+ */
+ if ((isCommit && entry->dropped_subid != InvalidSubTransactionId) ||
+ (!isCommit && entry->created_subid != InvalidSubTransactionId))
+ {
+ gtr_storage_dropped(entry->relid, entry->rlocator);
+ }
+ else
+ {
+ entry->created_subid = InvalidSubTransactionId;
+ entry->dropped_subid = InvalidSubTransactionId;
+ }
+}
+
+/*
+ * AtEOSubXact_StorageCleanup
+ *
+ * Clean up storage records for a single global temporary relation at
+ * subtransaction commit or abort.
+ *
+ * NB: this processing must be idempotent, because EOXactStorageListAdd()
+ * doesn't bother to prevent duplicate entries in eoxact_storage_list[].
+ */
+static void
+AtEOSubXact_StorageCleanup(GtrStorageEntry *entry, bool isCommit,
+ SubTransactionId mySubid,
+ SubTransactionId parentSubid)
+{
+ /*
+ * Is it storage created in the current subtransaction?
+ *
+ * During subcommit, mark it as belonging to the parent, instead, as long
+ * as it has not been deleted. Otherwise, the global temporary relation
+ * that was using this storage may no longer have any storage; mark the
+ * relation as invalid and remove the storage hash entry, forcing the
+ * relation to be reinitialized and have new storage created, if
+ * necessary.
+ */
+ if (entry->created_subid == mySubid)
+ {
+ Assert(entry->dropped_subid == mySubid ||
+ entry->dropped_subid == InvalidSubTransactionId);
+
+ if (isCommit && entry->dropped_subid == InvalidSubTransactionId)
+ entry->created_subid = parentSubid;
+ else
+ gtr_storage_dropped(entry->relid, entry->rlocator);
+ }
+
+ /* Update the storage dropped subid */
+ if (entry->dropped_subid == mySubid)
+ {
+ if (isCommit)
+ entry->dropped_subid = parentSubid;
+ else
+ entry->dropped_subid = InvalidSubTransactionId;
+ }
+}
+
+/*
+ * gtr_remove_all_usage_on_exit
+ *
+ * Backend exit callback to remove all records of this backend's use of
+ * global temporary relations from the shared usage hash table.
+ */
+static void
+gtr_remove_all_usage_on_exit(int code, Datum arg)
+{
+ HASH_SEQ_STATUS status;
+ GtrUsageEntry *local_entry;
+ GtrSharedUsageKey key;
+ GtrSharedUsageEntry *shared_entry;
+
+ /* Loop over all the global temporary relations we were using */
+ hash_seq_init(&status, gtr_local_usage);
+ while ((local_entry = hash_seq_search(&status)) != NULL)
+ {
+ /* Find the shared usage entry */
+ key.dbid = MyDatabaseId;
+ key.relid = local_entry->relid;
+ shared_entry = dshash_find(gtr_shared_usage, &key, true);
+
+ if (shared_entry->usage_count > 1)
+ {
+ /* Other backends are still using the relation */
+ shared_entry->usage_count--;
+ dshash_release_lock(gtr_shared_usage, shared_entry);
+ }
+ else
+ {
+ /* No more backends using it */
+ dshash_delete_entry(gtr_shared_usage, shared_entry);
+ }
+
+ /*
+ * NB: It is possible for gtr_remove_usage() to run after this, so we
+ * need to remove all local entries too.
+ */
+ (void) hash_search(gtr_local_usage,
+ &local_entry->relid, HASH_REMOVE, NULL);
+ }
+}
+
+/*
+ * gtr_init_usage_tables
+ *
+ * Initialize the local and shared usage hash tables for global temporary
+ * relations, if not already done.
+ */
+static void
+gtr_init_usage_tables(void)
+{
+ HASHCTL ctl;
+ MemoryContext oldcontext;
+
+ /* Local usage table */
+ if (gtr_local_usage == NULL)
+ {
+ ctl.keysize = sizeof(Oid);
+ ctl.entrysize = sizeof(GtrUsageEntry);
+
+ gtr_local_usage = hash_create("Global temporary relations in use locally",
+ 128, &ctl, HASH_ELEM | HASH_BLOBS);
+ }
+
+ /* Shared usage table */
+ if (gtr_shared_usage == NULL)
+ {
+ /* Use a lock to ensure only one process creates the table */
+ LWLockAcquire(GlobalTempRelControlLock, LW_EXCLUSIVE);
+
+ /* Be sure any local memory allocated by DSA routines is persistent */
+ oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+
+ if (gtr_shmem_control->dshash_handle == DSA_HANDLE_INVALID)
+ {
+ /* Initialize dynamic shared hash table to track shared usage */
+ gtr_shared_usage_dsa = dsa_create(LWTRANCHE_GLOBAL_TEMP_REL_DSA);
+ dsa_pin(gtr_shared_usage_dsa);
+ dsa_pin_mapping(gtr_shared_usage_dsa);
+
+ gtr_shared_usage = dshash_create(gtr_shared_usage_dsa,
+ >r_shared_usage_params, NULL);
+
+ /* Store handles in shared memory for other backends to use */
+ gtr_shmem_control->dsa_handle = dsa_get_handle(gtr_shared_usage_dsa);
+ gtr_shmem_control->dshash_handle =
+ dshash_get_hash_table_handle(gtr_shared_usage);
+ }
+ else
+ {
+ /* Attach to existing dynamic shared hash table */
+ gtr_shared_usage_dsa = dsa_attach(gtr_shmem_control->dsa_handle);
+ dsa_pin_mapping(gtr_shared_usage_dsa);
+
+ gtr_shared_usage = dshash_attach(gtr_shared_usage_dsa,
+ >r_shared_usage_params,
+ gtr_shmem_control->dshash_handle,
+ NULL);
+ }
+
+ MemoryContextSwitchTo(oldcontext);
+ LWLockRelease(GlobalTempRelControlLock);
+
+ /* Register callback to remove all our usage records on exit */
+ before_shmem_exit(gtr_remove_all_usage_on_exit, 0);
+ }
+}
+
+/*
+ * gtr_record_usage
+ *
+ * Record the fact that we're using a global temporary relation by adding
+ * entries to the local and shared usage hash tables.
+ *
+ * Note: This is intentionally idempotent.
+ */
+static void
+gtr_record_usage(Oid relid)
+{
+ GtrUsageEntry *local_entry;
+ GtrSharedUsageKey key;
+ GtrSharedUsageEntry *shared_entry;
+ bool found;
+
+ /* Initialize the usage tables, if necessary */
+ gtr_init_usage_tables();
+
+ /* Add local usage entry, if not already there */
+ local_entry = hash_search(gtr_local_usage, &relid, HASH_ENTER, &found);
+ if (found)
+ return; /* already recorded, nothing to do */
+
+ /* Record the usage as starting in the current subtransaction */
+ local_entry->started_subid = GetCurrentSubTransactionId();
+ local_entry->stopped_subid = InvalidSubTransactionId;
+
+ /* Flag the usage entry for eoxact cleanup */
+ EOXactUsageListAdd(relid);
+
+ /* Add/update shared usage entry */
+ key.dbid = MyDatabaseId;
+ key.relid = relid;
+ shared_entry = dshash_find_or_insert(gtr_shared_usage, &key, &found);
+
+ if (found)
+ shared_entry->usage_count++;
+ else
+ shared_entry->usage_count = 1;
+
+ dshash_release_lock(gtr_shared_usage, shared_entry);
+}
+
+/*
+ * gtr_remove_usage
+ *
+ * Remove our usage records for a global temporary relation that we're no
+ * longer using.
+ *
+ * Note: This is intentionally idempotent.
+ */
+static void
+gtr_remove_usage(Oid relid)
+{
+ GtrSharedUsageKey key;
+ GtrSharedUsageEntry *shared_entry;
+
+ /* Initialize the usage tables, if necessary */
+ gtr_init_usage_tables();
+
+ /* Remove local usage entry */
+ if (!hash_search(gtr_local_usage, &relid, HASH_REMOVE, NULL))
+ return; /* nothing to do */
+
+ /* Update/delete shared usage entry */
+ key.dbid = MyDatabaseId;
+ key.relid = relid;
+ shared_entry = dshash_find(gtr_shared_usage, &key, true);
+
+ if (shared_entry->usage_count > 1)
+ {
+ /* Other backends are still using the relation */
+ shared_entry->usage_count--;
+ dshash_release_lock(gtr_shared_usage, shared_entry);
+ }
+ else
+ {
+ /* No more backends using it */
+ dshash_delete_entry(gtr_shared_usage, shared_entry);
+ }
+}
+
+/*
+ * AtEOXact_UsageCleanup
+ *
+ * Clean up usage records for a single global temporary relation at
+ * main-transaction commit or abort.
+ *
+ * NB: this processing must be idempotent, because EOXactUsageListAdd()
+ * doesn't bother to prevent duplicate entries in eoxact_usage_list[].
+ */
+static void
+AtEOXact_UsageCleanup(GtrUsageEntry *entry, bool isCommit)
+{
+ /*
+ * If the relation is no longer in use after this transaction ends, remove
+ * the usage hash table entries for it. Otherwise, reset the hash entry's
+ * subids to zero.
+ */
+ if ((isCommit && entry->stopped_subid != InvalidSubTransactionId) ||
+ (!isCommit && entry->started_subid != InvalidSubTransactionId))
+ {
+ gtr_remove_usage(entry->relid);
+ }
+ else
+ {
+ entry->started_subid = InvalidSubTransactionId;
+ entry->stopped_subid = InvalidSubTransactionId;
+ }
+}
+
+/*
+ * AtEOSubXact_UsageCleanup
+ *
+ * Clean up usage records for a single global temporary relation at
+ * subtransaction commit or abort.
+ *
+ * NB: this processing must be idempotent, because EOXactUsageListAdd()
+ * doesn't bother to prevent duplicate entries in eoxact_usage_list[].
+ */
+static void
+AtEOSubXact_UsageCleanup(GtrUsageEntry *entry, bool isCommit,
+ SubTransactionId mySubid,
+ SubTransactionId parentSubid)
+{
+ /*
+ * Did usage start in the current subtransaction?
+ *
+ * During subcommit, mark it as starting in the parent, instead, as long
+ * as it has not been stopped. Otherwise, the global temporary relation
+ * is no longer in use.
+ */
+ if (entry->started_subid == mySubid)
+ {
+ Assert(entry->stopped_subid == mySubid ||
+ entry->stopped_subid == InvalidSubTransactionId);
+
+ if (isCommit && entry->stopped_subid == InvalidSubTransactionId)
+ entry->started_subid = parentSubid;
+ else
+ gtr_remove_usage(entry->relid);
+ }
+
+ /* Update the usage stopped subid */
+ if (entry->stopped_subid == mySubid)
+ {
+ if (isCommit)
+ entry->stopped_subid = parentSubid;
+ else
+ entry->stopped_subid = InvalidSubTransactionId;
+ }
+}
+
+/*
+ * gtr_process_invalidated_gtrs
+ *
+ * Process the list of invalidated global temporary relations and work out
+ * which relations have been dropped. Note that this will include locally
+ * dropped relations as well as relations dropped by other backends.
+ */
+static void
+gtr_process_invalidated_gtrs(void)
+{
+ MemoryContext oldcontext;
+ Oid relid;
+
+ /*
+ * Scan the gtrs_invalidated list and add any dropped relations to the
+ * gtrs_dropped list. Since the transaction might fail later on, we need
+ * the gtrs_dropped list to persist until we can successfully process it.
+ */
+ oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+
+ /*
+ * As we scan the gtrs_invalidated list, more invalidation messages might
+ * arrive, and be added to the end of the list, so we need to be prepared
+ * for the list growing as we traverse it.
+ */
+ for (int i = 0; i < list_length(gtrs_invalidated); i++)
+ {
+ relid = list_nth_oid(gtrs_invalidated, i);
+
+ /* Ignore relations we've already found */
+ if (list_member_oid(gtrs_dropped, relid))
+ continue;
+
+ /* Ignore relations that still exist */
+ if (SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+ continue;
+
+ /* Relation dropped; add it to the gtrs_dropped list */
+ gtrs_dropped = lappend_oid(gtrs_dropped, relid);
+ }
+
+ /* All invalidation messages processed; clear the list */
+ list_free(gtrs_invalidated);
+ gtrs_invalidated = NIL;
+
+ MemoryContextSwitchTo(oldcontext);
+}
+
+/*
+ * TrackGlobalTempRelationStorage
+ *
+ * Track about-to-be-created or scheduled-to-be-deleted storage for a global
+ * temporary relation, and arrange for all storage created to be deleted on
+ * backend exit.
+ *
+ * This is called for global temporary relations whenever storage is created
+ * using RelationCreateStorage() or deleted using RelationDropStorage().
+ */
+void
+TrackGlobalTempRelationStorage(Oid relid, RelFileLocator rlocator,
+ ProcNumber backend, bool create)
+{
+ GtrStorageEntry *entry;
+ bool found;
+ SMgrRelation srel;
+
+ if (create)
+ {
+ /* Initialize the storage table, if necessary */
+ gtr_init_storage_table();
+
+ /* Insert an entry to track the storage */
+ entry = hash_search(gtr_local_storage, &rlocator, HASH_ENTER, &found);
+ if (found)
+ elog(ERROR, "Storage already exists for relation %u", relid);
+
+ /*
+ * We're about to create storage for a global temporary relation.
+ * First, check if storage already exists and if so, delete it --- can
+ * happen if a previous backend with the same ProcNumber crashed, and
+ * RemovePgTempFiles() didn't delete it. The old storage is deleted
+ * non-transactionally, so this is never rolled back.
+ */
+ srel = smgropen(rlocator, backend);
+ if (smgrexists(srel, MAIN_FORKNUM))
+ smgrdounlinkall(&srel, 1, false);
+ smgrclose(srel);
+
+ /* Mark the storage as created in the current subtransaction */
+ entry->relid = relid;
+ entry->created_subid = GetCurrentSubTransactionId();
+ entry->dropped_subid = InvalidSubTransactionId;
+ }
+ else
+ {
+ /* Mark the storage as deleted in the current subtransaction */
+ entry = gtr_local_storage == NULL ? NULL :
+ hash_search(gtr_local_storage, &rlocator, HASH_FIND, NULL);
+ if (entry == NULL)
+ elog(ERROR, "Storage not found for relation %u", relid);
+
+ entry->dropped_subid = GetCurrentSubTransactionId();
+ }
+
+ /* Flag the storage for eoxact cleanup */
+ EOXactStorageListAdd(rlocator);
+}
+
+/*
+ * ReassignGlobalTempRelationStorage
+ *
+ * Reassign global temporary relation storage to a different relation. This
+ * is needed for operations such as ALTER TABLE and REPACK, that rewrite a
+ * relation's contents by building a transient relation and then swapping its
+ * storage with the original relation. We must mark the new storage as
+ * belonging to the original relation here, otherwise it would be deleted
+ * when the transient relation is dropped.
+ *
+ * Note: we have no way of undoing this reassignment in case of rollback, so
+ * we do not assign the original storage to the transient relation, since
+ * that would leave it in an invalid state after rollback. This isn't a
+ * problem for the new storage, since that is dropped on rollback. Thus,
+ * this operates in the same way as TRUNCATE, where both the old and new
+ * storage are temporarily marked as belonging to the same relation. On
+ * commit, the old storage is dropped and the relation is left pointing to
+ * the new storage, and on rollback the new storage is dropped and the
+ * relation is reloaded and made to point to the old storage.
+ */
+void
+ReassignGlobalTempRelationStorage(RelFileLocator rlocator,
+ Oid newRelid)
+{
+ GtrStorageEntry *entry;
+
+ /* Must already be tracking the storage */
+ entry = hash_search(gtr_local_storage, &rlocator, HASH_FIND, NULL);
+ if (entry == NULL)
+ elog(ERROR, "could not find global temp relation storage {spcOid: %u, dbOid: %u, relNumber: %u}",
+ rlocator.spcOid, rlocator.dbOid, rlocator.relNumber);
+
+ /* Reassign it */
+ entry->relid = newRelid;
+}
+
+/*
+ * InitGlobalTempRelation
+ *
+ * Initialize a global temporary relation for use in this backend, if we
+ * haven't already done so.
+ *
+ * This is intended for global temporary relations that were created in some
+ * other backend. Such relations will have valid catalog entries, but may
+ * have no local storage for this backend yet.
+ *
+ * Note: This is intentionally idempotent.
+ */
+void
+InitGlobalTempRelation(Relation relation)
+{
+ /*
+ * Certain operations (e.g., pg_table_size()) may open global temporary
+ * relations from parallel workers. We allow this, but only the parallel
+ * leader is allowed to initialize the relation. Checks in the planner
+ * should prevent workers from actually attempting to read from or write
+ * to the relation.
+ */
+ if (IsParallelWorker())
+ return;
+
+ /* Create storage for the relation, if it has none */
+ if (RELKIND_HAS_STORAGE(relation->rd_rel->relkind) &&
+ (gtr_local_storage == NULL ||
+ hash_search(gtr_local_storage,
+ &relation->rd_locator, HASH_FIND, NULL) == NULL))
+ {
+ /* Create (and track) storage for the relation */
+ if (RELKIND_HAS_TABLE_AM(relation->rd_rel->relkind))
+ table_relation_set_new_filelocator(relation,
+ &relation->rd_locator,
+ relation->rd_rel->relpersistence,
+ &relation->rd_rel->relfrozenxid,
+ &relation->rd_rel->relminmxid);
+ else
+ RelationCreateStorage(relation->rd_id,
+ relation->rd_locator,
+ relation->rd_rel->relpersistence,
+ true);
+
+ /* Register the ON COMMIT action for relation, if it's DELETE ROWS */
+ Assert(relation->rd_rel->reloncommit == RELONCOMMIT_PRESERVE_ROWS ||
+ relation->rd_rel->reloncommit == RELONCOMMIT_DELETE_ROWS);
+
+ if (relation->rd_rel->reloncommit == RELONCOMMIT_DELETE_ROWS)
+ register_on_commit_action(relation->rd_id, ONCOMMIT_DELETE_ROWS);
+ }
+
+ /* The remaining initialization works as if we had created it locally */
+ GlobalTempRelationCreated(relation);
+}
+
+/*
+ * InvalidateGlobalTempRelation
+ *
+ * Process an invalidation message for a relation.
+ *
+ * We are only interested in global temporary relations that we are currently
+ * using, but the relcache will call this for all invalidated relations, not
+ * just global temporary relations, since it has no way of knowing the
+ * difference for relations no longer in its cache. We filter out the ones
+ * we're not interested in.
+ *
+ * For a whole-relcache invalidation, RelationCacheInvalidate() will invoke
+ * this with relid = InvalidOid.
+ */
+void
+InvalidateGlobalTempRelation(Oid relid)
+{
+ MemoryContext oldcontext;
+ HASH_SEQ_STATUS status;
+ GtrUsageEntry *entry;
+
+ /* Quick exit if we haven't used any global temporary relations */
+ if (gtr_local_usage == NULL)
+ return;
+
+ /* Be sure any memory allocated for gtrs_invalidated is persistent */
+ oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+
+ /*
+ * We can't do any DB access here, so just make a record of the
+ * invalidations that might be of interest to us (those for in-use global
+ * temporary relations). We don't care about global temporary relations
+ * that we haven't touched, or any other types of relations.
+ */
+ if (OidIsValid(relid))
+ {
+ /* Invalidate rel if it's a locally in-use global temp relation */
+ if (hash_search(gtr_local_usage, &relid, HASH_FIND, NULL))
+ {
+ gtrs_invalidated = list_append_unique_oid(gtrs_invalidated, relid);
+ }
+ }
+ else
+ {
+ /* Invalidate all global temporary relations in use locally */
+ hash_seq_init(&status, gtr_local_usage);
+ while ((entry = hash_seq_search(&status)) != NULL)
+ {
+ gtrs_invalidated = list_append_unique_oid(gtrs_invalidated,
+ entry->relid);
+ }
+ }
+
+ MemoryContextSwitchTo(oldcontext);
+}
+
+/*
+ * GlobalTempRelationCreated
+ *
+ * Initialize a just-created global temporary relation for use in this
+ * backend. This is also used by InitGlobalTempRelation() for just-opened
+ * relations created in other backends, after we have created local storage
+ * for them.
+ *
+ * Note: This is intentionally idempotent.
+ */
+void
+GlobalTempRelationCreated(Relation relation)
+{
+ /* Record our use of the relation */
+ gtr_record_usage(relation->rd_id);
+}
+
+/*
+ * GlobalTempRelationDropped
+ *
+ * Tidy up after a global temporary relation has been dropped.
+ *
+ * Note: This is intentionally idempotent. It is called for locally dropped
+ * relations as well as relations that we were using that were dropped by
+ * other backends, which means it can be called twice for the same relation.
+ */
+void
+GlobalTempRelationDropped(Oid relid)
+{
+ GtrUsageEntry *entry;
+
+ /*
+ * Mark the relation's usage as ending in the current subtransaction, if
+ * we haven't done so already.
+ */
+ entry = hash_search(gtr_local_usage, &relid, HASH_FIND, NULL);
+
+ if (entry && entry->stopped_subid == InvalidSubTransactionId)
+ {
+ entry->stopped_subid = GetCurrentSubTransactionId();
+
+ /* Flag the usage entry for eoxact cleanup */
+ EOXactUsageListAdd(relid);
+ }
+
+ /* Forget any ON COMMIT action for the relation */
+ remove_on_commit_action(relid);
+}
+
+/*
+ * PreCommit_GlobalTempRelation
+ *
+ * Process all dropped global temporary relations that we were using. For
+ * relations dropped by other backends, we need to delete any local storage
+ * that we created. For relations dropped by this backend, the storage
+ * should have already been scheduled for deletion, so we ought to have
+ * nothing to do.
+ */
+void
+PreCommit_GlobalTempRelation(void)
+{
+ HASH_SEQ_STATUS status;
+ GtrStorageEntry *storage_entry;
+ Relation rel;
+
+ /*
+ * Update the list of dropped global temporary relations from the list of
+ * invalidated relations.
+ */
+ gtr_process_invalidated_gtrs();
+
+ /* Quick exit if no global temporary relations were dropped */
+ if (gtrs_dropped == NIL)
+ return;
+
+ /*
+ * Schedule all local storage that we created for dropped relations to be
+ * deleted at transaction commit. AtEOXact_GlobalTempRelation() will do
+ * the remaining cleanup work for gtrs_dropped and the hash tables.
+ */
+ hash_seq_init(&status, gtr_local_storage);
+ while ((storage_entry = hash_seq_search(&status)) != NULL)
+ {
+ /* Ignore storage already scheduled to be deleted */
+ if (storage_entry->dropped_subid != InvalidSubTransactionId)
+ continue;
+
+ /* Ignore entries not in the dropped list */
+ if (!list_member_oid(gtrs_dropped, storage_entry->relid))
+ continue;
+
+ /* Need a faked-up Relation descriptor */
+ rel = CreateFakeRelcacheEntry(storage_entry->rlocator);
+ rel->rd_id = storage_entry->relid;
+ rel->rd_backend = ProcNumberForTempRelations();
+ rel->rd_smgr = NULL;
+ rel->rd_rel->relpersistence = RELPERSISTENCE_GLOBAL_TEMP;
+
+ /* Schedule the storage to be deleted and tidy up */
+ RelationDropStorage(rel);
+ FreeFakeRelcacheEntry(rel);
+ }
+
+ /*
+ * Perform additional tidying up for all dropped relations.
+ */
+ foreach_oid(relid, gtrs_dropped)
+ {
+ GlobalTempRelationDropped(relid);
+ }
+}
+
+/*
+ * AtEOXact_GlobalTempRelation
+ *
+ * Clean up storage and usage records at main-transaction commit or abort.
+ */
+void
+AtEOXact_GlobalTempRelation(bool isCommit)
+{
+ HASH_SEQ_STATUS status;
+ GtrStorageEntry *storage_entry;
+ GtrUsageEntry *usage_entry;
+
+ /*
+ * Unless the eoxact_storage_list[] overflowed, we only need to examine
+ * the storage listed in it. Otherwise fall back on a hash_seq_search
+ * scan --- see similar code in AtEOXact_RelationCache().
+ */
+ if (eoxact_storage_list_overflowed)
+ {
+ hash_seq_init(&status, gtr_local_storage);
+ while ((storage_entry = hash_seq_search(&status)) != NULL)
+ {
+ AtEOXact_StorageCleanup(storage_entry, isCommit);
+ }
+ }
+ else
+ {
+ for (int i = 0; i < eoxact_storage_list_len; i++)
+ {
+ storage_entry = hash_search(gtr_local_storage,
+ &eoxact_storage_list[i],
+ HASH_FIND, NULL);
+ if (storage_entry)
+ AtEOXact_StorageCleanup(storage_entry, isCommit);
+ }
+ }
+
+ /* Similarly, cleanup usage records */
+ if (eoxact_usage_list_overflowed)
+ {
+ hash_seq_init(&status, gtr_local_usage);
+ while ((usage_entry = hash_seq_search(&status)) != NULL)
+ {
+ AtEOXact_UsageCleanup(usage_entry, isCommit);
+ }
+ }
+ else
+ {
+ for (int i = 0; i < eoxact_usage_list_len; i++)
+ {
+ usage_entry = hash_search(gtr_local_usage, &eoxact_usage_list[i],
+ HASH_FIND, NULL);
+ if (usage_entry)
+ AtEOXact_UsageCleanup(usage_entry, isCommit);
+ }
+ }
+
+ /* Now we're out of the transaction and can clear the lists */
+ eoxact_storage_list_len = 0;
+ eoxact_storage_list_overflowed = false;
+ eoxact_usage_list_len = 0;
+ eoxact_usage_list_overflowed = false;
+ list_free(gtrs_dropped);
+ gtrs_dropped = NIL;
+}
+
+/*
+ * AtEOSubXact_GlobalTempRelation
+ *
+ * Clean up storage and usage records at sub-transaction commit or abort.
+ */
+void
+AtEOSubXact_GlobalTempRelation(bool isCommit, SubTransactionId mySubid,
+ SubTransactionId parentSubid)
+{
+ HASH_SEQ_STATUS status;
+ GtrStorageEntry *storage_entry;
+ GtrUsageEntry *usage_entry;
+
+ /*
+ * Unless the eoxact_storage_list[] overflowed, we only need to examine
+ * the storage listed in it. Otherwise fall back on a hash_seq_search
+ * scan. Same logic as in AtEOXact_GlobalTempRelation().
+ */
+ if (eoxact_storage_list_overflowed)
+ {
+ hash_seq_init(&status, gtr_local_storage);
+ while ((storage_entry = hash_seq_search(&status)) != NULL)
+ {
+ AtEOSubXact_StorageCleanup(storage_entry, isCommit, mySubid,
+ parentSubid);
+ }
+ }
+ else
+ {
+ for (int i = 0; i < eoxact_storage_list_len; i++)
+ {
+ storage_entry = hash_search(gtr_local_storage,
+ &eoxact_storage_list[i],
+ HASH_FIND, NULL);
+ if (storage_entry)
+ AtEOSubXact_StorageCleanup(storage_entry, isCommit, mySubid,
+ parentSubid);
+ }
+ }
+
+ /* Similarly, cleanup usage records */
+ if (eoxact_usage_list_overflowed)
+ {
+ hash_seq_init(&status, gtr_local_usage);
+ while ((usage_entry = hash_seq_search(&status)) != NULL)
+ {
+ AtEOSubXact_UsageCleanup(usage_entry, isCommit, mySubid,
+ parentSubid);
+ }
+ }
+ else
+ {
+ for (int i = 0; i < eoxact_usage_list_len; i++)
+ {
+ usage_entry = hash_search(gtr_local_usage, &eoxact_usage_list[i],
+ HASH_FIND, NULL);
+ if (usage_entry)
+ AtEOSubXact_UsageCleanup(usage_entry, isCommit, mySubid,
+ parentSubid);
+ }
+ }
+
+ /* Don't reset the lists; we still need more cleanup later */
+}
+
+/*
+ * IsOtherUsingGlobalTempRelation
+ *
+ * Test if any other backend is using the specified global temporary
+ * relation. The caller should have an exclusive lock on the relation, or
+ * else the result could be quickly out-dated.
+ */
+bool
+IsOtherUsingGlobalTempRelation(Oid relid)
+{
+ bool used_locally;
+ GtrSharedUsageKey key;
+ GtrSharedUsageEntry *entry;
+ int usage_count;
+
+ gtr_init_usage_tables();
+
+ /* Are we using the relation? (expect true) */
+ (void) hash_search(gtr_local_usage, &relid, HASH_FIND, &used_locally);
+
+ /* Total usage count (including us) */
+ key.dbid = MyDatabaseId;
+ key.relid = relid;
+ entry = dshash_find(gtr_shared_usage, &key, false);
+
+ if (entry)
+ {
+ usage_count = entry->usage_count;
+ Assert(usage_count > 0);
+ dshash_release_lock(gtr_shared_usage, entry);
+ }
+ else
+ usage_count = 0;
+
+ return used_locally ? (usage_count > 1) : (usage_count > 0);
+}
+
+/*
+ * AllGlobalTempRelationsInUse
+ *
+ * Returns a list of OIDs of all global temporary relations in use (by any
+ * backend) in the specified database (or all databases, if dbid is
+ * InvalidOid).
+ *
+ * Note: The result may be almost immediately out-dated.
+ */
+List *
+AllGlobalTempRelationsInUse(Oid dbid)
+{
+ List *rels_in_use = NIL;
+ dshash_seq_status status;
+ GtrSharedUsageEntry *entry;
+
+ gtr_init_usage_tables();
+
+ dshash_seq_init(&status, gtr_shared_usage, false);
+ while ((entry = dshash_seq_next(&status)) != NULL)
+ {
+ Assert(entry->usage_count > 0);
+ if (!OidIsValid(dbid) || entry->key.dbid == dbid)
+ rels_in_use = lappend_oid(rels_in_use, entry->key.relid);
+ }
+ dshash_seq_term(&status);
+
+ return rels_in_use;
+}
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 684a44946fc..012de434321 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -36,6 +36,7 @@
#include "access/tableam.h"
#include "catalog/binary_upgrade.h"
#include "catalog/catalog.h"
+#include "catalog/global_temp.h"
#include "catalog/heap.h"
#include "catalog/index.h"
#include "catalog/objectaccess.h"
@@ -389,7 +390,8 @@ heap_create(const char *relname,
relpersistence,
relfrozenxid, relminmxid);
else if (RELKIND_HAS_STORAGE(rel->rd_rel->relkind))
- RelationCreateStorage(rel->rd_locator, relpersistence, true);
+ RelationCreateStorage(rel->rd_id, rel->rd_locator,
+ relpersistence, true);
else
Assert(false);
}
@@ -398,8 +400,13 @@ heap_create(const char *relname,
* If a tablespace is specified, removal of that tablespace is normally
* protected by the existence of a physical file; but for relations with
* no files, add a pg_shdepend entry to account for that.
+ *
+ * Note, however, that although global temporary relations may have files,
+ * those files will go away at the end of the session, and so provide no
+ * protection, and we must add a pg_shdepend entry in this case too.
*/
- if (!create_storage && reltablespace != InvalidOid)
+ if ((!create_storage || relpersistence == RELPERSISTENCE_GLOBAL_TEMP) &&
+ reltablespace != InvalidOid)
recordDependencyOnTablespace(RelationRelationId, relid,
reltablespace);
@@ -992,6 +999,10 @@ InsertPgClassTuple(Relation pg_class_desc,
CatalogTupleInsert(pg_class_desc, tup);
heap_freetuple(tup);
+
+ /* Additional setup for global temporary relations */
+ if (RELATION_IS_GLOBAL_TEMP(new_rel_desc))
+ GlobalTempRelationCreated(new_rel_desc);
}
/* --------------------------------
@@ -1601,6 +1612,7 @@ DeleteRelationTuple(Oid relid)
{
Relation pg_class_desc;
HeapTuple tup;
+ char relpersistence;
/* Grab an appropriate lock on the pg_class relation */
pg_class_desc = table_open(RelationRelationId, RowExclusiveLock);
@@ -1608,6 +1620,7 @@ DeleteRelationTuple(Oid relid)
tup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
if (!HeapTupleIsValid(tup))
elog(ERROR, "cache lookup failed for relation %u", relid);
+ relpersistence = ((Form_pg_class) GETSTRUCT(tup))->relpersistence;
/* delete the relation tuple from pg_class, and finish up */
CatalogTupleDelete(pg_class_desc, &tup->t_self);
@@ -1615,6 +1628,10 @@ DeleteRelationTuple(Oid relid)
ReleaseSysCache(tup);
table_close(pg_class_desc, RowExclusiveLock);
+
+ /* Additional tidying up for global temporary relations */
+ if (relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+ GlobalTempRelationDropped(relid);
}
/*
diff --git a/src/backend/catalog/information_schema.sql b/src/backend/catalog/information_schema.sql
index 4f0e2492937..a91a848cd17 100644
--- a/src/backend/catalog/information_schema.sql
+++ b/src/backend/catalog/information_schema.sql
@@ -1952,6 +1952,7 @@ CREATE VIEW tables AS
CAST(
CASE WHEN nc.oid = pg_my_temp_schema() THEN 'LOCAL TEMPORARY'
+ WHEN c.relpersistence = 'g' THEN 'GLOBAL TEMPORARY'
WHEN c.relkind IN ('r', 'p') THEN 'BASE TABLE'
WHEN c.relkind = 'v' THEN 'VIEW'
WHEN c.relkind = 'f' THEN 'FOREIGN'
diff --git a/src/backend/catalog/meson.build b/src/backend/catalog/meson.build
index 11d21c5ad6b..7285ab2dfcf 100644
--- a/src/backend/catalog/meson.build
+++ b/src/backend/catalog/meson.build
@@ -4,6 +4,7 @@ backend_sources += files(
'aclchk.c',
'catalog.c',
'dependency.c',
+ 'global_temp.c',
'heap.c',
'index.c',
'indexing.c',
diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index 56b87d878e8..b8555cdfa22 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -734,7 +734,8 @@ RangeVarGetCreationNamespace(const RangeVar *newRelation)
* to a no-longer-existent namespace.
*
* As a further side-effect, if the selected namespace is a temporary namespace,
- * we mark the RangeVar as RELPERSISTENCE_TEMP.
+ * we mark the RangeVar as RELPERSISTENCE_TEMP, unless it was marked as
+ * RELPERSISTENCE_GLOBAL_TEMP, in which case an error is raised.
*/
Oid
RangeVarGetAndCheckCreationNamespace(RangeVar *relation,
@@ -858,9 +859,19 @@ RangeVarAdjustRelationPersistence(RangeVar *newRelation, Oid nspid)
else
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
- errmsg("cannot create temporary relation in non-temporary schema")));
+ errmsg("cannot create local temporary relation in non-temporary schema")));
}
break;
+ case RELPERSISTENCE_GLOBAL_TEMP:
+ if (isTempOrTempToastNamespace(nspid))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+ errmsg("cannot create global temporary relation in temporary schema")));
+ else if (isAnyTempNamespace(nspid))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+ errmsg("cannot create relations in temporary schemas of other sessions")));
+ break;
case RELPERSISTENCE_PERMANENT:
if (isTempOrTempToastNamespace(nspid))
newRelation->relpersistence = RELPERSISTENCE_TEMP;
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 1ec94c851b2..846165e1a5e 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -100,7 +100,8 @@ check_publication_add_relation(PublicationRelInfo *pri)
errdetail("This operation is not supported for conflict log tables.")));
/* UNLOGGED and TEMP relations cannot be part of publication. */
- if (targetrel->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
+ if (targetrel->rd_rel->relpersistence == RELPERSISTENCE_TEMP ||
+ targetrel->rd_rel->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg(errormsg, relname),
diff --git a/src/backend/catalog/storage.c b/src/backend/catalog/storage.c
index e443a4993c5..1259abee5aa 100644
--- a/src/backend/catalog/storage.c
+++ b/src/backend/catalog/storage.c
@@ -19,11 +19,13 @@
#include "postgres.h"
+#include "access/parallel.h"
#include "access/visibilitymap.h"
#include "access/xact.h"
#include "access/xlog.h"
#include "access/xloginsert.h"
#include "access/xlogutils.h"
+#include "catalog/global_temp.h"
#include "catalog/storage.h"
#include "catalog/storage_xlog.h"
#include "miscadmin.h"
@@ -119,14 +121,18 @@ AddPendingSync(const RelFileLocator *rlocator)
* pass register_delete = false.
*/
SMgrRelation
-RelationCreateStorage(RelFileLocator rlocator, char relpersistence,
+RelationCreateStorage(Oid relid, RelFileLocator rlocator, char relpersistence,
bool register_delete)
{
SMgrRelation srel;
ProcNumber procNumber;
bool needs_wal;
- Assert(!IsInParallelMode()); /* couldn't update pendingSyncHash */
+ /* relid is only needed for global temporary relations */
+ Assert(OidIsValid(relid) || relpersistence != RELPERSISTENCE_GLOBAL_TEMP);
+
+ /* per InitGlobalTempRelation(), this cannot be a parallel worker */
+ Assert(!IsParallelWorker()); /* couldn't update pendingSyncHash */
switch (relpersistence)
{
@@ -134,6 +140,12 @@ RelationCreateStorage(RelFileLocator rlocator, char relpersistence,
procNumber = ProcNumberForTempRelations();
needs_wal = false;
break;
+ case RELPERSISTENCE_GLOBAL_TEMP:
+ /* Track storage created for global temporary relations */
+ procNumber = ProcNumberForTempRelations();
+ TrackGlobalTempRelationStorage(relid, rlocator, procNumber, true);
+ needs_wal = false;
+ break;
case RELPERSISTENCE_UNLOGGED:
procNumber = INVALID_PROC_NUMBER;
needs_wal = false;
@@ -208,6 +220,11 @@ RelationDropStorage(Relation rel)
{
PendingRelDelete *pending;
+ /* Track to-be-deleted storage for global temporary relations */
+ if (RELATION_IS_GLOBAL_TEMP(rel))
+ TrackGlobalTempRelationStorage(rel->rd_id, rel->rd_locator,
+ rel->rd_backend, false);
+
/* Add the relation to the list of stuff to delete at commit */
pending = (PendingRelDelete *)
MemoryContextAlloc(TopMemoryContext, sizeof(PendingRelDelete));
diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c
index f0819d15ab7..52b86b01a22 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -410,11 +410,13 @@ 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. Likewise, global temporary relations don't need to be copied,
+ * though their pg_class entries never go away.
*/
if (classForm->reltablespace == GLOBALTABLESPACE_OID ||
!RELKIND_HAS_STORAGE(classForm->relkind) ||
- classForm->relpersistence == RELPERSISTENCE_TEMP)
+ classForm->relpersistence == RELPERSISTENCE_TEMP ||
+ classForm->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
return NULL;
/*
@@ -444,7 +446,8 @@ ScanSourceDatabasePgClassTuple(HeapTupleData *tuple, Oid tbid, Oid dbid,
relinfo->reloid = classForm->oid;
/* Temporary relations were rejected above. */
- Assert(classForm->relpersistence != RELPERSISTENCE_TEMP);
+ Assert(classForm->relpersistence != RELPERSISTENCE_TEMP &&
+ classForm->relpersistence != RELPERSISTENCE_GLOBAL_TEMP);
relinfo->permanent =
(classForm->relpersistence == RELPERSISTENCE_PERMANENT) ? true : false;
diff --git a/src/backend/commands/lockcmds.c b/src/backend/commands/lockcmds.c
index 13232a36196..e4701af3bb1 100644
--- a/src/backend/commands/lockcmds.c
+++ b/src/backend/commands/lockcmds.c
@@ -109,7 +109,8 @@ RangeVarCallbackForLockTable(const RangeVar *rv, Oid relid, Oid oldrelid,
* transaction.
*/
relpersistence = get_rel_persistence(relid);
- if (relpersistence == RELPERSISTENCE_TEMP)
+ if (relpersistence == RELPERSISTENCE_TEMP ||
+ relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
MyXactFlags |= XACT_FLAGS_ACCESSEDTEMPNAMESPACE;
/* Check permissions. */
diff --git a/src/backend/commands/propgraphcmds.c b/src/backend/commands/propgraphcmds.c
index 78cbf7d08d0..308b207affe 100644
--- a/src/backend/commands/propgraphcmds.c
+++ b/src/backend/commands/propgraphcmds.c
@@ -117,6 +117,11 @@ CreatePropGraph(ParseState *pstate, const CreatePropGraphStmt *stmt)
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("property graphs cannot be unlogged because they do not have storage")));
+ 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/repack.c b/src/backend/commands/repack.c
index faa07d1a118..7a85edb9317 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -43,6 +43,7 @@
#include "access/xlog.h"
#include "catalog/catalog.h"
#include "catalog/dependency.h"
+#include "catalog/global_temp.h"
#include "catalog/heap.h"
#include "catalog/index.h"
#include "catalog/namespace.h"
@@ -1676,6 +1677,15 @@ swap_relation_files(Oid r1, Oid r2, bool target_is_pg_class,
rel2->rd_newRelfilelocatorSubid = rel1->rd_newRelfilelocatorSubid;
rel2->rd_firstRelfilelocatorSubid = rel1->rd_firstRelfilelocatorSubid;
RelationAssumeNewRelfilelocator(rel1);
+
+ /*
+ * If they're global temp relations, reassign rel2's storage to rel1.
+ * NB: We intentionally do not reassign rel1's storage to rel2, since
+ * that would leave it in an invalid state on rollback.
+ */
+ if (RELATION_IS_GLOBAL_TEMP(rel1))
+ ReassignGlobalTempRelationStorage(rel2->rd_locator, rel1->rd_id);
+
relation_close(rel1, NoLock);
relation_close(rel2, NoLock);
}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 95abaf4890c..ba139560f90 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -30,6 +30,7 @@
#include "access/xlog.h"
#include "access/xloginsert.h"
#include "catalog/catalog.h"
+#include "catalog/global_temp.h"
#include "catalog/heap.h"
#include "catalog/index.h"
#include "catalog/namespace.h"
@@ -175,6 +176,7 @@ typedef struct AlteredTableInfo
Oid relid; /* Relation to work on */
char relkind; /* Its relkind */
TupleDesc oldDesc; /* Pre-modification tuple descriptor */
+ char oldrelpersistence; /* Pre-modification relpersistence */
/*
* Transiently set during Phase 2, normally set to NULL.
@@ -849,11 +851,17 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
/*
* Check consistency of arguments
*/
- if (stmt->oncommit != ONCOMMIT_NOOP
- && stmt->relation->relpersistence != RELPERSISTENCE_TEMP)
+ if (stmt->oncommit != ONCOMMIT_NOOP &&
+ 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 cannot be used on global temporary tables")));
if (stmt->partspec != NULL)
{
@@ -1711,7 +1719,8 @@ RemoveRelations(DropStmt *drop)
* callback retrieved the rel's persistence for us.
*/
if (drop->concurrent &&
- state.actual_relpersistence != RELPERSISTENCE_TEMP)
+ state.actual_relpersistence != RELPERSISTENCE_TEMP &&
+ state.actual_relpersistence != RELPERSISTENCE_GLOBAL_TEMP)
{
Assert(list_length(drop->objects) == 1 &&
drop->removeType == OBJECT_INDEX);
@@ -2779,14 +2788,17 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
/*
* If the parent is permanent, so must be all of its partitions. Note
- * that inheritance allows that case.
+ * that inheritance allows that case. For these purposes, global
+ * temporary tables behave like permanent tables.
*/
if (is_partition &&
relation->rd_rel->relpersistence != RELPERSISTENCE_TEMP &&
relpersistence == RELPERSISTENCE_TEMP)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("cannot create a temporary relation as partition of permanent relation \"%s\"",
+ errmsg(RELATION_IS_GLOBAL_TEMP(relation)
+ ? "cannot create a local temporary relation as partition of global temporary relation \"%s\""
+ : "cannot create a temporary relation as partition of permanent relation \"%s\"",
RelationGetRelationName(relation))));
/* Permanent rels cannot inherit from temporary ones */
@@ -2796,6 +2808,8 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg(!is_partition
? "cannot inherit from temporary relation \"%s\""
+ : relpersistence == RELPERSISTENCE_GLOBAL_TEMP
+ ? "cannot create a global temporary relation as partition of local temporary relation \"%s\""
: "cannot create a permanent relation as partition of temporary relation \"%s\"",
RelationGetRelationName(relation))));
@@ -3855,10 +3869,12 @@ SetRelationTableSpace(Relation rel,
UnlockTuple(pg_class, &otid, InplaceUpdateTupleLock);
/*
- * Record dependency on tablespace. This is only required for relations
- * that have no physical storage.
+ * Record dependency on tablespace. This is required for relations that
+ * have no physical storage, and for global temporary relations whose
+ * physical storage is temporary.
*/
- if (!RELKIND_HAS_STORAGE(rel->rd_rel->relkind))
+ if (!RELKIND_HAS_STORAGE(rel->rd_rel->relkind) ||
+ RELATION_IS_GLOBAL_TEMP(rel))
changeDependencyOnTablespace(RelationRelationId, reloid,
rd_rel->reltablespace);
@@ -6039,6 +6055,18 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot rewrite temporary tables of other sessions")));
+ /*
+ * Don't allow rewrite on global temporary tables, if they're
+ * being used by other backends ... we have no way to rewrite
+ * local storage of another backend.
+ */
+ if (RELATION_IS_GLOBAL_TEMP(OldHeap) &&
+ IsOtherUsingGlobalTempRelation(RelationGetRelid(OldHeap)))
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot rewrite global temporary table \"%s\" because it is being used in another session",
+ RelationGetRelationName(OldHeap)));
+
/*
* Select destination tablespace (same as original unless user
* requested a change)
@@ -6135,11 +6163,22 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
/*
* If required, test the current data within the table against new
* constraints generated by ALTER TABLE commands, but don't
- * rebuild data.
+ * rebuild data. Don't allow this for global temporary tables, if
+ * they're being used by other backends ... we have no way to scan
+ * local storage of another backend.
*/
if (tab->constraints != NIL || tab->verify_new_notnull ||
tab->partition_constraint != NULL)
+ {
+ if (tab->oldrelpersistence == RELPERSISTENCE_GLOBAL_TEMP &&
+ IsOtherUsingGlobalTempRelation(tab->relid))
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot add or alter constraints of global temporary table \"%s\" because it is being used in another session",
+ get_rel_name(tab->relid)));
+
ATRewriteTable(tab, InvalidOid);
+ }
/*
* If we had SET TABLESPACE but no reason to reconstruct tuples,
@@ -6701,6 +6740,7 @@ ATGetQueueEntry(List **wqueue, Relation rel)
tab->rel = NULL; /* set later */
tab->relkind = rel->rd_rel->relkind;
tab->oldDesc = CreateTupleDescCopyConstr(RelationGetDescr(rel));
+ tab->oldrelpersistence = rel->rd_rel->relpersistence;
tab->newAccessMethod = InvalidOid;
tab->chgAccessMethod = false;
tab->newTableSpace = InvalidOid;
@@ -10291,11 +10331,17 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
if (pkrel->rd_rel->relpersistence != RELPERSISTENCE_TEMP)
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
- errmsg("constraints on temporary tables may reference only temporary tables")));
+ errmsg("constraints on local temporary tables may reference only local temporary tables")));
if (!pkrel->rd_islocaltemp || !rel->rd_islocaltemp)
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
- errmsg("constraints on temporary tables must involve temporary tables of this session")));
+ errmsg("constraints on local temporary tables must involve local temporary tables of this session")));
+ break;
+ case RELPERSISTENCE_GLOBAL_TEMP:
+ if (!RELATION_IS_GLOBAL_TEMP(pkrel))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+ errmsg("constraints on global temporary tables may reference only global temporary tables")));
break;
}
@@ -17699,7 +17745,8 @@ index_copy_data(Relation rel, RelFileLocator newrlocator)
* NOTE: any conflict in relfilenumber value will be caught in
* RelationCreateStorage().
*/
- dstrel = RelationCreateStorage(newrlocator, rel->rd_rel->relpersistence, true);
+ dstrel = RelationCreateStorage(rel->rd_id, newrlocator,
+ rel->rd_rel->relpersistence, true);
/* copy main fork */
RelationCopyStorage(RelationGetSmgr(rel), dstrel, MAIN_FORKNUM,
@@ -19360,6 +19407,7 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged)
switch (rel->rd_rel->relpersistence)
{
case RELPERSISTENCE_TEMP:
+ case RELPERSISTENCE_GLOBAL_TEMP:
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("cannot change logged status of table \"%s\" because it is temporary",
@@ -20992,7 +21040,9 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
attachrel->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("cannot attach a temporary relation as partition of permanent relation \"%s\"",
+ errmsg(RELATION_IS_GLOBAL_TEMP(rel)
+ ? "cannot attach a local temporary relation as partition of global temporary relation \"%s\""
+ : "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 */
@@ -21000,7 +21050,9 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
attachrel->rd_rel->relpersistence != RELPERSISTENCE_TEMP)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("cannot attach a permanent relation as partition of temporary relation \"%s\"",
+ errmsg(RELATION_IS_GLOBAL_TEMP(attachrel)
+ ? "cannot attach a global temporary relation as partition of local temporary relation \"%s\""
+ : "cannot attach a permanent relation as partition of temporary relation \"%s\"",
RelationGetRelationName(rel))));
/* If the parent is temp, it must belong to this session */
@@ -23079,7 +23131,9 @@ createPartitionTable(List **wqueue, RangeVar *newPartName,
newPartName->relpersistence == RELPERSISTENCE_TEMP)
ereport(ERROR,
errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("cannot create a temporary relation as partition of permanent relation \"%s\"",
+ errmsg(parent_relform->relpersistence == RELPERSISTENCE_GLOBAL_TEMP
+ ? "cannot create a local temporary relation as partition of global temporary relation \"%s\""
+ : "cannot create a temporary relation as partition of permanent relation \"%s\"",
RelationGetRelationName(parent_rel)));
/* Permanent rels cannot be partitions belonging to a temporary parent. */
@@ -23087,7 +23141,9 @@ createPartitionTable(List **wqueue, RangeVar *newPartName,
parent_relform->relpersistence == RELPERSISTENCE_TEMP)
ereport(ERROR,
errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("cannot create a permanent relation as partition of temporary relation \"%s\"",
+ errmsg(newPartName->relpersistence == RELPERSISTENCE_GLOBAL_TEMP
+ ? "cannot create a global temporary relation as partition of local temporary relation \"%s\""
+ : "cannot create a permanent relation as partition of temporary relation \"%s\"",
RelationGetRelationName(parent_rel)));
/*
diff --git a/src/backend/commands/tablespace.c b/src/backend/commands/tablespace.c
index d91fcf0facf..aa3093fb2f0 100644
--- a/src/backend/commands/tablespace.c
+++ b/src/backend/commands/tablespace.c
@@ -1159,7 +1159,8 @@ GetDefaultTablespace(char relpersistence, bool partitioned)
Oid result;
/* The temp-table case is handled elsewhere */
- if (relpersistence == RELPERSISTENCE_TEMP)
+ if (relpersistence == RELPERSISTENCE_TEMP ||
+ relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
{
PrepareTempTablespaces();
return GetNextTempTableSpace();
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
index 1bd78a4cdf0..618963880c0 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -476,6 +476,15 @@ DefineView(ViewStmt *stmt, const char *queryString,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("views cannot be unlogged because they do not have storage")));
+ /*
+ * Global temporary views are not sensible either. This used to generate
+ * a warning in the parser, but now we raise an error.
+ */
+ 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
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index c134594a21a..efbc2b754d3 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -645,6 +645,8 @@ static void
set_rel_consider_parallel(PlannerInfo *root, RelOptInfo *rel,
RangeTblEntry *rte)
{
+ char relpersistence;
+
/*
* The flag has previously been initialized to false, so we can just
* return if it becomes clear that we can't safely set it.
@@ -672,7 +674,9 @@ set_rel_consider_parallel(PlannerInfo *root, RelOptInfo *rel,
* the rest of the necessary infrastructure right now anyway. So
* for now, bail out if we see a temporary table.
*/
- if (get_rel_persistence(rte->relid) == RELPERSISTENCE_TEMP)
+ relpersistence = get_rel_persistence(rte->relid);
+ if (relpersistence == RELPERSISTENCE_TEMP ||
+ relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
return;
/*
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index ff4e1388c55..ac659e43fe4 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -3913,31 +3913,21 @@ 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
+ * NOTE: we accept both GLOBAL and LOCAL options. GLOBAL results in
+ * SQL-spec-compliant temp table behavior. 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.
+ * implement LOCAL as meaning the same as our default temp table behavior of
+ * keeping the table definition private to the session that created it, and
+ * dropping the table at the end of the session, so we just treat LOCAL as a
+ * noise word. (Actually, the SQL-spec mandates that either GLOBAL or LOCAL
+ * must be specified, but we allow it to be omitted.)
*/
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 +13962,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/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 127bd9e7da3..6fb388f57b9 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2050,7 +2050,10 @@ do_autovacuum(void)
/*
* Check if it is a temp table (presumably, of some other backend's).
- * We cannot safely process other backends' temp tables.
+ * We cannot safely process other backends' local temp tables, so just
+ * record any that appear to be orphaned, so we can drop them later.
+ * Global temporary tables cannot be processed either, but they cannot
+ * be orphaned in this way either, so we simply ignore them.
*/
if (classForm->relpersistence == RELPERSISTENCE_TEMP)
{
@@ -2072,6 +2075,8 @@ do_autovacuum(void)
}
continue;
}
+ else if (classForm->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+ continue;
/* Fetch reloptions and the pgstat entry for this table */
relopts = extract_autovac_opts(tuple, pg_class_desc);
@@ -2149,7 +2154,8 @@ do_autovacuum(void)
/*
* We cannot safely process other backends' temp tables, so skip 'em.
*/
- if (classForm->relpersistence == RELPERSISTENCE_TEMP)
+ if (classForm->relpersistence == RELPERSISTENCE_TEMP ||
+ classForm->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
continue;
relid = classForm->oid;
@@ -2244,7 +2250,8 @@ do_autovacuum(void)
*/
if (!((classForm->relkind == RELKIND_RELATION ||
classForm->relkind == RELKIND_MATVIEW) &&
- classForm->relpersistence == RELPERSISTENCE_TEMP))
+ (classForm->relpersistence == RELPERSISTENCE_TEMP ||
+ classForm->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)))
{
UnlockRelationOid(relid, AccessExclusiveLock);
continue;
@@ -3680,7 +3687,8 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
form->relkind != RELKIND_MATVIEW &&
form->relkind != RELKIND_TOASTVALUE)
continue;
- if (form->relpersistence == RELPERSISTENCE_TEMP)
+ if (form->relpersistence == RELPERSISTENCE_TEMP ||
+ form->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
continue;
avopts = extract_autovac_opts(tup, RelationGetDescr(rel));
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index 68557c16cb9..6c083a77faa 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -192,6 +192,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "access/xloginsert.h"
+#include "catalog/global_temp.h"
#include "catalog/indexing.h"
#include "catalog/pg_class.h"
#include "catalog/pg_database.h"
@@ -1500,6 +1501,11 @@ BuildRelationList(bool temp_relations, bool include_shared)
if (!temp_relations)
continue;
}
+ else if (pgc->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+ {
+ /* Deal with global temporary relations separately below */
+ continue;
+ }
else
{
/*
@@ -1526,6 +1532,23 @@ BuildRelationList(bool temp_relations, bool include_shared)
CommitTransactionCommand();
+ /*
+ * If we were asked for temporary relations, include all global temporary
+ * relations currently in use. This list can be out of date as soon as it
+ * is returned, but that doesn't matter because we only need to worry
+ * about those that were in use when the "inprogress-on" state was set,
+ * and are still in use now. This does not require database access.
+ */
+ if (temp_relations)
+ {
+ List *gtrs_in_use;
+
+ gtrs_in_use = AllGlobalTempRelationsInUse(MyDatabaseId);
+
+ RelationList = list_concat(RelationList, gtrs_in_use);
+ list_free(gtrs_in_use);
+ }
+
return RelationList;
}
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 9ab282a76d1..ebdc7838560 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -1236,6 +1236,7 @@ PinBufferForBlock(Relation rel,
/* Persistence should be set before */
Assert((persistence == RELPERSISTENCE_TEMP ||
+ persistence == RELPERSISTENCE_GLOBAL_TEMP ||
persistence == RELPERSISTENCE_PERMANENT ||
persistence == RELPERSISTENCE_UNLOGGED));
@@ -1245,7 +1246,8 @@ PinBufferForBlock(Relation rel,
smgr->smgr_rlocator.locator.relNumber,
smgr->smgr_rlocator.backend);
- if (persistence == RELPERSISTENCE_TEMP)
+ if (persistence == RELPERSISTENCE_TEMP ||
+ persistence == RELPERSISTENCE_GLOBAL_TEMP)
bufHdr = LocalBufferAlloc(smgr, forkNum, blockNum, foundPtr);
else
bufHdr = BufferAlloc(smgr, persistence, forkNum, blockNum,
@@ -1327,7 +1329,8 @@ ReadBuffer_common(Relation rel, SMgrRelation smgr, char smgr_persistence,
IOContext io_context;
IOObject io_object;
- if (persistence == RELPERSISTENCE_TEMP)
+ if (persistence == RELPERSISTENCE_TEMP ||
+ persistence == RELPERSISTENCE_GLOBAL_TEMP)
{
io_context = IOCONTEXT_NORMAL;
io_object = IOOBJECT_TEMP_RELATION;
@@ -1391,7 +1394,8 @@ StartReadBuffersImpl(ReadBuffersOperation *operation,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot access temporary tables of other sessions")));
- if (operation->persistence == RELPERSISTENCE_TEMP)
+ if (operation->persistence == RELPERSISTENCE_TEMP ||
+ operation->persistence == RELPERSISTENCE_GLOBAL_TEMP)
{
io_context = IOCONTEXT_NORMAL;
io_object = IOOBJECT_TEMP_RELATION;
@@ -1692,7 +1696,8 @@ TrackBufferHit(IOObject io_object, IOContext io_context,
smgr->smgr_rlocator.backend,
true);
- if (persistence == RELPERSISTENCE_TEMP)
+ if (persistence == RELPERSISTENCE_TEMP ||
+ persistence == RELPERSISTENCE_GLOBAL_TEMP)
pgBufferUsage.local_blks_hit += 1;
else
pgBufferUsage.shared_blks_hit += 1;
@@ -1763,7 +1768,8 @@ WaitReadBuffers(ReadBuffersOperation *operation)
IOObject io_object;
bool needed_wait = false;
- if (operation->persistence == RELPERSISTENCE_TEMP)
+ if (operation->persistence == RELPERSISTENCE_TEMP ||
+ operation->persistence == RELPERSISTENCE_GLOBAL_TEMP)
{
io_context = IOCONTEXT_NORMAL;
io_object = IOOBJECT_TEMP_RELATION;
@@ -1953,7 +1959,8 @@ AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress)
instr_time io_start;
StartBufferIOResult status;
- if (persistence == RELPERSISTENCE_TEMP)
+ if (persistence == RELPERSISTENCE_TEMP ||
+ persistence == RELPERSISTENCE_GLOBAL_TEMP)
{
io_context = IOCONTEXT_NORMAL;
io_object = IOOBJECT_TEMP_RELATION;
@@ -1972,7 +1979,8 @@ AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress)
if (flags & READ_BUFFERS_SYNCHRONOUSLY)
ioh_flags |= PGAIO_HF_SYNCHRONOUS;
- if (persistence == RELPERSISTENCE_TEMP)
+ if (persistence == RELPERSISTENCE_TEMP ||
+ persistence == RELPERSISTENCE_GLOBAL_TEMP)
ioh_flags |= PGAIO_HF_REFERENCES_LOCAL;
/*
@@ -2133,7 +2141,8 @@ 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 ?
+ persistence == RELPERSISTENCE_TEMP ||
+ persistence == RELPERSISTENCE_GLOBAL_TEMP ?
PGAIO_HCB_LOCAL_BUFFER_READV :
PGAIO_HCB_SHARED_BUFFER_READV,
flags);
@@ -2156,7 +2165,8 @@ 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 (persistence == RELPERSISTENCE_TEMP ||
+ persistence == RELPERSISTENCE_GLOBAL_TEMP)
pgBufferUsage.local_blks_read += io_buffers_len;
else
pgBufferUsage.shared_blks_read += io_buffers_len;
@@ -2766,7 +2776,8 @@ ExtendBufferedRelCommon(BufferManagerRelation bmr,
BMR_GET_SMGR(bmr)->smgr_rlocator.backend,
extend_by);
- if (bmr.relpersistence == RELPERSISTENCE_TEMP)
+ if (bmr.relpersistence == RELPERSISTENCE_TEMP ||
+ bmr.relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
{
/*
* Reject attempts to extend non-local temporary relations; we have no
@@ -5500,9 +5511,10 @@ CreateAndCopyRelationData(RelFileLocator src_rlocator,
* Create and copy all forks of the relation. During create database we
* have a separate cleanup mechanism which deletes complete database
* directory. Therefore, each individual relation doesn't need to be
- * registered for cleanup.
+ * registered for cleanup. Also, the relid isn't needed, since it's not a
+ * global temporary relation.
*/
- RelationCreateStorage(dst_rlocator, relpersistence, false);
+ RelationCreateStorage(InvalidOid, dst_rlocator, relpersistence, false);
/* copy main fork. */
RelationCopyStorageUsingBuffer(src_rlocator, dst_rlocator, MAIN_FORKNUM,
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 560659f9568..f99703bd790 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -370,6 +370,7 @@ WaitLSN "Waiting to read or update shared Wait-for-LSN state."
LogicalDecodingControl "Waiting to read or update logical decoding status information."
DataChecksumsWorker "Waiting for data checksums worker."
AioWorkerControl "Waiting to update AIO worker information."
+GlobalTempRelControl "Waiting to update global temporary relation information."
#
# END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE)
@@ -417,6 +418,8 @@ XactSLRU "Waiting to access the transaction status SLRU cache."
ParallelVacuumDSA "Waiting for parallel vacuum dynamic shared memory allocation."
AioUringCompletion "Waiting for another process to complete IO via io_uring."
ShmemIndex "Waiting to find or allocate space in shared memory."
+GlobalTempRelDSA "Waiting for global temporary relation dynamic shared memory allocation."
+GlobalTempRelHash "Waiting to access global temporary relation shared usage table."
# No "ABI_compatibility" region here as WaitEventLWLock has its own C code.
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..e09ea8fe220 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -1032,6 +1032,9 @@ pg_relation_filepath(PG_FUNCTION_ARGS)
Assert(backend != INVALID_PROC_NUMBER);
}
break;
+ case RELPERSISTENCE_GLOBAL_TEMP:
+ backend = ProcNumberForTempRelations();
+ break;
default:
elog(ERROR, "invalid relpersistence: %c", relform->relpersistence);
backend = INVALID_PROC_NUMBER; /* placate compiler */
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 1b20f1f82e2..d9a96dd2e75 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -41,6 +41,7 @@
#include "access/xact.h"
#include "catalog/binary_upgrade.h"
#include "catalog/catalog.h"
+#include "catalog/global_temp.h"
#include "catalog/indexing.h"
#include "catalog/namespace.h"
#include "catalog/partition.h"
@@ -1172,8 +1173,9 @@ retry:
else
{
/*
- * If it's a temp table, but not one of ours, we have to use
- * the slow, grotty method to figure out the owning backend.
+ * If it's a local temp table, but not one of ours, we have to
+ * use the slow, grotty method to figure out the owning
+ * backend.
*
* Note: it's possible that rd_backend gets set to
* MyProcNumber here, in case we are looking at a pg_class
@@ -1190,6 +1192,10 @@ retry:
relation->rd_islocaltemp = false;
}
break;
+ case RELPERSISTENCE_GLOBAL_TEMP:
+ relation->rd_backend = ProcNumberForTempRelations();
+ relation->rd_islocaltemp = false;
+ break;
default:
elog(ERROR, "invalid relpersistence: %c",
relation->rd_rel->relpersistence);
@@ -2115,6 +2121,14 @@ RelationIdGetRelation(Oid relationId)
{
RelationRebuildRelation(rd);
+ /*
+ * If it's a global temporary relation, make sure it has been
+ * initialized for use in this backend (a prior initialization
+ * might have been rolled back).
+ */
+ if (RELATION_IS_GLOBAL_TEMP(rd))
+ InitGlobalTempRelation(rd);
+
/*
* Normally entries need to be valid here, but before the relcache
* has been initialized, not enough infrastructure exists to
@@ -2134,7 +2148,11 @@ RelationIdGetRelation(Oid relationId)
*/
rd = RelationBuildDesc(relationId, true);
if (RelationIsValid(rd))
+ {
RelationIncrementReferenceCount(rd);
+ if (RELATION_IS_GLOBAL_TEMP(rd))
+ InitGlobalTempRelation(rd);
+ }
return rd;
}
@@ -2208,6 +2226,21 @@ RelationDecrementReferenceCount(Relation rel)
ResourceOwnerForgetRelationRef(CurrentResourceOwner, rel);
}
+/*
+ * RelationMarkInvalid
+ * Mark a relation as invalid, if it's in the relcache, forcing it to be
+ * reloaded on next access.
+ */
+void
+RelationMarkInvalid(Oid relid)
+{
+ Relation relation;
+
+ RelationIdCacheLookup(relid, relation);
+ if (RelationIsValid(relation) && relation->rd_isvalid)
+ RelationInvalidateRelation(relation);
+}
+
/*
* RelationClose - close an open relation
*
@@ -2957,6 +2990,9 @@ RelationCacheInvalidateEntry(Oid relationId)
if (in_progress_list[i].reloid == relationId)
in_progress_list[i].invalidated = true;
}
+
+ /* Additional processing required for global temporary relations */
+ InvalidateGlobalTempRelation(relationId);
}
/*
@@ -3101,6 +3137,9 @@ RelationCacheInvalidate(bool debug_discard)
/* Any RelationBuildDesc() on the stack must start over. */
for (i = 0; i < in_progress_list_len; i++)
in_progress_list[i].invalidated = true;
+
+ /* Invalidate all in-use global temporary relations */
+ InvalidateGlobalTempRelation(InvalidOid);
}
static void
@@ -3657,6 +3696,7 @@ RelationBuildLocalRelation(const char *relname,
{
case RELPERSISTENCE_UNLOGGED:
case RELPERSISTENCE_PERMANENT:
+ Assert(!isTempOrTempToastNamespace(relnamespace));
rel->rd_backend = INVALID_PROC_NUMBER;
rel->rd_islocaltemp = false;
break;
@@ -3665,6 +3705,11 @@ RelationBuildLocalRelation(const char *relname,
rel->rd_backend = ProcNumberForTempRelations();
rel->rd_islocaltemp = true;
break;
+ case RELPERSISTENCE_GLOBAL_TEMP:
+ Assert(!isTempOrTempToastNamespace(relnamespace));
+ rel->rd_backend = ProcNumberForTempRelations();
+ rel->rd_islocaltemp = false;
+ break;
default:
elog(ERROR, "invalid relpersistence: %c", relpersistence);
break;
@@ -3901,7 +3946,8 @@ RelationSetNewRelfilenumber(Relation relation, char persistence)
/* handle these directly, at least for now */
SMgrRelation srel;
- srel = RelationCreateStorage(newrlocator, persistence, true);
+ srel = RelationCreateStorage(relation->rd_id, newrlocator,
+ persistence, true);
smgrclose(srel);
}
else
diff --git a/src/backend/utils/cache/relfilenumbermap.c b/src/backend/utils/cache/relfilenumbermap.c
index 6f970fafa05..1f9fe87ebb0 100644
--- a/src/backend/utils/cache/relfilenumbermap.c
+++ b/src/backend/utils/cache/relfilenumbermap.c
@@ -213,7 +213,8 @@ RelidByRelfilenumber(Oid reltablespace, RelFileNumber relfilenumber)
{
Form_pg_class classform = (Form_pg_class) GETSTRUCT(ntp);
- if (classform->relpersistence == RELPERSISTENCE_TEMP)
+ if (classform->relpersistence == RELPERSISTENCE_TEMP ||
+ classform->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
continue;
if (found)
diff --git a/src/bin/pg_amcheck/pg_amcheck.c b/src/bin/pg_amcheck/pg_amcheck.c
index 09ba0596400..27497d8bca3 100644
--- a/src/bin/pg_amcheck/pg_amcheck.c
+++ b/src/bin/pg_amcheck/pg_amcheck.c
@@ -859,7 +859,8 @@ prepare_heap_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
appendPQExpBuffer(sql,
"\n) v WHERE c.oid = %u "
- "AND c.relpersistence != " CppAsString2(RELPERSISTENCE_TEMP),
+ "AND c.relpersistence != " CppAsString2(RELPERSISTENCE_TEMP) " "
+ "AND c.relpersistence != " CppAsString2(RELPERSISTENCE_GLOBAL_TEMP),
rel->reloid);
}
@@ -893,6 +894,7 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
"WHERE c.oid = %u "
"AND c.oid = i.indexrelid "
"AND c.relpersistence != " CppAsString2(RELPERSISTENCE_TEMP) " "
+ "AND c.relpersistence != " CppAsString2(RELPERSISTENCE_GLOBAL_TEMP) " "
"AND i.indisready AND i.indisvalid AND i.indislive",
rel->datinfo->amcheck_schema,
(opts.heapallindexed ? "true" : "false"),
@@ -908,6 +910,7 @@ prepare_btree_command(PQExpBuffer sql, RelationInfo *rel, PGconn *conn)
"WHERE c.oid = %u "
"AND c.oid = i.indexrelid "
"AND c.relpersistence != " CppAsString2(RELPERSISTENCE_TEMP) " "
+ "AND c.relpersistence != " CppAsString2(RELPERSISTENCE_GLOBAL_TEMP) " "
"AND i.indisready AND i.indisvalid AND i.indislive",
rel->datinfo->amcheck_schema,
(opts.heapallindexed ? "true" : "false"),
@@ -1954,8 +1957,9 @@ compile_relation_list_one_db(PGconn *conn, SimplePtrList *relations,
* until firing off the amcheck command, as the state of an index may
* change by then.
*/
- appendPQExpBufferStr(&sql, "\nWHERE c.relpersistence != "
- CppAsString2(RELPERSISTENCE_TEMP));
+ appendPQExpBufferStr(&sql,
+ "\nWHERE c.relpersistence != " CppAsString2(RELPERSISTENCE_TEMP)
+ "\nAND c.relpersistence != " CppAsString2(RELPERSISTENCE_GLOBAL_TEMP));
if (opts.excludetbl || opts.excludeidx || opts.excludensp)
appendPQExpBufferStr(&sql, "\nAND ep.pattern_id IS NULL");
@@ -2024,7 +2028,8 @@ compile_relation_list_one_db(PGconn *conn, SimplePtrList *relations,
"\nAND (t.relname ~ ep.rel_regex OR ep.rel_regex IS NULL)"
"\nAND ep.heap_only"
"\nWHERE ep.pattern_id IS NULL"
- "\nAND t.relpersistence != " CppAsString2(RELPERSISTENCE_TEMP));
+ "\nAND t.relpersistence != " CppAsString2(RELPERSISTENCE_TEMP)
+ "\nAND t.relpersistence != " CppAsString2(RELPERSISTENCE_GLOBAL_TEMP));
appendPQExpBufferStr(&sql,
"\n)");
}
@@ -2043,7 +2048,8 @@ compile_relation_list_one_db(PGconn *conn, SimplePtrList *relations,
"ON r.oid = i.indrelid "
"INNER JOIN pg_catalog.pg_class c "
"ON i.indexrelid = c.oid "
- "AND c.relpersistence != " CppAsString2(RELPERSISTENCE_TEMP));
+ "AND c.relpersistence != " CppAsString2(RELPERSISTENCE_TEMP) " "
+ "AND c.relpersistence != " CppAsString2(RELPERSISTENCE_GLOBAL_TEMP));
if (opts.excludeidx || opts.excludensp)
appendPQExpBufferStr(&sql,
"\nINNER JOIN pg_catalog.pg_namespace n "
@@ -2082,7 +2088,8 @@ compile_relation_list_one_db(PGconn *conn, SimplePtrList *relations,
"ON t.oid = i.indrelid"
"\nINNER JOIN pg_catalog.pg_class c "
"ON i.indexrelid = c.oid "
- "AND c.relpersistence != " CppAsString2(RELPERSISTENCE_TEMP));
+ "AND c.relpersistence != " CppAsString2(RELPERSISTENCE_TEMP) " "
+ "AND c.relpersistence != " CppAsString2(RELPERSISTENCE_GLOBAL_TEMP));
if (opts.excludeidx)
appendPQExpBufferStr(&sql,
"\nLEFT OUTER JOIN exclude_pat ep "
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index f67daf85911..f52f5f5a704 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -3024,6 +3024,10 @@ makeTableDataInfo(DumpOptions *dopt, TableInfo *tbinfo)
if (tbinfo->relkind == RELKIND_PARTITIONED_TABLE)
return;
+ /* Don't dump data in global temporary tables */
+ 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)
@@ -7177,6 +7181,7 @@ getTables(Archive *fout, int *numTables)
int i_relhasoids;
int i_relhastriggers;
int i_relpersistence;
+ int i_reloncommit;
int i_relispopulated;
int i_relreplident;
int i_relrowsec;
@@ -7227,6 +7232,12 @@ getTables(Archive *fout, int *numTables)
else
appendPQExpBufferStr(query, "0 AS relallfrozen, ");
+ if (fout->remoteVersion >= 200000)
+ appendPQExpBufferStr(query, "c.reloncommit, ");
+ else
+ appendPQExpBufferStr(query,
+ CppAsString2(RELONCOMMIT_PRESERVE_ROWS) " AS reloncommit, ");
+
appendPQExpBufferStr(query,
"c.relhastriggers, c.relpersistence, "
"c.reloftype, "
@@ -7368,6 +7379,7 @@ getTables(Archive *fout, int *numTables)
i_relhasoids = PQfnumber(res, "relhasoids");
i_relhastriggers = PQfnumber(res, "relhastriggers");
i_relpersistence = PQfnumber(res, "relpersistence");
+ i_reloncommit = PQfnumber(res, "reloncommit");
i_relispopulated = PQfnumber(res, "relispopulated");
i_relreplident = PQfnumber(res, "relreplident");
i_relrowsec = PQfnumber(res, "relrowsecurity");
@@ -7446,6 +7458,7 @@ getTables(Archive *fout, int *numTables)
tblinfo[i].hasoids = (strcmp(PQgetvalue(res, i, i_relhasoids), "t") == 0);
tblinfo[i].hastriggers = (strcmp(PQgetvalue(res, i, i_relhastriggers), "t") == 0);
tblinfo[i].relpersistence = *(PQgetvalue(res, i, i_relpersistence));
+ tblinfo[i].reloncommit = *(PQgetvalue(res, i, i_reloncommit));
tblinfo[i].relispopulated = (strcmp(PQgetvalue(res, i, i_relispopulated), "t") == 0);
tblinfo[i].relreplident = *(PQgetvalue(res, i, i_relreplident));
tblinfo[i].rowsec = (strcmp(PQgetvalue(res, i, i_relrowsec), "t") == 0);
@@ -17205,7 +17218,9 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
appendPQExpBuffer(q, "CREATE %s%s %s",
(tbinfo->relpersistence == RELPERSISTENCE_UNLOGGED &&
tbinfo->relkind != RELKIND_PARTITIONED_TABLE) ?
- "UNLOGGED " : "",
+ "UNLOGGED " :
+ tbinfo->relpersistence == RELPERSISTENCE_GLOBAL_TEMP ?
+ "GLOBAL TEMP " : "",
reltypename,
qualrelname);
@@ -17456,6 +17471,10 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
appendPQExpBufferChar(q, ')');
}
+ /* Dump ON COMMIT action (global temporary tables only) */
+ if (tbinfo->reloncommit == RELONCOMMIT_DELETE_ROWS)
+ appendPQExpBufferStr(q, "\nON COMMIT DELETE ROWS");
+
/* Dump generic options if any */
if (ftoptions && ftoptions[0])
appendPQExpBuffer(q, "\nOPTIONS (\n %s\n)", ftoptions);
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 5a6726d8b12..1f863186cba 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -309,6 +309,7 @@ typedef struct _tableInfo
const char *rolname;
char relkind;
char relpersistence; /* relation persistence */
+ char reloncommit; /* ON COMMIT action (for global temp table) */
bool relispopulated; /* relation is populated */
char relreplident; /* replica identifier */
char *reltablespace; /* relation tablespace */
diff --git a/src/bin/pg_upgrade/info.c b/src/bin/pg_upgrade/info.c
index 37fff93892f..91924857158 100644
--- a/src/bin/pg_upgrade/info.c
+++ b/src/bin/pg_upgrade/info.c
@@ -503,6 +503,9 @@ get_rel_infos_query(void)
" ON c.relnamespace = n.oid "
" WHERE relkind IN (" CppAsString2(RELKIND_RELATION) ", "
CppAsString2(RELKIND_MATVIEW) "%s) AND "
+ /* exclude global temporary tables */
+ " 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 e710db8e102..daaa9f9e69d 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2026,13 +2026,23 @@ describeOneTableDetails(const char *schemaname,
if (tableinfo.relpersistence == RELPERSISTENCE_UNLOGGED)
printfPQExpBuffer(&title, _("Unlogged table \"%s.%s\""),
schemaname, relationname);
+ else if (tableinfo.relpersistence == RELPERSISTENCE_TEMP)
+ printfPQExpBuffer(&title, _("Temporary 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);
break;
case RELKIND_VIEW:
- printfPQExpBuffer(&title, _("View \"%s.%s\""),
- schemaname, relationname);
+ if (tableinfo.relpersistence == RELPERSISTENCE_TEMP)
+ printfPQExpBuffer(&title, _("Temporary view \"%s.%s\""),
+ schemaname, relationname);
+ else
+ printfPQExpBuffer(&title, _("View \"%s.%s\""),
+ schemaname, relationname);
break;
case RELKIND_MATVIEW:
printfPQExpBuffer(&title, _("Materialized view \"%s.%s\""),
@@ -2070,6 +2080,12 @@ describeOneTableDetails(const char *schemaname,
if (tableinfo.relpersistence == RELPERSISTENCE_UNLOGGED)
printfPQExpBuffer(&title, _("Unlogged partitioned table \"%s.%s\""),
schemaname, relationname);
+ else if (tableinfo.relpersistence == RELPERSISTENCE_TEMP)
+ printfPQExpBuffer(&title, _("Temporary partitioned table \"%s.%s\""),
+ schemaname, relationname);
+ else if (tableinfo.relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+ printfPQExpBuffer(&title, _("Global temporary partitioned table \"%s.%s\""),
+ schemaname, relationname);
else
printfPQExpBuffer(&title, _("Partitioned table \"%s.%s\""),
schemaname, relationname);
@@ -4178,10 +4194,12 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys
",\n CASE c.relpersistence "
"WHEN " CppAsString2(RELPERSISTENCE_PERMANENT) " THEN '%s' "
"WHEN " CppAsString2(RELPERSISTENCE_TEMP) " THEN '%s' "
+ "WHEN " CppAsString2(RELPERSISTENCE_GLOBAL_TEMP) " THEN '%s' "
"WHEN " CppAsString2(RELPERSISTENCE_UNLOGGED) " THEN '%s' "
"END as \"%s\"",
gettext_noop("permanent"),
gettext_noop("temporary"),
+ gettext_noop("global temporary"),
gettext_noop("unlogged"),
gettext_noop("Persistence"));
translate_columns[cols_so_far] = true;
diff --git a/src/bin/scripts/vacuuming.c b/src/bin/scripts/vacuuming.c
index 855a5754c98..ef7eb8ed327 100644
--- a/src/bin/scripts/vacuuming.c
+++ b/src/bin/scripts/vacuuming.c
@@ -624,7 +624,9 @@ retrieve_objects(PGconn *conn, vacuumingOptions *vacopts,
*/
appendPQExpBufferStr(&catalog_query,
" WHERE c.relpersistence OPERATOR(pg_catalog.!=) "
- CppAsString2(RELPERSISTENCE_TEMP) "\n");
+ CppAsString2(RELPERSISTENCE_TEMP)
+ "\nAND c.relpersistence OPERATOR(pg_catalog.!=) "
+ CppAsString2(RELPERSISTENCE_GLOBAL_TEMP) "\n");
/*
* Used to match the tables or schemas listed by the user, for the WHERE
diff --git a/src/include/catalog/global_temp.h b/src/include/catalog/global_temp.h
new file mode 100644
index 00000000000..8d05c02f297
--- /dev/null
+++ b/src/include/catalog/global_temp.h
@@ -0,0 +1,45 @@
+/*-------------------------------------------------------------------------
+ *
+ * global_temp.h
+ * Global temporary relation management.
+ *
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * src/include/catalog/global_temp.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef GLOBAL_TEMP_H
+#define GLOBAL_TEMP_H
+
+#include "storage/relfilelocator.h"
+#include "utils/rel.h"
+
+extern void TrackGlobalTempRelationStorage(Oid relid, RelFileLocator rlocator,
+ ProcNumber backend, bool create);
+
+extern void ReassignGlobalTempRelationStorage(RelFileLocator rlocator,
+ Oid newRelid);
+
+extern void InitGlobalTempRelation(Relation relation);
+
+extern void InvalidateGlobalTempRelation(Oid relid);
+
+extern void GlobalTempRelationCreated(Relation relation);
+
+extern void GlobalTempRelationDropped(Oid relid);
+
+extern void PreCommit_GlobalTempRelation(void);
+
+extern void AtEOXact_GlobalTempRelation(bool isCommit);
+
+extern void AtEOSubXact_GlobalTempRelation(bool isCommit,
+ SubTransactionId mySubid,
+ SubTransactionId parentSubid);
+
+extern bool IsOtherUsingGlobalTempRelation(Oid relid);
+
+extern List *AllGlobalTempRelationsInUse(Oid dbId);
+
+#endif /* GLOBAL_TEMP_H */
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index 0e2c53591c4..bf0d8ca7bc1 100644
--- a/src/include/catalog/pg_class.h
+++ b/src/include/catalog/pg_class.h
@@ -185,7 +185,8 @@ 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_TEMP 't' /* temp table (in temp schema) */
+#define RELPERSISTENCE_GLOBAL_TEMP 'g' /* global temporary table */
/* on-commit action (only temporary tables support delete/drop) */
#define RELONCOMMIT_PRESERVE_ROWS 'p' /* default: keep data on commit */
diff --git a/src/include/catalog/storage.h b/src/include/catalog/storage.h
index 70f619a6d6f..7827c4ba710 100644
--- a/src/include/catalog/storage.h
+++ b/src/include/catalog/storage.h
@@ -22,7 +22,7 @@
/* GUC variables */
extern PGDLLIMPORT int wal_skip_threshold;
-extern SMgrRelation RelationCreateStorage(RelFileLocator rlocator,
+extern SMgrRelation RelationCreateStorage(Oid relid, RelFileLocator rlocator,
char relpersistence,
bool register_delete);
extern void RelationDropStorage(Relation rel);
diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h
index d7eb648bd27..557ff4dcbe3 100644
--- a/src/include/storage/lwlocklist.h
+++ b/src/include/storage/lwlocklist.h
@@ -89,6 +89,7 @@ PG_LWLOCK(54, WaitLSN)
PG_LWLOCK(55, LogicalDecodingControl)
PG_LWLOCK(56, DataChecksumsWorker)
PG_LWLOCK(57, AioWorkerControl)
+PG_LWLOCK(58, GlobalTempRelControl)
/*
* There also exist several built-in LWLock tranches. As with the predefined
@@ -140,3 +141,5 @@ PG_LWLOCKTRANCHE(XACT_SLRU, XactSLRU)
PG_LWLOCKTRANCHE(PARALLEL_VACUUM_DSA, ParallelVacuumDSA)
PG_LWLOCKTRANCHE(AIO_URING_COMPLETION, AioUringCompletion)
PG_LWLOCKTRANCHE(SHMEM_INDEX, ShmemIndex)
+PG_LWLOCKTRANCHE(GLOBAL_TEMP_REL_DSA, GlobalTempRelDSA)
+PG_LWLOCKTRANCHE(GLOBAL_TEMP_REL_HASH, GlobalTempRelHash)
diff --git a/src/include/storage/subsystemlist.h b/src/include/storage/subsystemlist.h
index 9ad619080be..89378d1d87f 100644
--- a/src/include/storage/subsystemlist.h
+++ b/src/include/storage/subsystemlist.h
@@ -88,3 +88,6 @@ PG_SHMEM_SUBSYSTEM(DataChecksumsShmemCallbacks)
/* AIO subsystem. This delegates to the method-specific callbacks */
PG_SHMEM_SUBSYSTEM(AioShmemCallbacks)
+
+/* global temporary relation usage table */
+PG_SHMEM_SUBSYSTEM(GlobalTempRelShmemCallbacks)
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 89c159b133f..79fc0c69c5d 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -58,7 +58,7 @@ typedef struct RelationData
SMgrRelation rd_smgr; /* cached file handle, or NULL */
int rd_refcnt; /* reference count */
ProcNumber rd_backend; /* owning backend's proc number, if temp rel */
- bool rd_islocaltemp; /* rel is a temp rel of this session */
+ bool rd_islocaltemp; /* rel is a local temp rel of this session */
bool rd_isnailed; /* rel is nailed in cache */
bool rd_isvalid; /* relcache entry is valid */
bool rd_indexvalid; /* is rd_indexlist valid? (also rd_pkindex and
@@ -646,13 +646,15 @@ RelationCloseSmgr(Relation relation)
* True if relation's pages are stored in local buffers.
*/
#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
- * If a rel is either temp or newly created in the current transaction,
- * it can be assumed to be accessible only to the current backend.
- * This is typically used to decide that we can skip acquiring locks.
+ * If a rel is either local temp or newly created in the current
+ * transaction, it can be assumed to be accessible only to the current
+ * backend. This is typically used to decide that we can skip acquiring
+ * locks.
*
* Beware of multiple eval of argument
*/
@@ -662,7 +664,8 @@ RelationCloseSmgr(Relation relation)
/*
* RELATION_IS_OTHER_TEMP
- * Test for a temporary relation that belongs to some other session.
+ * Test for a local temporary relation that belongs to some other
+ * session.
*
* Reading another session's temp-table data through never works right:
* the owning session keeps the data in its private local buffer pool,
@@ -679,6 +682,13 @@ RelationCloseSmgr(Relation relation)
((relation)->rd_rel->relpersistence == RELPERSISTENCE_TEMP && \
!(relation)->rd_islocaltemp)
+/*
+ * RELATION_IS_GLOBAL_TEMP
+ * True if the relation is a global temporary relation.
+ */
+#define RELATION_IS_GLOBAL_TEMP(relation) \
+ ((relation)->rd_rel->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+
/*
* RelationIsScannable
@@ -727,5 +737,6 @@ RelationCloseSmgr(Relation relation)
/* routines in utils/cache/relcache.c */
extern void RelationIncrementReferenceCount(Relation rel);
extern void RelationDecrementReferenceCount(Relation rel);
+extern void RelationMarkInvalid(Oid relid);
#endif /* REL_H */
diff --git a/src/test/isolation/expected/global-temp.out b/src/test/isolation/expected/global-temp.out
new file mode 100644
index 00000000000..5ac8eccbba6
--- /dev/null
+++ b/src/test/isolation/expected/global-temp.out
@@ -0,0 +1,252 @@
+Parsed test spec with 2 sessions
+
+starting permutation: ins1 ins2 sel1 sel2
+step ins1: INSERT INTO tmp VALUES (1, 's1');
+step ins2: INSERT INTO tmp VALUES (1, 's2');
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+ 1|s1
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+ 1|s2
+(1 row)
+
+
+starting permutation: ins1p1 ins1p2 ins2p1 ins2p2 sel1p sel2p
+step ins1p1: INSERT INTO tmp_parted VALUES (1, 's1 p1');
+step ins1p2: INSERT INTO tmp_parted VALUES (2, 's1 p2');
+step ins2p1: INSERT INTO tmp_parted VALUES (1, 's2 p1');
+step ins2p2: INSERT INTO tmp_parted VALUES (2, 's2 p2');
+step sel1p: SELECT tableoid::regclass, * FROM tmp_parted;
+tableoid|key|val
+--------+---+-----
+tmp_p1 | 1|s1 p1
+tmp_p2 | 2|s1 p2
+(2 rows)
+
+step sel2p: SELECT tableoid::regclass, * FROM tmp_parted;
+tableoid|key|val
+--------+---+-----
+tmp_p1 | 1|s2 p1
+tmp_p2 | 2|s2 p2
+(2 rows)
+
+
+starting permutation: ins1 b2 ins2 sel1 sel2 c2 sel1 sel2
+step ins1: INSERT INTO tmp VALUES (1, 's1');
+step b2: BEGIN;
+step ins2: INSERT INTO tmp VALUES (1, 's2');
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+ 1|s1
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+ 1|s2
+(1 row)
+
+step c2: COMMIT;
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+ 1|s1
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+ 1|s2
+(1 row)
+
+
+starting permutation: ins1 b2 ins2 sel1 sel2 r2 sel1 sel2
+step ins1: INSERT INTO tmp VALUES (1, 's1');
+step b2: BEGIN;
+step ins2: INSERT INTO tmp VALUES (1, 's2');
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+ 1|s1
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+ 1|s2
+(1 row)
+
+step r2: ROLLBACK;
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+ 1|s1
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+(0 rows)
+
+
+starting permutation: ins1 b2 ins2 sel1 sel2 sp2 r2 sel1 sel2
+step ins1: INSERT INTO tmp VALUES (1, 's1');
+step b2: BEGIN;
+step ins2: INSERT INTO tmp VALUES (1, 's2');
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+ 1|s1
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+ 1|s2
+(1 row)
+
+step sp2: SAVEPOINT sp;
+step r2: ROLLBACK;
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+ 1|s1
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+(0 rows)
+
+
+starting permutation: ins1 b2 sp2 ins2 sel1 sel2 rsp2 sel1 sel2 r2 sel1 sel2
+step ins1: INSERT INTO tmp VALUES (1, 's1');
+step b2: BEGIN;
+step sp2: SAVEPOINT sp;
+step ins2: INSERT INTO tmp VALUES (1, 's2');
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+ 1|s1
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+ 1|s2
+(1 row)
+
+step rsp2: ROLLBACK TO SAVEPOINT sp;
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+ 1|s1
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+(0 rows)
+
+step r2: ROLLBACK;
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+ 1|s1
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+(0 rows)
+
+
+starting permutation: ins1 b2 ins2 sp2 t2 rsp2 sel1 sel2 r2 sel1 sel2
+step ins1: INSERT INTO tmp VALUES (1, 's1');
+step b2: BEGIN;
+step ins2: INSERT INTO tmp VALUES (1, 's2');
+step sp2: SAVEPOINT sp;
+step t2: TRUNCATE tmp;
+step rsp2: ROLLBACK TO SAVEPOINT sp;
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+ 1|s1
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+ 1|s2
+(1 row)
+
+step r2: ROLLBACK;
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+ 1|s1
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+(0 rows)
+
+
+starting permutation: create1 ins1_2 alter1a alter1b ins2_2 seltype1 seltype2 drop1
+step create1: CREATE GLOBAL TEMP TABLE tmp2 (key int, val text);
+step ins1_2: INSERT INTO tmp2 VALUES (1, 's1');
+step alter1a: ALTER TABLE tmp2 ALTER COLUMN key SET DATA TYPE numeric;
+step alter1b: ALTER TABLE tmp2 ALTER COLUMN val SET NOT NULL;
+step ins2_2: INSERT INTO tmp2 VALUES (1, 's2');
+step seltype1: SELECT key, pg_typeof(key), val FROM tmp2;
+key|pg_typeof|val
+---+---------+---
+ 1|numeric |s1
+(1 row)
+
+step seltype2: SELECT key, pg_typeof(key), val FROM tmp2;
+key|pg_typeof|val
+---+---------+---
+ 1|numeric |s2
+(1 row)
+
+step drop1: DROP TABLE tmp2;
+
+starting permutation: create1 ins1_2 ins2_2 alter1a alter1b seltype1 seltype2 drop1
+step create1: CREATE GLOBAL TEMP TABLE tmp2 (key int, val text);
+step ins1_2: INSERT INTO tmp2 VALUES (1, 's1');
+step ins2_2: INSERT INTO tmp2 VALUES (1, 's2');
+step alter1a: ALTER TABLE tmp2 ALTER COLUMN key SET DATA TYPE numeric;
+ERROR: cannot rewrite global temporary table "tmp2" because it is being used in another session
+step alter1b: ALTER TABLE tmp2 ALTER COLUMN val SET NOT NULL;
+ERROR: cannot add or alter constraints of global temporary table "tmp2" because it is being used in another session
+step seltype1: SELECT key, pg_typeof(key), val FROM tmp2;
+key|pg_typeof|val
+---+---------+---
+ 1|integer |s1
+(1 row)
+
+step seltype2: SELECT key, pg_typeof(key), val FROM tmp2;
+key|pg_typeof|val
+---+---------+---
+ 1|integer |s2
+(1 row)
+
+step drop1: DROP TABLE tmp2;
+
+starting permutation: create1dr ins1_2 ins2_2 drop1 create1dr ins1_2 ins2_2 drop1
+step create1dr: CREATE GLOBAL TEMP TABLE tmp2 (key int, val text) ON COMMIT DELETE ROWS;
+step ins1_2: INSERT INTO tmp2 VALUES (1, 's1');
+step ins2_2: INSERT INTO tmp2 VALUES (1, 's2');
+step drop1: DROP TABLE tmp2;
+step create1dr: CREATE GLOBAL TEMP TABLE tmp2 (key int, val text) ON COMMIT DELETE ROWS;
+step ins1_2: INSERT INTO tmp2 VALUES (1, 's1');
+step ins2_2: INSERT INTO tmp2 VALUES (1, 's2');
+step drop1: DROP TABLE tmp2;
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b8ebe92553c..7d1aacec267 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -128,3 +128,4 @@ test: matview-write-skew
test: lock-nowait
test: for-portion-of
test: ddl-dependency-locking
+test: global-temp
diff --git a/src/test/isolation/specs/global-temp.spec b/src/test/isolation/specs/global-temp.spec
new file mode 100644
index 00000000000..db242426fe5
--- /dev/null
+++ b/src/test/isolation/specs/global-temp.spec
@@ -0,0 +1,60 @@
+# Test global temporary relations
+
+setup {
+ CREATE GLOBAL TEMP TABLE tmp (key int, val text);
+
+ CREATE GLOBAL TEMP TABLE tmp_parted (key int, val text) PARTITION BY LIST (key);
+ CREATE GLOBAL TEMP TABLE tmp_p1 PARTITION OF tmp_parted FOR VALUES IN (1);
+ CREATE GLOBAL TEMP TABLE tmp_p2 PARTITION OF tmp_parted FOR VALUES IN ((2), (3));
+}
+
+teardown {
+ DROP TABLE tmp, tmp_parted;
+}
+
+session s1
+step ins1 { INSERT INTO tmp VALUES (1, 's1'); }
+step ins1p1 { INSERT INTO tmp_parted VALUES (1, 's1 p1'); }
+step ins1p2 { INSERT INTO tmp_parted VALUES (2, 's1 p2'); }
+step sel1 { SELECT * FROM tmp; }
+step sel1p { SELECT tableoid::regclass, * FROM tmp_parted; }
+step create1 { CREATE GLOBAL TEMP TABLE tmp2 (key int, val text); }
+step create1dr { CREATE GLOBAL TEMP TABLE tmp2 (key int, val text) ON COMMIT DELETE ROWS; }
+step ins1_2 { INSERT INTO tmp2 VALUES (1, 's1'); }
+step alter1a { ALTER TABLE tmp2 ALTER COLUMN key SET DATA TYPE numeric; }
+step alter1b { ALTER TABLE tmp2 ALTER COLUMN val SET NOT NULL; }
+step seltype1 { SELECT key, pg_typeof(key), val FROM tmp2; }
+step drop1 { DROP TABLE tmp2; }
+
+session s2
+step b2 { BEGIN; }
+step ins2 { INSERT INTO tmp VALUES (1, 's2'); }
+step ins2p1 { INSERT INTO tmp_parted VALUES (1, 's2 p1'); }
+step ins2p2 { INSERT INTO tmp_parted VALUES (2, 's2 p2'); }
+step sel2 { SELECT * FROM tmp; }
+step sel2p { SELECT tableoid::regclass, * FROM tmp_parted; }
+step t2 { TRUNCATE tmp; }
+step c2 { COMMIT; }
+step r2 { ROLLBACK; }
+step sp2 { SAVEPOINT sp; }
+step rsp2 { ROLLBACK TO SAVEPOINT sp; }
+step ins2_2 { INSERT INTO tmp2 VALUES (1, 's2'); }
+step seltype2 { SELECT key, pg_typeof(key), val FROM tmp2; }
+
+# Basic effects
+permutation ins1 ins2 sel1 sel2
+permutation ins1p1 ins1p2 ins2p1 ins2p2 sel1p sel2p
+
+# Test rollback of GTT initialization
+permutation ins1 b2 ins2 sel1 sel2 c2 sel1 sel2
+permutation ins1 b2 ins2 sel1 sel2 r2 sel1 sel2
+permutation ins1 b2 ins2 sel1 sel2 sp2 r2 sel1 sel2
+permutation ins1 b2 sp2 ins2 sel1 sel2 rsp2 sel1 sel2 r2 sel1 sel2
+permutation ins1 b2 ins2 sp2 t2 rsp2 sel1 sel2 r2 sel1 sel2
+
+# Test prevention of ALTER TABLE with rewrite, if in use
+permutation create1 ins1_2 alter1a alter1b ins2_2 seltype1 seltype2 drop1
+permutation create1 ins1_2 ins2_2 alter1a alter1b seltype1 seltype2 drop1
+
+# Test DROP with ON COMMIT DELETE ROWS
+permutation create1dr ins1_2 ins2_2 drop1 create1dr ins1_2 ins2_2 drop1
diff --git a/src/test/modules/test_checksums/t/010_global_temp.pl b/src/test/modules/test_checksums/t/010_global_temp.pl
new file mode 100644
index 00000000000..4355ebe17bd
--- /dev/null
+++ b/src/test/modules/test_checksums/t/010_global_temp.pl
@@ -0,0 +1,56 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# global temporary tables
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize node with checksums disabled.
+my $node = PostgreSQL::Test::Cluster->new('global_temp_table_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Create a global temporary table in an interactive psql process. Should act
+# as a barrier for checksum enablement to block on.
+my $bsession = $node->background_psql('postgres');
+$bsession->query_safe(
+ 'CREATE GLOBAL TEMPORARY TABLE gtt AS SELECT * FROM generate_series(1, 10000) x;');
+
+# Ensure that checksums are disabled
+test_checksum_state($node, 'off');
+
+# In another session, make sure we can see the blocking global temporary table
+# but start processing anyways and check that we are blocked with a proper
+# wait event.
+my $result = $node->safe_psql('postgres',
+ "SELECT relpersistence FROM pg_catalog.pg_class WHERE relname = 'gtt';");
+is($result, 'g', 'ensure we can see the global temporary table');
+
+# Enable, but stop waiting at inprogress-on since it will sit there until the
+# above temporary table is removed.
+enable_data_checksums($node, wait => 'inprogress-on');
+
+# Ensure that checksum enablement continues to block
+sleep(1);
+test_checksum_state($node, 'inprogress-on');
+
+# Make sure background session can still read back its data
+$result = $bsession->query_safe('SELECT count(*) FROM gtt WHERE x % 2 = 0;');
+is($result, '5000', 'ensure global temporary table can still be read');
+
+# Quit background session and check that checksum enablement unblocks
+$bsession->quit;
+wait_for_checksum_state($node, 'on');
+
+$node->stop;
+done_testing();
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index e167a41ce79..a71ce0a0cf9 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -3593,6 +3593,7 @@ FROM pg_class,
pg_filenode_relation(reltablespace, pg_relation_filenode(oid)) AS mapped_oid
WHERE relkind IN ('r', 'i', 'S', 't', 'm')
AND relpersistence != 't'
+ AND relpersistence != 'g'
AND mapped_oid IS DISTINCT FROM oid;
SELECT m.* FROM filenode_mapping m LEFT JOIN pg_class c ON c.oid = m.oid
WHERE c.oid IS NOT NULL OR m.mapped_oid IS NOT NULL;
@@ -4099,9 +4100,12 @@ DROP TABLE parent CASCADE;
-- check any TEMP-ness
CREATE TEMP TABLE temp_parted (a int) PARTITION BY LIST (a);
CREATE TABLE perm_part (a int);
+CREATE GLOBAL TEMP TABLE global_temp_part (a int);
ALTER TABLE temp_parted ATTACH PARTITION perm_part FOR VALUES IN (1);
ERROR: cannot attach a permanent relation as partition of temporary relation "temp_parted"
-DROP TABLE temp_parted, perm_part;
+ALTER TABLE temp_parted ATTACH PARTITION global_temp_part FOR VALUES IN (1);
+ERROR: cannot attach a global temporary relation as partition of local temporary relation "temp_parted"
+DROP TABLE temp_parted, perm_part, global_temp_part;
-- check that the table being attached is not a typed table
CREATE TYPE mytype AS (a int);
CREATE TABLE fail_part OF mytype;
diff --git a/src/test/regress/expected/create_property_graph.out b/src/test/regress/expected/create_property_graph.out
index 2a52a396fb4..8969da5e384 100644
--- a/src/test/regress/expected/create_property_graph.out
+++ b/src/test/regress/expected/create_property_graph.out
@@ -117,6 +117,8 @@ SELECT pg_get_propgraphdef('g5'::regclass);
-- error cases
CREATE UNLOGGED PROPERTY GRAPH gx VERTEX TABLES (xx, yy);
ERROR: property graphs cannot be unlogged because they do not have storage
+CREATE GLOBAL TEMP PROPERTY GRAPH gx VERTEX TABLES (xx, yy);
+ERROR: property graphs cannot be global temporary because they do not have storage
CREATE PROPERTY GRAPH gx VERTEX TABLES (xx, yy);
ERROR: relation "xx" does not exist
CREATE PROPERTY GRAPH gx VERTEX TABLES (t1 KEY (a), t2 KEY (i), t1 KEY (a));
diff --git a/src/test/regress/expected/create_table.out b/src/test/regress/expected/create_table.out
index 2df8761ae8e..80525971873 100644
--- a/src/test/regress/expected/create_table.out
+++ b/src/test/regress/expected/create_table.out
@@ -46,7 +46,7 @@ CREATE TABLE pg_temp.implicitly_temp (a int primary key); -- OK
CREATE TEMP TABLE explicitly_temp (a int primary key); -- also OK
CREATE TEMP TABLE pg_temp.doubly_temp (a int primary key); -- also OK
CREATE TEMP TABLE public.temp_to_perm (a int primary key); -- not OK
-ERROR: cannot create temporary relation in non-temporary schema
+ERROR: cannot create local temporary relation in non-temporary schema
LINE 1: CREATE TEMP TABLE public.temp_to_perm (a int primary key);
^
DROP TABLE unlogged1, public.unlogged2;
@@ -1054,13 +1054,21 @@ drop table boolspart;
-- partitions mixing temporary and permanent relations
create table perm_parted (a int) partition by list (a);
create temporary table temp_parted (a int) partition by list (a);
+create global temporary table global_temp_parted (a int) partition by list (a);
create table perm_part partition of temp_parted default; -- error
ERROR: cannot create a permanent relation as partition of temporary relation "temp_parted"
+create table perm_part partition of global_temp_parted default; -- ok
create temp table temp_part partition of perm_parted default; -- error
ERROR: cannot create a temporary relation as partition of permanent relation "perm_parted"
+create temp table temp_part partition of global_temp_parted default; -- error
+ERROR: cannot create a local temporary relation as partition of global temporary relation "global_temp_parted"
create temp table temp_part partition of temp_parted default; -- ok
+create global temp table global_temp_part partition of temp_parted default; -- error
+ERROR: cannot create a global temporary relation as partition of local temporary relation "temp_parted"
+create global temp table global_temp_part partition of perm_parted default; -- ok
drop table perm_parted cascade;
drop table temp_parted cascade;
+drop table global_temp_parted cascade;
-- check that adding partitions to a table while it is being used is prevented
create table tab_part_create (a int) partition by list (a);
create or replace function func_part_create() returns trigger
diff --git a/src/test/regress/expected/create_view.out b/src/test/regress/expected/create_view.out
index 63cf4b4371d..175376168f1 100644
--- a/src/test/regress/expected/create_view.out
+++ b/src/test/regress/expected/create_view.out
@@ -123,7 +123,7 @@ CREATE VIEW temp_view_test.v2 AS SELECT * FROM base_table;
CREATE VIEW temp_view_test.v3_temp AS SELECT * FROM temp_table;
NOTICE: view "v3_temp" will be a temporary view
DETAIL: It depends on temporary table temp_table.
-ERROR: cannot create temporary relation in non-temporary schema
+ERROR: cannot create local temporary relation in non-temporary schema
-- should fail
CREATE SCHEMA test_view_schema
CREATE TEMP VIEW testview AS SELECT 1;
diff --git a/src/test/regress/expected/global_temp.out b/src/test/regress/expected/global_temp.out
new file mode 100644
index 00000000000..43e9116f7d5
--- /dev/null
+++ b/src/test/regress/expected/global_temp.out
@@ -0,0 +1,290 @@
+--
+-- GLOBAL TEMP
+--
+CREATE SCHEMA global_temp_tests;
+GRANT USAGE ON SCHEMA global_temp_tests TO PUBLIC;
+SET search_path = global_temp_tests;
+CREATE ROLE regress_global_temp_user;
+GRANT CREATE ON SCHEMA global_temp_tests TO regress_global_temp_user;
+GRANT CREATE ON DATABASE regression TO regress_global_temp_user;
+SET ROLE regress_global_temp_user;
+-- Test table creation
+CREATE GLOBAL TEMP TABLE pg_temp.tmp1 (a int); -- fail
+ERROR: cannot create global temporary relation in temporary schema
+LINE 1: CREATE GLOBAL TEMP TABLE pg_temp.tmp1 (a int);
+ ^
+CREATE GLOBAL TEMP TABLE tmp1 (a int);
+CREATE SCHEMA global_temp_xxx CREATE GLOBAL TEMP TABLE tmp2 (a int);
+CREATE SCHEMA global_temp_yyy;
+CREATE GLOBAL TEMP TABLE global_temp_yyy.tmp3 (a int);
+\d tmp1
+ Global temporary table "global_temp_tests.tmp1"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ a | integer | | |
+
+\dt+ global_temp_*.tmp*
+ List of tables
+ Schema | Name | Type | Owner | Persistence | Size | Description
+-------------------+------+-------+--------------------------+------------------+---------+-------------
+ global_temp_tests | tmp1 | table | regress_global_temp_user | global temporary | 0 bytes |
+ global_temp_xxx | tmp2 | table | regress_global_temp_user | global temporary | 0 bytes |
+ global_temp_yyy | tmp3 | table | regress_global_temp_user | global temporary | 0 bytes |
+(3 rows)
+
+-- Information schema
+SELECT table_catalog, table_schema, table_name, table_type
+FROM information_schema.tables
+WHERE table_name ~ 'tmp' AND table_schema ~ 'global_temp'
+ORDER BY table_name;
+ table_catalog | table_schema | table_name | table_type
+---------------+-------------------+------------+------------------
+ regression | global_temp_tests | tmp1 | GLOBAL TEMPORARY
+ regression | global_temp_xxx | tmp2 | GLOBAL TEMPORARY
+ regression | global_temp_yyy | tmp3 | GLOBAL TEMPORARY
+(3 rows)
+
+DROP SCHEMA global_temp_xxx CASCADE;
+NOTICE: drop cascades to table global_temp_xxx.tmp2
+DROP SCHEMA global_temp_yyy CASCADE;
+NOTICE: drop cascades to table global_temp_yyy.tmp3
+-- Basic tests
+INSERT INTO tmp1 VALUES (1);
+SELECT * FROM tmp1;
+ a
+---
+ 1
+(1 row)
+
+\c
+SET search_path = global_temp_tests;
+SELECT * FROM tmp1;
+ a
+---
+(0 rows)
+
+-- Test ON COMMIT DELETE ROWS
+CREATE GLOBAL TEMP TABLE tmp2 (a int) ON COMMIT DELETE ROWS;
+BEGIN;
+INSERT INTO tmp2 VALUES (1);
+SELECT * FROM tmp2;
+ a
+---
+ 1
+(1 row)
+
+COMMIT;
+SELECT * FROM tmp2;
+ a
+---
+(0 rows)
+
+-- Repeat test in a new session
+\c
+SET search_path = global_temp_tests;
+BEGIN;
+INSERT INTO tmp2 VALUES (1);
+SELECT * FROM tmp2;
+ a
+---
+ 1
+(1 row)
+
+COMMIT;
+SELECT * FROM tmp2;
+ a
+---
+(0 rows)
+
+DROP TABLE tmp2;
+-- ON COMMIT DROP not allowed
+CREATE GLOBAL TEMP TABLE tmp2 (a int) ON COMMIT DROP; -- fail
+ERROR: ON COMMIT DROP cannot be used on global temporary tables
+-- Two-phase commit not allowed with global temp tables
+BEGIN;
+SELECT * FROM tmp1;
+ a
+---
+(0 rows)
+
+PREPARE TRANSACTION 'twophase'; -- fail
+ERROR: cannot PREPARE a transaction that has operated on temporary objects
+-- Test partitioned global temp table
+CREATE GLOBAL TEMP TABLE tmp2 (a int) PARTITION BY LIST (a);
+CREATE GLOBAL TEMP TABLE tmp2_p1 PARTITION OF tmp2 FOR VALUES IN (1);
+CREATE TABLE tmp2_p2 PARTITION OF tmp2 FOR VALUES IN (2);
+CREATE TEMP TABLE local_tmp (a int);
+ALTER TABLE tmp2 ATTACH PARTITION local_tmp FOR VALUES IN (3); -- fail
+ERROR: cannot attach a local temporary relation as partition of global temporary relation "tmp2"
+INSERT INTO tmp2 VALUES (1), (2);
+SELECT * FROM tmp2 ORDER BY a;
+ a
+---
+ 1
+ 2
+(2 rows)
+
+\c
+SET search_path = global_temp_tests;
+SELECT * FROM tmp2 ORDER BY a;
+ a
+---
+ 2
+(1 row)
+
+DROP TABLE tmp2;
+-- Test ALTER TABLE with rewrite
+CREATE GLOBAL TEMP TABLE tmp2 (a int);
+INSERT INTO tmp2 VALUES (1);
+ALTER TABLE tmp2 ALTER COLUMN a SET DATA TYPE numeric;
+SELECT a, pg_typeof(a) FROM tmp2;
+ a | pg_typeof
+---+-----------
+ 1 | numeric
+(1 row)
+
+DROP TABLE tmp2;
+-- Test foreign keys
+CREATE TABLE perm_pk_rel (a int PRIMARY KEY);
+CREATE TEMP TABLE temp_pk_rel (a int PRIMARY KEY);
+CREATE GLOBAL TEMP TABLE tmp2 (a int REFERENCES perm_pk_rel); -- fail
+ERROR: constraints on global temporary tables may reference only global temporary tables
+CREATE GLOBAL TEMP TABLE tmp2 (a int REFERENCES temp_pk_rel); -- fail
+ERROR: constraints on global temporary tables may reference only global temporary tables
+DROP TABLE perm_pk_rel, temp_pk_rel;
+-- Test ALTER TABLE ... SET TABLESPACE
+CREATE GLOBAL TEMP TABLE tmp2 (a int);
+INSERT INTO tmp2 VALUES (1);
+SELECT * FROM tmp2;
+ a
+---
+ 1
+(1 row)
+
+SELECT regexp_replace(pg_relation_filepath('tmp2'), '(\d+)', 'NNN', 'g');
+ regexp_replace
+-------------------
+ base/NNN/tNNN_NNN
+(1 row)
+
+ALTER TABLE tmp2 SET TABLESPACE regress_tblspace;
+SELECT * FROM tmp2;
+ a
+---
+ 1
+(1 row)
+
+SELECT regexp_replace(pg_relation_filepath('tmp2'), '(\d+)', 'NNN', 'g');
+ regexp_replace
+---------------------------------------
+ pg_tblspc/NNN/PG_NNN_NNN/NNN/tNNN_NNN
+(1 row)
+
+DROP TABLE tmp2;
+-- Test dependency on tablespace
+SET allow_in_place_tablespaces = true;
+CREATE TABLESPACE regress_temp_test_tablespace LOCATION '';
+CREATE GLOBAL TEMP TABLE tmp2 (a int) TABLESPACE regress_temp_test_tablespace;
+\c
+SET search_path = global_temp_tests;
+DROP TABLESPACE regress_temp_test_tablespace; -- fail
+ERROR: tablespace "regress_temp_test_tablespace" cannot be dropped because some objects depend on it
+DETAIL: tablespace for table tmp2
+DROP TABLE tmp2;
+DROP TABLESPACE regress_temp_test_tablespace;
+SET allow_in_place_tablespaces = true;
+CREATE TABLESPACE regress_temp_test_tablespace LOCATION '';
+CREATE GLOBAL TEMP TABLE tmp2 (a int);
+ALTER TABLE tmp2 SET TABLESPACE regress_temp_test_tablespace;
+\c
+SET search_path = global_temp_tests;
+DROP TABLESPACE regress_temp_test_tablespace; -- fail
+ERROR: tablespace "regress_temp_test_tablespace" cannot be dropped because some objects depend on it
+DETAIL: tablespace for table tmp2
+DROP TABLE tmp2;
+DROP TABLESPACE regress_temp_test_tablespace;
+-- Test TRUNCATE
+INSERT INTO tmp1 VALUES (1);
+BEGIN;
+TRUNCATE tmp1;
+SELECT * FROM tmp1;
+ a
+---
+(0 rows)
+
+ROLLBACK;
+SELECT * FROM tmp1;
+ a
+---
+ 1
+(1 row)
+
+BEGIN;
+SAVEPOINT sp1;
+TRUNCATE tmp1;
+SELECT * FROM tmp1;
+ a
+---
+(0 rows)
+
+RELEASE sp1;
+SELECT * FROM tmp1;
+ a
+---
+(0 rows)
+
+ROLLBACK;
+SELECT * FROM tmp1;
+ a
+---
+ 1
+(1 row)
+
+BEGIN;
+SAVEPOINT sp1;
+TRUNCATE tmp1;
+SELECT * FROM tmp1;
+ a
+---
+(0 rows)
+
+ROLLBACK TO sp1;
+SELECT * FROM tmp1;
+ a
+---
+ 1
+(1 row)
+
+COMMIT;
+SELECT * FROM tmp1;
+ a
+---
+ 1
+(1 row)
+
+TRUNCATE tmp1;
+SELECT * FROM tmp1;
+ a
+---
+(0 rows)
+
+-- Test view creation
+INSERT INTO tmp1 VALUES (1);
+CREATE VIEW v AS SELECT * FROM tmp1;
+SELECT * FROM v;
+ a
+---
+ 1
+(1 row)
+
+DROP VIEW v;
+CREATE TEMP VIEW v AS SELECT * FROM tmp1;
+SELECT * FROM v;
+ a
+---
+ 1
+(1 row)
+
+DROP VIEW v;
+CREATE GLOBAL TEMP VIEW v AS SELECT * FROM tmp1; -- fail
+ERROR: views cannot be global temporary because they do not have storage
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 1d21d3eb446..d441f514a45 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -505,7 +505,7 @@ ORDER BY 1;
SELECT c1.oid, c1.relname
FROM pg_class as c1
WHERE relkind NOT IN ('r', 'i', 'S', 't', 'v', 'm', 'c', 'f', 'p', 'I') OR
- relpersistence NOT IN ('p', 'u', 't') OR
+ relpersistence NOT IN ('p', 'u', 't', 'g') OR
relreplident NOT IN ('d', 'n', 'f', 'i');
oid | relname
-----+---------
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 8fa0a6c47fb..0e8a7491274 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -101,8 +101,10 @@ test: publication subscription
# ----------
# Another group of parallel tests
# select_views depends on create_view
+# NB: global_temp.sql does reconnects which transiently uses 2 connections,
+# so keep this parallel group to at most 19 tests
# ----------
-test: select_views portals_p2 foreign_key dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass stats_rewrite graph_table
+test: select_views portals_p2 foreign_key dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass stats_rewrite graph_table global_temp
# ----------
# Another group of parallel tests (JSON related)
diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql
index f5f13bbd3e7..9011959e50d 100644
--- a/src/test/regress/sql/alter_table.sql
+++ b/src/test/regress/sql/alter_table.sql
@@ -2210,6 +2210,7 @@ FROM pg_class,
pg_filenode_relation(reltablespace, pg_relation_filenode(oid)) AS mapped_oid
WHERE relkind IN ('r', 'i', 'S', 't', 'm')
AND relpersistence != 't'
+ AND relpersistence != 'g'
AND mapped_oid IS DISTINCT FROM oid;
SELECT m.* FROM filenode_mapping m LEFT JOIN pg_class c ON c.oid = m.oid
WHERE c.oid IS NOT NULL OR m.mapped_oid IS NOT NULL;
@@ -2474,8 +2475,10 @@ DROP TABLE parent CASCADE;
-- check any TEMP-ness
CREATE TEMP TABLE temp_parted (a int) PARTITION BY LIST (a);
CREATE TABLE perm_part (a int);
+CREATE GLOBAL TEMP TABLE global_temp_part (a int);
ALTER TABLE temp_parted ATTACH PARTITION perm_part FOR VALUES IN (1);
-DROP TABLE temp_parted, perm_part;
+ALTER TABLE temp_parted ATTACH PARTITION global_temp_part FOR VALUES IN (1);
+DROP TABLE temp_parted, perm_part, global_temp_part;
-- check that the table being attached is not a typed table
CREATE TYPE mytype AS (a int);
diff --git a/src/test/regress/sql/create_property_graph.sql b/src/test/regress/sql/create_property_graph.sql
index 3b27d4170bb..90d66c7f830 100644
--- a/src/test/regress/sql/create_property_graph.sql
+++ b/src/test/regress/sql/create_property_graph.sql
@@ -98,6 +98,7 @@ SELECT pg_get_propgraphdef('g5'::regclass);
-- error cases
CREATE UNLOGGED PROPERTY GRAPH gx VERTEX TABLES (xx, yy);
+CREATE GLOBAL TEMP PROPERTY GRAPH gx VERTEX TABLES (xx, yy);
CREATE PROPERTY GRAPH gx VERTEX TABLES (xx, yy);
CREATE PROPERTY GRAPH gx VERTEX TABLES (t1 KEY (a), t2 KEY (i), t1 KEY (a));
ALTER PROPERTY GRAPH g3 ADD VERTEX TABLES (t3 KEY (x)); -- duplicate alias
diff --git a/src/test/regress/sql/create_table.sql b/src/test/regress/sql/create_table.sql
index 80e424e6bda..bc69608e55e 100644
--- a/src/test/regress/sql/create_table.sql
+++ b/src/test/regress/sql/create_table.sql
@@ -686,11 +686,17 @@ drop table boolspart;
-- partitions mixing temporary and permanent relations
create table perm_parted (a int) partition by list (a);
create temporary table temp_parted (a int) partition by list (a);
+create global temporary table global_temp_parted (a int) partition by list (a);
create table perm_part partition of temp_parted default; -- error
+create table perm_part partition of global_temp_parted default; -- ok
create temp table temp_part partition of perm_parted default; -- error
+create temp table temp_part partition of global_temp_parted default; -- error
create temp table temp_part partition of temp_parted default; -- ok
+create global temp table global_temp_part partition of temp_parted default; -- error
+create global temp table global_temp_part partition of perm_parted default; -- ok
drop table perm_parted cascade;
drop table temp_parted cascade;
+drop table global_temp_parted cascade;
-- check that adding partitions to a table while it is being used is prevented
create table tab_part_create (a int) partition by list (a);
diff --git a/src/test/regress/sql/global_temp.sql b/src/test/regress/sql/global_temp.sql
new file mode 100644
index 00000000000..139e0f74b4b
--- /dev/null
+++ b/src/test/regress/sql/global_temp.sql
@@ -0,0 +1,160 @@
+--
+-- GLOBAL TEMP
+--
+CREATE SCHEMA global_temp_tests;
+GRANT USAGE ON SCHEMA global_temp_tests TO PUBLIC;
+SET search_path = global_temp_tests;
+CREATE ROLE regress_global_temp_user;
+GRANT CREATE ON SCHEMA global_temp_tests TO regress_global_temp_user;
+GRANT CREATE ON DATABASE regression TO regress_global_temp_user;
+SET ROLE regress_global_temp_user;
+
+-- Test table creation
+CREATE GLOBAL TEMP TABLE pg_temp.tmp1 (a int); -- fail
+CREATE GLOBAL TEMP TABLE tmp1 (a int);
+CREATE SCHEMA global_temp_xxx CREATE GLOBAL TEMP TABLE tmp2 (a int);
+CREATE SCHEMA global_temp_yyy;
+CREATE GLOBAL TEMP TABLE global_temp_yyy.tmp3 (a int);
+
+\d tmp1
+\dt+ global_temp_*.tmp*
+
+-- Information schema
+SELECT table_catalog, table_schema, table_name, table_type
+FROM information_schema.tables
+WHERE table_name ~ 'tmp' AND table_schema ~ 'global_temp'
+ORDER BY table_name;
+
+DROP SCHEMA global_temp_xxx CASCADE;
+DROP SCHEMA global_temp_yyy CASCADE;
+
+-- Basic tests
+INSERT INTO tmp1 VALUES (1);
+SELECT * FROM tmp1;
+\c
+SET search_path = global_temp_tests;
+SELECT * FROM tmp1;
+
+-- Test ON COMMIT DELETE ROWS
+CREATE GLOBAL TEMP TABLE tmp2 (a int) ON COMMIT DELETE ROWS;
+BEGIN;
+INSERT INTO tmp2 VALUES (1);
+SELECT * FROM tmp2;
+COMMIT;
+SELECT * FROM tmp2;
+
+-- Repeat test in a new session
+\c
+SET search_path = global_temp_tests;
+BEGIN;
+INSERT INTO tmp2 VALUES (1);
+SELECT * FROM tmp2;
+COMMIT;
+SELECT * FROM tmp2;
+DROP TABLE tmp2;
+
+-- ON COMMIT DROP not allowed
+CREATE GLOBAL TEMP TABLE tmp2 (a int) ON COMMIT DROP; -- fail
+
+-- Two-phase commit not allowed with global temp tables
+BEGIN;
+SELECT * FROM tmp1;
+PREPARE TRANSACTION 'twophase'; -- fail
+
+-- Test partitioned global temp table
+CREATE GLOBAL TEMP TABLE tmp2 (a int) PARTITION BY LIST (a);
+CREATE GLOBAL TEMP TABLE tmp2_p1 PARTITION OF tmp2 FOR VALUES IN (1);
+CREATE TABLE tmp2_p2 PARTITION OF tmp2 FOR VALUES IN (2);
+CREATE TEMP TABLE local_tmp (a int);
+ALTER TABLE tmp2 ATTACH PARTITION local_tmp FOR VALUES IN (3); -- fail
+INSERT INTO tmp2 VALUES (1), (2);
+SELECT * FROM tmp2 ORDER BY a;
+\c
+SET search_path = global_temp_tests;
+SELECT * FROM tmp2 ORDER BY a;
+DROP TABLE tmp2;
+
+-- Test ALTER TABLE with rewrite
+CREATE GLOBAL TEMP TABLE tmp2 (a int);
+INSERT INTO tmp2 VALUES (1);
+ALTER TABLE tmp2 ALTER COLUMN a SET DATA TYPE numeric;
+SELECT a, pg_typeof(a) FROM tmp2;
+DROP TABLE tmp2;
+
+-- Test foreign keys
+CREATE TABLE perm_pk_rel (a int PRIMARY KEY);
+CREATE TEMP TABLE temp_pk_rel (a int PRIMARY KEY);
+CREATE GLOBAL TEMP TABLE tmp2 (a int REFERENCES perm_pk_rel); -- fail
+CREATE GLOBAL TEMP TABLE tmp2 (a int REFERENCES temp_pk_rel); -- fail
+DROP TABLE perm_pk_rel, temp_pk_rel;
+
+-- Test ALTER TABLE ... SET TABLESPACE
+CREATE GLOBAL TEMP TABLE tmp2 (a int);
+INSERT INTO tmp2 VALUES (1);
+SELECT * FROM tmp2;
+SELECT regexp_replace(pg_relation_filepath('tmp2'), '(\d+)', 'NNN', 'g');
+ALTER TABLE tmp2 SET TABLESPACE regress_tblspace;
+SELECT * FROM tmp2;
+SELECT regexp_replace(pg_relation_filepath('tmp2'), '(\d+)', 'NNN', 'g');
+DROP TABLE tmp2;
+
+-- Test dependency on tablespace
+SET allow_in_place_tablespaces = true;
+CREATE TABLESPACE regress_temp_test_tablespace LOCATION '';
+CREATE GLOBAL TEMP TABLE tmp2 (a int) TABLESPACE regress_temp_test_tablespace;
+\c
+SET search_path = global_temp_tests;
+DROP TABLESPACE regress_temp_test_tablespace; -- fail
+DROP TABLE tmp2;
+DROP TABLESPACE regress_temp_test_tablespace;
+
+SET allow_in_place_tablespaces = true;
+CREATE TABLESPACE regress_temp_test_tablespace LOCATION '';
+CREATE GLOBAL TEMP TABLE tmp2 (a int);
+ALTER TABLE tmp2 SET TABLESPACE regress_temp_test_tablespace;
+\c
+SET search_path = global_temp_tests;
+DROP TABLESPACE regress_temp_test_tablespace; -- fail
+DROP TABLE tmp2;
+DROP TABLESPACE regress_temp_test_tablespace;
+
+-- Test TRUNCATE
+INSERT INTO tmp1 VALUES (1);
+BEGIN;
+TRUNCATE tmp1;
+SELECT * FROM tmp1;
+ROLLBACK;
+SELECT * FROM tmp1;
+
+BEGIN;
+SAVEPOINT sp1;
+TRUNCATE tmp1;
+SELECT * FROM tmp1;
+RELEASE sp1;
+SELECT * FROM tmp1;
+ROLLBACK;
+SELECT * FROM tmp1;
+
+BEGIN;
+SAVEPOINT sp1;
+TRUNCATE tmp1;
+SELECT * FROM tmp1;
+ROLLBACK TO sp1;
+SELECT * FROM tmp1;
+COMMIT;
+SELECT * FROM tmp1;
+
+TRUNCATE tmp1;
+SELECT * FROM tmp1;
+
+-- Test view creation
+INSERT INTO tmp1 VALUES (1);
+CREATE VIEW v AS SELECT * FROM tmp1;
+SELECT * FROM v;
+DROP VIEW v;
+
+CREATE TEMP VIEW v AS SELECT * FROM tmp1;
+SELECT * FROM v;
+DROP VIEW v;
+
+CREATE GLOBAL TEMP VIEW v AS SELECT * FROM tmp1; -- fail
diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql
index 95d5b6e0915..b1f0a60abea 100644
--- a/src/test/regress/sql/type_sanity.sql
+++ b/src/test/regress/sql/type_sanity.sql
@@ -367,7 +367,7 @@ ORDER BY 1;
SELECT c1.oid, c1.relname
FROM pg_class as c1
WHERE relkind NOT IN ('r', 'i', 'S', 't', 'v', 'm', 'c', 'f', 'p', 'I') OR
- relpersistence NOT IN ('p', 'u', 't') OR
+ relpersistence NOT IN ('p', 'u', 't', 'g') OR
relreplident NOT IN ('d', 'n', 'f', 'i');
-- All tables, indexes, partitioned indexes and matviews should have an
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..ddf9827ee25 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1161,6 +1161,7 @@ GistTsVectorOptions
GistVacState
GlobalChannelEntry
GlobalChannelKey
+GlobalTempRelShmemControl
GlobalTransaction
GlobalTransactionData
GlobalVisHorizonKind
@@ -1190,6 +1191,10 @@ GroupingSet
GroupingSetData
GroupingSetKind
GroupingSetsPath
+GtrStorageEntry
+GtrSharedUsageEntry
+GtrSharedUsageKey
+GtrUsageEntry
GucAction
GucBoolAssignHook
GucBoolCheckHook
--
2.51.0
[text/x-patch] v8-0003-Support-indexes-on-global-temporary-tables.patch (36.2K, ../CAEZATCVjaZFa80S-V_+nvnzRK1ZiLUPx8jwdyAtRVHAmPZQQMg@mail.gmail.com/4-v8-0003-Support-indexes-on-global-temporary-tables.patch)
download | inline diff:
From 80cfc34ce5fdea48e8fca675e7f6ca07bebedf6c Mon Sep 17 00:00:00 2001
From: Dean Rasheed <[email protected]>
Date: Tue, 9 Jun 2026 13:50:47 +0100
Subject: [PATCH v8 03/10] Support indexes on global temporary tables.
As with local temporary tables, concurrent index build/reindex/drop is
not supported.
---
contrib/bloom/blinsert.c | 6 +-
contrib/bloom/bloom.h | 2 +-
src/backend/access/brin/brin.c | 4 +-
src/backend/access/gin/gininsert.c | 8 +-
src/backend/access/gist/gist.c | 6 +-
src/backend/access/hash/hash.c | 12 +-
src/backend/access/nbtree/nbtree.c | 6 +-
src/backend/access/spgist/spginsert.c | 6 +-
src/backend/access/transam/xloginsert.c | 3 +-
src/backend/catalog/global_temp.c | 24 +++
src/backend/catalog/index.c | 5 +-
src/backend/commands/indexcmds.c | 27 ++-
src/backend/optimizer/plan/planner.c | 1 +
src/bin/scripts/reindexdb.c | 4 +
src/include/access/amapi.h | 4 +-
src/include/access/brin_internal.h | 2 +-
src/include/access/gin_private.h | 2 +-
src/include/access/gist_private.h | 2 +-
src/include/access/hash.h | 2 +-
src/include/access/nbtree.h | 2 +-
src/include/access/spgist.h | 2 +-
src/test/isolation/expected/global-temp.out | 144 ++++++++++++++++
src/test/isolation/specs/global-temp.spec | 25 ++-
.../modules/dummy_index_am/dummy_index_am.c | 4 +-
src/test/regress/expected/global_temp.out | 158 ++++++++++++------
src/test/regress/sql/global_temp.sql | 41 ++++-
26 files changed, 405 insertions(+), 97 deletions(-)
diff --git a/contrib/bloom/blinsert.c b/contrib/bloom/blinsert.c
index df24856d9ae..804c77ae768 100644
--- a/contrib/bloom/blinsert.c
+++ b/contrib/bloom/blinsert.c
@@ -159,13 +159,13 @@ blbuild(Relation heap, Relation index, IndexInfo *indexInfo)
}
/*
- * Build an empty bloom index in the initialization fork.
+ * Build an empty bloom index in the specified fork.
*/
void
-blbuildempty(Relation index)
+blbuildempty(Relation index, ForkNumber forknum)
{
/* Initialize the meta page */
- BloomInitMetapage(index, INIT_FORKNUM);
+ BloomInitMetapage(index, forknum);
}
/*
diff --git a/contrib/bloom/bloom.h b/contrib/bloom/bloom.h
index 9a4125bdfb1..4f00584006b 100644
--- a/contrib/bloom/bloom.h
+++ b/contrib/bloom/bloom.h
@@ -197,7 +197,7 @@ extern void blrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
extern void blendscan(IndexScanDesc scan);
extern IndexBuildResult *blbuild(Relation heap, Relation index,
struct IndexInfo *indexInfo);
-extern void blbuildempty(Relation index);
+extern void blbuildempty(Relation index, ForkNumber forknum);
extern IndexBulkDeleteResult *blbulkdelete(IndexVacuumInfo *info,
IndexBulkDeleteResult *stats, IndexBulkDeleteCallback callback,
void *callback_state);
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index bdb30752e09..94494c9d8fa 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -1276,12 +1276,12 @@ brinbuild(Relation heap, Relation index, IndexInfo *indexInfo)
}
void
-brinbuildempty(Relation index)
+brinbuildempty(Relation index, ForkNumber forknum)
{
Buffer metabuf;
/* An empty BRIN index has a metapage only. */
- metabuf = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM, NULL,
+ metabuf = ExtendBufferedRel(BMR_REL(index), forknum, NULL,
EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK);
/* Initialize and xlog metabuffer. */
diff --git a/src/backend/access/gin/gininsert.c b/src/backend/access/gin/gininsert.c
index cb9ed3b563c..e9c2d1eedf1 100644
--- a/src/backend/access/gin/gininsert.c
+++ b/src/backend/access/gin/gininsert.c
@@ -806,18 +806,18 @@ ginbuild(Relation heap, Relation index, IndexInfo *indexInfo)
}
/*
- * ginbuildempty() -- build an empty gin index in the initialization fork
+ * ginbuildempty() -- build an empty gin index in the specified fork
*/
void
-ginbuildempty(Relation index)
+ginbuildempty(Relation index, ForkNumber forknum)
{
Buffer RootBuffer,
MetaBuffer;
/* An empty GIN index has two pages. */
- MetaBuffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM, NULL,
+ MetaBuffer = ExtendBufferedRel(BMR_REL(index), forknum, NULL,
EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK);
- RootBuffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM, NULL,
+ RootBuffer = ExtendBufferedRel(BMR_REL(index), forknum, NULL,
EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK);
/* Initialize and xlog metabuffer and root buffer. */
diff --git a/src/backend/access/gist/gist.c b/src/backend/access/gist/gist.c
index 8565e225be7..9eae449786b 100644
--- a/src/backend/access/gist/gist.c
+++ b/src/backend/access/gist/gist.c
@@ -134,15 +134,15 @@ createTempGistContext(void)
}
/*
- * gistbuildempty() -- build an empty gist index in the initialization fork
+ * gistbuildempty() -- build an empty gist index in the specified fork
*/
void
-gistbuildempty(Relation index)
+gistbuildempty(Relation index, ForkNumber forknum)
{
Buffer buffer;
/* Initialize the root page */
- buffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM, NULL,
+ buffer = ExtendBufferedRel(BMR_REL(index), forknum, NULL,
EB_SKIP_EXTENSION_LOCK | EB_LOCK_FIRST);
/* Initialize and xlog buffer */
diff --git a/src/backend/access/hash/hash.c b/src/backend/access/hash/hash.c
index 8d8cd30dc38..cdcbb9efae0 100644
--- a/src/backend/access/hash/hash.c
+++ b/src/backend/access/hash/hash.c
@@ -175,10 +175,10 @@ hashbuild(Relation heap, Relation index, IndexInfo *indexInfo)
* metapage, nor the first bitmap page.
*/
sort_threshold = (maintenance_work_mem * (Size) 1024) / BLCKSZ;
- if (index->rd_rel->relpersistence != RELPERSISTENCE_TEMP)
- sort_threshold = Min(sort_threshold, NBuffers);
- else
+ if (RelationUsesLocalBuffers(index))
sort_threshold = Min(sort_threshold, NLocBuffer);
+ else
+ sort_threshold = Min(sort_threshold, NBuffers);
if (num_buckets >= sort_threshold)
buildstate.spool = _h_spoolinit(heap, index, num_buckets);
@@ -215,12 +215,12 @@ hashbuild(Relation heap, Relation index, IndexInfo *indexInfo)
}
/*
- * hashbuildempty() -- build an empty hash index in the initialization fork
+ * hashbuildempty() -- build an empty hash index in the specified fork
*/
void
-hashbuildempty(Relation index)
+hashbuildempty(Relation index, ForkNumber forknum)
{
- _hash_init(index, 0, INIT_FORKNUM);
+ _hash_init(index, 0, forknum);
}
/*
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 3df2c752ead..be1b5ce51f9 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -177,16 +177,16 @@ bthandler(PG_FUNCTION_ARGS)
}
/*
- * btbuildempty() -- build an empty btree index in the initialization fork
+ * btbuildempty() -- build an empty btree index in the specified fork
*/
void
-btbuildempty(Relation index)
+btbuildempty(Relation index, ForkNumber forknum)
{
bool allequalimage = _bt_allequalimage(index, false);
BulkWriteState *bulkstate;
BulkWriteBuffer metabuf;
- bulkstate = smgr_bulk_start_rel(index, INIT_FORKNUM);
+ bulkstate = smgr_bulk_start_rel(index, forknum);
/* Construct metapage. */
metabuf = smgr_bulk_get_buf(bulkstate);
diff --git a/src/backend/access/spgist/spginsert.c b/src/backend/access/spgist/spginsert.c
index 780ef646a54..1a14f84174a 100644
--- a/src/backend/access/spgist/spginsert.c
+++ b/src/backend/access/spgist/spginsert.c
@@ -148,15 +148,15 @@ spgbuild(Relation heap, Relation index, IndexInfo *indexInfo)
}
/*
- * Build an empty SPGiST index in the initialization fork
+ * Build an empty SPGiST index in the specified fork
*/
void
-spgbuildempty(Relation index)
+spgbuildempty(Relation index, ForkNumber forknum)
{
BulkWriteState *bulkstate;
BulkWriteBuffer buf;
- bulkstate = smgr_bulk_start_rel(index, INIT_FORKNUM);
+ bulkstate = smgr_bulk_start_rel(index, forknum);
/* Construct metapage. */
buf = smgr_bulk_get_buf(bulkstate);
diff --git a/src/backend/access/transam/xloginsert.c b/src/backend/access/transam/xloginsert.c
index f2e10b82b7d..9b099d87f35 100644
--- a/src/backend/access/transam/xloginsert.c
+++ b/src/backend/access/transam/xloginsert.c
@@ -561,7 +561,8 @@ XLogSimpleInsertInt64(RmgrId rmid, uint8 info, int64 value)
XLogRecPtr
XLogGetFakeLSN(Relation rel)
{
- if (rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
+ if (rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP ||
+ rel->rd_rel->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
{
/*
* Temporary relations are only accessible in our session, so a simple
diff --git a/src/backend/catalog/global_temp.c b/src/backend/catalog/global_temp.c
index 098a3f4de0e..6da94c44e86 100644
--- a/src/backend/catalog/global_temp.c
+++ b/src/backend/catalog/global_temp.c
@@ -53,8 +53,10 @@
*/
#include "postgres.h"
+#include "access/amapi.h"
#include "access/genam.h"
#include "access/parallel.h"
+#include "access/table.h"
#include "access/tableam.h"
#include "access/xact.h"
#include "access/xlogutils.h"
@@ -852,6 +854,28 @@ InitGlobalTempRelation(Relation relation)
if (relation->rd_rel->reloncommit == RELONCOMMIT_DELETE_ROWS)
register_on_commit_action(relation->rd_id, ONCOMMIT_DELETE_ROWS);
+
+ /*
+ * If it's an index, build an empty index in the main fork.
+ *
+ * If the table is not empty (can happen if another session added the
+ * index after we populated the table), then mark it as invalid. The
+ * user will need to do a REINDEX to build it.
+ */
+ if (relation->rd_rel->relkind == RELKIND_INDEX)
+ {
+ Relation heapRelation;
+ BlockNumber nblocks;
+
+ relation->rd_indam->ambuildempty(relation, MAIN_FORKNUM);
+
+ heapRelation = table_open(relation->rd_index->indrelid, AccessShareLock);
+ nblocks = RelationGetNumberOfBlocks(heapRelation);
+ table_close(heapRelation, AccessShareLock);
+
+ if (nblocks > 0)
+ relation->rd_index->indisvalid = false;
+ }
}
/* The remaining initialization works as if we had created it locally */
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index ffc7f6c254e..330c7756758 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2157,7 +2157,8 @@ index_drop(Oid indexId, bool concurrent, bool concurrent_lock_mode)
* lock (see comments in RemoveRelations), and a non-concurrent DROP is
* more efficient.
*/
- Assert(get_rel_persistence(indexId) != RELPERSISTENCE_TEMP ||
+ Assert((get_rel_persistence(indexId) != RELPERSISTENCE_TEMP &&
+ get_rel_persistence(indexId) != RELPERSISTENCE_GLOBAL_TEMP) ||
(!concurrent && !concurrent_lock_mode));
/*
@@ -3113,7 +3114,7 @@ index_build(Relation heapRelation,
{
smgrcreate(RelationGetSmgr(indexRelation), INIT_FORKNUM, false);
log_smgrcreate(&indexRelation->rd_locator, INIT_FORKNUM);
- indexRelation->rd_indam->ambuildempty(indexRelation);
+ indexRelation->rd_indam->ambuildempty(indexRelation, INIT_FORKNUM);
}
/*
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 713bb5d10f1..8baac58ef81 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -626,8 +626,16 @@ DefineIndex(ParseState *pstate,
* is more efficient. Do this before any use of the concurrent option is
* done.
*/
- if (stmt->concurrent && get_rel_persistence(tableId) != RELPERSISTENCE_TEMP)
- concurrent = true;
+ if (stmt->concurrent)
+ {
+ char relpersistence = get_rel_persistence(tableId);
+
+ if (relpersistence == RELPERSISTENCE_TEMP ||
+ relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+ concurrent = false;
+ else
+ concurrent = true;
+ }
else
concurrent = false;
@@ -3106,7 +3114,8 @@ ReindexIndex(const ReindexStmt *stmt, const ReindexParams *params, bool isTopLev
if (relkind == RELKIND_PARTITIONED_INDEX)
ReindexPartitions(stmt, indOid, params, isTopLevel);
else if ((params->options & REINDEXOPT_CONCURRENTLY) != 0 &&
- persistence != RELPERSISTENCE_TEMP)
+ persistence != RELPERSISTENCE_TEMP &&
+ persistence != RELPERSISTENCE_GLOBAL_TEMP)
ReindexRelationConcurrently(stmt, indOid, params);
else
{
@@ -3202,6 +3211,7 @@ static Oid
ReindexTable(const ReindexStmt *stmt, const ReindexParams *params, bool isTopLevel)
{
Oid heapOid;
+ char persistence;
bool result;
const RangeVar *relation = stmt->relation;
@@ -3219,10 +3229,13 @@ ReindexTable(const ReindexStmt *stmt, const ReindexParams *params, bool isTopLev
0,
RangeVarCallbackMaintainsTable, NULL);
+ persistence = get_rel_persistence(heapOid);
+
if (get_rel_relkind(heapOid) == RELKIND_PARTITIONED_TABLE)
ReindexPartitions(stmt, heapOid, params, isTopLevel);
else if ((params->options & REINDEXOPT_CONCURRENTLY) != 0 &&
- get_rel_persistence(heapOid) != RELPERSISTENCE_TEMP)
+ persistence != RELPERSISTENCE_TEMP &&
+ persistence != RELPERSISTENCE_GLOBAL_TEMP)
{
result = ReindexRelationConcurrently(stmt, heapOid, params);
@@ -3646,7 +3659,8 @@ ReindexMultipleInternal(const ReindexStmt *stmt, const List *relids, const Reind
Assert(!RELKIND_HAS_PARTITIONS(relkind));
if ((params->options & REINDEXOPT_CONCURRENTLY) != 0 &&
- relpersistence != RELPERSISTENCE_TEMP)
+ relpersistence != RELPERSISTENCE_TEMP &&
+ relpersistence != RELPERSISTENCE_GLOBAL_TEMP)
{
ReindexParams newparams = *params;
@@ -4088,7 +4102,8 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein
idx->amId = indexRel->rd_rel->relam;
/* This function shouldn't be called for temporary relations. */
- if (indexRel->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
+ if (indexRel->rd_rel->relpersistence == RELPERSISTENCE_TEMP ||
+ indexRel->rd_rel->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
elog(ERROR, "cannot reindex a temporary table concurrently");
pgstat_progress_start_command(PROGRESS_COMMAND_CREATE_INDEX, idx->tableId);
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 846bd7c1fbe..495e48be638 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -7364,6 +7364,7 @@ plan_create_index_workers(Oid tableOid, Oid indexOid)
* safe.
*/
if (heap->rd_rel->relpersistence == RELPERSISTENCE_TEMP ||
+ heap->rd_rel->relpersistence == RELPERSISTENCE_GLOBAL_TEMP ||
!is_parallel_safe(root, (Node *) RelationGetIndexExpressions(index)) ||
!is_parallel_safe(root, (Node *) RelationGetIndexPredicate(index)))
{
diff --git a/src/bin/scripts/reindexdb.c b/src/bin/scripts/reindexdb.c
index d7fb16d3c85..70da07d2369 100644
--- a/src/bin/scripts/reindexdb.c
+++ b/src/bin/scripts/reindexdb.c
@@ -655,6 +655,8 @@ get_parallel_tables_list(PGconn *conn, ReindexType type,
CppAsString2(RELKIND_MATVIEW) ")\n"
" AND c.relpersistence != "
CppAsString2(RELPERSISTENCE_TEMP) "\n"
+ " AND c.relpersistence != "
+ CppAsString2(RELPERSISTENCE_GLOBAL_TEMP) "\n"
" ORDER BY c.relpages DESC;");
break;
@@ -678,6 +680,8 @@ get_parallel_tables_list(PGconn *conn, ReindexType type,
CppAsString2(RELKIND_MATVIEW) ")\n"
" AND c.relpersistence != "
CppAsString2(RELPERSISTENCE_TEMP) "\n"
+ " AND c.relpersistence != "
+ CppAsString2(RELPERSISTENCE_GLOBAL_TEMP) "\n"
" AND ns.nspname IN (");
for (cell = user_list->head; cell; cell = cell->next)
diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h
index 79240333530..b3b9cb6039b 100644
--- a/src/include/access/amapi.h
+++ b/src/include/access/amapi.h
@@ -15,6 +15,7 @@
#include "access/cmptype.h"
#include "access/genam.h"
#include "access/stratnum.h"
+#include "common/relpath.h"
#include "nodes/nodes.h"
#include "nodes/pg_list.h"
@@ -115,7 +116,8 @@ typedef IndexBuildResult *(*ambuild_function) (Relation heapRelation,
IndexInfo *indexInfo);
/* build empty index */
-typedef void (*ambuildempty_function) (Relation indexRelation);
+typedef void (*ambuildempty_function) (Relation indexRelation,
+ ForkNumber forknum);
/* insert this tuple */
typedef bool (*aminsert_function) (Relation indexRelation,
diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h
index e17c7fee511..10137275c4c 100644
--- a/src/include/access/brin_internal.h
+++ b/src/include/access/brin_internal.h
@@ -90,7 +90,7 @@ extern BrinDesc *brin_build_desc(Relation rel);
extern void brin_free_desc(BrinDesc *bdesc);
extern IndexBuildResult *brinbuild(Relation heap, Relation index,
IndexInfo *indexInfo);
-extern void brinbuildempty(Relation index);
+extern void brinbuildempty(Relation index, ForkNumber forknum);
extern bool brininsert(Relation idxRel, Datum *values, bool *nulls,
ItemPointer heaptid, Relation heapRel,
IndexUniqueCheck checkUnique,
diff --git a/src/include/access/gin_private.h b/src/include/access/gin_private.h
index 6725ee2839f..1ce7bffbe70 100644
--- a/src/include/access/gin_private.h
+++ b/src/include/access/gin_private.h
@@ -110,7 +110,7 @@ extern char *ginbuildphasename(int64 phasenum);
/* gininsert.c */
extern IndexBuildResult *ginbuild(Relation heap, Relation index,
struct IndexInfo *indexInfo);
-extern void ginbuildempty(Relation index);
+extern void ginbuildempty(Relation index, ForkNumber forknum);
extern bool gininsert(Relation index, Datum *values, bool *isnull,
ItemPointer ht_ctid, Relation heapRel,
IndexUniqueCheck checkUnique,
diff --git a/src/include/access/gist_private.h b/src/include/access/gist_private.h
index 44514f1cb8d..334c3de4541 100644
--- a/src/include/access/gist_private.h
+++ b/src/include/access/gist_private.h
@@ -399,7 +399,7 @@ typedef struct GiSTOptions
} GiSTOptions;
/* gist.c */
-extern void gistbuildempty(Relation index);
+extern void gistbuildempty(Relation index, ForkNumber forknum);
extern bool gistinsert(Relation r, Datum *values, bool *isnull,
ItemPointer ht_ctid, Relation heapRel,
IndexUniqueCheck checkUnique,
diff --git a/src/include/access/hash.h b/src/include/access/hash.h
index a8702f0e5ea..a719d374af1 100644
--- a/src/include/access/hash.h
+++ b/src/include/access/hash.h
@@ -362,7 +362,7 @@ typedef struct HashOptions
extern IndexBuildResult *hashbuild(Relation heap, Relation index,
struct IndexInfo *indexInfo);
-extern void hashbuildempty(Relation index);
+extern void hashbuildempty(Relation index, ForkNumber forknum);
extern bool hashinsert(Relation rel, Datum *values, bool *isnull,
ItemPointer ht_ctid, Relation heapRel,
IndexUniqueCheck checkUnique,
diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index 3097e9bb1af..8053dfe3d6a 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1151,7 +1151,7 @@ typedef struct BTOptions
/*
* external entry points for btree, in nbtree.c
*/
-extern void btbuildempty(Relation index);
+extern void btbuildempty(Relation index, ForkNumber forknum);
extern bool btinsert(Relation rel, Datum *values, bool *isnull,
ItemPointer ht_ctid, Relation heapRel,
IndexUniqueCheck checkUnique,
diff --git a/src/include/access/spgist.h b/src/include/access/spgist.h
index 083d93f8ffd..392c6538cbf 100644
--- a/src/include/access/spgist.h
+++ b/src/include/access/spgist.h
@@ -195,7 +195,7 @@ extern bytea *spgoptions(Datum reloptions, bool validate);
/* spginsert.c */
extern IndexBuildResult *spgbuild(Relation heap, Relation index,
struct IndexInfo *indexInfo);
-extern void spgbuildempty(Relation index);
+extern void spgbuildempty(Relation index, ForkNumber forknum);
extern bool spginsert(Relation index, Datum *values, bool *isnull,
ItemPointer ht_ctid, Relation heapRel,
IndexUniqueCheck checkUnique,
diff --git a/src/test/isolation/expected/global-temp.out b/src/test/isolation/expected/global-temp.out
index 5ac8eccbba6..3a6af7eb23e 100644
--- a/src/test/isolation/expected/global-temp.out
+++ b/src/test/isolation/expected/global-temp.out
@@ -250,3 +250,147 @@ step create1dr: CREATE GLOBAL TEMP TABLE tmp2 (key int, val text) ON COMMIT DELE
step ins1_2: INSERT INTO tmp2 VALUES (1, 's1');
step ins2_2: INSERT INTO tmp2 VALUES (1, 's2');
step drop1: DROP TABLE tmp2;
+
+starting permutation: ins1 idx1 sel1_idx ins2 sel2_idx
+step ins1: INSERT INTO tmp VALUES (1, 's1');
+step idx1: CREATE INDEX tmp_val_idx ON tmp(val);
+step sel1_idx:
+ SET enable_seqscan = off;
+ SET enable_bitmapscan = off;
+ EXPLAIN (COSTS OFF)
+ SELECT * FROM tmp WHERE val = 's1';
+ SELECT * FROM tmp WHERE val = 's1';
+
+QUERY PLAN
+-----------------------------------
+Index Scan using tmp_val_idx on tmp
+ Index Cond: (val = 's1'::text)
+(2 rows)
+
+key|val
+---+---
+ 1|s1
+(1 row)
+
+step ins2: INSERT INTO tmp VALUES (1, 's2');
+step sel2_idx:
+ SET enable_seqscan = off;
+ SET enable_bitmapscan = off;
+ EXPLAIN (COSTS OFF)
+ SELECT * FROM tmp WHERE val = 's2';
+ SELECT * FROM tmp WHERE val = 's2';
+
+QUERY PLAN
+-----------------------------------
+Index Scan using tmp_val_idx on tmp
+ Index Cond: (val = 's2'::text)
+(2 rows)
+
+key|val
+---+---
+ 1|s2
+(1 row)
+
+
+starting permutation: ins1 ins2 idx1 sel1_idx sel2_idx
+step ins1: INSERT INTO tmp VALUES (1, 's1');
+step ins2: INSERT INTO tmp VALUES (1, 's2');
+step idx1: CREATE INDEX tmp_val_idx ON tmp(val);
+step sel1_idx:
+ SET enable_seqscan = off;
+ SET enable_bitmapscan = off;
+ EXPLAIN (COSTS OFF)
+ SELECT * FROM tmp WHERE val = 's1';
+ SELECT * FROM tmp WHERE val = 's1';
+
+QUERY PLAN
+-----------------------------------
+Index Scan using tmp_val_idx on tmp
+ Index Cond: (val = 's1'::text)
+(2 rows)
+
+key|val
+---+---
+ 1|s1
+(1 row)
+
+step sel2_idx:
+ SET enable_seqscan = off;
+ SET enable_bitmapscan = off;
+ EXPLAIN (COSTS OFF)
+ SELECT * FROM tmp WHERE val = 's2';
+ SELECT * FROM tmp WHERE val = 's2';
+
+QUERY PLAN
+----------------------------
+Seq Scan on tmp
+ Disabled: true
+ Filter: (val = 's2'::text)
+(3 rows)
+
+key|val
+---+---
+ 1|s2
+(1 row)
+
+
+starting permutation: ins1 ins2 idx1 sel1_idx sel2_idx reidx2 sel2_idx
+step ins1: INSERT INTO tmp VALUES (1, 's1');
+step ins2: INSERT INTO tmp VALUES (1, 's2');
+step idx1: CREATE INDEX tmp_val_idx ON tmp(val);
+step sel1_idx:
+ SET enable_seqscan = off;
+ SET enable_bitmapscan = off;
+ EXPLAIN (COSTS OFF)
+ SELECT * FROM tmp WHERE val = 's1';
+ SELECT * FROM tmp WHERE val = 's1';
+
+QUERY PLAN
+-----------------------------------
+Index Scan using tmp_val_idx on tmp
+ Index Cond: (val = 's1'::text)
+(2 rows)
+
+key|val
+---+---
+ 1|s1
+(1 row)
+
+step sel2_idx:
+ SET enable_seqscan = off;
+ SET enable_bitmapscan = off;
+ EXPLAIN (COSTS OFF)
+ SELECT * FROM tmp WHERE val = 's2';
+ SELECT * FROM tmp WHERE val = 's2';
+
+QUERY PLAN
+----------------------------
+Seq Scan on tmp
+ Disabled: true
+ Filter: (val = 's2'::text)
+(3 rows)
+
+key|val
+---+---
+ 1|s2
+(1 row)
+
+step reidx2: REINDEX INDEX tmp_val_idx;
+step sel2_idx:
+ SET enable_seqscan = off;
+ SET enable_bitmapscan = off;
+ EXPLAIN (COSTS OFF)
+ SELECT * FROM tmp WHERE val = 's2';
+ SELECT * FROM tmp WHERE val = 's2';
+
+QUERY PLAN
+-----------------------------------
+Index Scan using tmp_val_idx on tmp
+ Index Cond: (val = 's2'::text)
+(2 rows)
+
+key|val
+---+---
+ 1|s2
+(1 row)
+
diff --git a/src/test/isolation/specs/global-temp.spec b/src/test/isolation/specs/global-temp.spec
index db242426fe5..5136230e859 100644
--- a/src/test/isolation/specs/global-temp.spec
+++ b/src/test/isolation/specs/global-temp.spec
@@ -1,9 +1,9 @@
# Test global temporary relations
setup {
- CREATE GLOBAL TEMP TABLE tmp (key int, val text);
+ CREATE GLOBAL TEMP TABLE tmp (key int PRIMARY KEY, val text);
- CREATE GLOBAL TEMP TABLE tmp_parted (key int, val text) PARTITION BY LIST (key);
+ CREATE GLOBAL TEMP TABLE tmp_parted (key int PRIMARY KEY, val text) PARTITION BY LIST (key);
CREATE GLOBAL TEMP TABLE tmp_p1 PARTITION OF tmp_parted FOR VALUES IN (1);
CREATE GLOBAL TEMP TABLE tmp_p2 PARTITION OF tmp_parted FOR VALUES IN ((2), (3));
}
@@ -25,6 +25,14 @@ step alter1a { ALTER TABLE tmp2 ALTER COLUMN key SET DATA TYPE numeric; }
step alter1b { ALTER TABLE tmp2 ALTER COLUMN val SET NOT NULL; }
step seltype1 { SELECT key, pg_typeof(key), val FROM tmp2; }
step drop1 { DROP TABLE tmp2; }
+step idx1 { CREATE INDEX tmp_val_idx ON tmp(val); }
+step sel1_idx {
+ SET enable_seqscan = off;
+ SET enable_bitmapscan = off;
+ EXPLAIN (COSTS OFF)
+ SELECT * FROM tmp WHERE val = 's1';
+ SELECT * FROM tmp WHERE val = 's1';
+}
session s2
step b2 { BEGIN; }
@@ -40,6 +48,14 @@ step sp2 { SAVEPOINT sp; }
step rsp2 { ROLLBACK TO SAVEPOINT sp; }
step ins2_2 { INSERT INTO tmp2 VALUES (1, 's2'); }
step seltype2 { SELECT key, pg_typeof(key), val FROM tmp2; }
+step sel2_idx {
+ SET enable_seqscan = off;
+ SET enable_bitmapscan = off;
+ EXPLAIN (COSTS OFF)
+ SELECT * FROM tmp WHERE val = 's2';
+ SELECT * FROM tmp WHERE val = 's2';
+}
+step reidx2 { REINDEX INDEX tmp_val_idx; }
# Basic effects
permutation ins1 ins2 sel1 sel2
@@ -58,3 +74,8 @@ permutation create1 ins1_2 ins2_2 alter1a alter1b seltype1 seltype2 drop1
# Test DROP with ON COMMIT DELETE ROWS
permutation create1dr ins1_2 ins2_2 drop1 create1dr ins1_2 ins2_2 drop1
+
+# Test val index
+permutation ins1 idx1 sel1_idx ins2 sel2_idx
+permutation ins1 ins2 idx1 sel1_idx sel2_idx
+permutation ins1 ins2 idx1 sel1_idx sel2_idx reidx2 sel2_idx
diff --git a/src/test/modules/dummy_index_am/dummy_index_am.c b/src/test/modules/dummy_index_am/dummy_index_am.c
index 31f8d2b8161..65b116280bf 100644
--- a/src/test/modules/dummy_index_am/dummy_index_am.c
+++ b/src/test/modules/dummy_index_am/dummy_index_am.c
@@ -166,10 +166,10 @@ dibuild(Relation heap, Relation index, IndexInfo *indexInfo)
}
/*
- * Build an empty index for the initialization fork.
+ * Build an empty index for the specified fork.
*/
static void
-dibuildempty(Relation index)
+dibuildempty(Relation index, ForkNumber forknum)
{
/* No need to build an init fork for a dummy index */
}
diff --git a/src/test/regress/expected/global_temp.out b/src/test/regress/expected/global_temp.out
index 43e9116f7d5..1b2450a49b5 100644
--- a/src/test/regress/expected/global_temp.out
+++ b/src/test/regress/expected/global_temp.out
@@ -13,7 +13,7 @@ CREATE GLOBAL TEMP TABLE pg_temp.tmp1 (a int); -- fail
ERROR: cannot create global temporary relation in temporary schema
LINE 1: CREATE GLOBAL TEMP TABLE pg_temp.tmp1 (a int);
^
-CREATE GLOBAL TEMP TABLE tmp1 (a int);
+CREATE GLOBAL TEMP TABLE tmp1 (a int PRIMARY KEY, b text);
CREATE SCHEMA global_temp_xxx CREATE GLOBAL TEMP TABLE tmp2 (a int);
CREATE SCHEMA global_temp_yyy;
CREATE GLOBAL TEMP TABLE global_temp_yyy.tmp3 (a int);
@@ -21,15 +21,18 @@ CREATE GLOBAL TEMP TABLE global_temp_yyy.tmp3 (a int);
Global temporary table "global_temp_tests.tmp1"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+---------
- a | integer | | |
+ a | integer | | not null |
+ b | text | | |
+Indexes:
+ "tmp1_pkey" PRIMARY KEY, btree (a)
\dt+ global_temp_*.tmp*
- List of tables
- Schema | Name | Type | Owner | Persistence | Size | Description
--------------------+------+-------+--------------------------+------------------+---------+-------------
- global_temp_tests | tmp1 | table | regress_global_temp_user | global temporary | 0 bytes |
- global_temp_xxx | tmp2 | table | regress_global_temp_user | global temporary | 0 bytes |
- global_temp_yyy | tmp3 | table | regress_global_temp_user | global temporary | 0 bytes |
+ List of tables
+ Schema | Name | Type | Owner | Persistence | Size | Description
+-------------------+------+-------+--------------------------+------------------+------------+-------------
+ global_temp_tests | tmp1 | table | regress_global_temp_user | global temporary | 8192 bytes |
+ global_temp_xxx | tmp2 | table | regress_global_temp_user | global temporary | 0 bytes |
+ global_temp_yyy | tmp3 | table | regress_global_temp_user | global temporary | 0 bytes |
(3 rows)
-- Information schema
@@ -49,20 +52,59 @@ NOTICE: drop cascades to table global_temp_xxx.tmp2
DROP SCHEMA global_temp_yyy CASCADE;
NOTICE: drop cascades to table global_temp_yyy.tmp3
-- Basic tests
-INSERT INTO tmp1 VALUES (1);
+INSERT INTO tmp1 VALUES (1, 'xxx');
SELECT * FROM tmp1;
- a
----
- 1
+ a | b
+---+-----
+ 1 | xxx
(1 row)
\c
SET search_path = global_temp_tests;
SELECT * FROM tmp1;
- a
----
+ a | b
+---+---
(0 rows)
+-- Test index
+INSERT INTO tmp1 VALUES (1, 'xxx');
+SET enable_seqscan = off;
+EXPLAIN (COSTS OFF)
+SELECT * FROM tmp1 WHERE a = 1;
+ QUERY PLAN
+------------------------------------
+ Index Scan using tmp1_pkey on tmp1
+ Index Cond: (a = 1)
+(2 rows)
+
+SELECT * FROM tmp1 WHERE a = 1;
+ a | b
+---+-----
+ 1 | xxx
+(1 row)
+
+RESET enable_seqscan;
+-- Test concurrent index build -- CONCURRENTLY is ignored with temp tables
+CREATE INDEX CONCURRENTLY tmp1_b_idx ON tmp1(b);
+SET enable_seqscan = off;
+EXPLAIN (COSTS OFF)
+SELECT * FROM tmp1 WHERE b = 'xxx';
+ QUERY PLAN
+-------------------------------------
+ Index Scan using tmp1_b_idx on tmp1
+ Index Cond: (b = 'xxx'::text)
+(2 rows)
+
+SELECT * FROM tmp1 WHERE b = 'xxx';
+ a | b
+---+-----
+ 1 | xxx
+(1 row)
+
+RESET enable_seqscan;
+REINDEX INDEX CONCURRENTLY tmp1_b_idx;
+REINDEX TABLE CONCURRENTLY tmp1;
+DROP INDEX CONCURRENTLY tmp1_b_idx;
-- Test ON COMMIT DELETE ROWS
CREATE GLOBAL TEMP TABLE tmp2 (a int) ON COMMIT DELETE ROWS;
BEGIN;
@@ -103,8 +145,8 @@ ERROR: ON COMMIT DROP cannot be used on global temporary tables
-- Two-phase commit not allowed with global temp tables
BEGIN;
SELECT * FROM tmp1;
- a
----
+ a | b
+---+---
(0 rows)
PREPARE TRANSACTION 'twophase'; -- fail
@@ -147,11 +189,33 @@ DROP TABLE tmp2;
-- Test foreign keys
CREATE TABLE perm_pk_rel (a int PRIMARY KEY);
CREATE TEMP TABLE temp_pk_rel (a int PRIMARY KEY);
+CREATE GLOBAL TEMP TABLE gtemp_pk_rel (a int PRIMARY KEY);
+CREATE TABLE tmp2 (a int REFERENCES gtemp_pk_rel); -- fail
+ERROR: constraints on permanent tables may reference only permanent tables
+CREATE TEMP TABLE tmp2 (a int REFERENCES gtemp_pk_rel); -- fail
+ERROR: constraints on local temporary tables may reference only local temporary tables
CREATE GLOBAL TEMP TABLE tmp2 (a int REFERENCES perm_pk_rel); -- fail
ERROR: constraints on global temporary tables may reference only global temporary tables
CREATE GLOBAL TEMP TABLE tmp2 (a int REFERENCES temp_pk_rel); -- fail
ERROR: constraints on global temporary tables may reference only global temporary tables
-DROP TABLE perm_pk_rel, temp_pk_rel;
+CREATE GLOBAL TEMP TABLE tmp2 (a int REFERENCES gtemp_pk_rel);
+INSERT INTO gtemp_pk_rel VALUES (1);
+INSERT INTO tmp2 VALUES (1);
+INSERT INTO tmp2 VALUES (2); -- fail
+ERROR: insert or update on table "tmp2" violates foreign key constraint "tmp2_a_fkey"
+DETAIL: Key (a)=(2) is not present in table "gtemp_pk_rel".
+DELETE FROM gtemp_pk_rel WHERE a = 1; -- fail
+ERROR: update or delete on table "gtemp_pk_rel" violates foreign key constraint "tmp2_a_fkey" on table "tmp2"
+DETAIL: Key (a)=(1) is still referenced from table "tmp2".
+ALTER TABLE tmp2 DROP CONSTRAINT tmp2_a_fkey;
+ALTER TABLE tmp2 ADD FOREIGN KEY (a) REFERENCES gtemp_pk_rel ON DELETE CASCADE;
+DELETE FROM gtemp_pk_rel WHERE a = 1;
+SELECT * FROM tmp2;
+ a
+---
+(0 rows)
+
+DROP TABLE perm_pk_rel, temp_pk_rel, tmp2;
-- Test ALTER TABLE ... SET TABLESPACE
CREATE GLOBAL TEMP TABLE tmp2 (a int);
INSERT INTO tmp2 VALUES (1);
@@ -204,85 +268,85 @@ DETAIL: tablespace for table tmp2
DROP TABLE tmp2;
DROP TABLESPACE regress_temp_test_tablespace;
-- Test TRUNCATE
-INSERT INTO tmp1 VALUES (1);
+INSERT INTO tmp1 VALUES (1, 'xxx');
BEGIN;
TRUNCATE tmp1;
SELECT * FROM tmp1;
- a
----
+ a | b
+---+---
(0 rows)
ROLLBACK;
SELECT * FROM tmp1;
- a
----
- 1
+ a | b
+---+-----
+ 1 | xxx
(1 row)
BEGIN;
SAVEPOINT sp1;
TRUNCATE tmp1;
SELECT * FROM tmp1;
- a
----
+ a | b
+---+---
(0 rows)
RELEASE sp1;
SELECT * FROM tmp1;
- a
----
+ a | b
+---+---
(0 rows)
ROLLBACK;
SELECT * FROM tmp1;
- a
----
- 1
+ a | b
+---+-----
+ 1 | xxx
(1 row)
BEGIN;
SAVEPOINT sp1;
TRUNCATE tmp1;
SELECT * FROM tmp1;
- a
----
+ a | b
+---+---
(0 rows)
ROLLBACK TO sp1;
SELECT * FROM tmp1;
- a
----
- 1
+ a | b
+---+-----
+ 1 | xxx
(1 row)
COMMIT;
SELECT * FROM tmp1;
- a
----
- 1
+ a | b
+---+-----
+ 1 | xxx
(1 row)
TRUNCATE tmp1;
SELECT * FROM tmp1;
- a
----
+ a | b
+---+---
(0 rows)
-- Test view creation
-INSERT INTO tmp1 VALUES (1);
+INSERT INTO tmp1 VALUES (1, 'xxx');
CREATE VIEW v AS SELECT * FROM tmp1;
SELECT * FROM v;
- a
----
- 1
+ a | b
+---+-----
+ 1 | xxx
(1 row)
DROP VIEW v;
CREATE TEMP VIEW v AS SELECT * FROM tmp1;
SELECT * FROM v;
- a
----
- 1
+ a | b
+---+-----
+ 1 | xxx
(1 row)
DROP VIEW v;
diff --git a/src/test/regress/sql/global_temp.sql b/src/test/regress/sql/global_temp.sql
index 139e0f74b4b..7255a35f2d4 100644
--- a/src/test/regress/sql/global_temp.sql
+++ b/src/test/regress/sql/global_temp.sql
@@ -11,7 +11,7 @@ SET ROLE regress_global_temp_user;
-- Test table creation
CREATE GLOBAL TEMP TABLE pg_temp.tmp1 (a int); -- fail
-CREATE GLOBAL TEMP TABLE tmp1 (a int);
+CREATE GLOBAL TEMP TABLE tmp1 (a int PRIMARY KEY, b text);
CREATE SCHEMA global_temp_xxx CREATE GLOBAL TEMP TABLE tmp2 (a int);
CREATE SCHEMA global_temp_yyy;
CREATE GLOBAL TEMP TABLE global_temp_yyy.tmp3 (a int);
@@ -29,12 +29,31 @@ DROP SCHEMA global_temp_xxx CASCADE;
DROP SCHEMA global_temp_yyy CASCADE;
-- Basic tests
-INSERT INTO tmp1 VALUES (1);
+INSERT INTO tmp1 VALUES (1, 'xxx');
SELECT * FROM tmp1;
\c
SET search_path = global_temp_tests;
SELECT * FROM tmp1;
+-- Test index
+INSERT INTO tmp1 VALUES (1, 'xxx');
+SET enable_seqscan = off;
+EXPLAIN (COSTS OFF)
+SELECT * FROM tmp1 WHERE a = 1;
+SELECT * FROM tmp1 WHERE a = 1;
+RESET enable_seqscan;
+
+-- Test concurrent index build -- CONCURRENTLY is ignored with temp tables
+CREATE INDEX CONCURRENTLY tmp1_b_idx ON tmp1(b);
+SET enable_seqscan = off;
+EXPLAIN (COSTS OFF)
+SELECT * FROM tmp1 WHERE b = 'xxx';
+SELECT * FROM tmp1 WHERE b = 'xxx';
+RESET enable_seqscan;
+REINDEX INDEX CONCURRENTLY tmp1_b_idx;
+REINDEX TABLE CONCURRENTLY tmp1;
+DROP INDEX CONCURRENTLY tmp1_b_idx;
+
-- Test ON COMMIT DELETE ROWS
CREATE GLOBAL TEMP TABLE tmp2 (a int) ON COMMIT DELETE ROWS;
BEGIN;
@@ -84,9 +103,21 @@ DROP TABLE tmp2;
-- Test foreign keys
CREATE TABLE perm_pk_rel (a int PRIMARY KEY);
CREATE TEMP TABLE temp_pk_rel (a int PRIMARY KEY);
+CREATE GLOBAL TEMP TABLE gtemp_pk_rel (a int PRIMARY KEY);
+CREATE TABLE tmp2 (a int REFERENCES gtemp_pk_rel); -- fail
+CREATE TEMP TABLE tmp2 (a int REFERENCES gtemp_pk_rel); -- fail
CREATE GLOBAL TEMP TABLE tmp2 (a int REFERENCES perm_pk_rel); -- fail
CREATE GLOBAL TEMP TABLE tmp2 (a int REFERENCES temp_pk_rel); -- fail
-DROP TABLE perm_pk_rel, temp_pk_rel;
+CREATE GLOBAL TEMP TABLE tmp2 (a int REFERENCES gtemp_pk_rel);
+INSERT INTO gtemp_pk_rel VALUES (1);
+INSERT INTO tmp2 VALUES (1);
+INSERT INTO tmp2 VALUES (2); -- fail
+DELETE FROM gtemp_pk_rel WHERE a = 1; -- fail
+ALTER TABLE tmp2 DROP CONSTRAINT tmp2_a_fkey;
+ALTER TABLE tmp2 ADD FOREIGN KEY (a) REFERENCES gtemp_pk_rel ON DELETE CASCADE;
+DELETE FROM gtemp_pk_rel WHERE a = 1;
+SELECT * FROM tmp2;
+DROP TABLE perm_pk_rel, temp_pk_rel, tmp2;
-- Test ALTER TABLE ... SET TABLESPACE
CREATE GLOBAL TEMP TABLE tmp2 (a int);
@@ -119,7 +150,7 @@ DROP TABLE tmp2;
DROP TABLESPACE regress_temp_test_tablespace;
-- Test TRUNCATE
-INSERT INTO tmp1 VALUES (1);
+INSERT INTO tmp1 VALUES (1, 'xxx');
BEGIN;
TRUNCATE tmp1;
SELECT * FROM tmp1;
@@ -148,7 +179,7 @@ TRUNCATE tmp1;
SELECT * FROM tmp1;
-- Test view creation
-INSERT INTO tmp1 VALUES (1);
+INSERT INTO tmp1 VALUES (1, 'xxx');
CREATE VIEW v AS SELECT * FROM tmp1;
SELECT * FROM v;
DROP VIEW v;
--
2.51.0
[text/x-patch] v8-0004-Support-global-temporary-sequences.patch (17.6K, ../CAEZATCVjaZFa80S-V_+nvnzRK1ZiLUPx8jwdyAtRVHAmPZQQMg@mail.gmail.com/5-v8-0004-Support-global-temporary-sequences.patch)
download | inline diff:
From 51193efa07d57e076f39f0b889b541e47be98082 Mon Sep 17 00:00:00 2001
From: Dean Rasheed <[email protected]>
Date: Tue, 9 Jun 2026 19:00:09 +0100
Subject: [PATCH v8 04/10] Support global temporary sequences.
---
src/backend/catalog/global_temp.c | 5 +
src/backend/commands/sequence.c | 66 ++++++++
src/backend/parser/parse_utilcmd.c | 3 +-
src/bin/pg_dump/pg_dump.c | 4 +-
src/bin/psql/describe.c | 6 +
src/include/commands/sequence.h | 1 +
src/test/isolation/expected/global-temp.out | 176 ++++++++++----------
src/test/isolation/specs/global-temp.spec | 2 +-
src/test/regress/expected/global_temp.out | 121 +++++++++-----
src/test/regress/sql/global_temp.sql | 10 +-
10 files changed, 257 insertions(+), 137 deletions(-)
diff --git a/src/backend/catalog/global_temp.c b/src/backend/catalog/global_temp.c
index 6da94c44e86..a6a2b1462c7 100644
--- a/src/backend/catalog/global_temp.c
+++ b/src/backend/catalog/global_temp.c
@@ -62,6 +62,7 @@
#include "access/xlogutils.h"
#include "catalog/global_temp.h"
#include "catalog/storage.h"
+#include "commands/sequence.h"
#include "commands/tablecmds.h"
#include "lib/dshash.h"
#include "miscadmin.h"
@@ -876,6 +877,10 @@ InitGlobalTempRelation(Relation relation)
if (nblocks > 0)
relation->rd_index->indisvalid = false;
}
+
+ /* If it's a sequence, initialize it */
+ if (relation->rd_rel->relkind == RELKIND_SEQUENCE)
+ InitGlobalTempSequence(relation);
}
/* The remaining initialization works as if we had created it locally */
diff --git a/src/backend/commands/sequence.c b/src/backend/commands/sequence.c
index 551667650ba..9a7cace6c37 100644
--- a/src/backend/commands/sequence.c
+++ b/src/backend/commands/sequence.c
@@ -321,6 +321,72 @@ ResetSequence(Oid seq_relid)
sequence_close(seq_rel, NoLock);
}
+/*
+ * InitGlobalTempSequence - initialize a global temporary sequence
+ *
+ * This is called the first time a global temporary sequence is accessed from
+ * a backend other than the backend that created it. On entry, the sequence
+ * should have valid catalog entries, and its physical disk file should have
+ * been created, but be empty.
+ */
+void
+InitGlobalTempSequence(Relation seq_rel)
+{
+ Oid seq_relid = RelationGetRelid(seq_rel);
+ SeqTable elm;
+ HeapTuple pgstuple;
+ Form_pg_sequence pgsform;
+ int64 startv;
+ int i;
+ Datum value[SEQ_COL_LASTCOL];
+ bool null[SEQ_COL_LASTCOL];
+ TupleDesc tupDesc;
+ HeapTuple tuple;
+
+ /* Find or create a hash table entry for this sequence */
+ if (seqhashtab == NULL)
+ create_seq_hashtable();
+
+ elm = (SeqTable) hash_search(seqhashtab, &seq_relid, HASH_ENTER, NULL);
+
+ /* Initialize the sequence state */
+ elm->filenumber = seq_rel->rd_rel->relfilenode;
+ elm->lxid = InvalidLocalTransactionId;
+ elm->last_valid = false;
+ elm->last = elm->cached = 0;
+
+ /* Read the sequence definition from pg_sequence */
+ pgstuple = SearchSysCache1(SEQRELID, ObjectIdGetDatum(seq_relid));
+ if (!HeapTupleIsValid(pgstuple))
+ elog(ERROR, "cache lookup failed for sequence %u", seq_relid);
+ pgsform = (Form_pg_sequence) GETSTRUCT(pgstuple);
+ startv = pgsform->seqstart;
+ ReleaseSysCache(pgstuple);
+
+ /* Build a new sequence tuple */
+ for (i = SEQ_COL_FIRSTCOL; i <= SEQ_COL_LASTCOL; i++)
+ {
+ switch (i)
+ {
+ case SEQ_COL_LASTVAL:
+ value[i - 1] = Int64GetDatumFast(startv);
+ break;
+ case SEQ_COL_LOG:
+ value[i - 1] = Int64GetDatum((int64) 0);
+ break;
+ case SEQ_COL_CALLED:
+ value[i - 1] = BoolGetDatum(false);
+ break;
+ }
+ null[i - 1] = false;
+ }
+ tupDesc = RelationGetDescr(seq_rel);
+ tuple = heap_form_tuple(tupDesc, value, null);
+
+ /* Initialize the sequence's data */
+ fill_seq_with_data(seq_rel, tuple);
+}
+
/*
* Initialize a sequence's relation with the specified tuple as content
*
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 58ccc7b68f2..2b36d6c7f98 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -496,7 +496,8 @@ generateSerialExtraStmts(CreateStmtContext *cxt, ColumnDef *column,
seqpersistence = cxt->rel ? cxt->rel->rd_rel->relpersistence : cxt->relation->relpersistence;
if (loggedEl)
{
- if (seqpersistence == RELPERSISTENCE_TEMP)
+ if (seqpersistence == RELPERSISTENCE_TEMP ||
+ seqpersistence == RELPERSISTENCE_GLOBAL_TEMP)
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("cannot set logged status of a temporary sequence"),
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index f52f5f5a704..9d5f5cac877 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -19330,7 +19330,9 @@ dumpSequence(Archive *fout, const TableInfo *tbinfo)
appendPQExpBuffer(query,
"CREATE %sSEQUENCE %s\n",
tbinfo->relpersistence == RELPERSISTENCE_UNLOGGED ?
- "UNLOGGED " : "",
+ "UNLOGGED " :
+ tbinfo->relpersistence == RELPERSISTENCE_GLOBAL_TEMP ?
+ "GLOBAL TEMP " : "",
fmtQualifiedDumpable(tbinfo));
if (seq->seqtype != SEQTYPE_BIGINT)
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index daaa9f9e69d..8ce4decb91e 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1806,6 +1806,12 @@ describeOneTableDetails(const char *schemaname,
if (tableinfo.relpersistence == RELPERSISTENCE_UNLOGGED)
printfPQExpBuffer(&title, _("Unlogged sequence \"%s.%s\""),
schemaname, relationname);
+ else if (tableinfo.relpersistence == RELPERSISTENCE_TEMP)
+ printfPQExpBuffer(&title, _("Temporary sequence \"%s.%s\""),
+ schemaname, relationname);
+ else if (tableinfo.relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+ printfPQExpBuffer(&title, _("Global temporary sequence \"%s.%s\""),
+ schemaname, relationname);
else
printfPQExpBuffer(&title, _("Sequence \"%s.%s\""),
schemaname, relationname);
diff --git a/src/include/commands/sequence.h b/src/include/commands/sequence.h
index 2c3c4a3f074..6f514ac6dd1 100644
--- a/src/include/commands/sequence.h
+++ b/src/include/commands/sequence.h
@@ -47,6 +47,7 @@ extern ObjectAddress AlterSequence(ParseState *pstate, AlterSeqStmt *stmt);
extern void SequenceChangePersistence(Oid relid, char newrelpersistence);
extern void DeleteSequenceTuple(Oid relid);
extern void ResetSequence(Oid seq_relid);
+extern void InitGlobalTempSequence(Relation seq_rel);
extern void SetSequence(Oid relid, int64 next, bool iscalled);
extern void ResetSequenceCaches(void);
diff --git a/src/test/isolation/expected/global-temp.out b/src/test/isolation/expected/global-temp.out
index 3a6af7eb23e..d46ca85acbb 100644
--- a/src/test/isolation/expected/global-temp.out
+++ b/src/test/isolation/expected/global-temp.out
@@ -4,15 +4,15 @@ starting permutation: ins1 ins2 sel1 sel2
step ins1: INSERT INTO tmp VALUES (1, 's1');
step ins2: INSERT INTO tmp VALUES (1, 's2');
step sel1: SELECT * FROM tmp;
-key|val
----+---
- 1|s1
+key|val|seq
+---+---+---
+ 1|s1 | 1
(1 row)
step sel2: SELECT * FROM tmp;
-key|val
----+---
- 1|s2
+key|val|seq
+---+---+---
+ 1|s2 | 1
(1 row)
@@ -41,28 +41,28 @@ step ins1: INSERT INTO tmp VALUES (1, 's1');
step b2: BEGIN;
step ins2: INSERT INTO tmp VALUES (1, 's2');
step sel1: SELECT * FROM tmp;
-key|val
----+---
- 1|s1
+key|val|seq
+---+---+---
+ 1|s1 | 1
(1 row)
step sel2: SELECT * FROM tmp;
-key|val
----+---
- 1|s2
+key|val|seq
+---+---+---
+ 1|s2 | 1
(1 row)
step c2: COMMIT;
step sel1: SELECT * FROM tmp;
-key|val
----+---
- 1|s1
+key|val|seq
+---+---+---
+ 1|s1 | 1
(1 row)
step sel2: SELECT * FROM tmp;
-key|val
----+---
- 1|s2
+key|val|seq
+---+---+---
+ 1|s2 | 1
(1 row)
@@ -71,27 +71,27 @@ step ins1: INSERT INTO tmp VALUES (1, 's1');
step b2: BEGIN;
step ins2: INSERT INTO tmp VALUES (1, 's2');
step sel1: SELECT * FROM tmp;
-key|val
----+---
- 1|s1
+key|val|seq
+---+---+---
+ 1|s1 | 1
(1 row)
step sel2: SELECT * FROM tmp;
-key|val
----+---
- 1|s2
+key|val|seq
+---+---+---
+ 1|s2 | 1
(1 row)
step r2: ROLLBACK;
step sel1: SELECT * FROM tmp;
-key|val
----+---
- 1|s1
+key|val|seq
+---+---+---
+ 1|s1 | 1
(1 row)
step sel2: SELECT * FROM tmp;
-key|val
----+---
+key|val|seq
+---+---+---
(0 rows)
@@ -100,28 +100,28 @@ step ins1: INSERT INTO tmp VALUES (1, 's1');
step b2: BEGIN;
step ins2: INSERT INTO tmp VALUES (1, 's2');
step sel1: SELECT * FROM tmp;
-key|val
----+---
- 1|s1
+key|val|seq
+---+---+---
+ 1|s1 | 1
(1 row)
step sel2: SELECT * FROM tmp;
-key|val
----+---
- 1|s2
+key|val|seq
+---+---+---
+ 1|s2 | 1
(1 row)
step sp2: SAVEPOINT sp;
step r2: ROLLBACK;
step sel1: SELECT * FROM tmp;
-key|val
----+---
- 1|s1
+key|val|seq
+---+---+---
+ 1|s1 | 1
(1 row)
step sel2: SELECT * FROM tmp;
-key|val
----+---
+key|val|seq
+---+---+---
(0 rows)
@@ -131,39 +131,39 @@ step b2: BEGIN;
step sp2: SAVEPOINT sp;
step ins2: INSERT INTO tmp VALUES (1, 's2');
step sel1: SELECT * FROM tmp;
-key|val
----+---
- 1|s1
+key|val|seq
+---+---+---
+ 1|s1 | 1
(1 row)
step sel2: SELECT * FROM tmp;
-key|val
----+---
- 1|s2
+key|val|seq
+---+---+---
+ 1|s2 | 1
(1 row)
step rsp2: ROLLBACK TO SAVEPOINT sp;
step sel1: SELECT * FROM tmp;
-key|val
----+---
- 1|s1
+key|val|seq
+---+---+---
+ 1|s1 | 1
(1 row)
step sel2: SELECT * FROM tmp;
-key|val
----+---
+key|val|seq
+---+---+---
(0 rows)
step r2: ROLLBACK;
step sel1: SELECT * FROM tmp;
-key|val
----+---
- 1|s1
+key|val|seq
+---+---+---
+ 1|s1 | 1
(1 row)
step sel2: SELECT * FROM tmp;
-key|val
----+---
+key|val|seq
+---+---+---
(0 rows)
@@ -175,27 +175,27 @@ step sp2: SAVEPOINT sp;
step t2: TRUNCATE tmp;
step rsp2: ROLLBACK TO SAVEPOINT sp;
step sel1: SELECT * FROM tmp;
-key|val
----+---
- 1|s1
+key|val|seq
+---+---+---
+ 1|s1 | 1
(1 row)
step sel2: SELECT * FROM tmp;
-key|val
----+---
- 1|s2
+key|val|seq
+---+---+---
+ 1|s2 | 1
(1 row)
step r2: ROLLBACK;
step sel1: SELECT * FROM tmp;
-key|val
----+---
- 1|s1
+key|val|seq
+---+---+---
+ 1|s1 | 1
(1 row)
step sel2: SELECT * FROM tmp;
-key|val
----+---
+key|val|seq
+---+---+---
(0 rows)
@@ -267,9 +267,9 @@ Index Scan using tmp_val_idx on tmp
Index Cond: (val = 's1'::text)
(2 rows)
-key|val
----+---
- 1|s1
+key|val|seq
+---+---+---
+ 1|s1 | 1
(1 row)
step ins2: INSERT INTO tmp VALUES (1, 's2');
@@ -286,9 +286,9 @@ Index Scan using tmp_val_idx on tmp
Index Cond: (val = 's2'::text)
(2 rows)
-key|val
----+---
- 1|s2
+key|val|seq
+---+---+---
+ 1|s2 | 1
(1 row)
@@ -309,9 +309,9 @@ Index Scan using tmp_val_idx on tmp
Index Cond: (val = 's1'::text)
(2 rows)
-key|val
----+---
- 1|s1
+key|val|seq
+---+---+---
+ 1|s1 | 1
(1 row)
step sel2_idx:
@@ -328,9 +328,9 @@ Seq Scan on tmp
Filter: (val = 's2'::text)
(3 rows)
-key|val
----+---
- 1|s2
+key|val|seq
+---+---+---
+ 1|s2 | 1
(1 row)
@@ -351,9 +351,9 @@ Index Scan using tmp_val_idx on tmp
Index Cond: (val = 's1'::text)
(2 rows)
-key|val
----+---
- 1|s1
+key|val|seq
+---+---+---
+ 1|s1 | 1
(1 row)
step sel2_idx:
@@ -370,9 +370,9 @@ Seq Scan on tmp
Filter: (val = 's2'::text)
(3 rows)
-key|val
----+---
- 1|s2
+key|val|seq
+---+---+---
+ 1|s2 | 1
(1 row)
step reidx2: REINDEX INDEX tmp_val_idx;
@@ -389,8 +389,8 @@ Index Scan using tmp_val_idx on tmp
Index Cond: (val = 's2'::text)
(2 rows)
-key|val
----+---
- 1|s2
+key|val|seq
+---+---+---
+ 1|s2 | 1
(1 row)
diff --git a/src/test/isolation/specs/global-temp.spec b/src/test/isolation/specs/global-temp.spec
index 5136230e859..4ec963e104a 100644
--- a/src/test/isolation/specs/global-temp.spec
+++ b/src/test/isolation/specs/global-temp.spec
@@ -1,7 +1,7 @@
# Test global temporary relations
setup {
- CREATE GLOBAL TEMP TABLE tmp (key int PRIMARY KEY, val text);
+ CREATE GLOBAL TEMP TABLE tmp (key int PRIMARY KEY, val text, seq serial);
CREATE GLOBAL TEMP TABLE tmp_parted (key int PRIMARY KEY, val text) PARTITION BY LIST (key);
CREATE GLOBAL TEMP TABLE tmp_p1 PARTITION OF tmp_parted FOR VALUES IN (1);
diff --git a/src/test/regress/expected/global_temp.out b/src/test/regress/expected/global_temp.out
index 1b2450a49b5..6d2751e6899 100644
--- a/src/test/regress/expected/global_temp.out
+++ b/src/test/regress/expected/global_temp.out
@@ -13,16 +13,17 @@ CREATE GLOBAL TEMP TABLE pg_temp.tmp1 (a int); -- fail
ERROR: cannot create global temporary relation in temporary schema
LINE 1: CREATE GLOBAL TEMP TABLE pg_temp.tmp1 (a int);
^
-CREATE GLOBAL TEMP TABLE tmp1 (a int PRIMARY KEY, b text);
+CREATE GLOBAL TEMP TABLE tmp1 (a int PRIMARY KEY, b text, c serial);
CREATE SCHEMA global_temp_xxx CREATE GLOBAL TEMP TABLE tmp2 (a int);
CREATE SCHEMA global_temp_yyy;
CREATE GLOBAL TEMP TABLE global_temp_yyy.tmp3 (a int);
\d tmp1
- Global temporary table "global_temp_tests.tmp1"
- Column | Type | Collation | Nullable | Default
---------+---------+-----------+----------+---------
+ Global temporary table "global_temp_tests.tmp1"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------------------------------
a | integer | | not null |
b | text | | |
+ c | integer | | not null | nextval('tmp1_c_seq'::regclass)
Indexes:
"tmp1_pkey" PRIMARY KEY, btree (a)
@@ -54,16 +55,16 @@ NOTICE: drop cascades to table global_temp_yyy.tmp3
-- Basic tests
INSERT INTO tmp1 VALUES (1, 'xxx');
SELECT * FROM tmp1;
- a | b
----+-----
- 1 | xxx
+ a | b | c
+---+-----+---
+ 1 | xxx | 1
(1 row)
\c
SET search_path = global_temp_tests;
SELECT * FROM tmp1;
- a | b
----+---
+ a | b | c
+---+---+---
(0 rows)
-- Test index
@@ -78,9 +79,9 @@ SELECT * FROM tmp1 WHERE a = 1;
(2 rows)
SELECT * FROM tmp1 WHERE a = 1;
- a | b
----+-----
- 1 | xxx
+ a | b | c
+---+-----+---
+ 1 | xxx | 1
(1 row)
RESET enable_seqscan;
@@ -96,9 +97,9 @@ SELECT * FROM tmp1 WHERE b = 'xxx';
(2 rows)
SELECT * FROM tmp1 WHERE b = 'xxx';
- a | b
----+-----
- 1 | xxx
+ a | b | c
+---+-----+---
+ 1 | xxx | 1
(1 row)
RESET enable_seqscan;
@@ -145,8 +146,8 @@ ERROR: ON COMMIT DROP cannot be used on global temporary tables
-- Two-phase commit not allowed with global temp tables
BEGIN;
SELECT * FROM tmp1;
- a | b
----+---
+ a | b | c
+---+---+---
(0 rows)
PREPARE TRANSACTION 'twophase'; -- fail
@@ -272,83 +273,113 @@ INSERT INTO tmp1 VALUES (1, 'xxx');
BEGIN;
TRUNCATE tmp1;
SELECT * FROM tmp1;
- a | b
----+---
+ a | b | c
+---+---+---
(0 rows)
ROLLBACK;
SELECT * FROM tmp1;
- a | b
----+-----
- 1 | xxx
+ a | b | c
+---+-----+---
+ 1 | xxx | 1
(1 row)
BEGIN;
SAVEPOINT sp1;
TRUNCATE tmp1;
SELECT * FROM tmp1;
- a | b
----+---
+ a | b | c
+---+---+---
(0 rows)
RELEASE sp1;
SELECT * FROM tmp1;
- a | b
----+---
+ a | b | c
+---+---+---
(0 rows)
ROLLBACK;
SELECT * FROM tmp1;
- a | b
----+-----
- 1 | xxx
+ a | b | c
+---+-----+---
+ 1 | xxx | 1
(1 row)
BEGIN;
SAVEPOINT sp1;
TRUNCATE tmp1;
SELECT * FROM tmp1;
- a | b
----+---
+ a | b | c
+---+---+---
(0 rows)
ROLLBACK TO sp1;
SELECT * FROM tmp1;
- a | b
----+-----
- 1 | xxx
+ a | b | c
+---+-----+---
+ 1 | xxx | 1
(1 row)
COMMIT;
SELECT * FROM tmp1;
- a | b
----+-----
- 1 | xxx
+ a | b | c
+---+-----+---
+ 1 | xxx | 1
(1 row)
TRUNCATE tmp1;
SELECT * FROM tmp1;
- a | b
----+---
+ a | b | c
+---+---+---
(0 rows)
-- Test view creation
INSERT INTO tmp1 VALUES (1, 'xxx');
CREATE VIEW v AS SELECT * FROM tmp1;
SELECT * FROM v;
- a | b
----+-----
- 1 | xxx
+ a | b | c
+---+-----+---
+ 1 | xxx | 2
(1 row)
DROP VIEW v;
CREATE TEMP VIEW v AS SELECT * FROM tmp1;
SELECT * FROM v;
- a | b
----+-----
- 1 | xxx
+ a | b | c
+---+-----+---
+ 1 | xxx | 2
(1 row)
DROP VIEW v;
CREATE GLOBAL TEMP VIEW v AS SELECT * FROM tmp1; -- fail
ERROR: views cannot be global temporary because they do not have storage
+-- Test global temp sequence
+CREATE GLOBAL TEMP SEQUENCE s MINVALUE 100 MAXVALUE 130 INCREMENT 10 START WITH 110 CYCLE;
+\d s
+ Global temporary sequence "global_temp_tests.s"
+ Type | Start | Minimum | Maximum | Increment | Cycles? | Cache
+--------+-------+---------+---------+-----------+---------+-------
+ bigint | 110 | 100 | 130 | 10 | yes | 1
+
+SELECT nextval('s') FROM generate_series(1, 5);
+ nextval
+---------
+ 110
+ 120
+ 130
+ 100
+ 110
+(5 rows)
+
+\c
+SET search_path = global_temp_tests;
+SELECT nextval('s') FROM generate_series(1, 5);
+ nextval
+---------
+ 110
+ 120
+ 130
+ 100
+ 110
+(5 rows)
+
diff --git a/src/test/regress/sql/global_temp.sql b/src/test/regress/sql/global_temp.sql
index 7255a35f2d4..2373eae0ad1 100644
--- a/src/test/regress/sql/global_temp.sql
+++ b/src/test/regress/sql/global_temp.sql
@@ -11,7 +11,7 @@ SET ROLE regress_global_temp_user;
-- Test table creation
CREATE GLOBAL TEMP TABLE pg_temp.tmp1 (a int); -- fail
-CREATE GLOBAL TEMP TABLE tmp1 (a int PRIMARY KEY, b text);
+CREATE GLOBAL TEMP TABLE tmp1 (a int PRIMARY KEY, b text, c serial);
CREATE SCHEMA global_temp_xxx CREATE GLOBAL TEMP TABLE tmp2 (a int);
CREATE SCHEMA global_temp_yyy;
CREATE GLOBAL TEMP TABLE global_temp_yyy.tmp3 (a int);
@@ -189,3 +189,11 @@ SELECT * FROM v;
DROP VIEW v;
CREATE GLOBAL TEMP VIEW v AS SELECT * FROM tmp1; -- fail
+
+-- Test global temp sequence
+CREATE GLOBAL TEMP SEQUENCE s MINVALUE 100 MAXVALUE 130 INCREMENT 10 START WITH 110 CYCLE;
+\d s
+SELECT nextval('s') FROM generate_series(1, 5);
+\c
+SET search_path = global_temp_tests;
+SELECT nextval('s') FROM generate_series(1, 5);
--
2.51.0
[text/x-patch] v8-0005-Support-global-temporary-catalog-tables-and-add-p.patch (91.2K, ../CAEZATCVjaZFa80S-V_+nvnzRK1ZiLUPx8jwdyAtRVHAmPZQQMg@mail.gmail.com/6-v8-0005-Support-global-temporary-catalog-tables-and-add-p.patch)
download | inline diff:
From 7a7f704c57fc41180f974c6b74a9f4b7431f2b99 Mon Sep 17 00:00:00 2001
From: Dean Rasheed <[email protected]>
Date: Wed, 10 Jun 2026 18:23:24 +0100
Subject: [PATCH v8 05/10] Support global temporary catalog tables and add
pg_temp_class.
This commit allows system catalog tables to be global temporary
tables, and adds the first such example: pg_temp_class. The idea is
that pg_temp_class will contain one row for each global temporary
table accessed in the session, allowing a subset of the attributes
from pg_class to be overridden locally for that session.
To avoid bootstrapping difficulties when pg_temp_class itself is first
accessed, and needs to insert rows describing itself and its index,
all inserts to pg_temp_class are held in a pending queue to be flushed
later (at the end of the current command or (sub)transaction commit).
In this initial commit, pg_temp_class only has oid, relfilenode, and
reltablespace attributes, allowing CLUSTER, REINDEX, REPACK, TRUNCATE,
and VACUUM FULL to make changes locally to the current session,
without affecting other running sessions. ALTER TABLE SET TABLESPACE
works similarly, except that it updates both pg_class and
pg_temp_class, so that it applies to the current session and any
future sessions, but not any other current sessions that have already
accessed the table.
---
src/backend/access/transam/xact.c | 13 +
src/backend/bootstrap/bootparse.y | 23 +-
src/backend/bootstrap/bootscanner.l | 1 +
src/backend/catalog/Catalog.pm | 2 +
src/backend/catalog/Makefile | 1 +
src/backend/catalog/genbki.pl | 42 ++
src/backend/catalog/global_temp.c | 23 +-
src/backend/catalog/index.c | 18 +
src/backend/catalog/meson.build | 1 +
src/backend/catalog/pg_temp_class.c | 713 ++++++++++++++++++++
src/backend/commands/repack.c | 94 ++-
src/backend/commands/tablecmds.c | 40 +-
src/backend/commands/vacuum.c | 18 +
src/backend/parser/parse_utilcmd.c | 9 +-
src/backend/utils/activity/pgstat_io.c | 11 +-
src/backend/utils/adt/dbsize.c | 11 +-
src/backend/utils/cache/inval.c | 17 +-
src/backend/utils/cache/lsyscache.c | 14 +
src/backend/utils/cache/relcache.c | 69 +-
src/backend/utils/cache/syscache.c | 7 +-
src/include/access/htup_details.h | 11 +
src/include/catalog/Makefile | 3 +-
src/include/catalog/genbki.h | 1 +
src/include/catalog/meson.build | 1 +
src/include/catalog/pg_temp_class.h | 150 ++++
src/test/isolation/expected/global-temp.out | 109 +++
src/test/isolation/specs/global-temp.spec | 37 +
src/test/recovery/t/018_wal_optimize.pl | 1 +
src/test/regress/expected/global_temp.out | 184 ++++-
src/test/regress/expected/oidjoins.out | 2 +
src/test/regress/expected/stats.out | 3 +-
src/test/regress/sql/global_temp.sql | 114 +++-
src/tools/pgindent/typedefs.list | 3 +
33 files changed, 1671 insertions(+), 75 deletions(-)
create mode 100644 src/backend/catalog/pg_temp_class.c
create mode 100644 src/include/catalog/pg_temp_class.h
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index e1b092014e9..7c631de9393 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -36,6 +36,7 @@
#include "catalog/index.h"
#include "catalog/namespace.h"
#include "catalog/pg_enum.h"
+#include "catalog/pg_temp_class.h"
#include "catalog/storage.h"
#include "commands/async.h"
#include "commands/tablecmds.h"
@@ -1148,6 +1149,9 @@ CommandCounterIncrement(void)
(errcode(ERRCODE_INVALID_TRANSACTION_STATE),
errmsg("cannot start commands during a parallel operation")));
+ /* Flush out any pending inserts to pg_temp_class */
+ PreCCI_PgTempClass();
+
currentCommandId += 1;
if (currentCommandId == InvalidCommandId)
{
@@ -2360,6 +2364,9 @@ CommitTransaction(void)
*/
PreCommit_on_commit_actions();
+ /* Flush out any pending inserts to pg_temp_class */
+ PreCommit_PgTempClass();
+
/*
* Synchronize files that are created and not WAL-logged during this
* transaction. This must happen before AtEOXact_RelationMap(), so that we
@@ -2633,6 +2640,9 @@ PrepareTransaction(void)
*/
PreCommit_on_commit_actions();
+ /* Flush out any pending inserts to pg_temp_class */
+ PreCommit_PgTempClass();
+
/*
* Synchronize files that are created and not WAL-logged during this
* transaction. This must happen before EndPrepare(), so that we don't see
@@ -5192,6 +5202,9 @@ CommitSubTransaction(void)
CallSubXactCallbacks(SUBXACT_EVENT_PRE_COMMIT_SUB, s->subTransactionId,
s->parent->subTransactionId);
+ /* Flush out any pending inserts to pg_temp_class */
+ PreSubCommit_PgTempClass();
+
/*
* If this subxact has started any unfinished parallel operation, clean up
* its workers and exit parallel mode. Warn about leaked resources.
diff --git a/src/backend/bootstrap/bootparse.y b/src/backend/bootstrap/bootparse.y
index 305a5654ff3..7026a138375 100644
--- a/src/backend/bootstrap/bootparse.y
+++ b/src/backend/bootstrap/bootparse.y
@@ -96,7 +96,7 @@ static int num_columns_read = 0;
%type <list> boot_index_params
%type <ielem> boot_index_param
%type <str> boot_ident
-%type <ival> optbootstrap optsharedrelation boot_column_nullness
+%type <ival> optbootstrap optsharedrelation opttemprelation boot_column_nullness
%type <oidval> oidspec optrowtypeoid
%token <str> ID
@@ -106,7 +106,7 @@ static int num_columns_read = 0;
/* All the rest are unreserved, and should be handled in boot_ident! */
%token <kw> OPEN XCLOSE XCREATE INSERT_TUPLE
%token <kw> XDECLARE INDEX ON USING XBUILD INDICES UNIQUE XTOAST
-%token <kw> OBJ_ID XBOOTSTRAP XSHARED_RELATION XROWTYPE_OID
+%token <kw> OBJ_ID XBOOTSTRAP XSHARED_RELATION XTEMP_RELATION XROWTYPE_OID
%token <kw> XFORCE XNOT XNULL
%start TopLevel
@@ -155,13 +155,14 @@ Boot_CloseStmt:
;
Boot_CreateStmt:
- XCREATE boot_ident oidspec optbootstrap optsharedrelation optrowtypeoid LPAREN
+ XCREATE boot_ident oidspec optbootstrap optsharedrelation opttemprelation optrowtypeoid LPAREN
{
do_start();
numattr = 0;
- elog(DEBUG4, "creating%s%s relation %s %u",
+ elog(DEBUG4, "creating%s%s%s relation %s %u",
$4 ? " bootstrap" : "",
$5 ? " shared" : "",
+ $6 ? " global temp" : "",
$2,
$3);
}
@@ -173,6 +174,7 @@ Boot_CreateStmt:
{
TupleDesc tupdesc;
bool shared_relation;
+ bool temp_relation;
bool mapped_relation;
do_start();
@@ -180,6 +182,7 @@ Boot_CreateStmt:
tupdesc = CreateTupleDesc(numattr, attrtypes);
shared_relation = $5;
+ temp_relation = $6;
/*
* The catalogs that use the relation mapper are the
@@ -211,6 +214,8 @@ Boot_CreateStmt:
HEAP_TABLE_AM_OID,
tupdesc,
RELKIND_RELATION,
+ temp_relation ?
+ RELPERSISTENCE_GLOBAL_TEMP :
RELPERSISTENCE_PERMANENT,
shared_relation,
mapped_relation,
@@ -229,13 +234,15 @@ Boot_CreateStmt:
PG_CATALOG_NAMESPACE,
shared_relation ? GLOBALTABLESPACE_OID : 0,
$3,
- $6,
+ $7,
InvalidOid,
BOOTSTRAP_SUPERUSERID,
HEAP_TABLE_AM_OID,
tupdesc,
NIL,
RELKIND_RELATION,
+ temp_relation ?
+ RELPERSISTENCE_GLOBAL_TEMP :
RELPERSISTENCE_PERMANENT,
shared_relation,
mapped_relation,
@@ -433,6 +440,11 @@ optsharedrelation:
| { $$ = 0; }
;
+opttemprelation:
+ XTEMP_RELATION { $$ = 1; }
+ | { $$ = 0; }
+ ;
+
optrowtypeoid:
XROWTYPE_OID oidspec { $$ = $2; }
| { $$ = InvalidOid; }
@@ -492,6 +504,7 @@ boot_ident:
| OBJ_ID { $$ = pstrdup($1); }
| XBOOTSTRAP { $$ = pstrdup($1); }
| XSHARED_RELATION { $$ = pstrdup($1); }
+ | XTEMP_RELATION { $$ = pstrdup($1); }
| XROWTYPE_OID { $$ = pstrdup($1); }
| XFORCE { $$ = pstrdup($1); }
| XNOT { $$ = pstrdup($1); }
diff --git a/src/backend/bootstrap/bootscanner.l b/src/backend/bootstrap/bootscanner.l
index 9674f2795d1..f8c1a671712 100644
--- a/src/backend/bootstrap/bootscanner.l
+++ b/src/backend/bootstrap/bootscanner.l
@@ -82,6 +82,7 @@ create { yylval->kw = "create"; return XCREATE; }
OID { yylval->kw = "OID"; return OBJ_ID; }
bootstrap { yylval->kw = "bootstrap"; return XBOOTSTRAP; }
shared_relation { yylval->kw = "shared_relation"; return XSHARED_RELATION; }
+temp_relation { yylval->kw = "temp_relation"; return XTEMP_RELATION; }
rowtype_oid { yylval->kw = "rowtype_oid"; return XROWTYPE_OID; }
insert { yylval->kw = "insert"; return INSERT_TUPLE; }
diff --git a/src/backend/catalog/Catalog.pm b/src/backend/catalog/Catalog.pm
index 219af5884d9..78e69b3f0d3 100644
--- a/src/backend/catalog/Catalog.pm
+++ b/src/backend/catalog/Catalog.pm
@@ -176,6 +176,8 @@ sub ParseHeader
$catalog{bootstrap} = /BKI_BOOTSTRAP/ ? ' bootstrap' : '';
$catalog{shared_relation} =
/BKI_SHARED_RELATION/ ? ' shared_relation' : '';
+ $catalog{temp_relation} =
+ /BKI_TEMP_RELATION/ ? ' temp_relation' : '';
if (/BKI_ROWTYPE_OID\(\s*
(?<rowtype_oid>\d+),\s*
(?<rowtype_oid_macro>\w+)\s*
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index 0fb085fd8ee..b13293a933e 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -46,6 +46,7 @@ OBJS = \
pg_shdepend.o \
pg_subscription.o \
pg_tablespace.o \
+ pg_temp_class.o \
pg_type.o \
storage.o \
toasting.o
diff --git a/src/backend/catalog/genbki.pl b/src/backend/catalog/genbki.pl
index 86f3135f9c7..9a3f0164962 100644
--- a/src/backend/catalog/genbki.pl
+++ b/src/backend/catalog/genbki.pl
@@ -174,6 +174,7 @@ foreach my $header (@ARGV)
index_oid_macro => $index->{index_oid_macro},
key => $key,
nbuckets => $syscache->{syscache_nbuckets},
+ table_is_temp => $catalogs{$tblname}->{temp_relation} eq "" ? 0 : 1,
};
$syscache_catalogs{$catname} = 1;
@@ -518,6 +519,7 @@ EOM
# .bki CREATE command for this catalog
print $bki "create $catname $catalog->{relation_oid}"
. $catalog->{shared_relation}
+ . $catalog->{temp_relation}
. $catalog->{bootstrap}
. $catalog->{rowtype_oid_clause};
@@ -838,6 +840,46 @@ foreach my $syscache (sort keys %syscaches)
print $syscache_ids_fh "} SysCacheIdentifier;\n";
print $syscache_ids_fh "#define SysCacheSize ($last_syscache + 1)\n\n";
+# Macro to test if a catalog relation is global temporary
+print $syscache_ids_fh "/* Is the specified catalog relation a global temporary relation? */\n";
+print $syscache_ids_fh "#define IsGlobalTempCatalogRelation(relid) \\\n";
+
+my $num_clauses = 0;
+foreach my $catname (sort keys %catalogs)
+{
+ my $catalog = $catalogs{$catname};
+
+ if ($catalog->{temp_relation})
+ {
+ print $syscache_ids_fh $num_clauses == 0 ? "\t(" : " || \\\n\t ";
+ print $syscache_ids_fh "(relid) == $catalog->{relation_oid_macro}";
+ $num_clauses++;
+
+ foreach my $index (@{ $catalog->{indexing} })
+ {
+ print $syscache_ids_fh " || \\\n\t (relid) == $index->{index_oid_macro}";
+ $num_clauses++;
+ }
+ }
+}
+print $syscache_ids_fh $num_clauses == 0 ? "false\n\n" : ")\n\n";
+
+# Macro to test if a syscache's relation is global temporary
+print $syscache_ids_fh "/* Does the specified SysCache use a global temporary relation? */\n";
+print $syscache_ids_fh "#define SysCacheRelationIsGlobalTemp(cacheId) \\\n";
+
+$num_clauses = 0;
+foreach my $syscache (sort keys %syscaches)
+{
+ if ($syscaches{$syscache}{table_is_temp})
+ {
+ print $syscache_ids_fh $num_clauses == 0 ? "\t(" : " || \\\n\t ";
+ print $syscache_ids_fh "(cacheId) == $syscache";
+ $num_clauses++;
+ }
+}
+print $syscache_ids_fh $num_clauses == 0 ? "false\n\n" : ")\n\n";
+
# Closing boilerplate for syscache_ids.h
print $syscache_ids_fh "#endif\t\t\t\t\t\t\t/* SYSCACHE_IDS_H */\n";
diff --git a/src/backend/catalog/global_temp.c b/src/backend/catalog/global_temp.c
index a6a2b1462c7..5f8a7e672eb 100644
--- a/src/backend/catalog/global_temp.c
+++ b/src/backend/catalog/global_temp.c
@@ -61,6 +61,7 @@
#include "access/xact.h"
#include "access/xlogutils.h"
#include "catalog/global_temp.h"
+#include "catalog/pg_temp_class.h"
#include "catalog/storage.h"
#include "commands/sequence.h"
#include "commands/tablecmds.h"
@@ -956,8 +957,17 @@ InvalidateGlobalTempRelation(Oid relid)
void
GlobalTempRelationCreated(Relation relation)
{
- /* Record our use of the relation */
- gtr_record_usage(relation->rd_id);
+ /*
+ * If this is the first time we've used this relation in this session,
+ * insert a pg_temp_class tuple for it, and update the usage hash tables.
+ */
+ if (gtr_local_usage == NULL ||
+ hash_search(gtr_local_usage,
+ &relation->rd_id, HASH_FIND, NULL) == NULL)
+ {
+ InsertPgTempClassTuple(relation);
+ gtr_record_usage(relation->rd_id);
+ }
}
/*
@@ -986,6 +996,9 @@ GlobalTempRelationDropped(Oid relid)
/* Flag the usage entry for eoxact cleanup */
EOXactUsageListAdd(relid);
+
+ /* Delete it's pg_temp_class tuple */
+ DeletePgTempClassTuple(relid);
}
/* Forget any ON COMMIT action for the relation */
@@ -1112,6 +1125,9 @@ AtEOXact_GlobalTempRelation(bool isCommit)
}
}
+ /* Perform any pg_temp_class processing */
+ AtEOXact_PgTempClass(isCommit);
+
/* Now we're out of the transaction and can clear the lists */
eoxact_storage_list_len = 0;
eoxact_storage_list_overflowed = false;
@@ -1183,6 +1199,9 @@ AtEOSubXact_GlobalTempRelation(bool isCommit, SubTransactionId mySubid,
}
}
+ /* Perform any pg_temp_class processing */
+ AtEOSubXact_PgTempClass(isCommit, mySubid, parentSubid);
+
/* Don't reset the lists; we still need more cleanup later */
}
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 330c7756758..f02d51704b4 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -49,6 +49,7 @@
#include "catalog/pg_opclass.h"
#include "catalog/pg_operator.h"
#include "catalog/pg_tablespace.h"
+#include "catalog/pg_temp_class.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
#include "catalog/storage.h"
@@ -3646,6 +3647,23 @@ reindex_index(const ReindexStmt *stmt, Oid indexId,
pg_rusage_init(&ru0);
+ /*
+ * Special case: cannot recreate pg_temp_class_oid_index --- to do so
+ * would require pg_temp_class to be a mapped relation (to avoid use of
+ * the index while rebuilding it) and the relmapper does not support
+ * temporary tables. It might be possible to make this work, but it
+ * doesn't seem worth the effort, so just punt.
+ */
+ if (indexId == TempClassOidIndexId)
+ {
+ ereport(NOTICE,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot reindex temporary system index \"%s\", skipping",
+ get_rel_name(indexId)));
+ RemoveReindexPending(indexId);
+ return;
+ }
+
/*
* Open and lock the parent heap relation. ShareLock is sufficient since
* we only need to be sure no schema or data changes are going on.
diff --git a/src/backend/catalog/meson.build b/src/backend/catalog/meson.build
index 7285ab2dfcf..5386d960b40 100644
--- a/src/backend/catalog/meson.build
+++ b/src/backend/catalog/meson.build
@@ -33,6 +33,7 @@ backend_sources += files(
'pg_shdepend.c',
'pg_subscription.c',
'pg_tablespace.c',
+ 'pg_temp_class.c',
'pg_type.c',
'storage.c',
'toasting.c',
diff --git a/src/backend/catalog/pg_temp_class.c b/src/backend/catalog/pg_temp_class.c
new file mode 100644
index 00000000000..1c68444c875
--- /dev/null
+++ b/src/backend/catalog/pg_temp_class.c
@@ -0,0 +1,713 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_temp_class.c
+ * routines to support manipulation of the pg_temp_class relation
+ *
+ * The pg_temp_class system catalog table is a global temporary table that
+ * stores local overrides to various fields from the pg_class table for the
+ * duration of the current session. Currently, this is only used for
+ * global temporary relations, though in the future, it might also be used
+ * for local temporary relations.
+ *
+ * Tuples are first added to pg_temp_class when global temporary relations
+ * (including pg_temp_class itself) are created or opened for the first
+ * time in a session. This "first time" might be repeated if a previous
+ * "first time" insert was rolled back.
+ *
+ * All tuples to be inserted into pg_temp_class are held in a "pending"
+ * queue, rather than being written out immediately, delaying the point at
+ * which the tuple for pg_temp_class itself is inserted until after the
+ * relation has been fully opened. This pending queue also serves a number
+ * of other useful purposes --- it prevents system catalog updates in what
+ * might otherwise be expected to be read-only contexts (for example, while
+ * opening a global temporary relation at parse time); it allows new
+ * pg_temp_class tuples to be seen in the current command, without having
+ * to issue a CommandCounterIncrement(); and it allows us to delay opening
+ * (and hence creating storage for) pg_temp_class itself, until we have to.
+ * This last point is more than just an optimization --- in a read-only
+ * context, where we know that we haven't written to pg_temp_class, we are
+ * careful to never open it, which would cause it to have local storage
+ * created.
+ *
+ * Pending inserts to pg_temp_class are held in a hash table, and may be
+ * updated or deleted using the functions defined here. They are flushed
+ * to the database at the end of any non-read-only command and when
+ * committing a transaction or subtransaction. Note, however, that the
+ * pending insert entries are kept in the hash table even after they have
+ * been flushed to the database, and they are kept up-to-date (and in sync
+ * with the database, if they have been flushed), until the end of the
+ * transaction, when they are discarded.
+ *
+ * NB: All reads and writes to pg_temp_class by backend code must go
+ * through the functions defined here (though user SQL queries may read it
+ * normally).
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * src/backend/catalog/pg_temp_class.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/htup_details.h"
+#include "access/table.h"
+#include "access/xact.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_temp_class.h"
+#include "miscadmin.h"
+#include "utils/hsearch.h"
+#include "utils/memutils.h"
+#include "utils/syscache.h"
+
+/*
+ * Have we opened and initialized pg_temp_class in this session?
+ */
+static bool pg_temp_class_opened = false;
+
+/*
+ * Subtransaction ID in which pg_temp_class was opened, if it was opened in
+ * the current transaction, else zero.
+ */
+static SubTransactionId pg_temp_class_subid = InvalidSubTransactionId;
+
+/* Cached copy of the pg_temp_class tuple descriptor */
+static TupleDesc pg_temp_class_tupdesc = NULL;
+
+/*
+ * Pending inserts to pg_temp_class.
+ *
+ * Each pending insert entry may be flushed to the database at any point
+ * during the transaction which added it, but the entry is kept up-to-date
+ * until the end of the transaction, allowing tuples to be retrieved from the
+ * hash table, even if the tuple in the database is not visible (if a
+ * CommandCounterIncrement() has not happened).
+ *
+ * A linked list of entries is built, if they are edited in subtransactions,
+ * in order to restore previous versions of the tuple on subrollback.
+ */
+typedef struct PendingInsert
+{
+ Oid relid; /* lookup key: OID the tuple is for */
+ HeapTuple tuple; /* copy of tuple to be inserted */
+ bool flushed; /* has tuple been flushed to the database? */
+ bool deleted; /* has it been deleted? */
+ SubTransactionId subid; /* subxact that added/updated this entry */
+ struct PendingInsert *prev; /* previous version, for subxact rollback */
+} PendingInsert;
+
+static HTAB *pending_inserts = NULL;
+static bool have_inserts_to_flush = false;
+
+/* Memory context for all tuples pending insert */
+static MemoryContext pending_inserts_tupctx = NULL;
+
+/*
+ * open_pg_temp_class
+ *
+ * Open pg_temp_class and make a note of the subtranscation ID, if this is
+ * the first time opening it.
+ */
+static Relation
+open_pg_temp_class(LOCKMODE lockmode)
+{
+ Relation pg_temp_class;
+
+ pg_temp_class = table_open(TempRelationRelationId, lockmode);
+ if (!pg_temp_class_opened)
+ {
+ pg_temp_class_opened = true;
+ pg_temp_class_subid = GetCurrentSubTransactionId();
+ }
+ return pg_temp_class;
+}
+
+/*
+ * init_pending_inserts_hashtable
+ *
+ * Initialize the pending inserts hashtable, if not already done.
+ */
+static void
+init_pending_inserts_hashtable(void)
+{
+ if (pending_inserts == NULL)
+ {
+ HASHCTL ctl;
+
+ /* Create the hash table */
+ ctl.keysize = sizeof(Oid);
+ ctl.entrysize = sizeof(PendingInsert);
+
+ pending_inserts = hash_create("Pending pg_temp_class inserts",
+ 128, &ctl, HASH_ELEM | HASH_BLOBS);
+
+ /* Create a separate memory context for all tuples in it */
+ pending_inserts_tupctx = AllocSetContextCreate(TopMemoryContext,
+ "Pending pg_temp_class tuples",
+ ALLOCSET_DEFAULT_SIZES);
+ }
+}
+
+/*
+ * get_pg_temp_class_tupdesc
+ *
+ * Returns the tuple descriptor for pg_temp_class.
+ */
+static TupleDesc
+get_pg_temp_class_tupdesc(void)
+{
+ /* Build the tuple descriptor the first time through */
+ if (pg_temp_class_tupdesc == NULL)
+ {
+ MemoryContext oldcontext;
+ TupleDesc tupdesc;
+
+ oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+
+ tupdesc = CreateTemplateTupleDesc(Natts_pg_temp_class);
+ TupleDescInitEntry(tupdesc,
+ (AttrNumber) Anum_pg_temp_class_oid,
+ "oid", OIDOID, -1, 0);
+ TupleDescInitEntry(tupdesc,
+ (AttrNumber) Anum_pg_temp_class_relfilenode,
+ "relfilenode", OIDOID, -1, 0);
+ TupleDescInitEntry(tupdesc,
+ (AttrNumber) Anum_pg_temp_class_reltablespace,
+ "reltablespace", OIDOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /* Cache it for all future use */
+ pg_temp_class_tupdesc = tupdesc;
+ }
+ return pg_temp_class_tupdesc;
+}
+
+/*
+ * heap_form_pg_temp_class_tuple
+ *
+ * Create a pg_temp_class tuple for the specified relation. All tuple data
+ * is taken from rel->rd_rel.
+ */
+static HeapTuple
+heap_form_pg_temp_class_tuple(Relation rel)
+{
+ Form_pg_class form = rel->rd_rel;
+ Datum values[Natts_pg_temp_class];
+ bool nulls[Natts_pg_temp_class] = {0};
+
+ values[Anum_pg_temp_class_oid - 1] = ObjectIdGetDatum(RelationGetRelid(rel));
+ values[Anum_pg_temp_class_relfilenode - 1] = ObjectIdGetDatum(form->relfilenode);
+ values[Anum_pg_temp_class_reltablespace - 1] = ObjectIdGetDatum(form->reltablespace);
+
+ return heap_form_tuple(get_pg_temp_class_tupdesc(), values, nulls);
+}
+
+/*
+ * prepare_pending_insert_for_edit
+ *
+ * Called before making any change to a pending insert entry. If the entry
+ * was created or last updated in a previous subtransaction, make a copy and
+ * save it, for subtransaction rollback.
+ */
+static void
+prepare_pending_insert_for_edit(PendingInsert *entry)
+{
+ SubTransactionId mySubid = GetCurrentSubTransactionId();
+
+ if (entry->subid != mySubid)
+ {
+ PendingInsert *save_entry;
+ MemoryContext oldcontext;
+
+ oldcontext = MemoryContextSwitchTo(pending_inserts_tupctx);
+ save_entry = palloc_object(PendingInsert);
+ save_entry->relid = entry->relid;
+ save_entry->tuple = heap_copytuple(entry->tuple);
+ save_entry->flushed = entry->flushed;
+ save_entry->deleted = entry->deleted;
+ save_entry->subid = entry->subid;
+ save_entry->prev = entry->prev;
+ MemoryContextSwitchTo(oldcontext);
+
+ entry->subid = mySubid;
+ entry->prev = save_entry;
+ }
+}
+
+/*
+ * flush_pending_pg_temp_class_inserts
+ *
+ * Flush any pending inserts to pg_temp_class.
+ */
+static void
+flush_pending_pg_temp_class_inserts(void)
+{
+ Relation pg_temp_class;
+ CatalogIndexState indstate;
+ HASH_SEQ_STATUS status;
+ PendingInsert *entry;
+
+ pg_temp_class = open_pg_temp_class(RowExclusiveLock);
+
+ /*
+ * Flush all pending inserts, not already flushed or deleted.
+ */
+ indstate = CatalogOpenIndexes(pg_temp_class);
+ hash_seq_init(&status, pending_inserts);
+ while ((entry = hash_seq_search(&status)) != NULL)
+ {
+ if (!entry->flushed && !entry->deleted)
+ {
+ CatalogTupleInsertWithInfo(pg_temp_class, entry->tuple, indstate);
+
+ /* Update the entry, saving a copy for rollback, if necessary */
+ prepare_pending_insert_for_edit(entry);
+ entry->flushed = true;
+ }
+ }
+ CatalogCloseIndexes(indstate);
+
+ table_close(pg_temp_class, RowExclusiveLock);
+
+ have_inserts_to_flush = false;
+}
+
+/*
+ * discard_pending_pg_temp_class_inserts
+ *
+ * Discard any pending inserts to pg_temp_class.
+ */
+static void
+discard_pending_pg_temp_class_inserts(void)
+{
+ /* Just blow away the hash table and tuple memory context */
+ hash_destroy(pending_inserts);
+ MemoryContextDelete(pending_inserts_tupctx);
+
+ pending_inserts = NULL;
+ pending_inserts_tupctx = NULL;
+ have_inserts_to_flush = false;
+}
+
+/*
+ * GetPgTempClassTuple
+ *
+ * Get the pg_temp_class tuple for a global temporary relation.
+ *
+ * Returns NULL if the tuple could not be found. Otherwise, the tuple
+ * returned should be freed with heap_freetuple().
+ */
+HeapTuple
+GetPgTempClassTuple(Oid relid)
+{
+ PendingInsert *entry = NULL;
+
+ /* If there is a pending insert for this relation, return that */
+ if (pending_inserts != NULL)
+ {
+ entry = hash_search(pending_inserts, &relid, HASH_FIND, NULL);
+ if (entry != NULL)
+ return entry->deleted ? NULL : heap_copytuple(entry->tuple);
+ }
+
+ /* If we haven't opened pg_temp_class yet, it must be empty */
+ if (!pg_temp_class_opened)
+ return NULL;
+
+ /* Otherwise, look for the tuple in the database */
+ return SearchSysCacheCopy1(TEMPRELOID, ObjectIdGetDatum(relid));
+}
+
+/*
+ * InsertPgTempClassTuple
+ *
+ * Insert a new pg_temp_class tuple for a global temporary relation.
+ *
+ * This is called when a global temporary relation is created or accessed for
+ * the first time in a session. All tuple data is taken from rel->rd_rel.
+ *
+ * Note: The new tuple is not written to the database unless and until
+ * CommandCounterIncrement() is called for a non-read-only command, or the
+ * (sub)transaction is committed. However, the new tuple *is* visible to all
+ * the functions defined here.
+ */
+void
+InsertPgTempClassTuple(Relation rel)
+{
+ Oid relid = RelationGetRelid(rel);
+ PendingInsert *entry;
+ bool found;
+ MemoryContext oldcontext;
+
+ /*
+ * Add a new tuple for the relation to the pending inserts hash table,
+ * taking care to allocate the tuple in the long-term memory context for
+ * pending insert tuples.
+ */
+ init_pending_inserts_hashtable();
+
+ entry = hash_search(pending_inserts, &relid, HASH_ENTER, &found);
+ if (found)
+ /* Should never try to re-insert the same relid */
+ elog(ERROR, "pg_temp_class tuple for relation %u already exists", relid);
+
+ oldcontext = MemoryContextSwitchTo(pending_inserts_tupctx);
+ entry->tuple = heap_form_pg_temp_class_tuple(rel);
+ entry->flushed = false;
+ entry->deleted = false;
+ entry->subid = GetCurrentSubTransactionId();
+ entry->prev = NULL;
+ MemoryContextSwitchTo(oldcontext);
+
+ have_inserts_to_flush = true;
+}
+
+/*
+ * UpdatePgTempClassTuple
+ *
+ * Update the pg_temp_class tuple for a global temporary relation.
+ */
+void
+UpdatePgTempClassTuple(Oid relid, HeapTuple newtuple)
+{
+ Relation pg_temp_class;
+ HeapTuple oldtuple;
+
+ /* Is there a pending insert for this relation? */
+ if (pending_inserts != NULL)
+ {
+ PendingInsert *entry;
+
+ entry = hash_search(pending_inserts, &relid, HASH_FIND, NULL);
+ if (entry != NULL)
+ {
+ Form_pg_temp_class old_form;
+ Form_pg_temp_class new_form;
+
+ /* Should not have been deleted */
+ if (entry->deleted)
+ elog(ERROR,
+ "pending insert for global temp relation %u was deleted",
+ relid);
+
+ /* Update the entry, saving a copy for rollback, if necessary */
+ prepare_pending_insert_for_edit(entry);
+ old_form = (Form_pg_temp_class) GETSTRUCT(entry->tuple);
+ new_form = (Form_pg_temp_class) GETSTRUCT(newtuple);
+ COPY_PG_TEMP_CLASS_ATTRS(new_form, old_form);
+
+ /*
+ * If it has not yet been flushed to the database, do so now. This
+ * is important, because the caller might be relying on a relcache
+ * invalidation being triggered.
+ */
+ if (!entry->flushed)
+ {
+ pg_temp_class = open_pg_temp_class(RowExclusiveLock);
+ CatalogTupleInsert(pg_temp_class, newtuple);
+ table_close(pg_temp_class, RowExclusiveLock);
+ entry->flushed = true;
+ return;
+ }
+ }
+ }
+
+ /* Update the tuple in the database */
+ pg_temp_class = open_pg_temp_class(RowExclusiveLock);
+
+ oldtuple = SearchSysCache1(TEMPRELOID, ObjectIdGetDatum(relid));
+ if (!HeapTupleIsValid(oldtuple))
+ elog(ERROR, "cache lookup failed for global temp relation %u", relid);
+
+ CatalogTupleUpdate(pg_temp_class, &oldtuple->t_self, newtuple);
+ ReleaseSysCache(oldtuple);
+
+ table_close(pg_temp_class, RowExclusiveLock);
+}
+
+/*
+ * DeletePgTempClassTuple
+ *
+ * Delete the pg_temp_class tuple for a global temporary relation.
+ */
+void
+DeletePgTempClassTuple(Oid relid)
+{
+ Relation pg_temp_class;
+ HeapTuple oldtuple;
+
+ /* Is there a pending insert for this relation? */
+ if (pending_inserts != NULL)
+ {
+ PendingInsert *entry;
+
+ entry = hash_search(pending_inserts, &relid, HASH_FIND, NULL);
+ if (entry != NULL)
+ {
+ /* Should not have already been deleted */
+ if (entry->deleted)
+ elog(ERROR,
+ "pending insert for global temp relation %u already deleted",
+ relid);
+
+ /* Update the entry, saving a copy for rollback, if necessary */
+ prepare_pending_insert_for_edit(entry);
+ entry->deleted = true;
+
+ /*
+ * If it has been flushed to the database, need to delete it there
+ * too. Otherwise, we're done.
+ */
+ if (!entry->flushed)
+ return;
+ }
+ }
+
+ /* Delete the tuple from the database */
+ pg_temp_class = open_pg_temp_class(RowExclusiveLock);
+
+ oldtuple = SearchSysCache1(TEMPRELOID, ObjectIdGetDatum(relid));
+ if (!HeapTupleIsValid(oldtuple))
+ elog(ERROR, "cache lookup failed for global temp relation %u", relid);
+
+ CatalogTupleDelete(pg_temp_class, &oldtuple->t_self);
+ ReleaseSysCache(oldtuple);
+
+ table_close(pg_temp_class, RowExclusiveLock);
+}
+
+/*
+ * GetPgClassAndPgTempClassTuples
+ *
+ * Get the pg_class tuple for a relation, and if it's a global temporary
+ * relation, also get the corresponding pg_temp_class tuple.
+ *
+ * If lock_tuple is true, the pg_class tuple will be locked, but not the
+ * pg_temp_class tuple.
+ *
+ * If check_temp is true, an error will be raised if a global temporary
+ * relation's pg_temp_class tuple is not found. After a global temporary
+ * relation has been opened, its pg_temp_class tuple should always exist.
+ *
+ * Returns NULL if the pg_class tuple could not be found. Otherwise, the
+ * tuple(s) returned should be freed with heap_freetuple().
+ */
+HeapTuple
+GetPgClassAndPgTempClassTuples(Oid relid, bool lock_tuple,
+ HeapTuple *temp_tuple, bool check_temp)
+{
+ HeapTuple tuple;
+
+ /* Get a copy of the pg_class tuple */
+ if (lock_tuple)
+ tuple = SearchSysCacheLockedCopy1(RELOID, ObjectIdGetDatum(relid));
+ else
+ tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+
+ if (HeapTupleIsValid(tuple) &&
+ ((Form_pg_class) GETSTRUCT(tuple))->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+ {
+ /* Get the pg_temp_class tuple, and check it exists, if requested */
+ *temp_tuple = GetPgTempClassTuple(relid);
+ if (check_temp && !HeapTupleIsValid(*temp_tuple))
+ elog(ERROR, "cache lookup failed for global temp relation %u", relid);
+ }
+ else
+ *temp_tuple = NULL;
+
+ return tuple;
+}
+
+/*
+ * GetEffectivePgClassTuple
+ *
+ * Get the effective pg_class tuple for a relation.
+ *
+ * This will fetch the pg_class tuple for the relation and then, if it's a
+ * global temporary relation, fetch the corresponding pg_temp_class tuple and
+ * use the values in it to override the corresponding values in the pg_class
+ * tuple. Thus, the result represents the effective state of the relation in
+ * this session.
+ *
+ * For a global temporary relation that has not yet been opened in this
+ * session, there will be no pg_temp_class tuple, and the pg_class tuple will
+ * be returned unchanged.
+ *
+ * Returns NULL if the pg_class tuple could not be found. Otherwise, the
+ * tuple returned should be freed with heap_freetuple().
+ */
+HeapTuple
+GetEffectivePgClassTuple(Oid relid)
+{
+ HeapTuple tuple;
+ HeapTuple temp_tuple;
+ Form_pg_class classform;
+ Form_pg_temp_class temp_classform;
+
+ /*
+ * Get the pg_class and pg_temp_class tuples. If we have the latter, use
+ * it to update the former.
+ */
+ tuple = GetPgClassAndPgTempClassTuples(relid, false, &temp_tuple, false);
+
+ if (HeapTupleIsValid(tuple) && HeapTupleIsValid(temp_tuple))
+ {
+ classform = (Form_pg_class) GETSTRUCT(tuple);
+ temp_classform = (Form_pg_temp_class) GETSTRUCT(temp_tuple);
+ COPY_PG_TEMP_CLASS_ATTRS(temp_classform, classform);
+ }
+ return tuple;
+}
+
+/*
+ * PreCCI_PgTempClass
+ *
+ * Pre-end-of-command processing; flush out any pending inserts.
+ */
+void
+PreCCI_PgTempClass(void)
+{
+ if (have_inserts_to_flush)
+ flush_pending_pg_temp_class_inserts();
+}
+
+/*
+ * PreCommit_PgTempClass
+ *
+ * Pre-commit processing; flush out any pending inserts.
+ */
+void
+PreCommit_PgTempClass(void)
+{
+ if (have_inserts_to_flush)
+ flush_pending_pg_temp_class_inserts();
+}
+
+/*
+ * PreSubCommit_PgTempClass
+ *
+ * Pre-subcommit processing; flush out any pending inserts.
+ */
+void
+PreSubCommit_PgTempClass(void)
+{
+ if (have_inserts_to_flush)
+ flush_pending_pg_temp_class_inserts();
+}
+
+/*
+ * AtEOXact_PgTempClass
+ *
+ * Main-transaction commit or abort processing.
+ */
+void
+AtEOXact_PgTempClass(bool isCommit)
+{
+ /*
+ * If pg_temp_class was first opened and initialized in this transaction,
+ * rollback undoes that, and it is no longer initialized.
+ */
+ if (!isCommit && pg_temp_class_subid != InvalidSubTransactionId)
+ pg_temp_class_opened = false;
+ pg_temp_class_subid = InvalidSubTransactionId;
+
+ /*
+ * Blow away the pending inserts hash table. On commit, there should be
+ * no remaining inserts to flush, but on rollback, there may be.
+ */
+ Assert(!(isCommit && have_inserts_to_flush));
+ if (pending_inserts != NULL)
+ discard_pending_pg_temp_class_inserts();
+}
+
+/*
+ * AtEOSubXact_PendingInsert
+ *
+ * Sub-transaction commit or abort processing for a single pending insert.
+ */
+static void
+AtEOSubXact_PendingInsert(PendingInsert *entry, bool isCommit,
+ SubTransactionId mySubid,
+ SubTransactionId parentSubid)
+{
+ /*
+ * Was the pending insert entry created or last updated in the current
+ * subtransaction?
+ *
+ * During subcommit, mark it as created/last updated in the parent,
+ * instead, and discard any saved copy from the parent's level.
+ *
+ * During subrollback, restore the most recent saved copy (from the
+ * parent's level or lower), or remove the entry, if there is no previous
+ * saved copy.
+ */
+ if (entry->subid == mySubid)
+ {
+ if (isCommit)
+ {
+ entry->subid = parentSubid;
+ if (entry->prev != NULL && entry->prev->subid == parentSubid)
+ entry->prev = entry->prev->prev;
+ }
+ else if (entry->prev != NULL)
+ {
+ Assert(entry->relid == entry->prev->relid);
+ entry->tuple = entry->prev->tuple;
+ entry->flushed = entry->prev->flushed;
+ entry->deleted = entry->prev->deleted;
+ entry->subid = entry->prev->subid;
+ entry->prev = entry->prev->prev;
+ }
+ else
+ {
+ hash_search(pending_inserts, &entry->relid, HASH_REMOVE, NULL);
+ }
+ }
+}
+
+/*
+ * AtEOSubXact_PgTempClass
+ *
+ * Sub-transaction commit or abort processing.
+ */
+void
+AtEOSubXact_PgTempClass(bool isCommit, SubTransactionId mySubid,
+ SubTransactionId parentSubid)
+{
+ /*
+ * Was pg_temp_class first opened and initialized in the current
+ * subtransaction?
+ *
+ * During subcommit, mark it as belonging to the parent. Otherwise, it is
+ * no longer initialized.
+ */
+ if (pg_temp_class_subid == mySubid)
+ {
+ if (isCommit)
+ pg_temp_class_subid = parentSubid;
+ else
+ {
+ pg_temp_class_opened = false;
+ pg_temp_class_subid = InvalidSubTransactionId;
+ }
+ }
+
+ /*
+ * Tidy up any pending inserts.
+ */
+ if (pending_inserts != NULL)
+ {
+ HASH_SEQ_STATUS status;
+ PendingInsert *entry;
+
+ hash_seq_init(&status, pending_inserts);
+ while ((entry = hash_seq_search(&status)) != NULL)
+ {
+ AtEOSubXact_PendingInsert(entry, isCommit, mySubid, parentSubid);
+ }
+ }
+}
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 7a85edb9317..81bc7dcc312 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -52,6 +52,7 @@
#include "catalog/pg_attrdef.h"
#include "catalog/pg_constraint.h"
#include "catalog/pg_inherits.h"
+#include "catalog/pg_temp_class.h"
#include "catalog/toasting.h"
#include "commands/defrem.h"
#include "commands/progress.h"
@@ -1338,11 +1339,17 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
* are only RECENTLY_DEAD. Then we'd fail while trying to copy those
* tuples.
*
- * We don't need to open the toast relation here, just lock it. The lock
- * will be held till end of transaction.
+ * If it's a global temporary relation, we must open it, to ensure that it
+ * is properly initialized, so we might as well do that for all relation
+ * types. Keep the relation locked till end of transaction.
*/
if (OldHeap->rd_rel->reltoastrelid)
- LockRelationOid(OldHeap->rd_rel->reltoastrelid, lmode);
+ {
+ Relation toastRel;
+
+ toastRel = relation_open(OldHeap->rd_rel->reltoastrelid, lmode);
+ relation_close(toastRel, NoLock);
+ }
/*
* If both tables have TOAST tables, perform toast swap by content. It is
@@ -1546,9 +1553,13 @@ swap_relation_files(Oid r1, Oid r2, bool target_is_pg_class,
{
Relation relRelation;
HeapTuple reltup1,
- reltup2;
+ reltup2,
+ temp_reltup1,
+ temp_reltup2;
Form_pg_class relform1,
relform2;
+ Form_pg_temp_class temp_relform1,
+ temp_relform2;
RelFileNumber relfilenumber1,
relfilenumber2;
RelFileNumber swaptemp;
@@ -1556,21 +1567,28 @@ swap_relation_files(Oid r1, Oid r2, bool target_is_pg_class,
Oid relam1,
relam2;
- /* We need writable copies of both pg_class tuples. */
+ /*
+ * We need writable copies of both pg_class tuples, and the corresponding
+ * pg_temp_class tuples, if they're global temporary relations.
+ */
relRelation = table_open(RelationRelationId, RowExclusiveLock);
- reltup1 = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(r1));
+ reltup1 = GetPgClassAndPgTempClassTuples(r1, false, &temp_reltup1, true);
if (!HeapTupleIsValid(reltup1))
elog(ERROR, "cache lookup failed for relation %u", r1);
relform1 = (Form_pg_class) GETSTRUCT(reltup1);
+ temp_relform1 = (Form_pg_temp_class) GETSTRUCT_SAFE(temp_reltup1);
- reltup2 = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(r2));
+ reltup2 = GetPgClassAndPgTempClassTuples(r2, false, &temp_reltup2, true);
if (!HeapTupleIsValid(reltup2))
elog(ERROR, "cache lookup failed for relation %u", r2);
relform2 = (Form_pg_class) GETSTRUCT(reltup2);
+ temp_relform2 = (Form_pg_temp_class) GETSTRUCT_SAFE(temp_reltup2);
+
+ Assert(HeapTupleIsValid(temp_reltup1) == HeapTupleIsValid(temp_reltup2));
- relfilenumber1 = relform1->relfilenode;
- relfilenumber2 = relform2->relfilenode;
+ relfilenumber1 = GetEffective_relfilenode(relform1, temp_relform1);
+ relfilenumber2 = GetEffective_relfilenode(relform2, temp_relform2);
relam1 = relform1->relam;
relam2 = relform2->relam;
@@ -1583,13 +1601,14 @@ swap_relation_files(Oid r1, Oid r2, bool target_is_pg_class,
*/
Assert(!target_is_pg_class);
- swaptemp = relform1->relfilenode;
- relform1->relfilenode = relform2->relfilenode;
- relform2->relfilenode = swaptemp;
+ SetEffective_relfilenode(relform1, temp_relform1, relfilenumber2);
+ SetEffective_relfilenode(relform2, temp_relform2, relfilenumber1);
- swaptemp = relform1->reltablespace;
- relform1->reltablespace = relform2->reltablespace;
- relform2->reltablespace = swaptemp;
+ swaptemp = GetEffective_reltablespace(relform1, temp_relform1);
+ SetEffective_reltablespace(relform1, temp_relform1,
+ GetEffective_reltablespace(relform2,
+ temp_relform2));
+ SetEffective_reltablespace(relform2, temp_relform2, swaptemp);
swaptemp = relform1->relam;
relform1->relam = relform2->relam;
@@ -1758,6 +1777,17 @@ swap_relation_files(Oid r1, Oid r2, bool target_is_pg_class,
CacheInvalidateRelcacheByTuple(reltup2);
}
+ /*
+ * For global temporary relations, update the tuples in pg_temp_class.
+ */
+ if (HeapTupleIsValid(temp_reltup1) && HeapTupleIsValid(temp_reltup2))
+ {
+ UpdatePgTempClassTuple(r1, temp_reltup1);
+ UpdatePgTempClassTuple(r2, temp_reltup2);
+ heap_freetuple(temp_reltup1);
+ heap_freetuple(temp_reltup2);
+ }
+
/*
* Now that pg_class has been updated with its relevant information for
* the swap, update the dependency of the relations to point to their new
@@ -2179,6 +2209,16 @@ get_tables_to_repack(RepackCommand cmd, bool usingindex, MemoryContext permcxt)
index = (Form_pg_index) GETSTRUCT(tuple);
+ /*
+ * Silently skip pg_temp_class --- it does not support relfilenode
+ * changes, because that would require it to be a mapped relation,
+ * and the relmapper does not support temporary tables. It might
+ * be possible to make this work, but it doesn't seem worth the
+ * effort.
+ */
+ if (index->indrelid == TempRelationRelationId)
+ continue;
+
/*
* Try to obtain a light lock on the index's table, to ensure it
* doesn't go away while we collect the list. If we cannot, just
@@ -2238,6 +2278,16 @@ get_tables_to_repack(RepackCommand cmd, bool usingindex, MemoryContext permcxt)
class = (Form_pg_class) GETSTRUCT(tuple);
+ /*
+ * Silently skip pg_temp_class --- it does not support relfilenode
+ * changes, because that would require it to be a mapped relation,
+ * and the relmapper does not support temporary tables. It might
+ * be possible to make this work, but it doesn't seem worth the
+ * effort.
+ */
+ if (class->oid == TempRelationRelationId)
+ continue;
+
/*
* Try to obtain a light lock on the table, to ensure it doesn't
* go away while we collect the list. If we cannot, just
@@ -2430,6 +2480,20 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
errmsg("cannot execute %s on temporary tables of other sessions",
RepackCommandAsString(stmt->command)));
+ /*
+ * Reject clustering pg_temp_class --- it does not support relfilenode
+ * changes, because that would require it to be a mapped relation, and the
+ * relmapper does not support temporary tables. It might be possible to
+ * make this work, but it doesn't seem worth the effort.
+ */
+ if (tableOid == TempRelationRelationId)
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ /*- translator: first %s is name of a SQL command, eg. REPACK */
+ errmsg("cannot execute %s on temporary system catalog \"%s\"",
+ RepackCommandAsString(stmt->command),
+ RelationGetRelationName(rel)));
+
/*
* For partitioned tables, let caller handle this. Otherwise, process it
* here and we're done.
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index ba139560f90..d3394fffd61 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -54,6 +54,7 @@
#include "catalog/pg_rewrite.h"
#include "catalog/pg_statistic_ext.h"
#include "catalog/pg_tablespace.h"
+#include "catalog/pg_temp_class.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
#include "catalog/storage.h"
@@ -3825,7 +3826,8 @@ CheckRelationTableSpaceMove(Relation rel, Oid newTableSpaceId)
/*
* SetRelationTableSpace
- * Set new reltablespace and relfilenumber in pg_class entry.
+ * Set new reltablespace and relfilenumber in pg_class (and/or
+ * pg_temp_class for a global temporary relation).
*
* newTableSpaceId is the new tablespace for the relation, and
* newRelFilenumber its new filenumber. If newRelFilenumber is
@@ -3845,33 +3847,55 @@ SetRelationTableSpace(Relation rel,
{
Relation pg_class;
HeapTuple tuple;
+ HeapTuple temp_tuple;
ItemPointerData otid;
Form_pg_class rd_rel;
+ Form_pg_temp_class temp_rd_rel;
Oid reloid = RelationGetRelid(rel);
Assert(CheckRelationTableSpaceMove(rel, newTableSpaceId));
- /* Get a modifiable copy of the relation's pg_class row. */
+ /*
+ * Get a modifiable copy of the relation's pg_class row and, for a global
+ * temporary relation, its pg_temp_class row.
+ */
pg_class = table_open(RelationRelationId, RowExclusiveLock);
- tuple = SearchSysCacheLockedCopy1(RELOID, ObjectIdGetDatum(reloid));
+ tuple = GetPgClassAndPgTempClassTuples(reloid, true, &temp_tuple, true);
if (!HeapTupleIsValid(tuple))
elog(ERROR, "cache lookup failed for relation %u", reloid);
otid = tuple->t_self;
rd_rel = (Form_pg_class) GETSTRUCT(tuple);
+ temp_rd_rel = (Form_pg_temp_class) GETSTRUCT_SAFE(temp_tuple);
- /* Update the pg_class row. */
- rd_rel->reltablespace = (newTableSpaceId == MyDatabaseTableSpace) ?
- InvalidOid : newTableSpaceId;
+ /*
+ * Update the pg_class and/or pg_temp_class rows. For global temporary
+ * relations, the new tablespace is set in both pg_class and pg_temp_class
+ * so that the change is made in the current session and for all future
+ * sessions. Other current sessions using the relation are not affected.
+ */
+ SetEffective_reltablespace(rd_rel, temp_rd_rel,
+ newTableSpaceId == MyDatabaseTableSpace ?
+ InvalidOid : newTableSpaceId);
if (RelFileNumberIsValid(newRelFilenumber))
- rd_rel->relfilenode = newRelFilenumber;
+ SetEffective_relfilenode(rd_rel, temp_rd_rel, newRelFilenumber);
+
CatalogTupleUpdate(pg_class, &otid, tuple);
UnlockTuple(pg_class, &otid, InplaceUpdateTupleLock);
+ if (HeapTupleIsValid(temp_tuple))
+ {
+ UpdatePgTempClassTuple(reloid, temp_tuple);
+ heap_freetuple(temp_tuple);
+ }
/*
* Record dependency on tablespace. This is required for relations that
* have no physical storage, and for global temporary relations whose
- * physical storage is temporary.
+ * physical storage is temporary. Note that a global temporary relation
+ * being used in another session will not see the change in tablespace,
+ * and will continue to use the original tablespace until the session
+ * exits, but its local temporary storage will prevent the old tablespace
+ * from being dropped until then.
*/
if (!RELKIND_HAS_STORAGE(rel->rd_rel->relkind) ||
RELATION_IS_GLOBAL_TEMP(rel))
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 38539a6fd3d..4e15028217c 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -37,6 +37,7 @@
#include "catalog/namespace.h"
#include "catalog/pg_database.h"
#include "catalog/pg_inherits.h"
+#include "catalog/pg_temp_class.h"
#include "commands/async.h"
#include "commands/defrem.h"
#include "commands/progress.h"
@@ -2155,6 +2156,23 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
return false;
}
+ /*
+ * VACUUM FULL on pg_temp_class is not supported --- it does not support
+ * relfilenode changes, because that would require it to be a mapped
+ * relation, and the relmapper does not support temporary tables. It might
+ * be possible to make this work, but it doesn't seem worth the effort, so
+ * do an "aggressive" VACUUM FREEZE instead.
+ */
+ if (relid == TempRelationRelationId && (params.options & VACOPT_FULL))
+ {
+ params.options &= ~VACOPT_FULL;
+ params.options |= VACOPT_FREEZE;
+ params.freeze_min_age = 0;
+ params.freeze_table_age = 0;
+ params.multixact_freeze_min_age = 0;
+ params.multixact_freeze_table_age = 0;
+ }
+
/*
* Silently ignore partitioned tables as there is no work to be done. The
* useful work is on their child partitions, which have been queued up for
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 2b36d6c7f98..a4afbccc3e6 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -40,6 +40,7 @@
#include "catalog/pg_opclass.h"
#include "catalog/pg_operator.h"
#include "catalog/pg_statistic_ext.h"
+#include "catalog/pg_temp_class.h"
#include "catalog/pg_type.h"
#include "commands/comment.h"
#include "commands/defrem.h"
@@ -1714,10 +1715,10 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx,
*constraintOid = InvalidOid;
/*
- * Fetch pg_class tuple of source index. We can't use the copy in the
- * relcache entry because it doesn't include optional fields.
+ * Fetch effective pg_class tuple of source index. We can't use the copy
+ * in the relcache entry because it doesn't include optional fields.
*/
- ht_idxrel = SearchSysCache1(RELOID, ObjectIdGetDatum(source_relid));
+ ht_idxrel = GetEffectivePgClassTuple(source_relid);
if (!HeapTupleIsValid(ht_idxrel))
elog(ERROR, "cache lookup failed for relation %u", source_relid);
idxrelrec = (Form_pg_class) GETSTRUCT(ht_idxrel);
@@ -2032,7 +2033,7 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx,
}
/* Clean up */
- ReleaseSysCache(ht_idxrel);
+ heap_freetuple(ht_idxrel);
ReleaseSysCache(ht_am);
return index;
diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c
index 38bae7b15d2..24c46734414 100644
--- a/src/backend/utils/activity/pgstat_io.c
+++ b/src/backend/utils/activity/pgstat_io.c
@@ -421,18 +421,17 @@ pgstat_tracks_io_object(BackendType bktype, IOObject io_object,
return false;
/*
- * In core Postgres, only regular backends and WAL Sender processes
- * executing queries will use local buffers and operate on temporary
- * relations. Parallel workers will not use local buffers (see
+ * In core Postgres, only initdb, regular backends, and WAL Sender
+ * processes executing queries will use local buffers and operate on
+ * temporary relations. Parallel workers will not use local buffers (see
* InitLocalBuffers()); however, extensions leveraging background workers
* have no such limitation, so track IO on IOOBJECT_TEMP_RELATION for
* BackendType B_BG_WORKER.
*/
no_temp_rel = bktype == B_AUTOVAC_LAUNCHER || bktype == B_BG_WRITER ||
bktype == B_CHECKPOINTER || bktype == B_AUTOVAC_WORKER ||
- bktype == B_STANDALONE_BACKEND || bktype == B_STARTUP ||
- bktype == B_WAL_SUMMARIZER || bktype == B_WAL_WRITER ||
- bktype == B_WAL_RECEIVER;
+ bktype == B_STARTUP || bktype == B_WAL_SUMMARIZER ||
+ bktype == B_WAL_WRITER || bktype == B_WAL_RECEIVER;
if (no_temp_rel && io_context == IOCONTEXT_NORMAL &&
io_object == IOOBJECT_TEMP_RELATION)
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index e09ea8fe220..df1961accb2 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/pg_temp_class.h"
#include "commands/tablespace.h"
#include "miscadmin.h"
#include "storage/fd.h"
@@ -901,7 +902,7 @@ pg_relation_filenode(PG_FUNCTION_ARGS)
HeapTuple tuple;
Form_pg_class relform;
- tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+ tuple = GetEffectivePgClassTuple(relid);
if (!HeapTupleIsValid(tuple))
PG_RETURN_NULL();
relform = (Form_pg_class) GETSTRUCT(tuple);
@@ -920,7 +921,7 @@ pg_relation_filenode(PG_FUNCTION_ARGS)
result = InvalidRelFileNumber;
}
- ReleaseSysCache(tuple);
+ heap_freetuple(tuple);
if (!RelFileNumberIsValid(result))
PG_RETURN_NULL();
@@ -978,7 +979,7 @@ pg_relation_filepath(PG_FUNCTION_ARGS)
ProcNumber backend;
RelPathStr path;
- tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+ tuple = GetEffectivePgClassTuple(relid);
if (!HeapTupleIsValid(tuple))
PG_RETURN_NULL();
relform = (Form_pg_class) GETSTRUCT(tuple);
@@ -1011,7 +1012,7 @@ pg_relation_filepath(PG_FUNCTION_ARGS)
if (!RelFileNumberIsValid(rlocator.relNumber))
{
- ReleaseSysCache(tuple);
+ heap_freetuple(tuple);
PG_RETURN_NULL();
}
@@ -1041,7 +1042,7 @@ pg_relation_filepath(PG_FUNCTION_ARGS)
break;
}
- ReleaseSysCache(tuple);
+ heap_freetuple(tuple);
path = relpathbackend(rlocator, backend, MAIN_FORKNUM);
diff --git a/src/backend/utils/cache/inval.c b/src/backend/utils/cache/inval.c
index 63dc36d4d91..19027e57496 100644
--- a/src/backend/utils/cache/inval.c
+++ b/src/backend/utils/cache/inval.c
@@ -51,10 +51,11 @@
* PrepareToInvalidateCacheTuple() routine provides the knowledge of which
* catcaches may need invalidation for a given tuple.
*
- * Also, whenever we see an operation on a pg_class, pg_attribute, or
- * pg_index tuple, we register a relcache flush operation for the relation
- * described by that tuple (as specified in CacheInvalidateHeapTuple()).
- * Likewise for pg_constraint tuples for foreign keys on relations.
+ * Also, whenever we see an operation on a pg_class, pg_temp_class,
+ * pg_attribute, or pg_index tuple, we register a relcache flush operation
+ * for the relation described by that tuple (as specified in
+ * CacheInvalidateHeapTuple()). Likewise for pg_constraint tuples for
+ * foreign keys on relations.
*
* We keep the relcache flush requests in lists separate from the catcache
* tuple flush requests. This allows us to issue all the pending catcache
@@ -119,6 +120,7 @@
#include "access/xloginsert.h"
#include "catalog/catalog.h"
#include "catalog/pg_constraint.h"
+#include "catalog/pg_temp_class.h"
#include "miscadmin.h"
#include "storage/procnumber.h"
#include "storage/sinval.h"
@@ -1496,6 +1498,13 @@ CacheInvalidateHeapTupleCommon(Relation relation,
else
databaseId = MyDatabaseId;
}
+ else if (tupleRelId == TempRelationRelationId)
+ {
+ Form_pg_temp_class temp_classtup = (Form_pg_temp_class) GETSTRUCT(tuple);
+
+ relationId = temp_classtup->oid;
+ databaseId = MyDatabaseId;
+ }
else if (tupleRelId == AttributeRelationId)
{
Form_pg_attribute atttup = (Form_pg_attribute) GETSTRUCT(tuple);
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 036de5f79ef..cbd25fa6144 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -40,6 +40,7 @@
#include "catalog/pg_range.h"
#include "catalog/pg_statistic.h"
#include "catalog/pg_subscription.h"
+#include "catalog/pg_temp_class.h"
#include "catalog/pg_transform.h"
#include "catalog/pg_type.h"
#include "miscadmin.h"
@@ -2368,6 +2369,19 @@ get_rel_tablespace(Oid relid)
Oid result;
result = reltup->reltablespace;
+
+ /* Global temporary relations may override reltablespace locally */
+ if (reltup->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+ {
+ HeapTuple temp_tp;
+
+ temp_tp = GetPgTempClassTuple(relid);
+ if (HeapTupleIsValid(temp_tp))
+ {
+ result = ((Form_pg_temp_class) GETSTRUCT(temp_tp))->reltablespace;
+ heap_freetuple(temp_tp);
+ }
+ }
ReleaseSysCache(tp);
return result;
}
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index d9a96dd2e75..af8b5237e45 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -62,6 +62,7 @@
#include "catalog/pg_statistic_ext.h"
#include "catalog/pg_subscription.h"
#include "catalog/pg_tablespace.h"
+#include "catalog/pg_temp_class.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
#include "catalog/schemapg.h"
@@ -338,6 +339,10 @@ static void unlink_initfile(const char *initfilename, int elevel);
* an attribute were to be added after scanning pg_class and before
* scanning pg_attribute, relnatts wouldn't match.
*
+ * If targetRelId is a global temporary relation, pg_temp_class is
+ * also scanned, and if a matching tuple is found, its attributes are
+ * used to override the corresponding attributes from pg_class.
+ *
* NB: the returned tuple has been copied into palloc'd storage
* and must eventually be freed with heap_freetuple.
*/
@@ -405,6 +410,36 @@ ScanPgRelation(Oid targetRelId, bool indexOK, bool force_non_historic)
table_close(pg_class_desc, AccessShareLock);
+ /*
+ * For global temporary relations, also scan pg_temp_class. We cannot do
+ * this for pg_temp_class itself, or its index, because they may not have
+ * been loaded yet. That's OK because we only really need relfilenumber
+ * and reltablespace to be correct at this stage, and we disallow changes
+ * to those attributes for these relations.
+ */
+ if (HeapTupleIsValid(pg_class_tuple) &&
+ targetRelId != TempRelationRelationId &&
+ targetRelId != TempClassOidIndexId)
+ {
+ Form_pg_class pg_class_form;
+
+ pg_class_form = (Form_pg_class) GETSTRUCT(pg_class_tuple);
+
+ if (pg_class_form->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+ {
+ HeapTuple pg_temp_class_tuple;
+ Form_pg_temp_class pg_temp_class_form;
+
+ pg_temp_class_tuple = GetPgTempClassTuple(targetRelId);
+ if (HeapTupleIsValid(pg_temp_class_tuple))
+ {
+ pg_temp_class_form = (Form_pg_temp_class) GETSTRUCT(pg_temp_class_tuple);
+ COPY_PG_TEMP_CLASS_ATTRS(pg_temp_class_form, pg_class_form);
+ heap_freetuple(pg_temp_class_tuple);
+ }
+ }
+ }
+
return pg_class_tuple;
}
@@ -3842,7 +3877,9 @@ RelationSetNewRelfilenumber(Relation relation, char persistence)
Relation pg_class;
ItemPointerData otid;
HeapTuple tuple;
+ HeapTuple temp_tuple;
Form_pg_class classform;
+ Form_pg_temp_class temp_classform;
MultiXactId minmulti = InvalidMultiXactId;
TransactionId freezeXid = InvalidTransactionId;
RelFileLocator newrlocator;
@@ -3879,17 +3916,19 @@ RelationSetNewRelfilenumber(Relation relation, char persistence)
errmsg("unexpected request for new relfilenumber in binary upgrade mode")));
/*
- * Get a writable copy of the pg_class tuple for the given relation.
+ * Get a writable copy of the relation's pg_class tuple and, for a global
+ * temporary relation, its pg_temp_class tuple.
*/
pg_class = table_open(RelationRelationId, RowExclusiveLock);
- tuple = SearchSysCacheLockedCopy1(RELOID,
- ObjectIdGetDatum(RelationGetRelid(relation)));
+ tuple = GetPgClassAndPgTempClassTuples(RelationGetRelid(relation), true,
+ &temp_tuple, true);
if (!HeapTupleIsValid(tuple))
elog(ERROR, "could not find tuple for relation %u",
RelationGetRelid(relation));
otid = tuple->t_self;
classform = (Form_pg_class) GETSTRUCT(tuple);
+ temp_classform = (Form_pg_temp_class) GETSTRUCT_SAFE(temp_tuple);
/*
* Schedule unlinking of the old storage at transaction commit, except
@@ -3995,8 +4034,8 @@ RelationSetNewRelfilenumber(Relation relation, char persistence)
}
else
{
- /* Normal case, update the pg_class entry */
- classform->relfilenode = newrelfilenumber;
+ /* Normal case, update the pg_class and pg_temp_class entries */
+ SetEffective_relfilenode(classform, temp_classform, newrelfilenumber);
/* relpages etc. never change for sequences */
if (relation->rd_rel->relkind != RELKIND_SEQUENCE)
@@ -4011,6 +4050,11 @@ RelationSetNewRelfilenumber(Relation relation, char persistence)
classform->relpersistence = persistence;
CatalogTupleUpdate(pg_class, &otid, tuple);
+ if (HeapTupleIsValid(temp_tuple))
+ {
+ UpdatePgTempClassTuple(RelationGetRelid(relation), temp_tuple);
+ heap_freetuple(temp_tuple);
+ }
}
UnlockTuple(pg_class, &otid, InplaceUpdateTupleLock);
@@ -4019,8 +4063,8 @@ RelationSetNewRelfilenumber(Relation relation, char persistence)
table_close(pg_class, RowExclusiveLock);
/*
- * Make the pg_class row change or relation map change visible. This will
- * cause the relcache entry to get updated, too.
+ * Make the pg_class and pg_temp_class row changes or relation map change
+ * visible. This will cause the relcache entry to get updated, too.
*/
CommandCounterIncrement();
@@ -6913,6 +6957,15 @@ write_item(const void *data, Size len, FILE *fp)
* of the latter. The special cases are relations where
* RelationCacheInitializePhase2/3 chooses to nail for efficiency reasons, but
* which do not support any syscache.
+ *
+ * Global temporary relations are never nailed (because that would required
+ * them to be mapped, and the relmapper does not support temporary relations),
+ * but they do all support syscaches. Despite this, we intentionally do not
+ * cache global temporary relations, since we don't want to load them on
+ * startup, because doing so would result in temporary relation storage being
+ * created when it might not be needed. Instead, all global temporary
+ * relations are lazily initialized, if and when they are needed. See also
+ * InitCatalogCachePhase2().
*/
bool
RelationIdIsInInitFile(Oid relationId)
@@ -6929,6 +6982,8 @@ RelationIdIsInInitFile(Oid relationId)
Assert(!RelationSupportsSysCache(relationId));
return true;
}
+ if (IsGlobalTempCatalogRelation(relationId))
+ return false;
return RelationSupportsSysCache(relationId);
}
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index f4233f9e31a..7fe380c2c06 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -176,6 +176,10 @@ InitCatalogCache(void)
* relcache with entries for the most-commonly-used system catalogs.
* Therefore, we invoke this routine when we need to write a new relcache
* init file.
+ *
+ * We skip caches based on global temporary relations because we don't want
+ * temporary relation storage to be needlessly created on startup. Instead,
+ * always initialize these caches on first use.
*/
void
InitCatalogCachePhase2(void)
@@ -185,7 +189,8 @@ InitCatalogCachePhase2(void)
Assert(CacheInitialized);
for (cacheId = 0; cacheId < SysCacheSize; cacheId++)
- InitCatCachePhase2(SysCache[cacheId], true);
+ if (!SysCacheRelationIsGlobalTemp(cacheId))
+ InitCatCachePhase2(SysCache[cacheId], true);
}
diff --git a/src/include/access/htup_details.h b/src/include/access/htup_details.h
index 77a6c48fd71..4084fad6c6b 100644
--- a/src/include/access/htup_details.h
+++ b/src/include/access/htup_details.h
@@ -721,6 +721,17 @@ GETSTRUCT(const HeapTupleData *tuple)
return ((char *) (tuple->t_data) + tuple->t_data->t_hoff);
}
+/*
+ * GETSTRUCT_SAFE - given a possibly NULL HeapTuple pointer, return the
+ * address of the user data or NULL
+ */
+static inline void *
+GETSTRUCT_SAFE(const HeapTupleData *tuple)
+{
+ return HeapTupleIsValid(tuple) ?
+ ((char *) (tuple->t_data) + tuple->t_data->t_hoff) : NULL;
+}
+
/*
* Accessor functions to be used with HeapTuple pointers.
*/
diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile
index bab57372b88..629a13edc24 100644
--- a/src/include/catalog/Makefile
+++ b/src/include/catalog/Makefile
@@ -86,7 +86,8 @@ CATALOG_HEADERS := \
pg_propgraph_element_label.h \
pg_propgraph_label.h \
pg_propgraph_label_property.h \
- pg_propgraph_property.h
+ pg_propgraph_property.h \
+ pg_temp_class.h
GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h)
diff --git a/src/include/catalog/genbki.h b/src/include/catalog/genbki.h
index 12d2a3e295b..2f1253281c5 100644
--- a/src/include/catalog/genbki.h
+++ b/src/include/catalog/genbki.h
@@ -44,6 +44,7 @@
/* Options that may appear after CATALOG (on the same line) */
#define BKI_BOOTSTRAP
#define BKI_SHARED_RELATION
+#define BKI_TEMP_RELATION
#define BKI_ROWTYPE_OID(oid,oidmacro)
#define BKI_SCHEMA_MACRO
diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build
index fa836e4ee25..404f8503276 100644
--- a/src/include/catalog/meson.build
+++ b/src/include/catalog/meson.build
@@ -74,6 +74,7 @@ catalog_headers = [
'pg_propgraph_label.h',
'pg_propgraph_label_property.h',
'pg_propgraph_property.h',
+ 'pg_temp_class.h',
]
# The .dat files we need can just be listed alphabetically.
diff --git a/src/include/catalog/pg_temp_class.h b/src/include/catalog/pg_temp_class.h
new file mode 100644
index 00000000000..80011ea0954
--- /dev/null
+++ b/src/include/catalog/pg_temp_class.h
@@ -0,0 +1,150 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_temp_class.h
+ * definition of the "temporary relation" system catalog (pg_temp_class)
+ *
+ * This is a global temporary system catalog table storing session-specific
+ * information about temporary relations. Currently, it is only used for
+ * global temporary relations. The attributes are a subset of those from
+ * pg_class, and their values take precedence over the values from pg_class.
+ *
+ * Portions Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * src/include/catalog/pg_temp_class.h
+ *
+ * NOTES
+ * The Catalog.pm module reads this file and derives schema
+ * information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_TEMP_CLASS_H
+#define PG_TEMP_CLASS_H
+
+#include "access/htup.h"
+#include "catalog/genbki.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_temp_class_d.h" /* IWYU pragma: export */
+#include "utils/rel.h"
+
+/* ----------------
+ * pg_temp_class definition. cpp turns this into
+ * typedef struct FormData_pg_temp_class
+ * ----------------
+ */
+BEGIN_CATALOG_STRUCT
+
+CATALOG(pg_temp_class,8082,TempRelationRelationId) BKI_TEMP_RELATION
+{
+ /* oid */
+ Oid oid BKI_LOOKUP(pg_class);
+
+ /* identifier of physical storage file */
+ /* relfilenode == 0 means it is a "mapped" relation, see relmapper.c */
+ Oid relfilenode BKI_DEFAULT(0);
+
+ /* identifier of table space for relation (0 means default for database) */
+ Oid reltablespace BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_tablespace);
+} FormData_pg_temp_class;
+
+END_CATALOG_STRUCT
+
+/* ----------------
+ * Form_pg_temp_class corresponds to a pointer to a tuple with
+ * the format of pg_temp_class relation.
+ * ----------------
+ */
+typedef FormData_pg_temp_class *Form_pg_temp_class;
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_temp_class_oid_index, 8083, TempClassOidIndexId, pg_temp_class, btree(oid oid_ops));
+
+MAKE_SYSCACHE(TEMPRELOID, pg_temp_class_oid_index, 128);
+
+/*
+ * Copy all pg_temp_class attributes from "source" to "target", where the
+ * source and target may be of type Form_pg_class or Form_pg_temp_class.
+ *
+ * Beware of multiple evaluations of arguments!
+ */
+#define COPY_PG_TEMP_CLASS_ATTRS(source, target) \
+ do { \
+ (target)->oid = (source)->oid; \
+ (target)->relfilenode = (source)->relfilenode; \
+ (target)->reltablespace = (source)->reltablespace; \
+ } while (0)
+
+/*
+ * Get the effective value of relfilenode from pg_class and pg_temp_class
+ * tuple data. The value from pg_temp_class (if present) takes precedence.
+ */
+static inline Oid
+GetEffective_relfilenode(Form_pg_class cf, Form_pg_temp_class tf)
+{
+ return tf != NULL ? tf->relfilenode : cf->relfilenode;
+}
+
+/*
+ * Get the effective value of reltablespace from pg_class and pg_temp_class
+ * tuple data. The value from pg_temp_class (if present) takes precedence.
+ */
+static inline Oid
+GetEffective_reltablespace(Form_pg_class cf, Form_pg_temp_class tf)
+{
+ return tf != NULL ? tf->reltablespace : cf->reltablespace;
+}
+
+/*
+ * Set the effective value of relfilenode in tuple form data from pg_class or
+ * pg_temp_class. The value is set in pg_temp_class instead of pg_class, if
+ * the pg_temp_class tuple form data is non-NULL.
+ */
+static inline void
+SetEffective_relfilenode(Form_pg_class cf, Form_pg_temp_class tf, Oid val)
+{
+ if (tf != NULL)
+ tf->relfilenode = val;
+ else
+ cf->relfilenode = val;
+}
+
+/*
+ * Set the effective value of reltablespace in tuple form data from pg_class
+ * and pg_temp_class. The value is set in pg_temp_class as well as pg_class,
+ * if the pg_temp_class tuple form data is non-NULL.
+ */
+static inline void
+SetEffective_reltablespace(Form_pg_class cf, Form_pg_temp_class tf, Oid val)
+{
+ /* NB: Value is set *both* locally and globally */
+ cf->reltablespace = val;
+ if (tf != NULL)
+ tf->reltablespace = val;
+}
+
+
+extern HeapTuple GetPgTempClassTuple(Oid relid);
+
+extern void InsertPgTempClassTuple(Relation rel);
+
+extern void UpdatePgTempClassTuple(Oid relid, HeapTuple newtuple);
+
+extern void DeletePgTempClassTuple(Oid relid);
+
+extern HeapTuple GetPgClassAndPgTempClassTuples(Oid relid, bool lock_tuple,
+ HeapTuple *temp_tuple,
+ bool check_temp);
+
+extern HeapTuple GetEffectivePgClassTuple(Oid relid);
+
+extern void PreCCI_PgTempClass(void);
+
+extern void PreCommit_PgTempClass(void);
+
+extern void PreSubCommit_PgTempClass(void);
+
+extern void AtEOXact_PgTempClass(bool isCommit);
+
+extern void AtEOSubXact_PgTempClass(bool isCommit, SubTransactionId mySubid,
+ SubTransactionId parentSubid);
+
+#endif /* PG_TEMP_CLASS_H */
diff --git a/src/test/isolation/expected/global-temp.out b/src/test/isolation/expected/global-temp.out
index d46ca85acbb..65025721db1 100644
--- a/src/test/isolation/expected/global-temp.out
+++ b/src/test/isolation/expected/global-temp.out
@@ -1,5 +1,16 @@
Parsed test spec with 2 sessions
+starting permutation: create_tblspace list_tblspaces
+step create_tblspace: CREATE TABLESPACE regress_isolation_tablespace LOCATION '';
+step list_tblspaces: SELECT spcname FROM pg_tablespace ORDER BY 1;
+spcname
+----------------------------
+pg_default
+pg_global
+regress_isolation_tablespace
+(3 rows)
+
+
starting permutation: ins1 ins2 sel1 sel2
step ins1: INSERT INTO tmp VALUES (1, 's1');
step ins2: INSERT INTO tmp VALUES (1, 's2');
@@ -394,3 +405,101 @@ key|val|seq
1|s2 | 1
(1 row)
+
+starting permutation: ins1 ins2 t2 sel1 sel2 ins2 t1 sel1 sel2 ins1 t2 sel1 sel2
+step ins1: INSERT INTO tmp VALUES (1, 's1');
+step ins2: INSERT INTO tmp VALUES (1, 's2');
+step t2: TRUNCATE tmp;
+step sel1: SELECT * FROM tmp;
+key|val|seq
+---+---+---
+ 1|s1 | 1
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val|seq
+---+---+---
+(0 rows)
+
+step ins2: INSERT INTO tmp VALUES (1, 's2');
+step t1: TRUNCATE tmp;
+step sel1: SELECT * FROM tmp;
+key|val|seq
+---+---+---
+(0 rows)
+
+step sel2: SELECT * FROM tmp;
+key|val|seq
+---+---+---
+ 1|s2 | 2
+(1 row)
+
+step ins1: INSERT INTO tmp VALUES (1, 's1');
+step t2: TRUNCATE tmp;
+step sel1: SELECT * FROM tmp;
+key|val|seq
+---+---+---
+ 1|s1 | 2
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val|seq
+---+---+---
+(0 rows)
+
+
+starting permutation: ins1 ins2 alt_tblspace get_tblspace1 get_tblspace2 sel1 sel2 reset_tblspace
+step ins1: INSERT INTO tmp VALUES (1, 's1');
+step ins2: INSERT INTO tmp VALUES (1, 's2');
+step alt_tblspace: ALTER TABLE tmp SET TABLESPACE regress_isolation_tablespace;
+step get_tblspace1:
+ SELECT s1.spcname, s2.spcname,
+ regexp_replace(pg_relation_filepath('tmp'), '(\d+)', 'NNN', 'g')
+ FROM pg_class c
+ JOIN pg_tablespace s1 ON s1.oid = c.reltablespace
+ LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ JOIN pg_tablespace s2 ON s2.oid = t.reltablespace
+ WHERE c.relname = 'tmp';
+
+spcname |spcname |regexp_replace
+----------------------------+----------------------------+-------------------------------------
+regress_isolation_tablespace|regress_isolation_tablespace|pg_tblspc/NNN/PG_NNN_NNN/NNN/tNNN_NNN
+(1 row)
+
+step get_tblspace2:
+ SELECT s1.spcname, s2.spcname,
+ regexp_replace(pg_relation_filepath('tmp'), '(\d+)', 'NNN', 'g')
+ FROM pg_class c
+ JOIN pg_tablespace s1 ON s1.oid = c.reltablespace
+ LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ LEFT JOIN pg_tablespace s2 ON s2.oid = t.reltablespace
+ WHERE c.relname = 'tmp';
+
+spcname |spcname|regexp_replace
+----------------------------+-------+-----------------
+regress_isolation_tablespace| |base/NNN/tNNN_NNN
+(1 row)
+
+step sel1: SELECT * FROM tmp;
+key|val|seq
+---+---+---
+ 1|s1 | 1
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val|seq
+---+---+---
+ 1|s2 | 1
+(1 row)
+
+step reset_tblspace: ALTER TABLE tmp SET TABLESPACE pg_default;
+
+starting permutation: drop_tblspace list_tblspaces
+step drop_tblspace: DROP TABLESPACE regress_isolation_tablespace;
+step list_tblspaces: SELECT spcname FROM pg_tablespace ORDER BY 1;
+spcname
+----------
+pg_default
+pg_global
+(2 rows)
+
diff --git a/src/test/isolation/specs/global-temp.spec b/src/test/isolation/specs/global-temp.spec
index 4ec963e104a..ecb07ad0b6e 100644
--- a/src/test/isolation/specs/global-temp.spec
+++ b/src/test/isolation/specs/global-temp.spec
@@ -13,6 +13,10 @@ teardown {
}
session s1
+setup { SET allow_in_place_tablespaces = true; }
+step create_tblspace { CREATE TABLESPACE regress_isolation_tablespace LOCATION ''; }
+step list_tblspaces { SELECT spcname FROM pg_tablespace ORDER BY 1; }
+step drop_tblspace { DROP TABLESPACE regress_isolation_tablespace; }
step ins1 { INSERT INTO tmp VALUES (1, 's1'); }
step ins1p1 { INSERT INTO tmp_parted VALUES (1, 's1 p1'); }
step ins1p2 { INSERT INTO tmp_parted VALUES (2, 's1 p2'); }
@@ -33,6 +37,18 @@ step sel1_idx {
SELECT * FROM tmp WHERE val = 's1';
SELECT * FROM tmp WHERE val = 's1';
}
+step t1 { TRUNCATE tmp; }
+step alt_tblspace { ALTER TABLE tmp SET TABLESPACE regress_isolation_tablespace; }
+step get_tblspace1 {
+ SELECT s1.spcname, s2.spcname,
+ regexp_replace(pg_relation_filepath('tmp'), '(\d+)', 'NNN', 'g')
+ FROM pg_class c
+ JOIN pg_tablespace s1 ON s1.oid = c.reltablespace
+ LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ JOIN pg_tablespace s2 ON s2.oid = t.reltablespace
+ WHERE c.relname = 'tmp';
+}
+step reset_tblspace { ALTER TABLE tmp SET TABLESPACE pg_default; }
session s2
step b2 { BEGIN; }
@@ -56,6 +72,18 @@ step sel2_idx {
SELECT * FROM tmp WHERE val = 's2';
}
step reidx2 { REINDEX INDEX tmp_val_idx; }
+step get_tblspace2 {
+ SELECT s1.spcname, s2.spcname,
+ regexp_replace(pg_relation_filepath('tmp'), '(\d+)', 'NNN', 'g')
+ FROM pg_class c
+ JOIN pg_tablespace s1 ON s1.oid = c.reltablespace
+ LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ LEFT JOIN pg_tablespace s2 ON s2.oid = t.reltablespace
+ WHERE c.relname = 'tmp';
+}
+
+# Create test tablespace for remaining tests
+permutation create_tblspace list_tblspaces
# Basic effects
permutation ins1 ins2 sel1 sel2
@@ -79,3 +107,12 @@ permutation create1dr ins1_2 ins2_2 drop1 create1dr ins1_2 ins2_2 drop1
permutation ins1 idx1 sel1_idx ins2 sel2_idx
permutation ins1 ins2 idx1 sel1_idx sel2_idx
permutation ins1 ins2 idx1 sel1_idx sel2_idx reidx2 sel2_idx
+
+# Test local TRUNCATE
+permutation ins1 ins2 t2 sel1 sel2 ins2 t1 sel1 sel2 ins1 t2 sel1 sel2
+
+# Test ALTER TABLE ... SET TABLESPACE
+permutation ins1 ins2 alt_tblspace get_tblspace1 get_tblspace2 sel1 sel2 reset_tblspace
+
+# Tidy up
+permutation drop_tblspace list_tblspaces
diff --git a/src/test/recovery/t/018_wal_optimize.pl b/src/test/recovery/t/018_wal_optimize.pl
index 8f25b5dd165..8fd95980f36 100644
--- a/src/test/recovery/t/018_wal_optimize.pl
+++ b/src/test/recovery/t/018_wal_optimize.pl
@@ -29,6 +29,7 @@ sub check_orphan_relfilenodes
'postgres', "
SELECT pg_relation_filepath(oid) FROM pg_class
WHERE reltablespace = 0 AND relpersistence <> 't' AND
+ relpersistence <> 'g' AND
pg_relation_filepath(oid) IS NOT NULL;");
is_deeply(
[
diff --git a/src/test/regress/expected/global_temp.out b/src/test/regress/expected/global_temp.out
index 6d2751e6899..cc9400284ff 100644
--- a/src/test/regress/expected/global_temp.out
+++ b/src/test/regress/expected/global_temp.out
@@ -67,6 +67,14 @@ SELECT * FROM tmp1;
---+---+---
(0 rows)
+-- Test pg_relation_filenode() matches global relfilenode
+SELECT relfilenode = pg_relation_filenode('tmp1'::regclass) AS ok
+ FROM pg_class WHERE oid = 'tmp1'::regclass;
+ ok
+----
+ t
+(1 row)
+
-- Test index
INSERT INTO tmp1 VALUES (1, 'xxx');
SET enable_seqscan = off;
@@ -105,7 +113,26 @@ SELECT * FROM tmp1 WHERE b = 'xxx';
RESET enable_seqscan;
REINDEX INDEX CONCURRENTLY tmp1_b_idx;
REINDEX TABLE CONCURRENTLY tmp1;
+-- Test REINDEX -- relfilenode only changes locally
+SELECT c.relfilenode AS global_relfilenode, t.relfilenode AS local_relfilenode
+ FROM pg_class c LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.relname = 'tmp1_b_idx' \gset
+REINDEX INDEX tmp1_b_idx;
+SELECT CASE WHEN c.relfilenode = :global_relfilenode THEN 'unchanged' ELSE 'changed' END AS global_relfilenode,
+ CASE WHEN t.relfilenode = :local_relfilenode THEN 'unchange' ELSE 'changed' END AS local_relfilenode
+ FROM pg_class c LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.relname = 'tmp1_b_idx';
+ global_relfilenode | local_relfilenode
+--------------------+-------------------
+ unchanged | changed
+(1 row)
+
DROP INDEX CONCURRENTLY tmp1_b_idx;
+-- REINDEX not allowed on pg_temp_class
+REINDEX INDEX pg_temp_class_oid_index;
+NOTICE: cannot reindex temporary system index "pg_temp_class_oid_index", skipping
+REINDEX TABLE pg_temp_class;
+NOTICE: cannot reindex temporary system index "pg_temp_class_oid_index", skipping
-- Test ON COMMIT DELETE ROWS
CREATE GLOBAL TEMP TABLE tmp2 (a int) ON COMMIT DELETE ROWS;
BEGIN;
@@ -217,7 +244,7 @@ SELECT * FROM tmp2;
(0 rows)
DROP TABLE perm_pk_rel, temp_pk_rel, tmp2;
--- Test ALTER TABLE ... SET TABLESPACE
+-- Test ALTER TABLE ... SET TABLESPACE -- reltablespace changes locally and globally
CREATE GLOBAL TEMP TABLE tmp2 (a int);
INSERT INTO tmp2 VALUES (1);
SELECT * FROM tmp2;
@@ -226,10 +253,15 @@ SELECT * FROM tmp2;
1
(1 row)
-SELECT regexp_replace(pg_relation_filepath('tmp2'), '(\d+)', 'NNN', 'g');
- regexp_replace
--------------------
- base/NNN/tNNN_NNN
+SELECT c.reltablespace AS global_tablespace,
+ t.reltablespace AS local_tablespace,
+ regexp_replace(pg_relation_filepath('tmp2'), '(\d+)', 'NNN', 'g')
+ FROM pg_class c
+ LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.relname = 'tmp2';
+ global_tablespace | local_tablespace | regexp_replace
+-------------------+------------------+-------------------
+ 0 | 0 | base/NNN/tNNN_NNN
(1 row)
ALTER TABLE tmp2 SET TABLESPACE regress_tblspace;
@@ -239,10 +271,16 @@ SELECT * FROM tmp2;
1
(1 row)
-SELECT regexp_replace(pg_relation_filepath('tmp2'), '(\d+)', 'NNN', 'g');
- regexp_replace
----------------------------------------
- pg_tblspc/NNN/PG_NNN_NNN/NNN/tNNN_NNN
+SELECT s1.spcname AS global_tablespace, s2.spcname AS local_tablespace,
+ regexp_replace(pg_relation_filepath('tmp2'), '(\d+)', 'NNN', 'g')
+ FROM pg_class c
+ JOIN pg_tablespace s1 ON s1.oid = c.reltablespace
+ LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ JOIN pg_tablespace s2 ON s2.oid = t.reltablespace
+ WHERE c.relname = 'tmp2';
+ global_tablespace | local_tablespace | regexp_replace
+-------------------+------------------+---------------------------------------
+ regress_tblspace | regress_tblspace | pg_tblspc/NNN/PG_NNN_NNN/NNN/tNNN_NNN
(1 row)
DROP TABLE tmp2;
@@ -333,13 +371,135 @@ SELECT * FROM tmp1;
---+---+---
(0 rows)
--- Test view creation
+-- Test CLUSTER -- relfilenode only changes locally
+SELECT c.relfilenode AS global_relfilenode, t.relfilenode AS local_relfilenode
+ FROM pg_class c LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.relname = 'tmp1' \gset
+CLUSTER tmp1 USING tmp1_pkey;
+SELECT CASE WHEN c.relfilenode = :global_relfilenode THEN 'unchanged' ELSE 'changed' END AS global_relfilenode,
+ CASE WHEN t.relfilenode = :local_relfilenode THEN 'unchange' ELSE 'changed' END AS local_relfilenode
+ FROM pg_class c LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.relname = 'tmp1';
+ global_relfilenode | local_relfilenode
+--------------------+-------------------
+ unchanged | changed
+(1 row)
+
+-- CLUSTER not allowed on pg_temp_class
+CLUSTER pg_temp_class; -- fail
+ERROR: cannot execute CLUSTER on temporary system catalog "pg_temp_class"
+-- Test REPACK -- relfilenode only changes locally
+SELECT c.relfilenode AS global_relfilenode, t.relfilenode AS local_relfilenode
+ FROM pg_class c LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.relname = 'tmp1' \gset
+REPACK tmp1;
+SELECT CASE WHEN c.relfilenode = :global_relfilenode THEN 'unchanged' ELSE 'changed' END AS global_relfilenode,
+ CASE WHEN t.relfilenode = :local_relfilenode THEN 'unchange' ELSE 'changed' END AS local_relfilenode
+ FROM pg_class c LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.relname = 'tmp1';
+ global_relfilenode | local_relfilenode
+--------------------+-------------------
+ unchanged | changed
+(1 row)
+
+-- REPACK not allowed on pg_temp_class
+REPACK pg_temp_class; -- fail
+ERROR: cannot execute REPACK on temporary system catalog "pg_temp_class"
+-- Test VACUUM FULL -- relfilenode only changes locally
+SELECT c.relfilenode AS global_relfilenode, t.relfilenode AS local_relfilenode
+ FROM pg_class c LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.relname = 'tmp1' \gset
+VACUUM FULL tmp1;
+SELECT CASE WHEN c.relfilenode = :global_relfilenode THEN 'unchanged' ELSE 'changed' END AS global_relfilenode,
+ CASE WHEN t.relfilenode = :local_relfilenode THEN 'unchange' ELSE 'changed' END AS local_relfilenode
+ FROM pg_class c LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.relname = 'tmp1';
+ global_relfilenode | local_relfilenode
+--------------------+-------------------
+ unchanged | changed
+(1 row)
+
+-- VACUUM FULL not allowed on pg_temp_class
+VACUUM FULL pg_temp_class; -- silently ignored
+-- Test pg_relation_filenode() now matches local relfilenode
+SELECT relfilenode = pg_relation_filenode('tmp1'::regclass) AS ok
+ FROM pg_temp_class WHERE oid = 'tmp1'::regclass;
+ ok
+----
+ t
+(1 row)
+
+-- VACUUM initializes toast tables
+\c
+SET search_path = global_temp_tests;
+SELECT EXISTS (SELECT 1 FROM pg_class t WHERE t.oid = c.reltoastrelid),
+ EXISTS (SELECT 1 FROM pg_temp_class t WHERE t.oid = c.reltoastrelid)
+FROM pg_class c
+WHERE oid = 'tmp1'::regclass;
+ exists | exists
+--------+--------
+ t | f
+(1 row)
+
+VACUUM tmp1;
+SELECT EXISTS (SELECT 1 FROM pg_class t WHERE t.oid = c.reltoastrelid),
+ EXISTS (SELECT 1 FROM pg_temp_class t WHERE t.oid = c.reltoastrelid)
+FROM pg_class c
+WHERE oid = 'tmp1'::regclass;
+ exists | exists
+--------+--------
+ t | t
+(1 row)
+
+\c
+SET search_path = global_temp_tests;
+VACUUM FULL tmp1;
+SELECT EXISTS (SELECT 1 FROM pg_class t WHERE t.oid = c.reltoastrelid),
+ EXISTS (SELECT 1 FROM pg_temp_class t WHERE t.oid = c.reltoastrelid)
+FROM pg_class c
+WHERE oid = 'tmp1'::regclass;
+ exists | exists
+--------+--------
+ t | t
+(1 row)
+
+-- Test subtransaction rollback of pending pg_temp_class inserts
+\c
+SET search_path = global_temp_tests;
+BEGIN;
+SELECT count(*) FROM tmp1;
+ count
+-------
+ 0
+(1 row)
+
+SAVEPOINT sp;
+DROP TABLE tmp1;
+ROLLBACK TO sp;
INSERT INTO tmp1 VALUES (1, 'xxx');
+COMMIT;
+SELECT oid::regclass FROM pg_temp_class ORDER BY 1;
+ oid
+-------------------------
+ pg_temp_class
+ pg_temp_class_oid_index
+ tmp1_c_seq
+ tmp1
+ tmp1_pkey
+(5 rows)
+
+SELECT * FROM tmp1;
+ a | b | c
+---+-----+---
+ 1 | xxx | 1
+(1 row)
+
+-- Test view creation
CREATE VIEW v AS SELECT * FROM tmp1;
SELECT * FROM v;
a | b | c
---+-----+---
- 1 | xxx | 2
+ 1 | xxx | 1
(1 row)
DROP VIEW v;
@@ -347,7 +507,7 @@ CREATE TEMP VIEW v AS SELECT * FROM tmp1;
SELECT * FROM v;
a | b | c
---+-----+---
- 1 | xxx | 2
+ 1 | xxx | 1
(1 row)
DROP VIEW v;
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index d64169b7bf0..3c3404e51b8 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -285,3 +285,5 @@ NOTICE: checking pg_propgraph_label_property {plpellabelid} => pg_propgraph_ele
NOTICE: checking pg_propgraph_property {pgppgid} => pg_class {oid}
NOTICE: checking pg_propgraph_property {pgptypid} => pg_type {oid}
NOTICE: checking pg_propgraph_property {pgpcollation} => pg_collation {oid}
+NOTICE: checking pg_temp_class {oid} => pg_class {oid}
+NOTICE: checking pg_temp_class {reltablespace} => pg_tablespace {oid}
diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out
index 03cbc1cdef5..b6d1d5fc322 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -88,6 +88,7 @@ standalone backend|relation|bulkwrite
standalone backend|relation|init
standalone backend|relation|normal
standalone backend|relation|vacuum
+standalone backend|temp relation|normal
standalone backend|wal|init
standalone backend|wal|normal
startup|relation|bulkread
@@ -111,7 +112,7 @@ walsummarizer|wal|init
walsummarizer|wal|normal
walwriter|wal|init
walwriter|wal|normal
-(95 rows)
+(96 rows)
\a
-- List of registered statistics kinds.
SELECT id, name, fixed_amount,
diff --git a/src/test/regress/sql/global_temp.sql b/src/test/regress/sql/global_temp.sql
index 2373eae0ad1..a19b25b5bc6 100644
--- a/src/test/regress/sql/global_temp.sql
+++ b/src/test/regress/sql/global_temp.sql
@@ -35,6 +35,10 @@ SELECT * FROM tmp1;
SET search_path = global_temp_tests;
SELECT * FROM tmp1;
+-- Test pg_relation_filenode() matches global relfilenode
+SELECT relfilenode = pg_relation_filenode('tmp1'::regclass) AS ok
+ FROM pg_class WHERE oid = 'tmp1'::regclass;
+
-- Test index
INSERT INTO tmp1 VALUES (1, 'xxx');
SET enable_seqscan = off;
@@ -52,8 +56,22 @@ SELECT * FROM tmp1 WHERE b = 'xxx';
RESET enable_seqscan;
REINDEX INDEX CONCURRENTLY tmp1_b_idx;
REINDEX TABLE CONCURRENTLY tmp1;
+
+-- Test REINDEX -- relfilenode only changes locally
+SELECT c.relfilenode AS global_relfilenode, t.relfilenode AS local_relfilenode
+ FROM pg_class c LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.relname = 'tmp1_b_idx' \gset
+REINDEX INDEX tmp1_b_idx;
+SELECT CASE WHEN c.relfilenode = :global_relfilenode THEN 'unchanged' ELSE 'changed' END AS global_relfilenode,
+ CASE WHEN t.relfilenode = :local_relfilenode THEN 'unchange' ELSE 'changed' END AS local_relfilenode
+ FROM pg_class c LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.relname = 'tmp1_b_idx';
DROP INDEX CONCURRENTLY tmp1_b_idx;
+-- REINDEX not allowed on pg_temp_class
+REINDEX INDEX pg_temp_class_oid_index;
+REINDEX TABLE pg_temp_class;
+
-- Test ON COMMIT DELETE ROWS
CREATE GLOBAL TEMP TABLE tmp2 (a int) ON COMMIT DELETE ROWS;
BEGIN;
@@ -119,14 +137,25 @@ DELETE FROM gtemp_pk_rel WHERE a = 1;
SELECT * FROM tmp2;
DROP TABLE perm_pk_rel, temp_pk_rel, tmp2;
--- Test ALTER TABLE ... SET TABLESPACE
+-- Test ALTER TABLE ... SET TABLESPACE -- reltablespace changes locally and globally
CREATE GLOBAL TEMP TABLE tmp2 (a int);
INSERT INTO tmp2 VALUES (1);
SELECT * FROM tmp2;
-SELECT regexp_replace(pg_relation_filepath('tmp2'), '(\d+)', 'NNN', 'g');
+SELECT c.reltablespace AS global_tablespace,
+ t.reltablespace AS local_tablespace,
+ regexp_replace(pg_relation_filepath('tmp2'), '(\d+)', 'NNN', 'g')
+ FROM pg_class c
+ LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.relname = 'tmp2';
ALTER TABLE tmp2 SET TABLESPACE regress_tblspace;
SELECT * FROM tmp2;
-SELECT regexp_replace(pg_relation_filepath('tmp2'), '(\d+)', 'NNN', 'g');
+SELECT s1.spcname AS global_tablespace, s2.spcname AS local_tablespace,
+ regexp_replace(pg_relation_filepath('tmp2'), '(\d+)', 'NNN', 'g')
+ FROM pg_class c
+ JOIN pg_tablespace s1 ON s1.oid = c.reltablespace
+ LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ JOIN pg_tablespace s2 ON s2.oid = t.reltablespace
+ WHERE c.relname = 'tmp2';
DROP TABLE tmp2;
-- Test dependency on tablespace
@@ -178,8 +207,85 @@ SELECT * FROM tmp1;
TRUNCATE tmp1;
SELECT * FROM tmp1;
--- Test view creation
+-- Test CLUSTER -- relfilenode only changes locally
+SELECT c.relfilenode AS global_relfilenode, t.relfilenode AS local_relfilenode
+ FROM pg_class c LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.relname = 'tmp1' \gset
+CLUSTER tmp1 USING tmp1_pkey;
+SELECT CASE WHEN c.relfilenode = :global_relfilenode THEN 'unchanged' ELSE 'changed' END AS global_relfilenode,
+ CASE WHEN t.relfilenode = :local_relfilenode THEN 'unchange' ELSE 'changed' END AS local_relfilenode
+ FROM pg_class c LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.relname = 'tmp1';
+
+-- CLUSTER not allowed on pg_temp_class
+CLUSTER pg_temp_class; -- fail
+
+-- Test REPACK -- relfilenode only changes locally
+SELECT c.relfilenode AS global_relfilenode, t.relfilenode AS local_relfilenode
+ FROM pg_class c LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.relname = 'tmp1' \gset
+REPACK tmp1;
+SELECT CASE WHEN c.relfilenode = :global_relfilenode THEN 'unchanged' ELSE 'changed' END AS global_relfilenode,
+ CASE WHEN t.relfilenode = :local_relfilenode THEN 'unchange' ELSE 'changed' END AS local_relfilenode
+ FROM pg_class c LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.relname = 'tmp1';
+
+-- REPACK not allowed on pg_temp_class
+REPACK pg_temp_class; -- fail
+
+-- Test VACUUM FULL -- relfilenode only changes locally
+SELECT c.relfilenode AS global_relfilenode, t.relfilenode AS local_relfilenode
+ FROM pg_class c LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.relname = 'tmp1' \gset
+VACUUM FULL tmp1;
+SELECT CASE WHEN c.relfilenode = :global_relfilenode THEN 'unchanged' ELSE 'changed' END AS global_relfilenode,
+ CASE WHEN t.relfilenode = :local_relfilenode THEN 'unchange' ELSE 'changed' END AS local_relfilenode
+ FROM pg_class c LEFT JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.relname = 'tmp1';
+
+-- VACUUM FULL not allowed on pg_temp_class
+VACUUM FULL pg_temp_class; -- silently ignored
+
+-- Test pg_relation_filenode() now matches local relfilenode
+SELECT relfilenode = pg_relation_filenode('tmp1'::regclass) AS ok
+ FROM pg_temp_class WHERE oid = 'tmp1'::regclass;
+
+-- VACUUM initializes toast tables
+\c
+SET search_path = global_temp_tests;
+SELECT EXISTS (SELECT 1 FROM pg_class t WHERE t.oid = c.reltoastrelid),
+ EXISTS (SELECT 1 FROM pg_temp_class t WHERE t.oid = c.reltoastrelid)
+FROM pg_class c
+WHERE oid = 'tmp1'::regclass;
+
+VACUUM tmp1;
+SELECT EXISTS (SELECT 1 FROM pg_class t WHERE t.oid = c.reltoastrelid),
+ EXISTS (SELECT 1 FROM pg_temp_class t WHERE t.oid = c.reltoastrelid)
+FROM pg_class c
+WHERE oid = 'tmp1'::regclass;
+
+\c
+SET search_path = global_temp_tests;
+VACUUM FULL tmp1;
+SELECT EXISTS (SELECT 1 FROM pg_class t WHERE t.oid = c.reltoastrelid),
+ EXISTS (SELECT 1 FROM pg_temp_class t WHERE t.oid = c.reltoastrelid)
+FROM pg_class c
+WHERE oid = 'tmp1'::regclass;
+
+-- Test subtransaction rollback of pending pg_temp_class inserts
+\c
+SET search_path = global_temp_tests;
+BEGIN;
+SELECT count(*) FROM tmp1;
+SAVEPOINT sp;
+DROP TABLE tmp1;
+ROLLBACK TO sp;
INSERT INTO tmp1 VALUES (1, 'xxx');
+COMMIT;
+SELECT oid::regclass FROM pg_temp_class ORDER BY 1;
+SELECT * FROM tmp1;
+
+-- Test view creation
CREATE VIEW v AS SELECT * FROM tmp1;
SELECT * FROM v;
DROP VIEW v;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index ddf9827ee25..6b38155a48a 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -956,6 +956,7 @@ FormData_pg_statistic_ext_data
FormData_pg_subscription
FormData_pg_subscription_rel
FormData_pg_tablespace
+FormData_pg_temp_class
FormData_pg_transform
FormData_pg_trigger
FormData_pg_ts_config
@@ -1021,6 +1022,7 @@ Form_pg_statistic_ext_data
Form_pg_subscription
Form_pg_subscription_rel
Form_pg_tablespace
+Form_pg_temp_class
Form_pg_transform
Form_pg_trigger
Form_pg_ts_config
@@ -2255,6 +2257,7 @@ PatternInfoArray
Pattern_Prefix_Status
Pattern_Type
PendingFsyncEntry
+PendingInsert
PendingListenAction
PendingListenEntry
PendingRelDelete
--
2.51.0
[text/x-patch] v8-0006-Add-relation-statistics-columns-to-pg_temp_class.patch (53.0K, ../CAEZATCVjaZFa80S-V_+nvnzRK1ZiLUPx8jwdyAtRVHAmPZQQMg@mail.gmail.com/7-v8-0006-Add-relation-statistics-columns-to-pg_temp_class.patch)
download | inline diff:
From 6dce3f5dbd502d1c24fd5bd372aac6505f64ac8f Mon Sep 17 00:00:00 2001
From: Dean Rasheed <[email protected]>
Date: Wed, 17 Jun 2026 02:04:59 +0100
Subject: [PATCH v8 06/10] Add relation statistics columns to pg_temp_class.
This adds relpages, reltuples, relallvisible, and relallfrozen columns
to pg_temp_class, and updates ANALYZE, CREATE INDEX, REPACK, VACUUM,
and pg_clear/restore_relation_stats() to apply statistics updates to
pg_temp_class instead of pg_class for global temporary relations. Each
session is then able to use its own local statistics when planning
queries.
---
src/backend/access/heap/heapam.c | 4 +
src/backend/catalog/catalog.c | 2 +
src/backend/catalog/heap.c | 41 +++--
src/backend/catalog/index.c | 137 +++++++++++-----
src/backend/catalog/pg_temp_class.c | 91 +++++++++++
src/backend/commands/repack.c | 61 ++++---
src/backend/commands/vacuum.c | 89 +++++++----
src/backend/statistics/relation_stats.c | 82 ++++++----
src/backend/utils/cache/lsyscache.c | 11 ++
src/backend/utils/cache/relcache.c | 9 +-
src/include/catalog/pg_temp_class.h | 166 ++++++++++++++++++++
src/include/utils/lsyscache.h | 1 +
src/test/isolation/expected/global-temp.out | 32 ++++
src/test/isolation/specs/global-temp.spec | 7 +
src/test/regress/expected/global_temp.out | 136 ++++++++++++++++
src/test/regress/sql/global_temp.sql | 82 ++++++++++
16 files changed, 809 insertions(+), 142 deletions(-)
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index abfd8e8970a..d2588779cb8 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -42,6 +42,7 @@
#include "access/xloginsert.h"
#include "catalog/pg_database.h"
#include "catalog/pg_database_d.h"
+#include "catalog/pg_temp_class.h"
#include "commands/vacuum.h"
#include "executor/instrument_node.h"
#include "pgstat.h"
@@ -4206,6 +4207,9 @@ check_lock_if_inplace_updateable_rel(Relation relation,
return;
}
break;
+ case TempRelationRelationId:
+ /* No lock required -- temp tables are only accessible by us */
+ return;
default:
Assert(!IsInplaceUpdateRelation(relation));
return;
diff --git a/src/backend/catalog/catalog.c b/src/backend/catalog/catalog.c
index 65d786685f2..b0415be2e4f 100644
--- a/src/backend/catalog/catalog.c
+++ b/src/backend/catalog/catalog.c
@@ -40,6 +40,7 @@
#include "catalog/pg_shseclabel.h"
#include "catalog/pg_subscription.h"
#include "catalog/pg_tablespace.h"
+#include "catalog/pg_temp_class.h"
#include "catalog/pg_type.h"
#include "miscadmin.h"
#include "utils/fmgroids.h"
@@ -195,6 +196,7 @@ bool
IsInplaceUpdateOid(Oid relid)
{
return (relid == RelationRelationId ||
+ relid == TempRelationRelationId ||
relid == DatabaseRelationId);
}
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 012de434321..fdd06ecc877 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -3577,10 +3577,10 @@ RemoveStatistics(Oid relid, AttrNumber attnum)
* with the heap relation to zero tuples.
*
* The routine will truncate and then reconstruct the indexes on
- * the specified relation. Caller must hold exclusive lock on rel.
+ * the specified relation. Caller must hold the specified lock on rel.
*/
static void
-RelationTruncateIndexes(Relation heapRelation)
+RelationTruncateIndexes(Relation heapRelation, LOCKMODE lockmode)
{
ListCell *indlist;
@@ -3591,8 +3591,8 @@ RelationTruncateIndexes(Relation heapRelation)
Relation currentIndex;
IndexInfo *indexInfo;
- /* Open the index relation; use exclusive lock, just to be sure */
- currentIndex = index_open(indexId, AccessExclusiveLock);
+ /* Open the index relation; use same lock as heap relation */
+ currentIndex = index_open(indexId, lockmode);
/*
* Fetch info needed for index_build. Since we know there are no
@@ -3634,13 +3634,23 @@ heap_truncate(List *relids)
List *relations = NIL;
ListCell *cell;
- /* Open relations for processing, and grab exclusive access on each */
+ /*
+ * Open relations for processing. For most relations, we must use
+ * AccessExclusiveLock to prevent schema and data changes. However, for
+ * global temporary relations, we must use RowExclusiveLock, because two
+ * backends trying to upgrade to an exclusive lock on the same relation
+ * here would deadlock. This is sufficent, because the relation's data is
+ * session-local.
+ */
foreach(cell, relids)
{
Oid rid = lfirst_oid(cell);
+ LOCKMODE lockmode;
Relation rel;
- rel = table_open(rid, AccessExclusiveLock);
+ lockmode = rel_is_global_temp(rid) ? RowExclusiveLock : AccessExclusiveLock;
+
+ rel = table_open(rid, lockmode);
relations = lappend(relations, rel);
}
@@ -3655,7 +3665,7 @@ heap_truncate(List *relids)
/* Truncate the relation */
heap_truncate_one_rel(rel);
- /* Close the relation, but keep exclusive lock on it until commit */
+ /* Close the relation, but keep lock on it until commit */
table_close(rel, NoLock);
}
}
@@ -3667,13 +3677,22 @@ heap_truncate(List *relids)
*
* This is not transaction-safe, because the truncation is done immediately
* and cannot be rolled back later. Caller is responsible for having
- * checked permissions etc, and must have obtained AccessExclusiveLock.
+ * checked permissions etc, and must have obtained the required lock, which is
+ * typically AccessExclusiveLock, except if it's a global temporary relation,
+ * in which case RowExclusiveLock is sufficient.
*/
void
heap_truncate_one_rel(Relation rel)
{
+ LOCKMODE lockmode;
Oid toastrelid;
+ /*
+ * For a global temporary relation, RowExclusiveLock is sufficient.
+ * Otherwise must use AccessExclusiveLock.
+ */
+ lockmode = RELATION_IS_GLOBAL_TEMP(rel) ? RowExclusiveLock : AccessExclusiveLock;
+
/*
* Truncate the relation. Partitioned tables have no storage, so there is
* nothing to do for them here.
@@ -3685,16 +3704,16 @@ heap_truncate_one_rel(Relation rel)
table_relation_nontransactional_truncate(rel);
/* If the relation has indexes, truncate the indexes too */
- RelationTruncateIndexes(rel);
+ RelationTruncateIndexes(rel, lockmode);
/* If there is a toast table, truncate that too */
toastrelid = rel->rd_rel->reltoastrelid;
if (OidIsValid(toastrelid))
{
- Relation toastrel = table_open(toastrelid, AccessExclusiveLock);
+ Relation toastrel = table_open(toastrelid, lockmode);
table_relation_nontransactional_truncate(toastrel);
- RelationTruncateIndexes(toastrel);
+ RelationTruncateIndexes(toastrel, lockmode);
/* keep the lock... */
table_close(toastrel, NoLock);
}
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index f02d51704b4..943de8a4fda 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -123,6 +123,7 @@ static void UpdateIndexRelation(Oid indexoid, Oid heapoid,
bool isvalid,
bool isready);
static void index_update_stats(Relation rel,
+ bool isreindex,
bool hasindex,
double reltuples);
static void IndexCheckExclusion(Relation heapRelation,
@@ -1275,6 +1276,7 @@ index_create(Relation heapRelation,
* having an index.
*/
index_update_stats(heapRelation,
+ false,
true,
-1.0);
/* Make the above update visible */
@@ -2806,28 +2808,37 @@ FormIndexDatum(IndexInfo *indexInfo,
/*
- * index_update_stats --- update pg_class entry after CREATE INDEX or REINDEX
+ * index_update_stats --- update effective pg_class entry after CREATE INDEX
+ * or REINDEX
*
- * This routine updates the pg_class row of either an index or its parent
- * relation after CREATE INDEX or REINDEX. Its rather bizarre API is designed
- * to ensure we can do all the necessary work in just one update.
+ * This routine updates the effective pg_class row of either an index or its
+ * parent relation after CREATE INDEX or REINDEX. Its rather bizarre API is
+ * designed to ensure we can do all the necessary work in just one update
+ * (except for a global temporary relation, which requires both pg_class and
+ * pg_temp_class to be updated).
*
+ * isreindex: recreated a previously-existing index; don't set relhasindex
* hasindex: set relhasindex to this value
* reltuples: if >= 0, set reltuples to this value; else no change
*
* If reltuples >= 0, relpages, relallvisible, and relallfrozen are also
* updated (using RelationGetNumberOfBlocks() and visibilitymap_count()).
*
+ * For a global temporary relation, relhasindex is set in pg_class and all the
+ * other fields are set in pg_temp_class. For any other type of relation, all
+ * the fields are set in pg_class.
+ *
* NOTE: an important side-effect of this operation is that an SI invalidation
* message is sent out to all backends --- including me --- causing relcache
* entries to be flushed or updated with the new data. This must happen even
- * if we find that no change is needed in the pg_class row. When updating
- * a heap entry, this ensures that other backends find out about the new
- * index. When updating an index, it's important because some index AMs
- * expect a relcache flush to occur after REINDEX.
+ * if we find that no change is needed in the pg_class or pg_temp_class rows.
+ * When updating a heap entry, this ensures that other backends find out about
+ * the new index. When updating an index, it's important because some index
+ * AMs expect a relcache flush to occur after REINDEX.
*/
static void
index_update_stats(Relation rel,
+ bool isreindex,
bool hasindex,
double reltuples)
{
@@ -2839,9 +2850,12 @@ index_update_stats(Relation rel,
Relation pg_class;
ScanKeyData key[1];
HeapTuple tuple;
+ HeapTuple temp_tuple;
void *state;
Form_pg_class rd_rel;
+ Form_pg_temp_class temp_rd_rel;
bool dirty;
+ bool temp_dirty;
/*
* As a special hack, if we are dealing with an empty table and the
@@ -2924,16 +2938,56 @@ index_update_stats(Relation rel,
* relallvisible) if the caller isn't providing an updated reltuples
* count, because that would bollix the reltuples/relpages ratio which is
* what's really important.
+ *
+ * If not for (1) above, pg_temp_class could be updated normally, and in
+ * fact we could work round (1) by simply not updating pg_temp_class in
+ * bootstrap mode, since its value just gets thrown away when initdb
+ * finishes. However, for consistency, we update it in-place, like
+ * pg_class --- see also vac_update_relstats().
*/
- pg_class = table_open(RelationRelationId, RowExclusiveLock);
+ /*
+ * For a global temporary relation, need a writable copy of its
+ * pg_temp_class tuple. Note: can't use systable_inplace_update_begin()
+ * here because the tuple might be a pending insert --- see header
+ * comments in pg_temp_class.c.
+ */
+ if (RELATION_IS_GLOBAL_TEMP(rel))
+ {
+ temp_tuple = GetPgTempClassTuple(relid);
+ if (!HeapTupleIsValid(temp_tuple))
+ elog(ERROR, "cache lookup failed for global temp relation %u", relid);
+ temp_rd_rel = (Form_pg_temp_class) GETSTRUCT(temp_tuple);
+ }
+ else
+ {
+ temp_tuple = NULL;
+ temp_rd_rel = NULL;
+ }
- ScanKeyInit(&key[0],
- Anum_pg_class_oid,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(relid));
- systable_inplace_update_begin(pg_class, ClassOidIndexId, true, NULL,
- 1, key, &tuple, &state);
+ /*
+ * If this is a reindex on a global temporary table, we don't need to set
+ * pg_class.relhasindex, and all other fields go in pg_temp_class, so we
+ * only need a read-only copy of the pg_class tuple. Otherwise, we need a
+ * writable copy of the pg_class tuple to scribble on.
+ */
+ if (isreindex && RELATION_IS_GLOBAL_TEMP(rel))
+ {
+ pg_class = NULL;
+ tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+ state = NULL;
+ }
+ else
+ {
+ pg_class = table_open(RelationRelationId, RowExclusiveLock);
+
+ ScanKeyInit(&key[0],
+ Anum_pg_class_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(relid));
+ systable_inplace_update_begin(pg_class, ClassOidIndexId, true, NULL,
+ 1, key, &tuple, &state);
+ }
if (!HeapTupleIsValid(tuple))
elog(ERROR, "could not find tuple for relation %u", relid);
@@ -2942,10 +2996,11 @@ index_update_stats(Relation rel,
/* Should this be a more comprehensive test? */
Assert(rd_rel->relkind != RELKIND_PARTITIONED_INDEX);
- /* Apply required updates, if any, to copied tuple */
+ /* Apply required updates, if any, to copied tuple(s) */
dirty = false;
- if (rd_rel->relhasindex != hasindex)
+ temp_dirty = false;
+ if (!isreindex && rd_rel->relhasindex != hasindex)
{
rd_rel->relhasindex = hasindex;
dirty = true;
@@ -2953,30 +3008,18 @@ index_update_stats(Relation rel,
if (update_stats)
{
- if (rd_rel->relpages != (int32) relpages)
- {
- rd_rel->relpages = (int32) relpages;
- dirty = true;
- }
- if (rd_rel->reltuples != (float4) reltuples)
- {
- rd_rel->reltuples = (float4) reltuples;
- dirty = true;
- }
- if (rd_rel->relallvisible != (int32) relallvisible)
- {
- rd_rel->relallvisible = (int32) relallvisible;
- dirty = true;
- }
- if (rd_rel->relallfrozen != (int32) relallfrozen)
- {
- rd_rel->relallfrozen = (int32) relallfrozen;
- dirty = true;
- }
+ SetEffective_relpages(rd_rel, temp_rd_rel, (int32) relpages,
+ &dirty, &temp_dirty);
+ SetEffective_reltuples(rd_rel, temp_rd_rel, (float4) reltuples,
+ &dirty, &temp_dirty);
+ SetEffective_relallvisible(rd_rel, temp_rd_rel, (int32) relallvisible,
+ &dirty, &temp_dirty);
+ SetEffective_relallfrozen(rd_rel, temp_rd_rel, (int32) relallfrozen,
+ &dirty, &temp_dirty);
}
/*
- * If anything changed, write out the tuple
+ * If anything changed, write out the tuple(s)
*/
if (dirty)
{
@@ -2985,7 +3028,8 @@ index_update_stats(Relation rel,
}
else
{
- systable_inplace_update_cancel(state);
+ if (state != NULL)
+ systable_inplace_update_cancel(state);
/*
* While we didn't change relhasindex, CREATE INDEX needs a
@@ -2997,9 +3041,18 @@ index_update_stats(Relation rel,
CacheInvalidateRelcacheByTuple(tuple);
}
+ if (HeapTupleIsValid(temp_tuple))
+ {
+ if (temp_dirty)
+ UpdatePgTempClassTupleInPlace(relid, temp_tuple);
+
+ heap_freetuple(temp_tuple);
+ }
+
heap_freetuple(tuple);
- table_close(pg_class, RowExclusiveLock);
+ if (RelationIsValid(pg_class))
+ table_close(pg_class, RowExclusiveLock);
}
@@ -3177,11 +3230,11 @@ index_build(Relation heapRelation,
* Update heap and index pg_class rows
*/
index_update_stats(heapRelation,
- true,
+ isreindex, true,
stats->heap_tuples);
index_update_stats(indexRelation,
- false,
+ isreindex, false,
stats->index_tuples);
/* Make the updated catalog row versions visible */
diff --git a/src/backend/catalog/pg_temp_class.c b/src/backend/catalog/pg_temp_class.c
index 1c68444c875..1372c65b7c1 100644
--- a/src/backend/catalog/pg_temp_class.c
+++ b/src/backend/catalog/pg_temp_class.c
@@ -58,6 +58,7 @@
#include "catalog/indexing.h"
#include "catalog/pg_temp_class.h"
#include "miscadmin.h"
+#include "utils/fmgroids.h"
#include "utils/hsearch.h"
#include "utils/memutils.h"
#include "utils/syscache.h"
@@ -176,6 +177,18 @@ get_pg_temp_class_tupdesc(void)
TupleDescInitEntry(tupdesc,
(AttrNumber) Anum_pg_temp_class_reltablespace,
"reltablespace", OIDOID, -1, 0);
+ TupleDescInitEntry(tupdesc,
+ (AttrNumber) Anum_pg_temp_class_relpages,
+ "relpages", INT4OID, -1, 0);
+ TupleDescInitEntry(tupdesc,
+ (AttrNumber) Anum_pg_temp_class_reltuples,
+ "reltuples", FLOAT4OID, -1, 0);
+ TupleDescInitEntry(tupdesc,
+ (AttrNumber) Anum_pg_temp_class_relallvisible,
+ "relallvisible", INT4OID, -1, 0);
+ TupleDescInitEntry(tupdesc,
+ (AttrNumber) Anum_pg_temp_class_relallfrozen,
+ "relallfrozen", INT4OID, -1, 0);
TupleDescFinalize(tupdesc);
MemoryContextSwitchTo(oldcontext);
@@ -202,6 +215,10 @@ heap_form_pg_temp_class_tuple(Relation rel)
values[Anum_pg_temp_class_oid - 1] = ObjectIdGetDatum(RelationGetRelid(rel));
values[Anum_pg_temp_class_relfilenode - 1] = ObjectIdGetDatum(form->relfilenode);
values[Anum_pg_temp_class_reltablespace - 1] = ObjectIdGetDatum(form->reltablespace);
+ values[Anum_pg_temp_class_relpages - 1] = Int32GetDatum(form->relpages);
+ values[Anum_pg_temp_class_reltuples - 1] = Float4GetDatum(form->reltuples);
+ values[Anum_pg_temp_class_relallvisible - 1] = Int32GetDatum(form->relallvisible);
+ values[Anum_pg_temp_class_relallfrozen - 1] = Int32GetDatum(form->relallfrozen);
return heap_form_tuple(get_pg_temp_class_tupdesc(), values, nulls);
}
@@ -429,6 +446,80 @@ UpdatePgTempClassTuple(Oid relid, HeapTuple newtuple)
table_close(pg_temp_class, RowExclusiveLock);
}
+/*
+ * UpdatePgTempClassTupleInPlace
+ *
+ * Do an in-place update of the pg_temp_class tuple for a global temporary
+ * relation.
+ */
+void
+UpdatePgTempClassTupleInPlace(Oid relid, HeapTuple newtuple)
+{
+ Relation pg_temp_class;
+ ScanKeyData key[1];
+ HeapTuple oldtuple;
+ void *inplace_state;
+
+ /* Is there a pending insert for this relation? */
+ if (pending_inserts != NULL)
+ {
+ PendingInsert *entry;
+
+ entry = hash_search(pending_inserts, &relid, HASH_FIND, NULL);
+ if (entry != NULL)
+ {
+ Form_pg_temp_class old_form;
+ Form_pg_temp_class new_form;
+
+ /* Should not have been deleted */
+ if (entry->deleted)
+ elog(ERROR,
+ "pending insert for global temp relation %u was deleted",
+ relid);
+
+ /* Update the entry, saving a copy for rollback, if necessary */
+ prepare_pending_insert_for_edit(entry);
+ old_form = (Form_pg_temp_class) GETSTRUCT(entry->tuple);
+ new_form = (Form_pg_temp_class) GETSTRUCT(newtuple);
+ COPY_PG_TEMP_CLASS_ATTRS(new_form, old_form);
+
+ /*
+ * If it has not yet been flushed to the database, do so now. This
+ * is important, because the caller might be relying on a relcache
+ * invalidation being triggered.
+ */
+ if (!entry->flushed)
+ {
+ pg_temp_class = open_pg_temp_class(RowExclusiveLock);
+ CatalogTupleInsert(pg_temp_class, newtuple);
+ table_close(pg_temp_class, RowExclusiveLock);
+ entry->flushed = true;
+ return;
+ }
+ }
+ }
+
+ /* Do an in-place update of the tuple in the database */
+ pg_temp_class = open_pg_temp_class(RowExclusiveLock);
+
+ ScanKeyInit(&key[0],
+ Anum_pg_temp_class_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(relid));
+
+ systable_inplace_update_begin(pg_temp_class, TempClassOidIndexId, true,
+ NULL, 1, key, &oldtuple, &inplace_state);
+ if (!HeapTupleIsValid(oldtuple))
+ elog(ERROR, "cache lookup failed for global temp relation %u", relid);
+
+ ItemPointerCopy(&oldtuple->t_self, &newtuple->t_self);
+ systable_inplace_update_finish(inplace_state, newtuple);
+
+ heap_freetuple(oldtuple);
+
+ table_close(pg_temp_class, RowExclusiveLock);
+}
+
/*
* DeletePgTempClassTuple
*
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 81bc7dcc312..5f11e699dc4 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -1298,7 +1298,9 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
{
Relation relRelation;
HeapTuple reltup;
+ HeapTuple temp_reltup;
Form_pg_class relform;
+ Form_pg_temp_class temp_relform;
TupleDesc oldTupDesc PG_USED_FOR_ASSERTS_ONLY;
TupleDesc newTupDesc PG_USED_FOR_ASSERTS_ONLY;
VacuumParams params;
@@ -1490,21 +1492,30 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
tups_recently_dead,
pg_rusage_show(&ru0))));
- /* Update pg_class to reflect the correct values of pages and tuples. */
+ /*
+ * Update pg_class / pg_temp_class to reflect the correct values of pages
+ * and tuples.
+ */
relRelation = table_open(RelationRelationId, RowExclusiveLock);
- reltup = SearchSysCacheCopy1(RELOID,
- ObjectIdGetDatum(RelationGetRelid(NewHeap)));
+ reltup = GetPgClassAndPgTempClassTuples(RelationGetRelid(NewHeap), false,
+ &temp_reltup, true);
if (!HeapTupleIsValid(reltup))
elog(ERROR, "cache lookup failed for relation %u",
RelationGetRelid(NewHeap));
relform = (Form_pg_class) GETSTRUCT(reltup);
+ temp_relform = (Form_pg_temp_class) GETSTRUCT_SAFE(temp_reltup);
- relform->relpages = num_pages;
- relform->reltuples = num_tuples;
+ SetEffective_relpages(relform, temp_relform, num_pages, NULL, NULL);
+ SetEffective_reltuples(relform, temp_relform, num_tuples, NULL, NULL);
/* Don't update the stats for pg_class. See swap_relation_files. */
- if (RelationGetRelid(OldHeap) != RelationRelationId)
+ if (HeapTupleIsValid(temp_reltup))
+ {
+ UpdatePgTempClassTuple(RelationGetRelid(NewHeap), temp_reltup);
+ heap_freetuple(temp_reltup);
+ }
+ else if (RelationGetRelid(OldHeap) != RelationRelationId)
CatalogTupleUpdate(relRelation, &reltup->t_self, reltup);
else
CacheInvalidateRelcacheByTuple(reltup);
@@ -1733,21 +1744,29 @@ swap_relation_files(Oid r1, Oid r2, bool target_is_pg_class,
int32 swap_allvisible;
int32 swap_allfrozen;
- swap_pages = relform1->relpages;
- relform1->relpages = relform2->relpages;
- relform2->relpages = swap_pages;
-
- swap_tuples = relform1->reltuples;
- relform1->reltuples = relform2->reltuples;
- relform2->reltuples = swap_tuples;
-
- swap_allvisible = relform1->relallvisible;
- relform1->relallvisible = relform2->relallvisible;
- relform2->relallvisible = swap_allvisible;
-
- swap_allfrozen = relform1->relallfrozen;
- relform1->relallfrozen = relform2->relallfrozen;
- relform2->relallfrozen = swap_allfrozen;
+ swap_pages = GetEffective_relpages(relform1, temp_relform1);
+ SetEffective_relpages(relform1, temp_relform1,
+ GetEffective_relpages(relform2, temp_relform2),
+ NULL, NULL);
+ SetEffective_relpages(relform2, temp_relform2, swap_pages, NULL, NULL);
+
+ swap_tuples = GetEffective_reltuples(relform1, temp_relform1);
+ SetEffective_reltuples(relform1, temp_relform1,
+ GetEffective_reltuples(relform2, temp_relform2),
+ NULL, NULL);
+ SetEffective_reltuples(relform2, temp_relform2, swap_tuples, NULL, NULL);
+
+ swap_allvisible = GetEffective_relallvisible(relform1, temp_relform1);
+ SetEffective_relallvisible(relform1, temp_relform1,
+ GetEffective_relallvisible(relform2, temp_relform2),
+ NULL, NULL);
+ SetEffective_relallvisible(relform2, temp_relform2, swap_allvisible, NULL, NULL);
+
+ swap_allfrozen = GetEffective_relallfrozen(relform1, temp_relform1);
+ SetEffective_relallfrozen(relform1, temp_relform1,
+ GetEffective_relallfrozen(relform2, temp_relform2),
+ NULL, NULL);
+ SetEffective_relallfrozen(relform2, temp_relform2, swap_allfrozen, NULL, NULL);
}
/*
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 4e15028217c..9c776d5a978 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -1393,9 +1393,10 @@ vac_estimate_reltuples(Relation relation,
* vac_update_relstats() -- update statistics for one relation
*
* Update the whole-relation statistics that are kept in its pg_class
- * row. There are additional stats that will be updated if we are
- * doing ANALYZE, but we always update these stats. This routine works
- * for both index and heap relation entries in pg_class.
+ * row (and its pg_temp_class row, for a global temporary relation).
+ * There are additional stats that will be updated if we are doing
+ * ANALYZE, but we always update these stats. This routine works for both
+ * index and heap relation entries in pg_class and pg_temp_class.
*
* We violate transaction semantics here by overwriting the rel's
* existing pg_class tuple with the new values. This is reasonably
@@ -1404,12 +1405,17 @@ vac_estimate_reltuples(Relation relation,
* we updated these tuples in the usual way, vacuuming pg_class itself
* wouldn't work very well --- by the time we got done with a vacuum
* cycle, most of the tuples in pg_class would've been obsoleted. Of
- * course, this only works for fixed-size not-null columns, but these are.
+ * course, this only works for fixed-size not-null columns, but these
+ * are. Likewise for pg_temp_class, which is also updated for a global
+ * temporary relation.
*
* Another reason for doing it this way is that when we are in a lazy
* VACUUM and have PROC_IN_VACUUM set, we mustn't do any regular updates.
* Somebody vacuuming pg_class might think they could delete a tuple
- * marked with xmin = our xid.
+ * marked with xmin = our xid. This isn't a problem for pg_temp_class
+ * because no other session can see our copy of its data, but it still
+ * makes sense to do an in-place update to avoid vacuumed pg_temp_class
+ * tuples being obsoleted.
*
* In addition to fundamentally nontransactional statistics such as
* relpages and relallvisible, we try to maintain certain lazily-updated
@@ -1424,8 +1430,8 @@ vac_estimate_reltuples(Relation relation,
* transaction. This is OK since postponing the flag maintenance is
* always allowable.
*
- * Note: num_tuples should count only *live* tuples, since
- * pg_class.reltuples is defined that way.
+ * Note: num_tuples should count only *live* tuples, since reltuples in
+ * pg_class and pg_temp_class is defined that way.
*
* This routine is shared by VACUUM and ANALYZE.
*/
@@ -1443,17 +1449,39 @@ vac_update_relstats(Relation relation,
Relation rd;
ScanKeyData key[1];
HeapTuple ctup;
+ HeapTuple temp_ctup;
void *inplace_state;
Form_pg_class pgcform;
+ Form_pg_temp_class temp_pgcform;
bool dirty,
+ temp_dirty,
futurexid,
futuremxid;
TransactionId oldfrozenxid;
MultiXactId oldminmulti;
+ /*
+ * For a global temporary relation, need a writable copy of its
+ * pg_temp_class tuple. Note: can't use systable_inplace_update_begin()
+ * here because the tuple might be a pending insert --- see header
+ * comments in pg_temp_class.c.
+ */
+ if (RELATION_IS_GLOBAL_TEMP(relation))
+ {
+ temp_ctup = GetPgTempClassTuple(relid);
+ if (!HeapTupleIsValid(temp_ctup))
+ elog(ERROR, "cache lookup failed for global temp relation %u", relid);
+ temp_pgcform = (Form_pg_temp_class) GETSTRUCT(temp_ctup);
+ }
+ else
+ {
+ temp_ctup = NULL;
+ temp_pgcform = NULL;
+ }
+
+ /* Fetch a copy of the pg_class tuple to scribble on */
rd = table_open(RelationRelationId, RowExclusiveLock);
- /* Fetch a copy of the tuple to scribble on */
ScanKeyInit(&key[0],
Anum_pg_class_oid,
BTEqualStrategyNumber, F_OIDEQ,
@@ -1465,29 +1493,20 @@ vac_update_relstats(Relation relation,
relid);
pgcform = (Form_pg_class) GETSTRUCT(ctup);
- /* Apply statistical updates, if any, to copied tuple */
+ /* Apply statistical updates, if any, to copied tuple(s) */
dirty = false;
- if (pgcform->relpages != (int32) num_pages)
- {
- pgcform->relpages = (int32) num_pages;
- dirty = true;
- }
- if (pgcform->reltuples != (float4) num_tuples)
- {
- pgcform->reltuples = (float4) num_tuples;
- dirty = true;
- }
- if (pgcform->relallvisible != (int32) num_all_visible_pages)
- {
- pgcform->relallvisible = (int32) num_all_visible_pages;
- dirty = true;
- }
- if (pgcform->relallfrozen != (int32) num_all_frozen_pages)
- {
- pgcform->relallfrozen = (int32) num_all_frozen_pages;
- dirty = true;
- }
+ temp_dirty = false;
+ SetEffective_relpages(pgcform, temp_pgcform, (int32) num_pages,
+ &dirty, &temp_dirty);
+ SetEffective_reltuples(pgcform, temp_pgcform, (float4) num_tuples,
+ &dirty, &temp_dirty);
+ SetEffective_relallvisible(pgcform, temp_pgcform,
+ (int32) num_all_visible_pages,
+ &dirty, &temp_dirty);
+ SetEffective_relallfrozen(pgcform, temp_pgcform,
+ (int32) num_all_frozen_pages,
+ &dirty, &temp_dirty);
/* Apply DDL updates, but not inside an outer transaction (see above) */
@@ -1570,12 +1589,22 @@ vac_update_relstats(Relation relation,
}
}
- /* If anything changed, write out the tuple. */
+ /* If anything changed, write out the tuple(s) */
if (dirty)
systable_inplace_update_finish(inplace_state, ctup);
else
systable_inplace_update_cancel(inplace_state);
+ if (HeapTupleIsValid(temp_ctup))
+ {
+ if (temp_dirty)
+ UpdatePgTempClassTupleInPlace(relid, temp_ctup);
+
+ heap_freetuple(temp_ctup);
+ }
+
+ heap_freetuple(ctup);
+
table_close(rd, RowExclusiveLock);
if (futurexid)
diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index d6631e9a9a4..26ec5b24692 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -20,6 +20,7 @@
#include "access/heapam.h"
#include "catalog/indexing.h"
#include "catalog/namespace.h"
+#include "catalog/pg_temp_class.h"
#include "nodes/makefuncs.h"
#include "statistics/stat_utils.h"
#include "utils/builtins.h"
@@ -79,10 +80,11 @@ relation_statistics_update(FunctionCallInfo fcinfo)
bool update_relallfrozen = false;
HeapTuple ctup;
Form_pg_class pgcform;
- int replaces[4] = {0};
- Datum values[4] = {0};
- bool nulls[4] = {0};
- int nreplaces = 0;
+ Relation rel;
+ HeapTuple temp_ctup;
+ Form_pg_temp_class temp_pgcform;
+ bool dirty;
+ bool temp_dirty;
Oid locked_table = InvalidOid;
stats_check_required_arg(fcinfo, relarginfo, RELSCHEMA_ARG);
@@ -139,52 +141,64 @@ relation_statistics_update(FunctionCallInfo fcinfo)
*/
crel = table_open(RelationRelationId, RowExclusiveLock);
- ctup = SearchSysCache1(RELOID, ObjectIdGetDatum(reloid));
+ ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(reloid));
if (!HeapTupleIsValid(ctup))
elog(ERROR, "pg_class entry for relid %u not found", reloid);
pgcform = (Form_pg_class) GETSTRUCT(ctup);
- if (update_relpages && relpages != pgcform->relpages)
+ /*
+ * For a global temporary table, need to update the pg_temp_class tuple
+ * instead. Force it into existence by opening the relation.
+ */
+ if (pgcform->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
{
- replaces[nreplaces] = Anum_pg_class_relpages;
- values[nreplaces] = UInt32GetDatum(relpages);
- nreplaces++;
- }
+ rel = relation_open(reloid, AccessShareLock);
+ relation_close(rel, AccessShareLock);
- if (update_reltuples && reltuples != pgcform->reltuples)
- {
- replaces[nreplaces] = Anum_pg_class_reltuples;
- values[nreplaces] = Float4GetDatum(reltuples);
- nreplaces++;
- }
+ temp_ctup = GetPgTempClassTuple(reloid);
+ if (!HeapTupleIsValid(temp_ctup))
+ elog(ERROR, "pg_temp_class entry for relid %u not found", reloid);
- if (update_relallvisible && relallvisible != pgcform->relallvisible)
- {
- replaces[nreplaces] = Anum_pg_class_relallvisible;
- values[nreplaces] = UInt32GetDatum(relallvisible);
- nreplaces++;
+ temp_pgcform = (Form_pg_temp_class) GETSTRUCT(temp_ctup);
}
-
- if (update_relallfrozen && relallfrozen != pgcform->relallfrozen)
+ else
{
- replaces[nreplaces] = Anum_pg_class_relallfrozen;
- values[nreplaces] = UInt32GetDatum(relallfrozen);
- nreplaces++;
+ temp_ctup = NULL;
+ temp_pgcform = NULL;
}
- if (nreplaces > 0)
+ dirty = false;
+ temp_dirty = false;
+
+ if (update_relpages)
+ SetEffective_relpages(pgcform, temp_pgcform, (int32) relpages,
+ &dirty, &temp_dirty);
+
+ if (update_reltuples)
+ SetEffective_reltuples(pgcform, temp_pgcform, (float4) reltuples,
+ &dirty, &temp_dirty);
+
+ if (update_relallvisible)
+ SetEffective_relallvisible(pgcform, temp_pgcform, (int32) relallvisible,
+ &dirty, &temp_dirty);
+
+ if (update_relallfrozen)
+ SetEffective_relallfrozen(pgcform, temp_pgcform, (int32) relallfrozen,
+ &dirty, &temp_dirty);
+
+ if (dirty)
+ CatalogTupleUpdate(crel, &ctup->t_self, ctup);
+
+ if (HeapTupleIsValid(temp_ctup))
{
- TupleDesc tupdesc = RelationGetDescr(crel);
- HeapTuple newtup;
+ if (temp_dirty)
+ UpdatePgTempClassTuple(reloid, temp_ctup);
- newtup = heap_modify_tuple_by_cols(ctup, tupdesc, nreplaces,
- replaces, values, nulls);
- CatalogTupleUpdate(crel, &newtup->t_self, newtup);
- heap_freetuple(newtup);
+ heap_freetuple(temp_ctup);
}
- ReleaseSysCache(ctup);
+ heap_freetuple(ctup);
/* release the lock, consistent with vac_update_relstats() */
table_close(crel, RowExclusiveLock);
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index cbd25fa6144..e8068a45cc8 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -2411,6 +2411,17 @@ get_rel_persistence(Oid relid)
return result;
}
+/*
+ * rel_is_global_temp
+ *
+ * Returns true if the given relation is a global temporary relation.
+ */
+bool
+rel_is_global_temp(Oid relid)
+{
+ return get_rel_persistence(relid) == RELPERSISTENCE_GLOBAL_TEMP;
+}
+
/*
* get_rel_relam
*
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index af8b5237e45..274132ced87 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4040,10 +4040,11 @@ RelationSetNewRelfilenumber(Relation relation, char persistence)
/* relpages etc. never change for sequences */
if (relation->rd_rel->relkind != RELKIND_SEQUENCE)
{
- classform->relpages = 0; /* it's empty until further notice */
- classform->reltuples = -1;
- classform->relallvisible = 0;
- classform->relallfrozen = 0;
+ /* it's empty until further notice */
+ SetEffective_relpages(classform, temp_classform, 0, NULL, NULL);
+ SetEffective_reltuples(classform, temp_classform, -1, NULL, NULL);
+ SetEffective_relallvisible(classform, temp_classform, 0, NULL, NULL);
+ SetEffective_relallfrozen(classform, temp_classform, 0, NULL, NULL);
}
classform->relfrozenxid = freezeXid;
classform->relminmxid = minmulti;
diff --git a/src/include/catalog/pg_temp_class.h b/src/include/catalog/pg_temp_class.h
index 80011ea0954..e6b77b50d23 100644
--- a/src/include/catalog/pg_temp_class.h
+++ b/src/include/catalog/pg_temp_class.h
@@ -45,6 +45,18 @@ CATALOG(pg_temp_class,8082,TempRelationRelationId) BKI_TEMP_RELATION
/* identifier of table space for relation (0 means default for database) */
Oid reltablespace BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_tablespace);
+
+ /* # of blocks (not always up-to-date) */
+ int32 relpages BKI_DEFAULT(0);
+
+ /* # of tuples (not always up-to-date; -1 means "unknown") */
+ float4 reltuples BKI_DEFAULT(-1);
+
+ /* # of all-visible blocks (not always up-to-date) */
+ int32 relallvisible BKI_DEFAULT(0);
+
+ /* # of all-frozen blocks (not always up-to-date) */
+ int32 relallfrozen BKI_DEFAULT(0);
} FormData_pg_temp_class;
END_CATALOG_STRUCT
@@ -71,6 +83,10 @@ MAKE_SYSCACHE(TEMPRELOID, pg_temp_class_oid_index, 128);
(target)->oid = (source)->oid; \
(target)->relfilenode = (source)->relfilenode; \
(target)->reltablespace = (source)->reltablespace; \
+ (target)->relpages = (source)->relpages; \
+ (target)->reltuples = (source)->reltuples; \
+ (target)->relallvisible = (source)->relallvisible; \
+ (target)->relallfrozen = (source)->relallfrozen; \
} while (0)
/*
@@ -93,6 +109,46 @@ GetEffective_reltablespace(Form_pg_class cf, Form_pg_temp_class tf)
return tf != NULL ? tf->reltablespace : cf->reltablespace;
}
+/*
+ * Get the effective value of relpages from pg_class and pg_temp_class tuple
+ * data. The value from pg_temp_class (if present) takes precedence.
+ */
+static inline int32
+GetEffective_relpages(Form_pg_class cf, Form_pg_temp_class tf)
+{
+ return tf != NULL ? tf->relpages : cf->relpages;
+}
+
+/*
+ * Get the effective value of reltuples from pg_class and pg_temp_class tuple
+ * data. The value from pg_temp_class (if present) takes precedence.
+ */
+static inline float4
+GetEffective_reltuples(Form_pg_class cf, Form_pg_temp_class tf)
+{
+ return tf != NULL ? tf->reltuples : cf->reltuples;
+}
+
+/*
+ * Get the effective value of relallvisible from pg_class and pg_temp_class
+ * tuple data. The value from pg_temp_class (if present) takes precedence.
+ */
+static inline int32
+GetEffective_relallvisible(Form_pg_class cf, Form_pg_temp_class tf)
+{
+ return tf != NULL ? tf->relallvisible : cf->relallvisible;
+}
+
+/*
+ * Get the effective value of relallfrozen from pg_class and pg_temp_class
+ * tuple data. The value from pg_temp_class (if present) takes precedence.
+ */
+static inline int32
+GetEffective_relallfrozen(Form_pg_class cf, Form_pg_temp_class tf)
+{
+ return tf != NULL ? tf->relallfrozen : cf->relallfrozen;
+}
+
/*
* Set the effective value of relfilenode in tuple form data from pg_class or
* pg_temp_class. The value is set in pg_temp_class instead of pg_class, if
@@ -121,6 +177,114 @@ SetEffective_reltablespace(Form_pg_class cf, Form_pg_temp_class tf, Oid val)
tf->reltablespace = val;
}
+/*
+ * Set the effective value of relpages in tuple form data from pg_class or
+ * pg_temp_class. The value is set in pg_temp_class instead of pg_class, if
+ * the pg_temp_class tuple form data is non-NULL. If non-NULL, the cdirty or
+ * tdirty flag is updated, if the value actually changes.
+ */
+static inline void
+SetEffective_relpages(Form_pg_class cf, Form_pg_temp_class tf, int32 val,
+ bool *cdirty, bool *tdirty)
+{
+ if (tf != NULL)
+ {
+ if (val != tf->relpages)
+ {
+ tf->relpages = val;
+ if (tdirty != NULL)
+ *tdirty = true;
+ }
+ }
+ else if (val != cf->relpages)
+ {
+ cf->relpages = val;
+ if (cdirty != NULL)
+ *cdirty = true;
+ }
+}
+
+/*
+ * Set the effective value of reltuples in tuple form data from pg_class or
+ * pg_temp_class. The value is set in pg_temp_class instead of pg_class, if
+ * the pg_temp_class tuple form data is non-NULL. If non-NULL, the cdirty or
+ * tdirty flag is updated, if the value actually changes.
+ */
+static inline void
+SetEffective_reltuples(Form_pg_class cf, Form_pg_temp_class tf, float4 val,
+ bool *cdirty, bool *tdirty)
+{
+ if (tf != NULL)
+ {
+ if (val != tf->reltuples)
+ {
+ tf->reltuples = val;
+ if (tdirty != NULL)
+ *tdirty = true;
+ }
+ }
+ else if (val != cf->reltuples)
+ {
+ cf->reltuples = val;
+ if (cdirty != NULL)
+ *cdirty = true;
+ }
+}
+
+/*
+ * Set the effective value of relallvisible in tuple form data from pg_class
+ * or pg_temp_class. The value is set in pg_temp_class instead of pg_class,
+ * if the pg_temp_class tuple form data is non-NULL. If non-NULL, the cdirty
+ * or tdirty flag is updated, if the value actually changes.
+ */
+static inline void
+SetEffective_relallvisible(Form_pg_class cf, Form_pg_temp_class tf, int32 val,
+ bool *cdirty, bool *tdirty)
+{
+ if (tf != NULL)
+ {
+ if (val != tf->relallvisible)
+ {
+ tf->relallvisible = val;
+ if (tdirty != NULL)
+ *tdirty = true;
+ }
+ }
+ else if (val != cf->relallvisible)
+ {
+ cf->relallvisible = val;
+ if (cdirty != NULL)
+ *cdirty = true;
+ }
+}
+
+/*
+ * Set the effective value of relallfrozen in tuple form data from pg_class or
+ * pg_temp_class. The value is set in pg_temp_class instead of pg_class, if
+ * the pg_temp_class tuple form data is non-NULL. If non-NULL, the cdirty or
+ * tdirty flag is updated, if the value actually changes.
+ */
+static inline void
+SetEffective_relallfrozen(Form_pg_class cf, Form_pg_temp_class tf, int32 val,
+ bool *cdirty, bool *tdirty)
+{
+ if (tf != NULL)
+ {
+ if (val != tf->relallfrozen)
+ {
+ tf->relallfrozen = val;
+ if (tdirty != NULL)
+ *tdirty = true;
+ }
+ }
+ else if (val != cf->relallfrozen)
+ {
+ cf->relallfrozen = val;
+ if (cdirty != NULL)
+ *cdirty = true;
+ }
+}
+
extern HeapTuple GetPgTempClassTuple(Oid relid);
@@ -128,6 +292,8 @@ extern void InsertPgTempClassTuple(Relation rel);
extern void UpdatePgTempClassTuple(Oid relid, HeapTuple newtuple);
+extern void UpdatePgTempClassTupleInPlace(Oid relid, HeapTuple newtuple);
+
extern void DeletePgTempClassTuple(Oid relid);
extern HeapTuple GetPgClassAndPgTempClassTuples(Oid relid, bool lock_tuple,
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 865980cb0f1..c556b667c5e 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -150,6 +150,7 @@ extern char get_rel_relkind(Oid relid);
extern bool get_rel_relispartition(Oid relid);
extern Oid get_rel_tablespace(Oid relid);
extern char get_rel_persistence(Oid relid);
+extern bool rel_is_global_temp(Oid relid);
extern Oid get_rel_relam(Oid relid);
extern Oid get_transform_fromsql(Oid typid, Oid langid, List *trftypes);
extern Oid get_transform_tosql(Oid typid, Oid langid, List *trftypes);
diff --git a/src/test/isolation/expected/global-temp.out b/src/test/isolation/expected/global-temp.out
index 65025721db1..bbaa768f479 100644
--- a/src/test/isolation/expected/global-temp.out
+++ b/src/test/isolation/expected/global-temp.out
@@ -252,6 +252,38 @@ key|pg_typeof|val
step drop1: DROP TABLE tmp2;
+starting permutation: create1dr b1 b2 ins1_2 ins2_2 sel1_2 sel2_2 c1 c2 sel1_2 sel2_2 drop1
+step create1dr: CREATE GLOBAL TEMP TABLE tmp2 (key int, val text) ON COMMIT DELETE ROWS;
+step b1: BEGIN;
+step b2: BEGIN;
+step ins1_2: INSERT INTO tmp2 VALUES (1, 's1');
+step ins2_2: INSERT INTO tmp2 VALUES (1, 's2');
+step sel1_2: SELECT * FROM tmp2;
+key|val
+---+---
+ 1|s1
+(1 row)
+
+step sel2_2: SELECT * FROM tmp2;
+key|val
+---+---
+ 1|s2
+(1 row)
+
+step c1: COMMIT;
+step c2: COMMIT;
+step sel1_2: SELECT * FROM tmp2;
+key|val
+---+---
+(0 rows)
+
+step sel2_2: SELECT * FROM tmp2;
+key|val
+---+---
+(0 rows)
+
+step drop1: DROP TABLE tmp2;
+
starting permutation: create1dr ins1_2 ins2_2 drop1 create1dr ins1_2 ins2_2 drop1
step create1dr: CREATE GLOBAL TEMP TABLE tmp2 (key int, val text) ON COMMIT DELETE ROWS;
step ins1_2: INSERT INTO tmp2 VALUES (1, 's1');
diff --git a/src/test/isolation/specs/global-temp.spec b/src/test/isolation/specs/global-temp.spec
index ecb07ad0b6e..a57fcfd3e53 100644
--- a/src/test/isolation/specs/global-temp.spec
+++ b/src/test/isolation/specs/global-temp.spec
@@ -14,6 +14,7 @@ teardown {
session s1
setup { SET allow_in_place_tablespaces = true; }
+step b1 { BEGIN; }
step create_tblspace { CREATE TABLESPACE regress_isolation_tablespace LOCATION ''; }
step list_tblspaces { SELECT spcname FROM pg_tablespace ORDER BY 1; }
step drop_tblspace { DROP TABLESPACE regress_isolation_tablespace; }
@@ -25,10 +26,12 @@ step sel1p { SELECT tableoid::regclass, * FROM tmp_parted; }
step create1 { CREATE GLOBAL TEMP TABLE tmp2 (key int, val text); }
step create1dr { CREATE GLOBAL TEMP TABLE tmp2 (key int, val text) ON COMMIT DELETE ROWS; }
step ins1_2 { INSERT INTO tmp2 VALUES (1, 's1'); }
+step sel1_2 { SELECT * FROM tmp2; }
step alter1a { ALTER TABLE tmp2 ALTER COLUMN key SET DATA TYPE numeric; }
step alter1b { ALTER TABLE tmp2 ALTER COLUMN val SET NOT NULL; }
step seltype1 { SELECT key, pg_typeof(key), val FROM tmp2; }
step drop1 { DROP TABLE tmp2; }
+step c1 { COMMIT; }
step idx1 { CREATE INDEX tmp_val_idx ON tmp(val); }
step sel1_idx {
SET enable_seqscan = off;
@@ -63,6 +66,7 @@ step r2 { ROLLBACK; }
step sp2 { SAVEPOINT sp; }
step rsp2 { ROLLBACK TO SAVEPOINT sp; }
step ins2_2 { INSERT INTO tmp2 VALUES (1, 's2'); }
+step sel2_2 { SELECT * FROM tmp2; }
step seltype2 { SELECT key, pg_typeof(key), val FROM tmp2; }
step sel2_idx {
SET enable_seqscan = off;
@@ -100,6 +104,9 @@ permutation ins1 b2 ins2 sp2 t2 rsp2 sel1 sel2 r2 sel1 sel2
permutation create1 ins1_2 alter1a alter1b ins2_2 seltype1 seltype2 drop1
permutation create1 ins1_2 ins2_2 alter1a alter1b seltype1 seltype2 drop1
+# Test concurrent ON COMMIT DELETE ROWS
+permutation create1dr b1 b2 ins1_2 ins2_2 sel1_2 sel2_2 c1 c2 sel1_2 sel2_2 drop1
+
# Test DROP with ON COMMIT DELETE ROWS
permutation create1dr ins1_2 ins2_2 drop1 create1dr ins1_2 ins2_2 drop1
diff --git a/src/test/regress/expected/global_temp.out b/src/test/regress/expected/global_temp.out
index cc9400284ff..8cec1b0cfca 100644
--- a/src/test/regress/expected/global_temp.out
+++ b/src/test/regress/expected/global_temp.out
@@ -494,6 +494,142 @@ SELECT * FROM tmp1;
1 | xxx | 1
(1 row)
+-- Test stats updates applied by CREATE INDEX, ANALYZE, VACUUM, and REPACK
+CREATE GLOBAL TEMP TABLE tmp2 (a int);
+INSERT INTO tmp2 SELECT * FROM generate_series(1, 100);
+SELECT c.oid::regclass,
+ c.relpages AS global_relpages, c.reltuples AS global_reltuples,
+ CASE WHEN t.relpages = 0 THEN 'zero' ELSE 'non-zero' END AS local_relpages,
+ t.reltuples AS local_reltuples
+ FROM pg_class c JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.oid = 'tmp2'::regclass;
+ oid | global_relpages | global_reltuples | local_relpages | local_reltuples
+------+-----------------+------------------+----------------+-----------------
+ tmp2 | 0 | -1 | zero | -1
+(1 row)
+
+CREATE INDEX tmp2_a_idx ON tmp2(a);
+SELECT c.oid::regclass,
+ c.relpages AS global_relpages, c.reltuples AS global_reltuples,
+ CASE WHEN t.relpages = 0 THEN 'zero' ELSE 'non-zero' END AS local_relpages,
+ t.reltuples AS local_reltuples
+ FROM pg_class c JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.oid = 'tmp2'::regclass OR c.oid = 'tmp2_a_idx'::regclass ORDER BY 1;
+ oid | global_relpages | global_reltuples | local_relpages | local_reltuples
+------------+-----------------+------------------+----------------+-----------------
+ tmp2 | 0 | -1 | non-zero | 100
+ tmp2_a_idx | 0 | 0 | non-zero | 100
+(2 rows)
+
+INSERT INTO tmp2 SELECT * FROM generate_series(101, 300);
+ANALYZE tmp2;
+SELECT c.oid::regclass,
+ c.relpages AS global_relpages, c.reltuples AS global_reltuples,
+ CASE WHEN t.relpages = 0 THEN 'zero' ELSE 'non-zero' END AS local_relpages,
+ t.reltuples AS local_reltuples
+ FROM pg_class c JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.oid = 'tmp2'::regclass OR c.oid = 'tmp2_a_idx'::regclass ORDER BY 1;
+ oid | global_relpages | global_reltuples | local_relpages | local_reltuples
+------------+-----------------+------------------+----------------+-----------------
+ tmp2 | 0 | -1 | non-zero | 300
+ tmp2_a_idx | 0 | 0 | non-zero | 300
+(2 rows)
+
+DELETE FROM tmp2 WHERE a % 2 = 0;
+VACUUM ANALYZE tmp2;
+SELECT c.oid::regclass,
+ c.relpages AS global_relpages, c.reltuples AS global_reltuples,
+ CASE WHEN t.relpages = 0 THEN 'zero' ELSE 'non-zero' END AS local_relpages,
+ t.reltuples AS local_reltuples
+ FROM pg_class c JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.oid = 'tmp2'::regclass OR c.oid = 'tmp2_a_idx'::regclass ORDER BY 1;
+ oid | global_relpages | global_reltuples | local_relpages | local_reltuples
+------------+-----------------+------------------+----------------+-----------------
+ tmp2 | 0 | -1 | non-zero | 150
+ tmp2_a_idx | 0 | 0 | non-zero | 150
+(2 rows)
+
+DELETE FROM tmp2 WHERE a % 3 = 0;
+REPACK (ANALYZE) tmp2;
+SELECT c.oid::regclass,
+ c.relpages AS global_relpages, c.reltuples AS global_reltuples,
+ CASE WHEN t.relpages = 0 THEN 'zero' ELSE 'non-zero' END AS local_relpages,
+ t.reltuples AS local_reltuples
+ FROM pg_class c JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.oid = 'tmp2'::regclass OR c.oid = 'tmp2_a_idx'::regclass ORDER BY 1;
+ oid | global_relpages | global_reltuples | local_relpages | local_reltuples
+------------+-----------------+------------------+----------------+-----------------
+ tmp2 | 0 | -1 | non-zero | 100
+ tmp2_a_idx | 0 | 0 | non-zero | 100
+(2 rows)
+
+-- Test stats usage
+CREATE FUNCTION row_estimate(query text) RETURNS int
+LANGUAGE plpgsql AS
+$$
+DECLARE
+ line text;
+BEGIN
+ FOR line IN EXECUTE FORMAT('EXPLAIN %s', query)
+ LOOP
+ RETURN (regexp_match(line, 'rows=(\d*)'))[1]::int;
+ END LOOP;
+END;
+$$;
+SELECT row_estimate('SELECT * FROM tmp2');
+ row_estimate
+--------------
+ 100
+(1 row)
+
+-- Test manually updating stats
+SELECT pg_clear_relation_stats('global_temp_tests', 'tmp2');
+ pg_clear_relation_stats
+-------------------------
+
+(1 row)
+
+SELECT oid::regclass, relpages, reltuples, relallvisible, relallfrozen
+ FROM pg_class WHERE oid = 'tmp2'::regclass;
+ oid | relpages | reltuples | relallvisible | relallfrozen
+------+----------+-----------+---------------+--------------
+ tmp2 | 0 | -1 | 0 | 0
+(1 row)
+
+SELECT oid::regclass, relpages, reltuples, relallvisible, relallfrozen
+ FROM pg_temp_class WHERE oid = 'tmp2'::regclass;
+ oid | relpages | reltuples | relallvisible | relallfrozen
+------+----------+-----------+---------------+--------------
+ tmp2 | 0 | -1 | 0 | 0
+(1 row)
+
+SELECT pg_restore_relation_stats(
+ 'schemaname', 'global_temp_tests',
+ 'relname', 'tmp2',
+ 'relpages', 5,
+ 'reltuples', 150::real,
+ 'relallvisible', 10,
+ 'relallfrozen', 20);
+ pg_restore_relation_stats
+---------------------------
+ t
+(1 row)
+
+SELECT oid::regclass, relpages, reltuples, relallvisible, relallfrozen
+ FROM pg_class WHERE oid = 'tmp2'::regclass;
+ oid | relpages | reltuples | relallvisible | relallfrozen
+------+----------+-----------+---------------+--------------
+ tmp2 | 0 | -1 | 0 | 0
+(1 row)
+
+SELECT oid::regclass, relpages, reltuples, relallvisible, relallfrozen
+ FROM pg_temp_class WHERE oid = 'tmp2'::regclass;
+ oid | relpages | reltuples | relallvisible | relallfrozen
+------+----------+-----------+---------------+--------------
+ tmp2 | 5 | 150 | 10 | 20
+(1 row)
+
+DROP TABLE tmp2;
-- Test view creation
CREATE VIEW v AS SELECT * FROM tmp1;
SELECT * FROM v;
diff --git a/src/test/regress/sql/global_temp.sql b/src/test/regress/sql/global_temp.sql
index a19b25b5bc6..921595862f2 100644
--- a/src/test/regress/sql/global_temp.sql
+++ b/src/test/regress/sql/global_temp.sql
@@ -285,6 +285,88 @@ COMMIT;
SELECT oid::regclass FROM pg_temp_class ORDER BY 1;
SELECT * FROM tmp1;
+-- Test stats updates applied by CREATE INDEX, ANALYZE, VACUUM, and REPACK
+CREATE GLOBAL TEMP TABLE tmp2 (a int);
+INSERT INTO tmp2 SELECT * FROM generate_series(1, 100);
+SELECT c.oid::regclass,
+ c.relpages AS global_relpages, c.reltuples AS global_reltuples,
+ CASE WHEN t.relpages = 0 THEN 'zero' ELSE 'non-zero' END AS local_relpages,
+ t.reltuples AS local_reltuples
+ FROM pg_class c JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.oid = 'tmp2'::regclass;
+
+CREATE INDEX tmp2_a_idx ON tmp2(a);
+SELECT c.oid::regclass,
+ c.relpages AS global_relpages, c.reltuples AS global_reltuples,
+ CASE WHEN t.relpages = 0 THEN 'zero' ELSE 'non-zero' END AS local_relpages,
+ t.reltuples AS local_reltuples
+ FROM pg_class c JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.oid = 'tmp2'::regclass OR c.oid = 'tmp2_a_idx'::regclass ORDER BY 1;
+
+INSERT INTO tmp2 SELECT * FROM generate_series(101, 300);
+ANALYZE tmp2;
+SELECT c.oid::regclass,
+ c.relpages AS global_relpages, c.reltuples AS global_reltuples,
+ CASE WHEN t.relpages = 0 THEN 'zero' ELSE 'non-zero' END AS local_relpages,
+ t.reltuples AS local_reltuples
+ FROM pg_class c JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.oid = 'tmp2'::regclass OR c.oid = 'tmp2_a_idx'::regclass ORDER BY 1;
+
+DELETE FROM tmp2 WHERE a % 2 = 0;
+VACUUM ANALYZE tmp2;
+SELECT c.oid::regclass,
+ c.relpages AS global_relpages, c.reltuples AS global_reltuples,
+ CASE WHEN t.relpages = 0 THEN 'zero' ELSE 'non-zero' END AS local_relpages,
+ t.reltuples AS local_reltuples
+ FROM pg_class c JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.oid = 'tmp2'::regclass OR c.oid = 'tmp2_a_idx'::regclass ORDER BY 1;
+
+DELETE FROM tmp2 WHERE a % 3 = 0;
+REPACK (ANALYZE) tmp2;
+SELECT c.oid::regclass,
+ c.relpages AS global_relpages, c.reltuples AS global_reltuples,
+ CASE WHEN t.relpages = 0 THEN 'zero' ELSE 'non-zero' END AS local_relpages,
+ t.reltuples AS local_reltuples
+ FROM pg_class c JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.oid = 'tmp2'::regclass OR c.oid = 'tmp2_a_idx'::regclass ORDER BY 1;
+
+-- Test stats usage
+CREATE FUNCTION row_estimate(query text) RETURNS int
+LANGUAGE plpgsql AS
+$$
+DECLARE
+ line text;
+BEGIN
+ FOR line IN EXECUTE FORMAT('EXPLAIN %s', query)
+ LOOP
+ RETURN (regexp_match(line, 'rows=(\d*)'))[1]::int;
+ END LOOP;
+END;
+$$;
+
+SELECT row_estimate('SELECT * FROM tmp2');
+
+-- Test manually updating stats
+SELECT pg_clear_relation_stats('global_temp_tests', 'tmp2');
+SELECT oid::regclass, relpages, reltuples, relallvisible, relallfrozen
+ FROM pg_class WHERE oid = 'tmp2'::regclass;
+SELECT oid::regclass, relpages, reltuples, relallvisible, relallfrozen
+ FROM pg_temp_class WHERE oid = 'tmp2'::regclass;
+
+SELECT pg_restore_relation_stats(
+ 'schemaname', 'global_temp_tests',
+ 'relname', 'tmp2',
+ 'relpages', 5,
+ 'reltuples', 150::real,
+ 'relallvisible', 10,
+ 'relallfrozen', 20);
+SELECT oid::regclass, relpages, reltuples, relallvisible, relallfrozen
+ FROM pg_class WHERE oid = 'tmp2'::regclass;
+SELECT oid::regclass, relpages, reltuples, relallvisible, relallfrozen
+ FROM pg_temp_class WHERE oid = 'tmp2'::regclass;
+
+DROP TABLE tmp2;
+
-- Test view creation
CREATE VIEW v AS SELECT * FROM tmp1;
SELECT * FROM v;
--
2.51.0
[text/x-patch] v8-0007-Add-relfrozenxid-and-relminmxid-columns-to-pg_tem.patch (55.8K, ../CAEZATCVjaZFa80S-V_+nvnzRK1ZiLUPx8jwdyAtRVHAmPZQQMg@mail.gmail.com/8-v8-0007-Add-relfrozenxid-and-relminmxid-columns-to-pg_tem.patch)
download | inline diff:
From bb1569986397e006fded150bb9ed804d2172d70f Mon Sep 17 00:00:00 2001
From: Dean Rasheed <[email protected]>
Date: Wed, 17 Jun 2026 10:04:41 +0100
Subject: [PATCH v8 07/10] Add relfrozenxid and relminmxid columns to
pg_temp_class.
This allows VACUUM results to be stored locally in pg_temp_class for
each global temporary table, while the corresponding pg_class fields
are kept invalid (zero). Additionally, each session stores its minimum
pg_temp_class.relfrozenxid and pg_temp_class.relminmxid values as
tempfrozenxid and tempminmxid in its PGPROC struct, allowing other
sessions to advance datfrozenxid and datminmxid, taking into account
the global temporary frozen XIDs from other sessions. Thus it is
possible to keep advancing datfrozenxid and datminmxid as long as each
session that uses global temporary tables runs VACUUM from time to
time. To help DBAs monitor this and diagnose problems, each backend's
tempfrozenxid and tempminmxid values are accessible via the
pg_stat_activity view.
---
src/backend/access/heap/vacuumlazy.c | 4 +-
src/backend/access/transam/twophase.c | 3 +
src/backend/catalog/global_temp.c | 17 +++
src/backend/catalog/heap.c | 13 +-
src/backend/catalog/pg_temp_class.c | 133 +++++++++++++++++
src/backend/catalog/system_views.sql | 2 +
src/backend/commands/repack.c | 28 +++-
src/backend/commands/vacuum.c | 138 ++++++++++++++++--
src/backend/storage/lmgr/proc.c | 7 +
src/backend/utils/adt/pgstatfuncs.c | 41 +++++-
src/backend/utils/cache/relcache.c | 78 +++++++---
src/include/catalog/pg_proc.dat | 6 +-
src/include/catalog/pg_temp_class.h | 84 +++++++++++
src/include/commands/vacuum.h | 2 +-
src/include/storage/proc.h | 9 ++
.../isolation/expected/vacuum-global-temp.out | 123 ++++++++++++++++
src/test/isolation/isolation_schedule | 1 +
.../isolation/specs/vacuum-global-temp.spec | 71 +++++++++
src/test/regress/expected/global_temp.out | 25 ++++
src/test/regress/expected/rules.out | 10 +-
src/test/regress/sql/global_temp.sql | 18 +++
21 files changed, 759 insertions(+), 54 deletions(-)
create mode 100644 src/test/isolation/expected/vacuum-global-temp.out
create mode 100644 src/test/isolation/specs/vacuum-global-temp.spec
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 39395aed0d5..cc77515ff58 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -919,7 +919,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
PROGRESS_VACUUM_PHASE_FINAL_CLEANUP);
/*
- * Prepare to update rel's pg_class entry.
+ * Prepare to update rel's pg_class and/or pg_temp_class entries.
*
* Aggressive VACUUMs must always be able to advance relfrozenxid to a
* value >= FreezeLimit, and relminmxid to a value >= MultiXactCutoff.
@@ -963,7 +963,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
new_rel_allfrozen = new_rel_allvisible;
/*
- * Now actually update rel's pg_class entry.
+ * Now actually update rel's pg_class and/or pg_temp_class entries.
*
* In principle new_live_tuples could be -1 indicating that we (still)
* don't know the tuple count. In practice that can't happen, since we
diff --git a/src/backend/access/transam/twophase.c b/src/backend/access/transam/twophase.c
index 1035e8b3fc7..094ff7ed58e 100644
--- a/src/backend/access/transam/twophase.c
+++ b/src/backend/access/transam/twophase.c
@@ -78,6 +78,7 @@
#include "access/commit_ts.h"
#include "access/htup_details.h"
+#include "access/multixact.h"
#include "access/subtrans.h"
#include "access/transam.h"
#include "access/twophase.h"
@@ -485,6 +486,8 @@ MarkAsPreparingGuts(GlobalTransaction gxact, FullTransactionId fxid,
/* subxid data must be filled later by GXactLoadSubxactData */
proc->subxidStatus.overflowed = false;
proc->subxidStatus.count = 0;
+ proc->tempfrozenxid = InvalidTransactionId;
+ proc->tempminmxid = InvalidMultiXactId;
gxact->prepared_at = prepared_at;
gxact->fxid = fxid;
diff --git a/src/backend/catalog/global_temp.c b/src/backend/catalog/global_temp.c
index 5f8a7e672eb..6cc89c59890 100644
--- a/src/backend/catalog/global_temp.c
+++ b/src/backend/catalog/global_temp.c
@@ -55,6 +55,7 @@
#include "access/amapi.h"
#include "access/genam.h"
+#include "access/multixact.h"
#include "access/parallel.h"
#include "access/table.h"
#include "access/tableam.h"
@@ -69,6 +70,7 @@
#include "miscadmin.h"
#include "storage/ipc.h"
#include "storage/lwlock.h"
+#include "storage/proc.h"
#include "storage/shmem.h"
#include "storage/subsystems.h"
#include "utils/memutils.h"
@@ -839,11 +841,23 @@ InitGlobalTempRelation(Relation relation)
{
/* Create (and track) storage for the relation */
if (RELKIND_HAS_TABLE_AM(relation->rd_rel->relkind))
+ {
table_relation_set_new_filelocator(relation,
&relation->rd_locator,
relation->rd_rel->relpersistence,
&relation->rd_rel->relfrozenxid,
&relation->rd_rel->relminmxid);
+
+ /*
+ * If tempfrozenxid and tempminmxid haven't been set for this
+ * backend, then set them now (first global temporary table
+ * accessed in this session).
+ */
+ if (!TransactionIdIsValid(MyProc->tempfrozenxid))
+ MyProc->tempfrozenxid = relation->rd_rel->relfrozenxid;
+ if (!MultiXactIdIsValid(MyProc->tempminmxid))
+ MyProc->tempminmxid = (MultiXactId) relation->rd_rel->relminmxid;
+ }
else
RelationCreateStorage(relation->rd_id,
relation->rd_locator,
@@ -1003,6 +1017,9 @@ GlobalTempRelationDropped(Oid relid)
/* Forget any ON COMMIT action for the relation */
remove_on_commit_action(relid);
+
+ /* Update this backend's tempfrozenxid and tempminmxid */
+ UpdateTempFrozenXids();
}
/*
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index fdd06ecc877..684e9482f15 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -979,8 +979,17 @@ InsertPgClassTuple(Relation pg_class_desc,
values[Anum_pg_class_relreplident - 1] = CharGetDatum(rd_rel->relreplident);
values[Anum_pg_class_relispartition - 1] = BoolGetDatum(rd_rel->relispartition);
values[Anum_pg_class_relrewrite - 1] = ObjectIdGetDatum(rd_rel->relrewrite);
- values[Anum_pg_class_relfrozenxid - 1] = TransactionIdGetDatum(rd_rel->relfrozenxid);
- values[Anum_pg_class_relminmxid - 1] = MultiXactIdGetDatum(rd_rel->relminmxid);
+
+ /*
+ * For global temporary relations, relfrozenxid and relminmxid are stored
+ * in pg_temp_class. Set them to Invalid in the pg_class tuple.
+ */
+ values[Anum_pg_class_relfrozenxid - 1] =
+ TransactionIdGetDatum(rd_rel->relpersistence == RELPERSISTENCE_GLOBAL_TEMP ?
+ InvalidTransactionId : rd_rel->relfrozenxid);
+ values[Anum_pg_class_relminmxid - 1] =
+ MultiXactIdGetDatum(rd_rel->relpersistence == RELPERSISTENCE_GLOBAL_TEMP ?
+ InvalidMultiXactId : rd_rel->relminmxid);
if (relacl != (Datum) 0)
values[Anum_pg_class_relacl - 1] = relacl;
else
diff --git a/src/backend/catalog/pg_temp_class.c b/src/backend/catalog/pg_temp_class.c
index 1372c65b7c1..4f294515479 100644
--- a/src/backend/catalog/pg_temp_class.c
+++ b/src/backend/catalog/pg_temp_class.c
@@ -53,11 +53,13 @@
#include "access/genam.h"
#include "access/htup_details.h"
+#include "access/multixact.h"
#include "access/table.h"
#include "access/xact.h"
#include "catalog/indexing.h"
#include "catalog/pg_temp_class.h"
#include "miscadmin.h"
+#include "storage/proc.h"
#include "utils/fmgroids.h"
#include "utils/hsearch.h"
#include "utils/memutils.h"
@@ -105,6 +107,14 @@ static bool have_inserts_to_flush = false;
/* Memory context for all tuples pending insert */
static MemoryContext pending_inserts_tupctx = NULL;
+/*
+ * Latest minimum values of relfrozenxid and relminmxid over all global
+ * temporary tables, if changed in the current transaction.
+ */
+static TransactionId min_relfrozenxid = InvalidTransactionId;
+static MultiXactId min_relminmxid = InvalidMultiXactId;
+static bool min_frozenxids_updated = false;
+
/*
* open_pg_temp_class
*
@@ -189,6 +199,12 @@ get_pg_temp_class_tupdesc(void)
TupleDescInitEntry(tupdesc,
(AttrNumber) Anum_pg_temp_class_relallfrozen,
"relallfrozen", INT4OID, -1, 0);
+ TupleDescInitEntry(tupdesc,
+ (AttrNumber) Anum_pg_temp_class_relfrozenxid,
+ "relfrozenxid", XIDOID, -1, 0);
+ TupleDescInitEntry(tupdesc,
+ (AttrNumber) Anum_pg_temp_class_relminmxid,
+ "relminmxid", XIDOID, -1, 0);
TupleDescFinalize(tupdesc);
MemoryContextSwitchTo(oldcontext);
@@ -219,6 +235,8 @@ heap_form_pg_temp_class_tuple(Relation rel)
values[Anum_pg_temp_class_reltuples - 1] = Float4GetDatum(form->reltuples);
values[Anum_pg_temp_class_relallvisible - 1] = Int32GetDatum(form->relallvisible);
values[Anum_pg_temp_class_relallfrozen - 1] = Int32GetDatum(form->relallfrozen);
+ values[Anum_pg_temp_class_relfrozenxid - 1] = TransactionIdGetDatum(form->relfrozenxid);
+ values[Anum_pg_temp_class_relminmxid - 1] = MultiXactIdGetDatum(form->relminmxid);
return heap_form_tuple(get_pg_temp_class_tupdesc(), values, nulls);
}
@@ -654,6 +672,110 @@ GetEffectivePgClassTuple(Oid relid)
return tuple;
}
+/*
+ * UpdateTempFrozenXids
+ *
+ * Update the tempfrozenxid and tempminmxid values for this backend by
+ * finding the minimum relfrozenxid and relminmxid values in pg_temp_class --
+ * i.e., the minimum frozen XIDs over all global temporary relations in use
+ * in this backend.
+ *
+ * The new values are set in this process's PGPROC struct when the current
+ * transaction is committed, or discarded if the transaction is rolled back.
+ *
+ * If no global temporary relations are in use, Invalid*Ids will be set.
+ */
+void
+UpdateTempFrozenXids(void)
+{
+ HASH_SEQ_STATUS status;
+ PendingInsert *entry;
+ Form_pg_temp_class temp_form;
+ TransactionId relfrozenxid;
+ MultiXactId relminmxid;
+ Relation pg_temp_class;
+ SysScanDesc scan;
+ HeapTuple tuple;
+
+ /* Defaults, if no global temporary relations are being used */
+ min_relfrozenxid = InvalidTransactionId;
+ min_relminmxid = InvalidMultiXactId;
+
+ /* Processing any pending inserts */
+ if (pending_inserts != NULL)
+ {
+ hash_seq_init(&status, pending_inserts);
+ while ((entry = hash_seq_search(&status)) != NULL)
+ {
+ temp_form = (Form_pg_temp_class) GETSTRUCT(entry->tuple);
+ relfrozenxid = temp_form->relfrozenxid;
+ relminmxid = (MultiXactId) temp_form->relminmxid;
+
+ /* Skip entries that have been deleted */
+ if (entry->deleted)
+ continue;
+
+ /* Ignore relations that don't hold unfrozen XIDs */
+ if (!TransactionIdIsValid(relfrozenxid) ||
+ !MultiXactIdIsValid(relminmxid))
+ continue;
+
+ /* Update the minimum values */
+ Assert(TransactionIdIsNormal(relfrozenxid));
+
+ if (!TransactionIdIsValid(min_relfrozenxid) ||
+ TransactionIdPrecedes(relfrozenxid, min_relfrozenxid))
+ min_relfrozenxid = relfrozenxid;
+
+ if (!MultiXactIdIsValid(min_relminmxid) ||
+ MultiXactIdPrecedes(relminmxid, min_relminmxid))
+ min_relminmxid = relminmxid;
+ }
+ }
+
+ /*
+ * Process all pg_temp_class entries. If we haven't opened pg_temp_class
+ * in this session yet, it must be empty, and we can skip it.
+ */
+ if (pg_temp_class_opened)
+ {
+ pg_temp_class = table_open(TempRelationRelationId, AccessShareLock);
+
+ scan = systable_beginscan(pg_temp_class, InvalidOid, false,
+ NULL, 0, NULL);
+
+ while ((tuple = systable_getnext(scan)) != NULL)
+ {
+ temp_form = (Form_pg_temp_class) GETSTRUCT(tuple);
+ relfrozenxid = temp_form->relfrozenxid;
+ relminmxid = (MultiXactId) temp_form->relminmxid;
+
+ /* Ignore relations that don't hold unfrozen XIDs */
+ if (!TransactionIdIsValid(relfrozenxid) ||
+ !MultiXactIdIsValid(relminmxid))
+ continue;
+
+ /* Update the minimum values */
+ Assert(TransactionIdIsNormal(relfrozenxid));
+
+ if (!TransactionIdIsValid(min_relfrozenxid) ||
+ TransactionIdPrecedes(relfrozenxid, min_relfrozenxid))
+ min_relfrozenxid = relfrozenxid;
+
+ if (!MultiXactIdIsValid(min_relminmxid) ||
+ MultiXactIdPrecedes(relminmxid, min_relminmxid))
+ min_relminmxid = relminmxid;
+ }
+
+ /* Tidy up */
+ systable_endscan(scan);
+ table_close(pg_temp_class, AccessShareLock);
+ }
+
+ /* Flag the new values as to be applied on commit */
+ min_frozenxids_updated = true;
+}
+
/*
* PreCCI_PgTempClass
*
@@ -713,6 +835,17 @@ AtEOXact_PgTempClass(bool isCommit)
Assert(!(isCommit && have_inserts_to_flush));
if (pending_inserts != NULL)
discard_pending_pg_temp_class_inserts();
+
+ /*
+ * On commit, save any new tempfrozenxid and tempminmxid values to our
+ * PGPROC struct. On rollback, any new values are simply discarded.
+ */
+ if (min_frozenxids_updated && isCommit)
+ {
+ MyProc->tempfrozenxid = min_relfrozenxid;
+ MyProc->tempminmxid = min_relminmxid;
+ }
+ min_frozenxids_updated = false;
}
/*
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 6c1c5545cb5..9275a895412 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -957,6 +957,8 @@ CREATE VIEW pg_stat_activity AS
S.state,
S.backend_xid,
S.backend_xmin,
+ S.tempfrozenxid,
+ S.tempminmxid,
S.query_id,
S.query,
S.backend_type
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 5f11e699dc4..0c704a531b2 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -259,6 +259,7 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
bool verbose = false;
bool analyze = false;
bool concurrently = false;
+ bool have_gtrs = false;
/* Parse option list */
foreach_node(DefElem, opt, stmt->params)
@@ -463,6 +464,8 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
CommitTransactionCommand();
continue;
}
+ if (RELATION_IS_GLOBAL_TEMP(rel))
+ have_gtrs = true;
/* functions in indexes may want a snapshot set */
PushActiveSnapshot(GetTransactionSnapshot());
@@ -478,6 +481,13 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
/* Start a new transaction for the cleanup work. */
StartTransactionCommand();
+ /*
+ * Update this backend's tempfrozenxid and tempminmxid, if we processed
+ * any global temporary relations.
+ */
+ if (have_gtrs)
+ UpdateTempFrozenXids();
+
/* Clean up working storage */
MemoryContextDelete(repack_context);
}
@@ -1728,13 +1738,17 @@ swap_relation_files(Oid r1, Oid r2, bool target_is_pg_class,
* and then fail to commit the pg_class update.
*/
- /* set rel1's frozen Xid and minimum MultiXid */
+ /*
+ * Set rel1's frozen Xid and minimum MultiXid. For a global temporary
+ * relation, the supplied values are set in pg_temp_class, instead of
+ * pg_class.
+ */
if (relform1->relkind != RELKIND_INDEX)
{
Assert(!TransactionIdIsValid(frozenXid) ||
TransactionIdIsNormal(frozenXid));
- relform1->relfrozenxid = frozenXid;
- relform1->relminmxid = cutoffMulti;
+ SetEffective_relfrozenxid(relform1, temp_relform1, frozenXid, NULL, NULL);
+ SetEffective_relminmxid(relform1, temp_relform1, cutoffMulti, NULL, NULL);
}
/* swap size statistics too, since new rel has freshly-updated stats */
@@ -2522,6 +2536,7 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
else
{
Oid indexOid = InvalidOid;
+ bool is_gtr = RELATION_IS_GLOBAL_TEMP(rel);
indexOid = determine_clustered_index(rel, stmt->usingindex,
stmt->indexname);
@@ -2554,6 +2569,13 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
CommandCounterIncrement();
}
+ /*
+ * Update this backend's tempfrozenxid and tempminmxid, if it was a
+ * global temporary relation.
+ */
+ if (is_gtr)
+ UpdateTempFrozenXids();
+
return NULL;
}
}
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 9c776d5a978..6ae3fa0d6a6 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -128,7 +128,8 @@ static void vac_truncate_clog(TransactionId frozenXID,
TransactionId lastSaneFrozenXid,
MultiXactId lastSaneMinMulti);
static bool vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
- BufferAccessStrategy bstrategy, bool isTopLevel);
+ BufferAccessStrategy bstrategy, bool isTopLevel,
+ bool *isGtr);
static double compute_parallel_delay(void);
static VacOptValue get_vacoptval_from_boolean(DefElem *def);
static bool vac_tid_reaped(ItemPointer itemptr, void *state);
@@ -500,6 +501,7 @@ vacuum(List *relations, const VacuumParams *params, BufferAccessStrategy bstrate
const char *stmttype;
volatile bool in_outer_xact,
use_own_xacts;
+ bool have_gtrs = false;
stmttype = (params->options & VACOPT_VACUUM) ? "VACUUM" : "ANALYZE";
@@ -628,11 +630,16 @@ vacuum(List *relations, const VacuumParams *params, BufferAccessStrategy bstrate
foreach(cur, relations)
{
VacuumRelation *vrel = lfirst_node(VacuumRelation, cur);
+ bool isGtr = false;
+ bool doAnalyse;
if (params->options & VACOPT_VACUUM)
{
- if (!vacuum_rel(vrel->oid, vrel->relation, *params, bstrategy,
- isTopLevel))
+ doAnalyse = vacuum_rel(vrel->oid, vrel->relation, *params,
+ bstrategy, isTopLevel, &isGtr);
+ if (isGtr)
+ have_gtrs = true;
+ if (!doAnalyse)
continue;
}
@@ -700,6 +707,17 @@ vacuum(List *relations, const VacuumParams *params, BufferAccessStrategy bstrate
StartTransactionCommand();
}
+ if (params->options & VACOPT_VACUUM && !AmAutoVacuumWorkerProcess())
+ {
+ /*
+ * Update this backend's tempfrozenxid and tempminmxid, if we vacuumed
+ * any global temporary relations. We skip this for autovacuum, which
+ * shouldn't vacuum any global temporary relations.
+ */
+ if (have_gtrs)
+ UpdateTempFrozenXids();
+ }
+
if ((params->options & VACOPT_VACUUM) &&
!(params->options & VACOPT_SKIP_DATABASE_STATS))
{
@@ -1125,7 +1143,7 @@ vacuum_get_cutoffs(Relation rel, const VacuumParams *params,
freeze_table_age = params->freeze_table_age;
multixact_freeze_table_age = params->multixact_freeze_table_age;
- /* Set pg_class fields in cutoffs */
+ /* Set pg_class / pg_temp_class fields in cutoffs */
cutoffs->relfrozenxid = rel->rd_rel->relfrozenxid;
cutoffs->relminmxid = rel->rd_rel->relminmxid;
@@ -1543,8 +1561,14 @@ vac_update_relstats(Relation relation,
* it's corrupt, and overwrite with the oldest remaining XID in the table.
* This should match vac_update_datfrozenxid() concerning what we consider
* to be "in the future".
+ *
+ * For a global temporary relation, frozenxid is only valid for the data
+ * in our local instance of the relation, and is stored in pg_temp_class,
+ * instead of pg_class. This contributes towards tempfrozenxid for this
+ * backend and allows vac_update_datfrozenxid() to advance datfrozenxid
+ * once every backend accessing the relation has vacuumed it.
*/
- oldfrozenxid = pgcform->relfrozenxid;
+ oldfrozenxid = GetEffective_relfrozenxid(pgcform, temp_pgcform);
futurexid = false;
if (frozenxid_updated)
*frozenxid_updated = false;
@@ -1559,15 +1583,15 @@ vac_update_relstats(Relation relation,
if (update)
{
- pgcform->relfrozenxid = frozenxid;
- dirty = true;
+ SetEffective_relfrozenxid(pgcform, temp_pgcform, frozenxid,
+ &dirty, &temp_dirty);
if (frozenxid_updated)
*frozenxid_updated = true;
}
}
/* Similarly for relminmxid */
- oldminmulti = pgcform->relminmxid;
+ oldminmulti = GetEffective_relminmxid(pgcform, temp_pgcform);
futuremxid = false;
if (minmulti_updated)
*minmulti_updated = false;
@@ -1582,8 +1606,8 @@ vac_update_relstats(Relation relation,
if (update)
{
- pgcform->relminmxid = minmulti;
- dirty = true;
+ SetEffective_relminmxid(pgcform, temp_pgcform, minmulti,
+ &dirty, &temp_dirty);
if (minmulti_updated)
*minmulti_updated = true;
}
@@ -1622,6 +1646,52 @@ vac_update_relstats(Relation relation,
}
+/*
+ * vac_get_min_tempfrozenxids() -- get min temp XIDs over all backends
+ *
+ * min_tempfrozenxid is set to the minimum tempfrozenxid from all
+ * backends (including us) connected to our database.
+ *
+ * min_tempminmxid is set to the minimum tempminmxid from all backends
+ * (including us) connected to our database.
+ *
+ * The values returned will be Invalid*Ids, if no backend is accessing
+ * global temporary tables in our database.
+ */
+static void
+vac_get_min_tempfrozenxids(TransactionId *min_tempfrozenxid,
+ MultiXactId *min_tempminmxid)
+{
+ /* Defaults, if no other backends found */
+ *min_tempfrozenxid = InvalidTransactionId;
+ *min_tempminmxid = InvalidMultiXactId;
+
+ LWLockAcquire(ProcArrayLock, LW_SHARED);
+ for (int i = 0; i < ProcGlobal->allProcCount; i++)
+ {
+ PGPROC *proc = GetPGProcByNumber(i);
+
+ /* Ignore backends not connected to our database */
+ if (proc->pid == 0)
+ continue;
+ if (proc->databaseId != MyDatabaseId)
+ continue;
+
+ /* Update the minimum return values */
+ if (TransactionIdIsValid(proc->tempfrozenxid) &&
+ (!TransactionIdIsValid(*min_tempfrozenxid) ||
+ TransactionIdPrecedes(proc->tempfrozenxid, *min_tempfrozenxid)))
+ *min_tempfrozenxid = proc->tempfrozenxid;
+
+ if (MultiXactIdIsValid(proc->tempminmxid) &&
+ (!MultiXactIdIsValid(*min_tempminmxid) ||
+ MultiXactIdPrecedes(proc->tempminmxid, *min_tempminmxid)))
+ *min_tempminmxid = proc->tempminmxid;
+ }
+ LWLockRelease(ProcArrayLock);
+}
+
+
/*
* vac_update_datfrozenxid() -- update pg_database.datfrozenxid for our DB
*
@@ -1656,6 +1726,8 @@ vac_update_datfrozenxid(void)
bool dirty = false;
ScanKeyData key[1];
void *inplace_state;
+ TransactionId min_tempfrozenxid;
+ MultiXactId min_tempminmxid;
/*
* Restrict this task to one backend per database. This avoids race
@@ -1710,10 +1782,17 @@ vac_update_datfrozenxid(void)
/*
* Only consider relations able to hold unfrozen XIDs (anything else
* should have InvalidTransactionId in relfrozenxid anyway).
+ *
+ * We exclude global temporary relations here too because, although
+ * they can hold unfrozen XIDs, their relfrozenxid and relminmxid
+ * values are set in pg_temp_class, instead of pg_class, and those
+ * values contribute to tempfrozenxid and tempminmxid, which we
+ * account for below.
*/
- if (classForm->relkind != RELKIND_RELATION &&
- classForm->relkind != RELKIND_MATVIEW &&
- classForm->relkind != RELKIND_TOASTVALUE)
+ if ((classForm->relkind != RELKIND_RELATION &&
+ classForm->relkind != RELKIND_MATVIEW &&
+ classForm->relkind != RELKIND_TOASTVALUE) ||
+ classForm->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
{
Assert(!TransactionIdIsValid(relfrozenxid));
Assert(!MultiXactIdIsValid(relminmxid));
@@ -1770,6 +1849,31 @@ vac_update_datfrozenxid(void)
systable_endscan(scan);
table_close(relation, AccessShareLock);
+ /*
+ * Account for tempfrozenxid and tempminmxid from all backends connected
+ * to our database. This amounts to min(pg_temp_class.relfrozenxid) and
+ * min(pg_temp_class.relminmxid) over all those backends.
+ */
+ vac_get_min_tempfrozenxids(&min_tempfrozenxid, &min_tempminmxid);
+
+ if (TransactionIdIsValid(min_tempfrozenxid))
+ {
+ Assert(TransactionIdIsNormal(min_tempfrozenxid));
+
+ if (TransactionIdPrecedes(lastSaneFrozenXid, min_tempfrozenxid))
+ bogus = true;
+ else if (TransactionIdPrecedes(min_tempfrozenxid, newFrozenXid))
+ newFrozenXid = min_tempfrozenxid;
+ }
+
+ if (MultiXactIdIsValid(min_tempminmxid))
+ {
+ if (MultiXactIdPrecedes(lastSaneMinMulti, min_tempminmxid))
+ bogus = true;
+ else if (MultiXactIdPrecedes(min_tempminmxid, newMinMulti))
+ newMinMulti = min_tempminmxid;
+ }
+
/* chicken out if bogus data found */
if (bogus)
return;
@@ -2040,7 +2144,7 @@ vac_truncate_clog(TransactionId frozenXID,
*/
static bool
vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
- BufferAccessStrategy bstrategy, bool isTopLevel)
+ BufferAccessStrategy bstrategy, bool isTopLevel, bool *isGtr)
{
LOCKMODE lmode;
Relation rel;
@@ -2126,6 +2230,10 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
return false;
}
+ /* tell caller if it was a global temporary relation */
+ if (RELATION_IS_GLOBAL_TEMP(rel))
+ *isGtr = true;
+
/*
* When recursing to a TOAST table, check privileges on the parent. NB:
* This is only safe to do because we hold a session lock on the main
@@ -2392,7 +2500,7 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
toast_vacuum_params.toast_parent = relid;
vacuum_rel(toast_relid, NULL, toast_vacuum_params, bstrategy,
- isTopLevel);
+ isTopLevel, isGtr);
}
/*
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index 7d01c981a1f..a9205e449d1 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -34,6 +34,7 @@
#include <sys/time.h>
#include "access/clog.h"
+#include "access/multixact.h"
#include "access/transam.h"
#include "access/twophase.h"
#include "access/xlogutils.h"
@@ -523,6 +524,10 @@ InitProcess(void)
/* Initialize wait event information. */
MyProc->wait_event_info = 0;
+ /* Initialize global temporary table information */
+ MyProc->tempfrozenxid = InvalidTransactionId;
+ MyProc->tempminmxid = InvalidMultiXactId;
+
/* Initialize fields for group transaction status update. */
MyProc->clogGroupMember = false;
MyProc->clogGroupMemberXid = InvalidTransactionId;
@@ -686,6 +691,8 @@ InitAuxiliaryProcess(void)
MyProc->backendType = MyBackendType;
MyProc->delayChkptFlags = 0;
MyProc->statusFlags = 0;
+ MyProc->tempfrozenxid = InvalidTransactionId;
+ MyProc->tempminmxid = InvalidMultiXactId;
MyProc->lwWaiting = LW_WS_NOT_WAITING;
MyProc->lwWaitMode = 0;
MyProc->waitLock = NULL;
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..cbd19461d3b 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -15,6 +15,7 @@
#include "postgres.h"
#include "access/htup_details.h"
+#include "access/multixact.h"
#include "access/xlog.h"
#include "access/xlogprefetcher.h"
#include "catalog/catalog.h"
@@ -355,7 +356,7 @@ pg_stat_get_progress_info(PG_FUNCTION_ARGS)
Datum
pg_stat_get_activity(PG_FUNCTION_ARGS)
{
-#define PG_STAT_GET_ACTIVITY_COLS 31
+#define PG_STAT_GET_ACTIVITY_COLS 33
int num_backends = pgstat_fetch_stat_numbackends();
int curr_backend;
int pid = PG_ARGISNULL(0) ? -1 : PG_GETARG_INT32(0);
@@ -451,6 +452,12 @@ pg_stat_get_activity(PG_FUNCTION_ARGS)
/* leader_pid */
nulls[29] = true;
+ /* tempfrozenxid */
+ nulls[31] = true;
+
+ /* tempminmxid */
+ nulls[32] = true;
+
proc = BackendPidGetProc(beentry->st_procpid);
if (proc == NULL && (beentry->st_backendType != B_BACKEND))
@@ -463,19 +470,24 @@ pg_stat_get_activity(PG_FUNCTION_ARGS)
}
/*
- * If a PGPROC entry was retrieved, display wait events and lock
- * group leader or apply leader information if any. To avoid
- * extra overhead, no extra lock is being held, so there is no
- * guarantee of consistency across multiple rows.
+ * If a PGPROC entry was retrieved, display wait events, lock
+ * group leader or apply leader information if any, tempfrozenxid
+ * and tempminmxid. To avoid extra overhead, no extra lock is
+ * being held, so there is no guarantee of consistency across
+ * multiple rows.
*/
if (proc != NULL)
{
uint32 raw_wait_event;
PGPROC *leader;
+ TransactionId tempfrozenxid;
+ MultiXactId tempminmxid;
raw_wait_event = UINT32_ACCESS_ONCE(proc->wait_event_info);
wait_event_type = pgstat_get_wait_event_type(raw_wait_event);
wait_event = pgstat_get_wait_event(raw_wait_event);
+ tempfrozenxid = (TransactionId) UINT32_ACCESS_ONCE(proc->tempfrozenxid);
+ tempminmxid = (MultiXactId) UINT32_ACCESS_ONCE(proc->tempminmxid);
leader = proc->lockGroupLeader;
@@ -499,6 +511,23 @@ pg_stat_get_activity(PG_FUNCTION_ARGS)
nulls[29] = false;
}
}
+
+ /*
+ * Show tempfrozenxid and tempminmxid, if valid. This leaves
+ * these fields as NULL if these XIDs are invalid (0), which
+ * indicates the backend is not using any global temporary
+ * relations.
+ */
+ if (TransactionIdIsValid(tempfrozenxid))
+ {
+ values[31] = TransactionIdGetDatum(tempfrozenxid);
+ nulls[31] = false;
+ }
+ if (MultiXactIdIsValid(tempminmxid))
+ {
+ values[32] = MultiXactIdGetDatum(tempminmxid);
+ nulls[32] = false;
+ }
}
if (wait_event_type)
@@ -698,6 +727,8 @@ pg_stat_get_activity(PG_FUNCTION_ARGS)
nulls[28] = true;
nulls[29] = true;
nulls[30] = true;
+ nulls[31] = true;
+ nulls[32] = true;
}
tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 274132ced87..e857e36eb4a 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -70,6 +70,7 @@
#include "commands/policy.h"
#include "commands/publicationcmds.h"
#include "commands/trigger.h"
+#include "commands/vacuum.h"
#include "common/int.h"
#include "miscadmin.h"
#include "nodes/makefuncs.h"
@@ -303,6 +304,7 @@ static void formrdesc(const char *relationName, Oid relationReltype,
bool isshared, int natts, const FormData_pg_attribute *attrs);
static HeapTuple ScanPgRelation(Oid targetRelId, bool indexOK, bool force_non_historic);
+static void ScanPgTempRelation(Oid relid, Form_pg_class pg_class_form);
static Relation AllocateRelationDesc(Form_pg_class relp);
static void RelationParseRelOptions(Relation relation, HeapTuple tuple);
static void RelationBuildTupleDesc(Relation relation);
@@ -415,7 +417,8 @@ ScanPgRelation(Oid targetRelId, bool indexOK, bool force_non_historic)
* this for pg_temp_class itself, or its index, because they may not have
* been loaded yet. That's OK because we only really need relfilenumber
* and reltablespace to be correct at this stage, and we disallow changes
- * to those attributes for these relations.
+ * to those attributes for these relations. Final initialization of
+ * pg_temp_class and its index is performed by RelationIdGetRelation().
*/
if (HeapTupleIsValid(pg_class_tuple) &&
targetRelId != TempRelationRelationId &&
@@ -426,23 +429,40 @@ ScanPgRelation(Oid targetRelId, bool indexOK, bool force_non_historic)
pg_class_form = (Form_pg_class) GETSTRUCT(pg_class_tuple);
if (pg_class_form->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
- {
- HeapTuple pg_temp_class_tuple;
- Form_pg_temp_class pg_temp_class_form;
-
- pg_temp_class_tuple = GetPgTempClassTuple(targetRelId);
- if (HeapTupleIsValid(pg_temp_class_tuple))
- {
- pg_temp_class_form = (Form_pg_temp_class) GETSTRUCT(pg_temp_class_tuple);
- COPY_PG_TEMP_CLASS_ATTRS(pg_temp_class_form, pg_class_form);
- heap_freetuple(pg_temp_class_tuple);
- }
- }
+ ScanPgTempRelation(targetRelId, pg_class_form);
}
return pg_class_tuple;
}
+/*
+ * ScanPgTempRelation
+ *
+ * This is used by ScanPgRelation to update a global temporary relation's
+ * relcache entry from its pg_temp_class tuple.
+ *
+ * In addition, it is used by RelationIdGetRelation() to perform final
+ * intialization for pg_temp_class itself, and its index --- as noted
+ * above, ScanPgRelation() cannot scan pg_temp_class when initializing
+ * pg_temp_class itself, or its index, and so this is used after building
+ * initial relcache entries, creating storage, and inserting
+ * pg_temp_class tuples for these relations.
+ */
+static void
+ScanPgTempRelation(Oid relid, Form_pg_class pg_class_form)
+{
+ HeapTuple pg_temp_class_tuple;
+ Form_pg_temp_class pg_temp_class_form;
+
+ pg_temp_class_tuple = GetPgTempClassTuple(relid);
+ if (HeapTupleIsValid(pg_temp_class_tuple))
+ {
+ pg_temp_class_form = (Form_pg_temp_class) GETSTRUCT(pg_temp_class_tuple);
+ COPY_PG_TEMP_CLASS_ATTRS(pg_temp_class_form, pg_class_form);
+ heap_freetuple(pg_temp_class_tuple);
+ }
+}
+
/*
* AllocateRelationDesc
*
@@ -2164,6 +2184,15 @@ RelationIdGetRelation(Oid relationId)
if (RELATION_IS_GLOBAL_TEMP(rd))
InitGlobalTempRelation(rd);
+ /*
+ * Special-case: if it's pg_temp_class or its index, update its
+ * relcache entry from the pg_temp_class tuple (ScanPgRelation()
+ * couldn't do this when first loading these relations).
+ */
+ if (rd->rd_id == TempRelationRelationId ||
+ rd->rd_id == TempClassOidIndexId)
+ ScanPgTempRelation(rd->rd_id, rd->rd_rel);
+
/*
* Normally entries need to be valid here, but before the relcache
* has been initialized, not enough infrastructure exists to
@@ -2187,6 +2216,9 @@ RelationIdGetRelation(Oid relationId)
RelationIncrementReferenceCount(rd);
if (RELATION_IS_GLOBAL_TEMP(rd))
InitGlobalTempRelation(rd);
+ if (rd->rd_id == TempRelationRelationId ||
+ rd->rd_id == TempClassOidIndexId)
+ ScanPgTempRelation(rd->rd_id, rd->rd_rel);
}
return rd;
}
@@ -4034,7 +4066,7 @@ RelationSetNewRelfilenumber(Relation relation, char persistence)
}
else
{
- /* Normal case, update the pg_class and pg_temp_class entries */
+ /* Normal case, update pg_class or pg_temp_class entry (not both) */
SetEffective_relfilenode(classform, temp_classform, newrelfilenumber);
/* relpages etc. never change for sequences */
@@ -4046,15 +4078,23 @@ RelationSetNewRelfilenumber(Relation relation, char persistence)
SetEffective_relallvisible(classform, temp_classform, 0, NULL, NULL);
SetEffective_relallfrozen(classform, temp_classform, 0, NULL, NULL);
}
- classform->relfrozenxid = freezeXid;
- classform->relminmxid = minmulti;
- classform->relpersistence = persistence;
+ SetEffective_relfrozenxid(classform, temp_classform, freezeXid, NULL, NULL);
+ SetEffective_relminmxid(classform, temp_classform, minmulti, NULL, NULL);
- CatalogTupleUpdate(pg_class, &otid, tuple);
+ /* relpersistence can only change for permanent relations */
if (HeapTupleIsValid(temp_tuple))
{
+ Assert(classform->relpersistence == persistence);
UpdatePgTempClassTuple(RelationGetRelid(relation), temp_tuple);
heap_freetuple(temp_tuple);
+
+ /* Update this backend's tempfrozenxid and tempminmxid */
+ UpdateTempFrozenXids();
+ }
+ else
+ {
+ classform->relpersistence = persistence;
+ CatalogTupleUpdate(pg_class, &otid, tuple);
}
}
@@ -4064,7 +4104,7 @@ RelationSetNewRelfilenumber(Relation relation, char persistence)
table_close(pg_class, RowExclusiveLock);
/*
- * Make the pg_class and pg_temp_class row changes or relation map change
+ * Make the pg_class/pg_temp_class row change or relation map change
* visible. This will cause the relcache entry to get updated, too.
*/
CommandCounterIncrement();
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..933ed9acb8b 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5690,9 +5690,9 @@
proname => 'pg_stat_get_activity', prorows => '100', proisstrict => 'f',
proretset => 't', provolatile => 's', proparallel => 'r',
prorettype => 'record', proargtypes => 'int4',
- proallargtypes => '{int4,oid,int4,oid,text,text,text,text,text,timestamptz,timestamptz,timestamptz,timestamptz,inet,text,int4,xid,xid,text,bool,text,text,int4,text,numeric,text,bool,text,bool,bool,int4,int8}',
- proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
- proargnames => '{pid,datid,pid,usesysid,application_name,state,query,wait_event_type,wait_event,xact_start,query_start,backend_start,state_change,client_addr,client_hostname,client_port,backend_xid,backend_xmin,backend_type,ssl,sslversion,sslcipher,sslbits,ssl_client_dn,ssl_client_serial,ssl_issuer_dn,gss_auth,gss_princ,gss_enc,gss_delegation,leader_pid,query_id}',
+ proallargtypes => '{int4,oid,int4,oid,text,text,text,text,text,timestamptz,timestamptz,timestamptz,timestamptz,inet,text,int4,xid,xid,text,bool,text,text,int4,text,numeric,text,bool,text,bool,bool,int4,int8,xid,xid}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
+ proargnames => '{pid,datid,pid,usesysid,application_name,state,query,wait_event_type,wait_event,xact_start,query_start,backend_start,state_change,client_addr,client_hostname,client_port,backend_xid,backend_xmin,backend_type,ssl,sslversion,sslcipher,sslbits,ssl_client_dn,ssl_client_serial,ssl_issuer_dn,gss_auth,gss_princ,gss_enc,gss_delegation,leader_pid,query_id,tempfrozenxid,tempminmxid}',
prosrc => 'pg_stat_get_activity' },
{ oid => '6318', descr => 'describe wait events',
proname => 'pg_get_wait_events', procost => '10', prorows => '250',
diff --git a/src/include/catalog/pg_temp_class.h b/src/include/catalog/pg_temp_class.h
index e6b77b50d23..a4629b12c9d 100644
--- a/src/include/catalog/pg_temp_class.h
+++ b/src/include/catalog/pg_temp_class.h
@@ -57,6 +57,12 @@ CATALOG(pg_temp_class,8082,TempRelationRelationId) BKI_TEMP_RELATION
/* # of all-frozen blocks (not always up-to-date) */
int32 relallfrozen BKI_DEFAULT(0);
+
+ /* all Xids < this are frozen in this rel */
+ TransactionId relfrozenxid BKI_DEFAULT(3); /* FirstNormalTransactionId */
+
+ /* all multixacts in this rel are >= this; it is really a MultiXactId */
+ TransactionId relminmxid BKI_DEFAULT(1); /* FirstMultiXactId */
} FormData_pg_temp_class;
END_CATALOG_STRUCT
@@ -87,6 +93,8 @@ MAKE_SYSCACHE(TEMPRELOID, pg_temp_class_oid_index, 128);
(target)->reltuples = (source)->reltuples; \
(target)->relallvisible = (source)->relallvisible; \
(target)->relallfrozen = (source)->relallfrozen; \
+ (target)->relfrozenxid = (source)->relfrozenxid; \
+ (target)->relminmxid = (source)->relminmxid; \
} while (0)
/*
@@ -149,6 +157,26 @@ GetEffective_relallfrozen(Form_pg_class cf, Form_pg_temp_class tf)
return tf != NULL ? tf->relallfrozen : cf->relallfrozen;
}
+/*
+ * Get the effective value of relfrozenxid from pg_class and pg_temp_class
+ * tuple data. The value from pg_temp_class (if present) takes precedence.
+ */
+static inline TransactionId
+GetEffective_relfrozenxid(Form_pg_class cf, Form_pg_temp_class tf)
+{
+ return tf != NULL ? tf->relfrozenxid : cf->relfrozenxid;
+}
+
+/*
+ * Get the effective value of relminmxid from pg_class and pg_temp_class tuple
+ * data. The value from pg_temp_class (if present) takes precedence.
+ */
+static inline MultiXactId
+GetEffective_relminmxid(Form_pg_class cf, Form_pg_temp_class tf)
+{
+ return tf != NULL ? tf->relminmxid : cf->relminmxid;
+}
+
/*
* Set the effective value of relfilenode in tuple form data from pg_class or
* pg_temp_class. The value is set in pg_temp_class instead of pg_class, if
@@ -285,6 +313,60 @@ SetEffective_relallfrozen(Form_pg_class cf, Form_pg_temp_class tf, int32 val,
}
}
+/*
+ * Set the effective value of relfrozenxid in tuple form data from pg_class or
+ * pg_temp_class. The value is set in pg_temp_class instead of pg_class, if
+ * the pg_temp_class tuple form data is non-NULL. If non-NULL, the cdirty or
+ * tdirty flag is updated, if the value actually changes.
+ */
+static inline void
+SetEffective_relfrozenxid(Form_pg_class cf, Form_pg_temp_class tf,
+ TransactionId val, bool *cdirty, bool *tdirty)
+{
+ if (tf != NULL)
+ {
+ if (val != tf->relfrozenxid)
+ {
+ tf->relfrozenxid = val;
+ if (tdirty != NULL)
+ *tdirty = true;
+ }
+ }
+ else if (val != cf->relfrozenxid)
+ {
+ cf->relfrozenxid = val;
+ if (cdirty != NULL)
+ *cdirty = true;
+ }
+}
+
+/*
+ * Set the effective value of relminmxid in tuple form data from pg_class or
+ * pg_temp_class. The value is set in pg_temp_class instead of pg_class, if
+ * the pg_temp_class tuple form data is non-NULL. If non-NULL, the cdirty or
+ * tdirty flag is updated, if the value actually changes.
+ */
+static inline void
+SetEffective_relminmxid(Form_pg_class cf, Form_pg_temp_class tf,
+ MultiXactId val, bool *cdirty, bool *tdirty)
+{
+ if (tf != NULL)
+ {
+ if (val != tf->relminmxid)
+ {
+ tf->relminmxid = val;
+ if (tdirty != NULL)
+ *tdirty = true;
+ }
+ }
+ else if (val != cf->relminmxid)
+ {
+ cf->relminmxid = val;
+ if (cdirty != NULL)
+ *cdirty = true;
+ }
+}
+
extern HeapTuple GetPgTempClassTuple(Oid relid);
@@ -302,6 +384,8 @@ extern HeapTuple GetPgClassAndPgTempClassTuples(Oid relid, bool lock_tuple,
extern HeapTuple GetEffectivePgClassTuple(Oid relid);
+extern void UpdateTempFrozenXids(void);
+
extern void PreCCI_PgTempClass(void);
extern void PreCommit_PgTempClass(void);
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index 956d9cea36d..9bd5e1ef717 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -257,7 +257,7 @@ typedef struct VacuumParams
struct VacuumCutoffs
{
/*
- * Existing pg_class fields at start of VACUUM
+ * Existing pg_class / pg_temp_class fields at start of VACUUM
*/
TransactionId relfrozenxid;
MultiXactId relminmxid;
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 3e1d1fad5f9..710b466f173 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -382,6 +382,15 @@ typedef struct PGPROC
************************************************************************/
uint32 wait_event_info; /* proc's wait information */
+
+ /************************************************************************
+ * Global temporary tables
+ ************************************************************************/
+
+ TransactionId tempfrozenxid; /* minimum relfrozenxid over all global
+ * temporary tables in use */
+ MultiXactId tempminmxid; /* minimum relminmxid over all global
+ * temporary tables in use */
}
PGPROC;
diff --git a/src/test/isolation/expected/vacuum-global-temp.out b/src/test/isolation/expected/vacuum-global-temp.out
new file mode 100644
index 00000000000..0028fc9bfcc
--- /dev/null
+++ b/src/test/isolation/expected/vacuum-global-temp.out
@@ -0,0 +1,123 @@
+Parsed test spec with 2 sessions
+
+starting permutation: create vacdml1 vac1 vacdml2 vac2 vacdml1 vac1prep vac1 vac1cmp vacdml2 vac2prep vac2 vac2cmp vacdml1 vac1prep vac1 vac1cmp vacdml2 vac2prep vac2 vac2cmp
+step create:
+ CREATE GLOBAL TEMP TABLE vactest (a int);
+ CREATE TABLE saved_xids(local_xid bigint, global_xid bigint, db_xid bigint);
+
+ CREATE FUNCTION save_xids() RETURNS void
+ BEGIN ATOMIC
+ DELETE FROM saved_xids;
+ INSERT INTO saved_xids VALUES (
+ (SELECT min(relfrozenxid::text::bigint) FROM pg_temp_class WHERE relfrozenxid != 0),
+ (SELECT min(relfrozenxid::text::bigint) FROM pg_class WHERE relfrozenxid != 0),
+ (SELECT datfrozenxid::text::bigint FROM pg_database WHERE datname = current_database())
+ );
+ END;
+
+ CREATE FUNCTION cmp_xids(xid1 bigint, xid2 bigint) RETURNS text
+ BEGIN ATOMIC
+ SELECT CASE
+ WHEN xid1 < xid2 THEN 'older'
+ WHEN xid1 > xid2 THEN 'younger'
+ ELSE 'same'
+ END;
+ END;
+
+ CREATE FUNCTION check_new_xids(OUT local_xid text,
+ OUT global_xid text,
+ OUT db_xid text)
+ BEGIN ATOMIC
+ WITH new_xids(new_local_xid, new_global_xid, new_db_xid) AS (
+ SELECT
+ (SELECT min(relfrozenxid::text::bigint) FROM pg_temp_class WHERE relfrozenxid != 0),
+ (SELECT min(relfrozenxid::text::bigint) FROM pg_class WHERE relfrozenxid != 0),
+ (SELECT datfrozenxid::text::bigint FROM pg_database WHERE datname = current_database())
+ )
+ SELECT cmp_xids(new_local_xid, local_xid),
+ cmp_xids(new_global_xid, global_xid),
+ cmp_xids(new_db_xid, db_xid)
+ FROM saved_xids, new_xids;
+ END;
+
+step vacdml1:
+ INSERT INTO vactest SELECT * FROM generate_series(1, 10);
+ DELETE FROM vactest WHERE a % 2 = 0;
+ INSERT INTO vactest SELECT a * 2 FROM vactest;
+
+step vac1: VACUUM (FREEZE);
+step vacdml2:
+ INSERT INTO vactest SELECT * FROM generate_series(1, 10);
+ UPDATE vactest SET a = a * 2 WHERE a % 2 = 1;
+
+step vac2: VACUUM (FREEZE);
+step vacdml1:
+ INSERT INTO vactest SELECT * FROM generate_series(1, 10);
+ DELETE FROM vactest WHERE a % 2 = 0;
+ INSERT INTO vactest SELECT a * 2 FROM vactest;
+
+step vac1prep: SELECT save_xids();
+save_xids
+---------
+
+(1 row)
+
+step vac1: VACUUM (FREEZE);
+step vac1cmp: SELECT * FROM check_new_xids();
+local_xid|global_xid|db_xid
+---------+----------+------
+younger |younger |same
+(1 row)
+
+step vacdml2:
+ INSERT INTO vactest SELECT * FROM generate_series(1, 10);
+ UPDATE vactest SET a = a * 2 WHERE a % 2 = 1;
+
+step vac2prep: SELECT save_xids();
+save_xids
+---------
+
+(1 row)
+
+step vac2: VACUUM (FREEZE);
+step vac2cmp: SELECT * FROM check_new_xids();
+local_xid|global_xid|db_xid
+---------+----------+-------
+younger |younger |younger
+(1 row)
+
+step vacdml1:
+ INSERT INTO vactest SELECT * FROM generate_series(1, 10);
+ DELETE FROM vactest WHERE a % 2 = 0;
+ INSERT INTO vactest SELECT a * 2 FROM vactest;
+
+step vac1prep: SELECT save_xids();
+save_xids
+---------
+
+(1 row)
+
+step vac1: VACUUM (FREEZE);
+step vac1cmp: SELECT * FROM check_new_xids();
+local_xid|global_xid|db_xid
+---------+----------+-------
+younger |younger |younger
+(1 row)
+
+step vacdml2:
+ INSERT INTO vactest SELECT * FROM generate_series(1, 10);
+ UPDATE vactest SET a = a * 2 WHERE a % 2 = 1;
+
+step vac2prep: SELECT save_xids();
+save_xids
+---------
+
+(1 row)
+
+step vac2: VACUUM (FREEZE);
+step vac2cmp: SELECT * FROM check_new_xids();
+local_xid|global_xid|db_xid
+---------+----------+-------
+younger |younger |younger
+(1 row)
+
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index 7d1aacec267..4253d60d134 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -129,3 +129,4 @@ test: lock-nowait
test: for-portion-of
test: ddl-dependency-locking
test: global-temp
+test: vacuum-global-temp
diff --git a/src/test/isolation/specs/vacuum-global-temp.spec b/src/test/isolation/specs/vacuum-global-temp.spec
new file mode 100644
index 00000000000..8d765703aad
--- /dev/null
+++ b/src/test/isolation/specs/vacuum-global-temp.spec
@@ -0,0 +1,71 @@
+# Test vacuuming global temporary relations
+
+teardown {
+ DROP FUNCTION save_xids, cmp_xids, check_new_xids;
+ DROP TABLE vactest, saved_xids;
+}
+
+session s1
+step create {
+ CREATE GLOBAL TEMP TABLE vactest (a int);
+ CREATE TABLE saved_xids(local_xid bigint, global_xid bigint, db_xid bigint);
+
+ CREATE FUNCTION save_xids() RETURNS void
+ BEGIN ATOMIC
+ DELETE FROM saved_xids;
+ INSERT INTO saved_xids VALUES (
+ (SELECT min(relfrozenxid::text::bigint) FROM pg_temp_class WHERE relfrozenxid != 0),
+ (SELECT min(relfrozenxid::text::bigint) FROM pg_class WHERE relfrozenxid != 0),
+ (SELECT datfrozenxid::text::bigint FROM pg_database WHERE datname = current_database())
+ );
+ END;
+
+ CREATE FUNCTION cmp_xids(xid1 bigint, xid2 bigint) RETURNS text
+ BEGIN ATOMIC
+ SELECT CASE
+ WHEN xid1 < xid2 THEN 'older'
+ WHEN xid1 > xid2 THEN 'younger'
+ ELSE 'same'
+ END;
+ END;
+
+ CREATE FUNCTION check_new_xids(OUT local_xid text,
+ OUT global_xid text,
+ OUT db_xid text)
+ BEGIN ATOMIC
+ WITH new_xids(new_local_xid, new_global_xid, new_db_xid) AS (
+ SELECT
+ (SELECT min(relfrozenxid::text::bigint) FROM pg_temp_class WHERE relfrozenxid != 0),
+ (SELECT min(relfrozenxid::text::bigint) FROM pg_class WHERE relfrozenxid != 0),
+ (SELECT datfrozenxid::text::bigint FROM pg_database WHERE datname = current_database())
+ )
+ SELECT cmp_xids(new_local_xid, local_xid),
+ cmp_xids(new_global_xid, global_xid),
+ cmp_xids(new_db_xid, db_xid)
+ FROM saved_xids, new_xids;
+ END;
+}
+step vacdml1 {
+ INSERT INTO vactest SELECT * FROM generate_series(1, 10);
+ DELETE FROM vactest WHERE a % 2 = 0;
+ INSERT INTO vactest SELECT a * 2 FROM vactest;
+}
+step vac1prep { SELECT save_xids(); }
+step vac1 { VACUUM (FREEZE); }
+step vac1cmp { SELECT * FROM check_new_xids(); }
+
+session s2
+step vacdml2 {
+ INSERT INTO vactest SELECT * FROM generate_series(1, 10);
+ UPDATE vactest SET a = a * 2 WHERE a % 2 = 1;
+}
+step vac2prep { SELECT save_xids(); }
+step vac2 { VACUUM (FREEZE); }
+step vac2cmp { SELECT * FROM check_new_xids(); }
+
+permutation
+ create vacdml1 vac1 vacdml2 vac2
+ vacdml1 vac1prep vac1 vac1cmp
+ vacdml2 vac2prep vac2 vac2cmp
+ vacdml1 vac1prep vac1 vac1cmp
+ vacdml2 vac2prep vac2 vac2cmp
diff --git a/src/test/regress/expected/global_temp.out b/src/test/regress/expected/global_temp.out
index 8cec1b0cfca..630a761d31c 100644
--- a/src/test/regress/expected/global_temp.out
+++ b/src/test/regress/expected/global_temp.out
@@ -48,6 +48,31 @@ ORDER BY table_name;
regression | global_temp_yyy | tmp3 | GLOBAL TEMPORARY
(3 rows)
+-- Check pg_stat_activity
+SELECT tempfrozenxid, tempminmxid -- not visible without privilege
+FROM pg_stat_activity
+WHERE pid = pg_backend_pid();
+ tempfrozenxid | tempminmxid
+---------------+-------------
+ |
+(1 row)
+
+RESET ROLE;
+GRANT pg_read_all_stats TO regress_global_temp_user;
+SET ROLE regress_global_temp_user;
+SELECT tempfrozenxid::text::bigint = (SELECT min(relfrozenxid::text::bigint)
+ FROM pg_temp_class
+ WHERE relfrozenxid::text != '0'),
+ tempminmxid::text::bigint = (SELECT min(relminmxid::text::bigint)
+ FROM pg_temp_class
+ WHERE relminmxid::text != '0')
+FROM pg_stat_activity
+WHERE pid = pg_backend_pid();
+ ?column? | ?column?
+----------+----------
+ t | t
+(1 row)
+
DROP SCHEMA global_temp_xxx CASCADE;
NOTICE: drop cascades to table global_temp_xxx.tmp2
DROP SCHEMA global_temp_yyy CASCADE;
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index e266f501d87..561cf60567e 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1795,10 +1795,12 @@ pg_stat_activity| SELECT s.datid,
s.state,
s.backend_xid,
s.backend_xmin,
+ s.tempfrozenxid,
+ s.tempminmxid,
s.query_id,
s.query,
s.backend_type
- FROM ((pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id)
+ FROM ((pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id, tempfrozenxid, tempminmxid)
LEFT JOIN pg_database d ON ((s.datid = d.oid)))
LEFT JOIN pg_authid u ON ((s.usesysid = u.oid)));
pg_stat_all_indexes| SELECT c.oid AS relid,
@@ -1944,7 +1946,7 @@ pg_stat_gssapi| SELECT pid,
gss_princ AS principal,
gss_enc AS encrypted,
gss_delegation AS credentials_delegated
- FROM pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id)
+ FROM pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id, tempfrozenxid, tempminmxid)
WHERE (client_port IS NOT NULL);
pg_stat_io| SELECT backend_type,
object,
@@ -2255,7 +2257,7 @@ pg_stat_replication| SELECT s.pid,
w.sync_priority,
w.sync_state,
w.reply_time
- FROM ((pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id)
+ FROM ((pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id, tempfrozenxid, tempminmxid)
JOIN pg_stat_get_wal_senders() w(pid, state, sent_lsn, write_lsn, flush_lsn, replay_lsn, write_lag, flush_lag, replay_lag, sync_priority, sync_state, reply_time) ON ((s.pid = w.pid)))
LEFT JOIN pg_authid u ON ((s.usesysid = u.oid)));
pg_stat_replication_slots| SELECT s.slot_name,
@@ -2292,7 +2294,7 @@ pg_stat_ssl| SELECT pid,
ssl_client_dn AS client_dn,
ssl_client_serial AS client_serial,
ssl_issuer_dn AS issuer_dn
- FROM pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id)
+ FROM pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id, tempfrozenxid, tempminmxid)
WHERE (client_port IS NOT NULL);
pg_stat_subscription| SELECT su.oid AS subid,
su.subname,
diff --git a/src/test/regress/sql/global_temp.sql b/src/test/regress/sql/global_temp.sql
index 921595862f2..f88960ec5b2 100644
--- a/src/test/regress/sql/global_temp.sql
+++ b/src/test/regress/sql/global_temp.sql
@@ -25,6 +25,24 @@ FROM information_schema.tables
WHERE table_name ~ 'tmp' AND table_schema ~ 'global_temp'
ORDER BY table_name;
+-- Check pg_stat_activity
+SELECT tempfrozenxid, tempminmxid -- not visible without privilege
+FROM pg_stat_activity
+WHERE pid = pg_backend_pid();
+
+RESET ROLE;
+GRANT pg_read_all_stats TO regress_global_temp_user;
+SET ROLE regress_global_temp_user;
+
+SELECT tempfrozenxid::text::bigint = (SELECT min(relfrozenxid::text::bigint)
+ FROM pg_temp_class
+ WHERE relfrozenxid::text != '0'),
+ tempminmxid::text::bigint = (SELECT min(relminmxid::text::bigint)
+ FROM pg_temp_class
+ WHERE relminmxid::text != '0')
+FROM pg_stat_activity
+WHERE pid = pg_backend_pid();
+
DROP SCHEMA global_temp_xxx CASCADE;
DROP SCHEMA global_temp_yyy CASCADE;
--
2.51.0
[text/x-patch] v8-0008-Add-pg_temp_statistic-global-temporary-catalog-ta.patch (32.7K, ../CAEZATCVjaZFa80S-V_+nvnzRK1ZiLUPx8jwdyAtRVHAmPZQQMg@mail.gmail.com/9-v8-0008-Add-pg_temp_statistic-global-temporary-catalog-ta.patch)
download | inline diff:
From 347db3574caf2bd7d138ae06049bac6373a8a896 Mon Sep 17 00:00:00 2001
From: Dean Rasheed <[email protected]>
Date: Thu, 18 Jun 2026 20:49:45 +0100
Subject: [PATCH v8 08/10] Add pg_temp_statistic global temporary catalog
table.
This has the exact same columns as pg_statistic, but it is a global
temporary table, and is used to hold statistics about other global
temporary relations, allowing them to be session-specific.
---
src/backend/catalog/heap.c | 22 ++-
src/backend/catalog/system_views.sql | 9 +-
src/backend/commands/analyze.c | 44 ++++--
src/backend/executor/nodeHash.c | 3 +-
src/backend/statistics/attribute_stats.c | 31 ++++-
src/backend/utils/adt/selfuncs.c | 18 ++-
src/backend/utils/cache/lsyscache.c | 12 +-
src/backend/utils/cache/relcache.c | 1 +
src/include/catalog/Makefile | 3 +-
src/include/catalog/meson.build | 1 +
src/include/catalog/pg_statistic.h | 3 +
src/include/catalog/pg_temp_statistic.h | 147 +++++++++++++++++++++
src/test/regress/expected/global_temp.out | 45 ++++++-
src/test/regress/expected/oidjoins.out | 11 ++
src/test/regress/expected/rules.out | 67 +++++++++-
src/test/regress/expected/sanity_check.out | 23 ++++
src/test/regress/sql/global_temp.sql | 19 +++
src/test/regress/sql/sanity_check.sql | 20 +++
18 files changed, 443 insertions(+), 36 deletions(-)
create mode 100644 src/include/catalog/pg_temp_statistic.h
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 684e9482f15..6d61650621a 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -53,6 +53,7 @@
#include "catalog/pg_statistic.h"
#include "catalog/pg_subscription_rel.h"
#include "catalog/pg_tablespace.h"
+#include "catalog/pg_temp_statistic.h"
#include "catalog/pg_type.h"
#include "catalog/storage.h"
#include "commands/tablecmds.h"
@@ -3497,6 +3498,13 @@ CopyStatistics(Oid fromrelid, Oid torelid)
Relation statrel;
CatalogIndexState indstate = NULL;
+ /*
+ * Note: This is currently only used for concurrent index building, which
+ * isn't supported on global temporary relations, so we never want
+ * pg_temp_statistic here.
+ */
+ Assert(!rel_is_global_temp(fromrelid) && !rel_is_global_temp(torelid));
+
statrel = table_open(StatisticRelationId, RowExclusiveLock);
/* Now search for stat records */
@@ -3545,12 +3553,22 @@ void
RemoveStatistics(Oid relid, AttrNumber attnum)
{
Relation pgstatistic;
+ Oid relidAttnumInhIndexId;
SysScanDesc scan;
ScanKeyData key[2];
int nkeys;
HeapTuple tuple;
- pgstatistic = table_open(StatisticRelationId, RowExclusiveLock);
+ if (rel_is_global_temp(relid))
+ {
+ pgstatistic = table_open(TempStatisticRelationId, RowExclusiveLock);
+ relidAttnumInhIndexId = TempStatisticRelidAttnumInhIndexId;
+ }
+ else
+ {
+ pgstatistic = table_open(StatisticRelationId, RowExclusiveLock);
+ relidAttnumInhIndexId = StatisticRelidAttnumInhIndexId;
+ }
ScanKeyInit(&key[0],
Anum_pg_statistic_starelid,
@@ -3568,7 +3586,7 @@ RemoveStatistics(Oid relid, AttrNumber attnum)
nkeys = 2;
}
- scan = systable_beginscan(pgstatistic, StatisticRelidAttnumInhIndexId, true,
+ scan = systable_beginscan(pgstatistic, relidAttnumInhIndexId, true,
NULL, nkeys, key);
/* we must loop even when attnum != 0, in case of inherited stats */
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 9275a895412..eac13b607e9 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -187,6 +187,11 @@ CREATE VIEW pg_sequences AS
WHERE NOT pg_is_other_temp_schema(N.oid)
AND relkind = 'S';
+CREATE VIEW pg_all_statistic AS
+ SELECT * FROM pg_statistic
+ UNION ALL
+ SELECT * FROM pg_temp_statistic;
+
CREATE VIEW pg_stats WITH (security_barrier) AS
SELECT
nspname AS schemaname,
@@ -268,7 +273,7 @@ CREATE VIEW pg_stats WITH (security_barrier) AS
WHEN stakind4 = 7 THEN stavalues4
WHEN stakind5 = 7 THEN stavalues5
END AS range_bounds_histogram
- FROM pg_statistic s JOIN pg_class c ON (c.oid = s.starelid)
+ FROM pg_all_statistic s JOIN pg_class c ON (c.oid = s.starelid)
JOIN pg_attribute a ON (c.oid = attrelid AND attnum = s.staattnum)
LEFT JOIN pg_namespace n ON (n.oid = c.relnamespace)
WHERE NOT attisdropped
@@ -276,6 +281,8 @@ CREATE VIEW pg_stats WITH (security_barrier) AS
AND (c.relrowsecurity = false OR NOT row_security_active(c.oid));
REVOKE ALL ON pg_statistic FROM public;
+REVOKE ALL ON pg_temp_statistic FROM public;
+REVOKE ALL ON pg_all_statistic FROM public;
CREATE VIEW pg_stats_ext WITH (security_barrier) AS
SELECT cn.nspname AS schemaname,
diff --git a/src/backend/commands/analyze.c b/src/backend/commands/analyze.c
index f66e80b757c..0db9810eba1 100644
--- a/src/backend/commands/analyze.c
+++ b/src/backend/commands/analyze.c
@@ -29,6 +29,7 @@
#include "catalog/index.h"
#include "catalog/indexing.h"
#include "catalog/pg_inherits.h"
+#include "catalog/pg_temp_statistic.h"
#include "commands/progress.h"
#include "commands/tablecmds.h"
#include "commands/vacuum.h"
@@ -176,9 +177,11 @@ analyze_rel(Oid relid, RangeVar *relation,
}
/*
- * We can ANALYZE any table except pg_statistic. See update_attstats
+ * We can ANALYZE any table except pg_statistic and pg_temp_statistic. See
+ * update_attstats
*/
- if (RelationGetRelid(onerel) == StatisticRelationId)
+ if (RelationGetRelid(onerel) == StatisticRelationId ||
+ RelationGetRelid(onerel) == TempStatisticRelationId)
{
relation_close(onerel, ShareUpdateExclusiveLock);
return;
@@ -1691,20 +1694,23 @@ acquire_inherited_sample_rows(Relation onerel, int elevel,
/*
* update_attstats() -- update attribute statistics for one relation
*
- * Statistics are stored in several places: the pg_class row for the
- * relation has stats about the whole relation, and there is a
- * pg_statistic row for each (non-system) attribute that has ever
- * been analyzed. The pg_class values are updated by VACUUM, not here.
+ * Statistics are stored in several places: the pg_class/pg_temp_class
+ * row for the relation has stats about the whole relation, and there is
+ * a pg_statistic/pg_temp_statistic row for each (non-system) attribute
+ * that has ever been analyzed. The pg_class/pg_temp_class values are
+ * updated by VACUUM, not here.
*
- * pg_statistic rows are just added or updated normally. This means
- * that pg_statistic will probably contain some deleted rows at the
- * completion of a vacuum cycle, unless it happens to get vacuumed last.
+ * pg_statistic/pg_temp_statistic rows are just added or updated
+ * normally. This means that pg_statistic/pg_temp_statistic will
+ * probably contain some deleted rows at the completion of a vacuum
+ * cycle, unless it happens to get vacuumed last.
*
- * To keep things simple, we punt for pg_statistic, and don't try
- * to compute or store rows for pg_statistic itself in pg_statistic.
+ * To keep things simple, we punt for pg_statistic and pg_temp_statistic,
+ * and don't try to compute or store rows for pg_statistic or
+ * pg_temp_statistic themselves in pg_statistic or pg_temp_statistic.
* This could possibly be made to work, but it's not worth the trouble.
* Note analyze_rel() has seen to it that we won't come here when
- * vacuuming pg_statistic itself.
+ * vacuuming pg_statistic or pg_temp_statistic themselves.
*
* Note: there would be a race condition here if two backends could
* ANALYZE the same table concurrently. Presently, we lock that out
@@ -1716,11 +1722,21 @@ update_attstats(Oid relid, bool inh, int natts, VacAttrStats **vacattrstats)
Relation sd;
int attno;
CatalogIndexState indstate = NULL;
+ SysCacheIdentifier cacheId;
if (natts <= 0)
return; /* nothing to do */
- sd = table_open(StatisticRelationId, RowExclusiveLock);
+ if (rel_is_global_temp(relid))
+ {
+ sd = table_open(TempStatisticRelationId, RowExclusiveLock);
+ cacheId = TEMPSTATRELATTINH;
+ }
+ else
+ {
+ sd = table_open(StatisticRelationId, RowExclusiveLock);
+ cacheId = STATRELATTINH;
+ }
for (attno = 0; attno < natts; attno++)
{
@@ -1811,7 +1827,7 @@ update_attstats(Oid relid, bool inh, int natts, VacAttrStats **vacattrstats)
}
/* Is there already a pg_statistic tuple for this attribute? */
- oldtup = SearchSysCache3(STATRELATTINH,
+ oldtup = SearchSysCache3(cacheId,
ObjectIdGetDatum(relid),
Int16GetDatum(stats->tupattnum),
BoolGetDatum(inh));
diff --git a/src/backend/executor/nodeHash.c b/src/backend/executor/nodeHash.c
index 8825bb6fa23..6ace6904810 100644
--- a/src/backend/executor/nodeHash.c
+++ b/src/backend/executor/nodeHash.c
@@ -2442,7 +2442,8 @@ ExecHashBuildSkewHash(HashState *hashstate, HashJoinTable hashtable,
/*
* Try to find the MCV statistics for the outer relation's join key.
*/
- statsTuple = SearchSysCache3(STATRELATTINH,
+ statsTuple = SearchSysCache3(rel_is_global_temp(node->skewTable) ?
+ TEMPSTATRELATTINH : STATRELATTINH,
ObjectIdGetDatum(node->skewTable),
Int16GetDatum(node->skewColumn),
BoolGetDatum(node->skewInherit));
diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index 1cc4d657231..60ffc7db04f 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -21,6 +21,7 @@
#include "catalog/indexing.h"
#include "catalog/namespace.h"
#include "catalog/pg_operator.h"
+#include "catalog/pg_temp_statistic.h"
#include "nodes/makefuncs.h"
#include "statistics/statistics.h"
#include "statistics/stat_utils.h"
@@ -137,6 +138,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
Oid locked_table = InvalidOid;
Relation starel;
+ SysCacheIdentifier cacheId;
HeapTuple statup;
Oid atttypid = InvalidOid;
@@ -332,9 +334,18 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
fmgr_info(F_ARRAY_IN, &array_in_fn);
- starel = table_open(StatisticRelationId, RowExclusiveLock);
+ if (rel_is_global_temp(reloid))
+ {
+ starel = table_open(TempStatisticRelationId, RowExclusiveLock);
+ cacheId = TEMPSTATRELATTINH;
+ }
+ else
+ {
+ starel = table_open(StatisticRelationId, RowExclusiveLock);
+ cacheId = STATRELATTINH;
+ }
- statup = SearchSysCache3(STATRELATTINH, ObjectIdGetDatum(reloid), Int16GetDatum(attnum), BoolGetDatum(inherited));
+ statup = SearchSysCache3(cacheId, ObjectIdGetDatum(reloid), Int16GetDatum(attnum), BoolGetDatum(inherited));
/* initialize from existing tuple if exists */
if (HeapTupleIsValid(statup))
@@ -567,12 +578,24 @@ upsert_pg_statistic(Relation starel, HeapTuple oldtup,
static bool
delete_pg_statistic(Oid reloid, AttrNumber attnum, bool stainherit)
{
- Relation sd = table_open(StatisticRelationId, RowExclusiveLock);
+ Relation sd;
HeapTuple oldtup;
bool result = false;
+ SysCacheIdentifier cacheId;
+
+ if (rel_is_global_temp(reloid))
+ {
+ sd = table_open(TempStatisticRelationId, RowExclusiveLock);
+ cacheId = TEMPSTATRELATTINH;
+ }
+ else
+ {
+ sd = table_open(StatisticRelationId, RowExclusiveLock);
+ cacheId = STATRELATTINH;
+ }
/* Is there already a pg_statistic tuple for this attribute? */
- oldtup = SearchSysCache3(STATRELATTINH,
+ oldtup = SearchSysCache3(cacheId,
ObjectIdGetDatum(reloid),
Int16GetDatum(attnum),
BoolGetDatum(stainherit));
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index cbc70fde716..82be6a4d2af 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -5838,7 +5838,8 @@ examine_variable(PlannerInfo *root, Node *node, int varRelid,
else if (index->indpred == NIL)
{
vardata->statsTuple =
- SearchSysCache3(STATRELATTINH,
+ SearchSysCache3(rel_is_global_temp(index->indexoid) ?
+ TEMPSTATRELATTINH : STATRELATTINH,
ObjectIdGetDatum(index->indexoid),
Int16GetDatum(pos + 1),
BoolGetDatum(false));
@@ -6068,7 +6069,8 @@ examine_simple_variable(PlannerInfo *root, Var *var,
* Plain table or parent of an inheritance appendrel, so look up the
* column in pg_statistic
*/
- vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+ vardata->statsTuple = SearchSysCache3(rel_is_global_temp(rte->relid) ?
+ TEMPSTATRELATTINH : STATRELATTINH,
ObjectIdGetDatum(rte->relid),
Int16GetDatum(var->varattno),
BoolGetDatum(rte->inh));
@@ -6545,7 +6547,8 @@ examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
}
else
{
- vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+ vardata->statsTuple = SearchSysCache3(rel_is_global_temp(relid) ?
+ TEMPSTATRELATTINH : STATRELATTINH,
ObjectIdGetDatum(relid),
Int16GetDatum(colnum),
BoolGetDatum(rte->inh));
@@ -6571,7 +6574,8 @@ examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
}
else
{
- vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+ vardata->statsTuple = SearchSysCache3(rel_is_global_temp(relid) ?
+ TEMPSTATRELATTINH : STATRELATTINH,
ObjectIdGetDatum(relid),
Int16GetDatum(colnum),
BoolGetDatum(false));
@@ -9125,7 +9129,8 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
else
{
vardata.statsTuple =
- SearchSysCache3(STATRELATTINH,
+ SearchSysCache3(rel_is_global_temp(rte->relid) ?
+ TEMPSTATRELATTINH : STATRELATTINH,
ObjectIdGetDatum(rte->relid),
Int16GetDatum(attnum),
BoolGetDatum(false));
@@ -9155,7 +9160,8 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
}
else
{
- vardata.statsTuple = SearchSysCache3(STATRELATTINH,
+ vardata.statsTuple = SearchSysCache3(rel_is_global_temp(index->indexoid) ?
+ TEMPSTATRELATTINH : STATRELATTINH,
ObjectIdGetDatum(index->indexoid),
Int16GetDatum(attnum),
BoolGetDatum(false));
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index e8068a45cc8..ecc34f07e36 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -41,6 +41,7 @@
#include "catalog/pg_statistic.h"
#include "catalog/pg_subscription.h"
#include "catalog/pg_temp_class.h"
+#include "catalog/pg_temp_statistic.h"
#include "catalog/pg_transform.h"
#include "catalog/pg_type.h"
#include "miscadmin.h"
@@ -3499,7 +3500,8 @@ get_attavgwidth(Oid relid, AttrNumber attnum)
if (stawidth > 0)
return stawidth;
}
- tp = SearchSysCache3(STATRELATTINH,
+ tp = SearchSysCache3(rel_is_global_temp(relid) ?
+ TEMPSTATRELATTINH : STATRELATTINH,
ObjectIdGetDatum(relid),
Int16GetDatum(attnum),
BoolGetDatum(false));
@@ -3593,7 +3595,9 @@ get_attstatsslot(AttStatsSlot *sslot, HeapTuple statstuple,
if (flags & ATTSTATSSLOT_VALUES)
{
- val = SysCacheGetAttrNotNull(STATRELATTINH, statstuple,
+ val = SysCacheGetAttrNotNull(IsTempStatisticTuple(statstuple) ?
+ TEMPSTATRELATTINH : STATRELATTINH,
+ statstuple,
Anum_pg_statistic_stavalues1 + i);
/*
@@ -3638,7 +3642,9 @@ get_attstatsslot(AttStatsSlot *sslot, HeapTuple statstuple,
if (flags & ATTSTATSSLOT_NUMBERS)
{
- val = SysCacheGetAttrNotNull(STATRELATTINH, statstuple,
+ val = SysCacheGetAttrNotNull(IsTempStatisticTuple(statstuple) ?
+ TEMPSTATRELATTINH : STATRELATTINH,
+ statstuple,
Anum_pg_statistic_stanumbers1 + i);
/*
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index e857e36eb4a..b144d0639b8 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -63,6 +63,7 @@
#include "catalog/pg_subscription.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_temp_class.h"
+#include "catalog/pg_temp_statistic.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
#include "catalog/schemapg.h"
diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile
index 629a13edc24..6aec03add22 100644
--- a/src/include/catalog/Makefile
+++ b/src/include/catalog/Makefile
@@ -87,7 +87,8 @@ CATALOG_HEADERS := \
pg_propgraph_label.h \
pg_propgraph_label_property.h \
pg_propgraph_property.h \
- pg_temp_class.h
+ pg_temp_class.h \
+ pg_temp_statistic.h
GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h)
diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build
index 404f8503276..db809c4b3c0 100644
--- a/src/include/catalog/meson.build
+++ b/src/include/catalog/meson.build
@@ -75,6 +75,7 @@ catalog_headers = [
'pg_propgraph_label_property.h',
'pg_propgraph_property.h',
'pg_temp_class.h',
+ 'pg_temp_statistic.h',
]
# The .dat files we need can just be listed alphabetically.
diff --git a/src/include/catalog/pg_statistic.h b/src/include/catalog/pg_statistic.h
index 032bf177b95..ff55f7f5bb4 100644
--- a/src/include/catalog/pg_statistic.h
+++ b/src/include/catalog/pg_statistic.h
@@ -28,6 +28,9 @@
*/
BEGIN_CATALOG_STRUCT
+/*
+ * NB: Any changes made here must be reflected in pg_temp_statistic.
+ */
CATALOG(pg_statistic,2619,StatisticRelationId)
{
/* These fields form the unique key for the entry: */
diff --git a/src/include/catalog/pg_temp_statistic.h b/src/include/catalog/pg_temp_statistic.h
new file mode 100644
index 00000000000..afad9dd660c
--- /dev/null
+++ b/src/include/catalog/pg_temp_statistic.h
@@ -0,0 +1,147 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_temp_statistic.h
+ * definition of the "temporary statistics" system catalog
+ * (pg_temp_statistic)
+ *
+ * This is a global temporary system catalog table storing session-specific
+ * statistics for temporary relations. Currently, it is only used for
+ * global temporary relations.
+ *
+ * Portions Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * src/include/catalog/pg_temp_statistic.h
+ *
+ * NOTES
+ * The Catalog.pm module reads this file and derives schema
+ * information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_TEMP_STATISTIC_H
+#define PG_TEMP_STATISTIC_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_temp_statistic_d.h" /* IWYU pragma: export */
+
+/* ----------------
+ * pg_temp_statistic definition. cpp turns this into
+ * typedef struct FormData_pg_temp_statistic
+ * ----------------
+ */
+BEGIN_CATALOG_STRUCT
+
+/*
+ * NB: The fields here must exactly match those in pg_statistic.
+ */
+CATALOG(pg_temp_statistic,8084,TempStatisticRelationId) BKI_TEMP_RELATION
+{
+ /* These fields form the unique key for the entry: */
+ Oid starelid BKI_LOOKUP(pg_class); /* relation containing
+ * attribute */
+ int16 staattnum; /* attribute (column) stats are for */
+ bool stainherit; /* true if inheritance children are included */
+
+ /* the fraction of the column's entries that are NULL: */
+ float4 stanullfrac;
+
+ /*
+ * stawidth is the average width in bytes of non-null entries. For
+ * fixed-width datatypes this is of course the same as the typlen, but for
+ * var-width types it is more useful. Note that this is the average width
+ * of the data as actually stored, post-TOASTing (eg, for a
+ * moved-out-of-line value, only the size of the pointer object is
+ * counted). This is the appropriate definition for the primary use of
+ * the statistic, which is to estimate sizes of in-memory hash tables of
+ * tuples.
+ */
+ int32 stawidth;
+
+ /* ----------------
+ * stadistinct indicates the (approximate) number of distinct non-null
+ * data values in the column. The interpretation is:
+ * 0 unknown or not computed
+ * > 0 actual number of distinct values
+ * < 0 negative of multiplier for number of rows
+ * The special negative case allows us to cope with columns that are
+ * unique (stadistinct = -1) or nearly so (for example, a column in which
+ * non-null values appear about twice on the average could be represented
+ * by stadistinct = -0.5 if there are no nulls, or -0.4 if 20% of the
+ * column is nulls). Because the number-of-rows statistic in pg_class may
+ * be updated more frequently than pg_statistic is, it's important to be
+ * able to describe such situations as a multiple of the number of rows,
+ * rather than a fixed number of distinct values. But in other cases a
+ * fixed number is correct (eg, a boolean column).
+ * ----------------
+ */
+ float4 stadistinct;
+
+ /* ----------------
+ * To allow keeping statistics on different kinds of datatypes,
+ * we do not hard-wire any particular meaning for the remaining
+ * statistical fields. Instead, we provide several "slots" in which
+ * statistical data can be placed. Each slot includes:
+ * kind integer code identifying kind of data (see below)
+ * op OID of associated operator, if needed
+ * coll OID of relevant collation, or 0 if none
+ * numbers float4 array (for statistical values)
+ * values anyarray (for representations of data values)
+ * The ID, operator, and collation fields are never NULL; they are zeroes
+ * in an unused slot. The numbers and values fields are NULL in an
+ * unused slot, and might also be NULL in a used slot if the slot kind
+ * has no need for one or the other.
+ * ----------------
+ */
+
+ int16 stakind1;
+ int16 stakind2;
+ int16 stakind3;
+ int16 stakind4;
+ int16 stakind5;
+
+ Oid staop1 BKI_LOOKUP_OPT(pg_operator);
+ Oid staop2 BKI_LOOKUP_OPT(pg_operator);
+ Oid staop3 BKI_LOOKUP_OPT(pg_operator);
+ Oid staop4 BKI_LOOKUP_OPT(pg_operator);
+ Oid staop5 BKI_LOOKUP_OPT(pg_operator);
+
+ Oid stacoll1 BKI_LOOKUP_OPT(pg_collation);
+ Oid stacoll2 BKI_LOOKUP_OPT(pg_collation);
+ Oid stacoll3 BKI_LOOKUP_OPT(pg_collation);
+ Oid stacoll4 BKI_LOOKUP_OPT(pg_collation);
+ Oid stacoll5 BKI_LOOKUP_OPT(pg_collation);
+
+#ifdef CATALOG_VARLEN /* variable-length fields start here */
+ float4 stanumbers1[1];
+ float4 stanumbers2[1];
+ float4 stanumbers3[1];
+ float4 stanumbers4[1];
+ float4 stanumbers5[1];
+
+ /*
+ * Values in these arrays are values of the column's data type, or of some
+ * related type such as an array element type. We presently have to cheat
+ * quite a bit to allow polymorphic arrays of this kind, but perhaps
+ * someday it'll be a less bogus facility.
+ */
+ anyarray stavalues1;
+ anyarray stavalues2;
+ anyarray stavalues3;
+ anyarray stavalues4;
+ anyarray stavalues5;
+#endif
+} FormData_pg_temp_statistic;
+
+END_CATALOG_STRUCT
+
+DECLARE_TOAST(pg_temp_statistic, 8085, 8086);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_temp_statistic_relid_att_inh_index, 8087, TempStatisticRelidAttnumInhIndexId, pg_temp_statistic, btree(starelid oid_ops, staattnum int2_ops, stainherit bool_ops));
+
+MAKE_SYSCACHE(TEMPSTATRELATTINH, pg_temp_statistic_relid_att_inh_index, 128);
+
+/* Is the specified tuple from pg_temp_statistic? */
+#define IsTempStatisticTuple(tuple) \
+ ((tuple)->t_tableOid == TempStatisticRelationId)
+
+#endif /* PG_TEMP_STATISTIC_H */
diff --git a/src/test/regress/expected/global_temp.out b/src/test/regress/expected/global_temp.out
index 630a761d31c..a2723875013 100644
--- a/src/test/regress/expected/global_temp.out
+++ b/src/test/regress/expected/global_temp.out
@@ -504,14 +504,16 @@ ROLLBACK TO sp;
INSERT INTO tmp1 VALUES (1, 'xxx');
COMMIT;
SELECT oid::regclass FROM pg_temp_class ORDER BY 1;
- oid
--------------------------
+ oid
+---------------------------------------
pg_temp_class
pg_temp_class_oid_index
+ pg_temp_statistic
+ pg_temp_statistic_relid_att_inh_index
tmp1_c_seq
tmp1
tmp1_pkey
-(5 rows)
+(7 rows)
SELECT * FROM tmp1;
a | b | c
@@ -654,6 +656,43 @@ SELECT oid::regclass, relpages, reltuples, relallvisible, relallfrozen
tmp2 | 5 | 150 | 10 | 20
(1 row)
+DROP TABLE tmp2;
+-- Test column stats
+CREATE GLOBAL TEMP TABLE tmp2 (a int, b int);
+INSERT INTO tmp2 SELECT x, floor(sqrt(x)) FROM generate_series(36, 99) x;
+ANALYZE tmp2;
+SELECT stavalues1 FROM pg_statistic
+ WHERE starelid = 'tmp2'::regclass AND staattnum = 2;
+ stavalues1
+------------
+(0 rows)
+
+SELECT stavalues1 FROM pg_temp_statistic
+ WHERE starelid = 'tmp2'::regclass AND staattnum = 2;
+ stavalues1
+------------
+ {9,8,7,6}
+(1 row)
+
+SELECT most_common_vals FROM pg_stats
+ WHERE tablename = 'tmp2' AND attname = 'b';
+ most_common_vals
+------------------
+ {9,8,7,6}
+(1 row)
+
+SELECT COUNT(*) FROM tmp2 WHERE b = 8;
+ count
+-------
+ 17
+(1 row)
+
+SELECT row_estimate('SELECT * FROM tmp2 WHERE b = 8');
+ row_estimate
+--------------
+ 17
+(1 row)
+
DROP TABLE tmp2;
-- Test view creation
CREATE VIEW v AS SELECT * FROM tmp1;
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 3c3404e51b8..be2efa78257 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -287,3 +287,14 @@ NOTICE: checking pg_propgraph_property {pgptypid} => pg_type {oid}
NOTICE: checking pg_propgraph_property {pgpcollation} => pg_collation {oid}
NOTICE: checking pg_temp_class {oid} => pg_class {oid}
NOTICE: checking pg_temp_class {reltablespace} => pg_tablespace {oid}
+NOTICE: checking pg_temp_statistic {starelid} => pg_class {oid}
+NOTICE: checking pg_temp_statistic {staop1} => pg_operator {oid}
+NOTICE: checking pg_temp_statistic {staop2} => pg_operator {oid}
+NOTICE: checking pg_temp_statistic {staop3} => pg_operator {oid}
+NOTICE: checking pg_temp_statistic {staop4} => pg_operator {oid}
+NOTICE: checking pg_temp_statistic {staop5} => pg_operator {oid}
+NOTICE: checking pg_temp_statistic {stacoll1} => pg_collation {oid}
+NOTICE: checking pg_temp_statistic {stacoll2} => pg_collation {oid}
+NOTICE: checking pg_temp_statistic {stacoll3} => pg_collation {oid}
+NOTICE: checking pg_temp_statistic {stacoll4} => pg_collation {oid}
+NOTICE: checking pg_temp_statistic {stacoll5} => pg_collation {oid}
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 561cf60567e..c3cafeb6e43 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1302,6 +1302,71 @@ pg_aios| SELECT pid,
f_localmem,
f_buffered
FROM pg_get_aios() pg_get_aios(pid, io_id, io_generation, state, operation, off, length, target, handle_data_len, raw_result, result, target_desc, f_sync, f_localmem, f_buffered);
+pg_all_statistic| SELECT pg_statistic.starelid,
+ pg_statistic.staattnum,
+ pg_statistic.stainherit,
+ pg_statistic.stanullfrac,
+ pg_statistic.stawidth,
+ pg_statistic.stadistinct,
+ pg_statistic.stakind1,
+ pg_statistic.stakind2,
+ pg_statistic.stakind3,
+ pg_statistic.stakind4,
+ pg_statistic.stakind5,
+ pg_statistic.staop1,
+ pg_statistic.staop2,
+ pg_statistic.staop3,
+ pg_statistic.staop4,
+ pg_statistic.staop5,
+ pg_statistic.stacoll1,
+ pg_statistic.stacoll2,
+ pg_statistic.stacoll3,
+ pg_statistic.stacoll4,
+ pg_statistic.stacoll5,
+ pg_statistic.stanumbers1,
+ pg_statistic.stanumbers2,
+ pg_statistic.stanumbers3,
+ pg_statistic.stanumbers4,
+ pg_statistic.stanumbers5,
+ pg_statistic.stavalues1,
+ pg_statistic.stavalues2,
+ pg_statistic.stavalues3,
+ pg_statistic.stavalues4,
+ pg_statistic.stavalues5
+ FROM pg_statistic
+UNION ALL
+ SELECT pg_temp_statistic.starelid,
+ pg_temp_statistic.staattnum,
+ pg_temp_statistic.stainherit,
+ pg_temp_statistic.stanullfrac,
+ pg_temp_statistic.stawidth,
+ pg_temp_statistic.stadistinct,
+ pg_temp_statistic.stakind1,
+ pg_temp_statistic.stakind2,
+ pg_temp_statistic.stakind3,
+ pg_temp_statistic.stakind4,
+ pg_temp_statistic.stakind5,
+ pg_temp_statistic.staop1,
+ pg_temp_statistic.staop2,
+ pg_temp_statistic.staop3,
+ pg_temp_statistic.staop4,
+ pg_temp_statistic.staop5,
+ pg_temp_statistic.stacoll1,
+ pg_temp_statistic.stacoll2,
+ pg_temp_statistic.stacoll3,
+ pg_temp_statistic.stacoll4,
+ pg_temp_statistic.stacoll5,
+ pg_temp_statistic.stanumbers1,
+ pg_temp_statistic.stanumbers2,
+ pg_temp_statistic.stanumbers3,
+ pg_temp_statistic.stanumbers4,
+ pg_temp_statistic.stanumbers5,
+ pg_temp_statistic.stavalues1,
+ pg_temp_statistic.stavalues2,
+ pg_temp_statistic.stavalues3,
+ pg_temp_statistic.stavalues4,
+ pg_temp_statistic.stavalues5
+ FROM pg_temp_statistic;
pg_available_extension_versions| SELECT e.name,
e.version,
(x.extname IS NOT NULL) AS installed,
@@ -2702,7 +2767,7 @@ pg_stats| SELECT n.nspname AS schemaname,
WHEN (s.stakind5 = 7) THEN s.stavalues5
ELSE NULL::anyarray
END AS range_bounds_histogram
- FROM (((pg_statistic s
+ FROM (((pg_all_statistic s
JOIN pg_class c ON ((c.oid = s.starelid)))
JOIN pg_attribute a ON (((c.oid = a.attrelid) AND (a.attnum = s.staattnum))))
LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace)))
diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out
index 8370c1561cc..5f5756d5596 100644
--- a/src/test/regress/expected/sanity_check.out
+++ b/src/test/regress/expected/sanity_check.out
@@ -25,3 +25,26 @@ SELECT relname, relkind
---------+---------
(0 rows)
+-- check that pg_statistic and pg_temp_statistic have the exact same columns
+WITH t1 AS (
+ SELECT attname, atttypid, attlen, attnum, atttypmod, attndims, attbyval,
+ attalign, attstorage, attcompression, attnotnull, atthasdef,
+ atthasmissing, attidentity, attgenerated, attisdropped, attislocal,
+ attinhcount, attcollation
+ FROM pg_attribute
+ WHERE attrelid = 'pg_statistic'::regclass
+), t2 AS (
+ SELECT attname, atttypid, attlen, attnum, atttypmod, attndims, attbyval,
+ attalign, attstorage, attcompression, attnotnull, atthasdef,
+ atthasmissing, attidentity, attgenerated, attisdropped, attislocal,
+ attinhcount, attcollation
+ FROM pg_attribute
+ WHERE attrelid = 'pg_temp_statistic'::regclass
+)
+(SELECT * FROM t1 EXCEPT SELECT * FROM t2)
+UNION ALL
+(SELECT * FROM t2 EXCEPT SELECT * FROM t1);
+ attname | atttypid | attlen | attnum | atttypmod | attndims | attbyval | attalign | attstorage | attcompression | attnotnull | atthasdef | atthasmissing | attidentity | attgenerated | attisdropped | attislocal | attinhcount | attcollation
+---------+----------+--------+--------+-----------+----------+----------+----------+------------+----------------+------------+-----------+---------------+-------------+--------------+--------------+------------+-------------+--------------
+(0 rows)
+
diff --git a/src/test/regress/sql/global_temp.sql b/src/test/regress/sql/global_temp.sql
index f88960ec5b2..3ecc766c3df 100644
--- a/src/test/regress/sql/global_temp.sql
+++ b/src/test/regress/sql/global_temp.sql
@@ -385,6 +385,25 @@ SELECT oid::regclass, relpages, reltuples, relallvisible, relallfrozen
DROP TABLE tmp2;
+-- Test column stats
+CREATE GLOBAL TEMP TABLE tmp2 (a int, b int);
+INSERT INTO tmp2 SELECT x, floor(sqrt(x)) FROM generate_series(36, 99) x;
+ANALYZE tmp2;
+
+SELECT stavalues1 FROM pg_statistic
+ WHERE starelid = 'tmp2'::regclass AND staattnum = 2;
+
+SELECT stavalues1 FROM pg_temp_statistic
+ WHERE starelid = 'tmp2'::regclass AND staattnum = 2;
+
+SELECT most_common_vals FROM pg_stats
+ WHERE tablename = 'tmp2' AND attname = 'b';
+
+SELECT COUNT(*) FROM tmp2 WHERE b = 8;
+SELECT row_estimate('SELECT * FROM tmp2 WHERE b = 8');
+
+DROP TABLE tmp2;
+
-- Test view creation
CREATE VIEW v AS SELECT * FROM tmp1;
SELECT * FROM v;
diff --git a/src/test/regress/sql/sanity_check.sql b/src/test/regress/sql/sanity_check.sql
index 162e5324b5d..ed0096d7823 100644
--- a/src/test/regress/sql/sanity_check.sql
+++ b/src/test/regress/sql/sanity_check.sql
@@ -19,3 +19,23 @@ SELECT relname, relkind
FROM pg_class
WHERE relkind IN ('v', 'c', 'f', 'p', 'I')
AND relfilenode <> 0;
+
+-- check that pg_statistic and pg_temp_statistic have the exact same columns
+WITH t1 AS (
+ SELECT attname, atttypid, attlen, attnum, atttypmod, attndims, attbyval,
+ attalign, attstorage, attcompression, attnotnull, atthasdef,
+ atthasmissing, attidentity, attgenerated, attisdropped, attislocal,
+ attinhcount, attcollation
+ FROM pg_attribute
+ WHERE attrelid = 'pg_statistic'::regclass
+), t2 AS (
+ SELECT attname, atttypid, attlen, attnum, atttypmod, attndims, attbyval,
+ attalign, attstorage, attcompression, attnotnull, atthasdef,
+ atthasmissing, attidentity, attgenerated, attisdropped, attislocal,
+ attinhcount, attcollation
+ FROM pg_attribute
+ WHERE attrelid = 'pg_temp_statistic'::regclass
+)
+(SELECT * FROM t1 EXCEPT SELECT * FROM t2)
+UNION ALL
+(SELECT * FROM t2 EXCEPT SELECT * FROM t1);
--
2.51.0
[text/x-patch] v8-0009-Add-pg_temp_statistic_ext_data-global-temporary-c.patch (36.7K, ../CAEZATCVjaZFa80S-V_+nvnzRK1ZiLUPx8jwdyAtRVHAmPZQQMg@mail.gmail.com/10-v8-0009-Add-pg_temp_statistic_ext_data-global-temporary-c.patch)
download | inline diff:
From 0340b1e1e7a5bafb9b4347f49a04034c13cb2acd Mon Sep 17 00:00:00 2001
From: Dean Rasheed <[email protected]>
Date: Thu, 18 Jun 2026 23:38:12 +0100
Subject: [PATCH v8 09/10] Add pg_temp_statistic_ext_data global temporary
catalog table.
This has the same columns as pg_statistic_ext_data, but it is a global
temporary table, and is used to hold extended statistics data for
global temporary tables, allowing the data to be session-specific
while the extended statistics object definitions remain global.
---
src/backend/catalog/system_views.sql | 14 +++-
src/backend/commands/statscmds.c | 21 ++++--
src/backend/optimizer/util/plancat.c | 13 ++--
src/backend/statistics/dependencies.c | 11 +--
src/backend/statistics/extended_stats.c | 27 ++++---
src/backend/statistics/extended_stats_funcs.c | 48 ++++++++++---
src/backend/statistics/mcv.c | 13 ++--
src/backend/statistics/mvdistinct.c | 10 ++-
src/backend/utils/adt/selfuncs.c | 5 +-
src/backend/utils/cache/relcache.c | 1 +
src/include/catalog/Makefile | 3 +-
src/include/catalog/meson.build | 1 +
src/include/catalog/pg_statistic_ext_data.h | 3 +
.../catalog/pg_temp_statistic_ext_data.h | 62 ++++++++++++++++
src/include/commands/defrem.h | 2 +-
src/include/statistics/statistics.h | 8 +--
src/test/regress/expected/global_temp.out | 71 +++++++++++++++++++
src/test/regress/expected/oidjoins.out | 1 +
src/test/regress/expected/rules.out | 19 ++++-
src/test/regress/expected/sanity_check.out | 24 +++++++
src/test/regress/sql/global_temp.sql | 41 +++++++++++
src/test/regress/sql/sanity_check.sql | 21 ++++++
22 files changed, 365 insertions(+), 54 deletions(-)
create mode 100644 src/include/catalog/pg_temp_statistic_ext_data.h
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index eac13b607e9..07b2d7a0027 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -284,6 +284,11 @@ REVOKE ALL ON pg_statistic FROM public;
REVOKE ALL ON pg_temp_statistic FROM public;
REVOKE ALL ON pg_all_statistic FROM public;
+CREATE VIEW pg_all_statistic_ext_data AS
+ SELECT * FROM pg_statistic_ext_data
+ UNION ALL
+ SELECT * FROM pg_temp_statistic_ext_data;
+
CREATE VIEW pg_stats_ext WITH (security_barrier) AS
SELECT cn.nspname AS schemaname,
c.relname AS tablename,
@@ -307,7 +312,7 @@ CREATE VIEW pg_stats_ext WITH (security_barrier) AS
m.most_common_freqs,
m.most_common_base_freqs
FROM pg_statistic_ext s JOIN pg_class c ON (c.oid = s.stxrelid)
- JOIN pg_statistic_ext_data sd ON (s.oid = sd.stxoid)
+ JOIN pg_all_statistic_ext_data sd ON (s.oid = sd.stxoid)
LEFT JOIN pg_namespace cn ON (cn.oid = c.relnamespace)
LEFT JOIN pg_namespace sn ON (sn.oid = s.stxnamespace)
LEFT JOIN LATERAL
@@ -404,7 +409,7 @@ CREATE VIEW pg_stats_ext_exprs WITH (security_barrier) AS
WHEN (stat.a).stakind5 = 7 THEN (stat.a).stavalues5
END) AS range_bounds_histogram
FROM pg_statistic_ext s JOIN pg_class c ON (c.oid = s.stxrelid)
- LEFT JOIN pg_statistic_ext_data sd ON (s.oid = sd.stxoid)
+ LEFT JOIN pg_all_statistic_ext_data sd ON (s.oid = sd.stxoid)
LEFT JOIN pg_namespace cn ON (cn.oid = c.relnamespace)
LEFT JOIN pg_namespace sn ON (sn.oid = s.stxnamespace)
JOIN LATERAL (
@@ -414,8 +419,11 @@ CREATE VIEW pg_stats_ext_exprs WITH (security_barrier) AS
WHERE pg_has_role(c.relowner, 'USAGE')
AND (c.relrowsecurity = false OR NOT row_security_active(c.oid));
--- unprivileged users may read pg_statistic_ext but not pg_statistic_ext_data
+-- unprivileged users may read pg_statistic_ext but not pg_statistic_ext_data,
+-- pg_temp_statistic_ext_data, or pg_all_statistic_ext_data
REVOKE ALL ON pg_statistic_ext_data FROM public;
+REVOKE ALL ON pg_temp_statistic_ext_data FROM public;
+REVOKE ALL ON pg_all_statistic_ext_data FROM public;
CREATE VIEW pg_publication_tables AS
SELECT
diff --git a/src/backend/commands/statscmds.c b/src/backend/commands/statscmds.c
index ce98ebb35ea..2b7c56095a1 100644
--- a/src/backend/commands/statscmds.c
+++ b/src/backend/commands/statscmds.c
@@ -25,6 +25,7 @@
#include "catalog/pg_namespace.h"
#include "catalog/pg_statistic_ext.h"
#include "catalog/pg_statistic_ext_data.h"
+#include "catalog/pg_temp_statistic_ext_data.h"
#include "commands/comment.h"
#include "commands/defrem.h"
#include "miscadmin.h"
@@ -789,14 +790,24 @@ AlterStatistics(AlterStatsStmt *stmt)
* exists, so don't error out.
*/
void
-RemoveStatisticsDataById(Oid statsOid, bool inh)
+RemoveStatisticsDataById(Oid relid, Oid statsOid, bool inh)
{
Relation relation;
+ SysCacheIdentifier cacheId;
HeapTuple tup;
- relation = table_open(StatisticExtDataRelationId, RowExclusiveLock);
+ if (rel_is_global_temp(relid))
+ {
+ relation = table_open(TempStatisticExtDataRelationId, RowExclusiveLock);
+ cacheId = TEMPSTATEXTDATASTXOID;
+ }
+ else
+ {
+ relation = table_open(StatisticExtDataRelationId, RowExclusiveLock);
+ cacheId = STATEXTDATASTXOID;
+ }
- tup = SearchSysCache2(STATEXTDATASTXOID, ObjectIdGetDatum(statsOid),
+ tup = SearchSysCache2(cacheId, ObjectIdGetDatum(statsOid),
BoolGetDatum(inh));
/* We don't know if the data row for inh value exists. */
@@ -844,8 +855,8 @@ RemoveStatisticsById(Oid statsOid)
*/
rel = table_open(relid, ShareUpdateExclusiveLock);
- RemoveStatisticsDataById(statsOid, true);
- RemoveStatisticsDataById(statsOid, false);
+ RemoveStatisticsDataById(relid, statsOid, true);
+ RemoveStatisticsDataById(relid, statsOid, false);
CacheInvalidateRelcacheByRelid(relid);
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index 7c4be174869..1c71b1ff969 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -1652,14 +1652,17 @@ get_relation_constraints(PlannerInfo *root,
* The result is stored in stainfos list.
*/
static void
-get_relation_statistics_worker(List **stainfos, RelOptInfo *rel,
+get_relation_statistics_worker(Oid relid, List **stainfos, RelOptInfo *rel,
Oid statOid, bool inh,
Bitmapset *keys, List *exprs)
{
+ SysCacheIdentifier cacheId;
Form_pg_statistic_ext_data dataForm;
HeapTuple dtup;
- dtup = SearchSysCache2(STATEXTDATASTXOID,
+ cacheId = rel_is_global_temp(relid) ? TEMPSTATEXTDATASTXOID : STATEXTDATASTXOID;
+
+ dtup = SearchSysCache2(cacheId,
ObjectIdGetDatum(statOid), BoolGetDatum(inh));
if (!HeapTupleIsValid(dtup))
return;
@@ -1824,9 +1827,11 @@ get_relation_statistics(PlannerInfo *root, RelOptInfo *rel,
/* extract statistics for possible values of stxdinherit flag */
- get_relation_statistics_worker(&stainfos, rel, statOid, true, keys, exprs);
+ get_relation_statistics_worker(RelationGetRelid(relation), &stainfos,
+ rel, statOid, true, keys, exprs);
- get_relation_statistics_worker(&stainfos, rel, statOid, false, keys, exprs);
+ get_relation_statistics_worker(RelationGetRelid(relation), &stainfos,
+ rel, statOid, false, keys, exprs);
ReleaseSysCache(htup);
bms_free(keys);
diff --git a/src/backend/statistics/dependencies.c b/src/backend/statistics/dependencies.c
index 95dcc218978..0d75b8d7840 100644
--- a/src/backend/statistics/dependencies.c
+++ b/src/backend/statistics/dependencies.c
@@ -685,20 +685,23 @@ dependency_is_fully_matched(MVDependency *dependency, Bitmapset *attnums)
* Load the functional dependencies for the indicated pg_statistic_ext tuple
*/
MVDependencies *
-statext_dependencies_load(Oid mvoid, bool inh)
+statext_dependencies_load(Oid relid, Oid mvoid, bool inh)
{
+ SysCacheIdentifier cacheId;
MVDependencies *result;
bool isnull;
Datum deps;
HeapTuple htup;
- htup = SearchSysCache2(STATEXTDATASTXOID,
+ cacheId = rel_is_global_temp(relid) ? TEMPSTATEXTDATASTXOID : STATEXTDATASTXOID;
+
+ htup = SearchSysCache2(cacheId,
ObjectIdGetDatum(mvoid),
BoolGetDatum(inh));
if (!HeapTupleIsValid(htup))
elog(ERROR, "cache lookup failed for statistics object %u", mvoid);
- deps = SysCacheGetAttr(STATEXTDATASTXOID, htup,
+ deps = SysCacheGetAttr(cacheId, htup,
Anum_pg_statistic_ext_data_stxddependencies, &isnull);
if (isnull)
elog(ERROR,
@@ -1608,7 +1611,7 @@ dependencies_clauselist_selectivity(PlannerInfo *root,
if (nmatched + nexprs < 2)
continue;
- deps = statext_dependencies_load(stat->statOid, rte->inh);
+ deps = statext_dependencies_load(rte->relid, stat->statOid, rte->inh);
/*
* The expressions may be represented by different attnums in the
diff --git a/src/backend/statistics/extended_stats.c b/src/backend/statistics/extended_stats.c
index 2b83355d26e..0c52bc083f6 100644
--- a/src/backend/statistics/extended_stats.c
+++ b/src/backend/statistics/extended_stats.c
@@ -23,6 +23,7 @@
#include "catalog/indexing.h"
#include "catalog/pg_statistic_ext.h"
#include "catalog/pg_statistic_ext_data.h"
+#include "catalog/pg_temp_statistic_ext_data.h"
#include "commands/defrem.h"
#include "commands/progress.h"
#include "executor/executor.h"
@@ -77,7 +78,7 @@ typedef struct StatExtEntry
static List *fetch_statentries_for_relation(Relation pg_statext, Relation rel);
static VacAttrStats **lookup_var_attr_stats(Bitmapset *attrs, List *exprs,
int nvacatts, VacAttrStats **vacatts);
-static void statext_store(Oid statOid, bool inh,
+static void statext_store(Oid relid, Oid statOid, bool inh,
MVNDistinct *ndistinct, MVDependencies *dependencies,
MCVList *mcv, Datum exprs, VacAttrStats **stats);
static int statext_compute_stattarget(int stattarget,
@@ -227,7 +228,7 @@ BuildRelationExtStatistics(Relation onerel, bool inh, double totalrows,
}
/* store the statistics in the catalog */
- statext_store(stat->statOid, inh,
+ statext_store(RelationGetRelid(onerel), stat->statOid, inh,
ndistinct, dependencies, mcv, exprstats, stats);
/* for reporting progress */
@@ -805,7 +806,7 @@ lookup_var_attr_stats(Bitmapset *attrs, List *exprs,
* tuple.
*/
static void
-statext_store(Oid statOid, bool inh,
+statext_store(Oid relid, Oid statOid, bool inh,
MVNDistinct *ndistinct, MVDependencies *dependencies,
MCVList *mcv, Datum exprs, VacAttrStats **stats)
{
@@ -814,7 +815,12 @@ statext_store(Oid statOid, bool inh,
Datum values[Natts_pg_statistic_ext_data];
bool nulls[Natts_pg_statistic_ext_data];
- pg_stextdata = table_open(StatisticExtDataRelationId, RowExclusiveLock);
+ if (rel_is_global_temp(relid))
+ pg_stextdata = table_open(TempStatisticExtDataRelationId,
+ RowExclusiveLock);
+ else
+ pg_stextdata = table_open(StatisticExtDataRelationId,
+ RowExclusiveLock);
memset(nulls, true, sizeof(nulls));
memset(values, 0, sizeof(values));
@@ -862,7 +868,7 @@ statext_store(Oid statOid, bool inh,
* Delete the old tuple if it exists, and insert a new one. It's easier
* than trying to update or insert, based on various conditions.
*/
- RemoveStatisticsDataById(statOid, inh);
+ RemoveStatisticsDataById(relid, statOid, inh);
/* form and insert a new tuple */
stup = heap_form_tuple(RelationGetDescr(pg_stextdata), values, nulls);
@@ -1891,7 +1897,7 @@ statext_mcv_clauselist_selectivity(PlannerInfo *root, List *clauses, int varReli
MCVList *mcv_list;
/* Load the MCV list stored in the statistics object */
- mcv_list = statext_mcv_load(stat->statOid, rte->inh);
+ mcv_list = statext_mcv_load(rte->relid, stat->statOid, rte->inh);
/*
* Compute the selectivity of the ORed list of clauses covered by
@@ -2450,8 +2456,9 @@ serialize_expr_stats(AnlExprData *exprdata, int nexprs)
* data to use.
*/
HeapTuple
-statext_expressions_load(Oid stxoid, bool inh, int idx)
+statext_expressions_load(Oid relid, Oid stxoid, bool inh, int idx)
{
+ SysCacheIdentifier cacheId;
bool isnull;
Datum value;
HeapTuple htup;
@@ -2460,12 +2467,14 @@ statext_expressions_load(Oid stxoid, bool inh, int idx)
HeapTupleData tmptup;
HeapTuple tup;
- htup = SearchSysCache2(STATEXTDATASTXOID,
+ cacheId = rel_is_global_temp(relid) ? TEMPSTATEXTDATASTXOID : STATEXTDATASTXOID;
+
+ htup = SearchSysCache2(cacheId,
ObjectIdGetDatum(stxoid), BoolGetDatum(inh));
if (!HeapTupleIsValid(htup))
elog(ERROR, "cache lookup failed for statistics object %u", stxoid);
- value = SysCacheGetAttr(STATEXTDATASTXOID, htup,
+ value = SysCacheGetAttr(cacheId, htup,
Anum_pg_statistic_ext_data_stxdexpr, &isnull);
if (isnull)
elog(ERROR,
diff --git a/src/backend/statistics/extended_stats_funcs.c b/src/backend/statistics/extended_stats_funcs.c
index a5dce8a2206..b0a3801a231 100644
--- a/src/backend/statistics/extended_stats_funcs.c
+++ b/src/backend/statistics/extended_stats_funcs.c
@@ -24,6 +24,7 @@
#include "catalog/pg_operator.h"
#include "catalog/pg_statistic_ext.h"
#include "catalog/pg_statistic_ext_data.h"
+#include "catalog/pg_temp_statistic_ext_data.h"
#include "miscadmin.h"
#include "nodes/makefuncs.h"
#include "nodes/nodeFuncs.h"
@@ -125,7 +126,7 @@ static bool extended_statistics_update(FunctionCallInfo fcinfo);
static HeapTuple get_pg_statistic_ext(Relation pg_stext, Oid nspoid,
const char *stxname);
-static bool delete_pg_statistic_ext_data(Oid stxoid, bool inherited);
+static bool delete_pg_statistic_ext_data(Oid relid, Oid stxoid, bool inherited);
/*
* Track the extended statistics kinds expected for a pg_statistic_ext
@@ -140,7 +141,8 @@ typedef struct
} StakindFlags;
static void expand_stxkind(HeapTuple tup, StakindFlags *enabled);
-static void upsert_pg_statistic_ext_data(const Datum *values,
+static void upsert_pg_statistic_ext_data(Oid relid,
+ const Datum *values,
const bool *nulls,
const bool *replaces);
@@ -265,16 +267,28 @@ expand_stxkind(HeapTuple tup, StakindFlags *enabled)
* Perform the actual storage of a pg_statistic_ext_data tuple.
*/
static void
-upsert_pg_statistic_ext_data(const Datum *values, const bool *nulls,
- const bool *replaces)
+upsert_pg_statistic_ext_data(Oid relid, const Datum *values,
+ const bool *nulls, const bool *replaces)
{
Relation pg_stextdata;
+ SysCacheIdentifier cacheId;
HeapTuple stxdtup;
HeapTuple newtup;
- pg_stextdata = table_open(StatisticExtDataRelationId, RowExclusiveLock);
+ if (rel_is_global_temp(relid))
+ {
+ pg_stextdata = table_open(TempStatisticExtDataRelationId,
+ RowExclusiveLock);
+ cacheId = TEMPSTATEXTDATASTXOID;
+ }
+ else
+ {
+ pg_stextdata = table_open(StatisticExtDataRelationId,
+ RowExclusiveLock);
+ cacheId = STATEXTDATASTXOID;
+ }
- stxdtup = SearchSysCache2(STATEXTDATASTXOID,
+ stxdtup = SearchSysCache2(cacheId,
values[Anum_pg_statistic_ext_data_stxoid - 1],
values[Anum_pg_statistic_ext_data_stxdinherit - 1]);
@@ -749,7 +763,7 @@ extended_statistics_update(FunctionCallInfo fcinfo)
success = false;
}
- upsert_pg_statistic_ext_data(values, nulls, replaces);
+ upsert_pg_statistic_ext_data(relid, values, nulls, replaces);
cleanup:
if (HeapTupleIsValid(tup))
@@ -1689,14 +1703,26 @@ exprs_error:
* row and "inherited" pair.
*/
static bool
-delete_pg_statistic_ext_data(Oid stxoid, bool inherited)
+delete_pg_statistic_ext_data(Oid relid, Oid stxoid, bool inherited)
{
- Relation sed = table_open(StatisticExtDataRelationId, RowExclusiveLock);
+ Relation sed;
+ SysCacheIdentifier cacheId;
HeapTuple oldtup;
bool result = false;
+ if (rel_is_global_temp(relid))
+ {
+ sed = table_open(TempStatisticExtDataRelationId, RowExclusiveLock);
+ cacheId = TEMPSTATEXTDATASTXOID;
+ }
+ else
+ {
+ sed = table_open(StatisticExtDataRelationId, RowExclusiveLock);
+ cacheId = STATEXTDATASTXOID;
+ }
+
/* Is there already a pg_statistic_ext_data tuple for this attribute? */
- oldtup = SearchSysCache2(STATEXTDATASTXOID,
+ oldtup = SearchSysCache2(cacheId,
ObjectIdGetDatum(stxoid),
BoolGetDatum(inherited));
@@ -1831,7 +1857,7 @@ pg_clear_extended_stats(PG_FUNCTION_ARGS)
PG_RETURN_VOID();
}
- delete_pg_statistic_ext_data(stxform->oid, inherited);
+ delete_pg_statistic_ext_data(relid, stxform->oid, inherited);
heap_freetuple(tup);
table_close(pg_stext, RowExclusiveLock);
diff --git a/src/backend/statistics/mcv.c b/src/backend/statistics/mcv.c
index 0b7da605a4c..f77b06ca9dc 100644
--- a/src/backend/statistics/mcv.c
+++ b/src/backend/statistics/mcv.c
@@ -553,18 +553,21 @@ build_column_frequencies(SortItem *groups, int ngroups,
* Load the MCV list for the indicated pg_statistic_ext_data tuple.
*/
MCVList *
-statext_mcv_load(Oid mvoid, bool inh)
+statext_mcv_load(Oid relid, Oid mvoid, bool inh)
{
+ SysCacheIdentifier cacheId;
MCVList *result;
bool isnull;
Datum mcvlist;
- HeapTuple htup = SearchSysCache2(STATEXTDATASTXOID,
- ObjectIdGetDatum(mvoid), BoolGetDatum(inh));
+ HeapTuple htup;
+ cacheId = rel_is_global_temp(relid) ? TEMPSTATEXTDATASTXOID : STATEXTDATASTXOID;
+
+ htup = SearchSysCache2(cacheId, ObjectIdGetDatum(mvoid), BoolGetDatum(inh));
if (!HeapTupleIsValid(htup))
elog(ERROR, "cache lookup failed for statistics object %u", mvoid);
- mcvlist = SysCacheGetAttr(STATEXTDATASTXOID, htup,
+ mcvlist = SysCacheGetAttr(cacheId, htup,
Anum_pg_statistic_ext_data_stxdmcv, &isnull);
if (isnull)
@@ -2058,7 +2061,7 @@ mcv_clauselist_selectivity(PlannerInfo *root, StatisticExtInfo *stat,
bool *matches = NULL;
/* load the MCV list stored in the statistics object */
- mcv = statext_mcv_load(stat->statOid, rte->inh);
+ mcv = statext_mcv_load(rte->relid, stat->statOid, rte->inh);
/* build a match bitmap for the clauses */
matches = mcv_get_match_bitmap(root, clauses, stat->keys, stat->exprs,
diff --git a/src/backend/statistics/mvdistinct.c b/src/backend/statistics/mvdistinct.c
index 4f8f578a22f..d321ef10ba7 100644
--- a/src/backend/statistics/mvdistinct.c
+++ b/src/backend/statistics/mvdistinct.c
@@ -28,6 +28,7 @@
#include "catalog/pg_statistic_ext.h"
#include "catalog/pg_statistic_ext_data.h"
#include "statistics/extended_stats_internal.h"
+#include "utils/lsyscache.h"
#include "utils/syscache.h"
#include "utils/typcache.h"
#include "varatt.h"
@@ -142,19 +143,22 @@ statext_ndistinct_build(double totalrows, StatsBuildData *data)
* Load the ndistinct value for the indicated pg_statistic_ext tuple
*/
MVNDistinct *
-statext_ndistinct_load(Oid mvoid, bool inh)
+statext_ndistinct_load(Oid relid, Oid mvoid, bool inh)
{
+ SysCacheIdentifier cacheId;
MVNDistinct *result;
bool isnull;
Datum ndist;
HeapTuple htup;
- htup = SearchSysCache2(STATEXTDATASTXOID,
+ cacheId = rel_is_global_temp(relid) ? TEMPSTATEXTDATASTXOID : STATEXTDATASTXOID;
+
+ htup = SearchSysCache2(cacheId,
ObjectIdGetDatum(mvoid), BoolGetDatum(inh));
if (!HeapTupleIsValid(htup))
elog(ERROR, "cache lookup failed for statistics object %u", mvoid);
- ndist = SysCacheGetAttr(STATEXTDATASTXOID, htup,
+ ndist = SysCacheGetAttr(cacheId, htup,
Anum_pg_statistic_ext_data_stxdndistinct, &isnull);
if (isnull)
elog(ERROR,
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 82be6a4d2af..692c6fadbe5 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -4677,7 +4677,7 @@ estimate_multivariate_ndistinct(PlannerInfo *root, RelOptInfo *rel,
Assert(nmatches_vars + nmatches_exprs > 1);
- stats = statext_ndistinct_load(statOid, rte->inh);
+ stats = statext_ndistinct_load(rte->relid, statOid, rte->inh);
/*
* If we have a match, search it for the specific item that matches (there
@@ -5937,7 +5937,8 @@ examine_variable(PlannerInfo *root, Node *node, int varRelid,
* Now we just create a new copy every time.
*/
vardata->statsTuple =
- statext_expressions_load(info->statOid, rte->inh, pos);
+ statext_expressions_load(rte->relid, info->statOid,
+ rte->inh, pos);
/* Nothing to release if no data found */
if (vardata->statsTuple != NULL)
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index b144d0639b8..7ea08c2428f 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -64,6 +64,7 @@
#include "catalog/pg_tablespace.h"
#include "catalog/pg_temp_class.h"
#include "catalog/pg_temp_statistic.h"
+#include "catalog/pg_temp_statistic_ext_data.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
#include "catalog/schemapg.h"
diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile
index 6aec03add22..f60a6454df2 100644
--- a/src/include/catalog/Makefile
+++ b/src/include/catalog/Makefile
@@ -88,7 +88,8 @@ CATALOG_HEADERS := \
pg_propgraph_label_property.h \
pg_propgraph_property.h \
pg_temp_class.h \
- pg_temp_statistic.h
+ pg_temp_statistic.h \
+ pg_temp_statistic_ext_data.h
GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h)
diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build
index db809c4b3c0..7599731de7d 100644
--- a/src/include/catalog/meson.build
+++ b/src/include/catalog/meson.build
@@ -76,6 +76,7 @@ catalog_headers = [
'pg_propgraph_property.h',
'pg_temp_class.h',
'pg_temp_statistic.h',
+ 'pg_temp_statistic_ext_data.h',
]
# The .dat files we need can just be listed alphabetically.
diff --git a/src/include/catalog/pg_statistic_ext_data.h b/src/include/catalog/pg_statistic_ext_data.h
index dbc4acc7d1a..e3a6fda0030 100644
--- a/src/include/catalog/pg_statistic_ext_data.h
+++ b/src/include/catalog/pg_statistic_ext_data.h
@@ -30,6 +30,9 @@
*/
BEGIN_CATALOG_STRUCT
+/*
+ * NB: Any changes made here must be reflected in pg_temp_statistic_ext_data.
+ */
CATALOG(pg_statistic_ext_data,3429,StatisticExtDataRelationId)
{
Oid stxoid BKI_LOOKUP(pg_statistic_ext); /* statistics object
diff --git a/src/include/catalog/pg_temp_statistic_ext_data.h b/src/include/catalog/pg_temp_statistic_ext_data.h
new file mode 100644
index 00000000000..eb3b4b2ed04
--- /dev/null
+++ b/src/include/catalog/pg_temp_statistic_ext_data.h
@@ -0,0 +1,62 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_temp_statistic_ext_data.h
+ * definition of the "temporary extended statistics data" system catalog
+ * (pg_temp_statistic_ext_data)
+ *
+ * This is a global temporary system catalog table storing the statistical
+ * data for extended statistics objects on temporary tables. Currently, it
+ * is only used for global temporary tables.
+ *
+ * Portions Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * src/include/catalog/pg_temp_statistic_ext_data.h
+ *
+ * NOTES
+ * The Catalog.pm module reads this file and derives schema
+ * information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_TEMP_STATISTIC_EXT_DATA_H
+#define PG_TEMP_STATISTIC_EXT_DATA_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_temp_statistic_ext_data_d.h" /* IWYU pragma: export */
+
+/* ----------------
+ * pg_temp_statistic_ext_data definition. cpp turns this into
+ * typedef struct FormData_pg_temp_statistic_ext_data
+ * ----------------
+ */
+BEGIN_CATALOG_STRUCT
+
+/*
+ * NB: The fields here must exactly match those in pg_statistic_ext_data.
+ */
+CATALOG(pg_temp_statistic_ext_data,8088,TempStatisticExtDataRelationId) BKI_TEMP_RELATION
+{
+ Oid stxoid BKI_LOOKUP(pg_statistic_ext); /* statistics object
+ * this data is for */
+ bool stxdinherit; /* true if inheritance children are included */
+
+#ifdef CATALOG_VARLEN /* variable-length fields start here */
+
+ pg_ndistinct stxdndistinct; /* ndistinct coefficients (serialized) */
+ pg_dependencies stxddependencies; /* dependencies (serialized) */
+ pg_mcv_list stxdmcv; /* MCV (serialized) */
+ pg_statistic stxdexpr[1]; /* stats for expressions */
+
+#endif
+
+} FormData_pg_temp_statistic_ext_data;
+
+END_CATALOG_STRUCT
+
+DECLARE_TOAST(pg_temp_statistic_ext_data, 8089, 8090);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_temp_statistic_ext_data_stxoid_inh_index, 8091, TempStatisticExtDataStxoidInhIndexId, pg_temp_statistic_ext_data, btree(stxoid oid_ops, stxdinherit bool_ops));
+
+MAKE_SYSCACHE(TEMPSTATEXTDATASTXOID, pg_temp_statistic_ext_data_stxoid_inh_index, 4);
+
+#endif /* PG_TEMP_STATISTIC_EXT_DATA_H */
diff --git a/src/include/commands/defrem.h b/src/include/commands/defrem.h
index d080ad59b71..9edc0a76a75 100644
--- a/src/include/commands/defrem.h
+++ b/src/include/commands/defrem.h
@@ -89,7 +89,7 @@ extern ObjectAddress AlterOperator(AlterOperatorStmt *stmt);
extern ObjectAddress CreateStatistics(CreateStatsStmt *stmt, bool check_rights);
extern ObjectAddress AlterStatistics(AlterStatsStmt *stmt);
extern void RemoveStatisticsById(Oid statsOid);
-extern void RemoveStatisticsDataById(Oid statsOid, bool inh);
+extern void RemoveStatisticsDataById(Oid relid, Oid statsOid, bool inh);
extern Oid StatisticsGetRelation(Oid statId, bool missing_ok);
/* commands/aggregatecmds.c */
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 8f9b9d237fd..2ab93adaf82 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -94,9 +94,9 @@ typedef struct MCVList
MCVItem items[FLEXIBLE_ARRAY_MEMBER]; /* array of MCV items */
} MCVList;
-extern MVNDistinct *statext_ndistinct_load(Oid mvoid, bool inh);
-extern MVDependencies *statext_dependencies_load(Oid mvoid, bool inh);
-extern MCVList *statext_mcv_load(Oid mvoid, bool inh);
+extern MVNDistinct *statext_ndistinct_load(Oid relid, Oid mvoid, bool inh);
+extern MVDependencies *statext_dependencies_load(Oid relid, Oid mvoid, bool inh);
+extern MCVList *statext_mcv_load(Oid relid, Oid mvoid, bool inh);
extern void BuildRelationExtStatistics(Relation onerel, bool inh, double totalrows,
int numrows, HeapTuple *rows,
@@ -126,6 +126,6 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
Bitmapset **clause_attnums,
List **clause_exprs,
int nclauses);
-extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
+extern HeapTuple statext_expressions_load(Oid relid, Oid stxoid, bool inh, int idx);
#endif /* STATISTICS_H */
diff --git a/src/test/regress/expected/global_temp.out b/src/test/regress/expected/global_temp.out
index a2723875013..10519ceee21 100644
--- a/src/test/regress/expected/global_temp.out
+++ b/src/test/regress/expected/global_temp.out
@@ -693,6 +693,77 @@ SELECT row_estimate('SELECT * FROM tmp2 WHERE b = 8');
17
(1 row)
+DROP TABLE tmp2;
+-- Test extended stats
+CREATE GLOBAL TEMP TABLE tmp2 (a int, b int, c int);
+INSERT INTO tmp2
+ SELECT x, floor(sqrt(x)), floor(sqrt(x)) + 100 FROM generate_series(36, 99) x;
+CREATE STATISTICS tmp2_stats ON b, c FROM tmp2;
+ANALYZE tmp2;
+SELECT stxname,
+ replace(d.stxdndistinct, '}, ', E'},\n') AS stxdndistinct,
+ replace(d.stxddependencies, '}, ', E'},\n') AS stxddependencies
+ FROM pg_statistic_ext s
+ LEFT JOIN pg_statistic_ext_data d ON d.stxoid = s.oid
+ WHERE s.stxname = 'tmp2_stats';
+ stxname | stxdndistinct | stxddependencies
+------------+---------------+------------------
+ tmp2_stats | |
+(1 row)
+
+SELECT stxname,
+ replace(d.stxdndistinct, '}, ', E'},\n') AS stxdndistinct,
+ replace(d.stxddependencies, '}, ', E'},\n') AS stxddependencies
+ FROM pg_statistic_ext s
+ LEFT JOIN pg_temp_statistic_ext_data d ON d.stxoid = s.oid
+ WHERE s.stxname = 'tmp2_stats';
+ stxname | stxdndistinct | stxddependencies
+------------+------------------------------------------+------------------------------------------------------------
+ tmp2_stats | [{"attributes": [2, 3], "ndistinct": 4}] | [{"attributes": [2], "dependency": 3, "degree": 1.000000},+
+ | | {"attributes": [3], "dependency": 2, "degree": 1.000000}]
+(1 row)
+
+SELECT m.*
+ FROM pg_statistic_ext s, pg_statistic_ext_data d,
+ pg_mcv_list_items(d.stxdmcv) m
+ WHERE s.stxname = 'tmp2_stats'
+ AND d.stxoid = s.oid;
+ index | values | nulls | frequency | base_frequency
+-------+--------+-------+-----------+----------------
+(0 rows)
+
+SELECT m.*
+ FROM pg_statistic_ext s, pg_temp_statistic_ext_data d,
+ pg_mcv_list_items(d.stxdmcv) m
+ WHERE s.stxname = 'tmp2_stats'
+ AND d.stxoid = s.oid;
+ index | values | nulls | frequency | base_frequency
+-------+---------+-------+-----------+----------------
+ 0 | {9,109} | {f,f} | 0.296875 | 0.088134765625
+ 1 | {8,108} | {f,f} | 0.265625 | 0.070556640625
+ 2 | {7,107} | {f,f} | 0.234375 | 0.054931640625
+ 3 | {6,106} | {f,f} | 0.203125 | 0.041259765625
+(4 rows)
+
+SELECT most_common_vals FROM pg_stats_ext
+ WHERE schemaname = 'global_temp_tests' AND tablename = 'tmp2';
+ most_common_vals
+-----------------------------------
+ {{9,109},{8,108},{7,107},{6,106}}
+(1 row)
+
+SELECT COUNT(*) FROM tmp2 WHERE b = 8 AND c = 108;
+ count
+-------
+ 17
+(1 row)
+
+SELECT row_estimate('SELECT * FROM tmp2 WHERE b = 8 AND c = 108');
+ row_estimate
+--------------
+ 17
+(1 row)
+
DROP TABLE tmp2;
-- Test view creation
CREATE VIEW v AS SELECT * FROM tmp1;
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index be2efa78257..bccae052a15 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -298,3 +298,4 @@ NOTICE: checking pg_temp_statistic {stacoll2} => pg_collation {oid}
NOTICE: checking pg_temp_statistic {stacoll3} => pg_collation {oid}
NOTICE: checking pg_temp_statistic {stacoll4} => pg_collation {oid}
NOTICE: checking pg_temp_statistic {stacoll5} => pg_collation {oid}
+NOTICE: checking pg_temp_statistic_ext_data {stxoid} => pg_statistic_ext {oid}
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index c3cafeb6e43..ed31c0e2998 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1367,6 +1367,21 @@ UNION ALL
pg_temp_statistic.stavalues4,
pg_temp_statistic.stavalues5
FROM pg_temp_statistic;
+pg_all_statistic_ext_data| SELECT pg_statistic_ext_data.stxoid,
+ pg_statistic_ext_data.stxdinherit,
+ pg_statistic_ext_data.stxdndistinct,
+ pg_statistic_ext_data.stxddependencies,
+ pg_statistic_ext_data.stxdmcv,
+ pg_statistic_ext_data.stxdexpr
+ FROM pg_statistic_ext_data
+UNION ALL
+ SELECT pg_temp_statistic_ext_data.stxoid,
+ pg_temp_statistic_ext_data.stxdinherit,
+ pg_temp_statistic_ext_data.stxdndistinct,
+ pg_temp_statistic_ext_data.stxddependencies,
+ pg_temp_statistic_ext_data.stxdmcv,
+ pg_temp_statistic_ext_data.stxdexpr
+ FROM pg_temp_statistic_ext_data;
pg_available_extension_versions| SELECT e.name,
e.version,
(x.extname IS NOT NULL) AS installed,
@@ -2793,7 +2808,7 @@ pg_stats_ext| SELECT cn.nspname AS schemaname,
m.most_common_base_freqs
FROM (((((pg_statistic_ext s
JOIN pg_class c ON ((c.oid = s.stxrelid)))
- JOIN pg_statistic_ext_data sd ON ((s.oid = sd.stxoid)))
+ JOIN pg_all_statistic_ext_data sd ON ((s.oid = sd.stxoid)))
LEFT JOIN pg_namespace cn ON ((cn.oid = c.relnamespace)))
LEFT JOIN pg_namespace sn ON ((sn.oid = s.stxnamespace)))
LEFT JOIN LATERAL ( SELECT array_agg(pg_mcv_list_items."values") AS most_common_vals,
@@ -2896,7 +2911,7 @@ pg_stats_ext_exprs| SELECT cn.nspname AS schemaname,
END AS range_bounds_histogram
FROM (((((pg_statistic_ext s
JOIN pg_class c ON ((c.oid = s.stxrelid)))
- LEFT JOIN pg_statistic_ext_data sd ON ((s.oid = sd.stxoid)))
+ LEFT JOIN pg_all_statistic_ext_data sd ON ((s.oid = sd.stxoid)))
LEFT JOIN pg_namespace cn ON ((cn.oid = c.relnamespace)))
LEFT JOIN pg_namespace sn ON ((sn.oid = s.stxnamespace)))
JOIN LATERAL ( SELECT unnest(pg_get_statisticsobjdef_expressions(s.oid)) AS expr,
diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out
index 5f5756d5596..3f14d8f9e8a 100644
--- a/src/test/regress/expected/sanity_check.out
+++ b/src/test/regress/expected/sanity_check.out
@@ -48,3 +48,27 @@ UNION ALL
---------+----------+--------+--------+-----------+----------+----------+----------+------------+----------------+------------+-----------+---------------+-------------+--------------+--------------+------------+-------------+--------------
(0 rows)
+-- check that pg_statistic_ext_data and pg_temp_statistic_ext_data have the
+-- exact same columns
+WITH t1 AS (
+ SELECT attname, atttypid, attlen, attnum, atttypmod, attndims, attbyval,
+ attalign, attstorage, attcompression, attnotnull, atthasdef,
+ atthasmissing, attidentity, attgenerated, attisdropped, attislocal,
+ attinhcount, attcollation
+ FROM pg_attribute
+ WHERE attrelid = 'pg_statistic_ext_data'::regclass
+), t2 AS (
+ SELECT attname, atttypid, attlen, attnum, atttypmod, attndims, attbyval,
+ attalign, attstorage, attcompression, attnotnull, atthasdef,
+ atthasmissing, attidentity, attgenerated, attisdropped, attislocal,
+ attinhcount, attcollation
+ FROM pg_attribute
+ WHERE attrelid = 'pg_temp_statistic_ext_data'::regclass
+)
+(SELECT * FROM t1 EXCEPT SELECT * FROM t2)
+UNION ALL
+(SELECT * FROM t2 EXCEPT SELECT * FROM t1);
+ attname | atttypid | attlen | attnum | atttypmod | attndims | attbyval | attalign | attstorage | attcompression | attnotnull | atthasdef | atthasmissing | attidentity | attgenerated | attisdropped | attislocal | attinhcount | attcollation
+---------+----------+--------+--------+-----------+----------+----------+----------+------------+----------------+------------+-----------+---------------+-------------+--------------+--------------+------------+-------------+--------------
+(0 rows)
+
diff --git a/src/test/regress/sql/global_temp.sql b/src/test/regress/sql/global_temp.sql
index 3ecc766c3df..4272824e894 100644
--- a/src/test/regress/sql/global_temp.sql
+++ b/src/test/regress/sql/global_temp.sql
@@ -404,6 +404,47 @@ SELECT row_estimate('SELECT * FROM tmp2 WHERE b = 8');
DROP TABLE tmp2;
+-- Test extended stats
+CREATE GLOBAL TEMP TABLE tmp2 (a int, b int, c int);
+INSERT INTO tmp2
+ SELECT x, floor(sqrt(x)), floor(sqrt(x)) + 100 FROM generate_series(36, 99) x;
+CREATE STATISTICS tmp2_stats ON b, c FROM tmp2;
+ANALYZE tmp2;
+
+SELECT stxname,
+ replace(d.stxdndistinct, '}, ', E'},\n') AS stxdndistinct,
+ replace(d.stxddependencies, '}, ', E'},\n') AS stxddependencies
+ FROM pg_statistic_ext s
+ LEFT JOIN pg_statistic_ext_data d ON d.stxoid = s.oid
+ WHERE s.stxname = 'tmp2_stats';
+
+SELECT stxname,
+ replace(d.stxdndistinct, '}, ', E'},\n') AS stxdndistinct,
+ replace(d.stxddependencies, '}, ', E'},\n') AS stxddependencies
+ FROM pg_statistic_ext s
+ LEFT JOIN pg_temp_statistic_ext_data d ON d.stxoid = s.oid
+ WHERE s.stxname = 'tmp2_stats';
+
+SELECT m.*
+ FROM pg_statistic_ext s, pg_statistic_ext_data d,
+ pg_mcv_list_items(d.stxdmcv) m
+ WHERE s.stxname = 'tmp2_stats'
+ AND d.stxoid = s.oid;
+
+SELECT m.*
+ FROM pg_statistic_ext s, pg_temp_statistic_ext_data d,
+ pg_mcv_list_items(d.stxdmcv) m
+ WHERE s.stxname = 'tmp2_stats'
+ AND d.stxoid = s.oid;
+
+SELECT most_common_vals FROM pg_stats_ext
+ WHERE schemaname = 'global_temp_tests' AND tablename = 'tmp2';
+
+SELECT COUNT(*) FROM tmp2 WHERE b = 8 AND c = 108;
+SELECT row_estimate('SELECT * FROM tmp2 WHERE b = 8 AND c = 108');
+
+DROP TABLE tmp2;
+
-- Test view creation
CREATE VIEW v AS SELECT * FROM tmp1;
SELECT * FROM v;
diff --git a/src/test/regress/sql/sanity_check.sql b/src/test/regress/sql/sanity_check.sql
index ed0096d7823..4bf00feba2c 100644
--- a/src/test/regress/sql/sanity_check.sql
+++ b/src/test/regress/sql/sanity_check.sql
@@ -39,3 +39,24 @@ WITH t1 AS (
(SELECT * FROM t1 EXCEPT SELECT * FROM t2)
UNION ALL
(SELECT * FROM t2 EXCEPT SELECT * FROM t1);
+
+-- check that pg_statistic_ext_data and pg_temp_statistic_ext_data have the
+-- exact same columns
+WITH t1 AS (
+ SELECT attname, atttypid, attlen, attnum, atttypmod, attndims, attbyval,
+ attalign, attstorage, attcompression, attnotnull, atthasdef,
+ atthasmissing, attidentity, attgenerated, attisdropped, attislocal,
+ attinhcount, attcollation
+ FROM pg_attribute
+ WHERE attrelid = 'pg_statistic_ext_data'::regclass
+), t2 AS (
+ SELECT attname, atttypid, attlen, attnum, atttypmod, attndims, attbyval,
+ attalign, attstorage, attcompression, attnotnull, atthasdef,
+ atthasmissing, attidentity, attgenerated, attisdropped, attislocal,
+ attinhcount, attcollation
+ FROM pg_attribute
+ WHERE attrelid = 'pg_temp_statistic_ext_data'::regclass
+)
+(SELECT * FROM t1 EXCEPT SELECT * FROM t2)
+UNION ALL
+(SELECT * FROM t2 EXCEPT SELECT * FROM t1);
--
2.51.0
[text/x-patch] v8-0010-Add-pg_temp_index-global-temporary-catalog-table.patch (69.1K, ../CAEZATCVjaZFa80S-V_+nvnzRK1ZiLUPx8jwdyAtRVHAmPZQQMg@mail.gmail.com/11-v8-0010-Add-pg_temp_index-global-temporary-catalog-table.patch)
download | inline diff:
From f84e2e962a115f80fd1eebe35d88808a7ef9032b Mon Sep 17 00:00:00 2001
From: Dean Rasheed <[email protected]>
Date: Sat, 20 Jun 2026 09:37:58 +0100
Subject: [PATCH v8 10/10] Add pg_temp_index global temporary catalog table.
This just has indexrelid and indisvalid columns, allowing the valid
state of an index to session-specific. This is needed when one session
defines an index on a global temporary table, while another session is
using the table, and has already populated it. In all other cases,
pg_index.indexrelid and pg_temp_index.indexrelid are expected to be
the same.
---
contrib/tcn/tcn.c | 7 +-
src/backend/access/transam/xact.c | 9 +-
src/backend/catalog/global_temp.c | 39 +-
src/backend/catalog/index.c | 48 +-
src/backend/catalog/pg_temp_class.c | 559 +++++++++++++++++---
src/backend/commands/indexcmds.c | 41 +-
src/backend/commands/repack.c | 3 +-
src/backend/commands/tablecmds.c | 39 +-
src/backend/utils/cache/lsyscache.c | 4 +-
src/backend/utils/cache/relcache.c | 44 +-
src/bin/psql/describe.c | 80 ++-
src/include/catalog/Makefile | 1 +
src/include/catalog/meson.build | 1 +
src/include/catalog/pg_temp_class.h | 9 +-
src/include/catalog/pg_temp_index.h | 66 +++
src/test/isolation/expected/global-temp.out | 81 +++
src/test/isolation/specs/global-temp.spec | 2 +
src/test/regress/expected/global_temp.out | 107 +++-
src/test/regress/expected/oidjoins.out | 1 +
src/test/regress/sql/global_temp.sql | 34 ++
src/tools/pgindent/typedefs.list | 2 +
21 files changed, 1040 insertions(+), 137 deletions(-)
create mode 100644 src/include/catalog/pg_temp_index.h
diff --git a/contrib/tcn/tcn.c b/contrib/tcn/tcn.c
index 6b4bc7960d9..3201f438c4b 100644
--- a/contrib/tcn/tcn.c
+++ b/contrib/tcn/tcn.c
@@ -16,6 +16,7 @@
#include "postgres.h"
#include "access/htup_details.h"
+#include "catalog/pg_temp_class.h"
#include "commands/async.h"
#include "commands/trigger.h"
#include "executor/spi.h"
@@ -135,7 +136,7 @@ triggered_change_notification(PG_FUNCTION_ARGS)
HeapTuple indexTuple;
Form_pg_index index;
- indexTuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(indexoid));
+ indexTuple = GetEffectivePgIndexTuple(indexoid);
if (!HeapTupleIsValid(indexTuple)) /* should not happen */
elog(ERROR, "cache lookup failed for index %u", indexoid);
index = (Form_pg_index) GETSTRUCT(indexTuple);
@@ -167,10 +168,10 @@ triggered_change_notification(PG_FUNCTION_ARGS)
Async_Notify(channel, payload.data);
}
- ReleaseSysCache(indexTuple);
+ heap_freetuple(indexTuple);
break;
}
- ReleaseSysCache(indexTuple);
+ heap_freetuple(indexTuple);
}
list_free(indexoidlist);
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 7c631de9393..8fbbb425e2c 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -37,6 +37,7 @@
#include "catalog/namespace.h"
#include "catalog/pg_enum.h"
#include "catalog/pg_temp_class.h"
+#include "catalog/pg_temp_index.h"
#include "catalog/storage.h"
#include "commands/async.h"
#include "commands/tablecmds.h"
@@ -1149,7 +1150,7 @@ CommandCounterIncrement(void)
(errcode(ERRCODE_INVALID_TRANSACTION_STATE),
errmsg("cannot start commands during a parallel operation")));
- /* Flush out any pending inserts to pg_temp_class */
+ /* Flush out any pending pg_temp_class and pg_temp_index inserts */
PreCCI_PgTempClass();
currentCommandId += 1;
@@ -2364,7 +2365,7 @@ CommitTransaction(void)
*/
PreCommit_on_commit_actions();
- /* Flush out any pending inserts to pg_temp_class */
+ /* Flush out any pending pg_temp_class and pg_temp_index inserts */
PreCommit_PgTempClass();
/*
@@ -2640,7 +2641,7 @@ PrepareTransaction(void)
*/
PreCommit_on_commit_actions();
- /* Flush out any pending inserts to pg_temp_class */
+ /* Flush out any pending pg_temp_class and pg_temp_index inserts */
PreCommit_PgTempClass();
/*
@@ -5202,7 +5203,7 @@ CommitSubTransaction(void)
CallSubXactCallbacks(SUBXACT_EVENT_PRE_COMMIT_SUB, s->subTransactionId,
s->parent->subTransactionId);
- /* Flush out any pending inserts to pg_temp_class */
+ /* Flush out any pending pg_temp_class and pg_temp_index inserts */
PreSubCommit_PgTempClass();
/*
diff --git a/src/backend/catalog/global_temp.c b/src/backend/catalog/global_temp.c
index 6cc89c59890..9abf1f23165 100644
--- a/src/backend/catalog/global_temp.c
+++ b/src/backend/catalog/global_temp.c
@@ -122,6 +122,7 @@ static bool eoxact_storage_list_overflowed = false;
typedef struct GtrUsageEntry
{
Oid relid; /* lookup key: OID of relation in use */
+ char relkind; /* relkind of the relation */
SubTransactionId started_subid; /* usage started in current xact */
SubTransactionId stopped_subid; /* usage ended with another subid set */
} GtrUsageEntry;
@@ -519,7 +520,7 @@ gtr_init_usage_tables(void)
* Note: This is intentionally idempotent.
*/
static void
-gtr_record_usage(Oid relid)
+gtr_record_usage(Oid relid, char relkind)
{
GtrUsageEntry *local_entry;
GtrSharedUsageKey key;
@@ -534,6 +535,9 @@ gtr_record_usage(Oid relid)
if (found)
return; /* already recorded, nothing to do */
+ /* Remember the relation's relkind */
+ local_entry->relkind = relkind;
+
/* Record the usage as starting in the current subtransaction */
local_entry->started_subid = GetCurrentSubTransactionId();
local_entry->stopped_subid = InvalidSubTransactionId;
@@ -973,14 +977,33 @@ GlobalTempRelationCreated(Relation relation)
{
/*
* If this is the first time we've used this relation in this session,
- * insert a pg_temp_class tuple for it, and update the usage hash tables.
+ * insert pg_temp_class and pg_temp_index tuples for it, and update the
+ * usage hash tables.
*/
if (gtr_local_usage == NULL ||
hash_search(gtr_local_usage,
&relation->rd_id, HASH_FIND, NULL) == NULL)
{
InsertPgTempClassTuple(relation);
- gtr_record_usage(relation->rd_id);
+
+ if (relation->rd_rel->relkind == RELKIND_INDEX ||
+ relation->rd_rel->relkind == RELKIND_PARTITIONED_INDEX)
+ {
+ /*
+ * When creating a new index locally, relation->rd_index will be
+ * NULL here. Mark it as valid for now --- UpdateIndexRelation()
+ * will update it later, if it's not actually valid (e.g., CREATE
+ * INDEX ... ON ONLY ...). Otherwise, for an index created in
+ * another session, relation->rd_index->indisvalid will accurately
+ * reflect whether or not the index needs to be marked invalid
+ * locally (if our instance of the index's table is not empty).
+ */
+ InsertPgTempIndexTuple(relation->rd_id,
+ relation->rd_index == NULL ||
+ relation->rd_index->indisvalid);
+ }
+
+ gtr_record_usage(relation->rd_id, relation->rd_rel->relkind);
}
}
@@ -1011,8 +1034,12 @@ GlobalTempRelationDropped(Oid relid)
/* Flag the usage entry for eoxact cleanup */
EOXactUsageListAdd(relid);
- /* Delete it's pg_temp_class tuple */
+ /* Delete it's pg_temp_class and pg_temp_index tuples */
DeletePgTempClassTuple(relid);
+
+ if (entry->relkind == RELKIND_INDEX ||
+ entry->relkind == RELKIND_PARTITIONED_INDEX)
+ DeletePgTempIndexTuple(relid);
}
/* Forget any ON COMMIT action for the relation */
@@ -1142,7 +1169,7 @@ AtEOXact_GlobalTempRelation(bool isCommit)
}
}
- /* Perform any pg_temp_class processing */
+ /* Perform any pg_temp_class and pg_temp_index processing */
AtEOXact_PgTempClass(isCommit);
/* Now we're out of the transaction and can clear the lists */
@@ -1216,7 +1243,7 @@ AtEOSubXact_GlobalTempRelation(bool isCommit, SubTransactionId mySubid,
}
}
- /* Perform any pg_temp_class processing */
+ /* Perform any pg_temp_class and pg_temp_index processing */
AtEOSubXact_PgTempClass(isCommit, mySubid, parentSubid);
/* Don't reset the lists; we still need more cleanup later */
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 943de8a4fda..99123bde736 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -50,6 +50,7 @@
#include "catalog/pg_operator.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_temp_class.h"
+#include "catalog/pg_temp_index.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
#include "catalog/storage.h"
@@ -121,7 +122,8 @@ static void UpdateIndexRelation(Oid indexoid, Oid heapoid,
bool isexclusion,
bool immediate,
bool isvalid,
- bool isready);
+ bool isready,
+ char relpersistence);
static void index_update_stats(Relation rel,
bool isreindex,
bool hasindex,
@@ -574,7 +576,8 @@ UpdateIndexRelation(Oid indexoid,
bool isexclusion,
bool immediate,
bool isvalid,
- bool isready)
+ bool isready,
+ char relpersistence)
{
int2vector *indkey;
oidvector *indcollation;
@@ -675,6 +678,23 @@ UpdateIndexRelation(Oid indexoid,
*/
table_close(pg_index, RowExclusiveLock);
heap_freetuple(tuple);
+
+ /*
+ * For an index on a global temporary table, GlobalTempRelationCreated()
+ * will have inserted a pg_temp_index tuple with indisvalid = true. If
+ * the index is actually not valid, fix that now.
+ */
+ if (relpersistence == RELPERSISTENCE_GLOBAL_TEMP && !isvalid)
+ {
+ tuple = GetPgTempIndexTuple(indexoid);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for global temp index %u", indexoid);
+
+ ((Form_pg_temp_index) GETSTRUCT(tuple))->indisvalid = isvalid;
+
+ UpdatePgTempIndexTuple(indexoid, tuple);
+ heap_freetuple(tuple);
+ }
}
@@ -1056,7 +1076,8 @@ index_create(Relation heapRelation,
isprimary, is_exclusion,
(constr_flags & INDEX_CONSTR_CREATE_DEFERRABLE) == 0,
!concurrent && !invalid,
- !concurrent);
+ !concurrent,
+ relpersistence);
/*
* Register relcache invalidation on the indexes' heap relation, to
@@ -3583,6 +3604,9 @@ index_set_state_flags(Oid indexId, IndexStateFlagsAction action)
HeapTuple indexTuple;
Form_pg_index indexForm;
+ /* This is not expected to be a global temporary index */
+ Assert(!rel_is_global_temp(indexId));
+
/* Open pg_index and fetch a writable copy of the index's tuple */
pg_index = table_open(IndexRelationId, RowExclusiveLock);
@@ -3942,18 +3966,26 @@ reindex_index(const ReindexStmt *stmt, Oid indexId,
{
Relation pg_index;
HeapTuple indexTuple;
+ HeapTuple temp_indexTuple;
Form_pg_index indexForm;
+ Form_pg_temp_index temp_indexForm;
bool index_bad;
+ /*
+ * For a global temporary index, we update indisvalid in both pg_index
+ * and pg_temp_index, so that the change applies to this session and
+ * all future sessions.
+ */
pg_index = table_open(IndexRelationId, RowExclusiveLock);
- indexTuple = SearchSysCacheCopy1(INDEXRELID,
- ObjectIdGetDatum(indexId));
+ indexTuple = GetPgIndexAndPgTempIndexTuples(indexId, &temp_indexTuple,
+ true);
if (!HeapTupleIsValid(indexTuple))
elog(ERROR, "cache lookup failed for index %u", indexId);
indexForm = (Form_pg_index) GETSTRUCT(indexTuple);
+ temp_indexForm = (Form_pg_temp_index) GETSTRUCT_SAFE(temp_indexTuple);
- index_bad = (!indexForm->indisvalid ||
+ index_bad = (!GetEffective_indisvalid(indexForm, temp_indexForm) ||
!indexForm->indisready ||
!indexForm->indislive);
if (index_bad ||
@@ -3964,9 +3996,13 @@ reindex_index(const ReindexStmt *stmt, Oid indexId,
else if (index_bad)
indexForm->indcheckxmin = true;
indexForm->indisvalid = true;
+ if (temp_indexForm != NULL)
+ temp_indexForm->indisvalid = true;
indexForm->indisready = true;
indexForm->indislive = true;
CatalogTupleUpdate(pg_index, &indexTuple->t_self, indexTuple);
+ if (HeapTupleIsValid(temp_indexTuple))
+ UpdatePgTempIndexTuple(indexId, temp_indexTuple);
/*
* Invalidate the relcache for the table, so that after we commit
diff --git a/src/backend/catalog/pg_temp_class.c b/src/backend/catalog/pg_temp_class.c
index 4f294515479..6386e30bfa8 100644
--- a/src/backend/catalog/pg_temp_class.c
+++ b/src/backend/catalog/pg_temp_class.c
@@ -1,7 +1,8 @@
/*-------------------------------------------------------------------------
*
* pg_temp_class.c
- * routines to support manipulation of the pg_temp_class relation
+ * routines to support manipulation of the pg_temp_class and
+ * pg_temp_index relations
*
* The pg_temp_class system catalog table is a global temporary table that
* stores local overrides to various fields from the pg_class table for the
@@ -38,9 +39,14 @@
* with the database, if they have been flushed), until the end of the
* transaction, when they are discarded.
*
- * NB: All reads and writes to pg_temp_class by backend code must go
- * through the functions defined here (though user SQL queries may read it
- * normally).
+ * Likewise pg_temp_index is the global temporary system catalog table that
+ * stores local overrides to fields (actually just indisvalid) in the
+ * pg_index table for the duration of the current session. It requires
+ * similar treatment, so for convenience we manage that here too.
+ *
+ * NB: All reads and writes to pg_temp_class and pg_temp_index by backend
+ * code must go through the functions defined here (though user SQL queries
+ * may read them normally).
*
* Copyright (c) 2026, PostgreSQL Global Development Group
*
@@ -58,29 +64,35 @@
#include "access/xact.h"
#include "catalog/indexing.h"
#include "catalog/pg_temp_class.h"
+#include "catalog/pg_temp_index.h"
#include "miscadmin.h"
#include "storage/proc.h"
#include "utils/fmgroids.h"
#include "utils/hsearch.h"
+#include "utils/lsyscache.h"
#include "utils/memutils.h"
#include "utils/syscache.h"
/*
- * Have we opened and initialized pg_temp_class in this session?
+ * Have we opened and initialized pg_temp_class and pg_temp_index in this
+ * session?
*/
static bool pg_temp_class_opened = false;
+static bool pg_temp_index_opened = false;
/*
- * Subtransaction ID in which pg_temp_class was opened, if it was opened in
- * the current transaction, else zero.
+ * Subtransaction IDs in which pg_temp_class and pg_temp_index were opened, if
+ * they were opened in the current transaction, else zero.
*/
static SubTransactionId pg_temp_class_subid = InvalidSubTransactionId;
+static SubTransactionId pg_temp_index_subid = InvalidSubTransactionId;
-/* Cached copy of the pg_temp_class tuple descriptor */
+/* Cached copies of the pg_temp_class and pg_temp_index tuple descriptors */
static TupleDesc pg_temp_class_tupdesc = NULL;
+static TupleDesc pg_temp_index_tupdesc = NULL;
/*
- * Pending inserts to pg_temp_class.
+ * Pending inserts to pg_temp_class and pg_temp_index.
*
* Each pending insert entry may be flushed to the database at any point
* during the transaction which added it, but the entry is kept up-to-date
@@ -101,7 +113,10 @@ typedef struct PendingInsert
struct PendingInsert *prev; /* previous version, for subxact rollback */
} PendingInsert;
-static HTAB *pending_inserts = NULL;
+static HTAB *pending_class_inserts = NULL;
+static HTAB *pending_index_inserts = NULL;
+
+/* Do we have any pending inserts of either kind that need flushing? */
static bool have_inserts_to_flush = false;
/* Memory context for all tuples pending insert */
@@ -136,27 +151,62 @@ open_pg_temp_class(LOCKMODE lockmode)
}
/*
- * init_pending_inserts_hashtable
+ * open_pg_temp_index
*
- * Initialize the pending inserts hashtable, if not already done.
+ * Open pg_temp_index and make a note of the subtranscation ID, if this is
+ * the first time opening it.
+ */
+static Relation
+open_pg_temp_index(LOCKMODE lockmode)
+{
+ Relation pg_temp_index;
+
+ pg_temp_index = table_open(TempIndexRelationId, lockmode);
+ if (!pg_temp_index_opened)
+ {
+ pg_temp_index_opened = true;
+ pg_temp_index_subid = GetCurrentSubTransactionId();
+ }
+ return pg_temp_index;
+}
+
+/*
+ * init_pending_inserts_hashtables
+ *
+ * Initialize the pending inserts hashtables, if not already done.
*/
static void
-init_pending_inserts_hashtable(void)
+init_pending_inserts_hashtables(void)
{
- if (pending_inserts == NULL)
+ if (pending_class_inserts == NULL)
+ {
+ HASHCTL ctl;
+
+ /* Create the hash table for pending pg_temp_class inserts */
+ ctl.keysize = sizeof(Oid);
+ ctl.entrysize = sizeof(PendingInsert);
+
+ pending_class_inserts = hash_create("Pending pg_temp_class inserts",
+ 128, &ctl, HASH_ELEM | HASH_BLOBS);
+ }
+
+ if (pending_index_inserts == NULL)
{
HASHCTL ctl;
- /* Create the hash table */
+ /* Create the hash table for pending pg_temp_index inserts */
ctl.keysize = sizeof(Oid);
ctl.entrysize = sizeof(PendingInsert);
- pending_inserts = hash_create("Pending pg_temp_class inserts",
- 128, &ctl, HASH_ELEM | HASH_BLOBS);
+ pending_index_inserts = hash_create("Pending pg_temp_index inserts",
+ 128, &ctl, HASH_ELEM | HASH_BLOBS);
+ }
- /* Create a separate memory context for all tuples in it */
+ if (pending_inserts_tupctx == NULL)
+ {
+ /* Create a separate memory context for all pending tuples */
pending_inserts_tupctx = AllocSetContextCreate(TopMemoryContext,
- "Pending pg_temp_class tuples",
+ "Pending pg_temp_class/index tuples",
ALLOCSET_DEFAULT_SIZES);
}
}
@@ -215,6 +265,39 @@ get_pg_temp_class_tupdesc(void)
return pg_temp_class_tupdesc;
}
+/*
+ * get_pg_temp_index_tupdesc
+ *
+ * Returns the tuple descriptor for pg_temp_index.
+ */
+static TupleDesc
+get_pg_temp_index_tupdesc(void)
+{
+ /* Build the tuple descriptor the first time through */
+ if (pg_temp_index_tupdesc == NULL)
+ {
+ MemoryContext oldcontext;
+ TupleDesc tupdesc;
+
+ oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+
+ tupdesc = CreateTemplateTupleDesc(Natts_pg_temp_index);
+ TupleDescInitEntry(tupdesc,
+ (AttrNumber) Anum_pg_temp_index_indexrelid,
+ "indexrelid", OIDOID, -1, 0);
+ TupleDescInitEntry(tupdesc,
+ (AttrNumber) Anum_pg_temp_index_indisvalid,
+ "indisvalid", BOOLOID, -1, 0);
+ TupleDescFinalize(tupdesc);
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /* Cache it for all future use */
+ pg_temp_index_tupdesc = tupdesc;
+ }
+ return pg_temp_index_tupdesc;
+}
+
/*
* heap_form_pg_temp_class_tuple
*
@@ -241,6 +324,23 @@ heap_form_pg_temp_class_tuple(Relation rel)
return heap_form_tuple(get_pg_temp_class_tupdesc(), values, nulls);
}
+/*
+ * heap_form_pg_temp_index_tuple
+ *
+ * Create a pg_temp_index tuple for the specified index relation.
+ */
+static HeapTuple
+heap_form_pg_temp_index_tuple(Oid indexrelid, bool indisvalid)
+{
+ Datum values[Natts_pg_temp_index];
+ bool nulls[Natts_pg_temp_index] = {0};
+
+ values[Anum_pg_temp_index_indexrelid - 1] = ObjectIdGetDatum(indexrelid);
+ values[Anum_pg_temp_index_indisvalid - 1] = BoolGetDatum(indisvalid);
+
+ return heap_form_tuple(get_pg_temp_index_tupdesc(), values, nulls);
+}
+
/*
* prepare_pending_insert_for_edit
*
@@ -274,57 +374,98 @@ prepare_pending_insert_for_edit(PendingInsert *entry)
}
/*
- * flush_pending_pg_temp_class_inserts
+ * flush_pending_inserts
*
- * Flush any pending inserts to pg_temp_class.
+ * Flush any pending inserts to pg_temp_class and pg_temp_index.
*/
static void
-flush_pending_pg_temp_class_inserts(void)
+flush_pending_inserts(void)
{
Relation pg_temp_class;
- CatalogIndexState indstate;
+ Relation pg_temp_index;
+ CatalogIndexState class_indstate;
+ CatalogIndexState index_indstate;
HASH_SEQ_STATUS status;
PendingInsert *entry;
+ /*
+ * Open pg_temp_class, pg_temp_index, and their indexes. Note that this
+ * might be the first time these have been opened in the current session,
+ * so we open them all first, in case they lead to more pending inserts.
+ */
pg_temp_class = open_pg_temp_class(RowExclusiveLock);
+ pg_temp_index = open_pg_temp_index(RowExclusiveLock);
+
+ class_indstate = CatalogOpenIndexes(pg_temp_class);
+ index_indstate = CatalogOpenIndexes(pg_temp_index);
/*
- * Flush all pending inserts, not already flushed or deleted.
+ * Flush all pending pg_temp_class inserts, not already flushed or
+ * deleted.
*/
- indstate = CatalogOpenIndexes(pg_temp_class);
- hash_seq_init(&status, pending_inserts);
+ hash_seq_init(&status, pending_class_inserts);
+ while ((entry = hash_seq_search(&status)) != NULL)
+ {
+ if (!entry->flushed && !entry->deleted)
+ {
+ CatalogTupleInsertWithInfo(pg_temp_class, entry->tuple,
+ class_indstate);
+
+ /* Update the entry, saving a copy for rollback, if necessary */
+ prepare_pending_insert_for_edit(entry);
+ entry->flushed = true;
+ }
+ }
+
+ /* And likewise for pending pg_temp_index inserts */
+ hash_seq_init(&status, pending_index_inserts);
while ((entry = hash_seq_search(&status)) != NULL)
{
if (!entry->flushed && !entry->deleted)
{
- CatalogTupleInsertWithInfo(pg_temp_class, entry->tuple, indstate);
+ CatalogTupleInsertWithInfo(pg_temp_index, entry->tuple,
+ index_indstate);
/* Update the entry, saving a copy for rollback, if necessary */
prepare_pending_insert_for_edit(entry);
entry->flushed = true;
}
}
- CatalogCloseIndexes(indstate);
+
+ /* Tidy up */
+ CatalogCloseIndexes(class_indstate);
+ CatalogCloseIndexes(index_indstate);
table_close(pg_temp_class, RowExclusiveLock);
+ table_close(pg_temp_index, RowExclusiveLock);
have_inserts_to_flush = false;
}
/*
- * discard_pending_pg_temp_class_inserts
+ * discard_pending_inserts
*
- * Discard any pending inserts to pg_temp_class.
+ * Discard any pending inserts to pg_temp_class and pg_temp_index.
*/
static void
-discard_pending_pg_temp_class_inserts(void)
+discard_pending_inserts(void)
{
- /* Just blow away the hash table and tuple memory context */
- hash_destroy(pending_inserts);
- MemoryContextDelete(pending_inserts_tupctx);
-
- pending_inserts = NULL;
- pending_inserts_tupctx = NULL;
+ /* Just blow away the hash tables and tuple memory context */
+ if (pending_class_inserts != NULL)
+ {
+ hash_destroy(pending_class_inserts);
+ pending_class_inserts = NULL;
+ }
+ if (pending_index_inserts != NULL)
+ {
+ hash_destroy(pending_index_inserts);
+ pending_index_inserts = NULL;
+ }
+ if (pending_inserts_tupctx != NULL)
+ {
+ MemoryContextDelete(pending_inserts_tupctx);
+ pending_inserts_tupctx = NULL;
+ }
have_inserts_to_flush = false;
}
@@ -342,9 +483,9 @@ GetPgTempClassTuple(Oid relid)
PendingInsert *entry = NULL;
/* If there is a pending insert for this relation, return that */
- if (pending_inserts != NULL)
+ if (pending_class_inserts != NULL)
{
- entry = hash_search(pending_inserts, &relid, HASH_FIND, NULL);
+ entry = hash_search(pending_class_inserts, &relid, HASH_FIND, NULL);
if (entry != NULL)
return entry->deleted ? NULL : heap_copytuple(entry->tuple);
}
@@ -357,6 +498,35 @@ GetPgTempClassTuple(Oid relid)
return SearchSysCacheCopy1(TEMPRELOID, ObjectIdGetDatum(relid));
}
+/*
+ * GetPgTempIndexTuple
+ *
+ * Get the pg_temp_index tuple for a global temporary index relation.
+ *
+ * Returns NULL if the tuple could not be found. Otherwise, the tuple
+ * returned should be freed with heap_freetuple().
+ */
+HeapTuple
+GetPgTempIndexTuple(Oid indexrelid)
+{
+ PendingInsert *entry;
+
+ /* Is there a pending insert for this relation? */
+ if (pending_index_inserts != NULL)
+ {
+ entry = hash_search(pending_index_inserts, &indexrelid, HASH_FIND, NULL);
+ if (entry != NULL)
+ return entry->deleted ? NULL : heap_copytuple(entry->tuple);
+ }
+
+ /* If we haven't opened pg_temp_index yet, it must be empty */
+ if (!pg_temp_index_opened)
+ return NULL;
+
+ /* Otherwise, fetch a copy of the tuple from the database */
+ return SearchSysCacheCopy1(TEMPINDEXRELID, ObjectIdGetDatum(indexrelid));
+}
+
/*
* InsertPgTempClassTuple
*
@@ -383,9 +553,9 @@ InsertPgTempClassTuple(Relation rel)
* taking care to allocate the tuple in the long-term memory context for
* pending insert tuples.
*/
- init_pending_inserts_hashtable();
+ init_pending_inserts_hashtables();
- entry = hash_search(pending_inserts, &relid, HASH_ENTER, &found);
+ entry = hash_search(pending_class_inserts, &relid, HASH_ENTER, &found);
if (found)
/* Should never try to re-insert the same relid */
elog(ERROR, "pg_temp_class tuple for relation %u already exists", relid);
@@ -401,6 +571,49 @@ InsertPgTempClassTuple(Relation rel)
have_inserts_to_flush = true;
}
+/*
+ * InsertPgTempIndexTuple
+ *
+ * Insert a new pg_temp_index tuple for a global temporary index relation.
+ *
+ * This is called when a global temporary index relation is created or
+ * accessed for the first time in a session.
+ *
+ * Note: The new tuple is not written to the database unless and until
+ * CommandCounterIncrement() is called for a non-read-only command, or the
+ * (sub)transaction is committed. However, the new tuple *is* visible to all
+ * the functions defined here.
+ */
+void
+InsertPgTempIndexTuple(Oid indexrelid, bool indisvalid)
+{
+ PendingInsert *entry;
+ bool found;
+ MemoryContext oldcontext;
+
+ /*
+ * Add a new tuple for the relation to the pending inserts hash table,
+ * taking care to allocate the tuple in the long-term memory context for
+ * pending insert tuples.
+ */
+ init_pending_inserts_hashtables();
+
+ entry = hash_search(pending_index_inserts, &indexrelid, HASH_ENTER, &found);
+ if (found)
+ /* Should never try to re-insert the same indexrelid */
+ elog(ERROR, "pg_temp_index tuple for index %u already exists", indexrelid);
+
+ oldcontext = MemoryContextSwitchTo(pending_inserts_tupctx);
+ entry->tuple = heap_form_pg_temp_index_tuple(indexrelid, indisvalid);
+ entry->flushed = false;
+ entry->deleted = false;
+ entry->subid = GetCurrentSubTransactionId();
+ entry->prev = NULL;
+ MemoryContextSwitchTo(oldcontext);
+
+ have_inserts_to_flush = true;
+}
+
/*
* UpdatePgTempClassTuple
*
@@ -413,11 +626,11 @@ UpdatePgTempClassTuple(Oid relid, HeapTuple newtuple)
HeapTuple oldtuple;
/* Is there a pending insert for this relation? */
- if (pending_inserts != NULL)
+ if (pending_class_inserts != NULL)
{
PendingInsert *entry;
- entry = hash_search(pending_inserts, &relid, HASH_FIND, NULL);
+ entry = hash_search(pending_class_inserts, &relid, HASH_FIND, NULL);
if (entry != NULL)
{
Form_pg_temp_class old_form;
@@ -479,11 +692,11 @@ UpdatePgTempClassTupleInPlace(Oid relid, HeapTuple newtuple)
void *inplace_state;
/* Is there a pending insert for this relation? */
- if (pending_inserts != NULL)
+ if (pending_class_inserts != NULL)
{
PendingInsert *entry;
- entry = hash_search(pending_inserts, &relid, HASH_FIND, NULL);
+ entry = hash_search(pending_class_inserts, &relid, HASH_FIND, NULL);
if (entry != NULL)
{
Form_pg_temp_class old_form;
@@ -538,6 +751,69 @@ UpdatePgTempClassTupleInPlace(Oid relid, HeapTuple newtuple)
table_close(pg_temp_class, RowExclusiveLock);
}
+/*
+ * UpdatePgTempIndexTuple
+ *
+ * Update the pg_temp_index tuple for a global temporary index relation.
+ */
+void
+UpdatePgTempIndexTuple(Oid indexrelid, HeapTuple newtuple)
+{
+ Relation pg_temp_index;
+ HeapTuple oldtuple;
+
+ /* Is there a pending insert for this relation? */
+ if (pending_index_inserts != NULL)
+ {
+ PendingInsert *entry;
+
+ entry = hash_search(pending_index_inserts, &indexrelid, HASH_FIND, NULL);
+ if (entry != NULL)
+ {
+ Form_pg_temp_index old_form;
+ Form_pg_temp_index new_form;
+
+ /* Should not have been deleted */
+ if (entry->deleted)
+ elog(ERROR,
+ "pending insert for global temp index %u was deleted",
+ indexrelid);
+
+ /* Update the entry, saving a copy for rollback, if necessary */
+ prepare_pending_insert_for_edit(entry);
+ old_form = (Form_pg_temp_index) GETSTRUCT(entry->tuple);
+ new_form = (Form_pg_temp_index) GETSTRUCT(newtuple);
+ old_form->indisvalid = new_form->indisvalid;
+
+ /*
+ * If it has not yet been flushed to the database, do so now. This
+ * is important, because the caller might be relying on a relcache
+ * invalidation being triggered.
+ */
+ if (!entry->flushed)
+ {
+ pg_temp_index = open_pg_temp_index(RowExclusiveLock);
+ CatalogTupleInsert(pg_temp_index, newtuple);
+ table_close(pg_temp_index, RowExclusiveLock);
+ entry->flushed = true;
+ return;
+ }
+ }
+ }
+
+ /* Update the tuple in the database */
+ pg_temp_index = open_pg_temp_index(RowExclusiveLock);
+
+ oldtuple = SearchSysCache1(TEMPINDEXRELID, ObjectIdGetDatum(indexrelid));
+ if (!HeapTupleIsValid(oldtuple))
+ elog(ERROR, "cache lookup failed for global temp index %u", indexrelid);
+
+ CatalogTupleUpdate(pg_temp_index, &oldtuple->t_self, newtuple);
+ ReleaseSysCache(oldtuple);
+
+ table_close(pg_temp_index, RowExclusiveLock);
+}
+
/*
* DeletePgTempClassTuple
*
@@ -550,11 +826,11 @@ DeletePgTempClassTuple(Oid relid)
HeapTuple oldtuple;
/* Is there a pending insert for this relation? */
- if (pending_inserts != NULL)
+ if (pending_class_inserts != NULL)
{
PendingInsert *entry;
- entry = hash_search(pending_inserts, &relid, HASH_FIND, NULL);
+ entry = hash_search(pending_class_inserts, &relid, HASH_FIND, NULL);
if (entry != NULL)
{
/* Should not have already been deleted */
@@ -589,6 +865,57 @@ DeletePgTempClassTuple(Oid relid)
table_close(pg_temp_class, RowExclusiveLock);
}
+/*
+ * DeletePgTempIndexTuple
+ *
+ * Delete the pg_temp_index tuple for a global temporary index relation.
+ */
+void
+DeletePgTempIndexTuple(Oid indexrelid)
+{
+ Relation pg_temp_index;
+ HeapTuple oldtuple;
+
+ /* Is there a pending insert for this relation? */
+ if (pending_index_inserts != NULL)
+ {
+ PendingInsert *entry;
+
+ entry = hash_search(pending_index_inserts, &indexrelid, HASH_FIND, NULL);
+ if (entry != NULL)
+ {
+ /* Should not have already been deleted */
+ if (entry->deleted)
+ elog(ERROR,
+ "pending insert for global temp relation %u already deleted",
+ indexrelid);
+
+ /* Update the entry, saving a copy for rollback, if necessary */
+ prepare_pending_insert_for_edit(entry);
+ entry->deleted = true;
+
+ /*
+ * If it has been flushed to the database, need to delete it there
+ * too. Otherwise, we're done.
+ */
+ if (!entry->flushed)
+ return;
+ }
+ }
+
+ /* Delete the tuple from the database */
+ pg_temp_index = open_pg_temp_index(RowExclusiveLock);
+
+ oldtuple = SearchSysCache1(TEMPINDEXRELID, ObjectIdGetDatum(indexrelid));
+ if (!HeapTupleIsValid(oldtuple))
+ elog(ERROR, "cache lookup failed for global temp index %u", indexrelid);
+
+ CatalogTupleDelete(pg_temp_index, &oldtuple->t_self);
+ ReleaseSysCache(oldtuple);
+
+ table_close(pg_temp_index, RowExclusiveLock);
+}
+
/*
* GetPgClassAndPgTempClassTuples
*
@@ -631,6 +958,39 @@ GetPgClassAndPgTempClassTuples(Oid relid, bool lock_tuple,
return tuple;
}
+/*
+ * GetPgIndexAndPgTempIndexTuples
+ *
+ * Get the pg_index tuple for an index relation, and if it's a global
+ * temporary index relation, also get the corresponding pg_temp_index tuple,
+ * if present.
+ *
+ * Returns NULL if the pg_index tuple could not be found. Otherwise, the
+ * tuple(s) returned should be freed with heap_freetuple().
+ */
+HeapTuple
+GetPgIndexAndPgTempIndexTuples(Oid indexrelid, HeapTuple *temp_tuple,
+ bool check_temp)
+{
+ HeapTuple tuple;
+
+ /* Get a copy of the pg_index tuple */
+ tuple = SearchSysCacheCopy1(INDEXRELID, ObjectIdGetDatum(indexrelid));
+
+ if (HeapTupleIsValid(tuple) &&
+ rel_is_global_temp(((Form_pg_index) GETSTRUCT(tuple))->indexrelid))
+ {
+ /* Get the pg_temp_index tuple, and check it exists, if requested */
+ *temp_tuple = GetPgTempIndexTuple(indexrelid);
+ if (check_temp && !HeapTupleIsValid(*temp_tuple))
+ elog(ERROR, "cache lookup failed for global temp index %u", indexrelid);
+ }
+ else
+ *temp_tuple = NULL;
+
+ return tuple;
+}
+
/*
* GetEffectivePgClassTuple
*
@@ -672,6 +1032,47 @@ GetEffectivePgClassTuple(Oid relid)
return tuple;
}
+/*
+ * GetEffectivePgIndexTuple
+ *
+ * Get the effective pg_index tuple for an index relation.
+ *
+ * This will fetch the pg_index tuple for the relation and then, if it's a
+ * global temporary relation, fetch the corresponding pg_temp_index tuple and
+ * use the values in it to override the corresponding values in the pg_index
+ * tuple (currently just indisvalid). Thus, the result represents the
+ * effective state of the index relation in this session.
+ *
+ * For a global temporary index relation that has not yet been opened in this
+ * session, there will be no pg_temp_index tuple, and the pg_index tuple will
+ * be returned unchanged.
+ *
+ * Returns NULL if the pg_index tuple could not be found. Otherwise, the
+ * tuple returned should be freed with heap_freetuple().
+ */
+HeapTuple
+GetEffectivePgIndexTuple(Oid indexrelid)
+{
+ HeapTuple tuple;
+ HeapTuple temp_tuple;
+ Form_pg_index indexform;
+ Form_pg_temp_index temp_indexform;
+
+ /*
+ * Get the pg_index and pg_temp_index tuples. If we have the latter, use
+ * it to update the former.
+ */
+ tuple = GetPgIndexAndPgTempIndexTuples(indexrelid, &temp_tuple, false);
+
+ if (HeapTupleIsValid(tuple) && HeapTupleIsValid(temp_tuple))
+ {
+ indexform = (Form_pg_index) GETSTRUCT(tuple);
+ temp_indexform = (Form_pg_temp_index) GETSTRUCT(temp_tuple);
+ indexform->indisvalid = temp_indexform->indisvalid;
+ }
+ return tuple;
+}
+
/*
* UpdateTempFrozenXids
*
@@ -701,10 +1102,10 @@ UpdateTempFrozenXids(void)
min_relfrozenxid = InvalidTransactionId;
min_relminmxid = InvalidMultiXactId;
- /* Processing any pending inserts */
- if (pending_inserts != NULL)
+ /* Processing any pending pg_temp_class inserts */
+ if (pending_class_inserts != NULL)
{
- hash_seq_init(&status, pending_inserts);
+ hash_seq_init(&status, pending_class_inserts);
while ((entry = hash_seq_search(&status)) != NULL)
{
temp_form = (Form_pg_temp_class) GETSTRUCT(entry->tuple);
@@ -785,7 +1186,7 @@ void
PreCCI_PgTempClass(void)
{
if (have_inserts_to_flush)
- flush_pending_pg_temp_class_inserts();
+ flush_pending_inserts();
}
/*
@@ -797,7 +1198,7 @@ void
PreCommit_PgTempClass(void)
{
if (have_inserts_to_flush)
- flush_pending_pg_temp_class_inserts();
+ flush_pending_inserts();
}
/*
@@ -809,7 +1210,7 @@ void
PreSubCommit_PgTempClass(void)
{
if (have_inserts_to_flush)
- flush_pending_pg_temp_class_inserts();
+ flush_pending_inserts();
}
/*
@@ -828,13 +1229,17 @@ AtEOXact_PgTempClass(bool isCommit)
pg_temp_class_opened = false;
pg_temp_class_subid = InvalidSubTransactionId;
+ /* Likewise for pg_temp_index */
+ if (!isCommit && pg_temp_index_subid != InvalidSubTransactionId)
+ pg_temp_index_opened = false;
+ pg_temp_index_subid = InvalidSubTransactionId;
+
/*
- * Blow away the pending inserts hash table. On commit, there should be
+ * Blow away the pending inserts hash tables. On commit, there should be
* no remaining inserts to flush, but on rollback, there may be.
*/
Assert(!(isCommit && have_inserts_to_flush));
- if (pending_inserts != NULL)
- discard_pending_pg_temp_class_inserts();
+ discard_pending_inserts();
/*
* On commit, save any new tempfrozenxid and tempminmxid values to our
@@ -854,8 +1259,8 @@ AtEOXact_PgTempClass(bool isCommit)
* Sub-transaction commit or abort processing for a single pending insert.
*/
static void
-AtEOSubXact_PendingInsert(PendingInsert *entry, bool isCommit,
- SubTransactionId mySubid,
+AtEOSubXact_PendingInsert(HTAB *pending_inserts, PendingInsert *entry,
+ bool isCommit, SubTransactionId mySubid,
SubTransactionId parentSubid)
{
/*
@@ -902,6 +1307,9 @@ void
AtEOSubXact_PgTempClass(bool isCommit, SubTransactionId mySubid,
SubTransactionId parentSubid)
{
+ HASH_SEQ_STATUS status;
+ PendingInsert *entry;
+
/*
* Was pg_temp_class first opened and initialized in the current
* subtransaction?
@@ -920,18 +1328,37 @@ AtEOSubXact_PgTempClass(bool isCommit, SubTransactionId mySubid,
}
}
- /*
- * Tidy up any pending inserts.
- */
- if (pending_inserts != NULL)
+ /* Likewise for pg_temp_index */
+ if (pg_temp_index_subid == mySubid)
{
- HASH_SEQ_STATUS status;
- PendingInsert *entry;
+ if (isCommit)
+ pg_temp_index_subid = parentSubid;
+ else
+ {
+ pg_temp_index_opened = false;
+ pg_temp_index_subid = InvalidSubTransactionId;
+ }
+ }
+
+ /* Tidy up any pending pg_temp_class inserts */
+ if (pending_class_inserts != NULL)
+ {
+ hash_seq_init(&status, pending_class_inserts);
+ while ((entry = hash_seq_search(&status)) != NULL)
+ {
+ AtEOSubXact_PendingInsert(pending_class_inserts, entry,
+ isCommit, mySubid, parentSubid);
+ }
+ }
- hash_seq_init(&status, pending_inserts);
+ /* Likewise for pg_temp_index */
+ if (pending_index_inserts != NULL)
+ {
+ hash_seq_init(&status, pending_index_inserts);
while ((entry = hash_seq_search(&status)) != NULL)
{
- AtEOSubXact_PendingInsert(entry, isCommit, mySubid, parentSubid);
+ AtEOSubXact_PendingInsert(pending_index_inserts, entry,
+ isCommit, mySubid, parentSubid);
}
}
}
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 8baac58ef81..beb90ba09a7 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -37,6 +37,8 @@
#include "catalog/pg_namespace.h"
#include "catalog/pg_opclass.h"
#include "catalog/pg_tablespace.h"
+#include "catalog/pg_temp_class.h"
+#include "catalog/pg_temp_index.h"
#include "catalog/pg_type.h"
#include "commands/comment.h"
#include "commands/defrem.h"
@@ -269,7 +271,7 @@ CheckIndexCompatible(Oid oldId,
0, NULL);
/* Get the soon-obsolete pg_index tuple. */
- tuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(oldId));
+ tuple = GetEffectivePgIndexTuple(oldId);
if (!HeapTupleIsValid(tuple))
elog(ERROR, "cache lookup failed for index %u", oldId);
indexForm = (Form_pg_index) GETSTRUCT(tuple);
@@ -282,7 +284,7 @@ CheckIndexCompatible(Oid oldId,
heap_attisnull(tuple, Anum_pg_index_indexprs, NULL) &&
indexForm->indisvalid))
{
- ReleaseSysCache(tuple);
+ heap_freetuple(tuple);
return false;
}
@@ -299,7 +301,7 @@ CheckIndexCompatible(Oid oldId,
ret = (memcmp(old_indclass->values, opclassIds, old_natts * sizeof(Oid)) == 0 &&
memcmp(old_indcollation->values, collationIds, old_natts * sizeof(Oid)) == 0);
- ReleaseSysCache(tuple);
+ heap_freetuple(tuple);
if (!ret)
return false;
@@ -1581,19 +1583,36 @@ DefineIndex(ParseState *pstate,
{
Relation pg_index = table_open(IndexRelationId, RowExclusiveLock);
HeapTuple tup,
- newtup;
+ temp_tup;
+ Form_pg_index form;
+ Form_pg_temp_index temp_form;
- tup = SearchSysCache1(INDEXRELID,
- ObjectIdGetDatum(indexRelationId));
+ /*
+ * For a global temporary index, we update indisvalid in both
+ * pg_index and pg_temp_index, so that the change applies to
+ * this session and all future sessions.
+ */
+ tup = GetPgIndexAndPgTempIndexTuples(indexRelationId,
+ &temp_tup, true);
if (!HeapTupleIsValid(tup))
elog(ERROR, "cache lookup failed for index %u",
indexRelationId);
- newtup = heap_copytuple(tup);
- ((Form_pg_index) GETSTRUCT(newtup))->indisvalid = false;
- CatalogTupleUpdate(pg_index, &tup->t_self, newtup);
- ReleaseSysCache(tup);
+ form = (Form_pg_index) GETSTRUCT(tup);
+ temp_form = (Form_pg_temp_index) GETSTRUCT_SAFE(temp_tup);
+
+ form->indisvalid = false;
+ if (temp_form != NULL)
+ temp_form->indisvalid = false;
+
+ CatalogTupleUpdate(pg_index, &tup->t_self, tup);
+ if (HeapTupleIsValid(temp_tup))
+ {
+ UpdatePgTempIndexTuple(indexRelationId, temp_tup);
+ heap_freetuple(temp_tup);
+ }
+
+ heap_freetuple(tup);
table_close(pg_index, RowExclusiveLock);
- heap_freetuple(newtup);
/*
* CCI here to make this update visible, in case this recurses
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 0c704a531b2..2ec0bcaca29 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -868,8 +868,7 @@ mark_index_clustered(Relation rel, Oid indexOid, bool is_internal)
{
Oid thisIndexOid = lfirst_oid(index);
- indexTuple = SearchSysCacheCopy1(INDEXRELID,
- ObjectIdGetDatum(thisIndexOid));
+ indexTuple = GetEffectivePgIndexTuple(thisIndexOid);
if (!HeapTupleIsValid(indexTuple))
elog(ERROR, "cache lookup failed for index %u", thisIndexOid);
indexForm = (Form_pg_index) GETSTRUCT(indexTuple);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d3394fffd61..4dff3969097 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -55,6 +55,7 @@
#include "catalog/pg_statistic_ext.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_temp_class.h"
+#include "catalog/pg_temp_index.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
#include "catalog/storage.h"
@@ -1859,7 +1860,7 @@ RangeVarCallbackForDropRelation(const RangeVar *rel, Oid relOid, Oid oldRelOid,
Form_pg_index indexform;
bool indisvalid;
- locTuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(relOid));
+ locTuple = GetEffectivePgIndexTuple(relOid);
if (!HeapTupleIsValid(locTuple))
{
ReleaseSysCache(tuple);
@@ -1868,7 +1869,7 @@ RangeVarCallbackForDropRelation(const RangeVar *rel, Oid relOid, Oid oldRelOid,
indexform = (Form_pg_index) GETSTRUCT(locTuple);
indisvalid = indexform->indisvalid;
- ReleaseSysCache(locTuple);
+ heap_freetuple(locTuple);
/* Mark object as being an invalid index of system catalogs */
if (!indisvalid)
@@ -14004,7 +14005,7 @@ transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
{
Oid indexoid = lfirst_oid(indexoidscan);
- indexTuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(indexoid));
+ indexTuple = GetEffectivePgIndexTuple(indexoid);
if (!HeapTupleIsValid(indexTuple))
elog(ERROR, "cache lookup failed for index %u", indexoid);
indexStruct = (Form_pg_index) GETSTRUCT(indexTuple);
@@ -14024,7 +14025,7 @@ transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
*indexOid = indexoid;
break;
}
- ReleaseSysCache(indexTuple);
+ heap_freetuple(indexTuple);
}
list_free(indexoidlist);
@@ -14062,7 +14063,7 @@ transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
*pk_has_without_overlaps = indexStruct->indisexclusion;
- ReleaseSysCache(indexTuple);
+ heap_freetuple(indexTuple);
return i;
}
@@ -14125,7 +14126,7 @@ transformFkeyCheckAttrs(Relation pkrel,
Form_pg_index indexStruct;
indexoid = lfirst_oid(indexoidscan);
- indexTuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(indexoid));
+ indexTuple = GetEffectivePgIndexTuple(indexoid);
if (!HeapTupleIsValid(indexTuple))
elog(ERROR, "cache lookup failed for index %u", indexoid);
indexStruct = (Form_pg_index) GETSTRUCT(indexTuple);
@@ -14201,7 +14202,7 @@ transformFkeyCheckAttrs(Relation pkrel,
if (found)
*pk_has_without_overlaps = indexStruct->indisexclusion;
}
- ReleaseSysCache(indexTuple);
+ heap_freetuple(indexTuple);
if (found)
break;
}
@@ -22489,14 +22490,13 @@ validatePartitionedIndex(Relation partedIdx, Relation partedTbl)
HeapTuple indTup;
Form_pg_index indexForm;
- indTup = SearchSysCache1(INDEXRELID,
- ObjectIdGetDatum(inhForm->inhrelid));
+ indTup = GetEffectivePgIndexTuple(inhForm->inhrelid);
if (!HeapTupleIsValid(indTup))
elog(ERROR, "cache lookup failed for index %u", inhForm->inhrelid);
indexForm = (Form_pg_index) GETSTRUCT(indTup);
if (indexForm->indisvalid)
tuples += 1;
- ReleaseSysCache(indTup);
+ heap_freetuple(indTup);
}
/* Done with pg_inherits */
@@ -22511,20 +22511,35 @@ validatePartitionedIndex(Relation partedIdx, Relation partedTbl)
{
Relation idxRel;
HeapTuple indTup;
+ HeapTuple temp_indTup;
Form_pg_index indexForm;
+ Form_pg_temp_index temp_indexForm;
+ /*
+ * For a global temporary index, we update indisvalid in both pg_index
+ * and pg_temp_index, so that the change applies to this session and
+ * all future sessions.
+ */
idxRel = table_open(IndexRelationId, RowExclusiveLock);
- indTup = SearchSysCacheCopy1(INDEXRELID,
- ObjectIdGetDatum(RelationGetRelid(partedIdx)));
+ indTup = GetPgIndexAndPgTempIndexTuples(RelationGetRelid(partedIdx),
+ &temp_indTup, true);
if (!HeapTupleIsValid(indTup))
elog(ERROR, "cache lookup failed for index %u",
RelationGetRelid(partedIdx));
indexForm = (Form_pg_index) GETSTRUCT(indTup);
+ temp_indexForm = (Form_pg_temp_index) GETSTRUCT_SAFE(temp_indTup);
indexForm->indisvalid = true;
+ if (temp_indexForm != NULL)
+ temp_indexForm->indisvalid = true;
updated = true;
CatalogTupleUpdate(idxRel, &indTup->t_self, indTup);
+ if (HeapTupleIsValid(temp_indTup))
+ {
+ UpdatePgTempIndexTuple(RelationGetRelid(partedIdx), temp_indTup);
+ heap_freetuple(temp_indTup);
+ }
table_close(idxRel, RowExclusiveLock);
heap_freetuple(indTup);
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index ecc34f07e36..d35b6c6ce12 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -3965,13 +3965,13 @@ get_index_isvalid(Oid index_oid)
HeapTuple tuple;
Form_pg_index rd_index;
- tuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(index_oid));
+ tuple = GetEffectivePgIndexTuple(index_oid);
if (!HeapTupleIsValid(tuple))
elog(ERROR, "cache lookup failed for index %u", index_oid);
rd_index = (Form_pg_index) GETSTRUCT(tuple);
isvalid = rd_index->indisvalid;
- ReleaseSysCache(tuple);
+ heap_freetuple(tuple);
return isvalid;
}
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 7ea08c2428f..fd46606928a 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -63,6 +63,7 @@
#include "catalog/pg_subscription.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_temp_class.h"
+#include "catalog/pg_temp_index.h"
#include "catalog/pg_temp_statistic.h"
#include "catalog/pg_temp_statistic_ext_data.h"
#include "catalog/pg_trigger.h"
@@ -1531,6 +1532,23 @@ RelationInitIndexAccessInfo(Relation relation)
MemoryContextSwitchTo(oldcontext);
ReleaseSysCache(tuple);
+ /*
+ * For global temporary indexes, update indisvalid from pg_temp_index.
+ * This won't work for system catalog indexes, because pg_temp_index may
+ * not be loaded at this point, but they shouldn't be invalid anyway.
+ */
+ if (RELATION_IS_GLOBAL_TEMP(relation) && !IsCatalogRelation(relation))
+ {
+ tuple = GetPgTempIndexTuple(RelationGetRelid(relation));
+ if (HeapTupleIsValid(tuple))
+ {
+ Form_pg_temp_index temp_form = (Form_pg_temp_index) GETSTRUCT(tuple);
+
+ relation->rd_index->indisvalid = temp_form->indisvalid;
+ heap_freetuple(tuple);
+ }
+ }
+
/*
* Look up the index's access method, save the OID of its handler function
*/
@@ -2441,8 +2459,7 @@ RelationReloadIndexInfo(Relation relation)
HeapTuple tuple;
Form_pg_index index;
- tuple = SearchSysCache1(INDEXRELID,
- ObjectIdGetDatum(RelationGetRelid(relation)));
+ tuple = GetEffectivePgIndexTuple(RelationGetRelid(relation));
if (!HeapTupleIsValid(tuple))
elog(ERROR, "cache lookup failed for index %u",
RelationGetRelid(relation));
@@ -2470,7 +2487,7 @@ RelationReloadIndexInfo(Relation relation)
HeapTupleHeaderSetXmin(relation->rd_indextuple->t_data,
HeapTupleHeaderGetXmin(tuple->t_data));
- ReleaseSysCache(tuple);
+ heap_freetuple(tuple);
}
/* Okay, now it's valid again */
@@ -5032,6 +5049,8 @@ RelationGetIndexList(Relation relation)
while (HeapTupleIsValid(htup = systable_getnext(indscan)))
{
Form_pg_index index = (Form_pg_index) GETSTRUCT(htup);
+ HeapTuple temp_htup;
+ bool indisvalid;
/*
* Ignore any indexes that are currently being dropped. This will
@@ -5055,6 +5074,21 @@ RelationGetIndexList(Relation relation)
!heap_attisnull(htup, Anum_pg_index_indpred, NULL))
continue;
+ /*
+ * Global temporary indexes may override indexisvalid locally.
+ */
+ indisvalid = index->indisvalid;
+ if (RELATION_IS_GLOBAL_TEMP(relation))
+ {
+ temp_htup = GetPgTempIndexTuple(index->indexrelid);
+
+ if (HeapTupleIsValid(temp_htup))
+ {
+ indisvalid = ((Form_pg_temp_index) GETSTRUCT(temp_htup))->indisvalid;
+ heap_freetuple(temp_htup);
+ }
+ }
+
/*
* Remember primary key index, if any. For regular tables we do this
* only if the index is valid; but for partitioned tables, then we do
@@ -5066,7 +5100,7 @@ RelationGetIndexList(Relation relation)
* partitioned tables.
*/
if (index->indisprimary &&
- (index->indisvalid ||
+ (indisvalid ||
relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE))
{
pkeyIndex = index->indexrelid;
@@ -5076,7 +5110,7 @@ RelationGetIndexList(Relation relation)
if (!index->indimmediate)
continue;
- if (!index->indisvalid)
+ if (!indisvalid)
continue;
/* remember explicitly chosen replica index */
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 8ce4decb91e..3e9c2b85517 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2406,6 +2406,20 @@ describeOneTableDetails(const char *schemaname,
char *indamname = PQgetvalue(result, 0, 8);
char *indtable = PQgetvalue(result, 0, 9);
char *indpred = PQgetvalue(result, 0, 10);
+ PGresult *temp_result = NULL;
+
+ if (pset.sversion >= 200000)
+ {
+ printfPQExpBuffer(&buf, "/* %s */\n", _("Get temp index details"));
+ appendPQExpBuffer(&buf,
+ "SELECT i.indisvalid\n"
+ "FROM pg_catalog.pg_temp_index i\n"
+ "WHERE i.indexrelid = '%s'", oid);
+
+ temp_result = PSQLexec(buf.data);
+ if (temp_result && PQntuples(temp_result) == 1)
+ indisvalid = PQgetvalue(temp_result, 0, 0);
+ }
if (strcmp(indisprimary, "t") == 0)
printfPQExpBuffer(&tmpbuf, _("primary key, "));
@@ -2450,6 +2464,8 @@ describeOneTableDetails(const char *schemaname,
if (tableinfo.relkind == RELKIND_INDEX)
add_tablespace_footer(&cont, tableinfo.relkind,
tableinfo.tablespace, true);
+
+ PQclear(temp_result);
}
PQclear(result);
@@ -2482,6 +2498,7 @@ describeOneTableDetails(const char *schemaname,
appendPQExpBufferStr(&buf, ", con.conperiod");
else
appendPQExpBufferStr(&buf, ", false AS conperiod");
+ appendPQExpBufferStr(&buf, ", i.indexrelid");
appendPQExpBuffer(&buf,
"\nFROM pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_index i\n"
" LEFT JOIN pg_catalog.pg_constraint con ON (conrelid = i.indrelid AND conindid = i.indexrelid AND contype IN ("
@@ -2502,67 +2519,94 @@ describeOneTableDetails(const char *schemaname,
printTableAddFooter(&cont, _("Indexes:"));
for (i = 0; i < tuples; i++)
{
+ char *indname = PQgetvalue(result, i, 0);
+ char *indisprimary = PQgetvalue(result, i, 1);
+ char *indisunique = PQgetvalue(result, i, 2);
+ char *indisclustered = PQgetvalue(result, i, 3);
+ char *indisvalid = PQgetvalue(result, i, 4);
+ char *indexdef = PQgetvalue(result, i, 5);
+ char *constraintdef = PQgetvalue(result, i, 6);
+ char *contype = PQgetvalue(result, i, 7);
+ char *condeferrable = PQgetvalue(result, i, 8);
+ char *condeferred = PQgetvalue(result, i, 9);
+ char *indisreplident = PQgetvalue(result, i, 10);
+ char *reltablespace = PQgetvalue(result, i, 11);
+ char *conperiod = PQgetvalue(result, i, 12);
+ char *indexrelid = PQgetvalue(result, i, 13);
+ PGresult *temp_result = NULL;
+
+ if (pset.sversion >= 200000)
+ {
+ printfPQExpBuffer(&buf, "/* %s */\n", _("Get temp index details"));
+ appendPQExpBuffer(&buf,
+ "SELECT i.indisvalid\n"
+ "FROM pg_catalog.pg_temp_index i\n"
+ "WHERE i.indexrelid = '%s'", indexrelid);
+
+ temp_result = PSQLexec(buf.data);
+ if (temp_result && PQntuples(temp_result) == 1)
+ indisvalid = PQgetvalue(temp_result, 0, 0);
+ }
+
/* untranslated index name */
- printfPQExpBuffer(&buf, " \"%s\"",
- PQgetvalue(result, i, 0));
+ printfPQExpBuffer(&buf, " \"%s\"", indname);
/*
* If exclusion constraint or PK/UNIQUE constraint WITHOUT
* OVERLAPS, print the constraintdef
*/
- if (strcmp(PQgetvalue(result, i, 7), "x") == 0 ||
- strcmp(PQgetvalue(result, i, 12), "t") == 0)
+ if (strcmp(contype, "x") == 0 ||
+ strcmp(conperiod, "t") == 0)
{
- appendPQExpBuffer(&buf, " %s",
- PQgetvalue(result, i, 6));
+ appendPQExpBuffer(&buf, " %s", constraintdef);
}
else
{
- const char *indexdef;
- const char *usingpos;
+ char *usingpos;
/* Label as primary key or unique (but not both) */
- if (strcmp(PQgetvalue(result, i, 1), "t") == 0)
+ if (strcmp(indisprimary, "t") == 0)
appendPQExpBufferStr(&buf, " PRIMARY KEY,");
- else if (strcmp(PQgetvalue(result, i, 2), "t") == 0)
+ else if (strcmp(indisunique, "t") == 0)
{
- if (strcmp(PQgetvalue(result, i, 7), "u") == 0)
+ if (strcmp(contype, "u") == 0)
appendPQExpBufferStr(&buf, " UNIQUE CONSTRAINT,");
else
appendPQExpBufferStr(&buf, " UNIQUE,");
}
/* Everything after "USING" is echoed verbatim */
- indexdef = PQgetvalue(result, i, 5);
usingpos = strstr(indexdef, " USING ");
if (usingpos)
indexdef = usingpos + 7;
appendPQExpBuffer(&buf, " %s", indexdef);
/* Need these for deferrable PK/UNIQUE indexes */
- if (strcmp(PQgetvalue(result, i, 8), "t") == 0)
+ if (strcmp(condeferrable, "t") == 0)
appendPQExpBufferStr(&buf, " DEFERRABLE");
- if (strcmp(PQgetvalue(result, i, 9), "t") == 0)
+ if (strcmp(condeferred, "t") == 0)
appendPQExpBufferStr(&buf, " INITIALLY DEFERRED");
}
/* Add these for all cases */
- if (strcmp(PQgetvalue(result, i, 3), "t") == 0)
+ if (strcmp(indisclustered, "t") == 0)
appendPQExpBufferStr(&buf, " CLUSTER");
- if (strcmp(PQgetvalue(result, i, 4), "t") != 0)
+ if (strcmp(indisvalid, "t") != 0)
appendPQExpBufferStr(&buf, " INVALID");
- if (strcmp(PQgetvalue(result, i, 10), "t") == 0)
+ if (strcmp(indisreplident, "t") == 0)
appendPQExpBufferStr(&buf, " REPLICA IDENTITY");
printTableAddFooter(&cont, buf.data);
/* Print tablespace of the index on the same line */
add_tablespace_footer(&cont, RELKIND_INDEX,
- atooid(PQgetvalue(result, i, 11)),
+ atooid(reltablespace),
false);
+
+ PQclear(temp_result);
}
}
PQclear(result);
diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile
index f60a6454df2..71ea60228d0 100644
--- a/src/include/catalog/Makefile
+++ b/src/include/catalog/Makefile
@@ -88,6 +88,7 @@ CATALOG_HEADERS := \
pg_propgraph_label_property.h \
pg_propgraph_property.h \
pg_temp_class.h \
+ pg_temp_index.h \
pg_temp_statistic.h \
pg_temp_statistic_ext_data.h
diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build
index 7599731de7d..d6ea84a2d46 100644
--- a/src/include/catalog/meson.build
+++ b/src/include/catalog/meson.build
@@ -75,6 +75,7 @@ catalog_headers = [
'pg_propgraph_label_property.h',
'pg_propgraph_property.h',
'pg_temp_class.h',
+ 'pg_temp_index.h',
'pg_temp_statistic.h',
'pg_temp_statistic_ext_data.h',
]
diff --git a/src/include/catalog/pg_temp_class.h b/src/include/catalog/pg_temp_class.h
index a4629b12c9d..ba577001225 100644
--- a/src/include/catalog/pg_temp_class.h
+++ b/src/include/catalog/pg_temp_class.h
@@ -369,20 +369,27 @@ SetEffective_relminmxid(Form_pg_class cf, Form_pg_temp_class tf,
extern HeapTuple GetPgTempClassTuple(Oid relid);
+extern HeapTuple GetPgTempIndexTuple(Oid indexrelid);
extern void InsertPgTempClassTuple(Relation rel);
+extern void InsertPgTempIndexTuple(Oid indexrelid, bool indisvalid);
extern void UpdatePgTempClassTuple(Oid relid, HeapTuple newtuple);
-
extern void UpdatePgTempClassTupleInPlace(Oid relid, HeapTuple newtuple);
+extern void UpdatePgTempIndexTuple(Oid indexrelid, HeapTuple newtuple);
extern void DeletePgTempClassTuple(Oid relid);
+extern void DeletePgTempIndexTuple(Oid indexrelid);
extern HeapTuple GetPgClassAndPgTempClassTuples(Oid relid, bool lock_tuple,
HeapTuple *temp_tuple,
bool check_temp);
+extern HeapTuple GetPgIndexAndPgTempIndexTuples(Oid indexrelid,
+ HeapTuple *temp_tuple,
+ bool check_temp);
extern HeapTuple GetEffectivePgClassTuple(Oid relid);
+extern HeapTuple GetEffectivePgIndexTuple(Oid indexrelid);
extern void UpdateTempFrozenXids(void);
diff --git a/src/include/catalog/pg_temp_index.h b/src/include/catalog/pg_temp_index.h
new file mode 100644
index 00000000000..ce79bf0e2c5
--- /dev/null
+++ b/src/include/catalog/pg_temp_index.h
@@ -0,0 +1,66 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_temp_index.h
+ * definition of the "temporary index" system catalog (pg_temp_index)
+ *
+ * This is a global temporary system catalog table storing session-specific
+ * information about temporary indexes. Currently, it is only used for global
+ * temporary indexes. The attributes (currently just indisvalid) are a subset
+ * of those from pg_index, and their values take precedence over the values
+ * from pg_index.
+ *
+ * Portions Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * src/include/catalog/pg_index.h
+ *
+ * NOTES
+ * The Catalog.pm module reads this file and derives schema
+ * information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_TEMP_INDEX_H
+#define PG_TEMP_INDEX_H
+
+#include "access/genam.h"
+#include "catalog/genbki.h"
+#include "catalog/pg_index.h"
+#include "catalog/pg_temp_index_d.h" /* IWYU pragma: export */
+
+/* ----------------
+ * pg_temp_index definition. cpp turns this into
+ * typedef struct FormData_pg_temp_index.
+ * ----------------
+ */
+BEGIN_CATALOG_STRUCT
+
+CATALOG(pg_temp_index,8092,TempIndexRelationId) BKI_TEMP_RELATION
+{
+ Oid indexrelid BKI_LOOKUP(pg_class); /* OID of the index */
+ bool indisvalid; /* is this index valid for use by queries? */
+} FormData_pg_temp_index;
+
+END_CATALOG_STRUCT
+
+/* ----------------
+ * Form_pg_temp_index corresponds to a pointer to a tuple with
+ * the format of pg_temp_index relation.
+ * ----------------
+ */
+typedef FormData_pg_temp_index *Form_pg_temp_index;
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_temp_index_indexrelid_index, 8093, TempIndexRelidIndexId, pg_temp_index, btree(indexrelid oid_ops));
+
+MAKE_SYSCACHE(TEMPINDEXRELID, pg_temp_index_indexrelid_index, 64);
+
+/*
+ * Get the effective value of indisvalid from pg_index and pg_temp_index tuple
+ * data. The value from pg_temp_index (if present) takes precedence.
+ */
+static inline bool
+GetEffective_indisvalid(Form_pg_index f, Form_pg_temp_index tf)
+{
+ return tf != NULL ? tf->indisvalid : f->indisvalid;
+}
+
+#endif /* PG_TEMP_INDEX_H */
diff --git a/src/test/isolation/expected/global-temp.out b/src/test/isolation/expected/global-temp.out
index bbaa768f479..b0f632779b6 100644
--- a/src/test/isolation/expected/global-temp.out
+++ b/src/test/isolation/expected/global-temp.out
@@ -438,6 +438,87 @@ key|val|seq
(1 row)
+starting permutation: ins1 ins2 idx1 sel1_idx sel2_idx analyze2 sel2_idx reidx2 sel2_idx
+step ins1: INSERT INTO tmp VALUES (1, 's1');
+step ins2: INSERT INTO tmp VALUES (1, 's2');
+step idx1: CREATE INDEX tmp_val_idx ON tmp(val);
+step sel1_idx:
+ SET enable_seqscan = off;
+ SET enable_bitmapscan = off;
+ EXPLAIN (COSTS OFF)
+ SELECT * FROM tmp WHERE val = 's1';
+ SELECT * FROM tmp WHERE val = 's1';
+
+QUERY PLAN
+-----------------------------------
+Index Scan using tmp_val_idx on tmp
+ Index Cond: (val = 's1'::text)
+(2 rows)
+
+key|val|seq
+---+---+---
+ 1|s1 | 1
+(1 row)
+
+step sel2_idx:
+ SET enable_seqscan = off;
+ SET enable_bitmapscan = off;
+ EXPLAIN (COSTS OFF)
+ SELECT * FROM tmp WHERE val = 's2';
+ SELECT * FROM tmp WHERE val = 's2';
+
+QUERY PLAN
+----------------------------
+Seq Scan on tmp
+ Disabled: true
+ Filter: (val = 's2'::text)
+(3 rows)
+
+key|val|seq
+---+---+---
+ 1|s2 | 1
+(1 row)
+
+step analyze2: ANALYZE tmp;
+step sel2_idx:
+ SET enable_seqscan = off;
+ SET enable_bitmapscan = off;
+ EXPLAIN (COSTS OFF)
+ SELECT * FROM tmp WHERE val = 's2';
+ SELECT * FROM tmp WHERE val = 's2';
+
+QUERY PLAN
+----------------------------
+Seq Scan on tmp
+ Disabled: true
+ Filter: (val = 's2'::text)
+(3 rows)
+
+key|val|seq
+---+---+---
+ 1|s2 | 1
+(1 row)
+
+step reidx2: REINDEX INDEX tmp_val_idx;
+step sel2_idx:
+ SET enable_seqscan = off;
+ SET enable_bitmapscan = off;
+ EXPLAIN (COSTS OFF)
+ SELECT * FROM tmp WHERE val = 's2';
+ SELECT * FROM tmp WHERE val = 's2';
+
+QUERY PLAN
+-----------------------------------
+Index Scan using tmp_val_idx on tmp
+ Index Cond: (val = 's2'::text)
+(2 rows)
+
+key|val|seq
+---+---+---
+ 1|s2 | 1
+(1 row)
+
+
starting permutation: ins1 ins2 t2 sel1 sel2 ins2 t1 sel1 sel2 ins1 t2 sel1 sel2
step ins1: INSERT INTO tmp VALUES (1, 's1');
step ins2: INSERT INTO tmp VALUES (1, 's2');
diff --git a/src/test/isolation/specs/global-temp.spec b/src/test/isolation/specs/global-temp.spec
index a57fcfd3e53..b3b207acb9d 100644
--- a/src/test/isolation/specs/global-temp.spec
+++ b/src/test/isolation/specs/global-temp.spec
@@ -85,6 +85,7 @@ step get_tblspace2 {
LEFT JOIN pg_tablespace s2 ON s2.oid = t.reltablespace
WHERE c.relname = 'tmp';
}
+step analyze2 { ANALYZE tmp; }
# Create test tablespace for remaining tests
permutation create_tblspace list_tblspaces
@@ -114,6 +115,7 @@ permutation create1dr ins1_2 ins2_2 drop1 create1dr ins1_2 ins2_2 drop1
permutation ins1 idx1 sel1_idx ins2 sel2_idx
permutation ins1 ins2 idx1 sel1_idx sel2_idx
permutation ins1 ins2 idx1 sel1_idx sel2_idx reidx2 sel2_idx
+permutation ins1 ins2 idx1 sel1_idx sel2_idx analyze2 sel2_idx reidx2 sel2_idx
# Test local TRUNCATE
permutation ins1 ins2 t2 sel1 sel2 ins2 t1 sel1 sel2 ins1 t2 sel1 sel2
diff --git a/src/test/regress/expected/global_temp.out b/src/test/regress/expected/global_temp.out
index 10519ceee21..693d12eaad4 100644
--- a/src/test/regress/expected/global_temp.out
+++ b/src/test/regress/expected/global_temp.out
@@ -227,6 +227,109 @@ SELECT * FROM tmp2 ORDER BY a;
2
(1 row)
+DROP TABLE tmp2;
+-- Test partitioned index validity
+CREATE GLOBAL TEMP TABLE tmp2 (a int) PARTITION BY LIST (a);
+CREATE GLOBAL TEMP TABLE tmp2_p1 PARTITION OF tmp2 FOR VALUES IN (1);
+CREATE INDEX tmp2_a_idx ON tmp2 (a);
+SELECT c.relname, i.indisvalid AS global_valid, t.indisvalid AS local_valid
+ FROM pg_class c JOIN pg_index i ON i.indexrelid = c.oid
+ LEFT JOIN pg_temp_index t ON t.indexrelid = i.indexrelid
+ WHERE c.relname ~ 'tmp2(.*)_a_idx'
+ ORDER BY c.relname;
+ relname | global_valid | local_valid
+---------------+--------------+-------------
+ tmp2_a_idx | t | t
+ tmp2_p1_a_idx | t | t
+(2 rows)
+
+\d tmp2
+Global temporary partitioned table "global_temp_tests.tmp2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ a | integer | | |
+Partition key: LIST (a)
+Indexes:
+ "tmp2_a_idx" btree (a)
+Number of partitions: 1 (Use \d+ to list them.)
+
+\d tmp2_a_idx
+Partitioned index "global_temp_tests.tmp2_a_idx"
+ Column | Type | Key? | Definition
+--------+---------+------+------------
+ a | integer | yes | a
+btree, for table "global_temp_tests.tmp2"
+Number of partitions: 1 (Use \d+ to list them.)
+
+DROP INDEX tmp2_a_idx;
+CREATE INDEX tmp2_a_idx ON ONLY tmp2 (a);
+SELECT c.relname, i.indisvalid AS global_valid, t.indisvalid AS local_valid
+ FROM pg_class c JOIN pg_index i ON i.indexrelid = c.oid
+ LEFT JOIN pg_temp_index t ON t.indexrelid = i.indexrelid
+ WHERE c.relname ~ 'tmp2(.*)_a_idx'
+ ORDER BY c.relname;
+ relname | global_valid | local_valid
+------------+--------------+-------------
+ tmp2_a_idx | f | f
+(1 row)
+
+\d tmp2
+Global temporary partitioned table "global_temp_tests.tmp2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ a | integer | | |
+Partition key: LIST (a)
+Indexes:
+ "tmp2_a_idx" btree (a) INVALID
+Number of partitions: 1 (Use \d+ to list them.)
+
+\d tmp2_a_idx
+Partitioned index "global_temp_tests.tmp2_a_idx"
+ Column | Type | Key? | Definition
+--------+---------+------+------------
+ a | integer | yes | a
+btree, for table "global_temp_tests.tmp2", invalid
+Number of partitions: 0
+
+CREATE INDEX tmp2_p1_a_idx ON tmp2_p1 (a);
+ALTER INDEX tmp2_a_idx ATTACH PARTITION tmp2_p1_a_idx;
+SELECT c.relname, i.indisvalid AS global_valid, t.indisvalid AS local_valid
+ FROM pg_class c JOIN pg_index i ON i.indexrelid = c.oid
+ LEFT JOIN pg_temp_index t ON t.indexrelid = i.indexrelid
+ WHERE c.relname ~ 'tmp2(.*)_a_idx'
+ ORDER BY c.relname;
+ relname | global_valid | local_valid
+---------------+--------------+-------------
+ tmp2_a_idx | t | t
+ tmp2_p1_a_idx | t | t
+(2 rows)
+
+\d tmp2
+Global temporary partitioned table "global_temp_tests.tmp2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ a | integer | | |
+Partition key: LIST (a)
+Indexes:
+ "tmp2_a_idx" btree (a)
+Number of partitions: 1 (Use \d+ to list them.)
+
+\d tmp2_a_idx
+Partitioned index "global_temp_tests.tmp2_a_idx"
+ Column | Type | Key? | Definition
+--------+---------+------+------------
+ a | integer | yes | a
+btree, for table "global_temp_tests.tmp2"
+Number of partitions: 1 (Use \d+ to list them.)
+
+\d tmp2_p1_a_idx
+Index "global_temp_tests.tmp2_p1_a_idx"
+ Column | Type | Key? | Definition
+--------+---------+------+------------
+ a | integer | yes | a
+Partition of: tmp2_a_idx
+btree, for table "global_temp_tests.tmp2_p1"
+
DROP TABLE tmp2;
-- Test ALTER TABLE with rewrite
CREATE GLOBAL TEMP TABLE tmp2 (a int);
@@ -510,10 +613,12 @@ SELECT oid::regclass FROM pg_temp_class ORDER BY 1;
pg_temp_class_oid_index
pg_temp_statistic
pg_temp_statistic_relid_att_inh_index
+ pg_temp_index
+ pg_temp_index_indexrelid_index
tmp1_c_seq
tmp1
tmp1_pkey
-(7 rows)
+(9 rows)
SELECT * FROM tmp1;
a | b | c
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index bccae052a15..2b4af8452e6 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -287,6 +287,7 @@ NOTICE: checking pg_propgraph_property {pgptypid} => pg_type {oid}
NOTICE: checking pg_propgraph_property {pgpcollation} => pg_collation {oid}
NOTICE: checking pg_temp_class {oid} => pg_class {oid}
NOTICE: checking pg_temp_class {reltablespace} => pg_tablespace {oid}
+NOTICE: checking pg_temp_index {indexrelid} => pg_class {oid}
NOTICE: checking pg_temp_statistic {starelid} => pg_class {oid}
NOTICE: checking pg_temp_statistic {staop1} => pg_operator {oid}
NOTICE: checking pg_temp_statistic {staop2} => pg_operator {oid}
diff --git a/src/test/regress/sql/global_temp.sql b/src/test/regress/sql/global_temp.sql
index 4272824e894..3f887d3d369 100644
--- a/src/test/regress/sql/global_temp.sql
+++ b/src/test/regress/sql/global_temp.sql
@@ -129,6 +129,40 @@ SET search_path = global_temp_tests;
SELECT * FROM tmp2 ORDER BY a;
DROP TABLE tmp2;
+-- Test partitioned index validity
+CREATE GLOBAL TEMP TABLE tmp2 (a int) PARTITION BY LIST (a);
+CREATE GLOBAL TEMP TABLE tmp2_p1 PARTITION OF tmp2 FOR VALUES IN (1);
+CREATE INDEX tmp2_a_idx ON tmp2 (a);
+SELECT c.relname, i.indisvalid AS global_valid, t.indisvalid AS local_valid
+ FROM pg_class c JOIN pg_index i ON i.indexrelid = c.oid
+ LEFT JOIN pg_temp_index t ON t.indexrelid = i.indexrelid
+ WHERE c.relname ~ 'tmp2(.*)_a_idx'
+ ORDER BY c.relname;
+\d tmp2
+\d tmp2_a_idx
+
+DROP INDEX tmp2_a_idx;
+CREATE INDEX tmp2_a_idx ON ONLY tmp2 (a);
+SELECT c.relname, i.indisvalid AS global_valid, t.indisvalid AS local_valid
+ FROM pg_class c JOIN pg_index i ON i.indexrelid = c.oid
+ LEFT JOIN pg_temp_index t ON t.indexrelid = i.indexrelid
+ WHERE c.relname ~ 'tmp2(.*)_a_idx'
+ ORDER BY c.relname;
+\d tmp2
+\d tmp2_a_idx
+
+CREATE INDEX tmp2_p1_a_idx ON tmp2_p1 (a);
+ALTER INDEX tmp2_a_idx ATTACH PARTITION tmp2_p1_a_idx;
+SELECT c.relname, i.indisvalid AS global_valid, t.indisvalid AS local_valid
+ FROM pg_class c JOIN pg_index i ON i.indexrelid = c.oid
+ LEFT JOIN pg_temp_index t ON t.indexrelid = i.indexrelid
+ WHERE c.relname ~ 'tmp2(.*)_a_idx'
+ ORDER BY c.relname;
+\d tmp2
+\d tmp2_a_idx
+\d tmp2_p1_a_idx
+DROP TABLE tmp2;
+
-- Test ALTER TABLE with rewrite
CREATE GLOBAL TEMP TABLE tmp2 (a int);
INSERT INTO tmp2 VALUES (1);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 6b38155a48a..e2e00c053dd 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -957,6 +957,7 @@ FormData_pg_subscription
FormData_pg_subscription_rel
FormData_pg_tablespace
FormData_pg_temp_class
+FormData_pg_temp_index
FormData_pg_transform
FormData_pg_trigger
FormData_pg_ts_config
@@ -1023,6 +1024,7 @@ Form_pg_subscription
Form_pg_subscription_rel
Form_pg_tablespace
Form_pg_temp_class
+Form_pg_temp_index
Form_pg_transform
Form_pg_trigger
Form_pg_ts_config
--
2.51.0
view thread (492+ messages)
reply
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Reply to all the recipients using the --to and --cc options:
reply via email
To: [email protected]
Cc: [email protected], [email protected], [email protected], [email protected]
Subject: Re: Global temporary tables
In-Reply-To: <CAEZATCVjaZFa80S-V_+nvnzRK1ZiLUPx8jwdyAtRVHAmPZQQMg@mail.gmail.com>
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox