From d4da5a5de43062c847af3924214627aefe94ba1e Mon Sep 17 00:00:00 2001
From: Greg Burd <greg@burd.me>
Date: Thu, 1 Oct 2026 10:19:05 -0400
Subject: [PATCH v9 1/7] Let an ordering index scan hand its ORDER BY values to
 the target list

An Index Scan that orders by an operator (ORDER BY col <-> const)
computes each returned row's ORDER BY value inside the index AM and
reports it in xs_orderbyvals.  If the same expression also appears in
the target list, the executor then evaluates it a second time in the
scan's projection (a third time if the AM asked for a recheck).  For an
expensive distance operator that is wasted work.

The AM contract only requires a non-rechecked distance to sort the same
way as the operator's result, not to equal it, so the executor cannot
substitute it blindly.  Add an optional IndexAmRoutine callback,
amcanreturnorderby(opfamily, opcintype, orderbyop): does the opclass
report, for a non-rechecked leaf entry and that ordering operator,
exactly the value the operator would return?  It takes the operator
because one opfamily can hold several ordering operators.  The planner
keeps the callback in IndexOptInfo, like amcostestimate, and
create_indexscan_plan() asks it for each ORDER BY key, carrying one
bool per key into the IndexScan.

For each qualifying key, setrefs.c replaces a top-level target list
expression that is equal() to that indexorderbyorig expression with
Var(INNER_VAR, i), and nodeIndexscan.c stores the returned row's ORDER
BY values in a virtual slot it installs as the projection's inner tuple
(a scan node has no inner plan, so INNER_VAR is otherwise unused
there).  The values stored are the AM's for a row it reported exact,
the recomputed ones for a row it asked to recheck, a reorder queue
entry's copy for a queued row, and freshly recomputed ones for an
EvalPlanQual test tuple.  Only float4/float8 ORDER BY expressions
qualify, as those are the only types
index_store_float8_orderby_distances() can deliver.

GiST answers yes for point_ops and SP-GiST for
quad_point_ops and kd_point_ops, whose leaf distances call
point_distance(), the function behind <->(point, point).  box_ops
doesn't qualify: its leaf distance is exact arithmetic on the box,
while <->(box, point) treats a point within EPSILON of an edge's line
as on it, so the two can differ in the last digits.  The polygon and
circle opclasses report lower bounds and are not affected, nor is any
other AM or any out-of-core opclass, which all default to no.

EXPLAIN VERBOSE deparses the INNER_VAR back to the ORDER BY expression
and reports "Order By Values Used: N".  Above such a scan, an upper
node's reference to the value now prints with one more pair of
parentheses, the same way any reference to a computed scan column does.

Author: Greg Burd <greg@burd.me>
Discussion: https://postgr.es/m/8s5lT8erXzBMugXJQ6Wginbp_gc2B4hmCYhF0Q0GpnuK87eCAOcKs5K31AWCwraCNFIQt42wwkow_MPPubHg485MWv-zDBY4NL_JX9yJEEg=@burd.me
---
 doc/src/sgml/indexam.sgml                     |  37 +++
 src/backend/access/gist/gist.c                |   1 +
 src/backend/access/gist/gistget.c             |  23 ++
 src/backend/access/spgist/spgscan.c           |  21 ++
 src/backend/access/spgist/spgutils.c          |   1 +
 src/backend/commands/explain.c                |   4 +
 src/backend/executor/nodeIndexscan.c          |  78 +++++-
 src/backend/optimizer/plan/createplan.c       |  20 ++
 src/backend/optimizer/plan/setrefs.c          |  60 ++++
 src/backend/optimizer/util/plancat.c          |   2 +
 src/backend/utils/adt/ruleutils.c             |  17 ++
 src/include/access/amapi.h                    |   9 +
 src/include/access/gist_private.h             |   1 +
 src/include/access/spgist.h                   |   1 +
 src/include/nodes/execnodes.h                 |   7 +
 src/include/nodes/pathnodes.h                 |   2 +
 src/include/nodes/plannodes.h                 |  11 +
 .../isolation/expected/eval-plan-qual-knn.out |  40 +++
 src/test/isolation/isolation_schedule         |   1 +
 .../isolation/specs/eval-plan-qual-knn.spec   |  44 +++
 src/test/regress/expected/gist.out            | 261 ++++++++++++++++++
 src/test/regress/sql/gist.sql                 | 143 ++++++++++
 src/tools/pgindent/typedefs.list              |   1 +
 23 files changed, 783 insertions(+), 2 deletions(-)
 create mode 100644 src/test/isolation/expected/eval-plan-qual-knn.out
 create mode 100644 src/test/isolation/specs/eval-plan-qual-knn.spec

diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml
index f48da318530..ee20c47750a 100644
--- a/doc/src/sgml/indexam.sgml
+++ b/doc/src/sgml/indexam.sgml
@@ -157,6 +157,7 @@ typedef struct IndexAmRoutine
     ambulkdelete_function ambulkdelete;
     amvacuumcleanup_function amvacuumcleanup;
     amcanreturn_function amcanreturn;   /* can be NULL */
+    amcanreturnorderby_function amcanreturnorderby; /* can be NULL */
     amcostestimate_function amcostestimate;
     amgettreeheight_function amgettreeheight;   /* can be NULL */
     amoptions_function amoptions;
@@ -481,6 +482,32 @@ amcanreturn (Relation indexRelation, int attno);
 
   <para>
 <programlisting>
