From 160528a634768a80ef1261a77dd2bdd2ecdf105f Mon Sep 17 00:00:00 2001
From: Dean Rasheed <dean.a.rasheed@gmail.com>
Date: Wed, 17 Jun 2026 10:04:41 +0100
Subject: [PATCH v2 6/9] Add relfrozenxid and relminmxid columns to
 pg_temp_class.

This allows VACUUM results to be stored locally in pg_temp_class for
each global temporary table. Additionally, each session stores its
minimum pg_temp_class.relfrozenxid and pg_temp_class.relminmxid values
in its PGPROC struct, allowing other sessions to advance
pg_class.relfrozenxid and pg_class.relminmxid for global temporary
tables, taking into account the frozen XIDs from other sessions. Thus
it is possible to keep advancing datfrozenxid and datminmxid as long
as each session that uses global temporary tables runs VACUUM from
time to time.
---
 src/backend/access/heap/vacuumlazy.c          |   4 +-
 src/backend/access/transam/twophase.c         |   3 +
 src/backend/catalog/global_temp.c             |  14 ++
 src/backend/catalog/pg_temp_class.c           | 103 +++++++++
 src/backend/commands/repack.c                 |  46 +++-
 src/backend/commands/vacuum.c                 | 209 ++++++++++++++++--
 src/backend/storage/lmgr/proc.c               |   7 +
 src/include/catalog/pg_temp_class.h           |  11 +
 src/include/commands/vacuum.h                 |   5 +-
 src/include/storage/proc.h                    |   9 +
 .../isolation/expected/vacuum-global-temp.out |  88 ++++++++
 src/test/isolation/isolation_schedule         |   1 +
 .../isolation/specs/vacuum-global-temp.spec   |  70 ++++++
 13 files changed, 552 insertions(+), 18 deletions(-)
 create mode 100644 src/test/isolation/expected/vacuum-global-temp.out
 create mode 100644 src/test/isolation/specs/vacuum-global-temp.spec

diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 39395aed0d5..cc77515ff58 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -919,7 +919,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
 								 PROGRESS_VACUUM_PHASE_FINAL_CLEANUP);
 
 	/*
-	 * Prepare to update rel's pg_class entry.
+	 * Prepare to update rel's pg_class and/or pg_temp_class entries.
 	 *
 	 * Aggressive VACUUMs must always be able to advance relfrozenxid to a
 	 * value >= FreezeLimit, and relminmxid to a value >= MultiXactCutoff.
@@ -963,7 +963,7 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
 		new_rel_allfrozen = new_rel_allvisible;
 
 	/*
-	 * Now actually update rel's pg_class entry.
+	 * Now actually update rel's pg_class and/or pg_temp_class entries.
 	 *
 	 * In principle new_live_tuples could be -1 indicating that we (still)
 	 * don't know the tuple count.  In practice that can't happen, since we
diff --git a/src/backend/access/transam/twophase.c b/src/backend/access/transam/twophase.c
index 1035e8b3fc7..094ff7ed58e 100644
--- a/src/backend/access/transam/twophase.c
+++ b/src/backend/access/transam/twophase.c
@@ -78,6 +78,7 @@
 
 #include "access/commit_ts.h"
 #include "access/htup_details.h"
+#include "access/multixact.h"
 #include "access/subtrans.h"
 #include "access/transam.h"
 #include "access/twophase.h"
@@ -485,6 +486,8 @@ MarkAsPreparingGuts(GlobalTransaction gxact, FullTransactionId fxid,
 	/* subxid data must be filled later by GXactLoadSubxactData */
 	proc->subxidStatus.overflowed = false;
 	proc->subxidStatus.count = 0;
+	proc->tempfrozenxid = InvalidTransactionId;
+	proc->tempminmxid = InvalidMultiXactId;
 
 	gxact->prepared_at = prepared_at;
 	gxact->fxid = fxid;
diff --git a/src/backend/catalog/global_temp.c b/src/backend/catalog/global_temp.c
index 1f9bf8b44e1..f130f3d03ec 100644
--- a/src/backend/catalog/global_temp.c
+++ b/src/backend/catalog/global_temp.c
@@ -55,6 +55,7 @@
 
 #include "access/amapi.h"
 #include "access/genam.h"
+#include "access/multixact.h"
 #include "access/table.h"
 #include "access/tableam.h"
 #include "access/xact.h"
@@ -67,6 +68,7 @@
 #include "miscadmin.h"
 #include "storage/ipc.h"
 #include "storage/lwlock.h"
+#include "storage/proc.h"
 #include "storage/shmem.h"
 #include "storage/subsystems.h"
 #include "utils/memutils.h"
