On 10/29/26 05:03, Kirill Reshke wrote:
> On Thu, 29 Oct 2026 at 04:05, Tomas Vondra <[email protected]> wrote:
>>
>>
>>
>> On 10/28/26 08:02, Kirill Reshke wrote:
>>> On Wed, 28 Oct 2026 at 01:59, Tomas Vondra <[email protected]> wrote:
>>>>
>>>> On 10/27/26 19:59, Kirill Reshke wrote:
>>>>> Hi
>>>>> While reviewing some reports in pgsql-bugs I bumped into simple SQL
>>>>> reproduced of BRIN out of range issue
>>>>>
>>>>> CREATE TABLE t (n numeric);
>>>>> INSERT INTO t VALUES (1), (1e1000);
>>>>> CREATE INDEX ON t USING brin (n numeric_minmax_multi_ops);
>>>>>
>>>>> This fails. brin_minmax_multi_distance_numeric computes heuristic, so
>>>>> it can be approximated. Patch uses numeric_sub_safe with an
>>>>> ErrorSaveContext and returns inf if any error occurred (which can only
>>>>> be an overflow in current code base, see make_result_safe)
>>>>>
>>>>> The numeric-distance overflow risk was discussed during the original
>>>>> minmax-multi development in 2018 [0], with normalizing value into 0-1
>>>>> range. I didn't fix that way, the issue fixed in the current
>>>>> implementation.
>>>>>
>>>>
>>>> Thanks for the report. I'll take a closer look at the fix, but it seems
>>>> like the right approach to me.
>>>
>>> Thanks for looking.
>>>
>>>> I wonder if we need to do anything for the other distance functions.
>>>
>>> Looks like there is an same issue with numrange_subdiff, like
>>>
>>> postgres=# SELECT numrange_subdiff('1e1000'::numeric, '2e1000'::numeric);
>>> ERROR:
>>> "-10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
>>> is out of range for type double precision
>>>
>>> Attached fixes numrange_subdiff in the same way as brin numeric distance.
>>>
>>> numrange_subdiff is called from the planner without try/catch or
>>> anything, so this overflow impact should be query failure, not index
>>> build failure.
>>>
>>> I did not however reproduced query failure, because of ANALYZE failure:
>>>
>>> ```
>>> postgres=# CREATE TABLE t2 (r nummultirange);
>>> CREATE TABLE
>>> postgres=# INSERT INTO t2 VALUES (
>>> '{[1e1000, 2e1000], [-9e131071, 9e131071]}'::nummultirange
>>> );
>>> INSERT 0 1
>>> postgres=# ANALYZE t;
>>> ERROR: value overflows numeric format
>>> ```
>>>
>>> so a table with extreme numeric bounds cannot be analyzed at all.
>>>
>>
>> Ah, but that's completely unrelated to the BRIN code, right? I was
>> puzzled for a bit, because I don't think we have minmax-multi opclasses
>> for [multi]ranges.
>
> Yes, but you were wondering about other distance functions, so I
> started to search for alike problems... and found one that is neither
> BRIN nor distance, my bad, should be a separate thread. The only thing
> they share is how fix looks - almost the same code to be changed .
>
Yup. It's a good catch, it just confused me for a bit.
I spent a bit of time polishing this up today, I'm attaching two patch
series - one for 19-master, one for 18 (and older). The numeric_sub_safe
was introduced in 19, older branches need to use numeric_sub_opt_error.
Now that I'm thinking about it, are the numeric overflow fixes actually
correct? They do this:
d = numeric_sub_safe(a2, a1, (Node *) &escontext);
if (SOFT_ERROR_OCCURRED(&escontext))
distance = get_float8_infinity();
But doesn't that simply mean the value does not fit into numeric, which
means some number of digits before/after decimal point. Can't it happen
that we end up with too small difference? Maybe that can't happen for
subtraction?
While looking at the BRIN minmax-multi code, I found two other issues in
the distance calculation for TID and OID. Those are less serious, as it
won't break the index, it just makes it less efficient (by constructing
ranges that are very wide).
For "tid", the item pointer -> double mapping was done in uint32, so it
wrapped every ~112GB or so. That was easy to fix by switching to double
arithmetics. There's also a minor issue with using MaxHeapTuplesPerPage,
which the tids may not respect. I didn't do anything about it, it seems
very unlikely in practice.
For OID, the issue is that it used "int4" distance, but that treats
values as int32 and not uint32 (as it should, for OIDs). The proper fix
needs a new pg_amproc entry, so can't be backpatched.
Both the TID/OID cases seem very low impact, few people are using BRIN
on those data types. And it merely makes the indexes less efficient (for
rare cases), not wrong.
regards
--
Tomas Vondra
From 33b660825828cd20aeaddbf577f680709fa008e4 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <[email protected]>
Date: Fri, 30 Oct 2026 14:12:04 +0100
Subject: [PATCH v18 1/3] Handle numeric overflow in numeric_minmax_multi_ops
When calculating the distance between numeric values, the code could
overflow in two ways. A subtraction of two finite values may still
overflow, or the result may not fit into float8.
The distances don't need to be perfectly accurate, although it may make
the index less efficient. And for most minmax-multi opclasses it works
like that, but for numeric the functions may error-out. That prevents
summarization, either when creating an idex or later during vacuum.
Fix by using numeric_sub_safe() and numeric_float8_no_overflow(), and
returning infinity when either of those fails. The older releases need
to use numeric_sub_opt_error(), but the principle is the same.
While at it, free the detoasted copies of arguments, and the calculated
numeric distance. For small values it does not matter, but for extreme
numeric values it may make a difference.
Backpatch to 14, where minmax-multi indexes were introduced.
Author: Kirill Reshke <[email protected]>
Backpatch-through: 14
Discussion: https://postgr.es/m/CALdSSPgBvXz-VSF0p-5k%3D_uFfN38cWrERmm_AC4K67N6MWQhmg%40mail.gmail.com
---
src/backend/access/brin/brin_minmax_multi.c | 31 +++++++++++++++++----
src/test/regress/expected/brin_multi.out | 10 +++++++
src/test/regress/sql/brin_multi.sql | 14 ++++++++++
3 files changed, 49 insertions(+), 6 deletions(-)
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index 4755c3d0af3..2795a86d033 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -77,6 +77,7 @@
#include "utils/inet.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
+#include "utils/numeric.h"
#include "utils/pg_lsn.h"
#include "utils/rel.h"
#include "utils/syscache.h"
@@ -2020,19 +2021,37 @@ brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS)
Datum
brin_minmax_multi_distance_numeric(PG_FUNCTION_ARGS)
{
- Datum d;
- Datum a1 = PG_GETARG_DATUM(0);
- Datum a2 = PG_GETARG_DATUM(1);
+ Numeric d;
+ Numeric a1 = PG_GETARG_NUMERIC(0);
+ Numeric a2 = PG_GETARG_NUMERIC(1);
+ float8 distance;
+ bool have_error = false;
/*
* We know the values are range boundaries, but the range may be collapsed
* (i.e. single points), with equal values.
*/
- Assert(DatumGetBool(DirectFunctionCall2(numeric_le, a1, a2)));
+ Assert(DatumGetBool(DirectFunctionCall2(numeric_le, NumericGetDatum(a1),
+ NumericGetDatum(a2))));
+
+ /*
+ * Calculate the float8 distance in an overflow-safe way. Return infinity
+ * instead of error-ing out.
+ */
+ d = numeric_sub_opt_error(a2, a1, &have_error);
+ if (have_error)
+ distance = get_float8_infinity();
+ else
+ {
+ distance = DatumGetFloat8(DirectFunctionCall1(numeric_float8_no_overflow,
+ NumericGetDatum(d)));
+ pfree(d);
+ }
- d = DirectFunctionCall2(numeric_sub, a2, a1); /* a2 - a1 */
+ PG_FREE_IF_COPY(a1, 0);
+ PG_FREE_IF_COPY(a2, 1);
- PG_RETURN_DATUM(DirectFunctionCall1(numeric_float8, d));
+ PG_RETURN_FLOAT8(distance);
}
/*
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
index cb5b5e53e86..300abc2e1ab 100644
--- a/src/test/regress/expected/brin_multi.out
+++ b/src/test/regress/expected/brin_multi.out
@@ -981,3 +981,13 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
DROP TABLE brin_interval_test;
RESET enable_seqscan;
RESET datestyle;
+-- test handling of overflow in numeric values
+CREATE TABLE brin_overflow_test (n numeric);
+-- overflow in subtraction
+INSERT INTO brin_overflow_test VALUES (-repeat('9', 131072)::numeric), (repeat('9', 131072)::numeric);
+CREATE INDEX ON brin_overflow_test USING brin (n numeric_minmax_multi_ops);
+TRUNCATE brin_overflow_test;
+-- overflow in conversion to float8
+INSERT INTO brin_overflow_test VALUES (1), (1e500);
+CREATE INDEX ON brin_overflow_test USING brin (n numeric_minmax_multi_ops);
+DROP TABLE brin_overflow_test;
diff --git a/src/test/regress/sql/brin_multi.sql b/src/test/regress/sql/brin_multi.sql
index dda9fb73e0c..2cbdff00047 100644
--- a/src/test/regress/sql/brin_multi.sql
+++ b/src/test/regress/sql/brin_multi.sql
@@ -704,3 +704,17 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
DROP TABLE brin_interval_test;
RESET enable_seqscan;
RESET datestyle;
+
+-- test handling of overflow in numeric values
+CREATE TABLE brin_overflow_test (n numeric);
+
+-- overflow in subtraction
+INSERT INTO brin_overflow_test VALUES (-repeat('9', 131072)::numeric), (repeat('9', 131072)::numeric);
+CREATE INDEX ON brin_overflow_test USING brin (n numeric_minmax_multi_ops);
+TRUNCATE brin_overflow_test;
+
+-- overflow in conversion to float8
+INSERT INTO brin_overflow_test VALUES (1), (1e500);
+CREATE INDEX ON brin_overflow_test USING brin (n numeric_minmax_multi_ops);
+
+DROP TABLE brin_overflow_test;
--
2.55.0
From aef274b212aa1186e5ea933fa48928a0f037d47e Mon Sep 17 00:00:00 2001
From: Tomas Vondra <[email protected]>
Date: Fri, 30 Oct 2026 16:01:29 +0100
Subject: [PATCH v18 2/3] Fix BRIN minmax-multi distance for tid values
The distance function for tid values maps item pointers to float8 as
(block * MaxHeapTuplesPerPage + offset)
but the multiplication was done in uint32 arithmetic, so for high block
values (above 14759338, ~112GB) the result wrapped around. The distance
would then be very inaccurate (possibly negative), resulting in a less
efficient index.
Fix by doing the arithmetics in double.
It's also possible the (user-supplied) item pointers have offsets
greater than MaxHeapTuplesPerPage, confusing the mapping to double
value. This commit does not try to address that, it's not clear what
would be a better mapping.
Author: Tomas Vondra <[email protected]>
Backpatch-through: 14
Discussion: https://postgr.es/m/CALdSSPgBvXz-VSF0p-5k%3D_uFfN38cWrERmm_AC4K67N6MWQhmg%40mail.gmail.com
---
src/backend/access/brin/brin_minmax_multi.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index 2795a86d033..a19dfe30ca6 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -2006,10 +2006,10 @@ brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS)
* We use the no-check variants here, because user-supplied values may
* have (ip_posid == 0). See ItemPointerCompare.
*/
- da1 = ItemPointerGetBlockNumberNoCheck(pa1) * MaxHeapTuplesPerPage +
+ da1 = (double) ItemPointerGetBlockNumberNoCheck(pa1) * MaxHeapTuplesPerPage +
ItemPointerGetOffsetNumberNoCheck(pa1);
- da2 = ItemPointerGetBlockNumberNoCheck(pa2) * MaxHeapTuplesPerPage +
+ da2 = (double) ItemPointerGetBlockNumberNoCheck(pa2) * MaxHeapTuplesPerPage +
ItemPointerGetOffsetNumberNoCheck(pa2);
PG_RETURN_FLOAT8(da2 - da1);
--
2.55.0
From 53c1c378014dfa6cd9addeb63fce5365a0a33679 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <[email protected]>
Date: Fri, 30 Oct 2026 16:58:13 +0100
Subject: [PATCH v18 3/3] Handle overflow when analyzing nummultirange values
The numrange_subdiff() function was calculating the difference simply by
calling numeric_sub and numeric_float8, but those functions are not
overflow-safe. It's possible to insert valid numeric ranges for which
the difference overflows either numeric or float8. When that happened,
the ANALYZE on the table failed.
Fixed by handling overflows in numrange_subdiff() - extreme differences
are treated as infinity.
Author: Kirill Reshke <[email protected]>
Backpatch-through: 14
Discussion: https://postgr.es/m/CALdSSPgBvXz-VSF0p-5k%3D_uFfN38cWrERmm_AC4K67N6MWQhmg%40mail.gmail.com
---
src/backend/utils/adt/rangetypes.c | 28 +++++++++++++++----
src/test/regress/expected/multirangetypes.out | 7 +++++
src/test/regress/sql/multirangetypes.sql | 9 ++++++
3 files changed, 38 insertions(+), 6 deletions(-)
diff --git a/src/backend/utils/adt/rangetypes.c b/src/backend/utils/adt/rangetypes.c
index 66cc0acf4a7..6ef12003218 100644
--- a/src/backend/utils/adt/rangetypes.c
+++ b/src/backend/utils/adt/rangetypes.c
@@ -41,7 +41,9 @@
#include "optimizer/optimizer.h"
#include "utils/builtins.h"
#include "utils/date.h"
+#include "utils/float.h"
#include "utils/lsyscache.h"
+#include "utils/numeric.h"
#include "utils/rangetypes.h"
#include "utils/sortsupport.h"
#include "utils/timestamp.h"
@@ -1702,15 +1704,29 @@ int8range_subdiff(PG_FUNCTION_ARGS)
Datum
numrange_subdiff(PG_FUNCTION_ARGS)
{
- Datum v1 = PG_GETARG_DATUM(0);
- Datum v2 = PG_GETARG_DATUM(1);
- Datum numresult;
+ Numeric v1 = PG_GETARG_NUMERIC(0);
+ Numeric v2 = PG_GETARG_NUMERIC(1);
+ Numeric numresult;
float8 floatresult;
+ bool have_error = false;
- numresult = DirectFunctionCall2(numeric_sub, v1, v2);
+ /*
+ * The difference can exceed numeric's range, or be unrepresentable
+ * in float8. Neither caller is prepared to handle an error, so
+ * saturate to infinity (or round to zero) instead.
+ */
+ numresult = numeric_sub_opt_error(v1, v2, &have_error);
+ if (have_error)
+ floatresult = get_float8_infinity();
+ else
+ {
+ floatresult = DatumGetFloat8(DirectFunctionCall1(
+ numeric_float8_no_overflow, NumericGetDatum(numresult)));
+ pfree(numresult);
+ }
- floatresult = DatumGetFloat8(DirectFunctionCall1(numeric_float8,
- numresult));
+ PG_FREE_IF_COPY(v1, 0);
+ PG_FREE_IF_COPY(v2, 1);
PG_RETURN_FLOAT8(floatresult);
}
diff --git a/src/test/regress/expected/multirangetypes.out b/src/test/regress/expected/multirangetypes.out
index 76ab1430c4c..e10e7debba6 100644
--- a/src/test/regress/expected/multirangetypes.out
+++ b/src/test/regress/expected/multirangetypes.out
@@ -3081,6 +3081,13 @@ ERROR: type "intr_multirange" already exists
DETAIL: Failed while creating a multirange type for type "intr".
HINT: You can manually specify a multirange type name using the "multirange_type_name" attribute.
drop type intr_multirange;
+---
+-- Check that numeric overflow during analyze does not make it fail
+---
+create table multirange_analyze_test (r nummultirange);
+insert into multirange_analyze_test values ('{[1e1000, 2e1000], [-9e131071, 9e131071]}'::nummultirange);
+analyze multirange_analyze_test;
+drop table multirange_analyze_test;
--
-- Test multiple multirange types over the same subtype and manual naming of
-- the multirange type.
diff --git a/src/test/regress/sql/multirangetypes.sql b/src/test/regress/sql/multirangetypes.sql
index c7953aad295..770ac522e84 100644
--- a/src/test/regress/sql/multirangetypes.sql
+++ b/src/test/regress/sql/multirangetypes.sql
@@ -680,6 +680,15 @@ create type intr_multirange as (x int, y int);
create type intr as range(subtype=int); -- should fail
drop type intr_multirange;
+---
+-- Check that numeric overflow during analyze does not make it fail
+---
+
+create table multirange_analyze_test (r nummultirange);
+insert into multirange_analyze_test values ('{[1e1000, 2e1000], [-9e131071, 9e131071]}'::nummultirange);
+analyze multirange_analyze_test;
+drop table multirange_analyze_test;
+
--
-- Test multiple multirange types over the same subtype and manual naming of
-- the multirange type.
--
2.55.0
From 1839ddc72adf78925aa2ff2f873c14caf1bb0fe3 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <[email protected]>
Date: Fri, 30 Oct 2026 14:12:04 +0100
Subject: [PATCH vmaster 1/4] Handle numeric overflow in
numeric_minmax_multi_ops
When calculating the distance between numeric values, the code could
overflow in two ways. A subtraction of two finite values may still
overflow, or the result may not fit into float8.
The distances don't need to be perfectly accurate, although it may make
the index less efficient. And for most minmax-multi opclasses it works
like that, but for numeric the functions may error-out. That prevents
summarization, either when creating an idex or later during vacuum.
Fix by using numeric_sub_safe() and numeric_float8_no_overflow(), and
returning infinity when either of those fails. The older releases need
to use numeric_sub_opt_error(), but the principle is the same.
While at it, free the detoasted copies of arguments, and the calculated
numeric distance. For small values it does not matter, but for extreme
numeric values it may make a difference.
Backpatch to 14, where minmax-multi indexes were introduced.
Author: Kirill Reshke <[email protected]>
Backpatch-through: 14
Discussion: https://postgr.es/m/CALdSSPgBvXz-VSF0p-5k%3D_uFfN38cWrERmm_AC4K67N6MWQhmg%40mail.gmail.com
---
src/backend/access/brin/brin_minmax_multi.c | 32 +++++++++++++++++----
src/test/regress/expected/brin_multi.out | 10 +++++++
src/test/regress/sql/brin_multi.sql | 14 +++++++++
3 files changed, 50 insertions(+), 6 deletions(-)
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index a0494e90242..8bb5bd1b1e4 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -69,6 +69,7 @@
#include "catalog/pg_am.h"
#include "catalog/pg_amop.h"
#include "catalog/pg_type.h"
+#include "nodes/miscnodes.h"
#include "utils/array.h"
#include "utils/builtins.h"
#include "utils/date.h"
@@ -77,6 +78,7 @@
#include "utils/inet.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
+#include "utils/numeric.h"
#include "utils/pg_lsn.h"
#include "utils/rel.h"
#include "utils/syscache.h"
@@ -2018,19 +2020,37 @@ brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS)
Datum
brin_minmax_multi_distance_numeric(PG_FUNCTION_ARGS)
{
- Datum d;
- Datum a1 = PG_GETARG_DATUM(0);
- Datum a2 = PG_GETARG_DATUM(1);
+ Numeric d;
+ Numeric a1 = PG_GETARG_NUMERIC(0);
+ Numeric a2 = PG_GETARG_NUMERIC(1);
+ float8 distance;
+ ErrorSaveContext escontext = {T_ErrorSaveContext};
/*
* We know the values are range boundaries, but the range may be collapsed
* (i.e. single points), with equal values.
*/
- Assert(DatumGetBool(DirectFunctionCall2(numeric_le, a1, a2)));
+ Assert(DatumGetBool(DirectFunctionCall2(numeric_le, NumericGetDatum(a1),
+ NumericGetDatum(a2))));
+
+ /*
+ * Calculate the float8 distance in an overflow-safe way. Return infinity
+ * instead of error-ing out.
+ */
+ d = numeric_sub_safe(a2, a1, (Node *) &escontext);
+ if (SOFT_ERROR_OCCURRED(&escontext))
+ distance = get_float8_infinity();
+ else
+ {
+ distance = DatumGetFloat8(DirectFunctionCall1(numeric_float8_no_overflow,
+ NumericGetDatum(d)));
+ pfree(d);
+ }
- d = DirectFunctionCall2(numeric_sub, a2, a1); /* a2 - a1 */
+ PG_FREE_IF_COPY(a1, 0);
+ PG_FREE_IF_COPY(a2, 1);
- PG_RETURN_DATUM(DirectFunctionCall1(numeric_float8, d));
+ PG_RETURN_FLOAT8(distance);
}
/*
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
index cb5b5e53e86..300abc2e1ab 100644
--- a/src/test/regress/expected/brin_multi.out
+++ b/src/test/regress/expected/brin_multi.out
@@ -981,3 +981,13 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
DROP TABLE brin_interval_test;
RESET enable_seqscan;
RESET datestyle;
+-- test handling of overflow in numeric values
+CREATE TABLE brin_overflow_test (n numeric);
+-- overflow in subtraction
+INSERT INTO brin_overflow_test VALUES (-repeat('9', 131072)::numeric), (repeat('9', 131072)::numeric);
+CREATE INDEX ON brin_overflow_test USING brin (n numeric_minmax_multi_ops);
+TRUNCATE brin_overflow_test;
+-- overflow in conversion to float8
+INSERT INTO brin_overflow_test VALUES (1), (1e500);
+CREATE INDEX ON brin_overflow_test USING brin (n numeric_minmax_multi_ops);
+DROP TABLE brin_overflow_test;
diff --git a/src/test/regress/sql/brin_multi.sql b/src/test/regress/sql/brin_multi.sql
index dda9fb73e0c..2cbdff00047 100644
--- a/src/test/regress/sql/brin_multi.sql
+++ b/src/test/regress/sql/brin_multi.sql
@@ -704,3 +704,17 @@ SELECT * FROM brin_interval_test WHERE a = '30 years'::interval;
DROP TABLE brin_interval_test;
RESET enable_seqscan;
RESET datestyle;
+
+-- test handling of overflow in numeric values
+CREATE TABLE brin_overflow_test (n numeric);
+
+-- overflow in subtraction
+INSERT INTO brin_overflow_test VALUES (-repeat('9', 131072)::numeric), (repeat('9', 131072)::numeric);
+CREATE INDEX ON brin_overflow_test USING brin (n numeric_minmax_multi_ops);
+TRUNCATE brin_overflow_test;
+
+-- overflow in conversion to float8
+INSERT INTO brin_overflow_test VALUES (1), (1e500);
+CREATE INDEX ON brin_overflow_test USING brin (n numeric_minmax_multi_ops);
+
+DROP TABLE brin_overflow_test;
--
2.55.0
From fc0c342f4946149914854eea8b5b7f88405d87f8 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <[email protected]>
Date: Fri, 30 Oct 2026 16:01:29 +0100
Subject: [PATCH vmaster 2/4] Fix BRIN minmax-multi distance for tid values
The distance function for tid values maps item pointers to float8 as
(block * MaxHeapTuplesPerPage + offset)
but the multiplication was done in uint32 arithmetic, so for high block
values (above 14759338, ~112GB) the result wrapped around. The distance
would then be very inaccurate (possibly negative), resulting in a less
efficient index.
Fix by doing the arithmetics in double.
It's also possible the (user-supplied) item pointers have offsets
greater than MaxHeapTuplesPerPage, confusing the mapping to double
value. This commit does not try to address that, it's not clear what
would be a better mapping.
Author: Tomas Vondra <[email protected]>
Backpatch-through: 14
Discussion: https://postgr.es/m/CALdSSPgBvXz-VSF0p-5k%3D_uFfN38cWrERmm_AC4K67N6MWQhmg%40mail.gmail.com
---
src/backend/access/brin/brin_minmax_multi.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index 8bb5bd1b1e4..08e68cb48f4 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -2005,10 +2005,10 @@ brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS)
* We use the no-check variants here, because user-supplied values may
* have (ip_posid == 0). See ItemPointerCompare.
*/
- da1 = ItemPointerGetBlockNumberNoCheck(pa1) * MaxHeapTuplesPerPage +
+ da1 = (double) ItemPointerGetBlockNumberNoCheck(pa1) * MaxHeapTuplesPerPage +
ItemPointerGetOffsetNumberNoCheck(pa1);
- da2 = ItemPointerGetBlockNumberNoCheck(pa2) * MaxHeapTuplesPerPage +
+ da2 = (double) ItemPointerGetBlockNumberNoCheck(pa2) * MaxHeapTuplesPerPage +
ItemPointerGetOffsetNumberNoCheck(pa2);
PG_RETURN_FLOAT8(da2 - da1);
--
2.55.0
From 9d713d2e99017f8e3c17d3f41eaf08c4daa9aeec Mon Sep 17 00:00:00 2001
From: Tomas Vondra <[email protected]>
Date: Fri, 30 Oct 2026 16:27:05 +0100
Subject: [PATCH vmaster 3/4] Fix BRIN minmax-multi distance calculation for
OID
The oid_minmax_multi_ops opclass simply calculated the distance using
brin_minmax_multi_distance_int4. That however interprets the values as
int32, so OIDs above PG_INT32_MAX were treated as negative values, and
ranges crossing the boundary ended with a negative distance.
With assert-enabled guild, this tripped the (a1 <= a2) check. Without
asserts, the index was less efficient.
Fix by introducing brin_minmax_multi_distance_oid, correctly treating
the OID values as unsigned.
This required a catalog change (and catversion bump), so it can't be
backpatched. It'd be possible to adjust brin_minmax_multi_distance_int4
to handle the (a1 > a2) case differently. But that'd lose the assert,
and it does not seem worth it (indexes on OIDs don't seem very common).
Author: Tomas Vondra <[email protected]>
Discussion: https://postgr.es/m/CALdSSPgBvXz-VSF0p-5k%3D_uFfN38cWrERmm_AC4K67N6MWQhmg%40mail.gmail.com
---
src/backend/access/brin/brin_minmax_multi.c | 21 +++++++++++++++++++++
src/include/catalog/pg_amproc.dat | 2 +-
src/include/catalog/pg_proc.dat | 4 ++++
src/test/regress/expected/brin_multi.out | 5 +++++
src/test/regress/sql/brin_multi.sql | 8 ++++++++
5 files changed, 39 insertions(+), 1 deletion(-)
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
index 08e68cb48f4..89b78d726e9 100644
--- a/src/backend/access/brin/brin_minmax_multi.c
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -1982,6 +1982,27 @@ brin_minmax_multi_distance_int8(PG_FUNCTION_ARGS)
PG_RETURN_FLOAT8((double) a2 - (double) a1);
}
+/*
+ * Compute the distance between two oid values (plain subtraction).
+ *
+ * OIDs are unsigned, so we can't use the int4 function - values above
+ * PG_INT32_MAX would be interpreted as negative, breaking the ordering.
+ */
+Datum
+brin_minmax_multi_distance_oid(PG_FUNCTION_ARGS)
+{
+ Oid a1 = PG_GETARG_OID(0);
+ Oid a2 = PG_GETARG_OID(1);
+
+ /*
+ * We know the values are range boundaries, but the range may be collapsed
+ * (i.e. single points), with equal values.
+ */
+ Assert(a1 <= a2);
+
+ PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
/*
* Compute the distance between two tid values (by mapping them to float8 and
* then subtracting them).
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 27fb8cd2d35..4b83b6eccd8 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -1129,7 +1129,7 @@
amproc => 'brin_minmax_multi_options' },
{ amprocfamily => 'brin/oid_minmax_multi_ops', amproclefttype => 'oid',
amprocrighttype => 'oid', amprocnum => '11',
- amproc => 'brin_minmax_multi_distance_int4' },
+ amproc => 'brin_minmax_multi_distance_oid' },
# bloom oid
{ amprocfamily => 'brin/oid_bloom_ops', amproclefttype => 'oid',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 1f21a8eb6d4..e9c0e3937cb 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -9157,6 +9157,10 @@
proname => 'brin_minmax_multi_distance_numeric', prorettype => 'float8',
proargtypes => 'internal internal',
prosrc => 'brin_minmax_multi_distance_numeric' },
+{ oid => '9126', descr => 'BRIN multi minmax oid distance',
+ proname => 'brin_minmax_multi_distance_oid', prorettype => 'float8',
+ proargtypes => 'internal internal',
+ prosrc => 'brin_minmax_multi_distance_oid' },
{ oid => '4627', descr => 'BRIN multi minmax tid distance',
proname => 'brin_minmax_multi_distance_tid', prorettype => 'float8',
proargtypes => 'internal internal',
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
index 300abc2e1ab..ff9f0c39774 100644
--- a/src/test/regress/expected/brin_multi.out
+++ b/src/test/regress/expected/brin_multi.out
@@ -991,3 +991,8 @@ TRUNCATE brin_overflow_test;
INSERT INTO brin_overflow_test VALUES (1), (1e500);
CREATE INDEX ON brin_overflow_test USING brin (n numeric_minmax_multi_ops);
DROP TABLE brin_overflow_test;
+-- OID values should not be treated as negative int4 values
+CREATE TABLE brin_oid_overflow_test (o oid);
+INSERT INTO brin_oid_overflow_test VALUES (1), (3000000000);
+CREATE INDEX ON brin_oid_overflow_test USING brin (o oid_minmax_multi_ops);
+DROP TABLE brin_oid_overflow_test;
diff --git a/src/test/regress/sql/brin_multi.sql b/src/test/regress/sql/brin_multi.sql
index 2cbdff00047..445ba1d718a 100644
--- a/src/test/regress/sql/brin_multi.sql
+++ b/src/test/regress/sql/brin_multi.sql
@@ -718,3 +718,11 @@ INSERT INTO brin_overflow_test VALUES (1), (1e500);
CREATE INDEX ON brin_overflow_test USING brin (n numeric_minmax_multi_ops);
DROP TABLE brin_overflow_test;
+
+-- OID values should not be treated as negative int4 values
+CREATE TABLE brin_oid_overflow_test (o oid);
+
+INSERT INTO brin_oid_overflow_test VALUES (1), (3000000000);
+CREATE INDEX ON brin_oid_overflow_test USING brin (o oid_minmax_multi_ops);
+
+DROP TABLE brin_oid_overflow_test;
--
2.55.0
From 4a459b95028db7757bc39b635b0f743f856f30cf Mon Sep 17 00:00:00 2001
From: Tomas Vondra <[email protected]>
Date: Fri, 30 Oct 2026 16:58:13 +0100
Subject: [PATCH vmaster 4/4] Handle overflow when analyzing nummultirange
values
The numrange_subdiff() function was calculating the difference simply by
calling numeric_sub and numeric_float8, but those functions are not
overflow-safe. It's possible to insert valid numeric ranges for which
the difference overflows either numeric or float8. When that happened,
the ANALYZE on the table failed.
Fixed by handling overflows in numrange_subdiff() - extreme differences
are treated as infinity.
Author: Kirill Reshke <[email protected]>
Backpatch-through: 14
Discussion: https://postgr.es/m/CALdSSPgBvXz-VSF0p-5k%3D_uFfN38cWrERmm_AC4K67N6MWQhmg%40mail.gmail.com
---
src/backend/utils/adt/rangetypes.c | 28 +++++++++++++++----
src/test/regress/expected/multirangetypes.out | 7 +++++
src/test/regress/sql/multirangetypes.sql | 9 ++++++
3 files changed, 38 insertions(+), 6 deletions(-)
diff --git a/src/backend/utils/adt/rangetypes.c b/src/backend/utils/adt/rangetypes.c
index 84d71761c1b..5ca2093a3ad 100644
--- a/src/backend/utils/adt/rangetypes.c
+++ b/src/backend/utils/adt/rangetypes.c
@@ -44,7 +44,9 @@
#include "port/pg_bitutils.h"
#include "utils/builtins.h"
#include "utils/date.h"
+#include "utils/float.h"
#include "utils/lsyscache.h"
+#include "utils/numeric.h"
#include "utils/rangetypes.h"
#include "utils/sortsupport.h"
#include "utils/timestamp.h"
@@ -1872,15 +1874,29 @@ int8range_subdiff(PG_FUNCTION_ARGS)
Datum
numrange_subdiff(PG_FUNCTION_ARGS)
{
- Datum v1 = PG_GETARG_DATUM(0);
- Datum v2 = PG_GETARG_DATUM(1);
- Datum numresult;
+ Numeric v1 = PG_GETARG_NUMERIC(0);
+ Numeric v2 = PG_GETARG_NUMERIC(1);
+ Numeric numresult;
float8 floatresult;
+ ErrorSaveContext escontext = {T_ErrorSaveContext};
- numresult = DirectFunctionCall2(numeric_sub, v1, v2);
+ /*
+ * The difference can exceed numeric's range, or be unrepresentable
+ * in float8. Neither caller is prepared to handle an error, so
+ * saturate to infinity (or round to zero) instead.
+ */
+ numresult = numeric_sub_safe(v1, v2, (Node *) &escontext);
+ if (SOFT_ERROR_OCCURRED(&escontext))
+ floatresult = get_float8_infinity();
+ else
+ {
+ floatresult = DatumGetFloat8(DirectFunctionCall1(
+ numeric_float8_no_overflow, NumericGetDatum(numresult)));
+ pfree(numresult);
+ }
- floatresult = DatumGetFloat8(DirectFunctionCall1(numeric_float8,
- numresult));
+ PG_FREE_IF_COPY(v1, 0);
+ PG_FREE_IF_COPY(v2, 1);
PG_RETURN_FLOAT8(floatresult);
}
diff --git a/src/test/regress/expected/multirangetypes.out b/src/test/regress/expected/multirangetypes.out
index d47ce4b6d6a..a57f5fa6ae6 100644
--- a/src/test/regress/expected/multirangetypes.out
+++ b/src/test/regress/expected/multirangetypes.out
@@ -3197,6 +3197,13 @@ ERROR: type "intr_multirange" already exists
DETAIL: Failed while creating a multirange type for type "intr".
HINT: You can manually specify a multirange type name using the "multirange_type_name" attribute.
drop type intr_multirange;
+---
+-- Check that numeric overflow during analyze does not make it fail
+---
+create table multirange_analyze_test (r nummultirange);
+insert into multirange_analyze_test values ('{[1e1000, 2e1000], [-9e131071, 9e131071]}'::nummultirange);
+analyze multirange_analyze_test;
+drop table multirange_analyze_test;
--
-- Test multiple multirange types over the same subtype and manual naming of
-- the multirange type.
diff --git a/src/test/regress/sql/multirangetypes.sql b/src/test/regress/sql/multirangetypes.sql
index cf0fff6fddd..9e7b1f0205d 100644
--- a/src/test/regress/sql/multirangetypes.sql
+++ b/src/test/regress/sql/multirangetypes.sql
@@ -702,6 +702,15 @@ create type intr_multirange as (x int, y int);
create type intr as range(subtype=int); -- should fail
drop type intr_multirange;
+---
+-- Check that numeric overflow during analyze does not make it fail
+---
+
+create table multirange_analyze_test (r nummultirange);
+insert into multirange_analyze_test values ('{[1e1000, 2e1000], [-9e131071, 9e131071]}'::nummultirange);
+analyze multirange_analyze_test;
+drop table multirange_analyze_test;
+
--
-- Test multiple multirange types over the same subtype and manual naming of
-- the multirange type.
--
2.55.0