+bool
+amcanreturnorderby (Oid opfamily, Oid opcintype, Oid orderbyop);
+</programlisting>
+   Check whether, for an ordering scan using ordering operator
+   <parameter>orderbyop</parameter> on a column of the operator class given
+   by <parameter>opfamily</parameter> and <parameter>opcintype</parameter>,
+   the values the access method reports in
+   <literal>scan-&gt;xs_orderbyvals</literal> with
+   <literal>xs_recheckorderby</literal> false are exactly the values the
+   operator would return for the heap tuple.  If so, the executor may return
+   them for a target list entry that is the same expression as the
+   <literal>ORDER BY</literal> key, instead of evaluating the operator again.
+   The answer is for that operator class and operator, not for the access
+   method as a whole: an operator class whose distances merely sort like the
+   operator's result, or differ from it in any digit, or depend on anything
+   other than the indexed value, must answer false.  The planner calls this
+   while building the plan; it is only consulted for an
+   <type>float8</type> or <type>float4</type> <literal>ORDER BY</literal>
+   expression, and for <type>float4</type> it is the <type>float4</type>
+   rounding of the distance the access method reports that must equal the
+   operator's result.  If the <structfield>amcanreturnorderby</structfield>
+   field is set to NULL, no substitution is made.
+  </para>
+
+  <para>
+<programlisting>
 void
 amcostestimate (PlannerInfo *root,
                 IndexPath *path,
@@ -989,6 +1016,16 @@ amtranslatecmptype (CompareType cmptype, Oid opfamily, Oid opcintype);
        of that form can be passed to <function>amrescan</function> as described
        previously.
       </para>
+      <para>
+       Such an access method returns each entry's ordering values in
+       <literal>scan-&gt;xs_orderbyvals</literal>, and sets
+       <literal>scan-&gt;xs_recheckorderby</literal> to true when those values are
+       only lower bounds that the executor must recompute from the heap
+       tuple.  When <literal>xs_recheckorderby</literal> is false the values
+       need only sort in the same order as the ordering operator's results;
+       they need not equal them.  See <function>amcanreturnorderby</function>
+       below for how an access method can promise that they do.
+      </para>
      </listitem>
     </itemizedlist>
   </para>
diff --git a/src/backend/access/gist/gist.c b/src/backend/access/gist/gist.c
index 8565e225be7..f6c4592a1f5 100644
--- a/src/backend/access/gist/gist.c
+++ b/src/backend/access/gist/gist.c
@@ -93,6 +93,7 @@ gisthandler(PG_FUNCTION_ARGS)
 		.ambulkdelete = gistbulkdelete,
 		.amvacuumcleanup = gistvacuumcleanup,
 		.amcanreturn = gistcanreturn,
+		.amcanreturnorderby = gistcanreturnorderby,
 		.amcostestimate = gistcostestimate,
 		.amgettreeheight = NULL,
 		.amoptions = gistoptions,
diff --git a/src/backend/access/gist/gistget.c b/src/backend/access/gist/gistget.c
index 111c310f9df..bd762d6cac4 100644
--- a/src/backend/access/gist/gistget.c
+++ b/src/backend/access/gist/gistget.c
@@ -23,6 +23,8 @@
 #include "pgstat.h"
 #include "storage/predicate.h"
 #include "utils/float.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
 
@@ -813,3 +815,24 @@ gistcanreturn(Relation index, int attno)
 	else
 		return false;
 }
+
+/*
+ * Are the distances the opclass reports for 'orderbyop' on a leaf entry,
+ * when it does not set *recheck, the operator's own result?
+ *
+ * The GiST contract only requires a non-rechecked distance to *sort* like
+ * the operator's result, so we cannot assume this of an arbitrary opclass.
+ * point_ops computes a leaf's distance by calling point_distance(), the
+ * function behind <->(point, point), so the executor may return it in place
+ * of evaluating the operator.  box_ops doesn't qualify: its leaf distance is
+ * exact arithmetic on the box, while <->(box, point) treats points within
+ * EPSILON of an edge's line as on it, so the two can differ in the last
+ * digits.  Other opclasses say no, as before.
+ */
+bool
+gistcanreturnorderby(Oid opfamily, Oid opcintype, Oid orderbyop)
+{
+	return (get_opfamily_proc(opfamily, opcintype, opcintype,
+							  GIST_DISTANCE_PROC) == F_GIST_POINT_DISTANCE &&
+			get_opcode(orderbyop) == F_POINT_DISTANCE);
+}
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 2cc5f06f5d7..c9b50be3484 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -24,6 +24,7 @@
 #include "storage/bufmgr.h"
 #include "utils/datum.h"
 #include "utils/float.h"
+#include "utils/fmgroids.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
@@ -1088,3 +1089,23 @@ spgcanreturn(Relation index, int attno)
 
 	return cache->config.canReturnData;
 }
+
+/*
+ * Are the distances the opclass reports for 'orderbyop' on a leaf tuple,
+ * when it does not set recheckDistances, the operator's own result?
+ *
+ * As for GiST, the contract only requires them to sort like the operator's
+ * result.  quad_point_ops and kd_point_ops (whose leaf function is
+ * spg_quad_leaf_consistent()) compute a leaf's distance with
+ * point_distance(), the function behind <->(point, point).  box_ops doesn't
+ * qualify, for the reason given in gistcanreturnorderby(); polygon sets
+ * recheckDistances.
+ */
+bool
+spgcanreturnorderby(Oid opfamily, Oid opcintype, Oid orderbyop)
+{
+	return (get_opfamily_proc(opfamily, opcintype, opcintype,
+							  SPGIST_LEAF_CONSISTENT_PROC) ==
+			F_SPG_QUAD_LEAF_CONSISTENT &&
+			get_opcode(orderbyop) == F_POINT_DISTANCE);
+}
diff --git a/src/backend/access/spgist/spgutils.c b/src/backend/access/spgist/spgutils.c
index f2ee333f60d..d07e197a594 100644
--- a/src/backend/access/spgist/spgutils.c
+++ b/src/backend/access/spgist/spgutils.c
@@ -78,6 +78,7 @@ spghandler(PG_FUNCTION_ARGS)
 		.ambulkdelete = spgbulkdelete,
 		.amvacuumcleanup = spgvacuumcleanup,
 		.amcanreturn = spgcanreturn,
+		.amcanreturnorderby = spgcanreturnorderby,
 		.amcostestimate = spgcostestimate,
 		.amgettreeheight = NULL,
 		.amoptions = spgoptions,
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 4b1ad4227ac..86c2a4afbde 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1974,6 +1974,10 @@ ExplainNode(PlanState *planstate, List *ancestors,
 										   planstate, es);
 			show_scan_qual(((IndexScan *) plan)->indexorderbyorig,
 						   "Order By", planstate, ancestors, es);
+			if (es->verbose && ((IndexScan *) plan)->indexorderbytlist > 0)
+				ExplainPropertyInteger("Order By Values Used", NULL,
+									   ((IndexScan *) plan)->indexorderbytlist,
+									   es);
 			show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
 			if (plan->qual)
 				show_instrumentation_count("Rows Removed by Filter", 1,
diff --git a/src/backend/executor/nodeIndexscan.c b/src/backend/executor/nodeIndexscan.c
index 129d005f187..c81c23895f0 100644
--- a/src/backend/executor/nodeIndexscan.c
+++ b/src/backend/executor/nodeIndexscan.c
@@ -163,6 +163,37 @@ IndexNext(IndexScanState *node)
 	return ExecClearTuple(slot);
 }
 
+/*
+ * Hand the ORDER BY values of the row about to be returned to the projection,
+ * when the targetlist asks for them (indexorderbytlist > 0, set by setrefs.c
+ * only for keys whose opclass passes amcanreturnorderby).  vals and nulls
+ * are what the caller established as this row's ordering values: the AM's
+ * when it reported them exact, the recomputed ones when it asked for a
+ * recheck, or a reorder queue entry's copy.  Never a lossy AM's lower bound.
+ * Only float8 and float4 values are handed over (see setrefs.c's
+ * replace_orderby_tlist_refs()), which are passed by value, so the slot can
+ * keep them after the caller frees its copies.
+ */
+static inline void
+IndexStoreOrderByValues(IndexScanState *node, const Datum *vals,
+						const bool *nulls)
+{
+	TupleTableSlot *oslot = node->iss_OrderBySlot;
+	int			i;
+
+	if (oslot == NULL)
+		return;
+	ExecClearTuple(oslot);
+	for (i = 0; i < node->iss_NumOrderByKeys; i++)
+	{
+		Assert(node->iss_OrderByTypByVals[i]);
+		oslot->tts_values[i] = vals[i];
+		oslot->tts_isnull[i] = nulls[i];
+	}
+	ExecStoreVirtualTuple(oslot);
+	node->ss.ps.ps_ExprContext->ecxt_innertuple = oslot;
+}
+
 /* ----------------------------------------------------------------
  *		IndexNextWithReorder
  *
@@ -251,6 +282,9 @@ IndexNextWithReorder(IndexScanState *node)
 			{
 				HeapTuple	tuple;
 
+				/* the values are by-value; see IndexStoreOrderByValues() */
+				IndexStoreOrderByValues(node, topmost->orderbyvals,
+										topmost->orderbynulls);
 				tuple = reorderqueue_pop(node);
 
 				/* Pass 'true', as the tuple in the queue is a palloc'd copy */
@@ -352,6 +386,7 @@ next_indextuple:
 		else
 		{
 			/* Can return this tuple immediately. */
+			IndexStoreOrderByValues(node, lastfetched_vals, lastfetched_nulls);
 			return slot;
 		}
 	}
@@ -404,7 +439,22 @@ IndexRecheck(IndexScanState *node, TupleTableSlot *slot)
 
 	/* Does the tuple meet the indexqual condition? */
 	econtext->ecxt_scantuple = slot;
-	return ExecQualAndReset(node->indexqualorig, econtext);
+	if (!ExecQualAndReset(node->indexqualorig, econtext))
+		return false;
+
+	/*
+	 * The EPQ test tuple never went through IndexNext, so the ORDER BY values
+	 * the targetlist reads (if any) are not those of this row.  Recompute
+	 * them from the tuple, as we would for a rechecked row.
+	 */
+	if (node->iss_OrderBySlot != NULL)
+	{
+		EvalOrderByExpressions(node, econtext);
+		IndexStoreOrderByValues(node, node->iss_OrderByValues,
+								node->iss_OrderByNulls);
+	}
+
+	return true;
 }
 
 
@@ -954,7 +1004,21 @@ ExecInitIndexScan(IndexScan *node, EState *estate, int eflags)
 	 * Initialize result type and projection.
 	 */
 	ExecInitResultTypeTL(&indexstate->ss.ps);
-	ExecAssignScanProjectionInfo(&indexstate->ss);
+
+	/*
+	 * A targetlist that reads the ORDER BY values (INNER_VAR Vars, see
+	 * setrefs.c) is never the scan tuple's physical layout, so it always
+	 * projects; ExecConditionalAssignProjectionInfo would assert on the
+	 * INNER_VAR while testing whether it can skip projection.
+	 */
+	if (node->indexorderbytlist > 0)
+	{
+		ExecInitResultSlot(&indexstate->ss.ps, &TTSOpsVirtual);
+		ExecAssignProjectionInfo(&indexstate->ss.ps,
+								 indexstate->ss.ss_ScanTupleSlot->tts_tupleDescriptor);
+	}
+	else
+		ExecAssignScanProjectionInfo(&indexstate->ss);
 
 	/*
 	 * initialize child expressions
@@ -1024,6 +1088,16 @@ ExecInitIndexScan(IndexScan *node, EState *estate, int eflags)
 						   NULL,	/* no ArrayKeys */
 						   NULL);
 
+	/*
+	 * A targetlist that reads the ORDER BY values needs a slot to read them
+	 * from, typed as the ORDER BY expressions (setrefs.c built it so).
+	 */
+	if (node->indexorderbytlist > 0)
+		indexstate->iss_OrderBySlot =
+			ExecAllocTableSlot(&estate->es_tupleTable,
+							   ExecTypeFromExprList(node->indexorderbyorig),
+							   &TTSOpsVirtual, 0);
+
 	/* Initialize sort support, if we need to re-check ORDER BY exprs */
 	if (indexstate->iss_NumOrderByKeys > 0)
 	{
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 5bc3a8e76d5..0d0037029cf 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -3037,6 +3037,26 @@ create_indexscan_plan(PlannerInfo *root,
 											indexorderbyops,
 											best_path->indexscandir);
 
+	if (!indexonly && indexorderbys != NIL)
+	{
+		List	   *exact = NIL;
+		ListCell   *lo,
+				   *lc;
+
+		forboth(lo, best_path->indexorderbys, lc, best_path->indexorderbycols)
+		{
+			int			indexcol = lfirst_int(lc);
+			OpExpr	   *orderby = lfirst_node(OpExpr, lo);
+
+			exact = lappend_int(exact,
+								indexinfo->amcanreturnorderby != NULL &&
+								indexinfo->amcanreturnorderby(indexinfo->opfamily[indexcol],
+															  indexinfo->opcintype[indexcol],
+															  orderby->opno));
+		}
+		((IndexScan *) scan_plan)->indexorderbyexact = exact;
+	}
+
 	copy_generic_path_info(&scan_plan->plan, &best_path->path);
 
 	return scan_plan;
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index af78892fafc..26449a6c8cb 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -134,6 +134,7 @@ static bool flatten_rtes_walker(Node *node, flatten_rtes_walker_context *cxt);
 static void add_rte_to_flat_rtable(PlannerGlobal *glob, List *rteperminfos,
 								   RangeTblEntry *rte);
 static Plan *set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset);
+static void replace_orderby_tlist_refs(IndexScan *splan);
 static Plan *set_indexonlyscan_references(PlannerInfo *root,
 										  IndexOnlyScan *plan,
 										  int rtoffset);
@@ -704,6 +705,8 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)
 				splan->indexorderbyorig =
 					fix_scan_list(root, splan->indexorderbyorig,
 								  rtoffset, NUM_EXEC_QUAL(plan));
+				/* after both are fixed, so equal() compares like with like */
+				replace_orderby_tlist_refs(splan);
 			}
 			break;
 		case T_IndexOnlyScan:
@@ -1367,6 +1370,63 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)
 	return plan;
 }
 
