From ca186eab164b5ba289ca0f5bd8c03e507647e52e 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 v10 02/11] 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.

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

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
 - ALTER TABLE ... SET TABLESPACE
 - CLUSTER
 - REINDEX
 - REPACK
 - TRUNCATE
 - VACUUM
---
 contrib/pg_prewarm/pg_prewarm.c               |   21 +
 doc/src/sgml/catalogs.sgml                    |    6 +-
 doc/src/sgml/logical-replication.sgml         |    5 +-
 doc/src/sgml/ref/alter_table.sgml             |    8 +
 doc/src/sgml/ref/create_table.sgml            |  140 +-
 src/backend/access/heap/heapam_handler.c      |    5 +-
 src/backend/access/transam/xact.c             |   26 +
 src/backend/catalog/Makefile                  |    1 +
 src/backend/catalog/catalog.c                 |    1 +
 src/backend/catalog/global_temp.c             | 1299 +++++++++++++++++
 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/propgraphcmds.c          |    5 +
 src/backend/commands/repack.c                 |   20 +
 src/backend/commands/subscriptioncmds.c       |   21 +
 src/backend/commands/tablecmds.c              |  230 ++-
 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                |    3 +
 src/backend/utils/cache/relcache.c            |   52 +-
 src/backend/utils/cache/relfilenumbermap.c    |    3 +-
 src/bin/pg_amcheck/pg_amcheck.c               |   19 +-
 src/bin/pg_dump/pg_dump.c                     |   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             |   36 +
 src/include/catalog/pg_class.h                |    3 +-
 src/include/catalog/storage.h                 |    2 +-
 src/include/storage/lwlocklist.h              |    3 +
 src/include/storage/subsystemlist.h           |    3 +
 src/include/utils/rel.h                       |   23 +-
 src/test/isolation/expected/global-temp.out   |  282 ++++
 src/test/isolation/isolation_schedule         |    1 +
 src/test/isolation/specs/global-temp.spec     |   75 +
 .../test_checksums/t/010_global_temp.pl       |   56 +
 src/test/regress/expected/alter_table.out     |   12 +-
 .../expected/create_property_graph.out        |    2 +
 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     |  297 ++++
 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 +-
 .../regress/sql/create_property_graph.sql     |    1 +
 src/test/regress/sql/create_table.sql         |    8 +
 src/test/regress/sql/global_temp.sql          |  168 +++
 src/test/regress/sql/type_sanity.sql          |    2 +-
 src/test/subscription/t/039_global_temp.pl    |  100 ++
 src/tools/pgindent/typedefs.list              |    5 +
 72 files changed, 3155 insertions(+), 217 deletions(-)
 create mode 100644 src/backend/catalog/global_temp.c
 create mode 100644 src/include/catalog/global_temp.h
 create mode 100644 src/test/isolation/expected/global-temp.out
 create mode 100644 src/test/isolation/specs/global-temp.spec
 create mode 100644 src/test/modules/test_checksums/t/010_global_temp.pl
 create mode 100644 src/test/regress/expected/global_temp.out
 create mode 100644 src/test/regress/sql/global_temp.sql
 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 eb2c9a82f1a..69a6f608303 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2159,8 +2159,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/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 e342585c7f0..8ad269461cc 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>
@@ -2415,51 +2443,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/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index bf87430cf01..54ae5e27efa 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..aa520ff6fdc 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"
@@ -2345,6 +2346,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 +2470,9 @@ CommitTransaction(void)
 	/* Clean up the relation cache */
 	AtEOXact_RelationCache(true);
 
+	/* Clean up storage and usage records for global temporary relations */
+	AtEOXact_GlobalTempRelation(true);
+
 	/* Clean up the type cache */
 	AtEOXact_TypeCache();
 
@@ -2608,6 +2620,12 @@ PrepareTransaction(void)
 	/* Shut down the deferred-trigger manager */
 	AfterTriggerEndXact(true);
 