@@ -824,11 +826,23 @@ InitGlobalTempRelation(Relation relation)
 	{
 		/* Create (and track) storage for the relation */
 		if (RELKIND_HAS_TABLE_AM(relation->rd_rel->relkind))
+		{
 			table_relation_set_new_filelocator(relation,
 											   &relation->rd_locator,
 											   relation->rd_rel->relpersistence,
 											   &relation->rd_rel->relfrozenxid,
 											   &relation->rd_rel->relminmxid);
+
+			/*
+			 * If tempfrozenxid and tempminmxid haven't been set for this
+			 * backend, then set them now (first global temporary table
+			 * accessed in this session).
+			 */
+			if (!TransactionIdIsValid(MyProc->tempfrozenxid))
+				MyProc->tempfrozenxid = relation->rd_rel->relfrozenxid;
+			if (!MultiXactIdIsValid(MyProc->tempminmxid))
+				MyProc->tempminmxid = (MultiXactId) relation->rd_rel->relminmxid;
+		}
 		else
 			RelationCreateStorage(relation->rd_id,
 								  relation->rd_locator,
diff --git a/src/backend/catalog/pg_temp_class.c b/src/backend/catalog/pg_temp_class.c
index e26b193c772..711802871e6 100644
--- a/src/backend/catalog/pg_temp_class.c
+++ b/src/backend/catalog/pg_temp_class.c
@@ -47,6 +47,7 @@
 
 #include "access/genam.h"
 #include "access/htup_details.h"
+#include "access/multixact.h"
 #include "access/table.h"
 #include "access/xact.h"
 #include "catalog/indexing.h"
@@ -151,6 +152,12 @@ get_pg_temp_class_tupdesc(void)
 		TupleDescInitEntry(tupdesc,
 						   (AttrNumber) Anum_pg_temp_class_relallfrozen,
 						   "relallfrozen", INT4OID, -1, 0);
+		TupleDescInitEntry(tupdesc,
+						   (AttrNumber) Anum_pg_temp_class_relfrozenxid,
+						   "relfrozenxid", XIDOID, -1, 0);
+		TupleDescInitEntry(tupdesc,
+						   (AttrNumber) Anum_pg_temp_class_relminmxid,
+						   "relminmxid", XIDOID, -1, 0);
 		TupleDescFinalize(tupdesc);
 
 		MemoryContextSwitchTo(oldcontext);
@@ -181,6 +188,8 @@ heap_form_pg_temp_class_tuple(Relation rel)
 	values[Anum_pg_temp_class_reltuples - 1] = Float4GetDatum(form->reltuples);
 	values[Anum_pg_temp_class_relallvisible - 1] = Int32GetDatum(form->relallvisible);
 	values[Anum_pg_temp_class_relallfrozen - 1] = Int32GetDatum(form->relallfrozen);
+	values[Anum_pg_temp_class_relfrozenxid - 1] = TransactionIdGetDatum(form->relfrozenxid);
+	values[Anum_pg_temp_class_relminmxid - 1] = MultiXactIdGetDatum(form->relminmxid);
 
 	return heap_form_tuple(get_pg_temp_class_tupdesc(), values, nulls);
 }
@@ -567,6 +576,100 @@ GetEffectivePgClassTuple(Oid relid)
 	return tuple;
 }
 
+/*
+ * GetPgTempClassMinFrozenXids
+ *
+ *	Get the minimum relfrozenxid and relminmxid values in pg_temp_class --
+ *	i.e., the minimum frozen XIDs over all global temporary relations in use
+ *	in this backend.  If no global temporary relations have been used,
+ *	Invalid*Ids will be returned.
+ */
+void
+GetPgTempClassMinFrozenXids(TransactionId *min_relfrozenxid,
+							MultiXactId *min_relminmxid)
+{
+	HASH_SEQ_STATUS status;
+	PendingInsert *entry;
+	Form_pg_temp_class temp_form;
+	TransactionId relfrozenxid;
+	MultiXactId relminmxid;
+	Relation	pg_temp_class;
+	SysScanDesc scan;
+	HeapTuple	tuple;
+
+	/* Defaults, if no global temporary relations are being used */
+	*min_relfrozenxid = InvalidTransactionId;
+	*min_relminmxid = InvalidMultiXactId;
+
+	/* Processing any pending inserts */
+	if (have_pending_inserts)
+	{
+		hash_seq_init(&status, pending_inserts);
+		while ((entry = hash_seq_search(&status)) != NULL)
+		{
+			temp_form = (Form_pg_temp_class) GETSTRUCT(entry->tuple);
+			relfrozenxid = temp_form->relfrozenxid;
+			relminmxid = (MultiXactId) temp_form->relminmxid;
+
+			/* Ignore relations that don't hold unfrozen XIDs */
+			if (!TransactionIdIsValid(relfrozenxid) ||
+				!MultiXactIdIsValid(relminmxid))
+				continue;
+
+			/* Update the minimum values */
+			Assert(TransactionIdIsNormal(relfrozenxid));
+
+			if (!TransactionIdIsValid(*min_relfrozenxid) ||
+				TransactionIdPrecedes(relfrozenxid, *min_relfrozenxid))
+				*min_relfrozenxid = relfrozenxid;
+
+			if (!MultiXactIdIsValid(*min_relminmxid) ||
+				MultiXactIdPrecedes(relminmxid, *min_relminmxid))
+				*min_relminmxid = relminmxid;
+		}
+	}
+
+	/*
+	 * If we haven't opened pg_temp_class yet, it must be empty, and we're
+	 * done.
+	 */
+	if (!pg_temp_class_opened)
+		return;
+
+	/* Scan pg_temp_class */
+	pg_temp_class = table_open(TempRelationRelationId, AccessShareLock);
+
+	scan = systable_beginscan(pg_temp_class, InvalidOid, false,
+							  NULL, 0, NULL);
+
+	while ((tuple = systable_getnext(scan)) != NULL)
+	{
+		temp_form = (Form_pg_temp_class) GETSTRUCT(tuple);
+		relfrozenxid = temp_form->relfrozenxid;
+		relminmxid = (MultiXactId) temp_form->relminmxid;
+
+		/* Ignore relations that don't hold unfrozen XIDs */
+		if (!TransactionIdIsValid(relfrozenxid) ||
+			!MultiXactIdIsValid(relminmxid))
+			continue;
+
+		/* Update the minimum values */
+		Assert(TransactionIdIsNormal(relfrozenxid));
+
+		if (!TransactionIdIsValid(*min_relfrozenxid) ||
+			TransactionIdPrecedes(relfrozenxid, *min_relfrozenxid))
+			*min_relfrozenxid = relfrozenxid;
+
+		if (!MultiXactIdIsValid(*min_relminmxid) ||
+			MultiXactIdPrecedes(relminmxid, *min_relminmxid))
+			*min_relminmxid = relminmxid;
+	}
+
+	/* Tidy up */
+	systable_endscan(scan);
+	table_close(pg_temp_class, AccessShareLock);
+}
+
 /*
  * PreCCI_PgTempClass
  *
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 5c55a071c89..0356317a047 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -256,6 +256,7 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	bool		verbose = false;
 	bool		analyze = false;
 	bool		concurrently = false;
+	bool		have_gtrs;
 
 	/* Parse option list */
 	foreach_node(DefElem, opt, stmt->params)