+/*
+ * replace_orderby_tlist_refs
+ *		Let an ordering IndexScan hand its ORDER BY values to its targetlist.
+ *
+ * For each ORDER BY key whose index column's opclass promises that the
+ * distances it reports for an exact (non-rechecked) row are the ordering
+ * operator's own result (indexorderbyexact[i], from amcanreturnorderby),
+ * replace each top-level targetlist expression that is equal() to the i'th
+ * indexorderbyorig expression with Var(INNER_VAR, i + 1).  nodeIndexscan.c
+ * then fills a virtual slot with the ORDER BY values of every returned row --
+ * the AM's for an exact row, the recomputed ones for a rechecked row -- and
+ * installs it as the projection's inner tuple.  A scan node has no inner
+ * plan, so INNER_VAR is otherwise unused here.
+ *
+ * Only float8/float4 expressions qualify: those are the only types
+ * index_store_float8_orderby_distances() can deliver.
+ */
+static void
+replace_orderby_tlist_refs(IndexScan *splan)
+{
+	ListCell   *lc;
+	int			nrepl = 0;
+
+	if (splan->indexorderbyexact == NIL)
+		return;
+	Assert(list_length(splan->indexorderbyexact) ==
+		   list_length(splan->indexorderbyorig));
+
+	foreach(lc, splan->scan.plan.targetlist)
+	{
+		TargetEntry *tle = lfirst_node(TargetEntry, lc);
+		ListCell   *lo,
+				   *le;
+		int			i = 0;
+
+		forboth(lo, splan->indexorderbyorig, le, splan->indexorderbyexact)
+		{
+			Expr	   *orig = (Expr *) lfirst(lo);
+			Oid			typ = exprType((Node *) orig);
+
+			i++;
+			if (lfirst_int(le) &&
+				(typ == FLOAT8OID || typ == FLOAT4OID) &&
+				equal(tle->expr, orig))
+			{
+				tle->expr = (Expr *) makeVar(INNER_VAR, i, typ,
+											 exprTypmod((Node *) orig),
+											 exprCollation((Node *) orig),
+											 0);
+				nrepl++;
+				break;
+			}
+		}
+	}
+	splan->indexorderbytlist = nrepl;
+}
+
 /*
  * set_indexonlyscan_references
  *		Do set_plan_references processing on an IndexOnlyScan
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index 22e0e8b11f8..bd496d71a2c 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -316,6 +316,7 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent,
 									  amroutine->amrestrpos != NULL);
 				info->amcostestimate = amroutine->amcostestimate;
 				Assert(info->amcostestimate != NULL);
+				info->amcanreturnorderby = amroutine->amcanreturnorderby;
 
 				/* Fetch index opclass options */
 				info->opclassoptions = RelationGetIndexAttOptions(indexRelation, true);
@@ -414,6 +415,7 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent,
 				info->amhasgetbitmap = false;
 				info->amcanmarkpos = false;
 				info->amcostestimate = NULL;
+				info->amcanreturnorderby = NULL;
 
 				info->sortopfamily = NULL;
 				info->reverse_sort = NULL;
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 908f9056fee..a5d96022782 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -8005,6 +8005,23 @@ resolve_special_varno(Node *node, deparse_context *context,
 		context->appendparents = save_appendparents;
 		return;
 	}
