From b1051fe3224fedb5595e8f8ace1acf8ec04cb30b Mon Sep 17 00:00:00 2001
From: Nathan Bossart <bossartn@amazon.com>
Date: Tue, 9 Feb 2021 18:07:52 +0000
Subject: [PATCH 1/1] Early prototype for partial heap only tuples (PHOT).

This patch demonstrates a few basic pieces of the partial heap only
tuples project.
---
 contrib/pageinspect/expected/page.out     |   4 +-
 contrib/pageinspect/heapfuncs.c           |   5 +
 src/backend/access/heap/heapam.c          | 151 +++++++++++++++++++++++++-----
 src/backend/access/heap/heapam_handler.c  |  22 +++--
 src/backend/access/heap/pruneheap.c       |  19 +++-
 src/backend/access/heap/vacuumlazy.c      |   2 +
 src/backend/access/index/indexam.c        |   3 +-
 src/backend/access/nbtree/nbtinsert.c     |   5 +-
 src/backend/access/rmgrdesc/heapdesc.c    |  38 ++++++++
 src/backend/access/table/tableam.c        |  12 ++-
 src/backend/commands/constraint.c         |   7 +-
 src/backend/commands/copyfrom.c           |   4 +-
 src/backend/executor/execIndexing.c       |  27 +++++-
 src/backend/executor/execReplication.c    |  14 ++-
 src/backend/executor/nodeBitmapHeapscan.c |  26 ++++-
 src/backend/executor/nodeModifyTable.c    |  18 ++--
 src/backend/replication/logical/decode.c  |  36 +++++++
 src/include/access/heapam.h               |   5 +-
 src/include/access/heapam_xlog.h          |  13 +++
 src/include/access/htup_details.h         |  57 ++++++++++-
 src/include/access/rmgrlist.h             |   1 +
 src/include/access/tableam.h              |  29 +++---
 src/include/executor/executor.h           |   3 +-
 23 files changed, 426 insertions(+), 75 deletions(-)

diff --git a/contrib/pageinspect/expected/page.out b/contrib/pageinspect/expected/page.out
index 4da28f0a1d..81807c89d8 100644
--- a/contrib/pageinspect/expected/page.out
+++ b/contrib/pageinspect/expected/page.out
@@ -140,6 +140,8 @@ SELECT unnest(raw_flags)
  HEAP_MOVED_IN
  HEAP_MOVED_OFF
  HEAP_ONLY_TUPLE
+ HEAP_PHOT_TUPLE
+ HEAP_PHOT_UPDATED
  HEAP_UPDATED
  HEAP_XMAX_COMMITTED
  HEAP_XMAX_EXCL_LOCK
@@ -149,7 +151,7 @@ SELECT unnest(raw_flags)
  HEAP_XMAX_LOCK_ONLY
  HEAP_XMIN_COMMITTED
  HEAP_XMIN_INVALID
-(19 rows)
+(21 rows)
 
 SELECT unnest(combined_flags)
   FROM heap_tuple_infomask_flags(x'FFFF'::int, x'FFFF'::int) ORDER BY 1;
diff --git a/contrib/pageinspect/heapfuncs.c b/contrib/pageinspect/heapfuncs.c
index 9abcee32af..bee811d5ef 100644
--- a/contrib/pageinspect/heapfuncs.c
+++ b/contrib/pageinspect/heapfuncs.c
@@ -587,6 +587,11 @@ heap_tuple_infomask_flags(PG_FUNCTION_ARGS)
 		flags[cnt++] = CStringGetTextDatum("HEAP_HOT_UPDATED");
 	if ((t_infomask2 & HEAP_ONLY_TUPLE) != 0)
 		flags[cnt++] = CStringGetTextDatum("HEAP_ONLY_TUPLE");