@@ -445,6 +446,7 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 
 	/* Cluster the tables, each in a separate transaction */
 	Assert(rel == NULL);
+	have_gtrs = false;
 	foreach_ptr(RelToCluster, rtc, rtcs)
 	{
 		/* Start a new transaction for each relation. */
@@ -460,6 +462,8 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 			CommitTransactionCommand();
 			continue;
 		}
+		if (RELATION_IS_GLOBAL_TEMP(rel))
+			have_gtrs = true;
 
 		/* functions in indexes may want a snapshot set */
 		PushActiveSnapshot(GetTransactionSnapshot());
@@ -475,6 +479,13 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Start a new transaction for the cleanup work. */
 	StartTransactionCommand();
 
+	/*
+	 * Update our PGPROC struct's tempfrozenxid and tempminmxid, if we
+	 * processed any global temporary relations.
+	 */
+	if (have_gtrs)
+		vac_update_tempfrozenxids();
+
 	/* Clean up working storage */
 	MemoryContextDelete(repack_context);
 }
@@ -1718,11 +1729,36 @@ swap_relation_files(Oid r1, Oid r2, bool target_is_pg_class,
 	 * and then fail to commit the pg_class update.
 	 */
 
-	/* set rel1's frozen Xid and minimum MultiXid */
+	/*
+	 * Set rel1's frozen Xid and minimum MultiXid.  For a global temporary
+	 * relation, the supplied values are set in pg_temp_class and pg_class,
+	 * except that the values in pg_class are constrained to be no later than
+	 * min_tempfrozenxid and min_tempminmxid from all other backends' global
+	 * temporary relations --- see comments in vac_update_relstats().
+	 */
 	if (relform1->relkind != RELKIND_INDEX)
 	{
 		Assert(!TransactionIdIsValid(frozenXid) ||
 			   TransactionIdIsNormal(frozenXid));
+
+		if (temp_relform1 != NULL)
+		{
+			TransactionId min_tempfrozenxid;
+			MultiXactId min_tempminmxid;
+
+			vac_get_min_tempfrozenxids(&min_tempfrozenxid, &min_tempminmxid);
+
+			temp_relform1->relfrozenxid = frozenXid;
+			temp_relform1->relminmxid = cutoffMulti;
+
+			if (TransactionIdIsValid(min_tempfrozenxid) &&
+				TransactionIdPrecedes(min_tempfrozenxid, frozenXid))
+				frozenXid = min_tempfrozenxid;
+
+			if (MultiXactIdIsValid(min_tempminmxid) &&
+				MultiXactIdPrecedes(min_tempminmxid, cutoffMulti))
+				cutoffMulti = min_tempminmxid;
+		}
 		relform1->relfrozenxid = frozenXid;
 		relform1->relminmxid = cutoffMulti;
 	}
@@ -2512,6 +2548,7 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	else
 	{
 		Oid			indexOid = InvalidOid;
+		bool		is_gtr = RELATION_IS_GLOBAL_TEMP(rel);
 
 		indexOid = determine_clustered_index(rel, stmt->usingindex,
 											 stmt->indexname);
@@ -2544,6 +2581,13 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 			CommandCounterIncrement();
 		}
 
+		/*
+		 * Update our PGPROC struct's tempfrozenxid and tempminmxid, if it was
+		 * a global temporary relation.
+		 */
+		if (is_gtr)
+			vac_update_tempfrozenxids();
+
 		return NULL;
 	}
 }
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 20c8aafdb70..ae8c4469bf3 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -128,7 +128,8 @@ static void vac_truncate_clog(TransactionId frozenXID,
 							  TransactionId lastSaneFrozenXid,
 							  MultiXactId lastSaneMinMulti);
 static bool vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
