From a6ded3acb6e99a35a85a5a6bcbbe7754977a231f Mon Sep 17 00:00:00 2001
From: Dean Rasheed <dean.a.rasheed@gmail.com>
Date: Tue, 9 Jun 2026 11:14:27 +0100
Subject: [PATCH v11 2/9] Basic support for global temporary tables.

This allows global temporary tables to be created using the
SQL-standard syntax:

  CREATE GLOBAL TEMP[ORARY] TABLE ...

Global temporary tables have a relpersistence value of
RELPERSISTENCE_GLOBAL_TEMP. The table definition is persistent, and
visible to all sessions, but the table's data is session-local, and is
deleted on backend exit.

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 transactionally, in case of rollback, and deleted on
backend exit. A global temporary table not being used by any session
has no storage, and so it requires a shared dependency on its
tablespace.

All usage of global temporary tables is recorded in a shared hash
table. Operations like those forms of ALTER TABLE that require the
table's storage to be rewritten, or addition of constraints, requiring
the table's data to be scanned, are forbidden if the table is being
used by another session.

Information about global temporary tables being used in the current
session can be returned using 2 new functions:
 - pg_gtr_info(relid) returns record
 - pg_gtrs_in_use() returns setof record

The following are allowed for global temporary tables:

 - ON COMMIT DELETE ROWS
 - Inheritance children of any kind
 - Inheritance from a permanent parent table or another GTT
 - Partitioning with GTTs as partitions
 - Foreign keys between GTTs (subject to index support, to be added in
   a subsequent commit)
 - Permanent and temporary views selecting from GTTs
 - Materialized views selecting from GTTs
 - ALTER TABLE ... SET TABLESPACE
 - REPACK
 - TRUNCATE

The following are intentionally not allowed:

 - ON COMMIT DROP
 - GTTs in a temporary schema
 - GTTs in a publication
 - A GTT as the subscription target
 - Global temporary views
 - Global temporary property graphs
 - Concurrent operations such as concurrent drop or reindex
 - Inheritance from a local temporary parent table
 - GTTs as partitions of permanent or local temporary tables
 - Permanent or local temporary partitions of GTTs
 - Foreign keys between a GTT and any other kind of table
 - SET LOGGED/UNLOGGED (GTTs are always unlogged)
 - Parallel workers accessing GTTs
 - Autovacuum on GTTs

The following do not currently work properly, and will be fixed in
subsequent commits:

 - Indexes on GTTs
 - Global temporary sequences
 - Per-session statistics
 - CLUSTER
 - REINDEX
 - VACUUM
---
 contrib/pg_prewarm/pg_prewarm.c               |   21 +
 doc/src/sgml/catalogs.sgml                    |   18 +-
 doc/src/sgml/func/func-admin.sgml             |   11 +-
 doc/src/sgml/func/func-info.sgml              |   43 +
 doc/src/sgml/logical-replication.sgml         |    5 +-
 doc/src/sgml/ref/alter_table.sgml             |    8 +
 doc/src/sgml/ref/create_table.sgml            |  140 +-
 doc/src/sgml/storage.sgml                     |   16 +-
 src/backend/access/heap/heapam_handler.c      |    5 +-
 src/backend/access/transam/xact.c             |   40 +-
 src/backend/catalog/Makefile                  |    1 +
 src/backend/catalog/catalog.c                 |    1 +
 src/backend/catalog/global_temp.c             | 1583 +++++++++++++++++
 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                 |   17 +-
 src/backend/commands/dbcommands.c             |    9 +-
 src/backend/commands/indexcmds.c              |    6 +
 src/backend/commands/lockcmds.c               |    3 +-
 src/backend/commands/repack.c                 |   76 +-
 src/backend/commands/subscriptioncmds.c       |   21 +
 src/backend/commands/tablecmds.c              |  264 ++-
 src/backend/commands/tablespace.c             |    3 +-
 src/backend/commands/vacuum.c                 |    6 +
 src/backend/commands/view.c                   |    9 +
 src/backend/optimizer/path/allpaths.c         |    6 +-
 src/backend/parser/analyze.c                  |    4 +-
 src/backend/parser/gram.y                     |   38 +-
 src/backend/postmaster/autovacuum.c           |   16 +-
 src/backend/postmaster/datachecksum_state.c   |   23 +
 src/backend/replication/logical/relation.c    |    8 +
 src/backend/storage/buffer/bufmgr.c           |   36 +-
 .../utils/activity/wait_event_names.txt       |    3 +
 src/backend/utils/adt/dbsize.c                |   14 +-
 src/backend/utils/cache/lsyscache.c           |   10 +
 src/backend/utils/cache/relcache.c            |   92 +-
 src/backend/utils/cache/relfilenumbermap.c    |    3 +-
 src/bin/pg_amcheck/pg_amcheck.c               |   19 +-
 src/bin/pg_dump/pg_dump.c                     |   23 +-
 src/bin/pg_dump/pg_dump.h                     |    1 +
 src/bin/pg_dump/t/002_pg_dump.pl              |   58 +-
 src/bin/pg_upgrade/info.c                     |    3 +
 src/bin/psql/describe.c                       |   22 +-
 src/bin/psql/tab-complete.in.c                |   46 +-
 src/bin/scripts/vacuuming.c                   |    4 +-
 src/include/catalog/global_temp.h             |  116 ++
 src/include/catalog/pg_class.h                |    3 +-
 src/include/catalog/pg_proc.dat               |   16 +
 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   |  528 ++++++
 src/test/isolation/isolation_schedule         |    1 +
 src/test/isolation/specs/global-temp.spec     |  153 ++
 src/test/modules/test_checksums/meson.build   |    1 +
 .../test_checksums/t/025_global_temp.pl       |   56 +
 src/test/recovery/t/001_stream_rep.pl         |   31 +-
 src/test/regress/expected/alter_table.out     |   12 +-
 src/test/regress/expected/create_table.out    |   20 +-
 src/test/regress/expected/create_view.out     |    2 +-
 src/test/regress/expected/foreign_data.out    |    4 +-
 src/test/regress/expected/global_temp.out     |  400 +++++
 src/test/regress/expected/inherit.out         |    2 +-
 src/test/regress/expected/matview.out         |    4 +-
 src/test/regress/expected/type_sanity.out     |    2 +-
 src/test/regress/parallel_schedule            |    4 +-
 src/test/regress/sql/alter_table.sql          |    5 +-
 src/test/regress/sql/create_table.sql         |    8 +
 src/test/regress/sql/global_temp.sql          |  239 +++
 src/test/regress/sql/type_sanity.sql          |    2 +-
 src/test/subscription/meson.build             |    1 +
 src/test/subscription/t/039_global_temp.pl    |  100 ++
 src/tools/pgindent/typedefs.list              |    7 +
 77 files changed, 4268 insertions(+), 256 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/025_global_temp.pl
 create mode 100644 src/test/regress/expected/global_temp.out
 create mode 100644 src/test/regress/sql/global_temp.sql
 create mode 100644 src/test/subscription/t/039_global_temp.pl

diff --git a/contrib/pg_prewarm/pg_prewarm.c b/contrib/pg_prewarm/pg_prewarm.c
index c2716086693..c6f63ac53c8 100644
--- a/contrib/pg_prewarm/pg_prewarm.c
+++ b/contrib/pg_prewarm/pg_prewarm.c
@@ -15,7 +15,9 @@
 #include <sys/stat.h>
 #include <unistd.h>
 
+#include "access/parallel.h"
 #include "access/relation.h"
+#include "access/xact.h"
 #include "catalog/index.h"
 #include "fmgr.h"
 #include "miscadmin.h"
@@ -160,10 +162,29 @@ pg_prewarm(PG_FUNCTION_ARGS)
 
 	/* Check that the fork exists. */
 	if (!smgrexists(RelationGetSmgr(rel), forkNumber))