+	/*
+	 * Skip pre-commit processing for global temporary relations --- don't
+	 * want to process SI messages that might operate on global temporary
+	 * relations.
+	 */
+
 	/*
 	 * Let ON COMMIT management do its thing (must happen after closing
 	 * cursors, to avoid dangling-reference problems)
@@ -2770,6 +2788,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 +3041,7 @@ AbortTransaction(void)
 		AtEOXact_Aio(false);
 		AtEOXact_Buffers(false);
 		AtEOXact_RelationCache(false);
+		AtEOXact_GlobalTempRelation(false);
 		AtEOXact_TypeCache();
 		AtEOXact_Inval(false);
 		AtEOXact_MultiXact();
@@ -5213,6 +5235,8 @@ CommitSubTransaction(void)
 						 true, false);
 	AtEOSubXact_RelationCache(true, s->subTransactionId,
 							  s->parent->subTransactionId);
+	AtEOSubXact_GlobalTempRelation(true, s->subTransactionId,
+								   s->parent->subTransactionId);
 	AtEOSubXact_TypeCache();
 	AtEOSubXact_Inval(true);
 	AtSubCommit_smgr();
@@ -5399,6 +5423,8 @@ AbortSubTransaction(void)
 		AtEOXact_Aio(false);
 		AtEOSubXact_RelationCache(false, s->subTransactionId,
 								  s->parent->subTransactionId);
+		AtEOSubXact_GlobalTempRelation(false, s->subTransactionId,
+									   s->parent->subTransactionId);
 		AtEOSubXact_TypeCache();
 		AtEOSubXact_Inval(false);
 		ResourceOwnerRelease(s->curTransactionOwner,
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index 26fa0c9b18c..0fb085fd8ee 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -17,6 +17,7 @@ OBJS = \
 	aclchk.o \
 	catalog.o \
 	dependency.o \
+	global_temp.o \
 	heap.o \
 	index.o \
 	indexing.o \
diff --git a/src/backend/catalog/catalog.c b/src/backend/catalog/catalog.c
index 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..38d84a7f2c4
--- /dev/null
+++ b/src/backend/catalog/global_temp.c
@@ -0,0 +1,1299 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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 "lib/dshash.h"
+#include "miscadmin.h"
+#include "storage/ipc.h"
+#include "storage/lwlock.h"
+#include "storage/shmem.h"
+#include "storage/subsystems.h"
+#include "utils/memutils.h"
+#include "utils/syscache.h"
+#include "utils/tuplestore.h"
+
+/*
+ * gtr_local_storage
+ *
+ *	Hash table to track local storage created by this backend for global
+ *	temporary relations.
+ */
+typedef struct GtrStorageEntry
+{
+	RelFileLocator rlocator;	/* lookup key: relfilelocator of storage */
+	Oid			relid;			/* OID of relation owning the storage */
+	SubTransactionId created_subid; /* storage was created in current xact */
+	SubTransactionId dropped_subid; /* dropped with another subid set */
+} GtrStorageEntry;
+
+static HTAB *gtr_local_storage;
+
+/*
+ * eoxact_storage_list[]
+ *
+ *	List of relfilelocators of storage that (might) need AtEOXact cleanup
+ *	work.  As with the relcache's eoxact_list[], this list intentionally has
+ *	limited size, and we switch to a full hash table traversal if the list
+ *	overflows.
+ */
+#define MAX_EOXACT_STORAGE_LIST 32
+static RelFileLocator eoxact_storage_list[MAX_EOXACT_STORAGE_LIST];
+static int	eoxact_storage_list_len = 0;
+static bool eoxact_storage_list_overflowed = false;
+
+#define EOXactStorageListAdd(rlocator) \
+	do { \
+		if (eoxact_storage_list_len < MAX_EOXACT_STORAGE_LIST) \
+			eoxact_storage_list[eoxact_storage_list_len++] = (rlocator); \
+		else \
+			eoxact_storage_list_overflowed = true; \
+	} while (0)
+
+/*
+ * gtr_local_usage
+ *
+ *	Hash table to track global temporary relations in use in this backend.
+ */
+typedef struct GtrUsageEntry
+{
+	Oid			relid;			/* lookup key: OID of relation in use */
+	SubTransactionId started_subid; /* usage started in current xact */
+	SubTransactionId stopped_subid; /* usage ended with another subid set */
+} GtrUsageEntry;
+
+static HTAB *gtr_local_usage;
+
+/*
+ * eoxact_usage_list[]
+ *
+ *	List of OIDs of global temporary relation usage entries that (might) need
+ *	AtEOXact cleanup work.  Cf. eoxact_storage_list[].
+ */
+#define MAX_EOXACT_USAGE_LIST 32
+static Oid	eoxact_usage_list[MAX_EOXACT_USAGE_LIST];
+static int	eoxact_usage_list_len = 0;
+static bool eoxact_usage_list_overflowed = false;
+
+#define EOXactUsageListAdd(relid) \
+	do { \
+		if (eoxact_usage_list_len < MAX_EOXACT_USAGE_LIST) \
+			eoxact_usage_list[eoxact_usage_list_len++] = (relid); \
+		else \
+			eoxact_usage_list_overflowed = true; \
+	} while (0)
+
+/*
+ * Invalidation message handling:
+ *
+ *	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.
+ *
+ *	Note: This is intentionally idempotent --- it does nothing if we already
+ *	have usage records for this relation.
+ */
+static void
+gtr_record_usage(Oid relid)
+{
+	GtrUsageEntry *local_entry;
+	GtrSharedUsageKey key;
+	GtrSharedUsageEntry *shared_entry;
+	bool		found;
+
+	/* Initialize the usage tables, if necessary */
+	gtr_init_usage_tables();
+
+	/* Add local usage entry, if not already there */
+	local_entry = hash_search(gtr_local_usage, &relid, HASH_ENTER, &found);
+	if (found)
+		return;					/* already recorded, nothing to do */
+
+	/* Record the usage as starting in the current subtransaction */
+	local_entry->started_subid = GetCurrentSubTransactionId();
+	local_entry->stopped_subid = InvalidSubTransactionId;
+
+	/* Flag the usage entry for eoxact cleanup */
+	EOXactUsageListAdd(relid);
+
+	/* Add/update shared usage entry */
+	key.dbid = MyDatabaseId;
+	key.relid = relid;
+	shared_entry = dshash_find_or_insert_extended(gtr_shared_usage,
+												  &key, &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 (found)
+		shared_entry->usage_count++;
+	else
+		shared_entry->usage_count = 1;
+
+	dshash_release_lock(gtr_shared_usage, shared_entry);
+}
+
+/*
+ * gtr_remove_usage
+ *
+ *	Remove our usage records for a global temporary relation that we're no
+ *	longer using.
+ *
+ *	Note: This is intentionally idempotent --- 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);
+	}
+	else
+	{
+		entry->started_subid = InvalidSubTransactionId;
+		entry->stopped_subid = InvalidSubTransactionId;
+	}
+}
+
+/*
+ * 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;
+	}
+}
+
+/*
+ * 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 = gtr_local_storage == NULL ? NULL :
+			hash_search(gtr_local_storage, &rlocator, HASH_FIND, NULL);
+		if (entry == NULL)
+			elog(ERROR, "Storage not found for relation %u", relid);
+
+		entry->dropped_subid = GetCurrentSubTransactionId();
+	}
+
+	/* Flag the storage for eoxact cleanup */
+	EOXactStorageListAdd(rlocator);
+}
+
+/*
+ * ReassignGlobalTempRelationStorage
+ *
+ *	Reassign global temporary relation storage to a different relation.  This
+ *	is needed for operations such as ALTER TABLE and REPACK, that rewrite a
+ *	relation's contents by building a transient relation and then swapping its
+ *	storage with the original relation.  We must mark the new storage as
+ *	belonging to the original relation here, otherwise it would be deleted
+ *	when the transient relation is dropped.
+ *
+ *	Note: we have no way of undoing this reassignment in case of rollback, so
+ *	we do not assign the original storage to the transient relation, since
+ *	that would leave it in an invalid state after rollback.  This isn't a
+ *	problem for the new storage, since that is dropped on rollback.  Thus,
+ *	this operates in the same way as TRUNCATE, 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 = hash_search(gtr_local_storage, &rlocator, HASH_FIND, NULL);
+	if (entry == NULL)
+		elog(ERROR, "could not find global temp relation storage {spcOid: %u, dbOid: %u, relNumber: %u}",
+			 rlocator.spcOid, rlocator.dbOid, rlocator.relNumber);
+
+	/* Reassign it */
+	entry->relid = newRelid;
+}
+
+/*
+ * InitGlobalTempRelation
+ *
+ *	Initialize a global temporary relation for use in this backend, if we
+ *	haven't already done so.
+ *
+ *	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) &&
+		(gtr_local_storage == NULL ||
+		 hash_search(gtr_local_storage,
+					 &relation->rd_locator, HASH_FIND, NULL) == NULL))
+	{
+		/* Create (and track) storage for the relation */
+		if (RELKIND_HAS_TABLE_AM(relation->rd_rel->relkind))
+			table_relation_set_new_filelocator(relation,
+											   &relation->rd_locator,
+											   relation->rd_rel->relpersistence,
+											   &relation->rd_rel->relfrozenxid,
+											   &relation->rd_rel->relminmxid);
+		else
+			RelationCreateStorage(relation->rd_id,
+								  relation->rd_locator,
+								  relation->rd_rel->relpersistence,
+								  true);
+
+		/*
+		 * Register the 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)
+{
+	/* Record our use of the relation */
+	gtr_record_usage(relation->rd_id);
+}
+
+/*
+ * 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 = hash_search(gtr_local_usage, &relid, HASH_FIND, NULL);
+	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 (hash_search(gtr_local_usage, &relid, HASH_FIND, 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 = hash_search(gtr_local_usage, &relid, HASH_FIND, NULL);
+			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 = hash_search(gtr_local_storage,
+										&eoxact_storage_list[i],
+										HASH_FIND, NULL);
+			if (storage_entry)
+				AtEOXact_StorageCleanup(storage_entry, isCommit);
+		}
+	}
+
+	/* Similarly, cleanup usage records */
+	if (eoxact_usage_list_overflowed)
+	{
+		hash_seq_init(&status, gtr_local_usage);
+		while ((usage_entry = hash_seq_search(&status)) != NULL)
+		{
+			AtEOXact_UsageCleanup(usage_entry, isCommit);
+		}
+	}
+	else
+	{
+		for (int i = 0; i < eoxact_usage_list_len; i++)
+		{
+			usage_entry = hash_search(gtr_local_usage, &eoxact_usage_list[i],
+									  HASH_FIND, NULL);
+			if (usage_entry)
+				AtEOXact_UsageCleanup(usage_entry, isCommit);
+		}
+	}
+
+	/* Now we're out of the transaction and can clear the lists */
+	eoxact_storage_list_len = 0;
+	eoxact_storage_list_overflowed = false;
+	eoxact_usage_list_len = 0;
+	eoxact_usage_list_overflowed = false;
+
+	/*
+	 * 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 = hash_search(gtr_local_storage,
+										&eoxact_storage_list[i],
+										HASH_FIND, NULL);
+			if (storage_entry)
+				AtEOSubXact_StorageCleanup(storage_entry, isCommit, mySubid,
+										   parentSubid);
+		}
+	}
+
+	/* Similarly, cleanup usage records */
+	if (eoxact_usage_list_overflowed)
+	{
+		hash_seq_init(&status, gtr_local_usage);
+		while ((usage_entry = hash_seq_search(&status)) != NULL)
+		{
+			AtEOSubXact_UsageCleanup(usage_entry, isCommit, mySubid,
+									 parentSubid);
+		}
+	}
+	else
+	{
+		for (int i = 0; i < eoxact_usage_list_len; i++)
+		{
+			usage_entry = hash_search(gtr_local_usage, &eoxact_usage_list[i],
+									  HASH_FIND, NULL);
+			if (usage_entry)
+				AtEOSubXact_UsageCleanup(usage_entry, isCommit, mySubid,
+										 parentSubid);
+		}
+	}
+
+	/* 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.
+ */
+bool
+IsGlobalTempRelationInUse(Oid relid)
+{
+	return gtr_local_usage != NULL &&
+		hash_search(gtr_local_usage, &relid, HASH_FIND, NULL) != 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;
+}
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 63543dc153f..75cd7e09fd1 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -36,6 +36,7 @@
 #include "access/tableam.h"
 #include "catalog/binary_upgrade.h"
 #include "catalog/catalog.h"
+#include "catalog/global_temp.h"
 #include "catalog/heap.h"
 #include "catalog/index.h"
 #include "catalog/objectaccess.h"
@@ -389,7 +390,8 @@ heap_create(const char *relname,
 											   relpersistence,
 											   relfrozenxid, relminmxid);
 		else if (RELKIND_HAS_STORAGE(rel->rd_rel->relkind))
-			RelationCreateStorage(rel->rd_locator, relpersistence, true);
+			RelationCreateStorage(rel->rd_id, rel->rd_locator,
+								  relpersistence, true);
 		else
 			Assert(false);
 	}
@@ -398,8 +400,13 @@ heap_create(const char *relname,
 	 * If a tablespace is specified, removal of that tablespace is normally
 	 * protected by the existence of a physical file; but for relations with
 	 * no files, add a pg_shdepend entry to account for that.
+	 *
+	 * Note, however, that although global temporary relations may have files,
+	 * those files will go away at the end of the session, and so provide no
+	 * protection, and we must add a pg_shdepend entry in this case too.
 	 */
-	if (!create_storage && reltablespace != InvalidOid)
+	if ((!create_storage || relpersistence == RELPERSISTENCE_GLOBAL_TEMP) &&
+		reltablespace != InvalidOid)
 		recordDependencyOnTablespace(RelationRelationId, relid,
 									 reltablespace);
 
@@ -992,6 +999,10 @@ InsertPgClassTuple(Relation pg_class_desc,
 	CatalogTupleInsert(pg_class_desc, tup);
 
 	heap_freetuple(tup);
+
+	/* If it's a global temporary relation, track our use of it */
+	if (RELATION_IS_GLOBAL_TEMP(new_rel_desc))
+		TrackGlobalTempRelation(new_rel_desc);
 }
 
 /* --------------------------------
@@ -1601,6 +1612,7 @@ DeleteRelationTuple(Oid relid)
 {
 	Relation	pg_class_desc;
 	HeapTuple	tup;
+	char		relpersistence;
 
 	/* Grab an appropriate lock on the pg_class relation */
 	pg_class_desc = table_open(RelationRelationId, RowExclusiveLock);
@@ -1608,6 +1620,7 @@ DeleteRelationTuple(Oid relid)
 	tup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
 	if (!HeapTupleIsValid(tup))
 		elog(ERROR, "cache lookup failed for relation %u", relid);
+	relpersistence = ((Form_pg_class) GETSTRUCT(tup))->relpersistence;
 
 	/* delete the relation tuple from pg_class, and finish up */
 	CatalogTupleDelete(pg_class_desc, &tup->t_self);
@@ -1615,6 +1628,10 @@ DeleteRelationTuple(Oid relid)
 	ReleaseSysCache(tup);
 
 	table_close(pg_class_desc, RowExclusiveLock);
+
+	/* 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 624d538a5c0..1d2262e7c10 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 b2daf9adb32..4649bbbe665 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 12af7d15536..e57e4ea09ec 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -101,7 +101,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/propgraphcmds.c b/src/backend/commands/propgraphcmds.c
index 2bdce331c49..ec38ba68842 100644
--- a/src/backend/commands/propgraphcmds.c
+++ b/src/backend/commands/propgraphcmds.c
@@ -119,6 +119,11 @@ CreatePropGraph(ParseState *pstate, const CreatePropGraphStmt *stmt)
 				(errcode(ERRCODE_SYNTAX_ERROR),
 				 errmsg("property graphs cannot be unlogged because they do not have storage")));
 
+	if (stmt->pgname->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("property graphs cannot be global temporary because they do not have storage")));
+
 	components_persistence = RELPERSISTENCE_PERMANENT;
 
 	foreach(lc, stmt->vertex_tables)
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 477c86b2ba6..95a13aadb68 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"
@@ -1644,6 +1645,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);
 	}
@@ -2154,6 +2164,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))
@@ -2191,6 +2206,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 fd144d783d9..a0f5dfecd0d 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.
@@ -820,11 +822,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)
 	{
@@ -1690,7 +1698,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);
@@ -2520,6 +2529,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.
@@ -2755,27 +2891,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))
@@ -3833,10 +3950,12 @@ SetRelationTableSpace(Relation rel,
 	UnlockTuple(pg_class, &otid, InplaceUpdateTupleLock);
 
 	/*
-	 * Record dependency on tablespace.  This is only required for relations
-	 * that have no physical storage.
+	 * Record dependency on tablespace.  This is required for relations that
+	 * have no physical storage, and for global temporary relations whose
+	 * physical storage is temporary.
 	 */
-	if (!RELKIND_HAS_STORAGE(rel->rd_rel->relkind))
+	if (!RELKIND_HAS_STORAGE(rel->rd_rel->relkind) ||
+		RELATION_IS_GLOBAL_TEMP(rel))
 		changeDependencyOnTablespace(RelationRelationId, reloid,
 									 rd_rel->reltablespace);
 
@@ -5990,6 +6109,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)
@@ -6086,11 +6217,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,
@@ -6652,6 +6794,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;
@@ -10248,11 +10391,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;
 	}
 
@@ -17845,7 +17994,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,
@@ -19533,6 +19683,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",
@@ -21126,21 +21277,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 e3c4a7fac87..a2097bede77 100644
--- a/src/backend/commands/tablespace.c
+++ b/src/backend/commands/tablespace.c
@@ -1159,7 +1159,8 @@ GetDefaultTablespace(char relpersistence, bool partitioned)
 	Oid			result;
 
 	/* The temp-table case is handled elsewhere */