-					   BufferAccessStrategy bstrategy, bool isTopLevel);
+					   BufferAccessStrategy bstrategy, bool isTopLevel,
+					   bool *isGtr);
 static double compute_parallel_delay(void);
 static VacOptValue get_vacoptval_from_boolean(DefElem *def);
 static bool vac_tid_reaped(ItemPointer itemptr, void *state);
@@ -500,6 +501,7 @@ vacuum(List *relations, const VacuumParams *params, BufferAccessStrategy bstrate
 	const char *stmttype;
 	volatile bool in_outer_xact,
 				use_own_xacts;
+	bool		have_gtrs;
 
 	stmttype = (params->options & VACOPT_VACUUM) ? "VACUUM" : "ANALYZE";
 
@@ -628,11 +630,16 @@ vacuum(List *relations, const VacuumParams *params, BufferAccessStrategy bstrate
 		foreach(cur, relations)
 		{
 			VacuumRelation *vrel = lfirst_node(VacuumRelation, cur);
+			bool		isGtr = false;
+			bool		doAnalyse;
 
 			if (params->options & VACOPT_VACUUM)
 			{
-				if (!vacuum_rel(vrel->oid, vrel->relation, *params, bstrategy,
-								isTopLevel))
+				doAnalyse = vacuum_rel(vrel->oid, vrel->relation, *params,
+									   bstrategy, isTopLevel, &isGtr);
+				if (isGtr)
+					have_gtrs = true;
+				if (!doAnalyse)
 					continue;
 			}
 
@@ -700,6 +707,17 @@ vacuum(List *relations, const VacuumParams *params, BufferAccessStrategy bstrate
 		StartTransactionCommand();
 	}
 
+	if (params->options & VACOPT_VACUUM && !AmAutoVacuumWorkerProcess())
+	{
+		/*
+		 * Update our PGPROC struct's tempfrozenxid and tempminmxid, if we
+		 * vacuumed any global temporary relations.  We skip this for
+		 * autovacuum, which shouldn't vacuum any global temporary relations.
+		 */
+		if (have_gtrs)
+			vac_update_tempfrozenxids();
+	}
+
 	if ((params->options & VACOPT_VACUUM) &&
 		!(params->options & VACOPT_SKIP_DATABASE_STATS))
 	{
@@ -1125,7 +1143,7 @@ vacuum_get_cutoffs(Relation rel, const VacuumParams *params,
 	freeze_table_age = params->freeze_table_age;
 	multixact_freeze_table_age = params->multixact_freeze_table_age;
 
-	/* Set pg_class fields in cutoffs */
+	/* Set pg_class / pg_temp_class fields in cutoffs */
 	cutoffs->relfrozenxid = rel->rd_rel->relfrozenxid;
 	cutoffs->relminmxid = rel->rd_rel->relminmxid;
 
@@ -1543,11 +1561,98 @@ vac_update_relstats(Relation relation,
 	 * it's corrupt, and overwrite with the oldest remaining XID in the table.
 	 * This should match vac_update_datfrozenxid() concerning what we consider
 	 * to be "in the future".
+	 *
+	 * For a global temporary relation, frozenxid is only valid for the data
+	 * in our local instance of the relation, and is stored in pg_temp_class.
+	 * We then try to update pg_class using min(frozenxid, min_tempfrozenxid)
+	 * so that it doesn't exceed the pg_temp_class.relfrozenxid value from any
+	 * other backend.  This allows vac_update_datfrozenxid() to advance
+	 * datfrozenxid once every backend accessing the relation has vacuumed it.
 	 */
-	oldfrozenxid = pgcform->relfrozenxid;
-	futurexid = false;
 	if (frozenxid_updated)
 		*frozenxid_updated = false;
+	if (minmulti_updated)
+		*minmulti_updated = false;
+
+	if (temp_pgcform != NULL)
+	{
+		TransactionId min_tempfrozenxid;
+		MultiXactId min_tempminmxid;
+
+		/* Update pg_temp_class.relfrozenxid */
+		futurexid = false;
+		oldfrozenxid = temp_pgcform->relfrozenxid;
+		if (TransactionIdIsNormal(frozenxid) && oldfrozenxid != frozenxid)
+		{
+			bool		update = false;
+
+			if (TransactionIdPrecedes(oldfrozenxid, frozenxid))
+				update = true;
+			else if (TransactionIdPrecedes(ReadNextTransactionId(), oldfrozenxid))
+				futurexid = update = true;
+
+			if (update)
+			{
+				temp_pgcform->relfrozenxid = frozenxid;
+				temp_dirty = true;
+				if (frozenxid_updated)
+					*frozenxid_updated = true;
+			}
+		}
+
+		/* Update pg_temp_class.relminmxid */
+		futuremxid = false;
+		oldminmulti = temp_pgcform->relminmxid;
+		if (MultiXactIdIsValid(minmulti) && oldminmulti != minmulti)
+		{
+			bool		update = false;
+
+			if (MultiXactIdPrecedes(oldminmulti, minmulti))
+				update = true;
+			else if (MultiXactIdPrecedes(ReadNextMultiXactId(), oldminmulti))
+				futuremxid = update = true;
+
+			if (update)
+			{
+				temp_pgcform->relminmxid = minmulti;
+				temp_dirty = true;
+				if (minmulti_updated)
+					*minmulti_updated = true;
+			}
+		}
+
+		/* Warn about overwriting XIDs in the future */
+		if (futurexid)
+			ereport(WARNING,
+					(errcode(ERRCODE_DATA_CORRUPTED),
+					 errmsg_internal("overwrote invalid pg_temp_class.relfrozenxid value %u with new value %u for table \"%s\"",
+									 oldfrozenxid, frozenxid,
+									 RelationGetRelationName(relation))));
+		if (futuremxid)
+			ereport(WARNING,
+					(errcode(ERRCODE_DATA_CORRUPTED),
+					 errmsg_internal("overwrote invalid rpg_temp_class.elminmxid value %u with new value %u for table \"%s\"",
+									 oldminmulti, minmulti,
+									 RelationGetRelationName(relation))));
+
+		/*
+		 * Constrain the pg_class XIDs to be no later than tempfrozenxid /
+		 * tempminmxid from any other backend.
+		 */
+		vac_get_min_tempfrozenxids(&min_tempfrozenxid, &min_tempminmxid);
+
+		if (TransactionIdIsValid(min_tempfrozenxid) &&
+			TransactionIdPrecedes(min_tempfrozenxid, frozenxid))
+			frozenxid = min_tempfrozenxid;
+
+		if (MultiXactIdIsValid(min_tempminmxid) &&
+			MultiXactIdPrecedes(min_tempminmxid, minmulti))
+			minmulti = min_tempminmxid;
+	}
+
+	/* Update pg_class.relfrozenxid */
+	futurexid = false;
+	oldfrozenxid = pgcform->relfrozenxid;
 	if (TransactionIdIsNormal(frozenxid) && oldfrozenxid != frozenxid)
 	{
 		bool		update = false;
@@ -1566,11 +1671,9 @@ vac_update_relstats(Relation relation,
 		}
 	}
 
-	/* Similarly for relminmxid */
-	oldminmulti = pgcform->relminmxid;
+	/* Update pg_class.relminmxid */
 	futuremxid = false;
-	if (minmulti_updated)
-		*minmulti_updated = false;
+	oldminmulti = pgcform->relminmxid;
 	if (MultiXactIdIsValid(minmulti) && oldminmulti != minmulti)
 	{
 		bool		update = false;
@@ -1610,18 +1713,92 @@ vac_update_relstats(Relation relation,
 	if (futurexid)
 		ereport(WARNING,
 				(errcode(ERRCODE_DATA_CORRUPTED),
-				 errmsg_internal("overwrote invalid relfrozenxid value %u with new value %u for table \"%s\"",
+				 errmsg_internal("overwrote invalid pg_class.relfrozenxid value %u with new value %u for table \"%s\"",
 								 oldfrozenxid, frozenxid,
 								 RelationGetRelationName(relation))));
 	if (futuremxid)
 		ereport(WARNING,
 				(errcode(ERRCODE_DATA_CORRUPTED),
-				 errmsg_internal("overwrote invalid relminmxid value %u with new value %u for table \"%s\"",
+				 errmsg_internal("overwrote invalid pg_class.relminmxid value %u with new value %u for table \"%s\"",
 								 oldminmulti, minmulti,
 								 RelationGetRelationName(relation))));
 }
 
 
+/*
+ *	vac_update_tempfrozenxids() -- update temp XIDs for this backend
+ *
+ *		This process's PGPROC struct's tempfrozenxid and tempminmxid are set
+ *		to the minumum relfrozenxid and relminmxid values from pg_temp_class.
+ *
+ *		These act as crude upper bounds for pg_class.relfrozenxid and
+ *		pg_class.relminmxid in other processes vacuuming global temporary
+ *		relations.
+ */
+void
+vac_update_tempfrozenxids(void)
+{
+	TransactionId min_relfrozenxid;
+	MultiXactId min_relminmxid;
+
+	/* Find the minimum frozen XIDs in pg_temp_class */
+	GetPgTempClassMinFrozenXids(&min_relfrozenxid, &min_relminmxid);
+
+	/* Update our PGPROC struct */
+	MyProc->tempfrozenxid = min_relfrozenxid;
+	MyProc->tempminmxid = min_relminmxid;
+}
+
+
+/*
+ *	vac_get_min_tempfrozenxids() -- get min temp XIDs over all other backends
+ *
+ *		min_tempfrozenxid is set to the minimum tempfrozenxid from all other
+ *		backends connected to our database.
+ *
+ *		min_tempminmxid is set to the minimum tempminmxid from all other
+ *		backends connected to our database.
+ *
+ *		These are used as the upper bounds for pg_class.relfrozenxid and
+ *		pg_class.relminmxid for global temporary tables, ensuring that they
+ *		don't exceed any current session's local pg_temp_class values.  The
+ *		values returned will be Invalid*Ids, if no other backend is accessing
+ *		global temporary tables in our database.
+ */
+void
+vac_get_min_tempfrozenxids(TransactionId *min_tempfrozenxid,
+						   MultiXactId *min_tempminmxid)
+{
+	/* Defaults, if no other backends found */
+	*min_tempfrozenxid = InvalidTransactionId;
+	*min_tempminmxid = InvalidMultiXactId;
+
+	for (int i = 0; i < ProcGlobal->allProcCount; i++)
+	{
+		PGPROC	   *proc = GetPGProcByNumber(i);
+
+		/* Ignore this backend and backends not connected to our database */
+		if (proc->pid == 0)
+			continue;
+		if (proc->databaseId != MyDatabaseId)
+			continue;
+		if (i == MyProcNumber)
+			continue;
+
+		/* Update the minimum return values */
+		if (TransactionIdIsValid(proc->tempfrozenxid) &&
+			(!TransactionIdIsValid(*min_tempfrozenxid) ||
+			 TransactionIdPrecedes(proc->tempfrozenxid, *min_tempfrozenxid)))
+			*min_tempfrozenxid = proc->tempfrozenxid;
+
+		if (MultiXactIdIsValid(proc->tempminmxid) &&
+			(!MultiXactIdIsValid(*min_tempminmxid) ||
+			 MultiXactIdPrecedes(proc->tempminmxid, *min_tempminmxid)))
+			*min_tempminmxid = proc->tempminmxid;
+	}
+}
+
+
 /*
  *	vac_update_datfrozenxid() -- update pg_database.datfrozenxid for our DB
  *
@@ -2040,7 +2217,7 @@ vac_truncate_clog(TransactionId frozenXID,
  */
 static bool
 vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
-		   BufferAccessStrategy bstrategy, bool isTopLevel)
+		   BufferAccessStrategy bstrategy, bool isTopLevel, bool *isGtr)
 {
 	LOCKMODE	lmode;
 	Relation	rel;
@@ -2126,6 +2303,10 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
 		return false;
 	}
 
+	/* tell caller if it was a global temporary relation */
+	if (RELATION_IS_GLOBAL_TEMP(rel))
+		*isGtr = true;
+
 	/*
 	 * When recursing to a TOAST table, check privileges on the parent.  NB:
 	 * This is only safe to do because we hold a session lock on the main
@@ -2392,7 +2573,7 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
 		toast_vacuum_params.toast_parent = relid;
 
 		vacuum_rel(toast_relid, NULL, toast_vacuum_params, bstrategy,
-				   isTopLevel);
+				   isTopLevel, isGtr);
 	}
 
 	/*
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index 6fa9de33e1c..d33f7749c00 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -34,6 +34,7 @@
 #include <sys/time.h>
 
 #include "access/clog.h"
+#include "access/multixact.h"
 #include "access/transam.h"
 #include "access/twophase.h"
 #include "access/xlogutils.h"
@@ -523,6 +524,10 @@ InitProcess(void)
 	/* Initialize wait event information. */
 	MyProc->wait_event_info = 0;
 
+	/* Initialize global temporary table information */
+	MyProc->tempfrozenxid = InvalidTransactionId;
+	MyProc->tempminmxid = InvalidMultiXactId;
+
 	/* Initialize fields for group transaction status update. */
 	MyProc->clogGroupMember = false;
 	MyProc->clogGroupMemberXid = InvalidTransactionId;
@@ -686,6 +691,8 @@ InitAuxiliaryProcess(void)
 	MyProc->backendType = MyBackendType;
 	MyProc->delayChkptFlags = 0;
 	MyProc->statusFlags = 0;
+	MyProc->tempfrozenxid = InvalidTransactionId;
+	MyProc->tempminmxid = InvalidMultiXactId;
 	MyProc->lwWaiting = LW_WS_NOT_WAITING;
 	MyProc->lwWaitMode = 0;
 	MyProc->waitLock = NULL;
diff --git a/src/include/catalog/pg_temp_class.h b/src/include/catalog/pg_temp_class.h
index e6b77b50d23..e30cb54922d 100644
--- a/src/include/catalog/pg_temp_class.h
+++ b/src/include/catalog/pg_temp_class.h
@@ -57,6 +57,12 @@ CATALOG(pg_temp_class,8082,TempRelationRelationId) BKI_TEMP_RELATION
 
 	/* # of all-frozen blocks (not always up-to-date) */
 	int32		relallfrozen BKI_DEFAULT(0);
+
+	/* all Xids < this are frozen in this rel */
+	TransactionId relfrozenxid BKI_DEFAULT(3);	/* FirstNormalTransactionId */
+
+	/* all multixacts in this rel are >= this; it is really a MultiXactId */
+	TransactionId relminmxid BKI_DEFAULT(1);	/* FirstMultiXactId */
 } FormData_pg_temp_class;
 
 END_CATALOG_STRUCT
@@ -87,6 +93,8 @@ MAKE_SYSCACHE(TEMPRELOID, pg_temp_class_oid_index, 128);
 		(target)->reltuples = (source)->reltuples; \
 		(target)->relallvisible = (source)->relallvisible; \
 		(target)->relallfrozen = (source)->relallfrozen; \
+		(target)->relfrozenxid = (source)->relfrozenxid; \
+		(target)->relminmxid = (source)->relminmxid; \
 	} while (0)
 
 /*
@@ -302,6 +310,9 @@ extern HeapTuple GetPgClassAndPgTempClassTuples(Oid relid, bool lock_tuple,
 
 extern HeapTuple GetEffectivePgClassTuple(Oid relid);
 
+extern void GetPgTempClassMinFrozenXids(TransactionId *min_relfrozenxid,
+										MultiXactId *min_relminmxid);
+
 extern void PreCCI_PgTempClass(void);
 
 extern void PreCommit_PgTempClass(void);
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index 956d9cea36d..83372869283 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -257,7 +257,7 @@ typedef struct VacuumParams
 struct VacuumCutoffs
 {
 	/*
-	 * Existing pg_class fields at start of VACUUM
+	 * Existing pg_class / pg_temp_class fields at start of VACUUM
 	 */
 	TransactionId relfrozenxid;
 	MultiXactId relminmxid;
@@ -386,6 +386,9 @@ extern void vac_update_relstats(Relation relation,
 extern bool vacuum_get_cutoffs(Relation rel, const VacuumParams *params,
 							   struct VacuumCutoffs *cutoffs);
 extern bool vacuum_xid_failsafe_check(const struct VacuumCutoffs *cutoffs);
+extern void vac_update_tempfrozenxids(void);
+extern void vac_get_min_tempfrozenxids(TransactionId *min_tempfrozenxid,
+									   MultiXactId *min_tempminmxid);
 extern void vac_update_datfrozenxid(void);
 extern void vacuum_delay_point(bool is_analyze);
 extern bool vacuum_is_permitted_for_relation(Oid relid, Form_pg_class reltuple,
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 3e1d1fad5f9..710b466f173 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -382,6 +382,15 @@ typedef struct PGPROC
 	 ************************************************************************/
 
 	uint32		wait_event_info;	/* proc's wait information */
+
+	/************************************************************************
+	 * Global temporary tables
+	 ************************************************************************/
+
+	TransactionId tempfrozenxid;	/* minimum relfrozenxid over all global
+									 * temporary tables in use */
+	MultiXactId tempminmxid;	/* minimum relminmxid over all global
+								 * temporary tables in use */
 }
 PGPROC;
 
diff --git a/src/test/isolation/expected/vacuum-global-temp.out b/src/test/isolation/expected/vacuum-global-temp.out
new file mode 100644
index 00000000000..51c5fb7e572
--- /dev/null
+++ b/src/test/isolation/expected/vacuum-global-temp.out
@@ -0,0 +1,88 @@
+Parsed test spec with 2 sessions
+
+starting permutation: create vacdml1 vac1 vacdml2 vac2 vacdml1 vac1prep vac1 vac1cmp vacdml2 vac2prep vac2 vac2cmp
+step create: 
+  CREATE GLOBAL TEMP TABLE vactest (a int);
+  CREATE TABLE saved_xids(local_xid int, global_xid int, db_xid int);
+
+  CREATE FUNCTION save_xids() RETURNS void
+  BEGIN ATOMIC
+    DELETE FROM saved_xids;
+    INSERT INTO saved_xids VALUES (
+      (SELECT min(relfrozenxid::text::int) FROM pg_temp_class WHERE relfrozenxid != 0),
+      (SELECT min(relfrozenxid::text::int) FROM pg_class WHERE relfrozenxid != 0),
+      (SELECT datfrozenxid::text::int FROM pg_database WHERE datname = current_database())
+    );
+  END;
+
+  CREATE FUNCTION cmp_xids(xid1 int, xid2 int) RETURNS text
+  BEGIN ATOMIC
+    SELECT CASE
+             WHEN xid1 < xid2 THEN 'older'
+             WHEN xid1 > xid2 THEN 'younger'
+             ELSE 'same'
+           END;
+  END;
+
+  CREATE FUNCTION check_new_xids(OUT local_xid text,
+                                 OUT global_xid text,
+                                 OUT db_xid text)
+  BEGIN ATOMIC
+    WITH new_xids(new_local_xid, new_global_xid, new_db_xid) AS (
+      SELECT
+        (SELECT min(relfrozenxid::text::int) FROM pg_temp_class WHERE relfrozenxid != 0),
+        (SELECT min(relfrozenxid::text::int) FROM pg_class WHERE relfrozenxid != 0),
+        (SELECT datfrozenxid::text::int FROM pg_database WHERE datname = current_database())
+    )
+    SELECT cmp_xids(new_local_xid, local_xid),
+           cmp_xids(new_global_xid, global_xid),
+           cmp_xids(new_db_xid, db_xid)
+    FROM saved_xids, new_xids;
+  END;
+
+step vacdml1: 
+  INSERT INTO vactest SELECT * FROM generate_series(1, 10);
+  DELETE FROM vactest WHERE a % 2 = 0;
+  INSERT INTO vactest SELECT a * 2 FROM vactest;
+
+step vac1: VACUUM (FREEZE);
+step vacdml2: 
+  INSERT INTO vactest SELECT * FROM generate_series(1, 10);
+  UPDATE vactest SET a = a * 2 WHERE a % 2 = 1;
+
+step vac2: VACUUM (FREEZE);
+step vacdml1: 
+  INSERT INTO vactest SELECT * FROM generate_series(1, 10);
+  DELETE FROM vactest WHERE a % 2 = 0;
+  INSERT INTO vactest SELECT a * 2 FROM vactest;
+
+step vac1prep: SELECT save_xids();
+save_xids
+---------
+         
+(1 row)
+
+step vac1: VACUUM (FREEZE);
+step vac1cmp: SELECT * FROM check_new_xids();
+local_xid|global_xid|db_xid 
+---------+----------+-------
+younger  |younger   |younger
+(1 row)
+
+step vacdml2: 
+  INSERT INTO vactest SELECT * FROM generate_series(1, 10);
+  UPDATE vactest SET a = a * 2 WHERE a % 2 = 1;
+
+step vac2prep: SELECT save_xids();
+save_xids
+---------
+         
+(1 row)
+
+step vac2: VACUUM (FREEZE);
+step vac2cmp: SELECT * FROM check_new_xids();
+local_xid|global_xid|db_xid 
+---------+----------+-------
+younger  |younger   |younger
+(1 row)
+
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index 7d1aacec267..4253d60d134 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -129,3 +129,4 @@ test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
 test: global-temp
+test: vacuum-global-temp
diff --git a/src/test/isolation/specs/vacuum-global-temp.spec b/src/test/isolation/specs/vacuum-global-temp.spec
new file mode 100644
index 00000000000..ccdeda24733
--- /dev/null
+++ b/src/test/isolation/specs/vacuum-global-temp.spec
@@ -0,0 +1,70 @@
+# Test vacuuming global temporary relations
+
+teardown {
+  DROP FUNCTION save_xids, cmp_xids, check_new_xids;
+  DROP TABLE vactest, saved_xids;
+}
+
+session s1
+step create {
+  CREATE GLOBAL TEMP TABLE vactest (a int);
+  CREATE TABLE saved_xids(local_xid int, global_xid int, db_xid int);
+
+  CREATE FUNCTION save_xids() RETURNS void
+  BEGIN ATOMIC
+    DELETE FROM saved_xids;
+    INSERT INTO saved_xids VALUES (
+      (SELECT min(relfrozenxid::text::int) FROM pg_temp_class WHERE relfrozenxid != 0),
+      (SELECT min(relfrozenxid::text::int) FROM pg_class WHERE relfrozenxid != 0),
+      (SELECT datfrozenxid::text::int FROM pg_database WHERE datname = current_database())
+    );
+  END;
+
+  CREATE FUNCTION cmp_xids(xid1 int, xid2 int) RETURNS text
+  BEGIN ATOMIC
+    SELECT CASE
+             WHEN xid1 < xid2 THEN 'older'
+             WHEN xid1 > xid2 THEN 'younger'
+             ELSE 'same'
+           END;
+  END;
+
+  CREATE FUNCTION check_new_xids(OUT local_xid text,
+                                 OUT global_xid text,
+                                 OUT db_xid text)
+  BEGIN ATOMIC
+    WITH new_xids(new_local_xid, new_global_xid, new_db_xid) AS (
+      SELECT
+        (SELECT min(relfrozenxid::text::int) FROM pg_temp_class WHERE relfrozenxid != 0),
+        (SELECT min(relfrozenxid::text::int) FROM pg_class WHERE relfrozenxid != 0),
+        (SELECT datfrozenxid::text::int FROM pg_database WHERE datname = current_database())
+    )
+    SELECT cmp_xids(new_local_xid, local_xid),
+           cmp_xids(new_global_xid, global_xid),
+           cmp_xids(new_db_xid, db_xid)
+    FROM saved_xids, new_xids;
+  END;
+}
+step vacdml1 {
+  INSERT INTO vactest SELECT * FROM generate_series(1, 10);
+  DELETE FROM vactest WHERE a % 2 = 0;
+  INSERT INTO vactest SELECT a * 2 FROM vactest;
+}
+step vac1prep { SELECT save_xids(); }
+step vac1 { VACUUM (FREEZE); }
+step vac1cmp { SELECT * FROM check_new_xids(); }
+
+session s2
+step vacdml2 {
+  INSERT INTO vactest SELECT * FROM generate_series(1, 10);
+  UPDATE vactest SET a = a * 2 WHERE a % 2 = 1;
+}
+step vac2prep { SELECT save_xids(); }
+step vac2 { VACUUM (FREEZE); }
+step vac2cmp { SELECT * FROM check_new_xids(); }
+
+permutation
+  create vacdml1 vac1 vacdml2 vac2
+  vacdml1 vac1prep vac1 vac1cmp
+  vacdml2 vac2prep vac2 vac2cmp
+
-- 
2.51.0