+	{
+		/*
+		 * Normally, we treat a missing fork as an error, but during parallel
+		 * operation, it can happen for a global temporary relation that
+		 * hasn't been used, and so has not been initialized, which isn't an
+		 * error.
+		 */
+		if ((IsInParallelMode() || 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/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 32e205c9f87..95e8d49e7a6 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2004,6 +2004,12 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
        Name of the on-disk file of this relation; zero means this
        is a <quote>mapped</quote> relation whose disk file name is determined
        by low-level state
+      </para>
+      <para>
+       For a global temporary relation, the
+       <structfield>relfilenode</structfield> value returned by
+       <link linkend="pg-gtr-info"><function>pg_gtr_info()</function></link>,
+       if non-null, takes precedence over the value from this catalog.
       </para></entry>
      </row>
 
@@ -2019,6 +2025,12 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
        except for partitioned tables, where this is the tablespace
        in which partitions will be created when one is not
        specified in the creation command.
+      </para>
+      <para>
+       For a global temporary relation, the
+       <structfield>reltablespace</structfield> value returned by
+       <link linkend="pg-gtr-info"><function>pg_gtr_info()</function></link>,
+       if non-null, takes precedence over the value from this catalog.
       </para></entry>
      </row>
 
@@ -2134,8 +2146,10 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
        <structfield>relpersistence</structfield> <type>char</type>
       </para>
       <para>
-       <literal>p</literal> = permanent table/sequence, <literal>u</literal> = unlogged table/sequence,
-       <literal>t</literal> = temporary table/sequence
+       <literal>p</literal> = permanent table/sequence,
+       <literal>u</literal> = unlogged table/sequence,
+       <literal>t</literal> = local temporary table/sequence,
+       <literal>g</literal> = global temporary table/sequence
       </para></entry>
      </row>
 
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 64b0e7bb972..7f761946357 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -1823,9 +1823,14 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
         For most relations the result is the same as
         <structname>pg_class</structname>.<structfield>relfilenode</structfield>,
         but for certain system catalogs <structfield>relfilenode</structfield>
-        is zero and this function must be used to get the correct value.  The
-        function returns NULL if passed a relation that does not have storage,
-        such as a view.
+        is zero and this function must be used to get the correct value.  For
+        global temporary relations the result matches the
+        <structfield>relfilenode</structfield> result from
+        <link linkend="pg-gtr-info"><function>pg_gtr_info()</function></link>,
+        if the relation has been used in the current session, and
+        <link linkend="catalog-pg-class"><structname>pg_class</structname></link>.<structfield>relfilenode</structfield>
+        otherwise.  The function returns NULL if passed a relation that does
+        not have storage, such as a view.
        </para></entry>
       </row>
 
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index e56c9a22c42..fe7677a03ec 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -346,6 +346,49 @@
        </para></entry>
       </row>
 
+      <row>
+       <entry id="pg-gtr-info" role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_gtr_info</primary>
+        </indexterm>
+        <function>pg_gtr_info</function> ( <type>oid</type> )
+        <returnvalue>record</returnvalue>
+        ( <parameter>relfilenode</parameter> <type>oid</type>,
+        <parameter>reltablespace</parameter> <type>oid</type> )
+       </para>
+       <para>
+        Returns information about a global temporary relation being used in
+        the current session.  The values returned initially match the values
+        from <link linkend="catalog-pg-class"><structname>pg_class</structname></link>,
+        but various commands may cause them to change. For example,
+        <xref linkend="sql-truncate"/> alters a global temporary relation's
+        <structfield>relfilenode</structfield> without updating the
+        <structname>pg_class</structname> tuple, and this function may be used
+        to retrieve the new value.  If the input is not the OID of a global
+        temporary relation, or that relation has not been used in the current
+        session, then the return values are all <literal>NULL</literal>.
+       </para></entry>
+      </row>
+
+      <row>
+       <entry id="pg-gtrs-in-use" role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_gtrs_in_use</primary>
+        </indexterm>
+        <function>pg_gtrs_in_use</function> ()
+        <returnvalue>setof record</returnvalue>
+        ( <parameter>oid</parameter> <type>oid</type>,
+        <parameter>relfilenode</parameter> <type>oid</type>,
+        <parameter>reltablespace</parameter> <type>oid</type> )
+       </para>
+       <para>
+        Returns information about all global temporary relations being used in
+        the current session.  The <parameter>oid</parameter> column contains
+        the OID of a global temporary relation, and the values in the remaining
+        columns match those returned by <function>pg_gtr_info()</function>.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 4701a3d9d18..1ac78b1665a 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -268,8 +268,9 @@
 
   <para>
    The schema definitions are not replicated, and the published tables must
-   exist on the subscriber.  Only regular tables may be
-   the target of replication.  For example, you can't replicate to a view.
+   exist on the subscriber.  Only permanent, regular tables may be the target
+   of replication.  For example, you can't replicate to a view or a temporary
+   table.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 0f9d698d170..6aed0826f2f 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -1566,6 +1566,14 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     occurred.  See <xref linkend="mvcc-caveats"/> for more details.
    </para>
 
+   <para>
+    If <command>ALTER TABLE</command> is invoked on a global temporary table,
+    and a table scan or rewrite is required, an error is thrown if any other
+    session is accessing the table because <productname>PostgreSQL</productname>
+    has no way to access the contents of a global temporary table in another
+    session.
+   </para>
+
    <para>
     The <literal>USING</literal> option of <literal>SET DATA TYPE</literal> can actually
     specify any expression involving the old values of the row; that is, it
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index fef24d8f3a2..eac59a1893d 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -139,16 +139,17 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
    The durability characteristics of a table are governed by its persistence
    mode.  By default, the data will be persistent and crash-safe.
    For less stringent requirements and better performance, a table can be
-   specified as <link linkend="sql-createtable-temporary">temporary</link>
+   specified as <link linkend="sql-createtable-temporary">global temporary</link>,
+   <link linkend="sql-createtable-temporary">local temporary</link>,
    or <link linkend="sql-createtable-unlogged">unlogged</link>.
   </para>
 
   <para>
    If a schema name is given (for example, <literal>CREATE TABLE
    myschema.mytable ...</literal>) then the table is created in the specified
-   schema.  Otherwise it is created in the current schema.  Temporary
+   schema.  Otherwise it is created in the current schema.  Local temporary
    tables exist in a special schema, so a schema name cannot be given
-   when creating a temporary table.  The name of the table must be
+   when creating a local temporary table.  The name of the table must be
    distinct from the name of any other relation (table, sequence, index, view,
    materialized view, or foreign table) in the same schema.
   </para>
@@ -190,18 +191,46 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
   <variablelist>
 
    <varlistentry id="sql-createtable-temporary">
-    <term><literal>TEMPORARY</literal> or <literal>TEMP</literal></term>
+    <term><literal>[ GLOBAL | LOCAL ] { TEMPORARY | TEMP }</literal></term>
     <listitem>
      <para>
-      If specified, the table is created as a temporary table.
-      Temporary tables are automatically dropped at the end of a
-      session, or optionally at the end of the current transaction
-      (see <literal>ON COMMIT</literal> below).  The default
-      search_path includes the temporary schema first and so identically
-      named existing permanent tables are not chosen for new plans
-      while the temporary table exists, unless they are referenced
-      with schema-qualified names. Any indexes created on a temporary
-      table are automatically temporary as well.
+      If specified, the table is created as a temporary table.  Temporary
+      tables may be either global or local.  If neither
+      <literal>GLOBAL</literal> nor <literal>LOCAL</literal> is specified,
+      then local is assumed.
+     </para>
+
+     <para>
+      Global temporary tables are created in the current schema by default,
+      or some other schema, if a schema-qualified table name is used, but
+      they may not be created in the temporary schema used for local
+      temporary tables.  The definition of a global temporary table is
+      persistent and visible to all sessions, but the table's contents are
+      session-local; each session may insert its own data, which is visible
+      only to that session, and is automatically deleted at the end of the
+      session, or optionally at the end of the current transaction (see
+      <link linkend="sql-createtable-parms-on-commit"><literal>ON COMMIT</literal></link>
+      below).
+     </para>
+
+     <para>
+      Local temporary tables are created in a special temporary schema,
+      visible only to the current session; the table definition and its
+      contents are not visible to other sessions.  Local temporary tables are
+      automatically dropped at the end of a session, or optionally at the end
+      of the current transaction (see
+      <link linkend="sql-createtable-parms-on-commit"><literal>ON COMMIT</literal></link>
+      below).  The default search_path includes the temporary schema first
+      and so identically named existing permanent tables are not chosen for
+      new plans while the local temporary table exists, unless they are
+      referenced with schema-qualified names.
+     </para>
+
+     <para>
+      Any indexes created on a local or global temporary table, and any
+      sequences automatically created (for identity or serial columns) are
+      assigned the same persistence mode (local or global temporary) as the
+      table.
      </para>
 
      <para>
@@ -212,14 +241,6 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       table is going to be used in complex queries, it is wise to run
       <command>ANALYZE</command> on the temporary table after it is populated.
      </para>
-
-     <para>
-      Optionally, <literal>GLOBAL</literal> or <literal>LOCAL</literal>
-      can be written before <literal>TEMPORARY</literal> or <literal>TEMP</literal>.
-      This presently makes no difference in <productname>PostgreSQL</productname>
-      and is deprecated; see
-      <xref linkend="sql-createtable-compatibility"/> below.
-     </para>
     </listitem>
    </varlistentry>
 
@@ -383,6 +404,9 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       The optional <literal>INHERITS</literal> clause specifies a list of
       tables from which the new table automatically inherits all
       columns.  Parent tables can be plain tables or foreign tables.
+      If the parent table is a local temporary table, the child table must
+      also be local temporary.  Otherwise, the child may have any persistence
+      mode.
      </para>
 
      <para>
@@ -484,7 +508,9 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       values using <literal>FOR VALUES</literal> or as a default partition
       using <literal>DEFAULT</literal>.  Any indexes, constraints and
       user-defined row-level triggers that exist in the parent table are cloned
-      on the new partition.
+      on the new partition.  Partitions must have the same persistence mode as
+      the parent table, except that a permanent parent table may have unlogged
+      partitions.
      </para>
 
      <para>
@@ -1260,7 +1286,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       addition of a foreign key constraint requires a
       <literal>SHARE ROW EXCLUSIVE</literal> lock on the referenced table.
       Note that foreign key constraints cannot be defined between temporary
-      tables and permanent tables.
+      tables and permanent tables, or between local temporary tables and
+      global temporary tables.
      </para>
 
      <para>
@@ -1534,7 +1561,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
           The temporary table will be dropped at the end of the current
           transaction block.  When used on a partitioned table, this action
           drops its partitions and when used on tables with inheritance
-          children, it drops the dependent children.
+          children, it drops the dependent children.  This option is not
+          supported on global temporary tables.
          </para>
         </listitem>
        </varlistentry>
@@ -2445,51 +2473,49 @@ CREATE TABLE cities_partdef
    <title>Temporary Tables</title>
 
    <para>
-    Although the syntax of <literal>CREATE TEMPORARY TABLE</literal>
-    resembles that of the SQL standard, the effect is not the same.  In the
-    standard,
-    temporary tables are defined just once and automatically exist (starting
-    with empty contents) in every session that needs them.
-    <productname>PostgreSQL</productname> instead
-    requires each session to issue its own <literal>CREATE TEMPORARY
-    TABLE</literal> command for each temporary table to be used.  This allows
-    different sessions to use the same temporary table name for different
-    purposes, whereas the standard's approach constrains all instances of a
-    given temporary table name to have the same table structure.
+    The syntax of <literal>CREATE TEMPORARY TABLE</literal> conforms to
+    the <acronym>SQL</acronym> standard, except that in
+    <productname>PostgreSQL</productname> the <literal>GLOBAL</literal>
+    and <literal>LOCAL</literal> keywords are optional, whereas in the
+    standard they are mandatory.
    </para>
 
    <para>
-    The standard's definition of the behavior of temporary tables is
-    widely ignored.  <productname>PostgreSQL</productname>'s behavior
-    on this point is similar to that of several other SQL databases.
-   </para>
-
-   <para>
-    The SQL standard also distinguishes between global and local temporary
-    tables, where a local temporary table has a separate set of contents for
-    each SQL module within each session, though its definition is still shared
-    across sessions.  Since <productname>PostgreSQL</productname> does not
-    support SQL modules, this distinction is not relevant in
-    <productname>PostgreSQL</productname>.
+    The behavior of global temporary tables conforms to the
+    <acronym>SQL</acronym> standard, except for the
+    <literal>ON COMMIT</literal> clause, as described below.
    </para>
 
    <para>
-    For compatibility's sake, <productname>PostgreSQL</productname> will
-    accept the <literal>GLOBAL</literal> and <literal>LOCAL</literal> keywords
-    in a temporary table declaration, but they currently have no effect.
-    Use of these keywords is discouraged, since future versions of
-    <productname>PostgreSQL</productname> might adopt a more
-    standard-compliant interpretation of their meaning.
+    Local temporary tables do not conform to the standard.  In the
+    <acronym>SQL</acronym> standard, local temporary tables have separate
+    contents for each <acronym>SQL</acronym> module within each session.
+    Since <productname>PostgreSQL</productname> does not support
+    <acronym>SQL</acronym> modules, this is not possible in
+    <productname>PostgreSQL</productname>.  Instead, in
+    <productname>PostgreSQL</productname>, local temporary tables are
+    entirely session-local; both the table definition and its contents
+    are visible only to the creating session.  This requires each session
+    to issue its own <literal>CREATE TEMPORARY TABLE</literal> command for
+    each local temporary table to be used.  This allows different sessions
+    to use the same local temporary table name for different purposes,
+    whereas the standard's approach constrains all instances of a
+    given temporary table name (whether local or global) to have the same
+    table structure.  <productname>PostgreSQL</productname>'s behavior
+    on this point is similar to that of several other <acronym>SQL</acronym>
+    databases.
    </para>
 
    <para>
     The <literal>ON COMMIT</literal> clause for temporary tables
-    also resembles the SQL standard, but has some differences.
-    If the <literal>ON COMMIT</literal> clause is omitted, SQL specifies that the
-    default behavior is <literal>ON COMMIT DELETE ROWS</literal>.  However, the
+    also resembles the <acronym>SQL</acronym> standard, but has some
+    differences.  If the <literal>ON COMMIT</literal> clause is omitted,
+    the <acronym>SQL</acronym> standard specifies that the default
+    behavior is <literal>ON COMMIT DELETE ROWS</literal>.  However, the
     default behavior in <productname>PostgreSQL</productname> is
     <literal>ON COMMIT PRESERVE ROWS</literal>.  The <literal>ON COMMIT
-    DROP</literal> option does not exist in SQL.
+    DROP</literal> option for local temporary tables does not exist in
+    <acronym>SQL</acronym>.
    </para>
   </refsect2>
 
diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml
index 83de016eaa5..051bfc49c13 100644
--- a/doc/src/sgml/storage.sgml
+++ b/doc/src/sgml/storage.sgml
@@ -205,7 +205,18 @@ which can be found in <structname>pg_class</structname>.<structfield>relfilenode
 for temporary relations, the file name is of the form
 <literal>t<replaceable>BBB</replaceable>_<replaceable>FFF</replaceable></literal>, where <replaceable>BBB</replaceable>
 is the process number of the backend which created the file, and <replaceable>FFF</replaceable>
-is the filenode number.  In either case, in addition to the main file (a/k/a
+is the filenode number.  Global temporary relations have no physical storage until
+they are used by a backend, at which point their filenode number can be found using
+<link linkend="pg-gtr-info"><function>pg_gtr_info()</function></link>.
+The initial value of a global temporary relation's filenode number is equal to
+<structname>pg_class</structname>.<structfield>relfilenode</structfield>,
+but as noted below, certain operations may cause the filenode to change,
+and this change is not reflected in <structname>pg_class</structname> for
+global temporary relations.
+</para>
+
+<para>
+In each case, in addition to the main file (a/k/a
 main fork), each table and index has a <firstterm>free space map</firstterm> (see <xref
 linkend="storage-fsm"/>), which stores information about free space available in
 the relation.  The free space map is stored in a file named with the filenode
@@ -221,7 +232,8 @@ with the suffix <literal>_init</literal> (see <xref linkend="storage-init"/>).
 <para>
 Note that while a table's filenode often matches its OID, this is
 <emphasis>not</emphasis> necessarily the case; some operations, like
-<command>TRUNCATE</command>, <command>REINDEX</command>, <command>CLUSTER</command> and some forms
+<command>TRUNCATE</command>, <command>REINDEX</command>, <command>CLUSTER</command>,
+<command>REPACK</command>, <command>VACUUM FULL</command>, and some forms
 of <command>ALTER TABLE</command>, can change the filenode while preserving the OID.
 Avoid assuming that filenode and table OID are the same.
 Also, for certain system catalogs including <structname>pg_class</structname> itself,
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index 6adb760b54f..e9145073fc1 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -508,7 +508,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
@@ -551,7 +551,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 aca92507ebd..7295d2bb952 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"
@@ -2252,6 +2253,9 @@ StartTransaction(void)
 	 */
 	s->state = TRANS_INPROGRESS;
 
+	/* Process any invalidated global temporary relations */
+	ProcessInvalidatedGlobalTempRelations();
+
 	/* Schedule transaction timeout */
 	if (TransactionTimeout > 0)
 		enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
@@ -2345,6 +2349,14 @@ CommitTransaction(void)
 	/* Shut down the deferred-trigger manager */
 	AfterTriggerEndXact(true);
 
+	/*
+	 * Process any invalidated global temporary relations, dealing with any
+	 * that were dropped by other backends.  This needs to be done before any
+	 * ON COMMIT handling, so that we don't try to perform ON COMMIT actions
+	 * on deleted global temporary tables.
+	 */
+	ProcessInvalidatedGlobalTempRelations();
+
 	/*
 	 * Let ON COMMIT management do its thing (must happen after closing
 	 * cursors, to avoid dangling-reference problems)
@@ -2461,6 +2473,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();
 
@@ -2608,6 +2623,14 @@ PrepareTransaction(void)
 	/* Shut down the deferred-trigger manager */
 	AfterTriggerEndXact(true);
 
+	/*
+	 * Process any invalidated global temporary relations, dealing with any
+	 * that were dropped by other backends.  This needs to be done before any
+	 * ON COMMIT handling, so that we don't try to perform ON COMMIT actions
+	 * on deleted global temporary tables.
+	 */
+	ProcessInvalidatedGlobalTempRelations();
+
 	/*
 	 * Let ON COMMIT management do its thing (must happen after closing
 	 * cursors, to avoid dangling-reference problems)
@@ -2770,6 +2793,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();
 
@@ -3020,6 +3046,7 @@ AbortTransaction(void)
 		AtEOXact_Aio(false);
 		AtEOXact_Buffers(false);
 		AtEOXact_RelationCache(false);
+		AtEOXact_GlobalTempRelation(false);
 		AtEOXact_TypeCache();
 		AtEOXact_Inval(false);
 		AtEOXact_MultiXact();
@@ -3125,14 +3152,15 @@ StartTransactionCommand(void)
 
 			/*
 			 * We are somewhere in a transaction block or subtransaction and
-			 * about to start a new command.  For now we do nothing, but
-			 * someday we may do command-local resource initialization. (Note
-			 * that any needed CommandCounterIncrement was done by the
-			 * previous CommitTransactionCommand.)
+			 * about to start a new command.  Check for shared-cache-inval
+			 * messages and process any invalidated global temporary
+			 * relations, as we did at the start of the transaction.
 			 */
 		case TBLOCK_INPROGRESS:
 		case TBLOCK_IMPLICIT_INPROGRESS:
 		case TBLOCK_SUBINPROGRESS:
+			AcceptInvalidationMessages();
+			ProcessInvalidatedGlobalTempRelations();
 			break;
 
 			/*
@@ -5213,6 +5241,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 +5429,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 cf9b88b3e25..d99bb724b83 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..60078952a16
--- /dev/null
+++ b/src/backend/catalog/global_temp.c
@@ -0,0 +1,1583 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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 backend is dropped by another
+ * backend, all local storage created for the relation in this backend is
+ * deleted.
+ *
+ * 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 "funcapi.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/fmgrprotos.h"
+#include "utils/memutils.h"
+#include "utils/syscache.h"
+#include "utils/tuplestore.h"
+
+/*
+ * GtrInfoHistory
+ *
+ *	Linked list recording the history of edits made to the information held
+ *	about a global temporary relation in a transaction.  When any of the
+ *	relation's info is edited in a transaction or subtransaction,  the old
+ *	values are copied and added to the end of the linked list, allowing the
+ *	changes to be restored on rollback or subrollback.  The head of the list
+ *	reflects the current values.
+ */
+typedef struct GtrInfoHistory
+{
+	GtrInfo		info;			/* the relation's session-local info */
+	SubTransactionId subid;		/* subxact ID in which it was edited */
+	struct GtrInfoHistory *prev;	/* previous version, for (sub)rollback */
+} GtrInfoHistory;
+
+/*
+ * 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;
+
+#define FIND_LOCAL_STORAGE_ENTRY(rlocator) \
+	(gtr_local_storage == NULL ? NULL : \
+	 hash_search(gtr_local_storage, &(rlocator), HASH_FIND, NULL))
+
+/*
+ * 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 */
+	GtrInfoHistory history;		/* history of rel's session-local info */
+	SubTransactionId started_subid; /* usage started in current xact */
+	SubTransactionId stopped_subid; /* usage ended with another subid set */
+} GtrUsageEntry;
+
+static HTAB *gtr_local_usage;
+
+#define FIND_LOCAL_USAGE_ENTRY(relid) \
+	(gtr_local_usage == NULL ? NULL : \
+	 hash_search(gtr_local_usage, &(relid), HASH_FIND, NULL))
+
+/*
+ * 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:
+ *
+ *	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 another backend (excludes locally dropped relations).
+ *
+ *	processed_dropped_subid
+ *		Subtransaction ID in which we processed global temporary relations
+ *		dropped by other backends.
+ */
+static List *gtrs_invalidated = NIL;
+static List *gtrs_dropped = NIL;
+static SubTransactionId processed_dropped_subid = InvalidSubTransactionId;
+
+/*
+ * 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 **) &gtr_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.
+ *
+ *	NOTE: Storage is deleted non-transactionally, and cannot be rolled back.
+ *	This is fine for an exit callback, but not for any other purposes.
+ */
+static void
+gtr_delete_all_storage_on_exit(int code, Datum arg)
+{
+	ProcNumber	backend;
+	HASH_SEQ_STATUS status;
+	GtrStorageEntry *entry;
+
+	/* Loop over all storage created and delete it */
+	backend = ProcNumberForTempRelations();
+	hash_seq_init(&status, gtr_local_storage);
+	while ((entry = hash_seq_search(&status)) != NULL)
+	{
+		SMgrRelation srel;
+
+		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 was 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 the storage record 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 InvalidSubTransactionId.
+	 */
+	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 the storage record 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);
+			return;
+		}
+	}
+
+	/* 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;
+
+	/* 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)
+	{
+		GtrSharedUsageKey key;
+		GtrSharedUsageEntry *shared_entry;
+
+		/*
+		 * Remove the local usage entry.  This might seem unnecessary on exit,
+		 * but it is possible for gtr_remove_usage() to run after this, so the
+		 * local and shared usage entries do need to be kept in sync.
+		 */
+		(void) hash_search(gtr_local_usage,
+						   &local_entry->relid, HASH_REMOVE, 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 == NULL)
+			continue;			/* should be impossible, but tolerate it */
+
+		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);
+		}
+	}
+}
+
+/*
+ * 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)
+{
+	/* Local usage table */
+	if (gtr_local_usage == NULL)
+	{
+		HASHCTL		ctl;
+
+		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)
+	{
+		MemoryContext oldcontext;
+
+		/* 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,
+											 &gtr_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,
+											 &gtr_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.
+ *
+ *	Returns the new or existing local usage entry, and sets *found to true, if
+ *	an existing entry was found.
+ */
+static GtrUsageEntry *
+gtr_record_usage(Oid relid, bool *found)
+{
+	GtrUsageEntry *local_entry;
+	GtrSharedUsageKey key;
+	GtrSharedUsageEntry *shared_entry;
+	bool		shared_entry_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 local_entry;		/* 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_extended(gtr_shared_usage,
+												  &key, &shared_entry_found,
+												  DSHASH_INSERT_NO_OOM);
+	if (shared_entry == NULL)
+	{
+		/* Remove the local usage entry, so the hash tables remain in sync */
+		hash_search(gtr_local_usage, &relid, HASH_REMOVE, NULL);
+		ereport(ERROR,
+				errcode(ERRCODE_OUT_OF_MEMORY),
+				errmsg("out of memory"),
+				errdetail("Could not insert global temporary table usage entry into shared hash table."));
+	}
+
+	if (shared_entry_found)
+		shared_entry->usage_count++;
+	else
+		shared_entry->usage_count = 1;
+
+	dshash_release_lock(gtr_shared_usage, shared_entry);
+
+	return local_entry;
+}
+
+/*
+ * gtr_remove_usage
+ *
+ *	Remove our usage records for a global temporary relation that we're no
+ *	longer using.
+ *
+ *	Note: This is intentionally idempotent --- it does nothing if we have
+ *	already removed the relation's usage records.
+ */
+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 == NULL)
+		return;					/* should be impossible, but tolerate it */
+
+	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 the 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 InvalidSubTransactionId.
+	 */
+	if ((isCommit && entry->stopped_subid != InvalidSubTransactionId) ||
+		(!isCommit && entry->started_subid != InvalidSubTransactionId))
+	{
+		gtr_remove_usage(entry->relid);
+		return;
+	}
+	else
+	{
+		entry->started_subid = InvalidSubTransactionId;
+		entry->stopped_subid = InvalidSubTransactionId;
+	}
+
+	/*
+	 * Was the entry's relation information edited in this transaction?
+	 *
+	 * On commit, reset the subid, marking it as no longer belonging to a
+	 * transaction, and discard any previous copy of the relation information
+	 * that was saved in case of rollback.  On rollback, restore the saved
+	 * copy, reflecting the state of the relation prior to this transaction,
+	 * which must exist, otherwise we would have removed the entry above.
+	 */
+	if (entry->history.subid != InvalidSubTransactionId)
+	{
+		GtrInfoHistory *prev = entry->history.prev;
+
+		/*
+		 * If there's a saved copy, it should be the version that existed
+		 * prior to this transaction.
+		 */
+		Assert(prev == NULL ||
+			   (prev->subid == InvalidSubTransactionId &&
+				prev->prev == NULL));
+
+		if (isCommit)
+		{
+			entry->history.subid = InvalidSubTransactionId;
+			entry->history.prev = NULL;
+		}
+		else
+		{
+			Assert(prev != NULL);
+			entry->history = *prev;
+		}
+	}
+}
+
+/*
+ * AtEOSubXact_UsageCleanup
+ *
+ *	Clean up the 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);
+			return;
+		}
+	}
+
+	/* Update the usage stopped subid */
+	if (entry->stopped_subid == mySubid)
+	{
+		if (isCommit)
+			entry->stopped_subid = parentSubid;
+		else
+			entry->stopped_subid = InvalidSubTransactionId;
+	}
+
+	/*
+	 * Was the entry's relation information edited in the current
+	 * subtransaction?
+	 *
+	 * On subcommit, mark it as edited in the parent, instead, and discard any
+	 * previous copy of the relation information that was saved in case of
+	 * subrollback, if it was for the parent subtransaction.  On subrollback,
+	 * restore the saved copy of the relation information from the parent
+	 * subtransaction (or possibly a lower level), which must exist, otherwise
+	 * we would have removed the entry above.
+	 */
+	if (entry->history.subid == mySubid)
+	{
+		GtrInfoHistory *prev = entry->history.prev;
+
+		/*
+		 * If there's a saved copy, it should be a version from the parent
+		 * subtransaction, or a lower level.
+		 */
+		Assert(prev == NULL || prev->subid <= parentSubid);
+
+		if (isCommit)
+		{
+			entry->history.subid = parentSubid;
+			if (prev != NULL && prev->subid == parentSubid)
+				entry->history.prev = prev->prev;
+		}
+		else
+		{
+			Assert(prev != NULL);
+			entry->history = *prev;
+		}
+	}
+}
+
+/*
+ * 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;
+
+	if (create)
+	{
+		bool		found;
+		SMgrRelation srel;
+
+		/* 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 = FIND_LOCAL_STORAGE_ENTRY(rlocator);
+		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, in that 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
+ *	relcache entry 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 = FIND_LOCAL_STORAGE_ENTRY(rlocator);
+	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.
+ *
+ *	NB: this processing must be idempotent, because it is called both when
+ *	opening a global temporary relation for the first time, and after a
+ *	relcache invalidation.  The relation may have been created in this
+ *	backend, or in some other backend.  Thus, it may or may not already have
+ *	storage and/or usage records (the existence of one does not imply the
+ *	other).
+ */
+void
+InitGlobalTempRelation(Relation relation)
+{
+	/*
+	 * Cannot create storage during parallel operation.  Checks in the planner
+	 * should prevent this happening directly from core code during query
+	 * execution, but some SQL-callable functions, such as pg_table_size(),
+	 * may open global temporary relations from parallel workers.  In that
+	 * case, if the relation hasn't already been initialized, we leave it
+	 * uninitialized, with no storage or usage records, and error out if such
+	 * a function actually attempts to read from the relation.
+	 */
+	if (IsInParallelMode() || IsParallelWorker())
+		return;
+
+	/*
+	 * Create storage for the relation, if it has none.  Relations created in
+	 * this backend will already have storage, but relations created in other
+	 * backends won't, when we see them for the first time.
+	 */
+	if (RELKIND_HAS_STORAGE(relation->rd_rel->relkind) &&
+		FIND_LOCAL_STORAGE_ENTRY(relation->rd_locator) == 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 relation's ON COMMIT action, if it's DELETE ROWS (may
+		 * be NONE, PRESERVE ROWS, or DELETE ROWS, but mustn't be DROP).
+		 */
+		Assert(relation->rd_rel->reloncommit == RELONCOMMIT_NONE ||
+			   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);
+	}
+
+	/* Track our use of the relation, if we haven't already done so */
+	TrackGlobalTempRelation(relation);
+}
+
+/*
+ * TrackGlobalTempRelation
+ *
+ *	Track our use of a global temporary relation, if we haven't already done
+ *	so.
+ *
+ *	NB: this processing must be idempotent, because it is called both when a
+ *	global temporary relation is created in this session, and when one that
+ *	was created by some other backend is opened for the first time, as well as
+ *	after a relcache invalidation.
+ */
+void
+TrackGlobalTempRelation(Relation relation)
+{
+	GtrUsageEntry *entry;
+	bool		found;
+
+	/* Record our use of the relation, if we haven't done so already */
+	entry = gtr_record_usage(relation->rd_id, &found);
+
+	/*
+	 * For a new entry, fill out the session-local relation information, with
+	 * initial values taken from the pg_class tuple.
+	 */
+	if (!found)
+	{
+		COPY_PG_CLASS_GTR_INFO(relation->rd_rel, &entry->history.info);
+
+		entry->history.subid = GetCurrentSubTransactionId();
+		entry->history.prev = NULL;
+	}
+}
+
+/*
+ * ForgetGlobalTempRelation
+ *
+ *	Forget our use of a global temporary relation that we have dropped.
+ */
+void
+ForgetGlobalTempRelation(Oid relid)
+{
+	GtrUsageEntry *entry;
+
+	Assert(gtr_local_usage != NULL);
+
+	/*
+	 * Mark the relation's usage as ending in the current subtransaction, and
+	 * flag it for eoxact cleanup.
+	 */
+	entry = FIND_LOCAL_USAGE_ENTRY(relid);
+	Assert(entry != NULL && entry->stopped_subid == InvalidSubTransactionId);
+
+	entry->stopped_subid = GetCurrentSubTransactionId();
+	EOXactUsageListAdd(relid);
+}
+
+/*
+ * InvalidateGlobalTempRelation
+ *
+ *	Accept 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, and process them later in
+ *	ProcessInvalidatedGlobalTempRelations().
+ *
+ *	For a whole-relcache invalidation, RelationCacheInvalidate() will invoke
+ *	this with relid = InvalidOid.
+ */
+void
+InvalidateGlobalTempRelation(Oid relid)
+{
+	MemoryContext oldcontext;
+
+	/* 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 (FIND_LOCAL_USAGE_ENTRY(relid) != NULL)
+			gtrs_invalidated = list_append_unique_oid(gtrs_invalidated, relid);
+	}
+	else
+	{
+		HASH_SEQ_STATUS status;
+		GtrUsageEntry *entry;
+
+		/* 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);
+}
+
+/*
+ * ProcessInvalidatedGlobalTempRelations
+ *
+ *	Process any invalidated global temporary relations, dealing with any that
+ *	have been dropped by other backends.  Global temporary relations dropped by
+ *	this backend need no additional processing, and are ignored.
+ */
+void
+ProcessInvalidatedGlobalTempRelations(void)
+{
+	/*
+	 * Scan the list of invalidated global temporary relations for any more
+	 * relations dropped by other backends (may already have found some in a
+	 * prior invocation).
+	 *
+	 * As we scan gtrs_invalidated, more invalidation messages may arrive and
+	 * be added to the end of the list, so we need to be prepared for the list
+	 * growing as we traverse it.
+	 */
+	if (gtrs_invalidated)
+	{
+		MemoryContext oldcontext;
+
+		oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+
+		for (int i = 0; i < list_length(gtrs_invalidated); i++)
+		{
+			Oid			relid = list_nth_oid(gtrs_invalidated, i);
+			GtrUsageEntry *entry;
+
+			/* Ignore relations we've already found */
+			if (list_member_oid(gtrs_dropped, relid))
+				continue;
+
+			/* Ignore relations we've already forgotten (dropped by us) */
+			entry = FIND_LOCAL_USAGE_ENTRY(relid);
+			if (entry == NULL || entry->stopped_subid != InvalidSubTransactionId)
+				continue;
+
+			/* Ignore relations that still exist */
+			if (SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+				continue;
+
+			/* Relation dropped by another backend; add it to the list */
+			gtrs_dropped = lappend_oid(gtrs_dropped, relid);
+
+			/*
+			 * Clear processed_dropped_subid; we have no longer processed all
+			 * the dropped relations.
+			 */
+			processed_dropped_subid = InvalidSubTransactionId;
+		}
+
+		/* All invalidation messages processed; clear the list */
+		list_free(gtrs_invalidated);
+		gtrs_invalidated = NIL;
+
+		MemoryContextSwitchTo(oldcontext);
+	}
+
+	/*
+	 * Process any dropped relations, if we haven't done so already.  If the
+	 * (sub)transaction is rolled back, this needs to be repeated, so we don't
+	 * clear gtrs_dropped here (it is only cleared upon successful commit),
+	 * but we do set processed_dropped_subid, so that we don't needlessly
+	 * repeat this later in the same transaction.
+	 */
+	if (gtrs_dropped && processed_dropped_subid == InvalidSubTransactionId)
+	{
+		/*
+		 * Delete and forget locally-created storage for dropped relations.
+		 * This is done non-transactionally, since gtrs_dropped contains only
+		 * relations dropped by other backends in committed transactions, so
+		 * this is never rolled back.
+		 */
+		if (gtr_local_storage != NULL)
+		{
+			ProcNumber	backend;
+			HASH_SEQ_STATUS status;
+			GtrStorageEntry *entry;
+
+			backend = ProcNumberForTempRelations();
+			hash_seq_init(&status, gtr_local_storage);
+			while ((entry = hash_seq_search(&status)) != NULL)
+			{
+				if (list_member_oid(gtrs_dropped, entry->relid))
+				{
+					SMgrRelation srel;
+
+					srel = smgropen(entry->rlocator, backend);
+					smgrdounlinkall(&srel, 1, false);
+					smgrclose(srel);
+
+					(void) hash_search(gtr_local_storage, &entry->rlocator,
+									   HASH_REMOVE, NULL);
+				}
+			}
+		}
+
+		/*
+		 * Remove all usage records and forget any ON COMMIT actions for the
+		 * dropped relations.  The former is non-transactional, but the latter
+		 * may be undone by a (sub)rollback.
+		 */
+		foreach_oid(relid, gtrs_dropped)
+		{
+			gtr_remove_usage(relid);
+			remove_on_commit_action(relid);
+		}
+
+		/* All dropped relations have been processed, as of this subxact */
+		processed_dropped_subid = GetCurrentSubTransactionId();
+	}
+}
+
+/*
+ * 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 = FIND_LOCAL_STORAGE_ENTRY(eoxact_storage_list[i]);
+			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 = FIND_LOCAL_USAGE_ENTRY(eoxact_usage_list[i]);
+			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;
+
+	/*
+	 * On commit, clear gtrs_dropped.  Otherwise keep it, so that dropped
+	 * relations are processed in the next transaction.
+	 */
+	if (gtrs_dropped && isCommit)
+	{
+		list_free(gtrs_dropped);
+		gtrs_dropped = NIL;
+	}
+	processed_dropped_subid = InvalidSubTransactionId;
+}
+
+/*
+ * 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 = FIND_LOCAL_STORAGE_ENTRY(eoxact_storage_list[i]);
+			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 = FIND_LOCAL_USAGE_ENTRY(eoxact_usage_list[i]);
+			if (usage_entry)
+				AtEOSubXact_UsageCleanup(usage_entry, isCommit, mySubid,
+										 parentSubid);
+		}
+	}
+
+	/* Update processed_dropped_subid */
+	if (processed_dropped_subid == mySubid)
+	{
+		if (isCommit)
+			processed_dropped_subid = parentSubid;
+		else
+			processed_dropped_subid = InvalidSubTransactionId;
+	}
+
+	/* Don't reset the lists; we still need more cleanup later */
+}
+
+/*
+ * IsGlobalTempRelationInUse
+ *
+ *	Test if the specified global temporary relation is being used by this
+ *	backend.  Note: this doesn't bother testing if it has been dropped.
+ */
+bool
+IsGlobalTempRelationInUse(Oid relid)
+{
+	return (FIND_LOCAL_USAGE_ENTRY(relid) != NULL);
+}
+
+/*
+ * 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);
+}
+
+/*
+ * GetAllGlobalTempRelationsInUse
+ *
+ *	Returns a list of OIDs of all global temporary relations in use (by any
+ *	backend, including us) in the specified database (or all databases, if
+ *	dbid is InvalidOid).
+ *
+ *	Note: The result may be almost immediately out-dated.
+ */
+List *
+GetAllGlobalTempRelationsInUse(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;
+}
+
+/*
+ * GetGlobalTempRelationInfo
+ *
+ *	Get the session-local information held about a global temporary relation
+ *	used in the current session.  Returns NULL if the relation is not a global
+ *	temporary relation, or it has been dropped, or it has not been used in the
+ *	current session.
+ */
+GtrInfo *
+GetGlobalTempRelationInfo(Oid relid)
+{
+	GtrUsageEntry *entry = FIND_LOCAL_USAGE_ENTRY(relid);
+
+	/* Reject unused and dropped global temporary relations */
+	if (entry == NULL || entry->stopped_subid != InvalidSubTransactionId)
+		return NULL;
+
+	/* Return its current session-local information */
+	return &entry->history.info;
+}
+
+/*
+ * GetGlobalTempRelationInfoForUpdate
+ *
+ *	Returns an updatable copy of the session-local information held for a
+ *	global temporary relation used in the current session.  The return value
+ *	is guaranteed to be non-NULL (it is an error to call this for anything
+ *	other than an in-use global temporary relation).
+ *
+ *	The caller may directly edit any fields of the returned struct.  Any edits
+ *	made will be automatically saved/reverted on (sub)commit/rollback.
+ */
+GtrInfo *
+GetGlobalTempRelationInfoForUpdate(Oid relid)
+{
+	SubTransactionId mySubid = GetCurrentSubTransactionId();
+	GtrUsageEntry *entry = FIND_LOCAL_USAGE_ENTRY(relid);
+
+	/* Must have a usage entry, and must not have been dropped */
+	if (entry == NULL)
+		elog(ERROR, "cache lookup failed for global temp relation %u", relid);
+	if (entry->stopped_subid != InvalidSubTransactionId)
+		elog(ERROR, "global temp relation %u has been dropped", relid);
+
+	/* Is this the first time updating it in this (sub)transaction? */
+	if (entry->history.subid != mySubid)
+	{
+		MemoryContext oldcontext;
+		GtrInfoHistory *history;
+
+		/*
+		 * Save a copy of the relation's info to history, in case of
+		 * (sub)rollback, using the main transaction's memory context, so that
+		 * the copy is automatically freed at the end of the transaction.
+		 */
+		oldcontext = MemoryContextSwitchTo(TopTransactionContext);
+
+		history = palloc_object(GtrInfoHistory);
+		*history = entry->history;
+
+		entry->history.subid = mySubid;
+		entry->history.prev = history;
+
+		MemoryContextSwitchTo(oldcontext);
+
+		/* Flag the usage entry for eoxact cleanup */
+		EOXactUsageListAdd(relid);
+	}
+	return &entry->history.info;
+}
+
+/*
+ * 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 an
+ *	in-use global temporary relation, fetch the corresponding GtrInfo 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 GtrInfo, 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 = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+
+	if (HeapTupleIsValid(tuple))
+	{
+		Form_pg_class class_form = (Form_pg_class) GETSTRUCT(tuple);
+
+		if (class_form->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+		{
+			GtrInfo    *gtr_info = GetGlobalTempRelationInfo(relid);
+
+			if (gtr_info != NULL)
+				COPY_PG_CLASS_GTR_INFO(gtr_info, class_form);
+		}
+	}
+	return tuple;
+}
+
+/*
+ * pg_gtr_info
+ *
+ *	SQL-callable function to retrieve the session-local information about an
+ *	in-use global temporary relation.
+ */
+Datum
+pg_gtr_info(PG_FUNCTION_ARGS)
+{
+	Oid			relid = PG_GETARG_OID(0);
+	TupleDesc	tupdesc;
+	GtrInfo    *gtr_info;
+	Datum		values[2];
+	bool		nulls[2];
+
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	gtr_info = GetGlobalTempRelationInfo(relid);
+	if (gtr_info == NULL)
+		PG_RETURN_NULL();
+
+	values[0] = ObjectIdGetDatum(gtr_info->relfilenode);
+	values[1] = ObjectIdGetDatum(gtr_info->reltablespace);
+
+	memset(nulls, 0, sizeof(nulls));
+
+	PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
+}
+
+/*
+ * pg_gtrs_in_use
+ *
+ *	SQL-callable function to retrieve the session-local information about all
+ *	in-use global temporary relations.
+ */
+Datum
+pg_gtrs_in_use(PG_FUNCTION_ARGS)
+{
+	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+
+	InitMaterializedSRF(fcinfo, 0);
+
+	if (gtr_local_usage != NULL)
+	{
+		bool		nulls[3];
+		HASH_SEQ_STATUS status;
+		GtrUsageEntry *entry;
+
+		/* All values are non-NULL for all rows */
+		memset(nulls, 0, sizeof(nulls));
+
+		/* Return (relid, gtr_info) for each relation */
+		hash_seq_init(&status, gtr_local_usage);
+		while ((entry = hash_seq_search(&status)) != NULL)
+		{
+			GtrInfo    *gtr_info = &entry->history.info;
+			Datum		values[3];
+
+			/* Ignore dropped relations */
+			if (entry->stopped_subid != InvalidSubTransactionId)
+				continue;
+
+			values[0] = ObjectIdGetDatum(entry->relid);
+			values[1] = ObjectIdGetDatum(gtr_info->relfilenode);
+			values[2] = ObjectIdGetDatum(gtr_info->reltablespace);
+
+			tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc,
+								 values, nulls);
+		}
+	}
+	return (Datum) 0;
+}
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 78463352d37..ae40e0bee40 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"
@@ -390,7 +391,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);
 	}