+	if ((t_infomask2 & HEAP_PHOT_UPDATED) != 0)
+		flags[cnt++] = CStringGetTextDatum("HEAP_PHOT_UPDATED");
+	if ((t_infomask2 & HEAP_PHOT_TUPLE) != 0)
+		flags[cnt++] = CStringGetTextDatum("HEAP_PHOT_TUPLE");
+
 
 	/* build value */
 	Assert(cnt <= bitcnt);
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 9926e2bd54..9aa0a50b57 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -1538,16 +1538,21 @@ heap_fetch(Relation relation,
 bool
 heap_hot_search_buffer(ItemPointer tid, Relation relation, Buffer buffer,
 					   Snapshot snapshot, HeapTuple heapTuple,
-					   bool *all_dead, bool first_call)
+					   bool *all_dead, bool first_call,
+					   Relation indrel, Bitmapset *interesting_attrs)
 {
 	Page		dp = (Page) BufferGetPage(buffer);
 	TransactionId prev_xmax = InvalidTransactionId;
 	BlockNumber blkno;
 	OffsetNumber offnum;
+	OffsetNumber prev_offnum = InvalidOffsetNumber;
 	bool		at_chain_start;
 	bool		valid;
 	bool		skip;
 	GlobalVisState *vistest = NULL;
+	HeapTuple prev_tup = NULL;
+	Bitmapset *ind_attrs = NULL;
+	bool all_attrs = false;
 
 	/* If this is not the first call, previous call returned a (live!) tuple */
 	if (all_dead)
@@ -1562,6 +1567,21 @@ heap_hot_search_buffer(ItemPointer tid, Relation relation, Buffer buffer,
 	Assert(TransactionIdIsValid(RecentXmin));
 	Assert(BufferGetBlockNumber(buffer) == blkno);
 
+	if (indrel)
+	{
+		Form_pg_index indexStruct = indrel->rd_index;
+
+		for (int i = 0; i < indexStruct->indnatts; i++)
+		{
+			int ind_attr = indexStruct->indkey.values[i] - FirstLowInvalidHeapAttributeNumber;
+			ind_attrs = bms_add_member(ind_attrs, ind_attr);
+		}
+	}
+	else if (interesting_attrs)
+		ind_attrs = interesting_attrs;
+	else
+		all_attrs = true;
+
 	/* Scan through possible multiple members of HOT-chain */
 	for (;;)
 	{
@@ -1599,6 +1619,24 @@ heap_hot_search_buffer(ItemPointer tid, Relation relation, Buffer buffer,
 		heapTuple->t_tableOid = RelationGetRelid(relation);
 		ItemPointerSet(&heapTuple->t_self, blkno, offnum);
 
+		/*
+		 * If this tuple was a PHOT update, make sure that we should keep
+		 * following it based on the index attributes and the modified ones.
+		 */
+		if (OffsetNumberIsValid(prev_offnum) &&
+			HeapTupleIsValid(prev_tup) && HeapTupleIsPartialHeapOnly(heapTuple))
+		{
+			Bitmapset *modified_attrs;
+
+			if (all_attrs)
+				break;
+
+			modified_attrs = HeapDetermineModifiedColumns(relation, bms_copy(ind_attrs),
+																	 prev_tup, heapTuple);
+			if (!bms_is_empty(modified_attrs))
+				break;
+		}
+
 		/*
 		 * Shouldn't see a HEAP_ONLY tuple at chain start.
 		 */
@@ -1661,13 +1699,15 @@ heap_hot_search_buffer(ItemPointer tid, Relation relation, Buffer buffer,
 		 * Check to see if HOT chain continues past this tuple; if so fetch
 		 * the next offnum and loop around.
 		 */
-		if (HeapTupleIsHotUpdated(heapTuple))
+		if (HeapTupleIsHotUpdated(heapTuple) || HeapTupleIsPartialHotUpdated(heapTuple))
 		{
 			Assert(ItemPointerGetBlockNumber(&heapTuple->t_data->t_ctid) ==
 				   blkno);
+			prev_offnum = offnum;
 			offnum = ItemPointerGetOffsetNumber(&heapTuple->t_data->t_ctid);
 			at_chain_start = false;
 			prev_xmax = HeapTupleHeaderGetUpdateXid(heapTuple->t_data);
+			prev_tup = heap_copytuple(heapTuple);
 		}
 		else
 			break;				/* end of chain */
@@ -3043,7 +3083,8 @@ simple_heap_delete(Relation relation, ItemPointer tid)
 TM_Result
 heap_update(Relation relation, ItemPointer otid, HeapTuple newtup,
 			CommandId cid, Snapshot crosscheck, bool wait,
-			TM_FailureData *tmfd, LockTupleMode *lockmode)
+			TM_FailureData *tmfd, LockTupleMode *lockmode,
+			Bitmapset **modified_attrs)
 {
 	TM_Result	result;
 	TransactionId xid = GetCurrentTransactionId();
@@ -3051,7 +3092,6 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup,
 	Bitmapset  *key_attrs;
 	Bitmapset  *id_attrs;
 	Bitmapset  *interesting_attrs;
-	Bitmapset  *modified_attrs;
 	ItemId		lp;
 	HeapTupleData oldtup;
 	HeapTuple	heaptup;
@@ -3070,6 +3110,7 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup,
 	bool		have_tuple_lock = false;
 	bool		iscombo;
 	bool		use_hot_update = false;
+	bool		use_phot_update = false;
 	bool		hot_attrs_checked = false;
 	bool		key_intact;
 	bool		all_visible_cleared = false;
@@ -3168,7 +3209,7 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup,
 	newtup->t_tableOid = RelationGetRelid(relation);
 
 	/* Determine columns modified by the update. */
-	modified_attrs = HeapDetermineModifiedColumns(relation, interesting_attrs,
+	*modified_attrs = HeapDetermineModifiedColumns(relation, interesting_attrs,
 												  &oldtup, newtup);
 
 	/*
@@ -3182,7 +3223,7 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup,
 	 * is updates that don't manipulate key columns, not those that
 	 * serendipitously arrive at the same key values.
 	 */
-	if (!bms_overlap(modified_attrs, key_attrs))
+	if (!bms_overlap(*modified_attrs, key_attrs))
 	{
 		*lockmode = LockTupleNoKeyExclusive;
 		mxact_status = MultiXactStatusNoKeyUpdate;
@@ -3439,7 +3480,6 @@ l2:
 		bms_free(hot_attrs);
 		bms_free(key_attrs);
 		bms_free(id_attrs);
-		bms_free(modified_attrs);
 		bms_free(interesting_attrs);
 		return result;
 	}
@@ -3747,8 +3787,25 @@ l2:
 		 * changed. If the page was already full, we may have skipped checking
 		 * for index columns, and also can't do a HOT update.
 		 */
-		if (hot_attrs_checked && !bms_overlap(modified_attrs, hot_attrs))
+		if (hot_attrs_checked && !bms_overlap(*modified_attrs, hot_attrs))
 			use_hot_update = true;
+
+		/*
+		 * PHOT only takes effect if HOT updating will not work AND if we are
+		 * not updating all of the indexed columns. Currently this can not be
+		 * done for catalog tables because the necessary changes haven't been
+		 * made to CatalogIndexInsert().
+		 */
+		if (hot_attrs_checked && !use_hot_update && !IsCatalogRelation(relation))
+		{
+			Bitmapset *updated_indexed_attrs = bms_intersect(*modified_attrs, hot_attrs);
+			bool updating_all_indexed_attrs = bms_equal(hot_attrs, updated_indexed_attrs);
+
+			if (!updating_all_indexed_attrs)
+				use_phot_update = true;
+
+			bms_free(updated_indexed_attrs);
+		}
 	}
 	else
 	{
@@ -3763,7 +3820,7 @@ l2:
 	 * logged.
 	 */
 	old_key_tuple = ExtractReplicaIdentity(relation, &oldtup,
-										   bms_overlap(modified_attrs, id_attrs),
+										   bms_overlap(*modified_attrs, id_attrs),
 										   &old_key_copied);
 
 	/* NO EREPORT(ERROR) from here till changes are logged */
@@ -3783,8 +3840,22 @@ l2:
 	 */
 	PageSetPrunable(page, xid);
 
+	/*
+	 * Clear all (P)HOT bits before we set them for the new tuple.  This must
+	 * happen before setting the (P)HOT bits, as some of the macros used for
+	 * setting these bits assert that others are not set.
+	 */
+	HeapTupleClearHotUpdated(&oldtup);
+	HeapTupleClearHeapOnly(heaptup);
+	HeapTupleClearHeapOnly(newtup);
+	HeapTupleClearPartialHotUpdated(&oldtup);
+	HeapTupleClearPartialHeapOnly(heaptup);
+	HeapTupleClearPartialHeapOnly(newtup);
+
 	if (use_hot_update)
 	{
+		Assert(!use_phot_update);
+
 		/* Mark the old tuple as HOT-updated */
 		HeapTupleSetHotUpdated(&oldtup);
 		/* And mark the new tuple as heap-only */
@@ -3792,12 +3863,17 @@ l2:
 		/* Mark the caller's copy too, in case different from heaptup */
 		HeapTupleSetHeapOnly(newtup);
 	}
-	else
+
+	if (use_phot_update)
 	{
-		/* Make sure tuples are correctly marked as not-HOT */
-		HeapTupleClearHotUpdated(&oldtup);
-		HeapTupleClearHeapOnly(heaptup);
-		HeapTupleClearHeapOnly(newtup);
+		Assert(!use_hot_update);
+
+		/* Mark the old tuple as PHOT-updated */
+		HeapTupleSetPartialHotUpdated(&oldtup);
+		/* And mark the new tuple as partial heap-only */
+		HeapTupleSetPartialHeapOnly(heaptup);
+		/* Mark the caller's copy too, in case different from heaptup */
+		HeapTupleSetPartialHeapOnly(newtup);
 	}
 
 	RelationPutHeapTuple(relation, newbuf, heaptup, false); /* insert new tuple */
@@ -3912,7 +3988,6 @@ l2:
 	bms_free(hot_attrs);
 	bms_free(key_attrs);
 	bms_free(id_attrs);
-	bms_free(modified_attrs);
 	bms_free(interesting_attrs);
 
 	return TM_Ok;
@@ -4038,11 +4113,14 @@ simple_heap_update(Relation relation, ItemPointer otid, HeapTuple tup)
 	TM_Result	result;
 	TM_FailureData tmfd;
 	LockTupleMode lockmode;
+	Bitmapset *modified_attrs; /* unused */
 
 	result = heap_update(relation, otid, tup,
 						 GetCurrentCommandId(true), InvalidSnapshot,
 						 true /* wait for commit */ ,
-						 &tmfd, &lockmode);
+						 &tmfd, &lockmode, &modified_attrs);
+	bms_free(modified_attrs);
+
 	switch (result)
 	{
 		case TM_SelfModified:
@@ -6439,6 +6517,7 @@ heap_prepare_freeze_tuple(HeapTupleHeader tuple,
 		frz->t_infomask &= ~HEAP_XMAX_BITS;
 		frz->t_infomask |= HEAP_XMAX_INVALID;
 		frz->t_infomask2 &= ~HEAP_HOT_UPDATED;
+		frz->t_infomask2 &= ~HEAP_PHOT_UPDATED;
 		frz->t_infomask2 &= ~HEAP_KEYS_UPDATED;
 		changed = true;
 	}
@@ -7334,7 +7413,7 @@ heap_index_delete_tuples(Relation rel, TM_IndexDeleteOp *delstate)
 
 			/* Are any tuples from this HOT chain non-vacuumable? */
 			if (heap_hot_search_buffer(&tmp, rel, buf, &SnapshotNonVacuumable,
-									   &heapTuple, NULL, true))
+									   &heapTuple, NULL, true, NULL, NULL))
 				continue;		/* can't delete entry */
 
 			/* Caller will delete, since whole HOT chain is vacuumable */
@@ -7979,6 +8058,7 @@ log_heap_update(Relation reln, Buffer oldbuf,
 	bool		need_tuple_data = RelationIsLogicallyLogged(reln);
 	bool		init;
 	int			bufflags;
+	bool		use_rm_heap3 = false;
 
 	/* Caller should not call me on a non-WAL-logged relation */
 	Assert(RelationNeedsWAL(reln));
@@ -7987,6 +8067,11 @@ log_heap_update(Relation reln, Buffer oldbuf,
 
 	if (HeapTupleIsHeapOnly(newtup))
 		info = XLOG_HEAP_HOT_UPDATE;
+	else if (HeapTupleIsPartialHeapOnly(newtup))
+	{
+		info = XLOG_HEAP3_PARTIAL_HOT_UPDATE;
+		use_rm_heap3 = true;
+	}
 	else
 		info = XLOG_HEAP_UPDATE;
 
@@ -8172,7 +8257,10 @@ log_heap_update(Relation reln, Buffer oldbuf,
 	/* filtering by origin on a row level is much more efficient */
 	XLogSetRecordFlags(XLOG_INCLUDE_ORIGIN);
 
-	recptr = XLogInsert(RM_HEAP_ID, info);
+	if (use_rm_heap3)
+		recptr = XLogInsert(RM_HEAP3_ID, info);
+	else
+		recptr = XLogInsert(RM_HEAP_ID, info);
 
 	return recptr;
 }
@@ -9035,7 +9123,7 @@ heap_xlog_multi_insert(XLogReaderState *record)
  * Handles UPDATE and HOT_UPDATE
  */
 static void
-heap_xlog_update(XLogReaderState *record, bool hot_update)
+heap_xlog_update(XLogReaderState *record, bool hot_update, bool phot_update)
 {
 	XLogRecPtr	lsn = record->EndRecPtr;
 	xl_heap_update *xlrec = (xl_heap_update *) XLogRecGetData(record);
@@ -9072,7 +9160,7 @@ heap_xlog_update(XLogReaderState *record, bool hot_update)
 	if (XLogRecGetBlockTag(record, 1, NULL, NULL, &oldblk))
 	{
 		/* HOT updates are never done across pages */
-		Assert(!hot_update);
+		Assert(!hot_update && !phot_update);
 	}
 	else
 		oldblk = newblk;
@@ -9126,6 +9214,8 @@ heap_xlog_update(XLogReaderState *record, bool hot_update)
 		htup->t_infomask2 &= ~HEAP_KEYS_UPDATED;
 		if (hot_update)
 			HeapTupleHeaderSetHotUpdated(htup);
+		else if (phot_update)
+			HeapTupleHeaderSetPartialHotUpdated(htup);
 		else
 			HeapTupleHeaderClearHotUpdated(htup);
 		fix_infomask_from_infobits(xlrec->old_infobits_set, &htup->t_infomask,
@@ -9302,7 +9392,7 @@ heap_xlog_update(XLogReaderState *record, bool hot_update)
 	 * don't bother to update the FSM in that case, it doesn't need to be
 	 * totally accurate anyway.
 	 */
-	if (newaction == BLK_NEEDS_REDO && !hot_update && freespace < BLCKSZ / 5)
+	if (newaction == BLK_NEEDS_REDO && !(hot_update || phot_update) && freespace < BLCKSZ / 5)
 		XLogRecordPageWithFreeSpace(rnode, newblk, freespace);
 }
 
@@ -9533,7 +9623,7 @@ heap_redo(XLogReaderState *record)
 			heap_xlog_delete(record);
 			break;
 		case XLOG_HEAP_UPDATE:
-			heap_xlog_update(record, false);
+			heap_xlog_update(record, false, false);
 			break;
 		case XLOG_HEAP_TRUNCATE:
 
@@ -9544,7 +9634,7 @@ heap_redo(XLogReaderState *record)
 			 */
 			break;
 		case XLOG_HEAP_HOT_UPDATE:
-			heap_xlog_update(record, true);
+			heap_xlog_update(record, true, false);
 			break;
 		case XLOG_HEAP_CONFIRM:
 			heap_xlog_confirm(record);
@@ -9600,6 +9690,21 @@ heap2_redo(XLogReaderState *record)
 	}
 }
 
+void
+heap3_redo(XLogReaderState *record)
+{
+	uint8		info = XLogRecGetInfo(record) & ~XLR_INFO_MASK;
+
+	switch (info & XLOG_HEAP_OPMASK)
+	{
+		case XLOG_HEAP3_PARTIAL_HOT_UPDATE:
+			heap_xlog_update(record, false, true);
+			break;
+		default:
+			elog(PANIC, "heap3_redo: unknown op code %u", info);
+	}
+}
+
 /*
  * Mask a heap page before performing consistency checks on it.
  */
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index 4a70e20a14..562997a779 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -113,7 +113,8 @@ heapam_index_fetch_tuple(struct IndexFetchTableData *scan,
 						 ItemPointer tid,
 						 Snapshot snapshot,
 						 TupleTableSlot *slot,
-						 bool *call_again, bool *all_dead)
+						 bool *call_again, bool *all_dead,
+						 Relation indrel)
 {
 	IndexFetchHeapData *hscan = (IndexFetchHeapData *) scan;
 	BufferHeapTupleTableSlot *bslot = (BufferHeapTupleTableSlot *) slot;
@@ -146,7 +147,8 @@ heapam_index_fetch_tuple(struct IndexFetchTableData *scan,
 											snapshot,
 											&bslot->base.tupdata,
 											all_dead,
-											!*call_again);
+											!*call_again,
+											indrel, NULL);
 	bslot->base.tupdata.t_self = *tid;
 	LockBuffer(hscan->xs_cbuf, BUFFER_LOCK_UNLOCK);
 
@@ -314,7 +316,8 @@ static TM_Result
 heapam_tuple_update(Relation relation, ItemPointer otid, TupleTableSlot *slot,
 					CommandId cid, Snapshot snapshot, Snapshot crosscheck,
 					bool wait, TM_FailureData *tmfd,
-					LockTupleMode *lockmode, bool *update_indexes)
+					LockTupleMode *lockmode, bool *update_all_indexes,
+					bool *update_modified_indexes, Bitmapset **modified_attrs)
 {
 	bool		shouldFree = true;
 	HeapTuple	tuple = ExecFetchSlotHeapTuple(slot, true, &shouldFree);
@@ -325,7 +328,7 @@ heapam_tuple_update(Relation relation, ItemPointer otid, TupleTableSlot *slot,
 	tuple->t_tableOid = slot->tts_tableOid;
 
 	result = heap_update(relation, otid, tuple, cid, crosscheck, wait,
-						 tmfd, lockmode);
+						 tmfd, lockmode, modified_attrs);
 	ItemPointerCopy(&tuple->t_self, &slot->tts_tid);
 
 	/*
@@ -335,8 +338,12 @@ heapam_tuple_update(Relation relation, ItemPointer otid, TupleTableSlot *slot,
 	 * t_self field.
 	 *
 	 * If it's a HOT update, we mustn't insert new index entries.
+	 *
+	 * If it's a PHOT update, we will not be updating all of the indexes, just
+	 * the ones that touch a modified attribute.
 	 */
-	*update_indexes = result == TM_Ok && !HeapTupleIsHeapOnly(tuple);
+	*update_all_indexes = result == TM_Ok && !(HeapTupleIsHeapOnly(tuple) || HeapTupleIsPartialHeapOnly(tuple));
+	*update_modified_indexes = result == TM_Ok && HeapTupleIsPartialHeapOnly(tuple);
 
 	if (shouldFree)
 		pfree(tuple);
@@ -2109,7 +2116,7 @@ heapam_estimate_rel_size(Relation rel, int32 *attr_widths,
 
 static bool
 heapam_scan_bitmap_next_block(TableScanDesc scan,
-							  TBMIterateResult *tbmres)
+							  TBMIterateResult *tbmres, Bitmapset *interesting_attrs)
 {
 	HeapScanDesc hscan = (HeapScanDesc) scan;
 	BlockNumber page = tbmres->blockno;
@@ -2173,7 +2180,8 @@ heapam_scan_bitmap_next_block(TableScanDesc scan,
 
 			ItemPointerSet(&tid, page, offnum);
 			if (heap_hot_search_buffer(&tid, scan->rs_rd, buffer, snapshot,
-									   &heapTuple, NULL, true))
+									   &heapTuple, NULL, true,
+									   NULL, interesting_attrs))
 				hscan->rs_vistuples[ntup++] = ItemPointerGetOffsetNumber(&tid);
 		}
 	}
diff --git a/src/backend/access/heap/pruneheap.c b/src/backend/access/heap/pruneheap.c
index e3a716a2a2..5feaa32c84 100644
--- a/src/backend/access/heap/pruneheap.c
+++ b/src/backend/access/heap/pruneheap.c
@@ -63,7 +63,7 @@ typedef struct
 /* Local functions */
 static int	heap_prune_chain(Buffer buffer,
 							 OffsetNumber rootoffnum,
-							 PruneState *prstate);
+							 PruneState *prstate, bool *phot);
 static void heap_prune_record_prunable(PruneState *prstate, TransactionId xid);
 static void heap_prune_record_redirect(PruneState *prstate,
 									   OffsetNumber offnum, OffsetNumber rdoffnum);
@@ -232,6 +232,7 @@ heap_page_prune(Relation relation, Buffer buffer,
 	OffsetNumber offnum,
 				maxoff;
 	PruneState	prstate;
+	bool phot = false;
 
 	/*
 	 * Our strategy is to scan the page and make lists of items to change,
@@ -279,13 +280,16 @@ heap_page_prune(Relation relation, Buffer buffer,
 			continue;
 
 		/* Process this item or chain of items */
-		ndeleted += heap_prune_chain(buffer, offnum, &prstate);
+		ndeleted += heap_prune_chain(buffer, offnum, &prstate, &phot);
 	}
 
 	/* Clear the offset information once we have processed the given page. */
 	if (off_loc)
 		*off_loc = InvalidOffsetNumber;
 
+	if (phot)
+		return ndeleted;
+
 	/* Any error while applying the changes is critical */
 	START_CRIT_SECTION();
 
@@ -489,7 +493,7 @@ heap_prune_satisfies_vacuum(PruneState *prstate, HeapTuple tup, Buffer buffer)
  * Returns the number of tuples (to be) deleted from the page.
  */
 static int
-heap_prune_chain(Buffer buffer, OffsetNumber rootoffnum, PruneState *prstate)
+heap_prune_chain(Buffer buffer, OffsetNumber rootoffnum, PruneState *prstate, bool *phot)
 {
 	int			ndeleted = 0;
 	Page		dp = (Page) BufferGetPage(buffer);
@@ -540,7 +544,8 @@ heap_prune_chain(Buffer buffer, OffsetNumber rootoffnum, PruneState *prstate)
 			 * gets there first will mark the tuple unused.
 			 */
 			if (heap_prune_satisfies_vacuum(prstate, &tup, buffer)
-				== HEAPTUPLE_DEAD && !HeapTupleHeaderIsHotUpdated(htup))
+				== HEAPTUPLE_DEAD && !HeapTupleHeaderIsHotUpdated(htup) &&
+				!HeapTupleHeaderIsPartialHotUpdated(htup))
 			{
 				heap_prune_record_unused(prstate, rootoffnum);
 				HeapTupleHeaderAdvanceLatestRemovedXid(htup,
@@ -606,6 +611,12 @@ heap_prune_chain(Buffer buffer, OffsetNumber rootoffnum, PruneState *prstate)
 		tup.t_len = ItemIdGetLength(lp);
 		ItemPointerSet(&(tup.t_self), BufferGetBlockNumber(buffer), offnum);
 
+		if (HeapTupleHeaderIsPartialHeapOnly(htup) || HeapTupleHeaderIsPartialHotUpdated(htup))
+		{
+			*phot = true;
+			break;
+		}
+
 		/*
 		 * Check the tuple XMIN against prior XMAX, if any
 		 */
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index f3d2265fad..10a516bec2 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -1349,6 +1349,8 @@ lazy_scan_heap(Relation onerel, VacuumParams *params, LVRelStats *vacrelstats,
 					 */
 					if (HeapTupleIsHotUpdated(&tuple) ||
 						HeapTupleIsHeapOnly(&tuple) ||
+						HeapTupleIsPartialHotUpdated(&tuple) ||
+						HeapTupleIsPartialHeapOnly(&tuple) ||
 						params->index_cleanup == VACOPT_TERNARY_DISABLED)
 						nkeep += 1;
 					else
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 3d2dbed708..d08f688bca 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -579,7 +579,8 @@ index_fetch_heap(IndexScanDesc scan, TupleTableSlot *slot)
 
 	found = table_index_fetch_tuple(scan->xs_heapfetch, &scan->xs_heaptid,
 									scan->xs_snapshot, slot,
-									&scan->xs_heap_continue, &all_dead);
+									&scan->xs_heap_continue, &all_dead,
+									scan->indexRelation);
 
 	if (found)
 		pgstat_count_heap_fetch(scan->indexRelation);
diff --git a/src/backend/access/nbtree/nbtinsert.c b/src/backend/access/nbtree/nbtinsert.c
index e333603912..78b71ab9bc 100644
--- a/src/backend/access/nbtree/nbtinsert.c
+++ b/src/backend/access/nbtree/nbtinsert.c
@@ -556,7 +556,8 @@ _bt_check_unique(Relation rel, BTInsertState insertstate, Relation heapRel,
 				 */
 				else if (table_index_fetch_tuple_check(heapRel, &htid,
 													   &SnapshotDirty,
-													   &all_dead))
+													   &all_dead,
+													   rel))
 				{
 					TransactionId xwait;
 
@@ -613,7 +614,7 @@ _bt_check_unique(Relation rel, BTInsertState insertstate, Relation heapRel,
 					 */
 					htid = itup->t_tid;
 					if (table_index_fetch_tuple_check(heapRel, &htid,
-													  SnapshotSelf, NULL))
+													  SnapshotSelf, NULL, rel))
 					{
 						/* Normal case --- it's still live */
 					}
diff --git a/src/backend/access/rmgrdesc/heapdesc.c b/src/backend/access/rmgrdesc/heapdesc.c
index e60e32b935..76a8b865fe 100644
--- a/src/backend/access/rmgrdesc/heapdesc.c
+++ b/src/backend/access/rmgrdesc/heapdesc.c
@@ -176,6 +176,26 @@ heap2_desc(StringInfo buf, XLogReaderState *record)
 						 xlrec->cmin, xlrec->cmax, xlrec->combocid);
 	}
 }
