From 38eae48e941e3e96774b4507a97187128ae7df24 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Thu, 19 Mar 2026 12:32:34 -0400
Subject: [PATCH v17 04/18] Implement VISITED_PAGES_LIMIT within heapam.

The new slot-based interface added by commit XXX to enable index
prefetching made it impossible to enforce VISITED_PAGES_LIMIT.  Fix the
problem by implementing generalizing VISITED_PAGES_LIMIT into a table AM
utility, implemented by heapam's index-only scan slot callbacks.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Andres Freund <andres@anarazel.de>
Discussion: https://postgr.es/m/CAH2-Wz=HJc+QV2AZ9mUY43aKL+n+a1JQ-7OGE=MOkqSAtoKJug@mail.gmail.com
---
 src/include/access/relscan.h             |  7 +++++++
 src/backend/access/heap/heapam_handler.c | 24 ++++++++++++++++++++++++
 src/backend/access/index/genam.c         |  1 +
 src/backend/utils/adt/selfuncs.c         |  7 +++++--
 4 files changed, 37 insertions(+), 2 deletions(-)

diff --git a/src/include/access/relscan.h b/src/include/access/relscan.h
index 301afa7b6..10f350b6f 100644
--- a/src/include/access/relscan.h
+++ b/src/include/access/relscan.h
@@ -381,6 +381,13 @@ typedef struct IndexScanDescData
 	bool	   *xs_orderbynulls;
 	bool		xs_recheckorderby;
 
+	/*
+	 * An approximate limit on the amount of work, measured in pages touched,
+	 * imposed on the index scan.  The default, 0, means no limit.  Used by
+	 * selfuncs.c to bound the cost of get_actual_variable_endpoint().
+	 */
+	uint8		xs_visited_pages_limit;
+
 	/* parallel index scan information, in shared memory */
 	struct ParallelIndexScanDescData *parallel_scan;
 } IndexScanDescData;
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index 7d3f81d74..a40f7fe32 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -657,6 +657,12 @@ heapam_index_getnext_slot(IndexScanDesc scan, ScanDirection direction,
 	ItemPointer tid;
 	bool		heap_continue = false;
 	bool		all_visible = false;
+	BlockNumber last_visited_block = InvalidBlockNumber;
+	uint8		n_visited_pages = 0,
+				xs_visited_pages_limit = 0;
+
+	if (index_only)
+		xs_visited_pages_limit = scan->xs_visited_pages_limit;
 
 	for (;;)
 	{
@@ -702,7 +708,25 @@ heapam_index_getnext_slot(IndexScanDesc scan, ScanDirection direction,
 
 				if (!heapam_index_fetch_heap(scan, slot, &heap_continue,
 											 amgetbatch))
+				{
+					/*
+					 * 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 1b21d421e..294f2ca47 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 3ff5d0c1d..9f623d97b 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -7168,9 +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.
+	 * 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));
 
@@ -7178,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

