postgres.git / summary / log / commit / refs
commit b3d9262bbddcbfd1be1dbd43a48bc155234d21e1
Author: Michael Paquier <michael@paquier.xyz>
Commit: Noah Misch <noah@leadboat.com>
Date: Mon Aug 10 13:38:05 2026 +0000
Fix multirange type handling in pg_restore_attribute_stats()
statatt_get_type() unconditionally converted multirange types to their
underlying range type. This choice affected all the type information,
like atttypid, atttyptype and operators.
This made the bounds histogram work correctly (range type is required),
but it was wrong for all the other stat kinds. MCV values for a
multirange column should be parsed as multirange arrays, not range
arrays. It also made the TYPTYPE_MULTIRANGE check for the range stats
validation as dead code, since atttyptype was always TYPTYPE_RANGE
after the conversion due to the centralized statatt_get_type().
pg_restore_extended_stats() handles the same case correctly: it keeps
the original type and explicitly converts to the range type only at the
point where range_histogram_bounds is built.
The fix of this issue is simple: the multirange-to-range conversion
needs to be moved from the centralized statatt_get_type() up to where
attribute stats build their range_histogram_bounds, matching what is
done for extended statistics restore.
The regression tests for multiranges with attribute stats are extended
to cover this case.
Author: OpenAI Security Research Team
Backpatch-through: 18
Security: CVE-2026-16238
src/backend/statistics/attribute_stats.c | 10 +++++++++-
src/backend/statistics/stat_utils.c | 7 -------
src/test/regress/expected/stats_import.out | 20 ++++++++++++++++++++
src/test/regress/sql/stats_import.sql | 14 ++++++++++++++
4 files changed, 43 insertions(+), 8 deletions(-)
diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index c133b8ad6c9..e65c8dac4a4 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -498,11 +498,19 @@ attribute_statistics_update_internal(Oid reloid,
{
bool converted = false;
Datum stavalues;
+ Oid bounds_typid = atttypid;
+
+ /*
+ * If it's a multirange, step down to the range type, as is done by
+ * multirange_typanalyze().
+ */
+ if (type_is_multirange(atttypid))
+ bounds_typid = get_multirange_range(atttypid);
stavalues = statatt_build_stavalues("range_bounds_histogram",
&array_in_fn,
PG_GETARG_DATUM(RANGE_BOUNDS_HISTOGRAM_ARG),
- atttypid, atttypmod,
+ bounds_typid, atttypmod,
&converted);
if (converted)
diff --git a/src/backend/statistics/stat_utils.c b/src/backend/statistics/stat_utils.c
index 0b190e88237..ba204cd5e77 100644
--- a/src/backend/statistics/stat_utils.c
+++ b/src/backend/statistics/stat_utils.c
@@ -491,13 +491,6 @@ statatt_get_type(Oid reloid, AttrNumber attnum,
}
ReleaseSysCache(atup);
- /*
- * If it's a multirange, step down to the range type, as is done by
- * multirange_typanalyze().
- */
- if (type_is_multirange(*atttypid))
- *atttypid = get_multirange_range(*atttypid);
-
/* finds the right operators even if atttypid is a domain */
typcache = lookup_type_cache(*atttypid, TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR);
*atttyptype = typcache->typtype;
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index fa086195e65..c484a77f9e0 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -1432,12 +1432,32 @@ VALUES
(1, 'red', '{[1,3),[5,9),[20,30)}'::int4multirange),
(2, 'red', '{[11,13),[15,19),[20,30)}'::int4multirange),
(3, 'red', '{[21,23),[25,29),[120,130)}'::int4multirange);
+-- warn: reject range values as ordinary multirange statistics
+SELECT pg_catalog.pg_restore_attribute_stats(
+ 'schemaname', 'stats_import',
+ 'relname', 'test_mr',
+ 'attname', 'mrange',
+ 'inherited', false,
+ 'most_common_vals', ARRAY['[1,3)']::text,
+ 'most_common_freqs', ARRAY[1.0]::real[]
+);
+WARNING: malformed multirange literal: "[1,3)"
+DETAIL: Missing left brace.
+ pg_restore_attribute_stats
+----------------------------
+ f
+(1 row)
+
-- ensure that we set attribute stats for a multirange
+-- MCVs and histograms retain the multirange type.
SELECT pg_catalog.pg_restore_attribute_stats(
'schemaname', 'stats_import',
'relname', 'test_mr',
'attname', 'mrange',
'inherited', false,
+ 'most_common_vals', ARRAY['{[1,3),[5,9)}', '{[11,13),[15,19)}']::text,
+ 'most_common_freqs', ARRAY[0.6, 0.4]::real[],
+ 'histogram_bounds', ARRAY['{[1,3)}', '{[11,13)}', '{[21,23)}']::text,
'range_length_histogram', '{19,29,109}'::text,
'range_empty_frac', '0'::real,
'range_bounds_histogram', '{"[1,30)","[11,30)","[21,130)"}'::text
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index 812a8335e6b..2eb50adf40b 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -1039,12 +1039,26 @@ VALUES
(2, 'red', '{[11,13),[15,19),[20,30)}'::int4multirange),
(3, 'red', '{[21,23),[25,29),[120,130)}'::int4multirange);
+-- warn: reject range values as ordinary multirange statistics
+SELECT pg_catalog.pg_restore_attribute_stats(
+ 'schemaname', 'stats_import',
+ 'relname', 'test_mr',
+ 'attname', 'mrange',
+ 'inherited', false,
+ 'most_common_vals', ARRAY['[1,3)']::text,
+ 'most_common_freqs', ARRAY[1.0]::real[]
+);
+
-- ensure that we set attribute stats for a multirange
+-- MCVs and histograms retain the multirange type.
SELECT pg_catalog.pg_restore_attribute_stats(
'schemaname', 'stats_import',
'relname', 'test_mr',
'attname', 'mrange',
'inherited', false,
+ 'most_common_vals', ARRAY['{[1,3),[5,9)}', '{[11,13),[15,19)}']::text,
+ 'most_common_freqs', ARRAY[0.6, 0.4]::real[],
+ 'histogram_bounds', ARRAY['{[1,3)}', '{[11,13)}', '{[21,23)}']::text,
'range_length_histogram', '{19,29,109}'::text,
'range_empty_frac', '0'::real,
'range_bounds_histogram', '{"[1,30)","[11,30)","[21,130)"}'::text
[parent: bf1bb7e29cb1]