@@ -399,8 +401,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);
 
@@ -993,6 +1000,10 @@ InsertPgClassTuple(Relation pg_class_desc,
 	CatalogTupleInsert(pg_class_desc, tup);
 
 	heap_freetuple(tup);
+
+	/* If it's a global temporary relation, track our use of it */
+	if (RELATION_IS_GLOBAL_TEMP(new_rel_desc))
+		TrackGlobalTempRelation(new_rel_desc);
 }
 
 /* --------------------------------
@@ -1600,6 +1611,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);
@@ -1607,6 +1619,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);
@@ -1614,6 +1627,10 @@ DeleteRelationTuple(Oid relid)
 	ReleaseSysCache(tup);
 
 	table_close(pg_class_desc, RowExclusiveLock);
+
+	/* If it's a global temporary relation, forget our use of it */
+	if (relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+		ForgetGlobalTempRelation(relid);
 }
 
 /*
diff --git a/src/backend/catalog/information_schema.sql b/src/backend/catalog/information_schema.sql
index 49adf66ba9b..f035476f06d 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 0647a198dea..392b24580c0 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 6b752c4c738..f2b4d641453 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -110,7 +110,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..62fc44b669f 100644
--- a/src/backend/catalog/storage.c
+++ b/src/backend/catalog/storage.c
@@ -24,6 +24,7 @@
 #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,7 +120,7 @@ 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;
@@ -128,12 +129,21 @@ RelationCreateStorage(RelFileLocator rlocator, char relpersistence,
 
 	Assert(!IsInParallelMode());	/* couldn't update pendingSyncHash */
 
