From d0836e27fb4e3163eccee51e2580fac1fac071b9 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Thu, 24 Sep 2026 01:53:04 -0400
Subject: [PATCH v1] Fix import of range statistics for domains.

WIP
---
 src/backend/statistics/attribute_stats.c      |  59 +++++---
 src/backend/statistics/extended_stats_funcs.c |  56 +++++---
 src/test/regress/expected/stats_import.out    | 130 +++++++++++++++++-
 src/test/regress/sql/stats_import.sql         | 109 +++++++++++++++
 4 files changed, 314 insertions(+), 40 deletions(-)

diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index c35892ce6d0..2539e097ac3 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -229,6 +229,8 @@ attribute_statistics_update_internal(Oid reloid,
 	Oid			elemtypid = InvalidOid;
 	Oid			elem_eq_opr = InvalidOid;
 
+	Oid			bounds_typid = InvalidOid;
+
 	FmgrInfo	array_in_fn;
 
 	bool		do_mcv = !PG_ARGISNULL(MOST_COMMON_FREQS_ARG) &&
@@ -333,18 +335,47 @@ attribute_statistics_update_internal(Oid reloid,
 	}
 
 	/* only range types can have range stats */
-	if ((do_range_length_histogram || do_bounds_histogram) &&
-		!(atttyptype == TYPTYPE_RANGE || atttyptype == TYPTYPE_MULTIRANGE))
+	if (do_range_length_histogram || do_bounds_histogram)
 	{
-		ereport(WARNING,
-				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				 errmsg("column \"%s\" is not a range type", attname),
-				 errdetail("Cannot set %s or %s.",
-						   "STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM", "STATISTIC_KIND_BOUNDS_HISTOGRAM")));
+		char		bounds_typtype = atttyptype;
 
-		do_bounds_histogram = false;
-		do_range_length_histogram = false;
-		result = false;
+		bounds_typid = atttypid;
+
+		/*
+		 * If attribute type is a domain, step down to the base type, which
+		 * should be the expected range or multirange type.
+		 */
+		if (bounds_typtype == TYPTYPE_DOMAIN)
+		{
+			bounds_typid = getBaseType(bounds_typid);
+			bounds_typtype = get_typtype(bounds_typid);
+		}
+
+		switch (bounds_typtype)
+		{
+			case TYPTYPE_RANGE:
+				/* Yes, it can have range stats */
+				break;
+			case TYPTYPE_MULTIRANGE:
+
+				/*
+				 * Yes, but we need to step down to the range type, as is done
+				 * by multirange_typanalyze().
+				 */
+				bounds_typid = get_multirange_range(bounds_typid);
+				break;
+			default:
+				ereport(WARNING,
+						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						errmsg("column \"%s\" is not a range type", attname),
+						errdetail("Cannot set %s or %s.",
+								  "STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM",
+								  "STATISTIC_KIND_BOUNDS_HISTOGRAM"));
+
+				do_bounds_histogram = false;
+				do_range_length_histogram = false;
+				result = false;
+		}
 	}
 
 	fmgr_info(F_ARRAY_IN, &array_in_fn);
@@ -498,14 +529,6 @@ 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,
diff --git a/src/backend/statistics/extended_stats_funcs.c b/src/backend/statistics/extended_stats_funcs.c
index cb965fdb868..b62b10a0dea 100644
--- a/src/backend/statistics/extended_stats_funcs.c
+++ b/src/backend/statistics/extended_stats_funcs.c
@@ -1125,6 +1125,7 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont,
 	Oid			elemeqopr = InvalidOid;
 	bool		found[NUM_ATTRIBUTE_STATS_ELEMS] = {0};
 	JsonbValue	val[NUM_ATTRIBUTE_STATS_ELEMS] = {0};
+	Oid			bounds_typid = InvalidOid;
 
 	/* Assume the worst by default. */
 	*pg_statistic_ok = false;