+	else if (var->varno == INNER_VAR && IsA(dpns->plan, IndexScan) &&
+			 ((IndexScan *) dpns->plan)->indexorderbytlist > 0)
+	{
+		/*
+		 * An IndexScan whose targetlist reads its own ORDER BY values (see
+		 * replace_orderby_tlist_refs in setrefs.c) uses INNER_VAR Vars
+		 * numbered by ORDER BY key; they deparse as the ORDER BY expressions.
+		 */
+		List	   *orderbys = ((IndexScan *) dpns->plan)->indexorderbyorig;
+
+		if (var->varattno < 1 || var->varattno > list_length(orderbys))
+			elog(ERROR, "bogus varattno for ORDER BY value var: %d",
+				 var->varattno);
+		resolve_special_varno((Node *) list_nth(orderbys, var->varattno - 1),
+							  context, callback, callback_arg);
+		return;
+	}
 	else if (var->varno == INNER_VAR && dpns->inner_tlist)
 	{
 		TargetEntry *tle;
diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h
index 79240333530..f520ba06e6a 100644
--- a/src/include/access/amapi.h
+++ b/src/include/access/amapi.h
@@ -144,6 +144,14 @@ typedef IndexBulkDeleteResult *(*amvacuumcleanup_function) (IndexVacuumInfo *inf
 /* can indexscan return IndexTuples? */
 typedef bool (*amcanreturn_function) (Relation indexRelation, int attno);
 
+/*
+ * Are the ORDER BY distances an opclass reports for ordering operator
+ * 'orderbyop' on an exact (xs_recheckorderby = false) leaf entry the
+ * operator's own result?
+ */
+typedef bool (*amcanreturnorderby_function) (Oid opfamily, Oid opcintype,
+											 Oid orderbyop);
+
 /* estimate cost of an indexscan */
 typedef void (*amcostestimate_function) (PlannerInfo *root,
 										 IndexPath *path,
@@ -300,6 +308,7 @@ typedef struct IndexAmRoutine
 	ambulkdelete_function ambulkdelete;
 	amvacuumcleanup_function amvacuumcleanup;
 	amcanreturn_function amcanreturn;	/* can be NULL */
+	amcanreturnorderby_function amcanreturnorderby; /* can be NULL */
 	amcostestimate_function amcostestimate;
 	amgettreeheight_function amgettreeheight;	/* can be NULL */
 	amoptions_function amoptions;
diff --git a/src/include/access/gist_private.h b/src/include/access/gist_private.h
index 39fdd27f5b9..d2b3b5d7ea3 100644
--- a/src/include/access/gist_private.h
+++ b/src/include/access/gist_private.h
@@ -462,6 +462,7 @@ extern void gistkillitems(IndexScanDesc scan);
 extern bool gistgettuple(IndexScanDesc scan, ScanDirection dir);
 extern int64 gistgetbitmap(IndexScanDesc scan, TIDBitmap *tbm);
 extern bool gistcanreturn(Relation index, int attno);
+extern bool gistcanreturnorderby(Oid opfamily, Oid opcintype, Oid orderbyop);
 
 /* gistvalidate.c */
 extern bool gistvalidate(Oid opclassoid);
diff --git a/src/include/access/spgist.h b/src/include/access/spgist.h
index 083d93f8ffd..f293c14455b 100644
--- a/src/include/access/spgist.h
+++ b/src/include/access/spgist.h
@@ -210,6 +210,7 @@ extern void spgrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
 extern int64 spggetbitmap(IndexScanDesc scan, TIDBitmap *tbm);
 extern bool spggettuple(IndexScanDesc scan, ScanDirection dir);
 extern bool spgcanreturn(Relation index, int attno);
+extern bool spgcanreturnorderby(Oid opfamily, Oid opcintype, Oid orderbyop);
 
 /* spgvacuum.c */
 extern IndexBulkDeleteResult *spgbulkdelete(IndexVacuumInfo *info,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index e3cea52e7df..8b9a81b42f6 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1751,6 +1751,13 @@ typedef struct IndexScanState
 	bool	   *iss_OrderByTypByVals;
 	int16	   *iss_OrderByTypLens;
 	Size		iss_PscanLen;
+
+	/*
+	 * When the plan's targetlist reads the ORDER BY values (indexorderbytlist
+	 * > 0), the values of the row being returned, as a virtual slot installed
+	 * as the projection's inner tuple.
+	 */
+	TupleTableSlot *iss_OrderBySlot;
 } IndexScanState;
 
 /* ----------------
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 8615db98a55..c4e0df4338e 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -1447,6 +1447,8 @@ typedef struct IndexOptInfo
 	/* AM's cost estimator */
 	/* Rather than include amapi.h here, we declare amcostestimate like this */
 	void		(*amcostestimate) (struct PlannerInfo *, struct IndexPath *, double, Cost *, Cost *, Selectivity *, double *, double *) pg_node_attr(read_write_ignore);
+	/* AM's amcanreturnorderby, or NULL */
+	bool		(*amcanreturnorderby) (Oid, Oid, Oid) pg_node_attr(read_write_ignore);
 } IndexOptInfo;
 
 /*
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 9e538f3c44a..1a5cf09dba0 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -616,6 +616,17 @@ typedef struct IndexScan
 	List	   *indexorderbyops;
 	/* forward or backward or don't care */
 	ScanDirection indexorderdir;
+
+	/*
+	 * Per indexorderbyorig entry: may setrefs.c replace a target-list
+	 * occurrence of that expression with an INNER_VAR reference to the scan's
+	 * ORDER BY value?  True when the index column's opclass reports exact
+	 * distances that are the operator's result (amcanreturnorderby).
+	 * indexorderbytlist counts the replacements actually made (0 if none),
+	 * for EXPLAIN.
+	 */
+	List	   *indexorderbyexact;
+	int			indexorderbytlist;
 } IndexScan;
 
 /* ----------------
diff --git a/src/test/isolation/expected/eval-plan-qual-knn.out b/src/test/isolation/expected/eval-plan-qual-knn.out
new file mode 100644
index 00000000000..3c1c4794b64
--- /dev/null
+++ b/src/test/isolation/expected/eval-plan-qual-knn.out
@@ -0,0 +1,40 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s2e s1b s1u s2s s1c
+step s2e: 
+    EXPLAIN (VERBOSE, COSTS OFF)
+    SELECT id, p <-> point(0, 0) AS d FROM epq_knn
+      WHERE p <@ box(point(0, 0), point(10, 10))
+      ORDER BY p <-> point(0, 0) LIMIT 4 FOR UPDATE;
+
+QUERY PLAN                                                   
+-------------------------------------------------------------
+Limit                                                        
+  Output: id, ((p <-> '(0,0)'::point)), ctid                 
+  ->  LockRows                                               
+        Output: id, ((p <-> '(0,0)'::point)), ctid           
+        ->  Index Scan using epq_knn_p on public.epq_knn     
+              Output: id, ((p <-> '(0,0)'::point)), ctid     
+              Index Cond: (epq_knn.p <@ '(10,10),(0,0)'::box)
+              Order By: (epq_knn.p <-> '(0,0)'::point)       
+              Order By Values Used: 1                        
+(9 rows)
+
+step s1b: BEGIN;
+step s1u: UPDATE epq_knn SET p = point(3.5, 3.5) WHERE id = 3;
+step s2s: 
+    SELECT id, p <-> point(0, 0) AS d, (p <-> point(0, 0)) = d AS same
+      FROM (SELECT id, p, p <-> point(0, 0) AS d FROM epq_knn
+              WHERE p <@ box(point(0, 0), point(10, 10))
+              ORDER BY p <-> point(0, 0) LIMIT 4 FOR UPDATE) s;
+ <waiting ...>
+step s1c: COMMIT;
+step s2s: <... completed>
+id|                 d|same
+--+------------------+----
+ 1|1.4142135623730951|t   
+ 2|2.8284271247461903|t   
+ 3| 4.949747468305833|t   
+ 4| 5.656854249492381|t   
+(4 rows)
+
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index bf9a2037c70..1703a3ec50b 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -44,6 +44,7 @@ test: subxid-overflow
 test: eval-plan-qual
 test: eval-plan-qual-trigger
 test: eval-plan-qual-partition-prune
+test: eval-plan-qual-knn
 test: inplace-inval
 test: intra-grant-inplace
 test: intra-grant-inplace-db
diff --git a/src/test/isolation/specs/eval-plan-qual-knn.spec b/src/test/isolation/specs/eval-plan-qual-knn.spec
new file mode 100644
index 00000000000..b485e88dbc3
--- /dev/null
+++ b/src/test/isolation/specs/eval-plan-qual-knn.spec
@@ -0,0 +1,44 @@
+# An ordering index scan that returns its ORDER BY values (amcanreturnorderby)
+# must not hand back the value it computed for the old row version when
+# EvalPlanQual re-checks a concurrently updated row: the value is recomputed
+# from the new version.
+
+setup
+{
+    CREATE TABLE epq_knn (id int, p point);
+    INSERT INTO epq_knn SELECT i, point(i, i) FROM generate_series(1, 20) i;
+    CREATE INDEX epq_knn_p ON epq_knn USING gist (p);
+}
+
+teardown
+{
+    DROP TABLE epq_knn;
+}
+
+session s1
+setup		{ SET enable_seqscan = off; SET enable_bitmapscan = off; }
+step s1b	{ BEGIN; }
+step s1u	{ UPDATE epq_knn SET p = point(3.5, 3.5) WHERE id = 3; }
+step s1c	{ COMMIT; }
+
+session s2
+setup		{ SET enable_seqscan = off; SET enable_bitmapscan = off; }
+step s2e
+{
+    EXPLAIN (VERBOSE, COSTS OFF)
+    SELECT id, p <-> point(0, 0) AS d FROM epq_knn
+      WHERE p <@ box(point(0, 0), point(10, 10))
+      ORDER BY p <-> point(0, 0) LIMIT 4 FOR UPDATE;
+}
+step s2s
+{
+    SELECT id, p <-> point(0, 0) AS d, (p <-> point(0, 0)) = d AS same
+      FROM (SELECT id, p, p <-> point(0, 0) AS d FROM epq_knn
+              WHERE p <@ box(point(0, 0), point(10, 10))
+              ORDER BY p <-> point(0, 0) LIMIT 4 FOR UPDATE) s;
+}
+
+# s2's scan computes id 3's distance from (3,3), then waits on s1's lock;
+# after s1 commits, EvalPlanQual re-checks id 3 with p = (3.5,3.5), and the
+# value returned must be that row's distance.
+permutation s2e s1b s1u s2s s1c
diff --git a/src/test/regress/expected/gist.out b/src/test/regress/expected/gist.out
index ac79f94aa80..9a280698d87 100644
--- a/src/test/regress/expected/gist.out
+++ b/src/test/regress/expected/gist.out
@@ -387,6 +387,267 @@ select p from gist_tbl order by circle(p,1) <-> point(0,0) limit 1;
 
 select p from gist_tbl order by circle(p,1) <-> point(0,0) limit 1;
 ERROR:  lossy distance functions are not supported in index-only scans
+drop index gist_tbl_multi_index;
+-- Test that an ordering Index Scan returns the AM's ORDER BY values to a
+-- targetlist entry equal to the ORDER BY expression, instead of evaluating
+-- the expression again, for an opclass that passes amcanreturnorderby.
+set enable_indexonlyscan = off;
+-- An expression index over a counting function proves the operator is not
+-- re-evaluated: the count stays at zero for the rows returned.
+create sequence gist_cnt_seq;
+create function gist_cnt_pt(point) returns point language plpgsql immutable
+  as $$ begin perform nextval('public.gist_cnt_seq'); return $1; end $$;
+create index gist_tbl_cnt_index on gist_tbl using gist (gist_cnt_pt(p));
+explain (verbose, costs off)
+select p, gist_cnt_pt(p) <-> point(0.201, 0.201) as dist
+  from gist_tbl order by gist_cnt_pt(p) <-> point(0.201, 0.201) limit 3;
+                               QUERY PLAN                               
+------------------------------------------------------------------------
+ Limit
+   Output: p, ((gist_cnt_pt(p) <-> '(0.201,0.201)'::point))
+   ->  Index Scan using gist_tbl_cnt_index on public.gist_tbl
+         Output: p, ((gist_cnt_pt(p) <-> '(0.201,0.201)'::point))
+         Order By: (gist_cnt_pt(gist_tbl.p) <-> '(0.201,0.201)'::point)
+         Order By Values Used: 1
+(6 rows)
+
+select setval('gist_cnt_seq', 1, false);
+ setval 
+--------
+      1
+(1 row)
+
+select p, gist_cnt_pt(p) <-> point(0.201, 0.201) as dist
+  from gist_tbl order by gist_cnt_pt(p) <-> point(0.201, 0.201) limit 3;
+      p      |         dist          
+-------------+-----------------------
+ (0.2,0.2)   | 0.0014142135623730963
+ (0.25,0.25) |   0.06929646455628163
+ (0.15,0.15) |   0.07212489168102787
+(3 rows)
+
+select nextval('gist_cnt_seq') - 1 as calls_during_scan;
+ calls_during_scan 
+-------------------
+                 0
+(1 row)
+
+-- Every copy of the ORDER BY expression is served from the index; an
+-- expression that merely contains it is not.
+explain (verbose, costs off)
+select gist_cnt_pt(p) <-> point(0,0) as d1,
+       (gist_cnt_pt(p) <-> point(0,0)) * 2 as d2,
+       gist_cnt_pt(p) <-> point(0,0) as d3
+  from gist_tbl order by gist_cnt_pt(p) <-> point(0,0) limit 2;
+                                                                         QUERY PLAN                                                                          
+-------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Limit
+   Output: ((gist_cnt_pt(p) <-> '(0,0)'::point)), (((gist_cnt_pt(p) <-> '(0,0)'::point) * '2'::double precision)), ((gist_cnt_pt(p) <-> '(0,0)'::point))
+   ->  Index Scan using gist_tbl_cnt_index on public.gist_tbl
+         Output: ((gist_cnt_pt(p) <-> '(0,0)'::point)), ((gist_cnt_pt(p) <-> '(0,0)'::point) * '2'::double precision), ((gist_cnt_pt(p) <-> '(0,0)'::point))
+         Order By: (gist_cnt_pt(gist_tbl.p) <-> '(0,0)'::point)
+         Order By Values Used: 2
+(6 rows)
+
+drop index gist_tbl_cnt_index;
+drop function gist_cnt_pt(point);
+drop sequence gist_cnt_seq;
+-- The values must equal the operator's, also for a row re-fetched under
+-- row locking, and for a rescan with a new ORDER BY argument (LATERAL).
+create index gist_tbl_point_index on gist_tbl using gist (p);
+select count(*) filter (where dist = p <-> point(5.2, 5.91)) as same,
+       count(*) as total
+  from (select p, p <-> point(5.2, 5.91) as dist from gist_tbl
+          order by p <-> point(5.2, 5.91) limit 200) ss;
+ same | total 
+------+-------
+  200 |   200
+(1 row)
+
+set extra_float_digits = 0;
+select p, p <-> point(5.2, 5.91) as dist from gist_tbl
+  where p <@ box(point(5,5), point(6,6)) order by p <-> point(5.2, 5.91)
+  limit 3 for update;
+      p      |       dist        
+-------------+-------------------
+ (5.55,5.55) | 0.502095608425328
+ (5.6,5.6)   |   0.5060632371552
+ (5.5,5.5)   | 0.508035431835221
+(3 rows)
+
+reset extra_float_digits;
+explain (verbose, costs off)
+select v.q, n.dist
+  from (values (point(0.3, 0.3)), (point(7.1, 7.2)), (point(42, 42))) v(q),
+       lateral (select p <-> v.q as dist from gist_tbl
+                order by p <-> v.q limit 2) n;
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ Nested Loop
+   Output: "*VALUES*".column1, ((gist_tbl.p <-> "*VALUES*".column1))
+   ->  Values Scan on "*VALUES*"
+         Output: "*VALUES*".column1
+   ->  Limit
+         Output: ((gist_tbl.p <-> "*VALUES*".column1))
+         ->  Index Scan using gist_tbl_point_index on public.gist_tbl
+               Output: ((gist_tbl.p <-> "*VALUES*".column1))
+               Order By: (gist_tbl.p <-> "*VALUES*".column1)
+               Order By Values Used: 1
+(10 rows)
+
+select count(*) filter (where n.dist = n.p <-> v.q) as same, count(*) as total
+  from (values (point(0.3, 0.3)), (point(7.1, 7.2)), (point(42, 42))) v(q),
+       lateral (select p, p <-> v.q as dist from gist_tbl
+                order by p <-> v.q limit 50) n;
+ same | total 
+------+-------
+  150 |   150
+(1 row)
+
+-- A multi-column index ordered by an exact key and a lossy one: the lossy
+-- one's distances are rechecked, rows pass through the reorder queue, and
+-- the exact key's value is still the operator's.
+create index gist_tbl_pc_index on gist_tbl using gist (p, c);
+explain (verbose, costs off)
+select p <-> point(1, 1) as dp, c <-> point(2, 2) as dc from gist_tbl
+  order by p <-> point(1, 1), c <-> point(2, 2) limit 5;
+                                       QUERY PLAN                                        
+-----------------------------------------------------------------------------------------
+ Limit
+   Output: ((p <-> '(1,1)'::point)), ((c <-> '(2,2)'::point))
+   ->  Index Scan using gist_tbl_pc_index on public.gist_tbl
+         Output: ((p <-> '(1,1)'::point)), (c <-> '(2,2)'::point)
+         Order By: ((gist_tbl.p <-> '(1,1)'::point) AND (gist_tbl.c <-> '(2,2)'::point))
+         Order By Values Used: 1
+(6 rows)
+
+select count(*) filter (where dp = p <-> point(1, 1)) as same_p,
+       count(*) filter (where dc = c <-> point(2, 2)) as same_c,
+       count(*) as total
+  from (select p, c, p <-> point(1, 1) as dp, c <-> point(2, 2) as dc
+          from gist_tbl
+          order by p <-> point(1, 1), c <-> point(2, 2) limit 200) ss;
+ same_p | same_c | total 
+--------+--------+-------
+    200 |    200 |   200
+(1 row)
+
+drop index gist_tbl_pc_index;
+drop index gist_tbl_point_index;
+-- The box opclass computes a leaf's distance exactly, but the operator treats
+-- a point within EPSILON of an edge's line as on it, so the two can differ in
+-- the last digits; it doesn't qualify, and the operator is evaluated.
+create index gist_tbl_box_index on gist_tbl using gist (b);
+explain (verbose, costs off)
+select b, b <-> point(5.2, 5.91) as dist from gist_tbl
+  where b <@ box(point(5,5), point(6,6)) order by b <-> point(5.2, 5.91);
+                       QUERY PLAN                       
+--------------------------------------------------------
+ Index Scan using gist_tbl_box_index on public.gist_tbl
+   Output: b, (b <-> '(5.2,5.91)'::point)
+   Index Cond: (gist_tbl.b <@ '(6,6),(5,5)'::box)
+   Order By: (gist_tbl.b <-> '(5.2,5.91)'::point)
+(4 rows)
+
+create temp table gist_box_eps (b box);
+insert into gist_box_eps values ('((0,0),(1,1))');
+create index on gist_box_eps using gist (b);
+select (b <-> point(0.9999996, 2)) = (select b <-> point(0.9999996, 2)
+                                       from gist_box_eps) as same
+  from gist_box_eps order by b <-> point(0.9999996, 2);
+ same 
+------
+ t
+(1 row)
+
+drop table gist_box_eps;
+drop index gist_tbl_box_index;
+-- The circle opclass's distance is a lower bound that is always rechecked,
+-- and its opclass does not pass amcanreturnorderby: the targetlist is left
+-- alone and the operator is evaluated as before.
+create index gist_tbl_circle_index on gist_tbl using gist (c);
+explain (verbose, costs off)
+select c <-> point(5.2, 5.91) as dist from gist_tbl
+  order by c <-> point(5.2, 5.91) limit 3;
+                           QUERY PLAN                            
+-----------------------------------------------------------------
+ Limit
+   Output: ((c <-> '(5.2,5.91)'::point))
+   ->  Index Scan using gist_tbl_circle_index on public.gist_tbl
+         Output: (c <-> '(5.2,5.91)'::point)
+         Order By: (gist_tbl.c <-> '(5.2,5.91)'::point)
+(5 rows)
+
+select count(*) filter (where dist = c <-> point(5.2, 5.91)) as same,
+       count(*) as total
+  from (select c, c <-> point(5.2, 5.91) as dist from gist_tbl
+          order by c <-> point(5.2, 5.91) limit 200) ss;
+ same | total 
+------+-------
+  200 |   200
+(1 row)
+
+drop index gist_tbl_circle_index;
+reset enable_indexonlyscan;
+-- SP-GiST's quad and k-d point opclasses qualify as well; its box opclass
+-- doesn't, for the same reason as GiST's.
+create temp table spg_orderby (p point, b box);
+insert into spg_orderby
+  select point(i % 97 * 0.31, i / 97 * 0.29),
+         box(point(i % 97 * 0.31, i / 97 * 0.29),
+             point(i % 97 * 0.31 + 0.2, i / 97 * 0.29 + 0.1))
+  from generate_series(1, 5000) i;
+create index spg_orderby_quad on spg_orderby using spgist (p);
+set enable_indexonlyscan = off;
+explain (verbose, costs off)
+select p <-> point(3, 4) as dist from spg_orderby
+  order by p <-> point(3, 4) limit 3;
+                           QUERY PLAN                           
+----------------------------------------------------------------
+ Limit
+   Output: ((p <-> '(3,4)'::point))
+   ->  Index Scan using spg_orderby_quad on pg_temp.spg_orderby
+         Output: ((p <-> '(3,4)'::point))
+         Order By: (spg_orderby.p <-> '(3,4)'::point)
+         Order By Values Used: 1
+(6 rows)
+
+select count(*) filter (where dist = p <-> point(3, 4)) as same,
+       count(*) as total
+  from (select p, p <-> point(3, 4) as dist from spg_orderby
+          order by p <-> point(3, 4) limit 300) ss;
+ same | total 
+------+-------
+  300 |   300
+(1 row)
+
+drop index spg_orderby_quad;
+create index spg_orderby_kd on spg_orderby using spgist (p kd_point_ops);
+select count(*) filter (where dist = p <-> point(3, 4)) as same,
+       count(*) as total
+  from (select p, p <-> point(3, 4) as dist from spg_orderby
+          order by p <-> point(3, 4) limit 300) ss;
+ same | total 
+------+-------
+  300 |   300
+(1 row)
+
+drop index spg_orderby_kd;
+create index spg_orderby_box on spg_orderby using spgist (b);
+explain (verbose, costs off)
+select b <-> point(3, 4) as dist from spg_orderby
+  order by b <-> point(3, 4) limit 3;
+                          QUERY PLAN                           
+---------------------------------------------------------------
+ Limit
+   Output: ((b <-> '(3,4)'::point))
+   ->  Index Scan using spg_orderby_box on pg_temp.spg_orderby
+         Output: (b <-> '(3,4)'::point)
+         Order By: (spg_orderby.b <-> '(3,4)'::point)
+(5 rows)
+
+reset enable_indexonlyscan;
+drop table spg_orderby;
 -- Test that an index-only scan deforms the tuple it reconstructs with the
 -- descriptor the AM formed it with, not the scan slot's descriptor.
 create temp table gist_ios_tupdesc (a inet, r numrange);
diff --git a/src/test/regress/sql/gist.sql b/src/test/regress/sql/gist.sql
index 57dcc082450..e05c18d7182 100644
--- a/src/test/regress/sql/gist.sql
+++ b/src/test/regress/sql/gist.sql
@@ -169,6 +169,149 @@ explain (verbose, costs off)
 select p from gist_tbl order by circle(p,1) <-> point(0,0) limit 1;
 select p from gist_tbl order by circle(p,1) <-> point(0,0) limit 1;
 
+drop index gist_tbl_multi_index;
+
+-- Test that an ordering Index Scan returns the AM's ORDER BY values to a
+-- targetlist entry equal to the ORDER BY expression, instead of evaluating
+-- the expression again, for an opclass that passes amcanreturnorderby.
+set enable_indexonlyscan = off;
+
+-- An expression index over a counting function proves the operator is not
+-- re-evaluated: the count stays at zero for the rows returned.
+create sequence gist_cnt_seq;
+create function gist_cnt_pt(point) returns point language plpgsql immutable
+  as $$ begin perform nextval('public.gist_cnt_seq'); return $1; end $$;
+create index gist_tbl_cnt_index on gist_tbl using gist (gist_cnt_pt(p));
+
+explain (verbose, costs off)
+select p, gist_cnt_pt(p) <-> point(0.201, 0.201) as dist
+  from gist_tbl order by gist_cnt_pt(p) <-> point(0.201, 0.201) limit 3;
+
+select setval('gist_cnt_seq', 1, false);
+select p, gist_cnt_pt(p) <-> point(0.201, 0.201) as dist
+  from gist_tbl order by gist_cnt_pt(p) <-> point(0.201, 0.201) limit 3;
+select nextval('gist_cnt_seq') - 1 as calls_during_scan;
+
+-- Every copy of the ORDER BY expression is served from the index; an
+-- expression that merely contains it is not.
+explain (verbose, costs off)
+select gist_cnt_pt(p) <-> point(0,0) as d1,
+       (gist_cnt_pt(p) <-> point(0,0)) * 2 as d2,
+       gist_cnt_pt(p) <-> point(0,0) as d3
+  from gist_tbl order by gist_cnt_pt(p) <-> point(0,0) limit 2;
+
+drop index gist_tbl_cnt_index;
+drop function gist_cnt_pt(point);
+drop sequence gist_cnt_seq;
+
+-- The values must equal the operator's, also for a row re-fetched under
+-- row locking, and for a rescan with a new ORDER BY argument (LATERAL).
+create index gist_tbl_point_index on gist_tbl using gist (p);
+
+select count(*) filter (where dist = p <-> point(5.2, 5.91)) as same,
+       count(*) as total
+  from (select p, p <-> point(5.2, 5.91) as dist from gist_tbl
+          order by p <-> point(5.2, 5.91) limit 200) ss;
+
+set extra_float_digits = 0;
+select p, p <-> point(5.2, 5.91) as dist from gist_tbl
+  where p <@ box(point(5,5), point(6,6)) order by p <-> point(5.2, 5.91)
+  limit 3 for update;
+reset extra_float_digits;
+
+explain (verbose, costs off)
+select v.q, n.dist
+  from (values (point(0.3, 0.3)), (point(7.1, 7.2)), (point(42, 42))) v(q),
+       lateral (select p <-> v.q as dist from gist_tbl
+                order by p <-> v.q limit 2) n;
+select count(*) filter (where n.dist = n.p <-> v.q) as same, count(*) as total
+  from (values (point(0.3, 0.3)), (point(7.1, 7.2)), (point(42, 42))) v(q),
+       lateral (select p, p <-> v.q as dist from gist_tbl
+                order by p <-> v.q limit 50) n;
+
+-- A multi-column index ordered by an exact key and a lossy one: the lossy
+-- one's distances are rechecked, rows pass through the reorder queue, and
+-- the exact key's value is still the operator's.
+create index gist_tbl_pc_index on gist_tbl using gist (p, c);
+explain (verbose, costs off)
+select p <-> point(1, 1) as dp, c <-> point(2, 2) as dc from gist_tbl
+  order by p <-> point(1, 1), c <-> point(2, 2) limit 5;
+select count(*) filter (where dp = p <-> point(1, 1)) as same_p,
+       count(*) filter (where dc = c <-> point(2, 2)) as same_c,
+       count(*) as total
+  from (select p, c, p <-> point(1, 1) as dp, c <-> point(2, 2) as dc
+          from gist_tbl
+          order by p <-> point(1, 1), c <-> point(2, 2) limit 200) ss;
+drop index gist_tbl_pc_index;
+drop index gist_tbl_point_index;
+
+-- The box opclass computes a leaf's distance exactly, but the operator treats
+-- a point within EPSILON of an edge's line as on it, so the two can differ in
+-- the last digits; it doesn't qualify, and the operator is evaluated.
+create index gist_tbl_box_index on gist_tbl using gist (b);
+
+explain (verbose, costs off)
+select b, b <-> point(5.2, 5.91) as dist from gist_tbl
+  where b <@ box(point(5,5), point(6,6)) order by b <-> point(5.2, 5.91);
+
+create temp table gist_box_eps (b box);
+insert into gist_box_eps values ('((0,0),(1,1))');
+create index on gist_box_eps using gist (b);
+select (b <-> point(0.9999996, 2)) = (select b <-> point(0.9999996, 2)
+                                       from gist_box_eps) as same
+  from gist_box_eps order by b <-> point(0.9999996, 2);
+drop table gist_box_eps;
+
+drop index gist_tbl_box_index;
+
+-- The circle opclass's distance is a lower bound that is always rechecked,
+-- and its opclass does not pass amcanreturnorderby: the targetlist is left
+-- alone and the operator is evaluated as before.
+create index gist_tbl_circle_index on gist_tbl using gist (c);
+
+explain (verbose, costs off)
+select c <-> point(5.2, 5.91) as dist from gist_tbl
+  order by c <-> point(5.2, 5.91) limit 3;
+
+select count(*) filter (where dist = c <-> point(5.2, 5.91)) as same,
+       count(*) as total
+  from (select c, c <-> point(5.2, 5.91) as dist from gist_tbl
+          order by c <-> point(5.2, 5.91) limit 200) ss;
+
+drop index gist_tbl_circle_index;
+reset enable_indexonlyscan;
+
+-- SP-GiST's quad and k-d point opclasses qualify as well; its box opclass
+-- doesn't, for the same reason as GiST's.
+create temp table spg_orderby (p point, b box);
+insert into spg_orderby
+  select point(i % 97 * 0.31, i / 97 * 0.29),
+         box(point(i % 97 * 0.31, i / 97 * 0.29),
+             point(i % 97 * 0.31 + 0.2, i / 97 * 0.29 + 0.1))
+  from generate_series(1, 5000) i;
+create index spg_orderby_quad on spg_orderby using spgist (p);
+set enable_indexonlyscan = off;
+explain (verbose, costs off)
+select p <-> point(3, 4) as dist from spg_orderby
+  order by p <-> point(3, 4) limit 3;
+select count(*) filter (where dist = p <-> point(3, 4)) as same,
+       count(*) as total
+  from (select p, p <-> point(3, 4) as dist from spg_orderby
+          order by p <-> point(3, 4) limit 300) ss;
+drop index spg_orderby_quad;
+create index spg_orderby_kd on spg_orderby using spgist (p kd_point_ops);
+select count(*) filter (where dist = p <-> point(3, 4)) as same,
+       count(*) as total
+  from (select p, p <-> point(3, 4) as dist from spg_orderby
+          order by p <-> point(3, 4) limit 300) ss;
+drop index spg_orderby_kd;
+create index spg_orderby_box on spg_orderby using spgist (b);
+explain (verbose, costs off)
+select b <-> point(3, 4) as dist from spg_orderby
+  order by b <-> point(3, 4) limit 3;
+reset enable_indexonlyscan;
+drop table spg_orderby;
+
 -- Test that an index-only scan deforms the tuple it reconstructs with the
 -- descriptor the AM formed it with, not the scan slot's descriptor.
 create temp table gist_ios_tupdesc (a inet, r numrange);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index a7a8dbdd23f..1d9246b1ff8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3567,6 +3567,7 @@ ambuildempty_function
 ambuildphasename_function
 ambulkdelete_function
 amcanreturn_function
+amcanreturnorderby_function
 amcostestimate_function
 amendscan_function
 amestimateparallelscan_function
-- 
2.50.1