+	/* relid is only needed for global temporary relations */
+	Assert(OidIsValid(relid) || relpersistence != RELPERSISTENCE_GLOBAL_TEMP);
+
 	switch (relpersistence)
 	{
 		case RELPERSISTENCE_TEMP:
 			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 +218,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 7e3fc59eafd..c19871db5ae 100644
--- a/src/backend/commands/dbcommands.c
+++ b/src/backend/commands/dbcommands.c
@@ -411,11 +411,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;
 
 	/*
@@ -445,7 +447,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/indexcmds.c b/src/backend/commands/indexcmds.c
index 5a0312fe772..baea283aed1 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -25,6 +25,7 @@
 #include "access/tableam.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
+#include "catalog/global_temp.h"
 #include "catalog/index.h"
 #include "catalog/indexing.h"
 #include "catalog/namespace.h"
@@ -3404,6 +3405,11 @@ ReindexMultipleTables(const ReindexStmt *stmt, const ReindexParams *params)
 			!isTempNamespace(classtuple->relnamespace))
 			continue;
 
+		/* Skip global temporary tables not in use */
+		if (classtuple->relpersistence == RELPERSISTENCE_GLOBAL_TEMP &&
+			!IsGlobalTempRelationInUse(relid))
+			continue;
+
 		/*
 		 * Check user/system classification.  SYSTEM processes all the
 		 * catalogs, and DATABASE processes everything that's not a catalog.
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/repack.c b/src/backend/commands/repack.c
index e0fc96558d9..ab03eafbfa9 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"
@@ -1420,11 +1421,19 @@ 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.
+	 * Normally we don't need to open the toast relation here, just lock it.
+	 * However, for a global temporary relation, we must open it to ensure
+	 * that it is properly initialized (it may not have been opened yet in
+	 * this session), so we may as well do that for all relation types.  The
+	 * lock will be held 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
@@ -1633,6 +1642,8 @@ swap_relation_files(Oid r1, Oid r2, bool target_is_pg_class,
 				reltup2;
 	Form_pg_class relform1,
 				relform2;
+	GtrInfo    *gtr_info1,
+			   *gtr_info2;
 	RelFileNumber relfilenumber1,
 				relfilenumber2;
 	RelFileNumber swaptemp;
@@ -1640,7 +1651,10 @@ 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 if they're global
+	 * temporary relations, writable copies of the corresponding GtrInfos.
+	 */
 	relRelation = table_open(RelationRelationId, RowExclusiveLock);
 
 	reltup1 = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(r1));
@@ -1648,13 +1662,28 @@ swap_relation_files(Oid r1, Oid r2, bool target_is_pg_class,
 		elog(ERROR, "cache lookup failed for relation %u", r1);
 	relform1 = (Form_pg_class) GETSTRUCT(reltup1);
 
+	if (relform1->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+		gtr_info1 = GetGlobalTempRelationInfoForUpdate(r1);
+	else
+		gtr_info1 = NULL;
+
 	reltup2 = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(r2));
 	if (!HeapTupleIsValid(reltup2))
 		elog(ERROR, "cache lookup failed for relation %u", r2);
 	relform2 = (Form_pg_class) GETSTRUCT(reltup2);
 
-	relfilenumber1 = relform1->relfilenode;
-	relfilenumber2 = relform2->relfilenode;
+	if (relform2->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+		gtr_info2 = GetGlobalTempRelationInfoForUpdate(r2);
+	else
+		gtr_info2 = NULL;
+
+	/* If r1 is global temporary, so should r2 be, and vice versa */
+	if ((gtr_info1 == NULL) != (gtr_info2 == NULL))
+		elog(ERROR, "relpersistence mismatch: cannot swap global temporary relation with a relation that is not global temporary");
+
+	/* Global temporary relations may have session-local relfilenode values */
+	relfilenumber1 = GetEffective_relfilenode(relform1, gtr_info1);
+	relfilenumber2 = GetEffective_relfilenode(relform2, gtr_info2);
 	relam1 = relform1->relam;
 	relam2 = relform2->relam;
 
@@ -1663,17 +1692,19 @@ swap_relation_files(Oid r1, Oid r2, bool target_is_pg_class,
 	{
 		/*
 		 * Normal non-mapped relations: swap relfilenumbers, reltablespaces,
-		 * relpersistence
+		 * relpersistence, etc.  For global temporary relations, relfilenode
+		 * and reltablespace need special handling.
 		 */
 		Assert(!target_is_pg_class);
 
-		swaptemp = relform1->relfilenode;
-		relform1->relfilenode = relform2->relfilenode;
-		relform2->relfilenode = swaptemp;
+		SetEffective_relfilenode(relform1, gtr_info1, relfilenumber2);
+		SetEffective_relfilenode(relform2, gtr_info2, relfilenumber1);
 
-		swaptemp = relform1->reltablespace;
-		relform1->reltablespace = relform2->reltablespace;
-		relform2->reltablespace = swaptemp;
+		swaptemp = GetEffective_reltablespace(relform1, gtr_info1);
+		SetEffective_reltablespace(relform1, gtr_info1,
+								   GetEffective_reltablespace(relform2,
+															  gtr_info2));
+		SetEffective_reltablespace(relform2, gtr_info2, swaptemp);
 
 		swaptemp = relform1->relam;
 		relform1->relam = relform2->relam;
@@ -1761,6 +1792,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 temporary 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);
 	}
@@ -2271,6 +2311,11 @@ get_tables_to_repack(RepackCommand cmd, bool usingindex, MemoryContext permcxt)
 				!isTempOrTempToastNamespace(relnamespace))
 				continue;
 
+			/* Skip global temporary relations not in use */
+			if (relpersistence == RELPERSISTENCE_GLOBAL_TEMP &&
+				!IsGlobalTempRelationInUse(index->indrelid))
+				continue;
+
 			/* noisily skip rels which the user can't process */
 			if (!repack_is_permitted_for_relation(cmd, index->indrelid,
 												  GetUserId(), false))
@@ -2308,6 +2353,11 @@ get_tables_to_repack(RepackCommand cmd, bool usingindex, MemoryContext permcxt)
 				!isTempOrTempToastNamespace(class->relnamespace))
 				continue;
 
+			/* Skip global temporary relations not in use */
+			if (class->relpersistence == RELPERSISTENCE_GLOBAL_TEMP &&
+				!IsGlobalTempRelationInUse(class->oid))
+				continue;
+
 			/* noisily skip rels which the user can't process */
 			if (!repack_is_permitted_for_relation(cmd, class->oid,
 												  GetUserId(), false))
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 22a61dca65d..3c45ccf362c 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -1018,14 +1018,25 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
 			{
 				Oid			relid;
 				char		relkind;
+				char		relpersistence;
 				RangeVar   *rv = pubrelinfo->rv;
 
 				relid = RangeVarGetRelid(rv, AccessShareLock, false);
 				relkind = get_rel_relkind(relid);
+				relpersistence = get_rel_persistence(relid);
 
 				/* Check for supported relkind. */
 				CheckSubscriptionRelkind(relkind, pubrelinfo->relkind,
 										 rv->schemaname, rv->relname);
+
+				/* Local relation must not be global temporary */
+				if (relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+					ereport(ERROR,
+							errcode(ERRCODE_WRONG_OBJECT_TYPE),
+							errmsg("cannot use relation \"%s.%s\" as logical replication target",
+								   rv->schemaname, rv->relname),
+							errdetail("This operation is not supported for global temporary relations."));
+
 				has_tables |= (relkind != RELKIND_SEQUENCE);
 				AddSubscriptionRelState(subid, relid, relation_state,
 										InvalidXLogRecPtr, true);
@@ -1219,14 +1230,24 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data,
 			RangeVar   *rv = pubrelinfo->rv;
 			Oid			relid;
 			char		relkind;
+			char		relpersistence;
 
 			relid = RangeVarGetRelid(rv, AccessShareLock, false);
 			relkind = get_rel_relkind(relid);
+			relpersistence = get_rel_persistence(relid);
 
 			/* Check for supported relkind. */
 			CheckSubscriptionRelkind(relkind, pubrelinfo->relkind,
 									 rv->schemaname, rv->relname);
 
+			/* Local relation must not be global temporary */
+			if (relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+				ereport(ERROR,
+						errcode(ERRCODE_WRONG_OBJECT_TYPE),
+						errmsg("cannot use relation \"%s.%s\" as logical replication target",
+							   rv->schemaname, rv->relname),
+						errdetail("This operation is not supported for global temporary relations."));
+
 			pubrel_local_oids[off++] = relid;
 
 			if (!bsearch(&relid, subrel_local_oids,
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 2f073ddb84a..b4671c37280 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"
@@ -173,6 +174,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.
@@ -814,11 +816,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)
 	{
@@ -1680,7 +1688,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);
@@ -2510,6 +2519,133 @@ storage_name(char c)
 	}
 }
 
+/*
+ * check_child_persistence
+ *		Check whether a child relation with the specified relperistence is
+ *		allowed to inherit from or be a partition of the specified parent
+ *		relation.
+ */
+static void
+check_child_persistence(Relation parent, char child_persistence,
+						bool is_partition, bool is_attach)
+{
+	switch (parent->rd_rel->relpersistence)
+	{
+		case RELPERSISTENCE_TEMP:
+
+			/*
+			 * Children of local temporary tables must be local temporary,
+			 * since the parent will be dropped when the session ends, which
+			 * would leave any other type of child hanging.
+			 */
+			if (child_persistence == RELPERSISTENCE_GLOBAL_TEMP)
+			{
+				if (is_partition && is_attach)
+					ereport(ERROR,
+							errcode(ERRCODE_WRONG_OBJECT_TYPE),
+							errmsg("cannot attach a global temporary relation as partition of local temporary relation \"%s\"",
+								   RelationGetRelationName(parent)));
+				else if (is_partition)
+					ereport(ERROR,
+							errcode(ERRCODE_WRONG_OBJECT_TYPE),
+							errmsg("cannot create a global temporary relation as partition of local temporary relation \"%s\"",
+								   RelationGetRelationName(parent)));
+				else
+					ereport(ERROR,
+							errcode(ERRCODE_WRONG_OBJECT_TYPE),
+							errmsg("global temporary relation cannot inherit from local temporary relation \"%s\"",
+								   RelationGetRelationName(parent)));
+			}
+			else if (child_persistence != RELPERSISTENCE_TEMP)
+			{
+				if (is_partition && is_attach)
+					ereport(ERROR,
+							errcode(ERRCODE_WRONG_OBJECT_TYPE),
+							errmsg("cannot attach a permanent relation as partition of local temporary relation \"%s\"",
+								   RelationGetRelationName(parent)));
+				else if (is_partition)
+					ereport(ERROR,
+							errcode(ERRCODE_WRONG_OBJECT_TYPE),
+							errmsg("cannot create a permanent relation as partition of local temporary relation \"%s\"",
+								   RelationGetRelationName(parent)));
+				else
+					ereport(ERROR,
+							errcode(ERRCODE_WRONG_OBJECT_TYPE),
+							errmsg("permanent relation cannot inherit from local temporary relation \"%s\"",
+								   RelationGetRelationName(parent)));
+			}
+			break;
+
+		case RELPERSISTENCE_GLOBAL_TEMP:
+
+			/*
+			 * Partitions of global temporary tables must be global temporary,
+			 * but inheritance children may be of any kind.
+			 */
+			if (child_persistence == RELPERSISTENCE_TEMP && is_partition)
+			{
+				if (is_attach)
+					ereport(ERROR,
+							errcode(ERRCODE_WRONG_OBJECT_TYPE),
+							errmsg("cannot attach a local temporary relation as partition of global temporary relation \"%s\"",
+								   RelationGetRelationName(parent)));
+				else
+					ereport(ERROR,
+							errcode(ERRCODE_WRONG_OBJECT_TYPE),
+							errmsg("cannot create a local temporary relation as partition of global temporary relation \"%s\"",
+								   RelationGetRelationName(parent)));
+			}
+			else if (child_persistence != RELPERSISTENCE_GLOBAL_TEMP && is_partition)
+			{
+				if (is_attach)
+					ereport(ERROR,
+							errcode(ERRCODE_WRONG_OBJECT_TYPE),
+							errmsg("cannot attach a permanent relation as partition of global temporary relation \"%s\"",
+								   RelationGetRelationName(parent)));
+				else
+					ereport(ERROR,
+							errcode(ERRCODE_WRONG_OBJECT_TYPE),
+							errmsg("cannot create a permanent relation as partition of global temporary relation \"%s\"",
+								   RelationGetRelationName(parent)));
+			}
+			break;
+
+		default:
+
+			/*
+			 * Partitions of permanent tables must be permanent, but
+			 * inheritance children may be of any kind.
+			 */
+			if (child_persistence == RELPERSISTENCE_TEMP && is_partition)
+			{
+				if (is_attach)
+					ereport(ERROR,
+							errcode(ERRCODE_WRONG_OBJECT_TYPE),
+							errmsg("cannot attach a local temporary relation as partition of permanent relation \"%s\"",
+								   RelationGetRelationName(parent)));
+				else
+					ereport(ERROR,
+							errcode(ERRCODE_WRONG_OBJECT_TYPE),
+							errmsg("cannot create a local temporary relation as partition of permanent relation \"%s\"",
+								   RelationGetRelationName(parent)));
+			}
+			else if (child_persistence == RELPERSISTENCE_GLOBAL_TEMP && is_partition)
+			{
+				if (is_attach)
+					ereport(ERROR,
+							errcode(ERRCODE_WRONG_OBJECT_TYPE),
+							errmsg("cannot attach a global temporary relation as partition of permanent relation \"%s\"",
+								   RelationGetRelationName(parent)));
+				else
+					ereport(ERROR,
+							errcode(ERRCODE_WRONG_OBJECT_TYPE),
+							errmsg("cannot create a global temporary relation as partition of permanent relation \"%s\"",
+								   RelationGetRelationName(parent)));
+			}
+			break;
+	}
+}
+
 /*----------
  * MergeAttributes
  *		Returns new schema given initial schema and superclasses.
@@ -2745,27 +2881,8 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
 					 errmsg("inherited relation \"%s\" is not a table or foreign table",
 							RelationGetRelationName(relation))));
 
-		/*
-		 * If the parent is permanent, so must be all of its partitions.  Note
-		 * that inheritance allows that case.
-		 */
-		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\"",
-							RelationGetRelationName(relation))));
-
-		/* Permanent rels cannot inherit from temporary ones */
-		if (relpersistence != RELPERSISTENCE_TEMP &&
-			relation->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
-			ereport(ERROR,
-					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-					 errmsg(!is_partition
-							? "cannot inherit from temporary relation \"%s\""
-							: "cannot create a permanent relation as partition of temporary relation \"%s\"",
-							RelationGetRelationName(relation))));
+		/* Check if it's OK to create a child with this relpersistence */
+		check_child_persistence(relation, relpersistence, is_partition, false);
 
 		/* If existing rel is temp, it must belong to this session */
 		if (RELATION_IS_OTHER_TEMP(relation))