@@ -1253,24 +1254,45 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont,
 
 	/*
 	 * These three fields can only be set if dealing with a range or
-	 * multi-range type.
+	 * multi-range type, or a domain of either.
 	 */
 	if (found[RANGE_LENGTH_HISTOGRAM_ELEM] ||
 		found[RANGE_EMPTY_FRAC_ELEM] ||
 		found[RANGE_BOUNDS_HISTOGRAM_ELEM])
 	{
-		if (typcache->typtype != TYPTYPE_RANGE &&
-			typcache->typtype != TYPTYPE_MULTIRANGE)
+		char		bounds_typtype = typcache->typtype;
+
+		bounds_typid = typid;
+
+		if (bounds_typtype == TYPTYPE_DOMAIN)
 		{
-			ereport(WARNING,
-					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					errmsg("could not parse \"%s\": invalid data in expression %d",
-						   argname, exprnum),
-					errhint("\"%s\", \"%s\", and \"%s\" can only be set for a range type.",
-							extexprargname[RANGE_LENGTH_HISTOGRAM_ELEM],
-							extexprargname[RANGE_EMPTY_FRAC_ELEM],
-							extexprargname[RANGE_BOUNDS_HISTOGRAM_ELEM]));
-			goto pg_statistic_error;
+			bounds_typid = getBaseType(typid);
+			bounds_typtype = get_typtype(bounds_typid);
+		}
+
+		switch (bounds_typtype)
+		{
+			case TYPTYPE_RANGE:
+				bounds_typid = typid;
+				break;
+			case TYPTYPE_MULTIRANGE:
+
+				/*
+				 * If it's a multirange, step down to the range type, as is
+				 * done by multirange_typanalyze().
+				 */
+				bounds_typid = get_multirange_range(bounds_typid);
+				break;
+			default:
+				ereport(WARNING,
+						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						errmsg("could not parse \"%s\": invalid data in expression %d",
+							   argname, exprnum),
+						errhint("\"%s\", \"%s\", and \"%s\" can only be set for a range type.",
+								extexprargname[RANGE_LENGTH_HISTOGRAM_ELEM],
+								extexprargname[RANGE_EMPTY_FRAC_ELEM],
+								extexprargname[RANGE_BOUNDS_HISTOGRAM_ELEM]));
+				goto pg_statistic_error;
 		}
 	}
 
@@ -1476,18 +1498,10 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont,
 		Datum		stavalues;
 		bool		val_ok = false;
 		char	   *s;
-		Oid			rtypid = typid;
-
-		/*
-		 * If it's a multirange, step down to the range type, as is done by
-		 * multirange_typanalyze().
-		 */
-		if (type_is_multirange(typid))
-			rtypid = get_multirange_range(typid);
 
 		s = jbv_string_get_cstr(&val[RANGE_BOUNDS_HISTOGRAM_ELEM]);
 
