diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 9498cbb..3451891 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -2198,13 +2198,9 @@ heap_prepare_insert(Relation relation, HeapTuple tup, TransactionId xid,
 	tup->t_data->t_infomask &= ~(HEAP_XACT_MASK);
 	tup->t_data->t_infomask2 &= ~(HEAP2_XACT_MASK);
 	tup->t_data->t_infomask |= HEAP_XMAX_INVALID;
+	HeapTupleHeaderSetXmin(tup->t_data, xid);
 	if (options & HEAP_INSERT_FROZEN)
-	{
-		tup->t_data->t_infomask |= HEAP_XMIN_COMMITTED;
-		HeapTupleHeaderSetXmin(tup->t_data, FrozenTransactionId);
-	}
-	else
-		HeapTupleHeaderSetXmin(tup->t_data, xid);
+		HeapTupleHeaderSetXminFrozen(tup->t_data);
 	HeapTupleHeaderSetCmin(tup->t_data, cid);
 	HeapTupleHeaderSetXmax(tup->t_data, 0);		/* for cleanliness */
 	tup->t_tableOid = RelationGetRelid(relation);
@@ -5087,19 +5083,15 @@ heap_freeze_tuple(HeapTupleHeader tuple, TransactionId cutoff_xid,
 	bool		changed = false;
 	TransactionId xid;
 
-	xid = HeapTupleHeaderGetXmin(tuple);
-	if (TransactionIdIsNormal(xid) &&
-		TransactionIdPrecedes(xid, cutoff_xid))
+	if (!HeapTupleHeaderXminFrozen(tuple))
 	{
-		HeapTupleHeaderSetXmin(tuple, FrozenTransactionId);
-
-		/*
-		 * Might as well fix the hint bits too; usually XMIN_COMMITTED will
-		 * already be set here, but there's a small chance not.
-		 */
-		Assert(!(tuple->t_infomask & HEAP_XMIN_INVALID));
-		tuple->t_infomask |= HEAP_XMIN_COMMITTED;
-		changed = true;
+		xid = HeapTupleHeaderGetXmin(tuple);
+		if (TransactionIdIsNormal(xid) &&
+			TransactionIdPrecedes(xid, cutoff_xid))
+		{
+			HeapTupleHeaderSetXminFrozen(tuple);
+			changed = true;
+		}
 	}
 
 	/*
@@ -5152,8 +5144,7 @@ heap_freeze_tuple(HeapTupleHeader tuple, TransactionId cutoff_xid,
 			 * Might as well fix the hint bits too; usually XMIN_COMMITTED
 			 * will already be set here, but there's a small chance not.
 			 */
-			Assert(!(tuple->t_infomask & HEAP_XMIN_INVALID));
-			tuple->t_infomask |= HEAP_XMIN_COMMITTED;
+			HeapTupleHeaderSetXminCommitted(tuple);
 			changed = true;
 		}
 	}
