From 2b63ce66833ad4e9180b04b4208d77d0fe52760d Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Sat, 1 Aug 2026 23:47:53 +0500
Subject: [PATCH v3] 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 <pg@bowt.ie>
Discussion: https://postgr.es/m/CAH2-Wzm1GpQ9qixP=Xs2TYdfnLM88OZ=yUFHNuajS_W7U90-tQ@mail.gmail.com
Author: Andrey Borodin <x4mmm@yandex-team.ru>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
---
 src/backend/utils/adt/rangetypes_gist.c       | 59 ++++++++++++++++++-
 src/test/regress/expected/multirangetypes.out | 12 ++++
 src/test/regress/sql/multirangetypes.sql      |  2 +
 3 files changed, 71 insertions(+), 2 deletions(-)

diff --git a/src/backend/utils/adt/rangetypes_gist.c b/src/backend/utils/adt/rangetypes_gist.c
index 1a01a8f4c3c..629e635054d 100644
--- a/src/backend/utils/adt/rangetypes_gist.c
+++ b/src/backend/utils/adt/rangetypes_gist.c
@@ -265,6 +265,47 @@ multirange_gist_compress(PG_FUNCTION_ARGS)
 	PG_RETURN_POINTER(entry);
 }
 
+/*
+ * Check if give 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 multiranges 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);
+
+	if (range_cmp_bounds(rangetyp, &lower1, &lower2) <= 0 &&
+		range_cmp_bounds(rangetyp, &upper1, &upper2) >= 0)
+		return true;
+	else
+		return false;
+}
+
+
 /* GiST query consistency check for multiranges */
 Datum
 multirange_gist_consistent(PG_FUNCTION_ARGS)
@@ -295,8 +336,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)