-		stavalues = array_in_safe(array_in_fn, s, rtypid, typmod, exprnum,
+		stavalues = array_in_safe(array_in_fn, s, bounds_typid, typmod, exprnum,
 								  extexprargname[RANGE_BOUNDS_HISTOGRAM_ELEM],
 								  &val_ok);
 
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index 4ce176c2667..f615ec94ed1 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -3827,8 +3827,133 @@ SELECT COUNT(*) FROM stats_import.test_range_expr_null
     19
 (1 row)
 
+-- BUG #19715
+CREATE DOMAIN stats_import.restore_stats_mr AS int4multirange;
+CREATE TABLE stats_import.mr_domain (u integer, v stats_import.restore_stats_mr);
+CREATE TABLE stats_import.mr_domain_clone (u integer, v stats_import.restore_stats_mr);
+-- Create statistics that force an expression
+CREATE STATISTICS stats_import.mr_domain_stat
+ON u, (stats_import.restore_stats_mr(v + '{[-10,-5)}'::stats_import.restore_stats_mr))
+FROM stats_import.mr_domain;
+CREATE STATISTICS stats_import.mr_domain_stat_clone
+ON u, (stats_import.restore_stats_mr(v + '{[-10,-5)}'::stats_import.restore_stats_mr))
+FROM stats_import.mr_domain_clone;
+INSERT INTO stats_import.mr_domain VALUES
+  (1, '{[1,3)}'), (2, '{[5,9)}'), (3, '{[11,15)}');
+ANALYZE stats_import.mr_domain;
+-- Confirm both table and expression index have rows
+SELECT COUNT(*)
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'mr_domain'
+AND inherited = false;
+ count 
+-------
+     2
+(1 row)
+
+SELECT COUNT(*)
+FROM pg_stats_ext_exprs AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'mr_domain_stat';
+ count 
+-------
+     1
+(1 row)
+
+--
+-- Copy stats from test to mr_domain to mr_domain_clone
+--
+SELECT s.schemaname, s.tablename, s.attname, s.inherited, r.*
+FROM pg_catalog.pg_stats AS s
+CROSS JOIN LATERAL
+    pg_catalog.pg_restore_attribute_stats(
+        'schemaname', 'stats_import',
+        'relname', 'mr_domain_clone',
+        'attname', s.attname::text,
+        'inherited', s.inherited,
+        'version', 150000,
+        'null_frac', s.null_frac,
+        'avg_width', s.avg_width,
+        'n_distinct', s.n_distinct,
+        'most_common_vals', s.most_common_vals::text,
+        'most_common_freqs', s.most_common_freqs,
+        'histogram_bounds', s.histogram_bounds::text,
+        'correlation', s.correlation,
+        'most_common_elems', s.most_common_elems::text,
+        'most_common_elem_freqs', s.most_common_elem_freqs,
+        'elem_count_histogram', s.elem_count_histogram,
+        'range_bounds_histogram', s.range_bounds_histogram::text,
+        'range_empty_frac', s.range_empty_frac,
+        'range_length_histogram', s.range_length_histogram::text) AS r
+WHERE s.schemaname = 'stats_import'
+AND s.tablename IN ('mr_domain')
+ORDER BY s.tablename, s.attname, s.inherited;
+  schemaname  | tablename | attname | inherited | r 
+--------------+-----------+---------+-----------+---
+ stats_import | mr_domain | u       | f         | t
+ stats_import | mr_domain | v       | f         | t
+(2 rows)
+
+SELECT relname, (stats).*
+FROM stats_import.pg_statistic_get_difference('mr_domain', 'mr_domain_clone')
+\gx
+(0 rows)
+
+-- Copy stats from mr_domain_stat to mr_domain_stat_clone
+SELECT e.statistics_name,
+  pg_catalog.pg_restore_extended_stats(
+    'schemaname', e.statistics_schemaname::text,
+    'relname', 'mr_domain_clone',
+    'statistics_schemaname', e.statistics_schemaname::text,
+    'statistics_name', 'mr_domain_stat_clone',
+    'inherited', e.inherited,
+    'n_distinct', e.n_distinct,
+    'dependencies', e.dependencies,
+    'most_common_vals', e.most_common_vals,
+    'most_common_freqs', e.most_common_freqs,
+    'most_common_base_freqs', e.most_common_base_freqs,
+    'exprs', x.exprs)
+FROM pg_stats_ext AS e
+CROSS JOIN LATERAL (
+  SELECT jsonb_agg(jsonb_strip_nulls(jsonb_build_object(
+            'null_frac', ee.null_frac::text,
+            'avg_width', ee.avg_width::text,
+            'n_distinct', ee.n_distinct::text,
+            'most_common_vals', ee.most_common_vals::text,
+            'most_common_freqs', ee.most_common_freqs::text,
+            'histogram_bounds', ee.histogram_bounds::text,
+            'correlation', ee.correlation::text,
+            'most_common_elems', ee.most_common_elems::text,
+            'most_common_elem_freqs', ee.most_common_elem_freqs::text,
+            'elem_count_histogram', ee.elem_count_histogram::text,
+            'range_length_histogram', ee.range_length_histogram::text,
+            'range_empty_frac', ee.range_empty_frac::text,
+            'range_bounds_histogram', ee.range_bounds_histogram::text)))
+    FROM pg_stats_ext_exprs AS ee
+    WHERE ee.statistics_schemaname = e.statistics_schemaname AND
+      ee.statistics_name = e.statistics_name AND
+      ee.inherited = e.inherited
+    ) AS x(exprs)
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'mr_domain_stat';
+ statistics_name | pg_restore_extended_stats 
+-----------------+---------------------------
+ mr_domain_stat  | t
+(1 row)
+
+SELECT statname, (stats).*
+FROM stats_import.pg_stats_ext_get_difference('mr_domain_stat', 'mr_domain_stat_clone')
+\gx
+(0 rows)
+
+SELECT statname, (stats).*
+FROM stats_import.pg_stats_ext_exprs_get_difference('mr_domain_stat', 'mr_domain_stat_clone')
+\gx
+(0 rows)
+
 DROP SCHEMA stats_import CASCADE;
-NOTICE:  drop cascades to 19 other objects
+NOTICE:  drop cascades to 22 other objects
 DETAIL:  drop cascades to view stats_import.pg_stats_stable
 drop cascades to view stats_import.pg_statistic_flat_t
 drop cascades to function stats_import.pg_statistic_flat(text)
@@ -3848,3 +3973,6 @@ drop cascades to view stats_import.testview
 drop cascades to table stats_import.test_clone
 drop cascades to table stats_import.test_mr_clone
 drop cascades to table stats_import.test_range_expr_null
+drop cascades to type stats_import.restore_stats_mr
+drop cascades to table stats_import.mr_domain
+drop cascades to table stats_import.mr_domain_clone
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index 748c9a2e000..74012ff5678 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -2647,4 +2647,113 @@ SELECT * FROM stats_import.test_range_expr_null
 SELECT COUNT(*) FROM stats_import.test_range_expr_null
   WHERE (rng * int4range(50, 150)) && '[60,70)'::int4range;
 
+-- BUG #19715
+CREATE DOMAIN stats_import.restore_stats_mr AS int4multirange;
+CREATE TABLE stats_import.mr_domain (u integer, v stats_import.restore_stats_mr);
+CREATE TABLE stats_import.mr_domain_clone (u integer, v stats_import.restore_stats_mr);
+
+-- Create statistics that force an expression
+CREATE STATISTICS stats_import.mr_domain_stat
+ON u, (stats_import.restore_stats_mr(v + '{[-10,-5)}'::stats_import.restore_stats_mr))
+FROM stats_import.mr_domain;
+CREATE STATISTICS stats_import.mr_domain_stat_clone
+ON u, (stats_import.restore_stats_mr(v + '{[-10,-5)}'::stats_import.restore_stats_mr))
+FROM stats_import.mr_domain_clone;
+
+INSERT INTO stats_import.mr_domain VALUES
+  (1, '{[1,3)}'), (2, '{[5,9)}'), (3, '{[11,15)}');
+ANALYZE stats_import.mr_domain;
+
+-- Confirm both table and expression index have rows
+SELECT COUNT(*)
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'mr_domain'
+AND inherited = false;
+
+SELECT COUNT(*)
+FROM pg_stats_ext_exprs AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'mr_domain_stat';
+
+
+--
+-- Copy stats from test to mr_domain to mr_domain_clone
+--
+SELECT s.schemaname, s.tablename, s.attname, s.inherited, r.*
+FROM pg_catalog.pg_stats AS s
+CROSS JOIN LATERAL
+    pg_catalog.pg_restore_attribute_stats(
+        'schemaname', 'stats_import',
+        'relname', 'mr_domain_clone',
+        'attname', s.attname::text,
+        'inherited', s.inherited,
+        'version', 150000,
+        'null_frac', s.null_frac,
+        'avg_width', s.avg_width,
+        'n_distinct', s.n_distinct,
+        'most_common_vals', s.most_common_vals::text,
+        'most_common_freqs', s.most_common_freqs,
+        'histogram_bounds', s.histogram_bounds::text,
+        'correlation', s.correlation,
+        'most_common_elems', s.most_common_elems::text,
+        'most_common_elem_freqs', s.most_common_elem_freqs,
+        'elem_count_histogram', s.elem_count_histogram,
+        'range_bounds_histogram', s.range_bounds_histogram::text,
+        'range_empty_frac', s.range_empty_frac,
+        'range_length_histogram', s.range_length_histogram::text) AS r
+WHERE s.schemaname = 'stats_import'
+AND s.tablename IN ('mr_domain')
+ORDER BY s.tablename, s.attname, s.inherited;
+
+SELECT relname, (stats).*
+FROM stats_import.pg_statistic_get_difference('mr_domain', 'mr_domain_clone')
+\gx
+
+-- Copy stats from mr_domain_stat to mr_domain_stat_clone
+SELECT e.statistics_name,
+  pg_catalog.pg_restore_extended_stats(
+    'schemaname', e.statistics_schemaname::text,
+    'relname', 'mr_domain_clone',
+    'statistics_schemaname', e.statistics_schemaname::text,
+    'statistics_name', 'mr_domain_stat_clone',
+    'inherited', e.inherited,
+    'n_distinct', e.n_distinct,
+    'dependencies', e.dependencies,
+    'most_common_vals', e.most_common_vals,
+    'most_common_freqs', e.most_common_freqs,
+    'most_common_base_freqs', e.most_common_base_freqs,
+    'exprs', x.exprs)
+FROM pg_stats_ext AS e
+CROSS JOIN LATERAL (
+  SELECT jsonb_agg(jsonb_strip_nulls(jsonb_build_object(
+            'null_frac', ee.null_frac::text,
+            'avg_width', ee.avg_width::text,
+            'n_distinct', ee.n_distinct::text,
+            'most_common_vals', ee.most_common_vals::text,
+            'most_common_freqs', ee.most_common_freqs::text,
+            'histogram_bounds', ee.histogram_bounds::text,
+            'correlation', ee.correlation::text,
+            'most_common_elems', ee.most_common_elems::text,
+            'most_common_elem_freqs', ee.most_common_elem_freqs::text,
+            'elem_count_histogram', ee.elem_count_histogram::text,
+            'range_length_histogram', ee.range_length_histogram::text,
+            'range_empty_frac', ee.range_empty_frac::text,
+            'range_bounds_histogram', ee.range_bounds_histogram::text)))
+    FROM pg_stats_ext_exprs AS ee
+    WHERE ee.statistics_schemaname = e.statistics_schemaname AND
+      ee.statistics_name = e.statistics_name AND
+      ee.inherited = e.inherited
+    ) AS x(exprs)
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'mr_domain_stat';
+
+SELECT statname, (stats).*
+FROM stats_import.pg_stats_ext_get_difference('mr_domain_stat', 'mr_domain_stat_clone')
+\gx
+
+SELECT statname, (stats).*
+FROM stats_import.pg_stats_ext_exprs_get_difference('mr_domain_stat', 'mr_domain_stat_clone')
+\gx
+
 DROP SCHEMA stats_import CASCADE;

base-commit: 4545cee303c257e58195e3d033c05bf38e2cd4d6
-- 
2.55.0