@@ -3779,7 +3896,9 @@ CheckRelationTableSpaceMove(Relation rel, Oid newTableSpaceId)
 
 /*
  * SetRelationTableSpace
- *		Set new reltablespace and relfilenumber in pg_class entry.
+ *		Set new reltablespace and relfilenumber in pg_class (and the
+ *		relation's session-local GtrInfo struct, if it's a global
+ *		temporary relation).
  *
  * newTableSpaceId is the new tablespace for the relation, and
  * newRelFilenumber its new filenumber.  If newRelFilenumber is
@@ -3801,11 +3920,15 @@ SetRelationTableSpace(Relation rel,
 	HeapTuple	tuple;
 	ItemPointerData otid;
 	Form_pg_class rd_rel;
+	GtrInfo    *gtr_info;
 	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, a modifiable copy of its GtrInfo.
+	 */
 	pg_class = table_open(RelationRelationId, RowExclusiveLock);
 
 	tuple = SearchSysCacheLockedCopy1(RELOID, ObjectIdGetDatum(reloid));
@@ -3814,19 +3937,37 @@ SetRelationTableSpace(Relation rel,
 	otid = tuple->t_self;
 	rd_rel = (Form_pg_class) GETSTRUCT(tuple);
 
-	/* Update the pg_class row. */
-	rd_rel->reltablespace = (newTableSpaceId == MyDatabaseTableSpace) ?
-		InvalidOid : newTableSpaceId;
+	if (rd_rel->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+		gtr_info = GetGlobalTempRelationInfoForUpdate(reloid);
+	else
+		gtr_info = NULL;
+
+	/*
+	 * Update the pg_class row and gtr_info.  For a global temporary relation,
+	 * the new tablespace is intentionally set in both pg_class and gtr_info,
+	 * 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, gtr_info,
+							   newTableSpaceId == MyDatabaseTableSpace ?
+							   InvalidOid : newTableSpaceId);
 	if (RelFileNumberIsValid(newRelFilenumber))
-		rd_rel->relfilenode = newRelFilenumber;
+		SetEffective_relfilenode(rd_rel, gtr_info, newRelFilenumber);
+
 	CatalogTupleUpdate(pg_class, &otid, tuple);
 	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.  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))
+	if (!RELKIND_HAS_STORAGE(rel->rd_rel->relkind) ||
+		RELATION_IS_GLOBAL_TEMP(rel))
 		changeDependencyOnTablespace(RelationRelationId, reloid,
 									 rd_rel->reltablespace);
 
@@ -5980,6 +6121,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)
@@ -6076,11 +6229,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,
@@ -6642,6 +6806,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;
@@ -10238,11 +10403,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;
 	}
 
@@ -17834,7 +18005,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,
@@ -19522,6 +19694,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",
@@ -21111,21 +21284,8 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
 						   RelationGetRelationName(rel),
 						   RelationGetRelationName(attachrel))));
 
-	/* If the parent is permanent, so must be all of its partitions. */
-	if (rel->rd_rel->relpersistence != RELPERSISTENCE_TEMP &&
-		attachrel->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
-		ereport(ERROR,
-				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-				 errmsg("cannot attach a temporary relation as partition of permanent relation \"%s\"",
-						RelationGetRelationName(rel))));
-
-	/* Temp parent cannot have a partition that is itself not a temp */
-	if (rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP &&
-		attachrel->rd_rel->relpersistence != RELPERSISTENCE_TEMP)
-		ereport(ERROR,
-				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-				 errmsg("cannot attach a permanent relation as partition of temporary relation \"%s\"",
-						RelationGetRelationName(rel))));
+	/* Check if it's OK to attach a partition with this relpersistence */
+	check_child_persistence(rel, attachrel->rd_rel->relpersistence, true, true);
 
 	/* If the parent is temp, it must belong to this session */
 	if (RELATION_IS_OTHER_TEMP(rel))
diff --git a/src/backend/commands/tablespace.c b/src/backend/commands/tablespace.c
index e01fb2db913..1b022da6d53 100644
--- a/src/backend/commands/tablespace.c
+++ b/src/backend/commands/tablespace.c
@@ -1171,7 +1171,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/vacuum.c b/src/backend/commands/vacuum.c
index d8c2f33c615..b6205a1dd40 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -35,6 +35,7 @@
 #include "access/tableam.h"
 #include "access/transam.h"
 #include "access/xact.h"
+#include "catalog/global_temp.h"
 #include "catalog/namespace.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_inherits.h"
@@ -1088,6 +1089,11 @@ get_all_vacuum_rels(MemoryContext vac_context, int options)
 			!isTempOrTempToastNamespace(classForm->relnamespace))
 			continue;
 
+		/* Skip global temporary relations not in use */
+		if (classForm->relpersistence == RELPERSISTENCE_GLOBAL_TEMP &&
+			!IsGlobalTempRelationInUse(relid))
+			continue;
+
 		/* check permissions of relation */
 		if (!vacuum_is_permitted_for_relation(relid, classForm, options, true))
 			continue;
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 de8f29c2b57..445356701a6 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -649,6 +649,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.
@@ -676,7 +678,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/analyze.c b/src/backend/parser/analyze.c
index 08f99dff711..abc5bfa54b1 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -3195,8 +3195,8 @@ transformCreateTableAsStmt(ParseState *pstate, CreateTableAsStmt *stmt)
 		if (query_uses_temp_object(query, &temp_object))
 			ereport(ERROR,
 					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-					 errmsg("materialized views must not use temporary objects"),
-					 errdetail("This view depends on temporary %s.",
+					 errmsg("materialized views must not use local temporary objects"),
+					 errdetail("This view depends on local temporary %s.",
 							   getObjectDescription(&temp_object, false))));
 
 		/*
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 0563453fe24..08ede85e37c 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -3830,31 +3830,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; }
 		;
@@ -13440,19 +13430,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 60ebe828900..f6cdf5bf918 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2085,7 +2085,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)
 		{
@@ -2107,6 +2110,8 @@ do_autovacuum(void)
 			}
 			continue;
 		}
+		else if (classForm->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+			continue;
 
 		/* Fetch reloptions and the pgstat entry for this table */
 		relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
@@ -2177,7 +2182,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;
@@ -2261,7 +2267,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;
@@ -3710,7 +3717,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;
 
 		relopts = get_effective_relopts(tup, RelationGetDescr(rel),
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index e2b673a96b5..7798c0312b4 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -210,6 +210,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"
@@ -1718,6 +1719,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
 		{
 			/*
@@ -1741,6 +1747,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 = GetAllGlobalTempRelationsInUse(MyDatabaseId);
+
+		RelationList = list_concat(RelationList, gtrs_in_use);
+		list_free(gtrs_in_use);
+	}
+
 	return RelationList;
 }
 
diff --git a/src/backend/replication/logical/relation.c b/src/backend/replication/logical/relation.c
index 8ffd2583afb..e234e22493a 100644
--- a/src/backend/replication/logical/relation.c
+++ b/src/backend/replication/logical/relation.c
@@ -440,6 +440,14 @@ logicalrep_rel_open(LogicalRepRelId remoteid, LOCKMODE lockmode)
 								 remoterel->relkind,
 								 remoterel->nspname, remoterel->relname);
 
+		/* Local relation must not be global temporary */
+		if (RELATION_IS_GLOBAL_TEMP(entry->localrel))
+			ereport(ERROR,
+					errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					errmsg("cannot use relation \"%s.%s\" as logical replication target",
+						   remoterel->nspname, remoterel->relname),
+					errdetail("This operation is not supported for global temporary relations."));
+
 		/*
 		 * Build the mapping of local attribute numbers to remote attribute
 		 * numbers and validate that we don't miss any replicated columns as
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 5c82865a084..14b45dd4d4d 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;
@@ -1954,7 +1960,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;
@@ -1973,7 +1980,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 3d366fd1114..710d4af69e7 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -372,6 +372,7 @@ LogicalDecodingControl	"Waiting to read or update logical decoding status inform
 DataChecksumsWorker	"Waiting for data checksums worker."
 AioWorkerControl	"Waiting to update AIO worker information."
 DataChecksumTransition	"Waiting for a data checksum state transition to be written to WAL."
+GlobalTempRelControl	"Waiting to update global temporary relation information."
 
 #
 # END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE)
@@ -419,6 +420,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..da7d724726b 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -15,6 +15,7 @@
 
 #include "access/htup_details.h"
 #include "access/relation.h"
+#include "catalog/global_temp.h"
 #include "catalog/namespace.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_database.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();
 	}
 
@@ -1032,13 +1033,16 @@ 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 */
 			break;
 	}
 
-	ReleaseSysCache(tuple);
+	heap_freetuple(tuple);
 
 	path = relpathbackend(rlocator, backend, MAIN_FORKNUM);
 
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 65b2d64529a..09e1d220775 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -18,6 +18,7 @@
 #include "access/hash.h"
 #include "access/htup_details.h"
 #include "bootstrap/bootstrap.h"
+#include "catalog/global_temp.h"
 #include "catalog/namespace.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_amop.h"
@@ -2374,6 +2375,15 @@ get_rel_tablespace(Oid relid)
 		Oid			result;
 
 		result = reltup->reltablespace;
+
+		/* Global temporary relations may override reltablespace locally */
+		if (reltup->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+		{
+			GtrInfo    *gtr_info = GetGlobalTempRelationInfo(relid);
+
+			if (gtr_info != NULL)
+				result = gtr_info->reltablespace;
+		}
 		ReleaseSysCache(tp);
 		return result;
 	}
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 2931b90be33..3a04ccbc271 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"
@@ -337,6 +338,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, the returned pg_class
+ *		tuple is updated to reflect any session-local overrides from changes
+ *		made to the relation in this session.
+ *
  *		NB: the returned tuple has been copied into palloc'd storage
  *		and must eventually be freed with heap_freetuple.
  */
@@ -404,6 +409,25 @@ ScanPgRelation(Oid targetRelId, bool indexOK, bool force_non_historic)
 
 	table_close(pg_class_desc, AccessShareLock);
 
+	/*
+	 * For a global temporary relation, update the pg_class tuple with any
+	 * session-local values.
+	 */
+	if (HeapTupleIsValid(pg_class_tuple))
+	{
+		Form_pg_class pg_class_form;
+
+		pg_class_form = (Form_pg_class) GETSTRUCT(pg_class_tuple);
+
+		if (pg_class_form->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+		{
+			GtrInfo    *gtr_info = GetGlobalTempRelationInfo(targetRelId);
+
+			if (gtr_info != NULL)
+				COPY_PG_CLASS_GTR_INFO(gtr_info, pg_class_form);
+		}
+	}
+
 	return pg_class_tuple;
 }
 
@@ -1172,8 +1196,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 +1215,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);
@@ -2118,6 +2147,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
@@ -2137,7 +2174,11 @@ RelationIdGetRelation(Oid relationId)
 	 */
 	rd = RelationBuildDesc(relationId, true);
 	if (RelationIsValid(rd))
+	{
 		RelationIncrementReferenceCount(rd);
+		if (RELATION_IS_GLOBAL_TEMP(rd))
+			InitGlobalTempRelation(rd);
+	}
 	return rd;
 }
 
@@ -2211,6 +2252,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
  *
@@ -2960,6 +3016,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);
 }
 
 /*
@@ -3104,6 +3163,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
@@ -3659,6 +3721,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;
@@ -3667,6 +3730,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;
@@ -3802,6 +3870,7 @@ RelationSetNewRelfilenumber(Relation relation, char persistence)
 	ItemPointerData otid;
 	HeapTuple	tuple;
 	Form_pg_class classform;
+	GtrInfo    *gtr_info;
 	MultiXactId minmulti = InvalidMultiXactId;
 	TransactionId freezeXid = InvalidTransactionId;
 	RelFileLocator newrlocator;
@@ -3838,7 +3907,8 @@ 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, a writable copy of its GtrInfo.
 	 */
 	pg_class = table_open(RelationRelationId, RowExclusiveLock);
 
@@ -3850,6 +3920,11 @@ RelationSetNewRelfilenumber(Relation relation, char persistence)
 	otid = tuple->t_self;
 	classform = (Form_pg_class) GETSTRUCT(tuple);
 
+	if (classform->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+		gtr_info = GetGlobalTempRelationInfoForUpdate(RelationGetRelid(relation));
+	else
+		gtr_info = NULL;
+
 	/*
 	 * Schedule unlinking of the old storage at transaction commit, except
 	 * when performing a binary upgrade, when we must do it immediately.
@@ -3905,7 +3980,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
@@ -3953,8 +4029,8 @@ RelationSetNewRelfilenumber(Relation relation, char persistence)
 	}
 	else
 	{
-		/* Normal case, update the pg_class entry */
-		classform->relfilenode = newrelfilenumber;
+		/* Normal case, update the pg_class and GtrInfo */
+		SetEffective_relfilenode(classform, gtr_info, newrelfilenumber);
 
 		/* relpages etc. never change for sequences */
 		if (relation->rd_rel->relkind != RELKIND_SEQUENCE)
@@ -3977,8 +4053,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 row, GtrInfo, or relation map change visible.  This
+	 * will cause the relcache entry to get updated, too.
 	 */
 	CommandCounterIncrement();
 
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 cced5181cce..6d681f3db9e 100644
--- a/src/bin/pg_amcheck/pg_amcheck.c
+++ b/src/bin/pg_amcheck/pg_amcheck.c
@@ -858,7 +858,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);
 }
 
@@ -892,6 +893,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"),
@@ -907,6 +909,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"),
@@ -1951,8 +1954,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");
 
@@ -2021,7 +2025,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)");
 	}
@@ -2040,7 +2045,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 "
@@ -2079,7 +2085,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 388c3b9c346..26d0bc89c0c 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -3022,6 +3022,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)
@@ -7175,6 +7179,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;
@@ -7225,6 +7230,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_NONE) " AS reloncommit, ");
+
 	appendPQExpBufferStr(query,
 						 "c.relhastriggers, c.relpersistence, "
 						 "c.reloftype, "
@@ -7365,6 +7376,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");
@@ -7443,6 +7455,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);
@@ -17159,7 +17172,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);
 
@@ -17410,6 +17425,12 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 			appendPQExpBufferChar(q, ')');
 		}
 
+		/* Dump ON COMMIT action (global temporary tables only) */
+		if (tbinfo->reloncommit == RELONCOMMIT_PRESERVE_ROWS)
+			appendPQExpBufferStr(q, "\nON COMMIT PRESERVE ROWS");
+		else 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 2bbb5d5773b..49be4490dc5 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_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 1299c837063..f2ec2559920 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -4960,7 +4960,63 @@ my %tests = (
 			no_table_access_method => 1,
 			only_dump_measurement => 1,
 		},
