From c1d6a412a943189372cc733b1041bff96e8c8e76 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Thu, 19 Mar 2026 12:32:34 -0400
Subject: [PATCH v16 04/17] Add VISITED_PAGES_LIMIT to heapam

---
 src/include/access/relscan.h             |  7 +++++++
 src/backend/access/heap/heapam_handler.c | 22 ++++++++++++++++++++++
 src/backend/access/index/genam.c         |  1 +
 src/backend/utils/adt/selfuncs.c         |  9 +++++----
 4 files changed, 35 insertions(+), 4 deletions(-)

diff --git a/src/include/access/relscan.h b/src/include/access/relscan.h
index 9c1563c88..13370b1c9 100644
--- a/src/include/access/relscan.h
+++ b/src/include/access/relscan.h
@@ -372,6 +372,13 @@ typedef struct IndexScanDescData
 
 	/* parallel index scan information, in shared memory */
 	struct ParallelIndexScanDescData *parallel_scan;
+
+	/*
+	 * Limit on distinct heap pages visited before giving up (0 = no limit).
+	 * Used by selfuncs.c to bound the cost of get_actual_variable_endpoint().
+	 */
+	uint8		xs_visited_pages_limit;
+
 } IndexScanDescData;
 
 /* Generic structure for parallel scans */
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index b8cc3d39c..1b58db199 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -824,6 +824,7 @@ heapam_index_only_getnext_slot(IndexScanDesc scan, ScanDirection direction,
 				heap_continue = false;
 
 	Assert(scan->xs_want_itup);
+	Assert(scan->xs_visited_pages_limit == 0);
 
 	for (;;)
 	{
@@ -906,6 +907,9 @@ heapam_index_only_batch_getnext_slot(IndexScanDesc scan, ScanDirection direction
 	ItemPointer tid;
 	bool		all_visible,
 				heap_continue = false;
+	BlockNumber last_visited_block = InvalidBlockNumber;
+	uint8		n_visited_pages = 0,
+				xs_visited_pages_limit = scan->xs_visited_pages_limit;
 
 	Assert(scan->xs_want_itup);
 
@@ -936,7 +940,25 @@ heapam_index_only_batch_getnext_slot(IndexScanDesc scan, ScanDirection direction
 				scan->instrument->nheapfetches++;
 
 			if (!heapam_index_fetch_heap(scan, slot, &heap_continue, true))
+			{
+				/*
+				 * No visible tuple.  If caller set a visited-pages limit
+				 * (only selfuncs.c does this), count distinct heap pages and
+				 * give up once we've visited too many.
+				 */
+				if (unlikely(xs_visited_pages_limit > 0))
+				{
+					Assert(hscan->xs_blk == ItemPointerGetBlockNumber(tid));
+
+					if (hscan->xs_blk != last_visited_block)
+					{
+						last_visited_block = hscan->xs_blk;
+						if (++n_visited_pages > xs_visited_pages_limit)
+							return false;	/* give up */
+					}
+				}
 				continue;		/* no visible tuple, try next index entry */
+			}
 
 			ExecClearTuple(slot);
 
diff --git a/src/backend/access/index/genam.c b/src/backend/access/index/genam.c
index 6e87169c2..d0b68f38e 100644
--- a/src/backend/access/index/genam.c
+++ b/src/backend/access/index/genam.c
@@ -126,6 +126,7 @@ RelationGetIndexScan(Relation indexRelation, int nkeys, int norderbys)
 	scan->xs_itupdesc = NULL;
 	scan->xs_hitup = NULL;
 	scan->xs_hitupdesc = NULL;
+	scan->xs_visited_pages_limit = 0;
 
 	scan->batch_index_opaque_size = 0;
 	scan->batch_tuples_workspace = 0;
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 1f8b0e684..768581cf7 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -7168,11 +7168,11 @@ get_actual_variable_endpoint(Relation heapRel,
 	 * pages.  When we fail for that reason, the caller will end up using
 	 * whatever extremal value is recorded in pg_statistic.
 	 *
-	 * XXX This can't work with the new table_index_getnext_slot interface,
-	 * which simply won't return a tuple that isn't visible to our snapshot.
-	 * table_index_getnext_slot will need some kind of callback that provides
-	 * a way for the scan to give up when the costs start to get out of hand.
+	 * We set xs_visited_pages_limit to tell the table AM to count distinct
+	 * heap pages visited for non-visible tuples and give up after the limit
+	 * is exceeded.
 	 */
+#define VISITED_PAGES_LIMIT 100
 	InitNonVacuumableSnapshot(SnapshotNonVacuumable,
 							  GlobalVisTestFor(heapRel));
 
@@ -7180,6 +7180,7 @@ get_actual_variable_endpoint(Relation heapRel,
 								 &SnapshotNonVacuumable, NULL,
 								 1, 0);
 	Assert(index_scan->xs_want_itup);
+	index_scan->xs_visited_pages_limit = VISITED_PAGES_LIMIT;
 	index_rescan(index_scan, scankeys, 1, NULL, 0);
 
 	/* Fetch first/next tuple in specified direction */
-- 
2.53.0