+void
+heap3_desc(StringInfo buf, XLogReaderState *record)
+{
+	char       *rec = XLogRecGetData(record);
+	uint8       info = XLogRecGetInfo(record) & ~XLR_INFO_MASK;
+
+	if (info == XLOG_HEAP3_PARTIAL_HOT_UPDATE)
+	{
+		xl_heap_update *xlrec = (xl_heap_update *) rec;
+
+		appendStringInfo(buf, "off %u xmax %u flags 0x%02X ",
+						 xlrec->old_offnum,
+						 xlrec->old_xmax,
+						 xlrec->flags);
+		out_infobits(buf, xlrec->old_infobits_set);
+		appendStringInfo(buf, "; new off %u xmax %u",
+						 xlrec->new_offnum,
+						 xlrec->new_xmax);
+	}
+}
 
 const char *
 heap_identify(uint8 info)
@@ -260,3 +280,21 @@ heap2_identify(uint8 info)
 
 	return id;
 }
+
+const char *
+heap3_identify(uint8 info)
+{
+	const char *id = NULL;
+
+	switch (info & ~XLR_INFO_MASK)
+	{
+		case XLOG_HEAP3_PARTIAL_HOT_UPDATE:
+			id = "PARTIAL_HOT_UPDATE";
+			break;
+		case XLOG_HEAP3_PARTIAL_HOT_UPDATE | XLOG_HEAP_INIT_PAGE:
+			id = "PARTIAL_HOT_UPDATE+INIT";
+			break;
+	}
+
+	return id;
+}
diff --git a/src/backend/access/table/tableam.c b/src/backend/access/table/tableam.c
index 5ea5bdd810..c88ee5e336 100644
--- a/src/backend/access/table/tableam.c
+++ b/src/backend/access/table/tableam.c
@@ -219,7 +219,8 @@ bool
 table_index_fetch_tuple_check(Relation rel,
 							  ItemPointer tid,
 							  Snapshot snapshot,
-							  bool *all_dead)
+							  bool *all_dead,
+							  Relation indrel)
 {
 	IndexFetchTableData *scan;
 	TupleTableSlot *slot;
@@ -229,7 +230,7 @@ table_index_fetch_tuple_check(Relation rel,
 	slot = table_slot_create(rel, NULL);
 	scan = table_index_fetch_begin(rel);
 	found = table_index_fetch_tuple(scan, tid, snapshot, slot, &call_again,
-									all_dead);
+									all_dead, indrel);
 	table_index_fetch_end(scan);
 	ExecDropSingleTupleTableSlot(slot);
 
@@ -346,7 +347,9 @@ void
 simple_table_tuple_update(Relation rel, ItemPointer otid,
 						  TupleTableSlot *slot,
 						  Snapshot snapshot,
-						  bool *update_indexes)
+						  bool *update_all_indexes,
+						  bool *update_modified_indexes,
+						  Bitmapset **modified_attrs)
 {
 	TM_Result	result;
 	TM_FailureData tmfd;
@@ -356,7 +359,8 @@ simple_table_tuple_update(Relation rel, ItemPointer otid,
 								GetCurrentCommandId(true),
 								snapshot, InvalidSnapshot,
 								true /* wait for commit */ ,
-								&tmfd, &lockmode, update_indexes);
+								&tmfd, &lockmode, update_all_indexes,
+								update_modified_indexes, modified_attrs);
 
 	switch (result)
 	{
diff --git a/src/backend/commands/constraint.c b/src/backend/commands/constraint.c
index d0063164a7..cd18bea224 100644
--- a/src/backend/commands/constraint.c
+++ b/src/backend/commands/constraint.c
@@ -106,18 +106,21 @@ unique_key_recheck(PG_FUNCTION_ARGS)
 	 * it's possible the index entry has also been marked dead, and even
 	 * removed.
 	 */
+	indexRel = index_open(trigdata->tg_trigger->tgconstrindid,
+						  RowExclusiveLock);
 	tmptid = checktid;
 	{
 		IndexFetchTableData *scan = table_index_fetch_begin(trigdata->tg_relation);
 		bool		call_again = false;
 
 		if (!table_index_fetch_tuple(scan, &tmptid, SnapshotSelf, slot,
-									 &call_again, NULL))
+									 &call_again, NULL, indexRel))
 		{
 			/*
 			 * All rows referenced by the index entry are dead, so skip the
 			 * check.
 			 */
+			index_close(indexRel, RowExclusiveLock);
 			ExecDropSingleTupleTableSlot(slot);
 			table_index_fetch_end(scan);
 			return PointerGetDatum(NULL);
@@ -130,8 +133,6 @@ unique_key_recheck(PG_FUNCTION_ARGS)
 	 * to update it.  (This protects against possible changes of the index
 	 * schema, not against concurrent updates.)
 	 */
-	indexRel = index_open(trigdata->tg_trigger->tgconstrindid,
-						  RowExclusiveLock);
 	indexInfo = BuildIndexInfo(indexRel);
 
 	/*
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 796ca7b3f7..f4f97ae0b6 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -343,7 +343,7 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 			recheckIndexes =
 				ExecInsertIndexTuples(resultRelInfo,
 									  buffer->slots[i], estate, false, false,
-									  NULL, NIL);
+									  NULL, NIL, false, NULL);
 			ExecARInsertTriggers(estate, resultRelInfo,
 								 slots[i], recheckIndexes,
 								 cstate->transition_capture);
@@ -1090,7 +1090,7 @@ CopyFrom(CopyFromState cstate)
 																   false,
 																   false,
 																   NULL,
-																   NIL);
+																   NIL, false, NULL);
 					}
 
 					/* AFTER ROW INSERT Triggers */
diff --git a/src/backend/executor/execIndexing.c b/src/backend/executor/execIndexing.c
index afe7ce87d4..46faaf2455 100644
--- a/src/backend/executor/execIndexing.c
+++ b/src/backend/executor/execIndexing.c
@@ -287,7 +287,9 @@ ExecInsertIndexTuples(ResultRelInfo *resultRelInfo,
 					  bool update,
 					  bool noDupErr,
 					  bool *specConflict,
-					  List *arbiterIndexes)
+					  List *arbiterIndexes,
+					  bool update_modified_indexes,
+					  Bitmapset *modified_attrs)
 {
 	ItemPointer tupleid = &slot->tts_tid;
 	List	   *result = NIL;
@@ -343,6 +345,29 @@ ExecInsertIndexTuples(ResultRelInfo *resultRelInfo,
 		if (!indexInfo->ii_ReadyForInserts)
 			continue;
 
+		/*
+		 * If the indexed attributes were not modified and this is a partial-HOT
+		 * update, skip it.
+		 */
+		if (update_modified_indexes)
+		{
+			bool should_update = false;
+			int j;
+
+			for (j = 0; j < indexInfo->ii_NumIndexAttrs; j++)
+			{
+				if (bms_is_member(indexInfo->ii_IndexAttrNumbers[j] - FirstLowInvalidHeapAttributeNumber,
+							modified_attrs))
+				{
+					should_update = true;
+					break;
+				}
+			}
+
+			if (!should_update)
+				continue;
+		}
+
 		/* Check for partial index */
 		if (indexInfo->ii_Predicate != NIL)
 		{
diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index 1e285e0349..fdbbb79d42 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -445,7 +445,7 @@ ExecSimpleRelationInsert(ResultRelInfo *resultRelInfo,
 		if (resultRelInfo->ri_NumIndices > 0)
 			recheckIndexes = ExecInsertIndexTuples(resultRelInfo,
 												   slot, estate, false, false,
-												   NULL, NIL);
+												   NULL, NIL, false, NULL);
 
 		/* AFTER ROW INSERT Triggers */
 		ExecARInsertTriggers(estate, resultRelInfo, slot,
@@ -493,7 +493,9 @@ ExecSimpleRelationUpdate(ResultRelInfo *resultRelInfo,
 	if (!skip_tuple)
 	{
 		List	   *recheckIndexes = NIL;
-		bool		update_indexes;
+		bool		update_all_indexes;
+		bool		update_modified_indexes;
+		Bitmapset  *modified_attrs;
 
 		/* Compute stored generated columns */
 		if (rel->rd_att->constr &&
@@ -508,12 +510,13 @@ ExecSimpleRelationUpdate(ResultRelInfo *resultRelInfo,
 			ExecPartitionCheck(resultRelInfo, slot, estate, true);
 
 		simple_table_tuple_update(rel, tid, slot, estate->es_snapshot,
-								  &update_indexes);
+								  &update_all_indexes, &update_modified_indexes,
+								  &modified_attrs);
 
-		if (resultRelInfo->ri_NumIndices > 0 && update_indexes)
+		if (resultRelInfo->ri_NumIndices > 0 && (update_all_indexes || update_modified_indexes))
 			recheckIndexes = ExecInsertIndexTuples(resultRelInfo,
 												   slot, estate, true, false,
-												   NULL, NIL);
+												   NULL, NIL, update_modified_indexes, modified_attrs);
 
 		/* AFTER ROW UPDATE Triggers */
 		ExecARUpdateTriggers(estate, resultRelInfo,
@@ -521,6 +524,7 @@ ExecSimpleRelationUpdate(ResultRelInfo *resultRelInfo,
 							 recheckIndexes, NULL);
 
 		list_free(recheckIndexes);
+		bms_free(modified_attrs);
 	}
 }
 
diff --git a/src/backend/executor/nodeBitmapHeapscan.c b/src/backend/executor/nodeBitmapHeapscan.c
index 2db1914aff..c74521e1e5 100644
--- a/src/backend/executor/nodeBitmapHeapscan.c
+++ b/src/backend/executor/nodeBitmapHeapscan.c
@@ -37,6 +37,7 @@
 
 #include <math.h>
 
+#include "access/genam.h"
 #include "access/relscan.h"
 #include "access/tableam.h"
 #include "access/transam.h"
@@ -79,8 +80,31 @@ BitmapHeapNext(BitmapHeapScanState *node)
 	TBMIterateResult *tbmres;
 	TupleTableSlot *slot;
 	ParallelBitmapHeapState *pstate = node->pstate;
+	Bitmapset *interesting_attrs = NULL;
 	dsa_area   *dsa = node->ss.ps.state->es_query_dsa;
 
+	/* hack for PHOT */
+	if (outerPlanState(&node->ss.ps))
+	{
+		PlanState *ps = outerPlanState(&node->ss.ps);
+		Plan *plan = ps->plan;
+
+		if (nodeTag(plan) == T_BitmapIndexScan)
+		{
+			BitmapIndexScan *scan = (BitmapIndexScan *) plan;
+			Oid indexid = scan->indexid;
+			Relation indrel = index_open(indexid, AccessShareLock);
+
+			for (int i = 0; i < indrel->rd_index->indnatts; i++)
+			{
+				int ind_attr = indrel->rd_index->indkey.values[i] - FirstLowInvalidHeapAttributeNumber;
+				interesting_attrs = bms_add_member(interesting_attrs, ind_attr);
+			}
+
+			index_close(indrel, AccessShareLock);
+		}
+	}
+
 	/*
 	 * extract necessary information from index scan node
 	 */
@@ -232,7 +256,7 @@ BitmapHeapNext(BitmapHeapScanState *node)
 				 */
 				node->return_empty_tuples = tbmres->ntuples;
 			}
-			else if (!table_scan_bitmap_next_block(scan, tbmres))
+			else if (!table_scan_bitmap_next_block(scan, tbmres, interesting_attrs))
 			{
 				/* AM doesn't think this block is valid, skip */
 				continue;
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 2993ba43e3..1a36f29153 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -658,7 +658,9 @@ ExecInsert(ModifyTableState *mtstate,
 			recheckIndexes = ExecInsertIndexTuples(resultRelInfo,
 												   slot, estate, false, true,
 												   &specConflict,
-												   arbiterIndexes);
+												   arbiterIndexes,
+												   false,
+												   NULL);
 
 			/* adjust the tuple's state accordingly */
 			table_tuple_complete_speculative(resultRelationDesc, slot,
@@ -697,7 +699,7 @@ ExecInsert(ModifyTableState *mtstate,
 			if (resultRelInfo->ri_NumIndices > 0)
 				recheckIndexes = ExecInsertIndexTuples(resultRelInfo,
 													   slot, estate, false,
-													   false, NULL, NIL);
+													   false, NULL, NIL, false, NULL);
 		}
 	}
 
@@ -1395,7 +1397,9 @@ ExecUpdate(ModifyTableState *mtstate,
 	{
 		LockTupleMode lockmode;
 		bool		partition_constraint_failed;
-		bool		update_indexes;
+		bool		update_all_indexes;
+		bool		update_modified_indexes;
+		Bitmapset *modified_attrs;
 
 		/*
 		 * Constraints might reference the tableoid column, so (re-)initialize
@@ -1500,7 +1504,8 @@ lreplace:;
 									estate->es_snapshot,
 									estate->es_crosscheck_snapshot,
 									true /* wait for commit */ ,
-									&tmfd, &lockmode, &update_indexes);
+									&tmfd, &lockmode, &update_all_indexes,
+									&update_modified_indexes, &modified_attrs);
 
 		switch (result)
 		{
@@ -1630,10 +1635,11 @@ lreplace:;
 		}
 
 		/* insert index entries for tuple if necessary */
-		if (resultRelInfo->ri_NumIndices > 0 && update_indexes)
+		if (resultRelInfo->ri_NumIndices > 0 && (update_all_indexes || update_modified_indexes))
 			recheckIndexes = ExecInsertIndexTuples(resultRelInfo,
 												   slot, estate, true, false,
-												   NULL, NIL);
+												   NULL, NIL, update_modified_indexes, modified_attrs);
+		bms_free(modified_attrs);
 	}
 
 	if (canSetTag)
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.c
index afa1df00d0..c0c1a74637 100644
--- a/src/backend/replication/logical/decode.c
+++ b/src/backend/replication/logical/decode.c
@@ -54,6 +54,7 @@ typedef struct XLogRecordBuffer
 static void DecodeXLogOp(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
 static void DecodeHeapOp(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
 static void DecodeHeap2Op(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
+static void DecodeHeap3Op(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
 static void DecodeXactOp(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
 static void DecodeStandbyOp(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
 static void DecodeLogicalMsgOp(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
@@ -145,6 +146,10 @@ LogicalDecodingProcessRecord(LogicalDecodingContext *ctx, XLogReaderState *recor
 			DecodeStandbyOp(ctx, &buf);
 			break;
 
+		case RM_HEAP3_ID:
+			DecodeHeap3Op(ctx, &buf);
+			break;
+
 		case RM_HEAP2_ID:
 			DecodeHeap2Op(ctx, &buf);
 			break;
@@ -416,6 +421,37 @@ DecodeStandbyOp(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
 	}
 }
 
+/*
+ * Handle rmgr HEAP3_ID records for DecodeRecordIntoRorderBuffer().
+ */
+static void
+DecodeHeap3Op(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
+{
+	uint8		 info = XLogRecGetInfo(buf->record) & XLOG_HEAP_OPMASK;
+	TransactionId xid = XLogRecGetXid(buf->record);
+	SnapBuild	*builder = ctx->snapshot_builder;
+
+	ReorderBufferProcessXid(ctx->reorder, xid, buf->origptr);
+
+	/*
+	 * If we don't have snapshot or we are just fast-forwarding, there is no
+	 * point in decoding data changes.
+	 */
+	if (SnapBuildCurrentState(builder) < SNAPBUILD_FULL_SNAPSHOT ||
+		ctx->fast_forward)
+		return;
+
+	switch (info)
+	{
+		case XLOG_HEAP3_PARTIAL_HOT_UPDATE:
+			if (SnapBuildProcessChange(builder, xid, buf->origptr))
+				DecodeUpdate(ctx, buf);
+			break;
+		default:
+			elog(ERROR, "unexpected RM_HEAP3_ID record type: %u", info);
+	}
+}
+
 /*
  * Handle rmgr HEAP2_ID records for DecodeRecordIntoReorderBuffer().
  */
diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h
index d96a47b1ce..ff54eadcd1 100644
--- a/src/include/access/heapam.h
+++ b/src/include/access/heapam.h
@@ -126,7 +126,7 @@ extern bool heap_fetch(Relation relation, Snapshot snapshot,
 					   HeapTuple tuple, Buffer *userbuf);
 extern bool heap_hot_search_buffer(ItemPointer tid, Relation relation,
 								   Buffer buffer, Snapshot snapshot, HeapTuple heapTuple,
-								   bool *all_dead, bool first_call);
+								   bool *all_dead, bool first_call, Relation indrel, Bitmapset *interesting_attrs);
 
 extern void heap_get_latest_tid(TableScanDesc scan, ItemPointer tid);
 
@@ -147,7 +147,8 @@ extern void heap_abort_speculative(Relation relation, ItemPointer tid);
 extern TM_Result heap_update(Relation relation, ItemPointer otid,
 							 HeapTuple newtup,
 							 CommandId cid, Snapshot crosscheck, bool wait,
-							 struct TM_FailureData *tmfd, LockTupleMode *lockmode);
+							 struct TM_FailureData *tmfd, LockTupleMode *lockmode,
+							 Bitmapset **modified_attrs);
 extern TM_Result heap_lock_tuple(Relation relation, HeapTuple tuple,
 								 CommandId cid, LockTupleMode mode, LockWaitPolicy wait_policy,
 								 bool follow_update,
diff --git a/src/include/access/heapam_xlog.h b/src/include/access/heapam_xlog.h
index 178d49710a..8b00bb148b 100644
--- a/src/include/access/heapam_xlog.h
+++ b/src/include/access/heapam_xlog.h
@@ -59,6 +59,16 @@
 #define XLOG_HEAP2_LOCK_UPDATED 0x60
 #define XLOG_HEAP2_NEW_CID		0x70
 
+/*
+ * We ran out of opcodes again, so heapam.c now has a third RmgrId.  These
+ * opcodes are associated with RM_HEAP3_ID, but are not logically different from
+ * the ones above associated with RM_HEAP_ID.  XLOG_HEAP_OPMASK applies to
+ * these, too.
+ *
+ * 0x10 through 0x70 are available for use.
+ */
+#define XLOG_HEAP3_PARTIAL_HOT_UPDATE	0x00
+
 /*
  * xl_heap_insert/xl_heap_multi_insert flag values, 8 bits are available.
  */
@@ -396,6 +406,9 @@ extern void heap2_redo(XLogReaderState *record);
 extern void heap2_desc(StringInfo buf, XLogReaderState *record);
 extern const char *heap2_identify(uint8 info);
 extern void heap_xlog_logical_rewrite(XLogReaderState *r);
+extern void heap3_redo(XLogReaderState *record);
+extern void heap3_desc(StringInfo buf, XLogReaderState *record);
+extern const char *heap3_identify(uint8 info);
 
 extern XLogRecPtr log_heap_cleanup_info(RelFileNode rnode,
 										TransactionId latestRemovedXid);
diff --git a/src/include/access/htup_details.h b/src/include/access/htup_details.h
index 7c62852e7f..57dcfa6dd8 100644
--- a/src/include/access/htup_details.h
+++ b/src/include/access/htup_details.h
@@ -274,7 +274,8 @@ struct HeapTupleHeaderData
  * information stored in t_infomask2:
  */
 #define HEAP_NATTS_MASK			0x07FF	/* 11 bits for number of attributes */
-/* bits 0x1800 are available */
+#define HEAP_PHOT_UPDATED		0x0800	/* tuple was PHOT-updated */
+#define HEAP_PHOT_TUPLE			0x1000	/* this is a partial heap-only tuple */
 #define HEAP_KEYS_UPDATED		0x2000	/* tuple was updated and key cols
 										 * modified, or tuple deleted */
 #define HEAP_HOT_UPDATED		0x4000	/* tuple was HOT-updated */
@@ -490,6 +491,7 @@ do { \
 
 #define HeapTupleHeaderSetHotUpdated(tup) \
 ( \
+	AssertMacro(((tup)->t_infomask2 & HEAP_PHOT_UPDATED) == 0), \
 	(tup)->t_infomask2 |= HEAP_HOT_UPDATED \
 )
 
@@ -505,6 +507,7 @@ do { \
 
 #define HeapTupleHeaderSetHeapOnly(tup) \
 ( \
+  AssertMacro(((tup)->t_infomask2 & HEAP_PHOT_TUPLE) == 0), \
   (tup)->t_infomask2 |= HEAP_ONLY_TUPLE \
 )
 
@@ -513,6 +516,40 @@ do { \
   (tup)->t_infomask2 &= ~HEAP_ONLY_TUPLE \
 )
 
+#define HeapTupleHeaderIsPartialHotUpdated(tup) \
+( \
+	((tup)->t_infomask2 & HEAP_PHOT_UPDATED) != 0 && \
+	((tup)->t_infomask & HEAP_XMAX_INVALID) == 0 && \
+	!HeapTupleHeaderXminInvalid(tup) \
+)
+
+#define HeapTupleHeaderSetPartialHotUpdated(tup) \
+( \
+	AssertMacro(((tup)->t_infomask2 & HEAP_HOT_UPDATED) == 0), \
+	(tup)->t_infomask2 |= HEAP_PHOT_UPDATED \
+)
+
+#define HeapTupleHeaderClearPartialHotUpdated(tup) \
+( \
+	(tup)->t_infomask2 &= ~HEAP_PHOT_UPDATED \
+)
+
+#define HeapTupleHeaderIsPartialHeapOnly(tup) \
+( \
+	((tup)->t_infomask2 & HEAP_PHOT_TUPLE) != 0 \
+)
+
+#define HeapTupleHeaderSetPartialHeapOnly(tup) \
+( \
+	AssertMacro(((tup)->t_infomask2 & HEAP_ONLY_TUPLE) == 0), \
+	(tup)->t_infomask2 |= HEAP_PHOT_TUPLE \
+)
+
+#define HeapTupleHeaderClearPartialHeapOnly(tup) \
+( \
+	(tup)->t_infomask2 &= ~HEAP_PHOT_TUPLE \
+)
+
 #define HeapTupleHeaderHasMatch(tup) \
 ( \
   ((tup)->t_infomask2 & HEAP_TUPLE_HAS_MATCH) != 0 \
@@ -691,6 +728,24 @@ struct MinimalTupleData
 #define HeapTupleClearHeapOnly(tuple) \
 		HeapTupleHeaderClearHeapOnly((tuple)->t_data)
 
+#define HeapTupleIsPartialHotUpdated(tuple) \
+		HeapTupleHeaderIsPartialHotUpdated((tuple)->t_data)
+
+#define HeapTupleSetPartialHotUpdated(tuple) \
+		HeapTupleHeaderSetPartialHotUpdated((tuple)->t_data)
+
+#define HeapTupleClearPartialHotUpdated(tuple) \
+		HeapTupleHeaderClearPartialHotUpdated((tuple)->t_data)
+
+#define HeapTupleIsPartialHeapOnly(tuple) \
+		HeapTupleHeaderIsPartialHeapOnly((tuple)->t_data)
+
+#define HeapTupleSetPartialHeapOnly(tuple) \
+		HeapTupleHeaderSetPartialHeapOnly((tuple)->t_data)
+
+#define HeapTupleClearPartialHeapOnly(tuple) \
+		HeapTupleHeaderClearPartialHeapOnly((tuple)->t_data)
+
 
 /* ----------------
  *		fastgetattr
diff --git a/src/include/access/rmgrlist.h b/src/include/access/rmgrlist.h
index f582cf535f..367b8cebf0 100644
--- a/src/include/access/rmgrlist.h
+++ b/src/include/access/rmgrlist.h
@@ -47,3 +47,4 @@ PG_RMGR(RM_COMMIT_TS_ID, "CommitTs", commit_ts_redo, commit_ts_desc, commit_ts_i
 PG_RMGR(RM_REPLORIGIN_ID, "ReplicationOrigin", replorigin_redo, replorigin_desc, replorigin_identify, NULL, NULL, NULL)
 PG_RMGR(RM_GENERIC_ID, "Generic", generic_redo, generic_desc, generic_identify, NULL, NULL, generic_mask)
 PG_RMGR(RM_LOGICALMSG_ID, "LogicalMessage", logicalmsg_redo, logicalmsg_desc, logicalmsg_identify, NULL, NULL, NULL)
+PG_RMGR(RM_HEAP3_ID, "Heap3", heap3_redo, heap3_desc, heap3_identify, NULL, NULL, heap_mask)
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index 33bffb6815..817507111c 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -403,7 +403,8 @@ typedef struct TableAmRoutine
 									  ItemPointer tid,
 									  Snapshot snapshot,
 									  TupleTableSlot *slot,
-									  bool *call_again, bool *all_dead);
+									  bool *call_again, bool *all_dead,
+									  Relation indrel);
 
 
 	/* ------------------------------------------------------------------------
@@ -495,7 +496,9 @@ typedef struct TableAmRoutine
 								 bool wait,
 								 TM_FailureData *tmfd,
 								 LockTupleMode *lockmode,
-								 bool *update_indexes);
+								 bool *update_all_indexes,
+								 bool *update_modified_indexes,
+								 Bitmapset **modified_attrs);
 
 	/* see table_tuple_lock() for reference about parameters */
 	TM_Result	(*tuple_lock) (Relation rel,
@@ -757,7 +760,7 @@ typedef struct TableAmRoutine
 	 * scan_bitmap_next_tuple need to exist, or neither.
 	 */
 	bool		(*scan_bitmap_next_block) (TableScanDesc scan,
-										   struct TBMIterateResult *tbmres);
+										   struct TBMIterateResult *tbmres, Bitmapset *interesting_attrs);
 
 	/*
 	 * Fetch the next tuple of a bitmap table scan into `slot` and return true
@@ -1124,7 +1127,8 @@ table_index_fetch_tuple(struct IndexFetchTableData *scan,
 						ItemPointer tid,
 						Snapshot snapshot,
 						TupleTableSlot *slot,
-						bool *call_again, bool *all_dead)
+						bool *call_again, bool *all_dead,
+						Relation indrel)
 {
 	/*
 	 * We don't expect direct calls to table_index_fetch_tuple with valid
@@ -1136,7 +1140,7 @@ table_index_fetch_tuple(struct IndexFetchTableData *scan,
 
 	return scan->rel->rd_tableam->index_fetch_tuple(scan, tid, snapshot,
 													slot, call_again,
-													all_dead);
+													all_dead, indrel);
 }
 
 /*
@@ -1148,7 +1152,8 @@ table_index_fetch_tuple(struct IndexFetchTableData *scan,
 extern bool table_index_fetch_tuple_check(Relation rel,
 										  ItemPointer tid,
 										  Snapshot snapshot,
-										  bool *all_dead);
+										  bool *all_dead,
+										  Relation indrel);
 
 
 /* ------------------------------------------------------------------------
@@ -1417,12 +1422,13 @@ static inline TM_Result
 table_tuple_update(Relation rel, ItemPointer otid, TupleTableSlot *slot,
 				   CommandId cid, Snapshot snapshot, Snapshot crosscheck,
 				   bool wait, TM_FailureData *tmfd, LockTupleMode *lockmode,
-				   bool *update_indexes)
+				   bool *update_all_indexes, bool *update_modified_indexes, Bitmapset **modified_attrs)
 {
 	return rel->rd_tableam->tuple_update(rel, otid, slot,
 										 cid, snapshot, crosscheck,
 										 wait, tmfd,
-										 lockmode, update_indexes);
+										 lockmode, update_all_indexes,
+										 update_modified_indexes, modified_attrs);
 }
 
 /*
@@ -1842,7 +1848,7 @@ table_relation_estimate_size(Relation rel, int32 *attr_widths,
  */
 static inline bool
 table_scan_bitmap_next_block(TableScanDesc scan,
-							 struct TBMIterateResult *tbmres)
+							 struct TBMIterateResult *tbmres, Bitmapset *interesting_attrs)
 {
 	/*
 	 * We don't expect direct calls to table_scan_bitmap_next_block with valid
@@ -1853,7 +1859,7 @@ table_scan_bitmap_next_block(TableScanDesc scan,
 		elog(ERROR, "unexpected table_scan_bitmap_next_block call during logical decoding");
 
 	return scan->rs_rd->rd_tableam->scan_bitmap_next_block(scan,
-														   tbmres);
+														   tbmres, interesting_attrs);
 }
 
 /*
@@ -1940,7 +1946,8 @@ extern void simple_table_tuple_delete(Relation rel, ItemPointer tid,
 									  Snapshot snapshot);
 extern void simple_table_tuple_update(Relation rel, ItemPointer otid,
 									  TupleTableSlot *slot, Snapshot snapshot,
-									  bool *update_indexes);
+									  bool *update_all_indexes, bool *update_modified_indexes,
+									  Bitmapset **modified_attrs);
 
 
 /* ----------------------------------------------------------------------------
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 071e363d54..9390f2dfd5 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -588,7 +588,8 @@ extern List *ExecInsertIndexTuples(ResultRelInfo *resultRelInfo,
 								   TupleTableSlot *slot, EState *estate,
 								   bool update,
 								   bool noDupErr,
-								   bool *specConflict, List *arbiterIndexes);
+								   bool *specConflict, List *arbiterIndexes,
+								   bool update_modified_indexes, Bitmapset *modified_attrs);
 extern bool ExecCheckIndexConstraints(ResultRelInfo *resultRelInfo,
 									  TupleTableSlot *slot,
 									  EState *estate, ItemPointer conflictTid,
-- 
2.16.6