-	});
+	},
+
+	# Global temporary tables
+	'CREATE GLOBAL TEMP TABLE regress_pg_dump_gtt' => {
+		create_sql => '
+			CREATE GLOBAL TEMP TABLE dump_test.regress_pg_dump_gtt (a text);
+			INSERT INTO dump_test.regress_pg_dump_gtt VALUES (\'data not dumped\');',
+		regexp => qr/^
+			\n\QCREATE GLOBAL TEMP TABLE dump_test.regress_pg_dump_gtt (\E\n
+			\s+\Qa text\E\n
+			\Q);\E\n/xm,
+		like => {
+			%full_runs, %dump_test_schema_runs, section_pre_data => 1,
+		},
+		unlike => {
+			exclude_dump_test_schema => 1,
+			only_dump_measurement => 1,
+		},
+	},
+
+	'CREATE GLOBAL TEMP TABLE regress_pg_dump_gtt_oc_pr ON COMMIT PRESERVE ROWS' => {
+		create_sql => '
+			CREATE GLOBAL TEMP TABLE dump_test.regress_pg_dump_gtt_oc_pr (a text)
+			  ON COMMIT PRESERVE ROWS;',
+		regexp => qr/^
+			\n\QCREATE GLOBAL TEMP TABLE dump_test.regress_pg_dump_gtt_oc_pr (\E\n
+			\s+\Qa text\E\n
+			\Q)\E\n
+			\QON COMMIT PRESERVE ROWS;\E\n/xm,
+		like => {
+			%full_runs, %dump_test_schema_runs, section_pre_data => 1,
+		},
+		unlike => {
+			exclude_dump_test_schema => 1,
+			only_dump_measurement => 1,
+		},
+	},
+
+	'CREATE GLOBAL TEMP TABLE regress_pg_dump_gtt_oc_dr ON COMMIT DELETE ROWS' => {
+		create_sql => '
+			CREATE GLOBAL TEMP TABLE dump_test.regress_pg_dump_gtt_oc_dr (a text)
+			  ON COMMIT DELETE ROWS;',
+		regexp => qr/^
+			\n\QCREATE GLOBAL TEMP TABLE dump_test.regress_pg_dump_gtt_oc_dr (\E\n
+			\s+\Qa text\E\n
+			\Q)\E\n
+			\QON COMMIT DELETE ROWS;\E\n/xm,
+		like => {
+			%full_runs, %dump_test_schema_runs, section_pre_data => 1,
+		},
+		unlike => {
+			exclude_dump_test_schema => 1,
+			only_dump_measurement => 1,
+		},
+	},
+
+);
 
 #########################################
 # Create a PG instance to test actually dumping from
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 f26343feeb0..878c4192076 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1949,13 +1949,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\""),
@@ -1993,6 +2003,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);
@@ -4108,10 +4124,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/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index b3bfe050b18..49d94e83210 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -1311,10 +1311,14 @@ static const pgsql_thing_t words_after_create[] = {
 	{"FOREIGN DATA WRAPPER", NULL, NULL, NULL},
 	{"FOREIGN TABLE", NULL, NULL, NULL},
 	{"FUNCTION", NULL, NULL, Query_for_list_of_functions},
+	{"GLOBAL", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER}, /* for CREATE GLOBAL
+																		 * TEMP TABLE ... */
 	{"GROUP", Query_for_list_of_roles},
 	{"INDEX", NULL, NULL, &Query_for_list_of_indexes},
 	{"LANGUAGE", Query_for_list_of_languages},
 	{"LARGE OBJECT", NULL, NULL, NULL, NULL, THING_NO_CREATE | THING_NO_DROP},
+	{"LOCAL", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER},	/* for CREATE LOCAL TEMP
+																		 * TABLE ... */
 	{"MATERIALIZED VIEW", NULL, NULL, &Query_for_list_of_matviews},
 	{"OPERATOR", NULL, NULL, NULL}, /* Querying for this is probably not such
 									 * a good idea. */
@@ -3787,8 +3791,19 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 
 /* CREATE TABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */
-	/* Complete "CREATE TEMP/TEMPORARY" with the possible temp objects */
-	else if (TailMatches("CREATE", "TEMP|TEMPORARY"))
+	/* Complete "CREATE GLOBAL|LOCAL" with TEMP or TEMPORARY */
+	else if (TailMatches("CREATE", "GLOBAL|LOCAL"))
+		COMPLETE_WITH("TEMP", "TEMPORARY");
+	/* Complete "CREATE GLOBAL TEMP/TEMPORARY" with TABLE */
+	else if (TailMatches("CREATE", "GLOBAL", "TEMP|TEMPORARY"))
+		COMPLETE_WITH("TABLE");
+
+	/*
+	 * Complete "CREATE [ LOCAL ] TEMP/TEMPORARY" with SEQUENCE, TABLE, or
+	 * VIEW.
+	 */
+	else if (TailMatches("CREATE", "TEMP|TEMPORARY") ||
+			 TailMatches("CREATE", "LOCAL", "TEMP|TEMPORARY"))
 		COMPLETE_WITH("SEQUENCE", "TABLE", "VIEW");
 	/* Complete "CREATE UNLOGGED" with TABLE or SEQUENCE */
 	else if (TailMatches("CREATE", "UNLOGGED"))
@@ -3804,36 +3819,47 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("FOR VALUES", "DEFAULT");
 	/* Complete CREATE TABLE <name> with '(', AS, OF or PARTITION OF */
 	else if (TailMatches("CREATE", "TABLE", MatchAny) ||
-			 TailMatches("CREATE", "TEMP|TEMPORARY|UNLOGGED", "TABLE", MatchAny))
+			 TailMatches("CREATE", "TEMP|TEMPORARY|UNLOGGED", "TABLE", MatchAny) ||
+			 TailMatches("CREATE", "GLOBAL|LOCAL", "TEMP|TEMPORARY", "TABLE", MatchAny))
 		COMPLETE_WITH("(", "AS", "OF", "PARTITION OF");
 	/* Complete CREATE TABLE <name> OF with list of composite types */
 	else if (TailMatches("CREATE", "TABLE", MatchAny, "OF") ||
-			 TailMatches("CREATE", "TEMP|TEMPORARY|UNLOGGED", "TABLE", MatchAny, "OF"))
+			 TailMatches("CREATE", "TEMP|TEMPORARY|UNLOGGED", "TABLE", MatchAny, "OF") ||
+			 TailMatches("CREATE", "GLOBAL|LOCAL", "TEMP|TEMPORARY", "TABLE", MatchAny, "OF"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_composite_datatypes);
 	/* Complete CREATE TABLE <name> [ (...) ] AS with list of keywords */
 	else if (TailMatches("CREATE", "TABLE", MatchAny, "AS") ||
 			 TailMatches("CREATE", "TABLE", MatchAny, "(*)", "AS") ||
 			 TailMatches("CREATE", "TEMP|TEMPORARY|UNLOGGED", "TABLE", MatchAny, "AS") ||
-			 TailMatches("CREATE", "TEMP|TEMPORARY|UNLOGGED", "TABLE", MatchAny, "(*)", "AS"))
+			 TailMatches("CREATE", "TEMP|TEMPORARY|UNLOGGED", "TABLE", MatchAny, "(*)", "AS") ||
+			 TailMatches("CREATE", "GLOBAL|LOCAL", "TEMP|TEMPORARY", "TABLE", MatchAny, "AS") ||
+			 TailMatches("CREATE", "GLOBAL|LOCAL", "TEMP|TEMPORARY", "TABLE", MatchAny, "(*)", "AS"))
 		COMPLETE_WITH("EXECUTE", "SELECT", "TABLE", "VALUES", "WITH");
 	/* Complete CREATE TABLE name (...) with supported options */
 	else if (TailMatches("CREATE", "TABLE", MatchAny, "(*)"))
 		COMPLETE_WITH("AS", "INHERITS (", "PARTITION BY", "USING", "TABLESPACE", "WITH (");
 	else if (TailMatches("CREATE", "UNLOGGED", "TABLE", MatchAny, "(*)"))
 		COMPLETE_WITH("AS", "INHERITS (", "USING", "TABLESPACE", "WITH (");
-	else if (TailMatches("CREATE", "TEMP|TEMPORARY", "TABLE", MatchAny, "(*)"))
+	else if (TailMatches("CREATE", "TEMP|TEMPORARY", "TABLE", MatchAny, "(*)") ||
+			 TailMatches("CREATE", "GLOBAL|LOCAL", "TEMP|TEMPORARY", "TABLE", MatchAny, "(*)"))
 		COMPLETE_WITH("AS", "INHERITS (", "ON COMMIT", "PARTITION BY", "USING",
 					  "TABLESPACE", "WITH (");
 	/* Complete CREATE TABLE (...) USING with table access methods */
 	else if (TailMatches("CREATE", "TABLE", MatchAny, "(*)", "USING") ||
-			 TailMatches("CREATE", "TEMP|TEMPORARY|UNLOGGED", "TABLE", MatchAny, "(*)", "USING"))
+			 TailMatches("CREATE", "TEMP|TEMPORARY|UNLOGGED", "TABLE", MatchAny, "(*)", "USING") ||
+			 TailMatches("CREATE", "GLOBAL|LOCAL", "TEMP|TEMPORARY", "TABLE", MatchAny, "(*)", "USING"))
 		COMPLETE_WITH_QUERY(Query_for_list_of_table_access_methods);
 	/* Complete CREATE TABLE (...) WITH with storage parameters */
 	else if (TailMatches("CREATE", "TABLE", MatchAny, "(*)", "WITH", "(") ||
-			 TailMatches("CREATE", "TEMP|TEMPORARY|UNLOGGED", "TABLE", MatchAny, "(*)", "WITH", "("))
+			 TailMatches("CREATE", "TEMP|TEMPORARY|UNLOGGED", "TABLE", MatchAny, "(*)", "WITH", "(") ||
+			 TailMatches("CREATE", "GLOBAL|LOCAL", "TEMP|TEMPORARY", "TABLE", MatchAny, "(*)", "WITH", "("))
 		COMPLETE_WITH_LIST(table_storage_parameters);
-	/* Complete CREATE TABLE ON COMMIT with actions */
-	else if (TailMatches("CREATE", "TEMP|TEMPORARY", "TABLE", MatchAny, "(*)", "ON", "COMMIT"))
+	/* Complete CREATE GLOBAL TEMP TABLE ON COMMIT with actions */
+	else if (TailMatches("CREATE", "GLOBAL", "TEMP|TEMPORARY", "TABLE", MatchAny, "(*)", "ON", "COMMIT"))
+		COMPLETE_WITH("DELETE ROWS", "PRESERVE ROWS");
+	/* Complete CREATE [ LOCAL ] TEMP TABLE ON COMMIT with actions */
+	else if (TailMatches("CREATE", "TEMP|TEMPORARY", "TABLE", MatchAny, "(*)", "ON", "COMMIT") ||
+			 TailMatches("CREATE", "LOCAL", "TEMP|TEMPORARY", "TABLE", MatchAny, "(*)", "ON", "COMMIT"))
 		COMPLETE_WITH("DELETE ROWS", "DROP", "PRESERVE ROWS");
 
 /* CREATE TABLESPACE */
diff --git a/src/bin/scripts/vacuuming.c b/src/bin/scripts/vacuuming.c
index 6a1cefbd888..22795c1603e 100644
--- a/src/bin/scripts/vacuuming.c
+++ b/src/bin/scripts/vacuuming.c
@@ -630,7 +630,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..5b41f9a7ecd
--- /dev/null
+++ b/src/include/catalog/global_temp.h
@@ -0,0 +1,116 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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"
+
+/*
+ * GtrInfo
+ *
+ *	Structure holding information about a global temporary relation that is
+ *	local to the current session.  These properties act as local overrides to
+ *	the information stored in pg_class, allowing it to vary between backends.
+ */
+typedef struct GtrInfo
+{
+	Oid			relfilenode;	/* the relation's physical storage file */
+	Oid			reltablespace;	/* the relation's tablespace identifier */
+} GtrInfo;
+
+/*
+ * Copy all pg_class attributes that may be session-local for a global
+ * temporary relation from "source" to "target", where the source and target
+ * may be of type Form_pg_class or GtrInfo *.
+ *
+ * Beware of multiple evaluations of arguments!
+ */
+#define COPY_PG_CLASS_GTR_INFO(source, target) \
+	do { \
+		(target)->relfilenode = (source)->relfilenode; \
+		(target)->reltablespace = (source)->reltablespace; \
+	} while (0)
+
+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 TrackGlobalTempRelation(Relation relation);
+extern void ForgetGlobalTempRelation(Oid relid);
+extern void InvalidateGlobalTempRelation(Oid relid);
+extern void ProcessInvalidatedGlobalTempRelations(void);
+extern void AtEOXact_GlobalTempRelation(bool isCommit);
+extern void AtEOSubXact_GlobalTempRelation(bool isCommit,
+										   SubTransactionId mySubid,
+										   SubTransactionId parentSubid);
+extern bool IsGlobalTempRelationInUse(Oid relid);
+extern bool IsOtherUsingGlobalTempRelation(Oid relid);
+extern List *GetAllGlobalTempRelationsInUse(Oid dbId);
+extern GtrInfo *GetGlobalTempRelationInfo(Oid relid);
+extern GtrInfo *GetGlobalTempRelationInfoForUpdate(Oid relid);
+extern HeapTuple GetEffectivePgClassTuple(Oid relid);
+
+/*
+ * Get the effective value of relfilenode for a relation.  For a global
+ * temporary relation, the value from gtr_info (if present) takes precedence.
+ */
+static inline Oid
+GetEffective_relfilenode(Form_pg_class class_form, GtrInfo *gtr_info)
+{
+	return gtr_info != NULL ? gtr_info->relfilenode : class_form->relfilenode;
+}
+
+/*
+ * Get the effective value of reltablespace for a relation.  For a global
+ * temporary relation, the value from gtr_info (if present) takes precedence.
+ */
+static inline Oid
+GetEffective_reltablespace(Form_pg_class class_form, GtrInfo *gtr_info)
+{
+	return gtr_info != NULL ? gtr_info->reltablespace : class_form->reltablespace;
+}
+
+/*
+ * Set the effective value of relfilenode for a relation.  For a global
+ * temporary relation, GetGlobalTempRelationInfoForUpdate() should have been
+ * used to obtain gtr_info, and it will be updated instead of the pg_class
+ * entry.  Otherwise, the value is set in the pg_class entry.
+ */
+static inline void
+SetEffective_relfilenode(Form_pg_class class_form, GtrInfo *gtr_info, Oid newval)
+{
+	if (gtr_info != NULL)
+		gtr_info->relfilenode = newval;
+	else
+		class_form->relfilenode = newval;
+}
+
+/*
+ * Set the effective value of reltablespace for a relation.  For a global
+ * temporary relation, GetGlobalTempRelationInfoForUpdate() should have been
+ * used to obtain gtr_info, and it will be updated *in addition to* updating
+ * the pg_class entry, since we want a tablespace change to apply to both the
+ * current session and all future sessions.
+ */
+static inline void
+SetEffective_reltablespace(Form_pg_class class_form, GtrInfo *gtr_info, Oid newval)
+{
+	/* NB: newval is set *both* locally and globally */
+	if (gtr_info != NULL)
+		gtr_info->reltablespace = newval;
+	class_form->reltablespace = newval;
+}
+
+#endif							/* GLOBAL_TEMP_H */
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index 4440b989cd5..1ff4cab2435 100644
--- a/src/include/catalog/pg_class.h
+++ b/src/include/catalog/pg_class.h
@@ -184,7 +184,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 values other than 'n' */
 #define		  RELONCOMMIT_NONE			'n' /* default: no action */
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f46427258e3..fd4cad0dddf 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12778,4 +12778,20 @@
   proname => 'hashoid8extended', prorettype => 'int8',
   proargtypes => 'oid8 int8', prosrc => 'hashoid8extended' },
 
+# Global temporary relation functions
+{ oid => '8082', descr => 'get information about an in-use global temporary relation',
+  proname => 'pg_gtr_info', provolatile => 's', proparallel => 'u',
+  proargtypes => 'oid', prorettype => 'record',
+  proallargtypes => '{oid,oid,oid}',
+  proargmodes => '{i,o,o}',
+  proargnames => '{oid,relfilenode,reltablespace}',
+  prosrc => 'pg_gtr_info' },
+{ oid => '8083', descr => 'get information about all in-use global temporary relations',
+  proname => 'pg_gtrs_in_use', provolatile => 's', proparallel => 'u',
+  proargtypes => '', proretset => 't', prorettype => 'record', prorows => '10',
+  proallargtypes => '{oid,oid,oid}',
+  proargmodes => '{o,o,o}',
+  proargnames => '{oid,relfilenode,reltablespace}',
+  prosrc => 'pg_gtrs_in_use' },
+
 ]
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 8d858be9927..a1b56cb7659 100644
--- a/src/include/storage/lwlocklist.h
+++ b/src/include/storage/lwlocklist.h
@@ -90,6 +90,7 @@ PG_LWLOCK(55, LogicalDecodingControl)
 PG_LWLOCK(56, DataChecksumsWorker)
 PG_LWLOCK(57, AioWorkerControl)
 PG_LWLOCK(58, DataChecksumTransition)
+PG_LWLOCK(59, GlobalTempRelControl)
 
 /*
  * There also exist several built-in LWLock tranches.  As with the predefined
@@ -141,3 +142,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 9c8d17337b8..07cd1683a91 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
@@ -674,13 +674,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
  */
@@ -690,7 +692,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,
@@ -707,6 +710,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
@@ -755,5 +765,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..7fceab3bb4c
--- /dev/null
+++ b/src/test/isolation/expected/global-temp.out
@@ -0,0 +1,528 @@
+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');
+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 alter1c alter1d 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 alter1c: ALTER TABLE tmp2 ADD CONSTRAINT tmp2_nn NOT NULL key;
+step alter1d: ALTER TABLE tmp2 ADD CONSTRAINT tmp2_chk CHECK (key > 0);
+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 alter1c alter1d 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 alter1c: ALTER TABLE tmp2 ADD CONSTRAINT tmp2_nn NOT NULL key;
+ERROR:  cannot add or alter constraints of global temporary table "tmp2" because it is being used in another session
+step alter1d: ALTER TABLE tmp2 ADD CONSTRAINT tmp2_chk CHECK (key > 0);
+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;
+
+starting permutation: create1 drop2 b1 prep1 cprep1
+step create1: CREATE GLOBAL TEMP TABLE tmp2 (key int, val text);
+step drop2: DROP TABLE tmp2;
+step b1: BEGIN;
+step prep1: PREPARE TRANSACTION 'tx';
+step cprep1: COMMIT PREPARED 'tx';
+
+starting permutation: create1 ins1_2 b1 drop2 prep1 cprep1
+step create1: CREATE GLOBAL TEMP TABLE tmp2 (key int, val text);
+step ins1_2: INSERT INTO tmp2 VALUES (1, 's1');
+step b1: BEGIN;
+step drop2: DROP TABLE tmp2;
+step prep1: PREPARE TRANSACTION 'tx';
+step cprep1: COMMIT PREPARED 'tx';
+
+starting permutation: create1 b1 ins1_2 drop2 prep1
+step create1: CREATE GLOBAL TEMP TABLE tmp2 (key int, val text);
+step b1: BEGIN;
+step ins1_2: INSERT INTO tmp2 VALUES (1, 's1');
+step drop2: DROP TABLE tmp2; <waiting ...>
+step prep1: PREPARE TRANSACTION 'tx';
+ERROR:  cannot PREPARE a transaction that has operated on temporary objects
+step drop2: <... completed>
+
+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
+---+---
+  1|s1 
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+(0 rows)
+
+step ins2: INSERT INTO tmp VALUES (1, 's2');
+step t1: TRUNCATE tmp;
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+(0 rows)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+  1|s2 
+(1 row)
+
+step ins1: INSERT INTO tmp VALUES (1, 's1');
+step t2: TRUNCATE tmp;
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+  1|s1 
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+(0 rows)
+
+
+starting permutation: ins1 ins2 alt_tblspace1 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_tblspace1: 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
+    LEFT JOIN pg_tablespace s1 ON s1.oid = c.reltablespace,
+    LATERAL pg_gtr_info(c.oid) t
+    LEFT 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
+    LEFT JOIN pg_tablespace s1 ON s1.oid = c.reltablespace,
+    LATERAL pg_gtr_info(c.oid) t
+    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
+---+---
+  1|s1 
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+  1|s2 
+(1 row)
+
+step reset_tblspace: ALTER TABLE tmp SET TABLESPACE pg_default;
+
+starting permutation: create1 ins1_2 used1 drop1 used1
+step create1: CREATE GLOBAL TEMP TABLE tmp2 (key int, val text);
+step ins1_2: INSERT INTO tmp2 VALUES (1, 's1');
+step used1: 
+  SELECT regexp_replace(oid::regclass::text, '_(\d+)', '_NNN', 'g')
+    FROM pg_gtrs_in_use()
+   ORDER BY 1;
+
+regexp_replace             
+---------------------------
+pg_toast.pg_toast_NNN      
+pg_toast.pg_toast_NNN_index
+tmp2                       
+(3 rows)
+
+step drop1: DROP TABLE tmp2;
+step used1: 
+  SELECT regexp_replace(oid::regclass::text, '_(\d+)', '_NNN', 'g')
+    FROM pg_gtrs_in_use()
+   ORDER BY 1;
+
+regexp_replace
+--------------
+(0 rows)
+
+
+starting permutation: create1 ins1_2 used1 drop2 used1
+step create1: CREATE GLOBAL TEMP TABLE tmp2 (key int, val text);
+step ins1_2: INSERT INTO tmp2 VALUES (1, 's1');
+step used1: 
+  SELECT regexp_replace(oid::regclass::text, '_(\d+)', '_NNN', 'g')
+    FROM pg_gtrs_in_use()
+   ORDER BY 1;
+
+regexp_replace             
+---------------------------
+pg_toast.pg_toast_NNN      
+pg_toast.pg_toast_NNN_index
+tmp2                       
+(3 rows)
+
+step drop2: DROP TABLE tmp2;
+step used1: 
+  SELECT regexp_replace(oid::regclass::text, '_(\d+)', '_NNN', 'g')
+    FROM pg_gtrs_in_use()
+   ORDER BY 1;
+
+regexp_replace
+--------------
+(0 rows)
+
+
+starting permutation: create1 ins1_2 used1 b1 drop2 used1 r1 used1
+step create1: CREATE GLOBAL TEMP TABLE tmp2 (key int, val text);
+step ins1_2: INSERT INTO tmp2 VALUES (1, 's1');
+step used1: 
+  SELECT regexp_replace(oid::regclass::text, '_(\d+)', '_NNN', 'g')
+    FROM pg_gtrs_in_use()
+   ORDER BY 1;
+
+regexp_replace             
+---------------------------
+pg_toast.pg_toast_NNN      
+pg_toast.pg_toast_NNN_index
+tmp2                       
+(3 rows)
+
+step b1: BEGIN;
+step drop2: DROP TABLE tmp2;
+step used1: 
+  SELECT regexp_replace(oid::regclass::text, '_(\d+)', '_NNN', 'g')
+    FROM pg_gtrs_in_use()
+   ORDER BY 1;
+
+regexp_replace
+--------------
+(0 rows)
+
+step r1: ROLLBACK;
+step used1: 
+  SELECT regexp_replace(oid::regclass::text, '_(\d+)', '_NNN', 'g')
+    FROM pg_gtrs_in_use()
+   ORDER BY 1;
+
+regexp_replace
+--------------
+(0 rows)
+
+
+starting permutation: create1 ins1_2 b1 used1 sp1 drop2 used1 rsp1 used1 r1 used1
+step create1: CREATE GLOBAL TEMP TABLE tmp2 (key int, val text);
+step ins1_2: INSERT INTO tmp2 VALUES (1, 's1');
+step b1: BEGIN;
+step used1: 
+  SELECT regexp_replace(oid::regclass::text, '_(\d+)', '_NNN', 'g')
+    FROM pg_gtrs_in_use()
+   ORDER BY 1;
+
+regexp_replace             
+---------------------------
+pg_toast.pg_toast_NNN      
+pg_toast.pg_toast_NNN_index
+tmp2                       
+(3 rows)
+
+step sp1: SAVEPOINT sp;
+step drop2: DROP TABLE tmp2;
+step used1: 
+  SELECT regexp_replace(oid::regclass::text, '_(\d+)', '_NNN', 'g')
+    FROM pg_gtrs_in_use()
+   ORDER BY 1;
+
+regexp_replace
+--------------
+(0 rows)
+
+step rsp1: ROLLBACK TO SAVEPOINT sp;
+step used1: 
+  SELECT regexp_replace(oid::regclass::text, '_(\d+)', '_NNN', 'g')
+    FROM pg_gtrs_in_use()
+   ORDER BY 1;
+
+regexp_replace
+--------------
+(0 rows)
+
+step r1: ROLLBACK;
+step used1: 
+  SELECT regexp_replace(oid::regclass::text, '_(\d+)', '_NNN', 'g')
+    FROM pg_gtrs_in_use()
+   ORDER BY 1;
+
+regexp_replace
+--------------
+(0 rows)
+
+
+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/isolation_schedule b/src/test/isolation/isolation_schedule
index 8470d50d2bc..2cf37a4caa4 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -131,3 +131,4 @@ test: ddl-dependency-locking
 test: tablespace-dependency-locking
 test: pub-concurrent-drop
 test: drop-owned-grant
+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..01b63fdaf9d
--- /dev/null
+++ b/src/test/isolation/specs/global-temp.spec
@@ -0,0 +1,153 @@
+# 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
+# Tablespace setup
+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; }
+
+# Transaction control
+step b1 { BEGIN; }
+step r1 { ROLLBACK; }
+step sp1 { SAVEPOINT sp; }
+step rsp1 { ROLLBACK TO SAVEPOINT sp; }
+step prep1 { PREPARE TRANSACTION 'tx'; }
+step cprep1 { COMMIT PREPARED 'tx'; }
+
+# Test basic effects
+step ins1 { INSERT INTO tmp VALUES (1, 's1'); }
+step sel1 { SELECT * FROM tmp; }
+step ins1p1 { INSERT INTO tmp_parted VALUES (1, 's1 p1'); }
+step ins1p2 { INSERT INTO tmp_parted VALUES (2, 's1 p2'); }
+step sel1p { SELECT tableoid::regclass, * FROM tmp_parted; }
+
+# Test prevention of ALTER TABLE with rewrite, if in use
+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 alter1c { ALTER TABLE tmp2 ADD CONSTRAINT tmp2_nn NOT NULL key; }
+step alter1d { ALTER TABLE tmp2 ADD CONSTRAINT tmp2_chk CHECK (key > 0); }
+step seltype1 { SELECT key, pg_typeof(key), val FROM tmp2; }
+step drop1 { DROP TABLE tmp2; }
+
+# Test DROP with ON COMMIT DELETE ROWS
+step create1dr { CREATE GLOBAL TEMP TABLE tmp2 (key int, val text) ON COMMIT DELETE ROWS; }
+
+# Test local TRUNCATE
+step t1 { TRUNCATE tmp; }
+
+# Test ALTER TABLE ... SET TABLESPACE
+step alt_tblspace1 { 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
+    LEFT JOIN pg_tablespace s1 ON s1.oid = c.reltablespace,
+    LATERAL pg_gtr_info(c.oid) t
+    LEFT JOIN pg_tablespace s2 ON s2.oid = t.reltablespace
+   WHERE c.relname = 'tmp';
+}
+step reset_tblspace { ALTER TABLE tmp SET TABLESPACE pg_default; }
+
+# Test DROP from other backend
+step used1 {
+  SELECT regexp_replace(oid::regclass::text, '_(\d+)', '_NNN', 'g')
+    FROM pg_gtrs_in_use()
+   ORDER BY 1;
+}
+
+session s2
+# Transaction control
+step b2 { BEGIN; }
+step c2 { COMMIT; }
+step r2 { ROLLBACK; }
+step sp2 { SAVEPOINT sp; }
+step rsp2 { ROLLBACK TO SAVEPOINT sp; }
+
+# Test basic effects
+step ins2 { INSERT INTO tmp VALUES (1, 's2'); }
+step sel2 { SELECT * FROM tmp; }
+step ins2p1 { INSERT INTO tmp_parted VALUES (1, 's2 p1'); }
+step ins2p2 { INSERT INTO tmp_parted VALUES (2, 's2 p2'); }
+step sel2p { SELECT tableoid::regclass, * FROM tmp_parted; }
+
+# Test prevention of ALTER TABLE with rewrite, if in use
+step ins2_2 { INSERT INTO tmp2 VALUES (1, 's2'); }
+step seltype2 { SELECT key, pg_typeof(key), val FROM tmp2; }
+
+# Test GTT inval in prepared transaction
+step drop2 { DROP TABLE tmp2; }
+
+# Test local TRUNCATE
+step t2 { TRUNCATE tmp; }
+
+# Test ALTER TABLE ... SET TABLESPACE
+step get_tblspace2 {
+  SELECT s1.spcname, s2.spcname,
+         regexp_replace(pg_relation_filepath('tmp'), '(\d+)', 'NNN', 'g')
+    FROM pg_class c
+    LEFT JOIN pg_tablespace s1 ON s1.oid = c.reltablespace,
+    LATERAL pg_gtr_info(c.oid) t
+    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
+
+# Test 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 alter1c alter1d
+            ins2_2 seltype1 seltype2 drop1
+permutation create1 ins1_2 ins2_2
+            alter1a alter1b alter1c alter1d
+            seltype1 seltype2 drop1
+
+# Test DROP with ON COMMIT DELETE ROWS
+permutation create1dr ins1_2 ins2_2 drop1 create1dr ins1_2 ins2_2 drop1
+
+# Test GTT inval in prepared transaction
+permutation create1 drop2 b1 prep1 cprep1
+permutation create1 ins1_2 b1 drop2 prep1 cprep1
+permutation create1 b1 ins1_2 drop2 prep1
+
+# 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_tblspace1 get_tblspace1 get_tblspace2
+            sel1 sel2 reset_tblspace
+
+# Test DROP from other backend
+permutation create1 ins1_2 used1 drop1 used1
+permutation create1 ins1_2 used1 drop2 used1
+permutation create1 ins1_2 used1 b1 drop2 used1 r1 used1
+permutation create1 ins1_2 b1 used1 sp1 drop2 used1 rsp1 used1 r1 used1
+
+# Tidy up
+permutation drop_tblspace list_tblspaces
diff --git a/src/test/modules/test_checksums/meson.build b/src/test/modules/test_checksums/meson.build
index 7eccd5156b9..000e240f5ff 100644
--- a/src/test/modules/test_checksums/meson.build
+++ b/src/test/modules/test_checksums/meson.build
@@ -48,6 +48,7 @@ tests += {
       't/022_rewind_state.pl',
       't/023_rewind_standby_target.pl',
       't/024_combinebackup_mixed.pl',
+      't/025_global_temp.pl',
     ],
   },
 }
