Remove extra Sort node above a btree-compatible Index Scan

Started by Alexandra Wang11 months ago4 messages
#1Alexandra Wang
alexandra.wang.oss@gmail.com
1 attachment(s)

Hello hackers,

While reviewing Mark Dilger’s patch series "Index AM API cleanup" [1]/messages/by-id/a5dfb7cd-7a89-48ab-a913-e5304eee0854@eisentraut.org,
I noticed some unexpected plan diffs when running the xtree and treeb
test modules. Specifically, an additional Sort node appears on top of
an Index Only Scan, even when the index key and sort key are the same.
For example:

--- src/test/modules/xtree/expected/interval.out
+++ src/test/modules/xtree/results/interval.out
@@ -375,10 +375,12 @@
 SET enable_seqscan TO false;
 EXPLAIN (COSTS OFF)
 SELECT f1 FROM INTERVAL_TBL_OF r1 ORDER BY f1;
-                             QUERY PLAN
---------------------------------------------------------------------
- Index Only Scan using interval_tbl_of_f1_idx on interval_tbl_of r1
-(1 row)
+                                QUERY PLAN
+--------------------------------------------------------------------------
+ Sort
+   Sort Key: f1
+   ->  Index Only Scan using interval_tbl_of_f1_idx on interval_tbl_of r1
+(3 rows)

I’ve attached a patch that removes this unnecessary Sort node for
B-tree-compatible indexes. This change should:
- Reduce the number of test failures in the xtree module from 43 to 30
- Reduce the size of regression.diffs from 234K to 123K

Since pathkey comparison is a hot path in query planning and exercised
by many test queries, I plan to gather more performance metrics.
However, in a simple test running make installcheck with and without
the patch, I observed no negative impact on the runtime of the
regression test suite (which doesn’t include other btree-like indexes)
and a positive impact on the same regression tests for xtree.

Regression tests (same plans):
w/o patch:
make installcheck 1.36s user 2.21s system 12% cpu 28.018 total
w/ patch:
make installcheck 1.32s user 2.12s system 12% cpu 28.089 total

xtree tests:
w/o patch (inferior plan w/ extra sort node):
make installcheck 1.52s user 2.42s system 10% cpu 36.817 total
w/ patch (good plan):
make installcheck 1.52s user 2.48s system 12% cpu 32.201 total

I’ve marked the patch as no-cfbot, as it applies only on top of the
aforementioned patch series [1]/messages/by-id/a5dfb7cd-7a89-48ab-a913-e5304eee0854@eisentraut.org.

Thoughts?

[1]: /messages/by-id/a5dfb7cd-7a89-48ab-a913-e5304eee0854@eisentraut.org
/messages/by-id/a5dfb7cd-7a89-48ab-a913-e5304eee0854@eisentraut.org

Best,
Alex

Attachments:

v1-0001-Remove-unnecessary-Sort-node-above-Index-Scan-of-.patch.no-cfbotapplication/octet-stream; name=v1-0001-Remove-unnecessary-Sort-node-above-Index-Scan-of-.patch.no-cfbotDownload
From 4ba8c1d67b323c9c2d9e41a2aad06d95df611bcb Mon Sep 17 00:00:00 2001
From: Alexandra Wang <alexandra.wang.oss@gmail.com>
Date: Wed, 23 Oct 2024 14:26:40 -0500
Subject: [PATCH v1] Remove unnecessary Sort node above Index Scan of
 btree-compatible AM

When determining whether two sort keys are equivalent, they may not
belong to the same operator family but could still use the same
underlying sort operator.  In such cases, a simple pointer comparison
of two pathkeys marks them as different, leading to the addition of an
unnecessary Sort node, even though the path is already sorted as
desired.

This patch improves pathkeys_contained_in() and
pathkeys_count_contained_in() by comparing the sort operators
directly, preventing redundant sorting.
---
 src/backend/optimizer/path/costsize.c   |  4 +--
 src/backend/optimizer/path/pathkeys.c   | 43 +++++++++++++++++++++++--
 src/backend/optimizer/plan/createplan.c |  6 ++--
 3 files changed, 43 insertions(+), 10 deletions(-)

diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index b7f6ab40dec..e95762ac9a4 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -3607,9 +3607,7 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 		opathkey = (PathKey *) linitial(opathkeys);
 		ipathkey = (PathKey *) linitial(ipathkeys);
 		/* debugging check */
-		if (opathkey->pk_opfamily != ipathkey->pk_opfamily ||
-			opathkey->pk_eclass->ec_collation != ipathkey->pk_eclass->ec_collation ||
-			opathkey->pk_strategy != ipathkey->pk_strategy ||
+		if (opathkey->pk_eclass->ec_collation != ipathkey->pk_eclass->ec_collation ||
 			opathkey->pk_cmptype != ipathkey->pk_cmptype ||
 			opathkey->pk_nulls_first != ipathkey->pk_nulls_first)
 			elog(ERROR, "left and right pathkeys do not match in mergejoin");
diff --git a/src/backend/optimizer/path/pathkeys.c b/src/backend/optimizer/path/pathkeys.c
index a0826ed5008..a709148ea9f 100644
--- a/src/backend/optimizer/path/pathkeys.c
+++ b/src/backend/optimizer/path/pathkeys.c
@@ -39,7 +39,7 @@ static bool matches_boolean_partition_clause(RestrictInfo *rinfo,
 											 int partkeycol);
 static Var *find_var_for_subquery_tle(RelOptInfo *rel, TargetEntry *tle);
 static bool right_merge_direction(PlannerInfo *root, PathKey *pathkey);
-
+static bool pathkeys_have_same_sortop(PathKey *pk1, PathKey *pk2);
 
 /****************************************************************************
  *		PATHKEY CONSTRUCTION AND REDUNDANCY TESTING
@@ -355,7 +355,7 @@ compare_pathkeys(List *keys1, List *keys2)
 		PathKey    *pathkey1 = (PathKey *) lfirst(key1);
 		PathKey    *pathkey2 = (PathKey *) lfirst(key2);
 
-		if (pathkey1 != pathkey2)
+		if (!pathkeys_have_same_sortop(pathkey1, pathkey2))
 			return PATHKEYS_DIFFERENT;	/* no need to keep looking */
 	}
 
@@ -585,6 +585,43 @@ get_useful_group_keys_orderings(PlannerInfo *root, Path *path)
 	return infos;
 }
 
+/*
+ * pathkeys_have_same_sortop
+ *
+ * Determines whether two PathKey objects should be considered equivalent
+ * for sorting purposes. Two PathKeys are treated as the same if:
+ *  - They are identical pointers OR
+ *  - They belong to the same equivalence class. AND
+ *  - They have the same comparison type and null ordering. AND
+ *  - They have the same sorting operator, even if they belong to different
+ *    operator families.
+ *
+ * The function retrieves the sort operator (sortop) for both PathKeys using
+ * their operator family, datatype, and strategy. If both PathKeys resolve to
+ * the same sort operator, they are considered equivalent.
+ */
+static bool
+pathkeys_have_same_sortop(PathKey *pk1, PathKey *pk2)
+{
+	Oid			pk_op1;
+	Oid			pk_op2;
+	Oid			pk_datatype;
+
+	if (pk1 == pk2)
+		return true;
+
+	if (pk1->pk_eclass != pk2->pk_eclass ||
+		pk1->pk_cmptype != pk2->pk_cmptype ||
+		pk1->pk_nulls_first != pk2->pk_nulls_first)
+		return false;
+
+	pk_datatype = ((EquivalenceMember *) linitial(pk1->pk_eclass->ec_members))->em_datatype;
+	pk_op1 = get_opfamily_member(pk1->pk_opfamily, pk_datatype, pk_datatype, pk1->pk_strategy);
+	pk_op2 = get_opfamily_member(pk2->pk_opfamily, pk_datatype, pk_datatype, pk2->pk_strategy);
+
+	return pk_op1 == pk_op2;
+}
+
 /*
  * pathkeys_count_contained_in
  *    Same as pathkeys_contained_in, but also sets length of longest
@@ -626,7 +663,7 @@ pathkeys_count_contained_in(List *keys1, List *keys2, int *n_common)
 		PathKey    *pathkey1 = (PathKey *) lfirst(key1);
 		PathKey    *pathkey2 = (PathKey *) lfirst(key2);
 
-		if (pathkey1 != pathkey2)
+		if (!pathkeys_have_same_sortop(pathkey1, pathkey2))
 		{
 			*n_common = n;
 			return false;
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 6a7a8d68db5..015f7f05801 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -4747,12 +4747,10 @@ create_mergejoin_plan(PlannerInfo *root,
 		 * matter which way we imagine this column to be ordered.)  But a
 		 * non-redundant inner pathkey had better match outer's ordering too.
 		 */
