public inbox for [email protected]
help / color / mirror / Atom feedReject ill-formed range bounds histograms in pg_restore_attribute_stats()
5+ messages / 2 participants
[nested] [flat]
* Reject ill-formed range bounds histograms in pg_restore_attribute_stats()
@ 2026-07-07 08:13 Ewan Young <[email protected]>
0 siblings, 2 replies; 5+ messages in thread
From: Ewan Young @ 2026-07-07 08:13 UTC (permalink / raw)
To: PostgreSQL Hackers <[email protected]>; +Cc: Michael Paquier <[email protected]>; Corey Huinker <[email protected]>
Hi,
pg_restore_attribute_stats() (and pg_set_attribute_stats()) stores a
STATISTIC_KIND_BOUNDS_HISTOGRAM verbatim from user input, checking only
that the argument is a one-dimensional, NULL-free array of the right
type. It does not verify the invariant that ANALYZE's
compute_range_stats() always establishes: the bounds histogram must
contain no empty ranges, and its lower and upper bounds must each be
sorted in ascending order.
The range and multirange selectivity estimators rely on that invariant.
They split the histogram into separate lower- and upper-bound histograms
and assume each is ordered. So an imported histogram that violates it
makes the planner misbehave when it later reads the statistics:
- a non-monotonic histogram makes calc_length_hist_frac() fail its
Assert(length2 >= length1) (rangetypes_selfuncs.c, and the identical
multirangetypes_selfuncs.c), i.e. a backend crash on an assert-enabled
build, or a nonsensical, possibly negative, selectivity otherwise;
- an empty range trips the "shouldn't happen" elog(ERROR, "bounds
histogram contains an empty range").
Reproducer on HEAD (assert build):
CREATE TABLE t (r int4range);
INSERT INTO t SELECT int4range(g, g+10) FROM generate_series(1,500) g;
ANALYZE t;
SELECT pg_restore_attribute_stats(
'schemaname', 'public', 'relname', 't',
'attname', 'r', 'inherited', false,
'range_bounds_histogram',
'{"[50,60)","[1,2)","[90,100)","[5,6)"}'::text,
'range_length_histogram', '{1,5,10,20}'::text,
'range_empty_frac', 0.0::real); -- accepted, returns true
EXPLAIN SELECT * FROM t WHERE r <@ int4range(1,100);
-- TRAP: failed Assert("length2 >= length1"), rangetypes_selfuncs.c
The same happens for a multirange column (whose bounds histogram is also
an array of ranges). Setting statistics requires table ownership /
MAINTAIN, so this is not a security issue, but it is another way for a
stats-import call to feed the planner data it cannot cope with, and the
functions already validate other properties and reject bad input with a
WARNING rather than crashing.
This is the same class of problem as commit f6e4ec0a705, which rejected
oversized MCV lists in pg_restore_extended_stats().
The attached patch validates the bounds histogram at import time and
rejects an ill-formed one with a WARNING, matching the treatment of the
other inconsistent inputs. Because the histogram element type is a range
for both range- and multirange-typed columns, the single check covers
both.
--
Regards,
Ewan Young
Attachments:
[application/octet-stream] v1-0001-Reject-ill-formed-range-bounds-histograms-in-pg_rest.patch (8.2K, ../../CAON2xHNM809WLR_g0ymKgU-tWxtbyH1Xvh4fqzRqy9YP2A59pg@mail.gmail.com/2-v1-0001-Reject-ill-formed-range-bounds-histograms-in-pg_rest.patch)
download | inline diff:
From c2b72f0563aafd83868ba8983f8a42da6c79529b Mon Sep 17 00:00:00 2001
From: Ewan Young <[email protected]>
Date: Tue, 7 Jul 2026 23:40:30 +0800
Subject: [PATCH] Reject ill-formed range bounds histograms in
pg_restore_attribute_stats()
pg_restore_attribute_stats() (and pg_set_attribute_stats()) stored a
STATISTIC_KIND_BOUNDS_HISTOGRAM verbatim, without checking the invariant
that ANALYZE's compute_range_stats() guarantees: the histogram must not
contain any empty range, and its lower and upper bounds must each appear
in ascending order. The range and multirange selectivity estimators rely
on this. An unsorted histogram makes calc_length_hist_frac() fail an
assertion (or compute a nonsensical, possibly negative, selectivity on a
non-assert build), and an empty range trips a "shouldn't happen"
elog(ERROR), when the planner later reads the imported statistics. For
instance, after importing an unsorted bounds histogram, a query such as
"WHERE r <@ ..." crashes the backend during planning.
Validate the bounds histogram at import time and reject an ill-formed one
with a WARNING, like other inconsistent inputs. This covers both range-
and multirange-typed columns, whose bounds histograms are both arrays of
ranges.
This is the same class of problem as commit f6e4ec0a705, which rejected
oversized MCV lists in pg_restore_extended_stats().
---
src/backend/statistics/attribute_stats.c | 89 +++++++++++++++++++++-
src/test/regress/expected/stats_import.out | 28 +++++++
src/test/regress/sql/stats_import.sql | 18 +++++
3 files changed, 134 insertions(+), 1 deletion(-)
diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index 1cc4d657231..9138434ba03 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -28,7 +28,9 @@
#include "utils/builtins.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
+#include "utils/rangetypes.h"
#include "utils/syscache.h"
+#include "utils/typcache.h"
/*
* Positional argument numbers, names, and types for
@@ -105,10 +107,94 @@ static struct StatsArgInfo cleararginfo[] =
};
static bool attribute_statistics_update(FunctionCallInfo fcinfo);
+static bool statatt_check_bounds_histogram(Datum arrayval);
static void upsert_pg_statistic(Relation starel, HeapTuple oldtup,
const Datum *values, const bool *nulls, const bool *replaces);
static bool delete_pg_statistic(Oid reloid, AttrNumber attnum, bool stainherit);
+/*
+ * Check that an imported bounds histogram (STATISTIC_KIND_BOUNDS_HISTOGRAM) is
+ * well-formed the way ANALYZE builds it in compute_range_stats(): it must not
+ * contain any empty range, and both its lower and its upper bounds must appear
+ * in non-decreasing order. The range and multirange selectivity estimators
+ * rely on this: they split the histogram into separate lower- and upper-bound
+ * histograms and assume each is sorted (e.g. calc_length_hist_frac() Asserts
+ * that successive bound distances do not decrease, and the estimator throws an
+ * internal error if it finds an empty range). Importing an unsorted or
+ * empty-containing histogram would therefore crash the backend, or produce
+ * nonsensical estimates on non-assert builds, when the histogram is later read
+ * by the planner. Reject such input with a WARNING, like other inconsistent
+ * inputs.
+ *
+ * For both range- and multirange-typed columns the histogram is an array of
+ * ranges, so we take the range type from the array's element type.
+ */
+static bool
+statatt_check_bounds_histogram(Datum arrayval)
+{
+ ArrayType *arr = DatumGetArrayTypeP(arrayval);
+ Oid rngtypid = ARR_ELEMTYPE(arr);
+ TypeCacheEntry *typcache;
+ int16 elmlen;
+ bool elmbyval;
+ char elmalign;
+ Datum *elems;
+ bool *nulls;
+ int nelems;
+ RangeBound prev_lower = {0};
+ RangeBound prev_upper = {0};
+
+ typcache = lookup_type_cache(rngtypid, TYPECACHE_RANGE_INFO);
+
+ /*
+ * The element type should always be a range type here, but be defensive:
+ * if it isn't, the bounds histogram is never consulted by the range
+ * estimator, so there is nothing to verify.
+ */
+ if (typcache->rngelemtype == NULL)
+ return true;
+
+ get_typlenbyvalalign(rngtypid, &elmlen, &elmbyval, &elmalign);
+ deconstruct_array(arr, rngtypid, elmlen, elmbyval, elmalign,
+ &elems, &nulls, &nelems);
+
+ for (int i = 0; i < nelems; i++)
+ {
+ RangeBound lower,
+ upper;
+ bool empty;
+
+ /* NULL elements are already rejected by statatt_build_stavalues() */
+ range_deserialize(typcache, DatumGetRangeTypeP(elems[i]),
+ &lower, &upper, &empty);
+
+ if (empty)
+ {
+ ereport(WARNING,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("\"%s\" must not contain empty ranges",
+ "range_bounds_histogram")));
+ return false;
+ }
+
+ if (i > 0 &&
+ (range_cmp_bounds(typcache, &lower, &prev_lower) < 0 ||
+ range_cmp_bounds(typcache, &upper, &prev_upper) < 0))
+ {
+ ereport(WARNING,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("\"%s\" must have its lower and upper bounds sorted in ascending order",
+ "range_bounds_histogram")));
+ return false;
+ }
+
+ prev_lower = lower;
+ prev_upper = upper;
+ }
+
+ return true;
+}
+
/*
* Insert or Update Attribute Statistics
*
@@ -488,7 +574,8 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
atttypid, atttypmod,
&converted);
- if (converted)
+ if (converted &&
+ statatt_check_bounds_histogram(stavalues))
{
statatt_set_slot(values, nulls, replaces,
STATISTIC_KIND_BOUNDS_HISTOGRAM,
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index bfdff77afa7..93c190f7194 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -1187,6 +1187,34 @@ AND attname = 'arange';
stats_import | test | arange | f | 0.29 | 0 | 0 | | | | | | | | {399,499,Infinity} | 0.5 | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
(1 row)
+-- warn: range bounds histogram whose bounds are not sorted ascending
+SELECT pg_catalog.pg_restore_attribute_stats(
+ 'schemaname', 'stats_import',
+ 'relname', 'test',
+ 'attname', 'arange',
+ 'inherited', false::boolean,
+ 'range_bounds_histogram', '{"[50,60)","[1,2)","[90,100)","[5,6)"}'::text
+ );
+WARNING: "range_bounds_histogram" must have its lower and upper bounds sorted in ascending order
+ pg_restore_attribute_stats
+----------------------------
+ f
+(1 row)
+
+-- warn: range bounds histogram containing an empty range
+SELECT pg_catalog.pg_restore_attribute_stats(
+ 'schemaname', 'stats_import',
+ 'relname', 'test',
+ 'attname', 'arange',
+ 'inherited', false::boolean,
+ 'range_bounds_histogram', '{empty,"[1,2)","[3,4)"}'::text
+ );
+WARNING: "range_bounds_histogram" must not contain empty ranges
+ pg_restore_attribute_stats
+----------------------------
+ f
+(1 row)
+
-- warn: cannot set most_common_elems for range type, rest ok
SELECT pg_catalog.pg_restore_attribute_stats(
'schemaname', 'stats_import',
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index 58140315efb..31ac64f41b9 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -885,6 +885,24 @@ AND tablename = 'test'
AND inherited = false
AND attname = 'arange';
+-- warn: range bounds histogram whose bounds are not sorted ascending
+SELECT pg_catalog.pg_restore_attribute_stats(
+ 'schemaname', 'stats_import',
+ 'relname', 'test',
+ 'attname', 'arange',
+ 'inherited', false::boolean,
+ 'range_bounds_histogram', '{"[50,60)","[1,2)","[90,100)","[5,6)"}'::text
+ );
+
+-- warn: range bounds histogram containing an empty range
+SELECT pg_catalog.pg_restore_attribute_stats(
+ 'schemaname', 'stats_import',
+ 'relname', 'test',
+ 'attname', 'arange',
+ 'inherited', false::boolean,
+ 'range_bounds_histogram', '{empty,"[1,2)","[3,4)"}'::text
+ );
+
-- warn: cannot set most_common_elems for range type, rest ok
SELECT pg_catalog.pg_restore_attribute_stats(
'schemaname', 'stats_import',
--
2.47.3
^ permalink raw reply [nested|flat] 5+ messages in thread
* Re: Reject ill-formed range bounds histograms in pg_restore_attribute_stats()
@ 2026-07-07 08:27 Michael Paquier <[email protected]>
parent: Ewan Young <[email protected]>
1 sibling, 0 replies; 5+ messages in thread
From: Michael Paquier @ 2026-07-07 08:27 UTC (permalink / raw)
To: Ewan Young <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>; Corey Huinker <[email protected]>
On Tue, Jul 07, 2026 at 04:13:30PM +0800, Ewan Young wrote:
> pg_restore_attribute_stats() (and pg_set_attribute_stats()) stores a
> STATISTIC_KIND_BOUNDS_HISTOGRAM verbatim from user input, checking only
> that the argument is a one-dimensional, NULL-free array of the right
> type. It does not verify the invariant that ANALYZE's
> compute_range_stats() always establishes: the bounds histogram must
> contain no empty ranges, and its lower and upper bounds must each be
> sorted in ascending order.
I'll check that tomorrow. Thanks for the report.
--
Michael
Attachments:
[application/pgp-signature] signature.asc (833B, ../../[email protected]/2-signature.asc)
download
^ permalink raw reply [nested|flat] 5+ messages in thread
* Re: Reject ill-formed range bounds histograms in pg_restore_attribute_stats()
@ 2026-07-08 02:17 Michael Paquier <[email protected]>
parent: Ewan Young <[email protected]>
1 sibling, 1 reply; 5+ messages in thread
From: Michael Paquier @ 2026-07-08 02:17 UTC (permalink / raw)
To: Ewan Young <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>; Corey Huinker <[email protected]>
On Tue, Jul 07, 2026 at 04:13:30PM +0800, Ewan Young wrote:
> The attached patch validates the bounds histogram at import time and
> rejects an ill-formed one with a WARNING, matching the treatment of the
> other inconsistent inputs. Because the histogram element type is a range
> for both range- and multirange-typed columns, the single check covers
> both.
I have been looking at that, and while the consequences of buggy
inputs are minor when loaded back, I don't really mind putting more
defenses to inform about that at the front of the restore functions.
Now, your patch is entirely blind about extended statistics; these can
also load histogram bounds. The function you are introducing to
filter the inputs provided could be reused in this secondary case,
just by moving to stat_utils.c.
I am not really convinced that any of this stuff would be worth a
backpatch, even if it's non-invasive. We don't have guards for
buggy infinite values, even if using empty or unordered bounds feels
kind of a stupid thing to do if one is a table owner.
--
Michael
Attachments:
[application/pgp-signature] signature.asc (833B, ../../[email protected]/2-signature.asc)
download
^ permalink raw reply [nested|flat] 5+ messages in thread
* Re: Reject ill-formed range bounds histograms in pg_restore_attribute_stats()
@ 2026-07-08 04:16 Ewan Young <[email protected]>
parent: Michael Paquier <[email protected]>
0 siblings, 1 reply; 5+ messages in thread
From: Ewan Young @ 2026-07-08 04:16 UTC (permalink / raw)
To: Michael Paquier <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>; Corey Huinker <[email protected]>
Thanks for looking!
On Wed, Jul 8, 2026 at 10:17 AM Michael Paquier <[email protected]> wrote:
>
> On Tue, Jul 07, 2026 at 04:13:30PM +0800, Ewan Young wrote:
> > The attached patch validates the bounds histogram at import time and
> > rejects an ill-formed one with a WARNING, matching the treatment of the
> > other inconsistent inputs. Because the histogram element type is a range
> > for both range- and multirange-typed columns, the single check covers
> > both.
>
> I have been looking at that, and while the consequences of buggy
> inputs are minor when loaded back, I don't really mind putting more
> defenses to inform about that at the front of the restore functions.
>
> Now, your patch is entirely blind about extended statistics; these can
> also load histogram bounds. The function you are introducing to
> filter the inputs provided could be reused in this secondary case,
> just by moving to stat_utils.c.
Done in v2. I moved the check to stat_utils.c as stats_check_bounds_histogram()
and now call it from both pg_restore_attribute_stats() and
pg_restore_extended_stats(). In the extended path I reject an ill-formed
histogram the same way the sibling stakinds there already do — WARNING plus
goto pg_statistic_error.
>
> I am not really convinced that any of this stuff would be worth a
> backpatch, even if it's non-invasive. We don't have guards for
> buggy infinite values, even if using empty or unordered bounds feels
> kind of a stupid thing to do if one is a table owner.
Agreed — v2 targets master only, no back-branch patches.
> --
> Michael
--
Regards,
Ewan Young
Attachments:
[application/x-patch] v2-0001-Reject-ill-formed-range-bounds-histograms-in-stat.patch (12.3K, ../../CAON2xHNGEB=Taga0P-osTcsbWTz6jha9yc74bkx9cASZTueA-g@mail.gmail.com/2-v2-0001-Reject-ill-formed-range-bounds-histograms-in-stat.patch)
download | inline diff:
From 1b06614bfdf5d91703cfa3ef166691feb0b5dda4 Mon Sep 17 00:00:00 2001
From: Ewan Young <[email protected]>
Date: Tue, 7 Jul 2026 23:40:30 +0800
Subject: [PATCH v2] Reject ill-formed range bounds histograms in stats import
functions
pg_restore_attribute_stats() and pg_restore_extended_stats() (and their
pg_set_*_stats() counterparts) stored a STATISTIC_KIND_BOUNDS_HISTOGRAM
verbatim, without checking the invariant that ANALYZE's
compute_range_stats() guarantees: the histogram must not contain any
empty range, and its lower and upper bounds must each appear in ascending
order. The range and multirange selectivity estimators rely on this. An
unsorted histogram makes calc_length_hist_frac() fail an assertion (or
compute a nonsensical, possibly negative, selectivity on a non-assert
build), and an empty range trips a "shouldn't happen" elog(ERROR), when
the planner later reads the imported statistics. For instance, after
importing an unsorted bounds histogram, a query such as "WHERE r <@ ..."
crashes the backend during planning.
Validate the bounds histogram at import time and reject an ill-formed one
with a WARNING, like other inconsistent inputs. The check lives in
stat_utils.c so that both the per-attribute and the extended-statistics
import paths, which can both load a bounds histogram, can share it. It
covers both range- and multirange-typed columns, whose bounds histograms
are both arrays of ranges.
This is the same class of problem as commit f6e4ec0a705, which rejected
oversized MCV lists in pg_restore_extended_stats().
---
src/backend/statistics/attribute_stats.c | 3 +-
src/backend/statistics/extended_stats_funcs.c | 2 +-
src/backend/statistics/stat_utils.c | 88 +++++++++++++++++++
src/include/statistics/stat_utils.h | 2 +
src/test/regress/expected/stats_import.out | 56 ++++++++++++
src/test/regress/sql/stats_import.sql | 34 +++++++
6 files changed, 183 insertions(+), 2 deletions(-)
diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index 1cc4d657231..cb0375374b2 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -488,7 +488,8 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
atttypid, atttypmod,
&converted);
- if (converted)
+ if (converted &&
+ stats_check_bounds_histogram(stavalues))
{
statatt_set_slot(values, nulls, replaces,
STATISTIC_KIND_BOUNDS_HISTOGRAM,
diff --git a/src/backend/statistics/extended_stats_funcs.c b/src/backend/statistics/extended_stats_funcs.c
index a5dce8a2206..f66e2360df4 100644
--- a/src/backend/statistics/extended_stats_funcs.c
+++ b/src/backend/statistics/extended_stats_funcs.c
@@ -1491,7 +1491,7 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont,
extexprargname[RANGE_BOUNDS_HISTOGRAM_ELEM],
&val_ok);
- if (val_ok)
+ if (val_ok && stats_check_bounds_histogram(stavalues))
statatt_set_slot(values, nulls, replaces,
STATISTIC_KIND_BOUNDS_HISTOGRAM,
InvalidOid, InvalidOid,
diff --git a/src/backend/statistics/stat_utils.c b/src/backend/statistics/stat_utils.c
index 0b190e88237..3369ac2f170 100644
--- a/src/backend/statistics/stat_utils.c
+++ b/src/backend/statistics/stat_utils.c
@@ -33,6 +33,7 @@
#include "utils/array.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
+#include "utils/rangetypes.h"
#include "utils/rel.h"
#include "utils/syscache.h"
#include "utils/typcache.h"
@@ -742,3 +743,90 @@ statatt_init_empty_tuple(Oid reloid, int16 attnum, bool inherited,
nulls[Anum_pg_statistic_stacoll1 + slotnum - 1] = false;
}
}
+
+/*
+ * Check that an imported bounds histogram (STATISTIC_KIND_BOUNDS_HISTOGRAM) is
+ * well-formed the way ANALYZE builds it in compute_range_stats(): it must not
+ * contain any empty range, and both its lower and its upper bounds must appear
+ * in non-decreasing order. The range and multirange selectivity estimators
+ * rely on this: they split the histogram into separate lower- and upper-bound
+ * histograms and assume each is sorted (e.g. calc_length_hist_frac() Asserts
+ * that successive bound distances do not decrease, and the estimator throws an
+ * internal error if it finds an empty range). Importing an unsorted or
+ * empty-containing histogram would therefore crash the backend, or produce
+ * nonsensical estimates on non-assert builds, when the histogram is later read
+ * by the planner. Reject such input with a WARNING, like other inconsistent
+ * inputs.
+ *
+ * This is shared between the per-attribute (pg_restore_attribute_stats()) and
+ * the extended-statistics (pg_restore_extended_stats()) import paths, both of
+ * which can load a bounds histogram.
+ *
+ * For both range- and multirange-typed columns the histogram is an array of
+ * ranges, so we take the range type from the array's element type.
+ */
+bool
+stats_check_bounds_histogram(Datum arrayval)
+{
+ ArrayType *arr = DatumGetArrayTypeP(arrayval);
+ Oid rngtypid = ARR_ELEMTYPE(arr);
+ TypeCacheEntry *typcache;
+ int16 elmlen;
+ bool elmbyval;
+ char elmalign;
+ Datum *elems;
+ bool *nulls;
+ int nelems;
+ RangeBound prev_lower = {0};
+ RangeBound prev_upper = {0};
+
+ typcache = lookup_type_cache(rngtypid, TYPECACHE_RANGE_INFO);
+
+ /*
+ * The element type should always be a range type here, but be defensive:
+ * if it isn't, the bounds histogram is never consulted by the range
+ * estimator, so there is nothing to verify.
+ */
+ if (typcache->rngelemtype == NULL)
+ return true;
+
+ get_typlenbyvalalign(rngtypid, &elmlen, &elmbyval, &elmalign);
+ deconstruct_array(arr, rngtypid, elmlen, elmbyval, elmalign,
+ &elems, &nulls, &nelems);
+
+ for (int i = 0; i < nelems; i++)
+ {
+ RangeBound lower,
+ upper;
+ bool empty;
+
+ /* NULL elements are already rejected by statatt_build_stavalues() */
+ range_deserialize(typcache, DatumGetRangeTypeP(elems[i]),
+ &lower, &upper, &empty);
+
+ if (empty)
+ {
+ ereport(WARNING,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("\"%s\" must not contain empty ranges",
+ "range_bounds_histogram")));
+ return false;
+ }
+
+ if (i > 0 &&
+ (range_cmp_bounds(typcache, &lower, &prev_lower) < 0 ||
+ range_cmp_bounds(typcache, &upper, &prev_upper) < 0))
+ {
+ ereport(WARNING,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("\"%s\" must have its lower and upper bounds sorted in ascending order",
+ "range_bounds_histogram")));
+ return false;
+ }
+
+ prev_lower = lower;
+ prev_upper = upper;
+ }
+
+ return true;
+}
diff --git a/src/include/statistics/stat_utils.h b/src/include/statistics/stat_utils.h
index 74da7790579..9f617a0f42b 100644
--- a/src/include/statistics/stat_utils.h
+++ b/src/include/statistics/stat_utils.h
@@ -58,4 +58,6 @@ extern Datum statatt_build_stavalues(const char *staname, FmgrInfo *array_in, Da
extern bool statatt_get_elem_type(Oid atttypid, char atttyptype,
Oid *elemtypid, Oid *elem_eq_opr);
+extern bool stats_check_bounds_histogram(Datum arrayval);
+
#endif /* STATS_UTILS_H */
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index bfdff77afa7..d4e5247b22a 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -1187,6 +1187,34 @@ AND attname = 'arange';
stats_import | test | arange | f | 0.29 | 0 | 0 | | | | | | | | {399,499,Infinity} | 0.5 | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
(1 row)
+-- warn: range bounds histogram whose bounds are not sorted ascending
+SELECT pg_catalog.pg_restore_attribute_stats(
+ 'schemaname', 'stats_import',
+ 'relname', 'test',
+ 'attname', 'arange',
+ 'inherited', false::boolean,
+ 'range_bounds_histogram', '{"[50,60)","[1,2)","[90,100)","[5,6)"}'::text
+ );
+WARNING: "range_bounds_histogram" must have its lower and upper bounds sorted in ascending order
+ pg_restore_attribute_stats
+----------------------------
+ f
+(1 row)
+
+-- warn: range bounds histogram containing an empty range
+SELECT pg_catalog.pg_restore_attribute_stats(
+ 'schemaname', 'stats_import',
+ 'relname', 'test',
+ 'attname', 'arange',
+ 'inherited', false::boolean,
+ 'range_bounds_histogram', '{empty,"[1,2)","[3,4)"}'::text
+ );
+WARNING: "range_bounds_histogram" must not contain empty ranges
+ pg_restore_attribute_stats
+----------------------------
+ f
+(1 row)
+
-- warn: cannot set most_common_elems for range type, rest ok
SELECT pg_catalog.pg_restore_attribute_stats(
'schemaname', 'stats_import',
@@ -2358,6 +2386,34 @@ HINT: "range_length_histogram", "range_empty_frac", and "range_bounds_histogram
f
(1 row)
+-- warn: range bounds histogram whose bounds are not sorted ascending
+SELECT pg_catalog.pg_restore_extended_stats(
+ 'schemaname', 'stats_import',
+ 'relname', 'test_mr',
+ 'statistics_schemaname', 'stats_import',
+ 'statistics_name', 'test_mr_stat',
+ 'inherited', false,
+ 'exprs', '[{"range_length_histogram": "{10179,10189,10199}", "range_empty_frac": "0", "range_bounds_histogram": "{\"[50,60)\",\"[1,2)\",\"[90,100)\"}"}]'::jsonb);
+WARNING: "range_bounds_histogram" must have its lower and upper bounds sorted in ascending order
+ pg_restore_extended_stats
+---------------------------
+ f
+(1 row)
+
+-- warn: range bounds histogram containing an empty range
+SELECT pg_catalog.pg_restore_extended_stats(
+ 'schemaname', 'stats_import',
+ 'relname', 'test_mr',
+ 'statistics_schemaname', 'stats_import',
+ 'statistics_name', 'test_mr_stat',
+ 'inherited', false,
+ 'exprs', '[{"range_length_histogram": "{10179,10189,10199}", "range_empty_frac": "0", "range_bounds_histogram": "{empty,\"[1,2)\",\"[3,4)\"}"}]'::jsonb);
+WARNING: "range_bounds_histogram" must not contain empty ranges
+ pg_restore_extended_stats
+---------------------------
+ f
+(1 row)
+
-- ok: multirange stats
SELECT pg_catalog.pg_restore_extended_stats(
'schemaname', 'stats_import',
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index 58140315efb..e54f04ad638 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -885,6 +885,24 @@ AND tablename = 'test'
AND inherited = false
AND attname = 'arange';
+-- warn: range bounds histogram whose bounds are not sorted ascending
+SELECT pg_catalog.pg_restore_attribute_stats(
+ 'schemaname', 'stats_import',
+ 'relname', 'test',
+ 'attname', 'arange',
+ 'inherited', false::boolean,
+ 'range_bounds_histogram', '{"[50,60)","[1,2)","[90,100)","[5,6)"}'::text
+ );
+
+-- warn: range bounds histogram containing an empty range
+SELECT pg_catalog.pg_restore_attribute_stats(
+ 'schemaname', 'stats_import',
+ 'relname', 'test',
+ 'attname', 'arange',
+ 'inherited', false::boolean,
+ 'range_bounds_histogram', '{empty,"[1,2)","[3,4)"}'::text
+ );
+
-- warn: cannot set most_common_elems for range type, rest ok
SELECT pg_catalog.pg_restore_attribute_stats(
'schemaname', 'stats_import',
@@ -1696,6 +1714,22 @@ SELECT pg_catalog.pg_restore_extended_stats(
'statistics_name', 'test_mr_stat',
'inherited', false,
'exprs', '[{"range_bounds_histogram": "{\"[1,10200)\"}", "range_length_histogram": "{10179}"}]'::jsonb);
+-- warn: range bounds histogram whose bounds are not sorted ascending
+SELECT pg_catalog.pg_restore_extended_stats(
+ 'schemaname', 'stats_import',
+ 'relname', 'test_mr',
+ 'statistics_schemaname', 'stats_import',
+ 'statistics_name', 'test_mr_stat',
+ 'inherited', false,
+ 'exprs', '[{"range_length_histogram": "{10179,10189,10199}", "range_empty_frac": "0", "range_bounds_histogram": "{\"[50,60)\",\"[1,2)\",\"[90,100)\"}"}]'::jsonb);
+-- warn: range bounds histogram containing an empty range
+SELECT pg_catalog.pg_restore_extended_stats(
+ 'schemaname', 'stats_import',
+ 'relname', 'test_mr',
+ 'statistics_schemaname', 'stats_import',
+ 'statistics_name', 'test_mr_stat',
+ 'inherited', false,
+ 'exprs', '[{"range_length_histogram": "{10179,10189,10199}", "range_empty_frac": "0", "range_bounds_histogram": "{empty,\"[1,2)\",\"[3,4)\"}"}]'::jsonb);
-- ok: multirange stats
SELECT pg_catalog.pg_restore_extended_stats(
--
2.47.3
^ permalink raw reply [nested|flat] 5+ messages in thread
* Re: Reject ill-formed range bounds histograms in pg_restore_attribute_stats()
@ 2026-07-09 05:28 Michael Paquier <[email protected]>
parent: Ewan Young <[email protected]>
0 siblings, 0 replies; 5+ messages in thread
From: Michael Paquier @ 2026-07-09 05:28 UTC (permalink / raw)
To: Ewan Young <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>; Corey Huinker <[email protected]>
On Wed, Jul 08, 2026 at 12:16:27PM +0800, Ewan Young wrote:
> Done in v2. I moved the check to stat_utils.c as stats_check_bounds_histogram()
> and now call it from both pg_restore_attribute_stats() and
> pg_restore_extended_stats(). In the extended path I reject an ill-formed
> histogram the same way the sibling stakinds there already do — WARNING plus
> goto pg_statistic_error.
Thanks. I have simplified some of the very-talkative comments,
adjusted one place where array_in_safe() was not mentioned (for
extstats expressions we rely on it for NULLs), tweaked a few more
minor things and applied on HEAD.
> Agreed — v2 targets master only, no back-branch patches.
If somebody has an argument to make here, please feel free.
--
Michael
Attachments:
[application/pgp-signature] signature.asc (833B, ../../[email protected]/2-signature.asc)
download
^ permalink raw reply [nested|flat] 5+ messages in thread
end of thread, other threads:[~2026-07-09 05:28 UTC | newest]
Thread overview: 5+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-07-07 08:13 Reject ill-formed range bounds histograms in pg_restore_attribute_stats() Ewan Young <[email protected]>
2026-07-07 08:27 ` Michael Paquier <[email protected]>
2026-07-08 02:17 ` Michael Paquier <[email protected]>
2026-07-08 04:16 ` Ewan Young <[email protected]>
2026-07-09 05:28 ` Michael Paquier <[email protected]>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox