From efec3efc13771d31a258e3c6170df1f6717cf915 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Thu, 12 Nov 2026 15:50:46 -0500
Subject: [PATCH v19842.2 3/3] Allow empty arrays of values in
 pg_restore_extended_stats().

ANALYZE can store an empty array of values for range_length_histogram
and most_common_elems, which pg_dump emits as '{}', since 261f89a976b.
These were rejected by pg_restore_extended_stats(), as an empty array
has no dimensions, and the statistics were lost after a dump/restore or
an upgrade *from* v19 or HEAD.  Empty arrays are not possible for these
two properties in v18 and older versions.

This is essentially adapting 2656f664616d to work on extended stats,
where the underlying functions are similar.

Reported-by: chunling qin <303677365@qq.com>
Author: Michael Paquier <michael@paquier.xyz>
Author: Corey Huinker <corey.huinker@gmail.com>
Discussion: https://postgr.es/m/19842-ca042326dd2430a4@postgresql.org
Bug: #19842
Backpatch-through: 19
---
 src/backend/statistics/extended_stats_funcs.c | 39 ++++++++++++-------
 src/test/regress/expected/stats_import.out    | 37 ++++++++++++++++++
 src/test/regress/sql/stats_import.sql         | 31 +++++++++++++++
 3 files changed, 92 insertions(+), 15 deletions(-)

diff --git a/src/backend/statistics/extended_stats_funcs.c b/src/backend/statistics/extended_stats_funcs.c
index 020d64086ce..798c8fc1d29 100644
--- a/src/backend/statistics/extended_stats_funcs.c
+++ b/src/backend/statistics/extended_stats_funcs.c
@@ -166,7 +166,7 @@ static bool key_in_expr_argnames(JsonbValue *key);
 static bool check_all_expr_argnames_valid(JsonbContainer *cont, AttrNumber exprnum);
 static Datum array_in_safe(FmgrInfo *array_in, const char *s, Oid typid,
 						   int32 typmod, AttrNumber exprnum,
-						   const char *element_name, bool *ok);
+						   const char *element_name, bool allow_empty, bool *ok);
 static Datum import_pg_statistic(Relation pgsd, JsonbContainer *cont,
 								 AttrNumber exprnum, FmgrInfo *array_in_fn,
 								 Oid typid, int32 typmod, Oid typcoll,
@@ -1063,9 +1063,11 @@ jbv_to_infunc_datum(JsonbValue *jval, PGFunction func, AttrNumber exprnum,
  */
 static Datum
 array_in_safe(FmgrInfo *array_in, const char *s, Oid typid, int32 typmod,
-			  AttrNumber exprnum, const char *element_name, bool *ok)
+			  AttrNumber exprnum, const char *element_name, bool allow_empty,
+			  bool *ok)
 {
 	Datum		result;
+	int			ndim;
 
 	ErrorSaveContext escontext = {
 		.type = T_ErrorSaveContext,
@@ -1096,12 +1098,19 @@ array_in_safe(FmgrInfo *array_in, const char *s, Oid typid, int32 typmod,
 		return (Datum) 0;
 	}
 
-	if (ARR_NDIM(DatumGetArrayTypeP(result)) != 1)
+	ndim = ARR_NDIM(DatumGetArrayTypeP(result));
+	if (ndim != 1 && !(allow_empty && ndim == 0))
 	{
-		ereport(WARNING,
-				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				 errmsg("could not import element \"%s\" in expression %d: must be a one-dimensional array",
-						element_name, exprnum)));
+		if (allow_empty)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("could not import element \"%s\" in expression %d: must be an empty or one-dimensional array",
+							element_name, exprnum)));
+		else
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("could not import element \"%s\" in expression %d: must be a one-dimensional array",
+							element_name, exprnum)));
 		return (Datum) 0;
 	}
 
@@ -1354,14 +1363,14 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont,
 		s = jbv_string_get_cstr(&val[MOST_COMMON_VALS_ELEM]);
 		stavalues = array_in_safe(array_in_fn, s, typid, typmod, exprnum,
 								  extexprargname[MOST_COMMON_VALS_ELEM],
-								  &val_ok);
+								  false, &val_ok);
 
 		pfree(s);
 
 		s = jbv_string_get_cstr(&val[MOST_COMMON_FREQS_ELEM]);
 		stanumbers = array_in_safe(array_in_fn, s, FLOAT4OID, -1, exprnum,
 								   extexprargname[MOST_COMMON_FREQS_ELEM],
-								   &num_ok);
+								   false, &num_ok);
 		pfree(s);
 
 		/* Only set the slot if both datums have been built */
@@ -1400,7 +1409,7 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont,
 
 		stavalues = array_in_safe(array_in_fn, s, typid, typmod, exprnum,
 								  extexprargname[HISTOGRAM_BOUNDS_ELEM],
-								  &val_ok);
+								  false, &val_ok);
 		pfree(s);
 
 		if (val_ok)
