public inbox for [email protected]  
help / color / mirror / Atom feed
[PATCH] btree_gist: add cross-type integer operator support for GiST
9+ messages / 3 participants
[nested] [flat]

* [PATCH] btree_gist: add cross-type integer operator support for GiST
@ 2026-05-02 17:26  Alexander Nestorov <[email protected]>
  0 siblings, 2 replies; 9+ messages in thread

From: Alexander Nestorov @ 2026-05-02 17:26 UTC (permalink / raw)
  To: pgsql-hackers

Hello hackers,

I'd like to submit a patch that adds cross-type operator support for the
three integer types (int2, int4, int8) to the btree_gist GiST operator
families. The patch also lays a general foundation for cross-type
dispatch that other type families can adopt.


Current problem:

GiST indexes are currently limited to same-type operators: the planner
can only match a query condition against an index column if the
operator's left and right argument types exactly match the types
registered in the index's operator family (pg_amop). This is true for
every data type that backs a GiST opclass.

When a query provides a value whose type is compatible but different
from the column type, the planner cannot use the index for that column.
The operator family lookup fails because no cross-type entry exists.
The planner must then either fall back to a sequential scan,
or in multi-column indexes use only the remaining column(s) and filter
the rest as a post-filter.

As a specific example, consider an int8 (bigint) column indexed with
btree_gist. The query:

    SELECT * FROM t WHERE camera_id = 1189;

Here 1189 is evaluated as int4 (integer). The planner resolves the
operator to =(int8,int4), which exists in pg_operator but is not
registered in the gist_int8_ops family. Only =(int8,int8) is
registered, so the column cannot be used as an index condition.

The workaround is to write an explicit cast in every query:

    WHERE camera_id = 1189::int8

This is fragile as ORMs, application parameter binding, and even hand-
written SQL queries produce values whose types do not exactly match
the column type.

A couple of self-contained reproduction scripts are included at the
end of this email. They create a table with a GiST index, insert
enough rows to make the plan difference visible, and run
EXPLAIN ANALYZE.


Proposed solution:

I'm working on a patch that extends the three btree_gist integer
operator families (gist_int2_ops, gist_int4_ops, gist_int8_ops)
with cross-type comparison and KNN-distance operators covering the
other two integer types.  Concretely:

    gist_int2_ops   <-  operators for (int2, int4) and (int2, int8)
    gist_int4_ops   <-  operators for (int4, int2) and (int4, int8)
    gist_int8_ops   <-  operators for (int8, int2) and (int8, int4)

For each pair I add all the standard btree_gist strategies for the
comparison operators (<, <=, =, >=, >, <>), plus the strategy for
the KNN distance operator (<->) used by ORDER BY.

I propose not to register separate cross-type support functions in
the operator family. GiST's amvalidate requires every support
function to have a matching left and right type, and registering
18 additional support functions (three families x two subtypes x
three strategies) would be verbose and error-prone. Instead, I
propose to dispatch cross-type queries directly inside the existing
consistent and distance functions and use the existing subtype
OID argument.

I'm thinking of introducing a general-purpose cross-type dispatch
table in btree_utils_num:

    typedef struct gbt_subtype_info
    {
        Oid         subtype;     /* right-hand Oid, e.g. INT4OID */
        gbt_cmp_fn  lt, le, eq, ge, gt;   /* comparison callbacks */
        gbt_dist_fn dist;                 /* KNN distance callback */
    } gbt_subtype_info;

Each integer opclass defines a static array of these entries:

    static const gbt_subtype_info gbt_int2_subtype_ops[] = {
        {INT4OID,  ... 6 comparison fns ... ,  distance fn},
        {INT8OID,  ... 6 comparison fns ... ,  distance fn},
        {InvalidOid}
    };

I'll replace the existing gbt_num_consistent() function with
gbt_num_consistent_x(), which takes a Datum query value and a
subtype Oid. If the subtype is InvalidOid or matches the indexed
type, the same-type path is used (backward compatible). Otherwise,
the function walks the dispatch table, finds the matching subtype
entry, and invokes the corresponding cross-type comparison or
distance callback.

Other btree_gist opclasses (float4/float8, date, timestamp, ...) and
even range-type GiST opfamilies in core can adopt the same pattern by
defining their own subtype dispatch tables and registering cross-type
operators via ALTER OPERATOR FAMILY.

I don't plan adding cross-type support for every data type as that
would result in a very bulky patch difficult to review, but I think
my proposal establishes the infrastructure so that follow-up work
for additional types is straightforward.

Is there interest in this patch? Should I proceed with polishing my
patch and sending it here for a review? I appreciate any feedback!

Thank you





Simple repro example:


DROP TABLE IF EXISTS camera_feeds_simple CASCADE;

CREATE TABLE camera_feeds_simple (
    id          SERIAL PRIMARY KEY,
    camera_id   int8 NOT NULL
);

CREATE EXTENSION IF NOT EXISTS btree_gist;

CREATE INDEX idx_camera_feeds_gist
    ON camera_feeds_simple
    USING GIST (camera_id);

SET enable_seqscan = OFF;
SET enable_bitmapscan = ON;

INSERT INTO camera_feeds_simple (camera_id) VALUES
    (1), (1), (1), (1), (2), (2), (2), (2), (3), (3), (3),
    (4), (4), (4), (4), (4), (4), (4), (4), (5), (5), (6), (6);

ANALYZE camera_feeds_simple;

\echo 'Triggering current behaviour that fails to use the index'

EXPLAIN (ANALYZE, COSTS, BUFFERS, TIMING, SUMMARY)
SELECT *
FROM camera_feeds_simple
WHERE camera_id = 4;

\echo 'Triggering query with manual cast workaround'

EXPLAIN (ANALYZE, COSTS, BUFFERS, TIMING, SUMMARY)
SELECT *
FROM camera_feeds_simple
WHERE camera_id = 4::int8;

RESET enable_seqscan;
RESET enable_bitmapscan;






Repro example with two columns:


DROP TABLE IF EXISTS camera_feeds_multi CASCADE;

CREATE TABLE camera_feeds_multi (
    id          SERIAL PRIMARY KEY,
    camera_id   int8 NOT NULL,
    timerange   TSTZRANGE NOT NULL
);

CREATE EXTENSION IF NOT EXISTS btree_gist;

CREATE INDEX idx_camera_feeds_gist
    ON camera_feeds_multi
    USING GIST (camera_id, timerange);

SET enable_seqscan = OFF;
SET enable_bitmapscan = ON;

INSERT INTO camera_feeds_multi (camera_id, timerange) VALUES
    (1, '[2026-04-01 06:00:00+00, 2026-04-01 12:00:00+00)'),
    (1, '[2026-04-01 12:00:00+00, 2026-04-01 18:00:00+00)'),
    (1, '[2026-04-01 18:00:00+00, 2026-04-02 06:00:00+00)'),
    (1, '[2026-04-02 06:00:00+00, 2026-04-02 12:00:00+00)'),

    (2, '[2026-04-01 07:00:00+00, 2026-04-01 09:00:00+00)'),
    (2, '[2026-04-01 08:30:00+00, 2026-04-01 11:00:00+00)'),
    (2, '[2026-04-01 10:00:00+00, 2026-04-01 14:00:00+00)'),
    (2, '[2026-04-01 13:00:00+00, 2026-04-01 17:00:00+00)'),

    (3, '[2026-03-15 00:00:00+00, 2026-03-15 23:59:59+00)'),
    (3, '[2026-03-20 08:00:00+00, 2026-03-20 20:00:00+00)'),
    (3, '[2026-04-01 00:00:00+00, 2026-04-01 23:59:59+00)'),

    (4, '[2026-04-01 00:00:00+00, 2026-04-01 06:00:00+00)'),
    (4, '[2026-04-01 06:00:00+00, 2026-04-01 08:00:00+00)'),
    (4, '[2026-04-01 08:00:00+00, 2026-04-01 12:00:00+00)'),
    (4, '[2026-04-01 12:00:00+00, 2026-04-01 14:00:00+00)'),
    (4, '[2026-04-01 14:00:00+00, 2026-04-01 18:00:00+00)'),
    (4, '[2026-04-01 18:00:00+00, 2026-04-01 22:00:00+00)'),
    (4, '[2026-04-01 22:00:00+00, 2026-04-02 00:00:00+00)'),
    (4, '[2026-04-01 09:00:00+00, 2026-04-01 15:00:00+00)'),

    (5, '[2026-03-30 00:00:00+00, 2026-04-02 00:00:00+00)'),
    (5, '[2026-04-01 10:00:00+00, 2026-04-01 10:30:00+00)'),

    (6, '[2026-04-01 12:00:00+00, 2026-04-01 12:00:00+00)'),
    (6, '[2026-04-01 12:00:00+00, 2026-04-01 13:00:00+00)');

ANALYZE camera_feeds_multi;

\echo 'Triggering current behaviour that fails to use the index'

EXPLAIN (ANALYZE, COSTS, BUFFERS, TIMING, SUMMARY)
SELECT *
FROM camera_feeds_multi
WHERE camera_id = 4
  AND timerange && '[2026-04-01 10:00:00+00, 2026-04-01 16:00:00+00)'::tstzrange
ORDER BY timerange;

\echo 'Triggering query with manual cast workaround'

EXPLAIN (ANALYZE, COSTS, BUFFERS, TIMING, SUMMARY)
SELECT *
FROM camera_feeds_multi
WHERE camera_id = 4::int8
  AND timerange && '[2026-04-01 10:00:00+00, 2026-04-01 16:00:00+00)'::tstzrange
ORDER BY timerange;

RESET enable_seqscan;
RESET enable_bitmapscan;


^ permalink  raw  reply  [nested|flat] 9+ messages in thread

* Re: [PATCH] btree_gist: add cross-type integer operator support for GiST
@ 2026-06-03 22:16  Alexander Nestorov <[email protected]>
  parent: Alexander Nestorov <[email protected]>
  1 sibling, 0 replies; 9+ messages in thread

From: Alexander Nestorov @ 2026-06-03 22:16 UTC (permalink / raw)
  To: pgsql-hackers

Hello hackers,

Following up on my previous message, I’m attaching a partial patch for the
btree_gist cross-type integer operator support.

This patch intentionally contains only the main code changes. It does not yet
include regression tests or documentation updates. I’m sending it this way to
make the core approach easier to review before adding the surrounding test and
documentation work.

I’d appreciate any feedback!

If the approach looks reasonable, I’ll send a more complete version with tests
and documentation.
Thank you


Attachments:

  [application/octet-stream] 0001-Implement-cross-type-operators-for-GiST-indexes.patch (30.2K, 3-0001-Implement-cross-type-operators-for-GiST-indexes.patch)
  download | inline diff:
From 7c9306af289d1b232168129db9b248751f1164e1 Mon Sep 17 00:00:00 2001
From: Alexander Nestorov <[email protected]>
Date: Thu, 4 Jun 2026 00:06:54 +0200
Subject: [PATCH] Implement cross-type operators for GiST indexes

---
 contrib/btree_gist/Makefile                  |   4 +-
 contrib/btree_gist/btree_gist--1.9--1.10.sql | 133 +++++++++++++++++
 contrib/btree_gist/btree_gist.control        |   2 +-
 contrib/btree_gist/btree_int2.c              |  87 +++++++++--
 contrib/btree_gist/btree_int4.c              |  86 +++++++++--
 contrib/btree_gist/btree_int8.c              |  86 +++++++++--
 contrib/btree_gist/btree_utils_num.c         | 148 +++++++++++++++++++
 contrib/btree_gist/btree_utils_num.h         |  85 +++++++++++
 contrib/btree_gist/meson.build               |   2 +
 9 files changed, 597 insertions(+), 36 deletions(-)
 create mode 100644 contrib/btree_gist/btree_gist--1.9--1.10.sql

diff --git a/contrib/btree_gist/Makefile b/contrib/btree_gist/Makefile
index fbbbca95598..1d0668d97ab 100644
--- a/contrib/btree_gist/Makefile
+++ b/contrib/btree_gist/Makefile
@@ -32,19 +32,19 @@ OBJS =  \
 EXTENSION = btree_gist
 DATA = btree_gist--1.0--1.1.sql \
        btree_gist--1.1--1.2.sql btree_gist--1.2--1.3.sql \
        btree_gist--1.3--1.4.sql btree_gist--1.4--1.5.sql \
        btree_gist--1.5--1.6.sql btree_gist--1.6--1.7.sql \
        btree_gist--1.7--1.8.sql btree_gist--1.8--1.9.sql \
-       btree_gist--1.9.sql
+       btree_gist--1.9.sql btree_gist--1.9--1.10.sql
 PGFILEDESC = "btree_gist - B-tree equivalent GiST operator classes"
 
 REGRESS = init int2 int4 int8 float4 float8 cash oid timestamp timestamptz \
         time timetz date interval macaddr macaddr8 inet cidr text varchar char \
         bytea bit varbit numeric uuid not_equal enum bool partitions \
-        stratnum without_overlaps
+        stratnum int_crosstype without_overlaps
 
 SHLIB_LINK += $(filter -lm, $(LIBS))
 
 ifdef USE_PGXS
 PG_CONFIG = pg_config
 PGXS := $(shell $(PG_CONFIG) --pgxs)
diff --git a/contrib/btree_gist/btree_gist--1.9--1.10.sql b/contrib/btree_gist/btree_gist--1.9--1.10.sql
new file mode 100644
index 00000000000..f338b854b30
--- /dev/null
+++ b/contrib/btree_gist/btree_gist--1.9--1.10.sql
@@ -0,0 +1,133 @@
+/* contrib/btree_gist/btree_gist--1.9--1.10.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION btree_gist UPDATE TO '1.10'" to load this file. \quit
+
+-- Add cross-type operator support for the integer trio (int2, int4, int8)
+-- to the existing GiST operator families.
+--
+-- GiST's amvalidate requires support functions in a family to have matching
+-- left/right input types, so the catalog additions below are deliberately
+-- pg_amop-only. The same-type consistent/distance support functions dispatch
+-- on the subtype OID and route mixed-width integer comparisons through their
+-- C-side dispatch tables.
+
+CREATE FUNCTION int2_int4_dist(int2, int4)
+RETURNS int4
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION int4_int2_dist(int4, int2)
+RETURNS int4
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION int2_int8_dist(int2, int8)
+RETURNS int8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION int8_int2_dist(int8, int2)
+RETURNS int8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION int4_int8_dist(int4, int8)
+RETURNS int8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION int8_int4_dist(int8, int4)
+RETURNS int8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE OPERATOR <-> (
+	LEFTARG = int2,
+	RIGHTARG = int4,
+	PROCEDURE = int2_int4_dist,
+	COMMUTATOR = '<->'
+);
+
+CREATE OPERATOR <-> (
+	LEFTARG = int4,
+	RIGHTARG = int2,
+	PROCEDURE = int4_int2_dist,
+	COMMUTATOR = '<->'
+);
+
+CREATE OPERATOR <-> (
+	LEFTARG = int2,
+	RIGHTARG = int8,
+	PROCEDURE = int2_int8_dist,
+	COMMUTATOR = '<->'
+);
+
+CREATE OPERATOR <-> (
+	LEFTARG = int8,
+	RIGHTARG = int2,
+	PROCEDURE = int8_int2_dist,
+	COMMUTATOR = '<->'
+);
+
+CREATE OPERATOR <-> (
+	LEFTARG = int4,
+	RIGHTARG = int8,
+	PROCEDURE = int4_int8_dist,
+	COMMUTATOR = '<->'
+);
+
+CREATE OPERATOR <-> (
+	LEFTARG = int8,
+	RIGHTARG = int4,
+	PROCEDURE = int8_int4_dist,
+	COMMUTATOR = '<->'
+);
+
+ALTER OPERATOR FAMILY gist_int2_ops USING gist ADD
+	OPERATOR	1	<  (int2, int4),
+	OPERATOR	2	<= (int2, int4),
+	OPERATOR	3	=  (int2, int4),
+	OPERATOR	4	>= (int2, int4),
+	OPERATOR	5	>  (int2, int4),
+	OPERATOR	6	<> (int2, int4),
+	OPERATOR	15	<-> (int2, int4) FOR ORDER BY pg_catalog.integer_ops,
+	OPERATOR	1	<  (int2, int8),
+	OPERATOR	2	<= (int2, int8),
+	OPERATOR	3	=  (int2, int8),
+	OPERATOR	4	>= (int2, int8),
+	OPERATOR	5	>  (int2, int8),
+	OPERATOR	6	<> (int2, int8),
+	OPERATOR	15	<-> (int2, int8) FOR ORDER BY pg_catalog.integer_ops;
+
+ALTER OPERATOR FAMILY gist_int4_ops USING gist ADD
+	OPERATOR	1	<  (int4, int2),
+	OPERATOR	2	<= (int4, int2),
+	OPERATOR	3	=  (int4, int2),
+	OPERATOR	4	>= (int4, int2),
+	OPERATOR	5	>  (int4, int2),
+	OPERATOR	6	<> (int4, int2),
+	OPERATOR	15	<-> (int4, int2) FOR ORDER BY pg_catalog.integer_ops,
+	OPERATOR	1	<  (int4, int8),
+	OPERATOR	2	<= (int4, int8),
+	OPERATOR	3	=  (int4, int8),
+	OPERATOR	4	>= (int4, int8),
+	OPERATOR	5	>  (int4, int8),
+	OPERATOR	6	<> (int4, int8),
+	OPERATOR	15	<-> (int4, int8) FOR ORDER BY pg_catalog.integer_ops;
+
+ALTER OPERATOR FAMILY gist_int8_ops USING gist ADD
+	OPERATOR	1	<  (int8, int2),
+	OPERATOR	2	<= (int8, int2),
+	OPERATOR	3	=  (int8, int2),
+	OPERATOR	4	>= (int8, int2),
+	OPERATOR	5	>  (int8, int2),
+	OPERATOR	6	<> (int8, int2),
+	OPERATOR	15	<-> (int8, int2) FOR ORDER BY pg_catalog.integer_ops,
+	OPERATOR	1	<  (int8, int4),
+	OPERATOR	2	<= (int8, int4),
+	OPERATOR	3	=  (int8, int4),
+	OPERATOR	4	>= (int8, int4),
+	OPERATOR	5	>  (int8, int4),
+	OPERATOR	6	<> (int8, int4),
+	OPERATOR	15	<-> (int8, int4) FOR ORDER BY pg_catalog.integer_ops;
diff --git a/contrib/btree_gist/btree_gist.control b/contrib/btree_gist/btree_gist.control
index 69d9341a0ad..e606fa6551d 100644
--- a/contrib/btree_gist/btree_gist.control
+++ b/contrib/btree_gist/btree_gist.control
@@ -1,6 +1,6 @@
 # btree_gist extension
 comment = 'support for indexing common datatypes in GiST'
-default_version = '1.9'
+default_version = '1.10'
 module_pathname = '$libdir/btree_gist'
 relocatable = true
 trusted = true
diff --git a/contrib/btree_gist/btree_int2.c b/contrib/btree_gist/btree_int2.c
index cc4b33177e3..1fdf55131b3 100644
--- a/contrib/btree_gist/btree_int2.c
+++ b/contrib/btree_gist/btree_int2.c
@@ -2,12 +2,13 @@
  * contrib/btree_gist/btree_int2.c
  */
 #include "postgres.h"
 
 #include "btree_gist.h"
 #include "btree_utils_num.h"
+#include "catalog/pg_type.h"
 #include "common/int.h"
 #include "utils/rel.h"
 #include "utils/sortsupport.h"
 
 typedef struct int16key
 {
@@ -73,25 +74,42 @@ gbt_int2key_cmp(const void *a, const void *b, FmgrInfo *flinfo)
 static float8
 gbt_int2_dist(const void *a, const void *b, FmgrInfo *flinfo)
 {
 	return GET_FLOAT_DISTANCE(int16, a, b);
 }
 
+/*
+ * Cross-type callbacks
+ */
+GBT_DEFINE_INT_CROSSTYPE(gbt_int2_x_int4, int16, DatumGetInt32)
+GBT_DEFINE_INT_CROSSTYPE(gbt_int2_x_int8, int16, DatumGetInt64)
+
+static const gbt_subtype_info gbt_int2_subtype_ops[] = {
+	{INT4OID,
+		gbt_int2_x_int4_lt, gbt_int2_x_int4_le, gbt_int2_x_int4_eq,
+	gbt_int2_x_int4_ge, gbt_int2_x_int4_gt, gbt_int2_x_int4_dist},
+	{INT8OID,
+		gbt_int2_x_int8_lt, gbt_int2_x_int8_le, gbt_int2_x_int8_eq,
+	gbt_int2_x_int8_ge, gbt_int2_x_int8_gt, gbt_int2_x_int8_dist},
+	{InvalidOid}
+};
 
 static const gbtree_ninfo tinfo =
 {
 	gbt_t_int2,
 	sizeof(int16),
 	4,							/* sizeof(gbtreekey4) */
 	gbt_int2gt,
 	gbt_int2ge,
 	gbt_int2eq,
 	gbt_int2le,
 	gbt_int2lt,
 	gbt_int2key_cmp,
-	gbt_int2_dist
+	gbt_int2_dist,
+	gbt_int2_subtype_ops,
+	INT2OID
 };
 
 
 PG_FUNCTION_INFO_V1(int2_dist);
 Datum
 int2_dist(PG_FUNCTION_ARGS)
@@ -109,12 +127,46 @@ int2_dist(PG_FUNCTION_ARGS)
 
 	ra = abs(r);
 
 	PG_RETURN_INT16(ra);
 }
 
+PG_FUNCTION_INFO_V1(int2_int4_dist);
+Datum
+int2_int4_dist(PG_FUNCTION_ARGS)
+{
+	int32		a = (int32) PG_GETARG_INT16(0);
+	int32		b = PG_GETARG_INT32(1);
+	int32		r;
+
+	if (pg_sub_s32_overflow(a, b, &r) ||
+		r == PG_INT32_MIN)
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("integer out of range")));
+
+	PG_RETURN_INT32(abs(r));
+}
+
+PG_FUNCTION_INFO_V1(int2_int8_dist);
+Datum
+int2_int8_dist(PG_FUNCTION_ARGS)
+{
+	int64		a = (int64) PG_GETARG_INT16(0);
+	int64		b = PG_GETARG_INT64(1);
+	int64		r;
+
+	if (pg_sub_s64_overflow(a, b, &r) ||
+		r == PG_INT64_MIN)
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("bigint out of range")));
+
+	PG_RETURN_INT64(i64abs(r));
+}
+
 
 /**************************************************
  * GiST support functions
  **************************************************/
 
 Datum
@@ -134,47 +186,60 @@ gbt_int2_fetch(PG_FUNCTION_ARGS)
 }
 
 Datum
 gbt_int2_consistent(PG_FUNCTION_ARGS)
 {
 	GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
-	int16		query = PG_GETARG_INT16(1);
+	Datum		queryDatum = PG_GETARG_DATUM(1);
 	StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2);
-#ifdef NOT_USED
 	Oid			subtype = PG_GETARG_OID(3);
-#endif
 	bool	   *recheck = (bool *) PG_GETARG_POINTER(4);
 	int16KEY   *kkk = (int16KEY *) DatumGetPointer(entry->key);
+	int16		query;
 	GBT_NUMKEY_R key;
 
 	/* All cases served by this function are exact */
 	*recheck = false;
 
+	/* Only decode as int16 on the same-type path to avoid silent truncation */
+	if (subtype == InvalidOid || subtype == INT2OID)
+		query = DatumGetInt16(queryDatum);
+	else
+		query = 0;
+
 	key.lower = (GBT_NUMKEY *) &kkk->lower;
 	key.upper = (GBT_NUMKEY *) &kkk->upper;
 
-	PG_RETURN_BOOL(gbt_num_consistent(&key, &query, &strategy,
-									  GIST_LEAF(entry), &tinfo, fcinfo->flinfo));
+	PG_RETURN_BOOL(gbt_num_consistent_x(&key, &query, queryDatum,
+										subtype, PG_GET_COLLATION(),
+										&strategy, GIST_LEAF(entry),
+										&tinfo, fcinfo->flinfo));
 }
 
 Datum
 gbt_int2_distance(PG_FUNCTION_ARGS)
 {
 	GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
-	int16		query = PG_GETARG_INT16(1);
-#ifdef NOT_USED
+	Datum		queryDatum = PG_GETARG_DATUM(1);
 	Oid			subtype = PG_GETARG_OID(3);
-#endif
 	int16KEY   *kkk = (int16KEY *) DatumGetPointer(entry->key);
+	int16		query;
 	GBT_NUMKEY_R key;
 
+	if (subtype == InvalidOid || subtype == INT2OID)
+		query = DatumGetInt16(queryDatum);
+	else
+		query = 0;
+
 	key.lower = (GBT_NUMKEY *) &kkk->lower;
 	key.upper = (GBT_NUMKEY *) &kkk->upper;
 
-	PG_RETURN_FLOAT8(gbt_num_distance(&key, &query, GIST_LEAF(entry),
-									  &tinfo, fcinfo->flinfo));
+	PG_RETURN_FLOAT8(gbt_num_distance_x(&key, &query, queryDatum,
+										subtype, PG_GET_COLLATION(),
+										GIST_LEAF(entry),
+										&tinfo, fcinfo->flinfo));
 }
 
 Datum
 gbt_int2_union(PG_FUNCTION_ARGS)
 {
 	GistEntryVector *entryvec = (GistEntryVector *) PG_GETARG_POINTER(0);
diff --git a/contrib/btree_gist/btree_int4.c b/contrib/btree_gist/btree_int4.c
index 47790578e6b..7c53ce9c56f 100644
--- a/contrib/btree_gist/btree_int4.c
+++ b/contrib/btree_gist/btree_int4.c
@@ -1,12 +1,13 @@
 /*
  * contrib/btree_gist/btree_int4.c
  */
 #include "postgres.h"
 #include "btree_gist.h"
 #include "btree_utils_num.h"
+#include "catalog/pg_type.h"
 #include "common/int.h"
 #include "utils/rel.h"
 #include "utils/sortsupport.h"
 
 typedef struct int32key
 {
@@ -71,25 +72,42 @@ gbt_int4key_cmp(const void *a, const void *b, FmgrInfo *flinfo)
 static float8
 gbt_int4_dist(const void *a, const void *b, FmgrInfo *flinfo)
 {
 	return GET_FLOAT_DISTANCE(int32, a, b);
 }
 
+/*
+ * Cross-type callbacks
+ */
+GBT_DEFINE_INT_CROSSTYPE(gbt_int4_x_int2, int32, DatumGetInt16)
+GBT_DEFINE_INT_CROSSTYPE(gbt_int4_x_int8, int32, DatumGetInt64)
+
+static const gbt_subtype_info gbt_int4_subtype_ops[] = {
+	{INT2OID,
+		gbt_int4_x_int2_lt, gbt_int4_x_int2_le, gbt_int4_x_int2_eq,
+	gbt_int4_x_int2_ge, gbt_int4_x_int2_gt, gbt_int4_x_int2_dist},
+	{INT8OID,
+		gbt_int4_x_int8_lt, gbt_int4_x_int8_le, gbt_int4_x_int8_eq,
+	gbt_int4_x_int8_ge, gbt_int4_x_int8_gt, gbt_int4_x_int8_dist},
+	{InvalidOid}
+};
 
 static const gbtree_ninfo tinfo =
 {
 	gbt_t_int4,
 	sizeof(int32),
 	8,							/* sizeof(gbtreekey8) */
 	gbt_int4gt,
 	gbt_int4ge,
 	gbt_int4eq,
 	gbt_int4le,
 	gbt_int4lt,
 	gbt_int4key_cmp,
-	gbt_int4_dist
+	gbt_int4_dist,
+	gbt_int4_subtype_ops,
+	INT4OID
 };
 
 
 PG_FUNCTION_INFO_V1(int4_dist);
 Datum
 int4_dist(PG_FUNCTION_ARGS)
@@ -107,12 +125,46 @@ int4_dist(PG_FUNCTION_ARGS)
 
 	ra = abs(r);
 
 	PG_RETURN_INT32(ra);
 }
 
+PG_FUNCTION_INFO_V1(int4_int2_dist);
+Datum
+int4_int2_dist(PG_FUNCTION_ARGS)
+{
+	int32		a = PG_GETARG_INT32(0);
+	int32		b = (int32) PG_GETARG_INT16(1);
+	int32		r;
+
+	if (pg_sub_s32_overflow(a, b, &r) ||
+		r == PG_INT32_MIN)
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("integer out of range")));
+
+	PG_RETURN_INT32(abs(r));
+}
+
+PG_FUNCTION_INFO_V1(int4_int8_dist);
+Datum
+int4_int8_dist(PG_FUNCTION_ARGS)
+{
+	int64		a = (int64) PG_GETARG_INT32(0);
+	int64		b = PG_GETARG_INT64(1);
+	int64		r;
+
+	if (pg_sub_s64_overflow(a, b, &r) ||
+		r == PG_INT64_MIN)
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("bigint out of range")));
+
+	PG_RETURN_INT64(i64abs(r));
+}
+
 
 /**************************************************
  * GiST support functions
  **************************************************/
 
 Datum
@@ -132,47 +184,59 @@ gbt_int4_fetch(PG_FUNCTION_ARGS)
 }
 
 Datum
 gbt_int4_consistent(PG_FUNCTION_ARGS)
 {
 	GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
-	int32		query = PG_GETARG_INT32(1);
+	Datum		queryDatum = PG_GETARG_DATUM(1);
 	StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2);
-#ifdef NOT_USED
 	Oid			subtype = PG_GETARG_OID(3);
-#endif
 	bool	   *recheck = (bool *) PG_GETARG_POINTER(4);
 	int32KEY   *kkk = (int32KEY *) DatumGetPointer(entry->key);
+	int32		query;
 	GBT_NUMKEY_R key;
 
 	/* All cases served by this function are exact */
 	*recheck = false;
 
+	if (subtype == InvalidOid || subtype == INT4OID)
+		query = DatumGetInt32(queryDatum);
+	else
+		query = 0;
+
 	key.lower = (GBT_NUMKEY *) &kkk->lower;
 	key.upper = (GBT_NUMKEY *) &kkk->upper;
 
-	PG_RETURN_BOOL(gbt_num_consistent(&key, &query, &strategy,
-									  GIST_LEAF(entry), &tinfo, fcinfo->flinfo));
+	PG_RETURN_BOOL(gbt_num_consistent_x(&key, &query, queryDatum,
+										subtype, PG_GET_COLLATION(),
+										&strategy, GIST_LEAF(entry),
+										&tinfo, fcinfo->flinfo));
 }
 
 Datum
 gbt_int4_distance(PG_FUNCTION_ARGS)
 {
 	GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
-	int32		query = PG_GETARG_INT32(1);
-#ifdef NOT_USED
+	Datum		queryDatum = PG_GETARG_DATUM(1);
 	Oid			subtype = PG_GETARG_OID(3);
-#endif
 	int32KEY   *kkk = (int32KEY *) DatumGetPointer(entry->key);
+	int32		query;
 	GBT_NUMKEY_R key;
 
+	if (subtype == InvalidOid || subtype == INT4OID)
+		query = DatumGetInt32(queryDatum);
+	else
+		query = 0;
+
 	key.lower = (GBT_NUMKEY *) &kkk->lower;
 	key.upper = (GBT_NUMKEY *) &kkk->upper;
 
-	PG_RETURN_FLOAT8(gbt_num_distance(&key, &query, GIST_LEAF(entry),
-									  &tinfo, fcinfo->flinfo));
+	PG_RETURN_FLOAT8(gbt_num_distance_x(&key, &query, queryDatum,
+										subtype, PG_GET_COLLATION(),
+										GIST_LEAF(entry),
+										&tinfo, fcinfo->flinfo));
 }
 
 Datum
 gbt_int4_union(PG_FUNCTION_ARGS)
 {
 	GistEntryVector *entryvec = (GistEntryVector *) PG_GETARG_POINTER(0);
diff --git a/contrib/btree_gist/btree_int8.c b/contrib/btree_gist/btree_int8.c
index f48122c8d84..352acb4ff76 100644
--- a/contrib/btree_gist/btree_int8.c
+++ b/contrib/btree_gist/btree_int8.c
@@ -2,12 +2,13 @@
  * contrib/btree_gist/btree_int8.c
  */
 #include "postgres.h"
 
 #include "btree_gist.h"
 #include "btree_utils_num.h"
+#include "catalog/pg_type.h"
 #include "common/int.h"
 #include "utils/rel.h"
 #include "utils/sortsupport.h"
 
 typedef struct int64key
 {
@@ -73,25 +74,42 @@ gbt_int8key_cmp(const void *a, const void *b, FmgrInfo *flinfo)
 static float8
 gbt_int8_dist(const void *a, const void *b, FmgrInfo *flinfo)
 {
 	return GET_FLOAT_DISTANCE(int64, a, b);
 }
 
+/*
+ * Cross-type callbacks
+ */
+GBT_DEFINE_INT_CROSSTYPE(gbt_int8_x_int2, int64, DatumGetInt16)
+GBT_DEFINE_INT_CROSSTYPE(gbt_int8_x_int4, int64, DatumGetInt32)
+
+static const gbt_subtype_info gbt_int8_subtype_ops[] = {
+	{INT2OID,
+		gbt_int8_x_int2_lt, gbt_int8_x_int2_le, gbt_int8_x_int2_eq,
+	gbt_int8_x_int2_ge, gbt_int8_x_int2_gt, gbt_int8_x_int2_dist},
+	{INT4OID,
+		gbt_int8_x_int4_lt, gbt_int8_x_int4_le, gbt_int8_x_int4_eq,
+	gbt_int8_x_int4_ge, gbt_int8_x_int4_gt, gbt_int8_x_int4_dist},
+	{InvalidOid}
+};
 
 static const gbtree_ninfo tinfo =
 {
 	gbt_t_int8,
 	sizeof(int64),
 	16,							/* sizeof(gbtreekey16) */
 	gbt_int8gt,
 	gbt_int8ge,
 	gbt_int8eq,
 	gbt_int8le,
 	gbt_int8lt,
 	gbt_int8key_cmp,
-	gbt_int8_dist
+	gbt_int8_dist,
+	gbt_int8_subtype_ops,
+	INT8OID
 };
 
 
 PG_FUNCTION_INFO_V1(int8_dist);
 Datum
 int8_dist(PG_FUNCTION_ARGS)
@@ -109,12 +127,46 @@ int8_dist(PG_FUNCTION_ARGS)
 
 	ra = i64abs(r);
 
 	PG_RETURN_INT64(ra);
 }
 
+PG_FUNCTION_INFO_V1(int8_int2_dist);
+Datum
+int8_int2_dist(PG_FUNCTION_ARGS)
+{
+	int64		a = PG_GETARG_INT64(0);
+	int64		b = (int64) PG_GETARG_INT16(1);
+	int64		r;
+
+	if (pg_sub_s64_overflow(a, b, &r) ||
+		r == PG_INT64_MIN)
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("bigint out of range")));
+
+	PG_RETURN_INT64(i64abs(r));
+}
+
+PG_FUNCTION_INFO_V1(int8_int4_dist);
+Datum
+int8_int4_dist(PG_FUNCTION_ARGS)
+{
+	int64		a = PG_GETARG_INT64(0);
+	int64		b = (int64) PG_GETARG_INT32(1);
+	int64		r;
+
+	if (pg_sub_s64_overflow(a, b, &r) ||
+		r == PG_INT64_MIN)
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("bigint out of range")));
+
+	PG_RETURN_INT64(i64abs(r));
+}
+
 
 /**************************************************
  * GiST support functions
  **************************************************/
 
 Datum
@@ -134,47 +186,59 @@ gbt_int8_fetch(PG_FUNCTION_ARGS)
 }
 
 Datum
 gbt_int8_consistent(PG_FUNCTION_ARGS)
 {
 	GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
-	int64		query = PG_GETARG_INT64(1);
+	Datum		queryDatum = PG_GETARG_DATUM(1);
 	StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2);
-#ifdef NOT_USED
 	Oid			subtype = PG_GETARG_OID(3);
-#endif
 	bool	   *recheck = (bool *) PG_GETARG_POINTER(4);
 	int64KEY   *kkk = (int64KEY *) DatumGetPointer(entry->key);
+	int64		query;
 	GBT_NUMKEY_R key;
 
 	/* All cases served by this function are exact */
 	*recheck = false;
 
+	if (subtype == InvalidOid || subtype == INT8OID)
+		query = DatumGetInt64(queryDatum);
+	else
+		query = 0;
+
 	key.lower = (GBT_NUMKEY *) &kkk->lower;
 	key.upper = (GBT_NUMKEY *) &kkk->upper;
 
-	PG_RETURN_BOOL(gbt_num_consistent(&key, &query, &strategy,
-									  GIST_LEAF(entry), &tinfo, fcinfo->flinfo));
+	PG_RETURN_BOOL(gbt_num_consistent_x(&key, &query, queryDatum,
+										subtype, PG_GET_COLLATION(),
+										&strategy, GIST_LEAF(entry),
+										&tinfo, fcinfo->flinfo));
 }
 
 Datum
 gbt_int8_distance(PG_FUNCTION_ARGS)
 {
 	GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
-	int64		query = PG_GETARG_INT64(1);
-#ifdef NOT_USED
+	Datum		queryDatum = PG_GETARG_DATUM(1);
 	Oid			subtype = PG_GETARG_OID(3);
-#endif
 	int64KEY   *kkk = (int64KEY *) DatumGetPointer(entry->key);
+	int64		query;
 	GBT_NUMKEY_R key;
 
+	if (subtype == InvalidOid || subtype == INT8OID)
+		query = DatumGetInt64(queryDatum);
+	else
+		query = 0;
+
 	key.lower = (GBT_NUMKEY *) &kkk->lower;
 	key.upper = (GBT_NUMKEY *) &kkk->upper;
 
-	PG_RETURN_FLOAT8(gbt_num_distance(&key, &query, GIST_LEAF(entry),
-									  &tinfo, fcinfo->flinfo));
+	PG_RETURN_FLOAT8(gbt_num_distance_x(&key, &query, queryDatum,
+										subtype, PG_GET_COLLATION(),
+										GIST_LEAF(entry),
+										&tinfo, fcinfo->flinfo));
 }
 
 Datum
 gbt_int8_union(PG_FUNCTION_ARGS)
 {
 	GistEntryVector *entryvec = (GistEntryVector *) PG_GETARG_POINTER(0);
diff --git a/contrib/btree_gist/btree_utils_num.c b/contrib/btree_gist/btree_utils_num.c
index 3affe4c2c46..68574b895c5 100644
--- a/contrib/btree_gist/btree_utils_num.c
+++ b/contrib/btree_gist/btree_utils_num.c
@@ -250,12 +250,32 @@ gbt_num_bin_union(Datum *u, GBT_NUMKEY *e, const gbtree_ninfo *tinfo, FmgrInfo *
 			memcpy(unconstify(GBT_NUMKEY *, ur.upper), rd.upper, tinfo->size);
 	}
 }
 
 
 
+/*
+ * Look up cross-type callbacks for a given query subtype. Returns NULL if
+ * the opclass doesn't advertise cross-type support for this subtype, in
+ * which case callers must fall back to the same-type path.
+ */
+static const gbt_subtype_info *
+gbt_find_subtype_ops(const gbtree_ninfo *tinfo, Oid subtype)
+{
+	const gbt_subtype_info *p;
+
+	if (subtype == InvalidOid || tinfo->subtype_ops == NULL)
+		return NULL;
+	for (p = tinfo->subtype_ops; p->subtype != InvalidOid; p++)
+	{
+		if (p->subtype == subtype)
+			return p;
+	}
+	return NULL;
+}
+
 /*
  * The GiST consistent method
  *
  * Note: we currently assume that no datatypes that use this routine are
  * collation-aware; so we don't bother passing collation through.
  */
@@ -304,12 +324,99 @@ gbt_num_consistent(const GBT_NUMKEY_R *key,
 			retval = false;
 	}
 
 	return retval;
 }
 
+/*
+ * Verify that "subtype" is one this opclass knows how to handle: it must
+ * either be InvalidOid / the opclass's native type, or be registered in
+ * the dispatch table. Anything else is an operator-family configuration
+ * error.
+ */
+static void
+gbt_check_subtype(const gbtree_ninfo *tinfo, Oid subtype)
+{
+	if (subtype == InvalidOid)
+		return;
+	if (tinfo->type_oid == InvalidOid)
+		return;
+	if (subtype == tinfo->type_oid)
+		return;
+	if (gbt_find_subtype_ops(tinfo, subtype) != NULL)
+		return;
+
+	elog(ERROR,
+		 "btree_gist: cross-type query with subtype %u is not supported "
+		 "by the opclass for type %u (operator-family dispatch table is "
+		 "out of sync with pg_amop)",
+		 subtype, tinfo->type_oid);
+}
+
+/*
+ * Cross-type aware consistent method.
+ */
+bool
+gbt_num_consistent_x(const GBT_NUMKEY_R *key,
+					 const void *query,
+					 Datum queryDatum,
+					 Oid subtype,
+					 Oid collation,
+					 const StrategyNumber *strategy,
+					 bool is_leaf,
+					 const gbtree_ninfo *tinfo,
+					 FmgrInfo *flinfo)
+{
+	const gbt_subtype_info *xt = gbt_find_subtype_ops(tinfo, subtype);
+	gbt_subtype_context cxt;
+
+	if (xt == NULL)
+	{
+		gbt_check_subtype(tinfo, subtype);
+		return gbt_num_consistent(key, query, strategy, is_leaf, tinfo, flinfo);
+	}
+
+	cxt.query = queryDatum;
+	cxt.subtype = subtype;
+	cxt.collation = collation;
+	cxt.flinfo = flinfo;
+	cxt.query_cache = NULL;
+
+	switch (*strategy)
+	{
+		case BTLessEqualStrategyNumber:
+			/* some k in [lower,upper] has k <= q iff lower <= q */
+			return xt->f_le(key->lower, &cxt);
+		case BTLessStrategyNumber:
+			/* leaf: key < q. internal: lower <= q (loose) */
+			return is_leaf ? xt->f_lt(key->lower, &cxt)
+				: xt->f_le(key->lower, &cxt);
+		case BTEqualStrategyNumber:
+			if (is_leaf)
+				return xt->f_eq(key->lower, &cxt);
+			/* internal: lower <= q <= upper */
+			return (xt->f_le(key->lower, &cxt) &&
+					xt->f_ge(key->upper, &cxt));
+		case BTGreaterStrategyNumber:
+
+			/*
+			 * leaf: key > q. internal: upper >= q (loose). Read upper on
+			 * leaves to match the same-type path.
+			 */
+			return is_leaf ? xt->f_gt(key->upper, &cxt)
+				: xt->f_ge(key->upper, &cxt);
+		case BTGreaterEqualStrategyNumber:
+			return xt->f_ge(key->upper, &cxt);
+		case BtreeGistNotEqualStrategyNumber:
+			return !(xt->f_eq(key->lower, &cxt) &&
+					 xt->f_eq(key->upper, &cxt));
+		default:
+			return false;
+	}
+}
+
 
 /*
  * The GiST distance method (for KNN-Gist)
  */
 
 float8
@@ -331,12 +438,53 @@ gbt_num_distance(const GBT_NUMKEY_R *key,
 	else
 		retval = 0.0;
 
 	return retval;
 }
 
+/*
+ * Cross-type aware distance method.
+ */
+float8
+gbt_num_distance_x(const GBT_NUMKEY_R *key,
+				   const void *query,
+				   Datum queryDatum,
+				   Oid subtype,
+				   Oid collation,
+				   bool is_leaf,
+				   const gbtree_ninfo *tinfo,
+				   FmgrInfo *flinfo)
+{
+	const gbt_subtype_info *xt = gbt_find_subtype_ops(tinfo, subtype);
+	gbt_subtype_context cxt;
+
+	if (xt == NULL)
+	{
+		gbt_check_subtype(tinfo, subtype);
+		return gbt_num_distance(key, query, is_leaf, tinfo, flinfo);
+	}
+
+	if (xt->f_dist == NULL)
+		elog(ERROR, "KNN search is not supported for btree_gist type %d with subtype %u",
+			 (int) tinfo->t, subtype);
+
+	cxt.query = queryDatum;
+	cxt.subtype = subtype;
+	cxt.collation = collation;
+	cxt.flinfo = flinfo;
+	cxt.query_cache = NULL;
+
+	/* q <= lower <=> lower >= q */
+	if (xt->f_ge(key->lower, &cxt))
+		return xt->f_dist(key->lower, &cxt);
+	/* q >= upper <=> upper <= q */
+	if (xt->f_le(key->upper, &cxt))
+		return xt->f_dist(key->upper, &cxt);
+	return 0.0;
+}
+
 
 GIST_SPLITVEC *
 gbt_num_picksplit(const GistEntryVector *entryvec, GIST_SPLITVEC *v,
 				  const gbtree_ninfo *tinfo, FmgrInfo *flinfo)
 {
 	OffsetNumber i,
diff --git a/contrib/btree_gist/btree_utils_num.h b/contrib/btree_gist/btree_utils_num.h
index 53e477d8b1e..dfb0a3bb41f 100644
--- a/contrib/btree_gist/btree_utils_num.h
+++ b/contrib/btree_gist/btree_utils_num.h
@@ -27,12 +27,42 @@ typedef struct
 	GBT_NUMKEY *t;
 } Nsrt;
 
 
 /* type description */
 
+/*
+ * Cross-type comparison support.
+ *
+ * For cross-type operator support an opclass may supply a NULL-terminated
+ * array of gbt_subtype_info entries, one per supported query subtype. Each
+ * callback receives the index key in its native C representation (pointer
+ * into the compressed key) and a context describing the query value.
+ */
+typedef struct gbt_subtype_context
+{
+	Datum		query;			/* Datum of cxt->subtype */
+	Oid			subtype;		/* right-hand/query operand type */
+	Oid			collation;		/* collation from the support call */
+	FmgrInfo   *flinfo;			/* support-function call context */
+	void	   *query_cache;	/* callback-owned per-call state */
+} gbt_subtype_context;
+
+typedef struct gbt_subtype_info
+{
+	Oid			subtype;		/* InvalidOid terminates the array */
+	bool		(*f_lt) (const void *key, gbt_subtype_context *cxt);
+	bool		(*f_le) (const void *key, gbt_subtype_context *cxt);
+	bool		(*f_eq) (const void *key, gbt_subtype_context *cxt);
+	bool		(*f_ge) (const void *key, gbt_subtype_context *cxt);
+	bool		(*f_gt) (const void *key, gbt_subtype_context *cxt);
+
+	/* NULL if no KNN */
+	float8		(*f_dist) (const void *key, gbt_subtype_context *cxt);
+} gbt_subtype_info;
+
 typedef struct
 {
 
 	/* Attribs */
 
 	enum gbtree_type t;			/* data type */
@@ -45,14 +75,59 @@ typedef struct
 	bool		(*f_ge) (const void *, const void *, FmgrInfo *);	/* greater or equal */
 	bool		(*f_eq) (const void *, const void *, FmgrInfo *);	/* equal */
 	bool		(*f_le) (const void *, const void *, FmgrInfo *);	/* less or equal */
 	bool		(*f_lt) (const void *, const void *, FmgrInfo *);	/* less than */
 	int			(*f_cmp) (const void *, const void *, FmgrInfo *);	/* key compare function */
 	float8		(*f_dist) (const void *, const void *, FmgrInfo *); /* key distance function */
+
+	/*
+	 * Optional NULL-terminated array of cross-type comparison callbacks. NULL
+	 * if the opclass only supports same-type comparisons.
+	 */
+	const gbt_subtype_info *subtype_ops;
+
+	/*
+	 * Native pg_type OID of the indexed type. Used by the _x APIs to validate
+	 * the subtype passed in from the planner. InvalidOid disables that check,
+	 * which is right for legacy opclasses that don't use _x.
+	 */
+	Oid			type_oid;
 } gbtree_ninfo;
 
+#define GBT_DEFINE_INT_CROSSTYPE(prefix, key_ctype, get_sub) \
+static bool \
+prefix##_lt(const void *k, gbt_subtype_context *cxt) \
+{ \
+	return (int64) *(const key_ctype *) k < (int64) get_sub(cxt->query); \
+} \
+static bool \
+prefix##_le(const void *k, gbt_subtype_context *cxt) \
+{ \
+	return (int64) *(const key_ctype *) k <= (int64) get_sub(cxt->query); \
+} \
+static bool \
+prefix##_eq(const void *k, gbt_subtype_context *cxt) \
+{ \
+	return (int64) *(const key_ctype *) k == (int64) get_sub(cxt->query); \
+} \
+static bool \
+prefix##_ge(const void *k, gbt_subtype_context *cxt) \
+{ \
+	return (int64) *(const key_ctype *) k >= (int64) get_sub(cxt->query); \
+} \
+static bool \
+prefix##_gt(const void *k, gbt_subtype_context *cxt) \
+{ \
+	return (int64) *(const key_ctype *) k > (int64) get_sub(cxt->query); \
+} \
+static float8 \
+prefix##_dist(const void *k, gbt_subtype_context *cxt) \
+{ \
+	return fabs((float8) *(const key_ctype *) k - (float8) get_sub(cxt->query)); \
+}
+
 
 /*
  *	Numeric btree functions
  */
 
 
@@ -92,15 +167,25 @@ typedef struct
 extern Interval *abs_interval(Interval *a);
 
 extern bool gbt_num_consistent(const GBT_NUMKEY_R *key, const void *query,
 							   const StrategyNumber *strategy, bool is_leaf,
 							   const gbtree_ninfo *tinfo, FmgrInfo *flinfo);
 
+extern bool gbt_num_consistent_x(const GBT_NUMKEY_R *key, const void *query,
+								 Datum queryDatum, Oid subtype, Oid collation,
+								 const StrategyNumber *strategy, bool is_leaf,
+								 const gbtree_ninfo *tinfo, FmgrInfo *flinfo);
+
 extern float8 gbt_num_distance(const GBT_NUMKEY_R *key, const void *query,
 							   bool is_leaf, const gbtree_ninfo *tinfo, FmgrInfo *flinfo);
 
+extern float8 gbt_num_distance_x(const GBT_NUMKEY_R *key, const void *query,
+								 Datum queryDatum, Oid subtype, Oid collation,
+								 bool is_leaf,
+								 const gbtree_ninfo *tinfo, FmgrInfo *flinfo);
+
 extern GIST_SPLITVEC *gbt_num_picksplit(const GistEntryVector *entryvec, GIST_SPLITVEC *v,
 										const gbtree_ninfo *tinfo, FmgrInfo *flinfo);
 
 extern GISTENTRY *gbt_num_compress(GISTENTRY *entry, const gbtree_ninfo *tinfo);
 
 extern GISTENTRY *gbt_num_fetch(GISTENTRY *entry, const gbtree_ninfo *tinfo);
diff --git a/contrib/btree_gist/meson.build b/contrib/btree_gist/meson.build
index 2b1a5463289..c640cb7d8d2 100644
--- a/contrib/btree_gist/meson.build
+++ b/contrib/btree_gist/meson.build
@@ -49,12 +49,13 @@ install_data(
   'btree_gist--1.4--1.5.sql',
   'btree_gist--1.5--1.6.sql',
   'btree_gist--1.6--1.7.sql',
   'btree_gist--1.7--1.8.sql',
   'btree_gist--1.8--1.9.sql',
   'btree_gist--1.9.sql',
+  'btree_gist--1.9--1.10.sql',
   kwargs: contrib_data_args,
 )
 
 tests += {
   'name': 'btree_gist',
   'sd': meson.current_source_dir(),
@@ -89,10 +90,11 @@ tests += {
       'uuid',
       'not_equal',
       'enum',
       'bool',
       'partitions',
       'stratnum',
+      'int_crosstype',
       'without_overlaps',
     ],
   },
 }
-- 
2.51.0



^ permalink  raw  reply  [nested|flat] 9+ messages in thread

* Re: [PATCH] btree_gist: add cross-type integer operator support for GiST
@ 2026-06-05 18:49  Andrey Borodin <[email protected]>
  parent: Alexander Nestorov <[email protected]>
  1 sibling, 1 reply; 9+ messages in thread

From: Andrey Borodin @ 2026-06-05 18:49 UTC (permalink / raw)
  To: Alexander Nestorov <[email protected]>; +Cc: pgsql-hackers

Hi Alexander!

Thanks for working on this - this looks like useful feature and
btree_gist users will appreciate it.

> When a query provides a value whose type is compatible but different
> from the column type, the planner cannot use the index for that column.
> [...] The workaround is to write an explicit cast in every query:
>     WHERE camera_id = 1189::int8
> This is fragile as ORMs, application parameter binding, and even hand-
> written SQL queries produce values whose types do not exactly match
> the column type.

This bites particularly hard with composite indexes that mix a PostGIS
column with a plain identifier - a layer number, tenant id, etc.:

    CREATE INDEX ON t USING gist (layer_id, geom);
    ... WHERE geom && :bbox AND layer_id = 42;

Today the integer side forces a cast to be index-usable, which seems ugly
and easy to forget, and the ORM argument above only makes it worse. So
I think this is worth pursuing.

A few things that seem important to me (though discussion may well prove
some of them minor):

1. GiST consistent() is CPU-bound (it runs for every key on every
visited page), so the bar here should be strictly zero speed regression
for existing same-type users. Note sk_subtype is the operator's right
operand type, so for ordinary same-type scans it is the native type,
not InvalidOid - meaning the same-type path now also goes through the
cross-type dispatch and scans the subtype table. Please add some fast
pathm and back it with a microbenchmark on a same-type workload.

2. On the "general foundation" framing:

> Other btree_gist opclasses (float4/float8, date, timestamp, ...) and
> even range-type GiST opfamilies in core can adopt the same pattern

I'd be cautious here. The scalar int64/fabs() dispatch shape does not
obviously fit range types (not scalar at all), and timestamp/date bring
their own questions (infinity, etc.). It may be better to solve types
as they actually come up and generalize the scaffolding as load on it
grows, rather than commit to a universal framework upfront.

3. The cross-type knowledge lives in two hand-maintained places - the
pg_amop entries in SQL and the C dispatch tables:

> I propose [...] to dispatch cross-type queries directly inside the
> existing consistent and distance functions and use the existing
> subtype OID argument.

They are only reconciled at query time via an elog(), not at
amvalidate/CREATE time. (Version skew between the .so and the catalog
isn't the worry - the new extension version ships with the new major.)
The concern is plain authoring drift: adding an amop without the
matching C entry, or vice versa, passes validation and only fails on a
live query. It would be nicer if we could assert that every cross-type
amop in the family has a corresponding dispatch entry (in amvalidate?).

4. KNN: the distance callbacks compute fabs() in float8, so for int8
values beyond 2^53 the lower-bound distance loses precision. This
matches the existing same-type int8 KNN, so it's not a regression, but
since you're widening the mix it's worth calling out as a known limit.

The missing int_crosstype test files (referenced from REGRESS/meson but
not in the patch) break make check, but I take it that's just the
not-yet-included test round.

Did you register your patch on the commitfest? [0]

Thank you!


Best regards, Andrey Borodin.

[0] https://commitfest.postgresql.org/59/







^ permalink  raw  reply  [nested|flat] 9+ messages in thread

* Re: [PATCH] btree_gist: add cross-type integer operator support for GiST
@ 2026-06-06 17:37  Alexander Nestorov <[email protected]>
  parent: Andrey Borodin <[email protected]>
  0 siblings, 1 reply; 9+ messages in thread

From: Alexander Nestorov @ 2026-06-06 17:37 UTC (permalink / raw)
  To: Andrey Borodin <[email protected]>; +Cc: pgsql-hackers

Hey Andrey!

I'm happy to hear you like the proposal!
Let me reply in order:

1)

Oh, good catch! Since the other points require some changes in the patch, I'll
first do them and then I'll make sure to create a benchmark and post the
results.

2)

Agreed, that's a fair point. I was speculating with a generalized "foundation"
because I thought I'd get questions about how I'd support other types if I went
with a simpler patch that covered only ints.

Let me rework that and send a new patch!

3)

I was also worried about this. I wrote some checks against a hardcoded list in
the int_crosstype.sql (which I didn't submit, you noticed), but that is not
checking anything besides the SQL part.

I checked amvalidate(), but I couldn't find a way to check if there are
inconsistencies between what is defined in C and what is defined in SQL.
That function only checks amproc/amop signatures and I wasn't able to see how it
could compare against the C dispatch.
There is a comment in opclasscmds.c, around line 400, that already hints about
it not being able to run such validation.

> we have no way to validate that the offered set of operators and
> functions are consistent with the AM's expectations.  It would be
> nice to provide such a check someday

I could create a new core hook, but maybe that would be a larger change that
will expand the scope of this patch, and since neither CREATE nor ALTER ADD
calls it, the hook would still need a test to fire it.

As an alternative, maybe I could use the regression suite? I can put the
knowledge of supported dispatch entries in the code, expose it as a validator
and assert it in the tests. Running "make check" would assert that these entries
agree with pg_amop in both directions.

Concretely: the integer consistent/distance functions would dispatch through a
small static table of supported subtype OIDs. A set-returning C function would
expose that same table to SQL, and a regression test would EXCEPT it against the
cross-type rows in pg_amop for gist_int{2,4,8}_ops in both directions. Adding an
amop without a matching dispatch entry (or vice versa) would show up as a diff
under make check.

Would you prefer the regression suite approach? Or do you think it might be
a better idea to find a way to do it with amvalidate()?

4)

All right, I'll make sure to add it to the next patch.

To sum up, agreeing on 3) means I'd be ready for working on the next version of
the patch. I just need your advice on that bit.

I was not aware of the existence of commitfest, let me register :)

Thank you very much Andrey, best regards!


^ permalink  raw  reply  [nested|flat] 9+ messages in thread

* Re: [PATCH] btree_gist: add cross-type integer operator support for GiST
@ 2026-06-13 23:35  Alexander Nestorov <[email protected]>
  parent: Alexander Nestorov <[email protected]>
  0 siblings, 1 reply; 9+ messages in thread

From: Alexander Nestorov @ 2026-06-13 23:35 UTC (permalink / raw)
  To: Andrey Borodin <[email protected]>; +Cc: pgsql-hackers

Hello Andrey!

Following up with the things I owed you: the benchmarks, the consistency check
and adding a note for the 2^53 case.

I added a fast path. Each integer opclass's consistent() / distance() now
detects the "same type" case and calls the original gbt_num_consistent() /
gbt_num_distance() directly.

To confirm there's no regression I ran a microbenchmark on an -O2 build, no
asserts, single client, over a 500k row int4 GiST index, with the following
options:

-c enable_seqscan=off \
-c enable_bitmapscan=off \
-c enable_sort=off \
-c max_parallel_workers_per_gather=0

This is the base for the bench:

CREATE EXTENSION IF NOT EXISTS btree_gist;
DROP TABLE IF EXISTS benchg;
CREATE TABLE benchg (a int4);
INSERT INTO benchg SELECT g FROM generate_series(0, 499999) g;
CREATE INDEX benchg_idx ON benchg USING gist (a);
VACUUM (ANALYZE, FREEZE) benchg;

And the two workloads:

consistent(), full-range index-only count(*):
SELECT count(*) FROM benchg WHERE a >= 0 AND a <= 499999;

distance(), full KNN ordering (ORDER BY a<->k over all rows):
SELECT count(*) FROM (SELECT a FROM benchg ORDER BY a <-> 250000 LIMIT 1000000) q;

The numbers in ms (12 repetitions, 15s each) before
(3e3d7875e95621b02311ea3443e5139e3bce944a) and after my patch:

  before   consistent   min/med/mean = 51.754 52.718 54.137 ms
  after    consistent   min/med/mean = 52.042 52.480 52.572 ms
  ------------------------------------------------------------------------
  before   distance     min/med/mean = 76.863 77.177 77.395 ms
  after    distance     min/med/mean = 77.357 77.803 77.980 ms

All numbers seem to be within measurement noise, except the consistent-before,
which is probably inflated by one slow rep.

Regarding the other point, I explored the regression suite path I mentioned.

The consistent() / distance() functions dispatch cross-type queries through a
single static table of supported subtype OIDs (gbt_int_crosstype_table in
btree_utils_num.c). I expose that exact table to SQL, in gbt_int_crosstype_subtypes(),
so there is no hand-maintained second copy of the list.

The int_crosstype.sql regression test then builds the set of cross-type
(lefttype, righttype, strategy) entries that should exist in pg_amop from that
function, and EXCEPTs it against the cross-type rows actually present in
gist_int{2,4,8}_ops:

  - a pg_amop row whose subtype the C dispatch does not handle shows up as
    "unexpected in pg_amop", and
  - a dispatch entry without the matching pg_amop rows shows up as
    "missing from pg_amop".

Either kind of drift produces a diff under `make check`. So adding an ALTER
OPERATOR FAMILY entry without a matching dispatch entry (or vice versa) fails
the suite (as I mentioned in my previous email, I'm not aware of a way to do
this with amvalidate() without patching core).

I'm attaching the new set of patches (this time I include the tests).

Best regards!


Attachments:

  [application/octet-stream] 0001-Implement-cross-type-operators-for-GiST-indexes.patch (26.4K, 3-0001-Implement-cross-type-operators-for-GiST-indexes.patch)
  download | inline diff:
From 363b9c22874cdbb6cf780899c784c813bb10f94e Mon Sep 17 00:00:00 2001
From: Alexander Nestorov <[email protected]>
Date: Thu, 4 Jun 2026 00:06:54 +0200
Subject: [PATCH] Implement cross-type operators for GiST indexes

---
 contrib/btree_gist/Makefile                  |   4 +-
 contrib/btree_gist/btree_gist--1.9--1.10.sql | 143 +++++++++++++++++
 contrib/btree_gist/btree_gist.control        |   2 +-
 contrib/btree_gist/btree_int2.c              |  70 ++++++--
 contrib/btree_gist/btree_int4.c              |  70 ++++++--
 contrib/btree_gist/btree_int8.c              |  70 ++++++--
 contrib/btree_gist/btree_utils_num.c         | 160 +++++++++++++++++++
 contrib/btree_gist/btree_utils_num.h         |  16 ++
 contrib/btree_gist/meson.build               |   2 +
 9 files changed, 501 insertions(+), 36 deletions(-)
 create mode 100644 contrib/btree_gist/btree_gist--1.9--1.10.sql

diff --git a/contrib/btree_gist/Makefile b/contrib/btree_gist/Makefile
index fbbbca95598..1d0668d97ab 100644
--- a/contrib/btree_gist/Makefile
+++ b/contrib/btree_gist/Makefile
@@ -32,19 +32,19 @@ OBJS =  \
 EXTENSION = btree_gist
 DATA = btree_gist--1.0--1.1.sql \
        btree_gist--1.1--1.2.sql btree_gist--1.2--1.3.sql \
        btree_gist--1.3--1.4.sql btree_gist--1.4--1.5.sql \
        btree_gist--1.5--1.6.sql btree_gist--1.6--1.7.sql \
        btree_gist--1.7--1.8.sql btree_gist--1.8--1.9.sql \
-       btree_gist--1.9.sql
+       btree_gist--1.9.sql btree_gist--1.9--1.10.sql
 PGFILEDESC = "btree_gist - B-tree equivalent GiST operator classes"
 
 REGRESS = init int2 int4 int8 float4 float8 cash oid timestamp timestamptz \
         time timetz date interval macaddr macaddr8 inet cidr text varchar char \
         bytea bit varbit numeric uuid not_equal enum bool partitions \
-        stratnum without_overlaps
+        stratnum int_crosstype without_overlaps
 
 SHLIB_LINK += $(filter -lm, $(LIBS))
 
 ifdef USE_PGXS
 PG_CONFIG = pg_config
 PGXS := $(shell $(PG_CONFIG) --pgxs)
diff --git a/contrib/btree_gist/btree_gist--1.9--1.10.sql b/contrib/btree_gist/btree_gist--1.9--1.10.sql
new file mode 100644
index 00000000000..c9ff1955204
--- /dev/null
+++ b/contrib/btree_gist/btree_gist--1.9--1.10.sql
@@ -0,0 +1,143 @@
+/* contrib/btree_gist/btree_gist--1.9--1.10.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION btree_gist UPDATE TO '1.10'" to load this file. \quit
+
+-- Add cross-type operator support for the integer trio (int2, int4, int8)
+-- to the existing GiST operator families.
+--
+-- GiST's amvalidate requires support functions in a family to have matching
+-- left/right input types, so the catalog additions below are deliberately
+-- pg_amop-only. The existing consistent/distance support functions dispatch
+-- on the subtype OID: same-type queries take the normal path, while mixed-width
+-- integer queries are promoted to int64 and compared by the integer cross-type
+-- helpers in btree_utils_num.c.
+
+CREATE FUNCTION int2_int4_dist(int2, int4)
+RETURNS int4
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION int4_int2_dist(int4, int2)
+RETURNS int4
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION int2_int8_dist(int2, int8)
+RETURNS int8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION int8_int2_dist(int8, int2)
+RETURNS int8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION int4_int8_dist(int4, int8)
+RETURNS int8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION int8_int4_dist(int8, int4)
+RETURNS int8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+-- Introspection helper exposing the integer query subtypes understood by the C
+-- cross-type dispatch (gbt_int_crosstype_table in btree_utils_num.c). The
+-- regression tests use it to assert that the cross-type pg_amop entries below
+-- never drift from what the dispatch can handle.
+CREATE FUNCTION gbt_int_crosstype_subtypes()
+RETURNS SETOF oid
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE PARALLEL SAFE;
+
+CREATE OPERATOR <-> (
+	LEFTARG = int2,
+	RIGHTARG = int4,
+	PROCEDURE = int2_int4_dist,
+	COMMUTATOR = '<->'
+);
+
+CREATE OPERATOR <-> (
+	LEFTARG = int4,
+	RIGHTARG = int2,
+	PROCEDURE = int4_int2_dist,
+	COMMUTATOR = '<->'
+);
+
+CREATE OPERATOR <-> (
+	LEFTARG = int2,
+	RIGHTARG = int8,
+	PROCEDURE = int2_int8_dist,
+	COMMUTATOR = '<->'
+);
+
+CREATE OPERATOR <-> (
+	LEFTARG = int8,
+	RIGHTARG = int2,
+	PROCEDURE = int8_int2_dist,
+	COMMUTATOR = '<->'
+);
+
+CREATE OPERATOR <-> (
+	LEFTARG = int4,
+	RIGHTARG = int8,
+	PROCEDURE = int4_int8_dist,
+	COMMUTATOR = '<->'
+);
+
+CREATE OPERATOR <-> (
+	LEFTARG = int8,
+	RIGHTARG = int4,
+	PROCEDURE = int8_int4_dist,
+	COMMUTATOR = '<->'
+);
+
+ALTER OPERATOR FAMILY gist_int2_ops USING gist ADD
+	OPERATOR	1	<  (int2, int4),
+	OPERATOR	2	<= (int2, int4),
+	OPERATOR	3	=  (int2, int4),
+	OPERATOR	4	>= (int2, int4),
+	OPERATOR	5	>  (int2, int4),
+	OPERATOR	6	<> (int2, int4),
+	OPERATOR	15	<-> (int2, int4) FOR ORDER BY pg_catalog.integer_ops,
+	OPERATOR	1	<  (int2, int8),
+	OPERATOR	2	<= (int2, int8),
+	OPERATOR	3	=  (int2, int8),
+	OPERATOR	4	>= (int2, int8),
+	OPERATOR	5	>  (int2, int8),
+	OPERATOR	6	<> (int2, int8),
+	OPERATOR	15	<-> (int2, int8) FOR ORDER BY pg_catalog.integer_ops;
+
+ALTER OPERATOR FAMILY gist_int4_ops USING gist ADD
+	OPERATOR	1	<  (int4, int2),
+	OPERATOR	2	<= (int4, int2),
+	OPERATOR	3	=  (int4, int2),
+	OPERATOR	4	>= (int4, int2),
+	OPERATOR	5	>  (int4, int2),
+	OPERATOR	6	<> (int4, int2),
+	OPERATOR	15	<-> (int4, int2) FOR ORDER BY pg_catalog.integer_ops,
+	OPERATOR	1	<  (int4, int8),
+	OPERATOR	2	<= (int4, int8),
+	OPERATOR	3	=  (int4, int8),
+	OPERATOR	4	>= (int4, int8),
+	OPERATOR	5	>  (int4, int8),
+	OPERATOR	6	<> (int4, int8),
+	OPERATOR	15	<-> (int4, int8) FOR ORDER BY pg_catalog.integer_ops;
+
+ALTER OPERATOR FAMILY gist_int8_ops USING gist ADD
+	OPERATOR	1	<  (int8, int2),
+	OPERATOR	2	<= (int8, int2),
+	OPERATOR	3	=  (int8, int2),
+	OPERATOR	4	>= (int8, int2),
+	OPERATOR	5	>  (int8, int2),
+	OPERATOR	6	<> (int8, int2),
+	OPERATOR	15	<-> (int8, int2) FOR ORDER BY pg_catalog.integer_ops,
+	OPERATOR	1	<  (int8, int4),
+	OPERATOR	2	<= (int8, int4),
+	OPERATOR	3	=  (int8, int4),
+	OPERATOR	4	>= (int8, int4),
+	OPERATOR	5	>  (int8, int4),
+	OPERATOR	6	<> (int8, int4),
+	OPERATOR	15	<-> (int8, int4) FOR ORDER BY pg_catalog.integer_ops;
diff --git a/contrib/btree_gist/btree_gist.control b/contrib/btree_gist/btree_gist.control
index 69d9341a0ad..e606fa6551d 100644
--- a/contrib/btree_gist/btree_gist.control
+++ b/contrib/btree_gist/btree_gist.control
@@ -1,6 +1,6 @@
 # btree_gist extension
 comment = 'support for indexing common datatypes in GiST'
-default_version = '1.9'
+default_version = '1.10'
 module_pathname = '$libdir/btree_gist'
 relocatable = true
 trusted = true
diff --git a/contrib/btree_gist/btree_int2.c b/contrib/btree_gist/btree_int2.c
index cc4b33177e3..d00b24d9a57 100644
--- a/contrib/btree_gist/btree_int2.c
+++ b/contrib/btree_gist/btree_int2.c
@@ -2,12 +2,13 @@
  * contrib/btree_gist/btree_int2.c
  */
 #include "postgres.h"
 
 #include "btree_gist.h"
 #include "btree_utils_num.h"
+#include "catalog/pg_type.h"
 #include "common/int.h"
 #include "utils/rel.h"
 #include "utils/sortsupport.h"
 
 typedef struct int16key
 {
@@ -73,13 +74,12 @@ gbt_int2key_cmp(const void *a, const void *b, FmgrInfo *flinfo)
 static float8
 gbt_int2_dist(const void *a, const void *b, FmgrInfo *flinfo)
 {
 	return GET_FLOAT_DISTANCE(int16, a, b);
 }
 
-
 static const gbtree_ninfo tinfo =
 {
 	gbt_t_int2,
 	sizeof(int16),
 	4,							/* sizeof(gbtreekey4) */
 	gbt_int2gt,
@@ -109,12 +109,46 @@ int2_dist(PG_FUNCTION_ARGS)
 
 	ra = abs(r);
 
 	PG_RETURN_INT16(ra);
 }
 
+PG_FUNCTION_INFO_V1(int2_int4_dist);
+Datum
+int2_int4_dist(PG_FUNCTION_ARGS)
+{
+	int32		a = (int32) PG_GETARG_INT16(0);
+	int32		b = PG_GETARG_INT32(1);
+	int32		r;
+
+	if (pg_sub_s32_overflow(a, b, &r) ||
+		r == PG_INT32_MIN)
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("integer out of range")));
+
+	PG_RETURN_INT32(abs(r));
+}
+
+PG_FUNCTION_INFO_V1(int2_int8_dist);
+Datum
+int2_int8_dist(PG_FUNCTION_ARGS)
+{
+	int64		a = (int64) PG_GETARG_INT16(0);
+	int64		b = PG_GETARG_INT64(1);
+	int64		r;
+
+	if (pg_sub_s64_overflow(a, b, &r) ||
+		r == PG_INT64_MIN)
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("bigint out of range")));
+
+	PG_RETURN_INT64(i64abs(r));
+}
+
 
 /**************************************************
  * GiST support functions
  **************************************************/
 
 Datum
@@ -134,47 +168,61 @@ gbt_int2_fetch(PG_FUNCTION_ARGS)
 }
 
 Datum
 gbt_int2_consistent(PG_FUNCTION_ARGS)
 {
 	GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
-	int16		query = PG_GETARG_INT16(1);
+	Datum		queryDatum = PG_GETARG_DATUM(1);
 	StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2);
-#ifdef NOT_USED
 	Oid			subtype = PG_GETARG_OID(3);
-#endif
 	bool	   *recheck = (bool *) PG_GETARG_POINTER(4);
 	int16KEY   *kkk = (int16KEY *) DatumGetPointer(entry->key);
+	int16		query;
 	GBT_NUMKEY_R key;
 
 	/* All cases served by this function are exact */
 	*recheck = false;
 
 	key.lower = (GBT_NUMKEY *) &kkk->lower;
 	key.upper = (GBT_NUMKEY *) &kkk->upper;
 
-	PG_RETURN_BOOL(gbt_num_consistent(&key, &query, &strategy,
-									  GIST_LEAF(entry), &tinfo, fcinfo->flinfo));
+	if (likely(subtype == InvalidOid || subtype == INT2OID))
+	{
+		query = DatumGetInt16(queryDatum);
+		PG_RETURN_BOOL(gbt_num_consistent(&key, &query, &strategy,
+										  GIST_LEAF(entry), &tinfo,
+										  fcinfo->flinfo));
+	}
+
+	PG_RETURN_BOOL(gbt_int_consistent_x((int64) kkk->lower, (int64) kkk->upper,
+										queryDatum, subtype, &strategy,
+										GIST_LEAF(entry)));
 }
 
 Datum
 gbt_int2_distance(PG_FUNCTION_ARGS)
 {
 	GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
-	int16		query = PG_GETARG_INT16(1);
-#ifdef NOT_USED
+	Datum		queryDatum = PG_GETARG_DATUM(1);
 	Oid			subtype = PG_GETARG_OID(3);
-#endif
 	int16KEY   *kkk = (int16KEY *) DatumGetPointer(entry->key);
+	int16		query;
 	GBT_NUMKEY_R key;
 
 	key.lower = (GBT_NUMKEY *) &kkk->lower;
 	key.upper = (GBT_NUMKEY *) &kkk->upper;
 
-	PG_RETURN_FLOAT8(gbt_num_distance(&key, &query, GIST_LEAF(entry),
-									  &tinfo, fcinfo->flinfo));
+	if (likely(subtype == InvalidOid || subtype == INT2OID))
+	{
+		query = DatumGetInt16(queryDatum);
+		PG_RETURN_FLOAT8(gbt_num_distance(&key, &query, GIST_LEAF(entry),
+										  &tinfo, fcinfo->flinfo));
+	}
+
+	PG_RETURN_FLOAT8(gbt_int_distance_x((int64) kkk->lower, (int64) kkk->upper,
+										queryDatum, subtype));
 }
 
 Datum
 gbt_int2_union(PG_FUNCTION_ARGS)
 {
 	GistEntryVector *entryvec = (GistEntryVector *) PG_GETARG_POINTER(0);
diff --git a/contrib/btree_gist/btree_int4.c b/contrib/btree_gist/btree_int4.c
index 47790578e6b..4df91f2057c 100644
--- a/contrib/btree_gist/btree_int4.c
+++ b/contrib/btree_gist/btree_int4.c
@@ -1,12 +1,13 @@
 /*
  * contrib/btree_gist/btree_int4.c
  */
 #include "postgres.h"
 #include "btree_gist.h"
 #include "btree_utils_num.h"
+#include "catalog/pg_type.h"
 #include "common/int.h"
 #include "utils/rel.h"
 #include "utils/sortsupport.h"
 
 typedef struct int32key
 {
@@ -71,13 +72,12 @@ gbt_int4key_cmp(const void *a, const void *b, FmgrInfo *flinfo)
 static float8
 gbt_int4_dist(const void *a, const void *b, FmgrInfo *flinfo)
 {
 	return GET_FLOAT_DISTANCE(int32, a, b);
 }
 
-
 static const gbtree_ninfo tinfo =
 {
 	gbt_t_int4,
 	sizeof(int32),
 	8,							/* sizeof(gbtreekey8) */
 	gbt_int4gt,
@@ -107,12 +107,46 @@ int4_dist(PG_FUNCTION_ARGS)
 
 	ra = abs(r);
 
 	PG_RETURN_INT32(ra);
 }
 
+PG_FUNCTION_INFO_V1(int4_int2_dist);
+Datum
+int4_int2_dist(PG_FUNCTION_ARGS)
+{
+	int32		a = PG_GETARG_INT32(0);
+	int32		b = (int32) PG_GETARG_INT16(1);
+	int32		r;
+
+	if (pg_sub_s32_overflow(a, b, &r) ||
+		r == PG_INT32_MIN)
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("integer out of range")));
+
+	PG_RETURN_INT32(abs(r));
+}
+
+PG_FUNCTION_INFO_V1(int4_int8_dist);
+Datum
+int4_int8_dist(PG_FUNCTION_ARGS)
+{
+	int64		a = (int64) PG_GETARG_INT32(0);
+	int64		b = PG_GETARG_INT64(1);
+	int64		r;
+
+	if (pg_sub_s64_overflow(a, b, &r) ||
+		r == PG_INT64_MIN)
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("bigint out of range")));
+
+	PG_RETURN_INT64(i64abs(r));
+}
+
 
 /**************************************************
  * GiST support functions
  **************************************************/
 
 Datum
@@ -132,47 +166,61 @@ gbt_int4_fetch(PG_FUNCTION_ARGS)
 }
 
 Datum
 gbt_int4_consistent(PG_FUNCTION_ARGS)
 {
 	GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
-	int32		query = PG_GETARG_INT32(1);
+	Datum		queryDatum = PG_GETARG_DATUM(1);
 	StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2);
-#ifdef NOT_USED
 	Oid			subtype = PG_GETARG_OID(3);
-#endif
 	bool	   *recheck = (bool *) PG_GETARG_POINTER(4);
 	int32KEY   *kkk = (int32KEY *) DatumGetPointer(entry->key);
+	int32		query;
 	GBT_NUMKEY_R key;
 
 	/* All cases served by this function are exact */
 	*recheck = false;
 
 	key.lower = (GBT_NUMKEY *) &kkk->lower;
 	key.upper = (GBT_NUMKEY *) &kkk->upper;
 
-	PG_RETURN_BOOL(gbt_num_consistent(&key, &query, &strategy,
-									  GIST_LEAF(entry), &tinfo, fcinfo->flinfo));
+	if (likely(subtype == InvalidOid || subtype == INT4OID))
+	{
+		query = DatumGetInt32(queryDatum);
+		PG_RETURN_BOOL(gbt_num_consistent(&key, &query, &strategy,
+										  GIST_LEAF(entry), &tinfo,
+										  fcinfo->flinfo));
+	}
+
+	PG_RETURN_BOOL(gbt_int_consistent_x((int64) kkk->lower, (int64) kkk->upper,
+										queryDatum, subtype, &strategy,
+										GIST_LEAF(entry)));
 }
 
 Datum
 gbt_int4_distance(PG_FUNCTION_ARGS)
 {
 	GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
-	int32		query = PG_GETARG_INT32(1);
-#ifdef NOT_USED
+	Datum		queryDatum = PG_GETARG_DATUM(1);
 	Oid			subtype = PG_GETARG_OID(3);
-#endif
 	int32KEY   *kkk = (int32KEY *) DatumGetPointer(entry->key);
+	int32		query;
 	GBT_NUMKEY_R key;
 
 	key.lower = (GBT_NUMKEY *) &kkk->lower;
 	key.upper = (GBT_NUMKEY *) &kkk->upper;
 
-	PG_RETURN_FLOAT8(gbt_num_distance(&key, &query, GIST_LEAF(entry),
-									  &tinfo, fcinfo->flinfo));
+	if (likely(subtype == InvalidOid || subtype == INT4OID))
+	{
+		query = DatumGetInt32(queryDatum);
+		PG_RETURN_FLOAT8(gbt_num_distance(&key, &query, GIST_LEAF(entry),
+										  &tinfo, fcinfo->flinfo));
+	}
+
+	PG_RETURN_FLOAT8(gbt_int_distance_x((int64) kkk->lower, (int64) kkk->upper,
+										queryDatum, subtype));
 }
 
 Datum
 gbt_int4_union(PG_FUNCTION_ARGS)
 {
 	GistEntryVector *entryvec = (GistEntryVector *) PG_GETARG_POINTER(0);
diff --git a/contrib/btree_gist/btree_int8.c b/contrib/btree_gist/btree_int8.c
index f48122c8d84..ae01273b709 100644
--- a/contrib/btree_gist/btree_int8.c
+++ b/contrib/btree_gist/btree_int8.c
@@ -2,12 +2,13 @@
  * contrib/btree_gist/btree_int8.c
  */
 #include "postgres.h"
 
 #include "btree_gist.h"
 #include "btree_utils_num.h"
+#include "catalog/pg_type.h"
 #include "common/int.h"
 #include "utils/rel.h"
 #include "utils/sortsupport.h"
 
 typedef struct int64key
 {
@@ -73,13 +74,12 @@ gbt_int8key_cmp(const void *a, const void *b, FmgrInfo *flinfo)
 static float8
 gbt_int8_dist(const void *a, const void *b, FmgrInfo *flinfo)
 {
 	return GET_FLOAT_DISTANCE(int64, a, b);
 }
 
-
 static const gbtree_ninfo tinfo =
 {
 	gbt_t_int8,
 	sizeof(int64),
 	16,							/* sizeof(gbtreekey16) */
 	gbt_int8gt,
@@ -109,12 +109,46 @@ int8_dist(PG_FUNCTION_ARGS)
 
 	ra = i64abs(r);
 
 	PG_RETURN_INT64(ra);
 }
 
+PG_FUNCTION_INFO_V1(int8_int2_dist);
+Datum
+int8_int2_dist(PG_FUNCTION_ARGS)
+{
+	int64		a = PG_GETARG_INT64(0);
+	int64		b = (int64) PG_GETARG_INT16(1);
+	int64		r;
+
+	if (pg_sub_s64_overflow(a, b, &r) ||
+		r == PG_INT64_MIN)
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("bigint out of range")));
+
+	PG_RETURN_INT64(i64abs(r));
+}
+
+PG_FUNCTION_INFO_V1(int8_int4_dist);
+Datum
+int8_int4_dist(PG_FUNCTION_ARGS)
+{
+	int64		a = PG_GETARG_INT64(0);
+	int64		b = (int64) PG_GETARG_INT32(1);
+	int64		r;
+
+	if (pg_sub_s64_overflow(a, b, &r) ||
+		r == PG_INT64_MIN)
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("bigint out of range")));
+
+	PG_RETURN_INT64(i64abs(r));
+}
+
 
 /**************************************************
  * GiST support functions
  **************************************************/
 
 Datum
@@ -134,47 +168,61 @@ gbt_int8_fetch(PG_FUNCTION_ARGS)
 }
 
 Datum
 gbt_int8_consistent(PG_FUNCTION_ARGS)
 {
 	GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
-	int64		query = PG_GETARG_INT64(1);
+	Datum		queryDatum = PG_GETARG_DATUM(1);
 	StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2);
-#ifdef NOT_USED
 	Oid			subtype = PG_GETARG_OID(3);
-#endif
 	bool	   *recheck = (bool *) PG_GETARG_POINTER(4);
 	int64KEY   *kkk = (int64KEY *) DatumGetPointer(entry->key);
+	int64		query;
 	GBT_NUMKEY_R key;
 
 	/* All cases served by this function are exact */
 	*recheck = false;
 
 	key.lower = (GBT_NUMKEY *) &kkk->lower;
 	key.upper = (GBT_NUMKEY *) &kkk->upper;
 
-	PG_RETURN_BOOL(gbt_num_consistent(&key, &query, &strategy,
-									  GIST_LEAF(entry), &tinfo, fcinfo->flinfo));
+	if (likely(subtype == InvalidOid || subtype == INT8OID))
+	{
+		query = DatumGetInt64(queryDatum);
+		PG_RETURN_BOOL(gbt_num_consistent(&key, &query, &strategy,
+										  GIST_LEAF(entry), &tinfo,
+										  fcinfo->flinfo));
+	}
+
+	PG_RETURN_BOOL(gbt_int_consistent_x(kkk->lower, kkk->upper,
+										queryDatum, subtype, &strategy,
+										GIST_LEAF(entry)));
 }
 
 Datum
 gbt_int8_distance(PG_FUNCTION_ARGS)
 {
 	GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
-	int64		query = PG_GETARG_INT64(1);
-#ifdef NOT_USED
+	Datum		queryDatum = PG_GETARG_DATUM(1);
 	Oid			subtype = PG_GETARG_OID(3);
-#endif
 	int64KEY   *kkk = (int64KEY *) DatumGetPointer(entry->key);
+	int64		query;
 	GBT_NUMKEY_R key;
 
 	key.lower = (GBT_NUMKEY *) &kkk->lower;
 	key.upper = (GBT_NUMKEY *) &kkk->upper;
 
-	PG_RETURN_FLOAT8(gbt_num_distance(&key, &query, GIST_LEAF(entry),
-									  &tinfo, fcinfo->flinfo));
+	if (likely(subtype == InvalidOid || subtype == INT8OID))
+	{
+		query = DatumGetInt64(queryDatum);
+		PG_RETURN_FLOAT8(gbt_num_distance(&key, &query, GIST_LEAF(entry),
+										  &tinfo, fcinfo->flinfo));
+	}
+
+	PG_RETURN_FLOAT8(gbt_int_distance_x(kkk->lower, kkk->upper,
+										queryDatum, subtype));
 }
 
 Datum
 gbt_int8_union(PG_FUNCTION_ARGS)
 {
 	GistEntryVector *entryvec = (GistEntryVector *) PG_GETARG_POINTER(0);
diff --git a/contrib/btree_gist/btree_utils_num.c b/contrib/btree_gist/btree_utils_num.c
index 3affe4c2c46..cff47e2d876 100644
--- a/contrib/btree_gist/btree_utils_num.c
+++ b/contrib/btree_gist/btree_utils_num.c
@@ -2,12 +2,14 @@
  * contrib/btree_gist/btree_utils_num.c
  */
 #include "postgres.h"
 
 #include "btree_gist.h"
 #include "btree_utils_num.h"
+#include "catalog/pg_type.h"
+#include "funcapi.h"
 #include "utils/cash.h"
 #include "utils/date.h"
 #include "utils/timestamp.h"
 
 
 GISTENTRY *
@@ -304,12 +306,144 @@ gbt_num_consistent(const GBT_NUMKEY_R *key,
 			retval = false;
 	}
 
 	return retval;
 }
 
+/*
+ * Cross-type dispatch table for the integer opclasses.
+ *
+ * This is the single source of truth for which query subtypes the integer
+ * cross-type path understands. gbt_int_query_to_int64() promotes exactly the
+ * subtypes listed here, and the SQL-visible gbt_int_crosstype_subtypes()
+ * exposes the same list so the regression tests can assert that the cross-type
+ * pg_amop entries in gist_int{2,4,8}_ops never drift from what the C code can
+ * actually handle.
+ */
+typedef int64 (*gbt_int_promote_fn) (Datum query);
+
+static int64
+gbt_int2_to_int64(Datum query)
+{
+	return (int64) DatumGetInt16(query);
+}
+
+static int64
+gbt_int4_to_int64(Datum query)
+{
+	return (int64) DatumGetInt32(query);
+}
+
+static int64
+gbt_int8_to_int64(Datum query)
+{
+	return DatumGetInt64(query);
+}
+
+typedef struct
+{
+	Oid			subtype;
+	gbt_int_promote_fn promote;
+}			gbt_int_crosstype;
+
+static const gbt_int_crosstype gbt_int_crosstype_table[] = {
+	{INT2OID, gbt_int2_to_int64},
+	{INT4OID, gbt_int4_to_int64},
+	{INT8OID, gbt_int8_to_int64},
+};
+
+/*
+ * Promote a cross-type integer query value to int64.
+ *
+ * A subtype outside gbt_int_crosstype_table means the C dispatch is out of sync
+ * with the operator-family registrations in pg_amop, so we treat it as an
+ * internal error.  The regression tests assert this never happens.
+ */
+static int64
+gbt_int_query_to_int64(Datum query, Oid subtype)
+{
+	int			i;
+
+	for (i = 0; i < lengthof(gbt_int_crosstype_table); i++)
+	{
+		if (gbt_int_crosstype_table[i].subtype == subtype)
+			return gbt_int_crosstype_table[i].promote(query);
+	}
+
+	elog(ERROR, "unrecognized subtype %u for btree_gist integer cross-type comparison",
+		 subtype);
+	return 0;					/* keep compiler quiet */
+}
+
+/*
+ * gbt_int_crosstype_subtypes
+ *
+ * Expose the contents of gbt_int_crosstype_table to SQL as a set of type OIDs.
+ * The btree_gist regression tests use this to check that the cross-type pg_amop
+ * entries in gist_int{2,4,8}_ops stay in agreement with the C dispatch.
+ */
+PG_FUNCTION_INFO_V1(gbt_int_crosstype_subtypes);
+Datum
+gbt_int_crosstype_subtypes(PG_FUNCTION_ARGS)
+{
+	FuncCallContext *funcctx;
+
+	if (SRF_IS_FIRSTCALL())
+		funcctx = SRF_FIRSTCALL_INIT();
+
+	funcctx = SRF_PERCALL_SETUP();
+
+	if (funcctx->call_cntr < lengthof(gbt_int_crosstype_table))
+	{
+		Oid			subtype = gbt_int_crosstype_table[funcctx->call_cntr].subtype;
+
+		SRF_RETURN_NEXT(funcctx, ObjectIdGetDatum(subtype));
+	}
+
+	SRF_RETURN_DONE(funcctx);
+}
+
+/*
+ * Cross-type consistent method for the integer opclasses.
+ *
+ * The key range [lower, upper] and the query value are all compared as int64.
+ * The strategy logic mirrors the same-type path in gbt_num_consistent(): the
+ * query value keeps its own width (no narrowing to the indexed column type),
+ * so out-of-range constants behave according to normal integer comparison.
+ */
+bool
+gbt_int_consistent_x(int64 lower, int64 upper, Datum query, Oid subtype,
+					 const StrategyNumber *strategy, bool is_leaf)
+{
+	int64		q = gbt_int_query_to_int64(query, subtype);
+
+	switch (*strategy)
+	{
+		case BTLessEqualStrategyNumber:
+			/* some k in [lower,upper] has k <= q iff lower <= q */
+			return lower <= q;
+		case BTLessStrategyNumber:
+			/* leaf: key < q. internal: lower <= q (loose) */
+			return is_leaf ? (lower < q) : (lower <= q);
+		case BTEqualStrategyNumber:
+			if (is_leaf)
+				return lower == q;
+			/* internal: lower <= q <= upper */
+			return (lower <= q && q <= upper);
+		case BTGreaterStrategyNumber:
+			/* leaf: key > q. internal: upper >= q (loose) */
+			return is_leaf ? (upper > q) : (upper >= q);
+		case BTGreaterEqualStrategyNumber:
+			return upper >= q;
+		case BtreeGistNotEqualStrategyNumber:
+			return !(lower == q && upper == q);
+		default:
+			return false;
+	}
+}
+
 
 /*
  * The GiST distance method (for KNN-Gist)
  */
 
 float8
@@ -331,12 +465,38 @@ gbt_num_distance(const GBT_NUMKEY_R *key,
 	else
 		retval = 0.0;
 
 	return retval;
 }
 
+/*
+ * Cross-type distance method for the integer opclasses.
+ *
+ * The distance from the query value to the key range is computed in int64 and
+ * returned as a float8, matching the same-type integer distance behaviour.
+ *
+ * Note that the subtraction is performed in float8, so when the key bound and
+ * the query value differ by more than 2^53 the returned distance loses
+ * precision. This only affects the ordering of KNN results that are nearly
+ * equidistant at that scale; it never changes which rows are returned.
+ */
+float8
+gbt_int_distance_x(int64 lower, int64 upper, Datum query, Oid subtype)
+{
+	int64		q = gbt_int_query_to_int64(query, subtype);
+
+	/* query below the range: distance to lower bound */
+	if (lower >= q)
+		return fabs((float8) lower - (float8) q);
+	/* query above the range: distance to upper bound */
+	if (upper <= q)
+		return fabs((float8) upper - (float8) q);
+	/* query inside the range */
+	return 0.0;
+}
+
 
 GIST_SPLITVEC *
 gbt_num_picksplit(const GistEntryVector *entryvec, GIST_SPLITVEC *v,
 				  const gbtree_ninfo *tinfo, FmgrInfo *flinfo)
 {
 	OffsetNumber i,
diff --git a/contrib/btree_gist/btree_utils_num.h b/contrib/btree_gist/btree_utils_num.h
index 53e477d8b1e..d378b0df37e 100644
--- a/contrib/btree_gist/btree_utils_num.h
+++ b/contrib/btree_gist/btree_utils_num.h
@@ -95,12 +95,28 @@ extern bool gbt_num_consistent(const GBT_NUMKEY_R *key, const void *query,
 							   const StrategyNumber *strategy, bool is_leaf,
 							   const gbtree_ninfo *tinfo, FmgrInfo *flinfo);
 
 extern float8 gbt_num_distance(const GBT_NUMKEY_R *key, const void *query,
 							   bool is_leaf, const gbtree_ninfo *tinfo, FmgrInfo *flinfo);
 
+/*
+ * Cross-type consistent/distance helpers for the integer opclasses
+ * (int2/int4/int8).  All three integer widths promote losslessly to int64, so
+ * a cross-type query reduces to a plain int64 comparison and there is no need
+ * for the per-type callback machinery the same-type path uses.  These helpers
+ * are deliberately integer-specific rather than a general cross-type framework;
+ * other type families should grow their own helpers shaped to their needs as
+ * the need arises.
+ */
+extern bool gbt_int_consistent_x(int64 lower, int64 upper,
+								 Datum query, Oid subtype,
+								 const StrategyNumber *strategy, bool is_leaf);
+
+extern float8 gbt_int_distance_x(int64 lower, int64 upper,
+								 Datum query, Oid subtype);
+
 extern GIST_SPLITVEC *gbt_num_picksplit(const GistEntryVector *entryvec, GIST_SPLITVEC *v,
 										const gbtree_ninfo *tinfo, FmgrInfo *flinfo);
 
 extern GISTENTRY *gbt_num_compress(GISTENTRY *entry, const gbtree_ninfo *tinfo);
 
 extern GISTENTRY *gbt_num_fetch(GISTENTRY *entry, const gbtree_ninfo *tinfo);
diff --git a/contrib/btree_gist/meson.build b/contrib/btree_gist/meson.build
index 2b1a5463289..c640cb7d8d2 100644
--- a/contrib/btree_gist/meson.build
+++ b/contrib/btree_gist/meson.build
@@ -49,12 +49,13 @@ install_data(
   'btree_gist--1.4--1.5.sql',
   'btree_gist--1.5--1.6.sql',
   'btree_gist--1.6--1.7.sql',
   'btree_gist--1.7--1.8.sql',
   'btree_gist--1.8--1.9.sql',
   'btree_gist--1.9.sql',
+  'btree_gist--1.9--1.10.sql',
   kwargs: contrib_data_args,
 )
 
 tests += {
   'name': 'btree_gist',
   'sd': meson.current_source_dir(),
@@ -89,10 +90,11 @@ tests += {
       'uuid',
       'not_equal',
       'enum',
       'bool',
       'partitions',
       'stratnum',
+      'int_crosstype',
       'without_overlaps',
     ],
   },
 }
-- 
2.51.0



  [application/octet-stream] 0002-Add-tests-for-cross-type-operators-for-GiST-indexes.patch (23.9K, 4-0002-Add-tests-for-cross-type-operators-for-GiST-indexes.patch)
  download | inline diff:
From e6204108239a2458be11aa2286edd7bd029a3681 Mon Sep 17 00:00:00 2001
From: Alexander Nestorov <[email protected]>
Date: Thu, 4 Jun 2026 00:08:27 +0200
Subject: [PATCH] Add tests for cross-type operators for GiST indexes

---
 contrib/btree_gist/expected/int_crosstype.out | 568 ++++++++++++++++++
 contrib/btree_gist/sql/int_crosstype.sql      | 236 ++++++++
 2 files changed, 804 insertions(+)
 create mode 100644 contrib/btree_gist/expected/int_crosstype.out
 create mode 100644 contrib/btree_gist/sql/int_crosstype.sql

diff --git a/contrib/btree_gist/expected/int_crosstype.out b/contrib/btree_gist/expected/int_crosstype.out
new file mode 100644
index 00000000000..bd2b90bd3f6
--- /dev/null
+++ b/contrib/btree_gist/expected/int_crosstype.out
@@ -0,0 +1,568 @@
+-- Cross-type operator support for int2/int4/int8 in GiST.
+--
+-- Verifies that (a) the cross-type B-tree-style operators registered in
+-- gist_int{2,4,8}_ops match the results of seqscans using the same operator
+-- expressions, (b) the KNN <-> operator works across types and uses the
+-- index, and (c) values outside the smaller subtype's range are handled
+-- according to normal comparison semantics, without narrowing or erroring.
+--
+-- Catalog invariant: the cross-type pg_amop entries in gist_int{2,4,8}_ops must
+-- agree, in both directions, with what the C cross-type dispatch can handle.
+-- The set of supported query subtypes is read from the C side via
+-- gbt_int_crosstype_subtypes(), so there is no hand-maintained second copy of
+-- the list here: registering a pg_amop row whose subtype the dispatch does not
+-- handle (or dropping a dispatch entry while its pg_amop rows remain, or vice
+-- versa) shows up as a diff below. Cross-type pg_amproc rows must also stay
+-- absent, since the dispatch reuses the same-type support functions.
+--
+WITH dispatch_subtypes(typ) AS (
+    SELECT s.subtype::regtype
+    FROM gbt_int_crosstype_subtypes() AS s(subtype)
+),
+gist_int_opclasses(opfamily, indextype) AS (
+    SELECT opc.opcname::text, opc.opcintype::regtype
+    FROM pg_opclass opc
+         JOIN pg_am am ON am.oid = opc.opcmethod
+    WHERE am.amname = 'gist'
+      AND opc.opcname IN ('gist_int2_ops', 'gist_int4_ops', 'gist_int8_ops')
+),
+expected_pairs(opfamily, lefttype, righttype) AS (
+    SELECT oc.opfamily, oc.indextype, ds.typ
+    FROM gist_int_opclasses oc
+         CROSS JOIN dispatch_subtypes ds
+    WHERE ds.typ <> oc.indextype
+),
+expected_amop(opfamily, lefttype, righttype, strategy, purpose) AS (
+    SELECT opfamily, lefttype, righttype, strategy, purpose
+    FROM expected_pairs
+         CROSS JOIN (VALUES
+             (1, 's'), (2, 's'), (3, 's'), (4, 's'),
+             (5, 's'), (6, 's'), (15, 'o')
+         ) AS strategy_purposes(strategy, purpose)
+),
+actual_amop AS (
+    SELECT opf.opfname::text AS opfamily,
+           amop.amoplefttype::regtype AS lefttype,
+           amop.amoprighttype::regtype AS righttype,
+           amop.amopstrategy::int AS strategy,
+           amop.amoppurpose::text AS purpose
+    FROM pg_amop amop
+         JOIN pg_opfamily opf ON opf.oid = amop.amopfamily
+         JOIN pg_am am ON am.oid = opf.opfmethod
+    WHERE am.amname = 'gist'
+      AND opf.opfname IN ('gist_int2_ops', 'gist_int4_ops', 'gist_int8_ops')
+      AND amop.amoplefttype <> amop.amoprighttype
+)
+SELECT *
+FROM (
+    SELECT 'missing from pg_amop' AS status, *
+    FROM (SELECT * FROM expected_amop EXCEPT SELECT * FROM actual_amop) missing
+    UNION ALL
+    SELECT 'unexpected in pg_amop' AS status, *
+    FROM (SELECT * FROM actual_amop EXCEPT SELECT * FROM expected_amop) unexpected
+) diff
+ORDER BY status, opfamily, lefttype::text, righttype::text, strategy, purpose;
+ status | opfamily | lefttype | righttype | strategy | purpose 
+--------+----------+----------+-----------+----------+---------
+(0 rows)
+
+SELECT opf.opfname AS opfamily,
+       amproc.amproclefttype::regtype AS lefttype,
+       amproc.amprocrighttype::regtype AS righttype,
+       amproc.amprocnum AS procnum,
+       amproc.amproc::regproc AS proc
+FROM pg_amproc amproc
+     JOIN pg_opfamily opf ON opf.oid = amproc.amprocfamily
+     JOIN pg_am am ON am.oid = opf.opfmethod
+WHERE am.amname = 'gist'
+  AND opf.opfname IN ('gist_int2_ops', 'gist_int4_ops', 'gist_int8_ops')
+  AND amproc.amproclefttype <> amproc.amprocrighttype
+ORDER BY opf.opfname,
+         amproc.amproclefttype::regtype::text,
+         amproc.amprocrighttype::regtype::text,
+         amproc.amprocnum,
+         amproc.amproc::regproc::text;
+ opfamily | lefttype | righttype | procnum | proc 
+----------+----------+-----------+---------+------
+(0 rows)
+
+CREATE TABLE ct_i2 (a int2);
+CREATE TABLE ct_i4 (a int4);
+CREATE TABLE ct_i8 (a int8);
+INSERT INTO ct_i2 SELECT g::int2 FROM generate_series(-100, 100) g;
+INSERT INTO ct_i4 SELECT g FROM generate_series(-100, 100) g;
+INSERT INTO ct_i8 SELECT g::int8 FROM generate_series(-100, 100) g;
+-- Add some values that are representable only in wider types, to exercise
+-- the path where the cross-type query constant is out of range of the key
+-- type.
+INSERT INTO ct_i4 VALUES (100000), (-100000);
+INSERT INTO ct_i8 VALUES (5000000000), (-5000000000);
+CREATE INDEX ct_i2_idx ON ct_i2 USING gist (a);
+CREATE INDEX ct_i4_idx ON ct_i4 USING gist (a);
+CREATE INDEX ct_i8_idx ON ct_i8 USING gist (a);
+ANALYZE ct_i2;
+ANALYZE ct_i4;
+ANALYZE ct_i8;
+SET enable_seqscan = off;
+SET enable_bitmapscan = off;
+-- int2 key x int4 query
+SELECT count(*) FROM ct_i2 WHERE a <  50::int4;
+ count 
+-------
+   150
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a <= 50::int4;
+ count 
+-------
+   151
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a =  50::int4;
+ count 
+-------
+     1
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a >= 50::int4;
+ count 
+-------
+    51
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a >  50::int4;
+ count 
+-------
+    50
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a <> 50::int4;
+ count 
+-------
+   200
+(1 row)
+
+-- query out of int2 range: matches nothing for =, everything for <>
+SELECT count(*) FROM ct_i2 WHERE a =  100000::int4;
+ count 
+-------
+     0
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a <> 100000::int4;
+ count 
+-------
+   201
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a <  100000::int4;
+ count 
+-------
+   201
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a >  100000::int4;
+ count 
+-------
+     0
+(1 row)
+
+-- int2 key x int8 query
+SELECT count(*) FROM ct_i2 WHERE a <  50::int8;
+ count 
+-------
+   150
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a <= 50::int8;
+ count 
+-------
+   151
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a =  50::int8;
+ count 
+-------
+     1
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a >= 50::int8;
+ count 
+-------
+    51
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a >  50::int8;
+ count 
+-------
+    50
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a <> 50::int8;
+ count 
+-------
+   200
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a =  5000000000::int8;
+ count 
+-------
+     0
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a <  5000000000::int8;
+ count 
+-------
+   201
+(1 row)
+
+-- int4 key x int2 query
+SELECT count(*) FROM ct_i4 WHERE a <  50::int2;
+ count 
+-------
+   151
+(1 row)
+
+SELECT count(*) FROM ct_i4 WHERE a <= 50::int2;
+ count 
+-------
+   152
+(1 row)
+
+SELECT count(*) FROM ct_i4 WHERE a =  50::int2;
+ count 
+-------
+     1
+(1 row)
+
+SELECT count(*) FROM ct_i4 WHERE a >= 50::int2;
+ count 
+-------
+    52
+(1 row)
+
+SELECT count(*) FROM ct_i4 WHERE a >  50::int2;
+ count 
+-------
+    51
+(1 row)
+
+SELECT count(*) FROM ct_i4 WHERE a <> 50::int2;
+ count 
+-------
+   202
+(1 row)
+
+-- int4 key x int8 query
+SELECT count(*) FROM ct_i4 WHERE a <  50::int8;
+ count 
+-------
+   151
+(1 row)
+
+SELECT count(*) FROM ct_i4 WHERE a <= 50::int8;
+ count 
+-------
+   152
+(1 row)
+
+SELECT count(*) FROM ct_i4 WHERE a =  50::int8;
+ count 
+-------
+     1
+(1 row)
+
+SELECT count(*) FROM ct_i4 WHERE a >= 50::int8;
+ count 
+-------
+    52
+(1 row)
+
+SELECT count(*) FROM ct_i4 WHERE a >  50::int8;
+ count 
+-------
+    51
+(1 row)
+
+SELECT count(*) FROM ct_i4 WHERE a <> 50::int8;
+ count 
+-------
+   202
+(1 row)
+
+SELECT count(*) FROM ct_i4 WHERE a =  5000000000::int8;
+ count 
+-------
+     0
+(1 row)
+
+SELECT count(*) FROM ct_i4 WHERE a <  5000000000::int8;
+ count 
+-------
+   203
+(1 row)
+
+-- int8 key x int2 query
+SELECT count(*) FROM ct_i8 WHERE a <  50::int2;
+ count 
+-------
+   151
+(1 row)
+
+SELECT count(*) FROM ct_i8 WHERE a <= 50::int2;
+ count 
+-------
+   152
+(1 row)
+
+SELECT count(*) FROM ct_i8 WHERE a =  50::int2;
+ count 
+-------
+     1
+(1 row)
+
+SELECT count(*) FROM ct_i8 WHERE a >= 50::int2;
+ count 
+-------
+    52
+(1 row)
+
+SELECT count(*) FROM ct_i8 WHERE a >  50::int2;
+ count 
+-------
+    51
+(1 row)
+
+SELECT count(*) FROM ct_i8 WHERE a <> 50::int2;
+ count 
+-------
+   202
+(1 row)
+
+-- int8 key x int4 query
+SELECT count(*) FROM ct_i8 WHERE a <  50::int4;
+ count 
+-------
+   151
+(1 row)
+
+SELECT count(*) FROM ct_i8 WHERE a <= 50::int4;
+ count 
+-------
+   152
+(1 row)
+
+SELECT count(*) FROM ct_i8 WHERE a =  50::int4;
+ count 
+-------
+     1
+(1 row)
+
+SELECT count(*) FROM ct_i8 WHERE a >= 50::int4;
+ count 
+-------
+    52
+(1 row)
+
+SELECT count(*) FROM ct_i8 WHERE a >  50::int4;
+ count 
+-------
+    51
+(1 row)
+
+SELECT count(*) FROM ct_i8 WHERE a <> 50::int4;
+ count 
+-------
+   202
+(1 row)
+
+-- Confirm the index is actually used for a cross-type predicate.
+EXPLAIN (COSTS OFF)
+SELECT count(*) FROM ct_i4 WHERE a = 50::int8;
+                   QUERY PLAN                   
+------------------------------------------------
+ Aggregate
+   ->  Index Only Scan using ct_i4_idx on ct_i4
+         Index Cond: (a = '50'::bigint)
+(3 rows)
+
+-- Cross-type KNN: int4 key ordered by int2 / int8 queries.
+EXPLAIN (COSTS OFF)
+SELECT a FROM ct_i4 ORDER BY a <-> '-100'::int2 LIMIT 3;
+                   QUERY PLAN                   
+------------------------------------------------
+ Limit
+   ->  Index Only Scan using ct_i4_idx on ct_i4
+         Order By: (a <-> '-100'::smallint)
+(3 rows)
+
+SELECT a FROM ct_i4 ORDER BY a <-> '-100'::int2 LIMIT 3;
+  a   
+------
+ -100
+  -99
+  -98
+(3 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT a FROM ct_i4 ORDER BY a <-> '-100'::int8 LIMIT 3;
+                   QUERY PLAN                   
+------------------------------------------------
+ Limit
+   ->  Index Only Scan using ct_i4_idx on ct_i4
+         Order By: (a <-> '-100'::bigint)
+(3 rows)
+
+SELECT a FROM ct_i4 ORDER BY a <-> '-100'::int8 LIMIT 3;
+  a   
+------
+ -100
+  -99
+  -98
+(3 rows)
+
+-- Cross-type KNN: int2 key ordered by int4 / int8 queries.
+SELECT a FROM ct_i2 ORDER BY a <-> '-100'::int4 LIMIT 3;
+  a   
+------
+ -100
+  -99
+  -98
+(3 rows)
+
+SELECT a FROM ct_i2 ORDER BY a <-> '-100'::int8 LIMIT 3;
+  a   
+------
+ -100
+  -99
+  -98
+(3 rows)
+
+-- Cross-type KNN: int8 key ordered by int2 / int4 queries.
+SELECT a FROM ct_i8 ORDER BY a <-> '-100'::int2 LIMIT 3;
+  a   
+------
+ -100
+  -99
+  -98
+(3 rows)
+
+SELECT a FROM ct_i8 ORDER BY a <-> '-100'::int4 LIMIT 3;
+  a   
+------
+ -100
+  -99
+  -98
+(3 rows)
+
+-- Combined: cross-type WHERE + cross-type ORDER BY on the same index.
+EXPLAIN (COSTS OFF)
+SELECT a FROM ct_i4 WHERE a < 80::int8 ORDER BY a <-> '-100'::int8 LIMIT 3;
+                   QUERY PLAN                   
+------------------------------------------------
+ Limit
+   ->  Index Only Scan using ct_i4_idx on ct_i4
+         Index Cond: (a < '80'::bigint)
+         Order By: (a <-> '-100'::bigint)
+(4 rows)
+
+SELECT a FROM ct_i4 WHERE a < 80::int8 ORDER BY a <-> '-100'::int8 LIMIT 3;
+  a   
+------
+ -100
+  -99
+  -98
+(3 rows)
+
+-- Standalone distance-function smoke tests (not going through the index),
+-- including the overflow-detection paths.
+SELECT int2_int4_dist(3::int2, 10::int4);
+ int2_int4_dist 
+----------------
+              7
+(1 row)
+
+SELECT int4_int2_dist(-5::int4, 5::int2);
+ int4_int2_dist 
+----------------
+             10
+(1 row)
+
+SELECT int2_int8_dist(3::int2, 10::int8);
+ int2_int8_dist 
+----------------
+              7
+(1 row)
+
+SELECT int8_int2_dist(100::int8, -5::int2);
+ int8_int2_dist 
+----------------
+            105
+(1 row)
+
+SELECT int4_int8_dist(100::int4, 5000000000::int8);
+ int4_int8_dist 
+----------------
+     4999999900
+(1 row)
+
+SELECT int8_int4_dist(5000000000::int8, 100::int4);
+ int8_int4_dist 
+----------------
+     4999999900
+(1 row)
+
+-- Overflow detection: INT32_MIN distance from a positive int2 can't fit
+-- in int32, should error.
+SELECT int2_int4_dist(1::int2, -2147483648::int4);
+ERROR:  integer out of range
+-- Likewise INT64_MIN distance from a positive int4 can't fit in int64.
+SELECT int4_int8_dist(1::int4, -9223372036854775808::int8);
+ERROR:  bigint out of range
+--
+-- Multi-column GiST index with mixed-type predicates. This is the
+-- original motivating case: without cross-type operator support the
+-- planner can only use one column as an Index Cond and applies the
+-- other(s) as a Filter post-scan. Here both columns should appear as
+-- Index Cond.
+--
+CREATE TABLE ct_multi (a int4, b int8);
+INSERT INTO ct_multi
+    SELECT g, (g * 2)::int8 FROM generate_series(-50, 50) g;
+CREATE INDEX ct_multi_idx ON ct_multi USING gist (a, b);
+ANALYZE ct_multi;
+EXPLAIN (COSTS OFF)
+SELECT count(*) FROM ct_multi WHERE a = 25::int8 AND b = 50::int4;
+                      QUERY PLAN                       
+-------------------------------------------------------
+ Aggregate
+   ->  Index Only Scan using ct_multi_idx on ct_multi
+         Index Cond: ((a = '25'::bigint) AND (b = 50))
+(3 rows)
+
+SELECT count(*) FROM ct_multi WHERE a = 25::int8 AND b = 50::int4;
+ count 
+-------
+     1
+(1 row)
+
+-- Mixed cross-type ranges across both columns.
+EXPLAIN (COSTS OFF)
+SELECT count(*) FROM ct_multi WHERE a < 10::int8 AND b > 0::int2;
+                            QUERY PLAN                            
+------------------------------------------------------------------
+ Aggregate
+   ->  Index Only Scan using ct_multi_idx on ct_multi
+         Index Cond: ((a < '10'::bigint) AND (b > '0'::smallint))
+(3 rows)
+
+SELECT count(*) FROM ct_multi WHERE a < 10::int8 AND b > 0::int2;
+ count 
+-------
+     9
+(1 row)
+
+DROP TABLE ct_multi;
+DROP TABLE ct_i2;
+DROP TABLE ct_i4;
+DROP TABLE ct_i8;
diff --git a/contrib/btree_gist/sql/int_crosstype.sql b/contrib/btree_gist/sql/int_crosstype.sql
new file mode 100644
index 00000000000..d38084f1941
--- /dev/null
+++ b/contrib/btree_gist/sql/int_crosstype.sql
@@ -0,0 +1,236 @@
+-- Cross-type operator support for int2/int4/int8 in GiST.
+--
+-- Verifies that (a) the cross-type B-tree-style operators registered in
+-- gist_int{2,4,8}_ops match the results of seqscans using the same operator
+-- expressions, (b) the KNN <-> operator works across types and uses the
+-- index, and (c) values outside the smaller subtype's range are handled
+-- according to normal comparison semantics, without narrowing or erroring.
+
+--
+-- Catalog invariant: the cross-type pg_amop entries in gist_int{2,4,8}_ops must
+-- agree, in both directions, with what the C cross-type dispatch can handle.
+-- The set of supported query subtypes is read from the C side via
+-- gbt_int_crosstype_subtypes(), so there is no hand-maintained second copy of
+-- the list here: registering a pg_amop row whose subtype the dispatch does not
+-- handle (or dropping a dispatch entry while its pg_amop rows remain, or vice
+-- versa) shows up as a diff below. Cross-type pg_amproc rows must also stay
+-- absent, since the dispatch reuses the same-type support functions.
+--
+WITH dispatch_subtypes(typ) AS (
+    SELECT s.subtype::regtype
+    FROM gbt_int_crosstype_subtypes() AS s(subtype)
+),
+gist_int_opclasses(opfamily, indextype) AS (
+    SELECT opc.opcname::text, opc.opcintype::regtype
+    FROM pg_opclass opc
+         JOIN pg_am am ON am.oid = opc.opcmethod
+    WHERE am.amname = 'gist'
+      AND opc.opcname IN ('gist_int2_ops', 'gist_int4_ops', 'gist_int8_ops')
+),
+expected_pairs(opfamily, lefttype, righttype) AS (
+    SELECT oc.opfamily, oc.indextype, ds.typ
+    FROM gist_int_opclasses oc
+         CROSS JOIN dispatch_subtypes ds
+    WHERE ds.typ <> oc.indextype
+),
+expected_amop(opfamily, lefttype, righttype, strategy, purpose) AS (
+    SELECT opfamily, lefttype, righttype, strategy, purpose
+    FROM expected_pairs
+         CROSS JOIN (VALUES
+             (1, 's'), (2, 's'), (3, 's'), (4, 's'),
+             (5, 's'), (6, 's'), (15, 'o')
+         ) AS strategy_purposes(strategy, purpose)
+),
+actual_amop AS (
+    SELECT opf.opfname::text AS opfamily,
+           amop.amoplefttype::regtype AS lefttype,
+           amop.amoprighttype::regtype AS righttype,
+           amop.amopstrategy::int AS strategy,
+           amop.amoppurpose::text AS purpose
+    FROM pg_amop amop
+         JOIN pg_opfamily opf ON opf.oid = amop.amopfamily
+         JOIN pg_am am ON am.oid = opf.opfmethod
+    WHERE am.amname = 'gist'
+      AND opf.opfname IN ('gist_int2_ops', 'gist_int4_ops', 'gist_int8_ops')
+      AND amop.amoplefttype <> amop.amoprighttype
+)
+SELECT *
+FROM (
+    SELECT 'missing from pg_amop' AS status, *
+    FROM (SELECT * FROM expected_amop EXCEPT SELECT * FROM actual_amop) missing
+    UNION ALL
+    SELECT 'unexpected in pg_amop' AS status, *
+    FROM (SELECT * FROM actual_amop EXCEPT SELECT * FROM expected_amop) unexpected
+) diff
+ORDER BY status, opfamily, lefttype::text, righttype::text, strategy, purpose;
+
+SELECT opf.opfname AS opfamily,
+       amproc.amproclefttype::regtype AS lefttype,
+       amproc.amprocrighttype::regtype AS righttype,
+       amproc.amprocnum AS procnum,
+       amproc.amproc::regproc AS proc
+FROM pg_amproc amproc
+     JOIN pg_opfamily opf ON opf.oid = amproc.amprocfamily
+     JOIN pg_am am ON am.oid = opf.opfmethod
+WHERE am.amname = 'gist'
+  AND opf.opfname IN ('gist_int2_ops', 'gist_int4_ops', 'gist_int8_ops')
+  AND amproc.amproclefttype <> amproc.amprocrighttype
+ORDER BY opf.opfname,
+         amproc.amproclefttype::regtype::text,
+         amproc.amprocrighttype::regtype::text,
+         amproc.amprocnum,
+         amproc.amproc::regproc::text;
+
+CREATE TABLE ct_i2 (a int2);
+CREATE TABLE ct_i4 (a int4);
+CREATE TABLE ct_i8 (a int8);
+
+INSERT INTO ct_i2 SELECT g::int2 FROM generate_series(-100, 100) g;
+INSERT INTO ct_i4 SELECT g FROM generate_series(-100, 100) g;
+INSERT INTO ct_i8 SELECT g::int8 FROM generate_series(-100, 100) g;
+
+-- Add some values that are representable only in wider types, to exercise
+-- the path where the cross-type query constant is out of range of the key
+-- type.
+INSERT INTO ct_i4 VALUES (100000), (-100000);
+INSERT INTO ct_i8 VALUES (5000000000), (-5000000000);
+
+CREATE INDEX ct_i2_idx ON ct_i2 USING gist (a);
+CREATE INDEX ct_i4_idx ON ct_i4 USING gist (a);
+CREATE INDEX ct_i8_idx ON ct_i8 USING gist (a);
+
+ANALYZE ct_i2;
+ANALYZE ct_i4;
+ANALYZE ct_i8;
+
+SET enable_seqscan = off;
+SET enable_bitmapscan = off;
+
+-- int2 key x int4 query
+SELECT count(*) FROM ct_i2 WHERE a <  50::int4;
+SELECT count(*) FROM ct_i2 WHERE a <= 50::int4;
+SELECT count(*) FROM ct_i2 WHERE a =  50::int4;
+SELECT count(*) FROM ct_i2 WHERE a >= 50::int4;
+SELECT count(*) FROM ct_i2 WHERE a >  50::int4;
+SELECT count(*) FROM ct_i2 WHERE a <> 50::int4;
+
+-- query out of int2 range: matches nothing for =, everything for <>
+SELECT count(*) FROM ct_i2 WHERE a =  100000::int4;
+SELECT count(*) FROM ct_i2 WHERE a <> 100000::int4;
+SELECT count(*) FROM ct_i2 WHERE a <  100000::int4;
+SELECT count(*) FROM ct_i2 WHERE a >  100000::int4;
+
+-- int2 key x int8 query
+SELECT count(*) FROM ct_i2 WHERE a <  50::int8;
+SELECT count(*) FROM ct_i2 WHERE a <= 50::int8;
+SELECT count(*) FROM ct_i2 WHERE a =  50::int8;
+SELECT count(*) FROM ct_i2 WHERE a >= 50::int8;
+SELECT count(*) FROM ct_i2 WHERE a >  50::int8;
+SELECT count(*) FROM ct_i2 WHERE a <> 50::int8;
+SELECT count(*) FROM ct_i2 WHERE a =  5000000000::int8;
+SELECT count(*) FROM ct_i2 WHERE a <  5000000000::int8;
+
+-- int4 key x int2 query
+SELECT count(*) FROM ct_i4 WHERE a <  50::int2;
+SELECT count(*) FROM ct_i4 WHERE a <= 50::int2;
+SELECT count(*) FROM ct_i4 WHERE a =  50::int2;
+SELECT count(*) FROM ct_i4 WHERE a >= 50::int2;
+SELECT count(*) FROM ct_i4 WHERE a >  50::int2;
+SELECT count(*) FROM ct_i4 WHERE a <> 50::int2;
+
+-- int4 key x int8 query
+SELECT count(*) FROM ct_i4 WHERE a <  50::int8;
+SELECT count(*) FROM ct_i4 WHERE a <= 50::int8;
+SELECT count(*) FROM ct_i4 WHERE a =  50::int8;
+SELECT count(*) FROM ct_i4 WHERE a >= 50::int8;
+SELECT count(*) FROM ct_i4 WHERE a >  50::int8;
+SELECT count(*) FROM ct_i4 WHERE a <> 50::int8;
+SELECT count(*) FROM ct_i4 WHERE a =  5000000000::int8;
+SELECT count(*) FROM ct_i4 WHERE a <  5000000000::int8;
+
+-- int8 key x int2 query
+SELECT count(*) FROM ct_i8 WHERE a <  50::int2;
+SELECT count(*) FROM ct_i8 WHERE a <= 50::int2;
+SELECT count(*) FROM ct_i8 WHERE a =  50::int2;
+SELECT count(*) FROM ct_i8 WHERE a >= 50::int2;
+SELECT count(*) FROM ct_i8 WHERE a >  50::int2;
+SELECT count(*) FROM ct_i8 WHERE a <> 50::int2;
+
+-- int8 key x int4 query
+SELECT count(*) FROM ct_i8 WHERE a <  50::int4;
+SELECT count(*) FROM ct_i8 WHERE a <= 50::int4;
+SELECT count(*) FROM ct_i8 WHERE a =  50::int4;
+SELECT count(*) FROM ct_i8 WHERE a >= 50::int4;
+SELECT count(*) FROM ct_i8 WHERE a >  50::int4;
+SELECT count(*) FROM ct_i8 WHERE a <> 50::int4;
+
+-- Confirm the index is actually used for a cross-type predicate.
+EXPLAIN (COSTS OFF)
+SELECT count(*) FROM ct_i4 WHERE a = 50::int8;
+
+-- Cross-type KNN: int4 key ordered by int2 / int8 queries.
+EXPLAIN (COSTS OFF)
+SELECT a FROM ct_i4 ORDER BY a <-> '-100'::int2 LIMIT 3;
+SELECT a FROM ct_i4 ORDER BY a <-> '-100'::int2 LIMIT 3;
+
+EXPLAIN (COSTS OFF)
+SELECT a FROM ct_i4 ORDER BY a <-> '-100'::int8 LIMIT 3;
+SELECT a FROM ct_i4 ORDER BY a <-> '-100'::int8 LIMIT 3;
+
+-- Cross-type KNN: int2 key ordered by int4 / int8 queries.
+SELECT a FROM ct_i2 ORDER BY a <-> '-100'::int4 LIMIT 3;
+SELECT a FROM ct_i2 ORDER BY a <-> '-100'::int8 LIMIT 3;
+
+-- Cross-type KNN: int8 key ordered by int2 / int4 queries.
+SELECT a FROM ct_i8 ORDER BY a <-> '-100'::int2 LIMIT 3;
+SELECT a FROM ct_i8 ORDER BY a <-> '-100'::int4 LIMIT 3;
+
+-- Combined: cross-type WHERE + cross-type ORDER BY on the same index.
+EXPLAIN (COSTS OFF)
+SELECT a FROM ct_i4 WHERE a < 80::int8 ORDER BY a <-> '-100'::int8 LIMIT 3;
+SELECT a FROM ct_i4 WHERE a < 80::int8 ORDER BY a <-> '-100'::int8 LIMIT 3;
+
+-- Standalone distance-function smoke tests (not going through the index),
+-- including the overflow-detection paths.
+SELECT int2_int4_dist(3::int2, 10::int4);
+SELECT int4_int2_dist(-5::int4, 5::int2);
+SELECT int2_int8_dist(3::int2, 10::int8);
+SELECT int8_int2_dist(100::int8, -5::int2);
+SELECT int4_int8_dist(100::int4, 5000000000::int8);
+SELECT int8_int4_dist(5000000000::int8, 100::int4);
+
+-- Overflow detection: INT32_MIN distance from a positive int2 can't fit
+-- in int32, should error.
+SELECT int2_int4_dist(1::int2, -2147483648::int4);
+-- Likewise INT64_MIN distance from a positive int4 can't fit in int64.
+SELECT int4_int8_dist(1::int4, -9223372036854775808::int8);
+
+--
+-- Multi-column GiST index with mixed-type predicates. This is the
+-- original motivating case: without cross-type operator support the
+-- planner can only use one column as an Index Cond and applies the
+-- other(s) as a Filter post-scan. Here both columns should appear as
+-- Index Cond.
+--
+CREATE TABLE ct_multi (a int4, b int8);
+INSERT INTO ct_multi
+    SELECT g, (g * 2)::int8 FROM generate_series(-50, 50) g;
+CREATE INDEX ct_multi_idx ON ct_multi USING gist (a, b);
+ANALYZE ct_multi;
+
+EXPLAIN (COSTS OFF)
+SELECT count(*) FROM ct_multi WHERE a = 25::int8 AND b = 50::int4;
+
+SELECT count(*) FROM ct_multi WHERE a = 25::int8 AND b = 50::int4;
+
+-- Mixed cross-type ranges across both columns.
+EXPLAIN (COSTS OFF)
+SELECT count(*) FROM ct_multi WHERE a < 10::int8 AND b > 0::int2;
+
+SELECT count(*) FROM ct_multi WHERE a < 10::int8 AND b > 0::int2;
+
+DROP TABLE ct_multi;
+
+DROP TABLE ct_i2;
+DROP TABLE ct_i4;
+DROP TABLE ct_i8;
-- 
2.51.0



^ permalink  raw  reply  [nested|flat] 9+ messages in thread

* Re: [PATCH] btree_gist: add cross-type integer operator support for GiST
@ 2026-06-22 12:20  Maxime Schoemans <[email protected]>
  parent: Alexander Nestorov <[email protected]>
  0 siblings, 1 reply; 9+ messages in thread

From: Maxime Schoemans @ 2026-06-22 12:20 UTC (permalink / raw)
  To: Alexander Nestorov <[email protected]>; +Cc: Andrey Borodin <[email protected]>; pgsql-hackers

Hi Alexander,

Thank you for bringing this thread to my attention, this is a very
interesting proposal. Personally, I have some doubts about the way
cross-type operators are handled in GiST, but this is another issue.
Within the current framework, this proposal implements things
correctly, so I have no conceptual issue with it.

Concerning the implementation itself, I have a couple comments:

1. In the consistent functions, I believe `subtype` can never be
`InvalidOid` based on the current code. It probably doesn't hurt to
keep it in the condition, but personally I'd drop it. Either way, I
thought it was worth mentioning.

2. I'm not sure I like the approach of casting everything to an
`int64` and using a new `gbt_int_consistent_x` function. It works,
so if others have no problem with it I can accept it, but I have
another suggestion: re-use `gbt_num_consistent` even for cross-type
cases by creating cross-type `gbtree_ninfo` objects with cross-type
`f_gt`, `f_ge`, `f_eq`, etc. functions. I have attached a v3
patchset with patch 0003 containing my suggestions. Feel free to
take them or leave them as you see fit. This suggestion requires
one important change in `gbt_num_consistent`:

```
	else
-		retval = (tinfo->f_le(key->lower, query, flinfo) &&
+		retval = (tinfo->f_ge(query, key->lower, flinfo) &&
				  tinfo->f_le(query, key->upper, flinfo));
	break;
```

This is to make sure that `gbt_num_consistent` call all the
comparison functions with `query` on the left and `key->lower/upper`
on the right, to allow for cross-type comparison functions in `tinfo`.

0003 keeps your SQL/catalog work unchanged, compiles cleanly, and
passes the existing btree_gist regression including the cross-type
cases in 0002 (with the SRF-based catalog-drift check dropped, per
point 3).

3. This is related to my suggestion in 2. as it removes the
`gbt_int_crosstype_subtypes` SQL function (there is no
`gbt_int_crosstype_table` anymore in C). Instead, the `switch`
clause in the consistent functions raises an error in its default
path, so RHS types that are not handled can be caught quickly. As
you mention, I don't think there is a way to catch such issues at
index validation time, so this is the best I could think of.

Note that in theory we could put the `gbtree_ninfo` objects in a
table and then write a function equivalent to
`gbt_int_crosstype_subtypes` to still have the test you suggested,
but I think that switch statements are a bit clearer and perhaps
more efficient than looping through a table for the dispatch.

To me this is similar to adding new same-type operators to a GiST
opclass. Nothing checks at validation time that all the operators
in the opclass are effectively handled by the given consistent
function. So I don't see it as a problem to have the same behaviour
for the cross-type operators.

Other than that, this is a good proposal and definitely something
worth having in postgres.
Please let me know what you think of my suggestions.

Best regards,
Maxime



Attachments:

  [application/applefile] v3-0001-Implement-cross-type-operators-for-GiST-indexes.patch (121B, 2-v3-0001-Implement-cross-type-operators-for-GiST-indexes.patch)
  download | inline diff:
	2
<=v3-0001-Implement-cross-type-operators-for-GiST-indexes.patch

  [application/applefile] v3-0002-Add-tests-for-cross-type-operators-for-GiST-index.patch (123B, 3-v3-0002-Add-tests-for-cross-type-operators-for-GiST-index.patch)
  download | inline diff:
	2
<?v3-0002-Add-tests-for-cross-type-operators-for-GiST-index.patch

  [application/octet-stream] v3-0003-Reuse-gbt_num_consistent-for-cross-type-integer-c.patch (29.8K, 4-v3-0003-Reuse-gbt_num_consistent-for-cross-type-integer-c.patch)
  download | inline diff:
From 347ed8923363a2ea0a7d4e348603825c6783cbf7 Mon Sep 17 00:00:00 2001
From: Maxime Schoemans <[email protected]>
Date: Mon, 22 Jun 2026 13:31:41 +0200
Subject: [PATCH v3 3/3] Reuse gbt_num_consistent for cross-type integer
 comparisons

---
 contrib/btree_gist/btree_gist--1.9--1.10.sql  |  13 +-
 contrib/btree_gist/btree_int2.c               |  98 +++++++---
 contrib/btree_gist/btree_int4.c               |  98 +++++++---
 contrib/btree_gist/btree_int8.c               |  98 +++++++---
 contrib/btree_gist/btree_utils_num.c          | 169 +-----------------
 contrib/btree_gist/btree_utils_num.h          |  55 ++++--
 contrib/btree_gist/expected/int_crosstype.out |  63 +------
 contrib/btree_gist/sql/int_crosstype.sql      |  60 +------
 8 files changed, 289 insertions(+), 365 deletions(-)

diff --git a/contrib/btree_gist/btree_gist--1.9--1.10.sql b/contrib/btree_gist/btree_gist--1.9--1.10.sql
index c9ff1955204..9cd57455086 100644
--- a/contrib/btree_gist/btree_gist--1.9--1.10.sql
+++ b/contrib/btree_gist/btree_gist--1.9--1.10.sql
@@ -10,8 +10,8 @@
 -- left/right input types, so the catalog additions below are deliberately
 -- pg_amop-only. The existing consistent/distance support functions dispatch
 -- on the subtype OID: same-type queries take the normal path, while mixed-width
--- integer queries are promoted to int64 and compared by the integer cross-type
--- helpers in btree_utils_num.c.
+-- integer queries select a cross-type comparison callback that reads the query
+-- and key sides at their own widths (see btree_int{2,4,8}.c).
 
 CREATE FUNCTION int2_int4_dist(int2, int4)
 RETURNS int4
@@ -43,15 +43,6 @@ RETURNS int8
 AS 'MODULE_PATHNAME'
 LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
 
--- Introspection helper exposing the integer query subtypes understood by the C
--- cross-type dispatch (gbt_int_crosstype_table in btree_utils_num.c). The
--- regression tests use it to assert that the cross-type pg_amop entries below
--- never drift from what the dispatch can handle.
-CREATE FUNCTION gbt_int_crosstype_subtypes()
-RETURNS SETOF oid
-AS 'MODULE_PATHNAME'
-LANGUAGE C IMMUTABLE PARALLEL SAFE;
-
 CREATE OPERATOR <-> (
 	LEFTARG = int2,
 	RIGHTARG = int4,
diff --git a/contrib/btree_gist/btree_int2.c b/contrib/btree_gist/btree_int2.c
index d00b24d9a57..ea9c206e552 100644
--- a/contrib/btree_gist/btree_int2.c
+++ b/contrib/btree_gist/btree_int2.c
@@ -91,6 +91,72 @@ static const gbtree_ninfo tinfo =
 	gbt_int2_dist
 };
 
+/*
+ * Cross-type GiST callbacks: the indexed key is int2, the query is int4 or
+ * int8.  Both reuse gbt_num_consistent()/gbt_num_distance() via a tinfo whose
+ * comparison/distance callbacks read the query (left) and key (right) sides at
+ * their own widths.  f_cmp is unused on these paths and left NULL.
+ */
+GBT_INT_CMP_FNS(gbt_int2_q4_, int32, int16)
+GBT_INT_CMP_FNS(gbt_int2_q8_, int64, int16)
+
+static const gbtree_ninfo tinfo_q4 =
+{
+	gbt_t_int2,
+	sizeof(int16),
+	4,
+	gbt_int2_q4_gt,
+	gbt_int2_q4_ge,
+	gbt_int2_q4_eq,
+	gbt_int2_q4_le,
+	gbt_int2_q4_lt,
+	NULL,
+	gbt_int2_q4_dist
+};
+
+static const gbtree_ninfo tinfo_q8 =
+{
+	gbt_t_int2,
+	sizeof(int16),
+	4,
+	gbt_int2_q8_gt,
+	gbt_int2_q8_ge,
+	gbt_int2_q8_eq,
+	gbt_int2_q8_le,
+	gbt_int2_q8_lt,
+	NULL,
+	gbt_int2_q8_dist
+};
+
+/*
+ * Cross-type dispatch shared by gbt_int2_consistent and gbt_int2_distance:
+ * select the tinfo for the query subtype and read the query value at its own
+ * width into caller-owned storage.
+ */
+static const gbtree_ninfo *
+gbt_int2_crosstype(Oid subtype, Datum d, gbt_intkey *q, const void **qp)
+{
+	switch (subtype)
+	{
+		case INT2OID:
+			q->i2 = DatumGetInt16(d);
+			*qp = &q->i2;
+			return &tinfo;
+		case INT4OID:
+			q->i4 = DatumGetInt32(d);
+			*qp = &q->i4;
+			return &tinfo_q4;
+		case INT8OID:
+			q->i8 = DatumGetInt64(d);
+			*qp = &q->i8;
+			return &tinfo_q8;
+		default:
+			elog(ERROR, "unrecognized subtype %u for btree_gist int2 cross-type comparison",
+				 subtype);
+			return NULL;		/* keep compiler quiet */
+	}
+}
+
 
 PG_FUNCTION_INFO_V1(int2_dist);
 Datum
@@ -176,7 +242,9 @@ gbt_int2_consistent(PG_FUNCTION_ARGS)
 	Oid			subtype = PG_GETARG_OID(3);
 	bool	   *recheck = (bool *) PG_GETARG_POINTER(4);
 	int16KEY   *kkk = (int16KEY *) DatumGetPointer(entry->key);
-	int16		query;
+	const gbtree_ninfo *ti;
+	gbt_intkey	query;
+	const void *qp;
 	GBT_NUMKEY_R key;
 
 	/* All cases served by this function are exact */
@@ -185,17 +253,10 @@ gbt_int2_consistent(PG_FUNCTION_ARGS)
 	key.lower = (GBT_NUMKEY *) &kkk->lower;
 	key.upper = (GBT_NUMKEY *) &kkk->upper;
 
-	if (likely(subtype == InvalidOid || subtype == INT2OID))
-	{
-		query = DatumGetInt16(queryDatum);
-		PG_RETURN_BOOL(gbt_num_consistent(&key, &query, &strategy,
-										  GIST_LEAF(entry), &tinfo,
-										  fcinfo->flinfo));
-	}
+	ti = gbt_int2_crosstype(subtype, queryDatum, &query, &qp);
 
-	PG_RETURN_BOOL(gbt_int_consistent_x((int64) kkk->lower, (int64) kkk->upper,
-										queryDatum, subtype, &strategy,
-										GIST_LEAF(entry)));
+	PG_RETURN_BOOL(gbt_num_consistent(&key, qp, &strategy, GIST_LEAF(entry),
+									  ti, fcinfo->flinfo));
 }
 
 Datum
@@ -205,21 +266,18 @@ gbt_int2_distance(PG_FUNCTION_ARGS)
 	Datum		queryDatum = PG_GETARG_DATUM(1);
 	Oid			subtype = PG_GETARG_OID(3);
 	int16KEY   *kkk = (int16KEY *) DatumGetPointer(entry->key);
-	int16		query;
+	const gbtree_ninfo *ti;
+	gbt_intkey	query;
+	const void *qp;
 	GBT_NUMKEY_R key;
 
 	key.lower = (GBT_NUMKEY *) &kkk->lower;
 	key.upper = (GBT_NUMKEY *) &kkk->upper;
 
-	if (likely(subtype == InvalidOid || subtype == INT2OID))
-	{
-		query = DatumGetInt16(queryDatum);
-		PG_RETURN_FLOAT8(gbt_num_distance(&key, &query, GIST_LEAF(entry),
-										  &tinfo, fcinfo->flinfo));
-	}
+	ti = gbt_int2_crosstype(subtype, queryDatum, &query, &qp);
 
-	PG_RETURN_FLOAT8(gbt_int_distance_x((int64) kkk->lower, (int64) kkk->upper,
-										queryDatum, subtype));
+	PG_RETURN_FLOAT8(gbt_num_distance(&key, qp, GIST_LEAF(entry),
+									  ti, fcinfo->flinfo));
 }
 
 Datum
diff --git a/contrib/btree_gist/btree_int4.c b/contrib/btree_gist/btree_int4.c
index 4df91f2057c..7e566890143 100644
--- a/contrib/btree_gist/btree_int4.c
+++ b/contrib/btree_gist/btree_int4.c
@@ -89,6 +89,72 @@ static const gbtree_ninfo tinfo =
 	gbt_int4_dist
 };
 
+/*
+ * Cross-type GiST callbacks: the indexed key is int4, the query is int2 or
+ * int8.  Both reuse gbt_num_consistent()/gbt_num_distance() via a tinfo whose
+ * comparison/distance callbacks read the query (left) and key (right) sides at
+ * their own widths.  f_cmp is unused on these paths and left NULL.
+ */
+GBT_INT_CMP_FNS(gbt_int4_q2_, int16, int32)
+GBT_INT_CMP_FNS(gbt_int4_q8_, int64, int32)
+
+static const gbtree_ninfo tinfo_q2 =
+{
+	gbt_t_int4,
+	sizeof(int32),
+	8,
+	gbt_int4_q2_gt,
+	gbt_int4_q2_ge,
+	gbt_int4_q2_eq,
+	gbt_int4_q2_le,
+	gbt_int4_q2_lt,
+	NULL,
+	gbt_int4_q2_dist
+};
+
+static const gbtree_ninfo tinfo_q8 =
+{
+	gbt_t_int4,
+	sizeof(int32),
+	8,
+	gbt_int4_q8_gt,
+	gbt_int4_q8_ge,
+	gbt_int4_q8_eq,
+	gbt_int4_q8_le,
+	gbt_int4_q8_lt,
+	NULL,
+	gbt_int4_q8_dist
+};
+
+/*
+ * Cross-type dispatch shared by gbt_int4_consistent and gbt_int4_distance:
+ * select the tinfo for the query subtype and read the query value at its own
+ * width into caller-owned storage.
+ */
+static const gbtree_ninfo *
+gbt_int4_crosstype(Oid subtype, Datum d, gbt_intkey *q, const void **qp)
+{
+	switch (subtype)
+	{
+		case INT2OID:
+			q->i2 = DatumGetInt16(d);
+			*qp = &q->i2;
+			return &tinfo_q2;
+		case INT4OID:
+			q->i4 = DatumGetInt32(d);
+			*qp = &q->i4;
+			return &tinfo;
+		case INT8OID:
+			q->i8 = DatumGetInt64(d);
+			*qp = &q->i8;
+			return &tinfo_q8;
+		default:
+			elog(ERROR, "unrecognized subtype %u for btree_gist int4 cross-type comparison",
+				 subtype);
+			return NULL;		/* keep compiler quiet */
+	}
+}
+
 
 PG_FUNCTION_INFO_V1(int4_dist);
 Datum
@@ -174,7 +240,9 @@ gbt_int4_consistent(PG_FUNCTION_ARGS)
 	Oid			subtype = PG_GETARG_OID(3);
 	bool	   *recheck = (bool *) PG_GETARG_POINTER(4);
 	int32KEY   *kkk = (int32KEY *) DatumGetPointer(entry->key);
-	int32		query;
+	const gbtree_ninfo *ti;
+	gbt_intkey	query;
+	const void *qp;
 	GBT_NUMKEY_R key;
 
 	/* All cases served by this function are exact */
@@ -183,17 +251,10 @@ gbt_int4_consistent(PG_FUNCTION_ARGS)
 	key.lower = (GBT_NUMKEY *) &kkk->lower;
 	key.upper = (GBT_NUMKEY *) &kkk->upper;
 
-	if (likely(subtype == InvalidOid || subtype == INT4OID))
-	{
-		query = DatumGetInt32(queryDatum);
-		PG_RETURN_BOOL(gbt_num_consistent(&key, &query, &strategy,
-										  GIST_LEAF(entry), &tinfo,
-										  fcinfo->flinfo));
-	}
+	ti = gbt_int4_crosstype(subtype, queryDatum, &query, &qp);
 
-	PG_RETURN_BOOL(gbt_int_consistent_x((int64) kkk->lower, (int64) kkk->upper,
-										queryDatum, subtype, &strategy,
-										GIST_LEAF(entry)));
+	PG_RETURN_BOOL(gbt_num_consistent(&key, qp, &strategy, GIST_LEAF(entry),
+									  ti, fcinfo->flinfo));
 }
 
 Datum
@@ -203,21 +264,18 @@ gbt_int4_distance(PG_FUNCTION_ARGS)
 	Datum		queryDatum = PG_GETARG_DATUM(1);
 	Oid			subtype = PG_GETARG_OID(3);
 	int32KEY   *kkk = (int32KEY *) DatumGetPointer(entry->key);
-	int32		query;
+	const gbtree_ninfo *ti;
+	gbt_intkey	query;
+	const void *qp;
 	GBT_NUMKEY_R key;
 
 	key.lower = (GBT_NUMKEY *) &kkk->lower;
 	key.upper = (GBT_NUMKEY *) &kkk->upper;
 
-	if (likely(subtype == InvalidOid || subtype == INT4OID))
-	{
-		query = DatumGetInt32(queryDatum);
-		PG_RETURN_FLOAT8(gbt_num_distance(&key, &query, GIST_LEAF(entry),
-										  &tinfo, fcinfo->flinfo));
-	}
+	ti = gbt_int4_crosstype(subtype, queryDatum, &query, &qp);
 
-	PG_RETURN_FLOAT8(gbt_int_distance_x((int64) kkk->lower, (int64) kkk->upper,
-										queryDatum, subtype));
+	PG_RETURN_FLOAT8(gbt_num_distance(&key, qp, GIST_LEAF(entry),
+									  ti, fcinfo->flinfo));
 }
 
 Datum
diff --git a/contrib/btree_gist/btree_int8.c b/contrib/btree_gist/btree_int8.c
index ae01273b709..81c8bb364cf 100644
--- a/contrib/btree_gist/btree_int8.c
+++ b/contrib/btree_gist/btree_int8.c
@@ -91,6 +91,72 @@ static const gbtree_ninfo tinfo =
 	gbt_int8_dist
 };
 
+/*
+ * Cross-type GiST callbacks: the indexed key is int8, the query is int2 or
+ * int4.  Both reuse gbt_num_consistent()/gbt_num_distance() via a tinfo whose
+ * comparison/distance callbacks read the query (left) and key (right) sides at
+ * their own widths.  f_cmp is unused on these paths and left NULL.
+ */
+GBT_INT_CMP_FNS(gbt_int8_q2_, int16, int64)
+GBT_INT_CMP_FNS(gbt_int8_q4_, int32, int64)
+
+static const gbtree_ninfo tinfo_q2 =
+{
+	gbt_t_int8,
+	sizeof(int64),
+	16,
+	gbt_int8_q2_gt,
+	gbt_int8_q2_ge,
+	gbt_int8_q2_eq,
+	gbt_int8_q2_le,
+	gbt_int8_q2_lt,
+	NULL,
+	gbt_int8_q2_dist
+};
+
+static const gbtree_ninfo tinfo_q4 =
+{
+	gbt_t_int8,
+	sizeof(int64),
+	16,
+	gbt_int8_q4_gt,
+	gbt_int8_q4_ge,
+	gbt_int8_q4_eq,
+	gbt_int8_q4_le,
+	gbt_int8_q4_lt,
+	NULL,
+	gbt_int8_q4_dist
+};
+
+/*
+ * Cross-type dispatch shared by gbt_int8_consistent and gbt_int8_distance:
+ * select the tinfo for the query subtype and read the query value at its own
+ * width into caller-owned storage.
+ */
+static const gbtree_ninfo *
+gbt_int8_crosstype(Oid subtype, Datum d, gbt_intkey *q, const void **qp)
+{
+	switch (subtype)
+	{
+		case INT2OID:
+			q->i2 = DatumGetInt16(d);
+			*qp = &q->i2;
+			return &tinfo_q2;
+		case INT4OID:
+			q->i4 = DatumGetInt32(d);
+			*qp = &q->i4;
+			return &tinfo_q4;
+		case INT8OID:
+			q->i8 = DatumGetInt64(d);
+			*qp = &q->i8;
+			return &tinfo;
+		default:
+			elog(ERROR, "unrecognized subtype %u for btree_gist int8 cross-type comparison",
+				 subtype);
+			return NULL;		/* keep compiler quiet */
+	}
+}
+
 
 PG_FUNCTION_INFO_V1(int8_dist);
 Datum
@@ -176,7 +242,9 @@ gbt_int8_consistent(PG_FUNCTION_ARGS)
 	Oid			subtype = PG_GETARG_OID(3);
 	bool	   *recheck = (bool *) PG_GETARG_POINTER(4);
 	int64KEY   *kkk = (int64KEY *) DatumGetPointer(entry->key);
-	int64		query;
+	const gbtree_ninfo *ti;
+	gbt_intkey	query;
+	const void *qp;
 	GBT_NUMKEY_R key;
 
 	/* All cases served by this function are exact */
@@ -185,17 +253,10 @@ gbt_int8_consistent(PG_FUNCTION_ARGS)
 	key.lower = (GBT_NUMKEY *) &kkk->lower;
 	key.upper = (GBT_NUMKEY *) &kkk->upper;
 
-	if (likely(subtype == InvalidOid || subtype == INT8OID))
-	{
-		query = DatumGetInt64(queryDatum);
-		PG_RETURN_BOOL(gbt_num_consistent(&key, &query, &strategy,
-										  GIST_LEAF(entry), &tinfo,
-										  fcinfo->flinfo));
-	}
+	ti = gbt_int8_crosstype(subtype, queryDatum, &query, &qp);
 
-	PG_RETURN_BOOL(gbt_int_consistent_x(kkk->lower, kkk->upper,
-										queryDatum, subtype, &strategy,
-										GIST_LEAF(entry)));
+	PG_RETURN_BOOL(gbt_num_consistent(&key, qp, &strategy, GIST_LEAF(entry),
+									  ti, fcinfo->flinfo));
 }
 
 Datum
@@ -205,21 +266,18 @@ gbt_int8_distance(PG_FUNCTION_ARGS)
 	Datum		queryDatum = PG_GETARG_DATUM(1);
 	Oid			subtype = PG_GETARG_OID(3);
 	int64KEY   *kkk = (int64KEY *) DatumGetPointer(entry->key);
-	int64		query;
+	const gbtree_ninfo *ti;
+	gbt_intkey	query;
+	const void *qp;
 	GBT_NUMKEY_R key;
 
 	key.lower = (GBT_NUMKEY *) &kkk->lower;
 	key.upper = (GBT_NUMKEY *) &kkk->upper;
 
-	if (likely(subtype == InvalidOid || subtype == INT8OID))
-	{
-		query = DatumGetInt64(queryDatum);
-		PG_RETURN_FLOAT8(gbt_num_distance(&key, &query, GIST_LEAF(entry),
-										  &tinfo, fcinfo->flinfo));
-	}
+	ti = gbt_int8_crosstype(subtype, queryDatum, &query, &qp);
 
-	PG_RETURN_FLOAT8(gbt_int_distance_x(kkk->lower, kkk->upper,
-										queryDatum, subtype));
+	PG_RETURN_FLOAT8(gbt_num_distance(&key, qp, GIST_LEAF(entry),
+									  ti, fcinfo->flinfo));
 }
 
 Datum
diff --git a/contrib/btree_gist/btree_utils_num.c b/contrib/btree_gist/btree_utils_num.c
index cff47e2d876..91f253d880c 100644
--- a/contrib/btree_gist/btree_utils_num.c
+++ b/contrib/btree_gist/btree_utils_num.c
@@ -5,8 +5,6 @@
 
 #include "btree_gist.h"
 #include "btree_utils_num.h"
-#include "catalog/pg_type.h"
-#include "funcapi.h"
 #include "utils/cash.h"
 #include "utils/date.h"
 #include "utils/timestamp.h"
@@ -271,6 +269,12 @@ gbt_num_consistent(const GBT_NUMKEY_R *key,
 {
 	bool		retval;
 
+	/*
+	 * Every comparison callback is invoked as f_xx(query, key): the query value
+	 * is always the left argument and the indexed key bound the right.  The
+	 * integer opclasses rely on this fixed order so their cross-type callbacks
+	 * can read each side at its own width.
+	 */
 	switch (*strategy)
 	{
 		case BTLessEqualStrategyNumber:
@@ -286,7 +290,7 @@ gbt_num_consistent(const GBT_NUMKEY_R *key,
 			if (is_leaf)
 				retval = tinfo->f_eq(query, key->lower, flinfo);
 			else
-				retval = (tinfo->f_le(key->lower, query, flinfo) &&
+				retval = (tinfo->f_ge(query, key->lower, flinfo) &&
 						  tinfo->f_le(query, key->upper, flinfo));
 			break;
 		case BTGreaterStrategyNumber:
@@ -309,139 +313,6 @@ gbt_num_consistent(const GBT_NUMKEY_R *key,
 	return retval;
 }
 
-/*
- * Cross-type dispatch table for the integer opclasses.
- *
- * This is the single source of truth for which query subtypes the integer
- * cross-type path understands. gbt_int_query_to_int64() promotes exactly the
- * subtypes listed here, and the SQL-visible gbt_int_crosstype_subtypes()
- * exposes the same list so the regression tests can assert that the cross-type
- * pg_amop entries in gist_int{2,4,8}_ops never drift from what the C code can
- * actually handle.
- */
-typedef int64 (*gbt_int_promote_fn) (Datum query);
-
-static int64
-gbt_int2_to_int64(Datum query)
-{
-	return (int64) DatumGetInt16(query);
-}
-
-static int64
-gbt_int4_to_int64(Datum query)
-{
-	return (int64) DatumGetInt32(query);
-}
-
-static int64
-gbt_int8_to_int64(Datum query)
-{
-	return DatumGetInt64(query);
-}
-
-typedef struct
-{
-	Oid			subtype;
-	gbt_int_promote_fn promote;
-}			gbt_int_crosstype;
-
-static const gbt_int_crosstype gbt_int_crosstype_table[] = {
-	{INT2OID, gbt_int2_to_int64},
-	{INT4OID, gbt_int4_to_int64},
-	{INT8OID, gbt_int8_to_int64},
-};
-
-/*
- * Promote a cross-type integer query value to int64.
- *
- * A subtype outside gbt_int_crosstype_table means the C dispatch is out of sync
- * with the operator-family registrations in pg_amop, so we treat it as an
- * internal error.  The regression tests assert this never happens.
- */
-static int64
-gbt_int_query_to_int64(Datum query, Oid subtype)
-{
-	int			i;
-
-	for (i = 0; i < lengthof(gbt_int_crosstype_table); i++)
-	{
-		if (gbt_int_crosstype_table[i].subtype == subtype)
-			return gbt_int_crosstype_table[i].promote(query);
-	}
-
-	elog(ERROR, "unrecognized subtype %u for btree_gist integer cross-type comparison",
-		 subtype);
-	return 0;					/* keep compiler quiet */
-}
-
-/*
- * gbt_int_crosstype_subtypes
- *
- * Expose the contents of gbt_int_crosstype_table to SQL as a set of type OIDs.
- * The btree_gist regression tests use this to check that the cross-type pg_amop
- * entries in gist_int{2,4,8}_ops stay in agreement with the C dispatch.
- */
-PG_FUNCTION_INFO_V1(gbt_int_crosstype_subtypes);
-Datum
-gbt_int_crosstype_subtypes(PG_FUNCTION_ARGS)
-{
-	FuncCallContext *funcctx;
-
-	if (SRF_IS_FIRSTCALL())
-		funcctx = SRF_FIRSTCALL_INIT();
-
-	funcctx = SRF_PERCALL_SETUP();
-
-	if (funcctx->call_cntr < lengthof(gbt_int_crosstype_table))
-	{
-		Oid			subtype = gbt_int_crosstype_table[funcctx->call_cntr].subtype;
-
-		SRF_RETURN_NEXT(funcctx, ObjectIdGetDatum(subtype));
-	}
-
-	SRF_RETURN_DONE(funcctx);
-}
-
-/*
- * Cross-type consistent method for the integer opclasses.
- *
- * The key range [lower, upper] and the query value are all compared as int64.
- * The strategy logic mirrors the same-type path in gbt_num_consistent(): the
- * query value keeps its own width (no narrowing to the indexed column type),
- * so out-of-range constants behave according to normal integer comparison.
- */
-bool
-gbt_int_consistent_x(int64 lower, int64 upper, Datum query, Oid subtype,
-					 const StrategyNumber *strategy, bool is_leaf)
-{
-	int64		q = gbt_int_query_to_int64(query, subtype);
-
-	switch (*strategy)
-	{
-		case BTLessEqualStrategyNumber:
-			/* some k in [lower,upper] has k <= q iff lower <= q */
-			return lower <= q;
-		case BTLessStrategyNumber:
-			/* leaf: key < q. internal: lower <= q (loose) */
-			return is_leaf ? (lower < q) : (lower <= q);
-		case BTEqualStrategyNumber:
-			if (is_leaf)
-				return lower == q;
-			/* internal: lower <= q <= upper */
-			return (lower <= q && q <= upper);
-		case BTGreaterStrategyNumber:
-			/* leaf: key > q. internal: upper >= q (loose) */
-			return is_leaf ? (upper > q) : (upper >= q);
-		case BTGreaterEqualStrategyNumber:
-			return upper >= q;
-		case BtreeGistNotEqualStrategyNumber:
-			return !(lower == q && upper == q);
-		default:
-			return false;
-	}
-}
-
-
 /*
  * The GiST distance method (for KNN-Gist)
  */
@@ -468,32 +339,6 @@ gbt_num_distance(const GBT_NUMKEY_R *key,
 	return retval;
 }
 
-/*
- * Cross-type distance method for the integer opclasses.
- *
- * The distance from the query value to the key range is computed in int64 and
- * returned as a float8, matching the same-type integer distance behaviour.
- *
- * Note that the subtraction is performed in float8, so when the key bound and
- * the query value differ by more than 2^53 the returned distance loses
- * precision. This only affects the ordering of KNN results that are nearly
- * equidistant at that scale; it never changes which rows are returned.
- */
-float8
-gbt_int_distance_x(int64 lower, int64 upper, Datum query, Oid subtype)
-{
-	int64		q = gbt_int_query_to_int64(query, subtype);
-
-	/* query below the range: distance to lower bound */
-	if (lower >= q)
-		return fabs((float8) lower - (float8) q);
-	/* query above the range: distance to upper bound */
-	if (upper <= q)
-		return fabs((float8) upper - (float8) q);
-	/* query inside the range */
-	return 0.0;
-}
-
 
 GIST_SPLITVEC *
 gbt_num_picksplit(const GistEntryVector *entryvec, GIST_SPLITVEC *v,
diff --git a/contrib/btree_gist/btree_utils_num.h b/contrib/btree_gist/btree_utils_num.h
index d378b0df37e..be09bdb2581 100644
--- a/contrib/btree_gist/btree_utils_num.h
+++ b/contrib/btree_gist/btree_utils_num.h
@@ -27,6 +27,18 @@ typedef struct
 	GBT_NUMKEY *t;
 } Nsrt;
 
+/*
+ * Query-value storage for the integer opclasses' cross-type path.  The caller
+ * owns one of these and passes its address to the per-type cross-type helper,
+ * which fills the right width and points the query pointer at it.
+ */
+typedef union
+{
+	int16		i2;
+	int32		i4;
+	int64		i8;
+}			gbt_intkey;
+
 
 /* type description */
 
@@ -86,7 +98,32 @@ typedef struct
 	 (ivp)->day * (24.0 * SECS_PER_HOUR) + \
 	 (ivp)->month * (30.0 * SECS_PER_DAY))
 
-#define GET_FLOAT_DISTANCE(t, arg1, arg2)	fabs( ((float8) *((const t *) (arg1))) - ((float8) *((const t *) (arg2))) )
+#define GET_FLOAT_DISTANCE2(t1, t2, arg1, arg2)	fabs( ((float8) *((const t1 *) (arg1))) - ((float8) *((const t2 *) (arg2))) )
+#define GET_FLOAT_DISTANCE(t, arg1, arg2)	GET_FLOAT_DISTANCE2(t, t, (arg1), (arg2))
+
+/*
+ * Generate the comparison/distance callbacks for a gbtree_ninfo whose query
+ * and key sides may be different (integer) types.  gbt_num_consistent() and
+ * gbt_num_distance() always invoke the callbacks as f_xx(query, key), so the
+ * first argument has the query type QT (the operator's right-hand subtype) and
+ * the second has the indexed key type KT.  Integer widening is value-preserving,
+ * so the comparisons need no explicit cast; the distance widens to float8 to
+ * avoid overflow in the subtraction.  Invoked with QT == KT this also generates
+ * the ordinary same-type callbacks.
+ */
+#define GBT_INT_CMP_FNS(prefix, QT, KT) \
+static bool prefix##gt(const void *a, const void *b, FmgrInfo *flinfo) \
+{ return *((const QT *) a) > *((const KT *) b); } \
+static bool prefix##ge(const void *a, const void *b, FmgrInfo *flinfo) \
+{ return *((const QT *) a) >= *((const KT *) b); } \
+static bool prefix##eq(const void *a, const void *b, FmgrInfo *flinfo) \
+{ return *((const QT *) a) == *((const KT *) b); } \
+static bool prefix##le(const void *a, const void *b, FmgrInfo *flinfo) \
+{ return *((const QT *) a) <= *((const KT *) b); } \
+static bool prefix##lt(const void *a, const void *b, FmgrInfo *flinfo) \
+{ return *((const QT *) a) < *((const KT *) b); } \
+static float8 prefix##dist(const void *a, const void *b, FmgrInfo *flinfo) \
+{ return GET_FLOAT_DISTANCE2(QT, KT, a, b); }
 
 
 extern Interval *abs_interval(Interval *a);
@@ -98,22 +135,6 @@ extern bool gbt_num_consistent(const GBT_NUMKEY_R *key, const void *query,
 extern float8 gbt_num_distance(const GBT_NUMKEY_R *key, const void *query,
 							   bool is_leaf, const gbtree_ninfo *tinfo, FmgrInfo *flinfo);
 
-/*
- * Cross-type consistent/distance helpers for the integer opclasses
- * (int2/int4/int8).  All three integer widths promote losslessly to int64, so
- * a cross-type query reduces to a plain int64 comparison and there is no need
- * for the per-type callback machinery the same-type path uses.  These helpers
- * are deliberately integer-specific rather than a general cross-type framework;
- * other type families should grow their own helpers shaped to their needs as
- * the need arises.
- */
-extern bool gbt_int_consistent_x(int64 lower, int64 upper,
-								 Datum query, Oid subtype,
-								 const StrategyNumber *strategy, bool is_leaf);
-
-extern float8 gbt_int_distance_x(int64 lower, int64 upper,
-								 Datum query, Oid subtype);
-
 extern GIST_SPLITVEC *gbt_num_picksplit(const GistEntryVector *entryvec, GIST_SPLITVEC *v,
 										const gbtree_ninfo *tinfo, FmgrInfo *flinfo);
 
diff --git a/contrib/btree_gist/expected/int_crosstype.out b/contrib/btree_gist/expected/int_crosstype.out
index bd2b90bd3f6..af03cf84a40 100644
--- a/contrib/btree_gist/expected/int_crosstype.out
+++ b/contrib/btree_gist/expected/int_crosstype.out
@@ -6,66 +6,11 @@
 -- index, and (c) values outside the smaller subtype's range are handled
 -- according to normal comparison semantics, without narrowing or erroring.
 --
--- Catalog invariant: the cross-type pg_amop entries in gist_int{2,4,8}_ops must
--- agree, in both directions, with what the C cross-type dispatch can handle.
--- The set of supported query subtypes is read from the C side via
--- gbt_int_crosstype_subtypes(), so there is no hand-maintained second copy of
--- the list here: registering a pg_amop row whose subtype the dispatch does not
--- handle (or dropping a dispatch entry while its pg_amop rows remain, or vice
--- versa) shows up as a diff below. Cross-type pg_amproc rows must also stay
--- absent, since the dispatch reuses the same-type support functions.
+-- The integer cross-type support is handled inside the existing
+-- consistent/distance support functions (which dispatch on the subtype OID),
+-- so the operator families must not contain any cross-type (different
+-- left/right input type) support-function entries.
 --
-WITH dispatch_subtypes(typ) AS (
-    SELECT s.subtype::regtype
-    FROM gbt_int_crosstype_subtypes() AS s(subtype)
-),
-gist_int_opclasses(opfamily, indextype) AS (
-    SELECT opc.opcname::text, opc.opcintype::regtype
-    FROM pg_opclass opc
-         JOIN pg_am am ON am.oid = opc.opcmethod
-    WHERE am.amname = 'gist'
-      AND opc.opcname IN ('gist_int2_ops', 'gist_int4_ops', 'gist_int8_ops')
-),
-expected_pairs(opfamily, lefttype, righttype) AS (
-    SELECT oc.opfamily, oc.indextype, ds.typ
-    FROM gist_int_opclasses oc
-         CROSS JOIN dispatch_subtypes ds
-    WHERE ds.typ <> oc.indextype
-),
-expected_amop(opfamily, lefttype, righttype, strategy, purpose) AS (
-    SELECT opfamily, lefttype, righttype, strategy, purpose
-    FROM expected_pairs
-         CROSS JOIN (VALUES
-             (1, 's'), (2, 's'), (3, 's'), (4, 's'),
-             (5, 's'), (6, 's'), (15, 'o')
-         ) AS strategy_purposes(strategy, purpose)
-),
-actual_amop AS (
-    SELECT opf.opfname::text AS opfamily,
-           amop.amoplefttype::regtype AS lefttype,
-           amop.amoprighttype::regtype AS righttype,
-           amop.amopstrategy::int AS strategy,
-           amop.amoppurpose::text AS purpose
-    FROM pg_amop amop
-         JOIN pg_opfamily opf ON opf.oid = amop.amopfamily
-         JOIN pg_am am ON am.oid = opf.opfmethod
-    WHERE am.amname = 'gist'
-      AND opf.opfname IN ('gist_int2_ops', 'gist_int4_ops', 'gist_int8_ops')
-      AND amop.amoplefttype <> amop.amoprighttype
-)
-SELECT *
-FROM (
-    SELECT 'missing from pg_amop' AS status, *
-    FROM (SELECT * FROM expected_amop EXCEPT SELECT * FROM actual_amop) missing
-    UNION ALL
-    SELECT 'unexpected in pg_amop' AS status, *
-    FROM (SELECT * FROM actual_amop EXCEPT SELECT * FROM expected_amop) unexpected
-) diff
-ORDER BY status, opfamily, lefttype::text, righttype::text, strategy, purpose;
- status | opfamily | lefttype | righttype | strategy | purpose 
---------+----------+----------+-----------+----------+---------
-(0 rows)
-
 SELECT opf.opfname AS opfamily,
        amproc.amproclefttype::regtype AS lefttype,
        amproc.amprocrighttype::regtype AS righttype,
diff --git a/contrib/btree_gist/sql/int_crosstype.sql b/contrib/btree_gist/sql/int_crosstype.sql
index d38084f1941..b337e26a1af 100644
--- a/contrib/btree_gist/sql/int_crosstype.sql
+++ b/contrib/btree_gist/sql/int_crosstype.sql
@@ -7,63 +7,11 @@
 -- according to normal comparison semantics, without narrowing or erroring.
 
 --
--- Catalog invariant: the cross-type pg_amop entries in gist_int{2,4,8}_ops must
--- agree, in both directions, with what the C cross-type dispatch can handle.
--- The set of supported query subtypes is read from the C side via
--- gbt_int_crosstype_subtypes(), so there is no hand-maintained second copy of
--- the list here: registering a pg_amop row whose subtype the dispatch does not
--- handle (or dropping a dispatch entry while its pg_amop rows remain, or vice
--- versa) shows up as a diff below. Cross-type pg_amproc rows must also stay
--- absent, since the dispatch reuses the same-type support functions.
+-- The integer cross-type support is handled inside the existing
+-- consistent/distance support functions (which dispatch on the subtype OID),
+-- so the operator families must not contain any cross-type (different
+-- left/right input type) support-function entries.
 --
-WITH dispatch_subtypes(typ) AS (
-    SELECT s.subtype::regtype
-    FROM gbt_int_crosstype_subtypes() AS s(subtype)
-),
-gist_int_opclasses(opfamily, indextype) AS (
-    SELECT opc.opcname::text, opc.opcintype::regtype
-    FROM pg_opclass opc
-         JOIN pg_am am ON am.oid = opc.opcmethod
-    WHERE am.amname = 'gist'
-      AND opc.opcname IN ('gist_int2_ops', 'gist_int4_ops', 'gist_int8_ops')
-),
-expected_pairs(opfamily, lefttype, righttype) AS (
-    SELECT oc.opfamily, oc.indextype, ds.typ
-    FROM gist_int_opclasses oc
-         CROSS JOIN dispatch_subtypes ds
-    WHERE ds.typ <> oc.indextype
-),
-expected_amop(opfamily, lefttype, righttype, strategy, purpose) AS (
-    SELECT opfamily, lefttype, righttype, strategy, purpose
-    FROM expected_pairs
-         CROSS JOIN (VALUES
-             (1, 's'), (2, 's'), (3, 's'), (4, 's'),
-             (5, 's'), (6, 's'), (15, 'o')
-         ) AS strategy_purposes(strategy, purpose)
-),
-actual_amop AS (
-    SELECT opf.opfname::text AS opfamily,
-           amop.amoplefttype::regtype AS lefttype,
-           amop.amoprighttype::regtype AS righttype,
-           amop.amopstrategy::int AS strategy,
-           amop.amoppurpose::text AS purpose
-    FROM pg_amop amop
-         JOIN pg_opfamily opf ON opf.oid = amop.amopfamily
-         JOIN pg_am am ON am.oid = opf.opfmethod
-    WHERE am.amname = 'gist'
-      AND opf.opfname IN ('gist_int2_ops', 'gist_int4_ops', 'gist_int8_ops')
-      AND amop.amoplefttype <> amop.amoprighttype
-)
-SELECT *
-FROM (
-    SELECT 'missing from pg_amop' AS status, *
-    FROM (SELECT * FROM expected_amop EXCEPT SELECT * FROM actual_amop) missing
-    UNION ALL
-    SELECT 'unexpected in pg_amop' AS status, *
-    FROM (SELECT * FROM actual_amop EXCEPT SELECT * FROM expected_amop) unexpected
-) diff
-ORDER BY status, opfamily, lefttype::text, righttype::text, strategy, purpose;
-
 SELECT opf.opfname AS opfamily,
        amproc.amproclefttype::regtype AS lefttype,
        amproc.amprocrighttype::regtype AS righttype,
-- 
2.50.1 (Apple Git-155)



^ permalink  raw  reply  [nested|flat] 9+ messages in thread

* Re: [PATCH] btree_gist: add cross-type integer operator support for GiST
@ 2026-06-23 22:53  Alexander Nestorov <[email protected]>
  parent: Maxime Schoemans <[email protected]>
  0 siblings, 1 reply; 9+ messages in thread

From: Alexander Nestorov @ 2026-06-23 22:53 UTC (permalink / raw)
  To: Maxime Schoemans <[email protected]>; +Cc: Andrey Borodin <[email protected]>; pgsql-hackers

Hey Maxime!

Thank you for reviewing my changes so far and for proposing the changes in your
patch! And thank you for the kind words!

1)

I think we still need to check `InvalidOid` because execIndexing.c:~800
initializes scan keys ('ScanKeyEntryInitialize') with `InvalidOid` as the
subtype. Running the 'not_equal', 'partitions' and 'without_overlaps' tests
confirms that 'InvalidOid' must be handled. I can add the check again in the
switch. (attaching patch)

2) and 3)

I completely agree, your patch makes it much cleaner.
I applied your patch (with the 'InvalidOid' fix) on top of mine and ran all the
tests and benchmarks that I prepared previously. These are the results before
the entire patchset and after:


duration=15s/run, reps=10, 1 client, interleaved, lower is better
------------------------------------------------------------------------
before consistent min/med/mean = 51.246   51.361   51.378 ms
after consistent min/med/mean    = 51.849   51.958   51.967 ms
------------------------------------------------------------------------
before distance min/med/mean = 76.441    76.561   76.567 ms
after distance min/med/mean    = 76.568   76.678   76.681 ms
------------------------------------------------------------------------

I added one more commit that adds a small description of this fix in the docs.

The current patchset has been rebased on top of ef01ca6dbca54e9bf3abea01c357b346847ebcf3

Best regards!


Attachments:

  [application/octet-stream] v4-0001-Implement-cross-type-operators-for-GiST-indexes.patch (26.4K, 3-v4-0001-Implement-cross-type-operators-for-GiST-indexes.patch)
  download | inline diff:
From 2c7258fdef6c91551996cb7e3f4184c5b4daa2f4 Mon Sep 17 00:00:00 2001
From: Alexander Nestorov <[email protected]>
Date: Thu, 4 Jun 2026 00:06:54 +0200
Subject: [PATCH] Implement cross-type operators for GiST indexes

---
 contrib/btree_gist/Makefile                  |   4 +-
 contrib/btree_gist/btree_gist--1.9--1.10.sql | 143 +++++++++++++++++
 contrib/btree_gist/btree_gist.control        |   2 +-
 contrib/btree_gist/btree_int2.c              |  70 ++++++--
 contrib/btree_gist/btree_int4.c              |  70 ++++++--
 contrib/btree_gist/btree_int8.c              |  70 ++++++--
 contrib/btree_gist/btree_utils_num.c         | 160 +++++++++++++++++++
 contrib/btree_gist/btree_utils_num.h         |  16 ++
 contrib/btree_gist/meson.build               |   2 +
 9 files changed, 501 insertions(+), 36 deletions(-)
 create mode 100644 contrib/btree_gist/btree_gist--1.9--1.10.sql

diff --git a/contrib/btree_gist/Makefile b/contrib/btree_gist/Makefile
index fbbbca95598..1d0668d97ab 100644
--- a/contrib/btree_gist/Makefile
+++ b/contrib/btree_gist/Makefile
@@ -32,19 +32,19 @@ OBJS =  \
 EXTENSION = btree_gist
 DATA = btree_gist--1.0--1.1.sql \
        btree_gist--1.1--1.2.sql btree_gist--1.2--1.3.sql \
        btree_gist--1.3--1.4.sql btree_gist--1.4--1.5.sql \
        btree_gist--1.5--1.6.sql btree_gist--1.6--1.7.sql \
        btree_gist--1.7--1.8.sql btree_gist--1.8--1.9.sql \
-       btree_gist--1.9.sql
+       btree_gist--1.9.sql btree_gist--1.9--1.10.sql
 PGFILEDESC = "btree_gist - B-tree equivalent GiST operator classes"
 
 REGRESS = init int2 int4 int8 float4 float8 cash oid timestamp timestamptz \
         time timetz date interval macaddr macaddr8 inet cidr text varchar char \
         bytea bit varbit numeric uuid not_equal enum bool partitions \
-        stratnum without_overlaps
+        stratnum int_crosstype without_overlaps
 
 SHLIB_LINK += $(filter -lm, $(LIBS))
 
 ifdef USE_PGXS
 PG_CONFIG = pg_config
 PGXS := $(shell $(PG_CONFIG) --pgxs)
diff --git a/contrib/btree_gist/btree_gist--1.9--1.10.sql b/contrib/btree_gist/btree_gist--1.9--1.10.sql
new file mode 100644
index 00000000000..c9ff1955204
--- /dev/null
+++ b/contrib/btree_gist/btree_gist--1.9--1.10.sql
@@ -0,0 +1,143 @@
+/* contrib/btree_gist/btree_gist--1.9--1.10.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION btree_gist UPDATE TO '1.10'" to load this file. \quit
+
+-- Add cross-type operator support for the integer trio (int2, int4, int8)
+-- to the existing GiST operator families.
+--
+-- GiST's amvalidate requires support functions in a family to have matching
+-- left/right input types, so the catalog additions below are deliberately
+-- pg_amop-only. The existing consistent/distance support functions dispatch
+-- on the subtype OID: same-type queries take the normal path, while mixed-width
+-- integer queries are promoted to int64 and compared by the integer cross-type
+-- helpers in btree_utils_num.c.
+
+CREATE FUNCTION int2_int4_dist(int2, int4)
+RETURNS int4
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION int4_int2_dist(int4, int2)
+RETURNS int4
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION int2_int8_dist(int2, int8)
+RETURNS int8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION int8_int2_dist(int8, int2)
+RETURNS int8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION int4_int8_dist(int4, int8)
+RETURNS int8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION int8_int4_dist(int8, int4)
+RETURNS int8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+-- Introspection helper exposing the integer query subtypes understood by the C
+-- cross-type dispatch (gbt_int_crosstype_table in btree_utils_num.c). The
+-- regression tests use it to assert that the cross-type pg_amop entries below
+-- never drift from what the dispatch can handle.
+CREATE FUNCTION gbt_int_crosstype_subtypes()
+RETURNS SETOF oid
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE PARALLEL SAFE;
+
+CREATE OPERATOR <-> (
+	LEFTARG = int2,
+	RIGHTARG = int4,
+	PROCEDURE = int2_int4_dist,
+	COMMUTATOR = '<->'
+);
+
+CREATE OPERATOR <-> (
+	LEFTARG = int4,
+	RIGHTARG = int2,
+	PROCEDURE = int4_int2_dist,
+	COMMUTATOR = '<->'
+);
+
+CREATE OPERATOR <-> (
+	LEFTARG = int2,
+	RIGHTARG = int8,
+	PROCEDURE = int2_int8_dist,
+	COMMUTATOR = '<->'
+);
+
+CREATE OPERATOR <-> (
+	LEFTARG = int8,
+	RIGHTARG = int2,
+	PROCEDURE = int8_int2_dist,
+	COMMUTATOR = '<->'
+);
+
+CREATE OPERATOR <-> (
+	LEFTARG = int4,
+	RIGHTARG = int8,
+	PROCEDURE = int4_int8_dist,
+	COMMUTATOR = '<->'
+);
+
+CREATE OPERATOR <-> (
+	LEFTARG = int8,
+	RIGHTARG = int4,
+	PROCEDURE = int8_int4_dist,
+	COMMUTATOR = '<->'
+);
+
+ALTER OPERATOR FAMILY gist_int2_ops USING gist ADD
+	OPERATOR	1	<  (int2, int4),
+	OPERATOR	2	<= (int2, int4),
+	OPERATOR	3	=  (int2, int4),
+	OPERATOR	4	>= (int2, int4),
+	OPERATOR	5	>  (int2, int4),
+	OPERATOR	6	<> (int2, int4),
+	OPERATOR	15	<-> (int2, int4) FOR ORDER BY pg_catalog.integer_ops,
+	OPERATOR	1	<  (int2, int8),
+	OPERATOR	2	<= (int2, int8),
+	OPERATOR	3	=  (int2, int8),
+	OPERATOR	4	>= (int2, int8),
+	OPERATOR	5	>  (int2, int8),
+	OPERATOR	6	<> (int2, int8),
+	OPERATOR	15	<-> (int2, int8) FOR ORDER BY pg_catalog.integer_ops;
+
+ALTER OPERATOR FAMILY gist_int4_ops USING gist ADD
+	OPERATOR	1	<  (int4, int2),
+	OPERATOR	2	<= (int4, int2),
+	OPERATOR	3	=  (int4, int2),
+	OPERATOR	4	>= (int4, int2),
+	OPERATOR	5	>  (int4, int2),
+	OPERATOR	6	<> (int4, int2),
+	OPERATOR	15	<-> (int4, int2) FOR ORDER BY pg_catalog.integer_ops,
+	OPERATOR	1	<  (int4, int8),
+	OPERATOR	2	<= (int4, int8),
+	OPERATOR	3	=  (int4, int8),
+	OPERATOR	4	>= (int4, int8),
+	OPERATOR	5	>  (int4, int8),
+	OPERATOR	6	<> (int4, int8),
+	OPERATOR	15	<-> (int4, int8) FOR ORDER BY pg_catalog.integer_ops;
+
+ALTER OPERATOR FAMILY gist_int8_ops USING gist ADD
+	OPERATOR	1	<  (int8, int2),
+	OPERATOR	2	<= (int8, int2),
+	OPERATOR	3	=  (int8, int2),
+	OPERATOR	4	>= (int8, int2),
+	OPERATOR	5	>  (int8, int2),
+	OPERATOR	6	<> (int8, int2),
+	OPERATOR	15	<-> (int8, int2) FOR ORDER BY pg_catalog.integer_ops,
+	OPERATOR	1	<  (int8, int4),
+	OPERATOR	2	<= (int8, int4),
+	OPERATOR	3	=  (int8, int4),
+	OPERATOR	4	>= (int8, int4),
+	OPERATOR	5	>  (int8, int4),
+	OPERATOR	6	<> (int8, int4),
+	OPERATOR	15	<-> (int8, int4) FOR ORDER BY pg_catalog.integer_ops;
diff --git a/contrib/btree_gist/btree_gist.control b/contrib/btree_gist/btree_gist.control
index 69d9341a0ad..e606fa6551d 100644
--- a/contrib/btree_gist/btree_gist.control
+++ b/contrib/btree_gist/btree_gist.control
@@ -1,6 +1,6 @@
 # btree_gist extension
 comment = 'support for indexing common datatypes in GiST'
-default_version = '1.9'
+default_version = '1.10'
 module_pathname = '$libdir/btree_gist'
 relocatable = true
 trusted = true
diff --git a/contrib/btree_gist/btree_int2.c b/contrib/btree_gist/btree_int2.c
index cc4b33177e3..d00b24d9a57 100644
--- a/contrib/btree_gist/btree_int2.c
+++ b/contrib/btree_gist/btree_int2.c
@@ -2,12 +2,13 @@
  * contrib/btree_gist/btree_int2.c
  */
 #include "postgres.h"
 
 #include "btree_gist.h"
 #include "btree_utils_num.h"
+#include "catalog/pg_type.h"
 #include "common/int.h"
 #include "utils/rel.h"
 #include "utils/sortsupport.h"
 
 typedef struct int16key
 {
@@ -73,13 +74,12 @@ gbt_int2key_cmp(const void *a, const void *b, FmgrInfo *flinfo)
 static float8
 gbt_int2_dist(const void *a, const void *b, FmgrInfo *flinfo)
 {
 	return GET_FLOAT_DISTANCE(int16, a, b);
 }
 
-
 static const gbtree_ninfo tinfo =
 {
 	gbt_t_int2,
 	sizeof(int16),
 	4,							/* sizeof(gbtreekey4) */
 	gbt_int2gt,
@@ -109,12 +109,46 @@ int2_dist(PG_FUNCTION_ARGS)
 
 	ra = abs(r);
 
 	PG_RETURN_INT16(ra);
 }
 
+PG_FUNCTION_INFO_V1(int2_int4_dist);
+Datum
+int2_int4_dist(PG_FUNCTION_ARGS)
+{
+	int32		a = (int32) PG_GETARG_INT16(0);
+	int32		b = PG_GETARG_INT32(1);
+	int32		r;
+
+	if (pg_sub_s32_overflow(a, b, &r) ||
+		r == PG_INT32_MIN)
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("integer out of range")));
+
+	PG_RETURN_INT32(abs(r));
+}
+
+PG_FUNCTION_INFO_V1(int2_int8_dist);
+Datum
+int2_int8_dist(PG_FUNCTION_ARGS)
+{
+	int64		a = (int64) PG_GETARG_INT16(0);
+	int64		b = PG_GETARG_INT64(1);
+	int64		r;
+
+	if (pg_sub_s64_overflow(a, b, &r) ||
+		r == PG_INT64_MIN)
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("bigint out of range")));
+
+	PG_RETURN_INT64(i64abs(r));
+}
+
 
 /**************************************************
  * GiST support functions
  **************************************************/
 
 Datum
@@ -134,47 +168,61 @@ gbt_int2_fetch(PG_FUNCTION_ARGS)
 }
 
 Datum
 gbt_int2_consistent(PG_FUNCTION_ARGS)
 {
 	GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
-	int16		query = PG_GETARG_INT16(1);
+	Datum		queryDatum = PG_GETARG_DATUM(1);
 	StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2);
-#ifdef NOT_USED
 	Oid			subtype = PG_GETARG_OID(3);
-#endif
 	bool	   *recheck = (bool *) PG_GETARG_POINTER(4);
 	int16KEY   *kkk = (int16KEY *) DatumGetPointer(entry->key);
+	int16		query;
 	GBT_NUMKEY_R key;
 
 	/* All cases served by this function are exact */
 	*recheck = false;
 
 	key.lower = (GBT_NUMKEY *) &kkk->lower;
 	key.upper = (GBT_NUMKEY *) &kkk->upper;
 
-	PG_RETURN_BOOL(gbt_num_consistent(&key, &query, &strategy,
-									  GIST_LEAF(entry), &tinfo, fcinfo->flinfo));
+	if (likely(subtype == InvalidOid || subtype == INT2OID))
+	{
+		query = DatumGetInt16(queryDatum);
+		PG_RETURN_BOOL(gbt_num_consistent(&key, &query, &strategy,
+										  GIST_LEAF(entry), &tinfo,
+										  fcinfo->flinfo));
+	}
+
+	PG_RETURN_BOOL(gbt_int_consistent_x((int64) kkk->lower, (int64) kkk->upper,
+										queryDatum, subtype, &strategy,
+										GIST_LEAF(entry)));
 }
 
 Datum
 gbt_int2_distance(PG_FUNCTION_ARGS)
 {
 	GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
-	int16		query = PG_GETARG_INT16(1);
-#ifdef NOT_USED
+	Datum		queryDatum = PG_GETARG_DATUM(1);
 	Oid			subtype = PG_GETARG_OID(3);
-#endif
 	int16KEY   *kkk = (int16KEY *) DatumGetPointer(entry->key);
+	int16		query;
 	GBT_NUMKEY_R key;
 
 	key.lower = (GBT_NUMKEY *) &kkk->lower;
 	key.upper = (GBT_NUMKEY *) &kkk->upper;
 
-	PG_RETURN_FLOAT8(gbt_num_distance(&key, &query, GIST_LEAF(entry),
-									  &tinfo, fcinfo->flinfo));
+	if (likely(subtype == InvalidOid || subtype == INT2OID))
+	{
+		query = DatumGetInt16(queryDatum);
+		PG_RETURN_FLOAT8(gbt_num_distance(&key, &query, GIST_LEAF(entry),
+										  &tinfo, fcinfo->flinfo));
+	}
+
+	PG_RETURN_FLOAT8(gbt_int_distance_x((int64) kkk->lower, (int64) kkk->upper,
+										queryDatum, subtype));
 }
 
 Datum
 gbt_int2_union(PG_FUNCTION_ARGS)
 {
 	GistEntryVector *entryvec = (GistEntryVector *) PG_GETARG_POINTER(0);
diff --git a/contrib/btree_gist/btree_int4.c b/contrib/btree_gist/btree_int4.c
index 47790578e6b..4df91f2057c 100644
--- a/contrib/btree_gist/btree_int4.c
+++ b/contrib/btree_gist/btree_int4.c
@@ -1,12 +1,13 @@
 /*
  * contrib/btree_gist/btree_int4.c
  */
 #include "postgres.h"
 #include "btree_gist.h"
 #include "btree_utils_num.h"
+#include "catalog/pg_type.h"
 #include "common/int.h"
 #include "utils/rel.h"
 #include "utils/sortsupport.h"
 
 typedef struct int32key
 {
@@ -71,13 +72,12 @@ gbt_int4key_cmp(const void *a, const void *b, FmgrInfo *flinfo)
 static float8
 gbt_int4_dist(const void *a, const void *b, FmgrInfo *flinfo)
 {
 	return GET_FLOAT_DISTANCE(int32, a, b);
 }
 
-
 static const gbtree_ninfo tinfo =
 {
 	gbt_t_int4,
 	sizeof(int32),
 	8,							/* sizeof(gbtreekey8) */
 	gbt_int4gt,
@@ -107,12 +107,46 @@ int4_dist(PG_FUNCTION_ARGS)
 
 	ra = abs(r);
 
 	PG_RETURN_INT32(ra);
 }
 
+PG_FUNCTION_INFO_V1(int4_int2_dist);
+Datum
+int4_int2_dist(PG_FUNCTION_ARGS)
+{
+	int32		a = PG_GETARG_INT32(0);
+	int32		b = (int32) PG_GETARG_INT16(1);
+	int32		r;
+
+	if (pg_sub_s32_overflow(a, b, &r) ||
+		r == PG_INT32_MIN)
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("integer out of range")));
+
+	PG_RETURN_INT32(abs(r));
+}
+
+PG_FUNCTION_INFO_V1(int4_int8_dist);
+Datum
+int4_int8_dist(PG_FUNCTION_ARGS)
+{
+	int64		a = (int64) PG_GETARG_INT32(0);
+	int64		b = PG_GETARG_INT64(1);
+	int64		r;
+
+	if (pg_sub_s64_overflow(a, b, &r) ||
+		r == PG_INT64_MIN)
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("bigint out of range")));
+
+	PG_RETURN_INT64(i64abs(r));
+}
+
 
 /**************************************************
  * GiST support functions
  **************************************************/
 
 Datum
@@ -132,47 +166,61 @@ gbt_int4_fetch(PG_FUNCTION_ARGS)
 }
 
 Datum
 gbt_int4_consistent(PG_FUNCTION_ARGS)
 {
 	GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
-	int32		query = PG_GETARG_INT32(1);
+	Datum		queryDatum = PG_GETARG_DATUM(1);
 	StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2);
-#ifdef NOT_USED
 	Oid			subtype = PG_GETARG_OID(3);
-#endif
 	bool	   *recheck = (bool *) PG_GETARG_POINTER(4);
 	int32KEY   *kkk = (int32KEY *) DatumGetPointer(entry->key);
+	int32		query;
 	GBT_NUMKEY_R key;
 
 	/* All cases served by this function are exact */
 	*recheck = false;
 
 	key.lower = (GBT_NUMKEY *) &kkk->lower;
 	key.upper = (GBT_NUMKEY *) &kkk->upper;
 
-	PG_RETURN_BOOL(gbt_num_consistent(&key, &query, &strategy,
-									  GIST_LEAF(entry), &tinfo, fcinfo->flinfo));
+	if (likely(subtype == InvalidOid || subtype == INT4OID))
+	{
+		query = DatumGetInt32(queryDatum);
+		PG_RETURN_BOOL(gbt_num_consistent(&key, &query, &strategy,
+										  GIST_LEAF(entry), &tinfo,
+										  fcinfo->flinfo));
+	}
+
+	PG_RETURN_BOOL(gbt_int_consistent_x((int64) kkk->lower, (int64) kkk->upper,
+										queryDatum, subtype, &strategy,
+										GIST_LEAF(entry)));
 }
 
 Datum
 gbt_int4_distance(PG_FUNCTION_ARGS)
 {
 	GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
-	int32		query = PG_GETARG_INT32(1);
-#ifdef NOT_USED
+	Datum		queryDatum = PG_GETARG_DATUM(1);
 	Oid			subtype = PG_GETARG_OID(3);
-#endif
 	int32KEY   *kkk = (int32KEY *) DatumGetPointer(entry->key);
+	int32		query;
 	GBT_NUMKEY_R key;
 
 	key.lower = (GBT_NUMKEY *) &kkk->lower;
 	key.upper = (GBT_NUMKEY *) &kkk->upper;
 
-	PG_RETURN_FLOAT8(gbt_num_distance(&key, &query, GIST_LEAF(entry),
-									  &tinfo, fcinfo->flinfo));
+	if (likely(subtype == InvalidOid || subtype == INT4OID))
+	{
+		query = DatumGetInt32(queryDatum);
+		PG_RETURN_FLOAT8(gbt_num_distance(&key, &query, GIST_LEAF(entry),
+										  &tinfo, fcinfo->flinfo));
+	}
+
+	PG_RETURN_FLOAT8(gbt_int_distance_x((int64) kkk->lower, (int64) kkk->upper,
+										queryDatum, subtype));
 }
 
 Datum
 gbt_int4_union(PG_FUNCTION_ARGS)
 {
 	GistEntryVector *entryvec = (GistEntryVector *) PG_GETARG_POINTER(0);
diff --git a/contrib/btree_gist/btree_int8.c b/contrib/btree_gist/btree_int8.c
index f48122c8d84..ae01273b709 100644
--- a/contrib/btree_gist/btree_int8.c
+++ b/contrib/btree_gist/btree_int8.c
@@ -2,12 +2,13 @@
  * contrib/btree_gist/btree_int8.c
  */
 #include "postgres.h"
 
 #include "btree_gist.h"
 #include "btree_utils_num.h"
+#include "catalog/pg_type.h"
 #include "common/int.h"
 #include "utils/rel.h"
 #include "utils/sortsupport.h"
 
 typedef struct int64key
 {
@@ -73,13 +74,12 @@ gbt_int8key_cmp(const void *a, const void *b, FmgrInfo *flinfo)
 static float8
 gbt_int8_dist(const void *a, const void *b, FmgrInfo *flinfo)
 {
 	return GET_FLOAT_DISTANCE(int64, a, b);
 }
 
-
 static const gbtree_ninfo tinfo =
 {
 	gbt_t_int8,
 	sizeof(int64),
 	16,							/* sizeof(gbtreekey16) */
 	gbt_int8gt,
@@ -109,12 +109,46 @@ int8_dist(PG_FUNCTION_ARGS)
 
 	ra = i64abs(r);
 
 	PG_RETURN_INT64(ra);
 }
 
+PG_FUNCTION_INFO_V1(int8_int2_dist);
+Datum
+int8_int2_dist(PG_FUNCTION_ARGS)
+{
+	int64		a = PG_GETARG_INT64(0);
+	int64		b = (int64) PG_GETARG_INT16(1);
+	int64		r;
+
+	if (pg_sub_s64_overflow(a, b, &r) ||
+		r == PG_INT64_MIN)
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("bigint out of range")));
+
+	PG_RETURN_INT64(i64abs(r));
+}
+
+PG_FUNCTION_INFO_V1(int8_int4_dist);
+Datum
+int8_int4_dist(PG_FUNCTION_ARGS)
+{
+	int64		a = PG_GETARG_INT64(0);
+	int64		b = (int64) PG_GETARG_INT32(1);
+	int64		r;
+
+	if (pg_sub_s64_overflow(a, b, &r) ||
+		r == PG_INT64_MIN)
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("bigint out of range")));
+
+	PG_RETURN_INT64(i64abs(r));
+}
+
 
 /**************************************************
  * GiST support functions
  **************************************************/
 
 Datum
@@ -134,47 +168,61 @@ gbt_int8_fetch(PG_FUNCTION_ARGS)
 }
 
 Datum
 gbt_int8_consistent(PG_FUNCTION_ARGS)
 {
 	GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
-	int64		query = PG_GETARG_INT64(1);
+	Datum		queryDatum = PG_GETARG_DATUM(1);
 	StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2);
-#ifdef NOT_USED
 	Oid			subtype = PG_GETARG_OID(3);
-#endif
 	bool	   *recheck = (bool *) PG_GETARG_POINTER(4);
 	int64KEY   *kkk = (int64KEY *) DatumGetPointer(entry->key);
+	int64		query;
 	GBT_NUMKEY_R key;
 
 	/* All cases served by this function are exact */
 	*recheck = false;
 
 	key.lower = (GBT_NUMKEY *) &kkk->lower;
 	key.upper = (GBT_NUMKEY *) &kkk->upper;
 
-	PG_RETURN_BOOL(gbt_num_consistent(&key, &query, &strategy,
-									  GIST_LEAF(entry), &tinfo, fcinfo->flinfo));
+	if (likely(subtype == InvalidOid || subtype == INT8OID))
+	{
+		query = DatumGetInt64(queryDatum);
+		PG_RETURN_BOOL(gbt_num_consistent(&key, &query, &strategy,
+										  GIST_LEAF(entry), &tinfo,
+										  fcinfo->flinfo));
+	}
+
+	PG_RETURN_BOOL(gbt_int_consistent_x(kkk->lower, kkk->upper,
+										queryDatum, subtype, &strategy,
+										GIST_LEAF(entry)));
 }
 
 Datum
 gbt_int8_distance(PG_FUNCTION_ARGS)
 {
 	GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
-	int64		query = PG_GETARG_INT64(1);
-#ifdef NOT_USED
+	Datum		queryDatum = PG_GETARG_DATUM(1);
 	Oid			subtype = PG_GETARG_OID(3);
-#endif
 	int64KEY   *kkk = (int64KEY *) DatumGetPointer(entry->key);
+	int64		query;
 	GBT_NUMKEY_R key;
 
 	key.lower = (GBT_NUMKEY *) &kkk->lower;
 	key.upper = (GBT_NUMKEY *) &kkk->upper;
 
-	PG_RETURN_FLOAT8(gbt_num_distance(&key, &query, GIST_LEAF(entry),
-									  &tinfo, fcinfo->flinfo));
+	if (likely(subtype == InvalidOid || subtype == INT8OID))
+	{
+		query = DatumGetInt64(queryDatum);
+		PG_RETURN_FLOAT8(gbt_num_distance(&key, &query, GIST_LEAF(entry),
+										  &tinfo, fcinfo->flinfo));
+	}
+
+	PG_RETURN_FLOAT8(gbt_int_distance_x(kkk->lower, kkk->upper,
+										queryDatum, subtype));
 }
 
 Datum
 gbt_int8_union(PG_FUNCTION_ARGS)
 {
 	GistEntryVector *entryvec = (GistEntryVector *) PG_GETARG_POINTER(0);
diff --git a/contrib/btree_gist/btree_utils_num.c b/contrib/btree_gist/btree_utils_num.c
index 3affe4c2c46..cff47e2d876 100644
--- a/contrib/btree_gist/btree_utils_num.c
+++ b/contrib/btree_gist/btree_utils_num.c
@@ -2,12 +2,14 @@
  * contrib/btree_gist/btree_utils_num.c
  */
 #include "postgres.h"
 
 #include "btree_gist.h"
 #include "btree_utils_num.h"
+#include "catalog/pg_type.h"
+#include "funcapi.h"
 #include "utils/cash.h"
 #include "utils/date.h"
 #include "utils/timestamp.h"
 
 
 GISTENTRY *
@@ -304,12 +306,144 @@ gbt_num_consistent(const GBT_NUMKEY_R *key,
 			retval = false;
 	}
 
 	return retval;
 }
 
+/*
+ * Cross-type dispatch table for the integer opclasses.
+ *
+ * This is the single source of truth for which query subtypes the integer
+ * cross-type path understands. gbt_int_query_to_int64() promotes exactly the
+ * subtypes listed here, and the SQL-visible gbt_int_crosstype_subtypes()
+ * exposes the same list so the regression tests can assert that the cross-type
+ * pg_amop entries in gist_int{2,4,8}_ops never drift from what the C code can
+ * actually handle.
+ */
+typedef int64 (*gbt_int_promote_fn) (Datum query);
+
+static int64
+gbt_int2_to_int64(Datum query)
+{
+	return (int64) DatumGetInt16(query);
+}
+
+static int64
+gbt_int4_to_int64(Datum query)
+{
+	return (int64) DatumGetInt32(query);
+}
+
+static int64
+gbt_int8_to_int64(Datum query)
+{
+	return DatumGetInt64(query);
+}
+
+typedef struct
+{
+	Oid			subtype;
+	gbt_int_promote_fn promote;
+}			gbt_int_crosstype;
+
+static const gbt_int_crosstype gbt_int_crosstype_table[] = {
+	{INT2OID, gbt_int2_to_int64},
+	{INT4OID, gbt_int4_to_int64},
+	{INT8OID, gbt_int8_to_int64},
+};
+
+/*
+ * Promote a cross-type integer query value to int64.
+ *
+ * A subtype outside gbt_int_crosstype_table means the C dispatch is out of sync
+ * with the operator-family registrations in pg_amop, so we treat it as an
+ * internal error.  The regression tests assert this never happens.
+ */
+static int64
+gbt_int_query_to_int64(Datum query, Oid subtype)
+{
+	int			i;
+
+	for (i = 0; i < lengthof(gbt_int_crosstype_table); i++)
+	{
+		if (gbt_int_crosstype_table[i].subtype == subtype)
+			return gbt_int_crosstype_table[i].promote(query);
+	}
+
+	elog(ERROR, "unrecognized subtype %u for btree_gist integer cross-type comparison",
+		 subtype);
+	return 0;					/* keep compiler quiet */
+}
+
+/*
+ * gbt_int_crosstype_subtypes
+ *
+ * Expose the contents of gbt_int_crosstype_table to SQL as a set of type OIDs.
+ * The btree_gist regression tests use this to check that the cross-type pg_amop
+ * entries in gist_int{2,4,8}_ops stay in agreement with the C dispatch.
+ */
+PG_FUNCTION_INFO_V1(gbt_int_crosstype_subtypes);
+Datum
+gbt_int_crosstype_subtypes(PG_FUNCTION_ARGS)
+{
+	FuncCallContext *funcctx;
+
+	if (SRF_IS_FIRSTCALL())
+		funcctx = SRF_FIRSTCALL_INIT();
+
+	funcctx = SRF_PERCALL_SETUP();
+
+	if (funcctx->call_cntr < lengthof(gbt_int_crosstype_table))
+	{
+		Oid			subtype = gbt_int_crosstype_table[funcctx->call_cntr].subtype;
+
+		SRF_RETURN_NEXT(funcctx, ObjectIdGetDatum(subtype));
+	}
+
+	SRF_RETURN_DONE(funcctx);
+}
+
+/*
+ * Cross-type consistent method for the integer opclasses.
+ *
+ * The key range [lower, upper] and the query value are all compared as int64.
+ * The strategy logic mirrors the same-type path in gbt_num_consistent(): the
+ * query value keeps its own width (no narrowing to the indexed column type),
+ * so out-of-range constants behave according to normal integer comparison.
+ */
+bool
+gbt_int_consistent_x(int64 lower, int64 upper, Datum query, Oid subtype,
+					 const StrategyNumber *strategy, bool is_leaf)
+{
+	int64		q = gbt_int_query_to_int64(query, subtype);
+
+	switch (*strategy)
+	{
+		case BTLessEqualStrategyNumber:
+			/* some k in [lower,upper] has k <= q iff lower <= q */
+			return lower <= q;
+		case BTLessStrategyNumber:
+			/* leaf: key < q. internal: lower <= q (loose) */
+			return is_leaf ? (lower < q) : (lower <= q);
+		case BTEqualStrategyNumber:
+			if (is_leaf)
+				return lower == q;
+			/* internal: lower <= q <= upper */
+			return (lower <= q && q <= upper);
+		case BTGreaterStrategyNumber:
+			/* leaf: key > q. internal: upper >= q (loose) */
+			return is_leaf ? (upper > q) : (upper >= q);
+		case BTGreaterEqualStrategyNumber:
+			return upper >= q;
+		case BtreeGistNotEqualStrategyNumber:
+			return !(lower == q && upper == q);
+		default:
+			return false;
+	}
+}
+
 
 /*
  * The GiST distance method (for KNN-Gist)
  */
 
 float8
@@ -331,12 +465,38 @@ gbt_num_distance(const GBT_NUMKEY_R *key,
 	else
 		retval = 0.0;
 
 	return retval;
 }
 
+/*
+ * Cross-type distance method for the integer opclasses.
+ *
+ * The distance from the query value to the key range is computed in int64 and
+ * returned as a float8, matching the same-type integer distance behaviour.
+ *
+ * Note that the subtraction is performed in float8, so when the key bound and
+ * the query value differ by more than 2^53 the returned distance loses
+ * precision. This only affects the ordering of KNN results that are nearly
+ * equidistant at that scale; it never changes which rows are returned.
+ */
+float8
+gbt_int_distance_x(int64 lower, int64 upper, Datum query, Oid subtype)
+{
+	int64		q = gbt_int_query_to_int64(query, subtype);
+
+	/* query below the range: distance to lower bound */
+	if (lower >= q)
+		return fabs((float8) lower - (float8) q);
+	/* query above the range: distance to upper bound */
+	if (upper <= q)
+		return fabs((float8) upper - (float8) q);
+	/* query inside the range */
+	return 0.0;
+}
+
 
 GIST_SPLITVEC *
 gbt_num_picksplit(const GistEntryVector *entryvec, GIST_SPLITVEC *v,
 				  const gbtree_ninfo *tinfo, FmgrInfo *flinfo)
 {
 	OffsetNumber i,
diff --git a/contrib/btree_gist/btree_utils_num.h b/contrib/btree_gist/btree_utils_num.h
index 53e477d8b1e..d378b0df37e 100644
--- a/contrib/btree_gist/btree_utils_num.h
+++ b/contrib/btree_gist/btree_utils_num.h
@@ -95,12 +95,28 @@ extern bool gbt_num_consistent(const GBT_NUMKEY_R *key, const void *query,
 							   const StrategyNumber *strategy, bool is_leaf,
 							   const gbtree_ninfo *tinfo, FmgrInfo *flinfo);
 
 extern float8 gbt_num_distance(const GBT_NUMKEY_R *key, const void *query,
 							   bool is_leaf, const gbtree_ninfo *tinfo, FmgrInfo *flinfo);
 
+/*
+ * Cross-type consistent/distance helpers for the integer opclasses
+ * (int2/int4/int8).  All three integer widths promote losslessly to int64, so
+ * a cross-type query reduces to a plain int64 comparison and there is no need
+ * for the per-type callback machinery the same-type path uses.  These helpers
+ * are deliberately integer-specific rather than a general cross-type framework;
+ * other type families should grow their own helpers shaped to their needs as
+ * the need arises.
+ */
+extern bool gbt_int_consistent_x(int64 lower, int64 upper,
+								 Datum query, Oid subtype,
+								 const StrategyNumber *strategy, bool is_leaf);
+
+extern float8 gbt_int_distance_x(int64 lower, int64 upper,
+								 Datum query, Oid subtype);
+
 extern GIST_SPLITVEC *gbt_num_picksplit(const GistEntryVector *entryvec, GIST_SPLITVEC *v,
 										const gbtree_ninfo *tinfo, FmgrInfo *flinfo);
 
 extern GISTENTRY *gbt_num_compress(GISTENTRY *entry, const gbtree_ninfo *tinfo);
 
 extern GISTENTRY *gbt_num_fetch(GISTENTRY *entry, const gbtree_ninfo *tinfo);
diff --git a/contrib/btree_gist/meson.build b/contrib/btree_gist/meson.build
index 2b1a5463289..c640cb7d8d2 100644
--- a/contrib/btree_gist/meson.build
+++ b/contrib/btree_gist/meson.build
@@ -49,12 +49,13 @@ install_data(
   'btree_gist--1.4--1.5.sql',
   'btree_gist--1.5--1.6.sql',
   'btree_gist--1.6--1.7.sql',
   'btree_gist--1.7--1.8.sql',
   'btree_gist--1.8--1.9.sql',
   'btree_gist--1.9.sql',
+  'btree_gist--1.9--1.10.sql',
   kwargs: contrib_data_args,
 )
 
 tests += {
   'name': 'btree_gist',
   'sd': meson.current_source_dir(),
@@ -89,10 +90,11 @@ tests += {
       'uuid',
       'not_equal',
       'enum',
       'bool',
       'partitions',
       'stratnum',
+      'int_crosstype',
       'without_overlaps',
     ],
   },
 }
-- 
2.51.0



  [application/octet-stream] v4-0002-Add-tests-for-cross-type-operators-for-GiST-indexes.patch (23.9K, 4-v4-0002-Add-tests-for-cross-type-operators-for-GiST-indexes.patch)
  download | inline diff:
From 138c09a5b13faac8edeb387eab26799e07ab1a07 Mon Sep 17 00:00:00 2001
From: Alexander Nestorov <[email protected]>
Date: Thu, 4 Jun 2026 00:08:27 +0200
Subject: [PATCH] Add tests for cross-type operators for GiST indexes

---
 contrib/btree_gist/expected/int_crosstype.out | 568 ++++++++++++++++++
 contrib/btree_gist/sql/int_crosstype.sql      | 236 ++++++++
 2 files changed, 804 insertions(+)
 create mode 100644 contrib/btree_gist/expected/int_crosstype.out
 create mode 100644 contrib/btree_gist/sql/int_crosstype.sql

diff --git a/contrib/btree_gist/expected/int_crosstype.out b/contrib/btree_gist/expected/int_crosstype.out
new file mode 100644
index 00000000000..bd2b90bd3f6
--- /dev/null
+++ b/contrib/btree_gist/expected/int_crosstype.out
@@ -0,0 +1,568 @@
+-- Cross-type operator support for int2/int4/int8 in GiST.
+--
+-- Verifies that (a) the cross-type B-tree-style operators registered in
+-- gist_int{2,4,8}_ops match the results of seqscans using the same operator
+-- expressions, (b) the KNN <-> operator works across types and uses the
+-- index, and (c) values outside the smaller subtype's range are handled
+-- according to normal comparison semantics, without narrowing or erroring.
+--
+-- Catalog invariant: the cross-type pg_amop entries in gist_int{2,4,8}_ops must
+-- agree, in both directions, with what the C cross-type dispatch can handle.
+-- The set of supported query subtypes is read from the C side via
+-- gbt_int_crosstype_subtypes(), so there is no hand-maintained second copy of
+-- the list here: registering a pg_amop row whose subtype the dispatch does not
+-- handle (or dropping a dispatch entry while its pg_amop rows remain, or vice
+-- versa) shows up as a diff below. Cross-type pg_amproc rows must also stay
+-- absent, since the dispatch reuses the same-type support functions.
+--
+WITH dispatch_subtypes(typ) AS (
+    SELECT s.subtype::regtype
+    FROM gbt_int_crosstype_subtypes() AS s(subtype)
+),
+gist_int_opclasses(opfamily, indextype) AS (
+    SELECT opc.opcname::text, opc.opcintype::regtype
+    FROM pg_opclass opc
+         JOIN pg_am am ON am.oid = opc.opcmethod
+    WHERE am.amname = 'gist'
+      AND opc.opcname IN ('gist_int2_ops', 'gist_int4_ops', 'gist_int8_ops')
+),
+expected_pairs(opfamily, lefttype, righttype) AS (
+    SELECT oc.opfamily, oc.indextype, ds.typ
+    FROM gist_int_opclasses oc
+         CROSS JOIN dispatch_subtypes ds
+    WHERE ds.typ <> oc.indextype
+),
+expected_amop(opfamily, lefttype, righttype, strategy, purpose) AS (
+    SELECT opfamily, lefttype, righttype, strategy, purpose
+    FROM expected_pairs
+         CROSS JOIN (VALUES
+             (1, 's'), (2, 's'), (3, 's'), (4, 's'),
+             (5, 's'), (6, 's'), (15, 'o')
+         ) AS strategy_purposes(strategy, purpose)
+),
+actual_amop AS (
+    SELECT opf.opfname::text AS opfamily,
+           amop.amoplefttype::regtype AS lefttype,
+           amop.amoprighttype::regtype AS righttype,
+           amop.amopstrategy::int AS strategy,
+           amop.amoppurpose::text AS purpose
+    FROM pg_amop amop
+         JOIN pg_opfamily opf ON opf.oid = amop.amopfamily
+         JOIN pg_am am ON am.oid = opf.opfmethod
+    WHERE am.amname = 'gist'
+      AND opf.opfname IN ('gist_int2_ops', 'gist_int4_ops', 'gist_int8_ops')
+      AND amop.amoplefttype <> amop.amoprighttype
+)
+SELECT *
+FROM (
+    SELECT 'missing from pg_amop' AS status, *
+    FROM (SELECT * FROM expected_amop EXCEPT SELECT * FROM actual_amop) missing
+    UNION ALL
+    SELECT 'unexpected in pg_amop' AS status, *
+    FROM (SELECT * FROM actual_amop EXCEPT SELECT * FROM expected_amop) unexpected
+) diff
+ORDER BY status, opfamily, lefttype::text, righttype::text, strategy, purpose;
+ status | opfamily | lefttype | righttype | strategy | purpose 
+--------+----------+----------+-----------+----------+---------
+(0 rows)
+
+SELECT opf.opfname AS opfamily,
+       amproc.amproclefttype::regtype AS lefttype,
+       amproc.amprocrighttype::regtype AS righttype,
+       amproc.amprocnum AS procnum,
+       amproc.amproc::regproc AS proc
+FROM pg_amproc amproc
+     JOIN pg_opfamily opf ON opf.oid = amproc.amprocfamily
+     JOIN pg_am am ON am.oid = opf.opfmethod
+WHERE am.amname = 'gist'
+  AND opf.opfname IN ('gist_int2_ops', 'gist_int4_ops', 'gist_int8_ops')
+  AND amproc.amproclefttype <> amproc.amprocrighttype
+ORDER BY opf.opfname,
+         amproc.amproclefttype::regtype::text,
+         amproc.amprocrighttype::regtype::text,
+         amproc.amprocnum,
+         amproc.amproc::regproc::text;
+ opfamily | lefttype | righttype | procnum | proc 
+----------+----------+-----------+---------+------
+(0 rows)
+
+CREATE TABLE ct_i2 (a int2);
+CREATE TABLE ct_i4 (a int4);
+CREATE TABLE ct_i8 (a int8);
+INSERT INTO ct_i2 SELECT g::int2 FROM generate_series(-100, 100) g;
+INSERT INTO ct_i4 SELECT g FROM generate_series(-100, 100) g;
+INSERT INTO ct_i8 SELECT g::int8 FROM generate_series(-100, 100) g;
+-- Add some values that are representable only in wider types, to exercise
+-- the path where the cross-type query constant is out of range of the key
+-- type.
+INSERT INTO ct_i4 VALUES (100000), (-100000);
+INSERT INTO ct_i8 VALUES (5000000000), (-5000000000);
+CREATE INDEX ct_i2_idx ON ct_i2 USING gist (a);
+CREATE INDEX ct_i4_idx ON ct_i4 USING gist (a);
+CREATE INDEX ct_i8_idx ON ct_i8 USING gist (a);
+ANALYZE ct_i2;
+ANALYZE ct_i4;
+ANALYZE ct_i8;
+SET enable_seqscan = off;
+SET enable_bitmapscan = off;
+-- int2 key x int4 query
+SELECT count(*) FROM ct_i2 WHERE a <  50::int4;
+ count 
+-------
+   150
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a <= 50::int4;
+ count 
+-------
+   151
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a =  50::int4;
+ count 
+-------
+     1
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a >= 50::int4;
+ count 
+-------
+    51
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a >  50::int4;
+ count 
+-------
+    50
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a <> 50::int4;
+ count 
+-------
+   200
+(1 row)
+
+-- query out of int2 range: matches nothing for =, everything for <>
+SELECT count(*) FROM ct_i2 WHERE a =  100000::int4;
+ count 
+-------
+     0
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a <> 100000::int4;
+ count 
+-------
+   201
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a <  100000::int4;
+ count 
+-------
+   201
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a >  100000::int4;
+ count 
+-------
+     0
+(1 row)
+
+-- int2 key x int8 query
+SELECT count(*) FROM ct_i2 WHERE a <  50::int8;
+ count 
+-------
+   150
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a <= 50::int8;
+ count 
+-------
+   151
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a =  50::int8;
+ count 
+-------
+     1
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a >= 50::int8;
+ count 
+-------
+    51
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a >  50::int8;
+ count 
+-------
+    50
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a <> 50::int8;
+ count 
+-------
+   200
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a =  5000000000::int8;
+ count 
+-------
+     0
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a <  5000000000::int8;
+ count 
+-------
+   201
+(1 row)
+
+-- int4 key x int2 query
+SELECT count(*) FROM ct_i4 WHERE a <  50::int2;
+ count 
+-------
+   151
+(1 row)
+
+SELECT count(*) FROM ct_i4 WHERE a <= 50::int2;
+ count 
+-------
+   152
+(1 row)
+
+SELECT count(*) FROM ct_i4 WHERE a =  50::int2;
+ count 
+-------
+     1
+(1 row)
+
+SELECT count(*) FROM ct_i4 WHERE a >= 50::int2;
+ count 
+-------
+    52
+(1 row)
+
+SELECT count(*) FROM ct_i4 WHERE a >  50::int2;
+ count 
+-------
+    51
+(1 row)
+
+SELECT count(*) FROM ct_i4 WHERE a <> 50::int2;
+ count 
+-------
+   202
+(1 row)
+
+-- int4 key x int8 query
+SELECT count(*) FROM ct_i4 WHERE a <  50::int8;
+ count 
+-------
+   151
+(1 row)
+
+SELECT count(*) FROM ct_i4 WHERE a <= 50::int8;
+ count 
+-------
+   152
+(1 row)
+
+SELECT count(*) FROM ct_i4 WHERE a =  50::int8;
+ count 
+-------
+     1
+(1 row)
+
+SELECT count(*) FROM ct_i4 WHERE a >= 50::int8;
+ count 
+-------
+    52
+(1 row)
+
+SELECT count(*) FROM ct_i4 WHERE a >  50::int8;
+ count 
+-------
+    51
+(1 row)
+
+SELECT count(*) FROM ct_i4 WHERE a <> 50::int8;
+ count 
+-------
+   202
+(1 row)
+
+SELECT count(*) FROM ct_i4 WHERE a =  5000000000::int8;
+ count 
+-------
+     0
+(1 row)
+
+SELECT count(*) FROM ct_i4 WHERE a <  5000000000::int8;
+ count 
+-------
+   203
+(1 row)
+
+-- int8 key x int2 query
+SELECT count(*) FROM ct_i8 WHERE a <  50::int2;
+ count 
+-------
+   151
+(1 row)
+
+SELECT count(*) FROM ct_i8 WHERE a <= 50::int2;
+ count 
+-------
+   152
+(1 row)
+
+SELECT count(*) FROM ct_i8 WHERE a =  50::int2;
+ count 
+-------
+     1
+(1 row)
+
+SELECT count(*) FROM ct_i8 WHERE a >= 50::int2;
+ count 
+-------
+    52
+(1 row)
+
+SELECT count(*) FROM ct_i8 WHERE a >  50::int2;
+ count 
+-------
+    51
+(1 row)
+
+SELECT count(*) FROM ct_i8 WHERE a <> 50::int2;
+ count 
+-------
+   202
+(1 row)
+
+-- int8 key x int4 query
+SELECT count(*) FROM ct_i8 WHERE a <  50::int4;
+ count 
+-------
+   151
+(1 row)
+
+SELECT count(*) FROM ct_i8 WHERE a <= 50::int4;
+ count 
+-------
+   152
+(1 row)
+
+SELECT count(*) FROM ct_i8 WHERE a =  50::int4;
+ count 
+-------
+     1
+(1 row)
+
+SELECT count(*) FROM ct_i8 WHERE a >= 50::int4;
+ count 
+-------
+    52
+(1 row)
+
+SELECT count(*) FROM ct_i8 WHERE a >  50::int4;
+ count 
+-------
+    51
+(1 row)
+
+SELECT count(*) FROM ct_i8 WHERE a <> 50::int4;
+ count 
+-------
+   202
+(1 row)
+
+-- Confirm the index is actually used for a cross-type predicate.
+EXPLAIN (COSTS OFF)
+SELECT count(*) FROM ct_i4 WHERE a = 50::int8;
+                   QUERY PLAN                   
+------------------------------------------------
+ Aggregate
+   ->  Index Only Scan using ct_i4_idx on ct_i4
+         Index Cond: (a = '50'::bigint)
+(3 rows)
+
+-- Cross-type KNN: int4 key ordered by int2 / int8 queries.
+EXPLAIN (COSTS OFF)
+SELECT a FROM ct_i4 ORDER BY a <-> '-100'::int2 LIMIT 3;
+                   QUERY PLAN                   
+------------------------------------------------
+ Limit
+   ->  Index Only Scan using ct_i4_idx on ct_i4
+         Order By: (a <-> '-100'::smallint)
+(3 rows)
+
+SELECT a FROM ct_i4 ORDER BY a <-> '-100'::int2 LIMIT 3;
+  a   
+------
+ -100
+  -99
+  -98
+(3 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT a FROM ct_i4 ORDER BY a <-> '-100'::int8 LIMIT 3;
+                   QUERY PLAN                   
+------------------------------------------------
+ Limit
+   ->  Index Only Scan using ct_i4_idx on ct_i4
+         Order By: (a <-> '-100'::bigint)
+(3 rows)
+
+SELECT a FROM ct_i4 ORDER BY a <-> '-100'::int8 LIMIT 3;
+  a   
+------
+ -100
+  -99
+  -98
+(3 rows)
+
+-- Cross-type KNN: int2 key ordered by int4 / int8 queries.
+SELECT a FROM ct_i2 ORDER BY a <-> '-100'::int4 LIMIT 3;
+  a   
+------
+ -100
+  -99
+  -98
+(3 rows)
+
+SELECT a FROM ct_i2 ORDER BY a <-> '-100'::int8 LIMIT 3;
+  a   
+------
+ -100
+  -99
+  -98
+(3 rows)
+
+-- Cross-type KNN: int8 key ordered by int2 / int4 queries.
+SELECT a FROM ct_i8 ORDER BY a <-> '-100'::int2 LIMIT 3;
+  a   
+------
+ -100
+  -99
+  -98
+(3 rows)
+
+SELECT a FROM ct_i8 ORDER BY a <-> '-100'::int4 LIMIT 3;
+  a   
+------
+ -100
+  -99
+  -98
+(3 rows)
+
+-- Combined: cross-type WHERE + cross-type ORDER BY on the same index.
+EXPLAIN (COSTS OFF)
+SELECT a FROM ct_i4 WHERE a < 80::int8 ORDER BY a <-> '-100'::int8 LIMIT 3;
+                   QUERY PLAN                   
+------------------------------------------------
+ Limit
+   ->  Index Only Scan using ct_i4_idx on ct_i4
+         Index Cond: (a < '80'::bigint)
+         Order By: (a <-> '-100'::bigint)
+(4 rows)
+
+SELECT a FROM ct_i4 WHERE a < 80::int8 ORDER BY a <-> '-100'::int8 LIMIT 3;
+  a   
+------
+ -100
+  -99
+  -98
+(3 rows)
+
+-- Standalone distance-function smoke tests (not going through the index),
+-- including the overflow-detection paths.
+SELECT int2_int4_dist(3::int2, 10::int4);
+ int2_int4_dist 
+----------------
+              7
+(1 row)
+
+SELECT int4_int2_dist(-5::int4, 5::int2);
+ int4_int2_dist 
+----------------
+             10
+(1 row)
+
+SELECT int2_int8_dist(3::int2, 10::int8);
+ int2_int8_dist 
+----------------
+              7
+(1 row)
+
+SELECT int8_int2_dist(100::int8, -5::int2);
+ int8_int2_dist 
+----------------
+            105
+(1 row)
+
+SELECT int4_int8_dist(100::int4, 5000000000::int8);
+ int4_int8_dist 
+----------------
+     4999999900
+(1 row)
+
+SELECT int8_int4_dist(5000000000::int8, 100::int4);
+ int8_int4_dist 
+----------------
+     4999999900
+(1 row)
+
+-- Overflow detection: INT32_MIN distance from a positive int2 can't fit
+-- in int32, should error.
+SELECT int2_int4_dist(1::int2, -2147483648::int4);
+ERROR:  integer out of range
+-- Likewise INT64_MIN distance from a positive int4 can't fit in int64.
+SELECT int4_int8_dist(1::int4, -9223372036854775808::int8);
+ERROR:  bigint out of range
+--
+-- Multi-column GiST index with mixed-type predicates. This is the
+-- original motivating case: without cross-type operator support the
+-- planner can only use one column as an Index Cond and applies the
+-- other(s) as a Filter post-scan. Here both columns should appear as
+-- Index Cond.
+--
+CREATE TABLE ct_multi (a int4, b int8);
+INSERT INTO ct_multi
+    SELECT g, (g * 2)::int8 FROM generate_series(-50, 50) g;
+CREATE INDEX ct_multi_idx ON ct_multi USING gist (a, b);
+ANALYZE ct_multi;
+EXPLAIN (COSTS OFF)
+SELECT count(*) FROM ct_multi WHERE a = 25::int8 AND b = 50::int4;
+                      QUERY PLAN                       
+-------------------------------------------------------
+ Aggregate
+   ->  Index Only Scan using ct_multi_idx on ct_multi
+         Index Cond: ((a = '25'::bigint) AND (b = 50))
+(3 rows)
+
+SELECT count(*) FROM ct_multi WHERE a = 25::int8 AND b = 50::int4;
+ count 
+-------
+     1
+(1 row)
+
+-- Mixed cross-type ranges across both columns.
+EXPLAIN (COSTS OFF)
+SELECT count(*) FROM ct_multi WHERE a < 10::int8 AND b > 0::int2;
+                            QUERY PLAN                            
+------------------------------------------------------------------
+ Aggregate
+   ->  Index Only Scan using ct_multi_idx on ct_multi
+         Index Cond: ((a < '10'::bigint) AND (b > '0'::smallint))
+(3 rows)
+
+SELECT count(*) FROM ct_multi WHERE a < 10::int8 AND b > 0::int2;
+ count 
+-------
+     9
+(1 row)
+
+DROP TABLE ct_multi;
+DROP TABLE ct_i2;
+DROP TABLE ct_i4;
+DROP TABLE ct_i8;
diff --git a/contrib/btree_gist/sql/int_crosstype.sql b/contrib/btree_gist/sql/int_crosstype.sql
new file mode 100644
index 00000000000..d38084f1941
--- /dev/null
+++ b/contrib/btree_gist/sql/int_crosstype.sql
@@ -0,0 +1,236 @@
+-- Cross-type operator support for int2/int4/int8 in GiST.
+--
+-- Verifies that (a) the cross-type B-tree-style operators registered in
+-- gist_int{2,4,8}_ops match the results of seqscans using the same operator
+-- expressions, (b) the KNN <-> operator works across types and uses the
+-- index, and (c) values outside the smaller subtype's range are handled
+-- according to normal comparison semantics, without narrowing or erroring.
+
+--
+-- Catalog invariant: the cross-type pg_amop entries in gist_int{2,4,8}_ops must
+-- agree, in both directions, with what the C cross-type dispatch can handle.
+-- The set of supported query subtypes is read from the C side via
+-- gbt_int_crosstype_subtypes(), so there is no hand-maintained second copy of
+-- the list here: registering a pg_amop row whose subtype the dispatch does not
+-- handle (or dropping a dispatch entry while its pg_amop rows remain, or vice
+-- versa) shows up as a diff below. Cross-type pg_amproc rows must also stay
+-- absent, since the dispatch reuses the same-type support functions.
+--
+WITH dispatch_subtypes(typ) AS (
+    SELECT s.subtype::regtype
+    FROM gbt_int_crosstype_subtypes() AS s(subtype)
+),
+gist_int_opclasses(opfamily, indextype) AS (
+    SELECT opc.opcname::text, opc.opcintype::regtype
+    FROM pg_opclass opc
+         JOIN pg_am am ON am.oid = opc.opcmethod
+    WHERE am.amname = 'gist'
+      AND opc.opcname IN ('gist_int2_ops', 'gist_int4_ops', 'gist_int8_ops')
+),
+expected_pairs(opfamily, lefttype, righttype) AS (
+    SELECT oc.opfamily, oc.indextype, ds.typ
+    FROM gist_int_opclasses oc
+         CROSS JOIN dispatch_subtypes ds
+    WHERE ds.typ <> oc.indextype
+),
+expected_amop(opfamily, lefttype, righttype, strategy, purpose) AS (
+    SELECT opfamily, lefttype, righttype, strategy, purpose
+    FROM expected_pairs
+         CROSS JOIN (VALUES
+             (1, 's'), (2, 's'), (3, 's'), (4, 's'),
+             (5, 's'), (6, 's'), (15, 'o')
+         ) AS strategy_purposes(strategy, purpose)
+),
+actual_amop AS (
+    SELECT opf.opfname::text AS opfamily,
+           amop.amoplefttype::regtype AS lefttype,
+           amop.amoprighttype::regtype AS righttype,
+           amop.amopstrategy::int AS strategy,
+           amop.amoppurpose::text AS purpose
+    FROM pg_amop amop
+         JOIN pg_opfamily opf ON opf.oid = amop.amopfamily
+         JOIN pg_am am ON am.oid = opf.opfmethod
+    WHERE am.amname = 'gist'
+      AND opf.opfname IN ('gist_int2_ops', 'gist_int4_ops', 'gist_int8_ops')
+      AND amop.amoplefttype <> amop.amoprighttype
+)
+SELECT *
+FROM (
+    SELECT 'missing from pg_amop' AS status, *
+    FROM (SELECT * FROM expected_amop EXCEPT SELECT * FROM actual_amop) missing
+    UNION ALL
+    SELECT 'unexpected in pg_amop' AS status, *
+    FROM (SELECT * FROM actual_amop EXCEPT SELECT * FROM expected_amop) unexpected
+) diff
+ORDER BY status, opfamily, lefttype::text, righttype::text, strategy, purpose;
+
+SELECT opf.opfname AS opfamily,
+       amproc.amproclefttype::regtype AS lefttype,
+       amproc.amprocrighttype::regtype AS righttype,
+       amproc.amprocnum AS procnum,
+       amproc.amproc::regproc AS proc
+FROM pg_amproc amproc
+     JOIN pg_opfamily opf ON opf.oid = amproc.amprocfamily
+     JOIN pg_am am ON am.oid = opf.opfmethod
+WHERE am.amname = 'gist'
+  AND opf.opfname IN ('gist_int2_ops', 'gist_int4_ops', 'gist_int8_ops')
+  AND amproc.amproclefttype <> amproc.amprocrighttype
+ORDER BY opf.opfname,
+         amproc.amproclefttype::regtype::text,
+         amproc.amprocrighttype::regtype::text,
+         amproc.amprocnum,
+         amproc.amproc::regproc::text;
+
+CREATE TABLE ct_i2 (a int2);
+CREATE TABLE ct_i4 (a int4);
+CREATE TABLE ct_i8 (a int8);
+
+INSERT INTO ct_i2 SELECT g::int2 FROM generate_series(-100, 100) g;
+INSERT INTO ct_i4 SELECT g FROM generate_series(-100, 100) g;
+INSERT INTO ct_i8 SELECT g::int8 FROM generate_series(-100, 100) g;
+
+-- Add some values that are representable only in wider types, to exercise
+-- the path where the cross-type query constant is out of range of the key
+-- type.
+INSERT INTO ct_i4 VALUES (100000), (-100000);
+INSERT INTO ct_i8 VALUES (5000000000), (-5000000000);
+
+CREATE INDEX ct_i2_idx ON ct_i2 USING gist (a);
+CREATE INDEX ct_i4_idx ON ct_i4 USING gist (a);
+CREATE INDEX ct_i8_idx ON ct_i8 USING gist (a);
+
+ANALYZE ct_i2;
+ANALYZE ct_i4;
+ANALYZE ct_i8;
+
+SET enable_seqscan = off;
+SET enable_bitmapscan = off;
+
+-- int2 key x int4 query
+SELECT count(*) FROM ct_i2 WHERE a <  50::int4;
+SELECT count(*) FROM ct_i2 WHERE a <= 50::int4;
+SELECT count(*) FROM ct_i2 WHERE a =  50::int4;
+SELECT count(*) FROM ct_i2 WHERE a >= 50::int4;
+SELECT count(*) FROM ct_i2 WHERE a >  50::int4;
+SELECT count(*) FROM ct_i2 WHERE a <> 50::int4;
+
+-- query out of int2 range: matches nothing for =, everything for <>
+SELECT count(*) FROM ct_i2 WHERE a =  100000::int4;
+SELECT count(*) FROM ct_i2 WHERE a <> 100000::int4;
+SELECT count(*) FROM ct_i2 WHERE a <  100000::int4;
+SELECT count(*) FROM ct_i2 WHERE a >  100000::int4;
+
+-- int2 key x int8 query
+SELECT count(*) FROM ct_i2 WHERE a <  50::int8;
+SELECT count(*) FROM ct_i2 WHERE a <= 50::int8;
+SELECT count(*) FROM ct_i2 WHERE a =  50::int8;
+SELECT count(*) FROM ct_i2 WHERE a >= 50::int8;
+SELECT count(*) FROM ct_i2 WHERE a >  50::int8;
+SELECT count(*) FROM ct_i2 WHERE a <> 50::int8;
+SELECT count(*) FROM ct_i2 WHERE a =  5000000000::int8;
+SELECT count(*) FROM ct_i2 WHERE a <  5000000000::int8;
+
+-- int4 key x int2 query
+SELECT count(*) FROM ct_i4 WHERE a <  50::int2;
+SELECT count(*) FROM ct_i4 WHERE a <= 50::int2;
+SELECT count(*) FROM ct_i4 WHERE a =  50::int2;
+SELECT count(*) FROM ct_i4 WHERE a >= 50::int2;
+SELECT count(*) FROM ct_i4 WHERE a >  50::int2;
+SELECT count(*) FROM ct_i4 WHERE a <> 50::int2;
+
+-- int4 key x int8 query
+SELECT count(*) FROM ct_i4 WHERE a <  50::int8;
+SELECT count(*) FROM ct_i4 WHERE a <= 50::int8;
+SELECT count(*) FROM ct_i4 WHERE a =  50::int8;
+SELECT count(*) FROM ct_i4 WHERE a >= 50::int8;
+SELECT count(*) FROM ct_i4 WHERE a >  50::int8;
+SELECT count(*) FROM ct_i4 WHERE a <> 50::int8;
+SELECT count(*) FROM ct_i4 WHERE a =  5000000000::int8;
+SELECT count(*) FROM ct_i4 WHERE a <  5000000000::int8;
+
+-- int8 key x int2 query
+SELECT count(*) FROM ct_i8 WHERE a <  50::int2;
+SELECT count(*) FROM ct_i8 WHERE a <= 50::int2;
+SELECT count(*) FROM ct_i8 WHERE a =  50::int2;
+SELECT count(*) FROM ct_i8 WHERE a >= 50::int2;
+SELECT count(*) FROM ct_i8 WHERE a >  50::int2;
+SELECT count(*) FROM ct_i8 WHERE a <> 50::int2;
+
+-- int8 key x int4 query
+SELECT count(*) FROM ct_i8 WHERE a <  50::int4;
+SELECT count(*) FROM ct_i8 WHERE a <= 50::int4;
+SELECT count(*) FROM ct_i8 WHERE a =  50::int4;
+SELECT count(*) FROM ct_i8 WHERE a >= 50::int4;
+SELECT count(*) FROM ct_i8 WHERE a >  50::int4;
+SELECT count(*) FROM ct_i8 WHERE a <> 50::int4;
+
+-- Confirm the index is actually used for a cross-type predicate.
+EXPLAIN (COSTS OFF)
+SELECT count(*) FROM ct_i4 WHERE a = 50::int8;
+
+-- Cross-type KNN: int4 key ordered by int2 / int8 queries.
+EXPLAIN (COSTS OFF)
+SELECT a FROM ct_i4 ORDER BY a <-> '-100'::int2 LIMIT 3;
+SELECT a FROM ct_i4 ORDER BY a <-> '-100'::int2 LIMIT 3;
+
+EXPLAIN (COSTS OFF)
+SELECT a FROM ct_i4 ORDER BY a <-> '-100'::int8 LIMIT 3;
+SELECT a FROM ct_i4 ORDER BY a <-> '-100'::int8 LIMIT 3;
+
+-- Cross-type KNN: int2 key ordered by int4 / int8 queries.
+SELECT a FROM ct_i2 ORDER BY a <-> '-100'::int4 LIMIT 3;
+SELECT a FROM ct_i2 ORDER BY a <-> '-100'::int8 LIMIT 3;
+
+-- Cross-type KNN: int8 key ordered by int2 / int4 queries.
+SELECT a FROM ct_i8 ORDER BY a <-> '-100'::int2 LIMIT 3;
+SELECT a FROM ct_i8 ORDER BY a <-> '-100'::int4 LIMIT 3;
+
+-- Combined: cross-type WHERE + cross-type ORDER BY on the same index.
+EXPLAIN (COSTS OFF)
+SELECT a FROM ct_i4 WHERE a < 80::int8 ORDER BY a <-> '-100'::int8 LIMIT 3;
+SELECT a FROM ct_i4 WHERE a < 80::int8 ORDER BY a <-> '-100'::int8 LIMIT 3;
+
+-- Standalone distance-function smoke tests (not going through the index),
+-- including the overflow-detection paths.
+SELECT int2_int4_dist(3::int2, 10::int4);
+SELECT int4_int2_dist(-5::int4, 5::int2);
+SELECT int2_int8_dist(3::int2, 10::int8);
+SELECT int8_int2_dist(100::int8, -5::int2);
+SELECT int4_int8_dist(100::int4, 5000000000::int8);
+SELECT int8_int4_dist(5000000000::int8, 100::int4);
+
+-- Overflow detection: INT32_MIN distance from a positive int2 can't fit
+-- in int32, should error.
+SELECT int2_int4_dist(1::int2, -2147483648::int4);
+-- Likewise INT64_MIN distance from a positive int4 can't fit in int64.
+SELECT int4_int8_dist(1::int4, -9223372036854775808::int8);
+
+--
+-- Multi-column GiST index with mixed-type predicates. This is the
+-- original motivating case: without cross-type operator support the
+-- planner can only use one column as an Index Cond and applies the
+-- other(s) as a Filter post-scan. Here both columns should appear as
+-- Index Cond.
+--
+CREATE TABLE ct_multi (a int4, b int8);
+INSERT INTO ct_multi
+    SELECT g, (g * 2)::int8 FROM generate_series(-50, 50) g;
+CREATE INDEX ct_multi_idx ON ct_multi USING gist (a, b);
+ANALYZE ct_multi;
+
+EXPLAIN (COSTS OFF)
+SELECT count(*) FROM ct_multi WHERE a = 25::int8 AND b = 50::int4;
+
+SELECT count(*) FROM ct_multi WHERE a = 25::int8 AND b = 50::int4;
+
+-- Mixed cross-type ranges across both columns.
+EXPLAIN (COSTS OFF)
+SELECT count(*) FROM ct_multi WHERE a < 10::int8 AND b > 0::int2;
+
+SELECT count(*) FROM ct_multi WHERE a < 10::int8 AND b > 0::int2;
+
+DROP TABLE ct_multi;
+
+DROP TABLE ct_i2;
+DROP TABLE ct_i4;
+DROP TABLE ct_i8;
-- 
2.51.0



  [application/octet-stream] v4-0003-Add-docs-for-cross-type-operators-for-GiST-indexes.patch (2.4K, 5-v4-0003-Add-docs-for-cross-type-operators-for-GiST-indexes.patch)
  download | inline diff:
From bf76a8c75ddff6253961523a9e2c03c3f71480ec Mon Sep 17 00:00:00 2001
From: Alexander Nestorov <[email protected]>
Date: Thu, 4 Jun 2026 00:09:08 +0200
Subject: [PATCH] Add docs for cross-type operators for GiST indexes

---
 doc/src/sgml/btree-gist.sgml | 24 ++++++++++++++++++++++++
 1 file changed, 24 insertions(+)

diff --git a/doc/src/sgml/btree-gist.sgml b/doc/src/sgml/btree-gist.sgml
index cc09ec83733..1b1dce707cf 100644
--- a/doc/src/sgml/btree-gist.sgml
+++ b/doc/src/sgml/btree-gist.sgml
@@ -49,12 +49,36 @@
   <type>float8</type>, <type>timestamp with time zone</type>,
   <type>timestamp without time zone</type>,
   <type>time without time zone</type>, <type>date</type>, <type>interval</type>,
   <type>oid</type>, and <type>money</type>.
  </para>
 
+ <para>
+  As of version 1.10, the operator classes for <type>int2</type>,
+  <type>int4</type> and <type>int8</type> additionally support cross-type
+  query operators. An index on any one of these integer types can be used
+  with a B-tree-style comparison (<literal>&lt;</literal>,
+  <literal>&lt;=</literal>, <literal>=</literal>, <literal>&gt;=</literal>,
+  <literal>&gt;</literal>, <literal>&lt;&gt;</literal>) or with the
+  distance operator (<literal>&lt;-&gt;</literal>) against a value of any
+  of the other two integer types, without an explicit cast. Query
+  constants outside the indexed column's range are compared according to
+  normal integer comparison semantics, without narrowing the query value
+  to the column type first.
+ </para>
+
+ <para>
+  The integer distance operator (<literal>&lt;-&gt;</literal>) used for
+  nearest-neighbor (<literal>ORDER BY</literal>) searches computes the
+  distance in <type>float8</type>. For <type>int8</type> values whose
+  distance from the query exceeds 2^53, the result loses precision and
+  may reorder results that are nearly equidistant; it never changes which
+  rows match. This applies equally to same-type and cross-type
+  <type>int8</type> distance searches.
+ </para>
+
  <para>
   By default <filename>btree_gist</filename> builds <acronym>GiST</acronym> index with
   <function>sortsupport</function> in <firstterm>sorted</firstterm> mode. This usually results in
   much faster index built speed. It is still possible to revert to buffered built strategy
   by using the <literal>buffering</literal> parameter when creating the index.
  </para>
-- 
2.51.0



  [application/octet-stream] v4-0004-Reuse-gbt_num_consistent-for-cross-type-integer-comp.patch (32.9K, 6-v4-0004-Reuse-gbt_num_consistent-for-cross-type-integer-comp.patch)
  download | inline diff:
From 4499a0f846262d6bfc8f01d9ca61ce4c41857722 Mon Sep 17 00:00:00 2001
From: Maxime Schoemans <[email protected]>
Date: Mon, 22 Jun 2026 13:31:41 +0200
Subject: [PATCH] Reuse gbt_num_consistent for cross-type integer comparisons

---
 contrib/btree_gist/btree_gist--1.9--1.10.sql  |  13 +-
 contrib/btree_gist/btree_int2.c               |  98 +++++++---
 contrib/btree_gist/btree_int4.c               |  98 +++++++---
 contrib/btree_gist/btree_int8.c               |  98 +++++++---
 contrib/btree_gist/btree_utils_num.c          | 169 +-----------------
 contrib/btree_gist/btree_utils_num.h          |  55 ++++--
 contrib/btree_gist/expected/int_crosstype.out |  63 +------
 contrib/btree_gist/sql/int_crosstype.sql      |  60 +------
 8 files changed, 289 insertions(+), 365 deletions(-)

diff --git a/contrib/btree_gist/btree_gist--1.9--1.10.sql b/contrib/btree_gist/btree_gist--1.9--1.10.sql
index c9ff1955204..9cd57455086 100644
--- a/contrib/btree_gist/btree_gist--1.9--1.10.sql
+++ b/contrib/btree_gist/btree_gist--1.9--1.10.sql
@@ -7,14 +7,14 @@
 -- to the existing GiST operator families.
 --
 -- GiST's amvalidate requires support functions in a family to have matching
 -- left/right input types, so the catalog additions below are deliberately
 -- pg_amop-only. The existing consistent/distance support functions dispatch
 -- on the subtype OID: same-type queries take the normal path, while mixed-width
--- integer queries are promoted to int64 and compared by the integer cross-type
--- helpers in btree_utils_num.c.
+-- integer queries select a cross-type comparison callback that reads the query
+-- and key sides at their own widths (see btree_int{2,4,8}.c).
 
 CREATE FUNCTION int2_int4_dist(int2, int4)
 RETURNS int4
 AS 'MODULE_PATHNAME'
 LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
 
@@ -40,21 +40,12 @@ LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
 
 CREATE FUNCTION int8_int4_dist(int8, int4)
 RETURNS int8
 AS 'MODULE_PATHNAME'
 LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
 
--- Introspection helper exposing the integer query subtypes understood by the C
--- cross-type dispatch (gbt_int_crosstype_table in btree_utils_num.c). The
--- regression tests use it to assert that the cross-type pg_amop entries below
--- never drift from what the dispatch can handle.
-CREATE FUNCTION gbt_int_crosstype_subtypes()
-RETURNS SETOF oid
-AS 'MODULE_PATHNAME'
-LANGUAGE C IMMUTABLE PARALLEL SAFE;
-
 CREATE OPERATOR <-> (
 	LEFTARG = int2,
 	RIGHTARG = int4,
 	PROCEDURE = int2_int4_dist,
 	COMMUTATOR = '<->'
 );
diff --git a/contrib/btree_gist/btree_int2.c b/contrib/btree_gist/btree_int2.c
index d00b24d9a57..ea9c206e552 100644
--- a/contrib/btree_gist/btree_int2.c
+++ b/contrib/btree_gist/btree_int2.c
@@ -88,12 +88,78 @@ static const gbtree_ninfo tinfo =
 	gbt_int2le,
 	gbt_int2lt,
 	gbt_int2key_cmp,
 	gbt_int2_dist
 };
 
+/*
+ * Cross-type GiST callbacks: the indexed key is int2, the query is int4 or
+ * int8.  Both reuse gbt_num_consistent()/gbt_num_distance() via a tinfo whose
+ * comparison/distance callbacks read the query (left) and key (right) sides at
+ * their own widths.  f_cmp is unused on these paths and left NULL.
+ */
+GBT_INT_CMP_FNS(gbt_int2_q4_, int32, int16)
+GBT_INT_CMP_FNS(gbt_int2_q8_, int64, int16)
+
+static const gbtree_ninfo tinfo_q4 =
+{
+	gbt_t_int2,
+	sizeof(int16),
+	4,
+	gbt_int2_q4_gt,
+	gbt_int2_q4_ge,
+	gbt_int2_q4_eq,
+	gbt_int2_q4_le,
+	gbt_int2_q4_lt,
+	NULL,
+	gbt_int2_q4_dist
+};
+
+static const gbtree_ninfo tinfo_q8 =
+{
+	gbt_t_int2,
+	sizeof(int16),
+	4,
+	gbt_int2_q8_gt,
+	gbt_int2_q8_ge,
+	gbt_int2_q8_eq,
+	gbt_int2_q8_le,
+	gbt_int2_q8_lt,
+	NULL,
+	gbt_int2_q8_dist
+};
+
+/*
+ * Cross-type dispatch shared by gbt_int2_consistent and gbt_int2_distance:
+ * select the tinfo for the query subtype and read the query value at its own
+ * width into caller-owned storage.
+ */
+static const gbtree_ninfo *
+gbt_int2_crosstype(Oid subtype, Datum d, gbt_intkey *q, const void **qp)
+{
+	switch (subtype)
+	{
+		case INT2OID:
+			q->i2 = DatumGetInt16(d);
+			*qp = &q->i2;
+			return &tinfo;
+		case INT4OID:
+			q->i4 = DatumGetInt32(d);
+			*qp = &q->i4;
+			return &tinfo_q4;
+		case INT8OID:
+			q->i8 = DatumGetInt64(d);
+			*qp = &q->i8;
+			return &tinfo_q8;
+		default:
+			elog(ERROR, "unrecognized subtype %u for btree_gist int2 cross-type comparison",
+				 subtype);
+			return NULL;		/* keep compiler quiet */
+	}
+}
+
 
 PG_FUNCTION_INFO_V1(int2_dist);
 Datum
 int2_dist(PG_FUNCTION_ARGS)
 {
 	int16		a = PG_GETARG_INT16(0);
@@ -173,56 +239,48 @@ gbt_int2_consistent(PG_FUNCTION_ARGS)
 	GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
 	Datum		queryDatum = PG_GETARG_DATUM(1);
 	StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2);
 	Oid			subtype = PG_GETARG_OID(3);
 	bool	   *recheck = (bool *) PG_GETARG_POINTER(4);
 	int16KEY   *kkk = (int16KEY *) DatumGetPointer(entry->key);
-	int16		query;
+	const gbtree_ninfo *ti;
+	gbt_intkey	query;
+	const void *qp;
 	GBT_NUMKEY_R key;
 
 	/* All cases served by this function are exact */
 	*recheck = false;
 
 	key.lower = (GBT_NUMKEY *) &kkk->lower;
 	key.upper = (GBT_NUMKEY *) &kkk->upper;
 
-	if (likely(subtype == InvalidOid || subtype == INT2OID))
-	{
-		query = DatumGetInt16(queryDatum);
-		PG_RETURN_BOOL(gbt_num_consistent(&key, &query, &strategy,
-										  GIST_LEAF(entry), &tinfo,
-										  fcinfo->flinfo));
-	}
+	ti = gbt_int2_crosstype(subtype, queryDatum, &query, &qp);
 
-	PG_RETURN_BOOL(gbt_int_consistent_x((int64) kkk->lower, (int64) kkk->upper,
-										queryDatum, subtype, &strategy,
-										GIST_LEAF(entry)));
+	PG_RETURN_BOOL(gbt_num_consistent(&key, qp, &strategy, GIST_LEAF(entry),
+									  ti, fcinfo->flinfo));
 }
 
 Datum
 gbt_int2_distance(PG_FUNCTION_ARGS)
 {
 	GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
 	Datum		queryDatum = PG_GETARG_DATUM(1);
 	Oid			subtype = PG_GETARG_OID(3);
 	int16KEY   *kkk = (int16KEY *) DatumGetPointer(entry->key);
-	int16		query;
+	const gbtree_ninfo *ti;
+	gbt_intkey	query;
+	const void *qp;
 	GBT_NUMKEY_R key;
 
 	key.lower = (GBT_NUMKEY *) &kkk->lower;
 	key.upper = (GBT_NUMKEY *) &kkk->upper;
 
-	if (likely(subtype == InvalidOid || subtype == INT2OID))
-	{
-		query = DatumGetInt16(queryDatum);
-		PG_RETURN_FLOAT8(gbt_num_distance(&key, &query, GIST_LEAF(entry),
-										  &tinfo, fcinfo->flinfo));
-	}
+	ti = gbt_int2_crosstype(subtype, queryDatum, &query, &qp);
 
-	PG_RETURN_FLOAT8(gbt_int_distance_x((int64) kkk->lower, (int64) kkk->upper,
-										queryDatum, subtype));
+	PG_RETURN_FLOAT8(gbt_num_distance(&key, qp, GIST_LEAF(entry),
+									  ti, fcinfo->flinfo));
 }
 
 Datum
 gbt_int2_union(PG_FUNCTION_ARGS)
 {
 	GistEntryVector *entryvec = (GistEntryVector *) PG_GETARG_POINTER(0);
diff --git a/contrib/btree_gist/btree_int4.c b/contrib/btree_gist/btree_int4.c
index 4df91f2057c..7e566890143 100644
--- a/contrib/btree_gist/btree_int4.c
+++ b/contrib/btree_gist/btree_int4.c
@@ -86,12 +86,78 @@ static const gbtree_ninfo tinfo =
 	gbt_int4le,
 	gbt_int4lt,
 	gbt_int4key_cmp,
 	gbt_int4_dist
 };
 
+/*
+ * Cross-type GiST callbacks: the indexed key is int4, the query is int2 or
+ * int8.  Both reuse gbt_num_consistent()/gbt_num_distance() via a tinfo whose
+ * comparison/distance callbacks read the query (left) and key (right) sides at
+ * their own widths.  f_cmp is unused on these paths and left NULL.
+ */
+GBT_INT_CMP_FNS(gbt_int4_q2_, int16, int32)
+GBT_INT_CMP_FNS(gbt_int4_q8_, int64, int32)
+
+static const gbtree_ninfo tinfo_q2 =
+{
+	gbt_t_int4,
+	sizeof(int32),
+	8,
+	gbt_int4_q2_gt,
+	gbt_int4_q2_ge,
+	gbt_int4_q2_eq,
+	gbt_int4_q2_le,
+	gbt_int4_q2_lt,
+	NULL,
+	gbt_int4_q2_dist
+};
+
+static const gbtree_ninfo tinfo_q8 =
+{
+	gbt_t_int4,
+	sizeof(int32),
+	8,
+	gbt_int4_q8_gt,
+	gbt_int4_q8_ge,
+	gbt_int4_q8_eq,
+	gbt_int4_q8_le,
+	gbt_int4_q8_lt,
+	NULL,
+	gbt_int4_q8_dist
+};
+
+/*
+ * Cross-type dispatch shared by gbt_int4_consistent and gbt_int4_distance:
+ * select the tinfo for the query subtype and read the query value at its own
+ * width into caller-owned storage.
+ */
+static const gbtree_ninfo *
+gbt_int4_crosstype(Oid subtype, Datum d, gbt_intkey *q, const void **qp)
+{
+	switch (subtype)
+	{
+		case INT2OID:
+			q->i2 = DatumGetInt16(d);
+			*qp = &q->i2;
+			return &tinfo_q2;
+		case INT4OID:
+			q->i4 = DatumGetInt32(d);
+			*qp = &q->i4;
+			return &tinfo;
+		case INT8OID:
+			q->i8 = DatumGetInt64(d);
+			*qp = &q->i8;
+			return &tinfo_q8;
+		default:
+			elog(ERROR, "unrecognized subtype %u for btree_gist int4 cross-type comparison",
+				 subtype);
+			return NULL;		/* keep compiler quiet */
+	}
+}
+
 
 PG_FUNCTION_INFO_V1(int4_dist);
 Datum
 int4_dist(PG_FUNCTION_ARGS)
 {
 	int32		a = PG_GETARG_INT32(0);
@@ -171,56 +237,48 @@ gbt_int4_consistent(PG_FUNCTION_ARGS)
 	GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
 	Datum		queryDatum = PG_GETARG_DATUM(1);
 	StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2);
 	Oid			subtype = PG_GETARG_OID(3);
 	bool	   *recheck = (bool *) PG_GETARG_POINTER(4);
 	int32KEY   *kkk = (int32KEY *) DatumGetPointer(entry->key);
-	int32		query;
+	const gbtree_ninfo *ti;
+	gbt_intkey	query;
+	const void *qp;
 	GBT_NUMKEY_R key;
 
 	/* All cases served by this function are exact */
 	*recheck = false;
 
 	key.lower = (GBT_NUMKEY *) &kkk->lower;
 	key.upper = (GBT_NUMKEY *) &kkk->upper;
 
-	if (likely(subtype == InvalidOid || subtype == INT4OID))
-	{
-		query = DatumGetInt32(queryDatum);
-		PG_RETURN_BOOL(gbt_num_consistent(&key, &query, &strategy,
-										  GIST_LEAF(entry), &tinfo,
-										  fcinfo->flinfo));
-	}
+	ti = gbt_int4_crosstype(subtype, queryDatum, &query, &qp);
 
-	PG_RETURN_BOOL(gbt_int_consistent_x((int64) kkk->lower, (int64) kkk->upper,
-										queryDatum, subtype, &strategy,
-										GIST_LEAF(entry)));
+	PG_RETURN_BOOL(gbt_num_consistent(&key, qp, &strategy, GIST_LEAF(entry),
+									  ti, fcinfo->flinfo));
 }
 
 Datum
 gbt_int4_distance(PG_FUNCTION_ARGS)
 {
 	GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
 	Datum		queryDatum = PG_GETARG_DATUM(1);
 	Oid			subtype = PG_GETARG_OID(3);
 	int32KEY   *kkk = (int32KEY *) DatumGetPointer(entry->key);
-	int32		query;
+	const gbtree_ninfo *ti;
+	gbt_intkey	query;
+	const void *qp;
 	GBT_NUMKEY_R key;
 
 	key.lower = (GBT_NUMKEY *) &kkk->lower;
 	key.upper = (GBT_NUMKEY *) &kkk->upper;
 
-	if (likely(subtype == InvalidOid || subtype == INT4OID))
-	{
-		query = DatumGetInt32(queryDatum);
-		PG_RETURN_FLOAT8(gbt_num_distance(&key, &query, GIST_LEAF(entry),
-										  &tinfo, fcinfo->flinfo));
-	}
+	ti = gbt_int4_crosstype(subtype, queryDatum, &query, &qp);
 
-	PG_RETURN_FLOAT8(gbt_int_distance_x((int64) kkk->lower, (int64) kkk->upper,
-										queryDatum, subtype));
+	PG_RETURN_FLOAT8(gbt_num_distance(&key, qp, GIST_LEAF(entry),
+									  ti, fcinfo->flinfo));
 }
 
 Datum
 gbt_int4_union(PG_FUNCTION_ARGS)
 {
 	GistEntryVector *entryvec = (GistEntryVector *) PG_GETARG_POINTER(0);
diff --git a/contrib/btree_gist/btree_int8.c b/contrib/btree_gist/btree_int8.c
index ae01273b709..81c8bb364cf 100644
--- a/contrib/btree_gist/btree_int8.c
+++ b/contrib/btree_gist/btree_int8.c
@@ -88,12 +88,78 @@ static const gbtree_ninfo tinfo =
 	gbt_int8le,
 	gbt_int8lt,
 	gbt_int8key_cmp,
 	gbt_int8_dist
 };
 
+/*
+ * Cross-type GiST callbacks: the indexed key is int8, the query is int2 or
+ * int4.  Both reuse gbt_num_consistent()/gbt_num_distance() via a tinfo whose
+ * comparison/distance callbacks read the query (left) and key (right) sides at
+ * their own widths.  f_cmp is unused on these paths and left NULL.
+ */
+GBT_INT_CMP_FNS(gbt_int8_q2_, int16, int64)
+GBT_INT_CMP_FNS(gbt_int8_q4_, int32, int64)
+
+static const gbtree_ninfo tinfo_q2 =
+{
+	gbt_t_int8,
+	sizeof(int64),
+	16,
+	gbt_int8_q2_gt,
+	gbt_int8_q2_ge,
+	gbt_int8_q2_eq,
+	gbt_int8_q2_le,
+	gbt_int8_q2_lt,
+	NULL,
+	gbt_int8_q2_dist
+};
+
+static const gbtree_ninfo tinfo_q4 =
+{
+	gbt_t_int8,
+	sizeof(int64),
+	16,
+	gbt_int8_q4_gt,
+	gbt_int8_q4_ge,
+	gbt_int8_q4_eq,
+	gbt_int8_q4_le,
+	gbt_int8_q4_lt,
+	NULL,
+	gbt_int8_q4_dist
+};
+
+/*
+ * Cross-type dispatch shared by gbt_int8_consistent and gbt_int8_distance:
+ * select the tinfo for the query subtype and read the query value at its own
+ * width into caller-owned storage.
+ */
+static const gbtree_ninfo *
+gbt_int8_crosstype(Oid subtype, Datum d, gbt_intkey *q, const void **qp)
+{
+	switch (subtype)
+	{
+		case INT2OID:
+			q->i2 = DatumGetInt16(d);
+			*qp = &q->i2;
+			return &tinfo_q2;
+		case INT4OID:
+			q->i4 = DatumGetInt32(d);
+			*qp = &q->i4;
+			return &tinfo_q4;
+		case INT8OID:
+			q->i8 = DatumGetInt64(d);
+			*qp = &q->i8;
+			return &tinfo;
+		default:
+			elog(ERROR, "unrecognized subtype %u for btree_gist int8 cross-type comparison",
+				 subtype);
+			return NULL;		/* keep compiler quiet */
+	}
+}
+
 
 PG_FUNCTION_INFO_V1(int8_dist);
 Datum
 int8_dist(PG_FUNCTION_ARGS)
 {
 	int64		a = PG_GETARG_INT64(0);
@@ -173,56 +239,48 @@ gbt_int8_consistent(PG_FUNCTION_ARGS)
 	GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
 	Datum		queryDatum = PG_GETARG_DATUM(1);
 	StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2);
 	Oid			subtype = PG_GETARG_OID(3);
 	bool	   *recheck = (bool *) PG_GETARG_POINTER(4);
 	int64KEY   *kkk = (int64KEY *) DatumGetPointer(entry->key);
-	int64		query;
+	const gbtree_ninfo *ti;
+	gbt_intkey	query;
+	const void *qp;
 	GBT_NUMKEY_R key;
 
 	/* All cases served by this function are exact */
 	*recheck = false;
 
 	key.lower = (GBT_NUMKEY *) &kkk->lower;
 	key.upper = (GBT_NUMKEY *) &kkk->upper;
 
-	if (likely(subtype == InvalidOid || subtype == INT8OID))
-	{
-		query = DatumGetInt64(queryDatum);
-		PG_RETURN_BOOL(gbt_num_consistent(&key, &query, &strategy,
-										  GIST_LEAF(entry), &tinfo,
-										  fcinfo->flinfo));
-	}
+	ti = gbt_int8_crosstype(subtype, queryDatum, &query, &qp);
 
-	PG_RETURN_BOOL(gbt_int_consistent_x(kkk->lower, kkk->upper,
-										queryDatum, subtype, &strategy,
-										GIST_LEAF(entry)));
+	PG_RETURN_BOOL(gbt_num_consistent(&key, qp, &strategy, GIST_LEAF(entry),
+									  ti, fcinfo->flinfo));
 }
 
 Datum
 gbt_int8_distance(PG_FUNCTION_ARGS)
 {
 	GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
 	Datum		queryDatum = PG_GETARG_DATUM(1);
 	Oid			subtype = PG_GETARG_OID(3);
 	int64KEY   *kkk = (int64KEY *) DatumGetPointer(entry->key);
-	int64		query;
+	const gbtree_ninfo *ti;
+	gbt_intkey	query;
+	const void *qp;
 	GBT_NUMKEY_R key;
 
 	key.lower = (GBT_NUMKEY *) &kkk->lower;
 	key.upper = (GBT_NUMKEY *) &kkk->upper;
 
-	if (likely(subtype == InvalidOid || subtype == INT8OID))
-	{
-		query = DatumGetInt64(queryDatum);
-		PG_RETURN_FLOAT8(gbt_num_distance(&key, &query, GIST_LEAF(entry),
-										  &tinfo, fcinfo->flinfo));
-	}
+	ti = gbt_int8_crosstype(subtype, queryDatum, &query, &qp);
 
-	PG_RETURN_FLOAT8(gbt_int_distance_x(kkk->lower, kkk->upper,
-										queryDatum, subtype));
+	PG_RETURN_FLOAT8(gbt_num_distance(&key, qp, GIST_LEAF(entry),
+									  ti, fcinfo->flinfo));
 }
 
 Datum
 gbt_int8_union(PG_FUNCTION_ARGS)
 {
 	GistEntryVector *entryvec = (GistEntryVector *) PG_GETARG_POINTER(0);
diff --git a/contrib/btree_gist/btree_utils_num.c b/contrib/btree_gist/btree_utils_num.c
index cff47e2d876..91f253d880c 100644
--- a/contrib/btree_gist/btree_utils_num.c
+++ b/contrib/btree_gist/btree_utils_num.c
@@ -2,14 +2,12 @@
  * contrib/btree_gist/btree_utils_num.c
  */
 #include "postgres.h"
 
 #include "btree_gist.h"
 #include "btree_utils_num.h"
-#include "catalog/pg_type.h"
-#include "funcapi.h"
 #include "utils/cash.h"
 #include "utils/date.h"
 #include "utils/timestamp.h"
 
 
 GISTENTRY *
@@ -268,12 +266,18 @@ gbt_num_consistent(const GBT_NUMKEY_R *key,
 				   bool is_leaf,
 				   const gbtree_ninfo *tinfo,
 				   FmgrInfo *flinfo)
 {
 	bool		retval;
 
+	/*
+	 * Every comparison callback is invoked as f_xx(query, key): the query value
+	 * is always the left argument and the indexed key bound the right.  The
+	 * integer opclasses rely on this fixed order so their cross-type callbacks
+	 * can read each side at its own width.
+	 */
 	switch (*strategy)
 	{
 		case BTLessEqualStrategyNumber:
 			retval = tinfo->f_ge(query, key->lower, flinfo);
 			break;
 		case BTLessStrategyNumber:
@@ -283,13 +287,13 @@ gbt_num_consistent(const GBT_NUMKEY_R *key,
 				retval = tinfo->f_ge(query, key->lower, flinfo);
 			break;
 		case BTEqualStrategyNumber:
 			if (is_leaf)
 				retval = tinfo->f_eq(query, key->lower, flinfo);
 			else
-				retval = (tinfo->f_le(key->lower, query, flinfo) &&
+				retval = (tinfo->f_ge(query, key->lower, flinfo) &&
 						  tinfo->f_le(query, key->upper, flinfo));
 			break;
 		case BTGreaterStrategyNumber:
 			if (is_leaf)
 				retval = tinfo->f_lt(query, key->upper, flinfo);
 			else
@@ -306,145 +310,12 @@ gbt_num_consistent(const GBT_NUMKEY_R *key,
 			retval = false;
 	}
 
 	return retval;
 }
 
-/*
- * Cross-type dispatch table for the integer opclasses.
- *
- * This is the single source of truth for which query subtypes the integer
- * cross-type path understands. gbt_int_query_to_int64() promotes exactly the
- * subtypes listed here, and the SQL-visible gbt_int_crosstype_subtypes()
- * exposes the same list so the regression tests can assert that the cross-type
- * pg_amop entries in gist_int{2,4,8}_ops never drift from what the C code can
- * actually handle.
- */
-typedef int64 (*gbt_int_promote_fn) (Datum query);
-
-static int64
-gbt_int2_to_int64(Datum query)
-{
-	return (int64) DatumGetInt16(query);
-}
-
-static int64
-gbt_int4_to_int64(Datum query)
-{
-	return (int64) DatumGetInt32(query);
-}
-
-static int64
-gbt_int8_to_int64(Datum query)
-{
-	return DatumGetInt64(query);
-}
-
-typedef struct
-{
-	Oid			subtype;
-	gbt_int_promote_fn promote;
-}			gbt_int_crosstype;
-
-static const gbt_int_crosstype gbt_int_crosstype_table[] = {
-	{INT2OID, gbt_int2_to_int64},
-	{INT4OID, gbt_int4_to_int64},
-	{INT8OID, gbt_int8_to_int64},
-};
-
-/*
- * Promote a cross-type integer query value to int64.
- *
- * A subtype outside gbt_int_crosstype_table means the C dispatch is out of sync
- * with the operator-family registrations in pg_amop, so we treat it as an
- * internal error.  The regression tests assert this never happens.
- */
-static int64
-gbt_int_query_to_int64(Datum query, Oid subtype)
-{
-	int			i;
-
-	for (i = 0; i < lengthof(gbt_int_crosstype_table); i++)
-	{
-		if (gbt_int_crosstype_table[i].subtype == subtype)
-			return gbt_int_crosstype_table[i].promote(query);
-	}
-
-	elog(ERROR, "unrecognized subtype %u for btree_gist integer cross-type comparison",
-		 subtype);
-	return 0;					/* keep compiler quiet */
-}
-
-/*
- * gbt_int_crosstype_subtypes
- *
- * Expose the contents of gbt_int_crosstype_table to SQL as a set of type OIDs.
- * The btree_gist regression tests use this to check that the cross-type pg_amop
- * entries in gist_int{2,4,8}_ops stay in agreement with the C dispatch.
- */
-PG_FUNCTION_INFO_V1(gbt_int_crosstype_subtypes);
-Datum
-gbt_int_crosstype_subtypes(PG_FUNCTION_ARGS)
-{
-	FuncCallContext *funcctx;
-
-	if (SRF_IS_FIRSTCALL())
-		funcctx = SRF_FIRSTCALL_INIT();
-
-	funcctx = SRF_PERCALL_SETUP();
-
-	if (funcctx->call_cntr < lengthof(gbt_int_crosstype_table))
-	{
-		Oid			subtype = gbt_int_crosstype_table[funcctx->call_cntr].subtype;
-
-		SRF_RETURN_NEXT(funcctx, ObjectIdGetDatum(subtype));
-	}
-
-	SRF_RETURN_DONE(funcctx);
-}
-
-/*
- * Cross-type consistent method for the integer opclasses.
- *
- * The key range [lower, upper] and the query value are all compared as int64.
- * The strategy logic mirrors the same-type path in gbt_num_consistent(): the
- * query value keeps its own width (no narrowing to the indexed column type),
- * so out-of-range constants behave according to normal integer comparison.
- */
-bool
-gbt_int_consistent_x(int64 lower, int64 upper, Datum query, Oid subtype,
-					 const StrategyNumber *strategy, bool is_leaf)
-{
-	int64		q = gbt_int_query_to_int64(query, subtype);
-
-	switch (*strategy)
-	{
-		case BTLessEqualStrategyNumber:
-			/* some k in [lower,upper] has k <= q iff lower <= q */
-			return lower <= q;
-		case BTLessStrategyNumber:
-			/* leaf: key < q. internal: lower <= q (loose) */
-			return is_leaf ? (lower < q) : (lower <= q);
-		case BTEqualStrategyNumber:
-			if (is_leaf)
-				return lower == q;
-			/* internal: lower <= q <= upper */
-			return (lower <= q && q <= upper);
-		case BTGreaterStrategyNumber:
-			/* leaf: key > q. internal: upper >= q (loose) */
-			return is_leaf ? (upper > q) : (upper >= q);
-		case BTGreaterEqualStrategyNumber:
-			return upper >= q;
-		case BtreeGistNotEqualStrategyNumber:
-			return !(lower == q && upper == q);
-		default:
-			return false;
-	}
-}
-
-
 /*
  * The GiST distance method (for KNN-Gist)
  */
 
 float8
 gbt_num_distance(const GBT_NUMKEY_R *key,
@@ -465,38 +336,12 @@ gbt_num_distance(const GBT_NUMKEY_R *key,
 	else
 		retval = 0.0;
 
 	return retval;
 }
 
-/*
- * Cross-type distance method for the integer opclasses.
- *
- * The distance from the query value to the key range is computed in int64 and
- * returned as a float8, matching the same-type integer distance behaviour.
- *
- * Note that the subtraction is performed in float8, so when the key bound and
- * the query value differ by more than 2^53 the returned distance loses
- * precision. This only affects the ordering of KNN results that are nearly
- * equidistant at that scale; it never changes which rows are returned.
- */
-float8
-gbt_int_distance_x(int64 lower, int64 upper, Datum query, Oid subtype)
-{
-	int64		q = gbt_int_query_to_int64(query, subtype);
-
-	/* query below the range: distance to lower bound */
-	if (lower >= q)
-		return fabs((float8) lower - (float8) q);
-	/* query above the range: distance to upper bound */
-	if (upper <= q)
-		return fabs((float8) upper - (float8) q);
-	/* query inside the range */
-	return 0.0;
-}
-
 
 GIST_SPLITVEC *
 gbt_num_picksplit(const GistEntryVector *entryvec, GIST_SPLITVEC *v,
 				  const gbtree_ninfo *tinfo, FmgrInfo *flinfo)
 {
 	OffsetNumber i,
diff --git a/contrib/btree_gist/btree_utils_num.h b/contrib/btree_gist/btree_utils_num.h
index d378b0df37e..be09bdb2581 100644
--- a/contrib/btree_gist/btree_utils_num.h
+++ b/contrib/btree_gist/btree_utils_num.h
@@ -24,12 +24,24 @@ typedef struct
 typedef struct
 {
 	int			i;
 	GBT_NUMKEY *t;
 } Nsrt;
 
+/*
+ * Query-value storage for the integer opclasses' cross-type path.  The caller
+ * owns one of these and passes its address to the per-type cross-type helper,
+ * which fills the right width and points the query pointer at it.
+ */
+typedef union
+{
+	int16		i2;
+	int32		i4;
+	int64		i8;
+}			gbt_intkey;
+
 
 /* type description */
 
 typedef struct
 {
 
@@ -83,40 +95,49 @@ typedef struct
  */
 #define INTERVAL_TO_SEC(ivp) \
 	(((double) (ivp)->time) / ((double) USECS_PER_SEC) + \
 	 (ivp)->day * (24.0 * SECS_PER_HOUR) + \
 	 (ivp)->month * (30.0 * SECS_PER_DAY))
 
-#define GET_FLOAT_DISTANCE(t, arg1, arg2)	fabs( ((float8) *((const t *) (arg1))) - ((float8) *((const t *) (arg2))) )
+#define GET_FLOAT_DISTANCE2(t1, t2, arg1, arg2)	fabs( ((float8) *((const t1 *) (arg1))) - ((float8) *((const t2 *) (arg2))) )
+#define GET_FLOAT_DISTANCE(t, arg1, arg2)	GET_FLOAT_DISTANCE2(t, t, (arg1), (arg2))
+
+/*
+ * Generate the comparison/distance callbacks for a gbtree_ninfo whose query
+ * and key sides may be different (integer) types.  gbt_num_consistent() and
+ * gbt_num_distance() always invoke the callbacks as f_xx(query, key), so the
+ * first argument has the query type QT (the operator's right-hand subtype) and
+ * the second has the indexed key type KT.  Integer widening is value-preserving,
+ * so the comparisons need no explicit cast; the distance widens to float8 to
+ * avoid overflow in the subtraction.  Invoked with QT == KT this also generates
+ * the ordinary same-type callbacks.
+ */
+#define GBT_INT_CMP_FNS(prefix, QT, KT) \
+static bool prefix##gt(const void *a, const void *b, FmgrInfo *flinfo) \
+{ return *((const QT *) a) > *((const KT *) b); } \
+static bool prefix##ge(const void *a, const void *b, FmgrInfo *flinfo) \
+{ return *((const QT *) a) >= *((const KT *) b); } \
+static bool prefix##eq(const void *a, const void *b, FmgrInfo *flinfo) \
+{ return *((const QT *) a) == *((const KT *) b); } \
+static bool prefix##le(const void *a, const void *b, FmgrInfo *flinfo) \
+{ return *((const QT *) a) <= *((const KT *) b); } \
+static bool prefix##lt(const void *a, const void *b, FmgrInfo *flinfo) \
+{ return *((const QT *) a) < *((const KT *) b); } \
+static float8 prefix##dist(const void *a, const void *b, FmgrInfo *flinfo) \
+{ return GET_FLOAT_DISTANCE2(QT, KT, a, b); }
 
 
 extern Interval *abs_interval(Interval *a);
 
 extern bool gbt_num_consistent(const GBT_NUMKEY_R *key, const void *query,
 							   const StrategyNumber *strategy, bool is_leaf,
 							   const gbtree_ninfo *tinfo, FmgrInfo *flinfo);
 
 extern float8 gbt_num_distance(const GBT_NUMKEY_R *key, const void *query,
 							   bool is_leaf, const gbtree_ninfo *tinfo, FmgrInfo *flinfo);
 
-/*
- * Cross-type consistent/distance helpers for the integer opclasses
- * (int2/int4/int8).  All three integer widths promote losslessly to int64, so
- * a cross-type query reduces to a plain int64 comparison and there is no need
- * for the per-type callback machinery the same-type path uses.  These helpers
- * are deliberately integer-specific rather than a general cross-type framework;
- * other type families should grow their own helpers shaped to their needs as
- * the need arises.
- */
-extern bool gbt_int_consistent_x(int64 lower, int64 upper,
-								 Datum query, Oid subtype,
-								 const StrategyNumber *strategy, bool is_leaf);
-
-extern float8 gbt_int_distance_x(int64 lower, int64 upper,
-								 Datum query, Oid subtype);
-
 extern GIST_SPLITVEC *gbt_num_picksplit(const GistEntryVector *entryvec, GIST_SPLITVEC *v,
 										const gbtree_ninfo *tinfo, FmgrInfo *flinfo);
 
 extern GISTENTRY *gbt_num_compress(GISTENTRY *entry, const gbtree_ninfo *tinfo);
 
 extern GISTENTRY *gbt_num_fetch(GISTENTRY *entry, const gbtree_ninfo *tinfo);
diff --git a/contrib/btree_gist/expected/int_crosstype.out b/contrib/btree_gist/expected/int_crosstype.out
index bd2b90bd3f6..af03cf84a40 100644
--- a/contrib/btree_gist/expected/int_crosstype.out
+++ b/contrib/btree_gist/expected/int_crosstype.out
@@ -3,72 +3,17 @@
 -- Verifies that (a) the cross-type B-tree-style operators registered in
 -- gist_int{2,4,8}_ops match the results of seqscans using the same operator
 -- expressions, (b) the KNN <-> operator works across types and uses the
 -- index, and (c) values outside the smaller subtype's range are handled
 -- according to normal comparison semantics, without narrowing or erroring.
 --
--- Catalog invariant: the cross-type pg_amop entries in gist_int{2,4,8}_ops must
--- agree, in both directions, with what the C cross-type dispatch can handle.
--- The set of supported query subtypes is read from the C side via
--- gbt_int_crosstype_subtypes(), so there is no hand-maintained second copy of
--- the list here: registering a pg_amop row whose subtype the dispatch does not
--- handle (or dropping a dispatch entry while its pg_amop rows remain, or vice
--- versa) shows up as a diff below. Cross-type pg_amproc rows must also stay
--- absent, since the dispatch reuses the same-type support functions.
+-- The integer cross-type support is handled inside the existing
+-- consistent/distance support functions (which dispatch on the subtype OID),
+-- so the operator families must not contain any cross-type (different
+-- left/right input type) support-function entries.
 --
-WITH dispatch_subtypes(typ) AS (
-    SELECT s.subtype::regtype
-    FROM gbt_int_crosstype_subtypes() AS s(subtype)
-),
-gist_int_opclasses(opfamily, indextype) AS (
-    SELECT opc.opcname::text, opc.opcintype::regtype
-    FROM pg_opclass opc
-         JOIN pg_am am ON am.oid = opc.opcmethod
-    WHERE am.amname = 'gist'
-      AND opc.opcname IN ('gist_int2_ops', 'gist_int4_ops', 'gist_int8_ops')
-),
-expected_pairs(opfamily, lefttype, righttype) AS (
-    SELECT oc.opfamily, oc.indextype, ds.typ
-    FROM gist_int_opclasses oc
-         CROSS JOIN dispatch_subtypes ds
-    WHERE ds.typ <> oc.indextype
-),
-expected_amop(opfamily, lefttype, righttype, strategy, purpose) AS (
-    SELECT opfamily, lefttype, righttype, strategy, purpose
-    FROM expected_pairs
-         CROSS JOIN (VALUES
-             (1, 's'), (2, 's'), (3, 's'), (4, 's'),
-             (5, 's'), (6, 's'), (15, 'o')
-         ) AS strategy_purposes(strategy, purpose)
-),
-actual_amop AS (
-    SELECT opf.opfname::text AS opfamily,
-           amop.amoplefttype::regtype AS lefttype,
-           amop.amoprighttype::regtype AS righttype,
-           amop.amopstrategy::int AS strategy,
-           amop.amoppurpose::text AS purpose
-    FROM pg_amop amop
-         JOIN pg_opfamily opf ON opf.oid = amop.amopfamily
-         JOIN pg_am am ON am.oid = opf.opfmethod
-    WHERE am.amname = 'gist'
-      AND opf.opfname IN ('gist_int2_ops', 'gist_int4_ops', 'gist_int8_ops')
-      AND amop.amoplefttype <> amop.amoprighttype
-)
-SELECT *
-FROM (
-    SELECT 'missing from pg_amop' AS status, *
-    FROM (SELECT * FROM expected_amop EXCEPT SELECT * FROM actual_amop) missing
-    UNION ALL
-    SELECT 'unexpected in pg_amop' AS status, *
-    FROM (SELECT * FROM actual_amop EXCEPT SELECT * FROM expected_amop) unexpected
-) diff
-ORDER BY status, opfamily, lefttype::text, righttype::text, strategy, purpose;
- status | opfamily | lefttype | righttype | strategy | purpose 
---------+----------+----------+-----------+----------+---------
-(0 rows)
-
 SELECT opf.opfname AS opfamily,
        amproc.amproclefttype::regtype AS lefttype,
        amproc.amprocrighttype::regtype AS righttype,
        amproc.amprocnum AS procnum,
        amproc.amproc::regproc AS proc
 FROM pg_amproc amproc
diff --git a/contrib/btree_gist/sql/int_crosstype.sql b/contrib/btree_gist/sql/int_crosstype.sql
index d38084f1941..b337e26a1af 100644
--- a/contrib/btree_gist/sql/int_crosstype.sql
+++ b/contrib/btree_gist/sql/int_crosstype.sql
@@ -4,69 +4,17 @@
 -- gist_int{2,4,8}_ops match the results of seqscans using the same operator
 -- expressions, (b) the KNN <-> operator works across types and uses the
 -- index, and (c) values outside the smaller subtype's range are handled
 -- according to normal comparison semantics, without narrowing or erroring.
 
 --
--- Catalog invariant: the cross-type pg_amop entries in gist_int{2,4,8}_ops must
--- agree, in both directions, with what the C cross-type dispatch can handle.
--- The set of supported query subtypes is read from the C side via
--- gbt_int_crosstype_subtypes(), so there is no hand-maintained second copy of
--- the list here: registering a pg_amop row whose subtype the dispatch does not
--- handle (or dropping a dispatch entry while its pg_amop rows remain, or vice
--- versa) shows up as a diff below. Cross-type pg_amproc rows must also stay
--- absent, since the dispatch reuses the same-type support functions.
+-- The integer cross-type support is handled inside the existing
+-- consistent/distance support functions (which dispatch on the subtype OID),
+-- so the operator families must not contain any cross-type (different
+-- left/right input type) support-function entries.
 --
-WITH dispatch_subtypes(typ) AS (
-    SELECT s.subtype::regtype
-    FROM gbt_int_crosstype_subtypes() AS s(subtype)
-),
-gist_int_opclasses(opfamily, indextype) AS (
-    SELECT opc.opcname::text, opc.opcintype::regtype
-    FROM pg_opclass opc
-         JOIN pg_am am ON am.oid = opc.opcmethod
-    WHERE am.amname = 'gist'
-      AND opc.opcname IN ('gist_int2_ops', 'gist_int4_ops', 'gist_int8_ops')
-),
-expected_pairs(opfamily, lefttype, righttype) AS (
-    SELECT oc.opfamily, oc.indextype, ds.typ
-    FROM gist_int_opclasses oc
-         CROSS JOIN dispatch_subtypes ds
-    WHERE ds.typ <> oc.indextype
-),
-expected_amop(opfamily, lefttype, righttype, strategy, purpose) AS (
-    SELECT opfamily, lefttype, righttype, strategy, purpose
-    FROM expected_pairs
-         CROSS JOIN (VALUES
-             (1, 's'), (2, 's'), (3, 's'), (4, 's'),
-             (5, 's'), (6, 's'), (15, 'o')
-         ) AS strategy_purposes(strategy, purpose)
-),
-actual_amop AS (
-    SELECT opf.opfname::text AS opfamily,
-           amop.amoplefttype::regtype AS lefttype,
-           amop.amoprighttype::regtype AS righttype,
-           amop.amopstrategy::int AS strategy,
-           amop.amoppurpose::text AS purpose
-    FROM pg_amop amop
-         JOIN pg_opfamily opf ON opf.oid = amop.amopfamily
-         JOIN pg_am am ON am.oid = opf.opfmethod
-    WHERE am.amname = 'gist'
-      AND opf.opfname IN ('gist_int2_ops', 'gist_int4_ops', 'gist_int8_ops')
-      AND amop.amoplefttype <> amop.amoprighttype
-)
-SELECT *
-FROM (
-    SELECT 'missing from pg_amop' AS status, *
-    FROM (SELECT * FROM expected_amop EXCEPT SELECT * FROM actual_amop) missing
-    UNION ALL
-    SELECT 'unexpected in pg_amop' AS status, *
-    FROM (SELECT * FROM actual_amop EXCEPT SELECT * FROM expected_amop) unexpected
-) diff
-ORDER BY status, opfamily, lefttype::text, righttype::text, strategy, purpose;
-
 SELECT opf.opfname AS opfamily,
        amproc.amproclefttype::regtype AS lefttype,
        amproc.amprocrighttype::regtype AS righttype,
        amproc.amprocnum AS procnum,
        amproc.amproc::regproc AS proc
 FROM pg_amproc amproc
-- 
2.51.0



  [application/octet-stream] v4-0005-Safe-guard-InvalidOid.patch (4.4K, 7-v4-0005-Safe-guard-InvalidOid.patch)
  download | inline diff:
From 5e1791ee0dae54a2e87eab0ae0832aa5cf19bbe5 Mon Sep 17 00:00:00 2001
From: Alexander Nestorov <[email protected]>
Date: Tue, 23 Jun 2026 23:07:21 +0200
Subject: [PATCH] Safe-guard InvalidOid

---
 contrib/btree_gist/btree_int2.c      | 2 ++
 contrib/btree_gist/btree_int4.c      | 2 ++
 contrib/btree_gist/btree_int8.c      | 2 ++
 contrib/btree_gist/btree_utils_num.c | 8 ++++----
 contrib/btree_gist/btree_utils_num.h | 2 +-
 src/tools/pgindent/typedefs.list     | 1 +
 6 files changed, 12 insertions(+), 5 deletions(-)

diff --git a/contrib/btree_gist/btree_int2.c b/contrib/btree_gist/btree_int2.c
index ea9c206e552..8d644579b23 100644
--- a/contrib/btree_gist/btree_int2.c
+++ b/contrib/btree_gist/btree_int2.c
@@ -135,12 +135,14 @@ static const gbtree_ninfo tinfo_q8 =
  */
 static const gbtree_ninfo *
 gbt_int2_crosstype(Oid subtype, Datum d, gbt_intkey *q, const void **qp)
 {
 	switch (subtype)
 	{
+		case InvalidOid:		/* same-type: exclusion/temporal constraint
+								 * checks pass the native type with subtype 0 */
 		case INT2OID:
 			q->i2 = DatumGetInt16(d);
 			*qp = &q->i2;
 			return &tinfo;
 		case INT4OID:
 			q->i4 = DatumGetInt32(d);
diff --git a/contrib/btree_gist/btree_int4.c b/contrib/btree_gist/btree_int4.c
index 7e566890143..325ee78f2ab 100644
--- a/contrib/btree_gist/btree_int4.c
+++ b/contrib/btree_gist/btree_int4.c
@@ -137,12 +137,14 @@ gbt_int4_crosstype(Oid subtype, Datum d, gbt_intkey *q, const void **qp)
 	switch (subtype)
 	{
 		case INT2OID:
 			q->i2 = DatumGetInt16(d);
 			*qp = &q->i2;
 			return &tinfo_q2;
+		case InvalidOid:		/* same-type: exclusion/temporal constraint
+								 * checks pass the native type with subtype 0 */
 		case INT4OID:
 			q->i4 = DatumGetInt32(d);
 			*qp = &q->i4;
 			return &tinfo;
 		case INT8OID:
 			q->i8 = DatumGetInt64(d);
diff --git a/contrib/btree_gist/btree_int8.c b/contrib/btree_gist/btree_int8.c
index 81c8bb364cf..62c06345e87 100644
--- a/contrib/btree_gist/btree_int8.c
+++ b/contrib/btree_gist/btree_int8.c
@@ -143,12 +143,14 @@ gbt_int8_crosstype(Oid subtype, Datum d, gbt_intkey *q, const void **qp)
 			*qp = &q->i2;
 			return &tinfo_q2;
 		case INT4OID:
 			q->i4 = DatumGetInt32(d);
 			*qp = &q->i4;
 			return &tinfo_q4;
+		case InvalidOid:		/* same-type: exclusion/temporal constraint
+								 * checks pass the native type with subtype 0 */
 		case INT8OID:
 			q->i8 = DatumGetInt64(d);
 			*qp = &q->i8;
 			return &tinfo;
 		default:
 			elog(ERROR, "unrecognized subtype %u for btree_gist int8 cross-type comparison",
diff --git a/contrib/btree_gist/btree_utils_num.c b/contrib/btree_gist/btree_utils_num.c
index 91f253d880c..145db8154c5 100644
--- a/contrib/btree_gist/btree_utils_num.c
+++ b/contrib/btree_gist/btree_utils_num.c
@@ -267,16 +267,16 @@ gbt_num_consistent(const GBT_NUMKEY_R *key,
 				   const gbtree_ninfo *tinfo,
 				   FmgrInfo *flinfo)
 {
 	bool		retval;
 
 	/*
-	 * Every comparison callback is invoked as f_xx(query, key): the query value
-	 * is always the left argument and the indexed key bound the right.  The
-	 * integer opclasses rely on this fixed order so their cross-type callbacks
-	 * can read each side at its own width.
+	 * Every comparison callback is invoked as f_xx(query, key): the query
+	 * value is always the left argument and the indexed key bound the right.
+	 * The integer opclasses rely on this fixed order so their cross-type
+	 * callbacks can read each side at its own width.
 	 */
 	switch (*strategy)
 	{
 		case BTLessEqualStrategyNumber:
 			retval = tinfo->f_ge(query, key->lower, flinfo);
 			break;
diff --git a/contrib/btree_gist/btree_utils_num.h b/contrib/btree_gist/btree_utils_num.h
index be09bdb2581..217b362c169 100644
--- a/contrib/btree_gist/btree_utils_num.h
+++ b/contrib/btree_gist/btree_utils_num.h
@@ -34,13 +34,13 @@ typedef struct
  */
 typedef union
 {
 	int16		i2;
 	int32		i4;
 	int64		i8;
-}			gbt_intkey;
+} gbt_intkey;
 
 
 /* type description */
 
 typedef struct
 {
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index c5db6ca6705..e29f76b6feb 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3808,12 +3808,13 @@ floating_decimal_32
 floating_decimal_64
 fmgr_hook_type
 foreign_glob_cxt
 foreign_loc_cxt
 freefunc
 fsec_t
+gbt_intkey
 gbt_vsrt_arg
 gbtree_ninfo
 gbtree_vinfo
 generate_series_fctx
 generate_series_numeric_fctx
 generate_series_timestamp_fctx
-- 
2.51.0



^ permalink  raw  reply  [nested|flat] 9+ messages in thread

* Re: [PATCH] btree_gist: add cross-type integer operator support for GiST
@ 2026-06-24 10:41  Maxime Schoemans <[email protected]>
  parent: Alexander Nestorov <[email protected]>
  0 siblings, 1 reply; 9+ messages in thread

From: Maxime Schoemans @ 2026-06-24 10:41 UTC (permalink / raw)
  To: Alexander Nestorov <[email protected]>; +Cc: Andrey Borodin <[email protected]>; pgsql-hackers

Hi Alexander,

> I think we still need to check `InvalidOid` because execIndexing.c:~800
> initializes scan keys ('ScanKeyEntryInitialize') with `InvalidOid` as the
> subtype. Running the 'not_equal', 'partitions' and 'without_overlaps' tests
> confirms that 'InvalidOid' must be handled. I can add the check again in the
> switch. (attaching patch)

You are completely right, I missed this while reading the source code.

> I completely agree, your patch makes it much cleaner.
> I applied your patch (with the 'InvalidOid' fix) on top of mine and ran all the
> tests and benchmarks that I prepared previously. [...]

I'm glad you like the suggestion, and thank you for the pgindent
fixes that I missed.

Since this implementation now replaces the initial one in 0001, it
might be worth squashing 0001 and 0004 (and the 0005 fixes) into a
single 0001 patch, unless you want to keep the history of this work
in the patch series.

> I added one more commit that adds a small description of this fix in the docs.

This patch also looks good to me, although I may not be the best
person to review doc changes.

One last tiny thing: 0001 adds int_crosstype to the regress tests
but the test file is only added in 0002, so 0001 on its own fails
`make check`. Might be worth moving the Makefile/meson.build
registration into 0002 so each patch passes its own tests.

I think that one additional thing needed before it's ready would be
to add commit messages to each patch.

Other than that it looks good, I don't have any additional code
comments.

Best,
Maxime







^ permalink  raw  reply  [nested|flat] 9+ messages in thread

* Re: [PATCH] btree_gist: add cross-type integer operator support for GiST
@ 2026-06-24 15:34  Alexander Nestorov <[email protected]>
  parent: Maxime Schoemans <[email protected]>
  0 siblings, 0 replies; 9+ messages in thread

From: Alexander Nestorov @ 2026-06-24 15:34 UTC (permalink / raw)
  To: Maxime Schoemans <[email protected]>; +Cc: Andrey Borodin <[email protected]>; pgsql-hackers

Hey Maxime!

I squashed your code-related changes on top of 0001, and the test-related changes into 0002.
I also moved my test-related changes (Makefile / meson.build) from 0001 to 0002.
I squashed the remaining fixes (the InvalidOid and the pgindent; 0005) into 0001 as well, so now we have:

- 0001 - code-related changes
- 0002 - test-related changes
- 0003 - docs changes

I also added one more test to 0002 ("Width-sensitivity guard") and added the commit messages.
Last but not least, I preserved your changes as co-author in 0001 and 0002.

I hope I didn't miss anything!

Best regards!


Attachments:

  [application/octet-stream] v5-0001-Implement-cross-type-operators-for-GiST-indexes.patch (30.4K, 3-v5-0001-Implement-cross-type-operators-for-GiST-indexes.patch)
  download | inline diff:
From 3800bca3c6be27f0077e5cb5232e56af1b40dc53 Mon Sep 17 00:00:00 2001
From: Alexander Nestorov <[email protected]>
Date: Thu, 4 Jun 2026 00:06:54 +0200
Subject: [PATCH] Implement cross-type operators for GiST indexes

btree_gist's GiST opclasses were same-type only: the planner could use
an index only when the query value's type exactly matched the indexed
column. A common case like an int8 column compared against an int4
literal (WHERE bigint_col = 42) therefore could not use the index
unless the query was written with an explicit cast, which is easy to
forget and which ORMs and parameter binding routinely get wrong.

Add cross-type query operator support among the integer trio (int2,
int4, int8) to the gist_int2_ops, gist_int4_ops and gist_int8_ops
families. Each family gains the six B-tree comparison strategies
(<, <=, =, >=, >, <>) and the <-> distance operator against the other
two integer types.

The additions are deliberately pg_amop-only: GiST's amvalidate requires
support functions in a family to have matching left/right input types,
so no cross-type consistent/distance functions are registered. Instead
the existing support functions dispatch on the operator's subtype OID.
Same-type queries take the normal path; cross-type queries select a
comparison/distance callback that reads the query and key sides at their
own widths, so out-of-range query constants compare by normal integer
semantics without being narrowed to the column type.

To let the cross-type path reuse gbt_num_consistent(), all comparison
callbacks are now invoked as f(query, key): query on the left, key on
the right; one call site whose order differed is normalized. Note that
the subtype is InvalidOid for exclusion-constraint and temporal PK/FK
checks, which is handled as the same-type case.

This is exposed as btree_gist version 1.10.

Co-authored-by: Maxime Schoemans <[email protected]>
---
 contrib/btree_gist/Makefile                  |   2 +-
 contrib/btree_gist/btree_gist--1.9--1.10.sql | 134 +++++++++++++++++++
 contrib/btree_gist/btree_gist.control        |   2 +-
 contrib/btree_gist/btree_int2.c              | 130 ++++++++++++++++--
 contrib/btree_gist/btree_int4.c              | 130 ++++++++++++++++--
 contrib/btree_gist/btree_int8.c              | 130 ++++++++++++++++--
 contrib/btree_gist/btree_utils_num.c         |  16 ++-
 contrib/btree_gist/btree_utils_num.h         |  39 +++++-
 contrib/btree_gist/meson.build               |   1 +
 src/tools/pgindent/typedefs.list             |   1 +
 10 files changed, 547 insertions(+), 38 deletions(-)
 create mode 100644 contrib/btree_gist/btree_gist--1.9--1.10.sql

diff --git a/contrib/btree_gist/Makefile b/contrib/btree_gist/Makefile
index fbbbca95598..380c7642fde 100644
--- a/contrib/btree_gist/Makefile
+++ b/contrib/btree_gist/Makefile
@@ -32,13 +32,13 @@ OBJS =  \
 EXTENSION = btree_gist
 DATA = btree_gist--1.0--1.1.sql \
        btree_gist--1.1--1.2.sql btree_gist--1.2--1.3.sql \
        btree_gist--1.3--1.4.sql btree_gist--1.4--1.5.sql \
        btree_gist--1.5--1.6.sql btree_gist--1.6--1.7.sql \
        btree_gist--1.7--1.8.sql btree_gist--1.8--1.9.sql \
-       btree_gist--1.9.sql
+       btree_gist--1.9.sql btree_gist--1.9--1.10.sql
 PGFILEDESC = "btree_gist - B-tree equivalent GiST operator classes"
 
 REGRESS = init int2 int4 int8 float4 float8 cash oid timestamp timestamptz \
         time timetz date interval macaddr macaddr8 inet cidr text varchar char \
         bytea bit varbit numeric uuid not_equal enum bool partitions \
         stratnum without_overlaps
diff --git a/contrib/btree_gist/btree_gist--1.9--1.10.sql b/contrib/btree_gist/btree_gist--1.9--1.10.sql
new file mode 100644
index 00000000000..9cd57455086
--- /dev/null
+++ b/contrib/btree_gist/btree_gist--1.9--1.10.sql
@@ -0,0 +1,134 @@
+/* contrib/btree_gist/btree_gist--1.9--1.10.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION btree_gist UPDATE TO '1.10'" to load this file. \quit
+
+-- Add cross-type operator support for the integer trio (int2, int4, int8)
+-- to the existing GiST operator families.
+--
+-- GiST's amvalidate requires support functions in a family to have matching
+-- left/right input types, so the catalog additions below are deliberately
+-- pg_amop-only. The existing consistent/distance support functions dispatch
+-- on the subtype OID: same-type queries take the normal path, while mixed-width
+-- integer queries select a cross-type comparison callback that reads the query
+-- and key sides at their own widths (see btree_int{2,4,8}.c).
+
+CREATE FUNCTION int2_int4_dist(int2, int4)
+RETURNS int4
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION int4_int2_dist(int4, int2)
+RETURNS int4
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION int2_int8_dist(int2, int8)
+RETURNS int8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION int8_int2_dist(int8, int2)
+RETURNS int8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION int4_int8_dist(int4, int8)
+RETURNS int8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION int8_int4_dist(int8, int4)
+RETURNS int8
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE OPERATOR <-> (
+	LEFTARG = int2,
+	RIGHTARG = int4,
+	PROCEDURE = int2_int4_dist,
+	COMMUTATOR = '<->'
+);
+
+CREATE OPERATOR <-> (
+	LEFTARG = int4,
+	RIGHTARG = int2,
+	PROCEDURE = int4_int2_dist,
+	COMMUTATOR = '<->'
+);
+
+CREATE OPERATOR <-> (
+	LEFTARG = int2,
+	RIGHTARG = int8,
+	PROCEDURE = int2_int8_dist,
+	COMMUTATOR = '<->'
+);
+
+CREATE OPERATOR <-> (
+	LEFTARG = int8,
+	RIGHTARG = int2,
+	PROCEDURE = int8_int2_dist,
+	COMMUTATOR = '<->'
+);
+
+CREATE OPERATOR <-> (
+	LEFTARG = int4,
+	RIGHTARG = int8,
+	PROCEDURE = int4_int8_dist,
+	COMMUTATOR = '<->'
+);
+
+CREATE OPERATOR <-> (
+	LEFTARG = int8,
+	RIGHTARG = int4,
+	PROCEDURE = int8_int4_dist,
+	COMMUTATOR = '<->'
+);
+
+ALTER OPERATOR FAMILY gist_int2_ops USING gist ADD
+	OPERATOR	1	<  (int2, int4),
+	OPERATOR	2	<= (int2, int4),
+	OPERATOR	3	=  (int2, int4),
+	OPERATOR	4	>= (int2, int4),
+	OPERATOR	5	>  (int2, int4),
+	OPERATOR	6	<> (int2, int4),
+	OPERATOR	15	<-> (int2, int4) FOR ORDER BY pg_catalog.integer_ops,
+	OPERATOR	1	<  (int2, int8),
+	OPERATOR	2	<= (int2, int8),
+	OPERATOR	3	=  (int2, int8),
+	OPERATOR	4	>= (int2, int8),
+	OPERATOR	5	>  (int2, int8),
+	OPERATOR	6	<> (int2, int8),
+	OPERATOR	15	<-> (int2, int8) FOR ORDER BY pg_catalog.integer_ops;
+
+ALTER OPERATOR FAMILY gist_int4_ops USING gist ADD
+	OPERATOR	1	<  (int4, int2),
+	OPERATOR	2	<= (int4, int2),
+	OPERATOR	3	=  (int4, int2),
+	OPERATOR	4	>= (int4, int2),
+	OPERATOR	5	>  (int4, int2),
+	OPERATOR	6	<> (int4, int2),
+	OPERATOR	15	<-> (int4, int2) FOR ORDER BY pg_catalog.integer_ops,
+	OPERATOR	1	<  (int4, int8),
+	OPERATOR	2	<= (int4, int8),
+	OPERATOR	3	=  (int4, int8),
+	OPERATOR	4	>= (int4, int8),
+	OPERATOR	5	>  (int4, int8),
+	OPERATOR	6	<> (int4, int8),
+	OPERATOR	15	<-> (int4, int8) FOR ORDER BY pg_catalog.integer_ops;
+
+ALTER OPERATOR FAMILY gist_int8_ops USING gist ADD
+	OPERATOR	1	<  (int8, int2),
+	OPERATOR	2	<= (int8, int2),
+	OPERATOR	3	=  (int8, int2),
+	OPERATOR	4	>= (int8, int2),
+	OPERATOR	5	>  (int8, int2),
+	OPERATOR	6	<> (int8, int2),
+	OPERATOR	15	<-> (int8, int2) FOR ORDER BY pg_catalog.integer_ops,
+	OPERATOR	1	<  (int8, int4),
+	OPERATOR	2	<= (int8, int4),
+	OPERATOR	3	=  (int8, int4),
+	OPERATOR	4	>= (int8, int4),
+	OPERATOR	5	>  (int8, int4),
+	OPERATOR	6	<> (int8, int4),
+	OPERATOR	15	<-> (int8, int4) FOR ORDER BY pg_catalog.integer_ops;
diff --git a/contrib/btree_gist/btree_gist.control b/contrib/btree_gist/btree_gist.control
index 69d9341a0ad..e606fa6551d 100644
--- a/contrib/btree_gist/btree_gist.control
+++ b/contrib/btree_gist/btree_gist.control
@@ -1,6 +1,6 @@
 # btree_gist extension
 comment = 'support for indexing common datatypes in GiST'
-default_version = '1.9'
+default_version = '1.10'
 module_pathname = '$libdir/btree_gist'
 relocatable = true
 trusted = true
diff --git a/contrib/btree_gist/btree_int2.c b/contrib/btree_gist/btree_int2.c
index cc4b33177e3..8d644579b23 100644
--- a/contrib/btree_gist/btree_int2.c
+++ b/contrib/btree_gist/btree_int2.c
@@ -2,12 +2,13 @@
  * contrib/btree_gist/btree_int2.c
  */
 #include "postgres.h"
 
 #include "btree_gist.h"
 #include "btree_utils_num.h"
+#include "catalog/pg_type.h"
 #include "common/int.h"
 #include "utils/rel.h"
 #include "utils/sortsupport.h"
 
 typedef struct int16key
 {
@@ -73,13 +74,12 @@ gbt_int2key_cmp(const void *a, const void *b, FmgrInfo *flinfo)
 static float8
 gbt_int2_dist(const void *a, const void *b, FmgrInfo *flinfo)
 {
 	return GET_FLOAT_DISTANCE(int16, a, b);
 }
 
-
 static const gbtree_ninfo tinfo =
 {
 	gbt_t_int2,
 	sizeof(int16),
 	4,							/* sizeof(gbtreekey4) */
 	gbt_int2gt,
@@ -88,12 +88,80 @@ static const gbtree_ninfo tinfo =
 	gbt_int2le,
 	gbt_int2lt,
 	gbt_int2key_cmp,
 	gbt_int2_dist
 };
 
+/*
+ * Cross-type GiST callbacks: the indexed key is int2, the query is int4 or
+ * int8.  Both reuse gbt_num_consistent()/gbt_num_distance() via a tinfo whose
+ * comparison/distance callbacks read the query (left) and key (right) sides at
+ * their own widths.  f_cmp is unused on these paths and left NULL.
+ */
+GBT_INT_CMP_FNS(gbt_int2_q4_, int32, int16)
+GBT_INT_CMP_FNS(gbt_int2_q8_, int64, int16)
+
+static const gbtree_ninfo tinfo_q4 =
+{
+	gbt_t_int2,
+	sizeof(int16),
+	4,
+	gbt_int2_q4_gt,
+	gbt_int2_q4_ge,
+	gbt_int2_q4_eq,
+	gbt_int2_q4_le,
+	gbt_int2_q4_lt,
+	NULL,
+	gbt_int2_q4_dist
+};
+
+static const gbtree_ninfo tinfo_q8 =
+{
+	gbt_t_int2,
+	sizeof(int16),
+	4,
+	gbt_int2_q8_gt,
+	gbt_int2_q8_ge,
+	gbt_int2_q8_eq,
+	gbt_int2_q8_le,
+	gbt_int2_q8_lt,
+	NULL,
+	gbt_int2_q8_dist
+};
+
+/*
+ * Cross-type dispatch shared by gbt_int2_consistent and gbt_int2_distance:
+ * select the tinfo for the query subtype and read the query value at its own
+ * width into caller-owned storage.
+ */
+static const gbtree_ninfo *
+gbt_int2_crosstype(Oid subtype, Datum d, gbt_intkey *q, const void **qp)
+{
+	switch (subtype)
+	{
+		case InvalidOid:		/* same-type: exclusion/temporal constraint
+								 * checks pass the native type with subtype 0 */
+		case INT2OID:
+			q->i2 = DatumGetInt16(d);
+			*qp = &q->i2;
+			return &tinfo;
+		case INT4OID:
+			q->i4 = DatumGetInt32(d);
+			*qp = &q->i4;
+			return &tinfo_q4;
+		case INT8OID:
+			q->i8 = DatumGetInt64(d);
+			*qp = &q->i8;
+			return &tinfo_q8;
+		default:
+			elog(ERROR, "unrecognized subtype %u for btree_gist int2 cross-type comparison",
+				 subtype);
+			return NULL;		/* keep compiler quiet */
+	}
+}
+
 
 PG_FUNCTION_INFO_V1(int2_dist);
 Datum
 int2_dist(PG_FUNCTION_ARGS)
 {
 	int16		a = PG_GETARG_INT16(0);
@@ -109,12 +177,46 @@ int2_dist(PG_FUNCTION_ARGS)
 
 	ra = abs(r);
 
 	PG_RETURN_INT16(ra);
 }
 
+PG_FUNCTION_INFO_V1(int2_int4_dist);
+Datum
+int2_int4_dist(PG_FUNCTION_ARGS)
+{
+	int32		a = (int32) PG_GETARG_INT16(0);
+	int32		b = PG_GETARG_INT32(1);
+	int32		r;
+
+	if (pg_sub_s32_overflow(a, b, &r) ||
+		r == PG_INT32_MIN)
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("integer out of range")));
+
+	PG_RETURN_INT32(abs(r));
+}
+
+PG_FUNCTION_INFO_V1(int2_int8_dist);
+Datum
+int2_int8_dist(PG_FUNCTION_ARGS)
+{
+	int64		a = (int64) PG_GETARG_INT16(0);
+	int64		b = PG_GETARG_INT64(1);
+	int64		r;
+
+	if (pg_sub_s64_overflow(a, b, &r) ||
+		r == PG_INT64_MIN)
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("bigint out of range")));
+
+	PG_RETURN_INT64(i64abs(r));
+}
+
 
 /**************************************************
  * GiST support functions
  **************************************************/
 
 Datum
@@ -134,47 +236,53 @@ gbt_int2_fetch(PG_FUNCTION_ARGS)
 }
 
 Datum
 gbt_int2_consistent(PG_FUNCTION_ARGS)
 {
 	GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
-	int16		query = PG_GETARG_INT16(1);
+	Datum		queryDatum = PG_GETARG_DATUM(1);
 	StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2);
-#ifdef NOT_USED
 	Oid			subtype = PG_GETARG_OID(3);
-#endif
 	bool	   *recheck = (bool *) PG_GETARG_POINTER(4);
 	int16KEY   *kkk = (int16KEY *) DatumGetPointer(entry->key);
+	const gbtree_ninfo *ti;
+	gbt_intkey	query;
+	const void *qp;
 	GBT_NUMKEY_R key;
 
 	/* All cases served by this function are exact */
 	*recheck = false;
 
 	key.lower = (GBT_NUMKEY *) &kkk->lower;
 	key.upper = (GBT_NUMKEY *) &kkk->upper;
 
-	PG_RETURN_BOOL(gbt_num_consistent(&key, &query, &strategy,
-									  GIST_LEAF(entry), &tinfo, fcinfo->flinfo));
+	ti = gbt_int2_crosstype(subtype, queryDatum, &query, &qp);
+
+	PG_RETURN_BOOL(gbt_num_consistent(&key, qp, &strategy, GIST_LEAF(entry),
+									  ti, fcinfo->flinfo));
 }
 
 Datum
 gbt_int2_distance(PG_FUNCTION_ARGS)
 {
 	GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
-	int16		query = PG_GETARG_INT16(1);
-#ifdef NOT_USED
+	Datum		queryDatum = PG_GETARG_DATUM(1);
 	Oid			subtype = PG_GETARG_OID(3);
-#endif
 	int16KEY   *kkk = (int16KEY *) DatumGetPointer(entry->key);
+	const gbtree_ninfo *ti;
+	gbt_intkey	query;
+	const void *qp;
 	GBT_NUMKEY_R key;
 
 	key.lower = (GBT_NUMKEY *) &kkk->lower;
 	key.upper = (GBT_NUMKEY *) &kkk->upper;
 
-	PG_RETURN_FLOAT8(gbt_num_distance(&key, &query, GIST_LEAF(entry),
-									  &tinfo, fcinfo->flinfo));
+	ti = gbt_int2_crosstype(subtype, queryDatum, &query, &qp);
+
+	PG_RETURN_FLOAT8(gbt_num_distance(&key, qp, GIST_LEAF(entry),
+									  ti, fcinfo->flinfo));
 }
 
 Datum
 gbt_int2_union(PG_FUNCTION_ARGS)
 {
 	GistEntryVector *entryvec = (GistEntryVector *) PG_GETARG_POINTER(0);
diff --git a/contrib/btree_gist/btree_int4.c b/contrib/btree_gist/btree_int4.c
index 47790578e6b..325ee78f2ab 100644
--- a/contrib/btree_gist/btree_int4.c
+++ b/contrib/btree_gist/btree_int4.c
@@ -1,12 +1,13 @@
 /*
  * contrib/btree_gist/btree_int4.c
  */
 #include "postgres.h"
 #include "btree_gist.h"
 #include "btree_utils_num.h"
+#include "catalog/pg_type.h"
 #include "common/int.h"
 #include "utils/rel.h"
 #include "utils/sortsupport.h"
 
 typedef struct int32key
 {
@@ -71,13 +72,12 @@ gbt_int4key_cmp(const void *a, const void *b, FmgrInfo *flinfo)
 static float8
 gbt_int4_dist(const void *a, const void *b, FmgrInfo *flinfo)
 {
 	return GET_FLOAT_DISTANCE(int32, a, b);
 }
 
-
 static const gbtree_ninfo tinfo =
 {
 	gbt_t_int4,
 	sizeof(int32),
 	8,							/* sizeof(gbtreekey8) */
 	gbt_int4gt,
@@ -86,12 +86,80 @@ static const gbtree_ninfo tinfo =
 	gbt_int4le,
 	gbt_int4lt,
 	gbt_int4key_cmp,
 	gbt_int4_dist
 };
 
+/*
+ * Cross-type GiST callbacks: the indexed key is int4, the query is int2 or
+ * int8.  Both reuse gbt_num_consistent()/gbt_num_distance() via a tinfo whose
+ * comparison/distance callbacks read the query (left) and key (right) sides at
+ * their own widths.  f_cmp is unused on these paths and left NULL.
+ */
+GBT_INT_CMP_FNS(gbt_int4_q2_, int16, int32)
+GBT_INT_CMP_FNS(gbt_int4_q8_, int64, int32)
+
+static const gbtree_ninfo tinfo_q2 =
+{
+	gbt_t_int4,
+	sizeof(int32),
+	8,
+	gbt_int4_q2_gt,
+	gbt_int4_q2_ge,
+	gbt_int4_q2_eq,
+	gbt_int4_q2_le,
+	gbt_int4_q2_lt,
+	NULL,
+	gbt_int4_q2_dist
+};
+
+static const gbtree_ninfo tinfo_q8 =
+{
+	gbt_t_int4,
+	sizeof(int32),
+	8,
+	gbt_int4_q8_gt,
+	gbt_int4_q8_ge,
+	gbt_int4_q8_eq,
+	gbt_int4_q8_le,
+	gbt_int4_q8_lt,
+	NULL,
+	gbt_int4_q8_dist
+};
+
+/*
+ * Cross-type dispatch shared by gbt_int4_consistent and gbt_int4_distance:
+ * select the tinfo for the query subtype and read the query value at its own
+ * width into caller-owned storage.
+ */
+static const gbtree_ninfo *
+gbt_int4_crosstype(Oid subtype, Datum d, gbt_intkey *q, const void **qp)
+{
+	switch (subtype)
+	{
+		case INT2OID:
+			q->i2 = DatumGetInt16(d);
+			*qp = &q->i2;
+			return &tinfo_q2;
+		case InvalidOid:		/* same-type: exclusion/temporal constraint
+								 * checks pass the native type with subtype 0 */
+		case INT4OID:
+			q->i4 = DatumGetInt32(d);
+			*qp = &q->i4;
+			return &tinfo;
+		case INT8OID:
+			q->i8 = DatumGetInt64(d);
+			*qp = &q->i8;
+			return &tinfo_q8;
+		default:
+			elog(ERROR, "unrecognized subtype %u for btree_gist int4 cross-type comparison",
+				 subtype);
+			return NULL;		/* keep compiler quiet */
+	}
+}
+
 
 PG_FUNCTION_INFO_V1(int4_dist);
 Datum
 int4_dist(PG_FUNCTION_ARGS)
 {
 	int32		a = PG_GETARG_INT32(0);
@@ -107,12 +175,46 @@ int4_dist(PG_FUNCTION_ARGS)
 
 	ra = abs(r);
 
 	PG_RETURN_INT32(ra);
 }
 
+PG_FUNCTION_INFO_V1(int4_int2_dist);
+Datum
+int4_int2_dist(PG_FUNCTION_ARGS)
+{
+	int32		a = PG_GETARG_INT32(0);
+	int32		b = (int32) PG_GETARG_INT16(1);
+	int32		r;
+
+	if (pg_sub_s32_overflow(a, b, &r) ||
+		r == PG_INT32_MIN)
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("integer out of range")));
+
+	PG_RETURN_INT32(abs(r));
+}
+
+PG_FUNCTION_INFO_V1(int4_int8_dist);
+Datum
+int4_int8_dist(PG_FUNCTION_ARGS)
+{
+	int64		a = (int64) PG_GETARG_INT32(0);
+	int64		b = PG_GETARG_INT64(1);
+	int64		r;
+
+	if (pg_sub_s64_overflow(a, b, &r) ||
+		r == PG_INT64_MIN)
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("bigint out of range")));
+
+	PG_RETURN_INT64(i64abs(r));
+}
+
 
 /**************************************************
  * GiST support functions
  **************************************************/
 
 Datum
@@ -132,47 +234,53 @@ gbt_int4_fetch(PG_FUNCTION_ARGS)
 }
 
 Datum
 gbt_int4_consistent(PG_FUNCTION_ARGS)
 {
 	GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
-	int32		query = PG_GETARG_INT32(1);
+	Datum		queryDatum = PG_GETARG_DATUM(1);
 	StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2);
-#ifdef NOT_USED
 	Oid			subtype = PG_GETARG_OID(3);
-#endif
 	bool	   *recheck = (bool *) PG_GETARG_POINTER(4);
 	int32KEY   *kkk = (int32KEY *) DatumGetPointer(entry->key);
+	const gbtree_ninfo *ti;
+	gbt_intkey	query;
+	const void *qp;
 	GBT_NUMKEY_R key;
 
 	/* All cases served by this function are exact */
 	*recheck = false;
 
 	key.lower = (GBT_NUMKEY *) &kkk->lower;
 	key.upper = (GBT_NUMKEY *) &kkk->upper;
 
-	PG_RETURN_BOOL(gbt_num_consistent(&key, &query, &strategy,
-									  GIST_LEAF(entry), &tinfo, fcinfo->flinfo));
+	ti = gbt_int4_crosstype(subtype, queryDatum, &query, &qp);
+
+	PG_RETURN_BOOL(gbt_num_consistent(&key, qp, &strategy, GIST_LEAF(entry),
+									  ti, fcinfo->flinfo));
 }
 
 Datum
 gbt_int4_distance(PG_FUNCTION_ARGS)
 {
 	GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
-	int32		query = PG_GETARG_INT32(1);
-#ifdef NOT_USED
+	Datum		queryDatum = PG_GETARG_DATUM(1);
 	Oid			subtype = PG_GETARG_OID(3);
-#endif
 	int32KEY   *kkk = (int32KEY *) DatumGetPointer(entry->key);
+	const gbtree_ninfo *ti;
+	gbt_intkey	query;
+	const void *qp;
 	GBT_NUMKEY_R key;
 
 	key.lower = (GBT_NUMKEY *) &kkk->lower;
 	key.upper = (GBT_NUMKEY *) &kkk->upper;
 
-	PG_RETURN_FLOAT8(gbt_num_distance(&key, &query, GIST_LEAF(entry),
-									  &tinfo, fcinfo->flinfo));
+	ti = gbt_int4_crosstype(subtype, queryDatum, &query, &qp);
+
+	PG_RETURN_FLOAT8(gbt_num_distance(&key, qp, GIST_LEAF(entry),
+									  ti, fcinfo->flinfo));
 }
 
 Datum
 gbt_int4_union(PG_FUNCTION_ARGS)
 {
 	GistEntryVector *entryvec = (GistEntryVector *) PG_GETARG_POINTER(0);
diff --git a/contrib/btree_gist/btree_int8.c b/contrib/btree_gist/btree_int8.c
index f48122c8d84..62c06345e87 100644
--- a/contrib/btree_gist/btree_int8.c
+++ b/contrib/btree_gist/btree_int8.c
@@ -2,12 +2,13 @@
  * contrib/btree_gist/btree_int8.c
  */
 #include "postgres.h"
 
 #include "btree_gist.h"
 #include "btree_utils_num.h"
+#include "catalog/pg_type.h"
 #include "common/int.h"
 #include "utils/rel.h"
 #include "utils/sortsupport.h"
 
 typedef struct int64key
 {
@@ -73,13 +74,12 @@ gbt_int8key_cmp(const void *a, const void *b, FmgrInfo *flinfo)
 static float8
 gbt_int8_dist(const void *a, const void *b, FmgrInfo *flinfo)
 {
 	return GET_FLOAT_DISTANCE(int64, a, b);
 }
 
-
 static const gbtree_ninfo tinfo =
 {
 	gbt_t_int8,
 	sizeof(int64),
 	16,							/* sizeof(gbtreekey16) */
 	gbt_int8gt,
@@ -88,12 +88,80 @@ static const gbtree_ninfo tinfo =
 	gbt_int8le,
 	gbt_int8lt,
 	gbt_int8key_cmp,
 	gbt_int8_dist
 };
 
+/*
+ * Cross-type GiST callbacks: the indexed key is int8, the query is int2 or
+ * int4.  Both reuse gbt_num_consistent()/gbt_num_distance() via a tinfo whose
+ * comparison/distance callbacks read the query (left) and key (right) sides at
+ * their own widths.  f_cmp is unused on these paths and left NULL.
+ */
+GBT_INT_CMP_FNS(gbt_int8_q2_, int16, int64)
+GBT_INT_CMP_FNS(gbt_int8_q4_, int32, int64)
+
+static const gbtree_ninfo tinfo_q2 =
+{
+	gbt_t_int8,
+	sizeof(int64),
+	16,
+	gbt_int8_q2_gt,
+	gbt_int8_q2_ge,
+	gbt_int8_q2_eq,
+	gbt_int8_q2_le,
+	gbt_int8_q2_lt,
+	NULL,
+	gbt_int8_q2_dist
+};
+
+static const gbtree_ninfo tinfo_q4 =
+{
+	gbt_t_int8,
+	sizeof(int64),
+	16,
+	gbt_int8_q4_gt,
+	gbt_int8_q4_ge,
+	gbt_int8_q4_eq,
+	gbt_int8_q4_le,
+	gbt_int8_q4_lt,
+	NULL,
+	gbt_int8_q4_dist
+};
+
+/*
+ * Cross-type dispatch shared by gbt_int8_consistent and gbt_int8_distance:
+ * select the tinfo for the query subtype and read the query value at its own
+ * width into caller-owned storage.
+ */
+static const gbtree_ninfo *
+gbt_int8_crosstype(Oid subtype, Datum d, gbt_intkey *q, const void **qp)
+{
+	switch (subtype)
+	{
+		case INT2OID:
+			q->i2 = DatumGetInt16(d);
+			*qp = &q->i2;
+			return &tinfo_q2;
+		case INT4OID:
+			q->i4 = DatumGetInt32(d);
+			*qp = &q->i4;
+			return &tinfo_q4;
+		case InvalidOid:		/* same-type: exclusion/temporal constraint
+								 * checks pass the native type with subtype 0 */
+		case INT8OID:
+			q->i8 = DatumGetInt64(d);
+			*qp = &q->i8;
+			return &tinfo;
+		default:
+			elog(ERROR, "unrecognized subtype %u for btree_gist int8 cross-type comparison",
+				 subtype);
+			return NULL;		/* keep compiler quiet */
+	}
+}
+
 
 PG_FUNCTION_INFO_V1(int8_dist);
 Datum
 int8_dist(PG_FUNCTION_ARGS)
 {
 	int64		a = PG_GETARG_INT64(0);
@@ -109,12 +177,46 @@ int8_dist(PG_FUNCTION_ARGS)
 
 	ra = i64abs(r);
 
 	PG_RETURN_INT64(ra);
 }
 
+PG_FUNCTION_INFO_V1(int8_int2_dist);
+Datum
+int8_int2_dist(PG_FUNCTION_ARGS)
+{
+	int64		a = PG_GETARG_INT64(0);
+	int64		b = (int64) PG_GETARG_INT16(1);
+	int64		r;
+
+	if (pg_sub_s64_overflow(a, b, &r) ||
+		r == PG_INT64_MIN)
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("bigint out of range")));
+
+	PG_RETURN_INT64(i64abs(r));
+}
+
+PG_FUNCTION_INFO_V1(int8_int4_dist);
+Datum
+int8_int4_dist(PG_FUNCTION_ARGS)
+{
+	int64		a = PG_GETARG_INT64(0);
+	int64		b = (int64) PG_GETARG_INT32(1);
+	int64		r;
+
+	if (pg_sub_s64_overflow(a, b, &r) ||
+		r == PG_INT64_MIN)
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("bigint out of range")));
+
+	PG_RETURN_INT64(i64abs(r));
+}
+
 
 /**************************************************
  * GiST support functions
  **************************************************/
 
 Datum
@@ -134,47 +236,53 @@ gbt_int8_fetch(PG_FUNCTION_ARGS)
 }
 
 Datum
 gbt_int8_consistent(PG_FUNCTION_ARGS)
 {
 	GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
-	int64		query = PG_GETARG_INT64(1);
+	Datum		queryDatum = PG_GETARG_DATUM(1);
 	StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2);
-#ifdef NOT_USED
 	Oid			subtype = PG_GETARG_OID(3);
-#endif
 	bool	   *recheck = (bool *) PG_GETARG_POINTER(4);
 	int64KEY   *kkk = (int64KEY *) DatumGetPointer(entry->key);
+	const gbtree_ninfo *ti;
+	gbt_intkey	query;
+	const void *qp;
 	GBT_NUMKEY_R key;
 
 	/* All cases served by this function are exact */
 	*recheck = false;
 
 	key.lower = (GBT_NUMKEY *) &kkk->lower;
 	key.upper = (GBT_NUMKEY *) &kkk->upper;
 
-	PG_RETURN_BOOL(gbt_num_consistent(&key, &query, &strategy,
-									  GIST_LEAF(entry), &tinfo, fcinfo->flinfo));
+	ti = gbt_int8_crosstype(subtype, queryDatum, &query, &qp);
+
+	PG_RETURN_BOOL(gbt_num_consistent(&key, qp, &strategy, GIST_LEAF(entry),
+									  ti, fcinfo->flinfo));
 }
 
 Datum
 gbt_int8_distance(PG_FUNCTION_ARGS)
 {
 	GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
-	int64		query = PG_GETARG_INT64(1);
-#ifdef NOT_USED
+	Datum		queryDatum = PG_GETARG_DATUM(1);
 	Oid			subtype = PG_GETARG_OID(3);
-#endif
 	int64KEY   *kkk = (int64KEY *) DatumGetPointer(entry->key);
+	const gbtree_ninfo *ti;
+	gbt_intkey	query;
+	const void *qp;
 	GBT_NUMKEY_R key;
 
 	key.lower = (GBT_NUMKEY *) &kkk->lower;
 	key.upper = (GBT_NUMKEY *) &kkk->upper;
 
-	PG_RETURN_FLOAT8(gbt_num_distance(&key, &query, GIST_LEAF(entry),
-									  &tinfo, fcinfo->flinfo));
+	ti = gbt_int8_crosstype(subtype, queryDatum, &query, &qp);
+
+	PG_RETURN_FLOAT8(gbt_num_distance(&key, qp, GIST_LEAF(entry),
+									  ti, fcinfo->flinfo));
 }
 
 Datum
 gbt_int8_union(PG_FUNCTION_ARGS)
 {
 	GistEntryVector *entryvec = (GistEntryVector *) PG_GETARG_POINTER(0);
diff --git a/contrib/btree_gist/btree_utils_num.c b/contrib/btree_gist/btree_utils_num.c
index 3affe4c2c46..0d6d578062a 100644
--- a/contrib/btree_gist/btree_utils_num.c
+++ b/contrib/btree_gist/btree_utils_num.c
@@ -266,12 +266,18 @@ gbt_num_consistent(const GBT_NUMKEY_R *key,
 				   bool is_leaf,
 				   const gbtree_ninfo *tinfo,
 				   FmgrInfo *flinfo)
 {
 	bool		retval;
 
+	/*
+	 * Every comparison callback is invoked as f_xx(query, key): the query
+	 * value is always the left argument and the indexed key bound the right.
+	 * The integer opclasses rely on this fixed order so their cross-type
+	 * callbacks can read each side at its own width.
+	 */
 	switch (*strategy)
 	{
 		case BTLessEqualStrategyNumber:
 			retval = tinfo->f_ge(query, key->lower, flinfo);
 			break;
 		case BTLessStrategyNumber:
@@ -281,13 +287,13 @@ gbt_num_consistent(const GBT_NUMKEY_R *key,
 				retval = tinfo->f_ge(query, key->lower, flinfo);
 			break;
 		case BTEqualStrategyNumber:
 			if (is_leaf)
 				retval = tinfo->f_eq(query, key->lower, flinfo);
 			else
-				retval = (tinfo->f_le(key->lower, query, flinfo) &&
+				retval = (tinfo->f_ge(query, key->lower, flinfo) &&
 						  tinfo->f_le(query, key->upper, flinfo));
 			break;
 		case BTGreaterStrategyNumber:
 			if (is_leaf)
 				retval = tinfo->f_lt(query, key->upper, flinfo);
 			else
@@ -304,13 +310,12 @@ gbt_num_consistent(const GBT_NUMKEY_R *key,
 			retval = false;
 	}
 
 	return retval;
 }
 
-
 /*
  * The GiST distance method (for KNN-Gist)
  */
 
 float8
 gbt_num_distance(const GBT_NUMKEY_R *key,
@@ -321,12 +326,19 @@ gbt_num_distance(const GBT_NUMKEY_R *key,
 {
 	float8		retval;
 
 	if (tinfo->f_dist == NULL)
 		elog(ERROR, "KNN search is not supported for btree_gist type %d",
 			 (int) tinfo->t);
+
+	/*
+	 * As in gbt_num_consistent(), every callback is invoked as f_xx(query, key):
+	 * the query value is the left argument and the indexed key bound the right.
+	 * The integer opclasses' cross-type callbacks read each side at its own
+	 * width, so keep this argument order if you ever touch the calls below.
+	 */
 	if (tinfo->f_le(query, key->lower, flinfo))
 		retval = tinfo->f_dist(query, key->lower, flinfo);
 	else if (tinfo->f_ge(query, key->upper, flinfo))
 		retval = tinfo->f_dist(query, key->upper, flinfo);
 	else
 		retval = 0.0;
diff --git a/contrib/btree_gist/btree_utils_num.h b/contrib/btree_gist/btree_utils_num.h
index 53e477d8b1e..217b362c169 100644
--- a/contrib/btree_gist/btree_utils_num.h
+++ b/contrib/btree_gist/btree_utils_num.h
@@ -24,12 +24,24 @@ typedef struct
 typedef struct
 {
 	int			i;
 	GBT_NUMKEY *t;
 } Nsrt;
 
+/*
+ * Query-value storage for the integer opclasses' cross-type path.  The caller
+ * owns one of these and passes its address to the per-type cross-type helper,
+ * which fills the right width and points the query pointer at it.
+ */
+typedef union
+{
+	int16		i2;
+	int32		i4;
+	int64		i8;
+} gbt_intkey;
+
 
 /* type description */
 
 typedef struct
 {
 
@@ -83,13 +95,38 @@ typedef struct
  */
 #define INTERVAL_TO_SEC(ivp) \
 	(((double) (ivp)->time) / ((double) USECS_PER_SEC) + \
 	 (ivp)->day * (24.0 * SECS_PER_HOUR) + \
 	 (ivp)->month * (30.0 * SECS_PER_DAY))
 
-#define GET_FLOAT_DISTANCE(t, arg1, arg2)	fabs( ((float8) *((const t *) (arg1))) - ((float8) *((const t *) (arg2))) )
+#define GET_FLOAT_DISTANCE2(t1, t2, arg1, arg2)	fabs( ((float8) *((const t1 *) (arg1))) - ((float8) *((const t2 *) (arg2))) )
+#define GET_FLOAT_DISTANCE(t, arg1, arg2)	GET_FLOAT_DISTANCE2(t, t, (arg1), (arg2))
+
+/*
+ * Generate the comparison/distance callbacks for a gbtree_ninfo whose query
+ * and key sides may be different (integer) types.  gbt_num_consistent() and
+ * gbt_num_distance() always invoke the callbacks as f_xx(query, key), so the
+ * first argument has the query type QT (the operator's right-hand subtype) and
+ * the second has the indexed key type KT.  Integer widening is value-preserving,
+ * so the comparisons need no explicit cast; the distance widens to float8 to
+ * avoid overflow in the subtraction.  Invoked with QT == KT this also generates
+ * the ordinary same-type callbacks.
+ */
+#define GBT_INT_CMP_FNS(prefix, QT, KT) \
+static bool prefix##gt(const void *a, const void *b, FmgrInfo *flinfo) \
+{ return *((const QT *) a) > *((const KT *) b); } \
+static bool prefix##ge(const void *a, const void *b, FmgrInfo *flinfo) \
+{ return *((const QT *) a) >= *((const KT *) b); } \
+static bool prefix##eq(const void *a, const void *b, FmgrInfo *flinfo) \
+{ return *((const QT *) a) == *((const KT *) b); } \
+static bool prefix##le(const void *a, const void *b, FmgrInfo *flinfo) \
+{ return *((const QT *) a) <= *((const KT *) b); } \
+static bool prefix##lt(const void *a, const void *b, FmgrInfo *flinfo) \
+{ return *((const QT *) a) < *((const KT *) b); } \
+static float8 prefix##dist(const void *a, const void *b, FmgrInfo *flinfo) \
+{ return GET_FLOAT_DISTANCE2(QT, KT, a, b); }
 
 
 extern Interval *abs_interval(Interval *a);
 
 extern bool gbt_num_consistent(const GBT_NUMKEY_R *key, const void *query,
 							   const StrategyNumber *strategy, bool is_leaf,
diff --git a/contrib/btree_gist/meson.build b/contrib/btree_gist/meson.build
index 2b1a5463289..43ba130e8e4 100644
--- a/contrib/btree_gist/meson.build
+++ b/contrib/btree_gist/meson.build
@@ -49,12 +49,13 @@ install_data(
   'btree_gist--1.4--1.5.sql',
   'btree_gist--1.5--1.6.sql',
   'btree_gist--1.6--1.7.sql',
   'btree_gist--1.7--1.8.sql',
   'btree_gist--1.8--1.9.sql',
   'btree_gist--1.9.sql',
+  'btree_gist--1.9--1.10.sql',
   kwargs: contrib_data_args,
 )
 
 tests += {
   'name': 'btree_gist',
   'sd': meson.current_source_dir(),
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index c5db6ca6705..e29f76b6feb 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3808,12 +3808,13 @@ floating_decimal_32
 floating_decimal_64
 fmgr_hook_type
 foreign_glob_cxt
 foreign_loc_cxt
 freefunc
 fsec_t
+gbt_intkey
 gbt_vsrt_arg
 gbtree_ninfo
 gbtree_vinfo
 generate_series_fctx
 generate_series_numeric_fctx
 generate_series_timestamp_fctx
-- 
2.51.0



  [application/octet-stream] v5-0002-Add-tests-for-cross-type-operators-for-GiST-indexes.patch (25.8K, 4-v5-0002-Add-tests-for-cross-type-operators-for-GiST-indexes.patch)
  download | inline diff:
From bdce40e8a75eea4f1df778376042dbcf8246d3a4 Mon Sep 17 00:00:00 2001
From: Alexander Nestorov <[email protected]>
Date: Thu, 4 Jun 2026 00:08:27 +0200
Subject: [PATCH] Add tests for cross-type operators for GiST indexes

Add a new int_crosstype regression test that drives the cross-type
comparison and <-> distance operators for every int2/int4/int8
combination through the index (with seqscan and bitmapscan disabled),
plus a multi-column index, query constants outside the indexed column's
range, and the standalone distance functions including their
overflow-detection paths.

Include a width-sensitivity guard: the constants are chosen so their low
16 bits collide with a value present in the data, so that reading either
side at the wrong width (a dispatch entry returning the wrong-width
callback, a truncated key, or the query/key arguments swapped in
gbt_num_consistent()/gbt_num_distance()) changes the result and fails
the test.

Also assert that the operator families contain no cross-type support
functions, matching the pg_amop-only design.

Co-authored-by: Maxime Schoemans <[email protected]>
---
 contrib/btree_gist/Makefile                   |   2 +-
 contrib/btree_gist/expected/int_crosstype.out | 577 ++++++++++++++++++
 contrib/btree_gist/meson.build                |   1 +
 contrib/btree_gist/sql/int_crosstype.sql      | 230 +++++++
 4 files changed, 809 insertions(+), 1 deletion(-)
 create mode 100644 contrib/btree_gist/expected/int_crosstype.out
 create mode 100644 contrib/btree_gist/sql/int_crosstype.sql

diff --git a/contrib/btree_gist/Makefile b/contrib/btree_gist/Makefile
index 380c7642fde..1d0668d97ab 100644
--- a/contrib/btree_gist/Makefile
+++ b/contrib/btree_gist/Makefile
@@ -38,13 +38,13 @@ DATA = btree_gist--1.0--1.1.sql \
        btree_gist--1.9.sql btree_gist--1.9--1.10.sql
 PGFILEDESC = "btree_gist - B-tree equivalent GiST operator classes"
 
 REGRESS = init int2 int4 int8 float4 float8 cash oid timestamp timestamptz \
         time timetz date interval macaddr macaddr8 inet cidr text varchar char \
         bytea bit varbit numeric uuid not_equal enum bool partitions \
-        stratnum without_overlaps
+        stratnum int_crosstype without_overlaps
 
 SHLIB_LINK += $(filter -lm, $(LIBS))
 
 ifdef USE_PGXS
 PG_CONFIG = pg_config
 PGXS := $(shell $(PG_CONFIG) --pgxs)
diff --git a/contrib/btree_gist/expected/int_crosstype.out b/contrib/btree_gist/expected/int_crosstype.out
new file mode 100644
index 00000000000..b891abdf25e
--- /dev/null
+++ b/contrib/btree_gist/expected/int_crosstype.out
@@ -0,0 +1,577 @@
+-- Cross-type operator support for int2/int4/int8 in GiST.
+--
+-- Verifies that (a) the cross-type B-tree-style operators registered in
+-- gist_int{2,4,8}_ops match the results of seqscans using the same operator
+-- expressions, (b) the KNN <-> operator works across types and uses the
+-- index, and (c) values outside the smaller subtype's range are handled
+-- according to normal comparison semantics, without narrowing or erroring.
+--
+-- The integer cross-type support is handled inside the existing
+-- consistent/distance support functions (which dispatch on the subtype OID),
+-- so the operator families must not contain any cross-type (different
+-- left/right input type) support-function entries.
+--
+SELECT opf.opfname AS opfamily,
+       amproc.amproclefttype::regtype AS lefttype,
+       amproc.amprocrighttype::regtype AS righttype,
+       amproc.amprocnum AS procnum,
+       amproc.amproc::regproc AS proc
+FROM pg_amproc amproc
+     JOIN pg_opfamily opf ON opf.oid = amproc.amprocfamily
+     JOIN pg_am am ON am.oid = opf.opfmethod
+WHERE am.amname = 'gist'
+  AND opf.opfname IN ('gist_int2_ops', 'gist_int4_ops', 'gist_int8_ops')
+  AND amproc.amproclefttype <> amproc.amprocrighttype
+ORDER BY opf.opfname,
+         amproc.amproclefttype::regtype::text,
+         amproc.amprocrighttype::regtype::text,
+         amproc.amprocnum,
+         amproc.amproc::regproc::text;
+ opfamily | lefttype | righttype | procnum | proc 
+----------+----------+-----------+---------+------
+(0 rows)
+
+CREATE TABLE ct_i2 (a int2);
+CREATE TABLE ct_i4 (a int4);
+CREATE TABLE ct_i8 (a int8);
+INSERT INTO ct_i2 SELECT g::int2 FROM generate_series(-100, 100) g;
+INSERT INTO ct_i4 SELECT g FROM generate_series(-100, 100) g;
+INSERT INTO ct_i8 SELECT g::int8 FROM generate_series(-100, 100) g;
+-- Add some values that are representable only in wider types, to exercise
+-- the path where the cross-type query constant is out of range of the key
+-- type.
+INSERT INTO ct_i4 VALUES (100000), (-100000);
+INSERT INTO ct_i8 VALUES (5000000000), (-5000000000);
+CREATE INDEX ct_i2_idx ON ct_i2 USING gist (a);
+CREATE INDEX ct_i4_idx ON ct_i4 USING gist (a);
+CREATE INDEX ct_i8_idx ON ct_i8 USING gist (a);
+ANALYZE ct_i2;
+ANALYZE ct_i4;
+ANALYZE ct_i8;
+SET enable_seqscan = off;
+SET enable_bitmapscan = off;
+-- int2 key x int4 query
+SELECT count(*) FROM ct_i2 WHERE a <  50::int4;
+ count 
+-------
+   150
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a <= 50::int4;
+ count 
+-------
+   151
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a =  50::int4;
+ count 
+-------
+     1
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a >= 50::int4;
+ count 
+-------
+    51
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a >  50::int4;
+ count 
+-------
+    50
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a <> 50::int4;
+ count 
+-------
+   200
+(1 row)
+
+-- query out of int2 range: matches nothing for =, everything for <>
+SELECT count(*) FROM ct_i2 WHERE a =  100000::int4;
+ count 
+-------
+     0
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a <> 100000::int4;
+ count 
+-------
+   201
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a <  100000::int4;
+ count 
+-------
+   201
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a >  100000::int4;
+ count 
+-------
+     0
+(1 row)
+
+-- int2 key x int8 query
+SELECT count(*) FROM ct_i2 WHERE a <  50::int8;
+ count 
+-------
+   150
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a <= 50::int8;
+ count 
+-------
+   151
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a =  50::int8;
+ count 
+-------
+     1
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a >= 50::int8;
+ count 
+-------
+    51
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a >  50::int8;
+ count 
+-------
+    50
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a <> 50::int8;
+ count 
+-------
+   200
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a =  5000000000::int8;
+ count 
+-------
+     0
+(1 row)
+
+SELECT count(*) FROM ct_i2 WHERE a <  5000000000::int8;
+ count 
+-------
+   201
+(1 row)
+
+-- int4 key x int2 query
+SELECT count(*) FROM ct_i4 WHERE a <  50::int2;
+ count 
+-------
+   151
+(1 row)
+
+SELECT count(*) FROM ct_i4 WHERE a <= 50::int2;
+ count 
+-------
+   152
+(1 row)
+
+SELECT count(*) FROM ct_i4 WHERE a =  50::int2;
+ count 
+-------
+     1
+(1 row)
+
+SELECT count(*) FROM ct_i4 WHERE a >= 50::int2;
+ count 
+-------
+    52
+(1 row)
+
+SELECT count(*) FROM ct_i4 WHERE a >  50::int2;
+ count 
+-------
+    51
+(1 row)
+
+SELECT count(*) FROM ct_i4 WHERE a <> 50::int2;
+ count 
+-------
+   202
+(1 row)
+
+-- int4 key x int8 query
+SELECT count(*) FROM ct_i4 WHERE a <  50::int8;
+ count 
+-------
+   151
+(1 row)
+
+SELECT count(*) FROM ct_i4 WHERE a <= 50::int8;
+ count 
+-------
+   152
+(1 row)
+
+SELECT count(*) FROM ct_i4 WHERE a =  50::int8;
+ count 
+-------
+     1
+(1 row)
+
+SELECT count(*) FROM ct_i4 WHERE a >= 50::int8;
+ count 
+-------
+    52
+(1 row)
+
+SELECT count(*) FROM ct_i4 WHERE a >  50::int8;
+ count 
+-------
+    51
+(1 row)
+
+SELECT count(*) FROM ct_i4 WHERE a <> 50::int8;
+ count 
+-------
+   202
+(1 row)
+
+SELECT count(*) FROM ct_i4 WHERE a =  5000000000::int8;
+ count 
+-------
+     0
+(1 row)
+
+SELECT count(*) FROM ct_i4 WHERE a <  5000000000::int8;
+ count 
+-------
+   203
+(1 row)
+
+-- int8 key x int2 query
+SELECT count(*) FROM ct_i8 WHERE a <  50::int2;
+ count 
+-------
+   151
+(1 row)
+
+SELECT count(*) FROM ct_i8 WHERE a <= 50::int2;
+ count 
+-------
+   152
+(1 row)
+
+SELECT count(*) FROM ct_i8 WHERE a =  50::int2;
+ count 
+-------
+     1
+(1 row)
+
+SELECT count(*) FROM ct_i8 WHERE a >= 50::int2;
+ count 
+-------
+    52
+(1 row)
+
+SELECT count(*) FROM ct_i8 WHERE a >  50::int2;
+ count 
+-------
+    51
+(1 row)
+
+SELECT count(*) FROM ct_i8 WHERE a <> 50::int2;
+ count 
+-------
+   202
+(1 row)
+
+-- int8 key x int4 query
+SELECT count(*) FROM ct_i8 WHERE a <  50::int4;
+ count 
+-------
+   151
+(1 row)
+
+SELECT count(*) FROM ct_i8 WHERE a <= 50::int4;
+ count 
+-------
+   152
+(1 row)
+
+SELECT count(*) FROM ct_i8 WHERE a =  50::int4;
+ count 
+-------
+     1
+(1 row)
+
+SELECT count(*) FROM ct_i8 WHERE a >= 50::int4;
+ count 
+-------
+    52
+(1 row)
+
+SELECT count(*) FROM ct_i8 WHERE a >  50::int4;
+ count 
+-------
+    51
+(1 row)
+
+SELECT count(*) FROM ct_i8 WHERE a <> 50::int4;
+ count 
+-------
+   202
+(1 row)
+
+-- Confirm the index is actually used for a cross-type predicate.
+EXPLAIN (COSTS OFF)
+SELECT count(*) FROM ct_i4 WHERE a = 50::int8;
+                   QUERY PLAN                   
+------------------------------------------------
+ Aggregate
+   ->  Index Only Scan using ct_i4_idx on ct_i4
+         Index Cond: (a = '50'::bigint)
+(3 rows)
+
+-- Cross-type KNN: int4 key ordered by int2 / int8 queries.
+EXPLAIN (COSTS OFF)
+SELECT a FROM ct_i4 ORDER BY a <-> '-100'::int2 LIMIT 3;
+                   QUERY PLAN                   
+------------------------------------------------
+ Limit
+   ->  Index Only Scan using ct_i4_idx on ct_i4
+         Order By: (a <-> '-100'::smallint)
+(3 rows)
+
+SELECT a FROM ct_i4 ORDER BY a <-> '-100'::int2 LIMIT 3;
+  a   
+------
+ -100
+  -99
+  -98
+(3 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT a FROM ct_i4 ORDER BY a <-> '-100'::int8 LIMIT 3;
+                   QUERY PLAN                   
+------------------------------------------------
+ Limit
+   ->  Index Only Scan using ct_i4_idx on ct_i4
+         Order By: (a <-> '-100'::bigint)
+(3 rows)
+
+SELECT a FROM ct_i4 ORDER BY a <-> '-100'::int8 LIMIT 3;
+  a   
+------
+ -100
+  -99
+  -98
+(3 rows)
+
+-- Cross-type KNN: int2 key ordered by int4 / int8 queries.
+SELECT a FROM ct_i2 ORDER BY a <-> '-100'::int4 LIMIT 3;
+  a   
+------
+ -100
+  -99
+  -98
+(3 rows)
+
+SELECT a FROM ct_i2 ORDER BY a <-> '-100'::int8 LIMIT 3;
+  a   
+------
+ -100
+  -99
+  -98
+(3 rows)
+
+-- Cross-type KNN: int8 key ordered by int2 / int4 queries.
+SELECT a FROM ct_i8 ORDER BY a <-> '-100'::int2 LIMIT 3;
+  a   
+------
+ -100
+  -99
+  -98
+(3 rows)
+
+SELECT a FROM ct_i8 ORDER BY a <-> '-100'::int4 LIMIT 3;
+  a   
+------
+ -100
+  -99
+  -98
+(3 rows)
+
+-- Combined: cross-type WHERE + cross-type ORDER BY on the same index.
+EXPLAIN (COSTS OFF)
+SELECT a FROM ct_i4 WHERE a < 80::int8 ORDER BY a <-> '-100'::int8 LIMIT 3;
+                   QUERY PLAN                   
+------------------------------------------------
+ Limit
+   ->  Index Only Scan using ct_i4_idx on ct_i4
+         Index Cond: (a < '80'::bigint)
+         Order By: (a <-> '-100'::bigint)
+(4 rows)
+
+SELECT a FROM ct_i4 WHERE a < 80::int8 ORDER BY a <-> '-100'::int8 LIMIT 3;
+  a   
+------
+ -100
+  -99
+  -98
+(3 rows)
+
+-- Standalone distance-function smoke tests (not going through the index),
+-- including the overflow-detection paths.
+SELECT int2_int4_dist(3::int2, 10::int4);
+ int2_int4_dist 
+----------------
+              7
+(1 row)
+
+SELECT int4_int2_dist(-5::int4, 5::int2);
+ int4_int2_dist 
+----------------
+             10
+(1 row)
+
+SELECT int2_int8_dist(3::int2, 10::int8);
+ int2_int8_dist 
+----------------
+              7
+(1 row)
+
+SELECT int8_int2_dist(100::int8, -5::int2);
+ int8_int2_dist 
+----------------
+            105
+(1 row)
+
+SELECT int4_int8_dist(100::int4, 5000000000::int8);
+ int4_int8_dist 
+----------------
+     4999999900
+(1 row)
+
+SELECT int8_int4_dist(5000000000::int8, 100::int4);
+ int8_int4_dist 
+----------------
+     4999999900
+(1 row)
+
+-- Overflow detection: INT32_MIN distance from a positive int2 can't fit
+-- in int32, should error.
+SELECT int2_int4_dist(1::int2, -2147483648::int4);
+ERROR:  integer out of range
+-- Likewise INT64_MIN distance from a positive int4 can't fit in int64.
+SELECT int4_int8_dist(1::int4, -9223372036854775808::int8);
+ERROR:  bigint out of range
+--
+-- Multi-column GiST index with mixed-type predicates. This is the
+-- original motivating case: without cross-type operator support the
+-- planner can only use one column as an Index Cond and applies the
+-- other(s) as a Filter post-scan. Here both columns should appear as
+-- Index Cond.
+--
+CREATE TABLE ct_multi (a int4, b int8);
+INSERT INTO ct_multi
+    SELECT g, (g * 2)::int8 FROM generate_series(-50, 50) g;
+CREATE INDEX ct_multi_idx ON ct_multi USING gist (a, b);
+ANALYZE ct_multi;
+EXPLAIN (COSTS OFF)
+SELECT count(*) FROM ct_multi WHERE a = 25::int8 AND b = 50::int4;
+                      QUERY PLAN                       
+-------------------------------------------------------
+ Aggregate
+   ->  Index Only Scan using ct_multi_idx on ct_multi
+         Index Cond: ((a = '25'::bigint) AND (b = 50))
+(3 rows)
+
+SELECT count(*) FROM ct_multi WHERE a = 25::int8 AND b = 50::int4;
+ count 
+-------
+     1
+(1 row)
+
+-- Mixed cross-type ranges across both columns.
+EXPLAIN (COSTS OFF)
+SELECT count(*) FROM ct_multi WHERE a < 10::int8 AND b > 0::int2;
+                            QUERY PLAN                            
+------------------------------------------------------------------
+ Aggregate
+   ->  Index Only Scan using ct_multi_idx on ct_multi
+         Index Cond: ((a < '10'::bigint) AND (b > '0'::smallint))
+(3 rows)
+
+SELECT count(*) FROM ct_multi WHERE a < 10::int8 AND b > 0::int2;
+ count 
+-------
+     9
+(1 row)
+
+DROP TABLE ct_multi;
+--
+-- Width-sensitivity guard (int16 vs int32).
+--
+-- The cross-type callbacks must read each side at its OWN width: the query at
+-- the subtype width and the key at the indexed-column width, never narrowing
+-- one to the other. These cases are constructed so that reading either side at
+-- the wrong width changes the answer -- a dispatch entry returning the
+-- wrong-width tinfo, an int4 key truncated to int16, or the query/key arguments
+-- being swapped in gbt_num_consistent()/gbt_num_distance() would all flip the
+-- result, because each wide constant shares its low 16 bits with a value present
+-- in the data. enable_seqscan/bitmapscan are already off, so the GiST
+-- consistent/distance path is exercised.
+--
+CREATE TABLE ct_w4 (k int4);
+INSERT INTO ct_w4 VALUES (1), (65538);		-- 65538 = 0x10002, low 16 bits = 2
+CREATE INDEX ct_w4_idx ON ct_w4 USING gist (k);
+ANALYZE ct_w4;
+-- int4 key vs int2 query: 65538 must compare as 65538, not as 2.
+-- correct: {65538}; an int4 key truncated to int16 (65538 -> 2) would give {}.
+SELECT k FROM ct_w4 WHERE k > 1000::int2 ORDER BY k;
+   k   
+-------
+ 65538
+(1 row)
+
+-- correct: {} (no key equals 2); a truncated key (65538 -> 2) would match.
+SELECT k FROM ct_w4 WHERE k = 2::int2 ORDER BY k;
+ k 
+---
+(0 rows)
+
+-- KNN: nearest to 2 is 1 (distance 1). enable_sort=off steers the planner to
+-- the GiST KNN distance path rather than a Sort on the operator. A truncated
+-- key (65538 -> 2, distance 0) would wrongly sort 65538 first. (We assert the
+-- result, not the plan shape, to stay portable across visibility-map state.)
+SET enable_sort = off;
+SELECT k FROM ct_w4 ORDER BY k <-> 2::int2 LIMIT 1;
+ k 
+---
+ 1
+(1 row)
+
+RESET enable_sort;
+CREATE TABLE ct_w2 (k int2);
+INSERT INTO ct_w2 VALUES (1), (50);
+CREATE INDEX ct_w2_idx ON ct_w2 USING gist (k);
+ANALYZE ct_w2;
+-- int2 key vs int4 query: 65537 must compare as 65537, not as 1 (0x10001).
+-- correct: 0; an int4 query truncated to int16 (65537 -> 1) would match k=1.
+SELECT count(*) FROM ct_w2 WHERE k = 65537::int4;
+ count 
+-------
+     0
+(1 row)
+
+-- correct: 0; a truncated query (65537 -> 1) would match k=50.
+SELECT count(*) FROM ct_w2 WHERE k > 65537::int4;
+ count 
+-------
+     0
+(1 row)
+
+DROP TABLE ct_w4;
+DROP TABLE ct_w2;
+DROP TABLE ct_i2;
+DROP TABLE ct_i4;
+DROP TABLE ct_i8;
diff --git a/contrib/btree_gist/meson.build b/contrib/btree_gist/meson.build
index 43ba130e8e4..c640cb7d8d2 100644
--- a/contrib/btree_gist/meson.build
+++ b/contrib/btree_gist/meson.build
@@ -90,10 +90,11 @@ tests += {
       'uuid',
       'not_equal',
       'enum',
       'bool',
       'partitions',
       'stratnum',
+      'int_crosstype',
       'without_overlaps',
     ],
   },
 }
diff --git a/contrib/btree_gist/sql/int_crosstype.sql b/contrib/btree_gist/sql/int_crosstype.sql
new file mode 100644
index 00000000000..5a388a6b99b
--- /dev/null
+++ b/contrib/btree_gist/sql/int_crosstype.sql
@@ -0,0 +1,230 @@
+-- Cross-type operator support for int2/int4/int8 in GiST.
+--
+-- Verifies that (a) the cross-type B-tree-style operators registered in
+-- gist_int{2,4,8}_ops match the results of seqscans using the same operator
+-- expressions, (b) the KNN <-> operator works across types and uses the
+-- index, and (c) values outside the smaller subtype's range are handled
+-- according to normal comparison semantics, without narrowing or erroring.
+
+--
+-- The integer cross-type support is handled inside the existing
+-- consistent/distance support functions (which dispatch on the subtype OID),
+-- so the operator families must not contain any cross-type (different
+-- left/right input type) support-function entries.
+--
+SELECT opf.opfname AS opfamily,
+       amproc.amproclefttype::regtype AS lefttype,
+       amproc.amprocrighttype::regtype AS righttype,
+       amproc.amprocnum AS procnum,
+       amproc.amproc::regproc AS proc
+FROM pg_amproc amproc
+     JOIN pg_opfamily opf ON opf.oid = amproc.amprocfamily
+     JOIN pg_am am ON am.oid = opf.opfmethod
+WHERE am.amname = 'gist'
+  AND opf.opfname IN ('gist_int2_ops', 'gist_int4_ops', 'gist_int8_ops')
+  AND amproc.amproclefttype <> amproc.amprocrighttype
+ORDER BY opf.opfname,
+         amproc.amproclefttype::regtype::text,
+         amproc.amprocrighttype::regtype::text,
+         amproc.amprocnum,
+         amproc.amproc::regproc::text;
+
+CREATE TABLE ct_i2 (a int2);
+CREATE TABLE ct_i4 (a int4);
+CREATE TABLE ct_i8 (a int8);
+
+INSERT INTO ct_i2 SELECT g::int2 FROM generate_series(-100, 100) g;
+INSERT INTO ct_i4 SELECT g FROM generate_series(-100, 100) g;
+INSERT INTO ct_i8 SELECT g::int8 FROM generate_series(-100, 100) g;
+
+-- Add some values that are representable only in wider types, to exercise
+-- the path where the cross-type query constant is out of range of the key
+-- type.
+INSERT INTO ct_i4 VALUES (100000), (-100000);
+INSERT INTO ct_i8 VALUES (5000000000), (-5000000000);
+
+CREATE INDEX ct_i2_idx ON ct_i2 USING gist (a);
+CREATE INDEX ct_i4_idx ON ct_i4 USING gist (a);
+CREATE INDEX ct_i8_idx ON ct_i8 USING gist (a);
+
+ANALYZE ct_i2;
+ANALYZE ct_i4;
+ANALYZE ct_i8;
+
+SET enable_seqscan = off;
+SET enable_bitmapscan = off;
+
+-- int2 key x int4 query
+SELECT count(*) FROM ct_i2 WHERE a <  50::int4;
+SELECT count(*) FROM ct_i2 WHERE a <= 50::int4;
+SELECT count(*) FROM ct_i2 WHERE a =  50::int4;
+SELECT count(*) FROM ct_i2 WHERE a >= 50::int4;
+SELECT count(*) FROM ct_i2 WHERE a >  50::int4;
+SELECT count(*) FROM ct_i2 WHERE a <> 50::int4;
+
+-- query out of int2 range: matches nothing for =, everything for <>
+SELECT count(*) FROM ct_i2 WHERE a =  100000::int4;
+SELECT count(*) FROM ct_i2 WHERE a <> 100000::int4;
+SELECT count(*) FROM ct_i2 WHERE a <  100000::int4;
+SELECT count(*) FROM ct_i2 WHERE a >  100000::int4;
+
+-- int2 key x int8 query
+SELECT count(*) FROM ct_i2 WHERE a <  50::int8;
+SELECT count(*) FROM ct_i2 WHERE a <= 50::int8;
+SELECT count(*) FROM ct_i2 WHERE a =  50::int8;
+SELECT count(*) FROM ct_i2 WHERE a >= 50::int8;
+SELECT count(*) FROM ct_i2 WHERE a >  50::int8;
+SELECT count(*) FROM ct_i2 WHERE a <> 50::int8;
+SELECT count(*) FROM ct_i2 WHERE a =  5000000000::int8;
+SELECT count(*) FROM ct_i2 WHERE a <  5000000000::int8;
+
+-- int4 key x int2 query
+SELECT count(*) FROM ct_i4 WHERE a <  50::int2;
+SELECT count(*) FROM ct_i4 WHERE a <= 50::int2;
+SELECT count(*) FROM ct_i4 WHERE a =  50::int2;
+SELECT count(*) FROM ct_i4 WHERE a >= 50::int2;
+SELECT count(*) FROM ct_i4 WHERE a >  50::int2;
+SELECT count(*) FROM ct_i4 WHERE a <> 50::int2;
+
+-- int4 key x int8 query
+SELECT count(*) FROM ct_i4 WHERE a <  50::int8;
+SELECT count(*) FROM ct_i4 WHERE a <= 50::int8;
+SELECT count(*) FROM ct_i4 WHERE a =  50::int8;
+SELECT count(*) FROM ct_i4 WHERE a >= 50::int8;
+SELECT count(*) FROM ct_i4 WHERE a >  50::int8;
+SELECT count(*) FROM ct_i4 WHERE a <> 50::int8;
+SELECT count(*) FROM ct_i4 WHERE a =  5000000000::int8;
+SELECT count(*) FROM ct_i4 WHERE a <  5000000000::int8;
+
+-- int8 key x int2 query
+SELECT count(*) FROM ct_i8 WHERE a <  50::int2;
+SELECT count(*) FROM ct_i8 WHERE a <= 50::int2;
+SELECT count(*) FROM ct_i8 WHERE a =  50::int2;
+SELECT count(*) FROM ct_i8 WHERE a >= 50::int2;
+SELECT count(*) FROM ct_i8 WHERE a >  50::int2;
+SELECT count(*) FROM ct_i8 WHERE a <> 50::int2;
+
+-- int8 key x int4 query
+SELECT count(*) FROM ct_i8 WHERE a <  50::int4;
+SELECT count(*) FROM ct_i8 WHERE a <= 50::int4;
+SELECT count(*) FROM ct_i8 WHERE a =  50::int4;
+SELECT count(*) FROM ct_i8 WHERE a >= 50::int4;
+SELECT count(*) FROM ct_i8 WHERE a >  50::int4;
+SELECT count(*) FROM ct_i8 WHERE a <> 50::int4;
+
+-- Confirm the index is actually used for a cross-type predicate.
+EXPLAIN (COSTS OFF)
+SELECT count(*) FROM ct_i4 WHERE a = 50::int8;
+
+-- Cross-type KNN: int4 key ordered by int2 / int8 queries.
+EXPLAIN (COSTS OFF)
+SELECT a FROM ct_i4 ORDER BY a <-> '-100'::int2 LIMIT 3;
+SELECT a FROM ct_i4 ORDER BY a <-> '-100'::int2 LIMIT 3;
+
+EXPLAIN (COSTS OFF)
+SELECT a FROM ct_i4 ORDER BY a <-> '-100'::int8 LIMIT 3;
+SELECT a FROM ct_i4 ORDER BY a <-> '-100'::int8 LIMIT 3;
+
+-- Cross-type KNN: int2 key ordered by int4 / int8 queries.
+SELECT a FROM ct_i2 ORDER BY a <-> '-100'::int4 LIMIT 3;
+SELECT a FROM ct_i2 ORDER BY a <-> '-100'::int8 LIMIT 3;
+
+-- Cross-type KNN: int8 key ordered by int2 / int4 queries.
+SELECT a FROM ct_i8 ORDER BY a <-> '-100'::int2 LIMIT 3;
+SELECT a FROM ct_i8 ORDER BY a <-> '-100'::int4 LIMIT 3;
+
+-- Combined: cross-type WHERE + cross-type ORDER BY on the same index.
+EXPLAIN (COSTS OFF)
+SELECT a FROM ct_i4 WHERE a < 80::int8 ORDER BY a <-> '-100'::int8 LIMIT 3;
+SELECT a FROM ct_i4 WHERE a < 80::int8 ORDER BY a <-> '-100'::int8 LIMIT 3;
+
+-- Standalone distance-function smoke tests (not going through the index),
+-- including the overflow-detection paths.
+SELECT int2_int4_dist(3::int2, 10::int4);
+SELECT int4_int2_dist(-5::int4, 5::int2);
+SELECT int2_int8_dist(3::int2, 10::int8);
+SELECT int8_int2_dist(100::int8, -5::int2);
+SELECT int4_int8_dist(100::int4, 5000000000::int8);
+SELECT int8_int4_dist(5000000000::int8, 100::int4);
+
+-- Overflow detection: INT32_MIN distance from a positive int2 can't fit
+-- in int32, should error.
+SELECT int2_int4_dist(1::int2, -2147483648::int4);
+-- Likewise INT64_MIN distance from a positive int4 can't fit in int64.
+SELECT int4_int8_dist(1::int4, -9223372036854775808::int8);
+
+--
+-- Multi-column GiST index with mixed-type predicates. This is the
+-- original motivating case: without cross-type operator support the
+-- planner can only use one column as an Index Cond and applies the
+-- other(s) as a Filter post-scan. Here both columns should appear as
+-- Index Cond.
+--
+CREATE TABLE ct_multi (a int4, b int8);
+INSERT INTO ct_multi
+    SELECT g, (g * 2)::int8 FROM generate_series(-50, 50) g;
+CREATE INDEX ct_multi_idx ON ct_multi USING gist (a, b);
+ANALYZE ct_multi;
+
+EXPLAIN (COSTS OFF)
+SELECT count(*) FROM ct_multi WHERE a = 25::int8 AND b = 50::int4;
+
+SELECT count(*) FROM ct_multi WHERE a = 25::int8 AND b = 50::int4;
+
+-- Mixed cross-type ranges across both columns.
+EXPLAIN (COSTS OFF)
+SELECT count(*) FROM ct_multi WHERE a < 10::int8 AND b > 0::int2;
+
+SELECT count(*) FROM ct_multi WHERE a < 10::int8 AND b > 0::int2;
+
+DROP TABLE ct_multi;
+
+--
+-- Width-sensitivity guard (int16 vs int32).
+--
+-- The cross-type callbacks must read each side at its OWN width: the query at
+-- the subtype width and the key at the indexed-column width, never narrowing
+-- one to the other. These cases are constructed so that reading either side at
+-- the wrong width changes the answer -- a dispatch entry returning the
+-- wrong-width tinfo, an int4 key truncated to int16, or the query/key arguments
+-- being swapped in gbt_num_consistent()/gbt_num_distance() would all flip the
+-- result, because each wide constant shares its low 16 bits with a value present
+-- in the data. enable_seqscan/bitmapscan are already off, so the GiST
+-- consistent/distance path is exercised.
+--
+CREATE TABLE ct_w4 (k int4);
+INSERT INTO ct_w4 VALUES (1), (65538);		-- 65538 = 0x10002, low 16 bits = 2
+CREATE INDEX ct_w4_idx ON ct_w4 USING gist (k);
+ANALYZE ct_w4;
+
+-- int4 key vs int2 query: 65538 must compare as 65538, not as 2.
+-- correct: {65538}; an int4 key truncated to int16 (65538 -> 2) would give {}.
+SELECT k FROM ct_w4 WHERE k > 1000::int2 ORDER BY k;
+-- correct: {} (no key equals 2); a truncated key (65538 -> 2) would match.
+SELECT k FROM ct_w4 WHERE k = 2::int2 ORDER BY k;
+
+-- KNN: nearest to 2 is 1 (distance 1). enable_sort=off steers the planner to
+-- the GiST KNN distance path rather than a Sort on the operator. A truncated
+-- key (65538 -> 2, distance 0) would wrongly sort 65538 first. (We assert the
+-- result, not the plan shape, to stay portable across visibility-map state.)
+SET enable_sort = off;
+SELECT k FROM ct_w4 ORDER BY k <-> 2::int2 LIMIT 1;
+RESET enable_sort;
+
+CREATE TABLE ct_w2 (k int2);
+INSERT INTO ct_w2 VALUES (1), (50);
+CREATE INDEX ct_w2_idx ON ct_w2 USING gist (k);
+ANALYZE ct_w2;
+
+-- int2 key vs int4 query: 65537 must compare as 65537, not as 1 (0x10001).
+-- correct: 0; an int4 query truncated to int16 (65537 -> 1) would match k=1.
+SELECT count(*) FROM ct_w2 WHERE k = 65537::int4;
+-- correct: 0; a truncated query (65537 -> 1) would match k=50.
+SELECT count(*) FROM ct_w2 WHERE k > 65537::int4;
+
+DROP TABLE ct_w4;
+DROP TABLE ct_w2;
+
+DROP TABLE ct_i2;
+DROP TABLE ct_i4;
+DROP TABLE ct_i8;
-- 
2.51.0



  [application/octet-stream] v5-0003-doc-Document-cross-type-operator-support-in-btree_gi.patch (2.9K, 5-v5-0003-doc-Document-cross-type-operator-support-in-btree_gi.patch)
  download | inline diff:
From efe64c8d1ffbc0729de850c79a47c9d1ce62d54e Mon Sep 17 00:00:00 2001
From: Alexander Nestorov <[email protected]>
Date: Thu, 4 Jun 2026 00:09:08 +0200
Subject: [PATCH] doc: Document cross-type operator support in btree_gist

Note that as of btree_gist 1.10 the int2, int4 and int8 operator
classes accept cross-type query operators (the B-tree comparison
operators and the <-> distance operator) against the other two
integer types without an explicit cast, and that out-of-range query
constants are compared by normal integer semantics.

Also document that the integer <-> distance is computed in float8, so
for int8 distances beyond 2^53 the result loses precision and may
reorder nearly-equidistant matches; it never changes which rows match.
---
 doc/src/sgml/btree-gist.sgml | 24 ++++++++++++++++++++++++
 1 file changed, 24 insertions(+)

diff --git a/doc/src/sgml/btree-gist.sgml b/doc/src/sgml/btree-gist.sgml
index cc09ec83733..1b1dce707cf 100644
--- a/doc/src/sgml/btree-gist.sgml
+++ b/doc/src/sgml/btree-gist.sgml
@@ -49,12 +49,36 @@
   <type>float8</type>, <type>timestamp with time zone</type>,
   <type>timestamp without time zone</type>,
   <type>time without time zone</type>, <type>date</type>, <type>interval</type>,
   <type>oid</type>, and <type>money</type>.
  </para>
 
+ <para>
+  As of version 1.10, the operator classes for <type>int2</type>,
+  <type>int4</type> and <type>int8</type> additionally support cross-type
+  query operators. An index on any one of these integer types can be used
+  with a B-tree-style comparison (<literal>&lt;</literal>,
+  <literal>&lt;=</literal>, <literal>=</literal>, <literal>&gt;=</literal>,
+  <literal>&gt;</literal>, <literal>&lt;&gt;</literal>) or with the
+  distance operator (<literal>&lt;-&gt;</literal>) against a value of any
+  of the other two integer types, without an explicit cast. Query
+  constants outside the indexed column's range are compared according to
+  normal integer comparison semantics, without narrowing the query value
+  to the column type first.
+ </para>
+
+ <para>
+  The integer distance operator (<literal>&lt;-&gt;</literal>) used for
+  nearest-neighbor (<literal>ORDER BY</literal>) searches computes the
+  distance in <type>float8</type>. For <type>int8</type> values whose
+  distance from the query exceeds 2^53, the result loses precision and
+  may reorder results that are nearly equidistant; it never changes which
+  rows match. This applies equally to same-type and cross-type
+  <type>int8</type> distance searches.
+ </para>
+
  <para>
   By default <filename>btree_gist</filename> builds <acronym>GiST</acronym> index with
   <function>sortsupport</function> in <firstterm>sorted</firstterm> mode. This usually results in
   much faster index built speed. It is still possible to revert to buffered built strategy
   by using the <literal>buffering</literal> parameter when creating the index.
  </para>
-- 
2.51.0



^ permalink  raw  reply  [nested|flat] 9+ messages in thread


end of thread, other threads:[~2026-06-24 15:34 UTC | newest]

Thread overview: 9+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-05-02 17:26 [PATCH] btree_gist: add cross-type integer operator support for GiST Alexander Nestorov <[email protected]>
2026-06-03 22:16 ` Alexander Nestorov <[email protected]>
2026-06-05 18:49 ` Andrey Borodin <[email protected]>
2026-06-06 17:37   ` Alexander Nestorov <[email protected]>
2026-06-13 23:35     ` Alexander Nestorov <[email protected]>
2026-06-22 12:20       ` Maxime Schoemans <[email protected]>
2026-06-23 22:53         ` Alexander Nestorov <[email protected]>
2026-06-24 10:41           ` Maxime Schoemans <[email protected]>
2026-06-24 15:34             ` Alexander Nestorov <[email protected]>

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox