GiST multirange index scans can fail to return rows
Hackorum builds and tests every patch posted to the lists, not only commitfest submissions. This is Hackorum's own CI rather than the PostgreSQL project's, and it is still under testing - please report anything that looks wrong.
You can run a PostgreSQL built from this patch straight from Docker, with no checkout and no build:
docker run --rm -p 5432:5432 ghcr.io/hackorum-dev/postgres-patch:t253275psql -h localhost -U postgresBuilt from patchset v7 (message #7), August 23, 2026 at 04:10 AM.
Every patchset is also pushed to a branch of our PostgreSQL fork, so you can check out the same tree CI built. Without a PostgreSQL checkout:
git clone --branch t253275_7 https://github.com/hackorum-dev/postgres.gitIn a checkout you already have, add the fork once:
git remote add hackorum https://github.com/hackorum-dev/postgres.gitthen, for this patchset and every later one:
git fetch hackorum t253275_7 && git checkout t253275_7Patchset v7 (message #7) is on t253275_7
The attached regression test patch (written by Claude Code) shows that
a GiST index on a multirange column can fail to return rows during
scans that use the contained-by operator.
The multirangetypes test already runs every GiST query twice, once
with a sequential scan and once with the index, and the two must
agree. The added query asks for rows contained in
'{[1,11),[21,31),[41,51)}', which is a value the test table already
contains. The sequential scan returns 501 and the index scan returns
500. The row it loses has a multirange that exactly matches the one
from the query.
The multirange GiST support was added by db6335b5b. This is an issue
on 14 and up.
--
Peter Geoghegan
On 1 Aug 2026, at 20:01, Peter Geoghegan <pg@bowt.ie> wrote:
<0001-Add-regression-test-for-multirange-GiST-containment-.patch>
Hi Peter,
I agree with the test and also think this is a bug. The check in
multirange_gist_consistent() is too strict and rejects tuples that
should reach the recheck. I think we should use
range_gist_consistent_int_multirange() for this case. Using the
internal predicate for every strategy would also be correct, but would
weaken filtering and, IMO, cause unnecessary heap rechecks.
PFA v2 patch set. The first patch is your test, and the second is the
fix as I see it.
Thanks!
Best regards, Andrey Borodin.
Attachments:
v2-0001-Add-regression-test-for-multirange-GiST-contained.patchapplication/octet-stream; name=v2-0001-Add-regression-test-for-multirange-GiST-contained.patch; x-unix-mode=0644Download+14-1
v2-0001-Add-regression-test-for-multirange-GiST-contained.patchapplication/octet-stream; name=v2-0001-Add-regression-test-for-multirange-GiST-contained.patch; x-unix-mode=0644Download+14-1
Hi Andrey,
I think that you forgot to attach the patch with the actual fix?
--
Peter Geoghegan
On Sun, Aug 2, 2026 at 2:12 AM Andrey Borodin <x4mmm@yandex-team.ru> wrote:
On 1 Aug 2026, at 20:01, Peter Geoghegan <pg@bowt.ie> wrote:
<0001-Add-regression-test-for-multirange-GiST-containment-.patch>
Hi Peter,
I agree with the test and also think this is a bug. The check in
multirange_gist_consistent() is too strict and rejects tuples that
should reach the recheck. I think we should use
range_gist_consistent_int_multirange() for this case. Using the
internal predicate for every strategy would also be correct, but would
weaken filtering and, IMO, cause unnecessary heap rechecks.PFA v2 patch set. The first patch is your test, and the second is the
fix as I see it.Thanks!
Best regards, Andrey Borodin.
--
Peter Geoghegan
On 2 Aug 2026, at 12:03, Peter Geoghegan <pg@bowt.ie> wrote:
I think that you forgot to attach the patch with the actual fix?
Oops. Sorry.
Best regards, Andrey Borodin.
Attachments:
t253275_4v2-0002-Fix-GiST-contained-by-scans-of-multiranges.patchapplication/octet-stream; name=v2-0002-Fix-GiST-contained-by-scans-of-multiranges.patch; x-unix-mode=0644Download+16-3
v2-0001-Add-regression-test-for-multirange-GiST-contained.patchapplication/octet-stream; name=v2-0001-Add-regression-test-for-multirange-GiST-contained.patch; x-unix-mode=0644Download+14-1
Hi Peter,
Hi Andrey,
On Sun, Aug 2, 2026 at 9:55 AM Andrey Borodin <x4mmm@yandex-team.ru> wrote:
On 2 Aug 2026, at 12:03, Peter Geoghegan <pg@bowt.ie> wrote:
I think that you forgot to attach the patch with the actual fix?
Oops. Sorry.
Peter, thank you for catching this. Andrey, thank you for the patch.
Yes, multirange_gist_consistent() re-uses consistent functions from
range opclass. And range_gist_consistent_leaf_multirange() generate
false negatives when applied to union range of multirange. I've
rechecked, and it appears to be the only case among the predicates.
Regarding this contained-by case, I think we can build more precise
test function. If we have multiranges q and k, and union() produces
union range from multirange.
q @> r doesn't mean q @> union(r)
But if q @> r then union(q) @> union(r)
------
Regards,
Alexander Korotkov
Supabase
On Sun, 2 Aug 2026 at 20:30, Alexander Korotkov <aekorotkov@gmail.com> wrote:
Hi Peter,
Hi Andrey,On Sun, Aug 2, 2026 at 9:55 AM Andrey Borodin <x4mmm@yandex-team.ru> wrote:
On 2 Aug 2026, at 12:03, Peter Geoghegan <pg@bowt.ie> wrote:
I think that you forgot to attach the patch with the actual fix?
Oops. Sorry.
Peter, thank you for catching this. Andrey, thank you for the patch.
Yes, multirange_gist_consistent() re-uses consistent functions from
range opclass. And range_gist_consistent_leaf_multirange() generate
false negatives when applied to union range of multirange. I've
rechecked, and it appears to be the only case among the predicates.Regarding this contained-by case, I think we can build more precise
test function. If we have multiranges q and k, and union() produces
union range from multirange.q @> r doesn't mean q @> union(r)
But if q @> r then union(q) @> union(r)
Hi!
I reviewed v3.
multirange_contains_union_range function comment contains typo at 'give'
'Every multiranges contains an infinite number of empty multiranges,
even an empty one.' also contain typos
Return statement can be :
return range_cmp_bounds(rangetyp, &lower1, &lower2) <= 0 &&
range_cmp_bounds(rangetyp, &upper1, &upper2) >= 0;
But I don't insist. Better for my taste.
--
Best regards,
Kirill Reshke
On Sun, Aug 2, 2026 at 8:04 PM Kirill Reshke <reshkekirill@gmail.com> wrote:
On Sun, 2 Aug 2026 at 20:30, Alexander Korotkov <aekorotkov@gmail.com> wrote:
On Sun, Aug 2, 2026 at 9:55 AM Andrey Borodin <x4mmm@yandex-team.ru> wrote:
On 2 Aug 2026, at 12:03, Peter Geoghegan <pg@bowt.ie> wrote:
I think that you forgot to attach the patch with the actual fix?
Oops. Sorry.
Peter, thank you for catching this. Andrey, thank you for the patch.
Yes, multirange_gist_consistent() re-uses consistent functions from
range opclass. And range_gist_consistent_leaf_multirange() generate
false negatives when applied to union range of multirange. I've
rechecked, and it appears to be the only case among the predicates.Regarding this contained-by case, I think we can build more precise
test function. If we have multiranges q and k, and union() produces
union range from multirange.q @> r doesn't mean q @> union(r)
But if q @> r then union(q) @> union(r)Hi!
I reviewed v3.multirange_contains_union_range function comment contains typo at 'give'
'Every multiranges contains an infinite number of empty multiranges,
even an empty one.' also contain typos
Thank you, fixed.
Return statement can be :
return range_cmp_bounds(rangetyp, &lower1, &lower2) <= 0 &&
range_cmp_bounds(rangetyp, &upper1, &upper2) >= 0;But I don't insist. Better for my taste.
Corrected as you proposed.
------
Regards,
Alexander Korotkov
Supabase
On 2 Aug 2026, at 23:11, Alexander Korotkov <aekorotkov@gmail.com> wrote:
<v4-0001-Fix-GiST-contained-by-scans-of-multiranges.patch>
Hi Alexander,
I read v4 and think that the new predicate is correct.
This comment does not look perfect to me:
Every multirange contains an infinite number of empty multiranges,
even an empty one.
Perhaps just say "The empty multirange is contained by every multirange."
There's typo "multirage" in the commit message :)
Thank you!
Best regards, Andrey Borodin.