From bdce40e8a75eea4f1df778376042dbcf8246d3a4 Mon Sep 17 00:00:00 2001 From: Alexander Nestorov 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 --- 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