From 0716c44d079541a9d4e9e66cc8776ce242e022d0 Mon Sep 17 00:00:00 2001 From: Peter Geoghegan Date: Sat, 1 Aug 2026 23:47:53 +0500 Subject: [PATCH v4] Fix GiST contained-by scans of multiranges GiST stores a multirange leaf key as its union range. When both the indexed value and the query have gaps, the indexed multirange can be contained by the query even though its union range is not. The current leaf check can therefore discard a matching row before heap recheck. Implement a special test function for this case. Union range of query multirage must contain the leaf key union range. Back-patch to all supported branches. Reported-by: Peter Geoghegan Discussion: https://postgr.es/m/CAH2-Wzm1GpQ9qixP=Xs2TYdfnLM88OZ=yUFHNuajS_W7U90-tQ@mail.gmail.com Author: Andrey Borodin Co-authored-by: Claude Opus 5 (1M context) --- src/backend/utils/adt/rangetypes_gist.c | 56 ++++++++++++++++++- src/test/regress/expected/multirangetypes.out | 12 ++++ src/test/regress/sql/multirangetypes.sql | 2 + 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/src/backend/utils/adt/rangetypes_gist.c b/src/backend/utils/adt/rangetypes_gist.c index 1a01a8f4c3c..56c1f80f280 100644 --- a/src/backend/utils/adt/rangetypes_gist.c +++ b/src/backend/utils/adt/rangetypes_gist.c @@ -265,6 +265,44 @@ multirange_gist_compress(PG_FUNCTION_ARGS) PG_RETURN_POINTER(entry); } +/* + * Check if given multirange might contain another multirange by the union + * range of latter. We can't consider multirange gaps as union range don't + * contain them. But the union range must contain another union range. + */ +static bool +multirange_contains_union_range(TypeCacheEntry *rangetyp, + const MultirangeType *mr, + const RangeType *r) +{ + RangeBound lower1, + upper1, + lower2, + upper2, + tmp; + bool empty; + + /* + * Every multirange contains an infinite number of empty multiranges, even + * an empty one. + */ + if (RangeIsEmpty(r)) + return true; + + if (MultirangeIsEmpty(mr)) + return false; + + /* Multirange might contain multirange if union range contains union range */ + range_deserialize(rangetyp, r, &lower2, &upper2, &empty); + Assert(!empty); + multirange_get_bounds(rangetyp, mr, 0, &lower1, &tmp); + multirange_get_bounds(rangetyp, mr, mr->rangeCount - 1, &tmp, &upper1); + + return (range_cmp_bounds(rangetyp, &lower1, &lower2) <= 0 && + range_cmp_bounds(rangetyp, &upper1, &upper2) >= 0); +} + + /* GiST query consistency check for multiranges */ Datum multirange_gist_consistent(PG_FUNCTION_ARGS) @@ -295,8 +333,22 @@ multirange_gist_consistent(PG_FUNCTION_ARGS) if (GIST_LEAF(entry)) { if (!OidIsValid(subtype) || subtype == ANYMULTIRANGEOID) - result = range_gist_consistent_leaf_multirange(typcache, strategy, key, - DatumGetMultirangeTypeP(query)); + { + /* + * The union range is not necessarily contained by a multirange + * that contains the original multirange, because it also covers + * the gaps in the original multirange. Use a special test + * function for this case. + */ + if (strategy != RANGESTRAT_CONTAINED_BY) + result = range_gist_consistent_leaf_multirange(typcache, strategy, + key, + DatumGetMultirangeTypeP(query)); + else + result = multirange_contains_union_range(typcache, + DatumGetMultirangeTypeP(query), + key); + } else if (subtype == ANYRANGEOID) result = range_gist_consistent_leaf_range(typcache, strategy, key, DatumGetRangeTypeP(query)); diff --git a/src/test/regress/expected/multirangetypes.out b/src/test/regress/expected/multirangetypes.out index d47ce4b6d6a..f6b1158ac41 100644 --- a/src/test/regress/expected/multirangetypes.out +++ b/src/test/regress/expected/multirangetypes.out @@ -2610,6 +2610,12 @@ select count(*) from test_multirange_gist where mr <@ '{(10,30),(40,60),(70,90)} 500 (1 row) +select count(*) from test_multirange_gist where mr <@ '{[1,11),[21,31),[41,51)}'::int4multirange; + count +------- + 501 +(1 row) + select count(*) from test_multirange_gist where mr << int4multirange(int4range(100,200), int4range(400,500)); count ------- @@ -2842,6 +2848,12 @@ select count(*) from test_multirange_gist where mr <@ '{(10,30),(40,60),(70,90)} 500 (1 row) +select count(*) from test_multirange_gist where mr <@ '{[1,11),[21,31),[41,51)}'::int4multirange; + count +------- + 501 +(1 row) + select count(*) from test_multirange_gist where mr << int4multirange(int4range(100,200), int4range(400,500)); count ------- diff --git a/src/test/regress/sql/multirangetypes.sql b/src/test/regress/sql/multirangetypes.sql index cf0fff6fddd..5ed7f9cbd9f 100644 --- a/src/test/regress/sql/multirangetypes.sql +++ b/src/test/regress/sql/multirangetypes.sql @@ -505,6 +505,7 @@ select count(*) from test_multirange_gist where mr @> '{}'::int4multirange; select count(*) from test_multirange_gist where mr @> int4multirange(int4range(10,20), int4range(30,40)); select count(*) from test_multirange_gist where mr && '{(10,20),(30,40),(50,60)}'::int4multirange; select count(*) from test_multirange_gist where mr <@ '{(10,30),(40,60),(70,90)}'::int4multirange; +select count(*) from test_multirange_gist where mr <@ '{[1,11),[21,31),[41,51)}'::int4multirange; select count(*) from test_multirange_gist where mr << int4multirange(int4range(100,200), int4range(400,500)); select count(*) from test_multirange_gist where mr >> int4multirange(int4range(100,200), int4range(400,500)); select count(*) from test_multirange_gist where mr &< int4multirange(int4range(100,200), int4range(400,500)); @@ -550,6 +551,7 @@ select count(*) from test_multirange_gist where mr @> '{}'::int4multirange; select count(*) from test_multirange_gist where mr @> int4multirange(int4range(10,20), int4range(30,40)); select count(*) from test_multirange_gist where mr && '{(10,20),(30,40),(50,60)}'::int4multirange; select count(*) from test_multirange_gist where mr <@ '{(10,30),(40,60),(70,90)}'::int4multirange; +select count(*) from test_multirange_gist where mr <@ '{[1,11),[21,31),[41,51)}'::int4multirange; select count(*) from test_multirange_gist where mr << int4multirange(int4range(100,200), int4range(400,500)); select count(*) from test_multirange_gist where mr >> int4multirange(int4range(100,200), int4range(400,500)); select count(*) from test_multirange_gist where mr &< int4multirange(int4range(100,200), int4range(400,500)); -- 2.50.1 (Apple Git-155)