-		if (opathkey->pk_opfamily != ipathkey->pk_opfamily ||
-			opathkey->pk_eclass->ec_collation != ipathkey->pk_eclass->ec_collation)
+		if (opathkey->pk_eclass->ec_collation != ipathkey->pk_eclass->ec_collation)
 			elog(ERROR, "left and right pathkeys do not match in mergejoin");
 		if (first_inner_match &&
-			(opathkey->pk_strategy != ipathkey->pk_strategy ||
-			 opathkey->pk_cmptype != ipathkey->pk_cmptype ||
+			(opathkey->pk_cmptype != ipathkey->pk_cmptype ||
 			 opathkey->pk_nulls_first != ipathkey->pk_nulls_first))
 			elog(ERROR, "left and right pathkeys do not match in mergejoin");
 
-- 
2.39.5 (Apple Git-154)

In reply to: Alexandra Wang (#1)
Re: Remove extra Sort node above a btree-compatible Index Scan

On Fri, Feb 28, 2025 at 12:23 AM Alexandra Wang
<alexandra.wang.oss@gmail.com> wrote:

While reviewing Mark Dilger’s patch series "Index AM API cleanup" [1],
I noticed some unexpected plan diffs when running the xtree and treeb
test modules. Specifically, an additional Sort node appears on top of
an Index Only Scan, even when the index key and sort key are the same.
For example:

--- src/test/modules/xtree/expected/interval.out
+++ src/test/modules/xtree/results/interval.out

What's the xtree module? There's no such test module?

--
Peter Geoghegan

#3Tom Lane
tgl@sss.pgh.pa.us
In reply to: Alexandra Wang (#1)
Re: Remove extra Sort node above a btree-compatible Index Scan

Alexandra Wang <alexandra.wang.oss@gmail.com> writes:

I’ve attached a patch that removes this unnecessary Sort node for
B-tree-compatible indexes.

This does not look right at all. You can't just ignore the opfamily
etc. while deciding whether two pathkeys represent the same sort
ordering, as you did in create_mergejoin_plan(). I don't like
pathkeys_have_same_sortop() either. The pathkey data structures
were designed to let pointer comparison be sufficient for deciding
whether pathkeys are equivalent: see the "canonical pathkey" stuff
in pathkeys.c. I think that this patch may be band-aiding over some
breakage of that concept, but you've not provided enough context to
figure out what the underlying problem is.

regards, tom lane

#4Alexandra Wang
alexandra.wang.oss@gmail.com
In reply to: Peter Geoghegan (#2)
4 attachment(s)
Re: Remove extra Sort node above a btree-compatible Index Scan

Hi Peter,

On Thu, Feb 27, 2025 at 11:31 PM Peter Geoghegan <pg@bowt.ie> wrote:

On Fri, Feb 28, 2025 at 12:23 AM Alexandra Wang
<alexandra.wang.oss@gmail.com> wrote:

While reviewing Mark Dilger’s patch series "Index AM API cleanup" [1],
I noticed some unexpected plan diffs when running the xtree and treeb
test modules. Specifically, an additional Sort node appears on top of
an Index Only Scan, even when the index key and sort key are the same.
For example:

--- src/test/modules/xtree/expected/interval.out
+++ src/test/modules/xtree/results/interval.out

What's the xtree module? There's no such test module?

Thank you so much for asking!

I borrowed the following three patches from the Index AM API cleanup
thread [1]/messages/by-id/a5dfb7cd-7a89-48ab-a913-e5304eee0854@eisentraut.org and attached them here for reference:

v20-0001: TEST - Add loadable modules as tests of the AM API
v20-0003: TEST - Stop requiring BTREE_AM_OID outside btree
v20-0012: Use CompareType more and StrategyNumber less

The v20-0001 patch introduces the xtree loadable test module. Xtree as
an extension is a shallow copy of the btree index, it should behave
exactly like btree. Specifically, it uses the same operators and
strategy numbers as btree, and all of its IndexAMRoutine methods point
to the corresponding btree methods. The only difference is that xtree
belongs to a different opfamily.

v20-0001 adds two loadable modules:
src/test/module/xtree (a shallow copy of btree)
src/test/module/xhash (a shallow copy of hash, not relevant to this
discussion)

These test modules are not meant for commit; they are used solely for
testing the Index AM API.

By running "make check" in src/test/module/xtree, a script included in
the patch copies all tests from src/test/regress and replaces all
btree indexes with xtree indexes, allowing verification of how the
Index AM API works with non-core indexes.

The other two patches serve the following purposes:

v20-0003 removes BTREE_AM_OID-specific assertions outside btree code,
allowing other Index AMs.

v20-0012 is nice to have since it also touches the PathKey struct and
adds a CompareType field in PathKey. CompareType is more generic for
sort ordering compared to AM-specific strategy numbers. I’d like to
include this patch because even if two AMs have different strategy
numbers, they could still map to the same CompareType. When it comes
to deciding whether two PathKeys represent the same ordering, what
really matters is the underlying sort operator, and CompareType is
very relevant in that regard.

So, with the above patches applied (v20-0012 is optional for this
demonstration), I can run the following queries:

-- setup
create extension xtree;
create table t (b int, x int);
create index on t using btree(b);
create index on t using xtree(x);
insert into t select i, i from generate_series(1, 1000)i;
analyze t;

-- queries
explain (costs off) select b from t where b < 10 order by (b);
explain (costs off) select x from t where x < 10 order by (x);

-- output:
test=# explain (costs off) select b from t where b < 10 order by (b);
QUERY PLAN
------------------------------------
Index Only Scan using t_b_idx on t
Index Cond: (b < 10)
(2 rows)

test=# explain (costs off) select x from t where x < 10 order by (x);
QUERY PLAN
------------------------------------------
Sort
Sort Key: x
-> Index Only Scan using t_x_idx on t
Index Cond: (x < 10)
(4 rows)

Notice that the xtree index scan introduces an unnecessary Sort node,
even though the Index Only Scan already returns rows of x sorted by
the same operator (int4lt (”<”)) used in the ORDER BY (x) clause.

The problem I'm trying to solve is to eliminate this redundant sort
for btree-like index AMs.

[1]: /messages/by-id/a5dfb7cd-7a89-48ab-a913-e5304eee0854@eisentraut.org
/messages/by-id/a5dfb7cd-7a89-48ab-a913-e5304eee0854@eisentraut.org

Best,
Alex

Attachments:

v20-0003-TEST-Stop-requiring-BTREE_AM_OID-from-within-btr.patch.no-cfbotapplication/octet-stream; name=v20-0003-TEST-Stop-requiring-BTREE_AM_OID-from-within-btr.patch.no-cfbotDownload
From bc4f2e01f6e420f04dae817374e547afa600e884 Mon Sep 17 00:00:00 2001
From: Andrew Dunstan <andrew@dunslane.net>
Date: Mon, 25 Nov 2024 15:40:28 -0500
Subject: [PATCH v20 03/16] [TEST] Stop requiring BTREE_AM_OID from within
 btree code

Checking from within btree code that the index AM is btree, either
with Assert or with if/then/elog, makes that code nonreusable by
other indexes, such as that recently added to
src/test/modules/xtree.  Remove those checks.
---
 src/backend/utils/sort/sortsupport.c       | 2 --
 src/backend/utils/sort/tuplesortvariants.c | 2 --
 src/include/access/nbtree.h                | 6 ++----
 3 files changed, 2 insertions(+), 8 deletions(-)

diff --git a/src/backend/utils/sort/sortsupport.c b/src/backend/utils/sort/sortsupport.c
index 6037031eaa3..9b855be690e 100644
--- a/src/backend/utils/sort/sortsupport.c
+++ b/src/backend/utils/sort/sortsupport.c
@@ -166,8 +166,6 @@ PrepareSortSupportFromIndexRel(Relation indexRel, int16 strategy,
 
 	Assert(ssup->comparator == NULL);
 
-	if (indexRel->rd_rel->relam != BTREE_AM_OID)
-		elog(ERROR, "unexpected non-btree AM: %u", indexRel->rd_rel->relam);
 	if (strategy != BTGreaterStrategyNumber &&
 		strategy != BTLessStrategyNumber)
 		elog(ERROR, "unexpected sort support strategy: %d", strategy);
diff --git a/src/backend/utils/sort/tuplesortvariants.c b/src/backend/utils/sort/tuplesortvariants.c
index 913c4ef455e..617ff0da9af 100644
--- a/src/backend/utils/sort/tuplesortvariants.c
+++ b/src/backend/utils/sort/tuplesortvariants.c
@@ -251,8 +251,6 @@ tuplesort_begin_cluster(TupleDesc tupDesc,
 	TuplesortClusterArg *arg;
 	int			i;
 
-	Assert(indexRel->rd_rel->relam == BTREE_AM_OID);
-
 	oldcontext = MemoryContextSwitchTo(base->maincontext);
 	arg = (TuplesortClusterArg *) palloc0(sizeof(TuplesortClusterArg));
 
diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h
index e4fdeca3402..e26399ce238 100644
--- a/src/include/access/nbtree.h
+++ b/src/include/access/nbtree.h
@@ -1131,16 +1131,14 @@ typedef struct BTOptions
 } BTOptions;
 
 #define BTGetFillFactor(relation) \
-	(AssertMacro(relation->rd_rel->relkind == RELKIND_INDEX && \
-				 relation->rd_rel->relam == BTREE_AM_OID), \
+	(AssertMacro(relation->rd_rel->relkind == RELKIND_INDEX), \
 	 (relation)->rd_options ? \
 	 ((BTOptions *) (relation)->rd_options)->fillfactor : \
 	 BTREE_DEFAULT_FILLFACTOR)
 #define BTGetTargetPageFreeSpace(relation) \
 	(BLCKSZ * (100 - BTGetFillFactor(relation)) / 100)
 #define BTGetDeduplicateItems(relation) \
-	(AssertMacro(relation->rd_rel->relkind == RELKIND_INDEX && \
-				 relation->rd_rel->relam == BTREE_AM_OID), \
+	(AssertMacro(relation->rd_rel->relkind == RELKIND_INDEX), \
 	((relation)->rd_options ? \
 	 ((BTOptions *) (relation)->rd_options)->deduplicate_items : true))
 
-- 
2.48.1

v20-0012-Use-CompareType-more-and-StrategyNumber-less.patch.no-cfbotapplication/octet-stream; name=v20-0012-Use-CompareType-more-and-StrategyNumber-less.patch.no-cfbotDownload
From 00dc3e0cae56d8c6a9b38afb43ad4d8dd7ea4267 Mon Sep 17 00:00:00 2001
From: Andrew Dunstan <andrew@dunslane.net>
Date: Tue, 26 Nov 2024 08:53:22 -0500
Subject: [PATCH v20 12/16] Use CompareType more and StrategyNumber less

Reduce the number of places that hardcode a Btree strategy number by
instead using comparison types.

Rather than hardcoding BTREE_AM_OID in get_ordering_op_properties,
add a parameter to filter the index access methods to consider.  For
mainline code, callers now pass BTREE_AM_OID for this filter, which
means the behavior (and the regression test expected output) is
unchanged.

Author: Mark Dilger <mark.dilger@enterprisedb.com>
Discussion: https://www.postgresql.org/message-id/flat/E72EAA49-354D-4C2E-8EB9-255197F55330@enterprisedb.com
---
 contrib/postgres_fdw/postgres_fdw.c        |   4 +-
 src/backend/commands/explain.c             |   3 +-
 src/backend/commands/indexcmds.c           |   5 +-
 src/backend/commands/matview.c             |   6 +-
 src/backend/executor/execExpr.c            |   2 +
 src/backend/executor/nodeIncrementalSort.c |   3 +-
 src/backend/executor/nodeIndexscan.c       |  14 +-
 src/backend/executor/nodeMergejoin.c       |   5 +-
 src/backend/optimizer/path/allpaths.c      |  25 ++--
 src/backend/optimizer/path/costsize.c      |   6 +
 src/backend/optimizer/path/equivclass.c    |   4 +-
 src/backend/optimizer/path/indxpath.c      |  26 +++-
 src/backend/optimizer/path/pathkeys.c      |  97 ++++++++++---
 src/backend/optimizer/plan/createplan.c    |  13 +-
 src/backend/optimizer/plan/planagg.c       |   4 +-
 src/backend/optimizer/plan/planner.c       |  13 +-
 src/backend/optimizer/prep/prepunion.c     |  19 ++-
 src/backend/optimizer/util/plancat.c       |  31 ++--
 src/backend/optimizer/util/predtest.c      |  18 ++-
 src/backend/parser/parse_clause.c          |   8 +-
 src/backend/parser/parse_expr.c            |   6 +-
 src/backend/partitioning/partprune.c       |  17 ++-
 src/backend/utils/adt/network.c            |  12 +-
 src/backend/utils/adt/selfuncs.c           | 120 +++++++--------
 src/backend/utils/cache/lsyscache.c        | 161 ++++++++++++++++-----
 src/backend/utils/cache/typcache.c         |  15 +-
 src/backend/utils/sort/sortsupport.c       |   9 +-
 src/include/nodes/pathnodes.h              |  10 +-
 src/include/optimizer/paths.h              |   8 +-
 src/include/utils/lsyscache.h              |  29 +++-
 src/include/utils/selfuncs.h               |   9 +-
 src/tools/pgindent/typedefs.list           |   2 +-
 32 files changed, 475 insertions(+), 229 deletions(-)

diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index de43727a2a0..6db93fc4b29 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -994,8 +994,10 @@ get_useful_pathkeys_for_relation(PlannerInfo *root, RelOptInfo *rel)
 
 		/* Looks like we can generate a pathkey, so let's do it. */
 		pathkey = make_canonical_pathkey(root, cur_ec,
+										 get_opfamily_method(linitial_oid(cur_ec->ec_opfamilies)),
 										 linitial_oid(cur_ec->ec_opfamilies),
-										 BTLessStrategyNumber,
+										 InvalidStrategy,
+										 COMPARE_LT,
 										 false);
 		useful_pathkeys_list = lappend(useful_pathkeys_list,
 									   list_make1(pathkey));
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index c0d614866a9..72354b9ec68 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -14,6 +14,7 @@
 #include "postgres.h"
 
 #include "access/xact.h"
+#include "catalog/pg_am.h"		/* for BTREE_AM_OID */
 #include "catalog/pg_type.h"
 #include "commands/createas.h"
 #include "commands/defrem.h"
@@ -3037,7 +3038,7 @@ show_sortorder_options(StringInfo buf, Node *sortexpr,
 			elog(ERROR, "cache lookup failed for operator %u", sortOperator);
 		appendStringInfo(buf, " USING %s", opname);
 		/* Determine whether operator would be considered ASC or DESC */
-		(void) get_equality_op_for_ordering_op(sortOperator, &reverse);
+		(void) get_equality_op_for_ordering_op(sortOperator, BTREE_AM_OID, &reverse);
 	}
 
 	/* Add NULLS FIRST/LAST only if it wouldn't be default */
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index c92f5620ec1..87124b02d45 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -1057,10 +1057,11 @@ DefineIndex(Oid tableId,
 						Oid			idx_eqop = InvalidOid;
 
 						if (stmt->unique && !stmt->iswithoutoverlaps)
-							idx_eqop = get_opfamily_member(idx_opfamily,
+							idx_eqop = get_opmethod_member(InvalidOid,
+														   idx_opfamily,
 														   idx_opcintype,
 														   idx_opcintype,
-														   BTEqualStrategyNumber);
+														   COMPARE_EQ);
 						else if (exclusion)
 							idx_eqop = indexInfo->ii_ExclusionOps[j];
 						Assert(idx_eqop);
diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index 0bfbc5ca6dc..3d93d62d213 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -779,11 +779,11 @@ refresh_by_match_merge(Oid matviewOid, Oid tempOid, Oid relowner,
 				opcintype = cla_tup->opcintype;
 				ReleaseSysCache(cla_ht);
 
-				op = get_opfamily_member(opfamily, opcintype, opcintype,
-										 BTEqualStrategyNumber);
+				op = get_opmethod_member(InvalidOid, opfamily, opcintype, opcintype,
+										 COMPARE_EQ);
 				if (!OidIsValid(op))
 					elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
-						 BTEqualStrategyNumber, opcintype, opcintype, opfamily);
+						 COMPARE_EQ, opcintype, opcintype, opfamily);
 
 				/*
 				 * If we find the same column with the same equality semantics
diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index 03566c4d181..dbf98383117 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -2097,7 +2097,9 @@ ExecInitExprRec(Expr *node, ExprState *state,
 					FunctionCallInfo fcinfo;
 
 					get_op_opfamily_properties(opno, opfamily, false,
+											   NULL,		/* don't need opmethod */
 											   &strategy,
+											   NULL,		/* don't need cmptype */
 											   &lefttype,
 											   &righttype);
 					proc = get_opfamily_proc(opfamily,
diff --git a/src/backend/executor/nodeIncrementalSort.c b/src/backend/executor/nodeIncrementalSort.c
index 975b0397e7a..7d666eb2980 100644
--- a/src/backend/executor/nodeIncrementalSort.c
+++ b/src/backend/executor/nodeIncrementalSort.c
@@ -78,6 +78,7 @@
 
 #include "postgres.h"
 
+#include "catalog/pg_am.h"			/* for BTREE_AM_OID */
 #include "executor/execdebug.h"
 #include "executor/nodeIncrementalSort.h"
 #include "miscadmin.h"
@@ -180,7 +181,7 @@ preparePresortedCols(IncrementalSortState *node)
 		key->attno = plannode->sort.sortColIdx[i];
 
 		equalityOp = get_equality_op_for_ordering_op(plannode->sort.sortOperators[i],
-													 NULL);
+													 BTREE_AM_OID, NULL);
 		if (!OidIsValid(equalityOp))
 			elog(ERROR, "missing equality operator for ordering operator %u",
 				 plannode->sort.sortOperators[i]);
diff --git a/src/backend/executor/nodeIndexscan.c b/src/backend/executor/nodeIndexscan.c
index c30b9c2c197..bab9e74fcad 100644
--- a/src/backend/executor/nodeIndexscan.c
+++ b/src/backend/executor/nodeIndexscan.c
@@ -1180,6 +1180,7 @@ ExecIndexBuildScanKeys(PlanState *planstate, Relation index,
 		RegProcedure opfuncid;	/* operator proc id used in scan */
 		Oid			opfamily;	/* opfamily of index column */
 		int			op_strategy;	/* operator's strategy number */
+		CompareType op_cmptype;
 		Oid			op_lefttype;	/* operator's declared input types */
 		Oid			op_righttype;
 		Expr	   *leftop;		/* expr on lhs of operator */
@@ -1222,7 +1223,9 @@ ExecIndexBuildScanKeys(PlanState *planstate, Relation index,
 			opfamily = index->rd_opfamily[varattno - 1];
 
 			get_op_opfamily_properties(opno, opfamily, isorderby,
+									   NULL,		/* don't need opmethod */
 									   &op_strategy,
+									   NULL,		/* don't need cmptype */
 									   &op_lefttype,
 									   &op_righttype);
 
@@ -1340,11 +1343,13 @@ ExecIndexBuildScanKeys(PlanState *planstate, Relation index,
 				opfamily = index->rd_opfamily[varattno - 1];
 
 				get_op_opfamily_properties(opno, opfamily, isorderby,
+										   NULL,		/* don't need opmethod */
 										   &op_strategy,
+										   &op_cmptype,
 										   &op_lefttype,
 										   &op_righttype);
 
-				if (op_strategy != rc->cmptype)
+				if (op_cmptype != rc->cmptype)
 					elog(ERROR, "RowCompare index qualification contains wrong operator");
 
 				opfuncid = get_opfamily_proc(opfamily,
@@ -1421,7 +1426,10 @@ ExecIndexBuildScanKeys(PlanState *planstate, Relation index,
 			MemSet(this_scan_key, 0, sizeof(ScanKeyData));
 			this_scan_key->sk_flags = SK_ROW_HEADER;
 			this_scan_key->sk_attno = first_sub_key->sk_attno;
-			this_scan_key->sk_strategy = rc->cmptype;
+			this_scan_key->sk_strategy = IndexAmTranslateCompareType(rc->cmptype,
+																	 index->rd_rel->relam,
+																	 0,  // FIXME
+																	 false);
 			/* sk_subtype, sk_collation, sk_func not used in a header */
 			this_scan_key->sk_argument = PointerGetDatum(first_sub_key);
 		}
@@ -1463,7 +1471,9 @@ ExecIndexBuildScanKeys(PlanState *planstate, Relation index,
 			opfamily = index->rd_opfamily[varattno - 1];
 
 			get_op_opfamily_properties(opno, opfamily, isorderby,
+									   NULL,		/* don't need opmethod */
 									   &op_strategy,
+									   NULL,		/* don't need cmptype */
 									   &op_lefttype,
 									   &op_righttype);
 
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index f70239a2c9d..84f92fb4f8a 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -196,6 +196,7 @@ MJExamineQuals(List *mergeclauses,
 		bool		reversed = mergereversals[iClause];
 		bool		nulls_first = mergenullsfirst[iClause];
 		int			op_strategy;
+		CompareType op_cmptype;
 		Oid			op_lefttype;
 		Oid			op_righttype;
 		Oid			sortfunc;
@@ -217,10 +218,12 @@ MJExamineQuals(List *mergeclauses,
 
 		/* Extract the operator's declared left/right datatypes */
 		get_op_opfamily_properties(qual->opno, opfamily, false,
+								   NULL,		/* don't need opmethod */
 								   &op_strategy,
+								   &op_cmptype,
 								   &op_lefttype,
 								   &op_righttype);
-		if (op_strategy != BTEqualStrategyNumber)	/* should not happen */
+		if (op_cmptype != COMPARE_EQ)			/* should not happen */
 			elog(ERROR, "cannot merge using non-equality operator %u",
 				 qual->opno);
 
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index b5bc9b602e2..e6050b492cf 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -2299,16 +2299,15 @@ find_window_run_conditions(Query *subquery, RangeTblEntry *rte, Index rti,
 
 	runopexpr = NULL;
 	runoperator = InvalidOid;
-	opinfos = get_op_btree_interpretation(opexpr->opno);
+	opinfos = get_op_index_interpretation(opexpr->opno);
 
 	foreach(lc, opinfos)
 	{
-		OpBtreeInterpretation *opinfo = (OpBtreeInterpretation *) lfirst(lc);
-		int			strategy = opinfo->strategy;
+		OpIndexInterpretation *opinfo = (OpIndexInterpretation *) lfirst(lc);
+		CompareType cmptype = opinfo->cmptype;
 
 		/* handle < / <= */
-		if (strategy == BTLessStrategyNumber ||
-			strategy == BTLessEqualStrategyNumber)
+		if (cmptype == COMPARE_LT || cmptype == COMPARE_LE)
 		{
 			/*
 			 * < / <= is supported for monotonically increasing functions in
@@ -2325,8 +2324,7 @@ find_window_run_conditions(Query *subquery, RangeTblEntry *rte, Index rti,
 			break;
 		}
 		/* handle > / >= */
-		else if (strategy == BTGreaterStrategyNumber ||
-				 strategy == BTGreaterEqualStrategyNumber)
+		else if (cmptype == COMPARE_GT || cmptype == COMPARE_GE)
 		{
 			/*
 			 * > / >= is supported for monotonically decreasing functions in
@@ -2343,9 +2341,9 @@ find_window_run_conditions(Query *subquery, RangeTblEntry *rte, Index rti,
 			break;
 		}
 		/* handle = */
-		else if (strategy == BTEqualStrategyNumber)
+		else if (cmptype == COMPARE_EQ)
 		{
-			int16		newstrategy;
+			CompareType newcmptype;
 
 			/*
 			 * When both monotonically increasing and decreasing then the
@@ -2369,19 +2367,20 @@ find_window_run_conditions(Query *subquery, RangeTblEntry *rte, Index rti,
 			 * below the value in the equality condition.
 			 */
 			if (res->monotonic & MONOTONICFUNC_INCREASING)
-				newstrategy = wfunc_left ? BTLessEqualStrategyNumber : BTGreaterEqualStrategyNumber;
+				newcmptype = wfunc_left ? COMPARE_LE : COMPARE_GE;
 			else
-				newstrategy = wfunc_left ? BTGreaterEqualStrategyNumber : BTLessEqualStrategyNumber;
+				newcmptype = wfunc_left ? COMPARE_GE : COMPARE_LE;
 
 			/* We must keep the original equality qual */
 			*keep_original = true;
 			runopexpr = opexpr;
 
 			/* determine the operator to use for the WindowFuncRunCondition */
-			runoperator = get_opfamily_member(opinfo->opfamily_id,
+			runoperator = get_opmethod_member(opinfo->opmethod,
+											  opinfo->opfamily_id,
 											  opinfo->oplefttype,
 											  opinfo->oprighttype,
-											  newstrategy);
+											  newcmptype);
 			break;
 		}
 	}
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 73d78617009..b7f6ab40dec 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -3610,6 +3610,7 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 		if (opathkey->pk_opfamily != ipathkey->pk_opfamily ||
 			opathkey->pk_eclass->ec_collation != ipathkey->pk_eclass->ec_collation ||
 			opathkey->pk_strategy != ipathkey->pk_strategy ||
+			opathkey->pk_cmptype != ipathkey->pk_cmptype ||
 			opathkey->pk_nulls_first != ipathkey->pk_nulls_first)
 			elog(ERROR, "left and right pathkeys do not match in mergejoin");
 
@@ -4096,7 +4097,10 @@ cached_scansel(PlannerInfo *root, RestrictInfo *rinfo, PathKey *pathkey)
 			cache->collation == pathkey->pk_eclass->ec_collation &&
 			cache->strategy == pathkey->pk_strategy &&
 			cache->nulls_first == pathkey->pk_nulls_first)
+		{
+			Assert(cache->cmptype == pathkey->pk_cmptype);
 			return cache;
+		}
 	}
 
 	/* Nope, do the computation */
@@ -4104,6 +4108,7 @@ cached_scansel(PlannerInfo *root, RestrictInfo *rinfo, PathKey *pathkey)
 					 (Node *) rinfo->clause,
 					 pathkey->pk_opfamily,
 					 pathkey->pk_strategy,
+					 pathkey->pk_cmptype,
 					 pathkey->pk_nulls_first,
 					 &leftstartsel,
 					 &leftendsel,
@@ -4117,6 +4122,7 @@ cached_scansel(PlannerInfo *root, RestrictInfo *rinfo, PathKey *pathkey)
 	cache->opfamily = pathkey->pk_opfamily;
 	cache->collation = pathkey->pk_eclass->ec_collation;
 	cache->strategy = pathkey->pk_strategy;
+	cache->cmptype = pathkey->pk_cmptype;
 	cache->nulls_first = pathkey->pk_nulls_first;
 	cache->leftstartsel = leftstartsel;
 	cache->leftendsel = leftendsel;
diff --git a/src/backend/optimizer/path/equivclass.c b/src/backend/optimizer/path/equivclass.c
index 0f9ecf5ee8b..eb68413a321 100644
--- a/src/backend/optimizer/path/equivclass.c
+++ b/src/backend/optimizer/path/equivclass.c
@@ -1788,8 +1788,8 @@ select_equality_operator(EquivalenceClass *ec, Oid lefttype, Oid righttype)
 		Oid			opfamily = lfirst_oid(lc);
 		Oid			opno;
 
-		opno = get_opfamily_member(opfamily, lefttype, righttype,
-								   BTEqualStrategyNumber);
+		opno = get_opmethod_member(InvalidOid, opfamily, lefttype, righttype,
+								   COMPARE_EQ);
 		if (!OidIsValid(opno))
 			continue;
 		/* If no barrier quals in query, don't worry about leaky operators */
diff --git a/src/backend/optimizer/path/indxpath.c b/src/backend/optimizer/path/indxpath.c
index 9202d9a3697..e0971beb1d1 100644
--- a/src/backend/optimizer/path/indxpath.c
+++ b/src/backend/optimizer/path/indxpath.c
@@ -3526,6 +3526,8 @@ expand_indexqual_rowcompare(PlannerInfo *root,
 {
 	IndexClause *iclause = makeNode(IndexClause);
 	RowCompareExpr *clause = (RowCompareExpr *) rinfo->clause;
+	CompareType op_cmptype;
+	Oid			op_method;
 	int			op_strategy;
 	Oid			op_lefttype;
 	Oid			op_righttype;
@@ -3553,7 +3555,9 @@ expand_indexqual_rowcompare(PlannerInfo *root,
 	}
 
 	get_op_opfamily_properties(expr_op, index->opfamily[indexcol], false,
+							   &op_method,
 							   &op_strategy,
+							   &op_cmptype,
 							   &op_lefttype,
 							   &op_righttype);
 
@@ -3614,7 +3618,9 @@ expand_indexqual_rowcompare(PlannerInfo *root,
 
 		/* Add operator info to lists */
 		get_op_opfamily_properties(expr_op, index->opfamily[i], false,
+								   &op_method,
 								   &op_strategy,
+								   &op_cmptype,
 								   &op_lefttype,
 								   &op_righttype);
 		expr_ops = lappend_oid(expr_ops, expr_op);
@@ -3647,8 +3653,7 @@ expand_indexqual_rowcompare(PlannerInfo *root,
 			/* very easy, just use the commuted operators */
 			new_ops = expr_ops;
 		}
-		else if (op_strategy == BTLessEqualStrategyNumber ||
-				 op_strategy == BTGreaterEqualStrategyNumber)
+		else if (op_cmptype == COMPARE_LE || op_cmptype == COMPARE_GE)
 		{
 			/* easy, just use the same (possibly commuted) operators */
 			new_ops = list_truncate(expr_ops, matching_cols);
@@ -3659,10 +3664,16 @@ expand_indexqual_rowcompare(PlannerInfo *root,
 			ListCell   *lefttypes_cell;
 			ListCell   *righttypes_cell;
 
-			if (op_strategy == BTLessStrategyNumber)
-				op_strategy = BTLessEqualStrategyNumber;
-			else if (op_strategy == BTGreaterStrategyNumber)
-				op_strategy = BTGreaterEqualStrategyNumber;
+			if (op_cmptype == COMPARE_LT)
+			{
+				op_cmptype = COMPARE_LE;
+				op_strategy = IndexAmTranslateCompareType(op_cmptype, op_method, 0, true);  // FIXME
+			}
+			else if (op_cmptype == COMPARE_GT)
+			{
+				op_cmptype = COMPARE_GE;
+				op_strategy = IndexAmTranslateCompareType(op_cmptype, op_method, 0, true);  // FIXME
+			}
 			else
 				elog(ERROR, "unexpected strategy number %d", op_strategy);
 			new_ops = NIL;
@@ -3760,8 +3771,7 @@ match_pathkeys_to_index(IndexOptInfo *index, List *pathkeys,
 
 
 		/* Pathkey must request default sort order for the target opfamily */
-		if (pathkey->pk_strategy != BTLessStrategyNumber ||
-			pathkey->pk_nulls_first)
+		if (pathkey->pk_cmptype != COMPARE_LT || pathkey->pk_nulls_first)
 			return;
 
 		/* If eclass is volatile, no hope of using an indexscan */
diff --git a/src/backend/optimizer/path/pathkeys.c b/src/backend/optimizer/path/pathkeys.c
index 154eb505d75..a0826ed5008 100644
--- a/src/backend/optimizer/path/pathkeys.c
+++ b/src/backend/optimizer/path/pathkeys.c
@@ -17,7 +17,9 @@
  */
 #include "postgres.h"
 
+#include "access/amapi.h"
 #include "access/stratnum.h"
+#include "catalog/pg_am.h"			/* for BTREE_AM_OID */
 #include "catalog/pg_opfamily.h"
 #include "nodes/nodeFuncs.h"
 #include "optimizer/cost.h"
@@ -53,18 +55,40 @@ static bool right_merge_direction(PlannerInfo *root, PathKey *pathkey);
  * merging EquivalenceClasses.
  */
 PathKey *
-make_canonical_pathkey(PlannerInfo *root,
-					   EquivalenceClass *eclass, Oid opfamily,
-					   int strategy, bool nulls_first)
+make_canonical_pathkey(PlannerInfo *root, EquivalenceClass *eclass,
+					   Oid opmethod, Oid opfamily, int strategy,
+					   CompareType cmptype, bool nulls_first)
 {
 	PathKey    *pk;
 	ListCell   *lc;
 	MemoryContext oldcontext;
 
+	Assert(OidIsValid(opmethod));
+	Assert(OidIsValid(opfamily));
+	Assert(opmethod == get_opfamily_method(opfamily));
+
 	/* Can't make canonical pathkeys if the set of ECs might still change */
 	if (!root->ec_merging_done)
 		elog(ERROR, "too soon to build canonical pathkeys");
 
+	/*
+	 * Can't make canonical pathkeys if neither the strategy nor the cmptype
+	 * were supplied.  For callers who only gave us one of them, help out
+	 * by looking up the other.  This simplifies the work the caller needs
+	 * to do and reduces code duplication.
+	 */
+	if (strategy == InvalidStrategy)
+	{
+		Assert(cmptype != COMPARE_INVALID);
+		Assert(OidIsValid(opfamily));
+		strategy = IndexAmTranslateCompareType(cmptype, opmethod, opfamily, false);
+	}
+	else if (cmptype == COMPARE_INVALID)
+	{
+		Assert(OidIsValid(opfamily));
+		cmptype = IndexAmTranslateStrategy(strategy, opmethod, opfamily, false);
+	}
+
 	/* The passed eclass might be non-canonical, so chase up to the top */
 	while (eclass->ec_merged)
 		eclass = eclass->ec_merged;
@@ -75,6 +99,7 @@ make_canonical_pathkey(PlannerInfo *root,
 		if (eclass == pk->pk_eclass &&
 			opfamily == pk->pk_opfamily &&
 			strategy == pk->pk_strategy &&
+			cmptype == pk->pk_cmptype &&
 			nulls_first == pk->pk_nulls_first)
 			return pk;
 	}
@@ -89,6 +114,7 @@ make_canonical_pathkey(PlannerInfo *root,
 	pk->pk_eclass = eclass;
 	pk->pk_opfamily = opfamily;
 	pk->pk_strategy = strategy;
+	pk->pk_cmptype = cmptype;
 	pk->pk_nulls_first = nulls_first;
 
 	root->canon_pathkeys = lappend(root->canon_pathkeys, pk);
@@ -197,6 +223,7 @@ pathkey_is_redundant(PathKey *new_pathkey, List *pathkeys)
 static PathKey *
 make_pathkey_from_sortinfo(PlannerInfo *root,
 						   Expr *expr,
+						   Oid opmethod,
 						   Oid opfamily,
 						   Oid opcintype,
 						   Oid collation,
@@ -206,12 +233,18 @@ make_pathkey_from_sortinfo(PlannerInfo *root,
 						   Relids rel,
 						   bool create_it)
 {
+	CompareType cmptype;
 	int16		strategy;
 	Oid			equality_op;
 	List	   *opfamilies;
 	EquivalenceClass *eclass;
 
-	strategy = reverse_sort ? BTGreaterStrategyNumber : BTLessStrategyNumber;
+	Assert(OidIsValid(opmethod));
+	Assert(OidIsValid(opfamily));
+	Assert(opmethod == get_opfamily_method(opfamily));
+
+	cmptype = reverse_sort ? COMPARE_GT : COMPARE_LT;
+	strategy = IndexAmTranslateCompareType(cmptype, opmethod, opfamily, false);
 
 	/*
 	 * EquivalenceClasses need to contain opfamily lists based on the family
@@ -219,13 +252,14 @@ make_pathkey_from_sortinfo(PlannerInfo *root,
 	 * more than one opfamily.  So we have to look up the opfamily's equality
 	 * operator and get its membership.
 	 */
-	equality_op = get_opfamily_member(opfamily,
+	equality_op = get_opmethod_member(opmethod,
+									  opfamily,
 									  opcintype,
 									  opcintype,
-									  BTEqualStrategyNumber);
+									  COMPARE_EQ);
 	if (!OidIsValid(equality_op))	/* shouldn't happen */
 		elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
-			 BTEqualStrategyNumber, opcintype, opcintype, opfamily);
+			 COMPARE_EQ, opcintype, opcintype, opfamily);
 	opfamilies = get_mergejoin_opfamilies(equality_op);
 	if (!opfamilies)			/* certainly should find some */
 		elog(ERROR, "could not find opfamilies for equality operator %u",
@@ -241,8 +275,8 @@ make_pathkey_from_sortinfo(PlannerInfo *root,
 		return NULL;
 
 	/* And finally we can find or create a PathKey node */
-	return make_canonical_pathkey(root, eclass, opfamily,
-								  strategy, nulls_first);
+	return make_canonical_pathkey(root, eclass, opmethod, opfamily,
+								  strategy, cmptype, nulls_first);
 }
 
 /*
@@ -255,20 +289,21 @@ make_pathkey_from_sortinfo(PlannerInfo *root,
 static PathKey *
 make_pathkey_from_sortop(PlannerInfo *root,
 						 Expr *expr,
+						 Oid opmethodfilter,
 						 Oid ordering_op,
 						 bool reverse_sort,
 						 bool nulls_first,
 						 Index sortref,
 						 bool create_it)
 {
-	Oid			opfamily,
+	Oid			opmethod,
+				opfamily,
 				opcintype,
 				collation;
-	int16		strategy;
 
 	/* Find the operator in pg_amop --- failure shouldn't happen */
-	if (!get_ordering_op_properties(ordering_op,
-									&opfamily, &opcintype, &strategy))
+	if (!get_ordering_op_properties(ordering_op, opmethodfilter, &opmethod,
+									&opfamily, &opcintype, NULL, NULL))
 		elog(ERROR, "operator %u is not a valid ordering operator",
 			 ordering_op);
 
@@ -277,6 +312,7 @@ make_pathkey_from_sortop(PlannerInfo *root,
 
 	return make_pathkey_from_sortinfo(root,
 									  expr,
+									  opmethod,
 									  opfamily,
 									  opcintype,
 									  collation,
@@ -783,6 +819,7 @@ build_index_pathkeys(PlannerInfo *root,
 		 */
 		cpathkey = make_pathkey_from_sortinfo(root,
 											  indexkey,
+											  index->relam,
 											  index->sortopfamily[i],
 											  index->opcintype[i],
 											  index->indexcollations[i],
@@ -942,6 +979,7 @@ build_partition_pathkeys(PlannerInfo *root, RelOptInfo *partrel,
 		 */
 		cpathkey = make_pathkey_from_sortinfo(root,
 											  keyCol,
+											  get_opfamily_method(partscheme->partopfamily[i]),		/* TODO: OPMETHOD */
 											  partscheme->partopfamily[i],
 											  partscheme->partopcintype[i],
 											  partscheme->partcollation[i],
@@ -1004,24 +1042,26 @@ build_expression_pathkey(PlannerInfo *root,
 						 bool create_it)
 {
 	List	   *pathkeys;
-	Oid			opfamily,
+	Oid			opmethod,
+				opfamily,
 				opcintype;
-	int16		strategy;
+	CompareType cmptype;
 	PathKey    *cpathkey;
 
 	/* Find the operator in pg_amop --- failure shouldn't happen */
-	if (!get_ordering_op_properties(opno,
-									&opfamily, &opcintype, &strategy))
+	if (!get_ordering_op_properties(opno, BTREE_AM_OID, &opmethod, &opfamily,
+									&opcintype, NULL, &cmptype))
 		elog(ERROR, "operator %u is not a valid ordering operator",
 			 opno);
 
 	cpathkey = make_pathkey_from_sortinfo(root,
 										  expr,
+										  opmethod,
 										  opfamily,
 										  opcintype,
 										  exprCollation((Node *) expr),
-										  (strategy == BTGreaterStrategyNumber),
-										  (strategy == BTGreaterStrategyNumber),
+										  (cmptype == COMPARE_GT),
+										  (cmptype == COMPARE_GT),
 										  0,
 										  rel,
 										  create_it);
@@ -1117,8 +1157,10 @@ convert_subquery_pathkeys(PlannerInfo *root, RelOptInfo *rel,
 					best_pathkey =
 						make_canonical_pathkey(root,
 											   outer_ec,
+											   get_opfamily_method(sub_pathkey->pk_opfamily),
 											   sub_pathkey->pk_opfamily,
 											   sub_pathkey->pk_strategy,
+											   sub_pathkey->pk_cmptype,
 											   sub_pathkey->pk_nulls_first);
 			}
 		}
@@ -1199,8 +1241,10 @@ convert_subquery_pathkeys(PlannerInfo *root, RelOptInfo *rel,
 
 					outer_pk = make_canonical_pathkey(root,
 													  outer_ec,
+													  get_opfamily_method(sub_pathkey->pk_opfamily),
 													  sub_pathkey->pk_opfamily,
 													  sub_pathkey->pk_strategy,
+													  sub_pathkey->pk_cmptype,
 													  sub_pathkey->pk_nulls_first);
 					/* score = # of equivalence peers */
 					score = list_length(outer_ec->ec_members) - 1;
@@ -1333,6 +1377,7 @@ build_join_pathkeys(PlannerInfo *root,
  */
 List *
 make_pathkeys_for_sortclauses(PlannerInfo *root,
+							  Oid opmethodfilter,
 							  List *sortclauses,
 							  List *tlist)
 {
@@ -1340,6 +1385,7 @@ make_pathkeys_for_sortclauses(PlannerInfo *root,
 	bool		sortable;
 
 	result = make_pathkeys_for_sortclauses_extended(root,
+													opmethodfilter,
 													&sortclauses,
 													tlist,
 													false,
@@ -1378,6 +1424,7 @@ make_pathkeys_for_sortclauses(PlannerInfo *root,
  */
 List *
 make_pathkeys_for_sortclauses_extended(PlannerInfo *root,
+									   Oid opmethodfilter,
 									   List **sortclauses,
 									   List *tlist,
 									   bool remove_redundant,
@@ -1411,6 +1458,7 @@ make_pathkeys_for_sortclauses_extended(PlannerInfo *root,
 		}
 		pathkey = make_pathkey_from_sortop(root,
 										   sortkey,
+										   opmethodfilter,
 										   sortcl->sortop,
 										   sortcl->reverse_sort,
 										   sortcl->nulls_first,
@@ -1815,8 +1863,10 @@ select_outer_pathkeys_for_merge(PlannerInfo *root,
 		scores[best_j] = -1;
 		pathkey = make_canonical_pathkey(root,
 										 ec,
+										 get_opfamily_method(linitial_oid(ec->ec_opfamilies)),
 										 linitial_oid(ec->ec_opfamilies),
-										 BTLessStrategyNumber,
+										 InvalidStrategy,
+										 COMPARE_LT,
 										 false);
 		/* can't be redundant because no duplicate ECs */
 		Assert(!pathkey_is_redundant(pathkey, pathkeys));
@@ -1908,8 +1958,10 @@ make_inner_pathkeys_for_merge(PlannerInfo *root,
 		else
 			pathkey = make_canonical_pathkey(root,
 											 ieclass,
+											 get_opfamily_method(opathkey->pk_opfamily),
 											 opathkey->pk_opfamily,
 											 opathkey->pk_strategy,
+											 opathkey->pk_cmptype,
 											 opathkey->pk_nulls_first);
 
 		/*
@@ -2134,12 +2186,13 @@ right_merge_direction(PlannerInfo *root, PathKey *pathkey)
 			 * want to prefer only one of the two possible directions, and we
 			 * might as well use this one.
 			 */
-			return (pathkey->pk_strategy == query_pathkey->pk_strategy);
+			return (pathkey->pk_strategy == query_pathkey->pk_strategy &&
+					pathkey->pk_cmptype == query_pathkey->pk_cmptype);
 		}
 	}
 
 	/* If no matching ORDER BY request, prefer the ASC direction */
-	return (pathkey->pk_strategy == BTLessStrategyNumber);
+	return (pathkey->pk_cmptype == COMPARE_LT);
 }
 
 /*
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 816a2b2a576..6a7a8d68db5 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -19,6 +19,7 @@
 #include <math.h>
 
 #include "access/sysattr.h"
+#include "catalog/pg_am.h"			/* for BTREE_AM_OID */
 #include "catalog/pg_class.h"
 #include "foreign/fdwapi.h"
 #include "miscadmin.h"
@@ -1883,7 +1884,7 @@ create_unique_plan(PlannerInfo *root, UniquePath *best_path, int flags)
 			 * cross-type operators then the equality operators are the ones
 			 * for the IN clause operators' RHS datatype.
 			 */
-			eqop = get_equality_op_for_ordering_op(sortop, NULL);
+			eqop = get_equality_op_for_ordering_op(sortop, BTREE_AM_OID, NULL);
 			if (!OidIsValid(eqop))	/* shouldn't happen */
 				elog(ERROR, "could not find equality operator for ordering operator %u",
 					 sortop);
@@ -4751,13 +4752,14 @@ create_mergejoin_plan(PlannerInfo *root,
 			elog(ERROR, "left and right pathkeys do not match in mergejoin");
 		if (first_inner_match &&
 			(opathkey->pk_strategy != ipathkey->pk_strategy ||
+			 opathkey->pk_cmptype != ipathkey->pk_cmptype ||
 			 opathkey->pk_nulls_first != ipathkey->pk_nulls_first))
 			elog(ERROR, "left and right pathkeys do not match in mergejoin");
 
 		/* OK, save info for executor */
 		mergefamilies[i] = opathkey->pk_opfamily;
 		mergecollations[i] = opathkey->pk_eclass->ec_collation;
-		mergereversals[i] = (opathkey->pk_strategy == BTGreaterStrategyNumber ? true : false);
+		mergereversals[i] = opathkey->pk_cmptype == COMPARE_GT ? true : false;
 		mergenullsfirst[i] = opathkey->pk_nulls_first;
 		i++;
 	}
@@ -6906,13 +6908,14 @@ make_unique_from_pathkeys(Plan *lefttree, List *pathkeys, int numCols)
 		 * Look up the correct equality operator from the PathKey's slightly
 		 * abstracted representation.
 		 */
-		eqop = get_opfamily_member(pathkey->pk_opfamily,
+		eqop = get_opmethod_member(InvalidOid,
+								   pathkey->pk_opfamily,
 								   pk_datatype,
 								   pk_datatype,
-								   BTEqualStrategyNumber);
+								   COMPARE_EQ);
 		if (!OidIsValid(eqop))	/* should not happen */
 			elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
-				 BTEqualStrategyNumber, pk_datatype, pk_datatype,
+				 COMPARE_EQ, pk_datatype, pk_datatype,
 				 pathkey->pk_opfamily);
 
 		uniqColIdx[keyno] = tle->resno;
diff --git a/src/backend/optimizer/plan/planagg.c b/src/backend/optimizer/plan/planagg.c
index 64605be3178..d98f2ef91fe 100644
--- a/src/backend/optimizer/plan/planagg.c
+++ b/src/backend/optimizer/plan/planagg.c
@@ -30,6 +30,7 @@
 
 #include "access/htup_details.h"
 #include "catalog/pg_aggregate.h"
+#include "catalog/pg_am.h"			/* for BTREE_AM_OID */
 #include "catalog/pg_type.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
@@ -160,7 +161,7 @@ preprocess_minmax_aggregates(PlannerInfo *root)
 		 * We'll need the equality operator that goes with the aggregate's
 		 * ordering operator.
 		 */
-		eqop = get_equality_op_for_ordering_op(mminfo->aggsortop, &reverse);
+		eqop = get_equality_op_for_ordering_op(mminfo->aggsortop, BTREE_AM_OID, &reverse);
 		if (!OidIsValid(eqop))	/* shouldn't happen */
 			elog(ERROR, "could not find equality operator for ordering operator %u",
 				 mminfo->aggsortop);
@@ -485,6 +486,7 @@ minmax_qp_callback(PlannerInfo *root, void *extra)
 
 	root->sort_pathkeys =
 		make_pathkeys_for_sortclauses(root,
+									  BTREE_AM_OID,
 									  root->parse->sortClause,
 									  root->parse->targetList);
 
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 36ee6dd43de..9527ab83a8d 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -23,6 +23,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_aggregate.h"
+#include "catalog/pg_am.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_type.h"
@@ -1459,6 +1460,7 @@ grouping_planner(PlannerInfo *root, double tuple_fraction,
 		 */
 		Assert(parse->distinctClause == NIL);
 		root->sort_pathkeys = make_pathkeys_for_sortclauses(root,
+															BTREE_AM_OID,
 															parse->sortClause,
 															root->processed_tlist);
 	}
@@ -3232,7 +3234,9 @@ adjust_group_pathkeys_for_groupagg(PlannerInfo *root)
 			else
 				sortlist = aggref->aggorder;
 
-			pathkeys = make_pathkeys_for_sortclauses(root, sortlist,
+			pathkeys = make_pathkeys_for_sortclauses(root,
+													 BTREE_AM_OID,
+													 sortlist,
 													 aggref->args);
 
 			/*
@@ -3375,6 +3379,7 @@ standard_qp_callback(PlannerInfo *root, void *extra)
 			 */
 			root->group_pathkeys =
 				make_pathkeys_for_sortclauses_extended(root,
+													   BTREE_AM_OID,
 													   &groupClause,
 													   tlist,
 													   false,
@@ -3408,6 +3413,7 @@ standard_qp_callback(PlannerInfo *root, void *extra)
 		 */
 		root->group_pathkeys =
 			make_pathkeys_for_sortclauses_extended(root,
+												   BTREE_AM_OID,
 												   &root->processed_groupClause,
 												   tlist,
 												   true,
@@ -3460,6 +3466,7 @@ standard_qp_callback(PlannerInfo *root, void *extra)
 		root->processed_distinctClause = list_copy(parse->distinctClause);
 		root->distinct_pathkeys =
 			make_pathkeys_for_sortclauses_extended(root,
+												   BTREE_AM_OID,
 												   &root->processed_distinctClause,
 												   tlist,
 												   true,
@@ -3474,6 +3481,7 @@ standard_qp_callback(PlannerInfo *root, void *extra)
 
 	root->sort_pathkeys =
 		make_pathkeys_for_sortclauses(root,
+									  BTREE_AM_OID,
 									  parse->sortClause,
 									  tlist);
 
@@ -3487,6 +3495,7 @@ standard_qp_callback(PlannerInfo *root, void *extra)
 
 		root->setop_pathkeys =
 			make_pathkeys_for_sortclauses_extended(root,
+												   BTREE_AM_OID,
 												   &groupClauses,
 												   tlist,
 												   false,
@@ -6146,6 +6155,7 @@ make_pathkeys_for_window(PlannerInfo *root, WindowClause *wc,
 		bool		sortable;
 
 		window_pathkeys = make_pathkeys_for_sortclauses_extended(root,
+																 BTREE_AM_OID,
 																 &wc->partitionClause,
 																 tlist,
 																 true,
@@ -6168,6 +6178,7 @@ make_pathkeys_for_window(PlannerInfo *root, WindowClause *wc,
 		List	   *orderby_pathkeys;
 
 		orderby_pathkeys = make_pathkeys_for_sortclauses(root,
+														 BTREE_AM_OID,
 														 wc->orderClause,
 														 tlist);
 
diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c
index eab44da65b8..e6d8b7c0a78 100644
--- a/src/backend/optimizer/prep/prepunion.c
+++ b/src/backend/optimizer/prep/prepunion.c
@@ -24,6 +24,7 @@
 #include "postgres.h"
 
 #include "access/htup_details.h"
+#include "catalog/pg_am.h"
 #include "catalog/pg_type.h"
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
@@ -730,7 +731,9 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root,
 		{
 			try_sorted = true;
 			/* Determine the pathkeys for sorting by the whole target list */
-			union_pathkeys = make_pathkeys_for_sortclauses(root, groupList,
+			union_pathkeys = make_pathkeys_for_sortclauses(root,
+														   BTREE_AM_OID,
+														   groupList,
 														   tlist);
 
 			root->query_pathkeys = union_pathkeys;
@@ -926,7 +929,10 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root,
 			/* Try Sort -> Unique on the Append path */
 			if (groupList != NIL)
 				path = (Path *) create_sort_path(root, result_rel, path,
-												 make_pathkeys_for_sortclauses(root, groupList, tlist),
+												 make_pathkeys_for_sortclauses(root,
+																			   BTREE_AM_OID,
+																			   groupList,
+																			   tlist),
 												 -1.0);
 
 			path = (Path *) create_upper_unique_path(root,
@@ -943,7 +949,10 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root,
 				path = gpath;
 
 				path = (Path *) create_sort_path(root, result_rel, path,
-												 make_pathkeys_for_sortclauses(root, groupList, tlist),
+												 make_pathkeys_for_sortclauses(root,
+																			   BTREE_AM_OID,
+																			   groupList,
+																			   tlist),
 												 -1.0);
 
 				path = (Path *) create_upper_unique_path(root,
@@ -1075,7 +1084,7 @@ generate_nonunion_paths(SetOperationStmt *op, PlannerInfo *root,
 	if (can_sort)
 	{
 		/* Determine the pathkeys for sorting by the whole target list */
-		nonunion_pathkeys = make_pathkeys_for_sortclauses(root, groupList,
+		nonunion_pathkeys = make_pathkeys_for_sortclauses(root, BTREE_AM_OID, groupList,
 														  tlist);
 
 		root->query_pathkeys = nonunion_pathkeys;
@@ -1196,6 +1205,7 @@ generate_nonunion_paths(SetOperationStmt *op, PlannerInfo *root,
 
 		/* First the left input ... */
 		pathkeys = make_pathkeys_for_sortclauses(root,
+												 BTREE_AM_OID,
 												 groupList,
 												 lpath_tlist);
 		if (pathkeys_contained_in(pathkeys, lpath->pathkeys))
@@ -1218,6 +1228,7 @@ generate_nonunion_paths(SetOperationStmt *op, PlannerInfo *root,
 
 		/* and now the same for the right. */
 		pathkeys = make_pathkeys_for_sortclauses(root,
+												 BTREE_AM_OID,
 												 groupList,
 												 rpath_tlist);
 		if (pathkeys_contained_in(pathkeys, rpath->pathkeys))
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index 71abb01f655..a4b0d3465e8 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -382,27 +382,36 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent,
 					{
 						int16		opt = indexRelation->rd_indoption[i];
 						Oid			ltopr;
-						Oid			btopfamily;
-						Oid			btopcintype;
-						int16		btstrategy;
+						Oid			opmethod;
+						Oid			opfamily;
+						Oid			opcintype;
+						CompareType cmptype;
 
 						info->reverse_sort[i] = (opt & INDOPTION_DESC) != 0;
 						info->nulls_first[i] = (opt & INDOPTION_NULLS_FIRST) != 0;
 
-						ltopr = get_opfamily_member(info->opfamily[i],
+						ltopr = get_opmethod_member(info->relam,
+													info->opfamily[i],
 													info->opcintype[i],
 													info->opcintype[i],
-													BTLessStrategyNumber);
+													COMPARE_LT);
 						if (OidIsValid(ltopr) &&
 							get_ordering_op_properties(ltopr,
-													   &btopfamily,
-													   &btopcintype,
-													   &btstrategy) &&
-							btopcintype == info->opcintype[i] &&
-							btstrategy == BTLessStrategyNumber)
+													   info->relam,
+													   &opmethod,
+													   &opfamily,
+													   &opcintype,
+													   NULL,
+													   &cmptype) &&
+							opcintype == info->opcintype[i] &&
+							cmptype == COMPARE_LT)
 						{
 							/* Successful mapping */
-							info->sortopfamily[i] = btopfamily;
+							Assert(info->relam == opmethod);
+							Assert(info->relam == get_opfamily_method(opfamily));
+							info->sortopfamily[i] = opfamily;
+
+							/* TODO: OPMETHOD: store opmethod here? */
 						}
 						else
 						{
diff --git a/src/backend/optimizer/util/predtest.c b/src/backend/optimizer/util/predtest.c
index b76fc81b08d..1aa07ffde7e 100644
--- a/src/backend/optimizer/util/predtest.c
+++ b/src/backend/optimizer/util/predtest.c
@@ -1613,7 +1613,7 @@ clause_is_strict_for(Node *clause, Node *subexpr, bool allow_false)
  * and in addition we use 6 to represent <>.  <> is not a btree-indexable
  * operator, but we assume here that if an equality operator of a btree
  * opfamily has a negator operator, the negator behaves as <> for the opfamily.
- * (This convention is also known to get_op_btree_interpretation().)
+ * (This convention is also known to get_op_index_interpretation().)
  *
  * BT_implies_table[] and BT_refutes_table[] are used for cases where we have
  * two identical subexpressions and we want to know whether one operator
@@ -2165,20 +2165,20 @@ lookup_proof_cache(Oid pred_op, Oid clause_op, bool refute_it)
 	 * operator.  This can happen in cases with incomplete sets of cross-type
 	 * comparison operators.
 	 */
-	clause_op_infos = get_op_btree_interpretation(clause_op);
+	clause_op_infos = get_op_index_interpretation(clause_op);
 	if (clause_op_infos)
-		pred_op_infos = get_op_btree_interpretation(pred_op);
+		pred_op_infos = get_op_index_interpretation(pred_op);
 	else						/* no point in looking */
 		pred_op_infos = NIL;
 
 	foreach(lcp, pred_op_infos)
 	{
-		OpBtreeInterpretation *pred_op_info = lfirst(lcp);
+		OpIndexInterpretation *pred_op_info = lfirst(lcp);
 		Oid			opfamily_id = pred_op_info->opfamily_id;
 
 		foreach(lcc, clause_op_infos)
 		{
-			OpBtreeInterpretation *clause_op_info = lfirst(lcc);
+			OpIndexInterpretation *clause_op_info = lfirst(lcc);
 			StrategyNumber pred_strategy,
 						clause_strategy,
 						test_strategy;
@@ -2187,7 +2187,8 @@ lookup_proof_cache(Oid pred_op, Oid clause_op, bool refute_it)
 			if (opfamily_id != clause_op_info->opfamily_id)
 				continue;
 			/* Lefttypes should match */
-			Assert(clause_op_info->oplefttype == pred_op_info->oplefttype);
+			if (clause_op_info->oplefttype != pred_op_info->oplefttype)
+				continue;
 
 			pred_strategy = pred_op_info->strategy;
 			clause_strategy = clause_op_info->strategy;
@@ -2221,10 +2222,11 @@ lookup_proof_cache(Oid pred_op, Oid clause_op, bool refute_it)
 			 */
 			if (test_strategy == BTNE)
 			{
-				test_op = get_opfamily_member(opfamily_id,
+				test_op = get_opmethod_member(InvalidOid,
+											  opfamily_id,
 											  pred_op_info->oprighttype,
 											  clause_op_info->oprighttype,
-											  BTEqualStrategyNumber);
+											  COMPARE_EQ);
 				if (OidIsValid(test_op))
 					test_op = get_negator(test_op);
 			}
diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c
index 2e64fcae7b2..7fee19dd1b6 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -2915,7 +2915,6 @@ transformWindowDefinitions(ParseState *pstate,
 		{
 			SortGroupClause *sortcl;
 			Node	   *sortkey;
-			int16		rangestrategy;
 
 			if (list_length(wc->orderClause) != 1)
 				ereport(ERROR,
@@ -2926,9 +2925,12 @@ transformWindowDefinitions(ParseState *pstate,
 			sortkey = get_sortgroupclause_expr(sortcl, *targetlist);
 			/* Find the sort operator in pg_amop */
 			if (!get_ordering_op_properties(sortcl->sortop,
+											BTREE_AM_OID,
+											NULL,
 											&rangeopfamily,
 											&rangeopcintype,
-											&rangestrategy))
+											NULL,
+											NULL))
 				elog(ERROR, "operator %u is not a valid ordering operator",
 					 sortcl->sortop);
 			/* Record properties of sort ordering */
@@ -3455,7 +3457,7 @@ addTargetToSortList(ParseState *pstate, TargetEntry *tle,
 			 * equality operator, and determine whether to consider it like
 			 * ASC or DESC.
 			 */
-			eqop = get_equality_op_for_ordering_op(sortop, &reverse);
+			eqop = get_equality_op_for_ordering_op(sortop, BTREE_AM_OID, &reverse);
 			if (!OidIsValid(eqop))
 				ereport(ERROR,
 						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index bad1df732ea..2f04db30efb 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -2902,7 +2902,7 @@ make_row_comparison_op(ParseState *pstate, List *opname,
 		Bitmapset  *this_strats;
 		ListCell   *j;
 
-		opinfo_lists[i] = get_op_btree_interpretation(opno);
+		opinfo_lists[i] = get_op_index_interpretation(opno);
 
 		/*
 		 * convert strategy numbers into a Bitmapset to make the intersection
@@ -2911,7 +2911,7 @@ make_row_comparison_op(ParseState *pstate, List *opname,
 		this_strats = NULL;
 		foreach(j, opinfo_lists[i])
 		{
-			OpBtreeInterpretation *opinfo = lfirst(j);
+			OpIndexInterpretation *opinfo = lfirst(j);
 
 			this_strats = bms_add_member(this_strats, opinfo->strategy);
 		}
@@ -2961,7 +2961,7 @@ make_row_comparison_op(ParseState *pstate, List *opname,
 
 		foreach(j, opinfo_lists[i])
 		{
-			OpBtreeInterpretation *opinfo = lfirst(j);
+			OpIndexInterpretation *opinfo = lfirst(j);
 
 			if (opinfo->strategy == cmptype)
 			{
diff --git a/src/backend/partitioning/partprune.c b/src/backend/partitioning/partprune.c
index 48a35f763e9..f55648efb78 100644
--- a/src/backend/partitioning/partprune.c
+++ b/src/backend/partitioning/partprune.c
@@ -1454,7 +1454,9 @@ gen_prune_steps_from_opexps(GeneratePruningStepsContext *context,
 				get_op_opfamily_properties(pc->opno,
 										   part_scheme->partopfamily[i],
 										   false,
+										   NULL,		/* don't need opmethod */
 										   &pc->op_strategy,
+										   NULL,		/* don't need cmptype */
 										   &lefttype,
 										   &righttype);
 
@@ -1982,7 +1984,10 @@ match_clause_to_partition_key(GeneratePruningStepsContext *context,
 		if (op_in_opfamily(opno, partopfamily))
 		{
 			get_op_opfamily_properties(opno, partopfamily, false,
-									   &op_strategy, &op_lefttype,
+									   NULL,		/* don't need opmethod */
+									   &op_strategy,
+									   NULL,		/* don't need cmptype */
+									   &op_lefttype,
 									   &op_righttype);
 		}
 		else
@@ -1996,7 +2001,10 @@ match_clause_to_partition_key(GeneratePruningStepsContext *context,
 			if (OidIsValid(negator) && op_in_opfamily(negator, partopfamily))
 			{
 				get_op_opfamily_properties(negator, partopfamily, false,
-										   &op_strategy, &op_lefttype,
+										   NULL,		/* don't need opmethod */
+										   &op_strategy,
+										   NULL,		/* don't need cmptype */
+										   &op_lefttype,
 										   &op_righttype);
 				if (op_strategy == BTEqualStrategyNumber)
 					is_opne_listp = true;	/* bingo */
@@ -2211,7 +2219,10 @@ match_clause_to_partition_key(GeneratePruningStepsContext *context,
 							righttype;
 
 				get_op_opfamily_properties(negator, partopfamily,
-										   false, &strategy,
+										   false,
+										   NULL,		/* don't need opmethod */
+										   &strategy,
+										   NULL,		/* don't need cmptype */
 										   &lefttype, &righttype);
 				if (strategy != BTEqualStrategyNumber)
 					return PARTCLAUSE_NOMATCH;
diff --git a/src/backend/utils/adt/network.c b/src/backend/utils/adt/network.c
index 450dacd031c..eedc72e7b1e 100644
--- a/src/backend/utils/adt/network.c
+++ b/src/backend/utils/adt/network.c
@@ -1114,15 +1114,15 @@ match_network_subset(Node *leftop,
 	 */
 	if (is_eq)
 	{
-		opr1oid = get_opfamily_member(opfamily, datatype, datatype,
-									  BTGreaterEqualStrategyNumber);
+		opr1oid = get_opmethod_member(InvalidOid, opfamily, datatype, datatype,
+									  COMPARE_GE);
 		if (opr1oid == InvalidOid)
 			elog(ERROR, "no >= operator for opfamily %u", opfamily);
 	}
 	else
 	{
-		opr1oid = get_opfamily_member(opfamily, datatype, datatype,
-									  BTGreaterStrategyNumber);
+		opr1oid = get_opmethod_member(InvalidOid, opfamily, datatype, datatype,
+									  COMPARE_GT);
 		if (opr1oid == InvalidOid)
 			elog(ERROR, "no > operator for opfamily %u", opfamily);
 	}
@@ -1140,8 +1140,8 @@ match_network_subset(Node *leftop,
 
 	/* create clause "key <= network_scan_last( rightopval )" */
 
-	opr2oid = get_opfamily_member(opfamily, datatype, datatype,
-								  BTLessEqualStrategyNumber);
+	opr2oid = get_opmethod_member(InvalidOid, opfamily, datatype, datatype,
+								  COMPARE_LE);
 	if (opr2oid == InvalidOid)
 		elog(ERROR, "no <= operator for opfamily %u", opfamily);
 
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index c2918c9c831..7a832c3767e 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -2953,8 +2953,8 @@ scalargejoinsel(PG_FUNCTION_ARGS)
  *		*rightstart, *rightend similarly for the right-hand variable.
  */
 void
-mergejoinscansel(PlannerInfo *root, Node *clause,
-				 Oid opfamily, int strategy, bool nulls_first,
+mergejoinscansel(PlannerInfo *root, Node *clause, Oid opfamily, int strategy,
+				 CompareType cmptype, bool nulls_first,
 				 Selectivity *leftstart, Selectivity *leftend,
 				 Selectivity *rightstart, Selectivity *rightend)
 {
@@ -3003,7 +3003,9 @@ mergejoinscansel(PlannerInfo *root, Node *clause,
 
 	/* Extract the operator's declared left/right datatypes */
 	get_op_opfamily_properties(opno, opfamily, false,
+							   NULL,		/* don't need opmethod */
 							   &op_strategy,
+							   NULL,		/* don't need cmptype */
 							   &op_lefttype,
 							   &op_righttype);
 	Assert(op_strategy == BTEqualStrategyNumber);
@@ -3015,19 +3017,17 @@ mergejoinscansel(PlannerInfo *root, Node *clause,
 	 * Note: we expect that pg_statistic histograms will be sorted by the '<'
 	 * operator, regardless of which sort direction we are considering.
 	 */
-	switch (strategy)
+	switch (cmptype)
 	{
-		case BTLessStrategyNumber:
+		case COMPARE_LT:
 			isgt = false;
 			if (op_lefttype == op_righttype)
 			{
 				/* easy case */
-				ltop = get_opfamily_member(opfamily,
-										   op_lefttype, op_righttype,
-										   BTLessStrategyNumber);
-				leop = get_opfamily_member(opfamily,
-										   op_lefttype, op_righttype,
-										   BTLessEqualStrategyNumber);
+				ltop = get_opmethod_member(InvalidOid, opfamily, op_lefttype,
+										   op_righttype, COMPARE_LT);
+				leop = get_opmethod_member(InvalidOid, opfamily, op_lefttype,
+										   op_righttype, COMPARE_LE);
 				lsortop = ltop;
 				rsortop = ltop;
 				lstatop = lsortop;
@@ -3037,75 +3037,61 @@ mergejoinscansel(PlannerInfo *root, Node *clause,
 			}
 			else
 			{
-				ltop = get_opfamily_member(opfamily,
-										   op_lefttype, op_righttype,
-										   BTLessStrategyNumber);
-				leop = get_opfamily_member(opfamily,
-										   op_lefttype, op_righttype,
-										   BTLessEqualStrategyNumber);
-				lsortop = get_opfamily_member(opfamily,
-											  op_lefttype, op_lefttype,
-											  BTLessStrategyNumber);
-				rsortop = get_opfamily_member(opfamily,
-											  op_righttype, op_righttype,
-											  BTLessStrategyNumber);
+				ltop = get_opmethod_member(InvalidOid, opfamily, op_lefttype,
+										   op_righttype, COMPARE_LT);
+				leop = get_opmethod_member(InvalidOid, opfamily, op_lefttype,
+										   op_righttype, COMPARE_LE);
+				lsortop = get_opmethod_member(InvalidOid, opfamily, op_lefttype,
+											  op_lefttype, COMPARE_LT);
+				rsortop = get_opmethod_member(InvalidOid, opfamily, op_righttype,
+											  op_righttype, COMPARE_LT);
 				lstatop = lsortop;
 				rstatop = rsortop;
-				revltop = get_opfamily_member(opfamily,
-											  op_righttype, op_lefttype,
-											  BTLessStrategyNumber);
-				revleop = get_opfamily_member(opfamily,
-											  op_righttype, op_lefttype,
-											  BTLessEqualStrategyNumber);
+				revltop = get_opmethod_member(InvalidOid, opfamily, op_righttype,
+											  op_lefttype, COMPARE_LT);
+				revleop = get_opmethod_member(InvalidOid, opfamily, op_righttype,
+											  op_lefttype, COMPARE_LE);
 			}
 			break;
-		case BTGreaterStrategyNumber:
+		case COMPARE_GT:
 			/* descending-order case */
 			isgt = true;
 			if (op_lefttype == op_righttype)
 			{
 				/* easy case */
-				ltop = get_opfamily_member(opfamily,
-										   op_lefttype, op_righttype,
-										   BTGreaterStrategyNumber);
-				leop = get_opfamily_member(opfamily,
-										   op_lefttype, op_righttype,
-										   BTGreaterEqualStrategyNumber);
+				ltop = get_opmethod_member(InvalidOid,
+										   opfamily, op_lefttype, op_righttype,
+										   COMPARE_GT);
+				leop = get_opmethod_member(InvalidOid,
+										   opfamily, op_lefttype, op_righttype,
+										   COMPARE_GE);
 				lsortop = ltop;
 				rsortop = ltop;
-				lstatop = get_opfamily_member(opfamily,
-											  op_lefttype, op_lefttype,
-											  BTLessStrategyNumber);
+				lstatop = get_opmethod_member(InvalidOid,
+											  opfamily, op_lefttype, op_lefttype,
+											  COMPARE_LT);
 				rstatop = lstatop;
 				revltop = ltop;
 				revleop = leop;
 			}
 			else
 			{
-				ltop = get_opfamily_member(opfamily,
-										   op_lefttype, op_righttype,
-										   BTGreaterStrategyNumber);
-				leop = get_opfamily_member(opfamily,
-										   op_lefttype, op_righttype,
-										   BTGreaterEqualStrategyNumber);
-				lsortop = get_opfamily_member(opfamily,
-											  op_lefttype, op_lefttype,
-											  BTGreaterStrategyNumber);
-				rsortop = get_opfamily_member(opfamily,
-											  op_righttype, op_righttype,
-											  BTGreaterStrategyNumber);
-				lstatop = get_opfamily_member(opfamily,
-											  op_lefttype, op_lefttype,
-											  BTLessStrategyNumber);
-				rstatop = get_opfamily_member(opfamily,
-											  op_righttype, op_righttype,
-											  BTLessStrategyNumber);
-				revltop = get_opfamily_member(opfamily,
-											  op_righttype, op_lefttype,
-											  BTGreaterStrategyNumber);
-				revleop = get_opfamily_member(opfamily,
-											  op_righttype, op_lefttype,
-											  BTGreaterEqualStrategyNumber);
+				ltop = get_opmethod_member(InvalidOid, opfamily, op_lefttype,
+										   op_righttype, COMPARE_GT);
+				leop = get_opmethod_member(InvalidOid, opfamily, op_lefttype,
+										   op_righttype, COMPARE_GE);
+				lsortop = get_opmethod_member(InvalidOid, opfamily, op_lefttype,
+											  op_lefttype, COMPARE_GT);
+				rsortop = get_opmethod_member(InvalidOid, opfamily, op_righttype,
+											  op_righttype, COMPARE_GT);
+				lstatop = get_opmethod_member(InvalidOid, opfamily, op_lefttype,
+											  op_lefttype, COMPARE_LT);
+				rstatop = get_opmethod_member(InvalidOid, opfamily, op_righttype,
+											  op_righttype, COMPARE_LT);
+				revltop = get_opmethod_member(InvalidOid, opfamily, op_righttype,
+											  op_lefttype, COMPARE_GT);
+				revleop = get_opmethod_member(InvalidOid, opfamily, op_righttype,
+											  op_lefttype, COMPARE_GE);
 			}
 			break;
 		default:
@@ -7135,10 +7121,11 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		Oid			sortop;
 		AttStatsSlot sslot;
 
-		sortop = get_opfamily_member(index->opfamily[0],
+		sortop = get_opmethod_member(InvalidOid,
+									 index->opfamily[0],
 									 index->opcintype[0],
 									 index->opcintype[0],
-									 BTLessStrategyNumber);
+									 COMPARE_LT);
 		if (OidIsValid(sortop) &&
 			get_attstatsslot(&sslot, vardata.statsTuple,
 							 STATISTIC_KIND_CORRELATION, sortop,
@@ -7369,7 +7356,10 @@ gincost_pattern(IndexOptInfo *index, int indexcol,
 	 * find a matching pg_amop entry.)
 	 */
 	get_op_opfamily_properties(clause_op, index->opfamily[indexcol], false,
-							   &strategy_op, &lefttype, &righttype);
+							   NULL,		/* don't need opmethod */
+							   &strategy_op,
+							   NULL,		/* don't need cmptype */
+							   &lefttype, &righttype);
 
 	/*
 	 * GIN always uses the "default" support functions, which are those with
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 7bd476f3de7..83b180a37dd 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -32,6 +32,7 @@
 #include "catalog/pg_opclass.h"
 #include "catalog/pg_opfamily.h"
 #include "catalog/pg_operator.h"
+#include "catalog/pg_opfamily.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_publication.h"
 #include "catalog/pg_range.h"
@@ -135,7 +136,9 @@ get_op_opfamily_sortfamily(Oid opno, Oid opfamily)
  */
 void
 get_op_opfamily_properties(Oid opno, Oid opfamily, bool ordering_op,
+						   Oid *opmethod,
 						   int *strategy,
+						   CompareType *cmptype,
 						   Oid *lefttype,
 						   Oid *righttype)
 {
@@ -150,12 +153,45 @@ get_op_opfamily_properties(Oid opno, Oid opfamily, bool ordering_op,
 		elog(ERROR, "operator %u is not a member of opfamily %u",
 			 opno, opfamily);
 	amop_tup = (Form_pg_amop) GETSTRUCT(tp);
-	*strategy = amop_tup->amopstrategy;
-	*lefttype = amop_tup->amoplefttype;
-	*righttype = amop_tup->amoprighttype;
+	if (opmethod)
+		*opmethod = amop_tup->amopmethod;
+	if (strategy)
+		*strategy = amop_tup->amopstrategy;
+	if (cmptype)
+		*cmptype = IndexAmTranslateStrategy(amop_tup->amopstrategy,
+											amop_tup->amopmethod,
+											opfamily,
+											true);
+	if (lefttype)
+		*lefttype = amop_tup->amoplefttype;
+	if (righttype)
+		*righttype = amop_tup->amoprighttype;
 	ReleaseSysCache(tp);
 }
 
+/*
+ * get_opmethod_member
+ *		Get the OID of the operator that implements the specified row
+ *		comparison with the specified datatypes for the specified opfamily.
+ *
+ * Returns InvalidOid if there is no pg_amop entry for the given keys.
+ */
+Oid
+get_opmethod_member(Oid opmethod, Oid opfamily, Oid lefttype, Oid righttype,
+					CompareType cmptype)
+{
+	StrategyNumber	strategy;
+
+	if (!OidIsValid(opmethod))
+	{
+		Assert(OidIsValid(opfamily));
+		opmethod = get_opfamily_method(opfamily);
+	}
+
+	strategy = IndexAmTranslateCompareType(cmptype, opmethod, opfamily, false);
+	return get_opfamily_member(opfamily, lefttype, righttype, strategy);
+}
+
 /*
  * get_opfamily_member
  *		Get the OID of the operator that implements the specified strategy
@@ -186,9 +222,9 @@ get_opfamily_member(Oid opfamily, Oid lefttype, Oid righttype,
 
 /*
  * get_ordering_op_properties
- *		Given the OID of an ordering operator (a btree "<" or ">" operator),
- *		determine its opfamily, its declared input datatype, and its
- *		strategy number (BTLessStrategyNumber or BTGreaterStrategyNumber).
+ *		Given the OID of an ordering operator (a "<" or ">" operator),
+ *		determine its opmethod, its opfamily, its declared input datatype, its
+ *		strategy number, and its row comparison type.
  *
  * Returns true if successful, false if no matching pg_amop entry exists.
  * (This indicates that the operator is not a valid ordering operator.)
@@ -205,17 +241,26 @@ get_opfamily_member(Oid opfamily, Oid lefttype, Oid righttype,
  * additional effort on ensuring consistency.
  */
 bool
-get_ordering_op_properties(Oid opno,
-						   Oid *opfamily, Oid *opcintype, int16 *strategy)
+get_ordering_op_properties(Oid opno, Oid opmethodfilter,
+						   Oid *opmethod, Oid *opfamily,
+						   Oid *opcintype, int16 *strategy,
+						   CompareType *cmptype)
 {
 	bool		result = false;
 	CatCList   *catlist;
 	int			i;
 
 	/* ensure outputs are initialized on failure */
-	*opfamily = InvalidOid;
-	*opcintype = InvalidOid;
-	*strategy = 0;
+	if (opmethod)
+		*opmethod = InvalidOid;
+	if (opfamily)
+		*opfamily = InvalidOid;
+	if (opcintype)
+		*opcintype = InvalidOid;
+	if (strategy)
+		*strategy = InvalidStrategy;
+	if (cmptype)
+		*cmptype = COMPARE_INVALID;
 
 	/*
 	 * Search pg_amop to see if the target operator is registered as the "<"
@@ -227,21 +272,36 @@ get_ordering_op_properties(Oid opno,
 	{
 		HeapTuple	tuple = &catlist->members[i]->tuple;
 		Form_pg_amop aform = (Form_pg_amop) GETSTRUCT(tuple);
+		CompareType am_cmptype;
 
-		/* must be btree */
-		if (aform->amopmethod != BTREE_AM_OID)
+		/* must be acceptable to our opmethod filter */
+		if (OidIsValid(opmethodfilter) && aform->amopmethod != opmethodfilter)
 			continue;
 
-		if (aform->amopstrategy == BTLessStrategyNumber ||
-			aform->amopstrategy == BTGreaterStrategyNumber)
+		am_cmptype = IndexAmTranslateStrategy(aform->amopstrategy,
+											  aform->amopmethod,
+											  aform->amopfamily,
+											  true);
+
+		if (am_cmptype == COMPARE_LT || am_cmptype == COMPARE_GT)
 		{
 			/* Found it ... should have consistent input types */
 			if (aform->amoplefttype == aform->amoprighttype)
 			{
 				/* Found a suitable opfamily, return info */
-				*opfamily = aform->amopfamily;
-				*opcintype = aform->amoplefttype;
-				*strategy = aform->amopstrategy;
+				if (opmethod)
+					*opmethod = aform->amopmethod;
+				if (opfamily)
+					*opfamily = aform->amopfamily;
+				if (opcintype)
+					*opcintype = aform->amoplefttype;
+				if (strategy)
+					*strategy = aform->amopstrategy;
+				if (cmptype)
+					*cmptype = IndexAmTranslateStrategy(aform->amopstrategy,
+														aform->amopmethod,
+														aform->amopfamily,
+														false);
 				result = true;
 				break;
 			}
@@ -265,24 +325,27 @@ get_ordering_op_properties(Oid opno,
  * (This indicates that the operator is not a valid ordering operator.)
  */
 Oid
-get_equality_op_for_ordering_op(Oid opno, bool *reverse)
+get_equality_op_for_ordering_op(Oid opno, Oid opmethodfilter, bool *reverse)
 {
 	Oid			result = InvalidOid;
+	Oid			opmethod;
 	Oid			opfamily;
 	Oid			opcintype;
 	int16		strategy;
+	CompareType cmptype;
 
 	/* Find the operator in pg_amop */
-	if (get_ordering_op_properties(opno,
-								   &opfamily, &opcintype, &strategy))
+	if (get_ordering_op_properties(opno, opmethodfilter, &opmethod, &opfamily,
+								   &opcintype, &strategy, &cmptype))
 	{
 		/* Found a suitable opfamily, get matching equality operator */
-		result = get_opfamily_member(opfamily,
+		result = get_opmethod_member(opmethod,
+									 opfamily,
 									 opcintype,
 									 opcintype,
-									 BTEqualStrategyNumber);
+									 COMPARE_EQ);
 		if (reverse)
-			*reverse = (strategy == BTGreaterStrategyNumber);
+			*reverse = (cmptype == COMPARE_GT);
 	}
 
 	return result;
@@ -330,9 +393,10 @@ get_ordering_op_for_equality_op(Oid opno, bool use_lhs_type)
 			Oid			typid;
 
 			typid = use_lhs_type ? aform->amoplefttype : aform->amoprighttype;
-			result = get_opfamily_member(aform->amopfamily,
+			result = get_opmethod_member(InvalidOid,
+										 aform->amopfamily,
 										 typid, typid,
-										 BTLessStrategyNumber);
+										 COMPARE_LT);
 			if (OidIsValid(result))
 				break;
 			/* failure probably shouldn't happen, but keep looking if so */
@@ -589,20 +653,20 @@ get_op_hash_functions(Oid opno,
 }
 
 /*
- * get_op_btree_interpretation
+ * get_op_index_interpretation
  *		Given an operator's OID, find out which btree opfamilies it belongs to,
  *		and what properties it has within each one.  The results are returned
- *		as a palloc'd list of OpBtreeInterpretation structs.
+ *		as a palloc'd list of OpIndexInterpretation structs.
  *
  * In addition to the normal btree operators, we consider a <> operator to be
  * a "member" of an opfamily if its negator is an equality operator of the
  * opfamily.  COMPARE_NE is returned as the strategy number for this case.
  */
 List *
-get_op_btree_interpretation(Oid opno)
+get_op_index_interpretation(Oid opno)
 {
 	List	   *result = NIL;
-	OpBtreeInterpretation *thisresult;
+	OpIndexInterpretation *thisresult;
 	CatCList   *catlist;
 	int			i;
 
@@ -625,10 +689,15 @@ get_op_btree_interpretation(Oid opno)
 		op_strategy = (StrategyNumber) op_form->amopstrategy;
 		Assert(op_strategy >= 1 && op_strategy <= 5);
 
-		thisresult = (OpBtreeInterpretation *)
-			palloc(sizeof(OpBtreeInterpretation));
+		thisresult = (OpIndexInterpretation *)
+			palloc(sizeof(OpIndexInterpretation));
+		thisresult->opmethod = op_form->amopmethod;
 		thisresult->opfamily_id = op_form->amopfamily;
 		thisresult->strategy = op_strategy;
+		thisresult->cmptype = IndexAmTranslateStrategy(thisresult->strategy,
+													   thisresult->opmethod,
+													   thisresult->opfamily_id,
+													   false);
 		thisresult->oplefttype = op_form->amoplefttype;
 		thisresult->oprighttype = op_form->amoprighttype;
 		result = lappend(result, thisresult);
@@ -668,8 +737,8 @@ get_op_btree_interpretation(Oid opno)
 					continue;
 
 				/* OK, report it with "strategy" COMPARE_NE */
-				thisresult = (OpBtreeInterpretation *)
-					palloc(sizeof(OpBtreeInterpretation));
+				thisresult = (OpIndexInterpretation *)
+					palloc(sizeof(OpIndexInterpretation));
 				thisresult->opfamily_id = op_form->amopfamily;
 				thisresult->strategy = COMPARE_NE;
 				thisresult->oplefttype = op_form->amoplefttype;
@@ -1180,6 +1249,30 @@ get_language_name(Oid langoid, bool missing_ok)
 	return NULL;
 }
 
+/*				---------- OPFAMILY CACHE ----------						 */
+
+/*
+ * get_opfamily_method
+ *
+ *		Returns the OID of the operator method the opfamily belongs to.
+ */
+Oid
+get_opfamily_method(Oid opfamily)
+{
+	HeapTuple	tp;
+	Form_pg_opfamily opf_tup;
+	Oid			result;
+
+	tp = SearchSysCache1(OPFAMILYOID, ObjectIdGetDatum(opfamily));
+	if (!HeapTupleIsValid(tp))
+		elog(ERROR, "cache lookup failed for opfamily %u", opfamily);
+	opf_tup = (Form_pg_opfamily) GETSTRUCT(tp);
+
+	result = opf_tup->opfmethod;
+	ReleaseSysCache(tp);
+	return result;
+}
+
 /*				---------- OPCLASS CACHE ----------						 */
 
 /*
diff --git a/src/backend/utils/cache/typcache.c b/src/backend/utils/cache/typcache.c
index 5a3b3788d02..92a780afa35 100644
--- a/src/backend/utils/cache/typcache.c
+++ b/src/backend/utils/cache/typcache.c
@@ -633,10 +633,11 @@ lookup_type_cache(Oid type_id, int flags)
 		Oid			eq_opr = InvalidOid;
 
 		if (typentry->btree_opf != InvalidOid)
-			eq_opr = get_opfamily_member(typentry->btree_opf,
+			eq_opr = get_opmethod_member(InvalidOid,
+										 typentry->btree_opf,
 										 typentry->btree_opintype,
 										 typentry->btree_opintype,
-										 BTEqualStrategyNumber);
+										 COMPARE_EQ);
 		if (eq_opr == InvalidOid &&
 			typentry->hash_opf != InvalidOid)
 			eq_opr = get_opfamily_member(typentry->hash_opf,
@@ -680,10 +681,11 @@ lookup_type_cache(Oid type_id, int flags)
 		Oid			lt_opr = InvalidOid;
 
 		if (typentry->btree_opf != InvalidOid)
-			lt_opr = get_opfamily_member(typentry->btree_opf,
+			lt_opr = get_opmethod_member(InvalidOid,
+										 typentry->btree_opf,
 										 typentry->btree_opintype,
 										 typentry->btree_opintype,
-										 BTLessStrategyNumber);
+										 COMPARE_LT);
 
 		/*
 		 * As above, make sure array_cmp or record_cmp will succeed; but again
@@ -705,10 +707,11 @@ lookup_type_cache(Oid type_id, int flags)
 		Oid			gt_opr = InvalidOid;
 
 		if (typentry->btree_opf != InvalidOid)
-			gt_opr = get_opfamily_member(typentry->btree_opf,
+			gt_opr = get_opmethod_member(InvalidOid,
+										 typentry->btree_opf,
 										 typentry->btree_opintype,
 										 typentry->btree_opintype,
-										 BTGreaterStrategyNumber);
+										 COMPARE_GT);
 
 		/*
 		 * As above, make sure array_cmp or record_cmp will succeed; but again
diff --git a/src/backend/utils/sort/sortsupport.c b/src/backend/utils/sort/sortsupport.c
index 9b855be690e..8bbbf4c4578 100644
--- a/src/backend/utils/sort/sortsupport.c
+++ b/src/backend/utils/sort/sortsupport.c
@@ -135,17 +135,18 @@ PrepareSortSupportFromOrderingOp(Oid orderingOp, SortSupport ssup)
 {
 	Oid			opfamily;
 	Oid			opcintype;
-	int16		strategy;
+	CompareType cmptype;
 
 	Assert(ssup->comparator == NULL);
 
 	/* Find the operator in pg_amop */
-	if (!get_ordering_op_properties(orderingOp, &opfamily, &opcintype,
-									&strategy))
+	if (!get_ordering_op_properties(orderingOp, BTREE_AM_OID, NULL, &opfamily,
+									&opcintype, NULL, &cmptype))
 		elog(ERROR, "operator %u is not a valid ordering operator",
 			 orderingOp);
-	ssup->ssup_reverse = (strategy == BTGreaterStrategyNumber);
+	ssup->ssup_reverse = (cmptype == COMPARE_GT);
 
+	/* TODO: OPMETHOD: pass into FinishSortSupportFunction? */
 	FinishSortSupportFunction(opfamily, opcintype, ssup);
 }
 
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index fbf05322c75..45a1f760f3b 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -1501,8 +1501,9 @@ typedef struct PathKey
 
 	/* the value that is ordered */
 	EquivalenceClass *pk_eclass pg_node_attr(copy_as_scalar, equal_as_scalar);
-	Oid			pk_opfamily;	/* btree opfamily defining the ordering */
-	int			pk_strategy;	/* sort direction (ASC or DESC) */
+	Oid			pk_opfamily;	/* index opfamily defining the ordering */
+	int			pk_strategy;	/* sort direction as opmethod specific strategy */
+	CompareType pk_cmptype;	/* sort direction (COMPARE_LT or COMPARE_GT) */
 	bool		pk_nulls_first; /* do NULLs come before normal values? */
 } PathKey;
 
@@ -2767,9 +2768,10 @@ typedef struct RestrictInfo
 typedef struct MergeScanSelCache
 {
 	/* Ordering details (cache lookup key) */
-	Oid			opfamily;		/* btree opfamily defining the ordering */
+	Oid			opfamily;		/* index opfamily defining the ordering */
 	Oid			collation;		/* collation for the ordering */
-	int			strategy;		/* sort direction (ASC or DESC) */
+	int			strategy;		/* sort direction as opmethod specific strategy */
+	CompareType cmptype;		/* sort direction (COMPARE_LT or COMPARE_GT) */
 	bool		nulls_first;	/* do NULLs come before normal values? */
 	/* Results */
 	Selectivity leftstartsel;	/* first-join fraction for clause left side */
diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h
index bc5dfd7db41..79407df1781 100644
--- a/src/include/optimizer/paths.h
+++ b/src/include/optimizer/paths.h
@@ -238,9 +238,11 @@ extern List *build_join_pathkeys(PlannerInfo *root,
 								 JoinType jointype,
 								 List *outer_pathkeys);
 extern List *make_pathkeys_for_sortclauses(PlannerInfo *root,
+										   Oid opmethodfilter,
 										   List *sortclauses,
 										   List *tlist);
 extern List *make_pathkeys_for_sortclauses_extended(PlannerInfo *root,
+													Oid opmethodfilter,
 													List **sortclauses,
 													List *tlist,
 													bool remove_redundant,
@@ -269,8 +271,10 @@ extern List *truncate_useless_pathkeys(PlannerInfo *root,
 extern bool has_useful_pathkeys(PlannerInfo *root, RelOptInfo *rel);
 extern List *append_pathkeys(List *target, List *source);
 extern PathKey *make_canonical_pathkey(PlannerInfo *root,
-									   EquivalenceClass *eclass, Oid opfamily,
-									   int strategy, bool nulls_first);
+									   EquivalenceClass *eclass, Oid opmethod,
+									   Oid opfamily, int strategy,
+									   CompareType cmptype,
+									   bool nulls_first);
 extern void add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 									List *live_childrels);
 
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 6fab7aa6009..912f82176ea 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -14,20 +14,23 @@
 #define LSYSCACHE_H
 
 #include "access/attnum.h"
+#include "access/cmptype.h"
 #include "access/htup.h"
 #include "nodes/pg_list.h"
 
 /* avoid including subscripting.h here */
 struct SubscriptRoutines;
 
-/* Result list element for get_op_btree_interpretation */
-typedef struct OpBtreeInterpretation
+/* Result list element for get_op_index_interpretation */
+typedef struct OpIndexInterpretation
 {
+	Oid			opmethod;		/* index access method of opfamily */
 	Oid			opfamily_id;	/* btree opfamily containing operator */
 	int			strategy;		/* its strategy number */
+	CompareType cmptype;		/* its generic row comparison type */
 	Oid			oplefttype;		/* declared left input datatype */
 	Oid			oprighttype;	/* declared right input datatype */
-} OpBtreeInterpretation;
+} OpIndexInterpretation;
 
 /* I/O function selector for get_type_io_data */
 typedef enum IOFuncSelector
@@ -68,22 +71,31 @@ extern PGDLLIMPORT get_attavgwidth_hook_type get_attavgwidth_hook;
 extern bool op_in_opfamily(Oid opno, Oid opfamily);
 extern int	get_op_opfamily_strategy(Oid opno, Oid opfamily);
 extern Oid	get_op_opfamily_sortfamily(Oid opno, Oid opfamily);
-extern void get_op_opfamily_properties(Oid opno, Oid opfamily, bool ordering_op,
+extern void get_op_opfamily_properties(Oid opno,
+									   Oid opfamily,
+									   bool ordering_op,
+									   Oid *opmethod,
 									   int *strategy,
+									   CompareType *cmptype,
 									   Oid *lefttype,
 									   Oid *righttype);
+extern Oid	get_opmethod_member(Oid opmethod, Oid opfamily, Oid lefttype,
+								Oid righttype, CompareType cmptype);
 extern Oid	get_opfamily_member(Oid opfamily, Oid lefttype, Oid righttype,
 								int16 strategy);
-extern bool get_ordering_op_properties(Oid opno,
-									   Oid *opfamily, Oid *opcintype, int16 *strategy);
-extern Oid	get_equality_op_for_ordering_op(Oid opno, bool *reverse);
+extern bool get_ordering_op_properties(Oid opno, Oid opmethodfilter,
+									   Oid *opmethod, Oid *opfamily,
+									   Oid *opcintype, int16 *strategy,
+									   CompareType *cmptype);
+extern Oid	get_equality_op_for_ordering_op(Oid opno, Oid opmethodfilter,
+											bool *reverse);
 extern Oid	get_ordering_op_for_equality_op(Oid opno, bool use_lhs_type);
 extern List *get_mergejoin_opfamilies(Oid opno);
 extern bool get_compatible_hash_operators(Oid opno,
 										  Oid *lhs_opno, Oid *rhs_opno);
 extern bool get_op_hash_functions(Oid opno,
 								  RegProcedure *lhs_procno, RegProcedure *rhs_procno);
-extern List *get_op_btree_interpretation(Oid opno);
+extern List *get_op_index_interpretation(Oid opno);
 extern bool equality_ops_are_compatible(Oid opno1, Oid opno2);
 extern bool comparison_ops_are_compatible(Oid opno1, Oid opno2);
 extern Oid	get_opfamily_proc(Oid opfamily, Oid lefttype, Oid righttype,
@@ -103,6 +115,7 @@ extern Oid	get_constraint_index(Oid conoid);
 extern char get_constraint_type(Oid conoid);
 
 extern char *get_language_name(Oid langoid, bool missing_ok);
+extern Oid	get_opfamily_method(Oid opfamily);
 extern Oid	get_opclass_family(Oid opclass);
 extern Oid	get_opclass_input_type(Oid opclass);
 extern bool get_opclass_opfamily_and_input_type(Oid opclass,
diff --git a/src/include/utils/selfuncs.h b/src/include/utils/selfuncs.h
index d35939651f3..ba4e0e9f97c 100644
--- a/src/include/utils/selfuncs.h
+++ b/src/include/utils/selfuncs.h
@@ -209,10 +209,11 @@ extern Selectivity rowcomparesel(PlannerInfo *root,
 								 RowCompareExpr *clause,
 								 int varRelid, JoinType jointype, SpecialJoinInfo *sjinfo);
 
-extern void mergejoinscansel(PlannerInfo *root, Node *clause,
-							 Oid opfamily, int strategy, bool nulls_first,
-							 Selectivity *leftstart, Selectivity *leftend,
-							 Selectivity *rightstart, Selectivity *rightend);
+extern void mergejoinscansel(PlannerInfo *root, Node *clause, Oid opfamily,
+							 int strategy, CompareType cmptype,
+							 bool nulls_first, Selectivity *leftstart,
+							 Selectivity *leftend, Selectivity *rightstart,
+							 Selectivity *rightend);
 
 extern double estimate_num_groups(PlannerInfo *root, List *groupExprs,
 								  double input_rows, List **pgset,
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index e3e09a2207e..3698a975921 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1764,11 +1764,11 @@ OnConflictAction
 OnConflictClause
 OnConflictExpr
 OnConflictSetState
-OpBtreeInterpretation
 OpClassCacheEnt
 OpExpr
 OpFamilyMember
 OpFamilyOpFuncGroup
+OpIndexInterpretation
 OpclassInfo
 Operator
 OperatorElement
-- 
2.48.1

v20-0001-TEST-Add-loadable-modules-as-tests-of-the-amapi.patch.no-cfbotapplication/octet-stream; name=v20-0001-TEST-Add-loadable-modules-as-tests-of-the-amapi.patch.no-cfbotDownload
From a04c95bf53c31d7b2dfcfa260a28c9779c3ad746 Mon Sep 17 00:00:00 2001
From: Andrew Dunstan <andrew@dunslane.net>
Date: Mon, 25 Nov 2024 15:28:12 -0500
Subject: [PATCH v20 01/16] [TEST] Add loadable modules as tests of the amapi

If the index access method API meets the needs of future access
methods, then surely it must at least meet the needs of present
access methods.  Copying and renaming hash and btree as "xash" and
"xtree", respectively, those access methods should perform exactly
the same as the in-core hash and btree methods.  Alas, they do not.
Running the standard in-core regression and isolation tests against
these add-on index access methods fail in some notable ways.

Rather than suffer the maintenance headaches of copying and slightly
modifying the regression and isolation test suites, add a script to
copy such files and insert "USING xtree" and "USING xash" clauses in
the appropriate places.

The regression tests fail after applying this patch.  That's the
point of the patch, not an oversight.  To not be a burden, these
modules are skipped during `make check-world`, and the directories
are entirely omitted from the meson build system.
---
 src/include/access/hash.h                     |   3 +-
 src/test/modules/Makefile                     |   3 +-
 src/test/modules/xash/.gitignore              |   5 +
 src/test/modules/xash/Makefile                |  69 ++
 src/test/modules/xash/access/xash.c           |  82 ++
 src/test/modules/xash/expected/.gitignore     |   1 +
 .../xash/isolation/expected/.gitignore        |   1 +
 .../modules/xash/isolation/specs/.gitignore   |   1 +
 src/test/modules/xash/module.c                |  43 ++
 src/test/modules/xash/sql/.gitignore          |   1 +
 src/test/modules/xash/t/.gitignore            |   0
 src/test/modules/xash/xash--1.0.sql           | 406 ++++++++++
 src/test/modules/xash/xash.conf               |   1 +
 src/test/modules/xash/xash.control            |   5 +
 src/test/modules/xtree/.gitignore             |   5 +
 src/test/modules/xtree/Makefile               |  69 ++
 src/test/modules/xtree/access/xtree.c         |  81 ++
 src/test/modules/xtree/expected/.gitignore    |   1 +
 .../xtree/isolation/expected/.gitignore       |   1 +
 .../modules/xtree/isolation/specs/.gitignore  |   1 +
 src/test/modules/xtree/module.c               |  17 +
 src/test/modules/xtree/sql/.gitignore         |   1 +
 src/test/modules/xtree/xtree--1.0.sql         | 714 ++++++++++++++++++
 src/test/modules/xtree/xtree.conf             |   1 +
 src/test/modules/xtree/xtree.control          |   5 +
 src/tools/clone_tests.pl                      | 449 +++++++++++
 26 files changed, 1963 insertions(+), 3 deletions(-)
 create mode 100644 src/test/modules/xash/.gitignore
 create mode 100644 src/test/modules/xash/Makefile
 create mode 100644 src/test/modules/xash/access/xash.c
 create mode 100644 src/test/modules/xash/expected/.gitignore
 create mode 100644 src/test/modules/xash/isolation/expected/.gitignore
 create mode 100644 src/test/modules/xash/isolation/specs/.gitignore
 create mode 100644 src/test/modules/xash/module.c
 create mode 100644 src/test/modules/xash/sql/.gitignore
 create mode 100644 src/test/modules/xash/t/.gitignore
 create mode 100644 src/test/modules/xash/xash--1.0.sql
 create mode 100644 src/test/modules/xash/xash.conf
 create mode 100644 src/test/modules/xash/xash.control
 create mode 100644 src/test/modules/xtree/.gitignore
 create mode 100644 src/test/modules/xtree/Makefile
 create mode 100644 src/test/modules/xtree/access/xtree.c
 create mode 100644 src/test/modules/xtree/expected/.gitignore
 create mode 100644 src/test/modules/xtree/isolation/expected/.gitignore
 create mode 100644 src/test/modules/xtree/isolation/specs/.gitignore
 create mode 100644 src/test/modules/xtree/module.c
 create mode 100644 src/test/modules/xtree/sql/.gitignore
 create mode 100644 src/test/modules/xtree/xtree--1.0.sql
 create mode 100644 src/test/modules/xtree/xtree.conf
 create mode 100644 src/test/modules/xtree/xtree.control
 create mode 100755 src/tools/clone_tests.pl

diff --git a/src/include/access/hash.h b/src/include/access/hash.h
index 073ad29b19b..305c2ffdc13 100644
--- a/src/include/access/hash.h
+++ b/src/include/access/hash.h
@@ -273,8 +273,7 @@ typedef struct HashOptions
 } HashOptions;
 
 #define HashGetFillFactor(relation) \
-	(AssertMacro(relation->rd_rel->relkind == RELKIND_INDEX && \
-				 relation->rd_rel->relam == HASH_AM_OID), \
+	(AssertMacro(relation->rd_rel->relkind == RELKIND_INDEX), \
 	 (relation)->rd_options ? \
 	 ((HashOptions *) (relation)->rd_options)->fillfactor :	\
 	 HASH_DEFAULT_FILLFACTOR)
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 4e4be3fa511..114b04376ac 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -4,6 +4,8 @@ subdir = src/test/modules
 top_builddir = ../../..
 include $(top_builddir)/src/Makefile.global
 
+# Subdirectories "xash" and "xtree" are excluded from the list, to prevent
+# `make check-world` from failing
 SUBDIRS = \
 		  brin \
 		  commit_ts \
@@ -43,7 +45,6 @@ SUBDIRS = \
 		  worker_spi \
 		  xid_wraparound
 
-
 ifeq ($(enable_injection_points),yes)
 SUBDIRS += injection_points gin typcache
 else
diff --git a/src/test/modules/xash/.gitignore b/src/test/modules/xash/.gitignore
new file mode 100644
index 00000000000..eaf4d5699ed
--- /dev/null
+++ b/src/test/modules/xash/.gitignore
@@ -0,0 +1,5 @@
+/log/
+/output_iso/
+/results/
+/tmp_check/
+/tmp_check_iso/
diff --git a/src/test/modules/xash/Makefile b/src/test/modules/xash/Makefile
new file mode 100644
index 00000000000..e2c06d0e8f3
--- /dev/null
+++ b/src/test/modules/xash/Makefile
@@ -0,0 +1,69 @@
+# xash/Makefile
+
+REGRESS_SHLIB=$(abs_top_builddir)/src/test/regress/regress$(DLSUFFIX)
+export REGRESS_SHLIB
+
+MODULE_big = xash
+OBJS = $(WIN32RES) access/xash.o module.o
+
+EXTENSION = xash
+DATA = xash--1.0.sql
+PGFILEDESC = "test copy of hash"
+
+LDFLAGS_SL += $(filter -lm, $(LIBS))
+
+# If the caller supplied a pklibdir, use that.  The build system will supply
+# this, and perhaps also folks making installcheck from pgxs, and maybe others.
+#
+ifdef pkglibdir
+REGRESS_OPTS = --dlpath=$(pkglibdir)
+else
+REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
+endif
+
+# The xash module does not supply its own regression tests.  Instead, it copies
+# regression tests from elsewhere in the project and changes hash indexes
+# contained within those tests to xash instead.  The tests to use are parsed
+# from parallel_schedule and isolation_schedule, as below.
+CLONE_SCRIPT=../../../tools/clone_tests.pl
+REGRESS_BY_REFERENCE=$(shell $(PERL) $(CLONE_SCRIPT) --schedule=../../regress/parallel_schedule)
+ISOLATION_BY_REFERENCE=$(shell $(PERL) $(CLONE_SCRIPT) --schedule=../../isolation/isolation_schedule)
+
+REGRESS_OPTS += --load-extension=xash
+REGRESS_OPTS += --temp-config $(srcdir)/xash.conf
+REGRESS = $(REGRESS_BY_REFERENCE)
+
+ISOLATION_OPTS = --load-extension=xash
+ISOLATION_OPTS += --inputdir=isolation
+ISOLATION_OPTS += --temp-config $(srcdir)/xash.conf
+ISOLATION = $(ISOLATION_BY_REFERENCE)
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/xash
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
+
+$(OBJS): tests_by_reference
+
+.PHONY: tests_by_reference
+tests_by_reference:
+	$(PERL) $(CLONE_SCRIPT) \
+		--regress="$(REGRESS_BY_REFERENCE)" \
+		--isolation="$(ISOLATION_BY_REFERENCE)" \
+		--src_idx_am=hash \
+		--dst_idx_am=xash \
+		--src_sql=../../regress/sql \
+		--src_expected=../../regress/expected \
+		--dst_sql=./sql \
+		--dst_expected=./expected \
+		--src_spec=../../isolation/specs \
+		--src_iso_expected=../../isolation/expected \
+		--dst_spec=./isolation/specs \
+		--dst_iso_expected=./isolation/expected \
+		--datadir_prefix="/../../regress"
diff --git a/src/test/modules/xash/access/xash.c b/src/test/modules/xash/access/xash.c
new file mode 100644
index 00000000000..c1a8ffbe2d8
--- /dev/null
+++ b/src/test/modules/xash/access/xash.c
@@ -0,0 +1,82 @@
+/*-------------------------------------------------------------------------
+ *
+ * xash.c
+ *	  Implementation of an index AM by reference to Hash.
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/test/modules/xash/access/xash.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+#include "access/hash.h"
+#include "commands/vacuum.h"
+#include "utils/index_selfuncs.h"
+
+/*
+ * Xash handler function: return IndexAmRoutine with access method parameters
+ * and callbacks.
+ */
+PG_FUNCTION_INFO_V1(xash_indexam_handler);
+Datum
+xash_indexam_handler(PG_FUNCTION_ARGS)
+{
+	IndexAmRoutine *amroutine = makeNode(IndexAmRoutine);
+
+	amroutine->amstrategies = HTMaxStrategyNumber;
+	amroutine->amsupport = HASHNProcs;
+	amroutine->amoptsprocnum = HASHOPTIONS_PROC;
+	amroutine->amcanorder = false;
+	amroutine->amcanorderbyop = false;
+	amroutine->amcanbackward = true;
+	amroutine->amcanunique = false;
+	amroutine->amcanmulticol = false;
+	amroutine->amoptionalkey = false;
+	amroutine->amsearcharray = false;
+	amroutine->amsearchnulls = false;
+	amroutine->amstorage = false;
+	amroutine->amclusterable = false;
+	amroutine->ampredlocks = true;
+	amroutine->amcanparallel = false;
+	amroutine->amtranslatestrategy = hashtranslatestrategy;
+	amroutine->amtranslatecmptype = hashtranslatecmptype;
+	amroutine->amcanbuildparallel = false;
+	amroutine->amcaninclude = false;
+	amroutine->amusemaintenanceworkmem = false;
+	amroutine->amsummarizing = false;
+	amroutine->amparallelvacuumoptions =
+		VACUUM_OPTION_PARALLEL_BULKDEL;
+	amroutine->amkeytype = INT4OID;
+
+	amroutine->ambuild = hashbuild;
+	amroutine->ambuildempty = hashbuildempty;
+	amroutine->aminsert = hashinsert;
+	amroutine->aminsertcleanup = NULL;
+	amroutine->ambulkdelete = hashbulkdelete;
+	amroutine->amvacuumcleanup = hashvacuumcleanup;
+	amroutine->amcanreturn = NULL;
+	amroutine->amcostestimate = hashcostestimate;
+	amroutine->amgettreeheight = NULL;
+	amroutine->amoptions = hashoptions;
+	amroutine->amproperty = NULL;
+	amroutine->ambuildphasename = NULL;
+	amroutine->amvalidate = hashvalidate;
+	amroutine->amadjustmembers = hashadjustmembers;
+	amroutine->ambeginscan = hashbeginscan;
+	amroutine->amrescan = hashrescan;
+	amroutine->amgettuple = hashgettuple;
+	amroutine->amgetbitmap = hashgetbitmap;
+	amroutine->amendscan = hashendscan;
+	amroutine->ammarkpos = NULL;
+	amroutine->amrestrpos = NULL;
+	amroutine->amestimateparallelscan = NULL;
+	amroutine->aminitparallelscan = NULL;
+	amroutine->amparallelrescan = NULL;
+
+	PG_RETURN_POINTER(amroutine);
+}
diff --git a/src/test/modules/xash/expected/.gitignore b/src/test/modules/xash/expected/.gitignore
new file mode 100644
index 00000000000..e87afd97c3b
--- /dev/null
+++ b/src/test/modules/xash/expected/.gitignore
@@ -0,0 +1 @@
+/*.out
diff --git a/src/test/modules/xash/isolation/expected/.gitignore b/src/test/modules/xash/isolation/expected/.gitignore
new file mode 100644
index 00000000000..e87afd97c3b
--- /dev/null
+++ b/src/test/modules/xash/isolation/expected/.gitignore
@@ -0,0 +1 @@
+/*.out
diff --git a/src/test/modules/xash/isolation/specs/.gitignore b/src/test/modules/xash/isolation/specs/.gitignore
new file mode 100644
index 00000000000..e9404350671
--- /dev/null
+++ b/src/test/modules/xash/isolation/specs/.gitignore
@@ -0,0 +1 @@
+/*.spec
diff --git a/src/test/modules/xash/module.c b/src/test/modules/xash/module.c
new file mode 100644
index 00000000000..30e19b94ec9
--- /dev/null
+++ b/src/test/modules/xash/module.c
@@ -0,0 +1,43 @@
+/*-------------------------------------------------------------------------
+ *
+ * module.c
+ *		EnterpriseDB xash plugin.
+ *
+ * Portions Copyright (c) 2022, EnterpriseDB Corporation. All Rights Reserved.
+ *
+ * IDENTIFICATION
+ *	  xash/module.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "fmgr.h"
+
+PG_MODULE_MAGIC;
+
+/*---- GUC variables ----*/
+
+
+/*---- Function declarations ----*/
+
+void		_PG_init(void);
+void		_PG_fini(void);
+
+/*---- Function definitions ----*/
+
+/*
+ * Module load callback
+ */
+void
+_PG_init(void)
+{
+}
+
+/*
+ * Module unload callback
+ */
+void
+_PG_fini(void)
+{
+}
diff --git a/src/test/modules/xash/sql/.gitignore b/src/test/modules/xash/sql/.gitignore
new file mode 100644
index 00000000000..606aaaa1c26
--- /dev/null
+++ b/src/test/modules/xash/sql/.gitignore
@@ -0,0 +1 @@
+/*.sql
diff --git a/src/test/modules/xash/t/.gitignore b/src/test/modules/xash/t/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/src/test/modules/xash/xash--1.0.sql b/src/test/modules/xash/xash--1.0.sql
new file mode 100644
index 00000000000..4f69c104c08
--- /dev/null
+++ b/src/test/modules/xash/xash--1.0.sql
@@ -0,0 +1,406 @@
+/* xash/xash--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION xash" to load this file. \quit
+
+CREATE FUNCTION xash_indexam_handler(internal)
+RETURNS index_am_handler
+AS 'MODULE_PATHNAME'
+LANGUAGE C;
+
+-- Access method
+CREATE ACCESS METHOD xash TYPE INDEX HANDLER xash_indexam_handler;
+COMMENT ON ACCESS METHOD xash IS 'test copy of hash';
+
+
+-- Operator families
+CREATE OPERATOR FAMILY "array_ops" USING "xash";
+
+CREATE OPERATOR FAMILY "bpchar_ops" USING "xash";
+
+CREATE OPERATOR FAMILY "char_ops" USING "xash";
+
+CREATE OPERATOR FAMILY "date_ops" USING "xash";
+
+CREATE OPERATOR FAMILY "float_ops" USING "xash";
+
+ALTER OPERATOR FAMILY "float_ops" USING "xash" ADD
+	OPERATOR 1 =("float4","float8"),
+	OPERATOR 1 =("float8","float4");
+
+
+CREATE OPERATOR FAMILY "network_ops" USING "xash";
+
+CREATE OPERATOR FAMILY "integer_ops" USING "xash";
+
+ALTER OPERATOR FAMILY "integer_ops" USING "xash" ADD
+	OPERATOR 1 =("int2","int4"),
+	OPERATOR 1 =("int2","int8"),
+	OPERATOR 1 =("int4","int2"),
+	OPERATOR 1 =("int4","int8"),
+	OPERATOR 1 =("int8","int2"),
+	OPERATOR 1 =("int8","int4");
+
+
+CREATE OPERATOR FAMILY "interval_ops" USING "xash";
+
+CREATE OPERATOR FAMILY "macaddr_ops" USING "xash";
+
+CREATE OPERATOR FAMILY "macaddr8_ops" USING "xash";
+
+CREATE OPERATOR FAMILY "numeric_ops" USING "xash";
+
+CREATE OPERATOR FAMILY "oid_ops" USING "xash";
+
+CREATE OPERATOR FAMILY "oidvector_ops" USING "xash";
+
+CREATE OPERATOR FAMILY "record_ops" USING "xash";
+
+CREATE OPERATOR FAMILY "text_ops" USING "xash";
+
+ALTER OPERATOR FAMILY "text_ops" USING "xash" ADD
+	OPERATOR 1 =("name","text"),
+	OPERATOR 1 =("text","name");
+
+
+CREATE OPERATOR FAMILY "time_ops" USING "xash";
+
+CREATE OPERATOR FAMILY "timestamptz_ops" USING "xash";
+
+CREATE OPERATOR FAMILY "timetz_ops" USING "xash";
+
+CREATE OPERATOR FAMILY "timestamp_ops" USING "xash";
+
+CREATE OPERATOR FAMILY "bool_ops" USING "xash";
+
+CREATE OPERATOR FAMILY "bytea_ops" USING "xash";
+
+CREATE OPERATOR FAMILY "xid_ops" USING "xash";
+
+CREATE OPERATOR FAMILY "xid8_ops" USING "xash";
+
+CREATE OPERATOR FAMILY "cid_ops" USING "xash";
+
+CREATE OPERATOR FAMILY "tid_ops" USING "xash";
+
+CREATE OPERATOR FAMILY "text_pattern_ops" USING "xash";
+
+CREATE OPERATOR FAMILY "bpchar_pattern_ops" USING "xash";
+
+CREATE OPERATOR FAMILY "aclitem_ops" USING "xash";
+
+CREATE OPERATOR FAMILY "uuid_ops" USING "xash";
+
+CREATE OPERATOR FAMILY "pg_lsn_ops" USING "xash";
+
+CREATE OPERATOR FAMILY "enum_ops" USING "xash";
+
+CREATE OPERATOR FAMILY "range_ops" USING "xash";
+
+CREATE OPERATOR FAMILY "jsonb_ops" USING "xash";
+
+CREATE OPERATOR FAMILY "multirange_ops" USING "xash";
+
+-- Operator classes
+
+CREATE OPERATOR CLASS "array_ops"
+	DEFAULT FOR TYPE "anyarray" USING "xash" FAMILY "array_ops" AS
+	OPERATOR 1 =("anyarray","anyarray"),
+	FUNCTION 1 "hash_array",
+	FUNCTION 2 "hash_array_extended";
+
+
+
+CREATE OPERATOR CLASS "bpchar_ops"
+	DEFAULT FOR TYPE "bpchar" USING "xash" FAMILY "bpchar_ops" AS
+	OPERATOR 1 =("bpchar","bpchar"),
+	FUNCTION 1 "hashbpchar",
+	FUNCTION 2 "hashbpcharextended";
+
+
+
+CREATE OPERATOR CLASS "char_ops"
+	DEFAULT FOR TYPE "char" USING "xash" FAMILY "char_ops" AS
+	OPERATOR 1 =("char","char"),
+	FUNCTION 1 "hashchar",
+	FUNCTION 2 "hashcharextended";
+
+
+
+CREATE OPERATOR CLASS "date_ops"
+	DEFAULT FOR TYPE "date" USING "xash" FAMILY "date_ops" AS
+	OPERATOR 1 =("date","date"),
+	FUNCTION 1 "hashint4",
+	FUNCTION 2 "hashint4extended";
+
+
+
+CREATE OPERATOR CLASS "float4_ops"
+	DEFAULT FOR TYPE "float4" USING "xash" FAMILY "float_ops" AS
+	OPERATOR 1 =("float4","float4"),
+	FUNCTION 1 "hashfloat4",
+	FUNCTION 2 "hashfloat4extended";
+
+
+
+CREATE OPERATOR CLASS "float8_ops"
+	DEFAULT FOR TYPE "float8" USING "xash" FAMILY "float_ops" AS
+	OPERATOR 1 =("float8","float8"),
+	FUNCTION 1 "hashfloat8",
+	FUNCTION 2 "hashfloat8extended";
+
+
+
+CREATE OPERATOR CLASS "inet_ops"
+	DEFAULT FOR TYPE "inet" USING "xash" FAMILY "network_ops" AS
+	OPERATOR 1 =("inet","inet"),
+	FUNCTION 1 "hashinet",
+	FUNCTION 2 "hashinetextended";
+
+
+
+CREATE OPERATOR CLASS "int2_ops"
+	DEFAULT FOR TYPE "int2" USING "xash" FAMILY "integer_ops" AS
+	OPERATOR 1 =("int2","int2"),
+	FUNCTION 1 "hashint2",
+	FUNCTION 2 "hashint2extended";
+
+
+
+CREATE OPERATOR CLASS "int4_ops"
+	DEFAULT FOR TYPE "int4" USING "xash" FAMILY "integer_ops" AS
+	OPERATOR 1 =("int4","int4"),
+	FUNCTION 1 "hashint4",
+	FUNCTION 2 "hashint4extended";
+
+
+
+CREATE OPERATOR CLASS "int8_ops"
+	DEFAULT FOR TYPE "int8" USING "xash" FAMILY "integer_ops" AS
+	OPERATOR 1 =("int8","int8"),
+	FUNCTION 1 "hashint8",
+	FUNCTION 2 "hashint8extended";
+
+
+
+CREATE OPERATOR CLASS "interval_ops"
+	DEFAULT FOR TYPE "interval" USING "xash" FAMILY "interval_ops" AS
+	OPERATOR 1 =("interval","interval"),
+	FUNCTION 1 "interval_hash",
+	FUNCTION 2 "interval_hash_extended";
+
+
+
+CREATE OPERATOR CLASS "macaddr_ops"
+	DEFAULT FOR TYPE "macaddr" USING "xash" FAMILY "macaddr_ops" AS
+	OPERATOR 1 =("macaddr","macaddr"),
+	FUNCTION 1 "hashmacaddr",
+	FUNCTION 2 "hashmacaddrextended";
+
+
+
+CREATE OPERATOR CLASS "macaddr8_ops"
+	DEFAULT FOR TYPE "macaddr8" USING "xash" FAMILY "macaddr8_ops" AS
+	OPERATOR 1 =("macaddr8","macaddr8"),
+	FUNCTION 1 "hashmacaddr8",
+	FUNCTION 2 "hashmacaddr8extended";
+
+
+
+CREATE OPERATOR CLASS "name_ops"
+	DEFAULT FOR TYPE "name" USING "xash" FAMILY "text_ops" AS
+	OPERATOR 1 =("name","name"),
+	FUNCTION 1 "hashname",
+	FUNCTION 2 "hashnameextended";
+
+
+
+CREATE OPERATOR CLASS "numeric_ops"
+	DEFAULT FOR TYPE "numeric" USING "xash" FAMILY "numeric_ops" AS
+	OPERATOR 1 =("numeric","numeric"),
+	FUNCTION 1 "hash_numeric",
+	FUNCTION 2 "hash_numeric_extended";
+
+
+
+CREATE OPERATOR CLASS "oid_ops"
+	DEFAULT FOR TYPE "oid" USING "xash" FAMILY "oid_ops" AS
+	OPERATOR 1 =("oid","oid"),
+	FUNCTION 1 "hashoid",
+	FUNCTION 2 "hashoidextended";
+
+
+
+CREATE OPERATOR CLASS "oidvector_ops"
+	DEFAULT FOR TYPE "oidvector" USING "xash" FAMILY "oidvector_ops" AS
+	OPERATOR 1 =("oidvector","oidvector"),
+	FUNCTION 1 "hashoidvector",
+	FUNCTION 2 "hashoidvectorextended";
+
+
+
+CREATE OPERATOR CLASS "record_ops"
+	DEFAULT FOR TYPE "record" USING "xash" FAMILY "record_ops" AS
+	OPERATOR 1 =("record","record"),
+	FUNCTION 1 "hash_record",
+	FUNCTION 2 "hash_record_extended";
+
+
+
+CREATE OPERATOR CLASS "text_ops"
+	DEFAULT FOR TYPE "text" USING "xash" FAMILY "text_ops" AS
+	OPERATOR 1 =("text","text"),
+	FUNCTION 1 "hashtext",
+	FUNCTION 2 "hashtextextended";
+
+
+
+CREATE OPERATOR CLASS "time_ops"
+	DEFAULT FOR TYPE "time" USING "xash" FAMILY "time_ops" AS
+	OPERATOR 1 =("time","time"),
+	FUNCTION 1 "time_hash",
+	FUNCTION 2 "time_hash_extended";
+
+
+
+CREATE OPERATOR CLASS "timestamptz_ops"
+	DEFAULT FOR TYPE "timestamptz" USING "xash" FAMILY "timestamptz_ops" AS
+	OPERATOR 1 =("timestamptz","timestamptz"),
+	FUNCTION 1 "timestamp_hash",
+	FUNCTION 2 "timestamp_hash_extended";
+
+
+
+CREATE OPERATOR CLASS "timetz_ops"
+	DEFAULT FOR TYPE "timetz" USING "xash" FAMILY "timetz_ops" AS
+	OPERATOR 1 =("timetz","timetz"),
+	FUNCTION 1 "timetz_hash",
+	FUNCTION 2 "timetz_hash_extended";
+
+
+
+CREATE OPERATOR CLASS "timestamp_ops"
+	DEFAULT FOR TYPE "timestamp" USING "xash" FAMILY "timestamp_ops" AS
+	OPERATOR 1 =("timestamp","timestamp"),
+	FUNCTION 1 "timestamp_hash",
+	FUNCTION 2 "timestamp_hash_extended";
+
+
+
+CREATE OPERATOR CLASS "bool_ops"
+	DEFAULT FOR TYPE "bool" USING "xash" FAMILY "bool_ops" AS
+	OPERATOR 1 =("bool","bool"),
+	FUNCTION 1 "hashchar",
+	FUNCTION 2 "hashcharextended";
+
+
+
+CREATE OPERATOR CLASS "bytea_ops"
+	DEFAULT FOR TYPE "bytea" USING "xash" FAMILY "bytea_ops" AS
+	OPERATOR 1 =("bytea","bytea"),
+	FUNCTION 1 "hashvarlena",
+	FUNCTION 2 "hashvarlenaextended";
+
+
+
+CREATE OPERATOR CLASS "xid_ops"
+	DEFAULT FOR TYPE "xid" USING "xash" FAMILY "xid_ops" AS
+	OPERATOR 1 =("xid","xid"),
+	FUNCTION 1 "hashint4",
+	FUNCTION 2 "hashint4extended";
+
+
+
+CREATE OPERATOR CLASS "xid8_ops"
+	DEFAULT FOR TYPE "xid8" USING "xash" FAMILY "xid8_ops" AS
+	OPERATOR 1 =("xid8","xid8"),
+	FUNCTION 1 "hashint8",
+	FUNCTION 2 "hashint8extended";
+
+
+
+CREATE OPERATOR CLASS "cid_ops"
+	DEFAULT FOR TYPE "cid" USING "xash" FAMILY "cid_ops" AS
+	OPERATOR 1 =("cid","cid"),
+	FUNCTION 1 "hashint4",
+	FUNCTION 2 "hashint4extended";
+
+
+
+CREATE OPERATOR CLASS "tid_ops"
+	DEFAULT FOR TYPE "tid" USING "xash" FAMILY "tid_ops" AS
+	OPERATOR 1 =("tid","tid"),
+	FUNCTION 1 "hashtid",
+	FUNCTION 2 "hashtidextended";
+
+
+
+CREATE OPERATOR CLASS "text_pattern_ops"
+	FOR TYPE "text" USING "xash" FAMILY "text_pattern_ops" AS
+	OPERATOR 1 =("text","text"),
+	FUNCTION 1 "hashtext",
+	FUNCTION 2 "hashtextextended";
+
+
+
+CREATE OPERATOR CLASS "bpchar_pattern_ops"
+	FOR TYPE "bpchar" USING "xash" FAMILY "bpchar_pattern_ops" AS
+	OPERATOR 1 =("bpchar","bpchar"),
+	FUNCTION 1 "hashbpchar",
+	FUNCTION 2 "hashbpcharextended";
+
+
+
+CREATE OPERATOR CLASS "aclitem_ops"
+	DEFAULT FOR TYPE "aclitem" USING "xash" FAMILY "aclitem_ops" AS
+	OPERATOR 1 =("aclitem","aclitem"),
+	FUNCTION 1 "hash_aclitem",
+	FUNCTION 2 "hash_aclitem_extended";
+
+
+
+CREATE OPERATOR CLASS "uuid_ops"
+	DEFAULT FOR TYPE "uuid" USING "xash" FAMILY "uuid_ops" AS
+	OPERATOR 1 =("uuid","uuid"),
+	FUNCTION 1 "uuid_hash",
+	FUNCTION 2 "uuid_hash_extended";
+
+
+
+CREATE OPERATOR CLASS "pg_lsn_ops"
+	DEFAULT FOR TYPE "pg_lsn" USING "xash" FAMILY "pg_lsn_ops" AS
+	OPERATOR 1 =("pg_lsn","pg_lsn"),
+	FUNCTION 1 "pg_lsn_hash",
+	FUNCTION 2 "pg_lsn_hash_extended";
+
+
+
+CREATE OPERATOR CLASS "enum_ops"
+	DEFAULT FOR TYPE "anyenum" USING "xash" FAMILY "enum_ops" AS
+	OPERATOR 1 =("anyenum","anyenum"),
+	FUNCTION 1 "hashenum",
+	FUNCTION 2 "hashenumextended";
+
+
+
+CREATE OPERATOR CLASS "range_ops"
+	DEFAULT FOR TYPE "anyrange" USING "xash" FAMILY "range_ops" AS
+	OPERATOR 1 =("anyrange","anyrange"),
+	FUNCTION 1 "hash_range",
+	FUNCTION 2 "hash_range_extended";
+
+
+
+CREATE OPERATOR CLASS "multirange_ops"
+	DEFAULT FOR TYPE "anymultirange" USING "xash" FAMILY "multirange_ops" AS
+	OPERATOR 1 =("anymultirange","anymultirange"),
+	FUNCTION 1 "hash_multirange",
+	FUNCTION 2 "hash_multirange_extended";
+
+
+
+CREATE OPERATOR CLASS "jsonb_ops"
+	DEFAULT FOR TYPE "jsonb" USING "xash" FAMILY "jsonb_ops" AS
+	OPERATOR 1 =("jsonb","jsonb"),
+	FUNCTION 1 "jsonb_hash",
+	FUNCTION 2 "jsonb_hash_extended";
diff --git a/src/test/modules/xash/xash.conf b/src/test/modules/xash/xash.conf
new file mode 100644
index 00000000000..0b9f46dfb03
--- /dev/null
+++ b/src/test/modules/xash/xash.conf
@@ -0,0 +1 @@
+shared_preload_libraries = 'xash'
diff --git a/src/test/modules/xash/xash.control b/src/test/modules/xash/xash.control
new file mode 100644
index 00000000000..254fcb29144
--- /dev/null
+++ b/src/test/modules/xash/xash.control
@@ -0,0 +1,5 @@
+# xash extension
+comment = 'test copy of hash'
+default_version = '1.0'
+module_pathname = '$libdir/xash'
+relocatable = true
diff --git a/src/test/modules/xtree/.gitignore b/src/test/modules/xtree/.gitignore
new file mode 100644
index 00000000000..eaf4d5699ed
--- /dev/null
+++ b/src/test/modules/xtree/.gitignore
@@ -0,0 +1,5 @@
+/log/
+/output_iso/
+/results/
+/tmp_check/
+/tmp_check_iso/
diff --git a/src/test/modules/xtree/Makefile b/src/test/modules/xtree/Makefile
new file mode 100644
index 00000000000..da2ccbc2f8c
--- /dev/null
+++ b/src/test/modules/xtree/Makefile
@@ -0,0 +1,69 @@
+# xtree/Makefile
+
+REGRESS_SHLIB=$(abs_top_builddir)/src/test/regress/regress$(DLSUFFIX)
+export REGRESS_SHLIB
+
+MODULE_big = xtree
+OBJS = $(WIN32RES) access/xtree.o module.o
+
+EXTENSION = xtree
+DATA = xtree--1.0.sql
+PGFILEDESC = "test copy of btree"
+
+LDFLAGS_SL += $(filter -lm, $(LIBS))
+
+# If the caller supplied a pklibdir, use that.  The build system will supply
+# this, and perhaps also folks making installcheck from pgxs, and maybe others.
+#
+ifdef pkglibdir
+REGRESS_OPTS = --dlpath=$(pkglibdir)
+else
+REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
+endif
+
+# The xtree module does not supply its own regression tests.  Instead, it copies
+# regression tests from elsewhere in the project and changes hash indexes
+# contained within those tests to xtree instead.  The tests to use are parsed
+# from parallel_schedule and isolation_schedule, as below.
+CLONE_SCRIPT=../../../tools/clone_tests.pl
+REGRESS_BY_REFERENCE=$(shell $(PERL) $(CLONE_SCRIPT) --schedule=../../regress/parallel_schedule)
+ISOLATION_BY_REFERENCE=$(shell $(PERL) $(CLONE_SCRIPT) --schedule=../../isolation/isolation_schedule)
+
+REGRESS_OPTS += --load-extension=xtree
+REGRESS_OPTS += --temp-config $(srcdir)/xtree.conf
+REGRESS = $(REGRESS_BY_REFERENCE)
+
+ISOLATION_OPTS = --load-extension=xtree
+ISOLATION_OPTS += --inputdir=isolation
+ISOLATION_OPTS += --temp-config $(srcdir)/xtree.conf
+ISOLATION = $(ISOLATION_BY_REFERENCE)
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/xtree
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
+
+$(OBJS): tests_by_reference
+
+.PHONY: tests_by_reference
+tests_by_reference:
+	$(PERL) $(CLONE_SCRIPT) \
+		--regress="$(REGRESS_BY_REFERENCE)" \
+		--isolation="$(ISOLATION_BY_REFERENCE)" \
+		--src_idx_am=btree \
+		--dst_idx_am=xtree \
+		--src_sql=../../regress/sql \
+		--src_expected=../../regress/expected \
+		--dst_sql=./sql \
+		--dst_expected=./expected \
+		--src_spec=../../isolation/specs \
+		--src_iso_expected=../../isolation/expected \
+		--dst_spec=./isolation/specs \
+		--dst_iso_expected=./isolation/expected \
+		--datadir_prefix="/../../regress"
diff --git a/src/test/modules/xtree/access/xtree.c b/src/test/modules/xtree/access/xtree.c
new file mode 100644
index 00000000000..9323a2809ae
--- /dev/null
+++ b/src/test/modules/xtree/access/xtree.c
@@ -0,0 +1,81 @@
+/*-------------------------------------------------------------------------
+ *
+ * xtree.c
+ *	  Implementation of an index AM by reference to B-Tree.
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/test/modules/xtree/access/xtree.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+#include "access/nbtree.h"
+#include "commands/vacuum.h"
+#include "utils/index_selfuncs.h"
+
+/*
+ * Xtree handler function: return IndexAmRoutine with access method parameters
+ * and callbacks.
+ */
+PG_FUNCTION_INFO_V1(xtree_indexam_handler);
+Datum
+xtree_indexam_handler(PG_FUNCTION_ARGS)
+{
+	IndexAmRoutine *amroutine = makeNode(IndexAmRoutine);
+
+	amroutine->amstrategies = BTMaxStrategyNumber;
+	amroutine->amsupport = BTNProcs;
+	amroutine->amoptsprocnum = BTOPTIONS_PROC;
+	amroutine->amcanorder = true;
+	amroutine->amcanorderbyop = false;
+	amroutine->amcanbackward = true;
+	amroutine->amcanunique = true;
+	amroutine->amcanmulticol = true;
+	amroutine->amoptionalkey = true;
+	amroutine->amsearcharray = true;
+	amroutine->amsearchnulls = true;
+	amroutine->amstorage = false;
+	amroutine->amclusterable = true;
+	amroutine->ampredlocks = true;
+	amroutine->amcanparallel = true;
+	amroutine->amtranslatestrategy = bttranslatestrategy;
+	amroutine->amtranslatecmptype = bttranslatecmptype;
+	amroutine->amcanbuildparallel = true;
+	amroutine->amcaninclude = true;
+	amroutine->amusemaintenanceworkmem = false;
+	amroutine->amsummarizing = false;
+	amroutine->amparallelvacuumoptions =
+		VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_COND_CLEANUP;
+	amroutine->amkeytype = InvalidOid;
+
+	amroutine->ambuild = btbuild;
+	amroutine->ambuildempty = btbuildempty;
+	amroutine->aminsert = btinsert;
+	amroutine->aminsertcleanup = NULL;
+	amroutine->ambulkdelete = btbulkdelete;
+	amroutine->amvacuumcleanup = btvacuumcleanup;
+	amroutine->amcanreturn = btcanreturn;
+	amroutine->amcostestimate = btcostestimate;
+	amroutine->amgettreeheight = btgettreeheight;
+	amroutine->amoptions = btoptions;
+	amroutine->amproperty = btproperty;
+	amroutine->ambuildphasename = btbuildphasename;
+	amroutine->amvalidate = btvalidate;
+	amroutine->amadjustmembers = btadjustmembers;
+	amroutine->ambeginscan = btbeginscan;
+	amroutine->amrescan = btrescan;
+	amroutine->amgettuple = btgettuple;
+	amroutine->amgetbitmap = btgetbitmap;
+	amroutine->amendscan = btendscan;
+	amroutine->ammarkpos = btmarkpos;
+	amroutine->amrestrpos = btrestrpos;
+	amroutine->amestimateparallelscan = btestimateparallelscan;
+	amroutine->aminitparallelscan = btinitparallelscan;
+	amroutine->amparallelrescan = btparallelrescan;
+
+	PG_RETURN_POINTER(amroutine);
+}
diff --git a/src/test/modules/xtree/expected/.gitignore b/src/test/modules/xtree/expected/.gitignore
new file mode 100644
index 00000000000..e87afd97c3b
--- /dev/null
+++ b/src/test/modules/xtree/expected/.gitignore
@@ -0,0 +1 @@
+/*.out
diff --git a/src/test/modules/xtree/isolation/expected/.gitignore b/src/test/modules/xtree/isolation/expected/.gitignore
new file mode 100644
index 00000000000..e87afd97c3b
--- /dev/null
+++ b/src/test/modules/xtree/isolation/expected/.gitignore
@@ -0,0 +1 @@
+/*.out
diff --git a/src/test/modules/xtree/isolation/specs/.gitignore b/src/test/modules/xtree/isolation/specs/.gitignore
new file mode 100644
index 00000000000..e9404350671
--- /dev/null
+++ b/src/test/modules/xtree/isolation/specs/.gitignore
@@ -0,0 +1 @@
+/*.spec
diff --git a/src/test/modules/xtree/module.c b/src/test/modules/xtree/module.c
new file mode 100644
index 00000000000..b39aa22750e
--- /dev/null
+++ b/src/test/modules/xtree/module.c
@@ -0,0 +1,17 @@
+/*-------------------------------------------------------------------------
+ *
+ * module.c
+ *		EnterpriseDB xtree plugin.
+ *
+ * Portions Copyright (c) 2022, EnterpriseDB Corporation. All Rights Reserved.
+ *
+ * IDENTIFICATION
+ *	  xtree/module.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "fmgr.h"
+
+PG_MODULE_MAGIC;
diff --git a/src/test/modules/xtree/sql/.gitignore b/src/test/modules/xtree/sql/.gitignore
new file mode 100644
index 00000000000..606aaaa1c26
--- /dev/null
+++ b/src/test/modules/xtree/sql/.gitignore
@@ -0,0 +1 @@
+/*.sql
diff --git a/src/test/modules/xtree/xtree--1.0.sql b/src/test/modules/xtree/xtree--1.0.sql
new file mode 100644
index 00000000000..f5f745ecea6
--- /dev/null
+++ b/src/test/modules/xtree/xtree--1.0.sql
@@ -0,0 +1,714 @@
+/* xtree/xtree--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION xtree" to load this file. \quit
+
+CREATE FUNCTION xtree_indexam_handler(internal)
+RETURNS index_am_handler
+AS 'MODULE_PATHNAME'
+LANGUAGE C;
+
+-- Access method
+CREATE ACCESS METHOD xtree TYPE INDEX HANDLER xtree_indexam_handler;
+COMMENT ON ACCESS METHOD xtree IS 'test copy of btree';
+
+
+-- Operator families
+CREATE OPERATOR FAMILY "array_ops" USING "xtree";
+
+CREATE OPERATOR FAMILY "bit_ops" USING "xtree";
+
+CREATE OPERATOR FAMILY "bool_ops" USING "xtree";
+
+CREATE OPERATOR FAMILY "bpchar_ops" USING "xtree";
+
+CREATE OPERATOR FAMILY "bytea_ops" USING "xtree";
+
+CREATE OPERATOR FAMILY "char_ops" USING "xtree";
+
+CREATE OPERATOR FAMILY "datetime_ops" USING "xtree";
+
+ALTER OPERATOR FAMILY "datetime_ops" USING "xtree" ADD
+	OPERATOR 1 <("date","timestamp"),
+	OPERATOR 2 <=("date","timestamp"),
+	OPERATOR 3 =("date","timestamp"),
+	OPERATOR 4 >=("date","timestamp"),
+	OPERATOR 5 >("date","timestamp"),
+	OPERATOR 1 <("date","timestamptz"),
+	OPERATOR 2 <=("date","timestamptz"),
+	OPERATOR 3 =("date","timestamptz"),
+	OPERATOR 4 >=("date","timestamptz"),
+	OPERATOR 5 >("date","timestamptz"),
+	OPERATOR 1 <("timestamp","date"),
+	OPERATOR 2 <=("timestamp","date"),
+	OPERATOR 3 =("timestamp","date"),
+	OPERATOR 4 >=("timestamp","date"),
+	OPERATOR 5 >("timestamp","date"),
+	OPERATOR 1 <("timestamp","timestamptz"),
+	OPERATOR 2 <=("timestamp","timestamptz"),
+	OPERATOR 3 =("timestamp","timestamptz"),
+	OPERATOR 4 >=("timestamp","timestamptz"),
+	OPERATOR 5 >("timestamp","timestamptz"),
+	OPERATOR 1 <("timestamptz","date"),
+	OPERATOR 2 <=("timestamptz","date"),
+	OPERATOR 3 =("timestamptz","date"),
+	OPERATOR 4 >=("timestamptz","date"),
+	OPERATOR 5 >("timestamptz","date"),
+	OPERATOR 1 <("timestamptz","timestamp"),
+	OPERATOR 2 <=("timestamptz","timestamp"),
+	OPERATOR 3 =("timestamptz","timestamp"),
+	OPERATOR 4 >=("timestamptz","timestamp"),
+	OPERATOR 5 >("timestamptz","timestamp"),
+	FUNCTION 1 ("date", "timestamp") "date_cmp_timestamp",
+	FUNCTION 1 ("date", "timestamptz") "date_cmp_timestamptz",
+	FUNCTION 1 ("timestamp", "date") "timestamp_cmp_date",
+	FUNCTION 1 ("timestamp", "timestamptz") "timestamp_cmp_timestamptz",
+	FUNCTION 1 ("timestamptz", "date") "timestamptz_cmp_date",
+	FUNCTION 1 ("timestamptz", "timestamp") "timestamptz_cmp_timestamp",
+	FUNCTION 3 ("date", "interval") "in_range"("date","date","interval","bool","bool"),
+	FUNCTION 3 ("timestamp", "interval") "in_range"("timestamp","timestamp","interval","bool","bool"),
+	FUNCTION 3 ("timestamptz", "interval") "in_range"("timestamptz","timestamptz","interval","bool","bool");
+
+
+CREATE OPERATOR FAMILY "float_ops" USING "xtree";
+
+ALTER OPERATOR FAMILY "float_ops" USING "xtree" ADD
+	OPERATOR 1 <("float4","float8"),
+	OPERATOR 2 <=("float4","float8"),
+	OPERATOR 3 =("float4","float8"),
+	OPERATOR 4 >=("float4","float8"),
+	OPERATOR 5 >("float4","float8"),
+	OPERATOR 1 <("float8","float4"),
+	OPERATOR 2 <=("float8","float4"),
+	OPERATOR 3 =("float8","float4"),
+	OPERATOR 4 >=("float8","float4"),
+	OPERATOR 5 >("float8","float4"),
+	FUNCTION 1 ("float4", "float8") "btfloat48cmp",
+	FUNCTION 1 ("float8", "float4") "btfloat84cmp",
+	FUNCTION 3 ("float4", "float8") "in_range"("float4","float4","float8","bool","bool");
+
+
+CREATE OPERATOR FAMILY "network_ops" USING "xtree";
+
+CREATE OPERATOR FAMILY "integer_ops" USING "xtree";
+
+ALTER OPERATOR FAMILY "integer_ops" USING "xtree" ADD
+	OPERATOR 1 <("int2","int4"),
+	OPERATOR 2 <=("int2","int4"),
+	OPERATOR 3 =("int2","int4"),
+	OPERATOR 4 >=("int2","int4"),
+	OPERATOR 5 >("int2","int4"),
+	OPERATOR 1 <("int2","int8"),
+	OPERATOR 2 <=("int2","int8"),
+	OPERATOR 3 =("int2","int8"),
+	OPERATOR 4 >=("int2","int8"),
+	OPERATOR 5 >("int2","int8"),
+	OPERATOR 1 <("int4","int2"),
+	OPERATOR 2 <=("int4","int2"),
+	OPERATOR 3 =("int4","int2"),
+	OPERATOR 4 >=("int4","int2"),
+	OPERATOR 5 >("int4","int2"),
+	OPERATOR 1 <("int4","int8"),
+	OPERATOR 2 <=("int4","int8"),
+	OPERATOR 3 =("int4","int8"),
+	OPERATOR 4 >=("int4","int8"),
+	OPERATOR 5 >("int4","int8"),
+	OPERATOR 1 <("int8","int2"),
+	OPERATOR 2 <=("int8","int2"),
+	OPERATOR 3 =("int8","int2"),
+	OPERATOR 4 >=("int8","int2"),
+	OPERATOR 5 >("int8","int2"),
+	OPERATOR 1 <("int8","int4"),
+	OPERATOR 2 <=("int8","int4"),
+	OPERATOR 3 =("int8","int4"),
+	OPERATOR 4 >=("int8","int4"),
+	OPERATOR 5 >("int8","int4"),
+	FUNCTION 1 ("int2", "int4") "btint24cmp",
+	FUNCTION 1 ("int2", "int8") "btint28cmp",
+	FUNCTION 3 ("int2", "int8") "in_range"("int2","int2","int8","bool","bool"),
+	FUNCTION 3 ("int2", "int4") "in_range"("int2","int2","int4","bool","bool"),
+	FUNCTION 1 ("int4", "int8") "btint48cmp",
+	FUNCTION 1 ("int4", "int2") "btint42cmp",
+	FUNCTION 3 ("int4", "int8") "in_range"("int4","int4","int8","bool","bool"),
+	FUNCTION 3 ("int4", "int2") "in_range"("int4","int4","int2","bool","bool"),
+	FUNCTION 1 ("int8", "int4") "btint84cmp",
+	FUNCTION 1 ("int8", "int2") "btint82cmp";
+
+
+CREATE OPERATOR FAMILY "interval_ops" USING "xtree";
+
+CREATE OPERATOR FAMILY "macaddr_ops" USING "xtree";
+
+CREATE OPERATOR FAMILY "macaddr8_ops" USING "xtree";
+
+CREATE OPERATOR FAMILY "numeric_ops" USING "xtree";
+
+CREATE OPERATOR FAMILY "oid_ops" USING "xtree";
+
+CREATE OPERATOR FAMILY "oidvector_ops" USING "xtree";
+
+CREATE OPERATOR FAMILY "record_ops" USING "xtree";
+
+CREATE OPERATOR FAMILY "record_image_ops" USING "xtree";
+
+CREATE OPERATOR FAMILY "text_ops" USING "xtree";
+
+ALTER OPERATOR FAMILY "text_ops" USING "xtree" ADD
+	OPERATOR 1 <("name","text"),
+	OPERATOR 2 <=("name","text"),
+	OPERATOR 3 =("name","text"),
+	OPERATOR 4 >=("name","text"),
+	OPERATOR 5 >("name","text"),
+	OPERATOR 1 <("text","name"),
+	OPERATOR 2 <=("text","name"),
+	OPERATOR 3 =("text","name"),
+	OPERATOR 4 >=("text","name"),
+	OPERATOR 5 >("text","name"),
+	FUNCTION 1 ("name", "text") "btnametextcmp",
+	FUNCTION 1 ("text", "name") "bttextnamecmp";
+
+
+CREATE OPERATOR FAMILY "time_ops" USING "xtree";
+
+ALTER OPERATOR FAMILY "time_ops" USING "xtree" ADD
+	FUNCTION 3 ("time", "interval") "in_range"("time","time","interval","bool","bool");
+
+
+CREATE OPERATOR FAMILY "timetz_ops" USING "xtree";
+
+ALTER OPERATOR FAMILY "timetz_ops" USING "xtree" ADD
+	FUNCTION 3 ("timetz", "interval") "in_range"("timetz","timetz","interval","bool","bool");
+
+
+CREATE OPERATOR FAMILY "varbit_ops" USING "xtree";
+
+CREATE OPERATOR FAMILY "text_pattern_ops" USING "xtree";
+
+CREATE OPERATOR FAMILY "bpchar_pattern_ops" USING "xtree";
+
+CREATE OPERATOR FAMILY "money_ops" USING "xtree";
+
+CREATE OPERATOR FAMILY "tid_ops" USING "xtree";
+
+CREATE OPERATOR FAMILY "xid8_ops" USING "xtree";
+
+CREATE OPERATOR FAMILY "uuid_ops" USING "xtree";
+
+CREATE OPERATOR FAMILY "pg_lsn_ops" USING "xtree";
+
+CREATE OPERATOR FAMILY "enum_ops" USING "xtree";
+
+CREATE OPERATOR FAMILY "tsvector_ops" USING "xtree";
+
+CREATE OPERATOR FAMILY "tsquery_ops" USING "xtree";
+
+CREATE OPERATOR FAMILY "range_ops" USING "xtree";
+
+CREATE OPERATOR FAMILY "jsonb_ops" USING "xtree";
+
+CREATE OPERATOR FAMILY "multirange_ops" USING "xtree";
+
+-- Operator classes
+
+CREATE OPERATOR CLASS "array_ops"
+	DEFAULT FOR TYPE "anyarray" USING "xtree" FAMILY "array_ops" AS
+	OPERATOR 1 <("anyarray","anyarray"),
+	OPERATOR 2 <=("anyarray","anyarray"),
+	OPERATOR 3 =("anyarray","anyarray"),
+	OPERATOR 4 >=("anyarray","anyarray"),
+	OPERATOR 5 >("anyarray","anyarray"),
+	FUNCTION 1 "btarraycmp";
+
+
+
+CREATE OPERATOR CLASS "bit_ops"
+	DEFAULT FOR TYPE "bit" USING "xtree" FAMILY "bit_ops" AS
+	OPERATOR 1 <("bit","bit"),
+	OPERATOR 2 <=("bit","bit"),
+	OPERATOR 3 =("bit","bit"),
+	OPERATOR 4 >=("bit","bit"),
+	OPERATOR 5 >("bit","bit"),
+	FUNCTION 1 "bitcmp",
+	FUNCTION 4 "btequalimage";
+
+
+
+CREATE OPERATOR CLASS "bool_ops"
+	DEFAULT FOR TYPE "bool" USING "xtree" FAMILY "bool_ops" AS
+	OPERATOR 1 <("bool","bool"),
+	OPERATOR 2 <=("bool","bool"),
+	OPERATOR 3 =("bool","bool"),
+	OPERATOR 4 >=("bool","bool"),
+	OPERATOR 5 >("bool","bool"),
+	FUNCTION 1 "btboolcmp",
+	FUNCTION 4 "btequalimage";
+
+
+
+CREATE OPERATOR CLASS "bpchar_ops"
+	DEFAULT FOR TYPE "bpchar" USING "xtree" FAMILY "bpchar_ops" AS
+	OPERATOR 1 <("bpchar","bpchar"),
+	OPERATOR 2 <=("bpchar","bpchar"),
+	OPERATOR 3 =("bpchar","bpchar"),
+	OPERATOR 4 >=("bpchar","bpchar"),
+	OPERATOR 5 >("bpchar","bpchar"),
+	FUNCTION 1 "bpcharcmp",
+	FUNCTION 2 "bpchar_sortsupport",
+	FUNCTION 4 "btvarstrequalimage";
+
+
+
+CREATE OPERATOR CLASS "bytea_ops"
+	DEFAULT FOR TYPE "bytea" USING "xtree" FAMILY "bytea_ops" AS
+	OPERATOR 1 <("bytea","bytea"),
+	OPERATOR 2 <=("bytea","bytea"),
+	OPERATOR 3 =("bytea","bytea"),
+	OPERATOR 4 >=("bytea","bytea"),
+	OPERATOR 5 >("bytea","bytea"),
+	FUNCTION 1 "byteacmp",
+	FUNCTION 2 "bytea_sortsupport",
+	FUNCTION 4 "btequalimage";
+
+
+
+CREATE OPERATOR CLASS "char_ops"
+	DEFAULT FOR TYPE "char" USING "xtree" FAMILY "char_ops" AS
+	OPERATOR 1 <("char","char"),
+	OPERATOR 2 <=("char","char"),
+	OPERATOR 3 =("char","char"),
+	OPERATOR 4 >=("char","char"),
+	OPERATOR 5 >("char","char"),
+	FUNCTION 1 "btcharcmp",
+	FUNCTION 4 "btequalimage";
+
+
+
+CREATE OPERATOR CLASS "date_ops"
+	DEFAULT FOR TYPE "date" USING "xtree" FAMILY "datetime_ops" AS
+	OPERATOR 1 <("date","date"),
+	OPERATOR 2 <=("date","date"),
+	OPERATOR 3 =("date","date"),
+	OPERATOR 4 >=("date","date"),
+	OPERATOR 5 >("date","date"),
+	FUNCTION 1 "date_cmp",
+	FUNCTION 2 "date_sortsupport",
+	FUNCTION 4 "btequalimage";
+
+
+
+CREATE OPERATOR CLASS "float4_ops"
+	DEFAULT FOR TYPE "float4" USING "xtree" FAMILY "float_ops" AS
+	OPERATOR 1 <("float4","float4"),
+	OPERATOR 2 <=("float4","float4"),
+	OPERATOR 3 =("float4","float4"),
+	OPERATOR 4 >=("float4","float4"),
+	OPERATOR 5 >("float4","float4"),
+	FUNCTION 1 "btfloat4cmp",
+	FUNCTION 2 "btfloat4sortsupport";
+
+
+
+CREATE OPERATOR CLASS "float8_ops"
+	DEFAULT FOR TYPE "float8" USING "xtree" FAMILY "float_ops" AS
+	OPERATOR 1 <("float8","float8"),
+	OPERATOR 2 <=("float8","float8"),
+	OPERATOR 3 =("float8","float8"),
+	OPERATOR 4 >=("float8","float8"),
+	OPERATOR 5 >("float8","float8"),
+	FUNCTION 1 "btfloat8cmp",
+	FUNCTION 2 "btfloat8sortsupport",
+	FUNCTION 3 "in_range"("float8","float8","float8","bool","bool");
+
+
+
+CREATE OPERATOR CLASS "inet_ops"
+	DEFAULT FOR TYPE "inet" USING "xtree" FAMILY "network_ops" AS
+	OPERATOR 1 <("inet","inet"),
+	OPERATOR 2 <=("inet","inet"),
+	OPERATOR 3 =("inet","inet"),
+	OPERATOR 4 >=("inet","inet"),
+	OPERATOR 5 >("inet","inet"),
+	FUNCTION 1 "network_cmp",
+	FUNCTION 2 "network_sortsupport",
+	FUNCTION 4 "btequalimage";
+
+
+
+CREATE OPERATOR CLASS "int2_ops"
+	DEFAULT FOR TYPE "int2" USING "xtree" FAMILY "integer_ops" AS
+	OPERATOR 1 <("int2","int2"),
+	OPERATOR 2 <=("int2","int2"),
+	OPERATOR 3 =("int2","int2"),
+	OPERATOR 4 >=("int2","int2"),
+	OPERATOR 5 >("int2","int2"),
+	FUNCTION 1 "btint2cmp",
+	FUNCTION 2 "btint2sortsupport",
+	FUNCTION 4 "btequalimage",
+	FUNCTION 3 "in_range"("int2","int2","int2","bool","bool");
+
+
+
+CREATE OPERATOR CLASS "int4_ops"
+	DEFAULT FOR TYPE "int4" USING "xtree" FAMILY "integer_ops" AS
+	OPERATOR 1 <("int4","int4"),
+	OPERATOR 2 <=("int4","int4"),
+	OPERATOR 3 =("int4","int4"),
+	OPERATOR 4 >=("int4","int4"),
+	OPERATOR 5 >("int4","int4"),
+	FUNCTION 1 "btint4cmp",
+	FUNCTION 2 "btint4sortsupport",
+	FUNCTION 4 "btequalimage",
+	FUNCTION 3 "in_range"("int4","int4","int4","bool","bool");
+
+
+
+CREATE OPERATOR CLASS "int8_ops"
+	DEFAULT FOR TYPE "int8" USING "xtree" FAMILY "integer_ops" AS
+	OPERATOR 1 <("int8","int8"),
+	OPERATOR 2 <=("int8","int8"),
+	OPERATOR 3 =("int8","int8"),
+	OPERATOR 4 >=("int8","int8"),
+	OPERATOR 5 >("int8","int8"),
+	FUNCTION 1 "btint8cmp",
+	FUNCTION 2 "btint8sortsupport",
+	FUNCTION 4 "btequalimage",
+	FUNCTION 3 "in_range"("int8","int8","int8","bool","bool");
+
+
+
+CREATE OPERATOR CLASS "interval_ops"
+	DEFAULT FOR TYPE "interval" USING "xtree" FAMILY "interval_ops" AS
+	OPERATOR 1 <("interval","interval"),
+	OPERATOR 2 <=("interval","interval"),
+	OPERATOR 3 =("interval","interval"),
+	OPERATOR 4 >=("interval","interval"),
+	OPERATOR 5 >("interval","interval"),
+	FUNCTION 1 "interval_cmp",
+	FUNCTION 3 "in_range"("interval","interval","interval","bool","bool");
+
+
+
+CREATE OPERATOR CLASS "macaddr_ops"
+	DEFAULT FOR TYPE "macaddr" USING "xtree" FAMILY "macaddr_ops" AS
+	OPERATOR 1 <("macaddr","macaddr"),
+	OPERATOR 2 <=("macaddr","macaddr"),
+	OPERATOR 3 =("macaddr","macaddr"),
+	OPERATOR 4 >=("macaddr","macaddr"),
+	OPERATOR 5 >("macaddr","macaddr"),
+	FUNCTION 1 "macaddr_cmp",
+	FUNCTION 2 "macaddr_sortsupport",
+	FUNCTION 4 "btequalimage";
+
+
+
+CREATE OPERATOR CLASS "macaddr8_ops"
+	DEFAULT FOR TYPE "macaddr8" USING "xtree" FAMILY "macaddr8_ops" AS
+	OPERATOR 1 <("macaddr8","macaddr8"),
+	OPERATOR 2 <=("macaddr8","macaddr8"),
+	OPERATOR 3 =("macaddr8","macaddr8"),
+	OPERATOR 4 >=("macaddr8","macaddr8"),
+	OPERATOR 5 >("macaddr8","macaddr8"),
+	FUNCTION 1 "macaddr8_cmp",
+	FUNCTION 4 "btequalimage";
+
+
+
+CREATE OPERATOR CLASS "name_ops"
+	DEFAULT FOR TYPE "name" USING "xtree" FAMILY "text_ops" AS
+	OPERATOR 1 <("name","name"),
+	OPERATOR 2 <=("name","name"),
+	OPERATOR 3 =("name","name"),
+	OPERATOR 4 >=("name","name"),
+	OPERATOR 5 >("name","name"),
+	FUNCTION 1 "btnamecmp",
+	FUNCTION 2 "btnamesortsupport",
+	FUNCTION 4 "btvarstrequalimage";
+
+
+
+CREATE OPERATOR CLASS "numeric_ops"
+	DEFAULT FOR TYPE "numeric" USING "xtree" FAMILY "numeric_ops" AS
+	OPERATOR 1 <("numeric","numeric"),
+	OPERATOR 2 <=("numeric","numeric"),
+	OPERATOR 3 =("numeric","numeric"),
+	OPERATOR 4 >=("numeric","numeric"),
+	OPERATOR 5 >("numeric","numeric"),
+	FUNCTION 1 "numeric_cmp",
+	FUNCTION 2 "numeric_sortsupport",
+	FUNCTION 3 "in_range"("numeric","numeric","numeric","bool","bool");
+
+
+
+CREATE OPERATOR CLASS "oid_ops"
+	DEFAULT FOR TYPE "oid" USING "xtree" FAMILY "oid_ops" AS
+	OPERATOR 1 <("oid","oid"),
+	OPERATOR 2 <=("oid","oid"),
+	OPERATOR 3 =("oid","oid"),
+	OPERATOR 4 >=("oid","oid"),
+	OPERATOR 5 >("oid","oid"),
+	FUNCTION 1 "btoidcmp",
+	FUNCTION 2 "btoidsortsupport",
+	FUNCTION 4 "btequalimage";
+
+
+
+CREATE OPERATOR CLASS "oidvector_ops"
+	DEFAULT FOR TYPE "oidvector" USING "xtree" FAMILY "oidvector_ops" AS
+	OPERATOR 1 <("oidvector","oidvector"),
+	OPERATOR 2 <=("oidvector","oidvector"),
+	OPERATOR 3 =("oidvector","oidvector"),
+	OPERATOR 4 >=("oidvector","oidvector"),
+	OPERATOR 5 >("oidvector","oidvector"),
+	FUNCTION 1 "btoidvectorcmp",
+	FUNCTION 4 "btequalimage";
+
+
+
+CREATE OPERATOR CLASS "record_ops"
+	DEFAULT FOR TYPE "record" USING "xtree" FAMILY "record_ops" AS
+	OPERATOR 1 <("record","record"),
+	OPERATOR 2 <=("record","record"),
+	OPERATOR 3 =("record","record"),
+	OPERATOR 4 >=("record","record"),
+	OPERATOR 5 >("record","record"),
+	FUNCTION 1 "btrecordcmp";
+
+
+
+CREATE OPERATOR CLASS "record_image_ops"
+	FOR TYPE "record" USING "xtree" FAMILY "record_image_ops" AS
+	OPERATOR 1 *<("record","record"),
+	OPERATOR 2 *<=("record","record"),
+	OPERATOR 3 *=("record","record"),
+	OPERATOR 4 *>=("record","record"),
+	OPERATOR 5 *>("record","record"),
+	FUNCTION 1 "btrecordimagecmp";
+
+
+
+CREATE OPERATOR CLASS "text_ops"
+	DEFAULT FOR TYPE "text" USING "xtree" FAMILY "text_ops" AS
+	OPERATOR 1 <("text","text"),
+	OPERATOR 2 <=("text","text"),
+	OPERATOR 3 =("text","text"),
+	OPERATOR 4 >=("text","text"),
+	OPERATOR 5 >("text","text"),
+	FUNCTION 1 "bttextcmp",
+	FUNCTION 2 "bttextsortsupport",
+	FUNCTION 4 "btvarstrequalimage";
+
+
+
+CREATE OPERATOR CLASS "time_ops"
+	DEFAULT FOR TYPE "time" USING "xtree" FAMILY "time_ops" AS
+	OPERATOR 1 <("time","time"),
+	OPERATOR 2 <=("time","time"),
+	OPERATOR 3 =("time","time"),
+	OPERATOR 4 >=("time","time"),
+	OPERATOR 5 >("time","time"),
+	FUNCTION 1 "time_cmp",
+	FUNCTION 4 "btequalimage";
+
+
+
+CREATE OPERATOR CLASS "timestamptz_ops"
+	DEFAULT FOR TYPE "timestamptz" USING "xtree" FAMILY "datetime_ops" AS
+	OPERATOR 1 <("timestamptz","timestamptz"),
+	OPERATOR 2 <=("timestamptz","timestamptz"),
+	OPERATOR 3 =("timestamptz","timestamptz"),
+	OPERATOR 4 >=("timestamptz","timestamptz"),
+	OPERATOR 5 >("timestamptz","timestamptz"),
+	FUNCTION 1 "timestamptz_cmp",
+	FUNCTION 2 "timestamp_sortsupport",
+	FUNCTION 4 "btequalimage";
+
+
+
+CREATE OPERATOR CLASS "timetz_ops"
+	DEFAULT FOR TYPE "timetz" USING "xtree" FAMILY "timetz_ops" AS
+	OPERATOR 1 <("timetz","timetz"),
+	OPERATOR 2 <=("timetz","timetz"),
+	OPERATOR 3 =("timetz","timetz"),
+	OPERATOR 4 >=("timetz","timetz"),
+	OPERATOR 5 >("timetz","timetz"),
+	FUNCTION 1 "timetz_cmp",
+	FUNCTION 4 "btequalimage";
+
+
+
+CREATE OPERATOR CLASS "varbit_ops"
+	DEFAULT FOR TYPE "varbit" USING "xtree" FAMILY "varbit_ops" AS
+	OPERATOR 1 <("varbit","varbit"),
+	OPERATOR 2 <=("varbit","varbit"),
+	OPERATOR 3 =("varbit","varbit"),
+	OPERATOR 4 >=("varbit","varbit"),
+	OPERATOR 5 >("varbit","varbit"),
+	FUNCTION 1 "varbitcmp",
+	FUNCTION 4 "btequalimage";
+
+
+
+CREATE OPERATOR CLASS "timestamp_ops"
+	DEFAULT FOR TYPE "timestamp" USING "xtree" FAMILY "datetime_ops" AS
+	OPERATOR 1 <("timestamp","timestamp"),
+	OPERATOR 2 <=("timestamp","timestamp"),
+	OPERATOR 3 =("timestamp","timestamp"),
+	OPERATOR 4 >=("timestamp","timestamp"),
+	OPERATOR 5 >("timestamp","timestamp"),
+	FUNCTION 1 "timestamp_cmp",
+	FUNCTION 2 "timestamp_sortsupport",
+	FUNCTION 4 "btequalimage";
+
+
+
+CREATE OPERATOR CLASS "text_pattern_ops"
+	FOR TYPE "text" USING "xtree" FAMILY "text_pattern_ops" AS
+	OPERATOR 1 ~<~("text","text"),
+	OPERATOR 2 ~<=~("text","text"),
+	OPERATOR 3 =("text","text"),
+	OPERATOR 4 ~>=~("text","text"),
+	OPERATOR 5 ~>~("text","text"),
+	FUNCTION 1 "bttext_pattern_cmp",
+	FUNCTION 2 "bttext_pattern_sortsupport",
+	FUNCTION 4 "btequalimage";
+
+
+
+CREATE OPERATOR CLASS "bpchar_pattern_ops"
+	FOR TYPE "bpchar" USING "xtree" FAMILY "bpchar_pattern_ops" AS
+	OPERATOR 1 ~<~("bpchar","bpchar"),
+	OPERATOR 2 ~<=~("bpchar","bpchar"),
+	OPERATOR 3 =("bpchar","bpchar"),
+	OPERATOR 4 ~>=~("bpchar","bpchar"),
+	OPERATOR 5 ~>~("bpchar","bpchar"),
+	FUNCTION 1 "btbpchar_pattern_cmp",
+	FUNCTION 2 "btbpchar_pattern_sortsupport",
+	FUNCTION 4 "btequalimage";
+
+
+
+CREATE OPERATOR CLASS "money_ops"
+	DEFAULT FOR TYPE "money" USING "xtree" FAMILY "money_ops" AS
+	OPERATOR 1 <("money","money"),
+	OPERATOR 2 <=("money","money"),
+	OPERATOR 3 =("money","money"),
+	OPERATOR 4 >=("money","money"),
+	OPERATOR 5 >("money","money"),
+	FUNCTION 1 "cash_cmp",
+	FUNCTION 4 "btequalimage";
+
+
+
+CREATE OPERATOR CLASS "tid_ops"
+	DEFAULT FOR TYPE "tid" USING "xtree" FAMILY "tid_ops" AS
+	OPERATOR 1 <("tid","tid"),
+	OPERATOR 2 <=("tid","tid"),
+	OPERATOR 3 =("tid","tid"),
+	OPERATOR 4 >=("tid","tid"),
+	OPERATOR 5 >("tid","tid"),
+	FUNCTION 1 "bttidcmp",
+	FUNCTION 4 "btequalimage";
+
+
+
+CREATE OPERATOR CLASS "xid8_ops"
+	DEFAULT FOR TYPE "xid8" USING "xtree" FAMILY "xid8_ops" AS
+	OPERATOR 1 <("xid8","xid8"),
+	OPERATOR 2 <=("xid8","xid8"),
+	OPERATOR 3 =("xid8","xid8"),
+	OPERATOR 4 >=("xid8","xid8"),
+	OPERATOR 5 >("xid8","xid8"),
+	FUNCTION 1 "xid8cmp",
+	FUNCTION 4 "btequalimage";
+
+
+
+CREATE OPERATOR CLASS "uuid_ops"
+	DEFAULT FOR TYPE "uuid" USING "xtree" FAMILY "uuid_ops" AS
+	OPERATOR 1 <("uuid","uuid"),
+	OPERATOR 2 <=("uuid","uuid"),
+	OPERATOR 3 =("uuid","uuid"),
+	OPERATOR 4 >=("uuid","uuid"),
+	OPERATOR 5 >("uuid","uuid"),
+	FUNCTION 1 "uuid_cmp",
+	FUNCTION 2 "uuid_sortsupport",
+	FUNCTION 4 "btequalimage";
+
+
+
+CREATE OPERATOR CLASS "pg_lsn_ops"
+	DEFAULT FOR TYPE "pg_lsn" USING "xtree" FAMILY "pg_lsn_ops" AS
+	OPERATOR 1 <("pg_lsn","pg_lsn"),
+	OPERATOR 2 <=("pg_lsn","pg_lsn"),
+	OPERATOR 3 =("pg_lsn","pg_lsn"),
+	OPERATOR 4 >=("pg_lsn","pg_lsn"),
+	OPERATOR 5 >("pg_lsn","pg_lsn"),
+	FUNCTION 1 "pg_lsn_cmp",
+	FUNCTION 4 "btequalimage";
+
+
+
+CREATE OPERATOR CLASS "enum_ops"
+	DEFAULT FOR TYPE "anyenum" USING "xtree" FAMILY "enum_ops" AS
+	OPERATOR 1 <("anyenum","anyenum"),
+	OPERATOR 2 <=("anyenum","anyenum"),
+	OPERATOR 3 =("anyenum","anyenum"),
+	OPERATOR 4 >=("anyenum","anyenum"),
+	OPERATOR 5 >("anyenum","anyenum"),
+	FUNCTION 1 "enum_cmp",
+	FUNCTION 4 "btequalimage";
+
+
+
+CREATE OPERATOR CLASS "tsvector_ops"
+	DEFAULT FOR TYPE "tsvector" USING "xtree" FAMILY "tsvector_ops" AS
+	OPERATOR 1 <("tsvector","tsvector"),
+	OPERATOR 2 <=("tsvector","tsvector"),
+	OPERATOR 3 =("tsvector","tsvector"),
+	OPERATOR 4 >=("tsvector","tsvector"),
+	OPERATOR 5 >("tsvector","tsvector"),
+	FUNCTION 1 "tsvector_cmp";
+
+
+
+CREATE OPERATOR CLASS "tsquery_ops"
+	DEFAULT FOR TYPE "tsquery" USING "xtree" FAMILY "tsquery_ops" AS
+	OPERATOR 1 <("tsquery","tsquery"),
+	OPERATOR 2 <=("tsquery","tsquery"),
+	OPERATOR 3 =("tsquery","tsquery"),
+	OPERATOR 4 >=("tsquery","tsquery"),
+	OPERATOR 5 >("tsquery","tsquery"),
+	FUNCTION 1 "tsquery_cmp";
+
+
+
+CREATE OPERATOR CLASS "range_ops"
+	DEFAULT FOR TYPE "anyrange" USING "xtree" FAMILY "range_ops" AS
+	OPERATOR 1 <("anyrange","anyrange"),
+	OPERATOR 2 <=("anyrange","anyrange"),
+	OPERATOR 3 =("anyrange","anyrange"),
+	OPERATOR 4 >=("anyrange","anyrange"),
+	OPERATOR 5 >("anyrange","anyrange"),
+	FUNCTION 1 "range_cmp";
+
+
+
+CREATE OPERATOR CLASS "multirange_ops"
+	DEFAULT FOR TYPE "anymultirange" USING "xtree" FAMILY "multirange_ops" AS
+	OPERATOR 1 <("anymultirange","anymultirange"),
+	OPERATOR 2 <=("anymultirange","anymultirange"),
+	OPERATOR 3 =("anymultirange","anymultirange"),
+	OPERATOR 4 >=("anymultirange","anymultirange"),
+	OPERATOR 5 >("anymultirange","anymultirange"),
+	FUNCTION 1 "multirange_cmp";
+
+
+
+CREATE OPERATOR CLASS "jsonb_ops"
+	DEFAULT FOR TYPE "jsonb" USING "xtree" FAMILY "jsonb_ops" AS
+	OPERATOR 1 <("jsonb","jsonb"),
+	OPERATOR 2 <=("jsonb","jsonb"),
+	OPERATOR 3 =("jsonb","jsonb"),
+	OPERATOR 4 >=("jsonb","jsonb"),
+	OPERATOR 5 >("jsonb","jsonb"),
+	FUNCTION 1 "jsonb_cmp";
diff --git a/src/test/modules/xtree/xtree.conf b/src/test/modules/xtree/xtree.conf
new file mode 100644
index 00000000000..5d990b84729
--- /dev/null
+++ b/src/test/modules/xtree/xtree.conf
@@ -0,0 +1 @@
+shared_preload_libraries = 'xtree'
diff --git a/src/test/modules/xtree/xtree.control b/src/test/modules/xtree/xtree.control
new file mode 100644
index 00000000000..0edd4b286d8
--- /dev/null
+++ b/src/test/modules/xtree/xtree.control
@@ -0,0 +1,5 @@
+# xtree extension
+comment = 'test copy of btree'
+default_version = '1.0'
+module_pathname = '$libdir/xtree'
+relocatable = true
diff --git a/src/tools/clone_tests.pl b/src/tools/clone_tests.pl
new file mode 100755
index 00000000000..c1c50ad90b0
--- /dev/null
+++ b/src/tools/clone_tests.pl
@@ -0,0 +1,449 @@
+# Make modified copies of regression tests from elsewhere in the source tree,
+# for use in testing a loadable module.  This copying mechanism saves us the
+# bloat of committing the same regression tests in the modules as elsewhere.
+#
+# Copyright (c) 2000-2024, PostgreSQL Global Development Group
+
+=pod
+
+=head1 NAME
+
+clone_tests.pl - script for copying regression and isolation test files from
+elsewhere in the postgresql source tree to a module, modifying the test to
+exercise one or more table or index AMs provided by the module.
+
+=head1 SYNOPSIS
+
+  copy_regression_test.pl \
+    --src_idx_am=btree \
+    --dst_idx_am=xtree \
+	--schedule=../../regress/parallel_schedule
+	--regress=../../regress/sql
+	--datadir_prefix=/../../regress
+
+  copy_regression_test.pl --schedule=../../regress/parallel_schedule
+
+=head1 OPTIONS
+
+=over
+
+=item --src_tbl_am
+
+=item --dst_tbl_am
+
+Optional argument pairs.  Multiple pairs may be specified.
+
+The name of a table AM used (or potentially used) in the original test for
+which we'd like to use a different table AM in the modified test.  For each
+src_tbl_am specified, there should be a dst_tbl_am also given, with the order
+of the arguments determining which source maps to which destination.
+
+=item --src_idx_am
+
+=item --dst_idx_am
+
+Like src_tbl_am and dst_tbl_am, but for index AMs.
+
+=item --schedule
+
+A schedule file listing all regression or isolation tests.  If given, a space
+separated list of all tests, in order, will be printed on STDOUT.
+
+=item --regress
+
+=item --isolation
+
+A space separated list of regression or isolation tests to be processed, such
+as the list returned by this script when handed a regression tests schedule
+file via the C<--schedule> option.
+
+=item --src_sql
+
+=item --src_expected
+
+The directory path to read the regression tests' .sql and .out files.
+
+=item --src_spec
+
+=item --src_iso_expected
+
+The directory path to read the isolation tests' .sql and .out files.
+
+	"dst_sql=s"          => \$dst_sql,
+	"dst_iso_expected=s" => \$dst_iso_expected,
+
+=item --dst_sql
+
+=item --dst_expected
+
+The directory path to write the regression tests' modified .sql and .out files.
+
+=item --dst_spec
+
+=item --dst_iso_expected
+
+The directory path to write the isolation tests' modified .spec and .out files.
+
+=item --datadir_prefix
+
+A prefix to be prepended to paths of data files within the tests being
+translated.  For instace, to convert tests from C<src/test/regress/sql> that
+contain a snippet like:
+
+=over 4
+
+\set filename :abs_srcdir '/data/hash.data'
+
+=back
+
+into a line in the resulting translated test like:
+
+=over 4
+
+\set filename :abs_srcdir '/../../regress/data/hash.data'
+
+=back
+
+provide the argument C<--datadir_prefix="/../../regress">.
+
+=back
+
+=cut
+
+use strict;
+# use warnings FATAL => 'all';
+use IO::File;
+use Getopt::Long;
+use FindBin;
+use lib "$FindBin::RealBin/../perl/";
+
+sub push_path
+{
+	my ($aryref, $option_name, $path) = @_;
+	die "$option_name: No such file: $path" unless -f $path;
+	push (@$aryref, $path);
+}
+
+sub require_equal_length_arrays
+{
+	my ($ary1, $ary2, $name) = @_;
+	die "different number of src_$name and dst_$name arguments given"
+		unless(scalar(@$ary1) == scalar(@$ary2));
+}
+
+
+
+my (@src_tbl_am,
+	@src_idx_am,
+	@src_regress,
+	@src_expected,
+	@src_spec,
+	@src_output_iso,
+	@dst_tbl_am,
+	@dst_idx_am,
+	@dst_regress,
+	@dst_expected,
+	@dst_spec,
+	@dst_output_iso,
+	@schedule,
+	$regress,
+	$src_sql,
+	$src_expected,
+	$dst_sql,
+	$dst_expected,
+	$isolation,
+	$src_spec,
+	$src_iso_expected,
+	$dst_spec,
+	$dst_iso_expected,
+	$datadir_prefix);
+GetOptions(
+	"src_tbl_am=s"       => \@src_tbl_am,
+	"src_idx_am=s"       => \@src_idx_am,
+	"dst_tbl_am=s"       => \@dst_tbl_am,
+	"dst_idx_am=s"       => \@dst_idx_am,
+	"schedule=s"         => \@schedule,
+	"regress=s"          => \$regress,
+	"src_sql=s"          => \$src_sql,
+	"src_expected=s"     => \$src_expected,
+	"dst_sql=s"          => \$dst_sql,
+	"dst_expected=s"     => \$dst_expected,
+	"isolation=s"        => \$isolation,
+	"src_spec=s"         => \$src_spec,
+	"src_iso_expected=s" => \$src_iso_expected,
+	"dst_spec=s"         => \$dst_spec,
+	"dst_iso_expected=s" => \$dst_iso_expected,
+	"datadir_prefix=s"   => \$datadir_prefix,
+	);
+
+my @regress = grep /\w+/, split(/\s+/, $regress);
+foreach my $regress (@regress)
+{
+	push (@src_regress, "$src_sql/$regress.sql");
+	push (@dst_regress, "$dst_sql/$regress.sql");
+	push (@src_expected, "$src_expected/$regress.out");
+	push (@dst_expected, "$dst_expected/$regress.out");
+}
+my @isolation = grep /\w+/, split(/\s+/, $isolation);
+foreach my $isolation (@isolation)
+{
+	push (@src_spec, "$src_spec/$isolation.spec");
+	push (@dst_spec, "$dst_spec/$isolation.spec");
+	push (@src_output_iso, "$src_iso_expected/$isolation.out");
+	push (@dst_output_iso, "$dst_iso_expected/$isolation.out");
+}
+require_equal_length_arrays(\@src_tbl_am, \@dst_tbl_am, "tbl_am");
+require_equal_length_arrays(\@src_idx_am, \@dst_idx_am, "idx_am");
+
+# First, define some regular expressions that make copying and modifying random
+# code more robust.
+#
+
+# Compiled regular expression for matching a single character (or escaped char)
+# within a quote except one that ends the quote
+our $quotecharre;
+$quotecharre = qr{
+					(?:
+						''				# double single-quote, does not close
+						|
+						(?> [^'])		# Non-closing singlequote, without backtracking
+						|
+						(?>\\')			# backslash escaped single-quote, without backtracking
+					)
+				}msx;
+
+# Compiled regular expression for matching a SQL quoted string
+our $quotere;
+$quotere = qr{
+					(?:
+						'				# Begin quote
+						$quotecharre*	# Any number of characters within the quote
+						'				# End quote
+					)
+				}msx;
+
+
+
+# Compiled regular expression for matching a code snippet that is balanced
+# relative to (), [], {} pairings, while not being fooled by unbalanced
+# snippets if they are within a single-quoted string.
+our $balancedre;
+$balancedre = qr{(
+					(?:
+						# A quote, which might include unbalanced
+						# parens/brackets/braces
+						(?> $quotere )
+						|
+						# non-special characters, no backtracking
+						(?> [^(){}\[\]]+ )
+						|
+						\(					# begin balanced parens
+							(?-1)			#   ... recurse
+						\)					# end balanced parens
+						|
+						\[					# begin balanced brackets
+							(?-1)			#   ... recurse
+						\]					# end balanced brackets
+						|
+						\{					# begin balanced braces
+							(?-1)			#   ... recurse
+						\}					# end balanced braces
+					)*
+				)}xms;
+
+our $unqualifiednamere = qr/\b\w+\b/;
+our $qualifiednamere = qr/\b\w+(?:\.\w+\b)?/;
+
+sub convert_indexes_to_use_am
+{
+	my ($code, $srcam, $dstam) = @_;
+
+	# If the srcam is btree, then we need to modify CREATE INDEX statments that
+	# say "USING btree" but also ones that omit a USING clause entirely.  For
+	# all other srcam, we look for an explicit "USING srcam".
+	my $using_srcam_re;
+	if (lc($srcam) eq 'btree')
+	{
+		$using_srcam_re = qr{
+								# Optionally, an explicit USING clause
+								(?: USING \s+ $srcam )?
+							}msix;
+	}
+	else
+	{
+		$using_srcam_re = qr{
+								# Mandatory explicit USING clause
+								(?: USING \s+ $srcam )
+								# No alternative
+							}msix;
+	}
+
+	$code =~ s{ (								# Begin capture of \$1
+				CREATE
+				\s+
+				(?: UNIQUE \s+ )?
+				INDEX
+				\s+
+				(?: CONCURRENTLY \s+ )?
+				(?:
+					(?!CONCURRENTLY)			# Don't match "concurrently" as a name
+					$unqualifiednamere			# Index name is optional
+					\s+
+				)?
+				ON
+				\s+
+				$qualifiednamere
+				)								# End capture of \$1
+				\s*
+				$using_srcam_re
+				(								# Begin capture of \$2
+				\s*
+				\(
+				)								# End capture of \$2
+			  }
+			  {$1 USING $dstam $2}msigx;
+
+	return $code;
+}
+
+sub convert_tables_to_use_am
+{
+	my ($code, $srcam, $dstam) = @_;
+
+	# If the srcam is heap, then we need to modify CREATE TABLE statments that
+	# say "USING heap" but also ones that omit a USING clause entirely.  For
+	# all other srcam, we look for an explicit "USING srcam".
+	my $using_srcam_re;
+	if (lc($srcam) eq 'heap')
+	{
+		$using_srcam_re = qr{
+								# Optionally, an explicit USING clause
+								(?: USING \s+ $srcam )?
+							}msix;
+	}
+	else
+	{
+		$using_srcam_re = qr{
+								# Mandatory explicit USING clause
+								(?: USING \s+ $srcam )
+								# No alternative
+							}msix;
+	}
+
+	# We don't need to capture the entire CREATE TABLE statement.  We just need
+	# the prefix up to where the optional USING clause belongs, so that we can
+	# add or replace with our own USING $dstam clause.
+	#
+	# We're not worried about whether the CREATE TABLE statement that we
+	# capture is syntactically correct, so long as we don't break valid syntax
+	# with our replacement.  If the test we modify has an intentionally invalid
+	# syntax, we still want to insert our "USING $dstam" clause, so long as we
+	# can reasonably determine where to do so.
+	$code =~ s{ (								# Begin capture of \$1
+				CREATE
+				\s+
+				(?: (?: TEMPORARY | TEMP ) \s+ )?
+				(?: UNLOGGED  \s+ )?
+				TABLE
+				\s+
+				(?: IF \s+ NOT \s+ EXISTS \s+ )?
+				(?! IF )		# Don't match "if" as the qualified name
+				$qualifiednamere \s*
+				(?: OF \s+ $qualifiednamere \s* )?
+				(?: PARTITION \s+ OF \s+ $qualifiednamere \s* )?
+				\( $balancedre \) \s*
+				(?: FOR \s+ VALUES \s+ (?:IN|FROM|TO|WITH) \s* \( $balancedre \) \s* )?
+				(?: INHERITS \s* \( $balancedre \) \s* )?
+				(?: PARTITION \s+ BY \s+ (?:RANGE|LIST|HASH) \s+ \( $balancedre \) \s* )?
+				)								# End capture of \$1
+				$using_srcam_re
+			  }
+			  {$1 USING $dstam }msigx;
+
+	return $code;
+}
+
+sub convert_expected_output
+{
+	my ($code, $srcam, $dstam) = @_;
+	$srcam = lc($srcam);
+	$dstam = lc($dstam);
+
+	# Convert anticipated error which includes the index AM in the error message
+	$code =~ s{ERROR:  access method "$srcam" does not support included columns}
+			  {ERROR:  access method "$dstam" does not support included columns}g;
+
+	# Convert \d table output that embeds the index AM name in the output
+	$code =~ s{("$unqualifiednamere")(?: UNIQUE,)? $srcam(?= \($balancedre\))}{$1 $dstam}g;
+
+	return $code;
+}
+
+sub convert_code
+{
+	my ($input, $output) = @_;
+
+	my $code = slurp($input);
+	for (my $i = 0; $i < scalar(@src_tbl_am); $i++)
+	{
+		$code = convert_tables_to_use_am($code, $src_tbl_am[$i], $dst_tbl_am[$i]);
+		$code = convert_expected_output($code, $src_tbl_am[$i], $dst_tbl_am[$i]);
+	}
+	for (my $i = 0; $i < scalar(@src_idx_am); $i++)
+	{
+		$code = convert_indexes_to_use_am($code, $src_idx_am[$i], $dst_idx_am[$i]);
+		$code = convert_expected_output($code, $src_idx_am[$i], $dst_idx_am[$i]);
+	}
+	if (defined $datadir_prefix)
+	{
+		$code =~ s{(?=/data/\w+\.data)}{$datadir_prefix}g;
+	}
+	spew($output, $code);
+}
+
+sub slurp
+{
+	my ($path) = @_;
+	my $fh = IO::File->new($path) or die "Cannot read $path: $!";
+	my $results;
+	{
+		undef(local $/);
+		$results = <$fh>;
+	}
+	$fh->close();
+	return $results;
+}
+
+sub spew
+{
+	my ($path, $contents) = @_;
+	my $fh = IO::File->new(">$path") or die "Cannot write $path: $!";
+	$fh->print($contents);
+	$fh->close();
+}
+
+# Handle user supplied arguments for translating tests, and/or the arguments for
+# parsing a regression or isolation schedule file.  At present, the build system
+# never invokes this script to do both together, but if that ever changes, we assume
+# the order we do them in doesn't matter.
+#
+
+# First, translate the tests, if requested...
+convert_code($src_regress[$_], $dst_regress[$_]) for (0..$#src_regress);
+convert_code($src_expected[$_], $dst_expected[$_]) for (0..$#src_expected);
+convert_code($src_spec[$_], $dst_spec[$_]) for (0..$#src_spec);
+convert_code($src_output_iso[$_], $dst_output_iso[$_]) for (0..$#src_output_iso);
+
+# Then, parse the schedule files, if requested...
+my $schedule = "";
+foreach my $path (@schedule)
+{
+	my $code = slurp($path);
+	while ($code =~ m/^test:(.*)$/mg)
+	{
+		$schedule .= $1;
+	}
+}
+
+# The build system reads the schedule from our stdout...
+print($schedule, "\n");

base-commit: 363a6e8c6fcf9f3e19fe673ae02554645974a388
-- 
2.48.1

v1-0001-Remove-unnecessary-Sort-node-above-Index-Scan-of-.patch.no-cfbotapplication/octet-stream; name=v1-0001-Remove-unnecessary-Sort-node-above-Index-Scan-of-.patch.no-cfbotDownload
From 4ba8c1d67b323c9c2d9e41a2aad06d95df611bcb Mon Sep 17 00:00:00 2001
From: Alexandra Wang <alexandra.wang.oss@gmail.com>
Date: Wed, 23 Oct 2024 14:26:40 -0500
Subject: [PATCH v1] Remove unnecessary Sort node above Index Scan of
 btree-compatible AM

When determining whether two sort keys are equivalent, they may not
belong to the same operator family but could still use the same
underlying sort operator.  In such cases, a simple pointer comparison
of two pathkeys marks them as different, leading to the addition of an
unnecessary Sort node, even though the path is already sorted as
desired.

This patch improves pathkeys_contained_in() and
pathkeys_count_contained_in() by comparing the sort operators
directly, preventing redundant sorting.
---
 src/backend/optimizer/path/costsize.c   |  4 +--
 src/backend/optimizer/path/pathkeys.c   | 43 +++++++++++++++++++++++--
 src/backend/optimizer/plan/createplan.c |  6 ++--
 3 files changed, 43 insertions(+), 10 deletions(-)

diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index b7f6ab40dec..e95762ac9a4 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -3607,9 +3607,7 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace,
 		opathkey = (PathKey *) linitial(opathkeys);
 		ipathkey = (PathKey *) linitial(ipathkeys);
 		/* debugging check */
-		if (opathkey->pk_opfamily != ipathkey->pk_opfamily ||
-			opathkey->pk_eclass->ec_collation != ipathkey->pk_eclass->ec_collation ||
-			opathkey->pk_strategy != ipathkey->pk_strategy ||
+		if (opathkey->pk_eclass->ec_collation != ipathkey->pk_eclass->ec_collation ||
 			opathkey->pk_cmptype != ipathkey->pk_cmptype ||
 			opathkey->pk_nulls_first != ipathkey->pk_nulls_first)
 			elog(ERROR, "left and right pathkeys do not match in mergejoin");
diff --git a/src/backend/optimizer/path/pathkeys.c b/src/backend/optimizer/path/pathkeys.c
index a0826ed5008..a709148ea9f 100644
--- a/src/backend/optimizer/path/pathkeys.c
+++ b/src/backend/optimizer/path/pathkeys.c
@@ -39,7 +39,7 @@ static bool matches_boolean_partition_clause(RestrictInfo *rinfo,
 											 int partkeycol);
 static Var *find_var_for_subquery_tle(RelOptInfo *rel, TargetEntry *tle);
 static bool right_merge_direction(PlannerInfo *root, PathKey *pathkey);
-
+static bool pathkeys_have_same_sortop(PathKey *pk1, PathKey *pk2);
 
 /****************************************************************************
  *		PATHKEY CONSTRUCTION AND REDUNDANCY TESTING
@@ -355,7 +355,7 @@ compare_pathkeys(List *keys1, List *keys2)
 		PathKey    *pathkey1 = (PathKey *) lfirst(key1);
 		PathKey    *pathkey2 = (PathKey *) lfirst(key2);
 
-		if (pathkey1 != pathkey2)
+		if (!pathkeys_have_same_sortop(pathkey1, pathkey2))
 			return PATHKEYS_DIFFERENT;	/* no need to keep looking */
 	}
 
@@ -585,6 +585,43 @@ get_useful_group_keys_orderings(PlannerInfo *root, Path *path)
 	return infos;
 }
 
+/*
+ * pathkeys_have_same_sortop
+ *
+ * Determines whether two PathKey objects should be considered equivalent
+ * for sorting purposes. Two PathKeys are treated as the same if:
+ *  - They are identical pointers OR
+ *  - They belong to the same equivalence class. AND
+ *  - They have the same comparison type and null ordering. AND
+ *  - They have the same sorting operator, even if they belong to different
+ *    operator families.
+ *
+ * The function retrieves the sort operator (sortop) for both PathKeys using
+ * their operator family, datatype, and strategy. If both PathKeys resolve to
+ * the same sort operator, they are considered equivalent.
+ */
+static bool
+pathkeys_have_same_sortop(PathKey *pk1, PathKey *pk2)
+{
+	Oid			pk_op1;
+	Oid			pk_op2;
+	Oid			pk_datatype;
+
+	if (pk1 == pk2)
+		return true;
+
+	if (pk1->pk_eclass != pk2->pk_eclass ||
+		pk1->pk_cmptype != pk2->pk_cmptype ||
+		pk1->pk_nulls_first != pk2->pk_nulls_first)
+		return false;
+
+	pk_datatype = ((EquivalenceMember *) linitial(pk1->pk_eclass->ec_members))->em_datatype;
+	pk_op1 = get_opfamily_member(pk1->pk_opfamily, pk_datatype, pk_datatype, pk1->pk_strategy);
+	pk_op2 = get_opfamily_member(pk2->pk_opfamily, pk_datatype, pk_datatype, pk2->pk_strategy);
+
+	return pk_op1 == pk_op2;
+}
+
 /*
  * pathkeys_count_contained_in
  *    Same as pathkeys_contained_in, but also sets length of longest
@@ -626,7 +663,7 @@ pathkeys_count_contained_in(List *keys1, List *keys2, int *n_common)
 		PathKey    *pathkey1 = (PathKey *) lfirst(key1);
 		PathKey    *pathkey2 = (PathKey *) lfirst(key2);
 
-		if (pathkey1 != pathkey2)
+		if (!pathkeys_have_same_sortop(pathkey1, pathkey2))
 		{
 			*n_common = n;
 			return false;
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 6a7a8d68db5..015f7f05801 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -4747,12 +4747,10 @@ create_mergejoin_plan(PlannerInfo *root,
 		 * matter which way we imagine this column to be ordered.)  But a
 		 * non-redundant inner pathkey had better match outer's ordering too.
 		 */
-		if (opathkey->pk_opfamily != ipathkey->pk_opfamily ||
-			opathkey->pk_eclass->ec_collation != ipathkey->pk_eclass->ec_collation)
+		if (opathkey->pk_eclass->ec_collation != ipathkey->pk_eclass->ec_collation)
 			elog(ERROR, "left and right pathkeys do not match in mergejoin");
 		if (first_inner_match &&
-			(opathkey->pk_strategy != ipathkey->pk_strategy ||
-			 opathkey->pk_cmptype != ipathkey->pk_cmptype ||
+			(opathkey->pk_cmptype != ipathkey->pk_cmptype ||
 			 opathkey->pk_nulls_first != ipathkey->pk_nulls_first))
 			elog(ERROR, "left and right pathkeys do not match in mergejoin");
 
-- 
2.39.5 (Apple Git-154)