diff --git a/src/test/modules/test_checksums/t/025_global_temp.pl b/src/test/modules/test_checksums/t/025_global_temp.pl
new file mode 100644
index 00000000000..4355ebe17bd
--- /dev/null
+++ b/src/test/modules/test_checksums/t/025_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/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl
index a4fa4b96c61..e578f1c6616 100644
--- a/src/test/recovery/t/001_stream_rep.pl
+++ b/src/test/recovery/t/001_stream_rep.pl
@@ -51,6 +51,9 @@ $node_primary->safe_psql('postgres',
 
 $node_primary->safe_psql(
 	'postgres', q{
+CREATE GLOBAL TEMP TABLE gtt (a int);
+INSERT INTO gtt VALUES (1);
+
 CREATE TABLE user_logins(id serial, who text);
 
 CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$
@@ -87,6 +90,21 @@ $result = $node_standby_1->safe_psql('postgres',
 );
 is($result, qq(1), 'check recovery state on standby 1');
 
+# Global temporary table should be inaccessible on standbys
+my ($ret, $stdout, $stderr) = $node_standby_1->psql(
+	'postgres', 'SELECT count(*) FROM gtt');
+like(
+	$stderr,
+	qr/ERROR:  cannot access temporary or unlogged relations during recovery/,
+	"Accessing GTT fails on standby 1");
+
+($ret, $stdout, $stderr) = $node_standby_2->psql(
+	'postgres', 'SELECT count(*) FROM gtt');
+like(
+	$stderr,
+	qr/ERROR:  cannot access temporary or unlogged relations during recovery/,
+	"Accessing GTT fails on standby 2");
+
 # Likewise, but for a sequence
 $node_primary->safe_psql('postgres',
 	"CREATE SEQUENCE seq1; SELECT nextval('seq1')");
@@ -119,6 +137,17 @@ is($node_standby_1->psql('postgres', 'INSERT INTO tab_int VALUES (1)'),
 is($node_standby_2->psql('postgres', 'INSERT INTO tab_int VALUES (1)'),
 	3, 'read-only queries on standby 2');
 
+# Test pg_relation_size() and pg_total_relation_size() on GTT on standby
+$result = $node_standby_1->safe_psql('postgres',
+	"SELECT pg_relation_size('gtt')");
+is( $result, qq(0), 'check pg_relation_size(GTT) on standby 1');
+
+$result = $node_standby_1->safe_psql('postgres', q{
+SELECT SUM(pg_total_relation_size(oid)) > 0
+FROM pg_class WHERE relkind='r'
+});
+is ( $result, qq(t), 'check pg_total_relation_size(*) on standby 1');
+
 # Tests for connection parameter target_session_attrs
 note "testing connection parameter \"target_session_attrs\"";
 
@@ -266,7 +295,7 @@ my $connstr_rep = "$connstr_common replication=1";
 my $connstr_db = "$connstr_common replication=database dbname=postgres";
 
 # Test SHOW ALL
-my ($ret, $stdout, $stderr) = $node_primary->psql(
+($ret, $stdout, $stderr) = $node_primary->psql(
 	'postgres', 'SHOW ALL;',
 	on_error_die => 1,
 	extra_params => [ '--dbname' => $connstr_rep ]);
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index e167a41ce79..ec5fb7b45ba 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;
+ERROR:  cannot attach a permanent relation as partition of local temporary relation "temp_parted"
+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;
@@ -4682,9 +4686,9 @@ create temp table temp_part_parent (a int) partition by list (a);
 create table perm_part_child (a int);
 create temp table temp_part_child (a int);
 alter table temp_part_parent attach partition perm_part_child default; -- error
-ERROR:  cannot attach a permanent relation as partition of temporary relation "temp_part_parent"
+ERROR:  cannot attach a permanent relation as partition of local temporary relation "temp_part_parent"
 alter table perm_part_parent attach partition temp_part_child default; -- error
-ERROR:  cannot attach a temporary relation as partition of permanent relation "perm_part_parent"
+ERROR:  cannot attach a local temporary relation as partition of permanent relation "perm_part_parent"
 alter table temp_part_parent attach partition temp_part_child default; -- ok
 drop table perm_part_parent cascade;
 drop table temp_part_parent cascade;
diff --git a/src/test/regress/expected/create_table.out b/src/test/regress/expected/create_table.out
index 2df8761ae8e..5a8ac732fea 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;
@@ -640,7 +640,7 @@ CREATE TEMP TABLE temp_parted (
 	a int
 ) PARTITION BY LIST (a);
 CREATE TABLE fail_part PARTITION OF temp_parted FOR VALUES IN ('a');
-ERROR:  cannot create a permanent relation as partition of temporary relation "temp_parted"
+ERROR:  cannot create a permanent relation as partition of local temporary relation "temp_parted"
 DROP TABLE temp_parted;
 -- check for partition bound overlap and other invalid specifications
 CREATE TABLE list_parted2 (
@@ -1054,13 +1054,25 @@ 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"
+ERROR:  cannot create a permanent relation as partition of local temporary relation "temp_parted"
+create table perm_part partition of global_temp_parted default; -- error
+ERROR:  cannot create a permanent relation as partition of global temporary relation "global_temp_parted"
+create table perm_part partition of perm_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"
+ERROR:  cannot create a local 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; -- error
+ERROR:  cannot create a global temporary relation as partition of permanent relation "perm_parted"
+create global temp table global_temp_part partition of global_temp_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 053fa56573f..d675c8c91af 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/foreign_data.out b/src/test/regress/expected/foreign_data.out
index d8e4cb12c3d..5fd753d8297 100644
--- a/src/test/regress/expected/foreign_data.out
+++ b/src/test/regress/expected/foreign_data.out
@@ -2212,10 +2212,10 @@ DROP TABLE fd_pt2;
 CREATE TEMP TABLE temp_parted (a int) PARTITION BY LIST (a);
 CREATE FOREIGN TABLE foreign_part PARTITION OF temp_parted DEFAULT
   SERVER s0;  -- ERROR
-ERROR:  cannot create a permanent relation as partition of temporary relation "temp_parted"
+ERROR:  cannot create a permanent relation as partition of local temporary relation "temp_parted"
 CREATE FOREIGN TABLE foreign_part (a int) SERVER s0;
 ALTER TABLE temp_parted ATTACH PARTITION foreign_part DEFAULT;  -- ERROR
-ERROR:  cannot attach a permanent relation as partition of temporary relation "temp_parted"
+ERROR:  cannot attach a permanent relation as partition of local temporary relation "temp_parted"
 DROP FOREIGN TABLE foreign_part;
 DROP TABLE temp_parted;
 -- Cleanup
diff --git a/src/test/regress/expected/global_temp.out b/src/test/regress/expected/global_temp.out
new file mode 100644
index 00000000000..493b0f96f81
--- /dev/null
+++ b/src/test/regress/expected/global_temp.out
@@ -0,0 +1,400 @@
+--
+-- 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 pg_gtr_info() and pg_gtrs_in_use()
+\c
+SET search_path = global_temp_tests;
+SELECT * FROM pg_gtr_info('tmp1'::regclass);
+ relfilenode | reltablespace 
+-------------+---------------
+             |              
+(1 row)
+
+SELECT * FROM pg_gtrs_in_use();
+ oid | relfilenode | reltablespace 
+-----+-------------+---------------
+(0 rows)
+
+SELECT * FROM tmp1;
+ a 
+---
+(0 rows)
+
+SELECT c.relfilenode = c.oid,
+       pg_relation_filenode('tmp1'::regclass) = c.relfilenode,
+       t.relfilenode = c.relfilenode,
+       t.reltablespace = c.reltablespace
+  FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+ WHERE c.oid = 'tmp1'::regclass;
+ ?column? | ?column? | ?column? | ?column? 
+----------+----------+----------+----------
+ t        | t        | t        | t
+(1 row)
+
+SELECT c.relname,
+       t.relfilenode = c.relfilenode,
+       t.reltablespace = c.reltablespace
+  FROM pg_gtrs_in_use() t LEFT JOIN pg_class c ON c.oid = t.oid
+ ORDER BY c.relname;
+ relname | ?column? | ?column? 
+---------+----------+----------
+ tmp1    | t        | t
+(1 row)
+
+-- 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 GLOBAL TEMP TABLE tmp2_p2 (a int);
+ALTER TABLE tmp2 ATTACH PARTITION tmp2_p2 FOR VALUES IN (2);
+CREATE TEMP TABLE local_tmp PARTITION OF tmp2 FOR VALUES IN (3); -- fail
+ERROR:  cannot create a local temporary relation as partition of global temporary relation "tmp2"
+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"
+CREATE TABLE perm PARTITION OF tmp2 FOR VALUES IN (3); -- fail
+ERROR:  cannot create a permanent relation as partition of global temporary relation "tmp2"
+CREATE TABLE perm (a int);
+ALTER TABLE tmp2 ATTACH PARTITION perm FOR VALUES IN (3); -- fail
+ERROR:  cannot attach a permanent relation as partition of global temporary relation "tmp2"
+INSERT INTO tmp2 VALUES (1), (2);
+SELECT tableoid::regclass, * FROM tmp2 ORDER BY a;
+ tableoid | a 
+----------+---
+ tmp2_p1  | 1
+ tmp2_p2  | 2
+(2 rows)
+
+\c
+SET search_path = global_temp_tests;
+SELECT tableoid::regclass, * FROM tmp2 ORDER BY a;
+ tableoid | a 
+----------+---
+(0 rows)
+
+DROP TABLE tmp2, perm;
+-- 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 -- reltablespace changes locally and globally
+CREATE GLOBAL TEMP TABLE tmp2 (a int);
+INSERT INTO tmp2 VALUES (1);
+SELECT * FROM tmp2;
+ a 
+---
+ 1
+(1 row)
+
+SELECT c.reltablespace AS global_tablespace,
+       t.reltablespace AS local_tablespace,
+       regexp_replace(pg_relation_filepath('tmp2'), '(\d+)', 'NNN', 'g')
+  FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+ WHERE c.oid = 'tmp2'::regclass;
+ global_tablespace | local_tablespace |  regexp_replace   
+-------------------+------------------+-------------------
+                 0 |                0 | base/NNN/tNNN_NNN
+(1 row)
+
+ALTER TABLE tmp2 SET TABLESPACE regress_tblspace;
+SELECT * FROM tmp2;
+ a 
+---
+ 1
+(1 row)
+
+SELECT s1.spcname AS global_tablespace, s2.spcname AS local_tablespace,
+       regexp_replace(pg_relation_filepath('tmp2'), '(\d+)', 'NNN', 'g')
+  FROM pg_class c
+  LEFT JOIN pg_tablespace s1 ON s1.oid = c.reltablespace,
+  LATERAL pg_gtr_info(c.oid) t
+  LEFT JOIN pg_tablespace s2 ON s2.oid = t.reltablespace
+ WHERE c.oid = 'tmp2'::regclass;
+ global_tablespace | local_tablespace |            regexp_replace             
+-------------------+------------------+---------------------------------------
+ regress_tblspace  | regress_tblspace | 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 REPACK -- relfilenode only changes locally
+SELECT c.relfilenode AS global_relfilenode, t.relfilenode AS local_relfilenode
+  FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+ WHERE c.oid = 'tmp1'::regclass \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, LATERAL pg_gtr_info(c.oid) t
+ WHERE c.oid = 'tmp1'::regclass;
+ global_relfilenode | local_relfilenode 
+--------------------+-------------------
+ unchanged          | changed
+(1 row)
+
+-- Test VACUUM FULL -- relfilenode only changes locally
+SELECT c.relfilenode AS global_relfilenode, t.relfilenode AS local_relfilenode
+  FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+ WHERE c.oid = 'tmp1'::regclass \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, LATERAL pg_gtr_info(c.oid) t
+ WHERE c.oid = 'tmp1'::regclass;
+ global_relfilenode | local_relfilenode 
+--------------------+-------------------
+ unchanged          | changed
+(1 row)
+
+-- Test subtransaction rollback of DROP
+\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);
+COMMIT;
+-- Re-check pg_gtrs_in_use()
+SELECT c.relname,
+       t.relfilenode = c.relfilenode,
+       t.reltablespace = c.reltablespace
+  FROM pg_gtrs_in_use() t LEFT JOIN pg_class c ON c.oid = t.oid
+ ORDER BY c.relname;
+ relname | ?column? | ?column? 
+---------+----------+----------
+ tmp1    | t        | t
+(1 row)
+
+-- Test view creation
+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/inherit.out b/src/test/regress/expected/inherit.out
index 0136aa53c96..c9db19fdf82 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -3142,7 +3142,7 @@ create table inh_perm_parent (a1 int);
 create temp table inh_temp_parent (a1 int);
 create temp table inh_temp_child () inherits (inh_perm_parent); -- ok
 create table inh_perm_child () inherits (inh_temp_parent); -- error
-ERROR:  cannot inherit from temporary relation "inh_temp_parent"
+ERROR:  permanent relation cannot inherit from local temporary relation "inh_temp_parent"
 create temp table inh_temp_child_2 () inherits (inh_temp_parent); -- ok
 insert into inh_perm_parent values (1);
 insert into inh_temp_parent values (2);
diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out
index 0355720dfc6..2eb7fd452b4 100644
--- a/src/test/regress/expected/matview.out
+++ b/src/test/regress/expected/matview.out
@@ -301,8 +301,8 @@ ERROR:  cannot lock rows in materialized view "mvtest_tvvm"
 -- we don't support temp materialized views, so disallow this case:
 CREATE TEMP TABLE mvtest_temp_t (id int NOT NULL, type text NOT NULL, amt numeric NOT NULL);
 CREATE MATERIALIZED VIEW mvtest_temp_tm AS SELECT * FROM mvtest_temp_t;
-ERROR:  materialized views must not use temporary objects
-DETAIL:  This view depends on temporary table mvtest_temp_t.
+ERROR:  materialized views must not use local temporary objects
+DETAIL:  This view depends on local temporary table mvtest_temp_t.
 -- test join of mv and view
 SELECT type, m.totamt AS mtot, v.totamt AS vtot FROM mvtest_tm m LEFT JOIN mvtest_tv v USING (type) ORDER BY type;
  type | mtot | vtot 
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 75063f87a4a..79cc7dc82a1 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
+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 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 9f6c2a4bb08..ce1f678ed3d 100644
--- a/src/test/regress/sql/alter_table.sql
+++ b/src/test/regress/sql/alter_table.sql
@@ -2212,6 +2212,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;
@@ -2476,8 +2477,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_table.sql b/src/test/regress/sql/create_table.sql
index 80e424e6bda..1f183deb34b 100644
--- a/src/test/regress/sql/create_table.sql
+++ b/src/test/regress/sql/create_table.sql
@@ -686,11 +686,19 @@ 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; -- error
+create table perm_part partition of perm_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; -- error
+create global temp table global_temp_part partition of global_temp_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..6fe921a5139
--- /dev/null
+++ b/src/test/regress/sql/global_temp.sql
@@ -0,0 +1,239 @@
+--
+-- 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 pg_gtr_info() and pg_gtrs_in_use()
+\c
+SET search_path = global_temp_tests;
+SELECT * FROM pg_gtr_info('tmp1'::regclass);
+SELECT * FROM pg_gtrs_in_use();
+
+SELECT * FROM tmp1;
+
+SELECT c.relfilenode = c.oid,
+       pg_relation_filenode('tmp1'::regclass) = c.relfilenode,
+       t.relfilenode = c.relfilenode,
+       t.reltablespace = c.reltablespace
+  FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+ WHERE c.oid = 'tmp1'::regclass;
+
+SELECT c.relname,
+       t.relfilenode = c.relfilenode,
+       t.reltablespace = c.reltablespace
+  FROM pg_gtrs_in_use() t LEFT JOIN pg_class c ON c.oid = t.oid
+ ORDER BY c.relname;
+
+-- 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 GLOBAL TEMP TABLE tmp2_p2 (a int);
+ALTER TABLE tmp2 ATTACH PARTITION tmp2_p2 FOR VALUES IN (2);
+
+CREATE TEMP TABLE local_tmp PARTITION OF tmp2 FOR VALUES IN (3); -- fail
+CREATE TEMP TABLE local_tmp (a int);
+ALTER TABLE tmp2 ATTACH PARTITION local_tmp FOR VALUES IN (3); -- fail
+
+CREATE TABLE perm PARTITION OF tmp2 FOR VALUES IN (3); -- fail
+CREATE TABLE perm (a int);
+ALTER TABLE tmp2 ATTACH PARTITION perm FOR VALUES IN (3); -- fail
+
+INSERT INTO tmp2 VALUES (1), (2);
+SELECT tableoid::regclass, * FROM tmp2 ORDER BY a;
+\c
+SET search_path = global_temp_tests;
+SELECT tableoid::regclass, * FROM tmp2 ORDER BY a;
+DROP TABLE tmp2, perm;
+
+-- 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 -- reltablespace changes locally and globally
+CREATE GLOBAL TEMP TABLE tmp2 (a int);
+INSERT INTO tmp2 VALUES (1);
+SELECT * FROM tmp2;
+SELECT c.reltablespace AS global_tablespace,
+       t.reltablespace AS local_tablespace,
+       regexp_replace(pg_relation_filepath('tmp2'), '(\d+)', 'NNN', 'g')
+  FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+ WHERE c.oid = 'tmp2'::regclass;
+
+ALTER TABLE tmp2 SET TABLESPACE regress_tblspace;
+SELECT * FROM tmp2;
+SELECT s1.spcname AS global_tablespace, s2.spcname AS local_tablespace,
+       regexp_replace(pg_relation_filepath('tmp2'), '(\d+)', 'NNN', 'g')
+  FROM pg_class c
+  LEFT JOIN pg_tablespace s1 ON s1.oid = c.reltablespace,
+  LATERAL pg_gtr_info(c.oid) t
+  LEFT JOIN pg_tablespace s2 ON s2.oid = t.reltablespace
+ WHERE c.oid = 'tmp2'::regclass;
+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 REPACK -- relfilenode only changes locally
+SELECT c.relfilenode AS global_relfilenode, t.relfilenode AS local_relfilenode
+  FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+ WHERE c.oid = 'tmp1'::regclass \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, LATERAL pg_gtr_info(c.oid) t
+ WHERE c.oid = 'tmp1'::regclass;
+
+-- Test VACUUM FULL -- relfilenode only changes locally
+SELECT c.relfilenode AS global_relfilenode, t.relfilenode AS local_relfilenode
+  FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+ WHERE c.oid = 'tmp1'::regclass \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, LATERAL pg_gtr_info(c.oid) t
+ WHERE c.oid = 'tmp1'::regclass;
+
+-- Test subtransaction rollback of DROP
+\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);
+COMMIT;
+
+-- Re-check pg_gtrs_in_use()
+SELECT c.relname,
+       t.relfilenode = c.relfilenode,
+       t.reltablespace = c.reltablespace
+  FROM pg_gtrs_in_use() t LEFT JOIN pg_class c ON c.oid = t.oid
+ ORDER BY c.relname;
+
+-- Test view creation
+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/test/subscription/meson.build b/src/test/subscription/meson.build
index e71e95c6297..db71c2665ec 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -48,6 +48,7 @@ tests += {
       't/036_sequences.pl',
       't/037_except.pl',
       't/038_walsnd_shutdown_timeout.pl',
+      't/039_global_temp.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/039_global_temp.pl b/src/test/subscription/t/039_global_temp.pl
new file mode 100644
index 00000000000..9c1851cb909
--- /dev/null
+++ b/src/test/subscription/t/039_global_temp.pl
@@ -0,0 +1,100 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# This tests that the target of logical replication cannot be global temporary
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Create a publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+# Create a subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init;
+$node_subscriber->start;
+
+# Create tables on publisher
+$node_publisher->safe_psql('postgres', qq(
+	CREATE TABLE perm_test (a int);
+	CREATE TABLE gtt_test (a int);
+	INSERT INTO perm_test VALUES (1);
+	INSERT INTO gtt_test VALUES (1);
+));
+
+# Create same tables on subscriber, except make gtt_test global temporary
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE TABLE perm_test (a int);
+	CREATE GLOBAL TEMP TABLE gtt_test (a int);
+));
+
+# Setup logical replication on publisher
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres', qq(
+	CREATE PUBLICATION regress_perm_pub FOR TABLE perm_test;
+	CREATE PUBLICATION regress_gtt_pub FOR TABLE gtt_test;
+));
+
+# Setup logical replication for GTT on subscriber -- should fail
+my ($ret, $stdout, $stderr) =
+	$node_subscriber->psql('postgres', qq(
+		CREATE SUBSCRIPTION regress_sub
+			CONNECTION '$publisher_connstr' PUBLICATION regress_gtt_pub;
+));
+like(
+	$stderr,
+	qr/ERROR:  cannot use relation "public\.gtt_test" as logical replication target
+.*DETAIL:  This operation is not supported for global temporary relations\./,
+	"could not use global temporary table as subscriber");
+
+# Setup logical replication for permanent table -- OK
+$node_subscriber->safe_psql('postgres', qq(
+	CREATE SUBSCRIPTION regress_sub
+		CONNECTION '$publisher_connstr' PUBLICATION regress_perm_pub
+));
+
+# Alter subscription to use GTT -- should fail
+($ret, $stdout, $stderr) =
+	$node_subscriber->psql('postgres',
+		"ALTER SUBSCRIPTION regress_sub SET PUBLICATION regress_gtt_pub;");
+like(
+	$stderr,
+	qr/ERROR:  cannot use relation "public\.gtt_test" as logical replication target
+.*DETAIL:  This operation is not supported for global temporary relations\./,
+	"could not use global temporary table as subscriber");
+
+# Replace the subscriber table with a permanent one and try again
+$node_subscriber->safe_psql('postgres', qq(
+	DROP TABLE gtt_test;
+	CREATE TABLE gtt_test (a int);
+	ALTER SUBSCRIPTION regress_sub SET PUBLICATION regress_gtt_pub;
+));
+
+# Wait for initial table sync to finish
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'regress_sub');
+
+# Replace the subscriber table with a global temporary table again
+$node_subscriber->safe_psql('postgres', qq(
+	DROP TABLE gtt_test;
+	CREATE GLOBAL TEMP TABLE gtt_test (a int);
+));
+
+# Insert another row in the publisher table
+my $offset = -s $node_subscriber->logfile;
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO gtt_test VALUES (2)");
+
+# Verify that an error is logged
+$offset = $node_subscriber->wait_for_log(
+	qr/ERROR:  cannot use relation "public\.gtt_test" as logical replication target
+.*DETAIL:  This operation is not supported for global temporary relations\./,
+	$offset);
+
+$node_subscriber->stop;
+$node_publisher->stop;
+
+done_testing();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0dc817cc2b8..379ce91116f 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1144,6 +1144,7 @@ GistTsVectorOptions
 GistVacState
 GlobalChannelEntry
 GlobalChannelKey
+GlobalTempRelShmemControl
 GlobalTransaction
 GlobalTransactionData
 GlobalVisHorizonKind
@@ -1167,6 +1168,12 @@ GroupingSet
 GroupingSetData
 GroupingSetKind
 GroupingSetsPath
+GtrInfo
+GtrInfoHistory
+GtrSharedUsageEntry
+GtrSharedUsageKey
+GtrStorageEntry
+GtrUsageEntry
 GucAction
 GucBoolAssignHook
 GucBoolCheckHook
-- 
2.51.0