@@ -1444,14 +1453,14 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont,
 		s = jbv_string_get_cstr(&val[MOST_COMMON_ELEMS_ELEM]);
 		stavalues = array_in_safe(array_in_fn, s, elemtypid, typmod, exprnum,
 								  extexprargname[MOST_COMMON_ELEMS_ELEM],
-								  &val_ok);
+								  true, &val_ok);
 		pfree(s);
 
 
 		s = jbv_string_get_cstr(&val[MOST_COMMON_ELEM_FREQS_ELEM]);
 		stanumbers = array_in_safe(array_in_fn, s, FLOAT4OID, -1, exprnum,
 								   extexprargname[MOST_COMMON_ELEM_FREQS_ELEM],
-								   &num_ok);
+								   false, &num_ok);
 		pfree(s);
 
 		/* Only set the slot if both datums have been built */
@@ -1474,7 +1483,7 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont,
 		s = jbv_string_get_cstr(&val[ELEM_COUNT_HISTOGRAM_ELEM]);
 		stanumbers = array_in_safe(array_in_fn, s, FLOAT4OID, -1, exprnum,
 								   extexprargname[ELEM_COUNT_HISTOGRAM_ELEM],
-								   &num_ok);
+								   false, &num_ok);
 		pfree(s);
 
 		if (num_ok)
@@ -1503,7 +1512,7 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont,
 
 		stavalues = array_in_safe(array_in_fn, s, rtypid, typmod, exprnum,
 								  extexprargname[RANGE_BOUNDS_HISTOGRAM_ELEM],
-								  &val_ok);
+								  false, &val_ok);
 
 		if (val_ok && statatt_check_bounds_histogram(stavalues))
 			statatt_set_slot(values, nulls, replaces,
@@ -1536,7 +1545,7 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont,
 		s = jbv_string_get_cstr(&val[RANGE_LENGTH_HISTOGRAM_ELEM]);
 		stavalues = array_in_safe(array_in_fn, s, FLOAT8OID, -1, exprnum,
 								  extexprargname[RANGE_LENGTH_HISTOGRAM_ELEM],
-								  &val_ok);
+								  true, &val_ok);
 
 		if (val_ok)
 			statatt_set_slot(values, nulls, replaces,
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index bbd32fc19a5..25f69c329d0 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -4388,6 +4388,43 @@ WARNING:  "most_common_elems" must be an empty or one-dimensional array
  f
 (1 row)
 
+-- BUG #19842
+TRUNCATE TABLE stats_import.test;
+INSERT INTO stats_import.test(id, arange)
+VALUES (1, int4range(1,5)), (2, 'empty'::int4range);
+CREATE STATISTICS stats_import.range_stat ON (arange * int4range(1,10))
+FROM stats_import.test;
+ANALYZE stats_import.test;
+SELECT * FROM pg_catalog.pg_restore_extended_stats(
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'statistics_schemaname', 'stats_import',
+    'statistics_name', 'range_stat',
+    'inherited', 'f'::boolean,
+    'exprs', '[{"avg_width": "13","null_frac": "0","n_distinct": "-1",
+                "range_empty_frac": "0.5"}]'::jsonb
+);
+WARNING:  could not parse "exprs": invalid element in expression -1
+HINT:  "range_length_histogram" and "range_empty_frac" must be both either strings or nulls.
+ pg_restore_extended_stats 
+---------------------------
+ f
+(1 row)
+
+SELECT * FROM pg_catalog.pg_restore_extended_stats(
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'statistics_schemaname', 'stats_import',
+    'statistics_name', 'range_stat',
+    'inherited', 'f'::boolean,
+    'exprs', '[{"avg_width": "13","null_frac": "0","n_distinct": "-1",
+                "range_length_histogram": "{}", "range_empty_frac": "0.5"}]'::jsonb
+);
+ pg_restore_extended_stats 
+---------------------------
+ t
+(1 row)
+
 DROP SCHEMA stats_import CASCADE;
 NOTICE:  drop cascades to 39 other objects
 DETAIL:  drop cascades to view stats_import.pg_stats_stable
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index 161673d3480..9c99f0d62ff 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -3103,4 +3103,35 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'most_common_elems', '{{one},{two}}'::text,
     'most_common_elem_freqs', '{0.1,0.1,0.1,0.1,0}'::real[]);
 
+-- BUG #19842
+TRUNCATE TABLE stats_import.test;
+
+INSERT INTO stats_import.test(id, arange)
+VALUES (1, int4range(1,5)), (2, 'empty'::int4range);
+
+CREATE STATISTICS stats_import.range_stat ON (arange * int4range(1,10))
+FROM stats_import.test;
+
+ANALYZE stats_import.test;
+
+SELECT * FROM pg_catalog.pg_restore_extended_stats(
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'statistics_schemaname', 'stats_import',
+    'statistics_name', 'range_stat',
+    'inherited', 'f'::boolean,
+    'exprs', '[{"avg_width": "13","null_frac": "0","n_distinct": "-1",
+                "range_empty_frac": "0.5"}]'::jsonb
+);
+
+SELECT * FROM pg_catalog.pg_restore_extended_stats(
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'statistics_schemaname', 'stats_import',
+    'statistics_name', 'range_stat',
+    'inherited', 'f'::boolean,
+    'exprs', '[{"avg_width": "13","null_frac": "0","n_distinct": "-1",
+                "range_length_histogram": "{}", "range_empty_frac": "0.5"}]'::jsonb
+);
+
 DROP SCHEMA stats_import CASCADE;
-- 
2.56.0