-	if (relpersistence == RELPERSISTENCE_TEMP)
+	if (relpersistence == RELPERSISTENCE_TEMP ||
+		relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
 	{
 		PrepareTempTablespaces();
 		return GetNextTempTableSpace();
diff --git a/src/backend/commands/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 24a6a8d11dd..4c15086c3c8 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 263d1b6e1cc..defd7e56efe 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -3538,8 +3538,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 c025eaaaa4e..f84bdb8dab9 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -3871,31 +3871,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; }
 		;
@@ -13928,19 +13918,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 f69258bc33d..c35bc8b709e 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -192,6 +192,7 @@
 #include "access/xact.h"
 #include "access/xlog.h"
 #include "access/xloginsert.h"
+#include "catalog/global_temp.h"
 #include "catalog/indexing.h"
 #include "catalog/pg_class.h"
 #include "catalog/pg_database.h"
@@ -1692,6 +1693,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
 		{
 			/*
@@ -1715,6 +1721,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 17f142e4c5b..9a7b4a9ea7d 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 0a70cffa081..e0003e4f05a 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -371,6 +371,7 @@ WaitLSN	"Waiting to read or update shared Wait-for-LSN state."
 LogicalDecodingControl	"Waiting to read or update logical decoding status information."
 DataChecksumsWorker	"Waiting for data checksums worker."
 AioWorkerControl	"Waiting to update AIO worker information."
+GlobalTempRelControl	"Waiting to update global temporary relation information."
 
 #
 # END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE)
@@ -418,6 +419,8 @@ XactSLRU	"Waiting to access the transaction status SLRU cache."
 ParallelVacuumDSA	"Waiting for parallel vacuum dynamic shared memory allocation."
 AioUringCompletion	"Waiting for another process to complete IO via io_uring."
 ShmemIndex	"Waiting to find or allocate space in shared memory."
+GlobalTempRelDSA	"Waiting for global temporary relation dynamic shared memory allocation."
+GlobalTempRelHash	"Waiting to access global temporary relation shared usage table."
 
 # No "ABI_compatibility" region here as WaitEventLWLock has its own C code.
 
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index cccc4a24c84..e09ea8fe220 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -1032,6 +1032,9 @@ pg_relation_filepath(PG_FUNCTION_ARGS)
 				Assert(backend != INVALID_PROC_NUMBER);
 			}
 			break;
+		case RELPERSISTENCE_GLOBAL_TEMP:
+			backend = ProcNumberForTempRelations();
+			break;
 		default:
 			elog(ERROR, "invalid relpersistence: %c", relform->relpersistence);
 			backend = INVALID_PROC_NUMBER;	/* placate compiler */
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index c5d2bcdc164..3aec71824ec 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -41,6 +41,7 @@
 #include "access/xact.h"
 #include "catalog/binary_upgrade.h"
 #include "catalog/catalog.h"
+#include "catalog/global_temp.h"
 #include "catalog/indexing.h"
 #include "catalog/namespace.h"
 #include "catalog/partition.h"
@@ -1172,8 +1173,9 @@ retry:
 			else
 			{
 				/*
-				 * If it's a temp table, but not one of ours, we have to use
-				 * the slow, grotty method to figure out the owning backend.
+				 * If it's a local temp table, but not one of ours, we have to
+				 * use the slow, grotty method to figure out the owning
+				 * backend.
 				 *
 				 * Note: it's possible that rd_backend gets set to
 				 * MyProcNumber here, in case we are looking at a pg_class
@@ -1190,6 +1192,10 @@ retry:
 				relation->rd_islocaltemp = false;
 			}
 			break;
+		case RELPERSISTENCE_GLOBAL_TEMP:
+			relation->rd_backend = ProcNumberForTempRelations();
+			relation->rd_islocaltemp = false;
+			break;
 		default:
 			elog(ERROR, "invalid relpersistence: %c",
 				 relation->rd_rel->relpersistence);
@@ -2115,6 +2121,14 @@ RelationIdGetRelation(Oid relationId)
 		{
 			RelationRebuildRelation(rd);
 
+			/*
+			 * If it's a global temporary relation, make sure it has been
+			 * initialized for use in this backend (a prior initialization
+			 * might have been rolled back).
+			 */
+			if (RELATION_IS_GLOBAL_TEMP(rd))
+				InitGlobalTempRelation(rd);
+
 			/*
 			 * Normally entries need to be valid here, but before the relcache
 			 * has been initialized, not enough infrastructure exists to
@@ -2134,7 +2148,11 @@ RelationIdGetRelation(Oid relationId)
 	 */
 	rd = RelationBuildDesc(relationId, true);
 	if (RelationIsValid(rd))
+	{
 		RelationIncrementReferenceCount(rd);
+		if (RELATION_IS_GLOBAL_TEMP(rd))
+			InitGlobalTempRelation(rd);
+	}
 	return rd;
 }
 
@@ -2208,6 +2226,21 @@ RelationDecrementReferenceCount(Relation rel)
 		ResourceOwnerForgetRelationRef(CurrentResourceOwner, rel);
 }
 
+/*
+ * RelationMarkInvalid
+ *		Mark a relation as invalid, if it's in the relcache, forcing it to be
+ *		reloaded on next access.
+ */
+void
+RelationMarkInvalid(Oid relid)
+{
+	Relation	relation;
+
+	RelationIdCacheLookup(relid, relation);
+	if (RelationIsValid(relation) && relation->rd_isvalid)
+		RelationInvalidateRelation(relation);
+}
+
 /*
  * RelationClose - close an open relation
  *
@@ -2957,6 +2990,9 @@ RelationCacheInvalidateEntry(Oid relationId)
 			if (in_progress_list[i].reloid == relationId)
 				in_progress_list[i].invalidated = true;
 	}
+
+	/* Additional processing required for global temporary relations */
+	InvalidateGlobalTempRelation(relationId);
 }
 
 /*
@@ -3101,6 +3137,9 @@ RelationCacheInvalidate(bool debug_discard)
 		/* Any RelationBuildDesc() on the stack must start over. */
 		for (i = 0; i < in_progress_list_len; i++)
 			in_progress_list[i].invalidated = true;
+
+	/* Invalidate all in-use global temporary relations */
+	InvalidateGlobalTempRelation(InvalidOid);
 }
 
 static void
@@ -3656,6 +3695,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;
@@ -3664,6 +3704,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;
@@ -3902,7 +3947,8 @@ RelationSetNewRelfilenumber(Relation relation, char persistence)
 		/* handle these directly, at least for now */
 		SMgrRelation srel;
 
-		srel = RelationCreateStorage(newrlocator, persistence, true);
+		srel = RelationCreateStorage(relation->rd_id, newrlocator,
+									 persistence, true);
 		smgrclose(srel);
 	}
 	else
diff --git a/src/backend/utils/cache/relfilenumbermap.c b/src/backend/utils/cache/relfilenumbermap.c
index 6f970fafa05..1f9fe87ebb0 100644
--- a/src/backend/utils/cache/relfilenumbermap.c
+++ b/src/backend/utils/cache/relfilenumbermap.c
@@ -213,7 +213,8 @@ RelidByRelfilenumber(Oid reltablespace, RelFileNumber relfilenumber)
 		{
 			Form_pg_class classform = (Form_pg_class) GETSTRUCT(ntp);
 
-			if (classform->relpersistence == RELPERSISTENCE_TEMP)
+			if (classform->relpersistence == RELPERSISTENCE_TEMP ||
+				classform->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
 				continue;
 
 			if (found)
diff --git a/src/bin/pg_amcheck/pg_amcheck.c b/src/bin/pg_amcheck/pg_amcheck.c
index 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 b5b257f9983..525cd102a86 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -3024,6 +3024,10 @@ makeTableDataInfo(DumpOptions *dopt, TableInfo *tbinfo)
 	if (tbinfo->relkind == RELKIND_PARTITIONED_TABLE)
 		return;
 
+	/* Don't dump data in global temporary tables */
+	if (tbinfo->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
+		return;
+
 	/* Don't dump data in unlogged tables, if so requested */
 	if (tbinfo->relpersistence == RELPERSISTENCE_UNLOGGED &&
 		dopt->no_unlogged_table_data)
@@ -7170,6 +7174,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;
@@ -7220,6 +7225,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, "
@@ -7370,6 +7381,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");
@@ -7448,6 +7460,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);
@@ -17235,7 +17248,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);
 
@@ -17486,6 +17501,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 9258948b583..1cc296fff55 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -4988,7 +4988,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 7a75b3ef720..c62ae57e9f5 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2027,13 +2027,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\""),
@@ -2071,6 +2081,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);
@@ -4190,10 +4206,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 1b74fa62c5c..bc74d8d546a 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -1319,10 +1319,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. */
@@ -3834,8 +3838,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"))
@@ -3851,36 +3866,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..a2d9163f2f1
--- /dev/null
+++ b/src/include/catalog/global_temp.h
@@ -0,0 +1,36 @@
+/*-------------------------------------------------------------------------
+ *
+ * global_temp.h
+ *	  Global temporary relation management.
+ *
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * src/include/catalog/global_temp.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef GLOBAL_TEMP_H
+#define GLOBAL_TEMP_H
+
+#include "storage/relfilelocator.h"
+#include "utils/rel.h"
+
+extern void TrackGlobalTempRelationStorage(Oid relid, RelFileLocator rlocator,
+										   ProcNumber backend, bool create);
+extern void ReassignGlobalTempRelationStorage(RelFileLocator rlocator,
+											  Oid newRelid);
+extern void InitGlobalTempRelation(Relation relation);
+extern void 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);
+
+#endif							/* GLOBAL_TEMP_H */
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index c5e84816127..ad509d59c4d 100644
--- a/src/include/catalog/pg_class.h
+++ b/src/include/catalog/pg_class.h
@@ -185,7 +185,8 @@ MAKE_SYSCACHE(RELNAMENSP, pg_class_relname_nsp_index, 128);
 
 #define		  RELPERSISTENCE_PERMANENT	'p' /* regular table */
 #define		  RELPERSISTENCE_UNLOGGED	'u' /* unlogged permanent table */
-#define		  RELPERSISTENCE_TEMP		't' /* temporary table */
+#define		  RELPERSISTENCE_TEMP		't' /* temp table (in temp schema) */
+#define		  RELPERSISTENCE_GLOBAL_TEMP 'g'	/* global temporary table */
 
 /* on-commit action; only temporary tables support values other than 'n' */
 #define		  RELONCOMMIT_NONE			'n' /* default: no action */
diff --git a/src/include/catalog/storage.h b/src/include/catalog/storage.h
index 70f619a6d6f..7827c4ba710 100644
--- a/src/include/catalog/storage.h
+++ b/src/include/catalog/storage.h
@@ -22,7 +22,7 @@
 /* GUC variables */
 extern PGDLLIMPORT int wal_skip_threshold;
 
-extern SMgrRelation RelationCreateStorage(RelFileLocator rlocator,
+extern SMgrRelation RelationCreateStorage(Oid relid, RelFileLocator rlocator,
 										  char relpersistence,
 										  bool register_delete);
 extern void RelationDropStorage(Relation rel);
diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h
index d7eb648bd27..557ff4dcbe3 100644
--- a/src/include/storage/lwlocklist.h
+++ b/src/include/storage/lwlocklist.h
@@ -89,6 +89,7 @@ PG_LWLOCK(54, WaitLSN)
 PG_LWLOCK(55, LogicalDecodingControl)
 PG_LWLOCK(56, DataChecksumsWorker)
 PG_LWLOCK(57, AioWorkerControl)
+PG_LWLOCK(58, GlobalTempRelControl)
 
 /*
  * There also exist several built-in LWLock tranches.  As with the predefined
@@ -140,3 +141,5 @@ PG_LWLOCKTRANCHE(XACT_SLRU, XactSLRU)
 PG_LWLOCKTRANCHE(PARALLEL_VACUUM_DSA, ParallelVacuumDSA)
 PG_LWLOCKTRANCHE(AIO_URING_COMPLETION, AioUringCompletion)
 PG_LWLOCKTRANCHE(SHMEM_INDEX, ShmemIndex)
+PG_LWLOCKTRANCHE(GLOBAL_TEMP_REL_DSA, GlobalTempRelDSA)
+PG_LWLOCKTRANCHE(GLOBAL_TEMP_REL_HASH, GlobalTempRelHash)
diff --git a/src/include/storage/subsystemlist.h b/src/include/storage/subsystemlist.h
index 9ad619080be..89378d1d87f 100644
--- a/src/include/storage/subsystemlist.h
+++ b/src/include/storage/subsystemlist.h
@@ -88,3 +88,6 @@ PG_SHMEM_SUBSYSTEM(DataChecksumsShmemCallbacks)
 
 /* AIO subsystem. This delegates to the method-specific callbacks */
 PG_SHMEM_SUBSYSTEM(AioShmemCallbacks)
+
+/* global temporary relation usage table */
+PG_SHMEM_SUBSYSTEM(GlobalTempRelShmemCallbacks)
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 41ab4586c6b..30480513370 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
@@ -647,13 +647,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
  */
@@ -663,7 +665,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,
@@ -680,6 +683,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
@@ -728,5 +738,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..3b50ae0c742
--- /dev/null
+++ b/src/test/isolation/expected/global-temp.out
@@ -0,0 +1,282 @@
+Parsed test spec with 2 sessions
+
+starting permutation: ins1 ins2 sel1 sel2
+step ins1: INSERT INTO tmp VALUES (1, 's1');
+step ins2: INSERT INTO tmp VALUES (1, 's2');
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+  1|s1 
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+  1|s2 
+(1 row)
+
+
+starting permutation: ins1p1 ins1p2 ins2p1 ins2p2 sel1p sel2p
+step ins1p1: INSERT INTO tmp_parted VALUES (1, 's1 p1');
+step ins1p2: INSERT INTO tmp_parted VALUES (2, 's1 p2');
+step ins2p1: INSERT INTO tmp_parted VALUES (1, 's2 p1');
+step ins2p2: INSERT INTO tmp_parted VALUES (2, 's2 p2');
+step sel1p: SELECT tableoid::regclass, * FROM tmp_parted;
+tableoid|key|val  
+--------+---+-----
+tmp_p1  |  1|s1 p1
+tmp_p2  |  2|s1 p2
+(2 rows)
+
+step sel2p: SELECT tableoid::regclass, * FROM tmp_parted;
+tableoid|key|val  
+--------+---+-----
+tmp_p1  |  1|s2 p1
+tmp_p2  |  2|s2 p2
+(2 rows)
+
+
+starting permutation: ins1 b2 ins2 sel1 sel2 c2 sel1 sel2
+step ins1: INSERT INTO tmp VALUES (1, 's1');
+step b2: BEGIN;
+step ins2: INSERT INTO tmp VALUES (1, 's2');
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+  1|s1 
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+  1|s2 
+(1 row)
+
+step c2: COMMIT;
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+  1|s1 
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+  1|s2 
+(1 row)
+
+
+starting permutation: ins1 b2 ins2 sel1 sel2 r2 sel1 sel2
+step ins1: INSERT INTO tmp VALUES (1, 's1');
+step b2: BEGIN;
+step ins2: INSERT INTO tmp VALUES (1, 's2');
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+  1|s1 
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+  1|s2 
+(1 row)
+
+step r2: ROLLBACK;
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+  1|s1 
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+(0 rows)
+
+
+starting permutation: ins1 b2 ins2 sel1 sel2 sp2 r2 sel1 sel2
+step ins1: INSERT INTO tmp VALUES (1, 's1');
+step b2: BEGIN;
+step ins2: INSERT INTO tmp VALUES (1, 's2');
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+  1|s1 
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+  1|s2 
+(1 row)
+
+step sp2: SAVEPOINT sp;
+step r2: ROLLBACK;
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+  1|s1 
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+(0 rows)
+
+
+starting permutation: ins1 b2 sp2 ins2 sel1 sel2 rsp2 sel1 sel2 r2 sel1 sel2
+step ins1: INSERT INTO tmp VALUES (1, 's1');
+step b2: BEGIN;
+step sp2: SAVEPOINT sp;
+step ins2: INSERT INTO tmp VALUES (1, 's2');
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+  1|s1 
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+  1|s2 
+(1 row)
+
+step rsp2: ROLLBACK TO SAVEPOINT sp;
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+  1|s1 
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+(0 rows)
+
+step r2: ROLLBACK;
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+  1|s1 
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+(0 rows)
+
+
+starting permutation: ins1 b2 ins2 sp2 t2 rsp2 sel1 sel2 r2 sel1 sel2
+step ins1: INSERT INTO tmp VALUES (1, 's1');
+step b2: BEGIN;
+step ins2: INSERT INTO tmp VALUES (1, 's2');
+step sp2: SAVEPOINT sp;
+step t2: TRUNCATE tmp;
+step rsp2: ROLLBACK TO SAVEPOINT sp;
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+  1|s1 
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+  1|s2 
+(1 row)
+
+step r2: ROLLBACK;
+step sel1: SELECT * FROM tmp;
+key|val
+---+---
+  1|s1 
+(1 row)
+
+step sel2: SELECT * FROM tmp;
+key|val
+---+---
+(0 rows)
+
+
+starting permutation: create1 ins1_2 alter1a alter1b 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>
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index 1fcf4e63238..c26a0da5d59 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -130,3 +130,4 @@ test: for-portion-of
 test: ddl-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..7ada392f96f
--- /dev/null
+++ b/src/test/isolation/specs/global-temp.spec
@@ -0,0 +1,75 @@
+# Test global temporary relations
+
+setup {
+  CREATE GLOBAL TEMP TABLE tmp (key int, val text);
+
+  CREATE GLOBAL TEMP TABLE tmp_parted (key int, val text) PARTITION BY LIST (key);
+  CREATE GLOBAL TEMP TABLE tmp_p1 PARTITION OF tmp_parted FOR VALUES IN (1);
+  CREATE GLOBAL TEMP TABLE tmp_p2 PARTITION OF tmp_parted FOR VALUES IN ((2), (3));
+}
+
+teardown {
+  DROP TABLE tmp, tmp_parted;
+}
+
+session s1
+step b1 { BEGIN; }
+step ins1 { INSERT INTO tmp VALUES (1, 's1'); }
+step ins1p1 { INSERT INTO tmp_parted VALUES (1, 's1 p1'); }
+step ins1p2 { INSERT INTO tmp_parted VALUES (2, 's1 p2'); }
+step sel1 { SELECT * FROM tmp; }
+step sel1p { SELECT tableoid::regclass, * FROM tmp_parted; }
+step create1 { CREATE GLOBAL TEMP TABLE tmp2 (key int, val text); }
+step create1dr { CREATE GLOBAL TEMP TABLE tmp2 (key int, val text) ON COMMIT DELETE ROWS; }
+step ins1_2 { INSERT INTO tmp2 VALUES (1, 's1'); }
+step alter1a { ALTER TABLE tmp2 ALTER COLUMN key SET DATA TYPE numeric; }
+step alter1b { ALTER TABLE tmp2 ALTER COLUMN val SET NOT NULL; }
+step 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; }
+step prep1 { PREPARE TRANSACTION 'tx'; }
+step cprep1 { COMMIT PREPARED 'tx'; }
+
+session s2
+step b2 { BEGIN; }
+step ins2 { INSERT INTO tmp VALUES (1, 's2'); }
+step ins2p1 { INSERT INTO tmp_parted VALUES (1, 's2 p1'); }
+step ins2p2 { INSERT INTO tmp_parted VALUES (2, 's2 p2'); }
+step sel2 { SELECT * FROM tmp; }
+step sel2p { SELECT tableoid::regclass, * FROM tmp_parted; }
+step t2 { TRUNCATE tmp; }
+step c2 { COMMIT; }
+step r2 { ROLLBACK; }
+step sp2 { SAVEPOINT sp; }
+step rsp2 { ROLLBACK TO SAVEPOINT sp; }
+step ins2_2 { INSERT INTO tmp2 VALUES (1, 's2'); }
+step seltype2 { SELECT key, pg_typeof(key), val FROM tmp2; }
+step drop2 { DROP TABLE tmp2; }
+
+# Basic effects
+permutation ins1 ins2 sel1 sel2
+permutation ins1p1 ins1p2 ins2p1 ins2p2 sel1p sel2p
+
+# Test rollback of GTT initialization
+permutation ins1 b2 ins2 sel1 sel2 c2 sel1 sel2
+permutation ins1 b2 ins2 sel1 sel2 r2 sel1 sel2
+permutation ins1 b2 ins2 sel1 sel2 sp2 r2 sel1 sel2
+permutation ins1 b2 sp2 ins2 sel1 sel2 rsp2 sel1 sel2 r2 sel1 sel2
+permutation ins1 b2 ins2 sp2 t2 rsp2 sel1 sel2 r2 sel1 sel2
+
+# Test prevention of ALTER TABLE with rewrite, if in use
+permutation create1 ins1_2
+            alter1a alter1b 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
diff --git a/src/test/modules/test_checksums/t/010_global_temp.pl b/src/test/modules/test_checksums/t/010_global_temp.pl
new file mode 100644
index 00000000000..4355ebe17bd
--- /dev/null
+++ b/src/test/modules/test_checksums/t/010_global_temp.pl
@@ -0,0 +1,56 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# global temporary tables
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+# Initialize node with checksums disabled.
+my $node = PostgreSQL::Test::Cluster->new('global_temp_table_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Create a global temporary table in an interactive psql process.  Should act
+# as a barrier for checksum enablement to block on.
+my $bsession = $node->background_psql('postgres');
+$bsession->query_safe(
+	'CREATE GLOBAL TEMPORARY TABLE gtt AS SELECT * FROM generate_series(1, 10000) x;');
+
+# Ensure that checksums are disabled
+test_checksum_state($node, 'off');
+
+# In another session, make sure we can see the blocking global temporary table
+# but start processing anyways and check that we are blocked with a proper
+# wait event.
+my $result = $node->safe_psql('postgres',
+	"SELECT relpersistence FROM pg_catalog.pg_class WHERE relname = 'gtt';");
+is($result, 'g', 'ensure we can see the global temporary table');
+
+# Enable, but stop waiting at inprogress-on since it will sit there until the
+# above temporary table is removed.
+enable_data_checksums($node, wait => 'inprogress-on');
+
+# Ensure that checksum enablement continues to block
+sleep(1);
+test_checksum_state($node, 'inprogress-on');
+
+# Make sure background session can still read back its data
+$result = $bsession->query_safe('SELECT count(*) FROM gtt WHERE x % 2 = 0;');
+is($result, '5000', 'ensure global temporary table can still be read');
+
+# Quit background session and check that checksum enablement unblocks
+$bsession->quit;
+wait_for_checksum_state($node, 'on');
+
+$node->stop;
+done_testing();
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index e167a41ce79..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_property_graph.out b/src/test/regress/expected/create_property_graph.out
index 5bbd6477ed6..ae825396620 100644
--- a/src/test/regress/expected/create_property_graph.out
+++ b/src/test/regress/expected/create_property_graph.out
@@ -123,6 +123,8 @@ SELECT pg_get_propgraphdef('g5'::regclass);
 -- error cases
 CREATE UNLOGGED PROPERTY GRAPH gx VERTEX TABLES (xx, yy);
 ERROR:  property graphs cannot be unlogged because they do not have storage
+CREATE GLOBAL TEMP PROPERTY GRAPH gx VERTEX TABLES (xx, yy);
+ERROR:  property graphs cannot be global temporary because they do not have storage
 CREATE PROPERTY GRAPH gx VERTEX TABLES (xx, yy);
 ERROR:  relation "xx" does not exist
 CREATE PROPERTY GRAPH gx VERTEX TABLES (t1 KEY (a), t2 KEY (i), t1 KEY (a));
diff --git a/src/test/regress/expected/create_table.out b/src/test/regress/expected/create_table.out
index 2df8761ae8e..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..a37d01056ab
--- /dev/null
+++ b/src/test/regress/expected/global_temp.out
@@ -0,0 +1,297 @@
+--
+-- GLOBAL TEMP
+--
+CREATE SCHEMA global_temp_tests;
+GRANT USAGE ON SCHEMA global_temp_tests TO PUBLIC;
+SET search_path = global_temp_tests;
+CREATE ROLE regress_global_temp_user;
+GRANT CREATE ON SCHEMA global_temp_tests TO regress_global_temp_user;
+GRANT CREATE ON DATABASE regression TO regress_global_temp_user;
+SET ROLE regress_global_temp_user;
+-- Test table creation
+CREATE GLOBAL TEMP TABLE pg_temp.tmp1 (a int); -- fail
+ERROR:  cannot create global temporary relation in temporary schema
+LINE 1: CREATE GLOBAL TEMP TABLE pg_temp.tmp1 (a int);
+                                 ^
+CREATE GLOBAL TEMP TABLE tmp1 (a int);
+CREATE SCHEMA global_temp_xxx CREATE GLOBAL TEMP TABLE tmp2 (a int);
+CREATE SCHEMA global_temp_yyy;
+CREATE GLOBAL TEMP TABLE global_temp_yyy.tmp3 (a int);
+\d tmp1
+  Global temporary table "global_temp_tests.tmp1"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+
+\dt+ global_temp_*.tmp*
+                                             List of tables
+      Schema       | Name | Type  |          Owner           |   Persistence    |  Size   | Description 
+-------------------+------+-------+--------------------------+------------------+---------+-------------
+ global_temp_tests | tmp1 | table | regress_global_temp_user | global temporary | 0 bytes | 
+ global_temp_xxx   | tmp2 | table | regress_global_temp_user | global temporary | 0 bytes | 
+ global_temp_yyy   | tmp3 | table | regress_global_temp_user | global temporary | 0 bytes | 
+(3 rows)
+
+-- Information schema
+SELECT table_catalog, table_schema, table_name, table_type
+FROM information_schema.tables
+WHERE table_name ~ 'tmp' AND table_schema ~ 'global_temp'
+ORDER BY table_name;
+ table_catalog |   table_schema    | table_name |    table_type    
+---------------+-------------------+------------+------------------
+ regression    | global_temp_tests | tmp1       | GLOBAL TEMPORARY
+ regression    | global_temp_xxx   | tmp2       | GLOBAL TEMPORARY
+ regression    | global_temp_yyy   | tmp3       | GLOBAL TEMPORARY
+(3 rows)
+
+DROP SCHEMA global_temp_xxx CASCADE;
+NOTICE:  drop cascades to table global_temp_xxx.tmp2
+DROP SCHEMA global_temp_yyy CASCADE;
+NOTICE:  drop cascades to table global_temp_yyy.tmp3
+-- Basic tests
+INSERT INTO tmp1 VALUES (1);
+SELECT * FROM tmp1;
+ a 
+---
+ 1
+(1 row)
+
+\c
+SET search_path = global_temp_tests;
+SELECT * FROM tmp1;
+ a 
+---
+(0 rows)
+
+-- Test ON COMMIT DELETE ROWS
+CREATE GLOBAL TEMP TABLE tmp2 (a int) ON COMMIT DELETE ROWS;
+BEGIN;
+INSERT INTO tmp2 VALUES (1);
+SELECT * FROM tmp2;
+ a 
+---
+ 1
+(1 row)
+
+COMMIT;
+SELECT * FROM tmp2;
+ a 
+---
+(0 rows)
+
+-- Repeat test in a new session
+\c
+SET search_path = global_temp_tests;
+BEGIN;
+INSERT INTO tmp2 VALUES (1);
+SELECT * FROM tmp2;
+ a 
+---
+ 1
+(1 row)
+
+COMMIT;
+SELECT * FROM tmp2;
+ a 
+---
+(0 rows)
+
+DROP TABLE tmp2;
+-- ON COMMIT DROP not allowed
+CREATE GLOBAL TEMP TABLE tmp2 (a int) ON COMMIT DROP; -- fail
+ERROR:  ON COMMIT DROP cannot be used on global temporary tables
+-- Two-phase commit not allowed with global temp tables
+BEGIN;
+SELECT * FROM tmp1;
+ a 
+---
+(0 rows)
+
+PREPARE TRANSACTION 'twophase'; -- fail
+ERROR:  cannot PREPARE a transaction that has operated on temporary objects
+-- Test partitioned global temp table
+CREATE GLOBAL TEMP TABLE tmp2 (a int) PARTITION BY LIST (a);
+CREATE GLOBAL TEMP TABLE tmp2_p1 PARTITION OF tmp2 FOR VALUES IN (1);
+CREATE 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
+CREATE GLOBAL TEMP TABLE tmp2 (a int);
+INSERT INTO tmp2 VALUES (1);
+SELECT * FROM tmp2;
+ a 
+---
+ 1
+(1 row)
+
+SELECT regexp_replace(pg_relation_filepath('tmp2'), '(\d+)', 'NNN', 'g');
+  regexp_replace   
+-------------------
+ base/NNN/tNNN_NNN
+(1 row)
+
+ALTER TABLE tmp2 SET TABLESPACE regress_tblspace;
+SELECT * FROM tmp2;
+ a 
+---
+ 1
+(1 row)
+
+SELECT regexp_replace(pg_relation_filepath('tmp2'), '(\d+)', 'NNN', 'g');
+            regexp_replace             
+---------------------------------------
+ pg_tblspc/NNN/PG_NNN_NNN/NNN/tNNN_NNN
+(1 row)
+
+DROP TABLE tmp2;
+-- Test dependency on tablespace
+SET allow_in_place_tablespaces = true;
+CREATE TABLESPACE regress_temp_test_tablespace LOCATION '';
+CREATE GLOBAL TEMP TABLE tmp2 (a int) TABLESPACE regress_temp_test_tablespace;
+\c
+SET search_path = global_temp_tests;
+DROP TABLESPACE regress_temp_test_tablespace; -- fail
+ERROR:  tablespace "regress_temp_test_tablespace" cannot be dropped because some objects depend on it
+DETAIL:  tablespace for table tmp2
+DROP TABLE tmp2;
+DROP TABLESPACE regress_temp_test_tablespace;
+SET allow_in_place_tablespaces = true;
+CREATE TABLESPACE regress_temp_test_tablespace LOCATION '';
+CREATE GLOBAL TEMP TABLE tmp2 (a int);
+ALTER TABLE tmp2 SET TABLESPACE regress_temp_test_tablespace;
+\c
+SET search_path = global_temp_tests;
+DROP TABLESPACE regress_temp_test_tablespace; -- fail
+ERROR:  tablespace "regress_temp_test_tablespace" cannot be dropped because some objects depend on it
+DETAIL:  tablespace for table tmp2
+DROP TABLE tmp2;
+DROP TABLESPACE regress_temp_test_tablespace;
+-- Test TRUNCATE
+INSERT INTO tmp1 VALUES (1);
+BEGIN;
+TRUNCATE tmp1;
+SELECT * FROM tmp1;
+ a 
+---
+(0 rows)
+
+ROLLBACK;
+SELECT * FROM tmp1;
+ a 
+---
+ 1
+(1 row)
+
+BEGIN;
+SAVEPOINT sp1;
+TRUNCATE tmp1;
+SELECT * FROM tmp1;
+ a 
+---
+(0 rows)
+
+RELEASE sp1;
+SELECT * FROM tmp1;
+ a 
+---
+(0 rows)
+
+ROLLBACK;
+SELECT * FROM tmp1;
+ a 
+---
+ 1
+(1 row)
+
+BEGIN;
+SAVEPOINT sp1;
+TRUNCATE tmp1;
+SELECT * FROM tmp1;
+ a 
+---
+(0 rows)
+
+ROLLBACK TO sp1;
+SELECT * FROM tmp1;
+ a 
+---
+ 1
+(1 row)
+
+COMMIT;
+SELECT * FROM tmp1;
+ a 
+---
+ 1
+(1 row)
+
+TRUNCATE tmp1;
+SELECT * FROM tmp1;
+ a 
+---
+(0 rows)
+
+-- Test view creation
+INSERT INTO tmp1 VALUES (1);
+CREATE VIEW v AS SELECT * FROM tmp1;
+SELECT * FROM v;
+ a 
+---
+ 1
+(1 row)
+
+DROP VIEW v;
+CREATE TEMP VIEW v AS SELECT * FROM tmp1;
+SELECT * FROM v;
+ a 
+---
+ 1
+(1 row)
+
+DROP VIEW v;
+CREATE GLOBAL TEMP VIEW v AS SELECT * FROM tmp1; -- fail
+ERROR:  views cannot be global temporary because they do not have storage
diff --git a/src/test/regress/expected/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 8356ca98ef2..42a9867fc72 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -101,8 +101,10 @@ test: publication subscription
 # ----------
 # Another group of parallel tests
 # select_views depends on create_view
+# NB: global_temp.sql does reconnects which transiently uses 2 connections,
+# so keep this parallel group to at most 19 tests
 # ----------
-test: select_views portals_p2 foreign_key dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass stats_rewrite graph_table
+test: select_views portals_p2 foreign_key dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass stats_rewrite graph_table global_temp
 
 # ----------
 # Another group of parallel tests (JSON related)
diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql
index 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_property_graph.sql b/src/test/regress/sql/create_property_graph.sql
index 2b8269d66ec..527993e4fd5 100644
--- a/src/test/regress/sql/create_property_graph.sql
+++ b/src/test/regress/sql/create_property_graph.sql
@@ -104,6 +104,7 @@ SELECT pg_get_propgraphdef('g5'::regclass);
 
 -- error cases
 CREATE UNLOGGED PROPERTY GRAPH gx VERTEX TABLES (xx, yy);
+CREATE GLOBAL TEMP PROPERTY GRAPH gx VERTEX TABLES (xx, yy);
 CREATE PROPERTY GRAPH gx VERTEX TABLES (xx, yy);
 CREATE PROPERTY GRAPH gx VERTEX TABLES (t1 KEY (a), t2 KEY (i), t1 KEY (a));
 ALTER PROPERTY GRAPH g3 ADD VERTEX TABLES (t3 KEY (x));  -- duplicate alias
diff --git a/src/test/regress/sql/create_table.sql b/src/test/regress/sql/create_table.sql
index 80e424e6bda..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..431d06f768d
--- /dev/null
+++ b/src/test/regress/sql/global_temp.sql
@@ -0,0 +1,168 @@
+--
+-- GLOBAL TEMP
+--
+CREATE SCHEMA global_temp_tests;
+GRANT USAGE ON SCHEMA global_temp_tests TO PUBLIC;
+SET search_path = global_temp_tests;
+CREATE ROLE regress_global_temp_user;
+GRANT CREATE ON SCHEMA global_temp_tests TO regress_global_temp_user;
+GRANT CREATE ON DATABASE regression TO regress_global_temp_user;
+SET ROLE regress_global_temp_user;
+
+-- Test table creation
+CREATE GLOBAL TEMP TABLE pg_temp.tmp1 (a int); -- fail
+CREATE GLOBAL TEMP TABLE tmp1 (a int);
+CREATE SCHEMA global_temp_xxx CREATE GLOBAL TEMP TABLE tmp2 (a int);
+CREATE SCHEMA global_temp_yyy;
+CREATE GLOBAL TEMP TABLE global_temp_yyy.tmp3 (a int);
+
+\d tmp1
+\dt+ global_temp_*.tmp*
+
+-- Information schema
+SELECT table_catalog, table_schema, table_name, table_type
+FROM information_schema.tables
+WHERE table_name ~ 'tmp' AND table_schema ~ 'global_temp'
+ORDER BY table_name;
+
+DROP SCHEMA global_temp_xxx CASCADE;
+DROP SCHEMA global_temp_yyy CASCADE;
+
+-- Basic tests
+INSERT INTO tmp1 VALUES (1);
+SELECT * FROM tmp1;
+\c
+SET search_path = global_temp_tests;
+SELECT * FROM tmp1;
+
+-- Test ON COMMIT DELETE ROWS
+CREATE GLOBAL TEMP TABLE tmp2 (a int) ON COMMIT DELETE ROWS;
+BEGIN;
+INSERT INTO tmp2 VALUES (1);
+SELECT * FROM tmp2;
+COMMIT;
+SELECT * FROM tmp2;
+
+-- Repeat test in a new session
+\c
+SET search_path = global_temp_tests;
+BEGIN;
+INSERT INTO tmp2 VALUES (1);
+SELECT * FROM tmp2;
+COMMIT;
+SELECT * FROM tmp2;
+DROP TABLE tmp2;
+
+-- ON COMMIT DROP not allowed
+CREATE GLOBAL TEMP TABLE tmp2 (a int) ON COMMIT DROP; -- fail
+
+-- Two-phase commit not allowed with global temp tables
+BEGIN;
+SELECT * FROM tmp1;
+PREPARE TRANSACTION 'twophase'; -- fail
+
+-- Test partitioned global temp table
+CREATE GLOBAL TEMP TABLE tmp2 (a int) PARTITION BY LIST (a);
+CREATE GLOBAL TEMP TABLE tmp2_p1 PARTITION OF tmp2 FOR VALUES IN (1);
+CREATE 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
+CREATE GLOBAL TEMP TABLE tmp2 (a int);
+INSERT INTO tmp2 VALUES (1);
+SELECT * FROM tmp2;
+SELECT regexp_replace(pg_relation_filepath('tmp2'), '(\d+)', 'NNN', 'g');
+ALTER TABLE tmp2 SET TABLESPACE regress_tblspace;
+SELECT * FROM tmp2;
+SELECT regexp_replace(pg_relation_filepath('tmp2'), '(\d+)', 'NNN', 'g');
+DROP TABLE tmp2;
+
+-- Test dependency on tablespace
+SET allow_in_place_tablespaces = true;
+CREATE TABLESPACE regress_temp_test_tablespace LOCATION '';
+CREATE GLOBAL TEMP TABLE tmp2 (a int) TABLESPACE regress_temp_test_tablespace;
+\c
+SET search_path = global_temp_tests;
+DROP TABLESPACE regress_temp_test_tablespace; -- fail
+DROP TABLE tmp2;
+DROP TABLESPACE regress_temp_test_tablespace;
+
+SET allow_in_place_tablespaces = true;
+CREATE TABLESPACE regress_temp_test_tablespace LOCATION '';
+CREATE GLOBAL TEMP TABLE tmp2 (a int);
+ALTER TABLE tmp2 SET TABLESPACE regress_temp_test_tablespace;
+\c
+SET search_path = global_temp_tests;
+DROP TABLESPACE regress_temp_test_tablespace; -- fail
+DROP TABLE tmp2;
+DROP TABLESPACE regress_temp_test_tablespace;
+
+-- Test TRUNCATE
+INSERT INTO tmp1 VALUES (1);
+BEGIN;
+TRUNCATE tmp1;
+SELECT * FROM tmp1;
+ROLLBACK;
+SELECT * FROM tmp1;
+
+BEGIN;
+SAVEPOINT sp1;
+TRUNCATE tmp1;
+SELECT * FROM tmp1;
+RELEASE sp1;
+SELECT * FROM tmp1;
+ROLLBACK;
+SELECT * FROM tmp1;
+
+BEGIN;
+SAVEPOINT sp1;
+TRUNCATE tmp1;
+SELECT * FROM tmp1;
+ROLLBACK TO sp1;
+SELECT * FROM tmp1;
+COMMIT;
+SELECT * FROM tmp1;
+
+TRUNCATE tmp1;
+SELECT * FROM tmp1;
+
+-- Test view creation
+INSERT INTO tmp1 VALUES (1);
+CREATE VIEW v AS SELECT * FROM tmp1;
+SELECT * FROM v;
+DROP VIEW v;
+
+CREATE TEMP VIEW v AS SELECT * FROM tmp1;
+SELECT * FROM v;
+DROP VIEW v;
+
+CREATE GLOBAL TEMP VIEW v AS SELECT * FROM tmp1; -- fail
diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql
index 95d5b6e0915..b1f0a60abea 100644
--- a/src/test/regress/sql/type_sanity.sql
+++ b/src/test/regress/sql/type_sanity.sql
@@ -367,7 +367,7 @@ ORDER BY 1;
 SELECT c1.oid, c1.relname
 FROM pg_class as c1
 WHERE relkind NOT IN ('r', 'i', 'S', 't', 'v', 'm', 'c', 'f', 'p', 'I') OR
-    relpersistence NOT IN ('p', 'u', 't') OR
+    relpersistence NOT IN ('p', 'u', 't', 'g') OR
     relreplident NOT IN ('d', 'n', 'f', 'i');
 
 -- All tables, indexes, partitioned indexes and matviews should have an
diff --git a/src/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 fb3ee1b071b..cd8d61c6cc3 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1161,6 +1161,7 @@ GistTsVectorOptions
 GistVacState
 GlobalChannelEntry
 GlobalChannelKey
+GlobalTempRelShmemControl
 GlobalTransaction
 GlobalTransactionData
 GlobalVisHorizonKind
@@ -1190,6 +1191,10 @@ GroupingSet
 GroupingSetData
 GroupingSetKind
 GroupingSetsPath
+GtrSharedUsageEntry
+GtrSharedUsageKey
+GtrStorageEntry
+GtrUsageEntry
 GucAction
 GucBoolAssignHook
 GucBoolCheckHook
-- 
2.43.0