@@ -5443,10 +5434,13 @@ heap_tuple_needs_freeze(HeapTupleHeader tuple, TransactionId cutoff_xid,
 {
 	TransactionId xid;
 
-	xid = HeapTupleHeaderGetXmin(tuple);
-	if (TransactionIdIsNormal(xid) &&
-		TransactionIdPrecedes(xid, cutoff_xid))
-		return true;
+	if (!HeapTupleHeaderXminFrozen(tuple))
+	{
+		xid = HeapTupleHeaderGetXmin(tuple);
+		if (TransactionIdIsNormal(xid) &&
+			TransactionIdPrecedes(xid, cutoff_xid))
+			return true;
+	}
 
 	if (!(tuple->t_infomask & HEAP_XMAX_INVALID))
 	{
@@ -5572,12 +5566,10 @@ HeapTupleHeaderAdvanceLatestRemovedXid(HeapTupleHeader tuple,
 	 * This needs to work on both master and standby, where it is used to
 	 * assess btree delete records.
 	 */
-	if ((tuple->t_infomask & HEAP_XMIN_COMMITTED) ||
-		(!(tuple->t_infomask & HEAP_XMIN_COMMITTED) &&
-		 !(tuple->t_infomask & HEAP_XMIN_INVALID) &&
-		 TransactionIdDidCommit(xmin)))
+	if (HeapTupleHeaderXminCommitted(tuple) ||
+		(!HeapTupleHeaderXminInvalid(tuple) && TransactionIdDidCommit(xmin)))
 	{
-		if (xmax != xmin &&
+		if ((HeapTupleHeaderXminFrozen(tuple) || xmax != xmin) &&
 			TransactionIdFollows(xmax, *latestRemovedXid))
 			*latestRemovedXid = xmax;
 	}
diff --git a/src/backend/access/heap/rewriteheap.c b/src/backend/access/heap/rewriteheap.c
index a3aad3a..09650d8 100644
--- a/src/backend/access/heap/rewriteheap.c
+++ b/src/backend/access/heap/rewriteheap.c
@@ -437,6 +437,7 @@ rewrite_heap_tuple(RewriteState state,
 		 * RECENTLY_DEAD if and only if the xmin is not before OldestXmin.
 		 */
 		if ((new_tuple->t_data->t_infomask & HEAP_UPDATED) &&
+			!HeapTupleHeaderXminFrozen(new_tuple->t_data) &&
 			!TransactionIdPrecedes(HeapTupleHeaderGetXmin(new_tuple->t_data),
 								   state->rs_oldest_xmin))
 		{
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 7966558..280c13c 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2317,6 +2317,7 @@ IndexBuildHeapScan(Relation heapRelation,
 					 * before commit there.  Give a warning if neither case
 					 * applies.
 					 */
+					Assert(!HeapTupleHeaderXminFrozen(heapTuple->t_data));
 					xwait = HeapTupleHeaderGetXmin(heapTuple->t_data);
 					if (!TransactionIdIsCurrentTransactionId(xwait))
 					{
diff --git a/src/backend/commands/analyze.c b/src/backend/commands/analyze.c
index d6d20fd..e2fc9a2 100644
--- a/src/backend/commands/analyze.c
+++ b/src/backend/commands/analyze.c
@@ -1177,6 +1177,7 @@ acquire_sample_rows(Relation onerel, int elevel,
 					 * has to adjust the numbers we send to the stats
 					 * collector to make this come out right.)
 					 */
+					Assert(!HeapTupleHeaderXminFrozen(targtuple.t_data));
 					if (TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetXmin(targtuple.t_data)))
 					{
 						sample_it = true;
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 878b625..01009f9 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -981,6 +981,7 @@ copy_heap_data(Oid OIDNewHeap, Oid OIDOldHeap, Oid OIDOldIndex,
 				 * there.  Give a warning if neither case applies; but in any
 				 * case we had better copy it.
 				 */
+				Assert(!HeapTupleHeaderXminFrozen(tuple->t_data));
 				if (!is_system_catalog &&
 					!TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetXmin(tuple->t_data)))
 					elog(WARNING, "concurrent insert in progress within table \"%s\"",
diff --git a/src/backend/commands/sequence.c b/src/backend/commands/sequence.c
index 49e409a..522ae33 100644
--- a/src/backend/commands/sequence.c
+++ b/src/backend/commands/sequence.c
@@ -364,10 +364,10 @@ fill_seq_with_data(Relation rel, HeapTuple tuple)
 		item = PageGetItem((Page) page, itemId);
 
 		HeapTupleHeaderSetXmin((HeapTupleHeader) item, FrozenTransactionId);
-		((HeapTupleHeader) item)->t_infomask |= HEAP_XMIN_COMMITTED;
+		HeapTupleHeaderSetXminFrozen((HeapTupleHeader) item);
 
 		HeapTupleHeaderSetXmin(tuple->t_data, FrozenTransactionId);
-		tuple->t_data->t_infomask |= HEAP_XMIN_COMMITTED;
+		HeapTupleHeaderSetXminFrozen(tuple->t_data);
 	}
 
 	MarkBufferDirty(buf);
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 9efe244..ebbea7c 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -1201,7 +1201,8 @@ AlterEnum(AlterEnumStmt *stmt, bool isTopLevel)
 	 * some cases that could theoretically be safe; but fortunately pg_dump
 	 * only needs the simplest case.
 	 */
-	if (HeapTupleHeaderGetXmin(tup->t_data) == GetCurrentTransactionId() &&
+	if (!HeapTupleHeaderXminFrozen(tup->t_data) &&
+		HeapTupleHeaderGetXmin(tup->t_data) == GetCurrentTransactionId() &&
 		!(tup->t_data->t_infomask & HEAP_UPDATED))
 		/* safe to do inside transaction block */ ;
 	else
diff --git a/src/backend/commands/vacuumlazy.c b/src/backend/commands/vacuumlazy.c
index 9d30415..af2639c 100644
--- a/src/backend/commands/vacuumlazy.c
+++ b/src/backend/commands/vacuumlazy.c
@@ -787,16 +787,19 @@ lazy_scan_heap(Relation onerel, LVRelStats *vacrelstats,
 					{
 						TransactionId xmin;
 
-						if (!(tuple.t_data->t_infomask & HEAP_XMIN_COMMITTED))
+						if (!HeapTupleHeaderXminCommitted(tuple.t_data))
 						{
 							all_visible = false;
 							break;
 						}
 
+
 						/*
 						 * The inserter definitely committed. But is it old
 						 * enough that everyone sees it as committed?
 						 */
+						if (HeapTupleHeaderXminFrozen(tuple.t_data))
+							break;
 						xmin = HeapTupleHeaderGetXmin(tuple.t_data);
 						if (!TransactionIdPrecedes(xmin, OldestXmin))
 						{
@@ -1709,7 +1712,7 @@ heap_page_is_all_visible(Buffer buf, TransactionId *visibility_cutoff_xid)
 					TransactionId xmin;
 
 					/* Check comments in lazy_scan_heap. */
-					if (!(tuple.t_data->t_infomask & HEAP_XMIN_COMMITTED))
+					if (!HeapTupleHeaderXminCommitted(tuple.t_data))
 					{
 						all_visible = false;
 						break;
@@ -1719,6 +1722,8 @@ heap_page_is_all_visible(Buffer buf, TransactionId *visibility_cutoff_xid)
 					 * The inserter definitely committed. But is it old
 					 * enough that everyone sees it as committed?
 					 */
+					if (HeapTupleHeaderXminFrozen(tuple.t_data))
+						break;
 					xmin = HeapTupleHeaderGetXmin(tuple.t_data);
 					if (!TransactionIdPrecedes(xmin, OldestXmin))
 					{
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index 954666c..e22be0e 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -188,6 +188,7 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent,
 			 * src/backend/access/heap/README.HOT for discussion.
 			 */
 			if (index->indcheckxmin &&
+				!HeapTupleHeaderXminFrozen(indexRelation->rd_indextuple->t_data) &&
 				!TransactionIdPrecedes(HeapTupleHeaderGetXmin(indexRelation->rd_indextuple->t_data),
 									   TransactionXmin))
 			{
diff --git a/src/backend/storage/lmgr/predicate.c b/src/backend/storage/lmgr/predicate.c
index 6029cfb..a4c6924 100644
--- a/src/backend/storage/lmgr/predicate.c
+++ b/src/backend/storage/lmgr/predicate.c
@@ -2475,7 +2475,10 @@ PredicateLockTuple(Relation relation, HeapTuple tuple, Snapshot snapshot)
 	{
 		TransactionId myxid;
 
-		targetxmin = HeapTupleHeaderGetXmin(tuple->t_data);
+		if (HeapTupleHeaderXminFrozen(tuple->t_data))
+			targetxmin = FrozenTransactionId;
+		else
+			targetxmin = HeapTupleHeaderGetXmin(tuple->t_data);
 
 		myxid = GetTopTransactionIdIfAny();
 		if (TransactionIdIsValid(myxid))
@@ -3901,6 +3904,7 @@ CheckForSerializableConflictOut(bool visible, Relation relation,
 		case HEAPTUPLE_LIVE:
 			if (visible)
 				return;
+			Assert(!HeapTupleHeaderXminFrozen(tuple->t_data));
 			xid = HeapTupleHeaderGetXmin(tuple->t_data);
 			break;
 		case HEAPTUPLE_RECENTLY_DEAD:
@@ -3912,6 +3916,7 @@ CheckForSerializableConflictOut(bool visible, Relation relation,
 			xid = HeapTupleHeaderGetUpdateXid(tuple->t_data);
 			break;
 		case HEAPTUPLE_INSERT_IN_PROGRESS:
+			Assert(!HeapTupleHeaderXminFrozen(tuple->t_data));
 			xid = HeapTupleHeaderGetXmin(tuple->t_data);
 			break;
 		case HEAPTUPLE_DEAD:
@@ -4279,12 +4284,19 @@ CheckForSerializableConflictIn(Relation relation, HeapTuple tuple,
 	 */
 	if (tuple != NULL)
 	{
+		TransactionId	xmin;
+
+		if (HeapTupleHeaderXminFrozen(tuple->t_data))
+			xmin = FrozenTransactionId;
+		else
+			xmin = HeapTupleHeaderGetXmin(tuple->t_data);
+
 		SET_PREDICATELOCKTARGETTAG_TUPLE(targettag,
 										 relation->rd_node.dbNode,
 										 relation->rd_id,
 						 ItemPointerGetBlockNumber(&(tuple->t_data->t_ctid)),
 						ItemPointerGetOffsetNumber(&(tuple->t_data->t_ctid)),
-									  HeapTupleHeaderGetXmin(tuple->t_data));
+										 xmin);
 		CheckTargetForConflictsIn(&targettag);
 	}
 
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 4322844..a67643a 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -2165,7 +2165,8 @@ RI_FKey_fk_upd_check_required(Trigger *trigger, Relation fk_rel,
 			 * UPDATE check.  (We could skip this if we knew the INSERT
 			 * trigger already fired, but there is no easy way to know that.)
 			 */
-			if (TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetXmin(old_row->t_data)))
+			if (!HeapTupleHeaderXminFrozen(old_row->t_data) &&
+				TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetXmin(old_row->t_data)))
 				return true;
 
 			/* If all old and new key values are equal, no check is needed */
@@ -2202,7 +2203,8 @@ RI_FKey_fk_upd_check_required(Trigger *trigger, Relation fk_rel,
 			 * UPDATE check.  (We could skip this if we knew the INSERT
 			 * trigger already fired, but there is no easy way to know that.)
 			 */
-			if (TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetXmin(old_row->t_data)))
+			if (!HeapTupleHeaderXminFrozen(old_row->t_data) &&
+				TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetXmin(old_row->t_data)))
 				return true;
 
 			/* If all old and new key values are equal, no check is needed */
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 7888d38..239582e 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -1764,6 +1764,7 @@ RelationReloadIndexInfo(Relation relation)
 	{
 		HeapTuple	tuple;
 		Form_pg_index index;
+		TransactionId xmin;
 
 		tuple = SearchSysCache1(INDEXRELID,
 								ObjectIdGetDatum(RelationGetRelid(relation)));
@@ -1789,8 +1790,11 @@ RelationReloadIndexInfo(Relation relation)
 		relation->rd_index->indislive = index->indislive;
 
 		/* Copy xmin too, as that is needed to make sense of indcheckxmin */
-		HeapTupleHeaderSetXmin(relation->rd_indextuple->t_data,
-							   HeapTupleHeaderGetXmin(tuple->t_data));
+		if (HeapTupleHeaderXminFrozen(relation->rd_indextuple->t_data))
+			xmin = FrozenTransactionId;
+		else
+			xmin = HeapTupleHeaderGetXmin(relation->rd_indextuple->t_data);
+		HeapTupleHeaderSetXmin(relation->rd_indextuple->t_data, xmin);
 
 		ReleaseSysCache(tuple);
 	}
diff --git a/src/backend/utils/time/combocid.c b/src/backend/utils/time/combocid.c
index 923355d..baacd4a 100644
--- a/src/backend/utils/time/combocid.c
+++ b/src/backend/utils/time/combocid.c
@@ -148,10 +148,12 @@ HeapTupleHeaderAdjustCmax(HeapTupleHeader tup,
 	/*
 	 * If we're marking a tuple deleted that was inserted by (any
 	 * subtransaction of) our transaction, we need to use a combo command id.
-	 * Test for HEAP_XMIN_COMMITTED first, because it's cheaper than a
-	 * TransactionIdIsCurrentTransactionId call.
+	 * It's cheaper to test HeapTupleHeaderXminCommitted before checking
+	 * TransactionIdIsCurrentTransactionId, but it's also necessary for
+	 * correctness: if HeapTupleHeaderXminCommitted returns true, the tuple
+	 * might even be frozen.
 	 */
-	if (!(tup->t_infomask & HEAP_XMIN_COMMITTED) &&
+	if (!HeapTupleHeaderXminCommitted(tup) &&
 		TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetXmin(tup)))
 	{
 		CommandId	cmin = HeapTupleHeaderGetCmin(tup);
diff --git a/src/backend/utils/time/tqual.c b/src/backend/utils/time/tqual.c
index 24384b4..03a0e57 100644
--- a/src/backend/utils/time/tqual.c
+++ b/src/backend/utils/time/tqual.c
@@ -165,9 +165,9 @@ HeapTupleSetHintBits(HeapTupleHeader tuple, Buffer buffer,
 bool
 HeapTupleSatisfiesSelf(HeapTupleHeader tuple, Snapshot snapshot, Buffer buffer)
 {
-	if (!(tuple->t_infomask & HEAP_XMIN_COMMITTED))
+	if (!HeapTupleHeaderXminCommitted(tuple))
 	{
-		if (tuple->t_infomask & HEAP_XMIN_INVALID)
+		if (HeapTupleHeaderXminInvalid(tuple))
 			return false;
 
 		/* Used by pre-9.0 binary upgrades */
@@ -353,9 +353,9 @@ HeapTupleSatisfiesSelf(HeapTupleHeader tuple, Snapshot snapshot, Buffer buffer)
 bool
 HeapTupleSatisfiesNow(HeapTupleHeader tuple, Snapshot snapshot, Buffer buffer)
 {
-	if (!(tuple->t_infomask & HEAP_XMIN_COMMITTED))
+	if (!HeapTupleHeaderXminCommitted(tuple))
 	{
-		if (tuple->t_infomask & HEAP_XMIN_INVALID)
+		if (HeapTupleHeaderXminInvalid(tuple))
 			return false;
 
 		/* Used by pre-9.0 binary upgrades */
@@ -549,9 +549,9 @@ bool
 HeapTupleSatisfiesToast(HeapTupleHeader tuple, Snapshot snapshot,
 						Buffer buffer)
 {
-	if (!(tuple->t_infomask & HEAP_XMIN_COMMITTED))
+	if (!HeapTupleHeaderXminCommitted(tuple))
 	{
-		if (tuple->t_infomask & HEAP_XMIN_INVALID)
+		if (HeapTupleHeaderXminInvalid(tuple))
 			return false;
 
 		/* Used by pre-9.0 binary upgrades */
@@ -630,9 +630,9 @@ HTSU_Result
 HeapTupleSatisfiesUpdate(HeapTupleHeader tuple, CommandId curcid,
 						 Buffer buffer)
 {
-	if (!(tuple->t_infomask & HEAP_XMIN_COMMITTED))
+	if (!HeapTupleHeaderXminCommitted(tuple))
 	{
-		if (tuple->t_infomask & HEAP_XMIN_INVALID)
+		if (HeapTupleHeaderXminInvalid(tuple))
 			return HeapTupleInvisible;
 
 		/* Used by pre-9.0 binary upgrades */
@@ -853,9 +853,9 @@ HeapTupleSatisfiesDirty(HeapTupleHeader tuple, Snapshot snapshot,
 {
 	snapshot->xmin = snapshot->xmax = InvalidTransactionId;
 
-	if (!(tuple->t_infomask & HEAP_XMIN_COMMITTED))
+	if (!HeapTupleHeaderXminCommitted(tuple))
 	{
-		if (tuple->t_infomask & HEAP_XMIN_INVALID)
+		if (HeapTupleHeaderXminInvalid(tuple))
 			return false;
 
 		/* Used by pre-9.0 binary upgrades */
@@ -1042,9 +1042,9 @@ bool
 HeapTupleSatisfiesMVCC(HeapTupleHeader tuple, Snapshot snapshot,
 					   Buffer buffer)
 {
-	if (!(tuple->t_infomask & HEAP_XMIN_COMMITTED))
+	if (!HeapTupleHeaderXminCommitted(tuple))
 	{
-		if (tuple->t_infomask & HEAP_XMIN_INVALID)
+		if (HeapTupleHeaderXminInvalid(tuple))
 			return false;
 
 		/* Used by pre-9.0 binary upgrades */
@@ -1145,7 +1145,8 @@ HeapTupleSatisfiesMVCC(HeapTupleHeader tuple, Snapshot snapshot,
 	 * By here, the inserting transaction has committed - have to check
 	 * when...
 	 */
-	if (XidInMVCCSnapshot(HeapTupleHeaderGetXmin(tuple), snapshot))
+	if (!HeapTupleHeaderXminFrozen(tuple)
+		&& XidInMVCCSnapshot(HeapTupleHeaderGetXmin(tuple), snapshot))
 		return false;			/* treat as still in progress */
 
 	if (tuple->t_infomask & HEAP_XMAX_INVALID)	/* xid invalid or aborted */
@@ -1241,9 +1242,9 @@ HeapTupleSatisfiesVacuum(HeapTupleHeader tuple, TransactionId OldestXmin,
 	 * If the inserting transaction aborted, then the tuple was never visible
 	 * to any other transaction, so we can delete it immediately.
 	 */
-	if (!(tuple->t_infomask & HEAP_XMIN_COMMITTED))
+	if (!HeapTupleHeaderXminCommitted(tuple))
 	{
-		if (tuple->t_infomask & HEAP_XMIN_INVALID)
+		if (HeapTupleHeaderXminInvalid(tuple))
 			return HEAPTUPLE_DEAD;
 		/* Used by pre-9.0 binary upgrades */
 		else if (tuple->t_infomask & HEAP_MOVED_OFF)
@@ -1471,8 +1472,8 @@ HeapTupleIsSurelyDead(HeapTupleHeader tuple, TransactionId OldestXmin)
 	 * invalid, then we assume it's still alive (since the presumption is that
 	 * all relevant hint bits were just set moments ago).
 	 */
-	if (!(tuple->t_infomask & HEAP_XMIN_COMMITTED))
-		return (tuple->t_infomask & HEAP_XMIN_INVALID) != 0 ? true : false;
+	if (!HeapTupleHeaderXminCommitted(tuple))
+		return HeapTupleHeaderXminInvalid(tuple) ? true : false;
 
 	/*
 	 * If the inserting transaction committed, but any deleting transaction
diff --git a/src/include/access/htup_details.h b/src/include/access/htup_details.h
index cd01ecd..56579ef 100644
--- a/src/include/access/htup_details.h
+++ b/src/include/access/htup_details.h
@@ -174,6 +174,7 @@ struct HeapTupleHeaderData
 						 HEAP_XMAX_KEYSHR_LOCK)
 #define HEAP_XMIN_COMMITTED		0x0100	/* t_xmin committed */
 #define HEAP_XMIN_INVALID		0x0200	/* t_xmin invalid/aborted */
+#define HEAP_XMIN_FROZEN		(HEAP_XMIN_COMMITTED|HEAP_XMIN_INVALID)
 #define HEAP_XMAX_COMMITTED		0x0400	/* t_xmax committed */
 #define HEAP_XMAX_INVALID		0x0800	/* t_xmax invalid/aborted */
 #define HEAP_XMAX_IS_MULTI		0x1000	/* t_xmax is a MultiXactId */
@@ -253,6 +254,37 @@ struct HeapTupleHeaderData
 	(tup)->t_choice.t_heap.t_xmin = (xid) \
 )
 
+#define HeapTupleHeaderXminCommitted(tup) \
+( \
+	((tup)->t_infomask & HEAP_XMIN_COMMITTED) == HEAP_XMIN_COMMITTED \
+)
+#define HeapTupleHeaderXminInvalid(tup) \
+( \
+	((tup)->t_infomask & (HEAP_XMIN_COMMITTED|HEAP_XMIN_INVALID)) == \
+		HEAP_XMIN_INVALID \
+)
+#define HeapTupleHeaderXminFrozen(tup) \
+( \
+	((tup)->t_infomask & (HEAP_XMIN_COMMITTED|HEAP_XMIN_INVALID)) == \
+		HEAP_XMIN_FROZEN \
+)
+
+#define HeapTupleHeaderSetXminCommitted(tup) \
+( \
+	AssertMacro(!HeapTupleHeaderXminInvalid(tup)), \
+	((tup)->t_infomask |= HEAP_XMIN_COMMITTED) \
+)
+#define HeapTupleHeaderSetXminInvalid(tup) \
+( \
+	AssertMacro(!HeapTupleHeaderXminCommitted(tup)), \
+	((tup)->t_infomask |= HEAP_XMIN_INVALID) \
+)
+#define HeapTupleHeaderSetXminFrozen(tup) \
+( \
+	AssertMacro(!HeapTupleHeaderXminInvalid(tup)), \
+	((tup)->t_infomask |= HEAP_XMIN_FROZEN) \
+)
+
 /*
  * HeapTupleHeaderGetRawXmax gets you the raw Xmax field.  To find out the Xid
  * that updated a tuple, you might need to resolve the MultiXactId if certain
@@ -373,7 +405,8 @@ do { \
 #define HeapTupleHeaderIsHotUpdated(tup) \
 ( \
 	((tup)->t_infomask2 & HEAP_HOT_UPDATED) != 0 && \
-	((tup)->t_infomask & (HEAP_XMIN_INVALID | HEAP_XMAX_INVALID)) == 0 \
+	((tup)->t_infomask & HEAP_XMAX_INVALID) == 0 && \
+	!HeapTupleHeaderXminInvalid(tup) \
 )
 
 #define HeapTupleHeaderSetHotUpdated(tup) \
