Subject: Re: Extended Statistics set/restore/clear functions.

From: Corey Huinker <[email-masked]>
Date: Fri, 24 Jan 2025 00:20:59 -0500

>
>
> I agree, and I appreciate you working on it.
>

I've tried to incorporate all your feedback:

* duplicates in pg_ndistinct and pg_dependencies now cause the input
function to fail, with tests for each

* all the non-array, non-parametery stuff moved out of import_mcvlist so
that import_mcvlist can move to mcv.c in a sane way

* whitespace issues

* validating attnums inside pg_ndistinct. I held off on doing
pg_dependencies pending feedback on how I handled pg_ndistinct. tests for
invalid positive (attnum isn't involved in the extended stats object), 0
attnum (just wrong) and invalid negative (more than the number of
expressions in the object)

Still to do:

* attnum checking for dependencies, should be simple, once we've locked
down how to do it for pg_ndistinct

* move import_expressions code to expressions.c along with some other
expression-specific stuff.

* more tests, probably

I'd like to merge these down to 3 patches again, but I'm keeping them
separate for this patchset to isolate the attnum-checking code for this
go-round.
From 878672ff4456a6df6955a207811beb3fe60f226e Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Thu, 23 Jan 2025 23:36:54 -0500
Subject: [PATCH v2 5/6] Add attnum bounds checking routines to pg_ndistinct

---
 .../statistics/extended_stats_internal.h      |  3 +
 src/backend/statistics/mvdistinct.c           | 62 +++++++++++++++++++
 2 files changed, 65 insertions(+)

diff --git a/src/include/statistics/extended_stats_internal.h b/src/include/statistics/extended_stats_internal.h
index df1a5308f4..d5d4cddad5 100644
--- a/src/include/statistics/extended_stats_internal.h
+++ b/src/include/statistics/extended_stats_internal.h
@@ -130,5 +130,8 @@ extern Datum import_mcvlist(HeapTuple tup, int elevel, int numattrs,
 							Oid *atttypids, int32 *atttypmods, Oid *atttypcolls,
 							int nitems, Datum *mcv_elems, bool *mcv_nulls,
 							bool *mcv_elem_nulls, float8 *freqs, float8 *base_freqs);
+extern bool pg_ndistinct_validate_items(MVNDistinct *ndistinct, int2vector *stxkeys,
+										int numexprs, int elevel);
+extern void free_pg_ndistinct(MVNDistinct *ndistinct);
 
 #endif							/* EXTENDED_STATS_INTERNAL_H */
diff --git a/src/backend/statistics/mvdistinct.c b/src/backend/statistics/mvdistinct.c
index e2a3acaf78..e9c02aaa63 100644
--- a/src/backend/statistics/mvdistinct.c
+++ b/src/backend/statistics/mvdistinct.c
@@ -630,6 +630,68 @@ pg_ndistinct_in(PG_FUNCTION_ARGS)
 	PG_RETURN_NULL();
 }
 
+/*
+ * Free allocations of an MVNDistinct
+ */
+void
+free_pg_ndistinct(MVNDistinct *ndistinct)
+{
+	for (int i = 0; i < ndistinct->nitems; i++)
+		pfree(ndistinct->items[i].attributes);
+
+	pfree(ndistinct);
+}
+
+/*
+ * Validate an MVNDistinct against the extended statistics object definition.
+ *
+ * Every MVNDistinctItem must be checked to ensure that the attnums in the
+ * attributes list correspond to attnums/expressions defined by the
+ * extended statistics object.
+ *
+ * Positive attnums are attributes which must be found in the stxkeys,
+ * while negative attnums correspond to an expr number, so the attnum 
+ * can't be below (0 - numexprs).
+ */
+bool
+pg_ndistinct_validate_items(MVNDistinct *ndistinct, int2vector *stxkeys, int numexprs, int elevel)
+{
+	int		attnum_expr_lowbound = 0 - numexprs;
+
+	for (int i = 0; i < ndistinct->nitems; i++)
+	{
+		MVNDistinctItem item = ndistinct->items[i];
+
+		for (int j = 0; j < item.nattributes; j++)
+		{
+			AttrNumber	attnum = item.attributes[j];
+			bool		ok = false;
+
+			if (attnum > 0)
+			{
+				for (int k = 0; k < stxkeys->dim1; k++)
+					if (attnum == stxkeys->values[k])
+					{
+						ok = true;
+						break;
+					}
+			}
+			else if ((attnum < 0) && (attnum >= attnum_expr_lowbound))
+				ok = true;
+
+			if (!ok)
+			{
+				ereport(elevel,
+						(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+						errmsg("pg_ndistinct: invalid attnum for this statistics object: %d", attnum)));
+				return false;
+			}
+		}
+	}
+	return true;
+}
+
+
 /*
  * pg_ndistinct
  *		output routine for type pg_ndistinct
-- 
2.48.1

From 7c7a16da15469622fa9285ba61a9f053f8858ad4 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 17 Dec 2024 03:30:55 -0500
Subject: [PATCH v2 1/6] Add working input function for pg_ndistinct.

This is needed to import extended statistics.
---
 src/backend/statistics/mvdistinct.c     | 297 +++++++++++++++++++++++-
 src/test/regress/expected/stats_ext.out |   7 +
 src/test/regress/sql/stats_ext.sql      |   3 +
 3 files changed, 301 insertions(+), 6 deletions(-)

diff --git a/src/backend/statistics/mvdistinct.c b/src/backend/statistics/mvdistinct.c
index 7e7a63405c..e2a3acaf78 100644
--- a/src/backend/statistics/mvdistinct.c
+++ b/src/backend/statistics/mvdistinct.c
@@ -27,10 +27,19 @@
 
 #include "catalog/pg_statistic_ext.h"
 #include "catalog/pg_statistic_ext_data.h"
+#include "common/int.h"
+#include "common/jsonapi.h"
+#include "fmgr.h"
 #include "lib/stringinfo.h"
+#include "mb/pg_wchar.h"
+#include "nodes/miscnodes.h"
+#include "nodes/pg_list.h"
 #include "statistics/extended_stats_internal.h"
 #include "statistics/statistics.h"
+#include "utils/builtins.h"
+#include "utils/float.h"
 #include "utils/fmgrprotos.h"
+#include "utils/palloc.h"
 #include "utils/syscache.h"
 #include "utils/typcache.h"
 #include "varatt.h"
@@ -328,21 +337,297 @@ statext_ndistinct_deserialize(bytea *data)
 	return ndistinct;
 }
 
+typedef struct
+{
+	const char *str;
+	bool		found_only_object;
+	List	   *distinct_items;
+	Node	   *escontext;
+
+	MVNDistinctItem *current_item;
+}			ndistinctParseState;
+
+/*
+ * Invoked at the start of each object in the JSON document.
+ * The entire JSON document should be one object with no sub-objects.
+ *
+ * If we're anywhere else in the document, it's an error.
+ */
+static JsonParseErrorType
+ndistinct_object_start(void *state)
+{
+	ndistinctParseState *parse = state;
+
+	if (parse->found_only_object == true)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+				 errdetail("Must begin with \"{\"")));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	parse->found_only_object = true;
+	return JSON_SUCCESS;
+}
+
+/*
+ * ndsitinct input format does not have arrays, so any array elements encountered
+ * are an error.
+ */
+static JsonParseErrorType
+ndistinct_array_start(void *state)
+{
+	ndistinctParseState *parse = state;
+
+	ereturn(parse->escontext, (Datum) 0,
+			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+			 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+			 errdetail("All ndistinct count values are scalar doubles.")));
+	return JSON_SEM_ACTION_FAILED;
+}
+
+static int
+attnum_compare(const void *aptr, const void *bptr)
+{
+	AttrNumber	a = *(const AttrNumber *) aptr;
+	AttrNumber	b = *(const AttrNumber *) bptr;
+
+	return pg_cmp_s16(a,b);
+}
+
+/*
+ * The object keys are themselves comma-separated lists of attnums
+ * with negative attnums representing one of the expressions defined
+ * in the extened statistics object.
+ */
+static JsonParseErrorType
+ndistinct_object_field_start(void *state, char *fname, bool isnull)
+{
+	ndistinctParseState *parse = state;
+	char	   *token;
+	char	   *saveptr;
+	const char *delim = ", ";
+	char	   *scratch;
+	List	   *attnum_list = NIL;
+	int			natts = 0;
+	MVNDistinctItem *item;
+	AttrNumber *attrsort;
+
+	if (isnull || fname == NULL)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+				 errdetail("All ndistinct attnum lists must be a comma separated list of attnums.")));
+
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	scratch = pstrdup(fname);
+
+	token = strtok_r(scratch, delim, &saveptr);
+
+	while (token != NULL)
+	{
+		attnum_list = lappend(attnum_list, (void *) token);
+
+		token = strtok_r(NULL, delim, &saveptr);
+	}
+	natts = attnum_list->length;
+
+	/*
+	 * We need at least 2 attnums for a ndistinct item, anything less is
+	 * malformed.
+	 */
+	if (natts < 2)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+				 errdetail("All ndistinct attnum lists must be a comma separated list of attnums.")));
+
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	item = palloc(sizeof(MVNDistinctItem));
+	item->nattributes = natts;
+	item->attributes = palloc0(natts * sizeof(AttrNumber));
+	attrsort = palloc0(natts * sizeof(AttrNumber));
+
+	for (int i = 0; i < natts; i++)
+	{
+		char	   *s = (char *) attnum_list->elements[i].ptr_value;
+
+		attrsort[i] = pg_strtoint16_safe(s, parse->escontext);
+		item->attributes[i] = attrsort[i];
+
+		if (SOFT_ERROR_OCCURRED(parse->escontext))
+			return JSON_SEM_ACTION_FAILED;
+	}
+
+	list_free(attnum_list);
+	pfree(scratch);
+
+	qsort(attrsort,natts,sizeof(AttrNumber),attnum_compare);
+	for (int i = 1; i < natts; i++)
+		if (attrsort[i] == attrsort[i-1])
+		{
+			ereturn(parse->escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("attnum list duplicate value found: %d", attrsort[i])));
+
+			return JSON_SEM_ACTION_FAILED;
+		}
+
+	pfree(attrsort);
+
+	/* add ndistinct-less MVNDistinctItem to the list */
+	parse->current_item = item;
+	parse->distinct_items = lappend(parse->distinct_items, (void *) item);
+	return JSON_SUCCESS;
+}
+
+/*
+ * ndsitinct input format does not have arrays, so any array elements encountered
+ * are an error.
+ */
+static JsonParseErrorType
+ndistinct_array_element_start(void *state, bool isnull)
+{
+	ndistinctParseState *parse = state;
+
+	ereturn(parse->escontext, (Datum) 0,
+			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+			 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+			 errdetail("Cannot contain array elements.")));
+
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ * Handle scalar events from the ndistinct input parser.
+ *
+ * There is only one case where we will encounter a scalar, and that is the
+ * ndsitinct value for the previous object key.
+ */
+static JsonParseErrorType
+ndistinct_scalar(void *state, char *token, JsonTokenType tokentype)
+{
+	ndistinctParseState *parse = state;
+
+	/* if the entire json is just one scalar, that's wrong */
+	if (parse->found_only_object != true)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+				 errdetail("Must begin with \"{\"")));
+
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	Assert(parse->current_item != NULL);
+
+	parse->current_item->ndistinct = float8in_internal(token, NULL, "double",
+													   token, parse->escontext);
+
+	if (SOFT_ERROR_OCCURRED(parse->escontext))
+		return JSON_SEM_ACTION_FAILED;
+
+	/* mark us done with this item */
+	parse->current_item = NULL;
+	return JSON_SUCCESS;
+}
+
 /*
  * pg_ndistinct_in
  *		input routine for type pg_ndistinct
  *
- * pg_ndistinct is real enough to be a table column, but it has no
- * operations of its own, and disallows input (just like pg_node_tree).
+ * example input: {"6, -1": 14, "6, -2": 9143, "-1, -2": 13454, "6, -1, -2": 14549}
+ *
+ * This import format is clearly a specific subset of JSON, therefore it makes
+ * sense to leverage those parsing utilities, and further validate it from there.
  */
 Datum
 pg_ndistinct_in(PG_FUNCTION_ARGS)
 {
-	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot accept a value of type %s", "pg_ndistinct")));
+	char	   *str = PG_GETARG_CSTRING(0);
 
-	PG_RETURN_VOID();			/* keep compiler quiet */
+	ndistinctParseState parse_state;
+	JsonParseErrorType result;
+	JsonLexContext *lex;
+	JsonSemAction sem_action;
+
+	/* initialize semantic state */
+	parse_state.str = str;
+	parse_state.found_only_object = false;
+	parse_state.distinct_items = NIL;
+	parse_state.escontext = fcinfo->context;
+	parse_state.current_item = NULL;
+
+	/* set callbacks */
+	sem_action.semstate = (void *) &parse_state;
+	sem_action.object_start = ndistinct_object_start;
+	sem_action.object_end = NULL;
+	sem_action.array_start = ndistinct_array_start;
+	sem_action.array_end = NULL;
+	sem_action.object_field_start = ndistinct_object_field_start;
+	sem_action.object_field_end = NULL;
+	sem_action.array_element_start = ndistinct_array_element_start;
+	sem_action.array_element_end = NULL;
+	sem_action.scalar = ndistinct_scalar;
+
+	lex = makeJsonLexContextCstringLen(NULL, str, strlen(str),
+									   PG_UTF8, true);
+	result = pg_parse_json(lex, &sem_action);
+	freeJsonLexContext(lex);
+	if (result == JSON_SUCCESS)
+	{
+		MVNDistinct *ndistinct;
+		int			nitems = parse_state.distinct_items->length;
+		bytea	   *bytes;
+
+		ndistinct = palloc(offsetof(MVNDistinct, items) +
+						   nitems * sizeof(MVNDistinctItem));
+
+		ndistinct->magic = STATS_NDISTINCT_MAGIC;
+		ndistinct->type = STATS_NDISTINCT_TYPE_BASIC;
+		ndistinct->nitems = nitems;
+
+		for (int i = 0; i < nitems; i++)
+		{
+			MVNDistinctItem *item = parse_state.distinct_items->elements[i].ptr_value;
+
+			ndistinct->items[i].ndistinct = item->ndistinct;
+			ndistinct->items[i].nattributes = item->nattributes;
+			ndistinct->items[i].attributes = item->attributes;
+
+			/*
+			 * free the MVNDistinctItem, but not the attributes we're still
+			 * using
+			 */
+			pfree(item);
+		}
+		bytes = statext_ndistinct_serialize(ndistinct);
+
+		list_free(parse_state.distinct_items);
+		for (int i = 0; i < nitems; i++)
+			pfree(ndistinct->items[i].attributes);
+		pfree(ndistinct);
+
+		PG_RETURN_BYTEA_P(bytes);
+	}
+	else if (result == JSON_SEM_ACTION_FAILED)
+		PG_RETURN_NULL();		/* escontext already set */
+
+	/* Anything else is a generic JSON parse error */
+	ereturn(parse_state.escontext, (Datum) 0,
+			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+			 errmsg("malformed pg_ndistinct: \"%s\"", str),
+			 errdetail("Must be valid JSON.")));
+	PG_RETURN_NULL();
 }
 
 /*
diff --git a/src/test/regress/expected/stats_ext.out b/src/test/regress/expected/stats_ext.out
index a4c7be487e..6f3da85101 100644
--- a/src/test/regress/expected/stats_ext.out
+++ b/src/test/regress/expected/stats_ext.out
@@ -3335,6 +3335,13 @@ SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x
  s_expr          | {1}
 (2 rows)
 
+-- new input functions
+SELECT '{"6, -1": 14, "6, -2": 9143, "-1, -2": 13454, "6, -1, -2": 14549}'::pg_ndistinct;
+                           pg_ndistinct                            
+-------------------------------------------------------------------
+ {"6, -1": 14, "6, -2": 9143, "-1, -2": 13454, "6, -1, -2": 14549}
+(1 row)
+
 -- Tidy up
 DROP OPERATOR <<< (int, int);
 DROP FUNCTION op_leak(int, int);
diff --git a/src/test/regress/sql/stats_ext.sql b/src/test/regress/sql/stats_ext.sql
index 5c786b16c6..a53564bed5 100644
--- a/src/test/regress/sql/stats_ext.sql
+++ b/src/test/regress/sql/stats_ext.sql
@@ -1686,6 +1686,9 @@ SELECT statistics_name, most_common_vals FROM pg_stats_ext x
 SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x
     WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*);
 
+-- new input functions
+SELECT '{"6, -1": 14, "6, -2": 9143, "-1, -2": 13454, "6, -1, -2": 14549}'::pg_ndistinct;
+
 -- Tidy up
 DROP OPERATOR <<< (int, int);
 DROP FUNCTION op_leak(int, int);
-- 
2.48.1

From 8f7075e8b65da545f20e354a68a8e1347d1eafc2 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Fri, 3 Jan 2025 13:43:29 -0500
Subject: [PATCH v2 4/6] Add extended statistics support functions.

Add pg_set_extended_stats(), pg_clear_extended_stats(), and
pg_restore_extended_stats(). These function closely mirror their
relation and attribute counterparts, but for extended statistics (i.e.
CREATE STATISTICS) objects.
---
 src/include/catalog/pg_proc.dat               |   23 +
 .../statistics/extended_stats_internal.h      |    4 +
 src/backend/catalog/system_functions.sql      |   17 +
 src/backend/statistics/extended_stats.c       | 1085 ++++++++++++++++-
 src/backend/statistics/mcv.c                  |  144 +++
 src/test/regress/expected/stats_import.out    |  330 +++++
 src/test/regress/sql/stats_import.sql         |  244 ++++
 doc/src/sgml/func.sgml                        |  124 ++
 8 files changed, 1969 insertions(+), 2 deletions(-)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 18560755d2..6803ecce44 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12434,5 +12434,28 @@
   proname => 'gist_stratnum_common', prorettype => 'int2',
   proargtypes => 'int4',
   prosrc => 'gist_stratnum_common' },
+{ oid => '9947',
+  descr => 'restore statistics on extended statistics object',
+  proname => 'pg_restore_extended_stats', provolatile => 'v', proisstrict => 'f',
+  provariadic => 'any',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'any',
+  proargnames => '{kwargs}',
+  proargmodes => '{v}',
+  prosrc => 'pg_restore_extended_stats' },
+{ oid => '9948',
+  descr => 'set statistics on extended statistics object',
+  proname => 'pg_set_extended_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'void',
+  proargtypes => 'regnamespace name bool pg_ndistinct pg_dependencies _text _bool _float8 _float8 _text',
+  proargnames => '{statistics_schemaname,statistics_name,inherited,n_distinct,dependencies,most_common_vals,most_common_val_nulls,most_common_freqs,most_common_base_freqs,exprs}',
+  prosrc => 'pg_set_extended_stats' },
+{ oid => '9949',
+  descr => 'clear statistics on extended statistics object',
+  proname => 'pg_clear_extended_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'void',
+  proargtypes => 'regnamespace name bool',
+  proargnames => '{statistics_schemaname,statistics_name,inherited}',
+  prosrc => 'pg_clear_extended_stats' },
 
 ]
diff --git a/src/include/statistics/extended_stats_internal.h b/src/include/statistics/extended_stats_internal.h
index efcb7dc354..df1a5308f4 100644
--- a/src/include/statistics/extended_stats_internal.h
+++ b/src/include/statistics/extended_stats_internal.h
@@ -126,5 +126,9 @@ extern Selectivity mcv_clause_selectivity_or(PlannerInfo *root,
 											 Selectivity *overlap_mcvsel,
 											 Selectivity *overlap_basesel,
 											 Selectivity *totalsel);
+extern Datum import_mcvlist(HeapTuple tup, int elevel, int numattrs,
+							Oid *atttypids, int32 *atttypmods, Oid *atttypcolls,
+							int nitems, Datum *mcv_elems, bool *mcv_nulls,
+							bool *mcv_elem_nulls, float8 *freqs, float8 *base_freqs);
 
 #endif							/* EXTENDED_STATS_INTERNAL_H */
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 591157b1d1..82901bd8e1 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -668,6 +668,23 @@ LANGUAGE INTERNAL
 CALLED ON NULL INPUT VOLATILE
 AS 'pg_set_attribute_stats';
 
+CREATE OR REPLACE FUNCTION
+  pg_set_extended_stats(statistics_schemaname regnamespace,
+                        statistics_name name,
+                        inherited bool,
+                        n_distinct pg_ndistinct DEFAULT NULL,
+                        dependencies pg_dependencies DEFAULT NULL,
+                        most_common_vals text[] DEFAULT NULL,
+                        most_common_val_nulls boolean[] DEFAULT NULL,
+                        most_common_freqs double precision[] DEFAULT NULL,
+                        most_common_base_freqs double precision[] DEFAULT NULL,
+                        exprs text[] DEFAULT NULL)
+RETURNS void
+LANGUAGE INTERNAL
+CALLED ON NULL INPUT VOLATILE
+AS 'pg_set_extended_stats';
+
+
 --
 -- The default permissions for functions mean that anyone can execute them.
 -- A number of functions shouldn't be executable by just anyone, but rather
diff --git a/src/backend/statistics/extended_stats.c b/src/backend/statistics/extended_stats.c
index a8b63ec088..b54b3aa533 100644
--- a/src/backend/statistics/extended_stats.c
+++ b/src/backend/statistics/extended_stats.c
@@ -18,22 +18,35 @@
 
 #include "access/detoast.h"
 #include "access/genam.h"
+#include "access/heapam.h"
+#include "access/htup.h"
 #include "access/htup_details.h"
 #include "access/table.h"
+#include "c.h"
 #include "catalog/indexing.h"
+#include "catalog/pg_collation.h"
+#include "catalog/pg_statistic_d.h"
 #include "catalog/pg_statistic_ext.h"
+#include "catalog/pg_statistic_ext_d.h"
 #include "catalog/pg_statistic_ext_data.h"
+#include "catalog/pg_statistic_ext_data_d.h"
+#include "catalog/pg_type_d.h"
 #include "commands/defrem.h"
 #include "commands/progress.h"
+#include "commands/vacuum.h"
 #include "executor/executor.h"
+#include "fmgr.h"
 #include "miscadmin.h"
+#include "nodes/miscnodes.h"
 #include "nodes/nodeFuncs.h"
 #include "optimizer/optimizer.h"
 #include "parser/parsetree.h"
 #include "pgstat.h"
 #include "postmaster/autovacuum.h"
 #include "statistics/extended_stats_internal.h"
+#include "statistics/stat_utils.h"
 #include "statistics/statistics.h"
+#include "storage/lockdefs.h"
 #include "utils/acl.h"
 #include "utils/array.h"
 #include "utils/attoptcache.h"
@@ -42,9 +55,11 @@
 #include "utils/fmgroids.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
+#include "utils/palloc.h"
 #include "utils/rel.h"
 #include "utils/selfuncs.h"
 #include "utils/syscache.h"
+#include "utils/typcache.h"
 
 /*
  * To avoid consuming too much memory during analysis and/or too much space
@@ -72,6 +87,71 @@ typedef struct StatExtEntry
 	List	   *exprs;			/* expressions */
 } StatExtEntry;
 
+enum extended_stats_argnum
+{
+	STATSCHEMA_ARG = 0,
+	STATNAME_ARG,
+	INHERITED_ARG,
+	NDISTINCT_ARG,
+	DEPENDENCIES_ARG,
+	MOST_COMMON_VALS_ARG,
+	MOST_COMMON_VAL_NULLS_ARG,
+	MOST_COMMON_FREQS_ARG,
+	MOST_COMMON_BASE_FREQS_ARG,
+	EXPRESSIONS_ARG,
+	NUM_EXTENDED_STATS_ARGS
+};
+
+static struct StatsArgInfo extarginfo[] =
+{
+	[STATSCHEMA_ARG] = {"statistics_schemaname", REGNAMESPACEOID},
+	[STATNAME_ARG] = {"statistics_name", NAMEOID},
+	[INHERITED_ARG] = {"inherited", BOOLOID},
+	[NDISTINCT_ARG] = {"n_distinct", PG_NDISTINCTOID},
+	[DEPENDENCIES_ARG] = {"dependencies", PG_DEPENDENCIESOID},
+	[MOST_COMMON_VALS_ARG] = {"most_common_vals", TEXTARRAYOID},
+	[MOST_COMMON_VAL_NULLS_ARG] = {"most_common_val_nulls", BOOLARRAYOID},
+	[MOST_COMMON_FREQS_ARG] = {"most_common_freqs", FLOAT8ARRAYOID},
+	[MOST_COMMON_BASE_FREQS_ARG] = {"most_common_base_freqs", FLOAT8ARRAYOID},
+	[EXPRESSIONS_ARG] = {"exprs", TEXTARRAYOID},
+	[NUM_EXTENDED_STATS_ARGS] = {0}
+};
+
+/*
+ * NOTE: the RANGE_LENGTH & RANGE_BOUNDS stats are not yet reflected in any
+ * version of pg_stat_ext_exprs.
+ */
+enum extended_stats_exprs_element
+{
+	NULL_FRAC_ELEM = 0,
+	AVG_WIDTH_ELEM,
+	N_DISTINCT_ELEM,
+	MOST_COMMON_VALS_ELEM,
+	MOST_COMMON_FREQS_ELEM,
+	HISTOGRAM_BOUNDS_ELEM,
+	CORRELATION_ELEM,
+	MOST_COMMON_ELEMS_ELEM,
+	MOST_COMMON_ELEM_FREQS_ELEM,
+	ELEM_COUNT_HISTOGRAM_ELEM,
+	NUM_ATTRIBUTE_STATS_ELEMS
+};
+
+static struct StatsArgInfo extexprarginfo[] =
+{
+	[NULL_FRAC_ELEM] = {"null_frac", FLOAT4OID},
+	[AVG_WIDTH_ELEM] = {"avg_width", INT4OID},
+	[N_DISTINCT_ELEM] = {"n_distinct", FLOAT4OID},
+	[MOST_COMMON_VALS_ELEM] = {"most_common_vals", TEXTOID},
+	[MOST_COMMON_FREQS_ELEM] = {"most_common_freqs", FLOAT4ARRAYOID},
+	[HISTOGRAM_BOUNDS_ELEM] = {"histogram_bounds", TEXTOID},
+	[CORRELATION_ELEM] = {"correlation", FLOAT4OID},
+	[MOST_COMMON_ELEMS_ELEM] = {"most_common_elems", TEXTOID},
+	[MOST_COMMON_ELEM_FREQS_ELEM] = {"most_common_elem_freqs", FLOAT4ARRAYOID},
+	[ELEM_COUNT_HISTOGRAM_ELEM] = {"elem_count_histogram", FLOAT4ARRAYOID},
+	[NUM_ATTRIBUTE_STATS_ELEMS] = {0}
+};
+
+static bool extended_statistics_update(FunctionCallInfo fcinfo, int elevel);
 
 static List *fetch_statentries_for_relation(Relation pg_statext, Oid relid);
 static VacAttrStats **lookup_var_attr_stats(Bitmapset *attrs, List *exprs,
@@ -99,6 +179,28 @@ static StatsBuildData *make_build_data(Relation rel, StatExtEntry *stat,
 									   int numrows, HeapTuple *rows,
 									   VacAttrStats **stats, int stattarget);
 
+static HeapTuple get_pg_statistic_ext(Relation pg_stext, Oid nspoid, Name stxname);
+static bool delete_pg_statistic_ext_data(Oid stxoid, bool inherited);
+
+typedef struct
+{
+	bool		ndistinct;
+	bool		dependencies;
+	bool		mcv;
+	bool		expressions;
+}			stakindFlags;
+
+static void expand_stxkind(HeapTuple tup, stakindFlags * enabled);
+static void upsert_pg_statistic_ext_data(Datum *values, bool *nulls, bool *replaces);
+static bool check_mcvlist_array(ArrayType *arr, int argindex,
+								int required_ndimss, int mcv_length,
+								int elevel);
+static Datum import_expressions(Relation pgsd, int elevel, int numexprs,
+								Oid *atttypids, int32 *atttypmods,
+								Oid *atttypcolls, ArrayType *exprs_arr);
+static bool text_to_float4(Datum input, Datum *output);
+static bool text_to_int4(Datum input, Datum *output);
+
 
 /*
  * Compute requested extended stats, using the rows sampled for the plain
@@ -2099,7 +2201,6 @@ examine_opclause_args(List *args, Node **exprp, Const **cstp,
 	return true;
 }
 
-
 /*
  * Compute statistics about expressions of a relation.
  */
@@ -2239,7 +2340,6 @@ compute_expr_stats(Relation onerel, AnlExprData *exprdata, int nexprs,
 	MemoryContextDelete(expr_context);
 }
 
-
 /*
  * Fetch function for analyzing statistics object expressions.
  *
@@ -2631,3 +2731,984 @@ make_build_data(Relation rel, StatExtEntry *stat, int numrows, HeapTuple *rows,
 
 	return result;
 }
+
+static HeapTuple
+get_pg_statistic_ext(Relation pg_stext, Oid nspoid, Name stxname)
+{
+	ScanKeyData key[2];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	Oid			stxoid = InvalidOid;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_statistic_ext_stxname,
+				BTEqualStrategyNumber,
+				F_NAMEEQ,
+				NameGetDatum(stxname));
+	ScanKeyInit(&key[1],
+				Anum_pg_statistic_ext_stxnamespace,
+				BTEqualStrategyNumber,
+				F_OIDEQ,
+				ObjectIdGetDatum(nspoid));
+
+	/*
+	 * Try to find matching pg_statistic_ext row.
+	 */
+	scan = systable_beginscan(pg_stext,
+							  StatisticExtNameIndexId,
+							  true,
+							  NULL,
+							  2,
+							  key);
+
+	/* Unique index, either we get a tuple or we don't. */
+	tup = systable_getnext(scan);
+
+	if (HeapTupleIsValid(tup))
+		stxoid = ((Form_pg_statistic_ext) GETSTRUCT(tup))->oid;
+
+	systable_endscan(scan);
+
+	if (!OidIsValid(stxoid))
+		return NULL;
+
+	return SearchSysCacheCopy1(STATEXTOID, ObjectIdGetDatum(stxoid));
+}
+
+/*
+ * Decode the stxkind column so that we know which stats types to expect.
+ */
+static void
+expand_stxkind(HeapTuple tup, stakindFlags * enabled)
+{
+	Datum		datum;
+	ArrayType  *arr;
+	char	   *kinds;
+
+	datum = SysCacheGetAttrNotNull(STATEXTOID,
+								   tup,
+								   Anum_pg_statistic_ext_stxkind);
+	arr = DatumGetArrayTypeP(datum);
+	if (ARR_NDIM(arr) != 1 || ARR_HASNULL(arr) || ARR_ELEMTYPE(arr) != CHAROID)
+		elog(ERROR, "stxkind is not a 1-D char array");
+
+	kinds = (char *) ARR_DATA_PTR(arr);
+
+	for (int i = 0; i < ARR_DIMS(arr)[0]; i++)
+		if (kinds[i] == STATS_EXT_NDISTINCT)
+			enabled->ndistinct = true;
+		else if (kinds[i] == STATS_EXT_DEPENDENCIES)
+			enabled->dependencies = true;
+		else if (kinds[i] == STATS_EXT_MCV)
+			enabled->mcv = true;
+		else if (kinds[i] == STATS_EXT_EXPRESSIONS)
+			enabled->expressions = true;
+}
+
+static void
+upsert_pg_statistic_ext_data(Datum *values, bool *nulls, bool *replaces)
+{
+	Relation	pg_stextdata;
+	HeapTuple	stxdtup;
+	HeapTuple	newtup;
+
+	pg_stextdata = table_open(StatisticExtDataRelationId, RowExclusiveLock);
+
+	stxdtup = SearchSysCache2(STATEXTDATASTXOID,
+							  values[Anum_pg_statistic_ext_data_stxoid - 1],
+							  values[Anum_pg_statistic_ext_data_stxdinherit - 1]);
+
+	if (HeapTupleIsValid(stxdtup))
+	{
+		newtup = heap_modify_tuple(stxdtup,
+								   RelationGetDescr(pg_stextdata),
+								   values,
+								   nulls,
+								   replaces);
+		CatalogTupleUpdate(pg_stextdata, &newtup->t_self, newtup);
+		ReleaseSysCache(stxdtup);
+	}
+	else
+	{
+		newtup = heap_form_tuple(RelationGetDescr(pg_stextdata), values, nulls);
+		CatalogTupleInsert(pg_stextdata, newtup);
+	}
+
+	heap_freetuple(newtup);
+
+	CommandCounterIncrement();
+
+	table_close(pg_stextdata, RowExclusiveLock);
+}
+
+/*
+ * Insert or Update Extended Statistics
+ *
+ * Major errors, such as the table not existing, the statistics object not
+ * existing, or a permissions failure are always reported at ERROR. Other
+ * errors, such as a conversion failure on one statistic kind, are reported
+ * at 'elevel', and other statistic kinds may still be updated.
+ */
+static bool
+extended_statistics_update(FunctionCallInfo fcinfo, int elevel)
+{
+	Oid			nspoid;
+	Name		stxname;
+	bool		inherited;
+	Relation	pg_stext;
+	HeapTuple	tup = NULL;
+
+	stakindFlags enabled;
+	stakindFlags has;
+
+	Form_pg_statistic_ext stxform;
+
+	Datum		values[Natts_pg_statistic_ext_data];
+	bool		nulls[Natts_pg_statistic_ext_data];
+	bool		replaces[Natts_pg_statistic_ext_data];
+
+	bool		success = true;
+
+	int			numattnums = 0;
+	int			numexprs = 0;
+	int			numattrs = 0;
+
+	/* arrays of type info, if we need them */
+	Oid		   *atttypids = NULL;
+	int32	   *atttypmods = NULL;
+	Oid		   *atttypcolls = NULL;
+
+	memset(nulls, false, sizeof(nulls));
+	memset(values, 0, sizeof(values));
+	memset(replaces, 0, sizeof(replaces));
+	memset(&enabled, 0, sizeof(enabled));
+
+	has.mcv = (!PG_ARGISNULL(MOST_COMMON_VALS_ARG) &&
+			   !PG_ARGISNULL(MOST_COMMON_VAL_NULLS_ARG) &&
+			   !PG_ARGISNULL(MOST_COMMON_FREQS_ARG) &&
+			   !PG_ARGISNULL(MOST_COMMON_BASE_FREQS_ARG));
+	has.ndistinct = !PG_ARGISNULL(NDISTINCT_ARG);
+	has.dependencies = !PG_ARGISNULL(DEPENDENCIES_ARG);
+	has.expressions = !PG_ARGISNULL(EXPRESSIONS_ARG);
+
+	if (RecoveryInProgress())
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("recovery is in progress"),
+				 errhint("Statistics cannot be modified during recovery.")));
+
+	stats_check_required_arg(fcinfo, extarginfo, STATSCHEMA_ARG);
+	nspoid = PG_GETARG_OID(STATSCHEMA_ARG);
+	stats_check_required_arg(fcinfo, extarginfo, STATNAME_ARG);
+	stxname = PG_GETARG_NAME(STATNAME_ARG);
+	stats_check_required_arg(fcinfo, extarginfo, INHERITED_ARG);
+	inherited = PG_GETARG_NAME(INHERITED_ARG);
+
+	pg_stext = table_open(StatisticExtRelationId, RowExclusiveLock);
+	tup = get_pg_statistic_ext(pg_stext, nspoid, stxname);
+
+	if (!HeapTupleIsValid(tup))
+	{
+		table_close(pg_stext, RowExclusiveLock);
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("Extended Statistics Object \"%s\".\"%s\" not found.",
+						get_namespace_name(nspoid),
+						NameStr(*stxname))));
+		return false;
+	}
+
+	stxform = (Form_pg_statistic_ext) GETSTRUCT(tup);
+	expand_stxkind(tup, &enabled);
+
+	/* lock table */
+	stats_lock_check_privileges(stxform->stxrelid);
+
+	if (has.mcv)
+	{
+		if (!enabled.mcv)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("MCV parameters \"%s\", \"%s\", \"%s\", and \"%s\" were all "
+							"specified for extended statistics object that does not expect MCV ",
+							extarginfo[MOST_COMMON_VALS_ARG].argname,
+							extarginfo[MOST_COMMON_VAL_NULLS_ARG].argname,
+							extarginfo[MOST_COMMON_FREQS_ARG].argname,
+							extarginfo[MOST_COMMON_BASE_FREQS_ARG].argname)));
+			has.mcv = false;
+			success = false;
+		}
+	}
+	else
+	{
+		/* The MCV args must all be NULL */
+		if (!PG_ARGISNULL(MOST_COMMON_VALS_ARG) ||
+			!PG_ARGISNULL(MOST_COMMON_VAL_NULLS_ARG) ||
+			!PG_ARGISNULL(MOST_COMMON_FREQS_ARG) ||
+			!PG_ARGISNULL(MOST_COMMON_BASE_FREQS_ARG))
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("MCV parameters \"%s\", \"%s\", \"%s\", and \"%s\" must be all specified if any are specified",
+							extarginfo[MOST_COMMON_VALS_ARG].argname,
+							extarginfo[MOST_COMMON_VAL_NULLS_ARG].argname,
+							extarginfo[MOST_COMMON_FREQS_ARG].argname,
+							extarginfo[MOST_COMMON_BASE_FREQS_ARG].argname)));
+	}
+
+	if (has.ndistinct && !enabled.ndistinct)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Parameters \"%s\" was specified for extended statistics object "
+						"that does not expect \"%s\"",
+						extarginfo[NDISTINCT_ARG].argname,
+						extarginfo[NDISTINCT_ARG].argname)));
+		has.ndistinct = false;
+		success = false;
+	}
+
+	if (has.dependencies && !enabled.dependencies)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Parameters \"%s\" was specified for extended statistics object "
+						"that does not expect \"%s\"",
+						extarginfo[DEPENDENCIES_ARG].argname,
+						extarginfo[DEPENDENCIES_ARG].argname)));
+		has.dependencies = false;
+		success = false;
+	}
+
+	if (has.expressions && !enabled.expressions)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Parameters \"%s\" was specified for extended statistics object "
+						"that does not expect \"%s\"",
+						extarginfo[DEPENDENCIES_ARG].argname,
+						extarginfo[DEPENDENCIES_ARG].argname)));
+		has.expressions = false;
+		success = false;
+	}
+
+	/*
+	 * Either of these statsistic types requires that we supply
+	 * semi-filled-out VacAttrStatP array.
+	 *
+	 *
+	 * It is not possible to use the existing lookup_var_attr_stats() and
+	 * examine_attribute() because these functions will skip attributes for
+	 * which attstattarget is 0, and we may have stats to import for those
+	 * attributes.
+	 */
+	if (has.mcv || has.expressions)
+	{
+		Datum		exprdatum;
+		bool		isnull;
+		List	   *exprs = NIL;
+
+		/* decode expression (if any) */
+		exprdatum = SysCacheGetAttr(STATEXTOID,
+									tup,
+									Anum_pg_statistic_ext_stxexprs,
+									&isnull);
+
+		if (!isnull)
+		{
+			char	   *s;
+
+			s = TextDatumGetCString(exprdatum);
+			exprs = (List *) stringToNode(s);
+			pfree(s);
+
+			/*
+			 * Run the expressions through eval_const_expressions. This is not
+			 * just an optimization, but is necessary, because the planner
+			 * will be comparing them to similarly-processed qual clauses, and
+			 * may fail to detect valid matches without this.  We must not use
+			 * canonicalize_qual, however, since these aren't qual
+			 * expressions.
+			 */
+			exprs = (List *) eval_const_expressions(NULL, (Node *) exprs);
+
+			/* May as well fix opfuncids too */
+			fix_opfuncids((Node *) exprs);
+		}
+
+		numattnums = stxform->stxkeys.dim1;
+		numexprs = list_length(exprs);
+		numattrs = numattnums + numexprs;
+
+		atttypids = palloc0(numattrs * sizeof(Oid));
+		atttypmods = palloc0(numattrs * sizeof(int32));
+		atttypcolls = palloc0(numattrs * sizeof(Oid));
+
+		for (int i = 0; i < numattnums; i++)
+		{
+			AttrNumber	attnum = stxform->stxkeys.values[i];
+
+			Oid			lt_opr;
+			Oid			eq_opr;
+			char		typetype;
+
+			/*
+			 * fetch attribute entries the same as are done for attribute
+			 * stats
+			 */
+			get_attr_stat_type(stxform->stxrelid,
+							   attnum,
+							   elevel,
+							   &atttypids[i],
+							   &atttypmods[i],
+							   &typetype,
+							   &atttypcolls[i],
+							   &lt_opr,
+							   &eq_opr);
+		}
+
+		for (int i = numattnums; i < numattrs; i++)
+		{
+			Node	   *expr = list_nth(exprs, i - numattnums);
+
+			atttypids[i] = exprType(expr);
+			atttypmods[i] = exprTypmod(expr);
+			atttypcolls[i] = exprCollation(expr);
+
+			/*
+			 * Duplicate logic from get_attr_stat_type
+			 */
+
+			/*
+			 * If it's a multirange, step down to the range type, as is done
+			 * by multirange_typanalyze().
+			 */
+			if (type_is_multirange(atttypids[i]))
+				atttypids[i] = get_multirange_range(atttypids[i]);
+
+			/*
+			 * Special case: collation for tsvector is DEFAULT_COLLATION_OID.
+			 * See compute_tsvector_stats().
+			 */
+			if (atttypids[i] == TSVECTOROID)
+				atttypcolls[i] = DEFAULT_COLLATION_OID;
+
+		}
+	}
+
+	/* Primary Key: cannot be NULL or replaced. */
+	values[Anum_pg_statistic_ext_data_stxoid - 1] = ObjectIdGetDatum(stxform->oid);
+	values[Anum_pg_statistic_ext_data_stxdinherit - 1] = BoolGetDatum(inherited);
+
+	if (has.ndistinct)
+	{
+		values[Anum_pg_statistic_ext_data_stxdndistinct - 1] = PG_GETARG_DATUM(NDISTINCT_ARG);
+		replaces[Anum_pg_statistic_ext_data_stxdndistinct - 1] = true;
+	}
+	else
+		nulls[Anum_pg_statistic_ext_data_stxdndistinct - 1] = true;
+
+	if (has.dependencies)
+	{
+		values[Anum_pg_statistic_ext_data_stxddependencies - 1] = PG_GETARG_DATUM(DEPENDENCIES_ARG);
+		replaces[Anum_pg_statistic_ext_data_stxddependencies - 1] = true;
+	}
+	else
+		nulls[Anum_pg_statistic_ext_data_stxddependencies - 1] = true;
+
+	if (has.mcv)
+	{
+		Datum		datum;
+		ArrayType  *mcv_arr = PG_GETARG_ARRAYTYPE_P(MOST_COMMON_VALS_ARG);
+		ArrayType  *nulls_arr = PG_GETARG_ARRAYTYPE_P(MOST_COMMON_VAL_NULLS_ARG);
+		ArrayType  *freqs_arr = PG_GETARG_ARRAYTYPE_P(MOST_COMMON_FREQS_ARG);
+		ArrayType  *base_freqs_arr = PG_GETARG_ARRAYTYPE_P(MOST_COMMON_BASE_FREQS_ARG);
+		int			nitems;
+		Datum	   *mcv_elems;
+		bool	   *mcv_nulls;
+		int			check_nummcv;
+
+		/*
+		 * The mcv_arr is an array of arrays of text, and we use it as the
+		 * reference array for checking the lengths of the other 3 arrays.
+		 */
+		if (ARR_NDIM(mcv_arr) != 2)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Parameters \"%s\" must be a text array of 2 dimensions.",
+							extarginfo[MOST_COMMON_VALS_ARG].argname)));
+			return (Datum) 0;
+		}
+
+		nitems = ARR_DIMS(mcv_arr)[0];
+
+		/* fixed length arrays that cannot contain NULLs */
+		if (!check_mcvlist_array(nulls_arr, MOST_COMMON_VAL_NULLS_ARG,
+								 2, nitems, elevel) ||
+			!check_mcvlist_array(freqs_arr, MOST_COMMON_FREQS_ARG,
+								 1, nitems, elevel) ||
+			!check_mcvlist_array(base_freqs_arr, MOST_COMMON_BASE_FREQS_ARG,
+								 1, nitems, elevel))
+			return (Datum) 0;
+
+
+		deconstruct_array_builtin(mcv_arr, TEXTOID, &mcv_elems,
+								  &mcv_nulls, &check_nummcv);
+
+		Assert(check_nummcv == (nitems * numattrs));
+
+		datum = import_mcvlist(tup, elevel, numattrs,
+							   atttypids, atttypmods, atttypcolls,
+							   nitems, mcv_elems, mcv_nulls,
+							   (bool *) ARR_DATA_PTR(nulls_arr),
+							   (float8 *) ARR_DATA_PTR(freqs_arr),
+							   (float8 *) ARR_DATA_PTR(base_freqs_arr));
+
+		values[Anum_pg_statistic_ext_data_stxdmcv - 1] = datum;
+		replaces[Anum_pg_statistic_ext_data_stxdmcv - 1] = true;
+	}
+	else
+		nulls[Anum_pg_statistic_ext_data_stxdmcv - 1] = true;
+
+	if (has.expressions)
+	{
+		Datum		datum;
+		Relation	pgsd;
+
+		pgsd = table_open(StatisticRelationId, RowExclusiveLock);
+
+		datum = import_expressions(pgsd, elevel, numexprs,
+								   &atttypids[numattnums], &atttypmods[numattnums],
+								   &atttypcolls[numattnums],
+								   PG_GETARG_ARRAYTYPE_P(EXPRESSIONS_ARG));
+
+		table_close(pgsd, RowExclusiveLock);
+
+		values[Anum_pg_statistic_ext_data_stxdexpr - 1] = datum;
+		replaces[Anum_pg_statistic_ext_data_stxdexpr - 1] = true;
+	}
+	else
+		nulls[Anum_pg_statistic_ext_data_stxdexpr - 1] = true;
+
+	upsert_pg_statistic_ext_data(values, nulls, replaces);
+
+	heap_freetuple(tup);
+	table_close(pg_stext, RowExclusiveLock);
+
+	if (atttypids != NULL)
+		pfree(atttypids);
+	if (atttypmods != NULL)
+		pfree(atttypmods);
+	if (atttypcolls != NULL)
+		pfree(atttypcolls);
+	return success;
+}
+
+/*
+ * Consistency checks to ensure that other mcvlist arrays are in alignment
+ * with the mcv array.
+ */
+static bool
+check_mcvlist_array(ArrayType *arr, int argindex, int required_ndims,
+					int mcv_length, int elevel)
+{
+	if (ARR_NDIM(arr) != required_ndims)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Parameter \"%s\" must be an array of %d dimensions.",
+						extarginfo[argindex].argname, required_ndims)));
+		return false;
+	}
+
+	if (array_contains_nulls(arr))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Array \"%s\" cannot contain NULLs.",
+						extarginfo[argindex].argname)));
+		return false;
+	}
+
+	if (ARR_DIMS(arr)[0] != mcv_length)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Parameters \"%s\" must have the same number of elements as \"%s\"",
+						extarginfo[argindex].argname,
+						extarginfo[MOST_COMMON_VALS_ARG].argname)));
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Create the stxdexprs datum using the user input in an array of array of
+ * text, referenced against the datatypes for the expressions.
+ */
+static Datum
+import_expressions(Relation pgsd, int elevel, int numexprs,
+				   Oid *atttypids, int32 *atttypmods,
+				   Oid *atttypcolls, ArrayType *exprs_arr)
+{
+	Datum	   *exprs_elems;
+	bool	   *exprs_nulls;
+	int			check_numexprs;
+	int			offset = 0;
+
+	FmgrInfo	array_in_fn;
+
+	Oid			pgstypoid = get_rel_type_id(StatisticRelationId);
+
+	ArrayBuildState *astate = NULL;
+
+
+	if (ARR_NDIM(exprs_arr) != 2)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Parameter \"%s\" must be a text array of 2 dimensions.",
+						extarginfo[EXPRESSIONS_ARG].argname)));
+		return (Datum) 0;
+	}
+
+	if (ARR_DIMS(exprs_arr)[0] != numexprs)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Parameter \"%s\" must have an outer dimension of %d elements.",
+						extarginfo[EXPRESSIONS_ARG].argname, numexprs)));
+		return (Datum) 0;
+	}
+	if (ARR_DIMS(exprs_arr)[1] != NUM_ATTRIBUTE_STATS_ELEMS)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Parameter \"%s\" must have an inner dimension of %d elements.",
+						extarginfo[EXPRESSIONS_ARG].argname,
+						NUM_ATTRIBUTE_STATS_ELEMS)));
+		return (Datum) 0;
+	}
+
+	fmgr_info(F_ARRAY_IN, &array_in_fn);
+
+	deconstruct_array_builtin(exprs_arr, TEXTOID, &exprs_elems,
+							  &exprs_nulls, &check_numexprs);
+
+	for (int i = 0; i < numexprs; i++)
+	{
+		Oid			typid = atttypids[i];
+		int32		typmod = atttypmods[i];
+		Oid			stacoll = atttypcolls[i];
+		TypeCacheEntry *typcache;
+
+		Oid			elemtypid = InvalidOid;
+		Oid			elem_eq_opr = InvalidOid;
+
+		bool		ok;
+
+		Datum		values[Natts_pg_statistic];
+		bool		nulls[Natts_pg_statistic];
+		bool		replaces[Natts_pg_statistic];
+
+		HeapTuple	pgstup;
+		Datum		pgstdat;
+
+		/* finds the right operators even if atttypid is a domain */
+		typcache = lookup_type_cache(typid, TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR);
+
+		init_empty_stats_tuple(InvalidOid, InvalidAttrNumber, false,
+							   values, nulls, replaces);
+
+		if (!exprs_nulls[offset + NULL_FRAC_ELEM])
+		{
+			ok = text_to_float4(exprs_elems[offset + NULL_FRAC_ELEM],
+								&values[Anum_pg_statistic_stanullfrac - 1]);
+
+			if (!ok)
+			{
+				char	   *s = TextDatumGetCString(exprs_elems[offset + NULL_FRAC_ELEM]);
+
+				ereport(elevel,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("Expression %s element \"%s\" does not match expected input type.",
+								extexprarginfo[NULL_FRAC_ELEM].argname, s)));
+				pfree(s);
+				return (Datum) 0;
+			}
+		}
+
+		if (!exprs_nulls[offset + AVG_WIDTH_ELEM])
+		{
+			ok = text_to_int4(exprs_elems[offset + AVG_WIDTH_ELEM],
+							  &values[Anum_pg_statistic_stawidth - 1]);
+
+			if (!ok)
+			{
+				char	   *s = TextDatumGetCString(exprs_elems[offset + NULL_FRAC_ELEM]);
+
+				ereport(elevel,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("Expression %s element \"%s\" does not match expected input type.",
+								extexprarginfo[AVG_WIDTH_ELEM].argname, s)));
+				pfree(s);
+				return (Datum) 0;
+			}
+		}
+
+		if (!exprs_nulls[offset + N_DISTINCT_ELEM])
+		{
+			ok = text_to_float4(exprs_elems[offset + N_DISTINCT_ELEM],
+								&values[Anum_pg_statistic_stadistinct - 1]);
+
+			if (!ok)
+			{
+				char	   *s = TextDatumGetCString(exprs_elems[offset + NULL_FRAC_ELEM]);
+
+				ereport(elevel,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("Expression %s element \"%s\" does not match expected input type.",
+								extexprarginfo[N_DISTINCT_ELEM].argname, s)));
+				pfree(s);
+				return (Datum) 0;
+			}
+		}
+
+		/*
+		 * The STAKIND statistics are the same as the ones found in attribute
+		 * stats. However, these are all derived from text columns, whereas
+		 * the ones derived for attribute stats are a mix of datatypes. This
+		 * limits the opportunities for code sharing between the two.
+		 */
+
+		/* STATISTIC_KIND_MCV */
+		if (exprs_nulls[offset + MOST_COMMON_VALS_ELEM] !=
+			exprs_nulls[offset + MOST_COMMON_FREQS_ELEM])
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Expression %s and %s must both be NOT NULL or both NULL.",
+							extexprarginfo[MOST_COMMON_VALS_ELEM].argname,
+							extexprarginfo[MOST_COMMON_FREQS_ELEM].argname)));
+			return (Datum) 0;
+		}
+
+		if (!exprs_nulls[offset + MOST_COMMON_VALS_ELEM])
+		{
+			Datum		stavalues;
+			Datum		stanumbers;
+
+			stavalues = text_to_stavalues(extexprarginfo[MOST_COMMON_VALS_ELEM].argname,
+										  &array_in_fn, exprs_elems[offset + MOST_COMMON_VALS_ELEM],
+										  typid, typmod, elevel, &ok);
+
+			if (!ok)
+				return (Datum) 0;
+
+			stanumbers = text_to_stavalues(extexprarginfo[MOST_COMMON_VALS_ELEM].argname,
+										   &array_in_fn, exprs_elems[offset + MOST_COMMON_FREQS_ELEM],
+										   FLOAT4OID, -1, elevel, &ok);
+
+			if (!ok)
+				return (Datum) 0;
+
+			set_stats_slot(values, nulls, replaces,
+						   STATISTIC_KIND_MCV,
+						   typcache->eq_opr, stacoll,
+						   stanumbers, false, stavalues, false);
+		}
+
+		/* STATISTIC_KIND_HISTOGRAM */
+		if (!exprs_nulls[offset + HISTOGRAM_BOUNDS_ELEM])
+		{
+			Datum		stavalues;
+
+			stavalues = text_to_stavalues(extexprarginfo[HISTOGRAM_BOUNDS_ELEM].argname,
+										  &array_in_fn, exprs_elems[offset + HISTOGRAM_BOUNDS_ELEM],
+										  typid, typmod, elevel, &ok);
+
+			if (!ok)
+				return (Datum) 0;
+
+			set_stats_slot(values, nulls, replaces,
+						   STATISTIC_KIND_HISTOGRAM,
+						   typcache->lt_opr, stacoll,
+						   0, true, stavalues, false);
+		}
+
+		/* STATISTIC_KIND_CORRELATION */
+		if (!exprs_nulls[offset + CORRELATION_ELEM])
+		{
+			Datum		corr[] = {(Datum) 0};
+			ArrayType  *arry;
+			Datum		stanumbers;
+
+			ok = text_to_float4(exprs_elems[offset + CORRELATION_ELEM], &corr[0]);
+
+			if (!ok)
+			{
+				char	   *s = TextDatumGetCString(exprs_elems[offset + CORRELATION_ELEM]);
+
+				ereport(elevel,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("Expression %s element \"%s\" does not match expected input type.",
+								extexprarginfo[CORRELATION_ELEM].argname, s)));
+				return (Datum) 0;
+			}
+
+			arry = construct_array_builtin(corr, 1, FLOAT4OID);
+
+			stanumbers = PointerGetDatum(arry);
+
+			set_stats_slot(values, nulls, replaces,
+						   STATISTIC_KIND_CORRELATION,
+						   typcache->lt_opr, stacoll,
+						   stanumbers, false, 0, true);
+		}
+
+		/* STATISTIC_KIND_MCELEM */
+		if (exprs_nulls[offset + MOST_COMMON_ELEMS_ELEM] !=
+			exprs_nulls[offset + MOST_COMMON_ELEM_FREQS_ELEM])
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Expression %s and %s must both be NOT NULL or both NULL.",
+							extexprarginfo[MOST_COMMON_ELEMS_ELEM].argname,
+							extexprarginfo[MOST_COMMON_ELEM_FREQS_ELEM].argname)));
+			return (Datum) 0;
+		}
+
+		/*
+		 * We only need to fetch element type and eq operator if we have a
+		 * stat of type MCELEM or DECHIST.
+		 */
+		if (!exprs_nulls[offset + MOST_COMMON_ELEMS_ELEM] ||
+			!exprs_nulls[offset + ELEM_COUNT_HISTOGRAM_ELEM])
+		{
+			if (!get_elem_stat_type(typid, typcache->typtype,
+									elevel, &elemtypid, &elem_eq_opr))
+			{
+				ereport(elevel,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 (errmsg("unable to determine element type of expression"))));
+				return (Datum) 0;
+			}
+		}
+
+		if (!exprs_nulls[offset + MOST_COMMON_ELEMS_ELEM])
+		{
+			Datum		stavalues;
+			Datum		stanumbers;
+
+			stavalues = text_to_stavalues(extexprarginfo[MOST_COMMON_ELEMS_ELEM].argname,
+										  &array_in_fn,
+										  exprs_elems[offset + MOST_COMMON_ELEMS_ELEM],
+										  elemtypid, typmod,
+										  elevel, &ok);
+
+			if (!ok)
+				return (Datum) 0;
+
+			stanumbers = text_to_stavalues(extexprarginfo[MOST_COMMON_ELEM_FREQS_ELEM].argname,
+										   &array_in_fn,
+										   exprs_elems[offset + MOST_COMMON_ELEM_FREQS_ELEM],
+										   FLOAT4OID, -1, elevel, &ok);
+
+			if (!ok)
+				return (Datum) 0;
+
+			set_stats_slot(values, nulls, replaces,
+						   STATISTIC_KIND_MCELEM,
+						   elem_eq_opr, stacoll,
+						   stanumbers, false, stavalues, false);
+		}
+
+		if (!exprs_nulls[offset + ELEM_COUNT_HISTOGRAM_ELEM])
+		{
+			Datum		stanumbers;
+
+			stanumbers = text_to_stavalues(extexprarginfo[ELEM_COUNT_HISTOGRAM_ELEM].argname,
+										   &array_in_fn,
+										   exprs_elems[offset + ELEM_COUNT_HISTOGRAM_ELEM],
+										   FLOAT4OID, -1, elevel, &ok);
+
+			if (!ok)
+				return (Datum) 0;
+
+			set_stats_slot(values, nulls, replaces, STATISTIC_KIND_DECHIST,
+						   elem_eq_opr, stacoll,
+						   stanumbers, false, 0, true);
+		}
+
+		/*
+		 * Currently there are no extended stats exports of the statistic
+		 * kinds STATISTIC_KIND_BOUNDS_HISTOGRAM or
+		 * STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM so these cannot be imported.
+		 * These may be added in the future.
+		 */
+
+		pgstup = heap_form_tuple(RelationGetDescr(pgsd), values, nulls);
+		pgstdat = heap_copy_tuple_as_datum(pgstup, RelationGetDescr(pgsd));
+		astate = accumArrayResult(astate, pgstdat, false, pgstypoid,
+								  CurrentMemoryContext);
+
+		offset += NUM_ATTRIBUTE_STATS_ELEMS;
+	}
+
+	pfree(exprs_elems);
+	pfree(exprs_nulls);
+
+	return makeArrayResult(astate, CurrentMemoryContext);
+}
+
+static bool
+text_to_float4(Datum input, Datum *output)
+{
+	ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+	char	   *s;
+	bool		ok;
+
+	s = TextDatumGetCString(input);
+	ok = DirectInputFunctionCallSafe(float4in, s, InvalidOid, -1,
+									 (Node *) &escontext, output);
+
+	pfree(s);
+	return ok;
+}
+
+
+static bool
+text_to_int4(Datum input, Datum *output)
+{
+	ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+	char	   *s;
+	bool		ok;
+
+	s = TextDatumGetCString(input);
+	ok = DirectInputFunctionCallSafe(int4in, s, InvalidOid, -1,
+									 (Node *) &escontext, output);
+
+	pfree(s);
+	return ok;
+}
+
+static bool
+delete_pg_statistic_ext_data(Oid stxoid, bool inherited)
+{
+	Relation	sed = table_open(StatisticExtDataRelationId, RowExclusiveLock);
+	HeapTuple	oldtup;
+	bool		result = false;
+
+	/* Is there already a pg_statistic tuple for this attribute? */
+	oldtup = SearchSysCache2(STATEXTDATASTXOID,
+							 ObjectIdGetDatum(stxoid),
+							 BoolGetDatum(inherited));
+
+	if (HeapTupleIsValid(oldtup))
+	{
+		CatalogTupleDelete(sed, &oldtup->t_self);
+		ReleaseSysCache(oldtup);
+		result = true;
+	}
+
+	table_close(sed, RowExclusiveLock);
+
+	CommandCounterIncrement();
+
+	return result;
+}
+
+ /*
+  * Import statistics for a given statistics object.
+  *
+  * Inserts or replaces a row in pg_statistic_ext_data for the given relation
+  * and statistic object schema+name. It takes input parameters that
+  * correspond to columns in the view pg_stats_ext and pg_stats_ext_exprs.
+  *
+  * Parameters are only superficially validated. Omitting a parameter or
+  * passing NULL leaves the statistic unchanged.
+  *
+  */
+Datum
+pg_set_extended_stats(PG_FUNCTION_ARGS)
+{
+	extended_statistics_update(fcinfo, ERROR);
+	PG_RETURN_VOID();
+}
+
+
+Datum
+pg_restore_extended_stats(PG_FUNCTION_ARGS)
+{
+	LOCAL_FCINFO(positional_fcinfo, NUM_EXTENDED_STATS_ARGS);
+	bool		result = true;
+
+	InitFunctionCallInfoData(*positional_fcinfo, NULL, NUM_EXTENDED_STATS_ARGS,
+							 InvalidOid, NULL, NULL);
+
+	if (!stats_fill_fcinfo_from_arg_pairs(fcinfo, positional_fcinfo,
+										  extarginfo, WARNING))
+		result = false;
+
+	if (!extended_statistics_update(positional_fcinfo, WARNING))
+		result = false;
+
+	PG_RETURN_BOOL(result);
+}
+
+/*
+ * Delete statistics for the given statistics object.
+ */
+Datum
+pg_clear_extended_stats(PG_FUNCTION_ARGS)
+{
+	Oid			nspoid;
+	Name		stxname;
+	bool		inherited;
+	Relation	pg_stext;
+	HeapTuple	tup;
+
+	Form_pg_statistic_ext stxform;
+
+
+	stats_check_required_arg(fcinfo, extarginfo, STATSCHEMA_ARG);
+	nspoid = PG_GETARG_OID(STATSCHEMA_ARG);
+	stats_check_required_arg(fcinfo, extarginfo, STATNAME_ARG);
+	stxname = PG_GETARG_NAME(STATNAME_ARG);
+	stats_check_required_arg(fcinfo, extarginfo, INHERITED_ARG);
+	inherited = PG_GETARG_NAME(INHERITED_ARG);
+
+	if (RecoveryInProgress())
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("recovery is in progress"),
+				 errhint("Statistics cannot be modified during recovery.")));
+
+	pg_stext = table_open(StatisticExtRelationId, RowExclusiveLock);
+	tup = get_pg_statistic_ext(pg_stext, nspoid, stxname);
+
+	if (!HeapTupleIsValid(tup))
+	{
+		table_close(pg_stext, RowExclusiveLock);
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("Extended Statistics Object \"%s\".\"%s\" not found.",
+						get_namespace_name(nspoid),
+						NameStr(*stxname))));
+		return false;
+	}
+
+	stxform = (Form_pg_statistic_ext) GETSTRUCT(tup);
+
+	stats_lock_check_privileges(stxform->stxrelid);
+
+	delete_pg_statistic_ext_data(stxform->oid, inherited);
+	heap_freetuple(tup);
+	table_close(pg_stext, RowExclusiveLock);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/statistics/mcv.c b/src/backend/statistics/mcv.c
index d98cda698d..73f78e0607 100644
--- a/src/backend/statistics/mcv.c
+++ b/src/backend/statistics/mcv.c
@@ -2173,3 +2173,147 @@ mcv_clause_selectivity_or(PlannerInfo *root, StatisticExtInfo *stat,
 
 	return s;
 }
+
+/*
+  * The MCV is an array of records, but this is expected as 4 separate arrays.
+  * It is not possible to have a generic input function for pg_mcv_list
+  * because the most_common_values is a composite type with element types
+  * defined by the specific statistics object.
+  */
+Datum
+import_mcvlist(HeapTuple tup, int elevel, int numattrs, Oid *atttypids,
+			   int32 *atttypmods, Oid *atttypcolls, int nitems,
+			   Datum *mcv_elems, bool *mcv_nulls,
+			   bool *mcv_elem_nulls, float8 *freqs, float8 *base_freqs)
+{
+	MCVList    *mcvlist;
+	bytea	   *bytes;
+
+	HeapTuple  *vatuples;
+	VacAttrStats **vastats;
+
+	/*
+	 * Allocate the MCV list structure, set the global parameters.
+	 */
+	mcvlist = (MCVList *) palloc0(offsetof(MCVList, items) +
+								  (sizeof(MCVItem) * nitems));
+
+	mcvlist->magic = STATS_MCV_MAGIC;
+	mcvlist->type = STATS_MCV_TYPE_BASIC;
+	mcvlist->ndimensions = numattrs;
+	mcvlist->nitems = nitems;
+
+	/* Set the values for the 1-D arrays and allocate space for the 2-D arrays */
+	for (int i = 0; i < nitems; i++)
+	{
+		MCVItem    *item = &mcvlist->items[i];
+
+		item->frequency = freqs[i];
+		item->base_frequency = base_freqs[i];
+		item->values = (Datum *) palloc0(sizeof(Datum) * numattrs);
+		item->isnull = (bool *) palloc0(sizeof(bool) * numattrs);
+	}
+
+	/* Walk through each dimension */
+	for (int j = 0; j < numattrs; j++)
+	{
+		FmgrInfo	finfo;
+		Oid			ioparam;
+		Oid			infunc;
+		int			index = j;
+
+		getTypeInputInfo(atttypids[j], &infunc, &ioparam);
+		fmgr_info(infunc, &finfo);
+
+		/* store info about data type OIDs */
+		mcvlist->types[j] = atttypids[j];
+
+		for (int i = 0; i < nitems; i++)
+		{
+			MCVItem    *item = &mcvlist->items[i];
+
+			/* These should be in agreement, but just to be safe check both */
+			if (mcv_elem_nulls[index] || mcv_nulls[index])
+			{
+				item->values[j] = (Datum) 0;
+				item->isnull[j] = true;
+			}
+			else
+			{
+				char	   *s = TextDatumGetCString(mcv_elems[index]);
+				ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+				if (!InputFunctionCallSafe(&finfo, s, ioparam, atttypmods[j],
+										   (fmNodePtr) &escontext, &item->values[j]))
+				{
+					ereport(elevel,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("MCV elemement \"%s\" does not match expected input type.", s)));
+					return (Datum) 0;
+				}
+
+				pfree(s);
+			}
+
+			index += numattrs;
+		}
+	}
+
+	/*
+	 * The function statext_mcv_serialize() requires an array of pointers to
+	 * VacAttrStats records, but only a few fields within those records have
+	 * to be filled out.
+	 */
+	vastats = (VacAttrStats **) palloc0(numattrs * sizeof(VacAttrStats));
+	vatuples = (HeapTuple *) palloc0(numattrs * sizeof(HeapTuple));
+
+	for (int i = 0; i < numattrs; i++)
+	{
+		Oid			typid = atttypids[i];
+		HeapTuple	typtuple;
+
+		typtuple = SearchSysCacheCopy1(TYPEOID, ObjectIdGetDatum(typid));
+
+		if (!HeapTupleIsValid(typtuple))
+			elog(ERROR, "cache lookup failed for type %u", typid);
+
+		vatuples[i] = typtuple;
+
+		vastats[i] = palloc0(sizeof(VacAttrStats));
+
+		vastats[i]->attrtype = (Form_pg_type) GETSTRUCT(typtuple);
+		vastats[i]->attrtypid = typid;
+		vastats[i]->attrcollid = atttypcolls[i];
+	}
+
+	bytes = statext_mcv_serialize(mcvlist, vastats);
+
+	for (int i = 0; i < numattrs; i++)
+	{
+		pfree(vatuples[i]);
+		pfree(vastats[i]);
+	}
+	pfree((void *) vatuples);
+	pfree((void *) vastats);
+
+	if (bytes == NULL)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Unable to import mcv list")));
+		return (Datum) 0;
+	}
+
+	for (int i = 0; i < nitems; i++)
+	{
+		MCVItem    *item = &mcvlist->items[i];
+
+		pfree(item->values);
+		pfree(item->isnull);
+	}
+	pfree(mcvlist);
+	pfree(mcv_elems);
+	pfree(mcv_nulls);
+
+	return PointerGetDatum(bytes);
+}
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index fb50da1cd8..9f3b45fc55 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -1414,11 +1414,15 @@ SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import.complex_type,
 UNION ALL
 SELECT 4, 'four', NULL, int4range(0,100), NULL;
 CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
+CREATE STATISTICS stats_import.test_stat ON name, comp, lower(arange), array_length(tags,1)
+FROM stats_import.test;
 -- Generate statistics on table with data
 ANALYZE stats_import.test;
 CREATE TABLE stats_import.test_clone ( LIKE stats_import.test )
     WITH (autovacuum_enabled = false);
 CREATE INDEX is_odd_clone ON stats_import.test_clone(((comp).a % 2 = 1));
+CREATE STATISTICS stats_import.test_stat_clone ON name, comp, lower(arange), array_length(tags,1)
+FROM stats_import.test_clone;
 --
 -- Copy stats from test to test_clone, and is_odd to is_odd_clone
 --
@@ -1801,6 +1805,332 @@ WHERE s.starelid = 'stats_import.is_odd'::regclass;
 ---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
 (0 rows)
 
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        n_distinct => '{"2, 3": 4, "2, -1": 4, "2, -2": 4, "3, -1": 4, "3, -2": 4, "-1, -2": 3, "2, 3, -1": 4, "2, 3, -2": 4, "2, -1, -2": 4, "3, -1, -2": 4, "2, 3, -1, -2": 4}'::pg_ndistinct
+      );
+ pg_set_extended_stats 
+-----------------------
+ 
+(1 row)
+
+SELECT
+    e.n_distinct, e.dependencies, e.most_common_vals, e.most_common_val_nulls,
+    e.most_common_freqs, e.most_common_base_freqs
+FROM pg_stats_ext AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false
+\gx
+-[ RECORD 1 ]----------+----------------------------------------------------------------------------------------------------------------------------------------------------------
+n_distinct             | {"2, 3": 4, "2, -1": 4, "2, -2": 4, "3, -1": 4, "3, -2": 4, "-1, -2": 3, "2, 3, -1": 4, "2, 3, -2": 4, "2, -1, -2": 4, "3, -1, -2": 4, "2, 3, -1, -2": 4}
+dependencies           | 
+most_common_vals       | 
+most_common_val_nulls  | 
+most_common_freqs      | 
+most_common_base_freqs | 
+
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        dependencies => '{"2 => 3": 1.000000, "2 => -1": 1.000000, "2 => -2": 1.000000, "3 => 2": 1.000000, "3 => -1": 1.000000, "3 => -2": 1.000000, "-1 => 2": 0.500000, "-1 => 3": 0.500000, "-1 => -2": 1.000000, "-2 => 2": 0.500000, "-2 => 3": 0.500000, "-2 => -1": 1.000000, "2, 3 => -1": 1.000000, "2, 3 => -2": 1.000000, "2, -1 => 3": 1.000000, "2, -1 => -2": 1.000000, "2, -2 => 3": 1.000000, "2, -2 => -1": 1.000000, "3, -1 => 2": 1.000000, "3, -1 => -2": 1.000000, "3, -2 => 2": 1.000000, "3, -2 => -1": 1.000000, "-1, -2 => 2": 0.500000, "-1, -2 => 3": 0.500000, "2, 3, -1 => -2": 1.000000, "2, 3, -2 => -1": 1.000000, "2, -1, -2 => 3": 1.000000, "3, -1, -2 => 2": 1.000000}'::pg_dependencies
+      );
+ pg_set_extended_stats 
+-----------------------
+ 
+(1 row)
+
+SELECT
+    e.n_distinct, e.dependencies, e.most_common_vals, e.most_common_val_nulls,
+    e.most_common_freqs, e.most_common_base_freqs
+FROM pg_stats_ext AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false
+\gx
+-[ RECORD 1 ]----------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+n_distinct             | {"2, 3": 4, "2, -1": 4, "2, -2": 4, "3, -1": 4, "3, -2": 4, "-1, -2": 3, "2, 3, -1": 4, "2, 3, -2": 4, "2, -1, -2": 4, "3, -1, -2": 4, "2, 3, -1, -2": 4}
+dependencies           | {"2 => 3": 1.000000, "2 => -1": 1.000000, "2 => -2": 1.000000, "3 => 2": 1.000000, "3 => -1": 1.000000, "3 => -2": 1.000000, "-1 => 2": 0.500000, "-1 => 3": 0.500000, "-1 => -2": 1.000000, "-2 => 2": 0.500000, "-2 => 3": 0.500000, "-2 => -1": 1.000000, "2, 3 => -1": 1.000000, "2, 3 => -2": 1.000000, "2, -1 => 3": 1.000000, "2, -1 => -2": 1.000000, "2, -2 => 3": 1.000000, "2, -2 => -1": 1.000000, "3, -1 => 2": 1.000000, "3, -1 => -2": 1.000000, "3, -2 => 2": 1.000000, "3, -2 => -1": 1.000000, "-1, -2 => 2": 0.500000, "-1, -2 => 3": 0.500000, "2, 3, -1 => -2": 1.000000, "2, 3, -2 => -1": 1.000000, "2, -1, -2 => 3": 1.000000, "3, -1, -2 => 2": 1.000000}
+most_common_vals       | 
+most_common_val_nulls  | 
+most_common_freqs      | 
+most_common_base_freqs | 
+
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        most_common_vals => '{{four,NULL,0,NULL},{one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")",1,2},{tre,"(3,3.3,TRE,03-03-2003,)",-1,3},{two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")",1,2}}'
+      );
+ERROR:  MCV parameters "most_common_vals", "most_common_val_nulls", "most_common_freqs", and "most_common_base_freqs" must be all specified if any are specified
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        most_common_val_nulls => '{{f,t,f,t},{f,f,f,f},{f,f,f,f},{f,f,f,f}}'
+      );
+ERROR:  MCV parameters "most_common_vals", "most_common_val_nulls", "most_common_freqs", and "most_common_base_freqs" must be all specified if any are specified
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        most_common_freqs => '{0.25,0.25,0.25,0.25}'
+      );
+ERROR:  MCV parameters "most_common_vals", "most_common_val_nulls", "most_common_freqs", and "most_common_base_freqs" must be all specified if any are specified
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        most_common_base_freqs => '{0.00390625,0.015625,0.00390625,0.015625}'
+      );
+ERROR:  MCV parameters "most_common_vals", "most_common_val_nulls", "most_common_freqs", and "most_common_base_freqs" must be all specified if any are specified
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        most_common_vals => '{{four,NULL,0,NULL},{one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")",1,2},{tre,"(3,3.3,TRE,03-03-2003,)",-1,3},{two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")",1,2}}',
+        most_common_val_nulls => '{{f,t,f,t},{f,f,f,f},{f,f,f,f},{f,f,f,f}}',
+        most_common_freqs => '{0.25,0.25,0.25,0.25}',
+        most_common_base_freqs => '{0.00390625,0.015625,0.00390625,0.015625}'
+      );
+ pg_set_extended_stats 
+-----------------------
+ 
+(1 row)
+
+SELECT
+    e.n_distinct, e.dependencies, e.most_common_vals, e.most_common_val_nulls,
+    e.most_common_freqs, e.most_common_base_freqs
+FROM pg_stats_ext AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false
+\gx
+-[ RECORD 1 ]----------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+n_distinct             | {"2, 3": 4, "2, -1": 4, "2, -2": 4, "3, -1": 4, "3, -2": 4, "-1, -2": 3, "2, 3, -1": 4, "2, 3, -2": 4, "2, -1, -2": 4, "3, -1, -2": 4, "2, 3, -1, -2": 4}
+dependencies           | {"2 => 3": 1.000000, "2 => -1": 1.000000, "2 => -2": 1.000000, "3 => 2": 1.000000, "3 => -1": 1.000000, "3 => -2": 1.000000, "-1 => 2": 0.500000, "-1 => 3": 0.500000, "-1 => -2": 1.000000, "-2 => 2": 0.500000, "-2 => 3": 0.500000, "-2 => -1": 1.000000, "2, 3 => -1": 1.000000, "2, 3 => -2": 1.000000, "2, -1 => 3": 1.000000, "2, -1 => -2": 1.000000, "2, -2 => 3": 1.000000, "2, -2 => -1": 1.000000, "3, -1 => 2": 1.000000, "3, -1 => -2": 1.000000, "3, -2 => 2": 1.000000, "3, -2 => -1": 1.000000, "-1, -2 => 2": 0.500000, "-1, -2 => 3": 0.500000, "2, 3, -1 => -2": 1.000000, "2, 3, -2 => -1": 1.000000, "2, -1, -2 => 3": 1.000000, "3, -1, -2 => 2": 1.000000}
+most_common_vals       | {{four,NULL,0,NULL},{one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")",1,2},{tre,"(3,3.3,TRE,03-03-2003,)",-1,3},{two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")",1,2}}
+most_common_val_nulls  | {{f,t,f,t},{f,f,f,f},{f,f,f,f},{f,f,f,f}}
+most_common_freqs      | {0.25,0.25,0.25,0.25}
+most_common_base_freqs | {0.00390625,0.015625,0.00390625,0.015625}
+
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        exprs => '{{0,4,-0.75,"{1}","{0.5}","{-1,0}",-0.6,NULL,NULL,NULL},{0.25,4,-0.5,"{2}","{0.5}",NULL,1,NULL,NULL,NULL}}'
+      );
+ pg_set_extended_stats 
+-----------------------
+ 
+(1 row)
+
+SELECT
+    e.inherited, e.null_frac, e.avg_width, e.n_distinct, e.most_common_vals,
+    e.most_common_freqs, e.histogram_bounds, e.correlation,
+    e.most_common_elems, e.most_common_elem_freqs, e.elem_count_histogram
+FROM pg_stats_ext_exprs AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+and e.inherited = false
+\gx
+-[ RECORD 1 ]----------+-------
+inherited              | f
+null_frac              | 0
+avg_width              | 4
+n_distinct             | -0.75
+most_common_vals       | {1}
+most_common_freqs      | {0.5}
+histogram_bounds       | {-1,0}
+correlation            | -0.6
+most_common_elems      | 
+most_common_elem_freqs | 
+elem_count_histogram   | 
+-[ RECORD 2 ]----------+-------
+inherited              | f
+null_frac              | 0.25
+avg_width              | 4
+n_distinct             | -0.5
+most_common_vals       | {2}
+most_common_freqs      | {0.5}
+histogram_bounds       | 
+correlation            | 1
+most_common_elems      | 
+most_common_elem_freqs | 
+elem_count_histogram   | 
+
+SELECT
+    pg_catalog.pg_clear_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false);
+ pg_clear_extended_stats 
+-------------------------
+ 
+(1 row)
+
+SELECT COUNT(*)
+FROM pg_stats_ext AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false;
+ count 
+-------
+     0
+(1 row)
+
+SELECT COUNT(*)
+FROM pg_stats_ext_exprs AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false;
+ count 
+-------
+     0
+(1 row)
+
+--
+-- Copy stats from test_stat to test_stat_clone
+--
+SELECT
+    e.statistics_name,
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', e.statistics_schemaname::regnamespace,
+        'statistics_name', 'test_stat_clone'::name,
+        'inherited', e.inherited,
+        'n_distinct', e.n_distinct,
+        'dependencies', e.dependencies,
+        'most_common_vals', e.most_common_vals,
+        'most_common_val_nulls', e.most_common_val_nulls,
+        '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
+        array_agg(
+            ARRAY[ee.null_frac::text, ee.avg_width::text,
+                  ee.n_distinct::text, ee.most_common_vals::text,
+                  ee.most_common_freqs::text, ee.histogram_bounds::text,
+                  ee.correlation::text, ee.most_common_elems::text,
+                  ee.most_common_elem_freqs::text,
+                  ee.elem_count_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 = 'test_stat';
+ statistics_name | pg_restore_extended_stats 
+-----------------+---------------------------
+ test_stat       | t
+(1 row)
+
+SELECT o.inherited,
+       o.n_distinct, o.dependencies, o.most_common_vals,
+       o.most_common_val_nulls, o.most_common_freqs,
+       o.most_common_base_freqs
+FROM pg_stats_ext AS o
+WHERE o.statistics_schemaname = 'stats_import'
+AND o.statistics_name = 'test_stat'
+EXCEPT
+SELECT n.inherited,
+       n.n_distinct, n.dependencies, n.most_common_vals,
+       n.most_common_val_nulls, n.most_common_freqs,
+       n.most_common_base_freqs
+FROM pg_stats_ext AS n
+WHERE n.statistics_schemaname = 'stats_import'
+AND n.statistics_name = 'test_stat_clone';
+ inherited | n_distinct | dependencies | most_common_vals | most_common_val_nulls | most_common_freqs | most_common_base_freqs 
+-----------+------------+--------------+------------------+-----------------------+-------------------+------------------------
+(0 rows)
+
+SELECT n.inherited,
+       n.n_distinct, n.dependencies, n.most_common_vals,
+       n.most_common_val_nulls, n.most_common_freqs,
+       n.most_common_base_freqs
+FROM pg_stats_ext AS n
+WHERE n.statistics_schemaname = 'stats_import'
+AND n.statistics_name = 'test_stat_clone'
+EXCEPT
+SELECT o.inherited,
+       o.n_distinct, o.dependencies, o.most_common_vals,
+       o.most_common_val_nulls, o.most_common_freqs,
+       o.most_common_base_freqs
+FROM pg_stats_ext AS o
+WHERE o.statistics_schemaname = 'stats_import'
+AND o.statistics_name = 'test_stat';
+ inherited | n_distinct | dependencies | most_common_vals | most_common_val_nulls | most_common_freqs | most_common_base_freqs 
+-----------+------------+--------------+------------------+-----------------------+-------------------+------------------------
+(0 rows)
+
+SELECT o.inherited,
+       o.null_frac, o.avg_width, o.n_distinct,
+       o.most_common_vals::text AS most_common_vals,
+       o.most_common_freqs,
+       o.histogram_bounds::text AS histogram_bounds,
+       o.correlation,
+       o.most_common_elems::text AS most_common_elems,
+       o.most_common_elem_freqs, o.elem_count_histogram
+FROM pg_stats_ext_exprs AS o
+WHERE o.statistics_schemaname = 'stats_import'
+AND o.statistics_name = 'test_stat'
+EXCEPT
+SELECT n.inherited,
+       n.null_frac, n.avg_width, n.n_distinct,
+       n.most_common_vals::text AS most_common_vals,
+       n.most_common_freqs,
+       n.histogram_bounds::text AS histogram_bounds,
+       n.correlation,
+       n.most_common_elems::text AS most_common_elems,
+       n.most_common_elem_freqs, n.elem_count_histogram
+FROM pg_stats_ext_exprs AS n
+WHERE n.statistics_schemaname = 'stats_import'
+AND n.statistics_name = 'test_stat_clone';
+ inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram 
+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------
+(0 rows)
+
+SELECT n.inherited,
+       n.null_frac, n.avg_width, n.n_distinct,
+       n.most_common_vals::text AS most_common_vals,
+       n.most_common_freqs,
+       n.histogram_bounds::text AS histogram_bounds,
+       n.correlation,
+       n.most_common_elems::text AS most_common_elems,
+       n.most_common_elem_freqs, n.elem_count_histogram
+FROM pg_stats_ext_exprs AS n
+WHERE n.statistics_schemaname = 'stats_import'
+AND n.statistics_name = 'test_stat_clone'
+EXCEPT
+SELECT o.inherited,
+       o.null_frac, o.avg_width, o.n_distinct,
+       o.most_common_vals::text AS most_common_vals,
+       o.most_common_freqs,
+       o.histogram_bounds::text AS histogram_bounds,
+       o.correlation,
+       o.most_common_elems::text AS most_common_elems,
+       o.most_common_elem_freqs, o.elem_count_histogram
+FROM pg_stats_ext_exprs AS o
+WHERE o.statistics_schemaname = 'stats_import'
+AND o.statistics_name = 'test_stat';
+ inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram 
+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------
+(0 rows)
+
 DROP SCHEMA stats_import CASCADE;
 NOTICE:  drop cascades to 6 other objects
 DETAIL:  drop cascades to type stats_import.complex_type
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index d3058bf8f6..98aa934d12 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -1062,6 +1062,9 @@ SELECT 4, 'four', NULL, int4range(0,100), NULL;
 
 CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
 
+CREATE STATISTICS stats_import.test_stat ON name, comp, lower(arange), array_length(tags,1)
+FROM stats_import.test;
+
 -- Generate statistics on table with data
 ANALYZE stats_import.test;
 
@@ -1070,6 +1073,9 @@ CREATE TABLE stats_import.test_clone ( LIKE stats_import.test )
 
 CREATE INDEX is_odd_clone ON stats_import.test_clone(((comp).a % 2 = 1));
 
+CREATE STATISTICS stats_import.test_stat_clone ON name, comp, lower(arange), array_length(tags,1)
+FROM stats_import.test_clone;
+
 --
 -- Copy stats from test to test_clone, and is_odd to is_odd_clone
 --
@@ -1381,4 +1387,242 @@ FROM pg_statistic s
 JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
 WHERE s.starelid = 'stats_import.is_odd'::regclass;
 
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        n_distinct => '{"2, 3": 4, "2, -1": 4, "2, -2": 4, "3, -1": 4, "3, -2": 4, "-1, -2": 3, "2, 3, -1": 4, "2, 3, -2": 4, "2, -1, -2": 4, "3, -1, -2": 4, "2, 3, -1, -2": 4}'::pg_ndistinct
+      );
+
+SELECT
+    e.n_distinct, e.dependencies, e.most_common_vals, e.most_common_val_nulls,
+    e.most_common_freqs, e.most_common_base_freqs
+FROM pg_stats_ext AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false
+\gx
+
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        dependencies => '{"2 => 3": 1.000000, "2 => -1": 1.000000, "2 => -2": 1.000000, "3 => 2": 1.000000, "3 => -1": 1.000000, "3 => -2": 1.000000, "-1 => 2": 0.500000, "-1 => 3": 0.500000, "-1 => -2": 1.000000, "-2 => 2": 0.500000, "-2 => 3": 0.500000, "-2 => -1": 1.000000, "2, 3 => -1": 1.000000, "2, 3 => -2": 1.000000, "2, -1 => 3": 1.000000, "2, -1 => -2": 1.000000, "2, -2 => 3": 1.000000, "2, -2 => -1": 1.000000, "3, -1 => 2": 1.000000, "3, -1 => -2": 1.000000, "3, -2 => 2": 1.000000, "3, -2 => -1": 1.000000, "-1, -2 => 2": 0.500000, "-1, -2 => 3": 0.500000, "2, 3, -1 => -2": 1.000000, "2, 3, -2 => -1": 1.000000, "2, -1, -2 => 3": 1.000000, "3, -1, -2 => 2": 1.000000}'::pg_dependencies
+      );
+
+SELECT
+    e.n_distinct, e.dependencies, e.most_common_vals, e.most_common_val_nulls,
+    e.most_common_freqs, e.most_common_base_freqs
+FROM pg_stats_ext AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false
+\gx
+
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        most_common_vals => '{{four,NULL,0,NULL},{one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")",1,2},{tre,"(3,3.3,TRE,03-03-2003,)",-1,3},{two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")",1,2}}'
+      );
+
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        most_common_val_nulls => '{{f,t,f,t},{f,f,f,f},{f,f,f,f},{f,f,f,f}}'
+      );
+
+
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        most_common_freqs => '{0.25,0.25,0.25,0.25}'
+      );
+
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        most_common_base_freqs => '{0.00390625,0.015625,0.00390625,0.015625}'
+      );
+
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        most_common_vals => '{{four,NULL,0,NULL},{one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")",1,2},{tre,"(3,3.3,TRE,03-03-2003,)",-1,3},{two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")",1,2}}',
+        most_common_val_nulls => '{{f,t,f,t},{f,f,f,f},{f,f,f,f},{f,f,f,f}}',
+        most_common_freqs => '{0.25,0.25,0.25,0.25}',
+        most_common_base_freqs => '{0.00390625,0.015625,0.00390625,0.015625}'
+      );
+
+SELECT
+    e.n_distinct, e.dependencies, e.most_common_vals, e.most_common_val_nulls,
+    e.most_common_freqs, e.most_common_base_freqs
+FROM pg_stats_ext AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false
+\gx
+
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        exprs => '{{0,4,-0.75,"{1}","{0.5}","{-1,0}",-0.6,NULL,NULL,NULL},{0.25,4,-0.5,"{2}","{0.5}",NULL,1,NULL,NULL,NULL}}'
+      );
+
+SELECT
+    e.inherited, e.null_frac, e.avg_width, e.n_distinct, e.most_common_vals,
+    e.most_common_freqs, e.histogram_bounds, e.correlation,
+    e.most_common_elems, e.most_common_elem_freqs, e.elem_count_histogram
+FROM pg_stats_ext_exprs AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+and e.inherited = false
+\gx
+
+SELECT
+    pg_catalog.pg_clear_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false);
+
+SELECT COUNT(*)
+FROM pg_stats_ext AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false;
+
+SELECT COUNT(*)
+FROM pg_stats_ext_exprs AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false;
+
+--
+-- Copy stats from test_stat to test_stat_clone
+--
+SELECT
+    e.statistics_name,
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', e.statistics_schemaname::regnamespace,
+        'statistics_name', 'test_stat_clone'::name,
+        'inherited', e.inherited,
+        'n_distinct', e.n_distinct,
+        'dependencies', e.dependencies,
+        'most_common_vals', e.most_common_vals,
+        'most_common_val_nulls', e.most_common_val_nulls,
+        '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
+        array_agg(
+            ARRAY[ee.null_frac::text, ee.avg_width::text,
+                  ee.n_distinct::text, ee.most_common_vals::text,
+                  ee.most_common_freqs::text, ee.histogram_bounds::text,
+                  ee.correlation::text, ee.most_common_elems::text,
+                  ee.most_common_elem_freqs::text,
+                  ee.elem_count_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 = 'test_stat';
+
+SELECT o.inherited,
+       o.n_distinct, o.dependencies, o.most_common_vals,
+       o.most_common_val_nulls, o.most_common_freqs,
+       o.most_common_base_freqs
+FROM pg_stats_ext AS o
+WHERE o.statistics_schemaname = 'stats_import'
+AND o.statistics_name = 'test_stat'
+EXCEPT
+SELECT n.inherited,
+       n.n_distinct, n.dependencies, n.most_common_vals,
+       n.most_common_val_nulls, n.most_common_freqs,
+       n.most_common_base_freqs
+FROM pg_stats_ext AS n
+WHERE n.statistics_schemaname = 'stats_import'
+AND n.statistics_name = 'test_stat_clone';
+
+SELECT n.inherited,
+       n.n_distinct, n.dependencies, n.most_common_vals,
+       n.most_common_val_nulls, n.most_common_freqs,
+       n.most_common_base_freqs
+FROM pg_stats_ext AS n
+WHERE n.statistics_schemaname = 'stats_import'
+AND n.statistics_name = 'test_stat_clone'
+EXCEPT
+SELECT o.inherited,
+       o.n_distinct, o.dependencies, o.most_common_vals,
+       o.most_common_val_nulls, o.most_common_freqs,
+       o.most_common_base_freqs
+FROM pg_stats_ext AS o
+WHERE o.statistics_schemaname = 'stats_import'
+AND o.statistics_name = 'test_stat';
+
+SELECT o.inherited,
+       o.null_frac, o.avg_width, o.n_distinct,
+       o.most_common_vals::text AS most_common_vals,
+       o.most_common_freqs,
+       o.histogram_bounds::text AS histogram_bounds,
+       o.correlation,
+       o.most_common_elems::text AS most_common_elems,
+       o.most_common_elem_freqs, o.elem_count_histogram
+FROM pg_stats_ext_exprs AS o
+WHERE o.statistics_schemaname = 'stats_import'
+AND o.statistics_name = 'test_stat'
+EXCEPT
+SELECT n.inherited,
+       n.null_frac, n.avg_width, n.n_distinct,
+       n.most_common_vals::text AS most_common_vals,
+       n.most_common_freqs,
+       n.histogram_bounds::text AS histogram_bounds,
+       n.correlation,
+       n.most_common_elems::text AS most_common_elems,
+       n.most_common_elem_freqs, n.elem_count_histogram
+FROM pg_stats_ext_exprs AS n
+WHERE n.statistics_schemaname = 'stats_import'
+AND n.statistics_name = 'test_stat_clone';
+
+SELECT n.inherited,
+       n.null_frac, n.avg_width, n.n_distinct,
+       n.most_common_vals::text AS most_common_vals,
+       n.most_common_freqs,
+       n.histogram_bounds::text AS histogram_bounds,
+       n.correlation,
+       n.most_common_elems::text AS most_common_elems,
+       n.most_common_elem_freqs, n.elem_count_histogram
+FROM pg_stats_ext_exprs AS n
+WHERE n.statistics_schemaname = 'stats_import'
+AND n.statistics_name = 'test_stat_clone'
+EXCEPT
+SELECT o.inherited,
+       o.null_frac, o.avg_width, o.n_distinct,
+       o.most_common_vals::text AS most_common_vals,
+       o.most_common_freqs,
+       o.histogram_bounds::text AS histogram_bounds,
+       o.correlation,
+       o.most_common_elems::text AS most_common_elems,
+       o.most_common_elem_freqs, o.elem_count_histogram
+FROM pg_stats_ext_exprs AS o
+WHERE o.statistics_schemaname = 'stats_import'
+AND o.statistics_name = 'test_stat';
+
 DROP SCHEMA stats_import CASCADE;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 47370e581a..d74f23eda2 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30344,6 +30344,130 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
     'inherited',   false,
     'avg_width',   125::integer,
     'null_frac',   0.5::real);
+</programlisting>
+        </para>
+        <para>
+         Minor errors are reported as a <literal>WARNING</literal> and
+         ignored, and remaining statistics will still be restored. If all
+         specified statistics are successfully restored, return
+         <literal>true</literal>, otherwise <literal>false</literal>.
+        </para>
+       </entry>
+      </row>
+      <row>
+       <entry role="func_table_entry">
+        <para role="func_signature">
+         <indexterm>
+          <primary>pg_set_extended_stats</primary>
+         </indexterm>
+         <function>pg_set_extended_stats</function> (
+         <parameter>statistics_schemaname</parameter> <type>regnamespace</type>,
+         <parameter>statistics_name</parameter> <type>name</type>,
+         <parameter>inherited</parameter> <type>boolean</type>,
+         <optional>, <parameter>n_distinct</parameter> <type>pg_ndistinct</type></optional>,
+         <optional>, <parameter>dependencies</parameter> <type>pg_dependencies</type></optional>,
+         <optional>, <parameter>most_common_vals</parameter> <type>text[]</type></optional>,
+         <optional>, <parameter>most_common_val_nulls</parameter> <type>boolean[]</type></optional>,
+         <optional>, <parameter>most_common_freqs</parameter> <type>double precision[]</type> </optional>,
+         <optional>, <parameter>most_common_base_freqs</parameter> <type>double precision[]</type> </optional>,
+         <optional>, <parameter>exprs</parameter> <type>text[]</type> </optional> )
+         <returnvalue>void</returnvalue>
+        </para>
+        <para>
+         Creates or updates statistics for the given statistics object to the 
+         specified values. The parameters correspond to attributes of the same
+         name found in the view <link
+         linkend="view-pg-stats-ext"><structname>pg_stats_ext</structname></link>
+         and <link linkend="view-pg-stats-ext-exprs"><structname>pg_stats_ext_exprs</structname></link>,
+         except for <parameter>exprs</parameter> which corresponds to
+         <structfield>stxexpr</structfield> on
+         <link linkend="catalog-pg-statistic-ext-data"><structname>pg_statistic_ext_data</structname></link>
+         as a two-dimensional text array. The outer dimension represents a single
+         <structname>pg_statistic</structname> object, and the inner dimension elements are the
+         statistics fields from <link linkend="view-pg-stats"><structname>pg_stats</structname></link>
+         (currently <structfield>null_frac</structfield> through
+         <structfield>elem_count_histogram</structfield>).
+        </para>
+        <para>
+         Optional parameters default to <literal>NULL</literal>, which leave
+         the corresponding statistic unchanged.
+        </para>
+        <para>
+         Ordinarily, these statistics are collected automatically or updated
+         as a part of <xref linkend="sql-vacuum"/> or <xref
+         linkend="sql-analyze"/>, so it's not necessary to call this
+         function. However, it may be useful when testing the effects of
+         statistics on the planner to understand or anticipate plan changes.
+        </para>
+        <para>
+         The caller must have the <literal>MAINTAIN</literal> privilege on
+         the table or be the owner of the database.
+        </para>
+       </entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry">
+        <para role="func_signature">
+         <indexterm>
+          <primary>pg_clear_extended_stats</primary>
+         </indexterm>
+         <function>pg_clear_extended_stats</function> (
+         <parameter>statistics_schemaname</parameter> <type>regnamespace</type>,
+         <parameter>statistics_name</parameter> <type>name</type>,
+         <parameter>inherited</parameter> <type>boolean</type> )
+         <returnvalue>void</returnvalue>
+        </para>
+        <para>
+         Clears statistics for the given statistics object, as
+         though the object was newly created.
+        </para>
+        <para>
+         The caller must have the <literal>MAINTAIN</literal> privilege on
+         the table or be the owner of the database.
+        </para>
+       </entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_restore_extended_stats</primary>
+        </indexterm>
+        <function>pg_restore_extended_stats</function> (
+        <literal>VARIADIC</literal> <parameter>kwargs</parameter> <type>"any"</type> )
+        <returnvalue>boolean</returnvalue>
+        </para>
+        <para>
+         Similar to <function>pg_set_extended_stats()</function>, but
+         intended for bulk restore of extended statistics. The tracked
+         statistics may change from version to version, so the primary purpose
+         of this function is to maintain a consistent function signature to
+         avoid errors when restoring statistics from previous versions.
+        </para>
+        <para>
+         Arguments are passed as pairs of <replaceable>argname</replaceable>
+         and <replaceable>argvalue</replaceable>, where
+         <replaceable>argname</replaceable> corresponds to a named argument in
+         <function>pg_set_extended_stats()</function> and
+         <replaceable>argvalue</replaceable> is of the corresponding type.
+        </para>
+        <para>
+         Additionally, this function supports argument name
+         <literal>version</literal> of type <type>integer</type>, which
+         specifies the version from which the statistics originated, improving
+         interpretation of older statistics.
+        </para>
+        <para>
+         For example, to set the <structname>n_distinct</structname> and
+         <structname>exprs</structname> for the object
+         <structname>my_extended_stat</structname>:
+<programlisting>
+ SELECT pg_restore_extended_stats(
+    'statistics_schemaname', 'public'::regnamespace,
+    'statistics_name', 'my_extended_stat'::name,
+    'n_distinct', '{"2, 3": 4, "2, -1": 4, "2, -2": 4, "3, -1": 4, "3, -2": 4, "-1, -2": 3, "2, 3, -1": 4, "2, 3, -2": 4, "2, -1, -2": 4, "3, -1, -2": 4, "2, 3, -1, -2": 4}'::pg_ndistinct,
+    'exprs' => '{{0,4,-0.75,"{1}","{0.5}","{-1,0}",-0.6,NULL,NULL,NULL},{0.25,4,-0.5,"{2}","{0.5}",NULL,1,NULL,NULL,NULL}}');
 </programlisting>
         </para>
         <para>
-- 
2.48.1

From 816c20877f12179dc16881f938a8f2a3f0c36ad5 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 17 Dec 2024 19:47:43 -0500
Subject: [PATCH v2 2/6] Add working input function for pg_dependencies.

This is needed to import extended statistics.
---
 src/backend/statistics/dependencies.c   | 348 +++++++++++++++++++++++-
 src/test/regress/expected/stats_ext.out |  18 ++
 src/test/regress/sql/stats_ext.sql      |   6 +
 3 files changed, 362 insertions(+), 10 deletions(-)

diff --git a/src/backend/statistics/dependencies.c b/src/backend/statistics/dependencies.c
index eb2fc4366b..ec26a2427e 100644
--- a/src/backend/statistics/dependencies.c
+++ b/src/backend/statistics/dependencies.c
@@ -13,18 +13,27 @@
  */
 #include "postgres.h"
 
+#include "access/attnum.h"
 #include "access/htup_details.h"
 #include "catalog/pg_statistic_ext.h"
 #include "catalog/pg_statistic_ext_data.h"
+#include "common/int.h"
+#include "common/jsonapi.h"
+#include "fmgr.h"
 #include "lib/stringinfo.h"
+#include "mb/pg_wchar.h"
+#include "nodes/miscnodes.h"
 #include "nodes/nodeFuncs.h"
 #include "nodes/nodes.h"
 #include "nodes/pathnodes.h"
+#include "nodes/pg_list.h"
 #include "optimizer/clauses.h"
 #include "optimizer/optimizer.h"
 #include "parser/parsetree.h"
 #include "statistics/extended_stats_internal.h"
 #include "statistics/statistics.h"
+#include "utils/builtins.h"
+#include "utils/float.h"
 #include "utils/fmgroids.h"
 #include "utils/fmgrprotos.h"
 #include "utils/lsyscache.h"
@@ -643,24 +652,343 @@ statext_dependencies_load(Oid mvoid, bool inh)
 	return result;
 }
 
+typedef struct
+{
+	const char *str;
+	bool		found_only_object;
+	List	   *dependency_list;
+	Node	   *escontext;
+
+	MVDependency *current_dependency;
+}			dependenciesParseState;
+
+/*
+ * Invoked at the start of each object in the JSON document.
+ * The entire JSON document should be one object with no sub-objects.
+ *
+ * If we're anywhere else in the document, it's an error.
+ */
+static JsonParseErrorType
+dependencies_object_start(void *state)
+{
+	dependenciesParseState *parse = state;
+
+	if (parse->found_only_object == true)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				 errdetail("Must begin with \"{\"")));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	parse->found_only_object = true;
+	return JSON_SUCCESS;
+}
+
+/*
+ * dependencies input format does not have arrays, so any array elements encountered
+ * are an error.
+ */
+static JsonParseErrorType
+dependencies_array_start(void *state)
+{
+	dependenciesParseState *parse = state;
+
+	ereturn(parse->escontext, (Datum) 0,
+			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+			 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+			 errdetail("All dependencies count values are scalar doubles.")));
+	return JSON_SEM_ACTION_FAILED;
+}
+
+static int
+attnum_compare(const void *aptr, const void *bptr)
+{
+	AttrNumber	a = *(const AttrNumber *) aptr;
+	AttrNumber	b = *(const AttrNumber *) bptr;
+
+	return pg_cmp_s16(a,b);
+}
+
+/*
+ * The object keys are themselves comma-separated lists of attnums
+ * with negative attnums representing one of the expressions defined
+ * in the extened statistics object, followed by a => and a final attnum.
+ *
+ * example: "-1, 2 => -1"
+ */
+static JsonParseErrorType
+dependencies_object_field_start(void *state, char *fname, bool isnull)
+{
+	dependenciesParseState *parse = state;
+	char	   *token;
+	char	   *saveptr;
+	const char *delim = ", ";
+	const char *arrow_delim = " => ";
+	char	   *scratch;
+	char	   *arrow_p;
+	char	   *after_arrow_p;
+	List	   *attnum_list = NIL;
+	int			natts = 0;
+	AttrNumber	final_attnum;
+	MVDependency *dep;
+	AttrNumber *attrsort;
+
+	if (isnull || fname == NULL)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				 errdetail("All dependencies attnum lists must be a comma separated list of attnums with a final => attnum.")));
+
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	scratch = pstrdup(fname);
+
+	/* The subtring ' => ' must occur exactly once */
+	arrow_p = strstr(scratch, arrow_delim);
+	if (arrow_p == NULL)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				 errdetail("All dependencies attnum lists must be a comma separated list of attnums with a final => attnum.")));
+
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	/*
+	 * Everything to the left of the arrow is the attribute list, so split
+	 * that off into its own string.
+	 *
+	 * Everything to the right should be the lone target attribute.
+	 */
+	*arrow_p = '\0';
+
+	/* look for the character immediately beyond the delimiter we just found */
+	after_arrow_p = arrow_p + strlen(arrow_delim);
+
+	/* We should not find another arrow delim */
+	if (strstr(after_arrow_p, arrow_delim) != NULL)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				 errdetail("All dependencies attnum lists must be a comma separated list of attnums with a final => attnum.")));
+
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	/* what is left should be exactly one attnum */
+	final_attnum = pg_strtoint16_safe(after_arrow_p, parse->escontext);
+
+	if (SOFT_ERROR_OCCURRED(parse->escontext))
+		return JSON_SEM_ACTION_FAILED;
+
+	/* Left of the arrow is just regular attnums */
+	token = strtok_r(scratch, delim, &saveptr);
+
+	while (token != NULL)
+	{
+		attnum_list = lappend(attnum_list, (void *) token);
+
+		token = strtok_r(NULL, delim, &saveptr);
+	}
+	natts = attnum_list->length;
+
+	/*
+	 * We need at least 2 attnums left of the arrow for a dependencies item,
+	 * anything less is malformed.
+	 */
+	if (natts < 1)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				 errdetail("All dependencies attnum lists must be a comma separated list of attnums.")));
+
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	/*
+	 * Allocate enough space for the dependency, the attnums in the list, plus
+	 * the final attnum
+	 */
+	dep = palloc0(offsetof(MVDependency, attributes) + ((natts + 1) * sizeof(AttrNumber)));
+	dep->nattributes = natts + 1;
+	dep->attributes[natts] = final_attnum;
+
+	attrsort = palloc0(dep->nattributes * sizeof(AttrNumber));
+	attrsort[natts] = final_attnum;
+
+	for (int i = 0; i < natts; i++)
+	{
+		char	   *s = (char *) attnum_list->elements[i].ptr_value;
+
+		attrsort[i] = pg_strtoint16_safe(s, parse->escontext);
+		dep->attributes[i] = attrsort[i];
+
+		if (SOFT_ERROR_OCCURRED(parse->escontext))
+			return JSON_SEM_ACTION_FAILED;
+	}
+
+	list_free(attnum_list);
+	pfree(scratch);
+
+	qsort(attrsort,dep->nattributes,sizeof(AttrNumber),attnum_compare);
+	for (int i = 1; i < dep->nattributes; i++)
+		if (attrsort[i] == attrsort[i-1])
+		{
+			ereturn(parse->escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("attnum list duplicate value found: %d", attrsort[i])));
+
+			return JSON_SEM_ACTION_FAILED;
+		}
+
+	pfree(attrsort);
+
+	/* add dependencies-less MVdependenciesItem to the list */
+	parse->current_dependency = dep;
+	parse->dependency_list = lappend(parse->dependency_list, (void *) dep);
+	return JSON_SUCCESS;
+}
+
+/*
+ * ndsitinct input format does not have arrays, so any array elements encountered
+ * are an error.
+ */
+static JsonParseErrorType
+dependencies_array_element_start(void *state, bool isnull)
+{
+	dependenciesParseState *parse = state;
+
+	ereturn(parse->escontext, (Datum) 0,
+			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+			 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+			 errdetail("Cannot contain array elements.")));
+
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ * Handle scalar events from the dependencies input parser.
+ *
+ * There is only one case where we will encounter a scalar, and that is the
+ * dependency degree for the previous object key.
+ */
+static JsonParseErrorType
+dependencies_scalar(void *state, char *token, JsonTokenType tokentype)
+{
+	dependenciesParseState *parse = state;
+
+	/* if the entire json is just one scalar, that's wrong */
+	if (parse->found_only_object != true)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				 errdetail("Must begin with \"{\"")));
+
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	Assert(parse->current_dependency != NULL);
+
+	parse->current_dependency->degree = float8in_internal(token, NULL, "double",
+														  token, parse->escontext);
+
+	if (SOFT_ERROR_OCCURRED(parse->escontext))
+		return JSON_SEM_ACTION_FAILED;
+
+	/* mark us done with this dependency */
+	parse->current_dependency = NULL;
+	return JSON_SUCCESS;
+}
+
 /*
  * pg_dependencies_in		- input routine for type pg_dependencies.
  *
- * pg_dependencies is real enough to be a table column, but it has no operations
- * of its own, and disallows input too
+ * example input:
+ *     {"-2 => 6": 0.292508,
+ *      "-2 => -1": 0.113999,
+ *      "6, -2 => -1": 0.348479,
+ *      "-1, -2 => 6": 0.839691}
+ *
+ * This import format is clearly a specific subset of JSON, therefore it makes
+ * sense to leverage those parsing utilities, and further validate it from there.
  */
 Datum
 pg_dependencies_in(PG_FUNCTION_ARGS)
 {
-	/*
-	 * pg_node_list stores the data in binary form and parsing text input is
-	 * not needed, so disallow this.
-	 */
-	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot accept a value of type %s", "pg_dependencies")));
+	char	   *str = PG_GETARG_CSTRING(0);
 
-	PG_RETURN_VOID();			/* keep compiler quiet */
+	dependenciesParseState parse_state;
+	JsonParseErrorType result;
+	JsonLexContext *lex;
+	JsonSemAction sem_action;
+
+	/* initialize the semantic state */
+	parse_state.str = str;
+	parse_state.found_only_object = false;
+	parse_state.dependency_list = NIL;
+	parse_state.escontext = fcinfo->context;
+	parse_state.current_dependency = NULL;
+
+	/* set callbacks */
+	sem_action.semstate = (void *) &parse_state;
+	sem_action.object_start = dependencies_object_start;
+	sem_action.object_end = NULL;
+	sem_action.array_start = dependencies_array_start;
+	sem_action.array_end = NULL;
+	sem_action.array_element_start = dependencies_array_element_start;
+	sem_action.array_element_end = NULL;
+	sem_action.object_field_start = dependencies_object_field_start;
+	sem_action.object_field_end = NULL;
+	sem_action.scalar = dependencies_scalar;
+
+	lex = makeJsonLexContextCstringLen(NULL, str, strlen(str), PG_UTF8, true);
+
+	result = pg_parse_json(lex, &sem_action);
+	freeJsonLexContext(lex);
+
+	if (result == JSON_SUCCESS)
+	{
+		List	   *list = parse_state.dependency_list;
+		int			ndeps = list->length;
+		MVDependencies *mvdeps;
+		bytea	   *bytes;
+
+		mvdeps = palloc0(offsetof(MVDependencies, deps) + ndeps * sizeof(MVDependency));
+		mvdeps->magic = STATS_DEPS_MAGIC;
+		mvdeps->type = STATS_DEPS_TYPE_BASIC;
+		mvdeps->ndeps = ndeps;
+
+		/* copy MVDependency structs out of the list into the MVDependencies */
+		for (int i = 0; i < ndeps; i++)
+			mvdeps->deps[i] = list->elements[i].ptr_value;
+		bytes = statext_dependencies_serialize(mvdeps);
+
+		list_free(list);
+		for (int i = 0; i < ndeps; i++)
+			pfree(mvdeps->deps[i]);
+		pfree(mvdeps);
+
+		PG_RETURN_BYTEA_P(bytes);
+	}
+	else if (result == JSON_SEM_ACTION_FAILED)
+		PG_RETURN_NULL();
+
+	/* Anything else is a generic JSON parse error */
+	ereturn(parse_state.escontext, (Datum) 0,
+			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+			 errmsg("malformed pg_dependencies: \"%s\"", str),
+			 errdetail("Must be valid JSON.")));
+
+	PG_RETURN_NULL();			/* keep compiler quiet */
 }
 
 /*
diff --git a/src/test/regress/expected/stats_ext.out b/src/test/regress/expected/stats_ext.out
index 6f3da85101..068feb7f54 100644
--- a/src/test/regress/expected/stats_ext.out
+++ b/src/test/regress/expected/stats_ext.out
@@ -3342,6 +3342,24 @@ SELECT '{"6, -1": 14, "6, -2": 9143, "-1, -2": 13454, "6, -1, -2": 14549}'::pg_n
  {"6, -1": 14, "6, -2": 9143, "-1, -2": 13454, "6, -1, -2": 14549}
 (1 row)
 
+-- can't have duplicates attnums in list
+SELECT '{"6, -1": 14, "6, -2": 9143, "-1, -1": 13454, "6, -1, -2": 14549}'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "{"6, -1": 14, "6, -2": 9143, "-1, -1": 13454, "6, -1, -2": 14549}"
+LINE 1: SELECT '{"6, -1": 14, "6, -2": 9143, "-1, -1": 13454, "6, -1...
+               ^
+DETAIL:  attnum list duplicate value found: -1
+SELECT '{"-2 => 6": 0.292508, "-2 => -1": 0.113999, "6, -2 => -1": 0.348479, "-1, -2 => 6": 0.839691}'::pg_dependencies;
+                                        pg_dependencies                                        
+-----------------------------------------------------------------------------------------------
+ {"-2 => 6": 0.292508, "-2 => -1": 0.113999, "6, -2 => -1": 0.348479, "-1, -2 => 6": 0.839691}
+(1 row)
+
+-- can't have duplicates attnums in list
+SELECT '{"6 => 6": 0.292508, "-2 => -1": 0.113999, "6, -2 => -1": 0.348479, "-1, -2 => 6": 0.839691}'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "{"6 => 6": 0.292508, "-2 => -1": 0.113999, "6, -2 => -1": 0.348479, "-1, -2 => 6": 0.839691}"
+LINE 1: SELECT '{"6 => 6": 0.292508, "-2 => -1": 0.113999, "6, -2 =>...
+               ^
+DETAIL:  attnum list duplicate value found: 6
 -- Tidy up
 DROP OPERATOR <<< (int, int);
 DROP FUNCTION op_leak(int, int);
diff --git a/src/test/regress/sql/stats_ext.sql b/src/test/regress/sql/stats_ext.sql
index a53564bed5..9070a994a7 100644
--- a/src/test/regress/sql/stats_ext.sql
+++ b/src/test/regress/sql/stats_ext.sql
@@ -1688,6 +1688,12 @@ SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x
 
 -- new input functions
 SELECT '{"6, -1": 14, "6, -2": 9143, "-1, -2": 13454, "6, -1, -2": 14549}'::pg_ndistinct;
+-- can't have duplicates attnums in list
+SELECT '{"6, -1": 14, "6, -2": 9143, "-1, -1": 13454, "6, -1, -2": 14549}'::pg_ndistinct;
+
+SELECT '{"-2 => 6": 0.292508, "-2 => -1": 0.113999, "6, -2 => -1": 0.348479, "-1, -2 => 6": 0.839691}'::pg_dependencies;
+-- can't have duplicates attnums in list
+SELECT '{"6 => 6": 0.292508, "-2 => -1": 0.113999, "6, -2 => -1": 0.348479, "-1, -2 => 6": 0.839691}'::pg_dependencies;
 
 -- Tidy up
 DROP OPERATOR <<< (int, int);
-- 
2.48.1

From b639f501204e2be3de856b42e7f0096eb4fb385c Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Thu, 26 Dec 2024 05:02:06 -0500
Subject: [PATCH v2 3/6] Expose attribute statistics functions for use in
 extended_stats.

Many of the operations of attribute stats have analogous operations in
extended stats.

* get_attr_stat_type()
* init_empty_stats_tuple()
* text_to_stavalues()
* get_elem_stat_type()
---
 src/include/statistics/statistics.h      | 17 +++++++++++++++++
 src/backend/statistics/attribute_stats.c | 24 +++++-------------------
 2 files changed, 22 insertions(+), 19 deletions(-)

diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 7dd0f97554..f47f192dff 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -127,4 +127,21 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
 												int nclauses);
 extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
 
+extern void get_attr_stat_type(Oid reloid, AttrNumber attnum, int elevel,
+							   Oid *atttypid, int32 *atttypmod,
+							   char *atttyptype, Oid *atttypcoll,
+							   Oid *eq_opr, Oid *lt_opr);
+extern void init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
+								   Datum *values, bool *nulls, bool *replaces);
+
+extern void set_stats_slot(Datum *values, bool *nulls, bool *replaces,
+						   int16 stakind, Oid staop, Oid stacoll,
+						   Datum stanumbers, bool stanumbers_isnull,
+						   Datum stavalues, bool stavalues_isnull);
+
+extern Datum text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d,
+							   Oid typid, int32 typmod, int elevel, bool *ok);
+extern bool get_elem_stat_type(Oid atttypid, char atttyptype, int elevel,
+							   Oid *elemtypid, Oid *elem_eq_opr);
+
 #endif							/* STATISTICS_H */
diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index 94f7dd63a0..f617165386 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -78,23 +78,9 @@ static struct StatsArgInfo attarginfo[] =
 
 static bool attribute_statistics_update(FunctionCallInfo fcinfo, int elevel);
 static Node *get_attr_expr(Relation rel, int attnum);
-static void get_attr_stat_type(Oid reloid, AttrNumber attnum, int elevel,
-							   Oid *atttypid, int32 *atttypmod,
-							   char *atttyptype, Oid *atttypcoll,
-							   Oid *eq_opr, Oid *lt_opr);
-static bool get_elem_stat_type(Oid atttypid, char atttyptype, int elevel,
-							   Oid *elemtypid, Oid *elem_eq_opr);
-static Datum text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d,
-							   Oid typid, int32 typmod, int elevel, bool *ok);
-static void set_stats_slot(Datum *values, bool *nulls, bool *replaces,
-						   int16 stakind, Oid staop, Oid stacoll,
-						   Datum stanumbers, bool stanumbers_isnull,
-						   Datum stavalues, bool stavalues_isnull);
 static void upsert_pg_statistic(Relation starel, HeapTuple oldtup,
 								Datum *values, bool *nulls, bool *replaces);
 static bool delete_pg_statistic(Oid reloid, AttrNumber attnum, bool stainherit);
-static void init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
-								   Datum *values, bool *nulls, bool *replaces);
 
 /*
  * Insert or Update Attribute Statistics
@@ -502,7 +488,7 @@ get_attr_expr(Relation rel, int attnum)
 /*
  * Derive type information from the attribute.
  */
-static void
+void
 get_attr_stat_type(Oid reloid, AttrNumber attnum, int elevel,
 				   Oid *atttypid, int32 *atttypmod,
 				   char *atttyptype, Oid *atttypcoll,
@@ -584,7 +570,7 @@ get_attr_stat_type(Oid reloid, AttrNumber attnum, int elevel,
 /*
  * Derive element type information from the attribute type.
  */
-static bool
+bool
 get_elem_stat_type(Oid atttypid, char atttyptype, int elevel,
 				   Oid *elemtypid, Oid *elem_eq_opr)
 {
@@ -624,7 +610,7 @@ get_elem_stat_type(Oid atttypid, char atttyptype, int elevel,
  * to false. If the resulting array contains NULLs, raise an error at elevel
  * and set ok to false. Otherwise, set ok to true.
  */
-static Datum
+Datum
 text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d, Oid typid,
 				  int32 typmod, int elevel, bool *ok)
 {
@@ -678,7 +664,7 @@ text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d, Oid typid,
  * Find and update the slot with the given stakind, or use the first empty
  * slot.
  */
-static void
+void
 set_stats_slot(Datum *values, bool *nulls, bool *replaces,
 			   int16 stakind, Oid staop, Oid stacoll,
 			   Datum stanumbers, bool stanumbers_isnull,
@@ -802,7 +788,7 @@ delete_pg_statistic(Oid reloid, AttrNumber attnum, bool stainherit)
 /*
  * Initialize values and nulls for a new stats tuple.
  */
-static void
+void
 init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
 					   Datum *values, bool *nulls, bool *replaces)
 {
-- 
2.48.1

From ad50749318d5eb989b222862d15bb9c7031ef00a Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Thu, 23 Jan 2025 23:59:08 -0500
Subject: [PATCH v2 6/6] Add pg_ndistinct attnum checking to extended stats
 import.

---
 src/backend/statistics/extended_stats.c    | 92 +++++++++++++---------
 src/test/regress/expected/stats_import.out | 27 +++++++
 src/test/regress/sql/stats_import.sql      | 28 +++++++
 3 files changed, 108 insertions(+), 39 deletions(-)

diff --git a/src/backend/statistics/extended_stats.c b/src/backend/statistics/extended_stats.c
index b54b3aa533..64c3a7001c 100644
--- a/src/backend/statistics/extended_stats.c
+++ b/src/backend/statistics/extended_stats.c
@@ -2869,6 +2869,9 @@ extended_statistics_update(FunctionCallInfo fcinfo, int elevel)
 
 	bool		success = true;
 
+	Datum		exprdatum;
+	bool		isnull;
+	List	   *exprs = NIL;
 	int			numattnums = 0;
 	int			numexprs = 0;
 	int			numattrs = 0;
@@ -2920,6 +2923,37 @@ extended_statistics_update(FunctionCallInfo fcinfo, int elevel)
 
 	stxform = (Form_pg_statistic_ext) GETSTRUCT(tup);
 	expand_stxkind(tup, &enabled);
+	numattnums = stxform->stxkeys.dim1;
+
+	/* decode expression (if any) */
+	exprdatum = SysCacheGetAttr(STATEXTOID,
+								tup,
+								Anum_pg_statistic_ext_stxexprs,
+								&isnull);
+
+	if (!isnull)
+	{
+		char	   *s;
+
+		s = TextDatumGetCString(exprdatum);
+		exprs = (List *) stringToNode(s);
+		pfree(s);
+
+		/*
+			* Run the expressions through eval_const_expressions. This is not
+			* just an optimization, but is necessary, because the planner
+			* will be comparing them to similarly-processed qual clauses, and
+			* may fail to detect valid matches without this.  We must not use
+			* canonicalize_qual, however, since these aren't qual
+			* expressions.
+			*/
+		exprs = (List *) eval_const_expressions(NULL, (Node *) exprs);
+
+		/* May as well fix opfuncids too */
+		fix_opfuncids((Node *) exprs);
+	}
+	numexprs = list_length(exprs);
+	numattrs = numattnums + numexprs;
 
 	/* lock table */
 	stats_lock_check_privileges(stxform->stxrelid);
@@ -3004,42 +3038,6 @@ extended_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	 */
 	if (has.mcv || has.expressions)
 	{
-		Datum		exprdatum;
-		bool		isnull;
-		List	   *exprs = NIL;
-
-		/* decode expression (if any) */
-		exprdatum = SysCacheGetAttr(STATEXTOID,
-									tup,
-									Anum_pg_statistic_ext_stxexprs,
-									&isnull);
-
-		if (!isnull)
-		{
-			char	   *s;
-
-			s = TextDatumGetCString(exprdatum);
-			exprs = (List *) stringToNode(s);
-			pfree(s);
-
-			/*
-			 * Run the expressions through eval_const_expressions. This is not
-			 * just an optimization, but is necessary, because the planner
-			 * will be comparing them to similarly-processed qual clauses, and
-			 * may fail to detect valid matches without this.  We must not use
-			 * canonicalize_qual, however, since these aren't qual
-			 * expressions.
-			 */
-			exprs = (List *) eval_const_expressions(NULL, (Node *) exprs);
-
-			/* May as well fix opfuncids too */
-			fix_opfuncids((Node *) exprs);
-		}
-
-		numattnums = stxform->stxkeys.dim1;
-		numexprs = list_length(exprs);
-		numattrs = numattnums + numexprs;
-
 		atttypids = palloc0(numattrs * sizeof(Oid));
 		atttypmods = palloc0(numattrs * sizeof(int32));
 		atttypcolls = palloc0(numattrs * sizeof(Oid));
@@ -3102,15 +3100,31 @@ extended_statistics_update(FunctionCallInfo fcinfo, int elevel)
 
 	if (has.ndistinct)
 	{
-		values[Anum_pg_statistic_ext_data_stxdndistinct - 1] = PG_GETARG_DATUM(NDISTINCT_ARG);
-		replaces[Anum_pg_statistic_ext_data_stxdndistinct - 1] = true;
+		Datum			ndistinct_datum = PG_GETARG_DATUM(NDISTINCT_ARG);
+		bytea		   *data = DatumGetByteaPP(ndistinct_datum);
+		MVNDistinct	   *ndistinct = statext_ndistinct_deserialize(data);
+
+		if (pg_ndistinct_validate_items(ndistinct, &stxform->stxkeys, numexprs, elevel))
+		{
+			values[Anum_pg_statistic_ext_data_stxdndistinct - 1] = ndistinct_datum;
+			replaces[Anum_pg_statistic_ext_data_stxdndistinct - 1] = true;
+		}
+		else
+		{
+			nulls[Anum_pg_statistic_ext_data_stxdndistinct - 1] = true;
+			success = false;
+		}
+
+		/* TODO: free ndist */
 	}
 	else
 		nulls[Anum_pg_statistic_ext_data_stxdndistinct - 1] = true;
 
 	if (has.dependencies)
 	{
-		values[Anum_pg_statistic_ext_data_stxddependencies - 1] = PG_GETARG_DATUM(DEPENDENCIES_ARG);
+		Datum	dependencies_datum = PG_GETARG_DATUM(DEPENDENCIES_ARG);
+
+		values[Anum_pg_statistic_ext_data_stxddependencies - 1] = dependencies_datum;
 		replaces[Anum_pg_statistic_ext_data_stxddependencies - 1] = true;
 	}
 	else
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index 9f3b45fc55..347c86a6c1 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -1805,6 +1805,33 @@ WHERE s.starelid = 'stats_import.is_odd'::regclass;
 ---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
 (0 rows)
 
+-- set n_distinct using at attnum (1) that is not in the statistics object
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        n_distinct => '{"2, 3": 4, "2, -1": 4, "2, -2": 4, "3, -1": 4, "3, -2": 4, "-1, -2": 3, "2, 3, -1": 4, "2, 3, -2": 4, "2, -1, -2": 4, "3, -1, -2": 4, "1, 3, -1, -2": 4}'::pg_ndistinct
+      );
+ERROR:  pg_ndistinct: invalid attnum for this statistics object: 1
+-- set n_distinct using at attnum that is 0
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        n_distinct => '{"2, 3": 4, "2, -1": 4, "2, 0": 4, "3, -1": 4, "3, -2": 4, "-1, -2": 3, "2, 3, -1": 4, "2, 3, -2": 4, "2, -1, -2": 4, "3, -1, -2": 4, "2, 3, -1, -2": 4}'::pg_ndistinct
+      );
+ERROR:  pg_ndistinct: invalid attnum for this statistics object: 0
+-- set n_distinct using at attnum that is outside the expression bounds(below -2)
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        n_distinct => '{"2, 3": 4, "2, -4": 4, "2, -2": 4, "3, -1": 4, "3, -2": 4, "-1, -2": 3, "2, 3, -1": 4, "2, 3, -2": 4, "2, -1, -2": 4, "3, -1, -2": 4, "1, 3, -1, -2": 4}'::pg_ndistinct
+      );
+ERROR:  pg_ndistinct: invalid attnum for this statistics object: -4
 SELECT
     pg_catalog.pg_set_extended_stats(
         statistics_schemaname => 'stats_import'::regnamespace,
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index 98aa934d12..3a7baf9c92 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -1387,6 +1387,34 @@ FROM pg_statistic s
 JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
 WHERE s.starelid = 'stats_import.is_odd'::regclass;
 
+
+-- set n_distinct using at attnum (1) that is not in the statistics object
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        n_distinct => '{"2, 3": 4, "2, -1": 4, "2, -2": 4, "3, -1": 4, "3, -2": 4, "-1, -2": 3, "2, 3, -1": 4, "2, 3, -2": 4, "2, -1, -2": 4, "3, -1, -2": 4, "1, 3, -1, -2": 4}'::pg_ndistinct
+      );
+
+-- set n_distinct using at attnum that is 0
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        n_distinct => '{"2, 3": 4, "2, -1": 4, "2, 0": 4, "3, -1": 4, "3, -2": 4, "-1, -2": 3, "2, 3, -1": 4, "2, 3, -2": 4, "2, -1, -2": 4, "3, -1, -2": 4, "2, 3, -1, -2": 4}'::pg_ndistinct
+      );
+
+-- set n_distinct using at attnum that is outside the expression bounds(below -2)
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        n_distinct => '{"2, 3": 4, "2, -4": 4, "2, -2": 4, "3, -1": 4, "3, -2": 4, "-1, -2": 3, "2, 3, -1": 4, "2, 3, -2": 4, "2, -1, -2": 4, "3, -1, -2": 4, "1, 3, -1, -2": 4}'::pg_ndistinct
+      );
+
 SELECT
     pg_catalog.pg_set_extended_stats(
         statistics_schemaname => 'stats_import'::regnamespace,
-- 
2.48.1

---

From: Corey Huinker <[email-masked]>
Date: Mon, 27 Jan 2025 14:52:15 -0500

> separate for this patchset to isolate the attnum-checking code for this

These are mock-ups of the to/from JSON functions, but building from/to text
rather than the not-yet-committed pg_ndistinct and pg_dependencies data
types. Currently they're done with JSON rather than JSONB because I assume
that the ordering within the datatype matters. We can probably preserve
order by adding an "order" field populated by WITH ORDINALITY.

To get all Jurrassic about this: I've spent some time thinking about
whether we CAN make these functions, it's time to consider whether we
SHOULD. And that leads me to a couple of points:

p1. We could switch to the new formats without any change to the internal
representation, but pg_dump would always need to know about the old formats.

p2. The JSON format is both more understandable and easier to manipulate.

p3. If we thought the number of people using extended stats was small, the
number of people tweaking extended stats is going to be smaller.

So that gives us a few paths forward:

o1. Switch to the new input/output format, and the queries inside these
functions get incorporated into some future pg_dump queries.

o2. Keep the old formats, create these functions inside the system and
whoever wants to use them can use them.

o3. Keep old formats, and make these functions work as the CASTs to and
from JSON/JSONB.

o4. Keep old formats, could create these functions in an extension.

o5. Keep old formats, leave these function definitions here for a future
intrepid hacker.

---

From: jian he <[email-masked]>
Date: Wed, 29 Jan 2025 00:24:59 +0800

hi.
I reviewed 0001 only.

in src/backend/statistics/mvdistinct.c

no need #include "nodes/pg_list.h" since
src/include/statistics/statistics.h sub level include "nodes/pg_list.h"

no need #include "utils/palloc.h"
sicne #include "postgres.h"
already included it.

 select '[{"6, -32768,,": -11}]'::pg_ndistinct;
ERROR:  malformed pg_ndistinct: "[{"6, -32768,,": -11}]"
LINE 1: select '[{"6, -32768,,": -11}]'::pg_ndistinct;
               ^
DETAIL:  All ndistinct count values are scalar doubles.
imho, this errdetail message is not good.

select '{}'::pg_ndistinct ;
segfault

select '{"1,":"1"}'::pg_ndistinct ;
ERROR:  malformed pg_ndistinct: "{"1,":"1"}"
LINE 1: select '{"1,":"1"}'::pg_ndistinct ;
               ^
DETAIL:  All ndistinct attnum lists must be a comma separated list of attnums.

imho, this errdetail message is not good. would be better saying that
"length of list of attnums must be larger than 1".

select  pt1.typnamespace, pt1.typarray, pt1.typcategory, pt1.typname
from    pg_type pt1
where pt1.typname ~*'distinct';

 typnamespace | typarray | typcategory |   typname
--------------+----------+-------------+--------------
           11 |        0 | Z           | pg_ndistinct

typcategory (Z) marked as Internal-use types. and there is no
pg_ndistinct array type,
not sure this is fine.

all the errcode one pair of the parenthesis is unnecessary.
for example
                (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
                 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
                 errdetail("Must begin with \"{\"")));
can change to
                errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
                 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
                 errdetail("Must begin with \"{\""));

see https://www.postgresql.org/docs/current/error-message-reporting.html

---

From: jian he <[email-masked]>
Date: Wed, 29 Jan 2025 15:50:09 +0800

hi.

select '{"1, 0B100101":"NaN"}'::pg_ndistinct;
      pg_ndistinct
------------------------
 {"1, 37": -2147483648}
(1 row)

this is not what we expected?
For the VALUE part of pg_ndistinct, float8 has 3 special values: inf, -inf, NaN.

For the key part of pg_ndistinct, see example.
select '{"1, 16\t":"1"}'::pg_ndistinct;
here \t is not tab character, ascii 9. it's two characters: backslash
and character "t".
so here it should error out?
(apply this to \n, \r, \b)

ending part should be:

    freeJsonLexContext(lex);
    if (result == JSON_SUCCESS)
    {
        ......
    }
    else
    {
       ereturn(parse_state.escontext, (Datum) 0,
                    errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
                    errmsg("malformed pg_ndistinct: \"%s\"", str),
                    errdetail("Must be valid JSON."));
       PG_RETURN_NULL();
    }
result should be either JSON_SUCCESS or anything else.

all these functions:
ndistinct_object_start, ndistinct_array_start,
ndistinct_object_field_start, ndistinct_array_element_start
have
ndistinctParseState *parse = state;

do we need to change it to
ndistinctParseState *parse = (ndistinctParseState *)state;
?

ndistinctParseState need to add to src/tools/pgindent/typedefs.list
probably change it to "typedef struct ndistinctParseState".
also struct ndistinctParseState need placed in the top of
src/backend/statistics/mvdistinct.c
not in line 340?

---

From: Corey Huinker <[email-masked]>
Date: Wed, 29 Jan 2025 14:56:33 -0500

> sicne #include "postgres.h"

Noted.

> imho, this errdetail message is not good.

What error message do you think is appropriate in that situation?

> select '{}'::pg_ndistinct ;

Mmm, gotta look into that!

> "length of list of attnums must be larger than 1".

That sounds better.

> typcategory (Z) marked as Internal-use types. and there is no

I think it's probably ok for now. The datatype currently has no utility
other than extended statistics, and I'm doubtful that it ever will.

---

From: Corey Huinker <[email-masked]>
Date: Wed, 29 Jan 2025 15:04:40 -0500

> select '{"1, 0B100101":"NaN"}'::pg_ndistinct;

I think my initial reaction is to just refuse those special values, but
I'll look into the parsing code to see what can be done.

> (apply this to \n, \r, \b)

I don't have a good answer as to what should happen here. Special cases
like this make Tomas' suggestion to change the in/out format more
attractive.

> ndistinctParseState *parse = (ndistinctParseState *)state;

The compiler isn't complaining so far, but I see no harm in it.

---

From: Corey Huinker <[email-masked]>
Date: Wed, 19 Feb 2025 23:43:16 -0500

> I'll look into the parsing code to see what can be done.

I noticed that the output function for pg_ndistinct casts that value to an
integer before formatting it %d, so it's being treated as an integer even
if it is not stored as one. After some consultation with Tomas, it made the
most sense to just replicate this on the input side as well, and that is
addressed in the patches below.

I've updated and rebased the patches.

The existing pg_ndistinct and pg_dependences formats were kept as-is. The
formats are clumsy, more processing-friendly formats would be easier, but
the need for such processing is minimal bordering on theoretical, so there
is little impact in keeping the historical format.

There are now checks to ensure that the pg_ndistinct or pg_dependencies
value assigned to an extended statistics object actually makes sense for
that object. What this amounts to is checking that for every attnum cited,
the positive attnums are also ones found the in the stxkeys of the
pg_statistic_ext tuple, and the negative attnums correspond do not exceed
the number of expressions in the attnum. In other words, if the stats
object has no expressions in it, then no negative numbers will be accepted,
if it has 2 expressions than any value -3 or lower will be rejected, etc.

All patches rebased to 71f17823ba010296da9946bd906bb8bcad6325bc.
From 60fe7c25693976676dc019ee3878d2d1a93f3c75 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 17 Dec 2024 03:30:55 -0500
Subject: [PATCH v3 1/4] Add working input function for pg_ndistinct.

This is needed to import extended statistics.
---
 .../statistics/extended_stats_internal.h      |   8 +
 src/backend/statistics/mvdistinct.c           | 359 +++++++++++++++++-
 src/test/regress/expected/stats_ext.out       |   7 +
 src/test/regress/sql/stats_ext.sql            |   3 +
 4 files changed, 371 insertions(+), 6 deletions(-)

diff --git a/src/include/statistics/extended_stats_internal.h b/src/include/statistics/extended_stats_internal.h
index efcb7dc3546..396915a8a97 100644
--- a/src/include/statistics/extended_stats_internal.h
+++ b/src/include/statistics/extended_stats_internal.h
@@ -127,4 +127,12 @@ extern Selectivity mcv_clause_selectivity_or(PlannerInfo *root,
 											 Selectivity *overlap_basesel,
 											 Selectivity *totalsel);
 
index 7e7a63405c8..e9c02aaa63e 100644
 
@@ -328,23 +337,361 @@ statext_ndistinct_deserialize(bytea *data)
 	return ndistinct;
 }
 
+typedef struct
+{
+	const char *str;

+/*
index 904d3e623f5..20333667e5f 100644
@@ -3358,6 +3358,13 @@ SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x
  s_expr          | {1}
 (2 rows)
 
+-- new input functions
index 88b33ccaef8..3539d7b5cd2 100644
@@ -1700,6 +1700,9 @@ SELECT statistics_name, most_common_vals FROM pg_stats_ext x
 
+-- new input functions

base-commit: 71f17823ba010296da9946bd906bb8bcad6325bc
-- 
2.48.1

From 59e3ed42dd528a1901ae42909398c6c6dd83e578 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 17 Dec 2024 19:47:43 -0500
Subject: [PATCH v3 2/4] Add working input function for pg_dependencies.

diff --git a/src/backend/statistics/dependencies.c b/src/backend/statistics/dependencies.c
index eb2fc4366b4..ec26a2427e2 100644
--- a/src/backend/statistics/dependencies.c
+++ b/src/backend/statistics/dependencies.c
@@ -13,18 +13,27 @@
  */
 #include "postgres.h"

+typedef struct
+{
+	const char *str;

 /*
diff --git a/src/test/regress/expected/stats_ext.out b/src/test/regress/expected/stats_ext.out
index 20333667e5f..489e6a19771 100644
--- a/src/test/regress/expected/stats_ext.out
+++ b/src/test/regress/expected/stats_ext.out
@@ -3365,6 +3365,24 @@ SELECT '{"6, -1": 14, "6, -2": 9143, "-1, -2": 13454, "6, -1, -2": 14549}'::pg_n
  {"6, -1": 14, "6, -2": 9143, "-1, -2": 13454, "6, -1, -2": 14549}
 (1 row)
 
index 3539d7b5cd2..da5c11aedd7 100644
@@ -1702,6 +1702,12 @@ SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x
 
 -- new input functions
 
 -- Tidy up

From bbafc4301447e5c402d3b4f2753501bb061e9222 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Thu, 26 Dec 2024 05:02:06 -0500
Subject: [PATCH v3 3/4] Expose attribute statistics functions for use in
 extended_stats.

* get_attr_stat_type()

index 7dd0f975545..f47f192dff0 100644
 
index 94f7dd63a03..f6171653864 100644

 /*

From 073f523605f9927ff210970c0cbe294a3307a2c3 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Fri, 3 Jan 2025 13:43:29 -0500
Subject: [PATCH v3 4/4] Add extended statistics support functions.

Add pg_set_extended_stats(), pg_clear_extended_stats(), and
pg_restore_extended_stats(). These function closely mirror their
relation and attribute counterparts, but for extended statistics (i.e.
CREATE STATISTICS) objects.
---
 src/include/catalog/pg_proc.dat               |   23 +
 .../statistics/extended_stats_internal.h      |    8 +
 src/backend/catalog/system_functions.sql      |   17 +
 src/backend/statistics/dependencies.c         |   65 +
 src/backend/statistics/extended_stats.c       | 1099 +++++++++++++++++
 src/backend/statistics/mcv.c                  |  144 +++
 src/backend/statistics/mvdistinct.c           |   14 +-
 src/test/regress/expected/stats_ext.out       |   13 +
 src/test/regress/expected/stats_import.out    |  449 ++++++-
 src/test/regress/sql/stats_ext.sql            |    4 +
 src/test/regress/sql/stats_import.sql         |  341 ++++-
 doc/src/sgml/func.sgml                        |  124 ++
 12 files changed, 2190 insertions(+), 111 deletions(-)

index 9e803d610d7..416d8205b84 100644
@@ -12463,5 +12463,28 @@
 
 ]
index 396915a8a97..68862bb1304 100644
@@ -126,6 +126,10 @@ extern Selectivity mcv_clause_selectivity_or(PlannerInfo *root,
 
 extern Datum import_mcvlist(HeapTuple tup, int elevel, int numattrs,
 							Oid *atttypids, int32 *atttypmods, Oid *atttypcolls,
@@ -134,5 +138,9 @@ extern Datum import_mcvlist(HeapTuple tup, int elevel, int numattrs,
 extern bool pg_ndistinct_validate_items(MVNDistinct *ndistinct, int2vector *stxkeys,
 										int numexprs, int elevel);
 extern void free_pg_ndistinct(MVNDistinct *ndistinct);
+extern bool pg_dependencies_validate_deps(MVDependencies *dependencies,
+										  int2vector *stxkeys, int numexprs,
+										  int elevel);
+extern void free_pg_dependencies(MVDependencies *dependencies);
 
index 591157b1d1b..82901bd8e10 100644
 
index ec26a2427e2..db424d48d24 100644
@@ -337,6 +337,10 @@ dependency_degree(StatsBuildData *data, int k, AttrNumber *dependency)
 	return (n_supporting_rows * 1.0 / data->numrows);
 }
 
+
+void
+free_pg_dependencies(MVDependencies *dependencies);
+
 /*
  * detects functional dependencies between groups of columns
  *
@@ -909,6 +913,55 @@ dependencies_scalar(void *state, char *token, JsonTokenType tokentype)
 	return JSON_SUCCESS;
 }
 
+/*
+ * Validate an MVDependencies against the extended statistics object definition.
+ *
+ * Every MVDependencies must be checked to ensure that the attnums in the
+ * attributes list correspond to attnums/expressions defined by the
+ * extended statistics object.
+ *
+ * Positive attnums are attributes which must be found in the stxkeys,
+ * while negative attnums correspond to an expr number, so the attnum
+ * can't be below (0 - numexprs).
+ */
+bool
+pg_dependencies_validate_deps(MVDependencies *dependencies, int2vector *stxkeys, int numexprs, int elevel)
+{
+	int		attnum_expr_lowbound = 0 - numexprs;
+
+	for (int i = 0; i < dependencies->ndeps; i++)
+	{
+		MVDependency *dep = dependencies->deps[i];
+
+		for (int j = 0; j < dep->nattributes; j++)
+		{
+			AttrNumber	attnum = dep->attributes[j];
+			bool		ok = false;
+
+			if (attnum > 0)
+			{
+				for (int k = 0; k < stxkeys->dim1; k++)
+					if (attnum == stxkeys->values[k])
+					{
+						ok = true;
+						break;
+					}
+			}
+			else if ((attnum < 0) && (attnum >= attnum_expr_lowbound))
+				ok = true;
+
+			if (!ok)
+			{
+				ereport(elevel,
+						(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+						errmsg("pg_dependencies: invalid attnum for this statistics object: %d", attnum)));
+				return false;
+			}
+		}
+	}
+	return true;
+}
+
 /*
  * pg_dependencies_in		- input routine for type pg_dependencies.
  *
@@ -991,6 +1044,18 @@ pg_dependencies_in(PG_FUNCTION_ARGS)
 	PG_RETURN_NULL();			/* keep compiler quiet */
 }
 
+/*
+ * Free allocations of an MVNDistinct
+ */
+void
+free_pg_dependencies(MVDependencies *dependencies)
+{
+	for (int i = 0; i < dependencies->ndeps; i++)
+		pfree(dependencies->deps[i]);
+
+	pfree(dependencies);
+}
+
 /*
  * pg_dependencies		- output routine for type pg_dependencies.
  */
diff --git a/src/backend/statistics/extended_stats.c b/src/backend/statistics/extended_stats.c
index a8b63ec0884..4c01346aaa4 100644
--- a/src/backend/statistics/extended_stats.c
+++ b/src/backend/statistics/extended_stats.c
@@ -18,11 +18,15 @@
 
@@ -33,6 +37,7 @@
@@ -72,6 +77,71 @@ typedef struct StatExtEntry

@@ -99,6 +169,28 @@ static StatsBuildData *make_build_data(Relation rel, StatExtEntry *stat,

 /*
  * Compute requested extended stats, using the rows sampled for the plain
@@ -2631,3 +2723,1010 @@ make_build_data(Relation rel, StatExtEntry *stat, int numrows, HeapTuple *rows,
 
 	return result;
 }
+
+static HeapTuple
+		free_pg_ndistinct(ndistinct);
+		bytea		   *data = DatumGetByteaPP(dependencies_datum);
+		MVDependencies *dependencies = statext_dependencies_deserialize(data);
+		if (pg_dependencies_validate_deps(dependencies, &stxform->stxkeys, numexprs, elevel))
+		free_pg_dependencies(dependencies);
index d98cda698d9..73f78e06078 100644
 
 	return s;
 }
+
+/*
index e9c02aaa63e..df56e3808aa 100644
@@ -516,6 +516,7 @@ static JsonParseErrorType
 ndistinct_scalar(void *state, char *token, JsonTokenType tokentype)
+	int64		ndistinct;
 
 	/* if the entire json is just one scalar, that's wrong */
 	if (parse->found_only_object != true)
@@ -530,12 +531,19 @@ ndistinct_scalar(void *state, char *token, JsonTokenType tokentype)
 
 	Assert(parse->current_item != NULL);
 
-	parse->current_item->ndistinct = float8in_internal(token, NULL, "double",
-													   token, parse->escontext);
+	/*
+	 * While the structure dictates that ndistinct in a double precision floating
+	 * point, in practice it has always been an integer, and it is output as such.
+	 * Therefore, we follow usage precendent over the actual storage structure,
+	 * and read it in as an integer.
+	 */
+	ndistinct = pg_strtoint64_safe(token, parse->escontext);
 
 	if (SOFT_ERROR_OCCURRED(parse->escontext))
 		return JSON_SEM_ACTION_FAILED;
 
+	parse->current_item->ndistinct = (double) ndistinct;
+
 	/* mark us done with this item */
 	parse->current_item = NULL;
 	return JSON_SUCCESS;
@@ -650,7 +658,7 @@ free_pg_ndistinct(MVNDistinct *ndistinct)
  * extended statistics object.
  *
  * Positive attnums are attributes which must be found in the stxkeys,
- * while negative attnums correspond to an expr number, so the attnum 
+ * while negative attnums correspond to an expr number, so the attnum
  * can't be below (0 - numexprs).
  */
 bool
diff --git a/src/test/regress/expected/stats_ext.out b/src/test/regress/expected/stats_ext.out
index 489e6a19771..e3e82aa5dfb 100644
--- a/src/test/regress/expected/stats_ext.out
+++ b/src/test/regress/expected/stats_ext.out
@@ -3371,6 +3371,19 @@ ERROR:  malformed pg_ndistinct: "{"6, -1": 14, "6, -2": 9143, "-1, -1": 13454, "
 LINE 1: SELECT '{"6, -1": 14, "6, -2": 9143, "-1, -1": 13454, "6, -1...
                ^
 DETAIL:  attnum list duplicate value found: -1
+-- can't use inf/-inf/NaN for ndistinct values.
+SELECT '{"6, -1": "Inf", "6, -2": 9143, "-1, -2": 13454, "6, -1, -2": 14549}'::pg_ndistinct;
+ERROR:  invalid input syntax for type bigint: "Inf"
+LINE 1: SELECT '{"6, -1": "Inf", "6, -2": 9143, "-1, -2": 13454, "6,...
+               ^
+SELECT '{"6, -1": "-Inf", "6, -2": 9143, "-1, -2": 13454, "6, -1, -2": 14549}'::pg_ndistinct;
+ERROR:  invalid input syntax for type bigint: "-Inf"
+LINE 1: SELECT '{"6, -1": "-Inf", "6, -2": 9143, "-1, -2": 13454, "6...
+               ^
+SELECT '{"6, -1": "NaN", "6, -2": 9143, "-1, -2": 13454, "6, -1, -2": 14549}'::pg_ndistinct;
+ERROR:  invalid input syntax for type bigint: "NaN"
+LINE 1: SELECT '{"6, -1": "NaN", "6, -2": 9143, "-1, -2": 13454, "6,...
+               ^
 SELECT '{"-2 => 6": 0.292508, "-2 => -1": 0.113999, "6, -2 => -1": 0.348479, "-1, -2 => 6": 0.839691}'::pg_dependencies;
                                         pg_dependencies                                        
 -----------------------------------------------------------------------------------------------
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index 0e8491131e3..c6052ee3a10 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -220,7 +220,6 @@ CREATE TABLE stats_import.part_child_1
   PARTITION OF stats_import.part_parent
   FOR VALUES FROM (0) TO (10)
   WITH (autovacuum_enabled = false);
-CREATE INDEX part_parent_i ON stats_import.part_parent(i);
 ANALYZE stats_import.part_parent;
 SELECT relpages
 FROM pg_class
@@ -232,15 +231,6 @@ WHERE oid = 'stats_import.part_parent'::regclass;
 
 -- although partitioned tables have no storage, setting relpages to a
 -- positive value is still allowed
-SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.part_parent_i'::regclass,
-        relpages => 2::integer);
- pg_set_relation_stats 
------------------------
- 
-(1 row)
-
 SELECT
     pg_catalog.pg_set_relation_stats(
         relation => 'stats_import.part_parent'::regclass,
@@ -250,48 +240,6 @@ SELECT
  
 (1 row)
 
---
--- Partitioned indexes aren't analyzed but it is possible to set
--- stats. The locking rules are different from normal indexes due to
--- the rules for in-place updates: both the partitioned table and the
--- partitioned index are locked in ShareUpdateExclusive mode.
---
-BEGIN;
-SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.part_parent_i'::regclass,
-        relpages => 2::integer);
- pg_set_relation_stats 
------------------------
- 
-(1 row)
-
-SELECT mode FROM pg_locks
-WHERE relation = 'stats_import.part_parent'::regclass AND
-      pid = pg_backend_pid() AND granted;
-           mode           
---------------------------
- ShareUpdateExclusiveLock
-(1 row)
-
-SELECT mode FROM pg_locks
-WHERE relation = 'stats_import.part_parent_i'::regclass AND
-      pid = pg_backend_pid() AND granted;
-           mode           
---------------------------
- ShareUpdateExclusiveLock
-(1 row)
-
-COMMIT;
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent_i'::regclass,
-        'relpages', 2::integer);
- pg_restore_relation_stats 
----------------------------
- t
-(1 row)
-
 -- nothing stops us from setting it to -1
 SELECT
     pg_catalog.pg_set_relation_stats(
@@ -1504,24 +1452,15 @@ SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import.complex_type,
 UNION ALL
 SELECT 4, 'four', NULL, int4range(0,100), NULL;
 CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
--- restoring stats on index
-SELECT * FROM pg_catalog.pg_restore_relation_stats(
-    'relation', 'stats_import.is_odd'::regclass,
-    'version', '180000'::integer,
-    'relpages', '11'::integer,
-    'reltuples', '10000'::real,
-    'relallvisible', '0'::integer
-);
- pg_restore_relation_stats 
----------------------------
- t
-(1 row)
-
+CREATE STATISTICS stats_import.test_stat ON name, comp, lower(arange), array_length(tags,1)
+FROM stats_import.test;
 -- Generate statistics on table with data
 ANALYZE stats_import.test;
 CREATE TABLE stats_import.test_clone ( LIKE stats_import.test )
     WITH (autovacuum_enabled = false);
 CREATE INDEX is_odd_clone ON stats_import.test_clone(((comp).a % 2 = 1));
+CREATE STATISTICS stats_import.test_stat_clone ON name, comp, lower(arange), array_length(tags,1)
+FROM stats_import.test_clone;
 --
 -- Copy stats from test to test_clone, and is_odd to is_odd_clone
 --
@@ -1904,6 +1843,386 @@ WHERE s.starelid = 'stats_import.is_odd'::regclass;
 ---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
 (0 rows)
 
+-- set dependencies using at attnum (1) that is not in the statistics object
+        dependencies => '{"2 => 1": 1.000000, "2 => -1": 1.000000, "2 => -2": 1.000000, "3 => 2": 1.000000, "3 => -1": 1.000000, "3 => -2": 1.000000, "-1 => 2": 0.500000, "-1 => 3": 0.500000, "-1 => -2": 1.000000, "-2 => 2": 0.500000, "-2 => 3": 0.500000, "-2 => -1": 1.000000, "2, 3 => -1": 1.000000, "2, 3 => -2": 1.000000, "2, -1 => 3": 1.000000, "2, -1 => -2": 1.000000, "2, -2 => 3": 1.000000, "2, -2 => -1": 1.000000, "3, -1 => 2": 1.000000, "3, -1 => -2": 1.000000, "3, -2 => 2": 1.000000, "3, -2 => -1": 1.000000, "-1, -2 => 2": 0.500000, "-1, -2 => 3": 0.500000, "2, 3, -1 => -2": 1.000000, "2, 3, -2 => -1": 1.000000, "2, -1, -2 => 3": 1.000000, "3, -1, -2 => 2": 1.000000}'::pg_dependencies
+ERROR:  pg_dependencies: invalid attnum for this statistics object: 1
+-- set dependencies using at attnum that is 0
+        dependencies => '{"2 => 3": 1.000000, "0 => -1": 1.000000, "2 => -2": 1.000000, "3 => 2": 1.000000, "3 => -1": 1.000000, "3 => -2": 1.000000, "-1 => 2": 0.500000, "-1 => 3": 0.500000, "-1 => -2": 1.000000, "-2 => 2": 0.500000, "-2 => 3": 0.500000, "-2 => -1": 1.000000, "2, 3 => -1": 1.000000, "2, 3 => -2": 1.000000, "2, -1 => 3": 1.000000, "2, -1 => -2": 1.000000, "2, -2 => 3": 1.000000, "2, -2 => -1": 1.000000, "3, -1 => 2": 1.000000, "3, -1 => -2": 1.000000, "3, -2 => 2": 1.000000, "3, -2 => -1": 1.000000, "-1, -2 => 2": 0.500000, "-1, -2 => 3": 0.500000, "2, 3, -1 => -2": 1.000000, "2, 3, -2 => -1": 1.000000, "2, -1, -2 => 3": 1.000000, "3, -1, -2 => 2": 1.000000}'::pg_dependencies
+ERROR:  pg_dependencies: invalid attnum for this statistics object: 0
+-- set dependencies using at attnum that is outside the expression bounds(below -2)
+        dependencies => '{"2 => 3": 1.000000, "2 => -3": 1.000000, "2 => -2": 1.000000, "3 => 2": 1.000000, "3 => -1": 1.000000, "3 => -2": 1.000000, "-1 => 2": 0.500000, "-1 => 3": 0.500000, "-1 => -2": 1.000000, "-2 => 2": 0.500000, "-2 => 3": 0.500000, "-2 => -1": 1.000000, "2, 3 => -1": 1.000000, "2, 3 => -2": 1.000000, "2, -1 => 3": 1.000000, "2, -1 => -2": 1.000000, "2, -2 => 3": 1.000000, "2, -2 => -1": 1.000000, "3, -1 => 2": 1.000000, "3, -1 => -2": 1.000000, "3, -2 => 2": 1.000000, "3, -2 => -1": 1.000000, "-1, -2 => 2": 0.500000, "-1, -2 => 3": 0.500000, "2, 3, -1 => -2": 1.000000, "2, 3, -2 => -1": 1.000000, "2, -1, -2 => 3": 1.000000, "3, -1, -2 => 2": 1.000000}'::pg_dependencies
+ERROR:  pg_dependencies: invalid attnum for this statistics object: -3
index da5c11aedd7..6c249f3d00c 100644
@@ -1704,6 +1704,10 @@ SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x
 -- can't have duplicates attnums in list
 SELECT '{"6, -1": 14, "6, -2": 9143, "-1, -1": 13454, "6, -1, -2": 14549}'::pg_ndistinct;
+-- can't use inf/-inf/NaN for ndistinct values.
+SELECT '{"6, -1": "Inf", "6, -2": 9143, "-1, -2": 13454, "6, -1, -2": 14549}'::pg_ndistinct;
+SELECT '{"6, -1": "-Inf", "6, -2": 9143, "-1, -2": 13454, "6, -1, -2": 14549}'::pg_ndistinct;
+SELECT '{"6, -1": "NaN", "6, -2": 9143, "-1, -2": 13454, "6, -1, -2": 14549}'::pg_ndistinct;
 
 SELECT '{"-2 => 6": 0.292508, "-2 => -1": 0.113999, "6, -2 => -1": 0.348479, "-1, -2 => 6": 0.839691}'::pg_dependencies;
 -- can't have duplicates attnums in list
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index 5e24c779d80..9b69e2cd48f 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -151,8 +151,6 @@ CREATE TABLE stats_import.part_child_1
   FOR VALUES FROM (0) TO (10)
   WITH (autovacuum_enabled = false);
 
-CREATE INDEX part_parent_i ON stats_import.part_parent(i);
-
 ANALYZE stats_import.part_parent;
 
 SELECT relpages
@@ -161,44 +159,11 @@ WHERE oid = 'stats_import.part_parent'::regclass;
 
 -- although partitioned tables have no storage, setting relpages to a
 -- positive value is still allowed
-SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.part_parent_i'::regclass,
-        relpages => 2::integer);
-
 SELECT
     pg_catalog.pg_set_relation_stats(
         relation => 'stats_import.part_parent'::regclass,
         relpages => 2::integer);
 
---
--- Partitioned indexes aren't analyzed but it is possible to set
--- stats. The locking rules are different from normal indexes due to
--- the rules for in-place updates: both the partitioned table and the
--- partitioned index are locked in ShareUpdateExclusive mode.
---
-BEGIN;
-
-SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.part_parent_i'::regclass,
-        relpages => 2::integer);
-
-SELECT mode FROM pg_locks
-WHERE relation = 'stats_import.part_parent'::regclass AND
-      pid = pg_backend_pid() AND granted;
-
-SELECT mode FROM pg_locks
-WHERE relation = 'stats_import.part_parent_i'::regclass AND
-      pid = pg_backend_pid() AND granted;
-
-COMMIT;
-
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent_i'::regclass,
-        'relpages', 2::integer);
-
 -- nothing stops us from setting it to -1
 SELECT
     pg_catalog.pg_set_relation_stats(
@@ -1121,14 +1086,8 @@ SELECT 4, 'four', NULL, int4range(0,100), NULL;

--- restoring stats on index
-SELECT * FROM pg_catalog.pg_restore_relation_stats(
-    'relation', 'stats_import.is_odd'::regclass,
-    'version', '180000'::integer,
-    'relpages', '11'::integer,
-    'reltuples', '10000'::real,
-    'relallvisible', '0'::integer
-);
+CREATE STATISTICS stats_import.test_stat ON name, comp, lower(arange), array_length(tags,1)
+FROM stats_import.test;
 
 -- Generate statistics on table with data
 ANALYZE stats_import.test;
@@ -1138,6 +1097,9 @@ CREATE TABLE stats_import.test_clone ( LIKE stats_import.test )

@@ -1449,4 +1411,297 @@ FROM pg_statistic s
 
+
+-- set dependencies using at attnum (1) that is not in the statistics object
+        dependencies => '{"2 => 1": 1.000000, "2 => -1": 1.000000, "2 => -2": 1.000000, "3 => 2": 1.000000, "3 => -1": 1.000000, "3 => -2": 1.000000, "-1 => 2": 0.500000, "-1 => 3": 0.500000, "-1 => -2": 1.000000, "-2 => 2": 0.500000, "-2 => 3": 0.500000, "-2 => -1": 1.000000, "2, 3 => -1": 1.000000, "2, 3 => -2": 1.000000, "2, -1 => 3": 1.000000, "2, -1 => -2": 1.000000, "2, -2 => 3": 1.000000, "2, -2 => -1": 1.000000, "3, -1 => 2": 1.000000, "3, -1 => -2": 1.000000, "3, -2 => 2": 1.000000, "3, -2 => -1": 1.000000, "-1, -2 => 2": 0.500000, "-1, -2 => 3": 0.500000, "2, 3, -1 => -2": 1.000000, "2, 3, -2 => -1": 1.000000, "2, -1, -2 => 3": 1.000000, "3, -1, -2 => 2": 1.000000}'::pg_dependencies
+-- set dependencies using at attnum that is 0
+        dependencies => '{"2 => 3": 1.000000, "0 => -1": 1.000000, "2 => -2": 1.000000, "3 => 2": 1.000000, "3 => -1": 1.000000, "3 => -2": 1.000000, "-1 => 2": 0.500000, "-1 => 3": 0.500000, "-1 => -2": 1.000000, "-2 => 2": 0.500000, "-2 => 3": 0.500000, "-2 => -1": 1.000000, "2, 3 => -1": 1.000000, "2, 3 => -2": 1.000000, "2, -1 => 3": 1.000000, "2, -1 => -2": 1.000000, "2, -2 => 3": 1.000000, "2, -2 => -1": 1.000000, "3, -1 => 2": 1.000000, "3, -1 => -2": 1.000000, "3, -2 => 2": 1.000000, "3, -2 => -1": 1.000000, "-1, -2 => 2": 0.500000, "-1, -2 => 3": 0.500000, "2, 3, -1 => -2": 1.000000, "2, 3, -2 => -1": 1.000000, "2, -1, -2 => 3": 1.000000, "3, -1, -2 => 2": 1.000000}'::pg_dependencies
+-- set dependencies using at attnum that is outside the expression bounds(below -2)
+        dependencies => '{"2 => 3": 1.000000, "2 => -3": 1.000000, "2 => -2": 1.000000, "3 => 2": 1.000000, "3 => -1": 1.000000, "3 => -2": 1.000000, "-1 => 2": 0.500000, "-1 => 3": 0.500000, "-1 => -2": 1.000000, "-2 => 2": 0.500000, "-2 => 3": 0.500000, "-2 => -1": 1.000000, "2, 3 => -1": 1.000000, "2, 3 => -2": 1.000000, "2, -1 => 3": 1.000000, "2, -1 => -2": 1.000000, "2, -2 => 3": 1.000000, "2, -2 => -1": 1.000000, "3, -1 => 2": 1.000000, "3, -1 => -2": 1.000000, "3, -2 => 2": 1.000000, "3, -2 => -1": 1.000000, "-1, -2 => 2": 0.500000, "-1, -2 => 3": 0.500000, "2, 3, -1 => -2": 1.000000, "2, 3, -2 => -1": 1.000000, "2, -1, -2 => 3": 1.000000, "3, -1, -2 => 2": 1.000000}'::pg_dependencies
index df32ee0bf5b..0ec126958af 100644
@@ -30400,6 +30400,130 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset

---

From: Corey Huinker <[email-masked]>
Date: Tue, 4 Mar 2025 15:30:11 -0500

> All patches rebased to 71f17823ba010296da9946bd906bb8bcad6325bc.

A rebasing, and a few changes
* regnamespace and name parameters changed to statistics_schemaname as text
and statistics_name as text, so that there's one less thing that can
potentially fail in an upgrade
* schema lookup and stat name lookup failures now issue a warning and
return false, rather than ERROR
* elevel replaced with hardcoded WARNING most everywhere, as has been done
with relation/attribute stats
From 136f0d0b04a8592220f0975a3529f4e34a227f48 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 17 Dec 2024 03:30:55 -0500
Subject: [PATCH v4 1/4] Add working input function for pg_ndistinct.

 

+typedef struct
+{
+	const char *str;

+/*
 
+-- new input functions
 
+-- new input functions

base-commit: b229c10164770769c3b5033785917ca7a43a2471
-- 
2.48.1

From b07a8d66cc29a28d2e4fa20a335980fe900b4c16 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 17 Dec 2024 19:47:43 -0500
Subject: [PATCH v4 2/4] Add working input function for pg_dependencies.

 
 
+typedef struct
+{
+	const char *str;

 /*

 -- new input functions
 
 -- Tidy up

From 8e98791aae6bc8833c6106732c8e2d3bb5a5a76d Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Thu, 26 Dec 2024 05:02:06 -0500
Subject: [PATCH v4 3/4] Expose attribute statistics functions for use in
 extended_stats.

* get_attr_stat_type()

index 7dd0f975545..a0ab4b7633c 100644
 
+extern void get_attr_stat_type(Oid reloid, AttrNumber attnum,
+							   Oid *atttypid, int32 *atttypmod,
+							   char *atttyptype, Oid *atttypcoll,
+							   Oid *eq_opr, Oid *lt_opr);
+extern void init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
+								   Datum *values, bool *nulls, bool *replaces);
+
+extern void set_stats_slot(Datum *values, bool *nulls, bool *replaces,
+						   int16 stakind, Oid staop, Oid stacoll,
+						   Datum stanumbers, bool stanumbers_isnull,
+						   Datum stavalues, bool stavalues_isnull);
+
+extern Datum text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d,
+							   Oid typid, int32 typmod, bool *ok);
+extern bool get_elem_stat_type(Oid atttypid, char atttyptype,
+							   Oid *elemtypid, Oid *elem_eq_opr);
+
 #endif							/* STATISTICS_H */
diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index 6bcbee0edba..c740098f13a 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -96,23 +96,9 @@ static struct StatsArgInfo cleararginfo[] =
 
 static bool attribute_statistics_update(FunctionCallInfo fcinfo);
 static Node *get_attr_expr(Relation rel, int attnum);
-static void get_attr_stat_type(Oid reloid, AttrNumber attnum,
-							   Oid *atttypid, int32 *atttypmod,
-							   char *atttyptype, Oid *atttypcoll,
-							   Oid *eq_opr, Oid *lt_opr);
-static bool get_elem_stat_type(Oid atttypid, char atttyptype,
-							   Oid *elemtypid, Oid *elem_eq_opr);
-static Datum text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d,
-							   Oid typid, int32 typmod, bool *ok);
-static void set_stats_slot(Datum *values, bool *nulls, bool *replaces,
-						   int16 stakind, Oid staop, Oid stacoll,
-						   Datum stanumbers, bool stanumbers_isnull,
-						   Datum stavalues, bool stavalues_isnull);
 static void upsert_pg_statistic(Relation starel, HeapTuple oldtup,
 								Datum *values, bool *nulls, bool *replaces);
 static bool delete_pg_statistic(Oid reloid, AttrNumber attnum, bool stainherit);
-static void init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
-								   Datum *values, bool *nulls, bool *replaces);
 
 /*
  * Insert or Update Attribute Statistics
@@ -560,7 +546,7 @@ get_attr_expr(Relation rel, int attnum)
 /*
  * Derive type information from the attribute.
  */
-static void
+void
 get_attr_stat_type(Oid reloid, AttrNumber attnum,
 				   Oid *atttypid, int32 *atttypmod,
 				   char *atttyptype, Oid *atttypcoll,
@@ -642,7 +628,7 @@ get_attr_stat_type(Oid reloid, AttrNumber attnum,
 /*
  * Derive element type information from the attribute type.
  */
-static bool
+bool
 get_elem_stat_type(Oid atttypid, char atttyptype,
 				   Oid *elemtypid, Oid *elem_eq_opr)
 {
@@ -682,7 +668,7 @@ get_elem_stat_type(Oid atttypid, char atttyptype,
  * to false. If the resulting array contains NULLs, raise a WARNING and set ok
  * to false. Otherwise, set ok to true.
  */
-static Datum
+Datum
 text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d, Oid typid,
 				  int32 typmod, bool *ok)
 {
@@ -735,7 +721,7 @@ text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d, Oid typid,
  * Find and update the slot with the given stakind, or use the first empty
  * slot.
  */
-static void
+void
 set_stats_slot(Datum *values, bool *nulls, bool *replaces,
 			   int16 stakind, Oid staop, Oid stacoll,
 			   Datum stanumbers, bool stanumbers_isnull,
@@ -859,7 +845,7 @@ delete_pg_statistic(Oid reloid, AttrNumber attnum, bool stainherit)
 /*
  * Initialize values and nulls for a new stats tuple.
  */
-static void
+void
 init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
 					   Datum *values, bool *nulls, bool *replaces)
 {
-- 
2.48.1

From 3930e686895f4088d4875d185645c5ecef46e750 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Fri, 3 Jan 2025 13:43:29 -0500
Subject: [PATCH v4 4/4] Add extended statistics support functions.

Add pg_restore_extended_stats() and pg_clear_extended_stats(). These
functions closely mirror their relation and attribute counterparts,
but for extended statistics (i.e.  CREATE STATISTICS) objects.
---
 src/include/catalog/pg_proc.dat               |   16 +
 .../statistics/extended_stats_internal.h      |    8 +
 src/backend/statistics/dependencies.c         |   65 +
 src/backend/statistics/extended_stats.c       | 1104 +++++++++++++++++
 src/backend/statistics/mcv.c                  |  144 +++
 src/backend/statistics/mvdistinct.c           |   14 +-
 src/test/regress/expected/stats_ext.out       |   13 +
 src/test/regress/expected/stats_import.out    |  441 +++++++
 src/test/regress/sql/stats_ext.sql            |    4 +
 src/test/regress/sql/stats_import.sql         |  304 +++++
 doc/src/sgml/func.sgml                        |   98 ++
 11 files changed, 2208 insertions(+), 3 deletions(-)

index cd9422d0bac..8239808a9fc 100644
@@ -12449,5 +12449,21 @@
+  proargtypes => 'text text bool',
 
 ]

 
+
+void
 
+/*
 
+/*
index a8b63ec0884..ce883a01cc1 100644
@@ -18,11 +18,16 @@
 
+#include "catalog/namespace.h"
@@ -33,6 +38,7 @@
@@ -72,6 +78,71 @@ typedef struct StatExtEntry
 
+	[STATSCHEMA_ARG] = {"statistics_schemaname", TEXTOID},
+	[STATNAME_ARG] = {"statistics_name", TEXTOID},
+static bool extended_statistics_update(FunctionCallInfo fcinfo);
 
@@ -99,6 +170,28 @@ static StatsBuildData *make_build_data(Relation rel, StatExtEntry *stat,
 
+static HeapTuple get_pg_statistic_ext(Relation pg_stext, Oid nspoid,
+									  const char *stxname);
+static bool delete_pg_statistic_ext_data(Oid stxoid, bool inherited);
+
+typedef struct
+{
+	bool		ndistinct;
+	bool		dependencies;
+	bool		mcv;
+	bool		expressions;
+}			stakindFlags;
+
+static void expand_stxkind(HeapTuple tup, stakindFlags * enabled);
+static void upsert_pg_statistic_ext_data(Datum *values, bool *nulls, bool *replaces);
+static bool check_mcvlist_array(ArrayType *arr, int argindex,
+								int required_ndimss, int mcv_length);
+static Datum import_expressions(Relation pgsd, int numexprs,
+								Oid *atttypids, int32 *atttypmods,
+								Oid *atttypcolls, ArrayType *exprs_arr);
+static bool text_to_float4(Datum input, Datum *output);
+static bool text_to_int4(Datum input, Datum *output);
+
 
 /*
  * Compute requested extended stats, using the rows sampled for the plain
@@ -2631,3 +2724,1014 @@ make_build_data(Relation rel, StatExtEntry *stat, int numrows, HeapTuple *rows,
 
 	return result;
 }
+
+static HeapTuple
+get_pg_statistic_ext(Relation pg_stext, Oid nspoid, const char *stxname)
+{
+	ScanKeyData key[2];
+	SysScanDesc scan;
+	HeapTuple	tup;
+				CStringGetDatum(stxname));
+ * as WARNINGs, and other statistic kinds may still be updated.
+extended_statistics_update(FunctionCallInfo fcinfo)
+	char	   *nspname;
+	char	   *stxname;
+		ereport(WARNING,
+		PG_RETURN_BOOL(false);
+	nspname = TextDatumGetCString(PG_GETARG_DATUM(STATSCHEMA_ARG));
+	stxname = TextDatumGetCString(PG_GETARG_DATUM(STATNAME_ARG));
+	nspoid = get_namespace_oid(nspname, true);
+	if (nspoid == InvalidOid)
+		ereport(WARNING,
+				 errmsg("Namespace \"%s\" not found.", stxname)));
+		PG_RETURN_BOOL(false);
+		ereport(WARNING,
+						get_namespace_name(nspoid), stxname)));
+		PG_RETURN_BOOL(false);
+			ereport(WARNING,
+			ereport(WARNING,
+		ereport(WARNING,
+		ereport(WARNING,
+		ereport(WARNING,
+		if (pg_ndistinct_validate_items(ndistinct, &stxform->stxkeys, numexprs, WARNING))
+		if (pg_dependencies_validate_deps(dependencies, &stxform->stxkeys, numexprs, WARNING))
+			ereport(WARNING,
+								 2, nitems) ||
+								 1, nitems) ||
+								 1, nitems))
+		datum = import_mcvlist(tup, WARNING, numattrs,
+		datum = import_expressions(pgsd, numexprs,
+					int mcv_length)
+		ereport(WARNING,
+		ereport(WARNING,
+		ereport(WARNING,
+import_expressions(Relation pgsd, int numexprs,
+		ereport(WARNING,
+		ereport(WARNING,
+		ereport(WARNING,
+				ereport(WARNING,
+				ereport(WARNING,
+				ereport(WARNING,
+			ereport(WARNING,
+										  typid, typmod, &ok);
+										   FLOAT4OID, -1, &ok);
+										  typid, typmod, &ok);
+				ereport(WARNING,
+			ereport(WARNING,
+									&elemtypid, &elem_eq_opr))
+				ereport(WARNING,
+										  elemtypid, typmod, &ok);
+										   FLOAT4OID, -1, &ok);
+										   FLOAT4OID, -1, &ok);
+	if (!stats_fill_fcinfo_from_arg_pairs(fcinfo, positional_fcinfo, extarginfo))
+	if (!extended_statistics_update(positional_fcinfo))
+	char	   *nspname;
+	char	   *stxname;
+	nspname = TextDatumGetCString(PG_GETARG_DATUM(STATSCHEMA_ARG));
+	stxname = TextDatumGetCString(PG_GETARG_DATUM(STATNAME_ARG));
+	nspoid = get_namespace_oid(nspname, true);
+	if (nspoid == InvalidOid)
+		ereport(WARNING,
+				 errmsg("Namespace \"%s\" not found.", stxname)));
+		ereport(WARNING,
+						nspname, stxname)));
 
 	return s;
 }
+
+/*

 

index 4df287e547f..014c77a5b32 100644
@@ -1020,11 +1020,15 @@ SELECT * FROM pg_catalog.pg_restore_relation_stats(
  t
 (1 row)
 
+CREATE STATISTICS stats_import.test_stat ON name, comp, lower(arange), array_length(tags,1)
+FROM stats_import.test;
 -- Generate statistics on table with data
 ANALYZE stats_import.test;
 CREATE TABLE stats_import.test_clone ( LIKE stats_import.test )
     WITH (autovacuum_enabled = false);
 CREATE INDEX is_odd_clone ON stats_import.test_clone(((comp).a % 2 = 1));
+CREATE STATISTICS stats_import.test_stat_clone ON name, comp, lower(arange), array_length(tags,1)
+FROM stats_import.test_clone;
 --
 -- Copy stats from test to test_clone, and is_odd to is_odd_clone
 --
@@ -1361,6 +1365,443 @@ SELECT pg_catalog.pg_clear_attribute_stats(
     attname => 'nope'::name,
     inherited => false::boolean);
 ERROR:  column "nope" of relation "test" does not exist
+-- set n_distinct using at attnum (1) that is not in the statistics object
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'n_distinct', '{"2, 3": 4, "2, -1": 4, "2, -2": 4, "3, -1": 4, "3, -2": 4, "-1, -2": 3, "2, 3, -1": 4, "2, 3, -2": 4, "2, -1, -2": 4, "3, -1, -2": 4, "1, 3, -1, -2": 4}'::pg_ndistinct
+      );
+WARNING:  pg_ndistinct: invalid attnum for this statistics object: 1
+ pg_restore_extended_stats 
+---------------------------
+ f
+(1 row)
+
+-- set n_distinct using at attnum that is 0
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'n_distinct', '{"2, 3": 4, "2, -1": 4, "2, 0": 4, "3, -1": 4, "3, -2": 4, "-1, -2": 3, "2, 3, -1": 4, "2, 3, -2": 4, "2, -1, -2": 4, "3, -1, -2": 4, "2, 3, -1, -2": 4}'::pg_ndistinct
+      );
+WARNING:  pg_ndistinct: invalid attnum for this statistics object: 0
+ pg_restore_extended_stats 
+---------------------------
+ f
+(1 row)
+
+-- set n_distinct using at attnum that is outside the expression bounds(below -2)
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'n_distinct', '{"2, 3": 4, "2, -4": 4, "2, -2": 4, "3, -1": 4, "3, -2": 4, "-1, -2": 3, "2, 3, -1": 4, "2, 3, -2": 4, "2, -1, -2": 4, "3, -1, -2": 4, "1, 3, -1, -2": 4}'::pg_ndistinct
+      );
+WARNING:  pg_ndistinct: invalid attnum for this statistics object: -4
+ pg_restore_extended_stats 
+---------------------------
+ f
+(1 row)
+
+-- ok
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'n_distinct', '{"2, 3": 4, "2, -1": 4, "2, -2": 4, "3, -1": 4, "3, -2": 4, "-1, -2": 3, "2, 3, -1": 4, "2, 3, -2": 4, "2, -1, -2": 4, "3, -1, -2": 4, "2, 3, -1, -2": 4}'::pg_ndistinct
+      );
+ pg_restore_extended_stats 
+---------------------------
+ t
+(1 row)
+
+SELECT
+    e.n_distinct, e.dependencies, e.most_common_vals, e.most_common_val_nulls,
+    e.most_common_freqs, e.most_common_base_freqs
+FROM pg_stats_ext AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false
+\gx
+-[ RECORD 1 ]----------+----------------------------------------------------------------------------------------------------------------------------------------------------------
+n_distinct             | {"2, 3": 4, "2, -1": 4, "2, -2": 4, "3, -1": 4, "3, -2": 4, "-1, -2": 3, "2, 3, -1": 4, "2, 3, -2": 4, "2, -1, -2": 4, "3, -1, -2": 4, "2, 3, -1, -2": 4}
+dependencies           | 
+most_common_vals       | 
+most_common_val_nulls  | 
+most_common_freqs      | 
+most_common_base_freqs | 
+
+-- set dependencies using at attnum (1) that is not in the statistics object
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'dependencies', '{"2 => 1": 1.000000, "2 => -1": 1.000000, "2 => -2": 1.000000, "3 => 2": 1.000000, "3 => -1": 1.000000, "3 => -2": 1.000000, "-1 => 2": 0.500000, "-1 => 3": 0.500000, "-1 => -2": 1.000000, "-2 => 2": 0.500000, "-2 => 3": 0.500000, "-2 => -1": 1.000000, "2, 3 => -1": 1.000000, "2, 3 => -2": 1.000000, "2, -1 => 3": 1.000000, "2, -1 => -2": 1.000000, "2, -2 => 3": 1.000000, "2, -2 => -1": 1.000000, "3, -1 => 2": 1.000000, "3, -1 => -2": 1.000000, "3, -2 => 2": 1.000000, "3, -2 => -1": 1.000000, "-1, -2 => 2": 0.500000, "-1, -2 => 3": 0.500000, "2, 3, -1 => -2": 1.000000, "2, 3, -2 => -1": 1.000000, "2, -1, -2 => 3": 1.000000, "3, -1, -2 => 2": 1.000000}'::pg_dependencies
+      );
+WARNING:  pg_dependencies: invalid attnum for this statistics object: 1
+ pg_restore_extended_stats 
+---------------------------
+ f
+(1 row)
+
+-- set dependencies using at attnum that is 0
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'dependencies', '{"2 => 3": 1.000000, "0 => -1": 1.000000, "2 => -2": 1.000000, "3 => 2": 1.000000, "3 => -1": 1.000000, "3 => -2": 1.000000, "-1 => 2": 0.500000, "-1 => 3": 0.500000, "-1 => -2": 1.000000, "-2 => 2": 0.500000, "-2 => 3": 0.500000, "-2 => -1": 1.000000, "2, 3 => -1": 1.000000, "2, 3 => -2": 1.000000, "2, -1 => 3": 1.000000, "2, -1 => -2": 1.000000, "2, -2 => 3": 1.000000, "2, -2 => -1": 1.000000, "3, -1 => 2": 1.000000, "3, -1 => -2": 1.000000, "3, -2 => 2": 1.000000, "3, -2 => -1": 1.000000, "-1, -2 => 2": 0.500000, "-1, -2 => 3": 0.500000, "2, 3, -1 => -2": 1.000000, "2, 3, -2 => -1": 1.000000, "2, -1, -2 => 3": 1.000000, "3, -1, -2 => 2": 1.000000}'::pg_dependencies
+      );
+WARNING:  pg_dependencies: invalid attnum for this statistics object: 0
+ pg_restore_extended_stats 
+---------------------------
+ f
+(1 row)
+
+-- set dependencies using at attnum that is outside the expression bounds(below -2)
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'dependencies', '{"2 => 3": 1.000000, "2 => -3": 1.000000, "2 => -2": 1.000000, "3 => 2": 1.000000, "3 => -1": 1.000000, "3 => -2": 1.000000, "-1 => 2": 0.500000, "-1 => 3": 0.500000, "-1 => -2": 1.000000, "-2 => 2": 0.500000, "-2 => 3": 0.500000, "-2 => -1": 1.000000, "2, 3 => -1": 1.000000, "2, 3 => -2": 1.000000, "2, -1 => 3": 1.000000, "2, -1 => -2": 1.000000, "2, -2 => 3": 1.000000, "2, -2 => -1": 1.000000, "3, -1 => 2": 1.000000, "3, -1 => -2": 1.000000, "3, -2 => 2": 1.000000, "3, -2 => -1": 1.000000, "-1, -2 => 2": 0.500000, "-1, -2 => 3": 0.500000, "2, 3, -1 => -2": 1.000000, "2, 3, -2 => -1": 1.000000, "2, -1, -2 => 3": 1.000000, "3, -1, -2 => 2": 1.000000}'::pg_dependencies
+      );
+WARNING:  pg_dependencies: invalid attnum for this statistics object: -3
+ pg_restore_extended_stats 
+---------------------------
+ f
+(1 row)
+
+-- ok
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'dependencies', '{"2 => 3": 1.000000, "2 => -1": 1.000000, "2 => -2": 1.000000, "3 => 2": 1.000000, "3 => -1": 1.000000, "3 => -2": 1.000000, "-1 => 2": 0.500000, "-1 => 3": 0.500000, "-1 => -2": 1.000000, "-2 => 2": 0.500000, "-2 => 3": 0.500000, "-2 => -1": 1.000000, "2, 3 => -1": 1.000000, "2, 3 => -2": 1.000000, "2, -1 => 3": 1.000000, "2, -1 => -2": 1.000000, "2, -2 => 3": 1.000000, "2, -2 => -1": 1.000000, "3, -1 => 2": 1.000000, "3, -1 => -2": 1.000000, "3, -2 => 2": 1.000000, "3, -2 => -1": 1.000000, "-1, -2 => 2": 0.500000, "-1, -2 => 3": 0.500000, "2, 3, -1 => -2": 1.000000, "2, 3, -2 => -1": 1.000000, "2, -1, -2 => 3": 1.000000, "3, -1, -2 => 2": 1.000000}'::pg_dependencies
+      );
+ pg_restore_extended_stats 
+---------------------------
+ t
+(1 row)
+
+SELECT
+    e.n_distinct, e.dependencies, e.most_common_vals, e.most_common_val_nulls,
+    e.most_common_freqs, e.most_common_base_freqs
+FROM pg_stats_ext AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false
+\gx
+-[ RECORD 1 ]----------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+n_distinct             | {"2, 3": 4, "2, -1": 4, "2, -2": 4, "3, -1": 4, "3, -2": 4, "-1, -2": 3, "2, 3, -1": 4, "2, 3, -2": 4, "2, -1, -2": 4, "3, -1, -2": 4, "2, 3, -1, -2": 4}
+dependencies           | {"2 => 3": 1.000000, "2 => -1": 1.000000, "2 => -2": 1.000000, "3 => 2": 1.000000, "3 => -1": 1.000000, "3 => -2": 1.000000, "-1 => 2": 0.500000, "-1 => 3": 0.500000, "-1 => -2": 1.000000, "-2 => 2": 0.500000, "-2 => 3": 0.500000, "-2 => -1": 1.000000, "2, 3 => -1": 1.000000, "2, 3 => -2": 1.000000, "2, -1 => 3": 1.000000, "2, -1 => -2": 1.000000, "2, -2 => 3": 1.000000, "2, -2 => -1": 1.000000, "3, -1 => 2": 1.000000, "3, -1 => -2": 1.000000, "3, -2 => 2": 1.000000, "3, -2 => -1": 1.000000, "-1, -2 => 2": 0.500000, "-1, -2 => 3": 0.500000, "2, 3, -1 => -2": 1.000000, "2, 3, -2 => -1": 1.000000, "2, -1, -2 => 3": 1.000000, "3, -1, -2 => 2": 1.000000}
+most_common_vals       | 
+most_common_val_nulls  | 
+most_common_freqs      | 
+most_common_base_freqs | 
+
+-- if any one mcv param specified, all four must be specified (part 1)
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'most_common_vals', '{{four,NULL,0,NULL},{one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")",1,2},{tre,"(3,3.3,TRE,03-03-2003,)",-1,3},{two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")",1,2}}'::text[]
+      );
+WARNING:  MCV parameters "most_common_vals", "most_common_val_nulls", "most_common_freqs", and "most_common_base_freqs" must be all specified if any are specified
+ pg_restore_extended_stats 
+---------------------------
+ f
+(1 row)
+
+-- if any one mcv param specified, all four must be specified (part 2)
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'most_common_val_nulls', '{{f,t,f,t},{f,f,f,f},{f,f,f,f},{f,f,f,f}}'::boolean[]
+      );
+WARNING:  MCV parameters "most_common_vals", "most_common_val_nulls", "most_common_freqs", and "most_common_base_freqs" must be all specified if any are specified
+ pg_restore_extended_stats 
+---------------------------
+ f
+(1 row)
+
+-- if any one mcv param specified, all four must be specified (part 3)
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'most_common_freqs', '{0.25,0.25,0.25,0.25}'::double precision[]
+      );
+WARNING:  MCV parameters "most_common_vals", "most_common_val_nulls", "most_common_freqs", and "most_common_base_freqs" must be all specified if any are specified
+ pg_restore_extended_stats 
+---------------------------
+ f
+(1 row)
+
+-- if any one mcv param specified, all four must be specified (part 4)
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'most_common_base_freqs', '{0.00390625,0.015625,0.00390625,0.015625}'::double precision[]
+      );
+WARNING:  MCV parameters "most_common_vals", "most_common_val_nulls", "most_common_freqs", and "most_common_base_freqs" must be all specified if any are specified
+ pg_restore_extended_stats 
+---------------------------
+ f
+(1 row)
+
+-- ok
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'most_common_vals', '{{four,NULL,0,NULL},{one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")",1,2},{tre,"(3,3.3,TRE,03-03-2003,)",-1,3},{two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")",1,2}}'::text[],
+        'most_common_val_nulls', '{{f,t,f,t},{f,f,f,f},{f,f,f,f},{f,f,f,f}}'::boolean[],
+        'most_common_freqs', '{0.25,0.25,0.25,0.25}'::double precision[],
+        'most_common_base_freqs', '{0.00390625,0.015625,0.00390625,0.015625}'::double precision[]
+      );
+ pg_restore_extended_stats 
+---------------------------
+ t
+(1 row)
+
+SELECT
+    e.n_distinct, e.dependencies, e.most_common_vals, e.most_common_val_nulls,
+    e.most_common_freqs, e.most_common_base_freqs
+FROM pg_stats_ext AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false
+\gx
+-[ RECORD 1 ]----------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+n_distinct             | {"2, 3": 4, "2, -1": 4, "2, -2": 4, "3, -1": 4, "3, -2": 4, "-1, -2": 3, "2, 3, -1": 4, "2, 3, -2": 4, "2, -1, -2": 4, "3, -1, -2": 4, "2, 3, -1, -2": 4}
+dependencies           | {"2 => 3": 1.000000, "2 => -1": 1.000000, "2 => -2": 1.000000, "3 => 2": 1.000000, "3 => -1": 1.000000, "3 => -2": 1.000000, "-1 => 2": 0.500000, "-1 => 3": 0.500000, "-1 => -2": 1.000000, "-2 => 2": 0.500000, "-2 => 3": 0.500000, "-2 => -1": 1.000000, "2, 3 => -1": 1.000000, "2, 3 => -2": 1.000000, "2, -1 => 3": 1.000000, "2, -1 => -2": 1.000000, "2, -2 => 3": 1.000000, "2, -2 => -1": 1.000000, "3, -1 => 2": 1.000000, "3, -1 => -2": 1.000000, "3, -2 => 2": 1.000000, "3, -2 => -1": 1.000000, "-1, -2 => 2": 0.500000, "-1, -2 => 3": 0.500000, "2, 3, -1 => -2": 1.000000, "2, 3, -2 => -1": 1.000000, "2, -1, -2 => 3": 1.000000, "3, -1, -2 => 2": 1.000000}
+most_common_vals       | {{four,NULL,0,NULL},{one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")",1,2},{tre,"(3,3.3,TRE,03-03-2003,)",-1,3},{two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")",1,2}}
+most_common_val_nulls  | {{f,t,f,t},{f,f,f,f},{f,f,f,f},{f,f,f,f}}
+most_common_freqs      | {0.25,0.25,0.25,0.25}
+most_common_base_freqs | {0.00390625,0.015625,0.00390625,0.015625}
+
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'exprs', '{{0,4,-0.75,"{1}","{0.5}","{-1,0}",-0.6,NULL,NULL,NULL},{0.25,4,-0.5,"{2}","{0.5}",NULL,1,NULL,NULL,NULL}}'::text[]
+      );
+ pg_restore_extended_stats 
+---------------------------
+ t
+(1 row)
+
+SELECT
+    e.inherited, e.null_frac, e.avg_width, e.n_distinct, e.most_common_vals,
+    e.most_common_freqs, e.histogram_bounds, e.correlation,
+    e.most_common_elems, e.most_common_elem_freqs, e.elem_count_histogram
+FROM pg_stats_ext_exprs AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+and e.inherited = false
+\gx
+-[ RECORD 1 ]----------+-------
+inherited              | f
+null_frac              | 0
+avg_width              | 4
+n_distinct             | -0.75
+most_common_vals       | {1}
+most_common_freqs      | {0.5}
+histogram_bounds       | {-1,0}
+correlation            | -0.6
+most_common_elems      | 
+most_common_elem_freqs | 
+elem_count_histogram   | 
+-[ RECORD 2 ]----------+-------
+inherited              | f
+null_frac              | 0.25
+avg_width              | 4
+n_distinct             | -0.5
+most_common_vals       | {2}
+most_common_freqs      | {0.5}
+histogram_bounds       | 
+correlation            | 1
+most_common_elems      | 
+most_common_elem_freqs | 
+elem_count_histogram   | 
+
+SELECT
+    pg_catalog.pg_clear_extended_stats(
+        statistics_schemaname => 'stats_import',
+        statistics_name => 'test_stat_clone',
+        inherited => false);
+ pg_clear_extended_stats 
+-------------------------
+ 
+(1 row)
+
+SELECT COUNT(*)
+FROM pg_stats_ext AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false;
+ count 
+-------
+     0
+(1 row)
+
+SELECT COUNT(*)
+FROM pg_stats_ext_exprs AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false;
+ count 
+-------
+     0
+(1 row)
+
+--
+-- Copy stats from test_stat to test_stat_clone
+--
+SELECT
+    e.statistics_name,
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', e.statistics_schemaname::text,
+        'statistics_name', 'test_stat_clone',
+        'inherited', e.inherited,
+        'n_distinct', e.n_distinct,
+        'dependencies', e.dependencies,
+        'most_common_vals', e.most_common_vals,
+        'most_common_val_nulls', e.most_common_val_nulls,
+        '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
+        array_agg(
+            ARRAY[ee.null_frac::text, ee.avg_width::text,
+                  ee.n_distinct::text, ee.most_common_vals::text,
+                  ee.most_common_freqs::text, ee.histogram_bounds::text,
+                  ee.correlation::text, ee.most_common_elems::text,
+                  ee.most_common_elem_freqs::text,
+                  ee.elem_count_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 = 'test_stat';
+ statistics_name | pg_restore_extended_stats 
+-----------------+---------------------------
+ test_stat       | t
+(1 row)
+
+SELECT o.inherited,
+       o.n_distinct, o.dependencies, o.most_common_vals,
+       o.most_common_val_nulls, o.most_common_freqs,
+       o.most_common_base_freqs
+FROM pg_stats_ext AS o
+WHERE o.statistics_schemaname = 'stats_import'
+AND o.statistics_name = 'test_stat'
+EXCEPT
+SELECT n.inherited,
+       n.n_distinct, n.dependencies, n.most_common_vals,
+       n.most_common_val_nulls, n.most_common_freqs,
+       n.most_common_base_freqs
+FROM pg_stats_ext AS n
+WHERE n.statistics_schemaname = 'stats_import'
+AND n.statistics_name = 'test_stat_clone';
+ inherited | n_distinct | dependencies | most_common_vals | most_common_val_nulls | most_common_freqs | most_common_base_freqs 
+-----------+------------+--------------+------------------+-----------------------+-------------------+------------------------
+(0 rows)
+
+SELECT n.inherited,
+       n.n_distinct, n.dependencies, n.most_common_vals,
+       n.most_common_val_nulls, n.most_common_freqs,
+       n.most_common_base_freqs
+FROM pg_stats_ext AS n
+WHERE n.statistics_schemaname = 'stats_import'
+AND n.statistics_name = 'test_stat_clone'
+EXCEPT
+SELECT o.inherited,
+       o.n_distinct, o.dependencies, o.most_common_vals,
+       o.most_common_val_nulls, o.most_common_freqs,
+       o.most_common_base_freqs
+FROM pg_stats_ext AS o
+WHERE o.statistics_schemaname = 'stats_import'
+AND o.statistics_name = 'test_stat';
+ inherited | n_distinct | dependencies | most_common_vals | most_common_val_nulls | most_common_freqs | most_common_base_freqs 
+-----------+------------+--------------+------------------+-----------------------+-------------------+------------------------
+(0 rows)
+
+SELECT o.inherited,
+       o.null_frac, o.avg_width, o.n_distinct,
+       o.most_common_vals::text AS most_common_vals,
+       o.most_common_freqs,
+       o.histogram_bounds::text AS histogram_bounds,
+       o.correlation,
+       o.most_common_elems::text AS most_common_elems,
+       o.most_common_elem_freqs, o.elem_count_histogram
+FROM pg_stats_ext_exprs AS o
+WHERE o.statistics_schemaname = 'stats_import'
+AND o.statistics_name = 'test_stat'
+EXCEPT
+SELECT n.inherited,
+       n.null_frac, n.avg_width, n.n_distinct,
+       n.most_common_vals::text AS most_common_vals,
+       n.most_common_freqs,
+       n.histogram_bounds::text AS histogram_bounds,
+       n.correlation,
+       n.most_common_elems::text AS most_common_elems,
+       n.most_common_elem_freqs, n.elem_count_histogram
+FROM pg_stats_ext_exprs AS n
+WHERE n.statistics_schemaname = 'stats_import'
+AND n.statistics_name = 'test_stat_clone';
+ inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram 
+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------
+(0 rows)
+
+SELECT n.inherited,
+       n.null_frac, n.avg_width, n.n_distinct,
+       n.most_common_vals::text AS most_common_vals,
+       n.most_common_freqs,
+       n.histogram_bounds::text AS histogram_bounds,
+       n.correlation,
+       n.most_common_elems::text AS most_common_elems,
+       n.most_common_elem_freqs, n.elem_count_histogram
+FROM pg_stats_ext_exprs AS n
+WHERE n.statistics_schemaname = 'stats_import'
+AND n.statistics_name = 'test_stat_clone'
+EXCEPT
+SELECT o.inherited,
+       o.null_frac, o.avg_width, o.n_distinct,
+       o.most_common_vals::text AS most_common_vals,
+       o.most_common_freqs,
+       o.histogram_bounds::text AS histogram_bounds,
+       o.correlation,
+       o.most_common_elems::text AS most_common_elems,
+       o.most_common_elem_freqs, o.elem_count_histogram
+FROM pg_stats_ext_exprs AS o
+WHERE o.statistics_schemaname = 'stats_import'
+AND o.statistics_name = 'test_stat';
+ inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram 
+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------
+(0 rows)
+
 DROP SCHEMA stats_import CASCADE;
 NOTICE:  drop cascades to 6 other objects
 DETAIL:  drop cascades to type stats_import.complex_type
diff --git a/src/test/regress/sql/stats_ext.sql b/src/test/regress/sql/stats_ext.sql
index da5c11aedd7..6c249f3d00c 100644
--- a/src/test/regress/sql/stats_ext.sql
+++ b/src/test/regress/sql/stats_ext.sql
@@ -1704,6 +1704,10 @@ SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x
 SELECT '{"6, -1": 14, "6, -2": 9143, "-1, -2": 13454, "6, -1, -2": 14549}'::pg_ndistinct;
 -- can't have duplicates attnums in list
 SELECT '{"6, -1": 14, "6, -2": 9143, "-1, -1": 13454, "6, -1, -2": 14549}'::pg_ndistinct;
+-- can't use inf/-inf/NaN for ndistinct values.
+SELECT '{"6, -1": "Inf", "6, -2": 9143, "-1, -2": 13454, "6, -1, -2": 14549}'::pg_ndistinct;
+SELECT '{"6, -1": "-Inf", "6, -2": 9143, "-1, -2": 13454, "6, -1, -2": 14549}'::pg_ndistinct;
+SELECT '{"6, -1": "NaN", "6, -2": 9143, "-1, -2": 13454, "6, -1, -2": 14549}'::pg_ndistinct;
 
 SELECT '{"-2 => 6": 0.292508, "-2 => -1": 0.113999, "6, -2 => -1": 0.348479, "-1, -2 => 6": 0.839691}'::pg_dependencies;
 -- can't have duplicates attnums in list
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index febda3d18d9..84462f94deb 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -696,6 +696,9 @@ SELECT * FROM pg_catalog.pg_restore_relation_stats(
     'relallfrozen', '0'::integer
 );

@@ -704,6 +707,9 @@ CREATE TABLE stats_import.test_clone ( LIKE stats_import.test )

+CREATE STATISTICS stats_import.test_stat_clone ON name, comp, lower(arange), array_length(tags,1)
+FROM stats_import.test_clone;
+
 --
 -- Copy stats from test to test_clone, and is_odd to is_odd_clone
 --
@@ -1004,4 +1010,302 @@ SELECT pg_catalog.pg_clear_attribute_stats(
     attname => 'nope'::name,
     inherited => false::boolean);
 
+-- set n_distinct using at attnum (1) that is not in the statistics object
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'n_distinct', '{"2, 3": 4, "2, -1": 4, "2, -2": 4, "3, -1": 4, "3, -2": 4, "-1, -2": 3, "2, 3, -1": 4, "2, 3, -2": 4, "2, -1, -2": 4, "3, -1, -2": 4, "1, 3, -1, -2": 4}'::pg_ndistinct
+      );
+
+-- set n_distinct using at attnum that is 0
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'n_distinct', '{"2, 3": 4, "2, -1": 4, "2, 0": 4, "3, -1": 4, "3, -2": 4, "-1, -2": 3, "2, 3, -1": 4, "2, 3, -2": 4, "2, -1, -2": 4, "3, -1, -2": 4, "2, 3, -1, -2": 4}'::pg_ndistinct
+      );
+
+-- set n_distinct using at attnum that is outside the expression bounds(below -2)
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'n_distinct', '{"2, 3": 4, "2, -4": 4, "2, -2": 4, "3, -1": 4, "3, -2": 4, "-1, -2": 3, "2, 3, -1": 4, "2, 3, -2": 4, "2, -1, -2": 4, "3, -1, -2": 4, "1, 3, -1, -2": 4}'::pg_ndistinct
+      );
+
+-- ok
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'n_distinct', '{"2, 3": 4, "2, -1": 4, "2, -2": 4, "3, -1": 4, "3, -2": 4, "-1, -2": 3, "2, 3, -1": 4, "2, 3, -2": 4, "2, -1, -2": 4, "3, -1, -2": 4, "2, 3, -1, -2": 4}'::pg_ndistinct
+      );
+
+SELECT
+    e.n_distinct, e.dependencies, e.most_common_vals, e.most_common_val_nulls,
+    e.most_common_freqs, e.most_common_base_freqs
+FROM pg_stats_ext AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false
+\gx
+
+-- set dependencies using at attnum (1) that is not in the statistics object
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'dependencies', '{"2 => 1": 1.000000, "2 => -1": 1.000000, "2 => -2": 1.000000, "3 => 2": 1.000000, "3 => -1": 1.000000, "3 => -2": 1.000000, "-1 => 2": 0.500000, "-1 => 3": 0.500000, "-1 => -2": 1.000000, "-2 => 2": 0.500000, "-2 => 3": 0.500000, "-2 => -1": 1.000000, "2, 3 => -1": 1.000000, "2, 3 => -2": 1.000000, "2, -1 => 3": 1.000000, "2, -1 => -2": 1.000000, "2, -2 => 3": 1.000000, "2, -2 => -1": 1.000000, "3, -1 => 2": 1.000000, "3, -1 => -2": 1.000000, "3, -2 => 2": 1.000000, "3, -2 => -1": 1.000000, "-1, -2 => 2": 0.500000, "-1, -2 => 3": 0.500000, "2, 3, -1 => -2": 1.000000, "2, 3, -2 => -1": 1.000000, "2, -1, -2 => 3": 1.000000, "3, -1, -2 => 2": 1.000000}'::pg_dependencies
+      );
+
+-- set dependencies using at attnum that is 0
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'dependencies', '{"2 => 3": 1.000000, "0 => -1": 1.000000, "2 => -2": 1.000000, "3 => 2": 1.000000, "3 => -1": 1.000000, "3 => -2": 1.000000, "-1 => 2": 0.500000, "-1 => 3": 0.500000, "-1 => -2": 1.000000, "-2 => 2": 0.500000, "-2 => 3": 0.500000, "-2 => -1": 1.000000, "2, 3 => -1": 1.000000, "2, 3 => -2": 1.000000, "2, -1 => 3": 1.000000, "2, -1 => -2": 1.000000, "2, -2 => 3": 1.000000, "2, -2 => -1": 1.000000, "3, -1 => 2": 1.000000, "3, -1 => -2": 1.000000, "3, -2 => 2": 1.000000, "3, -2 => -1": 1.000000, "-1, -2 => 2": 0.500000, "-1, -2 => 3": 0.500000, "2, 3, -1 => -2": 1.000000, "2, 3, -2 => -1": 1.000000, "2, -1, -2 => 3": 1.000000, "3, -1, -2 => 2": 1.000000}'::pg_dependencies
+      );
+
+-- set dependencies using at attnum that is outside the expression bounds(below -2)
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'dependencies', '{"2 => 3": 1.000000, "2 => -3": 1.000000, "2 => -2": 1.000000, "3 => 2": 1.000000, "3 => -1": 1.000000, "3 => -2": 1.000000, "-1 => 2": 0.500000, "-1 => 3": 0.500000, "-1 => -2": 1.000000, "-2 => 2": 0.500000, "-2 => 3": 0.500000, "-2 => -1": 1.000000, "2, 3 => -1": 1.000000, "2, 3 => -2": 1.000000, "2, -1 => 3": 1.000000, "2, -1 => -2": 1.000000, "2, -2 => 3": 1.000000, "2, -2 => -1": 1.000000, "3, -1 => 2": 1.000000, "3, -1 => -2": 1.000000, "3, -2 => 2": 1.000000, "3, -2 => -1": 1.000000, "-1, -2 => 2": 0.500000, "-1, -2 => 3": 0.500000, "2, 3, -1 => -2": 1.000000, "2, 3, -2 => -1": 1.000000, "2, -1, -2 => 3": 1.000000, "3, -1, -2 => 2": 1.000000}'::pg_dependencies
+      );
+
+-- ok
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'dependencies', '{"2 => 3": 1.000000, "2 => -1": 1.000000, "2 => -2": 1.000000, "3 => 2": 1.000000, "3 => -1": 1.000000, "3 => -2": 1.000000, "-1 => 2": 0.500000, "-1 => 3": 0.500000, "-1 => -2": 1.000000, "-2 => 2": 0.500000, "-2 => 3": 0.500000, "-2 => -1": 1.000000, "2, 3 => -1": 1.000000, "2, 3 => -2": 1.000000, "2, -1 => 3": 1.000000, "2, -1 => -2": 1.000000, "2, -2 => 3": 1.000000, "2, -2 => -1": 1.000000, "3, -1 => 2": 1.000000, "3, -1 => -2": 1.000000, "3, -2 => 2": 1.000000, "3, -2 => -1": 1.000000, "-1, -2 => 2": 0.500000, "-1, -2 => 3": 0.500000, "2, 3, -1 => -2": 1.000000, "2, 3, -2 => -1": 1.000000, "2, -1, -2 => 3": 1.000000, "3, -1, -2 => 2": 1.000000}'::pg_dependencies
+      );
+
+SELECT
+    e.n_distinct, e.dependencies, e.most_common_vals, e.most_common_val_nulls,
+    e.most_common_freqs, e.most_common_base_freqs
+FROM pg_stats_ext AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false
+\gx
+
+-- if any one mcv param specified, all four must be specified (part 1)
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'most_common_vals', '{{four,NULL,0,NULL},{one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")",1,2},{tre,"(3,3.3,TRE,03-03-2003,)",-1,3},{two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")",1,2}}'::text[]
+      );
+
+-- if any one mcv param specified, all four must be specified (part 2)
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'most_common_val_nulls', '{{f,t,f,t},{f,f,f,f},{f,f,f,f},{f,f,f,f}}'::boolean[]
+      );
+
+-- if any one mcv param specified, all four must be specified (part 3)
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'most_common_freqs', '{0.25,0.25,0.25,0.25}'::double precision[]
+      );
+
+-- if any one mcv param specified, all four must be specified (part 4)
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'most_common_base_freqs', '{0.00390625,0.015625,0.00390625,0.015625}'::double precision[]
+      );
+
+-- ok
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'most_common_vals', '{{four,NULL,0,NULL},{one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")",1,2},{tre,"(3,3.3,TRE,03-03-2003,)",-1,3},{two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")",1,2}}'::text[],
+        'most_common_val_nulls', '{{f,t,f,t},{f,f,f,f},{f,f,f,f},{f,f,f,f}}'::boolean[],
+        'most_common_freqs', '{0.25,0.25,0.25,0.25}'::double precision[],
+        'most_common_base_freqs', '{0.00390625,0.015625,0.00390625,0.015625}'::double precision[]
+      );
+
+SELECT
+    e.n_distinct, e.dependencies, e.most_common_vals, e.most_common_val_nulls,
+    e.most_common_freqs, e.most_common_base_freqs
+FROM pg_stats_ext AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false
+\gx
+
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'exprs', '{{0,4,-0.75,"{1}","{0.5}","{-1,0}",-0.6,NULL,NULL,NULL},{0.25,4,-0.5,"{2}","{0.5}",NULL,1,NULL,NULL,NULL}}'::text[]
+      );
+
+SELECT
+    e.inherited, e.null_frac, e.avg_width, e.n_distinct, e.most_common_vals,
+    e.most_common_freqs, e.histogram_bounds, e.correlation,
+    e.most_common_elems, e.most_common_elem_freqs, e.elem_count_histogram
+FROM pg_stats_ext_exprs AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+and e.inherited = false
+\gx
+
+SELECT
+    pg_catalog.pg_clear_extended_stats(
+        statistics_schemaname => 'stats_import',
+        statistics_name => 'test_stat_clone',
+        inherited => false);
+
+SELECT COUNT(*)
+FROM pg_stats_ext AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false;
+
+SELECT COUNT(*)
+FROM pg_stats_ext_exprs AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false;
+
+--
+-- Copy stats from test_stat to test_stat_clone
+--
+SELECT
+    e.statistics_name,
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', e.statistics_schemaname::text,
+        'statistics_name', 'test_stat_clone',
+        'inherited', e.inherited,
+        'n_distinct', e.n_distinct,
+        'dependencies', e.dependencies,
+        'most_common_vals', e.most_common_vals,
+        'most_common_val_nulls', e.most_common_val_nulls,
+        '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
+        array_agg(
+            ARRAY[ee.null_frac::text, ee.avg_width::text,
+                  ee.n_distinct::text, ee.most_common_vals::text,
+                  ee.most_common_freqs::text, ee.histogram_bounds::text,
+                  ee.correlation::text, ee.most_common_elems::text,
+                  ee.most_common_elem_freqs::text,
+                  ee.elem_count_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 = 'test_stat';
+
+SELECT o.inherited,
+       o.n_distinct, o.dependencies, o.most_common_vals,
+       o.most_common_val_nulls, o.most_common_freqs,
+       o.most_common_base_freqs
+FROM pg_stats_ext AS o
+WHERE o.statistics_schemaname = 'stats_import'
+AND o.statistics_name = 'test_stat'
+EXCEPT
+SELECT n.inherited,
+       n.n_distinct, n.dependencies, n.most_common_vals,
+       n.most_common_val_nulls, n.most_common_freqs,
+       n.most_common_base_freqs
+FROM pg_stats_ext AS n
+WHERE n.statistics_schemaname = 'stats_import'
+AND n.statistics_name = 'test_stat_clone';
+
+SELECT n.inherited,
+       n.n_distinct, n.dependencies, n.most_common_vals,
+       n.most_common_val_nulls, n.most_common_freqs,
+       n.most_common_base_freqs
+FROM pg_stats_ext AS n
+WHERE n.statistics_schemaname = 'stats_import'
+AND n.statistics_name = 'test_stat_clone'
+EXCEPT
+SELECT o.inherited,
+       o.n_distinct, o.dependencies, o.most_common_vals,
+       o.most_common_val_nulls, o.most_common_freqs,
+       o.most_common_base_freqs
+FROM pg_stats_ext AS o
+WHERE o.statistics_schemaname = 'stats_import'
+AND o.statistics_name = 'test_stat';
+
+SELECT o.inherited,
+       o.null_frac, o.avg_width, o.n_distinct,
+       o.most_common_vals::text AS most_common_vals,
+       o.most_common_freqs,
+       o.histogram_bounds::text AS histogram_bounds,
+       o.correlation,
+       o.most_common_elems::text AS most_common_elems,
+       o.most_common_elem_freqs, o.elem_count_histogram
+FROM pg_stats_ext_exprs AS o
+WHERE o.statistics_schemaname = 'stats_import'
+AND o.statistics_name = 'test_stat'
+EXCEPT
+SELECT n.inherited,
+       n.null_frac, n.avg_width, n.n_distinct,
+       n.most_common_vals::text AS most_common_vals,
+       n.most_common_freqs,
+       n.histogram_bounds::text AS histogram_bounds,
+       n.correlation,
+       n.most_common_elems::text AS most_common_elems,
+       n.most_common_elem_freqs, n.elem_count_histogram
+FROM pg_stats_ext_exprs AS n
+WHERE n.statistics_schemaname = 'stats_import'
+AND n.statistics_name = 'test_stat_clone';
+
+SELECT n.inherited,
+       n.null_frac, n.avg_width, n.n_distinct,
+       n.most_common_vals::text AS most_common_vals,
+       n.most_common_freqs,
+       n.histogram_bounds::text AS histogram_bounds,
+       n.correlation,
+       n.most_common_elems::text AS most_common_elems,
+       n.most_common_elem_freqs, n.elem_count_histogram
+FROM pg_stats_ext_exprs AS n
+WHERE n.statistics_schemaname = 'stats_import'
+AND n.statistics_name = 'test_stat_clone'
+EXCEPT
+SELECT o.inherited,
+       o.null_frac, o.avg_width, o.n_distinct,
+       o.most_common_vals::text AS most_common_vals,
+       o.most_common_freqs,
+       o.histogram_bounds::text AS histogram_bounds,
+       o.correlation,
+       o.most_common_elems::text AS most_common_elems,
+       o.most_common_elem_freqs, o.elem_count_histogram
+FROM pg_stats_ext_exprs AS o
+WHERE o.statistics_schemaname = 'stats_import'
+AND o.statistics_name = 'test_stat';
+
 DROP SCHEMA stats_import CASCADE;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index bf31b1f3eee..bf0fa8d6c5a 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30476,6 +30476,104 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
         </para>
        </entry>
       </row>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_restore_extended_stats</primary>
+        </indexterm>
+        <function>pg_restore_extended_stats</function> (
+        <literal>VARIADIC</literal> <parameter>kwargs</parameter> <type>"any"</type> )
+        <returnvalue>boolean</returnvalue>
+        </para>
+        <para>
+         Creates or updates statistics for statistics objects.  Ordinarily,
+         these statistics are collected automatically or updated as a part of
+         <xref linkend="sql-vacuum"/> or <xref linkend="sql-analyze"/>, so
+         it's not necessary to call this function.  However, it is useful
+         after a restore to enable the optimizer to choose better plans if
+         <command>ANALYZE</command> has not been run yet.
+        </para>
+        <para>
+         The tracked statistics may change from version to version, so
+         arguments are passed as pairs of <replaceable>argname</replaceable>
+         and <replaceable>argvalue</replaceable> in the form:
+<programlisting>
+ SELECT pg_restore_extended_stats(
+    '<replaceable>arg1name</replaceable>', '<replaceable>arg1value</replaceable>'::<replaceable>arg1type</replaceable>,
+    '<replaceable>arg2name</replaceable>', '<replaceable>arg2value</replaceable>'::<replaceable>arg2type</replaceable>,
+    '<replaceable>arg3name</replaceable>', '<replaceable>arg3value</replaceable>'::<replaceable>arg3type</replaceable>);
+</programlisting>
+        </para>
+        <para>
+         For example, to set the <structfield>n_distinct</structfield>,
+         <structfield>dependencies</structfield>, and <structfield>exprs</structfield>
+         values for the statistics object <structname>myschema.mystatsobj</structname>:
+<programlisting>
+ SELECT pg_restore_extended_stats(
+    'statistics_schemaname', 'myschema'::name,
+    'statistics_name',       'mytable'::name,
+    'inherited',             false,
+    'n_distinct',            '{"2, 3": 4, "2, -1": 4, "2, -2": 4, "3, -1": 4, "3, -2": 4}'::pg_ndistinct,
+    'dependencies',          '{"2 => 1": 1.000000, "2 => -1": 1.000000, "2 => -2": 1.000000}'::pg_dependencies
+    'exprs',                 '{{0,4,-0.75,"{1}","{0.5}","{-1,0}",-0.6,NULL,NULL,NULL},{0.25,4,-0.5,"{2}","{0.5}",NULL,1,NULL,NULL,NULL}}'::text[]);
+</programlisting>
+        </para>
+        <para>
+         The required arguments are <literal>statistics_schemaname</literal> with a value
+         of type <type>name</type>, which specifies the statistics object's schema;
+         <literal>statistics_name</literal> with a value of type <type>name</type>, which specifies
+         the name of the statistics object; and <literal>inherited</literal>, which
+         specifies whether the statistics include values from child tables.
+         Other arguments are the names and values of statistics corresponding
+         to columns in <link linkend="view-pg-stats-ext"><structname>pg_stats_ext</structname>
+         </link>. To accept statistics for any expressions in the extended statistics object, the
+         parameter <literal>exprs</literal> with a type <type>text[]</type> is available, the array
+         must be two dimensional with an outer array in length equal to the number of expressions in
+         the object, and the inner array elements for each of the statistical columns in <link
+         linkend="view-pg-stats-ext-exprs"><structname>pg_stats_ext_exprs</structname></link>, some
+         of which are themselves arrays.
+        </para>
+        <para>
+         Additionally, this function accepts argument name
+         <literal>version</literal> of type <type>integer</type>, which
+         specifies the server version from which the statistics originated.
+         This is anticipated to be helpful in porting statistics from older
+         versions of <productname>PostgreSQL</productname>.
+        </para>
+        <para>
+         Minor errors are reported as a <literal>WARNING</literal> and
+         ignored, and remaining statistics will still be restored. If all
+         specified statistics are successfully restored, returns
+         <literal>true</literal>, otherwise <literal>false</literal>.
+        </para>
+        <para>
+         The caller must have the <literal>MAINTAIN</literal> privilege on the
+         table or be the owner of the database.
+        </para>
+       </entry>
+      </row>
+      <row>
+       <entry role="func_table_entry">
+        <para role="func_signature">
+         <indexterm>
+          <primary>pg_clear_extended_stats</primary>
+         </indexterm>
+         <function>pg_clear_extended_stats</function> (
+         <parameter>statistics_schemaname</parameter> <type>name</type>,
+         <parameter>statistics_name</parameter> <type>name</type>,
+         <parameter>inherited</parameter> <type>boolean</type> )
+         <returnvalue>void</returnvalue>
+        </para>
+        <para>
+         Clears statistics for the given statistics object, as
+         though the object was newly created.
+        </para>
+        <para>
+         The caller must have the <literal>MAINTAIN</literal> privilege on
+         the table or be the owner of the database.
+        </para>
+       </entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
-- 
2.48.1

---

From: Corey Huinker <[email-masked]>
Date: Mon, 31 Mar 2025 01:10:23 -0400

Just rebasing.
From 6eee574fc4cf8f32c8ed04d549552d4657b1ed38 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 17 Dec 2024 03:30:55 -0500
Subject: [PATCH v4 1/4] Add working input function for pg_ndistinct.

 

+typedef struct
+{
+	const char *str;

+/*
index 686d8c93aa8..9197c3d25f3 100644
 
+-- new input functions
index b71a6cd089f..10e5f21335e 100644
 
+-- new input functions

base-commit: 2a5e709e721cf5f890cde51755b84cfe25d1c4d9
-- 
2.49.0

From 795771580acee9802f1f1081c6aa007c333bdbfe Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 17 Dec 2024 19:47:43 -0500
Subject: [PATCH v4 2/4] Add working input function for pg_dependencies.

 
 
+typedef struct
+{
+	const char *str;

 /*
index 9197c3d25f3..4f1e6ab89f1 100644
 
index 10e5f21335e..9e84b0a4e01 100644
 
 -- new input functions
 
 -- Tidy up
2.49.0

From dcf64e419cfcc3b0800fa1c51fc030fedbf2ba5c Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Thu, 26 Dec 2024 05:02:06 -0500
Subject: [PATCH v4 3/4] Expose attribute statistics functions for use in
 extended_stats.

* get_attr_stat_type()

index f5eb17ba42d..d4923da6ee9 100644
@@ -100,23 +100,9 @@ static struct StatsArgInfo cleararginfo[] =

 /*
  * Insert or Update Attribute Statistics
@@ -568,7 +554,7 @@ get_attr_expr(Relation rel, int attnum)
 /*
  * Derive type information from the attribute.
  */
-static void
+void
 get_attr_stat_type(Oid reloid, AttrNumber attnum,
 				   Oid *atttypid, int32 *atttypmod,
 				   char *atttyptype, Oid *atttypcoll,
@@ -650,7 +636,7 @@ get_attr_stat_type(Oid reloid, AttrNumber attnum,
 /*
  * Derive element type information from the attribute type.
  */
-static bool
+bool
 get_elem_stat_type(Oid atttypid, char atttyptype,
 				   Oid *elemtypid, Oid *elem_eq_opr)
 {
@@ -690,7 +676,7 @@ get_elem_stat_type(Oid atttypid, char atttyptype,
  * to false. If the resulting array contains NULLs, raise a WARNING and set ok
  * to false. Otherwise, set ok to true.
  */
-static Datum
+Datum
 text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d, Oid typid,
 				  int32 typmod, bool *ok)
 {
@@ -743,7 +729,7 @@ text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d, Oid typid,
  * Find and update the slot with the given stakind, or use the first empty
  * slot.
  */
-static void
+void
 set_stats_slot(Datum *values, bool *nulls, bool *replaces,
 			   int16 stakind, Oid staop, Oid stacoll,
 			   Datum stanumbers, bool stanumbers_isnull,
@@ -867,7 +853,7 @@ delete_pg_statistic(Oid reloid, AttrNumber attnum, bool stainherit)
 /*
  * Initialize values and nulls for a new stats tuple.
  */
-static void
+void
 init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
 					   Datum *values, bool *nulls, bool *replaces)
 {
-- 
2.49.0

From a8fdec64a930f10406ee211ec0dc68164513c3c5 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Fri, 3 Jan 2025 13:43:29 -0500
Subject: [PATCH v4 4/4] Add extended statistics support functions.

 src/test/regress/sql/stats_import.sql         |  305 +++++
 11 files changed, 2209 insertions(+), 3 deletions(-)

index 8b68b16d79d..d01f68f2d08 100644
@@ -12492,5 +12492,21 @@
 
 ]

 
+
+void
 
+/*
 
+/*

 

 /*
 
 	return result;
 }
+
+static HeapTuple
 
 	return s;
 }
+
+/*

 

index 4f1e6ab89f1..a2300745d63 100644
index 48d6392b4ad..330ee5799a7 100644
@@ -1084,11 +1084,15 @@ SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import.complex_type,
@@ -1342,6 +1346,443 @@ AND attname = 'i';
 (1 row)
 
 DROP TABLE stats_temp;
index 9e84b0a4e01..1a329f0b07d 100644
 
 SELECT '{"-2 => 6": 0.292508, "-2 => -1": 0.113999, "6, -2 => -1": 0.348479, "-1, -2 => 6": 0.839691}'::pg_dependencies;
 -- can't have duplicates attnums in list
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index d140733a750..bb03e046129 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -766,6 +766,9 @@ SELECT 4, 'four', NULL, int4range(0,100), NULL;

 
@@ -774,6 +777,9 @@ CREATE TABLE stats_import.test_clone ( LIKE stats_import.test )

@@ -970,4 +976,303 @@ AND tablename = 'stats_temp'
 AND inherited = false
 AND attname = 'i';
 DROP TABLE stats_temp;
index 5bf6656deca..f92dfc12f1b 100644
@@ -30580,6 +30580,104 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
2.49.0

---

From: Corey Huinker <[email-masked]>
Date: Thu, 29 May 2025 18:32:17 -0400

> Just rebasing.

At pgconf.dev this year, the subject of changing the formats of
pg_ndistinct and pg_depdentencies came up again.

To recap: presently these datatypes have no working input function, but
would need one for statistics import to work on extended statistics. The
existing input formats are technically JSON, but the keys themselves are a
comma-separated list of attnums, so they require additional parsing. That
parsing is already done in the patches in this thread, but overall the
format is terrible for any sort of manipulation, like the manipulation that
people might want to do to translate the values to a table with a different
column order (say, after a restore of a table that had dropped columns), or
to do query planner experiments.

Because the old formats don't have a corresponding input function, there is
no risk of the ouptut not matching required inputs, but there will be once
we add new input functions, so this is our last chance to change the format
to something we like better.

The old format can be trivially translated via functions posted earlier in
this thread back in January (pg_xlat_ndistinct_to_attnames,
pg_xlat_dependencies_to_attnames) as well as the reverse (s/_to_/_from_/),
so dumping values from older versions will not be difficult.

I believe that we should take this opportunity to make the change. While we
don't have a pressing need to manipulate these structures now, we might in
the future and failing to do so now makes a later change much harder.

With that in mind, I'd like people to have a look at the proposed format
change if pg_ndistinct (the changes to pg_dependencies are similar), to see
if they want to make any improvements or comments. As you can see, the new
format is much less compact (about 3x as large), which could get bad if the
number of elements grew by a lot, but the number of elements is tied to the
number of factors in the extended support (N choose N, then N choose N-1,
etc, excluding choose 1), so this can't get too out of hand.

Existing format (newlines/formatting added by me to make head-to-head
comparison easier):

'{"2, 3": 4,
  "2, -1": 4,
  "2, -2": 4,
  "3, -1": 4,
  "3, -2": 4,
  "-1, -2": 3,
  "2, 3, -1": 4,
  "2, 3, -2": 4,
  "2, -1, -2": 4,
  "3, -1, -2": 4}'::pg_ndistinct

Proposed new format (again, all formatting here is just for ease of humans
reading):

' [ {"attributes" : [2,3], "ndistinct" : 4},
    {"attributes" : [2,-1], "ndistinct" : 4},
    {"attributes" : [2,-2], "ndistinct" : 4},
    {"attributes" : [3,-1], "ndistinct" : 4},
    {"attributes" : [3,-2], "ndistinct" : 4},
    {"attributes" : [-1,-2], "ndistinct" : 3},
    {"attributes" : [2,3,-1], "ndistinct" : 4},
    {"attributes" : [2,3,-2], "ndistinct" : 4},
    {"attributes" : [2,-1,-2], "ndistinct" : 4},
    {"attributes" : [3,-1,-2], "ndistinct" : 4}]'::pg_ndistinct

The pg_dependencies structure is only slightly more complex:

An abbreviated example:

{"2 => 1": 1.000000, "2 => -1": 1.000000, ..., "2, -2 => -1": 1.000000, "3,
-1 => 2": 1.000000},

Becomes:

[ {"attributes": [2], "dependency": 1, "degree": 1.000000},
  {"attributes": [2], "dependency": -1, "degree": 1.000000},
  {"attributes": [2, -2], "dependency":  -1, "degree": 1.000000},
   ...,
   {"attributes": [2, -2], "dependency": -1, "degree": 1.000000},
   {"attributes": [3, -1], "dependency": 2, "degree": 1.000000}]

Any thoughts on using/improving these structures?

---

From: Corey Huinker <[email-masked]>
Date: Sat, 21 Jun 2025 20:12:28 -0400

> Any thoughts on using/improving these structures?

Hearing no objections, here is the latest patchset.

0001 - Changes input/output functions of pg_ndistinct to the format
described earlier.
0002 - Changes input/output functions of pg_dependencies to the format
described earlier.
0003 - Makes some previously internal/static attribute stats functions
visible to extended_stats.c, because the exprs attribute is basically an
array of partially filled-out pg_statistic rows.
0004 - Adds pg_restore_attribute_stats(), pg_clear_attribute_stats(), in
the pattern of their relation/attribute brethren.
0005 - adds the dumping and restoring of extended statistics back to v10.
No command line flag changes needed.
From f8bd684ec5bcec57505696907e34e4f640b15da4 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Sat, 7 Jun 2025 23:17:03 -0400
Subject: [PATCH v4 1/5] Refactor output format of pg_ndistinct and add working
 input function.

The existing format of pg_ndistinct uses a single-object JSON structure
where each key is itself a comma-separated list of attnums. While this
is a very compact format, it's confusing to read and is difficult to
manipulate values within the object. This wasn't a concern until
statistics import functions were introduced, enabling users to inject
hypothetical statistics into an object to observe their effect on the
query planner.

The new format is an array of objects, each object must have the keys
"attributes", which must contain an array of attnums, and "ndistinct",
which must be an integer. This is a quirk because the underlying
internal storage is a double, but the value stored was always an
integer.

The change in format is adequately described from the changes to
src/test/regress/expected/stats_ext.out so description here is
redundant.
---
 src/backend/statistics/mvdistinct.c     | 463 +++++++++++++++++++++++-
 src/test/regress/expected/stats_ext.out |  56 ++-
 src/test/regress/sql/stats_ext.sql      |  12 +
 3 files changed, 503 insertions(+), 28 deletions(-)

diff --git a/src/backend/statistics/mvdistinct.c b/src/backend/statistics/mvdistinct.c
index 7e7a63405c8..003dc3a74ab 100644
--- a/src/backend/statistics/mvdistinct.c
+++ b/src/backend/statistics/mvdistinct.c
@@ -27,9 +27,15 @@
 
@@ -328,28 +334,453 @@ statext_ndistinct_deserialize(bytea *data)
 	return ndistinct;
 }
 
+typedef enum
+{
+	NDIST_EXPECT_START = 0,
+	NDIST_EXPECT_ITEM,
+	NDIST_EXPECT_KEY,
+	NDIST_EXPECT_ATTNUM_LIST,
+	NDIST_EXPECT_ATTNUM,
+	NDIST_EXPECT_NDISTINCT,
+	NDIST_EXPECT_COMPLETE
+}			ndistinctSemanticState;
+
+typedef struct
+{
+	const char *str;
+	ndistinctSemanticState state;
+
+	List	   *distinct_items; /* Accumulated complete MVNDistinctItems */
+	Node	   *escontext;
+
+	bool		found_attributes;	/* Item has an attributes key */
+	bool		found_ndistinct;	/* Item has ndistinct key */
+	List	   *attnum_list;	/* Accumulated attributes attnums */
+	int64		ndistinct;
+}			ndistinctParseState;
+
+/*
+ * Invoked at the start of each MVNDistinctItem.
+ *
+ * The entire JSON document shoul be one array of MVNDistinctItem objects.
+ *
+ * If we're anywhere else in the document, it's an error.
+ */
+static JsonParseErrorType
+ndistinct_object_start(void *state)
+{
+	ndistinctParseState *parse = state;
+
+	if (parse->state != NDIST_EXPECT_ITEM)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+				 errdetail("Expected Item object")));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	/* Now we expect to see attributes/ndistinct keys */
+	parse->state = NDIST_EXPECT_KEY;
+	return JSON_SUCCESS;
+}
+
+/*
+ * Routine to allow qsorting of AttNumbers
+ */
+static int
+attnum_compare(const void *aptr, const void *bptr)
+{
+	AttrNumber	a = *(const AttrNumber *) aptr;
+	AttrNumber	b = *(const AttrNumber *) bptr;
+
+	return pg_cmp_s16(a, b);
+}
+
+
+/*
+ * Invoked at the end of an object.
+ *
+ * Check to ensure that it was a complete MVNDistinctItem
+ *
+ */
+static JsonParseErrorType
+ndistinct_object_end(void *state)
+{
+	ndistinctParseState *parse = state;
+
+	int			natts = 0;
+	AttrNumber *attrsort;
+
+	MVNDistinctItem *item;
+
+	if (!parse->found_attributes)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+				 errdetail("Item must contain \"attributes\" key")));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	if (!parse->found_ndistinct)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+				 errdetail("Item must contain \"ndistinct\" key")));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	if (parse->attnum_list == NIL)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+				 errdetail("The \"attributes\" key must be an non-empty array")));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	/*
+	 * We need at least 2 attnums for a ndistinct item, anything less is
+	 * malformed.
+	 */
+	natts = parse->attnum_list->length;
+	if (natts < 2)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+				 errdetail("The attributes key must contain an array of at least two attnums")));
+
+		return JSON_SEM_ACTION_FAILED;
+	}
+	attrsort = palloc0(natts * sizeof(AttrNumber));
+
+	/* Create the MVNDistinctItem */
+	item = palloc(sizeof(MVNDistinctItem));
+	item->nattributes = natts;
+	item->attributes = palloc0(natts * sizeof(AttrNumber));
+	item->ndistinct = (double) parse->ndistinct;
+
+	/* fill out both attnum list and sortable list */
+	for (int i = 0; i < natts; i++)
+	{
+		attrsort[i] = (AttrNumber) parse->attnum_list->elements[i].int_value;
+		item->attributes[i] = attrsort[i];
+	}
+
+	/* Check attrsort for uniqueness */
+	qsort(attrsort, natts, sizeof(AttrNumber), attnum_compare);
+	for (int i = 1; i < natts; i++)
+		if (attrsort[i] == attrsort[i - 1])
+		{
+			ereturn(parse->escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					 errdetail("attnum list duplicate value found: %d", attrsort[i])));
+
+			return JSON_SEM_ACTION_FAILED;
+		}
+	pfree(attrsort);
+
+	parse->distinct_items = lappend(parse->distinct_items, (void *) item);
+
+	/* reset item state vars */
+	list_free(parse->attnum_list);
+	parse->attnum_list = NIL;
+	parse->ndistinct = 0;
+	parse->found_attributes = false;
+	parse->found_ndistinct = false;
+
+	/* Now we are looking for the next MVNDistinctItem */
+	parse->state = NDIST_EXPECT_ITEM;
+	return JSON_SUCCESS;
+}
+
+
+/*
+ * ndsitinct input format has two types of arrays, the outer MVNDistinctItem
+ * array, and the attnum list array within each MVNDistinctItem.
+ */
+static JsonParseErrorType
+ndistinct_array_start(void *state)
+{
+	ndistinctParseState *parse = state;
+
+	switch (parse->state)
+	{
+		case NDIST_EXPECT_ATTNUM_LIST:
+			parse->state = NDIST_EXPECT_ATTNUM;
+			break;
+
+		case NDIST_EXPECT_START:
+			parse->state = NDIST_EXPECT_ITEM;
+			break;
+
+		default:
+			ereturn(parse->escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					 errdetail("Array found in unexpected place")));
+			return JSON_SEM_ACTION_FAILED;
+	}
+
+	return JSON_SUCCESS;
+}
+
+
+static JsonParseErrorType
+ndistinct_array_end(void *state)
+{
+	ndistinctParseState *parse = state;
+
+	/* The attnum list is complete, look for more MVNDistinctItem keys */
+	if (parse->state == NDIST_EXPECT_ATTNUM)
+	{
+		parse->state = NDIST_EXPECT_KEY;
+		return JSON_SUCCESS;
+	}
+
+	if (parse->state == NDIST_EXPECT_ITEM)
+	{
+		parse->state = NDIST_EXPECT_COMPLETE;
+		return JSON_SUCCESS;
+	}
+
+	ereturn(parse->escontext, (Datum) 0,
+			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+			 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+			 errdetail("Array found in unexpected place")));
+	return JSON_SEM_ACTION_FAILED;
+}
+
+
+/*
+ * The valid keys for the MVNDistinctItem object are:
+ *   - attributes
+ *   - ndistinct
+ */
+static JsonParseErrorType
+ndistinct_object_field_start(void *state, char *fname, bool isnull)
+{
+	ndistinctParseState *parse = state;
+
+	const char *attributes = "attributes";
+	const char *ndistinct = "ndistinct";
+
+	if (strcmp(fname, attributes) == 0)
+	{
+		parse->found_attributes = true;
+		parse->state = NDIST_EXPECT_ATTNUM_LIST;
+		return JSON_SUCCESS;
+	}
+
+	if (strcmp(fname, ndistinct) == 0)
+	{
+		parse->found_ndistinct = true;
+		parse->state = NDIST_EXPECT_NDISTINCT;
+		return JSON_SUCCESS;
+	}
+
+	ereturn(parse->escontext, (Datum) 0,
+			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+			 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+			 errdetail("Only allowed keys are \%s\" and \%s\".", attributes, ndistinct)));
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ *
+ */
+static JsonParseErrorType
+ndistinct_array_element_start(void *state, bool isnull)
+{
+	ndistinctParseState *parse = state;
+
+	if (parse->state == NDIST_EXPECT_ATTNUM)
+	{
+		if (isnull)
+		{
+			ereturn(parse->escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					 errdetail("Attnum list elements cannot be null.")));
+
+			return JSON_SEM_ACTION_FAILED;
+		}
+		return JSON_SUCCESS;
+	}
+
+	if (parse->state == NDIST_EXPECT_ITEM)
+	{
+		if (isnull)
+		{
+			ereturn(parse->escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					 errdetail("Item list elements cannot be null.")));
+
+			return JSON_SEM_ACTION_FAILED;
+		}
+
+		return JSON_SUCCESS;
+	}
+
+	ereturn(parse->escontext, (Datum) 0,
+			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+			 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+			 errdetail("Unexpected array element.")));
+
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ * Handle scalar events from the ndistinct input parser.
+ *
+ */
+static JsonParseErrorType
+ndistinct_scalar(void *state, char *token, JsonTokenType tokentype)
+{
+	ndistinctParseState *parse = state;
+
+	if (parse->state == NDIST_EXPECT_ATTNUM)
+	{
+		AttrNumber	attnum = pg_strtoint16_safe(token, parse->escontext);
+
+		if (SOFT_ERROR_OCCURRED(parse->escontext))
+			return JSON_SEM_ACTION_FAILED;
+
+		parse->attnum_list = lappend_int(parse->attnum_list, (int) attnum);
+		return JSON_SUCCESS;
+	}
+
+	if (parse->state == NDIST_EXPECT_NDISTINCT)
+	{
+		/*
+		 * While the structure dictates that ndistinct in a double precision
+		 * floating point, in practice it has always been an integer, and it
+		 * is output as such. Therefore, we follow usage precendent over the
+		 * actual storage structure, and read it in as an integer.
+		 */
+		parse->ndistinct = pg_strtoint64_safe(token, parse->escontext);
+
+		if (SOFT_ERROR_OCCURRED(parse->escontext))
+			return JSON_SEM_ACTION_FAILED;
+
+		parse->state = NDIST_EXPECT_KEY;
+		return JSON_SUCCESS;
+	}
+
+	ereturn(parse->escontext, (Datum) 0,
+			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+			 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+			 errdetail("Unexpected scalar.")));
+
+	return JSON_SEM_ACTION_FAILED;
+}
+
 /*
  * pg_ndistinct_in
  *		input routine for type pg_ndistinct
  *
- * pg_ndistinct is real enough to be a table column, but it has no
- * operations of its own, and disallows input (just like pg_node_tree).
+ * example input:
+ *     [{"attributes": [6, -1], "ndistinct": 14},
+ *      {"attributes": [6, -2], "ndistinct": 9143},
+ *      {"attributes": [-1,-2], "ndistinct": 13454},
+ *      {"attributes": [6, -1, -2], "ndistinct": 14549}]
  */
 Datum
 pg_ndistinct_in(PG_FUNCTION_ARGS)
 {
-	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot accept a value of type %s", "pg_ndistinct")));
+	char	   *str = PG_GETARG_CSTRING(0);
 
+	parse_state.state = NDIST_EXPECT_START;
+	parse_state.found_attributes = false;
+	parse_state.found_ndistinct = false;
+	parse_state.attnum_list = NIL;
+	parse_state.ndistinct = 0;
+	sem_action.object_end = ndistinct_object_end;
+	sem_action.array_end = ndistinct_array_end;
 
 /*
  * pg_ndistinct
  *		output routine for type pg_ndistinct
  *
- * Produces a human-readable representation of the value.
+ * Produces a human-readable representation of the value, in the format:
+ *   [{"attributes": [attnum,. ..], "ndistinct": int}, ...]
+ *
  */
 Datum
 pg_ndistinct_out(PG_FUNCTION_ARGS)
@@ -360,26 +791,26 @@ pg_ndistinct_out(PG_FUNCTION_ARGS)
 	StringInfoData str;
 
 	initStringInfo(&str);
-	appendStringInfoChar(&str, '{');
+	appendStringInfoChar(&str, '[');
 
 	for (i = 0; i < ndist->nitems; i++)
 	{
-		int			j;
 		MVNDistinctItem item = ndist->items[i];
 
 		if (i > 0)
 			appendStringInfoString(&str, ", ");
 
-		for (j = 0; j < item.nattributes; j++)
-		{
-			AttrNumber	attnum = item.attributes[j];
+		Assert(item.nattributes > 0);	/* TODO: elog? */
 
-			appendStringInfo(&str, "%s%d", (j == 0) ? "\"" : ", ", attnum);
-		}
-		appendStringInfo(&str, "\": %d", (int) item.ndistinct);
+		appendStringInfo(&str, "{\"attributes\": [%d", item.attributes[0]);
+
+		for (int j = 1; j < item.nattributes; j++)
+			appendStringInfo(&str, ", %d", item.attributes[j]);
+
+		appendStringInfo(&str, "], \"ndistinct\": %d}", (int) item.ndistinct);
 	}
 
-	appendStringInfoChar(&str, '}');
+	appendStringInfoChar(&str, ']');
 
 	PG_RETURN_CSTRING(str.data);
 }
diff --git a/src/test/regress/expected/stats_ext.out b/src/test/regress/expected/stats_ext.out
index 6359e5fb689..ae79eb57c67 100644
--- a/src/test/regress/expected/stats_ext.out
+++ b/src/test/regress/expected/stats_ext.out
@@ -447,9 +447,9 @@ SELECT s.stxkind, d.stxdndistinct
   FROM pg_statistic_ext s, pg_statistic_ext_data d
  WHERE s.stxrelid = 'ndistinct'::regclass
    AND d.stxoid = s.oid;
- stxkind |                    stxdndistinct                    
----------+-----------------------------------------------------
- {d,f,m} | {"3, 4": 11, "3, 6": 11, "4, 6": 11, "3, 4, 6": 11}
+ stxkind |                                                                              stxdndistinct                                                                              
+---------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ {d,f,m} | [{"attributes": [3, 4], "ndistinct": 11}, {"attributes": [3, 6], "ndistinct": 11}, {"attributes": [4, 6], "ndistinct": 11}, {"attributes": [3, 4, 6], "ndistinct": 11}]
 (1 row)
 
 -- minor improvement, make sure the ctid does not break the matching
@@ -529,9 +529,9 @@ SELECT s.stxkind, d.stxdndistinct
   FROM pg_statistic_ext s, pg_statistic_ext_data d
  WHERE s.stxrelid = 'ndistinct'::regclass
    AND d.stxoid = s.oid;
- stxkind |                      stxdndistinct                       
----------+----------------------------------------------------------
- {d,f,m} | {"3, 4": 221, "3, 6": 247, "4, 6": 323, "3, 4, 6": 1000}
+ stxkind |                                                                                stxdndistinct                                                                                 
+---------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ {d,f,m} | [{"attributes": [3, 4], "ndistinct": 221}, {"attributes": [3, 6], "ndistinct": 247}, {"attributes": [4, 6], "ndistinct": 323}, {"attributes": [3, 4, 6], "ndistinct": 1000}]
 (1 row)
 
 -- correct estimates
@@ -678,9 +678,9 @@ SELECT s.stxkind, d.stxdndistinct
   FROM pg_statistic_ext s, pg_statistic_ext_data d
  WHERE s.stxrelid = 'ndistinct'::regclass
    AND d.stxoid = s.oid;
- stxkind |                           stxdndistinct                           
----------+-------------------------------------------------------------------
- {d,e}   | {"-1, -2": 221, "-1, -3": 247, "-2, -3": 323, "-1, -2, -3": 1000}
+ stxkind |                                                                                     stxdndistinct                                                                                     
+---------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ {d,e}   | [{"attributes": [-1, -2], "ndistinct": 221}, {"attributes": [-1, -3], "ndistinct": 247}, {"attributes": [-2, -3], "ndistinct": 323}, {"attributes": [-1, -2, -3], "ndistinct": 1000}]
 (1 row)
 
 SELECT * FROM check_estimated_rows('SELECT COUNT(*) FROM ndistinct GROUP BY (a+1), (b+100)');
@@ -727,9 +727,9 @@ SELECT s.stxkind, d.stxdndistinct
   FROM pg_statistic_ext s, pg_statistic_ext_data d
  WHERE s.stxrelid = 'ndistinct'::regclass
    AND d.stxoid = s.oid;
- stxkind |                        stxdndistinct                        
----------+-------------------------------------------------------------
- {d,e}   | {"3, 4": 221, "3, -1": 247, "4, -1": 323, "3, 4, -1": 1000}
+ stxkind |                                                                                  stxdndistinct                                                                                  
+---------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ {d,e}   | [{"attributes": [3, 4], "ndistinct": 221}, {"attributes": [3, -1], "ndistinct": 247}, {"attributes": [4, -1], "ndistinct": 323}, {"attributes": [3, 4, -1], "ndistinct": 1000}]
 (1 row)
 
 SELECT * FROM check_estimated_rows('SELECT COUNT(*) FROM ndistinct GROUP BY a, b');
@@ -3455,4 +3455,36 @@ SELECT FROM sb_1 LEFT JOIN sb_2
 
 RESET enable_nestloop;
 RESET enable_mergejoin;
+-- Test input function of pg_ndistinct.
+SELECT '[{"attributes" : [2,3], "ndistinct" : 4},
+         {"attributes" : [2,-1], "ndistinct" : 4},
+         {"attributes" : [2,3,-1], "ndistinct" : 4},
+         {"attributes" : [1,3,-1,-2], "ndistinct" : 4}]'::pg_ndistinct;
+                                                                                 pg_ndistinct                                                                                  
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ [{"attributes": [2, 3], "ndistinct": 4}, {"attributes": [2, -1], "ndistinct": 4}, {"attributes": [2, 3, -1], "ndistinct": 4}, {"attributes": [1, 3, -1, -2], "ndistinct": 4}]
+(1 row)
+
+-- error, cannot duplicate attribute
+SELECT '[{"attributes" : [2,3], "ndistinct" : 4},
+         {"attributes" : [2,-1], "ndistinct" : 4},
+         {"attributes" : [2,3,2], "ndistinct" : 4},
+         {"attributes" : [1,3,-1,-2], "ndistinct" : 4}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : 4},
+         {"attributes" : [2,-1], "ndistinct" : 4},
+         {"attributes" : [2,3,2], "ndistinct" : 4},
+         {"attributes" : [1,3,-1,-2], "ndistinct" : 4}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "ndistinct" : 4},
+               ^
+DETAIL:  attnum list duplicate value found: 2
+-- Test input function of pg_dependencies.
+SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": 1.0000},
+         {"attributes" : [2,-1], "dependency" : 4, "degree": 0.0000},
+         {"attributes" : [2,3,-1], "dependency" : 4, "degree": 0.5000},
+         {"attributes" : [1,3,-1,-2], "dependency" : 4, "degree": 1.0000}]'::pg_dependencies;
+                                                                                                                          pg_dependencies                                                                                                                          
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ [{"attributes": [2, 3], "dependency": 4, "degree": 1.000000}, {"attributes": [2, -1], "dependency": 4, "degree": 0.000000}, {"attributes": [2, 3, -1], "dependency": 4, "degree": 0.500000}, {"attributes": [1, 3, -1, -2], "dependency": 4, "degree": 1.000000}]
+(1 row)
+
 DROP TABLE sb_1, sb_2 CASCADE;
diff --git a/src/test/regress/sql/stats_ext.sql b/src/test/regress/sql/stats_ext.sql
index da4f2fe9c93..4ffc33d8457 100644
--- a/src/test/regress/sql/stats_ext.sql
+++ b/src/test/regress/sql/stats_ext.sql
@@ -1758,4 +1758,16 @@ SELECT FROM sb_1 LEFT JOIN sb_2
 RESET enable_nestloop;
 RESET enable_mergejoin;
 
+-- Test input function of pg_ndistinct.
+SELECT '[{"attributes" : [2,3], "ndistinct" : 4},
+         {"attributes" : [2,-1], "ndistinct" : 4},
+         {"attributes" : [2,3,-1], "ndistinct" : 4},
+         {"attributes" : [1,3,-1,-2], "ndistinct" : 4}]'::pg_ndistinct;
+
+-- error, cannot duplicate attribute
+SELECT '[{"attributes" : [2,3], "ndistinct" : 4},
+         {"attributes" : [2,-1], "ndistinct" : 4},
+         {"attributes" : [2,3,2], "ndistinct" : 4},
+         {"attributes" : [1,3,-1,-2], "ndistinct" : 4}]'::pg_ndistinct;
+
 DROP TABLE sb_1, sb_2 CASCADE;

base-commit: ea06263c4aa5abadc97a6928c6b2aff0e29698ae
-- 
2.49.0

From 86c0ef5871e7e9068478d1bd8f34da11aeabba43 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Sun, 8 Jun 2025 00:15:40 -0400
Subject: [PATCH v4 2/5] Refactor output format of pg_dependencies and add
 working input function.

The existing format of pg_dependencies uses a single-object JSON structure
where each key is itself a comma-separated list of attnums. While this
is a very compact format, it's confusing to read and is difficult to
manipulate values within the object. This wasn't a concern until
statistics import functions were introduced, enabling users to inject
hypothetical statistics into an object to observe their effect on the
query planner.

The new format is an array of objects, each object must have the keys
"attributes", which must contain an array of attnums, "dependency",
which must be an integer, and "degree", which must be a float.

The change in format is adequately described from the changes to
src/test/regress/expected/stats_ext.out so description here is
redundant.
---
 src/backend/statistics/dependencies.c   | 491 ++++++++++++++++++++++--
 src/test/regress/expected/stats_ext.out |  24 +-
 src/test/regress/sql/stats_ext.sql      |  12 +
 3 files changed, 496 insertions(+), 31 deletions(-)

diff --git a/src/backend/statistics/dependencies.c b/src/backend/statistics/dependencies.c
index eb2fc4366b4..fd6125fc9da 100644
--- a/src/backend/statistics/dependencies.c
+++ b/src/backend/statistics/dependencies.c
@@ -13,18 +13,26 @@
  */
 #include "postgres.h"
 
@@ -643,24 +651,459 @@ statext_dependencies_load(Oid mvoid, bool inh)
 	return result;
 }
 
+typedef enum
+{
+	DEPS_EXPECT_START = 0,
+	DEPS_EXPECT_ITEM,
+	DEPS_EXPECT_KEY,
+	DEPS_EXPECT_ATTNUM_LIST,
+	DEPS_EXPECT_ATTNUM,
+	DEPS_EXPECT_DEPENDENCY,
+	DEPS_EXPECT_DEGREE,
+	DEPS_PARSE_COMPLETE
+}			depsParseSemanticState;
+
+typedef struct
+{
+	const char *str;
+	depsParseSemanticState state;
+
+	List	   *dependency_list;
+	Node	   *escontext;
+
+	bool		found_attributes;	/* Item has an attributes key */
+	bool		found_dependency;	/* Item has an dependency key */
+	bool		found_degree;	/* Item has degree key */
+	List	   *attnum_list;	/* Accumulated attributes attnums */
+	AttrNumber	dependency;
+	double		degree;
+}			dependenciesParseState;
+
+/*
+ * Invoked at the start of each MVDependency object.
+ *
+ * The entire JSON document shoul be one array of MVDependency objects.
+ *
+ * If we're anywhere else in the document, it's an error.
+ */
+static JsonParseErrorType
+dependencies_object_start(void *state)
+{
+	dependenciesParseState *parse = state;
+
+	if (parse->state != DEPS_EXPECT_ITEM)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				 errdetail("Expected Item object")));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	/* Now we expect to see attributes/dependency/degree keys */
+	parse->state = DEPS_EXPECT_KEY;
+	return JSON_SUCCESS;
+}
+
+static int
+attnum_compare(const void *aptr, const void *bptr)
+{
+	AttrNumber	a = *(const AttrNumber *) aptr;
+	AttrNumber	b = *(const AttrNumber *) bptr;
+
+	return pg_cmp_s16(a, b);
+}
+
+static JsonParseErrorType
+dependencies_object_end(void *state)
+{
+	dependenciesParseState *parse = state;
+
+	MVDependency *dep;
+	AttrNumber *attrsort;
+
+	int			natts = 0;
+
+	if (!parse->found_attributes)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				 errdetail("Item must contain \"attributes\" key")));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	if (!parse->found_dependency)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				 errdetail("Item must contain \"dependencies\" key")));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	if (!parse->found_degree)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				 errdetail("Item must contain \"degree\" key")));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	if (parse->attnum_list == NIL)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				 errdetail("The \"attributes\" key must be an non-empty array")));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	/*
+	 * We need at least 1 attnum for a dependencies item, anything less is
+	 * malformed.
+	 */
+	natts = parse->attnum_list->length;
+	if (natts < 1)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				 errdetail("The attributes key must contain an array of at least one attnum")));
+
+		return JSON_SEM_ACTION_FAILED;
+	}
+	attrsort = palloc0(natts * sizeof(AttrNumber));
+
+	/*
+	 * Allocate enough space for the dependency, the attnums in the list, plus
+	 * the final attnum
+	 */
+	dep = palloc0(offsetof(MVDependency, attributes) + ((natts + 1) * sizeof(AttrNumber)));
+	dep->nattributes = natts + 1;
+
+	dep->attributes[natts] = parse->dependency;
+	dep->degree = parse->degree;
+
+	attrsort = palloc0(dep->nattributes * sizeof(AttrNumber));
+	attrsort[natts] = parse->dependency;
+
+	for (int i = 0; i < natts; i++)
+	{
+		attrsort[i] = (AttrNumber) parse->attnum_list->elements[i].int_value;
+		dep->attributes[i] = attrsort[i];
+	}
+
+	/* Check attrsort for uniqueness */
+	qsort(attrsort, natts + 1, sizeof(AttrNumber), attnum_compare);
+	for (int i = 1; i < dep->nattributes; i++)
+		if (attrsort[i] == attrsort[i - 1])
+		{
+			ereturn(parse->escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					 errdetail("attnum list duplicate value found: %d", attrsort[i])));
+
+			return JSON_SEM_ACTION_FAILED;
+		}
+	pfree(attrsort);
+
+	parse->dependency_list = lappend(parse->dependency_list, (void *) dep);
+
+	/* reset dep item state vars */
+	list_free(parse->attnum_list);
+	parse->attnum_list = NIL;
+	parse->dependency = 0;
+	parse->degree = 0.0;
+	parse->found_attributes = false;
+	parse->found_dependency = false;
+	parse->found_degree = false;
+
+	/* Now we are looking for the next MVDependency */
+	parse->state = DEPS_EXPECT_ITEM;
+	return JSON_SUCCESS;
+}
+
+/*
+ * dependencies input format does not have arrays, so any array elements encountered
+ * are an error.
+ */
+static JsonParseErrorType
+dependencies_array_start(void *state)
+{
+	dependenciesParseState *parse = state;
+
+	switch (parse->state)
+	{
+		case DEPS_EXPECT_ATTNUM_LIST:
+			parse->state = DEPS_EXPECT_ATTNUM;
+			break;
+		case DEPS_EXPECT_START:
+			parse->state = DEPS_EXPECT_ITEM;
+			break;
+		default:
+			ereturn(parse->escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					 errdetail("Array found in unexpected place")));
+			return JSON_SEM_ACTION_FAILED;
+	}
+
+	return JSON_SUCCESS;
+}
+
+/*
+ * Either the end of an attnum list or the whole object
+ */
+static JsonParseErrorType
+dependencies_array_end(void *state)
+{
+	dependenciesParseState *parse = state;
+
+	switch (parse->state)
+	{
+		case DEPS_EXPECT_ATTNUM:
+			parse->state = DEPS_EXPECT_KEY;
+			break;
+
+		case DEPS_EXPECT_ITEM:
+			parse->state = DEPS_PARSE_COMPLETE;
+			break;
+
+		default:
+			ereturn(parse->escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					 errdetail("Array found in unexpected place")));
+			return JSON_SEM_ACTION_FAILED;
+	}
+	return JSON_SUCCESS;
+}
+
+/*
+ * The valid keys for the MVDependency object are:
+ *   - attributes
+ *   - depeendency
+ *   - degree
+ */
+static JsonParseErrorType
+dependencies_object_field_start(void *state, char *fname, bool isnull)
+{
+	dependenciesParseState *parse = state;
+
+	const char *attributes = "attributes";
+	const char *dependency = "dependency";
+	const char *degree = "degree";
+
+	if (strcmp(fname, attributes) == 0)
+	{
+		parse->found_attributes = true;
+		parse->state = DEPS_EXPECT_ATTNUM_LIST;
+		return JSON_SUCCESS;
+	}
+
+	if (strcmp(fname, dependency) == 0)
+	{
+		parse->found_dependency = true;
+		parse->state = DEPS_EXPECT_DEPENDENCY;
+		return JSON_SUCCESS;
+	}
+
+	if (strcmp(fname, degree) == 0)
+	{
+		parse->found_degree = true;
+		parse->state = DEPS_EXPECT_DEGREE;
+		return JSON_SUCCESS;
+	}
+
+	ereturn(parse->escontext, (Datum) 0,
+			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+			 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+			 errdetail("Only allowed keys are \%s\", \"%s\" and \%s\".",
+					   attributes, dependency, degree)));
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ * ndsitinct input format does not have arrays, so any array elements encountered
+ * are an error.
+ */
+static JsonParseErrorType
+dependencies_array_element_start(void *state, bool isnull)
+{
+	dependenciesParseState *parse = state;
+
+	if (parse->state == DEPS_EXPECT_ATTNUM)
+	{
+		if (isnull)
+		{
+			ereturn(parse->escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					 errdetail("Attnum list elements cannot be null.")));
+
+			return JSON_SEM_ACTION_FAILED;
+		}
+		return JSON_SUCCESS;
+	}
+
+	if (parse->state == DEPS_EXPECT_ITEM)
+	{
+		if (isnull)
+		{
+			ereturn(parse->escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					 errdetail("Item list elements cannot be null.")));
+
+			return JSON_SEM_ACTION_FAILED;
+		}
+
+		return JSON_SUCCESS;
+	}
+
+	ereturn(parse->escontext, (Datum) 0,
+			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+			 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+			 errdetail("Unexpected array element.")));
+
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ * Handle scalar events from the dependencies input parser.
+ *
+ * There is only one case where we will encounter a scalar, and that is the
+ * dependency degree for the previous object key.
+ */
+static JsonParseErrorType
+dependencies_scalar(void *state, char *token, JsonTokenType tokentype)
+{
+	dependenciesParseState *parse = state;
+
+	if (parse->state == DEPS_EXPECT_ATTNUM)
+	{
+		AttrNumber	attnum = pg_strtoint16_safe(token, parse->escontext);
+
+		if (SOFT_ERROR_OCCURRED(parse->escontext))
+			return JSON_SEM_ACTION_FAILED;
+
+		parse->attnum_list = lappend_int(parse->attnum_list, (int) attnum);
+		return JSON_SUCCESS;
+	}
+
+	if (parse->state == DEPS_EXPECT_DEPENDENCY)
+	{
+		parse->dependency = (AttrNumber) pg_strtoint16_safe(token, parse->escontext);
+
+		if (SOFT_ERROR_OCCURRED(parse->escontext))
+			return JSON_SEM_ACTION_FAILED;
+
+		return JSON_SUCCESS;
+	}
+
+
+	if (parse->state == DEPS_EXPECT_DEGREE)
+	{
+		parse->degree = float8in_internal(token, NULL, "double",
+										  token, parse->escontext);
+
+		if (SOFT_ERROR_OCCURRED(parse->escontext))
+			return JSON_SEM_ACTION_FAILED;
+
+		return JSON_SUCCESS;
+	}
+
+	ereturn(parse->escontext, (Datum) 0,
+			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+			 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+			 errdetail("Unexpected scalar.")));
+	return JSON_SEM_ACTION_FAILED;
+}
+
 /*
  * pg_dependencies_in		- input routine for type pg_dependencies.
  *
- * pg_dependencies is real enough to be a table column, but it has no operations
- * of its own, and disallows input too
+ * This format is valid JSON, with the expected format:
+ *    [{"attributes": [1,2], "dependency": -1, "degree": 1.0000},
+ *     {"attributes": [1,-1], "dependency": 2, "degree": 0.0000},
+ *     {"attributes": [2,-1], "dependency": 1, "degree": 1.0000}]
+ *
  */
 Datum
 pg_dependencies_in(PG_FUNCTION_ARGS)
 {
-	/*
-	 * pg_node_list stores the data in binary form and parsing text input is
-	 * not needed, so disallow this.
-	 */
-	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot accept a value of type %s", "pg_dependencies")));
+	char	   *str = PG_GETARG_CSTRING(0);
 
+	parse_state.state = DEPS_EXPECT_START;
+	parse_state.attnum_list = NIL;
+	parse_state.dependency = 0;
+	parse_state.degree = 0.0;
+	parse_state.found_attributes = false;
+	parse_state.found_dependency = false;
+	parse_state.found_degree = false;
+	sem_action.object_end = dependencies_object_end;
+	sem_action.array_end = dependencies_array_end;
 
 /*
@@ -671,34 +1114,32 @@ pg_dependencies_out(PG_FUNCTION_ARGS)
 {
 	bytea	   *data = PG_GETARG_BYTEA_PP(0);
 	MVDependencies *dependencies = statext_dependencies_deserialize(data);
-	int			i,
-				j;
 	StringInfoData str;
 
 	initStringInfo(&str);
-	appendStringInfoChar(&str, '{');
+	appendStringInfoChar(&str, '[');
 
-	for (i = 0; i < dependencies->ndeps; i++)
+	for (int i = 0; i < dependencies->ndeps; i++)
 	{
 		MVDependency *dependency = dependencies->deps[i];
 
 		if (i > 0)
 			appendStringInfoString(&str, ", ");
 
-		appendStringInfoChar(&str, '"');
-		for (j = 0; j < dependency->nattributes; j++)
-		{
-			if (j == dependency->nattributes - 1)
-				appendStringInfoString(&str, " => ");
-			else if (j > 0)
-				appendStringInfoString(&str, ", ");
+		Assert(dependency->nattributes > 1);	/* TODO: elog? */
 
-			appendStringInfo(&str, "%d", dependency->attributes[j]);
-		}
-		appendStringInfo(&str, "\": %f", dependency->degree);
+		appendStringInfo(&str, "{\"attributes\": [%d",
+						 dependency->attributes[0]);
+
+		for (int j = 1; j < dependency->nattributes - 1; j++)
+			appendStringInfo(&str, ", %d", dependency->attributes[j]);
+
+		appendStringInfo(&str, "], \"dependency\": %d, \"degree\": %f}",
+						 dependency->attributes[dependency->nattributes - 1],
+						 dependency->degree);
 	}
 
-	appendStringInfoChar(&str, '}');
+	appendStringInfoChar(&str, ']');
 
 	PG_RETURN_CSTRING(str.data);
 }
diff --git a/src/test/regress/expected/stats_ext.out b/src/test/regress/expected/stats_ext.out
index ae79eb57c67..babfe4acda0 100644
--- a/src/test/regress/expected/stats_ext.out
+++ b/src/test/regress/expected/stats_ext.out
@@ -1281,9 +1281,9 @@ CREATE STATISTICS func_deps_stat (dependencies) ON a, b, c FROM functional_depen
 ANALYZE functional_dependencies;
 -- print the detected dependencies
 SELECT dependencies FROM pg_stats_ext WHERE statistics_name = 'func_deps_stat';
-                                                dependencies                                                
-------------------------------------------------------------------------------------------------------------
- {"3 => 4": 1.000000, "3 => 6": 1.000000, "4 => 6": 1.000000, "3, 4 => 6": 1.000000, "3, 6 => 4": 1.000000}
+                                                                                                                                               dependencies                                                                                                                                               
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ [{"attributes": [3], "dependency": 4, "degree": 1.000000}, {"attributes": [3], "dependency": 6, "degree": 1.000000}, {"attributes": [4], "dependency": 6, "degree": 1.000000}, {"attributes": [3, 4], "dependency": 6, "degree": 1.000000}, {"attributes": [3, 6], "dependency": 4, "degree": 1.000000}]
 (1 row)
 
 SELECT * FROM check_estimated_rows('SELECT * FROM functional_dependencies WHERE a = 1 AND b = ''1''');
@@ -1623,9 +1623,9 @@ CREATE STATISTICS func_deps_stat (dependencies) ON (a * 2), upper(b), (c + 1) FR
 ANALYZE functional_dependencies;
 -- print the detected dependencies
 SELECT dependencies FROM pg_stats_ext WHERE statistics_name = 'func_deps_stat';
-                                                      dependencies                                                      
-------------------------------------------------------------------------------------------------------------------------
- {"-1 => -2": 1.000000, "-1 => -3": 1.000000, "-2 => -3": 1.000000, "-1, -2 => -3": 1.000000, "-1, -3 => -2": 1.000000}
+                                                                                                                                                     dependencies                                                                                                                                                     
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ [{"attributes": [-1], "dependency": -2, "degree": 1.000000}, {"attributes": [-1], "dependency": -3, "degree": 1.000000}, {"attributes": [-2], "dependency": -3, "degree": 1.000000}, {"attributes": [-1, -2], "dependency": -3, "degree": 1.000000}, {"attributes": [-1, -3], "dependency": -2, "degree": 1.000000}]
 (1 row)
 
 SELECT * FROM check_estimated_rows('SELECT * FROM functional_dependencies WHERE (a * 2) = 2 AND upper(b) = ''1''');
@@ -3487,4 +3487,16 @@ SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": 1.0000},
  [{"attributes": [2, 3], "dependency": 4, "degree": 1.000000}, {"attributes": [2, -1], "dependency": 4, "degree": 0.000000}, {"attributes": [2, 3, -1], "dependency": 4, "degree": 0.500000}, {"attributes": [1, 3, -1, -2], "dependency": 4, "degree": 1.000000}]
 (1 row)
 
+-- error, cannot duplicate attribute
+SELECT '[{"attributes": [6], "dependency": 6, "degree": 0.292508},
+         {"attributes": [-2], "dependency": -1, "degree": 0.113999},
+         {"attributes": [6, -2], "dependency": -1, "degree": 0.348479},
+         {"attributes": [-1, -2], "dependency": 6, "degree": 0.839691}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes": [6], "dependency": 6, "degree": 0.292508},
+         {"attributes": [-2], "dependency": -1, "degree": 0.113999},
+         {"attributes": [6, -2], "dependency": -1, "degree": 0.348479},
+         {"attributes": [-1, -2], "dependency": 6, "degree": 0.839691}]"
+LINE 1: SELECT '[{"attributes": [6], "dependency": 6, "degree": 0.29...
+               ^
+DETAIL:  attnum list duplicate value found: 6
 DROP TABLE sb_1, sb_2 CASCADE;
diff --git a/src/test/regress/sql/stats_ext.sql b/src/test/regress/sql/stats_ext.sql
index 4ffc33d8457..879c5ad370b 100644
--- a/src/test/regress/sql/stats_ext.sql
+++ b/src/test/regress/sql/stats_ext.sql
@@ -1770,4 +1770,16 @@ SELECT '[{"attributes" : [2,3], "ndistinct" : 4},
          {"attributes" : [2,3,2], "ndistinct" : 4},
          {"attributes" : [1,3,-1,-2], "ndistinct" : 4}]'::pg_ndistinct;
 
+-- Test input function of pg_dependencies.
+SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": 1.0000},
+         {"attributes" : [2,-1], "dependency" : 4, "degree": 0.0000},
+         {"attributes" : [2,3,-1], "dependency" : 4, "degree": 0.5000},
+         {"attributes" : [1,3,-1,-2], "dependency" : 4, "degree": 1.0000}]'::pg_dependencies;
+
+-- error, cannot duplicate attribute
+SELECT '[{"attributes": [6], "dependency": 6, "degree": 0.292508},
+         {"attributes": [-2], "dependency": -1, "degree": 0.113999},
+         {"attributes": [6, -2], "dependency": -1, "degree": 0.348479},
+         {"attributes": [-1, -2], "dependency": 6, "degree": 0.839691}]'::pg_dependencies;
+
 DROP TABLE sb_1, sb_2 CASCADE;
-- 
2.49.0

From f04d644ebbcd9ca6ced6f57cbb03d5d73aa12fa8 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Thu, 26 Dec 2024 05:02:06 -0500
Subject: [PATCH v4 3/5] Expose attribute statistics functions for use in
 extended_stats.

* get_attr_stat_type()

index ab198076401..6d5006a13c1 100644

 /*

From f4400091f2e125c4bc79af8d7d50f48f96df4567 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Fri, 3 Jan 2025 13:43:29 -0500
Subject: [PATCH v4 4/5] Add extended statistics support functions.

Add pg_restore_extended_stats() and pg_clear_extended_stats(). These
functions closely mirror their relation and attribute counterparts,
but for extended statistics (i.e.  CREATE STATISTICS) objects.
---
 src/include/catalog/pg_proc.dat               |   18 +
 .../statistics/extended_stats_internal.h      |   17 +
 src/backend/statistics/dependencies.c         |   65 +
 src/backend/statistics/extended_stats.c       | 1104 +++++++++++++++++
 src/backend/statistics/mcv.c                  |  144 +++
 src/backend/statistics/mvdistinct.c           |   62 +
 src/test/regress/expected/stats_import.out    |  588 +++++++++
 src/test/regress/sql/stats_import.sql         |  452 +++++++
 doc/src/sgml/func.sgml                        |   98 ++
 9 files changed, 2548 insertions(+)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index d3d28a263fa..5236b777007 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12547,6 +12547,24 @@
   proargtypes => 'int4',
   prosrc => 'gist_translate_cmptype_common' },
 
+# Extended Statistics Import
+{ oid => '9947',
+  descr => 'restore statistics on extended statistics object',
+  proname => 'pg_restore_extended_stats', provolatile => 'v', proisstrict => 'f',
+  provariadic => 'any',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'any',
+  proargnames => '{kwargs}',
+  proargmodes => '{v}',
+  prosrc => 'pg_restore_extended_stats' },
+{ oid => '9948',
+  descr => 'clear statistics on extended statistics object',
+  proname => 'pg_clear_extended_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'void',
+  proargtypes => 'text text bool',
+  proargnames => '{statistics_schemaname,statistics_name,inherited}',
+  prosrc => 'pg_clear_extended_stats' },
+
 # AIO related functions
 { oid => '9200', descr => 'information about in-progress asynchronous IOs',
   proname => 'pg_get_aios', prorows => '100', proretset => 't',
diff --git a/src/include/statistics/extended_stats_internal.h b/src/include/statistics/extended_stats_internal.h
index efcb7dc3546..ba7f5dcad82 100644
--- a/src/include/statistics/extended_stats_internal.h
+++ b/src/include/statistics/extended_stats_internal.h
@@ -127,4 +127,21 @@ extern Selectivity mcv_clause_selectivity_or(PlannerInfo *root,
 											 Selectivity *overlap_basesel,
 											 Selectivity *totalsel);
 
index fd6125fc9da..aee0bcb90d8 100644
@@ -336,6 +336,10 @@ dependency_degree(StatsBuildData *data, int k, AttrNumber *dependency)
 
+
+void
+free_pg_dependencies(MVDependencies *dependencies);
+
 /*
  * detects functional dependencies between groups of columns
  *
@@ -1022,6 +1026,55 @@ dependencies_scalar(void *state, char *token, JsonTokenType tokentype)
 	return JSON_SEM_ACTION_FAILED;
 }
 
+/*
@@ -1106,6 +1159,18 @@ pg_dependencies_in(PG_FUNCTION_ARGS)
 
+/*

 

 /*
 
 	return result;
 }
+
+static HeapTuple
 
 	return s;
 }
+
+/*
index 003dc3a74ab..4c24e580c2a 100644
@@ -774,6 +774,68 @@ pg_ndistinct_in(PG_FUNCTION_ARGS)
 	PG_RETURN_NULL();
 }
 
+/*
index 48d6392b4ad..d852e046f9e 100644
@@ -1342,6 +1346,590 @@ AND attname = 'i';
 (1 row)
 
 DROP TABLE stats_temp;
+-- set n_distinct using at attnum (1) that is not in the statistics object
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'n_distinct', '[{"attributes" : [2,3], "ndistinct" : 4},
+                        {"attributes" : [2,-1], "ndistinct" : 4},
+                        {"attributes" : [2,-2], "ndistinct" : 4},
+                        {"attributes" : [3,-1], "ndistinct" : 4},
+                        {"attributes" : [3,-2], "ndistinct" : 4},
+                        {"attributes" : [-1,-2], "ndistinct" : 3},
+                        {"attributes" : [2,3,-1], "ndistinct" : 4},
+                        {"attributes" : [2,3,-2], "ndistinct" : 4},
+                        {"attributes" : [2,-1,-2], "ndistinct" : 4},
+                        {"attributes" : [3,-1,-2], "ndistinct" : 4},
+                        {"attributes" : [1,3,-1,-2], "ndistinct" : 4}]'::pg_ndistinct
+      );
+WARNING:  pg_ndistinct: invalid attnum for this statistics object: 1
+ pg_restore_extended_stats 
+---------------------------
+ f
+(1 row)
+
+-- set n_distinct using at attnum that is 0
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'n_distinct', '[{"attributes" : [2,3], "ndistinct" : 4},
+                        {"attributes" : [2,-1], "ndistinct" : 4},
+                        {"attributes" : [2,0], "ndistinct" : 4},
+                        {"attributes" : [3,-1], "ndistinct" : 4},
+                        {"attributes" : [3,-2], "ndistinct" : 4},
+                        {"attributes" : [-1,-2], "ndistinct" : 3},
+                        {"attributes" : [2,3,-1], "ndistinct" : 4},
+                        {"attributes" : [2,3,-2], "ndistinct" : 4},
+                        {"attributes" : [2,-1,-2], "ndistinct" : 4},
+                        {"attributes" : [3,-1,-2], "ndistinct" : 4}]'::pg_ndistinct
+      );
+WARNING:  pg_ndistinct: invalid attnum for this statistics object: 0
+ pg_restore_extended_stats 
+---------------------------
+ f
+(1 row)
+
+-- set n_distinct using at attnum that is outside the expression bounds(below -2)
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'n_distinct', '[{"attributes" : [2,3], "ndistinct" : 4},
+                        {"attributes" : [2,-4], "ndistinct" : 4},
+                        {"attributes" : [3,-1], "ndistinct" : 4},
+                        {"attributes" : [3,-2], "ndistinct" : 4},
+                        {"attributes" : [-1,-2], "ndistinct" : 3},
+                        {"attributes" : [2,3,-1], "ndistinct" : 4},
+                        {"attributes" : [2,3,-2], "ndistinct" : 4},
+                        {"attributes" : [2,-1,-2], "ndistinct" : 4},
+                        {"attributes" : [3,-1,-2], "ndistinct" : 4}]'::pg_ndistinct
+      );
+WARNING:  pg_ndistinct: invalid attnum for this statistics object: -4
+ pg_restore_extended_stats 
+---------------------------
+ f
+(1 row)
+
+-- ok
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'n_distinct', '[{"attributes" : [2,3], "ndistinct" : 4},
+                        {"attributes" : [2,-1], "ndistinct" : 4},
+                        {"attributes" : [3,-1], "ndistinct" : 4},
+                        {"attributes" : [3,-2], "ndistinct" : 4},
+                        {"attributes" : [-1,-2], "ndistinct" : 3},
+                        {"attributes" : [2,3,-1], "ndistinct" : 4},
+                        {"attributes" : [2,3,-2], "ndistinct" : 4},
+                        {"attributes" : [2,-1,-2], "ndistinct" : 4},
+                        {"attributes" : [3,-1,-2], "ndistinct" : 4}]'::pg_ndistinct
+      );
+ pg_restore_extended_stats 
+---------------------------
+ t
+(1 row)
+
+SELECT
+    e.n_distinct, e.dependencies, e.most_common_vals, e.most_common_val_nulls,
+    e.most_common_freqs, e.most_common_base_freqs
+FROM pg_stats_ext AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false
+\gx
+-[ RECORD 1 ]----------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+n_distinct             | [{"attributes": [2, 3], "ndistinct": 4}, {"attributes": [2, -1], "ndistinct": 4}, {"attributes": [3, -1], "ndistinct": 4}, {"attributes": [3, -2], "ndistinct": 4}, {"attributes": [-1, -2], "ndistinct": 3}, {"attributes": [2, 3, -1], "ndistinct": 4}, {"attributes": [2, 3, -2], "ndistinct": 4}, {"attributes": [2, -1, -2], "ndistinct": 4}, {"attributes": [3, -1, -2], "ndistinct": 4}]
+dependencies           | 
+most_common_vals       | 
+most_common_val_nulls  | 
+most_common_freqs      | 
+most_common_base_freqs | 
+
+-- set dependencies using at attnum (1) that is not in the statistics object
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'dependencies', '[{"attributes": [2], "dependency": 1, "degree": 1.000000},
+                          {"attributes": [2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [3], "dependency": 2, "degree": 1.000000},
+                          {"attributes": [3], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [3], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [-1], "dependency": 2, "degree": 0.500000},
+                          {"attributes": [-1], "dependency": 3, "degree": 0.500000},
+                          {"attributes": [-1], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [-2], "dependency": 2, "degree": 0.500000},
+                          {"attributes": [-2], "dependency": 3, "degree": 0.500000},
+                          {"attributes": [-2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2,3], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2,3], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [2,-1], "dependency": 3, "degree": 1.000000},
+                          {"attributes": [2,-1], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [2,-2], "dependency": 3, "degree": 1.000000},
+                          {"attributes": [2,-2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [3,-1], "dependency": 2, "degree": 1.000000},
+                          {"attributes": [3,-1], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [3,-2], "dependency": 2, "degree": 1.000000},
+                          {"attributes": [3,-2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [-1,-2], "dependency": 2, "degree": 0.500000},
+                          {"attributes": [-1,-2], "dependency": 3, "degree": 0.500000},
+                          {"attributes": [2,3], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2,3], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [2,3,-2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2,-1,-2], "dependency": 3, "degree": 1.000000},
+                          {"attributes": [3,-1,-2], "dependency": 2, "degree": 1.000000}]'::pg_dependencies
+      );
+WARNING:  pg_dependencies: invalid attnum for this statistics object: 1
+ pg_restore_extended_stats 
+---------------------------
+ f
+(1 row)
+
+-- set dependencies using at attnum that is 0
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'dependencies', '[{"attributes": [2], "dependency": 3, "degree": 1.000000},
+                          {"attributes": [0], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [3], "dependency": 2, "degree": 1.000000},
+                          {"attributes": [3], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [3], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [-1], "dependency": 2, "degree": 0.500000},
+                          {"attributes": [-1], "dependency": 3, "degree": 0.500000},
+                          {"attributes": [-1], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [-2], "dependency": 2, "degree": 0.500000},
+                          {"attributes": [-2], "dependency": 3, "degree": 0.500000},
+                          {"attributes": [-2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2,3], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2,3], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [2,-1], "dependency": 3, "degree": 1.000000},
+                          {"attributes": [2,-1], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [2,-2], "dependency": 3, "degree": 1.000000},
+                          {"attributes": [2,-2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [3,-1], "dependency": 2, "degree": 1.000000},
+                          {"attributes": [3,-1], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [3,-2], "dependency": 2, "degree": 1.000000},
+                          {"attributes": [3,-2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [-1,-2], "dependency": 2, "degree": 0.500000},
+                          {"attributes": [-1,-2], "dependency": 3, "degree": 0.500000},
+                          {"attributes": [2,3], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2,3], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [2,3,-2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2,-1,-2], "dependency": 3, "degree": 1.000000},
+                          {"attributes": [3,-1,-2], "dependency": 2, "degree": 1.000000}]'::pg_dependencies
+      );
+WARNING:  pg_dependencies: invalid attnum for this statistics object: 0
+ pg_restore_extended_stats 
+---------------------------
+ f
+(1 row)
+
+-- set dependencies using at attnum that is outside the expression bounds(below -2)
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'dependencies', '[{"attributes": [2], "dependency": -3, "degree": 1.000000},
+                          {"attributes": [2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [3], "dependency": 2, "degree": 1.000000},
+                          {"attributes": [3], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [3], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [-1], "dependency": 2, "degree": 0.500000},
+                          {"attributes": [-1], "dependency": 3, "degree": 0.500000},
+                          {"attributes": [-1], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [-2], "dependency": 2, "degree": 0.500000},
+                          {"attributes": [-2], "dependency": 3, "degree": 0.500000},
+                          {"attributes": [-2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2,3], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2,3], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [2,-1], "dependency": 3, "degree": 1.000000},
+                          {"attributes": [2,-1], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [2,-2], "dependency": 3, "degree": 1.000000},
+                          {"attributes": [2,-2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [3,-1], "dependency": 2, "degree": 1.000000},
+                          {"attributes": [3,-1], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [3,-2], "dependency": 2, "degree": 1.000000},
+                          {"attributes": [3,-2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [-1,-2], "dependency": 2, "degree": 0.500000},
+                          {"attributes": [-1,-2], "dependency": 3, "degree": 0.500000},
+                          {"attributes": [2,3], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2,3], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [2,3,-2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2,-1,-2], "dependency": 3, "degree": 1.000000},
+                          {"attributes": [3,-1,-2], "dependency": 2, "degree": 1.000000}]'::pg_dependencies
+      );
+WARNING:  pg_dependencies: invalid attnum for this statistics object: -3
+ pg_restore_extended_stats 
+---------------------------
+ f
+(1 row)
+
+-- ok
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'dependencies', '[{"attributes": [2], "dependency": 3, "degree": 1.000000},
+                          {"attributes": [2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [3], "dependency": 2, "degree": 1.000000},
+                          {"attributes": [3], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [3], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [-1], "dependency": 2, "degree": 0.500000},
+                          {"attributes": [-1], "dependency": 3, "degree": 0.500000},
+                          {"attributes": [-1], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [-2], "dependency": 2, "degree": 0.500000},
+                          {"attributes": [-2], "dependency": 3, "degree": 0.500000},
+                          {"attributes": [-2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2,3], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2,3], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [2,-1], "dependency": 3, "degree": 1.000000},
+                          {"attributes": [2,-1], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [2,-2], "dependency": 3, "degree": 1.000000},
+                          {"attributes": [2,-2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [3,-1], "dependency": 2, "degree": 1.000000},
+                          {"attributes": [3,-1], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [3,-2], "dependency": 2, "degree": 1.000000},
+                          {"attributes": [3,-2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [-1,-2], "dependency": 2, "degree": 0.500000},
+                          {"attributes": [-1,-2], "dependency": 3, "degree": 0.500000},
+                          {"attributes": [2,3], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2,3], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [2,3,-2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2,-1,-2], "dependency": 3, "degree": 1.000000},
+                          {"attributes": [3,-1,-2], "dependency": 2, "degree": 1.000000}]'::pg_dependencies
+      );
+ pg_restore_extended_stats 
+---------------------------
+ t
+(1 row)
+
+SELECT
+    e.n_distinct, e.dependencies, e.most_common_vals, e.most_common_val_nulls,
+    e.most_common_freqs, e.most_common_base_freqs
+FROM pg_stats_ext AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false
+\gx
+-[ RECORD 1 ]----------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+n_distinct             | [{"attributes": [2, 3], "ndistinct": 4}, {"attributes": [2, -1], "ndistinct": 4}, {"attributes": [3, -1], "ndistinct": 4}, {"attributes": [3, -2], "ndistinct": 4}, {"attributes": [-1, -2], "ndistinct": 3}, {"attributes": [2, 3, -1], "ndistinct": 4}, {"attributes": [2, 3, -2], "ndistinct": 4}, {"attributes": [2, -1, -2], "ndistinct": 4}, {"attributes": [3, -1, -2], "ndistinct": 4}]
+dependencies           | [{"attributes": [2], "dependency": 3, "degree": 1.000000}, {"attributes": [2], "dependency": -1, "degree": 1.000000}, {"attributes": [2], "dependency": -2, "degree": 1.000000}, {"attributes": [3], "dependency": 2, "degree": 1.000000}, {"attributes": [3], "dependency": -1, "degree": 1.000000}, {"attributes": [3], "dependency": -2, "degree": 1.000000}, {"attributes": [-1], "dependency": 2, "degree": 0.500000}, {"attributes": [-1], "dependency": 3, "degree": 0.500000}, {"attributes": [-1], "dependency": -2, "degree": 1.000000}, {"attributes": [-2], "dependency": 2, "degree": 0.500000}, {"attributes": [-2], "dependency": 3, "degree": 0.500000}, {"attributes": [-2], "dependency": -1, "degree": 1.000000}, {"attributes": [2, 3], "dependency": -1, "degree": 1.000000}, {"attributes": [2, 3], "dependency": -2, "degree": 1.000000}, {"attributes": [2, -1], "dependency": 3, "degree": 1.000000}, {"attributes": [2, -1], "dependency": -2, "degree": 1.000000}, {"attributes": [2, -2], "dependency": 3, "degree": 1.000000}, {"attributes": [2, -2], "dependency": -1, "degree": 1.000000}, {"attributes": [3, -1], "dependency": 2, "degree": 1.000000}, {"attributes": [3, -1], "dependency": -2, "degree": 1.000000}, {"attributes": [3, -2], "dependency": 2, "degree": 1.000000}, {"attributes": [3, -2], "dependency": -1, "degree": 1.000000}, {"attributes": [-1, -2], "dependency": 2, "degree": 0.500000}, {"attributes": [-1, -2], "dependency": 3, "degree": 0.500000}, {"attributes": [2, 3], "dependency": -1, "degree": 1.000000}, {"attributes": [2, 3], "dependency": -2, "degree": 1.000000}, {"attributes": [2, 3, -2], "dependency": -1, "degree": 1.000000}, {"attributes": [2, -1, -2], "dependency": 3, "degree": 1.000000}, {"attributes": [3, -1, -2], "dependency": 2, "degree": 1.000000}]
+most_common_vals       | 
+most_common_val_nulls  | 
+most_common_freqs      | 
+most_common_base_freqs | 
+
+-- if any one mcv param specified, all four must be specified (part 1)
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'most_common_vals', '{{four,NULL,0,NULL},{one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")",1,2},{tre,"(3,3.3,TRE,03-03-2003,)",-1,3},{two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")",1,2}}'::text[]
+      );
+WARNING:  MCV parameters "most_common_vals", "most_common_val_nulls", "most_common_freqs", and "most_common_base_freqs" must be all specified if any are specified
+ pg_restore_extended_stats 
+---------------------------
+ f
+(1 row)
+
+-- if any one mcv param specified, all four must be specified (part 2)
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'most_common_val_nulls', '{{f,t,f,t},{f,f,f,f},{f,f,f,f},{f,f,f,f}}'::boolean[]
+      );
+WARNING:  MCV parameters "most_common_vals", "most_common_val_nulls", "most_common_freqs", and "most_common_base_freqs" must be all specified if any are specified
+ pg_restore_extended_stats 
+---------------------------
+ f
+(1 row)
+
+-- if any one mcv param specified, all four must be specified (part 3)
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'most_common_freqs', '{0.25,0.25,0.25,0.25}'::double precision[]
+      );
+WARNING:  MCV parameters "most_common_vals", "most_common_val_nulls", "most_common_freqs", and "most_common_base_freqs" must be all specified if any are specified
+ pg_restore_extended_stats 
+---------------------------
+ f
+(1 row)
+
+-- if any one mcv param specified, all four must be specified (part 4)
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'most_common_base_freqs', '{0.00390625,0.015625,0.00390625,0.015625}'::double precision[]
+      );
+WARNING:  MCV parameters "most_common_vals", "most_common_val_nulls", "most_common_freqs", and "most_common_base_freqs" must be all specified if any are specified
+ pg_restore_extended_stats 
+---------------------------
+ f
+(1 row)
+
+-- ok
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'most_common_vals', '{{four,NULL,0,NULL},{one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")",1,2},{tre,"(3,3.3,TRE,03-03-2003,)",-1,3},{two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")",1,2}}'::text[],
+        'most_common_val_nulls', '{{f,t,f,t},{f,f,f,f},{f,f,f,f},{f,f,f,f}}'::boolean[],
+        'most_common_freqs', '{0.25,0.25,0.25,0.25}'::double precision[],
+        'most_common_base_freqs', '{0.00390625,0.015625,0.00390625,0.015625}'::double precision[]
+      );
+ pg_restore_extended_stats 
+---------------------------
+ t
+(1 row)
+
+SELECT
+    e.n_distinct, e.dependencies, e.most_common_vals, e.most_common_val_nulls,
+    e.most_common_freqs, e.most_common_base_freqs
+FROM pg_stats_ext AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false
+\gx
+-[ RECORD 1 ]----------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+n_distinct             | [{"attributes": [2, 3], "ndistinct": 4}, {"attributes": [2, -1], "ndistinct": 4}, {"attributes": [3, -1], "ndistinct": 4}, {"attributes": [3, -2], "ndistinct": 4}, {"attributes": [-1, -2], "ndistinct": 3}, {"attributes": [2, 3, -1], "ndistinct": 4}, {"attributes": [2, 3, -2], "ndistinct": 4}, {"attributes": [2, -1, -2], "ndistinct": 4}, {"attributes": [3, -1, -2], "ndistinct": 4}]
+dependencies           | [{"attributes": [2], "dependency": 3, "degree": 1.000000}, {"attributes": [2], "dependency": -1, "degree": 1.000000}, {"attributes": [2], "dependency": -2, "degree": 1.000000}, {"attributes": [3], "dependency": 2, "degree": 1.000000}, {"attributes": [3], "dependency": -1, "degree": 1.000000}, {"attributes": [3], "dependency": -2, "degree": 1.000000}, {"attributes": [-1], "dependency": 2, "degree": 0.500000}, {"attributes": [-1], "dependency": 3, "degree": 0.500000}, {"attributes": [-1], "dependency": -2, "degree": 1.000000}, {"attributes": [-2], "dependency": 2, "degree": 0.500000}, {"attributes": [-2], "dependency": 3, "degree": 0.500000}, {"attributes": [-2], "dependency": -1, "degree": 1.000000}, {"attributes": [2, 3], "dependency": -1, "degree": 1.000000}, {"attributes": [2, 3], "dependency": -2, "degree": 1.000000}, {"attributes": [2, -1], "dependency": 3, "degree": 1.000000}, {"attributes": [2, -1], "dependency": -2, "degree": 1.000000}, {"attributes": [2, -2], "dependency": 3, "degree": 1.000000}, {"attributes": [2, -2], "dependency": -1, "degree": 1.000000}, {"attributes": [3, -1], "dependency": 2, "degree": 1.000000}, {"attributes": [3, -1], "dependency": -2, "degree": 1.000000}, {"attributes": [3, -2], "dependency": 2, "degree": 1.000000}, {"attributes": [3, -2], "dependency": -1, "degree": 1.000000}, {"attributes": [-1, -2], "dependency": 2, "degree": 0.500000}, {"attributes": [-1, -2], "dependency": 3, "degree": 0.500000}, {"attributes": [2, 3], "dependency": -1, "degree": 1.000000}, {"attributes": [2, 3], "dependency": -2, "degree": 1.000000}, {"attributes": [2, 3, -2], "dependency": -1, "degree": 1.000000}, {"attributes": [2, -1, -2], "dependency": 3, "degree": 1.000000}, {"attributes": [3, -1, -2], "dependency": 2, "degree": 1.000000}]
+most_common_vals       | {{four,NULL,0,NULL},{one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")",1,2},{tre,"(3,3.3,TRE,03-03-2003,)",-1,3},{two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")",1,2}}
+most_common_val_nulls  | {{f,t,f,t},{f,f,f,f},{f,f,f,f},{f,f,f,f}}
+most_common_freqs      | {0.25,0.25,0.25,0.25}
+most_common_base_freqs | {0.00390625,0.015625,0.00390625,0.015625}
+
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'exprs', '{{0,4,-0.75,"{1}","{0.5}","{-1,0}",-0.6,NULL,NULL,NULL},{0.25,4,-0.5,"{2}","{0.5}",NULL,1,NULL,NULL,NULL}}'::text[]
+      );
+ pg_restore_extended_stats 
+---------------------------
+ t
+(1 row)
+
+SELECT
+    e.inherited, e.null_frac, e.avg_width, e.n_distinct, e.most_common_vals,
+    e.most_common_freqs, e.histogram_bounds, e.correlation,
+    e.most_common_elems, e.most_common_elem_freqs, e.elem_count_histogram
+FROM pg_stats_ext_exprs AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+and e.inherited = false
+\gx
+-[ RECORD 1 ]----------+-------
+inherited              | f
+null_frac              | 0
+avg_width              | 4
+n_distinct             | -0.75
+most_common_vals       | {1}
+most_common_freqs      | {0.5}
+histogram_bounds       | {-1,0}
+correlation            | -0.6
+most_common_elems      | 
+most_common_elem_freqs | 
+elem_count_histogram   | 
+-[ RECORD 2 ]----------+-------
+inherited              | f
+null_frac              | 0.25
+avg_width              | 4
+n_distinct             | -0.5
+most_common_vals       | {2}
+most_common_freqs      | {0.5}
+histogram_bounds       | 
+correlation            | 1
+most_common_elems      | 
+most_common_elem_freqs | 
+elem_count_histogram   | 
+
+SELECT
+    pg_catalog.pg_clear_extended_stats(
+        statistics_schemaname => 'stats_import',
+        statistics_name => 'test_stat_clone',
+        inherited => false);
+ pg_clear_extended_stats 
+-------------------------
+ 
+(1 row)
+
+SELECT COUNT(*)
+FROM pg_stats_ext AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false;
+ count 
+-------
+     0
+(1 row)
+
+SELECT COUNT(*)
+FROM pg_stats_ext_exprs AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false;
+ count 
+-------
+     0
+(1 row)
+
+--
+-- Copy stats from test_stat to test_stat_clone
+--
+SELECT
+    e.statistics_name,
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', e.statistics_schemaname::text,
+        'statistics_name', 'test_stat_clone',
+        'inherited', e.inherited,
+        'n_distinct', e.n_distinct,
+        'dependencies', e.dependencies,
+        'most_common_vals', e.most_common_vals,
+        'most_common_val_nulls', e.most_common_val_nulls,
+        '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
+        array_agg(
+            ARRAY[ee.null_frac::text, ee.avg_width::text,
+                  ee.n_distinct::text, ee.most_common_vals::text,
+                  ee.most_common_freqs::text, ee.histogram_bounds::text,
+                  ee.correlation::text, ee.most_common_elems::text,
+                  ee.most_common_elem_freqs::text,
+                  ee.elem_count_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 = 'test_stat';
+ statistics_name | pg_restore_extended_stats 
+-----------------+---------------------------
+ test_stat       | t
+(1 row)
+
+SELECT o.inherited,
+       o.n_distinct, o.dependencies, o.most_common_vals,
+       o.most_common_val_nulls, o.most_common_freqs,
+       o.most_common_base_freqs
+FROM pg_stats_ext AS o
+WHERE o.statistics_schemaname = 'stats_import'
+AND o.statistics_name = 'test_stat'
+EXCEPT
+SELECT n.inherited,
+       n.n_distinct, n.dependencies, n.most_common_vals,
+       n.most_common_val_nulls, n.most_common_freqs,
+       n.most_common_base_freqs
+FROM pg_stats_ext AS n
+WHERE n.statistics_schemaname = 'stats_import'
+AND n.statistics_name = 'test_stat_clone';
+ inherited | n_distinct | dependencies | most_common_vals | most_common_val_nulls | most_common_freqs | most_common_base_freqs 
+-----------+------------+--------------+------------------+-----------------------+-------------------+------------------------
+(0 rows)
+
+SELECT n.inherited,
+       n.n_distinct, n.dependencies, n.most_common_vals,
+       n.most_common_val_nulls, n.most_common_freqs,
+       n.most_common_base_freqs
+FROM pg_stats_ext AS n
+WHERE n.statistics_schemaname = 'stats_import'
+AND n.statistics_name = 'test_stat_clone'
+EXCEPT
+SELECT o.inherited,
+       o.n_distinct, o.dependencies, o.most_common_vals,
+       o.most_common_val_nulls, o.most_common_freqs,
+       o.most_common_base_freqs
+FROM pg_stats_ext AS o
+WHERE o.statistics_schemaname = 'stats_import'
+AND o.statistics_name = 'test_stat';
+ inherited | n_distinct | dependencies | most_common_vals | most_common_val_nulls | most_common_freqs | most_common_base_freqs 
+-----------+------------+--------------+------------------+-----------------------+-------------------+------------------------
+(0 rows)
+
+SELECT o.inherited,
+       o.null_frac, o.avg_width, o.n_distinct,
+       o.most_common_vals::text AS most_common_vals,
+       o.most_common_freqs,
+       o.histogram_bounds::text AS histogram_bounds,
+       o.correlation,
+       o.most_common_elems::text AS most_common_elems,
+       o.most_common_elem_freqs, o.elem_count_histogram
+FROM pg_stats_ext_exprs AS o
+WHERE o.statistics_schemaname = 'stats_import'
+AND o.statistics_name = 'test_stat'
+EXCEPT
+SELECT n.inherited,
+       n.null_frac, n.avg_width, n.n_distinct,
+       n.most_common_vals::text AS most_common_vals,
+       n.most_common_freqs,
+       n.histogram_bounds::text AS histogram_bounds,
+       n.correlation,
+       n.most_common_elems::text AS most_common_elems,
+       n.most_common_elem_freqs, n.elem_count_histogram
+FROM pg_stats_ext_exprs AS n
+WHERE n.statistics_schemaname = 'stats_import'
+AND n.statistics_name = 'test_stat_clone';
+ inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram 
+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------
+(0 rows)
+
+SELECT n.inherited,
+       n.null_frac, n.avg_width, n.n_distinct,
+       n.most_common_vals::text AS most_common_vals,
+       n.most_common_freqs,
+       n.histogram_bounds::text AS histogram_bounds,
+       n.correlation,
+       n.most_common_elems::text AS most_common_elems,
+       n.most_common_elem_freqs, n.elem_count_histogram
+FROM pg_stats_ext_exprs AS n
+WHERE n.statistics_schemaname = 'stats_import'
+AND n.statistics_name = 'test_stat_clone'
+EXCEPT
+SELECT o.inherited,
+       o.null_frac, o.avg_width, o.n_distinct,
+       o.most_common_vals::text AS most_common_vals,
+       o.most_common_freqs,
+       o.histogram_bounds::text AS histogram_bounds,
+       o.correlation,
+       o.most_common_elems::text AS most_common_elems,
+       o.most_common_elem_freqs, o.elem_count_histogram
+FROM pg_stats_ext_exprs AS o
+WHERE o.statistics_schemaname = 'stats_import'
+AND o.statistics_name = 'test_stat';
+ inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram 
+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------
+(0 rows)
+
 DROP SCHEMA stats_import CASCADE;
 NOTICE:  drop cascades to 6 other objects
 DETAIL:  drop cascades to type stats_import.complex_type
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index d140733a750..4dd568be9fc 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -766,6 +766,9 @@ SELECT 4, 'four', NULL, int4range(0,100), NULL;

 

+CREATE STATISTICS stats_import.test_stat_clone ON name, comp, lower(arange), array_length(tags,1)
+FROM stats_import.test_clone;
+
 --
 -- Copy stats from test to test_clone, and is_odd to is_odd_clone
 --
@@ -970,4 +976,450 @@ AND tablename = 'stats_temp'
 AND inherited = false
 AND attname = 'i';
 DROP TABLE stats_temp;
+
+-- set n_distinct using at attnum (1) that is not in the statistics object
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'n_distinct', '[{"attributes" : [2,3], "ndistinct" : 4},
+                        {"attributes" : [2,-1], "ndistinct" : 4},
+                        {"attributes" : [2,-2], "ndistinct" : 4},
+                        {"attributes" : [3,-1], "ndistinct" : 4},
+                        {"attributes" : [3,-2], "ndistinct" : 4},
+                        {"attributes" : [-1,-2], "ndistinct" : 3},
+                        {"attributes" : [2,3,-1], "ndistinct" : 4},
+                        {"attributes" : [2,3,-2], "ndistinct" : 4},
+                        {"attributes" : [2,-1,-2], "ndistinct" : 4},
+                        {"attributes" : [3,-1,-2], "ndistinct" : 4},
+                        {"attributes" : [1,3,-1,-2], "ndistinct" : 4}]'::pg_ndistinct
+      );
+
+-- set n_distinct using at attnum that is 0
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'n_distinct', '[{"attributes" : [2,3], "ndistinct" : 4},
+                        {"attributes" : [2,-1], "ndistinct" : 4},
+                        {"attributes" : [2,0], "ndistinct" : 4},
+                        {"attributes" : [3,-1], "ndistinct" : 4},
+                        {"attributes" : [3,-2], "ndistinct" : 4},
+                        {"attributes" : [-1,-2], "ndistinct" : 3},
+                        {"attributes" : [2,3,-1], "ndistinct" : 4},
+                        {"attributes" : [2,3,-2], "ndistinct" : 4},
+                        {"attributes" : [2,-1,-2], "ndistinct" : 4},
+                        {"attributes" : [3,-1,-2], "ndistinct" : 4}]'::pg_ndistinct
+      );
+
+-- set n_distinct using at attnum that is outside the expression bounds(below -2)
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'n_distinct', '[{"attributes" : [2,3], "ndistinct" : 4},
+                        {"attributes" : [2,-4], "ndistinct" : 4},
+                        {"attributes" : [3,-1], "ndistinct" : 4},
+                        {"attributes" : [3,-2], "ndistinct" : 4},
+                        {"attributes" : [-1,-2], "ndistinct" : 3},
+                        {"attributes" : [2,3,-1], "ndistinct" : 4},
+                        {"attributes" : [2,3,-2], "ndistinct" : 4},
+                        {"attributes" : [2,-1,-2], "ndistinct" : 4},
+                        {"attributes" : [3,-1,-2], "ndistinct" : 4}]'::pg_ndistinct
+      );
+
+-- ok
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'n_distinct', '[{"attributes" : [2,3], "ndistinct" : 4},
+                        {"attributes" : [2,-1], "ndistinct" : 4},
+                        {"attributes" : [3,-1], "ndistinct" : 4},
+                        {"attributes" : [3,-2], "ndistinct" : 4},
+                        {"attributes" : [-1,-2], "ndistinct" : 3},
+                        {"attributes" : [2,3,-1], "ndistinct" : 4},
+                        {"attributes" : [2,3,-2], "ndistinct" : 4},
+                        {"attributes" : [2,-1,-2], "ndistinct" : 4},
+                        {"attributes" : [3,-1,-2], "ndistinct" : 4}]'::pg_ndistinct
+      );
+
+SELECT
+    e.n_distinct, e.dependencies, e.most_common_vals, e.most_common_val_nulls,
+    e.most_common_freqs, e.most_common_base_freqs
+FROM pg_stats_ext AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false
+\gx
+
+-- set dependencies using at attnum (1) that is not in the statistics object
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'dependencies', '[{"attributes": [2], "dependency": 1, "degree": 1.000000},
+                          {"attributes": [2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [3], "dependency": 2, "degree": 1.000000},
+                          {"attributes": [3], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [3], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [-1], "dependency": 2, "degree": 0.500000},
+                          {"attributes": [-1], "dependency": 3, "degree": 0.500000},
+                          {"attributes": [-1], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [-2], "dependency": 2, "degree": 0.500000},
+                          {"attributes": [-2], "dependency": 3, "degree": 0.500000},
+                          {"attributes": [-2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2,3], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2,3], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [2,-1], "dependency": 3, "degree": 1.000000},
+                          {"attributes": [2,-1], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [2,-2], "dependency": 3, "degree": 1.000000},
+                          {"attributes": [2,-2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [3,-1], "dependency": 2, "degree": 1.000000},
+                          {"attributes": [3,-1], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [3,-2], "dependency": 2, "degree": 1.000000},
+                          {"attributes": [3,-2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [-1,-2], "dependency": 2, "degree": 0.500000},
+                          {"attributes": [-1,-2], "dependency": 3, "degree": 0.500000},
+                          {"attributes": [2,3], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2,3], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [2,3,-2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2,-1,-2], "dependency": 3, "degree": 1.000000},
+                          {"attributes": [3,-1,-2], "dependency": 2, "degree": 1.000000}]'::pg_dependencies
+      );
+
+-- set dependencies using at attnum that is 0
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'dependencies', '[{"attributes": [2], "dependency": 3, "degree": 1.000000},
+                          {"attributes": [0], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [3], "dependency": 2, "degree": 1.000000},
+                          {"attributes": [3], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [3], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [-1], "dependency": 2, "degree": 0.500000},
+                          {"attributes": [-1], "dependency": 3, "degree": 0.500000},
+                          {"attributes": [-1], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [-2], "dependency": 2, "degree": 0.500000},
+                          {"attributes": [-2], "dependency": 3, "degree": 0.500000},
+                          {"attributes": [-2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2,3], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2,3], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [2,-1], "dependency": 3, "degree": 1.000000},
+                          {"attributes": [2,-1], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [2,-2], "dependency": 3, "degree": 1.000000},
+                          {"attributes": [2,-2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [3,-1], "dependency": 2, "degree": 1.000000},
+                          {"attributes": [3,-1], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [3,-2], "dependency": 2, "degree": 1.000000},
+                          {"attributes": [3,-2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [-1,-2], "dependency": 2, "degree": 0.500000},
+                          {"attributes": [-1,-2], "dependency": 3, "degree": 0.500000},
+                          {"attributes": [2,3], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2,3], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [2,3,-2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2,-1,-2], "dependency": 3, "degree": 1.000000},
+                          {"attributes": [3,-1,-2], "dependency": 2, "degree": 1.000000}]'::pg_dependencies
+      );
+
+-- set dependencies using at attnum that is outside the expression bounds(below -2)
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'dependencies', '[{"attributes": [2], "dependency": -3, "degree": 1.000000},
+                          {"attributes": [2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [3], "dependency": 2, "degree": 1.000000},
+                          {"attributes": [3], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [3], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [-1], "dependency": 2, "degree": 0.500000},
+                          {"attributes": [-1], "dependency": 3, "degree": 0.500000},
+                          {"attributes": [-1], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [-2], "dependency": 2, "degree": 0.500000},
+                          {"attributes": [-2], "dependency": 3, "degree": 0.500000},
+                          {"attributes": [-2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2,3], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2,3], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [2,-1], "dependency": 3, "degree": 1.000000},
+                          {"attributes": [2,-1], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [2,-2], "dependency": 3, "degree": 1.000000},
+                          {"attributes": [2,-2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [3,-1], "dependency": 2, "degree": 1.000000},
+                          {"attributes": [3,-1], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [3,-2], "dependency": 2, "degree": 1.000000},
+                          {"attributes": [3,-2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [-1,-2], "dependency": 2, "degree": 0.500000},
+                          {"attributes": [-1,-2], "dependency": 3, "degree": 0.500000},
+                          {"attributes": [2,3], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2,3], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [2,3,-2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2,-1,-2], "dependency": 3, "degree": 1.000000},
+                          {"attributes": [3,-1,-2], "dependency": 2, "degree": 1.000000}]'::pg_dependencies
+      );
+
+-- ok
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'dependencies', '[{"attributes": [2], "dependency": 3, "degree": 1.000000},
+                          {"attributes": [2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [3], "dependency": 2, "degree": 1.000000},
+                          {"attributes": [3], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [3], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [-1], "dependency": 2, "degree": 0.500000},
+                          {"attributes": [-1], "dependency": 3, "degree": 0.500000},
+                          {"attributes": [-1], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [-2], "dependency": 2, "degree": 0.500000},
+                          {"attributes": [-2], "dependency": 3, "degree": 0.500000},
+                          {"attributes": [-2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2,3], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2,3], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [2,-1], "dependency": 3, "degree": 1.000000},
+                          {"attributes": [2,-1], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [2,-2], "dependency": 3, "degree": 1.000000},
+                          {"attributes": [2,-2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [3,-1], "dependency": 2, "degree": 1.000000},
+                          {"attributes": [3,-1], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [3,-2], "dependency": 2, "degree": 1.000000},
+                          {"attributes": [3,-2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [-1,-2], "dependency": 2, "degree": 0.500000},
+                          {"attributes": [-1,-2], "dependency": 3, "degree": 0.500000},
+                          {"attributes": [2,3], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2,3], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [2,3,-2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2,-1,-2], "dependency": 3, "degree": 1.000000},
+                          {"attributes": [3,-1,-2], "dependency": 2, "degree": 1.000000}]'::pg_dependencies
+      );
+
+SELECT
+    e.n_distinct, e.dependencies, e.most_common_vals, e.most_common_val_nulls,
+    e.most_common_freqs, e.most_common_base_freqs
+FROM pg_stats_ext AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false
+\gx
+
+-- if any one mcv param specified, all four must be specified (part 1)
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'most_common_vals', '{{four,NULL,0,NULL},{one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")",1,2},{tre,"(3,3.3,TRE,03-03-2003,)",-1,3},{two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")",1,2}}'::text[]
+      );
+
+-- if any one mcv param specified, all four must be specified (part 2)
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'most_common_val_nulls', '{{f,t,f,t},{f,f,f,f},{f,f,f,f},{f,f,f,f}}'::boolean[]
+      );
+
+-- if any one mcv param specified, all four must be specified (part 3)
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'most_common_freqs', '{0.25,0.25,0.25,0.25}'::double precision[]
+      );
+
+-- if any one mcv param specified, all four must be specified (part 4)
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'most_common_base_freqs', '{0.00390625,0.015625,0.00390625,0.015625}'::double precision[]
+      );
+
+-- ok
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'most_common_vals', '{{four,NULL,0,NULL},{one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")",1,2},{tre,"(3,3.3,TRE,03-03-2003,)",-1,3},{two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")",1,2}}'::text[],
+        'most_common_val_nulls', '{{f,t,f,t},{f,f,f,f},{f,f,f,f},{f,f,f,f}}'::boolean[],
+        'most_common_freqs', '{0.25,0.25,0.25,0.25}'::double precision[],
+        'most_common_base_freqs', '{0.00390625,0.015625,0.00390625,0.015625}'::double precision[]
+      );
+
+SELECT
+    e.n_distinct, e.dependencies, e.most_common_vals, e.most_common_val_nulls,
+    e.most_common_freqs, e.most_common_base_freqs
+FROM pg_stats_ext AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false
+\gx
+
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'exprs', '{{0,4,-0.75,"{1}","{0.5}","{-1,0}",-0.6,NULL,NULL,NULL},{0.25,4,-0.5,"{2}","{0.5}",NULL,1,NULL,NULL,NULL}}'::text[]
+      );
+
+SELECT
+    e.inherited, e.null_frac, e.avg_width, e.n_distinct, e.most_common_vals,
+    e.most_common_freqs, e.histogram_bounds, e.correlation,
+    e.most_common_elems, e.most_common_elem_freqs, e.elem_count_histogram
+FROM pg_stats_ext_exprs AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+and e.inherited = false
+\gx
+
+SELECT
+    pg_catalog.pg_clear_extended_stats(
+        statistics_schemaname => 'stats_import',
+        statistics_name => 'test_stat_clone',
+        inherited => false);
+
+SELECT COUNT(*)
+FROM pg_stats_ext AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false;
+
+SELECT COUNT(*)
+FROM pg_stats_ext_exprs AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false;
+
+--
+-- Copy stats from test_stat to test_stat_clone
+--
+SELECT
+    e.statistics_name,
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', e.statistics_schemaname::text,
+        'statistics_name', 'test_stat_clone',
+        'inherited', e.inherited,
+        'n_distinct', e.n_distinct,
+        'dependencies', e.dependencies,
+        'most_common_vals', e.most_common_vals,
+        'most_common_val_nulls', e.most_common_val_nulls,
+        '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
+        array_agg(
+            ARRAY[ee.null_frac::text, ee.avg_width::text,
+                  ee.n_distinct::text, ee.most_common_vals::text,
+                  ee.most_common_freqs::text, ee.histogram_bounds::text,
+                  ee.correlation::text, ee.most_common_elems::text,
+                  ee.most_common_elem_freqs::text,
+                  ee.elem_count_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 = 'test_stat';
+
+SELECT o.inherited,
+       o.n_distinct, o.dependencies, o.most_common_vals,
+       o.most_common_val_nulls, o.most_common_freqs,
+       o.most_common_base_freqs
+FROM pg_stats_ext AS o
+WHERE o.statistics_schemaname = 'stats_import'
+AND o.statistics_name = 'test_stat'
+EXCEPT
+SELECT n.inherited,
+       n.n_distinct, n.dependencies, n.most_common_vals,
+       n.most_common_val_nulls, n.most_common_freqs,
+       n.most_common_base_freqs
+FROM pg_stats_ext AS n
+WHERE n.statistics_schemaname = 'stats_import'
+AND n.statistics_name = 'test_stat_clone';
+
+SELECT n.inherited,
+       n.n_distinct, n.dependencies, n.most_common_vals,
+       n.most_common_val_nulls, n.most_common_freqs,
+       n.most_common_base_freqs
+FROM pg_stats_ext AS n
+WHERE n.statistics_schemaname = 'stats_import'
+AND n.statistics_name = 'test_stat_clone'
+EXCEPT
+SELECT o.inherited,
+       o.n_distinct, o.dependencies, o.most_common_vals,
+       o.most_common_val_nulls, o.most_common_freqs,
+       o.most_common_base_freqs
+FROM pg_stats_ext AS o
+WHERE o.statistics_schemaname = 'stats_import'
+AND o.statistics_name = 'test_stat';
+
+SELECT o.inherited,
+       o.null_frac, o.avg_width, o.n_distinct,
+       o.most_common_vals::text AS most_common_vals,
+       o.most_common_freqs,
+       o.histogram_bounds::text AS histogram_bounds,
+       o.correlation,
+       o.most_common_elems::text AS most_common_elems,
+       o.most_common_elem_freqs, o.elem_count_histogram
+FROM pg_stats_ext_exprs AS o
+WHERE o.statistics_schemaname = 'stats_import'
+AND o.statistics_name = 'test_stat'
+EXCEPT
+SELECT n.inherited,
+       n.null_frac, n.avg_width, n.n_distinct,
+       n.most_common_vals::text AS most_common_vals,
+       n.most_common_freqs,
+       n.histogram_bounds::text AS histogram_bounds,
+       n.correlation,
+       n.most_common_elems::text AS most_common_elems,
+       n.most_common_elem_freqs, n.elem_count_histogram
+FROM pg_stats_ext_exprs AS n
+WHERE n.statistics_schemaname = 'stats_import'
+AND n.statistics_name = 'test_stat_clone';
+
+SELECT n.inherited,
+       n.null_frac, n.avg_width, n.n_distinct,
+       n.most_common_vals::text AS most_common_vals,
+       n.most_common_freqs,
+       n.histogram_bounds::text AS histogram_bounds,
+       n.correlation,
+       n.most_common_elems::text AS most_common_elems,
+       n.most_common_elem_freqs, n.elem_count_histogram
+FROM pg_stats_ext_exprs AS n
+WHERE n.statistics_schemaname = 'stats_import'
+AND n.statistics_name = 'test_stat_clone'
+EXCEPT
+SELECT o.inherited,
+       o.null_frac, o.avg_width, o.n_distinct,
+       o.most_common_vals::text AS most_common_vals,
+       o.most_common_freqs,
+       o.histogram_bounds::text AS histogram_bounds,
+       o.correlation,
+       o.most_common_elems::text AS most_common_elems,
+       o.most_common_elem_freqs, o.elem_count_histogram
+FROM pg_stats_ext_exprs AS o
+WHERE o.statistics_schemaname = 'stats_import'
+AND o.statistics_name = 'test_stat';
+
 DROP SCHEMA stats_import CASCADE;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index a6d79765c1a..5e11406f760 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30649,6 +30649,104 @@ SELECT pg_restore_attribute_stats(
         </para>
        </entry>
       </row>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_restore_extended_stats</primary>
+        </indexterm>
+        <function>pg_restore_extended_stats</function> (
+        <literal>VARIADIC</literal> <parameter>kwargs</parameter> <type>"any"</type> )
+        <returnvalue>boolean</returnvalue>
+        </para>
+        <para>
+         Creates or updates statistics for statistics objects.  Ordinarily,
+         these statistics are collected automatically or updated as a part of
+         <xref linkend="sql-vacuum"/> or <xref linkend="sql-analyze"/>, so
+         it's not necessary to call this function.  However, it is useful
+         after a restore to enable the optimizer to choose better plans if
+         <command>ANALYZE</command> has not been run yet.
+        </para>
+        <para>
+         The tracked statistics may change from version to version, so
+         arguments are passed as pairs of <replaceable>argname</replaceable>
+         and <replaceable>argvalue</replaceable> in the form:
+<programlisting>
+ SELECT pg_restore_extended_stats(
+    '<replaceable>arg1name</replaceable>', '<replaceable>arg1value</replaceable>'::<replaceable>arg1type</replaceable>,
+    '<replaceable>arg2name</replaceable>', '<replaceable>arg2value</replaceable>'::<replaceable>arg2type</replaceable>,
+    '<replaceable>arg3name</replaceable>', '<replaceable>arg3value</replaceable>'::<replaceable>arg3type</replaceable>);
+</programlisting>
+        </para>
+        <para>
+         For example, to set the <structfield>n_distinct</structfield>,
+         <structfield>dependencies</structfield>, and <structfield>exprs</structfield>
+         values for the statistics object <structname>myschema.mystatsobj</structname>:
+<programlisting>
+ SELECT pg_restore_extended_stats(
+    'statistics_schemaname', 'myschema'::name,
+    'statistics_name',       'mytable'::name,
+    'inherited',             false,
+    'n_distinct',            '{"2, 3": 4, "2, -1": 4, "2, -2": 4, "3, -1": 4, "3, -2": 4}'::pg_ndistinct,
+    'dependencies',          '{"2 => 1": 1.000000, "2 => -1": 1.000000, "2 => -2": 1.000000}'::pg_dependencies
+    'exprs',                 '{{0,4,-0.75,"{1}","{0.5}","{-1,0}",-0.6,NULL,NULL,NULL},{0.25,4,-0.5,"{2}","{0.5}",NULL,1,NULL,NULL,NULL}}'::text[]);
+</programlisting>
+        </para>
+        <para>
+         The required arguments are <literal>statistics_schemaname</literal> with a value
+         of type <type>name</type>, which specifies the statistics object's schema;
+         <literal>statistics_name</literal> with a value of type <type>name</type>, which specifies
+         the name of the statistics object; and <literal>inherited</literal>, which
+         specifies whether the statistics include values from child tables.
+         Other arguments are the names and values of statistics corresponding
+         to columns in <link linkend="view-pg-stats-ext"><structname>pg_stats_ext</structname>
+         </link>. To accept statistics for any expressions in the extended statistics object, the
+         parameter <literal>exprs</literal> with a type <type>text[]</type> is available, the array
+         must be two dimensional with an outer array in length equal to the number of expressions in
+         the object, and the inner array elements for each of the statistical columns in <link
+         linkend="view-pg-stats-ext-exprs"><structname>pg_stats_ext_exprs</structname></link>, some
+         of which are themselves arrays.
+        </para>
+        <para>
+         Additionally, this function accepts argument name
+         <literal>version</literal> of type <type>integer</type>, which
+         specifies the server version from which the statistics originated.
+         This is anticipated to be helpful in porting statistics from older
+         versions of <productname>PostgreSQL</productname>.
+        </para>
+        <para>
+         Minor errors are reported as a <literal>WARNING</literal> and
+         ignored, and remaining statistics will still be restored. If all
+         specified statistics are successfully restored, returns
+         <literal>true</literal>, otherwise <literal>false</literal>.
+        </para>
+        <para>
+         The caller must have the <literal>MAINTAIN</literal> privilege on the
+         table or be the owner of the database.
+        </para>
+       </entry>
+      </row>
+      <row>
+       <entry role="func_table_entry">
+        <para role="func_signature">
+         <indexterm>
+          <primary>pg_clear_extended_stats</primary>
+         </indexterm>
+         <function>pg_clear_extended_stats</function> (
+         <parameter>statistics_schemaname</parameter> <type>name</type>,
+         <parameter>statistics_name</parameter> <type>name</type>,
+         <parameter>inherited</parameter> <type>boolean</type> )
+         <returnvalue>void</returnvalue>
+        </para>
+        <para>
+         Clears statistics for the given statistics object, as
+         though the object was newly created.
+        </para>
+        <para>
+         The caller must have the <literal>MAINTAIN</literal> privilege on
+         the table or be the owner of the database.
+        </para>
+       </entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
-- 
2.49.0

From b5e704aa17d16adf14198935f95957d77bb54b3b Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Sat, 21 Jun 2025 03:16:24 -0400
Subject: [PATCH v4 5/5] Include Extended Statistics in pg_dump.

Incorporate the new pg_restore_extended_stats() function into pg_dump.
This detects the existence of extended statistics statistics (i.e.
pg_statistic_ext_data rows).

This handles many of the changes that have happened to extended
statistic statistics over the various versions, including:

* Format change for pg_ndistinct and pg_dependencies in current
  development version. Earlier versions have the format translated via
  the pg_dump SQL statement.

* Inherited extended statistics were introduced in v15.

* Expressions were introduced to extended statistics in v14.

* MCV extended statistics were introduced in v13.

* pg_statistic_ext_data and pg_stats_ext introduced in v12, prior to
  that ndstinct and depdendencies data (the only kind of stats that
  existed were directly on pg_statistic_ext.

* Extended Statistics were introduced in v10, so there is no support for
  prior versions necessary.
---
 src/bin/pg_dump/pg_backup.h          |   1 +
 src/bin/pg_dump/pg_backup_archiver.c |   3 +-
 src/bin/pg_dump/pg_dump.c            | 229 +++++++++++++++++++++++++++
 src/bin/pg_dump/t/002_pg_dump.pl     |  28 ++++
 4 files changed, 260 insertions(+), 1 deletion(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index af0007fb6d2..90a572000e2 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -68,6 +68,7 @@ enum _dumpPreparedQueries
 	PREPQUERY_DUMPCOMPOSITETYPE,
 	PREPQUERY_DUMPDOMAIN,
 	PREPQUERY_DUMPENUMTYPE,
+	PREPQUERY_DUMPEXTSTATSSTATS,
 	PREPQUERY_DUMPFUNC,
 	PREPQUERY_DUMPOPR,
 	PREPQUERY_DUMPRANGETYPE,
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 197c1295d93..8f4a22c3e32 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -2980,7 +2980,8 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 		strcmp(te->desc, "SEARCHPATH") == 0)
 		return REQ_SPECIAL;
 
-	if (strcmp(te->desc, "STATISTICS DATA") == 0)
+	if ((strcmp(te->desc, "STATISTICS DATA") == 0) ||
+		(strcmp(te->desc, "EXTENDED STATISTICS DATA") == 0))
 	{
 		if (!ropt->dumpStatistics)
 			return 0;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index db944ec2230..0201fbaa188 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -315,6 +315,7 @@ static void dumpSequenceData(Archive *fout, const TableDataInfo *tdinfo);
 static void dumpIndex(Archive *fout, const IndxInfo *indxinfo);
 static void dumpIndexAttach(Archive *fout, const IndexAttachInfo *attachinfo);
 static void dumpStatisticsExt(Archive *fout, const StatsExtInfo *statsextinfo);
+static void dumpStatisticsExtStats(Archive *fout, const StatsExtInfo *statsextinfo);
 static void dumpConstraint(Archive *fout, const ConstraintInfo *coninfo);
 static void dumpTableConstraintComment(Archive *fout, const ConstraintInfo *coninfo);
 static void dumpTSParser(Archive *fout, const TSParserInfo *prsinfo);
@@ -8045,6 +8046,9 @@ getExtendedStatistics(Archive *fout)
 
 		/* Decide whether we want to dump it */
 		selectDumpableStatisticsObject(&(statsextinfo[i]), fout);
+
+		if (fout->dopt->dumpStatistics)
+			statsextinfo[i].dobj.components |= DUMP_COMPONENT_STATISTICS;
 	}
 
 	PQclear(res);
@@ -11444,6 +11448,7 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 			break;
 		case DO_STATSEXT:
 			dumpStatisticsExt(fout, (const StatsExtInfo *) dobj);
+			dumpStatisticsExtStats(fout, (const StatsExtInfo *) dobj);
 			break;
 		case DO_REFRESH_MATVIEW:
 			refreshMatViewData(fout, (const TableDataInfo *) dobj);
@@ -18145,6 +18150,230 @@ dumpStatisticsExt(Archive *fout, const StatsExtInfo *statsextinfo)
 	free(qstatsextname);
 }
 
+/*
+ * dumpStatisticsExtStats
+ *	  write out to fout the stats for an extended statistics object
+ */
+static void
+dumpStatisticsExtStats(Archive *fout, const StatsExtInfo *statsextinfo)
+{
+	DumpOptions *dopt = fout->dopt;
+	PQExpBuffer query;
+	PGresult   *res;
+	int			nstats;
+
+	/* Do nothing if not dumping statistics */
+	if (!dopt->dumpStatistics)
+		return;
+
+	if (!fout->is_prepared[PREPQUERY_DUMPEXTSTATSSTATS])
+	{
+		PQExpBuffer pq = createPQExpBuffer();
+
+		/*
+		 * Set up query for constraint-specific details.
+		 *
+		 * 19+: query pg_stats_ext and pg_stats_ext_exprs as-is 15-18: query
+		 * pg_stats_ext translating the ndistinct and depdendencies, 14:
+		 * inherited is always NULL 12-13: no pg_stats_ext_exprs 10-11: no
+		 * pg_stats_ext, join pg_statistic_ext and pg_namespace
+		 */
+
+		appendPQExpBufferStr(pq,
+							 "PREPARE getExtStatsStats(pg_catalog.name, pg_catalog.name) AS\n"
+							 "SELECT ");
+
+		/* Versions 15+ have inherited stats */
+		if (fout->remoteVersion >= 150000)
+			appendPQExpBufferStr(pq, "e.inherited, ");
+		else
+			appendPQExpBufferStr(pq, "false AS inherited, ");
+
+		/*
+		 * Versions < 19 use the old ndistintinct and depdendencies formats
+		 * Versions < 12 use the pg_statistic_ext columns
+		 *
+		 * TODO: Until v18 is released the master branch has a
+		 * server_version_num of 180000. We will update this to 190000 as soon
+		 * as the master branch updates.
+		 */
+		if (fout->remoteVersion >= 180000)
+			appendPQExpBufferStr(pq, "e.n_distinct, e.dependencies, ");
+		else
+			appendPQExpBufferStr(pq,
+								 "( "
+								 "SELECT json_agg( "
+								 "    json_build_object( "
+								 "        'attributes', "
+								 "         string_to_array(kv.key, ', ')::integer[], "
+								 "        'ndistinct', "
+								 "        kv.value::bigint )) "
+								 "FROM json_each_text(e.n_distinct::text::json) AS kv"
+								 ") AS n_distinct, "
+								 "( "
+								 "SELECT json_agg( "
+								 "    json_build_object( "
+								 "        'attributes', "
+								 "        string_to_array( "
+								 "            split_part(kv.key, ' => ', 1), "
+								 "            ', ')::integer[], "
+								 "        'dependency', "
+								 "        split_part(kv.key, ' => ', 2)::integer, "
+								 "        'degree', "
+								 "        kv.value::double precision )) "
+								 "FROM json_each_text(e.dependencies::text::json) AS kv "
+								 ") AS dependencies, ");
+
+		/* Versions < 12 do not have MCV */
+		if (fout->remoteVersion >= 130000)
+			appendPQExpBufferStr(pq,
+								 "e.most_common_vals, e.most_common_val_nulls, "
+								 "e.most_common_freqs, e.most_common_base_freqs, ");
+		else
+			appendPQExpBufferStr(pq,
+								 "NULL AS most_common_vals, NULL AS most_common_val_nulls, "
+								 "NULL AS most_common_freqs, NULL AS most_common_base_freqs, ");
+
+		/* Expressions were introduced in v14 */
+		if (fout->remoteVersion >= 140000)
+		{
+			appendPQExpBufferStr(pq,
+								 "( "
+								 "SELECT array_agg( "
+								 "    ARRAY[ee.null_frac::text, ee.avg_width::text, "
+								 "          ee.n_distinct::text, ee.most_common_vals::text, "
+								 "          ee.most_common_freqs::text, ee.histogram_bounds::text, "
+								 "          ee.correlation::text, ee.most_common_elems::text, "
+								 "          ee.most_common_elem_freqs::text, "
+								 "          ee.elem_count_histogram::text]) "
+								 "FROM pg_stats_ext_exprs AS ee "
+								 "WHERE ee.statistics_schemaname = $1 "
+								 "AND ee.statistics_name = $2 ");
+
+			/* Inherited expressions introduced in v15 */
+			if (fout->remoteVersion >= 150000)
+				appendPQExpBufferStr(pq, "AND ee.inherited = e.inherited");
+
+			appendPQExpBufferStr(pq, ") AS exprs ");
+		}
+		else
+			appendPQExpBufferStr(pq, "NULL AS exprs ");
+
+		/* pg_stats_ext introduced in v12 */
+		if (fout->remoteVersion >= 120000)
+			appendPQExpBufferStr(pq,
+								 "FROM pg_catalog.pg_stats_ext AS e "
+								 "WHERE e.statistics_schemaname = $1 "
+								 "AND e.statistics_name = $2 ");
+		else
+			appendPQExpBufferStr(pq,
+								 "FROM ( "
+								 "SELECT s.stxndistinct AS n_distinct, "
+								 "    s.stxdependencies AS dependencies "
+								 "FROM pg_catalog.pg_statistics_ext AS s "
+								 "JOIN pg_catalog.pg_namespace AS n "
+								 "ON n.oid = s.stxnamespace "
+								 "WHERE n.nspname = $1 "
+								 "AND e.stxname = $2 "
+								 ") AS e ");
+
+		appendPQExpBufferStr(pq, "ORDER BY e.inherited");
+
+		ExecuteSqlStatement(fout, pq->data);
+
+		fout->is_prepared[PREPQUERY_DUMPEXTSTATSSTATS] = true;
+
+		destroyPQExpBuffer(pq);
+	}
+
+	query = createPQExpBuffer();
+
+	appendPQExpBufferStr(query, "EXECUTE getExtStatsStats(");
+	appendStringLiteralAH(query, statsextinfo->dobj.namespace->dobj.name, fout);
+	appendPQExpBufferStr(query, "::pg_catalog.name, ");
+	appendStringLiteralAH(query, statsextinfo->dobj.name, fout);
+	appendPQExpBufferStr(query, "::pg_catalog.name)");
+
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+	destroyPQExpBuffer(query);
+
+	nstats = PQntuples(res);
+
+	if (nstats > 0)
+	{
+		PQExpBuffer out = createPQExpBuffer();
+
+		int			i_inherited = PQfnumber(res, "inherited");
+		int			i_ndistinct = PQfnumber(res, "n_distinct");
+		int			i_dependencies = PQfnumber(res, "dependencies");
+		int			i_mcv = PQfnumber(res, "most_common_vals");
+		int			i_mcv_nulls = PQfnumber(res, "most_common_val_nulls");
+		int			i_mcf = PQfnumber(res, "most_common_freqs");
+		int			i_mcbf = PQfnumber(res, "most_common_base_freqs");
+		int			i_exprs = PQfnumber(res, "exprs");
+
+		for (int i = 0; i < nstats; i++)
+		{
+			if (PQgetisnull(res, i, i_inherited))
+				pg_fatal("inherited cannot be NULL");
+
+			appendPQExpBufferStr(out,
+								 "SELECT * FROM pg_catalog.pg_restore_extended_stats(\n");
+			appendPQExpBuffer(out, "\t'version', '%d'::integer,\n",
+							  fout->remoteVersion);
+			appendPQExpBufferStr(out, "\t'statistics_schemaname', ");
+			appendStringLiteralAH(out, statsextinfo->dobj.namespace->dobj.name, fout);
+			appendPQExpBufferStr(out, ",\n\t'statistics_name', ");
+			appendStringLiteralAH(out, statsextinfo->dobj.name, fout);
+			appendNamedArgument(out, fout, "inherited", "boolean",
+								PQgetvalue(res, i, i_inherited));
+
+			if (!PQgetisnull(res, i, i_ndistinct))
+				appendNamedArgument(out, fout, "n_distinct", "pg_ndistinct",
+									PQgetvalue(res, i, i_ndistinct));
+
+			if (!PQgetisnull(res, i, i_dependencies))
+				appendNamedArgument(out, fout, "dependencies", "pg_dependencies",
+									PQgetvalue(res, i, i_dependencies));
+
+			if (!PQgetisnull(res, i, i_mcv))
+				appendNamedArgument(out, fout, "most_common_vals", "text[]",
+									PQgetvalue(res, i, i_mcv));
+
+			if (!PQgetisnull(res, i, i_mcv_nulls))
+				appendNamedArgument(out, fout, "most_common_val_nulls", "boolean[]",
+									PQgetvalue(res, i, i_mcv_nulls));
+
+			if (!PQgetisnull(res, i, i_mcf))
+				appendNamedArgument(out, fout, "most_common_freqs", "double precision[]",
+									PQgetvalue(res, i, i_mcf));
+
+			if (!PQgetisnull(res, i, i_mcbf))
+				appendNamedArgument(out, fout, "most_common_base_freqs", "double precision[]",
+									PQgetvalue(res, i, i_mcbf));
+
+			if (!PQgetisnull(res, i, i_exprs))
+				appendNamedArgument(out, fout, "exprs", "text[]",
+									PQgetvalue(res, i, i_exprs));
+
+			appendPQExpBufferStr(out, "\n);\n");
+		}
+
+		ArchiveEntry(fout, nilCatalogId, createDumpId(),
+					 ARCHIVE_OPTS(.tag = statsextinfo->dobj.name,
+								  .namespace = statsextinfo->dobj.namespace->dobj.name,
+								  .owner = statsextinfo->rolname,
+								  .description = "EXTENDED STATISTICS DATA",
+								  .section = SECTION_POST_DATA,
+								  .createStmt = out->data,
+								  .deps = &statsextinfo->dobj.dumpId,
+								  .nDeps = 1));
+		destroyPQExpBuffer(out);
+	}
+	PQclear(res);
+}
+
 /*
  * dumpConstraint
  *	  write out to fout a user-defined constraint
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 386e21e0c59..7b1dc802657 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -4934,6 +4934,34 @@ my %tests = (
 		},
 	},
 
+	#
+	# EXTENDED stats will end up in SECTION_POST_DATA.
+	#
+	'extended_statistics_import' => {
+		create_sql => '
+			CREATE TABLE dump_test.has_ext_stats
+			AS SELECT g.g AS x, g.g / 2 AS y FROM generate_series(1,100) AS g(g);
+			CREATE STATISTICS dump_test.es1 ON x, (y % 2) FROM dump_test.has_ext_stats;
+			ANALYZE dump_test.has_ext_stats;',
+		regexp => qr/^
+			\QSELECT * FROM pg_catalog.pg_restore_extended_stats(\E\s+/xm,
+		like => {
+			%full_runs,
+			%dump_test_schema_runs,
+			no_data_no_schema => 1,
+			no_schema => 1,
+			section_post_data => 1,
+			statistics_only => 1,
+			schema_only_with_statistics => 1,
+		},
+		unlike => {
+			exclude_dump_test_schema => 1,
+			no_statistics => 1,
+			only_dump_measurement => 1,
+			schema_only => 1,
+		},
+	},
+
 	#
 	# While attribute stats (aka pg_statistic stats) only appear for tables
 	# that have been analyzed, all tables will have relation stats because
-- 
2.49.0

---

From: Corey Huinker <[email-masked]>
Date: Tue, 12 Aug 2025 13:28:14 -0400

> No command line flag changes needed.

Rebased. Enjoy.
From b84c59439207aef241406a6c283d31476222d605 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Sat, 7 Jun 2025 23:17:03 -0400
Subject: [PATCH v5 1/5] Refactor output format of pg_ndistinct and add working
 input function.

 
 
+typedef enum
+{

 /*
  * pg_ndistinct
 
 	initStringInfo(&str);

 		if (i > 0)

 
 
index c66e09f8b16..1d73bcf40ff 100644

 -- correct estimates

 SELECT * FROM check_estimated_rows('SELECT COUNT(*) FROM ndistinct GROUP BY a, b');
@@ -3518,4 +3518,36 @@ SELECT FROM sb_1 LEFT JOIN sb_2
 
 RESET enable_nestloop;
 RESET enable_mergejoin;
index 9ce4c670ecb..5749bfc6a8a 100644
@@ -1799,4 +1799,16 @@ SELECT FROM sb_1 LEFT JOIN sb_2
 RESET enable_nestloop;
 RESET enable_mergejoin;

base-commit: b227b0bb4e032e19b3679bedac820eba3ac0d1cf
-- 
2.50.1

From 8a24b676e1421b1f3378acb370367498e40668ac Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Sun, 8 Jun 2025 00:15:40 -0400
Subject: [PATCH v5 2/5] Refactor output format of pg_dependencies and add
 working input function.

 
 
+typedef enum
+{
+	DEPS_EXPECT_START = 0,
+	DEPS_EXPECT_ITEM,
+	DEPS_EXPECT_KEY,

 /*
 
 	initStringInfo(&str);

 		if (i > 0)

 
 
index 1d73bcf40ff..432690fbd53 100644

 SELECT * FROM check_estimated_rows('SELECT * FROM functional_dependencies WHERE (a * 2) = 2 AND upper(b) = ''1''');
@@ -3550,4 +3550,16 @@ SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": 1.0000},
  [{"attributes": [2, 3], "dependency": 4, "degree": 1.000000}, {"attributes": [2, -1], "dependency": 4, "degree": 0.000000}, {"attributes": [2, 3, -1], "dependency": 4, "degree": 0.500000}, {"attributes": [1, 3, -1, -2], "dependency": 4, "degree": 1.000000}]
 (1 row)
 
index 5749bfc6a8a..f542fcb2b83 100644
@@ -1811,4 +1811,16 @@ SELECT '[{"attributes" : [2,3], "ndistinct" : 4},
 
2.50.1

From 5e713561fe3a55e7a37d15f35a0797ff913d225a Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Thu, 26 Dec 2024 05:02:06 -0500
Subject: [PATCH v5 3/5] Expose attribute statistics functions for use in
 extended_stats.

* get_attr_stat_type()

index e8241926d2c..77511d4326c 100644

 /*
2.50.1

From bb2b7699ab4cef75ee1a75d34f0de9b7b59089ed Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Fri, 3 Jan 2025 13:43:29 -0500
Subject: [PATCH v5 4/5] Add extended statistics support functions.

 doc/src/sgml/func/func-admin.sgml             |   98 ++

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 118d6da1ace..f7c78cc6a15 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12566,6 +12566,24 @@
   proname => 'gist_translate_cmptype_common', prorettype => 'int2',
   proargtypes => 'int4', prosrc => 'gist_translate_cmptype_common' },
 
 { oid => '6399', descr => 'information about in-progress asynchronous IOs',

+
+void
 
+/*
 
+/*
index af0b99243c6..9ef8349bcb9 100644

 

 /*
  * Compute requested extended stats, using the rows sampled for the plain
@@ -2611,3 +2704,1014 @@ make_build_data(Relation rel, StatExtEntry *stat, int numrows, HeapTuple *rows,
 
 	return result;
 }
+
+static HeapTuple
index f59fb821543..7094192c48a 100644
 
 	return s;
 }
+
+/*
 
+/*
 
 DROP TABLE stats_temp;

 

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 446fdfe56f4..1e1880edb5c 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -2147,6 +2147,104 @@ SELECT pg_restore_attribute_stats(
2.50.1

From d81be45ee1a819d8270510342841f78b27995a16 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Sat, 21 Jun 2025 03:16:24 -0400
Subject: [PATCH v5 5/5] Include Extended Statistics in pg_dump.

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index d9041dad720..df708e4ced6 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -68,6 +68,7 @@ enum _dumpPreparedQueries
 	PREPQUERY_DUMPCOMPOSITETYPE,
 	PREPQUERY_DUMPDOMAIN,
 	PREPQUERY_DUMPENUMTYPE,
+	PREPQUERY_DUMPEXTSTATSSTATS,
 	PREPQUERY_DUMPFUNC,
 	PREPQUERY_DUMPOPR,
 	PREPQUERY_DUMPRANGETYPE,
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 3c3acbaccdb..c9a5b6e5a75 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3008,7 +3008,8 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 		strcmp(te->desc, "SEARCHPATH") == 0)
 		return REQ_SPECIAL;
 
index fc7a6639163..ea494d9605d 100644
@@ -324,6 +324,7 @@ static void dumpSequenceData(Archive *fout, const TableDataInfo *tdinfo);
@@ -8177,6 +8178,9 @@ getExtendedStatistics(Archive *fout)

 	PQclear(res);
@@ -11629,6 +11633,7 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 			break;
 		case DO_STATSEXT:
 			dumpStatisticsExt(fout, (const StatsExtInfo *) dobj);
+			dumpStatisticsExtStats(fout, (const StatsExtInfo *) dobj);
 			break;
 		case DO_REFRESH_MATVIEW:
 			refreshMatViewData(fout, (const TableDataInfo *) dobj);
@@ -18432,6 +18437,230 @@ dumpStatisticsExt(Archive *fout, const StatsExtInfo *statsextinfo)
 	free(qstatsextname);
 }
 
+/*
index e7a2d64f741..08a1fc3fa03 100644
@@ -5013,6 +5013,34 @@ my %tests = (
 		},
 	},
 
+	#
2.50.1

---

From: Corey Huinker <[email-masked]>
Date: Fri, 12 Sep 2025 00:55:56 -0400

> Rebased. Enjoy.

Rebased.

From 9590e6d911ce5c200e372df4be259c0385842b88 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Sat, 7 Jun 2025 23:17:03 -0400
Subject: [PATCH v6 1/5] Refactor output format of pg_ndistinct and add working
 input function.

 
 
+typedef enum
+{

 /*
  * pg_ndistinct
 
 	initStringInfo(&str);

 		if (i > 0)

 
 
index a1f83b58b23..3f07e3799e4 100644
@@ -473,9 +473,9 @@ SELECT s.stxkind, d.stxdndistinct
 
@@ -555,9 +555,9 @@ SELECT s.stxkind, d.stxdndistinct
 
 -- correct estimates
@@ -704,9 +704,9 @@ SELECT s.stxkind, d.stxdndistinct
 
@@ -753,9 +753,9 @@ SELECT s.stxkind, d.stxdndistinct
 
 SELECT * FROM check_estimated_rows('SELECT COUNT(*) FROM ndistinct GROUP BY a, b');
@@ -3544,4 +3544,36 @@ SELECT FROM sb_1 LEFT JOIN sb_2
 
 RESET enable_nestloop;
 RESET enable_mergejoin;
index 823c7db9dab..b3d7977f1ae 100644
@@ -1811,4 +1811,16 @@ SELECT FROM sb_1 LEFT JOIN sb_2
 RESET enable_nestloop;
 RESET enable_mergejoin;

base-commit: 2d756ebbe857e3d395d18350bf232300ebd23981
-- 
2.51.0

From 90c1cdcb32641983f5933a7f997cc34bc9d86d64 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Sun, 8 Jun 2025 00:15:40 -0400
Subject: [PATCH v6 2/5] Refactor output format of pg_dependencies and add
 working input function.

 
 
+typedef enum
+{
+	DEPS_EXPECT_START = 0,
+	DEPS_EXPECT_ITEM,
+	DEPS_EXPECT_KEY,

 /*
 
 	initStringInfo(&str);

 		if (i > 0)

 
 
index 3f07e3799e4..c182c7706aa 100644
@@ -1307,9 +1307,9 @@ CREATE STATISTICS func_deps_stat (dependencies) ON a, b, c FROM functional_depen
 
@@ -1649,9 +1649,9 @@ CREATE STATISTICS func_deps_stat (dependencies) ON (a * 2), upper(b), (c + 1) FR
 
 SELECT * FROM check_estimated_rows('SELECT * FROM functional_dependencies WHERE (a * 2) = 2 AND upper(b) = ''1''');
@@ -3576,4 +3576,16 @@ SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": 1.0000},
  [{"attributes": [2, 3], "dependency": 4, "degree": 1.000000}, {"attributes": [2, -1], "dependency": 4, "degree": 0.000000}, {"attributes": [2, 3, -1], "dependency": 4, "degree": 0.500000}, {"attributes": [1, 3, -1, -2], "dependency": 4, "degree": 1.000000}]
 (1 row)
 
index b3d7977f1ae..626a0aa94af 100644
@@ -1823,4 +1823,16 @@ SELECT '[{"attributes" : [2,3], "ndistinct" : 4},
 
2.51.0

From c6509db19dcd6f7530630d22aebf2c87f28475a4 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Thu, 26 Dec 2024 05:02:06 -0500
Subject: [PATCH v6 3/5] Expose attribute statistics functions for use in
 extended_stats.

* get_attr_stat_type()

index 1db6a7f784c..cc5a2557f07 100644

 /*
  * Insert or Update Attribute Statistics
@@ -571,7 +557,7 @@ get_attr_expr(Relation rel, int attnum)
 /*
  * Derive type information from the attribute.
  */
-static void
+void
 get_attr_stat_type(Oid reloid, AttrNumber attnum,
 				   Oid *atttypid, int32 *atttypmod,
 				   char *atttyptype, Oid *atttypcoll,
@@ -653,7 +639,7 @@ get_attr_stat_type(Oid reloid, AttrNumber attnum,
 /*
  * Derive element type information from the attribute type.
  */
-static bool
+bool
 get_elem_stat_type(Oid atttypid, char atttyptype,
 				   Oid *elemtypid, Oid *elem_eq_opr)
 {
@@ -693,7 +679,7 @@ get_elem_stat_type(Oid atttypid, char atttyptype,
  * to false. If the resulting array contains NULLs, raise a WARNING and set ok
  * to false. Otherwise, set ok to true.
  */
-static Datum
+Datum
 text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d, Oid typid,
 				  int32 typmod, bool *ok)
 {
@@ -746,7 +732,7 @@ text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d, Oid typid,
  * Find and update the slot with the given stakind, or use the first empty
  * slot.
  */
-static void
+void
 set_stats_slot(Datum *values, bool *nulls, bool *replaces,
 			   int16 stakind, Oid staop, Oid stacoll,
 			   Datum stanumbers, bool stanumbers_isnull,
@@ -870,7 +856,7 @@ delete_pg_statistic(Oid reloid, AttrNumber attnum, bool stainherit)
 /*
  * Initialize values and nulls for a new stats tuple.
  */
-static void
+void
 init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
 					   Datum *values, bool *nulls, bool *replaces)
 {
-- 
2.51.0

From ea44e6817bf911d76a615886fb3ae7a3f85e54ce Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Fri, 3 Jan 2025 13:43:29 -0500
Subject: [PATCH v6 4/5] Add extended statistics support functions.

index 03e82d28c87..73146a94b9a 100644
@@ -12578,6 +12578,24 @@

 
+
+void
 
+/*
 
+/*
index 3c3d2d315c6..f2c8d4c69cc 100644

 

 /*
  * Compute requested extended stats, using the rows sampled for the plain
@@ -2612,3 +2705,1014 @@ make_build_data(Relation rel, StatExtEntry *stat, int numrows, HeapTuple *rows,
 
 	return result;
 }
+
+static HeapTuple
 
 	return s;
 }
+
+/*
 
+/*
index 9e615ccd0af..1d1736ffcfc 100644
 
 DROP TABLE stats_temp;

 

index 57ff333159f..b52e22e4fb0 100644
@@ -2148,6 +2148,104 @@ SELECT pg_restore_attribute_stats(
2.51.0

From 8cc535c08284750751891848b1aa81c5ca8f48d9 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Sat, 21 Jun 2025 03:16:24 -0400
Subject: [PATCH v6 5/5] Include Extended Statistics in pg_dump.

index 058b5d659ba..5fc39402be5 100644
 
index b4c45ad803e..84db6ca9489 100644
@@ -8245,6 +8246,9 @@ getExtendedStatistics(Archive *fout)

 	PQclear(res);
@@ -11697,6 +11701,7 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 			break;
 		case DO_STATSEXT:
 			dumpStatisticsExt(fout, (const StatsExtInfo *) dobj);
+			dumpStatisticsExtStats(fout, (const StatsExtInfo *) dobj);
 			break;
 		case DO_REFRESH_MATVIEW:
 			refreshMatViewData(fout, (const TableDataInfo *) dobj);
@@ -18500,6 +18505,230 @@ dumpStatisticsExt(Archive *fout, const StatsExtInfo *statsextinfo)
 	free(qstatsextname);
 }
 
+/*
 
+	#
2.51.0

---

From: Corey Huinker <[email-masked]>
Date: Sat, 18 Oct 2025 20:27:58 -0400

> Rebased.

And rebased again to conform to  688dc6299 and 4bd919129.
From e801b5d31d2a34f4a43f2ac373a892996ecdbfaf Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Sat, 7 Jun 2025 23:17:03 -0400
Subject: [PATCH v7 1/5] Refactor output format of pg_ndistinct and add working
 input function.

The change in format is adequately described from the changes to
src/test/regress/expected/stats_ext.out so description here is
redundant.
---
 src/backend/statistics/mvdistinct.c     | 463 +++++++++++++++++++++++-
 src/test/regress/expected/stats_ext.out |  46 ++-
 src/test/regress/sql/stats_ext.sql      |  12 +
 3 files changed, 493 insertions(+), 28 deletions(-)

 
+typedef enum
+{

 /*
  * pg_ndistinct
 
 	initStringInfo(&str);

 		if (i > 0)

 
 
index 73a7ef97355..82d4d479ec8 100644
@@ -480,9 +480,9 @@ SELECT s.stxkind, d.stxdndistinct
 
@@ -562,9 +562,9 @@ SELECT s.stxkind, d.stxdndistinct
 
 -- correct estimates
@@ -711,9 +711,9 @@ SELECT s.stxkind, d.stxdndistinct
 
@@ -760,9 +760,9 @@ SELECT s.stxkind, d.stxdndistinct
 
 SELECT * FROM check_estimated_rows('SELECT COUNT(*) FROM ndistinct GROUP BY a, b');
@@ -3569,6 +3569,28 @@ SELECT * FROM check_estimated_rows('SELECT * FROM sb_2 WHERE extstat_small(y)');
        196 |    196
 (1 row)
 
 DROP FUNCTION extstat_small(x numeric);
index 96771600d57..dd49c530e3a 100644
@@ -1831,6 +1831,18 @@ ANALYZE sb_2;
 
 SELECT * FROM check_estimated_rows('SELECT * FROM sb_2 WHERE extstat_small(y)');
 
 DROP FUNCTION extstat_small(x numeric);

base-commit: 615ff828e1cb8eaa2a987d4390c5c9970fc1a3e6
-- 
2.51.0

From 242e68f0bd1e7fff8d31117abf8c9991398bcbbb Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Sun, 8 Jun 2025 00:15:40 -0400
Subject: [PATCH v7 2/5] Refactor output format of pg_dependencies and add
 working input function.

The change in format is adequately described from the changes to
src/test/regress/expected/stats_ext.out so description here is
redundant.
---
 src/backend/statistics/dependencies.c   | 491 ++++++++++++++++++++++--
 src/test/regress/expected/stats_ext.out |  34 +-
 src/test/regress/sql/stats_ext.sql      |  12 +
 3 files changed, 506 insertions(+), 31 deletions(-)

 
+typedef enum
+{
+	DEPS_EXPECT_START = 0,
+	DEPS_EXPECT_ITEM,
+	DEPS_EXPECT_KEY,

 /*
 
 	initStringInfo(&str);

 		if (i > 0)

 
 
index 82d4d479ec8..6609b039453 100644
@@ -1314,9 +1314,9 @@ CREATE STATISTICS func_deps_stat (dependencies) ON a, b, c FROM functional_depen
 
@@ -1656,9 +1656,9 @@ CREATE STATISTICS func_deps_stat (dependencies) ON (a * 2), upper(b), (c + 1) FR
 
 SELECT * FROM check_estimated_rows('SELECT * FROM functional_dependencies WHERE (a * 2) = 2 AND upper(b) = ''1''');
@@ -3591,6 +3591,28 @@ ERROR:  malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : 4},
 LINE 1: SELECT '[{"attributes" : [2,3], "ndistinct" : 4},
                ^
 DETAIL:  attnum list duplicate value found: 2
+-- Test input function of pg_dependencies.
+SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": 1.0000},
+         {"attributes" : [2,-1], "dependency" : 4, "degree": 0.0000},
+         {"attributes" : [2,3,-1], "dependency" : 4, "degree": 0.5000},
+         {"attributes" : [1,3,-1,-2], "dependency" : 4, "degree": 1.0000}]'::pg_dependencies;
+                                                                                                                          pg_dependencies                                                                                                                          
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ [{"attributes": [2, 3], "dependency": 4, "degree": 1.000000}, {"attributes": [2, -1], "dependency": 4, "degree": 0.000000}, {"attributes": [2, 3, -1], "dependency": 4, "degree": 0.500000}, {"attributes": [1, 3, -1, -2], "dependency": 4, "degree": 1.000000}]
+(1 row)
+
+-- error, cannot duplicate attribute
+SELECT '[{"attributes": [6], "dependency": 6, "degree": 0.292508},
+         {"attributes": [-2], "dependency": -1, "degree": 0.113999},
+         {"attributes": [6, -2], "dependency": -1, "degree": 0.348479},
+         {"attributes": [-1, -2], "dependency": 6, "degree": 0.839691}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes": [6], "dependency": 6, "degree": 0.292508},
+         {"attributes": [-2], "dependency": -1, "degree": 0.113999},
+         {"attributes": [6, -2], "dependency": -1, "degree": 0.348479},
+         {"attributes": [-1, -2], "dependency": 6, "degree": 0.839691}]"
+LINE 1: SELECT '[{"attributes": [6], "dependency": 6, "degree": 0.29...
+               ^
+DETAIL:  attnum list duplicate value found: 6
 -- Tidy up
 DROP TABLE sb_1, sb_2 CASCADE;
 DROP FUNCTION extstat_small(x numeric);
diff --git a/src/test/regress/sql/stats_ext.sql b/src/test/regress/sql/stats_ext.sql
index dd49c530e3a..c674d189a51 100644
--- a/src/test/regress/sql/stats_ext.sql
+++ b/src/test/regress/sql/stats_ext.sql
@@ -1843,6 +1843,18 @@ SELECT '[{"attributes" : [2,3], "ndistinct" : 4},
          {"attributes" : [2,3,2], "ndistinct" : 4},
          {"attributes" : [1,3,-1,-2], "ndistinct" : 4}]'::pg_ndistinct;
 
 DROP FUNCTION extstat_small(x numeric);
-- 
2.51.0

From 2f0ed0bf4af3229e7b71cf6e6650227a5affbf58 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Thu, 26 Dec 2024 05:02:06 -0500
Subject: [PATCH v7 3/5] Expose attribute statistics functions for use in
 extended_stats.

* get_attr_stat_type()

index c5df83282e0..838d2afb0ff 100644
@@ -102,23 +102,9 @@ static struct StatsArgInfo cleararginfo[] =

 /*
  * Insert or Update Attribute Statistics
@@ -574,7 +560,7 @@ get_attr_expr(Relation rel, int attnum)
 /*
  * Derive type information from the attribute.
  */
-static void
+void
 get_attr_stat_type(Oid reloid, AttrNumber attnum,
 				   Oid *atttypid, int32 *atttypmod,
 				   char *atttyptype, Oid *atttypcoll,
@@ -656,7 +642,7 @@ get_attr_stat_type(Oid reloid, AttrNumber attnum,
 /*
  * Derive element type information from the attribute type.
  */
-static bool
+bool
 get_elem_stat_type(Oid atttypid, char atttyptype,
 				   Oid *elemtypid, Oid *elem_eq_opr)
 {
@@ -696,7 +682,7 @@ get_elem_stat_type(Oid atttypid, char atttyptype,
  * to false. If the resulting array contains NULLs, raise a WARNING and set ok
  * to false. Otherwise, set ok to true.
  */
-static Datum
+Datum
 text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d, Oid typid,
 				  int32 typmod, bool *ok)
 {
@@ -749,7 +735,7 @@ text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d, Oid typid,
  * Find and update the slot with the given stakind, or use the first empty
  * slot.
  */
-static void
+void
 set_stats_slot(Datum *values, bool *nulls, bool *replaces,
 			   int16 stakind, Oid staop, Oid stacoll,
 			   Datum stanumbers, bool stanumbers_isnull,
@@ -873,7 +859,7 @@ delete_pg_statistic(Oid reloid, AttrNumber attnum, bool stainherit)
 /*
  * Initialize values and nulls for a new stats tuple.
  */
-static void
+void
 init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
 					   Datum *values, bool *nulls, bool *replaces)
 {
-- 
2.51.0

From c05844379753ab41e5a7ec2095458f75085ef22a Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Fri, 3 Jan 2025 13:43:29 -0500
Subject: [PATCH v7 4/5] Add extended statistics support functions.

 src/backend/statistics/extended_stats.c       | 1126 +++++++++++++++++
 9 files changed, 2570 insertions(+)

index b51d2b17379..877c1dfc80b 100644
@@ -12591,6 +12591,24 @@

 
+
+void
 
+/*
 
+/*
index 3c3d2d315c6..e582d71ab3f 100644
@@ -18,21 +18,28 @@
 
+#include "nodes/makefuncs.h"
@@ -72,6 +79,71 @@ typedef struct StatExtEntry

@@ -99,6 +171,28 @@ static StatsBuildData *make_build_data(Relation rel, StatExtEntry *stat,

 /*
  * Compute requested extended stats, using the rows sampled for the plain
@@ -2612,3 +2706,1035 @@ make_build_data(Relation rel, StatExtEntry *stat, int numrows, HeapTuple *rows,
 
 	return result;
 }
+
+static HeapTuple
+	Relation	rel;
+	Oid			locked_table = InvalidOid;
+	/* Roundabout way of getting a RangeVar on the underlying table */
+	rel = relation_open(stxform->stxrelid, AccessShareLock);
+	/* no need to fetch reloid, we already have it */
+	RangeVarGetRelidExtended(makeRangeVar(nspname,
+													RelationGetRelationName(rel), -1),
+				   ShareUpdateExclusiveLock, 0,
+				   RangeVarCallbackForStats, &locked_table);
+	relation_close(rel, AccessShareLock);
+	Relation	rel;
+	Oid			locked_table = InvalidOid;
+	/* Roundabout way of getting a RangeVar on the underlying table */
+	rel = relation_open(stxform->stxrelid, AccessShareLock);
+	/* no need to fetch reloid, we already have it */
+	RangeVarGetRelidExtended(makeRangeVar(nspname,
+													RelationGetRelationName(rel), -1),
+				   ShareUpdateExclusiveLock, 0,
+				   RangeVarCallbackForStats, &locked_table);
+	relation_close(rel, AccessShareLock);
index f59fb821543..a917079ceb0 100644
 
 	return s;
 }
+
+/*
+										   (Node *) &escontext, &item->values[j]))
 
+/*
index 98ce7dc2841..266e29b66f0 100644
 
 DROP TABLE stats_temp;

 

index 1b465bc8ba7..574d4a35a64 100644
@@ -2167,6 +2167,104 @@ SELECT pg_restore_attribute_stats(

From 7041a6333c9ad273d16cf844cf9260dd4e795fb8 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Sat, 21 Jun 2025 03:16:24 -0400
Subject: [PATCH v7 5/5] Include Extended Statistics in pg_dump.

index 59eaecb4ed7..1bfd296e0ee 100644
 
index 4b8cd49df09..5b2f3c1fbef 100644
@@ -8258,6 +8259,9 @@ getExtendedStatistics(Archive *fout)

 	PQclear(res);
@@ -11712,6 +11716,7 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 			break;
 		case DO_STATSEXT:
 			dumpStatisticsExt(fout, (const StatsExtInfo *) dobj);
+			dumpStatisticsExtStats(fout, (const StatsExtInfo *) dobj);
 			break;
 		case DO_REFRESH_MATVIEW:
 			refreshMatViewData(fout, (const TableDataInfo *) dobj);
@@ -18515,6 +18520,230 @@ dumpStatisticsExt(Archive *fout, const StatsExtInfo *statsextinfo)
 	free(qstatsextname);
 }
 
+/*
index 445a541abf6..6681265974f 100644
@@ -4772,6 +4772,34 @@ my %tests = (
 		},
 	},
 
+	#

---

From: Michael Paquier <[email-masked]>
Date: Tue, 21 Oct 2025 14:48:37 +0900

> And rebased again to conform to  688dc6299 and 4bd919129.

The format redesign for extended stats is pretty nice, as done in
0001.  I think that this patch should be split in two, actually, as it
tackles two issues:
- One patch for the format change.
- Second patch for the introduction of the input function, useful on
its own to allow more regression tests paths for the format generated.

Similar split comment for 0002 regarding pg_dependencies.  The format
change is one thing.  The input function is cool to have for input
validation, still not absolutely mandatory for the core part of the
format change.

The functions exposed in 0003 should be renamed to match more with the
style of the rest, aka it is a bit hard to figure out what they do at
first sight.  Presumably, these should be prefixed with some
"statext_", except text_to_stavalues() which could still be named the
same.

Do you have some numbers regarding the increase in size this generates
for the catalogs?

0004 has been designed following the same model as the relation and
attribute stats.  That sounds OK here.

[...]

It would be nice to document why such things are around.  That would
be less guessing for somebody reading the code.

Reusing this small sequence from your pg_dump patch, executed on a v14
backend:
create schema dump_test;
CREATE TABLE dump_test.has_ext_stats
  AS SELECT g.g AS x, g.g / 2 AS y FROM generate_series(1,100) AS g(g);
CREATE STATISTICS dump_test.es1 ON x, (y % 2) FROM dump_test.has_ext_stats;
ANALYZE dump_test.has_ext_stats;

Then pg_dump fails:
pg_dump: error: query failed: ERROR:  column e.inherited does not exist
LINE 2: ...hemaname = $1 AND e.statistics_name = $2 ORDER BY e.inherite...

+        * TODO: Until v18 is released the master branch has a
+        * server_version_num of 180000. We will update this to 190000
as soon
+        * as the master branch updates.

This part has not been updated.

+       Assert(item.nattributes > 0);   /* TODO: elog? */
[...]
+       Assert(dependency->nattributes > 1);    /* TODO: elog? */
Yes and yes.  It seems like it should be possible to craft some input
that triggers these..

+void

Double declaration of this routine in dependencies.c.

Perhaps some of the regression tests could use some jsonb_pretty() in
the outputs generated.  Some of the results generated are very hard to
parse, something that would become harder in the buildfarm.  This
comment starts with 0001 for stxdndistinct.

I have mixed feelings about 0005, FWIW.  I am wondering if we should
not lift the needle a bit here and only support the dump of extended
statistics when dealing with a backend of at least v19.  This would
mean that we would only get the full benefit of this feature once
people upgrade to v20 or dump from a pg_dump with --statistics from at
least v19, but with the long-term picture in mind this would also make
the dump/restore picture of the patch dead simple (spoiler: I like
simple). 

Tomas, what is your take about the format changes and my argument
about the backward requirements of pg_dump (about not dumping these
stats if connecting to a server older than v18, included)?
--
Michael

---

From: Michael Paquier <[email-masked]>
Date: Tue, 21 Oct 2025 14:57:00 +0900

> stats if connecting to a server older than v18, included)?

By the way, the patch is still needed in the previous CF of September:
https://commitfest.postgresql.org/patch/5517/

You may want to move it.
--
Michael

---

From: Corey Huinker <[email-masked]>
Date: Wed, 22 Oct 2025 14:55:31 +0300

> "statext_", except text_to_stavalues() which could still be named the

That prefix would probably be statatt_ or statattr_.

> Do you have some numbers regarding the increase in size this generates

Sorry, I don't understand. There shouldn't be any increase inside the
catalogs as the internal storage of the datatypes hasn't changed, so I can
only conclude that you're referring to something else.

> be less guessing for somebody reading the code.

The equivalent structures in attribute_stats.c will need documenting too.

Noted.

> Yes and yes.  It seems like it should be possible to craft some input

+1

> comment starts with 0001 for stxdndistinct.

Can do.

> the dump/restore picture of the patch dead simple (spoiler: I like

I also like simple.

Right now we have a situation where the vast majority of databases can
carry forward all of their stats via pg_upgrade, except for those databases
that have extended stats. The trouble is, most customers don't know if
their database uses extended statistics or not, and those that do are in
for some bad query plans if they haven't run vacuumdb --missing-stats-only.
Explaining that to customers is complicated, especially when most of them
do not know what extended stats are, let alone whether they have them. It
would be a lot simpler to just say "all stats are carried over on upgrade",
and vacuumdb becomes unnecessary, making upgrades one step simpler as well.

Given that, I think that the admittedly ugly transformation is worth it,
and sequestering it inside pg_dump is the smallest footprint it can have.
Earlier in this thread I posted some functions that did the translation
from the existing formats to the proposed new formats. We could include
those as new system functions, and that would make the dump code very
simple. Having said that, I don't know that there would be use for those
functions except inside pg_dump, hence the decision to do the transforms
right in the dump query.

If the format translation is a barrier to fetching existing extended stats,
then I'd be more inclined to keep the existing pg_ndistinct and
pg_dependencies data formats as they are now.

---

From: Michael Paquier <[email-masked]>
Date: Thu, 23 Oct 2025 08:46:27 +0900

> only conclude that you're referring to something else.

The new format meant more characters, perhaps I've just missed
something while quickly testing the patch..  Anyway, that's OK at this
stage.

> The equivalent structures in attribute_stats.c will need documenting too.

Right.  This sounds like a separate patch to me, impacting HEAD.

> and vacuumdb becomes unnecessary, making upgrades one step simpler as well.

Okay.

> functions except inside pg_dump, hence the decision to do the transforms

I'd prefer the new format.  One killer pushing in favor of the new
format that you are making upthread in favor of is that it makes much
easier the viewing, editing and injecting of these stats.  It's the
part of the patch where we would need Tomas' input on the matter
before deciding anything, I guess, as primary author of the original
facilities.  My view of the problem is just one opinion.

> pg_dependencies data formats as they are now.

Not necessarily, it can be possible to also take that in multiple
steps rather than a single one:
- First do the basics in v19 with the new format.
- Raise the bar to older versions.
--
Michael

---

From: Michael Paquier <[email-masked]>
Date: Thu, 23 Oct 2025 09:35:04 +0900

On Thu, Oct 23, 2025 at 08:46:27AM +0900, Michael Paquier wrote:
> easier the viewing, editing and injecting of these stats.

This is missing an "argument", as in "One killer argument pushing in
favor.." :D
--
Michael

---

From: Tomas Vondra <[email-masked]>
Date: Fri, 31 Oct 2025 21:22:55 +0100

> facilities.  My view of the problem is just one opinion.

Sorry for not paying much attention to this thread ...

My opinion is that we should both use the new format and keep the
pg_dump code to allow upgrading from older pre-19 versions.

There really is nothing special about the current format - I should have
used JSON (or any other established format) from the beginning. But I
only saw that as human-readable version of ephemeral data, it didn't
occur to me we'll use this to export/import stats cross versions. So if
we need to adjust that to make new use cases more convenient, let's bite
the bullet now.

If doing both is too complex / ugly, I think the pg_upgrade capability
is more valuable. I'd rather keep the old, less convenient format to
have pg_upgrade support for all versions.

Otherwise users may not benefit from this pg_upgrade feature for a
couple more years. Plenty of users delay upgrading until the EOL gets
close, and so might be unable to dump/restore extended stats for the
next ~5 years.

regards

-- 
Tomas Vondra

---

From: Michael Paquier <[email-masked]>
Date: Tue, 4 Nov 2025 16:13:55 +0900

> Sorry for not paying much attention to this thread ...

It was PGEU last week, it's not surprising knowing that you are in..
Europe :)

> pg_dump code to allow upgrading from older pre-19 versions.

Okay, thanks for the input.

> have pg_upgrade support for all versions.

Hmm.  I have been eyeing at the dump code once again, and it's not
that bad on a second lookup, so I'd be OK to incorporate the whole in
a review.

Okay.

I still think that we should split things a bit more in the patch set.
Corey has sent me a proposal in this direction, where most of the
entries can be reviewed and potentially applied separately:
1. pg_ndistinct output function change.
2. pg_ndistinct input function addition.
3. pg_dependencies output function change
4. pg_dependencies input function
5. Expose attribute statistics function and rename them attstat_* or
statatt_*
6. pg_restore_extended_stats
7. pg_dump with no ability to fetch old-format
pg_ndistinct/pg_dependences.
8. pg_dump working back as far as possible

I am not completely sold about the changes in the input/output
functions, but I'd be happy to consider more options with a rebased
patch set (with the dump issue on older clusters issue, while on it).
--
Michael

---

From: Corey Huinker <[email-masked]>
Date: Wed, 5 Nov 2025 01:38:56 -0500

> next ~5 years

Paquier's response got sidetracked because of an errant subject line
change, so I will try to recap:

* pg_dump code changes no longer seem as bad on second look
* proceed with breakout per off-list discussion

in that off-list discussion I proposed (though I was mostly echoing what I
thought Paquier wanted):

1. pg_ndistinct output function change.
2. pg_ndistinct input function addition.
3. pg_dependencies output function change
4. pg_dependencies input function
5. Expose attribute statistics function and rename them attstat_* or
statatt_*   (edit: and fix lack of comments on the enums and arrays)
6. pg_restore_extended_stats
7. pg_dump with no ability to fetch old-format pg_ndistinct/pg_dependences.
(edit: and fix inherited bug)
8. pg_dump working back as far as possible

Given that the pg_dump code no longer seems as bad, and Tomas is very much
in support of it, I've opted not to split out steps 7/8.
From 9814e4d66682898a91bd81a92cae4cebe6e7218e Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 4 Nov 2025 15:24:35 -0500
Subject: [PATCH v5 1/7] Refactor output format of pg_ndistinct.

The change in format is adequately described from the changes to
src/test/regress/expected/stats_ext.out so description here is
redundant.
---
 src/backend/statistics/mvdistinct.c     |  19 +--
 src/test/regress/expected/stats_ext.out | 156 +++++++++++++++++++++---
 src/test/regress/sql/stats_ext.sql      |  12 +-
 3 files changed, 154 insertions(+), 33 deletions(-)

diff --git a/src/backend/statistics/mvdistinct.c b/src/backend/statistics/mvdistinct.c
index 7e7a63405c8..ca60841b813 100644
--- a/src/backend/statistics/mvdistinct.c
+++ b/src/backend/statistics/mvdistinct.c
@@ -360,26 +360,27 @@ pg_ndistinct_out(PG_FUNCTION_ARGS)
 	StringInfoData str;
 
 	initStringInfo(&str);

 		if (i > 0)
 
-		for (j = 0; j < item.nattributes; j++)
-		{
-			AttrNumber	attnum = item.attributes[j];
+		if (item.nattributes <= 0)
+			elog(ERROR, "invalid zero-length attribute array in MVNDistinct");

 
 	PG_RETURN_CSTRING(str.data);
 }
diff --git a/src/test/regress/expected/stats_ext.out b/src/test/regress/expected/stats_ext.out
index 73a7ef97355..2dc771369e5 100644
--- a/src/test/regress/expected/stats_ext.out
+++ b/src/test/regress/expected/stats_ext.out
@@ -196,7 +196,7 @@ Statistics objects:
     "public.ab1_a_b_stats" ON a, b FROM ab1; STATISTICS 0
 
 ANALYZE ab1;
-SELECT stxname, stxdndistinct, stxddependencies, stxdmcv, stxdinherit
+SELECT stxname, jsonb_pretty(d.stxdndistinct::text::jsonb) AS stxdndistinct, stxddependencies, stxdmcv, stxdinherit
   FROM pg_statistic_ext s LEFT JOIN pg_statistic_ext_data d ON (d.stxoid = s.oid)
  WHERE s.stxname = 'ab1_a_b_stats';
     stxname    | stxdndistinct | stxddependencies | stxdmcv | stxdinherit 
@@ -476,13 +476,43 @@ SELECT * FROM check_estimated_rows('SELECT COUNT(*) FROM ndistinct GROUP BY a, (
 -- correct command
 CREATE STATISTICS s10 ON a, b, c FROM ndistinct;
 ANALYZE ndistinct;
-SELECT s.stxkind, d.stxdndistinct
+SELECT s.stxkind, jsonb_pretty(d.stxdndistinct::text::jsonb) AS stxdndistinct
   FROM pg_statistic_ext s, pg_statistic_ext_data d
  WHERE s.stxrelid = 'ndistinct'::regclass
    AND d.stxoid = s.oid;
- stxkind |                    stxdndistinct                    
----------+-----------------------------------------------------
- {d,f,m} | {"3, 4": 11, "3, 6": 11, "4, 6": 11, "3, 4, 6": 11}
+ stxkind |      stxdndistinct       
+---------+--------------------------
+ {d,f,m} | [                       +
+         |     {                   +
+         |         "ndistinct": 11,+
+         |         "attributes": [ +
+         |             3,          +
+         |             4           +
+         |         ]               +
+         |     },                  +
+         |     {                   +
+         |         "ndistinct": 11,+
+         |         "attributes": [ +
+         |             3,          +
+         |             6           +
+         |         ]               +
+         |     },                  +
+         |     {                   +
+         |         "ndistinct": 11,+
+         |         "attributes": [ +
+         |             4,          +
+         |             6           +
+         |         ]               +
+         |     },                  +
+         |     {                   +
+         |         "ndistinct": 11,+
+         |         "attributes": [ +
+         |             3,          +
+         |             4,          +
+         |             6           +
+         |         ]               +
+         |     }                   +
+         | ]
 (1 row)
 
 -- minor improvement, make sure the ctid does not break the matching
@@ -558,13 +588,43 @@ INSERT INTO ndistinct (a, b, c, filler1)
             mod(i,23) || ' dollars and zero cents'
        FROM generate_series(1,1000) s(i);
 ANALYZE ndistinct;
-SELECT s.stxkind, d.stxdndistinct
+SELECT s.stxkind, jsonb_pretty(d.stxdndistinct::text::jsonb) AS stxdndistinct
   FROM pg_statistic_ext s, pg_statistic_ext_data d
  WHERE s.stxrelid = 'ndistinct'::regclass
    AND d.stxoid = s.oid;
- stxkind |                      stxdndistinct                       
----------+----------------------------------------------------------
- {d,f,m} | {"3, 4": 221, "3, 6": 247, "4, 6": 323, "3, 4, 6": 1000}
+ stxkind |       stxdndistinct        
+---------+----------------------------
+ {d,f,m} | [                         +
+         |     {                     +
+         |         "ndistinct": 221, +
+         |         "attributes": [   +
+         |             3,            +
+         |             4             +
+         |         ]                 +
+         |     },                    +
+         |     {                     +
+         |         "ndistinct": 247, +
+         |         "attributes": [   +
+         |             3,            +
+         |             6             +
+         |         ]                 +
+         |     },                    +
+         |     {                     +
+         |         "ndistinct": 323, +
+         |         "attributes": [   +
+         |             4,            +
+         |             6             +
+         |         ]                 +
+         |     },                    +
+         |     {                     +
+         |         "ndistinct": 1000,+
+         |         "attributes": [   +
+         |             3,            +
+         |             4,            +
+         |             6             +
+         |         ]                 +
+         |     }                     +
+         | ]
 (1 row)
 
 -- correct estimates
@@ -623,7 +683,7 @@ SELECT * FROM check_estimated_rows('SELECT COUNT(*) FROM ndistinct GROUP BY a, (
 (1 row)
 
 DROP STATISTICS s10;
-SELECT s.stxkind, d.stxdndistinct
+SELECT s.stxkind, jsonb_pretty(d.stxdndistinct::text::jsonb) AS stxdndistinct
   FROM pg_statistic_ext s, pg_statistic_ext_data d
  WHERE s.stxrelid = 'ndistinct'::regclass
    AND d.stxoid = s.oid;
@@ -707,13 +767,43 @@ SELECT * FROM check_estimated_rows('SELECT COUNT(*) FROM ndistinct GROUP BY a, (
 
 CREATE STATISTICS s10 (ndistinct) ON (a+1), (b+100), (2*c) FROM ndistinct;
 ANALYZE ndistinct;
-SELECT s.stxkind, d.stxdndistinct
+SELECT s.stxkind, jsonb_pretty(d.stxdndistinct::text::jsonb) AS stxdndistinct
   FROM pg_statistic_ext s, pg_statistic_ext_data d
  WHERE s.stxrelid = 'ndistinct'::regclass
    AND d.stxoid = s.oid;
- stxkind |                           stxdndistinct                           
----------+-------------------------------------------------------------------
- {d,e}   | {"-1, -2": 221, "-1, -3": 247, "-2, -3": 323, "-1, -2, -3": 1000}
+ stxkind |       stxdndistinct        
+---------+----------------------------
+ {d,e}   | [                         +
+         |     {                     +
+         |         "ndistinct": 221, +
+         |         "attributes": [   +
+         |             -1,           +
+         |             -2            +
+         |         ]                 +
+         |     },                    +
+         |     {                     +
+         |         "ndistinct": 247, +
+         |         "attributes": [   +
+         |             -1,           +
+         |             -3            +
+         |         ]                 +
+         |     },                    +
+         |     {                     +
+         |         "ndistinct": 323, +
+         |         "attributes": [   +
+         |             -2,           +
+         |             -3            +
+         |         ]                 +
+         |     },                    +
+         |     {                     +
+         |         "ndistinct": 1000,+
+         |         "attributes": [   +
+         |             -1,           +
+         |             -2,           +
+         |             -3            +
+         |         ]                 +
+         |     }                     +
+         | ]
 (1 row)
 
 SELECT * FROM check_estimated_rows('SELECT COUNT(*) FROM ndistinct GROUP BY (a+1), (b+100)');
@@ -756,13 +846,43 @@ SELECT * FROM check_estimated_rows('SELECT COUNT(*) FROM ndistinct GROUP BY a, b
 
 CREATE STATISTICS s10 (ndistinct) ON a, b, (2*c) FROM ndistinct;
 ANALYZE ndistinct;
-SELECT s.stxkind, d.stxdndistinct
+SELECT s.stxkind, jsonb_pretty(d.stxdndistinct::text::jsonb) AS stxdndistinct
   FROM pg_statistic_ext s, pg_statistic_ext_data d
  WHERE s.stxrelid = 'ndistinct'::regclass
    AND d.stxoid = s.oid;
- stxkind |                        stxdndistinct                        
----------+-------------------------------------------------------------
- {d,e}   | {"3, 4": 221, "3, -1": 247, "4, -1": 323, "3, 4, -1": 1000}
+ stxkind |       stxdndistinct        
+---------+----------------------------
+ {d,e}   | [                         +
+         |     {                     +
+         |         "ndistinct": 221, +
+         |         "attributes": [   +
+         |             3,            +
+         |             4             +
+         |         ]                 +
+         |     },                    +
+         |     {                     +
+         |         "ndistinct": 247, +
+         |         "attributes": [   +
+         |             3,            +
+         |             -1            +
+         |         ]                 +
+         |     },                    +
+         |     {                     +
+         |         "ndistinct": 323, +
+         |         "attributes": [   +
+         |             4,            +
+         |             -1            +
+         |         ]                 +
+         |     },                    +
+         |     {                     +
+         |         "ndistinct": 1000,+
+         |         "attributes": [   +
+         |             3,            +
+         |             4,            +
+         |             -1            +
+         |         ]                 +
+         |     }                     +
+         | ]
 (1 row)
 
 SELECT * FROM check_estimated_rows('SELECT COUNT(*) FROM ndistinct GROUP BY a, b');
diff --git a/src/test/regress/sql/stats_ext.sql b/src/test/regress/sql/stats_ext.sql
index 96771600d57..207c431e68c 100644
--- a/src/test/regress/sql/stats_ext.sql
+++ b/src/test/regress/sql/stats_ext.sql
@@ -125,7 +125,7 @@ ALTER TABLE ab1 ALTER a SET STATISTICS -1;
 ALTER STATISTICS ab1_a_b_stats SET STATISTICS 0;
 \d ab1
 ANALYZE ab1;
-SELECT stxname, stxdndistinct, stxddependencies, stxdmcv, stxdinherit
+SELECT stxname, jsonb_pretty(d.stxdndistinct::text::jsonb) AS stxdndistinct, stxddependencies, stxdmcv, stxdinherit
   FROM pg_statistic_ext s LEFT JOIN pg_statistic_ext_data d ON (d.stxoid = s.oid)
  WHERE s.stxname = 'ab1_a_b_stats';
 ALTER STATISTICS ab1_a_b_stats SET STATISTICS -1;
@@ -297,7 +297,7 @@ CREATE STATISTICS s10 ON a, b, c FROM ndistinct;
 
 ANALYZE ndistinct;
 
-SELECT s.stxkind, d.stxdndistinct
+SELECT s.stxkind, jsonb_pretty(d.stxdndistinct::text::jsonb) AS stxdndistinct
   FROM pg_statistic_ext s, pg_statistic_ext_data d
  WHERE s.stxrelid = 'ndistinct'::regclass
    AND d.stxoid = s.oid;
@@ -338,7 +338,7 @@ INSERT INTO ndistinct (a, b, c, filler1)
 
 ANALYZE ndistinct;
 
-SELECT s.stxkind, d.stxdndistinct
+SELECT s.stxkind, jsonb_pretty(d.stxdndistinct::text::jsonb) AS stxdndistinct
   FROM pg_statistic_ext s, pg_statistic_ext_data d
  WHERE s.stxrelid = 'ndistinct'::regclass
    AND d.stxoid = s.oid;
@@ -364,7 +364,7 @@ SELECT * FROM check_estimated_rows('SELECT COUNT(*) FROM ndistinct GROUP BY a, (
 
 DROP STATISTICS s10;
 
-SELECT s.stxkind, d.stxdndistinct
+SELECT s.stxkind, jsonb_pretty(d.stxdndistinct::text::jsonb) AS stxdndistinct
   FROM pg_statistic_ext s, pg_statistic_ext_data d
  WHERE s.stxrelid = 'ndistinct'::regclass
    AND d.stxoid = s.oid;
@@ -399,7 +399,7 @@ CREATE STATISTICS s10 (ndistinct) ON (a+1), (b+100), (2*c) FROM ndistinct;
 
 ANALYZE ndistinct;
 
-SELECT s.stxkind, d.stxdndistinct
+SELECT s.stxkind, jsonb_pretty(d.stxdndistinct::text::jsonb) AS stxdndistinct
   FROM pg_statistic_ext s, pg_statistic_ext_data d
  WHERE s.stxrelid = 'ndistinct'::regclass
    AND d.stxoid = s.oid;
@@ -423,7 +423,7 @@ CREATE STATISTICS s10 (ndistinct) ON a, b, (2*c) FROM ndistinct;
 
 ANALYZE ndistinct;
 
-SELECT s.stxkind, d.stxdndistinct
+SELECT s.stxkind, jsonb_pretty(d.stxdndistinct::text::jsonb) AS stxdndistinct
   FROM pg_statistic_ext s, pg_statistic_ext_data d
  WHERE s.stxrelid = 'ndistinct'::regclass
    AND d.stxoid = s.oid;

base-commit: 5509055d6956745532e65ab218e15b99d87d66ce
-- 
2.51.1

From c28a9686478a2a28f23ac0e9ce809b905374a97a Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 4 Nov 2025 15:35:58 -0500
Subject: [PATCH v5 2/7] Add working input function for pg_ndistinct.

This will consume the format that was established when the output
function for pg_ndistinct was recently changed.

This will be needed for importing extended statistics.
---
 src/backend/statistics/mvdistinct.c     | 445 +++++++++++++++++++++++-
 src/test/regress/expected/stats_ext.out |  22 ++
 src/test/regress/sql/stats_ext.sql      |  12 +
 3 files changed, 472 insertions(+), 7 deletions(-)

diff --git a/src/backend/statistics/mvdistinct.c b/src/backend/statistics/mvdistinct.c
index ca60841b813..5b7b3aa26a4 100644
--- a/src/backend/statistics/mvdistinct.c
+++ b/src/backend/statistics/mvdistinct.c
@@ -27,9 +27,15 @@

+typedef enum
+{

 /*
  * pg_ndistinct
index 2dc771369e5..1d837badb96 100644
@@ -3689,6 +3689,28 @@ SELECT * FROM check_estimated_rows('SELECT * FROM sb_2 WHERE extstat_small(y)');
        196 |    196
 (1 row)
 
index 207c431e68c..2c306dcc971 100644

2.51.1

From 9b63dbf4af482503d69c994f63daf1b381c0856d Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 4 Nov 2025 15:59:31 -0500
Subject: [PATCH v5 3/7] Refactor output format of pg_dependencies.

The change in format is adequately described from the changes to
src/test/regress/expected/stats_ext.out so description here is
redundant.
---
 src/backend/statistics/dependencies.c   | 29 ++++----
 src/test/regress/expected/stats_ext.out | 95 ++++++++++++++++++++++---
 src/test/regress/sql/stats_ext.sql      |  7 +-
 3 files changed, 104 insertions(+), 27 deletions(-)

diff --git a/src/backend/statistics/dependencies.c b/src/backend/statistics/dependencies.c
index eb2fc4366b4..c150ddfb594 100644
--- a/src/backend/statistics/dependencies.c
+++ b/src/backend/statistics/dependencies.c
@@ -671,34 +671,33 @@ pg_dependencies_out(PG_FUNCTION_ARGS)
 {
 	bytea	   *data = PG_GETARG_BYTEA_PP(0);
 	MVDependencies *dependencies = statext_dependencies_deserialize(data);
-	int			i,
-				j;
 	StringInfoData str;
 
 	initStringInfo(&str);

 		if (i > 0)
 
-		appendStringInfoChar(&str, '"');
-		for (j = 0; j < dependency->nattributes; j++)
-		{
-			if (j == dependency->nattributes - 1)
-				appendStringInfoString(&str, " => ");
-			else if (j > 0)
-				appendStringInfoString(&str, ", ");
+		if (dependency->nattributes <= 1)
+			elog(ERROR, "invalid zero-length nattributes array in MVDependencies");

 
 	PG_RETURN_CSTRING(str.data);
 }
diff --git a/src/test/regress/expected/stats_ext.out b/src/test/regress/expected/stats_ext.out
index 1d837badb96..8cea29de314 100644
--- a/src/test/regress/expected/stats_ext.out
+++ b/src/test/regress/expected/stats_ext.out
@@ -196,7 +196,8 @@ Statistics objects:
     "public.ab1_a_b_stats" ON a, b FROM ab1; STATISTICS 0
 
 ANALYZE ab1;
-SELECT stxname, jsonb_pretty(d.stxdndistinct::text::jsonb) AS stxdndistinct, stxddependencies, stxdmcv, stxdinherit
+SELECT stxname, jsonb_pretty(d.stxdndistinct::text::jsonb) AS stxdndistinct,
+       jsonb_pretty(d.stxddependencies::text::jsonb) AS stxddependencies, stxdmcv, stxdinherit
   FROM pg_statistic_ext s LEFT JOIN pg_statistic_ext_data d ON (d.stxoid = s.oid)
  WHERE s.stxname = 'ab1_a_b_stats';
     stxname    | stxdndistinct | stxddependencies | stxdmcv | stxdinherit 
@@ -1433,10 +1434,48 @@ SELECT * FROM check_estimated_rows('SELECT * FROM functional_dependencies WHERE
 CREATE STATISTICS func_deps_stat (dependencies) ON a, b, c FROM functional_dependencies;
 ANALYZE functional_dependencies;
 -- print the detected dependencies
-SELECT dependencies FROM pg_stats_ext WHERE statistics_name = 'func_deps_stat';
-                                                dependencies                                                
-------------------------------------------------------------------------------------------------------------
- {"3 => 4": 1.000000, "3 => 6": 1.000000, "4 => 6": 1.000000, "3, 4 => 6": 1.000000, "3, 6 => 4": 1.000000}
+SELECT jsonb_pretty(dependencies::text::jsonb) AS dependencies FROM pg_stats_ext WHERE statistics_name = 'func_deps_stat';
+        dependencies         
+-----------------------------
+ [                          +
+     {                      +
+         "degree": 1.000000,+
+         "attributes": [    +
+             3              +
+         ],                 +
+         "dependency": 4    +
+     },                     +
+     {                      +
+         "degree": 1.000000,+
+         "attributes": [    +
+             3              +
+         ],                 +
+         "dependency": 6    +
+     },                     +
+     {                      +
+         "degree": 1.000000,+
+         "attributes": [    +
+             4              +
+         ],                 +
+         "dependency": 6    +
+     },                     +
+     {                      +
+         "degree": 1.000000,+
+         "attributes": [    +
+             3,             +
+             4              +
+         ],                 +
+         "dependency": 6    +
+     },                     +
+     {                      +
+         "degree": 1.000000,+
+         "attributes": [    +
+             3,             +
+             6              +
+         ],                 +
+         "dependency": 4    +
+     }                      +
+ ]
 (1 row)
 
 SELECT * FROM check_estimated_rows('SELECT * FROM functional_dependencies WHERE a = 1 AND b = ''1''');
@@ -1775,10 +1814,48 @@ SELECT * FROM check_estimated_rows('SELECT * FROM functional_dependencies WHERE
 CREATE STATISTICS func_deps_stat (dependencies) ON (a * 2), upper(b), (c + 1) FROM functional_dependencies;
 ANALYZE functional_dependencies;
 -- print the detected dependencies
-SELECT dependencies FROM pg_stats_ext WHERE statistics_name = 'func_deps_stat';
-                                                      dependencies                                                      
-------------------------------------------------------------------------------------------------------------------------
- {"-1 => -2": 1.000000, "-1 => -3": 1.000000, "-2 => -3": 1.000000, "-1, -2 => -3": 1.000000, "-1, -3 => -2": 1.000000}
+SELECT jsonb_pretty(dependencies::text::jsonb) AS dependencies FROM pg_stats_ext WHERE statistics_name = 'func_deps_stat';
+        dependencies         
+-----------------------------
+ [                          +
+     {                      +
+         "degree": 1.000000,+
+         "attributes": [    +
+             -1             +
+         ],                 +
+         "dependency": -2   +
+     },                     +
+     {                      +
+         "degree": 1.000000,+
+         "attributes": [    +
+             -1             +
+         ],                 +
+         "dependency": -3   +
+     },                     +
+     {                      +
+         "degree": 1.000000,+
+         "attributes": [    +
+             -2             +
+         ],                 +
+         "dependency": -3   +
+     },                     +
+     {                      +
+         "degree": 1.000000,+
+         "attributes": [    +
+             -1,            +
+             -2             +
+         ],                 +
+         "dependency": -3   +
+     },                     +
+     {                      +
+         "degree": 1.000000,+
+         "attributes": [    +
+             -1,            +
+             -3             +
+         ],                 +
+         "dependency": -2   +
+     }                      +
+ ]
 (1 row)
 
 SELECT * FROM check_estimated_rows('SELECT * FROM functional_dependencies WHERE (a * 2) = 2 AND upper(b) = ''1''');
diff --git a/src/test/regress/sql/stats_ext.sql b/src/test/regress/sql/stats_ext.sql
index 2c306dcc971..e8aa6b58009 100644
--- a/src/test/regress/sql/stats_ext.sql
+++ b/src/test/regress/sql/stats_ext.sql
@@ -125,7 +125,8 @@ ALTER TABLE ab1 ALTER a SET STATISTICS -1;
 ALTER STATISTICS ab1_a_b_stats SET STATISTICS 0;
 \d ab1
 ANALYZE ab1;
-SELECT stxname, jsonb_pretty(d.stxdndistinct::text::jsonb) AS stxdndistinct, stxddependencies, stxdmcv, stxdinherit
+SELECT stxname, jsonb_pretty(d.stxdndistinct::text::jsonb) AS stxdndistinct,
+       jsonb_pretty(d.stxddependencies::text::jsonb) AS stxddependencies, stxdmcv, stxdinherit
   FROM pg_statistic_ext s LEFT JOIN pg_statistic_ext_data d ON (d.stxoid = s.oid)
  WHERE s.stxname = 'ab1_a_b_stats';
 ALTER STATISTICS ab1_a_b_stats SET STATISTICS -1;
@@ -708,7 +709,7 @@ CREATE STATISTICS func_deps_stat (dependencies) ON a, b, c FROM functional_depen
 ANALYZE functional_dependencies;
 
 -- print the detected dependencies
-SELECT dependencies FROM pg_stats_ext WHERE statistics_name = 'func_deps_stat';
+SELECT jsonb_pretty(dependencies::text::jsonb) AS dependencies FROM pg_stats_ext WHERE statistics_name = 'func_deps_stat';

@@ -844,7 +845,7 @@ CREATE STATISTICS func_deps_stat (dependencies) ON (a * 2), upper(b), (c + 1) FR
 ANALYZE functional_dependencies;
 
 -- print the detected dependencies
-SELECT dependencies FROM pg_stats_ext WHERE statistics_name = 'func_deps_stat';
+SELECT jsonb_pretty(dependencies::text::jsonb) AS dependencies FROM pg_stats_ext WHERE statistics_name = 'func_deps_stat';

-- 
2.51.1

From 5d39e2bae4d952205c4952e23a41220b940ccbd4 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 4 Nov 2025 16:12:44 -0500
Subject: [PATCH v5 4/7] Add working input function for pg_dependencies.

This will consume the format that was established when the output
function for pg_dependencies was recently changed.

This will be needed for importing extended statistics.
---
 src/backend/statistics/dependencies.c   | 463 +++++++++++++++++++++++-
 src/test/regress/expected/stats_ext.out |  22 ++
 src/test/regress/sql/stats_ext.sql      |  12 +
 3 files changed, 487 insertions(+), 10 deletions(-)

diff --git a/src/backend/statistics/dependencies.c b/src/backend/statistics/dependencies.c
index c150ddfb594..573d0f722de 100644
--- a/src/backend/statistics/dependencies.c
+++ b/src/backend/statistics/dependencies.c
@@ -13,18 +13,26 @@
  */
 #include "postgres.h"

+typedef enum
+{
+	DEPS_EXPECT_START = 0,
+	DEPS_EXPECT_ITEM,
+	DEPS_EXPECT_KEY,

 /*
index 8cea29de314..6219b9674b0 100644
@@ -3788,6 +3788,28 @@ ERROR:  malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : 4},
index e8aa6b58009..6a5fbfd31ad 100644
@@ -1844,6 +1844,18 @@ SELECT '[{"attributes" : [2,3], "ndistinct" : 4},
 
2.51.1

From 37fde4e31a2fd19d9b7179dac7f9dbc5453fcd0d Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 4 Nov 2025 23:50:01 -0500
Subject: [PATCH v5 5/7] Expose attribute statistics functions for use in
 extended_stats.

* get_attr_stat_type() renamed to statatt_get_type()
* init_empty_stats_tuple() renamed to statatt_init_empty_tuple()
* text_to_stavalues()
* get_elem_stat_type() renamed to statatt_get_elem_type()

Also, add comments explaining the function argument index enums, and the
arrays that are indexed by those enums.
---
 src/include/statistics/statistics.h      |  17 +++
 src/backend/statistics/attribute_stats.c | 132 +++++++++++------------
 2 files changed, 83 insertions(+), 66 deletions(-)

index 7dd0f975545..0df66b352a1 100644
 
+extern void statatt_get_type(Oid reloid, AttrNumber attnum,
+							 Oid *atttypid, int32 *atttypmod,
+							 char *atttyptype, Oid *atttypcoll,
+							 Oid *eq_opr, Oid *lt_opr);
+extern void statatt_init_empty_tuple(Oid reloid, int16 attnum, bool inherited,
+									 Datum *values, bool *nulls, bool *replaces);
+
+extern void statatt_set_slot(Datum *values, bool *nulls, bool *replaces,
+							 int16 stakind, Oid staop, Oid stacoll,
+							 Datum stanumbers, bool stanumbers_isnull,
+							 Datum stavalues, bool stavalues_isnull);
+
+extern Datum text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d,
+							   Oid typid, int32 typmod, bool *ok);
+extern bool statatt_get_elem_type(Oid atttypid, char atttyptype,
+								  Oid *elemtypid, Oid *elem_eq_opr);
+
 #endif							/* STATISTICS_H */
diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index 401bf571f27..bbb63f9be73 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -36,6 +36,9 @@
 #define DEFAULT_AVG_WIDTH      Int32GetDatum(0) /* unknown */
 #define DEFAULT_N_DISTINCT     Float4GetDatum(0.0)	/* unknown */
 
+/*
+ * An index of the args for attribute_statistics_update
+ */
 enum attribute_stats_argnum
 {
 	ATTRELSCHEMA_ARG = 0,
@@ -59,6 +62,10 @@ enum attribute_stats_argnum
 	NUM_ATTRIBUTE_STATS_ARGS
 };
 
+/*
+ * The argument names and typoids of the arguments for
+ * attribute_statistics_update.
+ */
 static struct StatsArgInfo attarginfo[] =
 {
 	[ATTRELSCHEMA_ARG] = {"schemaname", TEXTOID},
@@ -82,6 +89,9 @@ static struct StatsArgInfo attarginfo[] =
 	[NUM_ATTRIBUTE_STATS_ARGS] = {0}
 };
 
+/*
+ * An index of the args for pg_clear_attribute_stats
+ */
 enum clear_attribute_stats_argnum
 {
 	C_ATTRELSCHEMA_ARG = 0,
@@ -91,6 +101,10 @@ enum clear_attribute_stats_argnum
 	C_NUM_ATTRIBUTE_STATS_ARGS
 };
 
+/*
+ * The argument names and typoids of the arguments for
+ * pg_clear_attribute_stats.
+ */
 static struct StatsArgInfo cleararginfo[] =
 {
 	[C_ATTRELSCHEMA_ARG] = {"relation", TEXTOID},
@@ -102,23 +116,9 @@ static struct StatsArgInfo cleararginfo[] =
 
 								const Datum *values, const bool *nulls, const bool *replaces);
 
 /*
  * Insert or Update Attribute Statistics
@@ -288,16 +288,16 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	}
 
 	/* derive information from attribute */
-	get_attr_stat_type(reloid, attnum,
-					   &atttypid, &atttypmod,
-					   &atttyptype, &atttypcoll,
-					   &eq_opr, &lt_opr);
+	statatt_get_type(reloid, attnum,
+					 &atttypid, &atttypmod,
+					 &atttyptype, &atttypcoll,
+					 &eq_opr, &lt_opr);
 
 	/* if needed, derive element type */
 	if (do_mcelem || do_dechist)
 	{
-		if (!get_elem_stat_type(atttypid, atttyptype,
-								&elemtypid, &elem_eq_opr))
+		if (!statatt_get_elem_type(atttypid, atttyptype,
+								   &elemtypid, &elem_eq_opr))
 		{
 			ereport(WARNING,
 					(errmsg("could not determine element type of column \"%s\"", attname),
@@ -351,7 +351,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	if (HeapTupleIsValid(statup))
 		heap_deform_tuple(statup, RelationGetDescr(starel), values, nulls);
 	else
-		init_empty_stats_tuple(reloid, attnum, inherited, values, nulls,
+		statatt_init_empty_tuple(reloid, attnum, inherited, values, nulls,
 							   replaces);
 
 	/* if specified, set to argument values */
@@ -384,10 +384,10 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 		if (converted)
 		{
-			set_stats_slot(values, nulls, replaces,
-						   STATISTIC_KIND_MCV,
-						   eq_opr, atttypcoll,
-						   stanumbers, false, stavalues, false);
+			statatt_set_slot(values, nulls, replaces,
+							 STATISTIC_KIND_MCV,
+							 eq_opr, atttypcoll,
+							 stanumbers, false, stavalues, false);
 		}
 		else
 			result = false;
@@ -407,10 +407,10 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 		if (converted)
 		{
-			set_stats_slot(values, nulls, replaces,
-						   STATISTIC_KIND_HISTOGRAM,
-						   lt_opr, atttypcoll,
-						   0, true, stavalues, false);
+			statatt_set_slot(values, nulls, replaces,
+							 STATISTIC_KIND_HISTOGRAM,
+							 lt_opr, atttypcoll,
+							 0, true, stavalues, false);
 		}
 		else
 			result = false;
@@ -423,10 +423,10 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 		ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
 		Datum		stanumbers = PointerGetDatum(arry);
 
-		set_stats_slot(values, nulls, replaces,
-					   STATISTIC_KIND_CORRELATION,
-					   lt_opr, atttypcoll,
-					   stanumbers, false, 0, true);
+		statatt_set_slot(values, nulls, replaces,
+						 STATISTIC_KIND_CORRELATION,
+						 lt_opr, atttypcoll,
+						 stanumbers, false, 0, true);
 	}
 
 	/* STATISTIC_KIND_MCELEM */
@@ -444,10 +444,10 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 		if (converted)
 		{
-			set_stats_slot(values, nulls, replaces,
-						   STATISTIC_KIND_MCELEM,
-						   elem_eq_opr, atttypcoll,
-						   stanumbers, false, stavalues, false);
+			statatt_set_slot(values, nulls, replaces,
+							 STATISTIC_KIND_MCELEM,
+							 elem_eq_opr, atttypcoll,
+							 stanumbers, false, stavalues, false);
 		}
 		else
 			result = false;
@@ -458,10 +458,10 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	{
 		Datum		stanumbers = PG_GETARG_DATUM(ELEM_COUNT_HISTOGRAM_ARG);
 
-		set_stats_slot(values, nulls, replaces,
-					   STATISTIC_KIND_DECHIST,
-					   elem_eq_opr, atttypcoll,
-					   stanumbers, false, 0, true);
+		statatt_set_slot(values, nulls, replaces,
+						 STATISTIC_KIND_DECHIST,
+						 elem_eq_opr, atttypcoll,
+						 stanumbers, false, 0, true);
 	}
 
 	/*
@@ -484,10 +484,10 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 		if (converted)
 		{
-			set_stats_slot(values, nulls, replaces,
-						   STATISTIC_KIND_BOUNDS_HISTOGRAM,
-						   InvalidOid, InvalidOid,
-						   0, true, stavalues, false);
+			statatt_set_slot(values, nulls, replaces,
+							 STATISTIC_KIND_BOUNDS_HISTOGRAM,
+							 InvalidOid, InvalidOid,
+							 0, true, stavalues, false);
 		}
 		else
 			result = false;
@@ -511,10 +511,10 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 		if (converted)
 		{
-			set_stats_slot(values, nulls, replaces,
-						   STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM,
-						   Float8LessOperator, InvalidOid,
-						   stanumbers, false, stavalues, false);
+			statatt_set_slot(values, nulls, replaces,
+							 STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM,
+							 Float8LessOperator, InvalidOid,
+							 stanumbers, false, stavalues, false);
 		}
 		else
 			result = false;
@@ -574,11 +574,11 @@ get_attr_expr(Relation rel, int attnum)
 /*
  * Derive type information from the attribute.
  */
-static void
-get_attr_stat_type(Oid reloid, AttrNumber attnum,
-				   Oid *atttypid, int32 *atttypmod,
-				   char *atttyptype, Oid *atttypcoll,
-				   Oid *eq_opr, Oid *lt_opr)
+void
+statatt_get_type(Oid reloid, AttrNumber attnum,
+				 Oid *atttypid, int32 *atttypmod,
+				 char *atttyptype, Oid *atttypcoll,
+				 Oid *eq_opr, Oid *lt_opr)
 {
 	Relation	rel = relation_open(reloid, AccessShareLock);
 	Form_pg_attribute attr;
@@ -656,9 +656,9 @@ get_attr_stat_type(Oid reloid, AttrNumber attnum,
 /*
  * Derive element type information from the attribute type.
  */
-static bool
-get_elem_stat_type(Oid atttypid, char atttyptype,
-				   Oid *elemtypid, Oid *elem_eq_opr)
+bool
+statatt_get_elem_type(Oid atttypid, char atttyptype,
+					  Oid *elemtypid, Oid *elem_eq_opr)
 {
 	TypeCacheEntry *elemtypcache;
 
@@ -696,7 +696,7 @@ get_elem_stat_type(Oid atttypid, char atttyptype,
  * to false. If the resulting array contains NULLs, raise a WARNING and set ok
  * to false. Otherwise, set ok to true.
  */
-static Datum
+Datum
 text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d, Oid typid,
 				  int32 typmod, bool *ok)
 {
@@ -749,11 +749,11 @@ text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d, Oid typid,
  * Find and update the slot with the given stakind, or use the first empty
  * slot.
  */
-static void
-set_stats_slot(Datum *values, bool *nulls, bool *replaces,
-			   int16 stakind, Oid staop, Oid stacoll,
-			   Datum stanumbers, bool stanumbers_isnull,
-			   Datum stavalues, bool stavalues_isnull)
+void
+statatt_set_slot(Datum *values, bool *nulls, bool *replaces,
+				 int16 stakind, Oid staop, Oid stacoll,
+				 Datum stanumbers, bool stanumbers_isnull,
+				 Datum stavalues, bool stavalues_isnull)
 {
 	int			slotidx;
 	int			first_empty = -1;
@@ -873,9 +873,9 @@ delete_pg_statistic(Oid reloid, AttrNumber attnum, bool stainherit)
 /*
  * Initialize values and nulls for a new stats tuple.
  */
-static void
-init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
-					   Datum *values, bool *nulls, bool *replaces)
+void
+statatt_init_empty_tuple(Oid reloid, int16 attnum, bool inherited,
+						 Datum *values, bool *nulls, bool *replaces)
 {
 	memset(nulls, true, sizeof(bool) * Natts_pg_statistic);
 	memset(replaces, true, sizeof(bool) * Natts_pg_statistic);
-- 
2.51.1

From 7613478abda4eab79f26c9c4def546a4414dc301 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Wed, 5 Nov 2025 00:36:16 -0500
Subject: [PATCH v5 6/7] Add extended statistics support functions.

Add pg_restore_extended_stats() and pg_clear_extended_stats(). These
functions closely mirror their relation and attribute counterparts,
but for extended statistics (i.e.  CREATE STATISTICS) objects.
---
 src/include/catalog/pg_proc.dat               |   18 +
 .../statistics/extended_stats_internal.h      |   17 +
 src/backend/statistics/dependencies.c         |   61 +
 src/backend/statistics/extended_stats.c       | 1141 ++++++++++++++++-
 src/backend/statistics/mcv.c                  |  144 +++
 src/backend/statistics/mvdistinct.c           |   62 +
 src/test/regress/expected/stats_import.out    |  588 +++++++++
 src/test/regress/sql/stats_import.sql         |  452 +++++++
 doc/src/sgml/func/func-admin.sgml             |   98 ++
 9 files changed, 2580 insertions(+), 1 deletion(-)

index 34b7fddb0e7..67cb47fcb47 100644
@@ -12594,6 +12594,24 @@

index 573d0f722de..7b310b9f55d 100644
@@ -1022,6 +1022,55 @@ dependencies_scalar(void *state, char *token, JsonTokenType tokentype)
 
+/*
@@ -1106,6 +1155,18 @@ pg_dependencies_in(PG_FUNCTION_ARGS)
 
+/*
index 3c3d2d315c6..23ab3cf87e1 100644
 
@@ -72,6 +79,84 @@ typedef struct StatExtEntry
 
+/*
+ * An index of the args for extended_statistics_update().
+ */
+ * The argument names and typoids of the arguments for
+ * extended_statistics_update().
+ * An index of the elements of the stxdexprs datum, which repeat for each
+ * expression in the extended statistics object.
+ * The argument names and typoids of the repeating arguments for stxdexprs.
 
@@ -99,6 +184,28 @@ static StatsBuildData *make_build_data(Relation rel, StatExtEntry *stat,
 
+								int required_ndims, int mcv_length);
 
 /*
  * Compute requested extended stats, using the rows sampled for the plain
@@ -121,7 +228,7 @@ BuildRelationExtStatistics(Relation onerel, bool inh, double totalrows,
 
 	/* Do nothing if there are no columns to analyze. */
 	if (!natts)
-		return;
+	return;
 
 	/* the list of stats has to be allocated outside the memory context */
 	pg_stext = table_open(StatisticExtRelationId, RowExclusiveLock);
@@ -2612,3 +2719,1035 @@ make_build_data(Relation rel, StatExtEntry *stat, int numrows, HeapTuple *rows,
 
 	return result;
 }
+
+static HeapTuple
+			statatt_get_type(stxform->stxrelid,
+		statatt_init_empty_tuple(InvalidOid, InvalidAttrNumber, false,
+			statatt_set_slot(values, nulls, replaces,
+			statatt_set_slot(values, nulls, replaces,
+			statatt_set_slot(values, nulls, replaces,
+			if (!statatt_get_elem_type(typid, typcache->typtype,
+			statatt_set_slot(values, nulls, replaces,
+			statatt_set_slot(values, nulls, replaces, STATISTIC_KIND_DECHIST,
 
 	return s;
 }
+
+/*
index 5b7b3aa26a4..09bec8ff718 100644
 
+/*
 
 DROP TABLE stats_temp;

 

2.51.1

From 81bb52bd37a2e1486884ffe2377bc56b162b2b30 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Wed, 5 Nov 2025 01:13:04 -0500
Subject: [PATCH v5 7/7] Include Extended Statistics in pg_dump.

* Extended Statistics were introduced in v10, so there is no support for
  prior versions necessary.
---
 src/bin/pg_dump/pg_backup.h          |   1 +
 src/bin/pg_dump/pg_backup_archiver.c |   3 +-
 src/bin/pg_dump/pg_dump.c            | 252 +++++++++++++++++++++++++++
 src/bin/pg_dump/t/002_pg_dump.pl     |  28 +++
 4 files changed, 283 insertions(+), 1 deletion(-)

index a00918bacb4..8c5850f9e9b 100644

 	PQclear(res);
@@ -18514,6 +18519,253 @@ dumpStatisticsExt(Archive *fout, const StatsExtInfo *statsextinfo)
 	free(qstatsextname);
 }
 
+/*
+		 * Versions 15+ have inherited stats.
+		 * Create this column in all version because we need to order by it later.
+		 * These transformations may look scary, but all we're doing is translating
+		 * {"3, 4": 11, "3, 6": 11, "4, 6": 11, "3, 4, 6": 11}
+		 * to
+		 * [{"ndistinct": 11, "attributes": [3,4]},
+		 *  {"ndistinct": 11, "attributes": [3,6]},
+		 *  {"ndistinct": 11, "attributes": [4,6]},
+		 *  {"ndistinct": 11, "attributes": [3,4,6]}]
+		 * and
+		 * {"3 => 4": 1.000000, "3 => 6": 1.000000, "4 => 6": 1.000000,
+		 *  "3, 4 => 6": 1.000000, "3, 6 => 4": 1.000000}
+		 *  to
+		 * [{"degree": 1.000000, "attributes": [3], "dependency": 4},
+		 *  {"degree": 1.000000, "attributes": [3], "dependency": 6},
+		 *  {"degree": 1.000000, "attributes": [4], "dependency": 6},
+		 *  {"degree": 1.000000, "attributes": [3,4], "dependency": 6},
+		 *  {"degree": 1.000000, "attributes": [3,6], "dependency": 4}]
+		if (fout->remoteVersion >= 190000)
+		/* we always have an inherited column, but it may be a constant */
+		appendPQExpBufferStr(pq, "ORDER BY inherited");
 
+	#
2.51.1

---

From: Michael Paquier <[email-masked]>
Date: Thu, 6 Nov 2025 17:26:58 +0900

> change, so I will try to recap:

That was a typo that found its way into the email subject.  Sorry
about that, that broke gmail's tracking at least.

> 8. pg_dump working back as far as possible

Thanks.  I have begun reviewing it (more a bit later, still need to
study more the structure of the code).  For now, I have extracted some
of the comment changes in 0005 and applied these independently.

> in support of it, I've opted not to split out steps 7/8.

That sounds like a way forward to me, then, in terms of using a new
format and make pg_dump intelligent enough to deal with it based on
what's in the past versions.
--
Michael

---

From: Corey Huinker <[email-masked]>
Date: Thu, 6 Nov 2025 13:35:34 -0500

> about that, that broke gmail's tracking at least.

So the mailing list archive will still pick it up? That's nice.

> of the comment changes in 0005 and applied these independently.

Rebased to reflect that commit.
From c774ad584ad222f26d35556cdda999b584d4463e Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 4 Nov 2025 15:24:35 -0500
Subject: [PATCH v7 1/7] Refactor output format of pg_ndistinct.

 
 	initStringInfo(&str);

 		if (i > 0)

 

 ANALYZE ab1;

 -- correct estimates
 
 DROP STATISTICS s10;

 

 ANALYZE ndistinct;

 ANALYZE ndistinct;

 DROP STATISTICS s10;

 ANALYZE ndistinct;

 ANALYZE ndistinct;

base-commit: 8fe7700b7eafe95d20244c5c85567f1573669e23
-- 
2.51.1

From 938c24365169fe48716965f8f474c671e2286e0f Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 4 Nov 2025 15:35:58 -0500
Subject: [PATCH v7 2/7] Add working input function for pg_ndistinct.

 
 
+typedef enum
+{

 /*
  * pg_ndistinct

 

From 5fb10c82a56ffbeafdbbb4631ff0b03f86be1fd6 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 4 Nov 2025 15:59:31 -0500
Subject: [PATCH v7 3/7] Refactor output format of pg_dependencies.

 
 	initStringInfo(&str);

 		if (i > 0)

 

 ANALYZE ab1;

 

 

-- 
2.51.1

From f53c54324705a2875e4f9a210c6231dca68a5059 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 4 Nov 2025 16:12:44 -0500
Subject: [PATCH v7 4/7] Add working input function for pg_dependencies.

 
 
+typedef enum
+{
+	DEPS_EXPECT_START = 0,
+	DEPS_EXPECT_ITEM,
+	DEPS_EXPECT_KEY,

 /*

From 74650540ace01a3c75f9dc49c20d4d1be324a640 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 4 Nov 2025 23:50:01 -0500
Subject: [PATCH v7 5/7] Expose attribute statistics functions for use in
 extended_stats.

Also, add comments explaining the function argument index enums, and the
arrays that are indexed by those enums.
---
 src/include/statistics/statistics.h      |  17 +++
 src/backend/statistics/attribute_stats.c | 126 +++++++++++------------
 2 files changed, 77 insertions(+), 66 deletions(-)

index ef4d768feab..d0c67a4128e 100644
@@ -64,6 +64,10 @@ enum attribute_stats_argnum
 	NUM_ATTRIBUTE_STATS_ARGS
 };
 
+/*
@@ -101,6 +105,10 @@ enum clear_attribute_stats_argnum
 
+/*
@@ -112,23 +120,9 @@ static struct StatsArgInfo cleararginfo[] =

 /*
  * Insert or Update Attribute Statistics
@@ -298,16 +292,16 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	}

@@ -361,7 +355,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 	/* if specified, set to argument values */
@@ -394,10 +388,10 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 		if (converted)
 		{
@@ -417,10 +411,10 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 		if (converted)
 		{
@@ -433,10 +427,10 @@ attribute_statistics_update(FunctionCallInfo fcinfo)

 	/* STATISTIC_KIND_MCELEM */
@@ -454,10 +448,10 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 		if (converted)
 		{
@@ -468,10 +462,10 @@ attribute_statistics_update(FunctionCallInfo fcinfo)

 	/*
@@ -494,10 +488,10 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 		if (converted)
 		{
@@ -521,10 +515,10 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 		if (converted)
 		{
@@ -584,11 +578,11 @@ get_attr_expr(Relation rel, int attnum)
@@ -666,9 +660,9 @@ get_attr_stat_type(Oid reloid, AttrNumber attnum,
 
@@ -706,7 +700,7 @@ get_elem_stat_type(Oid atttypid, char atttyptype,
@@ -759,11 +753,11 @@ text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d, Oid typid,
@@ -883,9 +877,9 @@ delete_pg_statistic(Oid reloid, AttrNumber attnum, bool stainherit)

From d219f73b7b07b7e2a6e6f566406029b955b70533 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Wed, 5 Nov 2025 00:36:16 -0500
Subject: [PATCH v7 6/7] Add extended statistics support functions.

 

+/*
 
+/*

+/*

 
 /*

 
 	return result;
 }
+
+static HeapTuple
 
 	return s;
 }
+
+/*
 
+/*
 
 DROP TABLE stats_temp;

 

From 0e5b4556e8897acd10e75294a0a60d690e12ec10 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Wed, 5 Nov 2025 01:13:04 -0500
Subject: [PATCH v7 7/7] Include Extended Statistics in pg_dump.

 

 	PQclear(res);
 
+/*
 
+	#

---

From: Michael Paquier <[email-masked]>
Date: Fri, 7 Nov 2025 16:51:15 +0900

> So the mailing list archive will still pick it up? That's nice.

It did.  My email client does not care much either.

> Rebased to reflect that commit.

I have spent a bit more time on this set.

Patch 0001 for ndistinct was missing a documentation update, we have
one query in perform.sgml that looks at stxdndistinct.  Patch 0003 is
looking OK here as well.

For dependencies, the format switches from a single json object
with key/vals like that:
"3 => 4": 1.000000
To a JSON array made of elements like that:
{"degree": 1.000000, "attributes": [3],"dependency": 4},

For ndistincts, we move from a JSON blob with key/vals like that:
"3, 4": 11
To a JSON array made of the following elements:
{"ndistinct": 11, "attributes": [3,4]}

Using a keyword within each element would force a stronger validation
when these get imported back, which is a good thing.  I like that.

Before going in-depth into the input functions to cross-check the
amount of validation we should do, have folks any comments about the
proposed format?  That's the key point this patch set depends on, and
I'd rather not spend more time the whole thing if somebody would like
a different format.  This is the format that Tomas has mentioned at
the top of the thread.  Note: as noted upthread, pg_dump would be in
charge of transferring the data of the old format to the new format at
the end.

While looking at 0002 and 0004 (which have a couple of issues
actually), I have been wondering about moving into a new file the four
data-type functions (in, out, send and receive) and the new input
functions that rely on a new JSON lexer and parser logic into for both
ndistinct and dependencies.  The new set of headers added at the top
of mvdistinct.c and dependencies.c for the new code points that a
separation may be better in the long-term, because the new code relies
on parts of the backend that the existing code does not care about,
and these files become larger than the relation and attribute stats
files.  I would be tempted to name these new files pg_dependencies.c
and pg_ndistinct.c, mapping with their catalog types.  With this
separation, it looks like the "core" parts in charge of the
calculations with ndistinct and dependencies can be kept on its own.
What do you think?

A second comment is for 0005.  The routines of attributes.c are
applied to the new clear and restore functions.  Shouldn't these be in
stats_utils.c at the end?  That's where the "common" functions used by
the stats manipulation logic are.
--
Michael
From 927622154b73e40945e9581ab5456864b4fdc5e6 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 4 Nov 2025 15:24:35 -0500
Subject: [PATCH v8 1/7] Refactor output format of pg_ndistinct.

The change in format is adequately described from the changes to
src/test/regress/expected/stats_ext.out so description here is
redundant.
---
 src/backend/statistics/mvdistinct.c     |  19 +--
 src/test/regress/expected/stats_ext.out | 156 +++++++++++++++++++++---
 src/test/regress/sql/stats_ext.sql      |  12 +-
 doc/src/sgml/perform.sgml               |  34 +++++-
 4 files changed, 186 insertions(+), 35 deletions(-)

index 7e7a63405c8b..ca60841b813e 100644
 
 	initStringInfo(&str);

 		if (i > 0)

 
 
index 73a7ef973559..2dc771369e52 100644
 
 ANALYZE ab1;

 -- correct estimates
 
 DROP STATISTICS s10;

 
 
index 96771600d578..207c431e68c9 100644
 
 ANALYZE ndistinct;

 ANALYZE ndistinct;

 DROP STATISTICS s10;

 ANALYZE ndistinct;

 ANALYZE ndistinct;
 
-SELECT s.stxkind, d.stxdndistinct
+SELECT s.stxkind, jsonb_pretty(d.stxdndistinct::text::jsonb) AS stxdndistinct
   FROM pg_statistic_ext s, pg_statistic_ext_data d
  WHERE s.stxrelid = 'ndistinct'::regclass
    AND d.stxoid = s.oid;
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index 106583fb2965..e0e9f0468cb8 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -1579,9 +1579,39 @@ ANALYZE zipcodes;
 SELECT stxkeys AS k, stxdndistinct AS nd
   FROM pg_statistic_ext join pg_statistic_ext_data on (oid = stxoid)
   WHERE stxname = 'stts2';
--[ RECORD 1 ]------------------------------------------------------&zwsp;--
+-[ RECORD 1 ]-------------------
 k  | 1 2 5
-nd | {"1, 2": 33178, "1, 5": 33178, "2, 5": 27435, "1, 2, 5": 33178}
+nd | [                          +
+   |     {                      +
+   |         "ndistinct": 33178,+
+   |         "attributes": [    +
+   |             1,             +
+   |             2              +
+   |         ]                  +
+   |     },                     +
+   |     {                      +
+   |         "ndistinct": 33178,+
+   |         "attributes": [    +
+   |             1,             +
+   |             5              +
+   |         ]                  +
+   |     },                     +
+   |     {                      +
+   |         "ndistinct": 27435,+
+   |         "attributes": [    +
+   |             2,             +
+   |             5              +
+   |         ]                  +
+   |     },                     +
+   |     {                      +
+   |         "ndistinct": 33178,+
+   |         "attributes": [    +
+   |             1,             +
+   |             2,             +
+   |             5              +
+   |         ]                  +
+   |     }                      +
+   | ]
 (1 row)
 </programlisting>
      This indicates that there are three combinations of columns that
-- 
2.51.0

From 0b61eb3337a8e9a7c1515b1cdf862c639a76b05e Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 4 Nov 2025 15:35:58 -0500
Subject: [PATCH v8 2/7] Add working input function for pg_ndistinct.

diff --git a/src/backend/statistics/mvdistinct.c b/src/backend/statistics/mvdistinct.c
index ca60841b813e..5b7b3aa26a42 100644
--- a/src/backend/statistics/mvdistinct.c
+++ b/src/backend/statistics/mvdistinct.c
@@ -27,9 +27,15 @@

+typedef enum
+{

 /*
  * pg_ndistinct
index 2dc771369e52..1d837badb96c 100644
 
index 207c431e68c9..2c306dcc971d 100644

From 98548efb5af50edc3f03444f8155f427e39fecd7 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 4 Nov 2025 15:59:31 -0500
Subject: [PATCH v8 3/7] Refactor output format of pg_dependencies.

index eb2fc4366b4a..c150ddfb5944 100644
 
 	initStringInfo(&str);

 		if (i > 0)

 
 
index 1d837badb96c..8cea29de3140 100644
 
 ANALYZE ab1;

index 2c306dcc971d..e8aa6b580096 100644

 

 
-- 
2.51.0

From 18c7c3a8ae24975d065bddb5603767cafe38a52d Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 4 Nov 2025 16:12:44 -0500
Subject: [PATCH v8 4/7] Add working input function for pg_dependencies.

diff --git a/src/backend/statistics/dependencies.c b/src/backend/statistics/dependencies.c
index c150ddfb5944..573d0f722def 100644
--- a/src/backend/statistics/dependencies.c
+++ b/src/backend/statistics/dependencies.c
@@ -13,18 +13,26 @@
  */
 #include "postgres.h"

+typedef enum
+{
+	DEPS_EXPECT_START = 0,
+	DEPS_EXPECT_ITEM,
+	DEPS_EXPECT_KEY,

 /*
index 8cea29de3140..6219b9674b0b 100644
index e8aa6b580096..6a5fbfd31ad8 100644

From 5e0a3c7cf741333bf94065616c60074170a84033 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 4 Nov 2025 23:50:01 -0500
Subject: [PATCH v8 5/7] Expose attribute statistics functions for use in
 extended_stats.

index 7dd0f9755454..0df66b352a10 100644
 
index ef4d768feab7..d0c67a4128e0 100644
 
+/*
 
+/*

 /*

 
 
 		if (converted)
 		{
 
 		if (converted)
 		{

 
 		if (converted)
 		{

 	/*
 
 		if (converted)
 		{
 
 		if (converted)
 		{

From 63bd21e23b1f65d176df1b92d45ee11c69fb2c5d Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Wed, 5 Nov 2025 00:36:16 -0500
Subject: [PATCH v8 6/7] Add extended statistics support functions.

index 34b7fddb0e7a..67cb47fcb474 100644
 
index efcb7dc35461..ba7f5dcad829 100644
 
index 573d0f722def..7b310b9f55d8 100644
 
+/*
 
+/*
index 3c3d2d315c6f..23ab3cf87e18 100644

+/*

 
 /*

 
 	return result;
 }
+
+static HeapTuple
index f59fb8215437..a917079ceb0b 100644
 
 	return s;
 }
+
+/*
index 5b7b3aa26a42..09bec8ff7188 100644
 
+/*
index 98ce7dc28410..266e29b66f09 100644
 
 DROP TABLE stats_temp;
index d140733a7502..4dd568be9fcf 100644

 

index 1b465bc8ba71..574d4a35a64f 100644

From 09fc2ae65c8a8b483d2d8c2a7b977d80fe27ebae Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Wed, 5 Nov 2025 01:13:04 -0500
Subject: [PATCH v8 7/7] Include Extended Statistics in pg_dump.

index d9041dad7206..df708e4ced69 100644
index 59eaecb4ed71..1bfd296e0ee9 100644
 
index a00918bacb40..8c5850f9e9b3 100644

 	PQclear(res);
 
+/*
index 445a541abf63..6681265974f6 100644
 
+	#

---

From: Corey Huinker <[email-masked]>
Date: Fri, 7 Nov 2025 17:28:50 -0500

> one query in perform.sgml that looks at stxdndistinct.  Patch 0003 is

Well spotted.

> For dependencies, the format switches from a single json object

I'm open to other formats, but aside from renaming the json keys (maybe
"attnums" or "keys" instead of "attributes"?), I'm not sure what really
could be done and still be JSON. I suppose we could go with a tuple format
like this:

'{({3,4},11),...}' for pg_ndistinct and
'{({3},4,1.00000),...}'  for pg_dependencies.

Those would certainly be more compact, but makes for a hard read by humans,
and while the JSON code is big, it's also proven in other parts of the
codebase, hence less risky.

> calculations with ndistinct and dependencies can be kept on its own.

A part of me thinks that everything that remains after removing
in/out/send/recv is just taking a table sample data structure and crunching
numbers to come up with the deserialized data structure...that's in/out
with a different starting/ending points.

There's no denying that JSON parsing is a very different code style than
statistical number crunching, and mixing the two is incongruous, so it's
worth a shot, and I'll try that for v9.

> the stats manipulation logic are.

I assume you're referring to attribute_stats.c. I think that would cause
stats_utils.c to have to pull in a lot of things from attribute_stats.c,
and that would create the exact sort of include-pollution that you're
trying to avoid in the mvdistinct.c/dependencies.c situation mentioned
above.

The one lone exception to this is text_to_stavalues(), which is a fancy
wrapper around array_in() and could perhaps be turned to even more generic
usage outside of stats in general.

The functions in question are needed because the exprs value is itself an
array of partly-filled-out pg_attribute tuples, so it's common to those two
needs, but specific to stats about attributes. Maybe we need an
attr_stats_utils.h?

---

From: Michael Paquier <[email-masked]>
Date: Sat, 8 Nov 2025 07:56:48 +0900

> codebase, hence less risky.

I've liked the human-readability factor of the format in the current
patches with names in the keys, and values assigned to each property.

Another thing that may be worth doing is pushing the names of the keys
and some its the JSON meta-data shaping the object into a new header
than can be loaded by both the backend and the frontend.  It would be
nice to not hardcode this knowledge in a bunch of places if we finish
by renaming these attributes.

> worth a shot, and I'll try that for v9.

Yeah, right.  Thanks.  The parsing pieces seem like pieces worth their
own file.

> needs, but specific to stats about attributes. Maybe we need an

Hmm, maybe.  I'd be OK to revisit these structures once we're happy
with the in/out structures.  That would be a good start point before
working on the SQL functions and the dump/restore bits in more
details.
--
Michael

---

From: Corey Huinker <[email-masked]>
Date: Mon, 10 Nov 2025 00:33:40 -0500

> by renaming these attributes.

It may not be quite what you wanted, but the attribute names are now static
constants in the new adt c files. It's possible/probable that you wanted
them in some header file, but so far I haven't had to create any new header
files, but that can be done if desired.

> own file.

That's done in the 0008-0009 patches. If I was starting from scratch, I
would have moved the pre-existing in/out/send/recv functions to their own
files in their own patches before changing the output format, but tacked on
at the end like they are it's easier to see what the changes were, and the
patches will probably get squashed together anyway.

> working on the SQL functions and the dump/restore bits in more

In addition to the changes detailed above, I fixed a few typos and
incorporated the v8 change.
From 5c4d188b3a4b0f9bc2c766293904b6f350db1f9b Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 4 Nov 2025 15:24:35 -0500
Subject: [PATCH v9 1/9] Refactor output format of pg_ndistinct.

 
 	initStringInfo(&str);

 		if (i > 0)

 

 ANALYZE ab1;

 -- correct estimates
 
 DROP STATISTICS s10;

 

 ANALYZE ndistinct;

 ANALYZE ndistinct;

 DROP STATISTICS s10;

 ANALYZE ndistinct;

 ANALYZE ndistinct;
 
index 106583fb296..e0e9f0468cb 100644

base-commit: 16a2f706951edc1b284a66902c9e582217d37d31
-- 
2.51.1

From dbb17ded1a3b4a76cda276d176ac959f301d26e4 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 4 Nov 2025 15:35:58 -0500
Subject: [PATCH v9 2/9] Add working input function for pg_ndistinct.

 
 
+typedef enum
+{

 /*
  * pg_ndistinct

 

From 6223e32218fc09da64b3f481a3803784bd4751f7 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 4 Nov 2025 15:59:31 -0500
Subject: [PATCH v9 3/9] Refactor output format of pg_dependencies.

 
 	initStringInfo(&str);

 		if (i > 0)

 

 ANALYZE ab1;

 

 

-- 
2.51.1

From 0e8b0d9541022ac556a204fcda25bc0a5dff6ad8 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 4 Nov 2025 16:12:44 -0500
Subject: [PATCH v9 4/9] Add working input function for pg_dependencies.

 
 
+typedef enum
+{
+	DEPS_EXPECT_START = 0,
+	DEPS_EXPECT_ITEM,
+	DEPS_EXPECT_KEY,

 /*

From 7601769b2b01c0ad4bdc98d6b63e95d7c3d3383c Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 4 Nov 2025 23:50:01 -0500
Subject: [PATCH v9 5/9] Expose attribute statistics functions for use in
 extended_stats.

 
 
+/*
 
+/*

 /*

 
 
 		if (converted)
 		{
 
 		if (converted)
 		{

 
 		if (converted)
 		{

 	/*
 
 		if (converted)
 		{
 
 		if (converted)
 		{

From 4af21f33b05a33e61fc6b5054d29a701d43cf923 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Wed, 5 Nov 2025 00:36:16 -0500
Subject: [PATCH v9 6/9] Add extended statistics support functions.

index 5cf9e12fcb9..84e9f176d19 100644

 
+/*
 
+/*

+/*

 
 /*

 
 	return result;
 }
+
+static HeapTuple
 
 	return s;
 }
+
+/*
 
+/*
 
 DROP TABLE stats_temp;

 

From 0a0b9d096ff86410b25628aa1f6484f7e89ed7ce Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Wed, 5 Nov 2025 01:13:04 -0500
Subject: [PATCH v9 7/9] Include Extended Statistics in pg_dump.

 

 	PQclear(res);
 
+/*
 
+	#

From 2ee4f9a778d4eec41ceb6af054d98dc08afad87c Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Sun, 9 Nov 2025 22:49:03 -0500
Subject: [PATCH v9 8/9] Make pg_ndinstinct a proper adt.

Move the in/out/send/recv functions for pg_ndistinct to pg_ndistinct.c,
which allows mvdistinct.c to focus on the transformation from sample
data to the internal MVDistinct structure.
---
 src/backend/statistics/mvdistinct.c  | 529 --------------------------
 src/backend/utils/adt/Makefile       |   1 +
 src/backend/utils/adt/meson.build    |   1 +
 src/backend/utils/adt/pg_ndistinct.c | 550 +++++++++++++++++++++++++++
 4 files changed, 552 insertions(+), 529 deletions(-)
 create mode 100644 src/backend/utils/adt/pg_ndistinct.c

diff --git a/src/backend/statistics/mvdistinct.c b/src/backend/statistics/mvdistinct.c
index 09bec8ff718..f6bf68db01a 100644
--- a/src/backend/statistics/mvdistinct.c
+++ b/src/backend/statistics/mvdistinct.c
@@ -27,16 +27,7 @@
 
 #include "catalog/pg_statistic_ext.h"
 #include "catalog/pg_statistic_ext_data.h"
-#include "common/int.h"
-#include "common/jsonapi.h"
-#include "lib/stringinfo.h"
-#include "mb/pg_wchar.h"
-#include "nodes/miscnodes.h"
-#include "nodes/pg_list.h"
 #include "statistics/extended_stats_internal.h"
-#include "statistics/statistics.h"
-#include "utils/builtins.h"
-#include "utils/fmgrprotos.h"
 #include "utils/syscache.h"
 #include "utils/typcache.h"
 #include "varatt.h"
@@ -334,458 +325,6 @@ statext_ndistinct_deserialize(bytea *data)
 	return ndistinct;
 }
 
-typedef enum
-{
-	NDIST_EXPECT_START = 0,
-	NDIST_EXPECT_ITEM,
-	NDIST_EXPECT_KEY,
-	NDIST_EXPECT_ATTNUM_LIST,
-	NDIST_EXPECT_ATTNUM,
-	NDIST_EXPECT_NDISTINCT,
-	NDIST_EXPECT_COMPLETE
-}			ndistinctSemanticState;
-
-typedef struct
-{
-	const char *str;
-	ndistinctSemanticState state;
-
-	List	   *distinct_items; /* Accumulated complete MVNDistinctItems */
-	Node	   *escontext;
-
-	bool		found_attributes;	/* Item has an attributes key */
-	bool		found_ndistinct;	/* Item has ndistinct key */
-	List	   *attnum_list;	/* Accumulated attributes attnums */
-	int64		ndistinct;
-}			ndistinctParseState;
-
-/*
- * Invoked at the start of each MVNDistinctItem.
- *
- * The entire JSON document shoul be one array of MVNDistinctItem objects.
- *
- * If we're anywhere else in the document, it's an error.
- */
-static JsonParseErrorType
-ndistinct_object_start(void *state)
-{
-	ndistinctParseState *parse = state;
-
-	if (parse->state != NDIST_EXPECT_ITEM)
-	{
-		ereturn(parse->escontext, (Datum) 0,
-				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
-				 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
-				 errdetail("Expected Item object")));
-		return JSON_SEM_ACTION_FAILED;
-	}
-
-	/* Now we expect to see attributes/ndistinct keys */
-	parse->state = NDIST_EXPECT_KEY;
-	return JSON_SUCCESS;
-}
-
-/*
- * Routine to allow qsorting of AttNumbers
- */
-static int
-attnum_compare(const void *aptr, const void *bptr)
-{
-	AttrNumber	a = *(const AttrNumber *) aptr;
-	AttrNumber	b = *(const AttrNumber *) bptr;
-
-	return pg_cmp_s16(a, b);
-}
-
-
-/*
- * Invoked at the end of an object.
- *
- * Check to ensure that it was a complete MVNDistinctItem
- *
- */
-static JsonParseErrorType
-ndistinct_object_end(void *state)
-{
-	ndistinctParseState *parse = state;
-
-	int			natts = 0;
-	AttrNumber *attrsort;
-
-	MVNDistinctItem *item;
-
-	if (!parse->found_attributes)
-	{
-		ereturn(parse->escontext, (Datum) 0,
-				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
-				 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
-				 errdetail("Item must contain \"attributes\" key")));
-		return JSON_SEM_ACTION_FAILED;
-	}
-
-	if (!parse->found_ndistinct)
-	{
-		ereturn(parse->escontext, (Datum) 0,
-				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
-				 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
-				 errdetail("Item must contain \"ndistinct\" key")));
-		return JSON_SEM_ACTION_FAILED;
-	}
-
-	if (parse->attnum_list == NIL)
-	{
-		ereturn(parse->escontext, (Datum) 0,
-				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
-				 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
-				 errdetail("The \"attributes\" key must be an non-empty array")));
-		return JSON_SEM_ACTION_FAILED;
-	}
-
-	/*
-	 * We need at least 2 attnums for a ndistinct item, anything less is
-	 * malformed.
-	 */
-	natts = parse->attnum_list->length;
-	if (natts < 2)
-	{
-		ereturn(parse->escontext, (Datum) 0,
-				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
-				 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
-				 errdetail("The attributes key must contain an array of at least two attnums")));
-
-		return JSON_SEM_ACTION_FAILED;
-	}
-	attrsort = palloc0(natts * sizeof(AttrNumber));
-
-	/* Create the MVNDistinctItem */
-	item = palloc(sizeof(MVNDistinctItem));
-	item->nattributes = natts;
-	item->attributes = palloc0(natts * sizeof(AttrNumber));
-	item->ndistinct = (double) parse->ndistinct;
-
-	/* fill out both attnum list and sortable list */
-	for (int i = 0; i < natts; i++)
-	{
-		attrsort[i] = (AttrNumber) parse->attnum_list->elements[i].int_value;
-		item->attributes[i] = attrsort[i];
-	}
-
-	/* Check attrsort for uniqueness */
-	qsort(attrsort, natts, sizeof(AttrNumber), attnum_compare);
-	for (int i = 1; i < natts; i++)
-		if (attrsort[i] == attrsort[i - 1])
-		{
-			ereturn(parse->escontext, (Datum) 0,
-					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
-					 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
-					 errdetail("attnum list duplicate value found: %d", attrsort[i])));
-
-			return JSON_SEM_ACTION_FAILED;
-		}
-	pfree(attrsort);
-
-	parse->distinct_items = lappend(parse->distinct_items, (void *) item);
-
-	/* reset item state vars */
-	list_free(parse->attnum_list);
-	parse->attnum_list = NIL;
-	parse->ndistinct = 0;
-	parse->found_attributes = false;
-	parse->found_ndistinct = false;
-
-	/* Now we are looking for the next MVNDistinctItem */
-	parse->state = NDIST_EXPECT_ITEM;
-	return JSON_SUCCESS;
-}
-
-
-/*
- * ndsitinct input format has two types of arrays, the outer MVNDistinctItem
- * array, and the attnum list array within each MVNDistinctItem.
- */
-static JsonParseErrorType
-ndistinct_array_start(void *state)
-{
-	ndistinctParseState *parse = state;
-
-	switch (parse->state)
-	{
-		case NDIST_EXPECT_ATTNUM_LIST:
-			parse->state = NDIST_EXPECT_ATTNUM;
-			break;
-
-		case NDIST_EXPECT_START:
-			parse->state = NDIST_EXPECT_ITEM;
-			break;
-
-		default:
-			ereturn(parse->escontext, (Datum) 0,
-					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
-					 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
-					 errdetail("Array found in unexpected place")));
-			return JSON_SEM_ACTION_FAILED;
-	}
-
-	return JSON_SUCCESS;
-}
-
-
-static JsonParseErrorType
-ndistinct_array_end(void *state)
-{
-	ndistinctParseState *parse = state;
-
-	/* The attnum list is complete, look for more MVNDistinctItem keys */
-	if (parse->state == NDIST_EXPECT_ATTNUM)
-	{
-		parse->state = NDIST_EXPECT_KEY;
-		return JSON_SUCCESS;
-	}
-
-	if (parse->state == NDIST_EXPECT_ITEM)
-	{
-		parse->state = NDIST_EXPECT_COMPLETE;
-		return JSON_SUCCESS;
-	}
-
-	ereturn(parse->escontext, (Datum) 0,
-			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
-			 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
-			 errdetail("Array found in unexpected place")));
-	return JSON_SEM_ACTION_FAILED;
-}
-
-
-/*
- * The valid keys for the MVNDistinctItem object are:
- *   - attributes
- *   - ndistinct
- */
-static JsonParseErrorType
-ndistinct_object_field_start(void *state, char *fname, bool isnull)
-{
-	ndistinctParseState *parse = state;
-
-	const char *attributes = "attributes";
-	const char *ndistinct = "ndistinct";
-
-	if (strcmp(fname, attributes) == 0)
-	{
-		parse->found_attributes = true;
-		parse->state = NDIST_EXPECT_ATTNUM_LIST;
-		return JSON_SUCCESS;
-	}
-
-	if (strcmp(fname, ndistinct) == 0)
-	{
-		parse->found_ndistinct = true;
-		parse->state = NDIST_EXPECT_NDISTINCT;
-		return JSON_SUCCESS;
-	}
-
-	ereturn(parse->escontext, (Datum) 0,
-			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
-			 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
-			 errdetail("Only allowed keys are \%s\" and \%s\".", attributes, ndistinct)));
-	return JSON_SEM_ACTION_FAILED;
-}
-
-/*
- *
- */
-static JsonParseErrorType
-ndistinct_array_element_start(void *state, bool isnull)
-{
-	ndistinctParseState *parse = state;
-
-	if (parse->state == NDIST_EXPECT_ATTNUM)
-	{
-		if (isnull)
-		{
-			ereturn(parse->escontext, (Datum) 0,
-					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
-					 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
-					 errdetail("Attnum list elements cannot be null.")));
-
-			return JSON_SEM_ACTION_FAILED;
-		}
-		return JSON_SUCCESS;
-	}
-
-	if (parse->state == NDIST_EXPECT_ITEM)
-	{
-		if (isnull)
-		{
-			ereturn(parse->escontext, (Datum) 0,
-					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
-					 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
-					 errdetail("Item list elements cannot be null.")));
-
-			return JSON_SEM_ACTION_FAILED;
-		}
-
-		return JSON_SUCCESS;
-	}
-
-	ereturn(parse->escontext, (Datum) 0,
-			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
-			 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
-			 errdetail("Unexpected array element.")));
-
-	return JSON_SEM_ACTION_FAILED;
-}
-
-/*
- * Handle scalar events from the ndistinct input parser.
- *
- */
-static JsonParseErrorType
-ndistinct_scalar(void *state, char *token, JsonTokenType tokentype)
-{
-	ndistinctParseState *parse = state;
-
-	if (parse->state == NDIST_EXPECT_ATTNUM)
-	{
-		AttrNumber	attnum = pg_strtoint16_safe(token, parse->escontext);
-
-		if (SOFT_ERROR_OCCURRED(parse->escontext))
-			return JSON_SEM_ACTION_FAILED;
-
-		parse->attnum_list = lappend_int(parse->attnum_list, (int) attnum);
-		return JSON_SUCCESS;
-	}
-
-	if (parse->state == NDIST_EXPECT_NDISTINCT)
-	{
-		/*
-		 * While the structure dictates that ndistinct in a double precision
-		 * floating point, in practice it has always been an integer, and it
-		 * is output as such. Therefore, we follow usage precendent over the
-		 * actual storage structure, and read it in as an integer.
-		 */
-		parse->ndistinct = pg_strtoint64_safe(token, parse->escontext);
-
-		if (SOFT_ERROR_OCCURRED(parse->escontext))
-			return JSON_SEM_ACTION_FAILED;
-
-		parse->state = NDIST_EXPECT_KEY;
-		return JSON_SUCCESS;
-	}
-
-	ereturn(parse->escontext, (Datum) 0,
-			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
-			 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
-			 errdetail("Unexpected scalar.")));
-
-	return JSON_SEM_ACTION_FAILED;
-}
-
-/*
- * pg_ndistinct_in
- *		input routine for type pg_ndistinct
- *
- * example input:
- *     [{"attributes": [6, -1], "ndistinct": 14},
- *      {"attributes": [6, -2], "ndistinct": 9143},
- *      {"attributes": [-1,-2], "ndistinct": 13454},
- *      {"attributes": [6, -1, -2], "ndistinct": 14549}]
- */
-Datum
-pg_ndistinct_in(PG_FUNCTION_ARGS)
-{
-	char	   *str = PG_GETARG_CSTRING(0);
-
-	ndistinctParseState parse_state;
-	JsonParseErrorType result;
-	JsonLexContext *lex;
-	JsonSemAction sem_action;
-
-	/* initialize semantic state */
-	parse_state.str = str;
-	parse_state.state = NDIST_EXPECT_START;
-	parse_state.distinct_items = NIL;
-	parse_state.escontext = fcinfo->context;
-	parse_state.found_attributes = false;
-	parse_state.found_ndistinct = false;
-	parse_state.attnum_list = NIL;
-	parse_state.ndistinct = 0;
-
-	/* set callbacks */
-	sem_action.semstate = (void *) &parse_state;
-	sem_action.object_start = ndistinct_object_start;
-	sem_action.object_end = ndistinct_object_end;
-	sem_action.array_start = ndistinct_array_start;
-	sem_action.array_end = ndistinct_array_end;
-	sem_action.object_field_start = ndistinct_object_field_start;
-	sem_action.object_field_end = NULL;
-	sem_action.array_element_start = ndistinct_array_element_start;
-	sem_action.array_element_end = NULL;
-	sem_action.scalar = ndistinct_scalar;
-
-	lex = makeJsonLexContextCstringLen(NULL, str, strlen(str),
-									   PG_UTF8, true);
-	result = pg_parse_json(lex, &sem_action);
-	freeJsonLexContext(lex);
-
-	if (result == JSON_SUCCESS)
-	{
-		MVNDistinct *ndistinct;
-		int			nitems = parse_state.distinct_items->length;
-		bytea	   *bytes;
-
-		ndistinct = palloc(offsetof(MVNDistinct, items) +
-						   nitems * sizeof(MVNDistinctItem));
-
-		ndistinct->magic = STATS_NDISTINCT_MAGIC;
-		ndistinct->type = STATS_NDISTINCT_TYPE_BASIC;
-		ndistinct->nitems = nitems;
-
-		for (int i = 0; i < nitems; i++)
-		{
-			MVNDistinctItem *item = parse_state.distinct_items->elements[i].ptr_value;
-
-			ndistinct->items[i].ndistinct = item->ndistinct;
-			ndistinct->items[i].nattributes = item->nattributes;
-			ndistinct->items[i].attributes = item->attributes;
-
-			/*
-			 * free the MVNDistinctItem, but not the attributes we're still
-			 * using
-			 */
-			pfree(item);
-		}
-		bytes = statext_ndistinct_serialize(ndistinct);
-
-		list_free(parse_state.distinct_items);
-		for (int i = 0; i < nitems; i++)
-			pfree(ndistinct->items[i].attributes);
-		pfree(ndistinct);
-
-		PG_RETURN_BYTEA_P(bytes);
-	}
-	else if (result == JSON_SEM_ACTION_FAILED)
-		PG_RETURN_NULL();		/* escontext already set */
-
-	/* Anything else is a generic JSON parse error */
-	ereturn(parse_state.escontext, (Datum) 0,
-			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
-			 errmsg("malformed pg_ndistinct: \"%s\"", str),
-			 errdetail("Must be valid JSON.")));
-	PG_RETURN_NULL();
-}
-
-/*
- * Free allocations of an MVNDistinct
- */
-void
-free_pg_ndistinct(MVNDistinct *ndistinct)
-{
-	for (int i = 0; i < ndistinct->nitems; i++)
-		pfree(ndistinct->items[i].attributes);
-
-	pfree(ndistinct);
-}
-
 /*
  * Validate an MVNDistinct against the extended statistics object definition.
  *
@@ -836,74 +375,6 @@ pg_ndistinct_validate_items(MVNDistinct *ndistinct, int2vector *stxkeys, int num
 }

-/*
- * pg_ndistinct
- *		output routine for type pg_ndistinct
- *
- * Produces a human-readable representation of the value, in the format:
- *   [{"attributes": [attnum,. ..], "ndistinct": int}, ...]
- *
- */
-Datum
-pg_ndistinct_out(PG_FUNCTION_ARGS)
-{
-	bytea	   *data = PG_GETARG_BYTEA_PP(0);
-	MVNDistinct *ndist = statext_ndistinct_deserialize(data);
-	int			i;
-	StringInfoData str;
-
-	initStringInfo(&str);
-	appendStringInfoChar(&str, '[');
-
-	for (i = 0; i < ndist->nitems; i++)
-	{
-		MVNDistinctItem item = ndist->items[i];
-
-		if (i > 0)
-			appendStringInfoString(&str, ", ");
-
-		if (item.nattributes <= 0)
-			elog(ERROR, "invalid zero-length attribute array in MVNDistinct");
-
-		appendStringInfo(&str, "{\"attributes\": [%d", item.attributes[0]);
-
-		for (int j = 1; j < item.nattributes; j++)
-			appendStringInfo(&str, ", %d", item.attributes[j]);
-
-		appendStringInfo(&str, "], \"ndistinct\": %d}", (int) item.ndistinct);
-	}
-
-	appendStringInfoChar(&str, ']');
-
-	PG_RETURN_CSTRING(str.data);
-}
-
-/*
- * pg_ndistinct_recv
- *		binary input routine for type pg_ndistinct
- */
-Datum
-pg_ndistinct_recv(PG_FUNCTION_ARGS)
-{
-	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot accept a value of type %s", "pg_ndistinct")));
-
-	PG_RETURN_VOID();			/* keep compiler quiet */
-}
-
-/*
- * pg_ndistinct_send
- *		binary output routine for type pg_ndistinct
- *
- * n-distinct is serialized into a bytea value, so let's send that.
- */
-Datum
-pg_ndistinct_send(PG_FUNCTION_ARGS)
-{
-	return byteasend(fcinfo);
-}
-
 /*
  * ndistinct_for_combination
  *		Estimates number of distinct values in a combination of columns.
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index cc68ac545a5..70ff8e45516 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -85,6 +85,7 @@ OBJS = \
 	pg_locale_icu.o \
 	pg_locale_libc.o \
 	pg_lsn.o \
+	pg_ndistinct.o \
 	pg_upgrade_support.o \
 	pgstatfuncs.o \
 	pseudorandomfuncs.o \
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index 12fa0c20912..b6b642c77a0 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -81,6 +81,7 @@ backend_sources += files(
   'pg_locale_icu.c',
   'pg_locale_libc.c',
   'pg_lsn.c',
+  'pg_ndistinct.c',
   'pg_upgrade_support.c',
   'pgstatfuncs.c',
   'pseudorandomfuncs.c',
diff --git a/src/backend/utils/adt/pg_ndistinct.c b/src/backend/utils/adt/pg_ndistinct.c
new file mode 100644
index 00000000000..9dd24f06ecf
--- /dev/null
+++ b/src/backend/utils/adt/pg_ndistinct.c
@@ -0,0 +1,550 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_ndistinct.c
+ *		pg_ndistinct data type support.
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/pg_ndistinct.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "common/int.h"
+#include "common/jsonapi.h"
+#include "mb/pg_wchar.h"
+#include "nodes/miscnodes.h"
+#include "nodes/pg_list.h"
+#include "statistics/statistics.h"
+#include "statistics/extended_stats_internal.h"
+#include "utils/builtins.h"
+#include "utils/syscache.h"
+
+/*
+ * The valid keys for the pg_ndistinct object:
+ */
+static const char *attributes = "attributes";
+static const char *ndistinct = "ndistinct";
+
+typedef enum
+{
+	NDIST_EXPECT_START = 0,
+	NDIST_EXPECT_ITEM,
+	NDIST_EXPECT_KEY,
+	NDIST_EXPECT_ATTNUM_LIST,
+	NDIST_EXPECT_ATTNUM,
+	NDIST_EXPECT_NDISTINCT,
+	NDIST_EXPECT_COMPLETE
+}			ndistinctSemanticState;
+
+typedef struct
+{
+	const char *str;
+	ndistinctSemanticState state;
+
+	List	   *distinct_items; /* Accumulated complete MVNDistinctItems */
+	Node	   *escontext;
+
+	bool		found_attributes;	/* Item has an attributes key */
+	bool		found_ndistinct;	/* Item has ndistinct key */
+	List	   *attnum_list;	/* Accumulated attributes attnums */
+	int64		ndistinct;
+}			ndistinctParseState;
+
+/*
+ * Invoked at the start of each MVNDistinctItem.
+ *
+ * The entire JSON document should be one array of MVNDistinctItem objects.
+ *
+ * If we're anywhere else in the document, it's an error.
+ */
+static JsonParseErrorType
+ndistinct_object_start(void *state)
+{
+	ndistinctParseState *parse = state;
+
+	if (parse->state != NDIST_EXPECT_ITEM)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+				 errdetail("Expected Item object")));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	/* Now we expect to see attributes/ndistinct keys */
+	parse->state = NDIST_EXPECT_KEY;
+	return JSON_SUCCESS;
+}
+
+/*
+ * Routine to allow qsorting of AttNumbers
+ */
+static int
+attnum_compare(const void *aptr, const void *bptr)
+{
+	AttrNumber	a = *(const AttrNumber *) aptr;
+	AttrNumber	b = *(const AttrNumber *) bptr;
+
+	return pg_cmp_s16(a, b);
+}
+
+
+/*
+ * Invoked at the end of an object.
+ *
+ * Check to ensure that it was a complete MVNDistinctItem
+ *
+ */
+static JsonParseErrorType
+ndistinct_object_end(void *state)
+{
+	ndistinctParseState *parse = state;
+
+	int			natts = 0;
+	AttrNumber *attrsort;
+
+	MVNDistinctItem *item;
+
+	if (!parse->found_attributes)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+				 errdetail("Item must contain \"%s\" key", attributes)));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	if (!parse->found_ndistinct)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+				 errdetail("Item must contain \"%s\" key", ndistinct)));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	if (parse->attnum_list == NIL)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+				 errdetail("The \"%s\" key must be an non-empty array", attributes)));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	/*
+	 * We need at least 2 attnums for a ndistinct item, anything less is
+	 * malformed.
+	 */
+	natts = parse->attnum_list->length;
+	if (natts < 2)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+				 errdetail("The \"%s\" key must contain an array of at least two attnums", attributes)));
+
+		return JSON_SEM_ACTION_FAILED;
+	}
+	attrsort = palloc0(natts * sizeof(AttrNumber));
+
+	/* Create the MVNDistinctItem */
+	item = palloc(sizeof(MVNDistinctItem));
+	item->nattributes = natts;
+	item->attributes = palloc0(natts * sizeof(AttrNumber));
+	item->ndistinct = (double) parse->ndistinct;
+
+	/* fill out both attnum list and sortable list */
+	for (int i = 0; i < natts; i++)
+	{
+		attrsort[i] = (AttrNumber) parse->attnum_list->elements[i].int_value;
+		item->attributes[i] = attrsort[i];
+	}
+
+	/* Check attrsort for uniqueness */
+	qsort(attrsort, natts, sizeof(AttrNumber), attnum_compare);
+	for (int i = 1; i < natts; i++)
+		if (attrsort[i] == attrsort[i - 1])
+		{
+			ereturn(parse->escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					 errdetail("attnum list duplicate value found: %d", attrsort[i])));
+
+			return JSON_SEM_ACTION_FAILED;
+		}
+	pfree(attrsort);
+
+	parse->distinct_items = lappend(parse->distinct_items, (void *) item);
+
+	/* reset item state vars */
+	list_free(parse->attnum_list);
+	parse->attnum_list = NIL;
+	parse->ndistinct = 0;
+	parse->found_attributes = false;
+	parse->found_ndistinct = false;
+
+	/* Now we are looking for the next MVNDistinctItem */
+	parse->state = NDIST_EXPECT_ITEM;
+	return JSON_SUCCESS;
+}
+
+
+/*
+ * ndsitinct input format has two types of arrays, the outer MVNDistinctItem
+ * array, and the attnum list array within each MVNDistinctItem.
+ */
+static JsonParseErrorType
+ndistinct_array_start(void *state)
+{
+	ndistinctParseState *parse = state;
+
+	switch (parse->state)
+	{
+		case NDIST_EXPECT_ATTNUM_LIST:
+			parse->state = NDIST_EXPECT_ATTNUM;
+			break;
+
+		case NDIST_EXPECT_START:
+			parse->state = NDIST_EXPECT_ITEM;
+			break;
+
+		default:
+			ereturn(parse->escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					 errdetail("Array found in unexpected place")));
+			return JSON_SEM_ACTION_FAILED;
+	}
+
+	return JSON_SUCCESS;
+}
+
+
+/*
+ *
+ */
+static JsonParseErrorType
+ndistinct_array_end(void *state)
+{
+	ndistinctParseState *parse = state;
+
+	/* The attnum list is complete, look for more MVNDistinctItem keys */
+	if (parse->state == NDIST_EXPECT_ATTNUM)
+	{
+		parse->state = NDIST_EXPECT_KEY;
+		return JSON_SUCCESS;
+	}
+
+	if (parse->state == NDIST_EXPECT_ITEM)
+	{
+		parse->state = NDIST_EXPECT_COMPLETE;
+		return JSON_SUCCESS;
+	}
+
+	ereturn(parse->escontext, (Datum) 0,
+			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+			 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+			 errdetail("Array found in unexpected place")));
+	return JSON_SEM_ACTION_FAILED;
+}
+
+
+/*
+ *
+ */
+static JsonParseErrorType
+ndistinct_object_field_start(void *state, char *fname, bool isnull)
+{
+	ndistinctParseState *parse = state;
+
+	if (strcmp(fname, attributes) == 0)
+	{
+		parse->found_attributes = true;
+		parse->state = NDIST_EXPECT_ATTNUM_LIST;
+		return JSON_SUCCESS;
+	}
+
+	if (strcmp(fname, ndistinct) == 0)
+	{
+		parse->found_ndistinct = true;
+		parse->state = NDIST_EXPECT_NDISTINCT;
+		return JSON_SUCCESS;
+	}
+
+	ereturn(parse->escontext, (Datum) 0,
+			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+			 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+			 errdetail("Only allowed keys are \%s\" and \%s\".", attributes, ndistinct)));
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ *
+ */
+static JsonParseErrorType
+ndistinct_array_element_start(void *state, bool isnull)
+{
+	ndistinctParseState *parse = state;
+
+	if (parse->state == NDIST_EXPECT_ATTNUM)
+	{
+		if (isnull)
+		{
+			ereturn(parse->escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					 errdetail("Attnum list elements cannot be null.")));
+
+			return JSON_SEM_ACTION_FAILED;
+		}
+		return JSON_SUCCESS;
+	}
+
+	if (parse->state == NDIST_EXPECT_ITEM)
+	{
+		if (isnull)
+		{
+			ereturn(parse->escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					 errdetail("Item list elements cannot be null.")));
+
+			return JSON_SEM_ACTION_FAILED;
+		}
+
+		return JSON_SUCCESS;
+	}
+
+	ereturn(parse->escontext, (Datum) 0,
+			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+			 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+			 errdetail("Unexpected array element.")));
+
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ * Handle scalar events from the ndistinct input parser.
+ *
+ */
+static JsonParseErrorType
+ndistinct_scalar(void *state, char *token, JsonTokenType tokentype)
+{
+	ndistinctParseState *parse = state;
+
+	if (parse->state == NDIST_EXPECT_ATTNUM)
+	{
+		AttrNumber	attnum = pg_strtoint16_safe(token, parse->escontext);
+
+		if (SOFT_ERROR_OCCURRED(parse->escontext))
+			return JSON_SEM_ACTION_FAILED;
+
+		parse->attnum_list = lappend_int(parse->attnum_list, (int) attnum);
+		return JSON_SUCCESS;
+	}
+
+	if (parse->state == NDIST_EXPECT_NDISTINCT)
+	{
+		/*
+		 * While the structure dictates that ndistinct in a double precision
+		 * floating point, in practice it has always been an integer, and it
+		 * is output as such. Therefore, we follow usage precendent over the
+		 * actual storage structure, and read it in as an integer.
+		 */
+		parse->ndistinct = pg_strtoint64_safe(token, parse->escontext);
+
+		if (SOFT_ERROR_OCCURRED(parse->escontext))
+			return JSON_SEM_ACTION_FAILED;
+
+		parse->state = NDIST_EXPECT_KEY;
+		return JSON_SUCCESS;
+	}
+
+	ereturn(parse->escontext, (Datum) 0,
+			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+			 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+			 errdetail("Unexpected scalar.")));
+
+	return JSON_SEM_ACTION_FAILED;
+}
+
+
+/*
+ * Free allocations of an MVNDistinct
+ */
+void
+free_pg_ndistinct(MVNDistinct *ndistinct)
+{
+	for (int i = 0; i < ndistinct->nitems; i++)
+		pfree(ndistinct->items[i].attributes);
+
+	pfree(ndistinct);
+}
+
+
+/*
+ * pg_ndistinct_in
+ *		input routine for type pg_ndistinct
+ *
+ * example input:
+ *     [{"attributes": [6, -1], "ndistinct": 14},
+ *      {"attributes": [6, -2], "ndistinct": 9143},
+ *      {"attributes": [-1,-2], "ndistinct": 13454},
+ *      {"attributes": [6, -1, -2], "ndistinct": 14549}]
+ */
+Datum
+pg_ndistinct_in(PG_FUNCTION_ARGS)
+{
+	char	   *str = PG_GETARG_CSTRING(0);
+
+	ndistinctParseState parse_state;
+	JsonParseErrorType result;
+	JsonLexContext *lex;
+	JsonSemAction sem_action;
+
+	/* initialize semantic state */
+	parse_state.str = str;
+	parse_state.state = NDIST_EXPECT_START;
+	parse_state.distinct_items = NIL;
+	parse_state.escontext = fcinfo->context;
+	parse_state.found_attributes = false;
+	parse_state.found_ndistinct = false;
+	parse_state.attnum_list = NIL;
+	parse_state.ndistinct = 0;
+
+	/* set callbacks */
+	sem_action.semstate = (void *) &parse_state;
+	sem_action.object_start = ndistinct_object_start;
+	sem_action.object_end = ndistinct_object_end;
+	sem_action.array_start = ndistinct_array_start;
+	sem_action.array_end = ndistinct_array_end;
+	sem_action.object_field_start = ndistinct_object_field_start;
+	sem_action.object_field_end = NULL;
+	sem_action.array_element_start = ndistinct_array_element_start;
+	sem_action.array_element_end = NULL;
+	sem_action.scalar = ndistinct_scalar;
+
+	lex = makeJsonLexContextCstringLen(NULL, str, strlen(str),
+									   PG_UTF8, true);
+	result = pg_parse_json(lex, &sem_action);
+	freeJsonLexContext(lex);
+
+	if (result == JSON_SUCCESS)
+	{
+		MVNDistinct *ndistinct;
+		int			nitems = parse_state.distinct_items->length;
+		bytea	   *bytes;
+
+		ndistinct = palloc(offsetof(MVNDistinct, items) +
+						   nitems * sizeof(MVNDistinctItem));
+
+		ndistinct->magic = STATS_NDISTINCT_MAGIC;
+		ndistinct->type = STATS_NDISTINCT_TYPE_BASIC;
+		ndistinct->nitems = nitems;
+
+		for (int i = 0; i < nitems; i++)
+		{
+			MVNDistinctItem *item = parse_state.distinct_items->elements[i].ptr_value;
+
+			ndistinct->items[i].ndistinct = item->ndistinct;
+			ndistinct->items[i].nattributes = item->nattributes;
+			ndistinct->items[i].attributes = item->attributes;
+
+			/*
+			 * free the MVNDistinctItem, but not the attributes we're still
+			 * using
+			 */
+			pfree(item);
+		}
+		bytes = statext_ndistinct_serialize(ndistinct);
+
+		list_free(parse_state.distinct_items);
+		for (int i = 0; i < nitems; i++)
+			pfree(ndistinct->items[i].attributes);
+		pfree(ndistinct);
+
+		PG_RETURN_BYTEA_P(bytes);
+	}
+	else if (result == JSON_SEM_ACTION_FAILED)
+		PG_RETURN_NULL();		/* escontext already set */
+
+	/* Anything else is a generic JSON parse error */
+	ereturn(parse_state.escontext, (Datum) 0,
+			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+			 errmsg("malformed pg_ndistinct: \"%s\"", str),
+			 errdetail("Must be valid JSON.")));
+	PG_RETURN_NULL();
+}
+
+/*
+ * pg_ndistinct
+ *		output routine for type pg_ndistinct
+ *
+ * Produces a human-readable representation of the value, in the format:
+ *   [{"attributes": [attnum,. ..], "ndistinct": int}, ...]
+ *
+ */
+Datum
+pg_ndistinct_out(PG_FUNCTION_ARGS)
+{
+	bytea	   *data = PG_GETARG_BYTEA_PP(0);
+	MVNDistinct *ndist = statext_ndistinct_deserialize(data);
+	int			i;
+	StringInfoData str;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '[');
+
+	for (i = 0; i < ndist->nitems; i++)
+	{
+		MVNDistinctItem item = ndist->items[i];
+
+		if (i > 0)
+			appendStringInfoString(&str, ", ");
+
+		if (item.nattributes <= 0)
+			elog(ERROR, "invalid zero-length attribute array in MVNDistinct");
+
+		appendStringInfo(&str, "{\"%s\": [%d", attributes, item.attributes[0]);
+
+		for (int j = 1; j < item.nattributes; j++)
+			appendStringInfo(&str, ", %d", item.attributes[j]);
+
+		appendStringInfo(&str, "], \"%s\": %d}", ndistinct, (int) item.ndistinct);
+	}
+
+	appendStringInfoChar(&str, ']');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * pg_ndistinct_recv
+ *		binary input routine for type pg_ndistinct
+ */
+Datum
+pg_ndistinct_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_ndistinct")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * pg_ndistinct_send
+ *		binary output routine for type pg_ndistinct
+ *
+ * n-distinct is serialized into a bytea value, so let's send that.
+ */
+Datum
+pg_ndistinct_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
-- 
2.51.1

From 5826e635f06e1e347ae5609ffdffdcf69fc13cd7 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Mon, 10 Nov 2025 00:13:14 -0500
Subject: [PATCH v9 9/9] Make pg_dependencies a proper adt.

Move the in/out/send/recv functions for pg_dependencies to pg_dependencies.c,
which allows dependencies.c to focus on the transformation from sample
data to the internal MVDependencies structure.
---
 src/backend/statistics/dependencies.c   | 539 -----------------------
 src/backend/utils/adt/Makefile          |   1 +
 src/backend/utils/adt/meson.build       |   1 +
 src/backend/utils/adt/pg_dependencies.c | 556 ++++++++++++++++++++++++
 4 files changed, 558 insertions(+), 539 deletions(-)
 create mode 100644 src/backend/utils/adt/pg_dependencies.c

diff --git a/src/backend/statistics/dependencies.c b/src/backend/statistics/dependencies.c
index 7b310b9f55d..b1d4e84de21 100644
--- a/src/backend/statistics/dependencies.c
+++ b/src/backend/statistics/dependencies.c
@@ -13,26 +13,18 @@
  */
 #include "postgres.h"
 
-#include "access/attnum.h"
 #include "access/htup_details.h"
 #include "catalog/pg_statistic_ext.h"
 #include "catalog/pg_statistic_ext_data.h"
-#include "common/int.h"
-#include "common/jsonapi.h"
 #include "lib/stringinfo.h"
-#include "mb/pg_wchar.h"
-#include "nodes/miscnodes.h"
 #include "nodes/nodeFuncs.h"
 #include "nodes/nodes.h"
 #include "nodes/pathnodes.h"
-#include "nodes/pg_list.h"
 #include "optimizer/clauses.h"
 #include "optimizer/optimizer.h"
 #include "parser/parsetree.h"
 #include "statistics/extended_stats_internal.h"
 #include "statistics/statistics.h"
-#include "utils/builtins.h"
-#include "utils/float.h"
 #include "utils/fmgroids.h"
 #include "utils/fmgrprotos.h"
 #include "utils/lsyscache.h"
@@ -651,377 +643,6 @@ statext_dependencies_load(Oid mvoid, bool inh)
 	return result;
 }
 
-typedef enum
-{
-	DEPS_EXPECT_START = 0,
-	DEPS_EXPECT_ITEM,
-	DEPS_EXPECT_KEY,
-	DEPS_EXPECT_ATTNUM_LIST,
-	DEPS_EXPECT_ATTNUM,
-	DEPS_EXPECT_DEPENDENCY,
-	DEPS_EXPECT_DEGREE,
-	DEPS_PARSE_COMPLETE
-}			depsParseSemanticState;
-
-typedef struct
-{
-	const char *str;
-	depsParseSemanticState state;
-
-	List	   *dependency_list;
-	Node	   *escontext;
-
-	bool		found_attributes;	/* Item has an attributes key */
-	bool		found_dependency;	/* Item has an dependency key */
-	bool		found_degree;	/* Item has degree key */
-	List	   *attnum_list;	/* Accumulated attributes attnums */
-	AttrNumber	dependency;
-	double		degree;
-}			dependenciesParseState;
-
-/*
- * Invoked at the start of each MVDependency object.
- *
- * The entire JSON document shoul be one array of MVDependency objects.
- *
- * If we're anywhere else in the document, it's an error.
- */
-static JsonParseErrorType
-dependencies_object_start(void *state)
-{
-	dependenciesParseState *parse = state;
-
-	if (parse->state != DEPS_EXPECT_ITEM)
-	{
-		ereturn(parse->escontext, (Datum) 0,
-				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
-				 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
-				 errdetail("Expected Item object")));
-		return JSON_SEM_ACTION_FAILED;
-	}
-
-	/* Now we expect to see attributes/dependency/degree keys */
-	parse->state = DEPS_EXPECT_KEY;
-	return JSON_SUCCESS;
-}
-
-static int
-attnum_compare(const void *aptr, const void *bptr)
-{
-	AttrNumber	a = *(const AttrNumber *) aptr;
-	AttrNumber	b = *(const AttrNumber *) bptr;
-
-	return pg_cmp_s16(a, b);
-}
-
-static JsonParseErrorType
-dependencies_object_end(void *state)
-{
-	dependenciesParseState *parse = state;
-
-	MVDependency *dep;
-	AttrNumber *attrsort;
-
-	int			natts = 0;
-
-	if (!parse->found_attributes)
-	{
-		ereturn(parse->escontext, (Datum) 0,
-				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
-				 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
-				 errdetail("Item must contain \"attributes\" key")));
-		return JSON_SEM_ACTION_FAILED;
-	}
-
-	if (!parse->found_dependency)
-	{
-		ereturn(parse->escontext, (Datum) 0,
-				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
-				 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
-				 errdetail("Item must contain \"dependencies\" key")));
-		return JSON_SEM_ACTION_FAILED;
-	}
-
-	if (!parse->found_degree)
-	{
-		ereturn(parse->escontext, (Datum) 0,
-				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
-				 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
-				 errdetail("Item must contain \"degree\" key")));
-		return JSON_SEM_ACTION_FAILED;
-	}
-
-	if (parse->attnum_list == NIL)
-	{
-		ereturn(parse->escontext, (Datum) 0,
-				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
-				 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
-				 errdetail("The \"attributes\" key must be an non-empty array")));
-		return JSON_SEM_ACTION_FAILED;
-	}
-
-	/*
-	 * We need at least 1 attnum for a dependencies item, anything less is
-	 * malformed.
-	 */
-	natts = parse->attnum_list->length;
-	if (natts < 1)
-	{
-		ereturn(parse->escontext, (Datum) 0,
-				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
-				 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
-				 errdetail("The attributes key must contain an array of at least one attnum")));
-
-		return JSON_SEM_ACTION_FAILED;
-	}
-	attrsort = palloc0(natts * sizeof(AttrNumber));
-
-	/*
-	 * Allocate enough space for the dependency, the attnums in the list, plus
-	 * the final attnum
-	 */
-	dep = palloc0(offsetof(MVDependency, attributes) + ((natts + 1) * sizeof(AttrNumber)));
-	dep->nattributes = natts + 1;
-
-	dep->attributes[natts] = parse->dependency;
-	dep->degree = parse->degree;
-
-	attrsort = palloc0(dep->nattributes * sizeof(AttrNumber));
-	attrsort[natts] = parse->dependency;
-
-	for (int i = 0; i < natts; i++)
-	{
-		attrsort[i] = (AttrNumber) parse->attnum_list->elements[i].int_value;
-		dep->attributes[i] = attrsort[i];
-	}
-
-	/* Check attrsort for uniqueness */
-	qsort(attrsort, natts + 1, sizeof(AttrNumber), attnum_compare);
-	for (int i = 1; i < dep->nattributes; i++)
-		if (attrsort[i] == attrsort[i - 1])
-		{
-			ereturn(parse->escontext, (Datum) 0,
-					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
-					 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
-					 errdetail("attnum list duplicate value found: %d", attrsort[i])));
-
-			return JSON_SEM_ACTION_FAILED;
-		}
-	pfree(attrsort);
-
-	parse->dependency_list = lappend(parse->dependency_list, (void *) dep);
-
-	/* reset dep item state vars */
-	list_free(parse->attnum_list);
-	parse->attnum_list = NIL;
-	parse->dependency = 0;
-	parse->degree = 0.0;
-	parse->found_attributes = false;
-	parse->found_dependency = false;
-	parse->found_degree = false;
-
-	/* Now we are looking for the next MVDependency */
-	parse->state = DEPS_EXPECT_ITEM;
-	return JSON_SUCCESS;
-}
-
-/*
- * dependencies input format does not have arrays, so any array elements encountered
- * are an error.
- */
-static JsonParseErrorType
-dependencies_array_start(void *state)
-{
-	dependenciesParseState *parse = state;
-
-	switch (parse->state)
-	{
-		case DEPS_EXPECT_ATTNUM_LIST:
-			parse->state = DEPS_EXPECT_ATTNUM;
-			break;
-		case DEPS_EXPECT_START:
-			parse->state = DEPS_EXPECT_ITEM;
-			break;
-		default:
-			ereturn(parse->escontext, (Datum) 0,
-					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
-					 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
-					 errdetail("Array found in unexpected place")));
-			return JSON_SEM_ACTION_FAILED;
-	}
-
-	return JSON_SUCCESS;
-}
-
-/*
- * Either the end of an attnum list or the whole object
- */
-static JsonParseErrorType
-dependencies_array_end(void *state)
-{
-	dependenciesParseState *parse = state;
-
-	switch (parse->state)
-	{
-		case DEPS_EXPECT_ATTNUM:
-			parse->state = DEPS_EXPECT_KEY;
-			break;
-
-		case DEPS_EXPECT_ITEM:
-			parse->state = DEPS_PARSE_COMPLETE;
-			break;
-
-		default:
-			ereturn(parse->escontext, (Datum) 0,
-					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
-					 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
-					 errdetail("Array found in unexpected place")));
-			return JSON_SEM_ACTION_FAILED;
-	}
-	return JSON_SUCCESS;
-}
-
-/*
- * The valid keys for the MVDependency object are:
- *   - attributes
- *   - depeendency
- *   - degree
- */
-static JsonParseErrorType
-dependencies_object_field_start(void *state, char *fname, bool isnull)
-{
-	dependenciesParseState *parse = state;
-
-	const char *attributes = "attributes";
-	const char *dependency = "dependency";
-	const char *degree = "degree";
-
-	if (strcmp(fname, attributes) == 0)
-	{
-		parse->found_attributes = true;
-		parse->state = DEPS_EXPECT_ATTNUM_LIST;
-		return JSON_SUCCESS;
-	}
-
-	if (strcmp(fname, dependency) == 0)
-	{
-		parse->found_dependency = true;
-		parse->state = DEPS_EXPECT_DEPENDENCY;
-		return JSON_SUCCESS;
-	}
-
-	if (strcmp(fname, degree) == 0)
-	{
-		parse->found_degree = true;
-		parse->state = DEPS_EXPECT_DEGREE;
-		return JSON_SUCCESS;
-	}
-
-	ereturn(parse->escontext, (Datum) 0,
-			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
-			 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
-			 errdetail("Only allowed keys are \%s\", \"%s\" and \%s\".",
-					   attributes, dependency, degree)));
-	return JSON_SEM_ACTION_FAILED;
-}
-
-/*
- * ndsitinct input format does not have arrays, so any array elements encountered
- * are an error.
- */
-static JsonParseErrorType
-dependencies_array_element_start(void *state, bool isnull)
-{
-	dependenciesParseState *parse = state;
-
-	if (parse->state == DEPS_EXPECT_ATTNUM)
-	{
-		if (isnull)
-		{
-			ereturn(parse->escontext, (Datum) 0,
-					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
-					 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
-					 errdetail("Attnum list elements cannot be null.")));
-
-			return JSON_SEM_ACTION_FAILED;
-		}
-		return JSON_SUCCESS;
-	}
-
-	if (parse->state == DEPS_EXPECT_ITEM)
-	{
-		if (isnull)
-		{
-			ereturn(parse->escontext, (Datum) 0,
-					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
-					 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
-					 errdetail("Item list elements cannot be null.")));
-
-			return JSON_SEM_ACTION_FAILED;
-		}
-
-		return JSON_SUCCESS;
-	}
-
-	ereturn(parse->escontext, (Datum) 0,
-			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
-			 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
-			 errdetail("Unexpected array element.")));
-
-	return JSON_SEM_ACTION_FAILED;
-}
-
-/*
- * Handle scalar events from the dependencies input parser.
- *
- * There is only one case where we will encounter a scalar, and that is the
- * dependency degree for the previous object key.
- */
-static JsonParseErrorType
-dependencies_scalar(void *state, char *token, JsonTokenType tokentype)
-{
-	dependenciesParseState *parse = state;
-
-	if (parse->state == DEPS_EXPECT_ATTNUM)
-	{
-		AttrNumber	attnum = pg_strtoint16_safe(token, parse->escontext);
-
-		if (SOFT_ERROR_OCCURRED(parse->escontext))
-			return JSON_SEM_ACTION_FAILED;
-
-		parse->attnum_list = lappend_int(parse->attnum_list, (int) attnum);
-		return JSON_SUCCESS;
-	}
-
-	if (parse->state == DEPS_EXPECT_DEPENDENCY)
-	{
-		parse->dependency = (AttrNumber) pg_strtoint16_safe(token, parse->escontext);
-
-		if (SOFT_ERROR_OCCURRED(parse->escontext))
-			return JSON_SEM_ACTION_FAILED;
-
-		return JSON_SUCCESS;
-	}
-
-
-	if (parse->state == DEPS_EXPECT_DEGREE)
-	{
-		parse->degree = float8in_internal(token, NULL, "double",
-										  token, parse->escontext);
-
-		if (SOFT_ERROR_OCCURRED(parse->escontext))
-			return JSON_SEM_ACTION_FAILED;
-
-		return JSON_SUCCESS;
-	}
-
-	ereturn(parse->escontext, (Datum) 0,
-			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
-			 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
-			 errdetail("Unexpected scalar.")));
-	return JSON_SEM_ACTION_FAILED;
-}
-
 /*
  * Validate an MVDependencies against the extended statistics object definition.
  *
@@ -1071,166 +692,6 @@ pg_dependencies_validate_deps(MVDependencies *dependencies, int2vector *stxkeys,
 	return true;
 }
 
-/*
- * pg_dependencies_in		- input routine for type pg_dependencies.
- *
- * This format is valid JSON, with the expected format:
- *    [{"attributes": [1,2], "dependency": -1, "degree": 1.0000},
- *     {"attributes": [1,-1], "dependency": 2, "degree": 0.0000},
- *     {"attributes": [2,-1], "dependency": 1, "degree": 1.0000}]
- *
- */
-Datum
-pg_dependencies_in(PG_FUNCTION_ARGS)
-{
-	char	   *str = PG_GETARG_CSTRING(0);
-
-	dependenciesParseState parse_state;
-	JsonParseErrorType result;
-	JsonLexContext *lex;
-	JsonSemAction sem_action;
-
-	/* initialize the semantic state */
-	parse_state.str = str;
-	parse_state.state = DEPS_EXPECT_START;
-	parse_state.dependency_list = NIL;
-	parse_state.attnum_list = NIL;
-	parse_state.dependency = 0;
-	parse_state.degree = 0.0;
-	parse_state.found_attributes = false;
-	parse_state.found_dependency = false;
-	parse_state.found_degree = false;
-	parse_state.escontext = fcinfo->context;
-
-	/* set callbacks */
-	sem_action.semstate = (void *) &parse_state;
-	sem_action.object_start = dependencies_object_start;
-	sem_action.object_end = dependencies_object_end;
-	sem_action.array_start = dependencies_array_start;
-	sem_action.array_end = dependencies_array_end;
-	sem_action.array_element_start = dependencies_array_element_start;
-	sem_action.array_element_end = NULL;
-	sem_action.object_field_start = dependencies_object_field_start;
-	sem_action.object_field_end = NULL;
-	sem_action.scalar = dependencies_scalar;
-
-	lex = makeJsonLexContextCstringLen(NULL, str, strlen(str), PG_UTF8, true);
-
-	result = pg_parse_json(lex, &sem_action);
-	freeJsonLexContext(lex);
-
-	if (result == JSON_SUCCESS)
-	{
-		List	   *list = parse_state.dependency_list;
-		int			ndeps = list->length;
-		MVDependencies *mvdeps;
-		bytea	   *bytes;
-
-		mvdeps = palloc0(offsetof(MVDependencies, deps) + ndeps * sizeof(MVDependency));
-		mvdeps->magic = STATS_DEPS_MAGIC;
-		mvdeps->type = STATS_DEPS_TYPE_BASIC;
-		mvdeps->ndeps = ndeps;
-
-		/* copy MVDependency structs out of the list into the MVDependencies */
-		for (int i = 0; i < ndeps; i++)
-			mvdeps->deps[i] = list->elements[i].ptr_value;
-		bytes = statext_dependencies_serialize(mvdeps);
-
-		list_free(list);
-		for (int i = 0; i < ndeps; i++)
-			pfree(mvdeps->deps[i]);
-		pfree(mvdeps);
-
-		PG_RETURN_BYTEA_P(bytes);
-	}
-	else if (result == JSON_SEM_ACTION_FAILED)
-		PG_RETURN_NULL();
-
-	/* Anything else is a generic JSON parse error */
-	ereturn(parse_state.escontext, (Datum) 0,
-			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
-			 errmsg("malformed pg_dependencies: \"%s\"", str),
-			 errdetail("Must be valid JSON.")));
-
-	PG_RETURN_NULL();			/* keep compiler quiet */
-}
-
-/*
- * Free allocations of an MVNDistinct
- */
-void
-free_pg_dependencies(MVDependencies *dependencies)
-{
-	for (int i = 0; i < dependencies->ndeps; i++)
-		pfree(dependencies->deps[i]);
-
-	pfree(dependencies);
-}
-
-/*
- * pg_dependencies		- output routine for type pg_dependencies.
- */
-Datum
-pg_dependencies_out(PG_FUNCTION_ARGS)
-{
-	bytea	   *data = PG_GETARG_BYTEA_PP(0);
-	MVDependencies *dependencies = statext_dependencies_deserialize(data);
-	StringInfoData str;
-
-	initStringInfo(&str);
-	appendStringInfoChar(&str, '[');
-
-	for (int i = 0; i < dependencies->ndeps; i++)
-	{
-		MVDependency *dependency = dependencies->deps[i];
-
-		if (i > 0)
-			appendStringInfoString(&str, ", ");
-
-		if (dependency->nattributes <= 1)
-			elog(ERROR, "invalid zero-length nattributes array in MVDependencies");
-
-		appendStringInfo(&str, "{\"attributes\": [%d",
-						 dependency->attributes[0]);
-
-		for (int j = 1; j < dependency->nattributes - 1; j++)
-			appendStringInfo(&str, ", %d", dependency->attributes[j]);
-
-		appendStringInfo(&str, "], \"dependency\": %d, \"degree\": %f}",
-						 dependency->attributes[dependency->nattributes - 1],
-						 dependency->degree);
-	}
-
-	appendStringInfoChar(&str, ']');
-
-	PG_RETURN_CSTRING(str.data);
-}
-
-/*
- * pg_dependencies_recv		- binary input routine for type pg_dependencies.
- */
-Datum
-pg_dependencies_recv(PG_FUNCTION_ARGS)
-{
-	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot accept a value of type %s", "pg_dependencies")));
-
-	PG_RETURN_VOID();			/* keep compiler quiet */
-}
-
-/*
- * pg_dependencies_send		- binary output routine for type pg_dependencies.
- *
- * Functional dependencies are serialized in a bytea value (although the type
- * is named differently), so let's just send that.
- */
-Datum
-pg_dependencies_send(PG_FUNCTION_ARGS)
-{
-	return byteasend(fcinfo);
-}
-
 /*
  * dependency_is_compatible_clause
  *		Determines if the clause is compatible with functional dependencies
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 70ff8e45516..ba40ada11ca 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -80,6 +80,7 @@ OBJS = \
 	oracle_compat.o \
 	orderedsetaggs.o \
 	partitionfuncs.o \
+	pg_dependencies.o \
 	pg_locale.o \
 	pg_locale_builtin.o \
 	pg_locale_icu.o \
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index b6b642c77a0..9c4c62d41da 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -76,6 +76,7 @@ backend_sources += files(
   'oracle_compat.c',
   'orderedsetaggs.c',
   'partitionfuncs.c',
+  'pg_dependencies.c',
   'pg_locale.c',
   'pg_locale_builtin.c',
   'pg_locale_icu.c',
diff --git a/src/backend/utils/adt/pg_dependencies.c b/src/backend/utils/adt/pg_dependencies.c
new file mode 100644
index 00000000000..722c000eeef
--- /dev/null
+++ b/src/backend/utils/adt/pg_dependencies.c
@@ -0,0 +1,556 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_dependencies.c
+ *		pg_dependencies data type support.
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/pg_dependencies.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/attnum.h"
+#include "common/int.h"
+#include "common/jsonapi.h"
+#include "mb/pg_wchar.h"
+#include "nodes/miscnodes.h"
+#include "nodes/pg_list.h"
+#include "statistics/extended_stats_internal.h"
+#include "utils/builtins.h"
+#include "utils/float.h"
+
+/*
+ * The valid keys for the pg_dependencies object:
+ */
+static const char *attributes = "attributes";
+static const char *dependency = "dependency";
+static const char *degree = "degree";
+
+typedef enum
+{
+	DEPS_EXPECT_START = 0,
+	DEPS_EXPECT_ITEM,
+	DEPS_EXPECT_KEY,
+	DEPS_EXPECT_ATTNUM_LIST,
+	DEPS_EXPECT_ATTNUM,
+	DEPS_EXPECT_DEPENDENCY,
+	DEPS_EXPECT_DEGREE,
+	DEPS_PARSE_COMPLETE
+}			depsParseSemanticState;
+
+typedef struct
+{
+	const char *str;
+	depsParseSemanticState state;
+
+	List	   *dependency_list;
+	Node	   *escontext;
+
+	bool		found_attributes;	/* Item has an attributes key */
+	bool		found_dependency;	/* Item has an dependency key */
+	bool		found_degree;	/* Item has degree key */
+	List	   *attnum_list;	/* Accumulated attributes attnums */
+	AttrNumber	dependency;
+	double		degree;
+}			dependenciesParseState;
+
+/*
+ * Invoked at the start of each MVDependency object.
+ *
+ * The entire JSON document should be one array of MVDependency objects.
+ *
+ * If we're anywhere else in the document, it's an error.
+ */
+static JsonParseErrorType
+dependencies_object_start(void *state)
+{
+	dependenciesParseState *parse = state;
+
+	if (parse->state != DEPS_EXPECT_ITEM)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				 errdetail("Expected Item object")));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	/* Now we expect to see attributes/dependency/degree keys */
+	parse->state = DEPS_EXPECT_KEY;
+	return JSON_SUCCESS;
+}
+
+static int
+attnum_compare(const void *aptr, const void *bptr)
+{
+	AttrNumber	a = *(const AttrNumber *) aptr;
+	AttrNumber	b = *(const AttrNumber *) bptr;
+
+	return pg_cmp_s16(a, b);
+}
+
+static JsonParseErrorType
+dependencies_object_end(void *state)
+{
+	dependenciesParseState *parse = state;
+
+	MVDependency *dep;
+	AttrNumber *attrsort;
+
+	int			natts = 0;
+
+	if (!parse->found_attributes)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				 errdetail("Item must contain \"%s\" key", attributes)));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	if (!parse->found_dependency)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				 errdetail("Item must contain \"%s\" key", dependency)));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	if (!parse->found_degree)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				 errdetail("Item must contain \"%s\" key", degree)));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	if (parse->attnum_list == NIL)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				 errdetail("The \"%s\" key must be an non-empty array", attributes)));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	/*
+	 * We need at least 1 attnum for a dependencies item, anything less is
+	 * malformed.
+	 */
+	natts = parse->attnum_list->length;
+	if (natts < 1)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				 errdetail("The %s key must contain an array of at least one attnum", attributes)));
+
+		return JSON_SEM_ACTION_FAILED;
+	}
+	attrsort = palloc0(natts * sizeof(AttrNumber));
+
+	/*
+	 * Allocate enough space for the dependency, the attnums in the list, plus
+	 * the final attnum
+	 */
+	dep = palloc0(offsetof(MVDependency, attributes) + ((natts + 1) * sizeof(AttrNumber)));
+	dep->nattributes = natts + 1;
+
+	dep->attributes[natts] = parse->dependency;
+	dep->degree = parse->degree;
+
+	attrsort = palloc0(dep->nattributes * sizeof(AttrNumber));
+	attrsort[natts] = parse->dependency;
+
+	for (int i = 0; i < natts; i++)
+	{
+		attrsort[i] = (AttrNumber) parse->attnum_list->elements[i].int_value;
+		dep->attributes[i] = attrsort[i];
+	}
+
+	/* Check attrsort for uniqueness */
+	qsort(attrsort, natts + 1, sizeof(AttrNumber), attnum_compare);
+	for (int i = 1; i < dep->nattributes; i++)
+		if (attrsort[i] == attrsort[i - 1])
+		{
+			ereturn(parse->escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					 errdetail("attnum list duplicate value found: %d", attrsort[i])));
+
+			return JSON_SEM_ACTION_FAILED;
+		}
+	pfree(attrsort);
+
+	parse->dependency_list = lappend(parse->dependency_list, (void *) dep);
+
+	/* reset dep item state vars */
+	list_free(parse->attnum_list);
+	parse->attnum_list = NIL;
+	parse->dependency = 0;
+	parse->degree = 0.0;
+	parse->found_attributes = false;
+	parse->found_dependency = false;
+	parse->found_degree = false;
+
+	/* Now we are looking for the next MVDependency */
+	parse->state = DEPS_EXPECT_ITEM;
+	return JSON_SUCCESS;
+}
+
+/*
+ * dependencies input format does not have arrays, so any array elements encountered
+ * are an error.
+ */
+static JsonParseErrorType
+dependencies_array_start(void *state)
+{
+	dependenciesParseState *parse = state;
+
+	switch (parse->state)
+	{
+		case DEPS_EXPECT_ATTNUM_LIST:
+			parse->state = DEPS_EXPECT_ATTNUM;
+			break;
+		case DEPS_EXPECT_START:
+			parse->state = DEPS_EXPECT_ITEM;
+			break;
+		default:
+			ereturn(parse->escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					 errdetail("Array found in unexpected place")));
+			return JSON_SEM_ACTION_FAILED;
+	}
+
+	return JSON_SUCCESS;
+}
+
+/*
+ * Either the end of an attnum list or the whole object
+ */
+static JsonParseErrorType
+dependencies_array_end(void *state)
+{
+	dependenciesParseState *parse = state;
+
+	switch (parse->state)
+	{
+		case DEPS_EXPECT_ATTNUM:
+			parse->state = DEPS_EXPECT_KEY;
+			break;
+
+		case DEPS_EXPECT_ITEM:
+			parse->state = DEPS_PARSE_COMPLETE;
+			break;
+
+		default:
+			ereturn(parse->escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					 errdetail("Array found in unexpected place")));
+			return JSON_SEM_ACTION_FAILED;
+	}
+	return JSON_SUCCESS;
+}
+
+/*
+ *
+ */
+static JsonParseErrorType
+dependencies_object_field_start(void *state, char *fname, bool isnull)
+{
+	dependenciesParseState *parse = state;
+
+	if (strcmp(fname, attributes) == 0)
+	{
+		parse->found_attributes = true;
+		parse->state = DEPS_EXPECT_ATTNUM_LIST;
+		return JSON_SUCCESS;
+	}
+
+	if (strcmp(fname, dependency) == 0)
+	{
+		parse->found_dependency = true;
+		parse->state = DEPS_EXPECT_DEPENDENCY;
+		return JSON_SUCCESS;
+	}
+
+	if (strcmp(fname, degree) == 0)
+	{
+		parse->found_degree = true;
+		parse->state = DEPS_EXPECT_DEGREE;
+		return JSON_SUCCESS;
+	}
+
+	ereturn(parse->escontext, (Datum) 0,
+			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+			 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+			 errdetail("Only allowed keys are \%s\", \"%s\" and \%s\".",
+					   attributes, dependency, degree)));
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ * ndsitinct input format does not have arrays, so any array elements encountered
+ * are an error.
+ */
+static JsonParseErrorType
+dependencies_array_element_start(void *state, bool isnull)
+{
+	dependenciesParseState *parse = state;
+
+	if (parse->state == DEPS_EXPECT_ATTNUM)
+	{
+		if (isnull)
+		{
+			ereturn(parse->escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					 errdetail("Attnum list elements cannot be null.")));
+
+			return JSON_SEM_ACTION_FAILED;
+		}
+		return JSON_SUCCESS;
+	}
+
+	if (parse->state == DEPS_EXPECT_ITEM)
+	{
+		if (isnull)
+		{
+			ereturn(parse->escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					 errdetail("Item list elements cannot be null.")));
+
+			return JSON_SEM_ACTION_FAILED;
+		}
+
+		return JSON_SUCCESS;
+	}
+
+	ereturn(parse->escontext, (Datum) 0,
+			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+			 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+			 errdetail("Unexpected array element.")));
+
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ * Handle scalar events from the dependencies input parser.
+ *
+ * There is only one case where we will encounter a scalar, and that is the
+ * dependency degree for the previous object key.
+ */
+static JsonParseErrorType
+dependencies_scalar(void *state, char *token, JsonTokenType tokentype)
+{
+	dependenciesParseState *parse = state;
+
+	if (parse->state == DEPS_EXPECT_ATTNUM)
+	{
+		AttrNumber	attnum = pg_strtoint16_safe(token, parse->escontext);
+
+		if (SOFT_ERROR_OCCURRED(parse->escontext))
+			return JSON_SEM_ACTION_FAILED;
+
+		parse->attnum_list = lappend_int(parse->attnum_list, (int) attnum);
+		return JSON_SUCCESS;
+	}
+
+	if (parse->state == DEPS_EXPECT_DEPENDENCY)
+	{
+		parse->dependency = (AttrNumber) pg_strtoint16_safe(token, parse->escontext);
+
+		if (SOFT_ERROR_OCCURRED(parse->escontext))
+			return JSON_SEM_ACTION_FAILED;
+
+		return JSON_SUCCESS;
+	}
+
+
+	if (parse->state == DEPS_EXPECT_DEGREE)
+	{
+		parse->degree = float8in_internal(token, NULL, "double",
+										  token, parse->escontext);
+
+		if (SOFT_ERROR_OCCURRED(parse->escontext))
+			return JSON_SEM_ACTION_FAILED;
+
+		return JSON_SUCCESS;
+	}
+
+	ereturn(parse->escontext, (Datum) 0,
+			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+			 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+			 errdetail("Unexpected scalar.")));
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ * pg_dependencies_in		- input routine for type pg_dependencies.
+ *
+ * This format is valid JSON, with the expected format:
+ *    [{"attributes": [1,2], "dependency": -1, "degree": 1.0000},
+ *     {"attributes": [1,-1], "dependency": 2, "degree": 0.0000},
+ *     {"attributes": [2,-1], "dependency": 1, "degree": 1.0000}]
+ *
+ */
+Datum
+pg_dependencies_in(PG_FUNCTION_ARGS)
+{
+	char	   *str = PG_GETARG_CSTRING(0);
+
+	dependenciesParseState parse_state;
+	JsonParseErrorType result;
+	JsonLexContext *lex;
+	JsonSemAction sem_action;
+
+	/* initialize the semantic state */
+	parse_state.str = str;
+	parse_state.state = DEPS_EXPECT_START;
+	parse_state.dependency_list = NIL;
+	parse_state.attnum_list = NIL;
+	parse_state.dependency = 0;
+	parse_state.degree = 0.0;
+	parse_state.found_attributes = false;
+	parse_state.found_dependency = false;
+	parse_state.found_degree = false;
+	parse_state.escontext = fcinfo->context;
+
+	/* set callbacks */
+	sem_action.semstate = (void *) &parse_state;
+	sem_action.object_start = dependencies_object_start;
+	sem_action.object_end = dependencies_object_end;
+	sem_action.array_start = dependencies_array_start;
+	sem_action.array_end = dependencies_array_end;
+	sem_action.array_element_start = dependencies_array_element_start;
+	sem_action.array_element_end = NULL;
+	sem_action.object_field_start = dependencies_object_field_start;
+	sem_action.object_field_end = NULL;
+	sem_action.scalar = dependencies_scalar;
+
+	lex = makeJsonLexContextCstringLen(NULL, str, strlen(str), PG_UTF8, true);
+
+	result = pg_parse_json(lex, &sem_action);
+	freeJsonLexContext(lex);
+
+	if (result == JSON_SUCCESS)
+	{
+		List	   *list = parse_state.dependency_list;
+		int			ndeps = list->length;
+		MVDependencies *mvdeps;
+		bytea	   *bytes;
+
+		mvdeps = palloc0(offsetof(MVDependencies, deps) + ndeps * sizeof(MVDependency));
+		mvdeps->magic = STATS_DEPS_MAGIC;
+		mvdeps->type = STATS_DEPS_TYPE_BASIC;
+		mvdeps->ndeps = ndeps;
+
+		/* copy MVDependency structs out of the list into the MVDependencies */
+		for (int i = 0; i < ndeps; i++)
+			mvdeps->deps[i] = list->elements[i].ptr_value;
+		bytes = statext_dependencies_serialize(mvdeps);
+
+		list_free(list);
+		for (int i = 0; i < ndeps; i++)
+			pfree(mvdeps->deps[i]);
+		pfree(mvdeps);
+
+		PG_RETURN_BYTEA_P(bytes);
+	}
+	else if (result == JSON_SEM_ACTION_FAILED)
+		PG_RETURN_NULL();
+
+	/* Anything else is a generic JSON parse error */
+	ereturn(parse_state.escontext, (Datum) 0,
+			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+			 errmsg("malformed pg_dependencies: \"%s\"", str),
+			 errdetail("Must be valid JSON.")));
+
+	PG_RETURN_NULL();			/* keep compiler quiet */
+}
+
+/*
+ * Free allocations of an MVNDistinct
+ */
+void
+free_pg_dependencies(MVDependencies *dependencies)
+{
+	for (int i = 0; i < dependencies->ndeps; i++)
+		pfree(dependencies->deps[i]);
+
+	pfree(dependencies);
+}
+
+/*
+ * pg_dependencies		- output routine for type pg_dependencies.
+ */
+Datum
+pg_dependencies_out(PG_FUNCTION_ARGS)
+{
+	bytea	   *data = PG_GETARG_BYTEA_PP(0);
+	MVDependencies *dependencies = statext_dependencies_deserialize(data);
+	StringInfoData str;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '[');
+
+	for (int i = 0; i < dependencies->ndeps; i++)
+	{
+		MVDependency *dep = dependencies->deps[i];
+
+		if (i > 0)
+			appendStringInfoString(&str, ", ");
+
+		if (dep->nattributes <= 1)
+			elog(ERROR, "invalid zero-length nattributes array in MVDependencies");
+
+		appendStringInfo(&str, "{\"%s\": [%d",
+				   		 attributes,
+						 dep->attributes[0]);
+
+		for (int j = 1; j < dep->nattributes - 1; j++)
+			appendStringInfo(&str, ", %d", dep->attributes[j]);
+
+		appendStringInfo(&str, "], \"%s\": %d, \"%s\": %f}",
+						 dependency, dep->attributes[dep->nattributes - 1],
+						 degree, dep->degree);
+	}
+
+	appendStringInfoChar(&str, ']');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * pg_dependencies_recv		- binary input routine for type pg_dependencies.
+ */
+Datum
+pg_dependencies_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_dependencies")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * pg_dependencies_send		- binary output routine for type pg_dependencies.
+ *
+ * Functional dependencies are serialized in a bytea value (although the type
+ * is named differently), so let's just send that.
+ */
+Datum
+pg_dependencies_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
-- 
2.51.1

---

From: Michael Paquier <[email-masked]>
Date: Tue, 11 Nov 2025 17:06:49 +0900

> files, but that can be done if desired.

No, that's not the best thing we can do with the dump/restore pieces
in mind.  Let's put that in a separate header.

> patches will probably get squashed together anyway.

Thanks for the new patch.  And FWIW I disagree with this approach:
cleanup and refactoring pieces make more sense if done first, as these
lead to less code churn in the final result.  So...  I've begun to put
my hands on the patch set.  The whole has been restructured a bit, as
per the attached.  Patch 0001 to 0004 feel OK here, these include two
code moves and the two output functions:
- Two new files for adt/, that I'm planning to apply soon as a
separate cleanup.
- New output functions, with keys added to a new header named
statistics_format.h, for frontend and backend consumption.

Next comes the input functions.  First, I am unhappy with the amount
of testing that has been put into ndistinct, first and only input
facility I've looked at in details for the moment.  I have quickly
spotted a couple a few issues while testing buggy input, like this one
that crashes on pointer dereference, not good obviously:
SELECT '[]'::pg_ndistinct;

There was a second one with the error message generated when using an
incorrect key value.

Second, the inputs are too permissive and could be more strictly
checked IMHO.  For example, patterns like that are incorrect, still
authorized with only the patches up to 0005 in:
- Duplicated list of attributes:
SELECT '[{"attributes" : [2,3], "ndistinct" : 4},
         {"attributes" : [2,3], "ndistinct" : 4}]'::pg_ndistinct;
- Partial (K,N) sets, for example say we take stats on attrs (1,2,3),
a partial input like this one is basically OK:
SELECT '[{"attributes" : [1,3], "ndistinct" : 4},
         {"attributes" : [1,2,3], "ndistinct" : 4}]'::pg_ndistinct;

These are checked in the patches that introduce the functions like
with pg_ndistinct_validate_items(), based on the list of stxkeys we
have.  However, I think that this is not enough by itself.  Shouldn't
we check that the list of items in the array is what we expect based
on the longest "attributes" array at least, even after a JSON that was
parsed?  That would be cheap to check in the output function itself,
at least as a first layer of checks before trying something with the
import function and cross-checking the list of attributes for the
extended statistics object.  This means checking that for N attributes
we have all the elements we'd expect in each element of the array,
without gaps or duplications, with an extra step done once the JSON
parsing is finished.  Except for this sanity issue this part of the
patch set should be mostly OK, plus more cleanup and more typo/grammar
fixes. 

I suspect a similar family of issues with pg_dependencies, and it
would be nice to move the tests with the input function into a new
regression file, like the other one.

I've rebased the full set using the new structure.  0001~0004 are
clean.  0005~ need more work and analysis, but that's a start.
--
Michael
From 8b1f72fa74b3b6cb348a3a08ac7c67cb8e2e4c5f Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Tue, 11 Nov 2025 13:27:13 +0900
Subject: [PATCH v10 1/9] Make pg_ndinstinct a proper adt.

Move the in/out/send/recv functions for pg_ndistinct to pg_ndistinct.c,
which allows mvdistinct.c to focus on the transformation from sample
data to the internal MVDistinct structure.
---
 src/backend/statistics/mvdistinct.c  |  85 ----------------------
 src/backend/utils/adt/Makefile       |   1 +
 src/backend/utils/adt/meson.build    |   1 +
 src/backend/utils/adt/pg_ndistinct.c | 102 +++++++++++++++++++++++++++
 4 files changed, 104 insertions(+), 85 deletions(-)
 create mode 100644 src/backend/utils/adt/pg_ndistinct.c

diff --git a/src/backend/statistics/mvdistinct.c b/src/backend/statistics/mvdistinct.c
index 7e7a63405c8b..fe452f53ae4b 100644
--- a/src/backend/statistics/mvdistinct.c
+++ b/src/backend/statistics/mvdistinct.c
@@ -27,10 +27,7 @@
 
@@ -328,88 +325,6 @@ statext_ndistinct_deserialize(bytea *data)
 	return ndistinct;
 }
 
-/*
- * pg_ndistinct_in
index cc68ac545a5f..70ff8e455169 100644
index 12fa0c209127..b6b642c77a04 100644
index 000000000000..5ce655bce755
@@ -0,0 +1,102 @@
+#include "lib/stringinfo.h"
+#include "utils/fmgrprotos.h"
+ * pg_ndistinct is real enough to be a table column, but it has no
+ * operations of its own, and disallows input (just like pg_node_tree).
+ * Produces a human-readable representation of the value.
+	appendStringInfoChar(&str, '{');
+		int			j;
+		for (j = 0; j < item.nattributes; j++)
+			appendStringInfo(&str, "%s%d", (j == 0) ? "\"" : ", ", attnum);
+		appendStringInfo(&str, "\": %d", (int) item.ndistinct);
+	appendStringInfoChar(&str, '}');

From abaa5fa56717fe3cfc61d61b5b0160fd4b83c606 Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Tue, 11 Nov 2025 13:40:24 +0900
Subject: [PATCH v10 2/9] Make pg_dependencies a proper adt.

Move the in/out/send/recv functions for pg_dependencies to
pg_dependencies.c, which allows dependencies.c to focus on the
transformation from sample data to the internal MVDependencies
structure.

This will make a couple of future improvements related to the input and
output format of these functions more isolated.
---
 src/backend/statistics/dependencies.c   |  91 ---------------------
 src/backend/utils/adt/Makefile          |   1 +
 src/backend/utils/adt/meson.build       |   1 +
 src/backend/utils/adt/pg_dependencies.c | 104 ++++++++++++++++++++++++
 4 files changed, 106 insertions(+), 91 deletions(-)
 create mode 100644 src/backend/utils/adt/pg_dependencies.c

index eb2fc4366b4a..6f63b4f3ffbf 100644
@@ -16,23 +16,17 @@
-#include "nodes/nodes.h"
-#include "nodes/pathnodes.h"
-#include "varatt.h"
 
 /* size of the struct header fields (magic, type, ndeps) */
 #define SizeOfHeader		(3 * sizeof(uint32))
@@ -643,91 +637,6 @@ statext_dependencies_load(Oid mvoid, bool inh)
 	return result;
 }
 
-/*
- * pg_dependencies_in		- input routine for type pg_dependencies.
- *
- * pg_dependencies is real enough to be a table column, but it has no operations
- * of its own, and disallows input too
- */
-Datum
-pg_dependencies_in(PG_FUNCTION_ARGS)
-{
-	/*
-	 * pg_node_list stores the data in binary form and parsing text input is
-	 * not needed, so disallow this.
-	 */
-	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot accept a value of type %s", "pg_dependencies")));
-
-	PG_RETURN_VOID();			/* keep compiler quiet */
-}
-
-/*
- * pg_dependencies		- output routine for type pg_dependencies.
- */
-Datum
-pg_dependencies_out(PG_FUNCTION_ARGS)
-{
-	bytea	   *data = PG_GETARG_BYTEA_PP(0);
-	MVDependencies *dependencies = statext_dependencies_deserialize(data);
-	int			i,
-				j;
-	StringInfoData str;
-
-	initStringInfo(&str);
-	appendStringInfoChar(&str, '{');
-
-	for (i = 0; i < dependencies->ndeps; i++)
-	{
-		MVDependency *dependency = dependencies->deps[i];
-
-		if (i > 0)
-			appendStringInfoString(&str, ", ");
-
-		appendStringInfoChar(&str, '"');
-		for (j = 0; j < dependency->nattributes; j++)
-		{
-			if (j == dependency->nattributes - 1)
-				appendStringInfoString(&str, " => ");
-			else if (j > 0)
-				appendStringInfoString(&str, ", ");
-
-			appendStringInfo(&str, "%d", dependency->attributes[j]);
-		}
-		appendStringInfo(&str, "\": %f", dependency->degree);
-	}
-
-	appendStringInfoChar(&str, '}');
-
-	PG_RETURN_CSTRING(str.data);
-}
-
-/*
- * pg_dependencies_recv		- binary input routine for type pg_dependencies.
- */
-Datum
-pg_dependencies_recv(PG_FUNCTION_ARGS)
-{
-	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot accept a value of type %s", "pg_dependencies")));
-
-	PG_RETURN_VOID();			/* keep compiler quiet */
-}
-
-/*
- * pg_dependencies_send		- binary output routine for type pg_dependencies.
- *
- * Functional dependencies are serialized in a bytea value (although the type
- * is named differently), so let's just send that.
- */
-Datum
-pg_dependencies_send(PG_FUNCTION_ARGS)
-{
-	return byteasend(fcinfo);
-}
-
 /*
  * dependency_is_compatible_clause
  *		Determines if the clause is compatible with functional dependencies
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 70ff8e455169..ba40ada11caf 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -80,6 +80,7 @@ OBJS = \
 	oracle_compat.o \
 	orderedsetaggs.o \
 	partitionfuncs.o \
+	pg_dependencies.o \
 	pg_locale.o \
 	pg_locale_builtin.o \
 	pg_locale_icu.o \
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index b6b642c77a04..9c4c62d41da1 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -76,6 +76,7 @@ backend_sources += files(
   'oracle_compat.c',
   'orderedsetaggs.c',
   'partitionfuncs.c',
+  'pg_dependencies.c',
   'pg_locale.c',
   'pg_locale_builtin.c',
   'pg_locale_icu.c',
diff --git a/src/backend/utils/adt/pg_dependencies.c b/src/backend/utils/adt/pg_dependencies.c
new file mode 100644
index 000000000000..a4f7c48683f5
--- /dev/null
+++ b/src/backend/utils/adt/pg_dependencies.c
@@ -0,0 +1,104 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_dependencies.c
+ *		pg_dependencies data type support.
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/pg_dependencies.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "lib/stringinfo.h"
+#include "statistics/extended_stats_internal.h"
+#include "utils/fmgrprotos.h"
+
+/*
+ * pg_dependencies_in		- input routine for type pg_dependencies.
+ *
+ * pg_dependencies is real enough to be a table column, but it has no operations
+ * of its own, and disallows input too
+ */
+Datum
+pg_dependencies_in(PG_FUNCTION_ARGS)
+{
+	/*
+	 * pg_node_list stores the data in binary form and parsing text input is
+	 * not needed, so disallow this.
+	 */
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_dependencies")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * pg_dependencies		- output routine for type pg_dependencies.
+ */
+Datum
+pg_dependencies_out(PG_FUNCTION_ARGS)
+{
+	bytea	   *data = PG_GETARG_BYTEA_PP(0);
+	MVDependencies *dependencies = statext_dependencies_deserialize(data);
+	int			i,
+				j;
+	StringInfoData str;
+
+	initStringInfo(&str);
+	appendStringInfoChar(&str, '{');
+
+	for (i = 0; i < dependencies->ndeps; i++)
+	{
+		MVDependency *dependency = dependencies->deps[i];
+
+		if (i > 0)
+			appendStringInfoString(&str, ", ");
+
+		appendStringInfoChar(&str, '"');
+		for (j = 0; j < dependency->nattributes; j++)
+		{
+			if (j == dependency->nattributes - 1)
+				appendStringInfoString(&str, " => ");
+			else if (j > 0)
+				appendStringInfoString(&str, ", ");
+
+			appendStringInfo(&str, "%d", dependency->attributes[j]);
+		}
+		appendStringInfo(&str, "\": %f", dependency->degree);
+	}
+
+	appendStringInfoChar(&str, '}');
+
+	PG_RETURN_CSTRING(str.data);
+}
+
+/*
+ * pg_dependencies_recv		- binary input routine for type pg_dependencies.
+ */
+Datum
+pg_dependencies_recv(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s", "pg_dependencies")));
+
+	PG_RETURN_VOID();			/* keep compiler quiet */
+}
+
+/*
+ * pg_dependencies_send		- binary output routine for type pg_dependencies.
+ *
+ * Functional dependencies are serialized in a bytea value (although the type
+ * is named differently), so let's just send that.
+ */
+Datum
+pg_dependencies_send(PG_FUNCTION_ARGS)
+{
+	return byteasend(fcinfo);
+}
-- 
2.51.0

From bde66013bb055834c578c43620a8a511354ebbd4 Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Tue, 11 Nov 2025 14:05:49 +0900
Subject: [PATCH v10 3/9] Refactor output format of pg_ndistinct.

The change in format is described from the changes to
src/test/regress/expected/stats_ext.out.
---
 src/include/statistics/statistics_format.h |  30 ++++
 src/backend/utils/adt/pg_ndistinct.c       |  22 +--
 src/test/regress/expected/stats_ext.out    | 156 ++++++++++++++++++---
 src/test/regress/sql/stats_ext.sql         |  12 +-
 doc/src/sgml/perform.sgml                  |  34 ++++-
 5 files changed, 219 insertions(+), 35 deletions(-)
 create mode 100644 src/include/statistics/statistics_format.h

diff --git a/src/include/statistics/statistics_format.h b/src/include/statistics/statistics_format.h
new file mode 100644
index 000000000000..ba97c0880be1
--- /dev/null
+++ b/src/include/statistics/statistics_format.h
@@ -0,0 +1,30 @@
+/*-------------------------------------------------------------------------
+ *
+ * statistics_format.h
+ *	  Data related to the format of extended statistics, usable by both
+ *	  frontend and backend code.
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/statistics/statistics_format.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef STATISTICS_FORMAT_H
+#define STATISTICS_FORMAT_H
+
+/* ----------
+ * pg_ndistinct in human-readable format is a JSON array made of elements with
+ * a predefined set of keys, like:
+ *
+ * [{"ndistinct": 11, "attributes": [3,4]},
+ *  {"ndistinct": 11, "attributes": [3,6]},
+ *  {"ndistinct": 11, "attributes": [4,6]},
+ *  {"ndistinct": 11, "attributes": [3,4,6]}]
+ * ----------
+ */
+#define PG_NDISTINCT_KEY_ATTRIBUTES	"attributes"
+#define PG_NDISTINCT_KEY_NDISTINCT	"ndistinct"
+
+#endif							/* STATISTICS_FORMAT_H */
diff --git a/src/backend/utils/adt/pg_ndistinct.c b/src/backend/utils/adt/pg_ndistinct.c
index 5ce655bce755..56b17758ea5b 100644
--- a/src/backend/utils/adt/pg_ndistinct.c
+++ b/src/backend/utils/adt/pg_ndistinct.c
@@ -16,6 +16,7 @@
 
 #include "lib/stringinfo.h"
 #include "statistics/extended_stats_internal.h"
+#include "statistics/statistics_format.h"
 #include "utils/fmgrprotos.h"

@@ -51,26 +52,29 @@ pg_ndistinct_out(PG_FUNCTION_ARGS)
 	StringInfoData str;
 
 	initStringInfo(&str);

 		if (i > 0)

-			appendStringInfo(&str, "%s%d", (j == 0) ? "\"" : ", ", attnum);
-		}
-		appendStringInfo(&str, "\": %d", (int) item.ndistinct);
+		appendStringInfo(&str, "{\"" PG_NDISTINCT_KEY_ATTRIBUTES "\": [%d",
+						 item.attributes[0]);
+
+		for (int j = 1; j < item.nattributes; j++)
+			appendStringInfo(&str, ", %d", item.attributes[j]);
+
+		appendStringInfo(&str, "], \"" PG_NDISTINCT_KEY_NDISTINCT "\": %d}",
+						 (int) item.ndistinct);
 	}

 
 ANALYZE ab1;

 -- correct estimates
 
 DROP STATISTICS s10;

 

 ANALYZE ndistinct;

 ANALYZE ndistinct;

 DROP STATISTICS s10;

 ANALYZE ndistinct;

 ANALYZE ndistinct;

From a13f1574a98dab7abd853fde521a83afd06df02a Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Tue, 11 Nov 2025 14:15:27 +0900
Subject: [PATCH v10 4/9] Refactor output format of pg_dependencies.

The change in format is adequately described from the changes to
src/test/regress/expected/stats_ext.out so description here is
redundant.
---
 src/include/statistics/statistics_format.h | 20 ++++-
 src/backend/utils/adt/pg_dependencies.c    | 31 +++----
 src/test/regress/expected/stats_ext.out    | 95 ++++++++++++++++++++--
 src/test/regress/sql/stats_ext.sql         |  7 +-
 4 files changed, 124 insertions(+), 29 deletions(-)

diff --git a/src/include/statistics/statistics_format.h b/src/include/statistics/statistics_format.h
index ba97c0880be1..40655b9ec3b1 100644
--- a/src/include/statistics/statistics_format.h
+++ b/src/include/statistics/statistics_format.h
@@ -20,11 +20,27 @@
  *
  * [{"ndistinct": 11, "attributes": [3,4]},
  *  {"ndistinct": 11, "attributes": [3,6]},
- *  {"ndistinct": 11, "attributes": [4,6]},
- *  {"ndistinct": 11, "attributes": [3,4,6]}]
+ *  ... ]
+ *
  * ----------
  */
 #define PG_NDISTINCT_KEY_ATTRIBUTES	"attributes"
 #define PG_NDISTINCT_KEY_NDISTINCT	"ndistinct"
 
+
+/* ----------
+ * pg_dependencies in human-readable format is a JSON array made of elements
+ * with a predefined set of keys, like:
+ *
+ * [{"degree": 1.000000, "attributes": [3], "dependency": 4},
+ *  {"degree": 1.000000, "attributes": [3], "dependency": 6},
+ *  ... ]
+ *
+ * ----------
+ */
+
+#define PG_DEPENDENCIES_KEY_ATTRIBUTES	"attributes"
+#define PG_DEPENDENCIES_KEY_DEPENDENCY	"dependency"
+#define PG_DEPENDENCIES_KEY_DEGREE		"degree"
+
 #endif							/* STATISTICS_FORMAT_H */
diff --git a/src/backend/utils/adt/pg_dependencies.c b/src/backend/utils/adt/pg_dependencies.c
index a4f7c48683f5..05d0102c09cc 100644
--- a/src/backend/utils/adt/pg_dependencies.c
+++ b/src/backend/utils/adt/pg_dependencies.c
@@ -16,6 +16,7 @@
 
 #include "lib/stringinfo.h"
 #include "statistics/extended_stats_internal.h"
+#include "statistics/statistics_format.h"
 #include "utils/fmgrprotos.h"
 
 /*
@@ -46,34 +47,34 @@ pg_dependencies_out(PG_FUNCTION_ARGS)
 {
 	bytea	   *data = PG_GETARG_BYTEA_PP(0);
 	MVDependencies *dependencies = statext_dependencies_deserialize(data);
-	int			i,
-				j;
 	StringInfoData str;
 
 	initStringInfo(&str);

 		if (i > 0)

-			appendStringInfo(&str, "%d", dependency->attributes[j]);
-		}
-		appendStringInfo(&str, "\": %f", dependency->degree);
+		appendStringInfo(&str, "{\"" PG_DEPENDENCIES_KEY_ATTRIBUTES "\": [%d",
+						 dependency->attributes[0]);
+
+		for (int j = 1; j < dependency->nattributes - 1; j++)
+			appendStringInfo(&str, ", %d", dependency->attributes[j]);
+
+		appendStringInfo(&str, "], \"" PG_DEPENDENCIES_KEY_DEPENDENCY "\": %d, "
+						 "\"" PG_DEPENDENCIES_KEY_DEGREE "\": %f}",
+						 dependency->attributes[dependency->nattributes - 1],
+						 dependency->degree);
 	}

index 2dc771369e52..3bf55353fe8e 100644
 
 ANALYZE ab1;

index 207c431e68c9..a47cfc80eef2 100644

 

 
-- 
2.51.0

From 7a9880e695696a7e82f927a2b17cba89088aa6f0 Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Tue, 11 Nov 2025 16:47:00 +0900
Subject: [PATCH v10 5/9] Add working input function for pg_ndistinct.

This will be needed for importing extended statistics.
---
 src/backend/utils/adt/pg_ndistinct.c       | 446 ++++++++++++++++++++-
 src/test/regress/expected/pg_ndistinct.out |  96 +++++
 src/test/regress/parallel_schedule         |   2 +-
 src/test/regress/sql/pg_ndistinct.sql      |  32 ++
 4 files changed, 568 insertions(+), 8 deletions(-)
 create mode 100644 src/test/regress/expected/pg_ndistinct.out
 create mode 100644 src/test/regress/sql/pg_ndistinct.sql

diff --git a/src/backend/utils/adt/pg_ndistinct.c b/src/backend/utils/adt/pg_ndistinct.c
index 56b17758ea5b..57f323c61555 100644
--- a/src/backend/utils/adt/pg_ndistinct.c
+++ b/src/backend/utils/adt/pg_ndistinct.c
@@ -14,34 +14,466 @@
 
 #include "postgres.h"
 
+#include "common/int.h"
 #include "statistics/statistics_format.h"
 
+typedef enum
+{
+} NDistinctSemanticState;
+	NDistinctSemanticState state;
+} NDistinctParseState;
+	NDistinctParseState *parse = state;
+	NDistinctParseState *parse = state;
+	NDistinctParseState *parse = state;
+	NDistinctParseState *parse = state;
+	NDistinctParseState *parse = state;
+			 errdetail("Only allowed keys are \"%s\" and \"%s\".", attributes, ndistinct)));
+	NDistinctParseState *parse = state;
+	NDistinctParseState *parse = state;
 
 /*
  * pg_ndistinct_in
 
+	NDistinctParseState parse_state;
+	if (result == JSON_SUCCESS &&
+		parse_state.distinct_items != NIL)
+			 * Free the MVNDistinctItem, but not the attributes we're still
+			 * using.
 
 /*
  * pg_ndistinct
  *		output routine for type pg_ndistinct
  *
- * Produces a human-readable representation of the value.
+ * Produces a human-readable representation of the value, in the format:
+ *   [{"attributes": [attnum,. ..], "ndistinct": int}, ...]
+ *
  */
 Datum
 pg_ndistinct_out(PG_FUNCTION_ARGS)
diff --git a/src/test/regress/expected/pg_ndistinct.out b/src/test/regress/expected/pg_ndistinct.out
new file mode 100644
index 000000000000..9dde98201f28
--- /dev/null
+++ b/src/test/regress/expected/pg_ndistinct.out
@@ -0,0 +1,96 @@
+-- Tests for type pg_distinct
+-- Invalid inputs
+SELECT '[]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[]"
+LINE 1: SELECT '[]'::pg_ndistinct;
+               ^
+DETAIL:  Must be valid JSON.
+SELECT '[null]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[null]"
+LINE 1: SELECT '[null]'::pg_ndistinct;
+               ^
+DETAIL:  Item list elements cannot be null.
+-- Invalid keys
+SELECT '[{"attributes_invalid" : [2,3], "ndistinct" : 4}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes_invalid" : [2,3], "ndistinct" : 4}]"
+LINE 1: SELECT '[{"attributes_invalid" : [2,3], "ndistinct" : 4}]'::...
+               ^
+DETAIL:  Only allowed keys are "attributes" and "ndistinct".
+SELECT '[{"attributes" : [2,3], "invalid" : 3, "ndistinct" : 4}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : [2,3], "invalid" : 3, "ndistinct" : 4}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "invalid" : 3, "ndistinct" :...
+               ^
+DETAIL:  Only allowed keys are "attributes" and "ndistinct".
+-- Valid keys, invalid values
+SELECT '[{"attributes" : null, "ndistinct" : 4}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : null, "ndistinct" : 4}]"
+LINE 1: SELECT '[{"attributes" : null, "ndistinct" : 4}]'::pg_ndisti...
+               ^
+DETAIL:  Unexpected scalar.
+SELECT '[{"attributes" : [2,null], "ndistinct" : 4}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : [2,null], "ndistinct" : 4}]"
+LINE 1: SELECT '[{"attributes" : [2,null], "ndistinct" : 4}]'::pg_nd...
+               ^
+DETAIL:  Attnum list elements cannot be null.
+SELECT '[{"attributes" : [2,3], "ndistinct" : null}]'::pg_ndistinct;
+ERROR:  invalid input syntax for type bigint: "null"
+LINE 1: SELECT '[{"attributes" : [2,3], "ndistinct" : null}]'::pg_nd...
+               ^
+SELECT '[{"attributes" : [2,"a"], "ndistinct" : 4}]'::pg_ndistinct;
+ERROR:  invalid input syntax for type smallint: "a"
+LINE 1: SELECT '[{"attributes" : [2,"a"], "ndistinct" : 4}]'::pg_ndi...
+               ^
+SELECT '[{"attributes" : [2,3], "ndistinct" : "a"}]'::pg_ndistinct;
+ERROR:  invalid input syntax for type bigint: "a"
+LINE 1: SELECT '[{"attributes" : [2,3], "ndistinct" : "a"}]'::pg_ndi...
+               ^
+SELECT '[{"attributes" : [2,3], "ndistinct" : []}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : []}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "ndistinct" : []}]'::pg_ndis...
+               ^
+DETAIL:  Array found in unexpected place
+SELECT '[{"attributes" : [2,3], "ndistinct" : [null]}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : [null]}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "ndistinct" : [null]}]'::pg_...
+               ^
+DETAIL:  Array found in unexpected place
+SELECT '[{"attributes" : [2,3], "ndistinct" : [1,null]}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : [1,null]}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "ndistinct" : [1,null]}]'::p...
+               ^
+DETAIL:  Array found in unexpected place
+SELECT '[{"attributes" : 1, "ndistinct" : 4}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : 1, "ndistinct" : 4}]"
+LINE 1: SELECT '[{"attributes" : 1, "ndistinct" : 4}]'::pg_ndistinct...
+               ^
+DETAIL:  Unexpected scalar.
+SELECT '[{"attributes" : "a", "ndistinct" : 4}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : "a", "ndistinct" : 4}]"
+LINE 1: SELECT '[{"attributes" : "a", "ndistinct" : 4}]'::pg_ndistin...
+               ^
+DETAIL:  Unexpected scalar.
+-- Duplicated attributes
+SELECT '[{"attributes" : [2,2], "ndistinct" : 4}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : [2,2], "ndistinct" : 4}]"
+LINE 1: SELECT '[{"attributes" : [2,2], "ndistinct" : 4}]'::pg_ndist...
+               ^
+DETAIL:  attnum list duplicate value found: 2
+-- Valid inputs
+-- Duplicated attribute lists.
+SELECT '[{"attributes" : [2,3], "ndistinct" : 4},
+         {"attributes" : [2,3], "ndistinct" : 4}]'::pg_ndistinct;
+                                   pg_ndistinct                                   
+----------------------------------------------------------------------------------
+ [{"attributes": [2, 3], "ndistinct": 4}, {"attributes": [2, 3], "ndistinct": 4}]
+(1 row)
+
+-- Partially-covered attribute lists.
+SELECT '[{"attributes" : [2,3], "ndistinct" : 4},
+         {"attributes" : [2,-1], "ndistinct" : 4},
+         {"attributes" : [2,3,-1], "ndistinct" : 4},
+         {"attributes" : [1,3,-1,-2], "ndistinct" : 4}]'::pg_ndistinct;
+                                                                                 pg_ndistinct                                                                                  
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ [{"attributes": [2, 3], "ndistinct": 4}, {"attributes": [2, -1], "ndistinct": 4}, {"attributes": [2, 3, -1], "ndistinct": 4}, {"attributes": [1, 3, -1, -2], "ndistinct": 4}]
+(1 row)
+
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index f56482fb9f12..f3f0b5f2f317 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,7 @@ test: strings md5 numerology point lseg line box path polygon circle date time t
 # geometry depends on point, lseg, line, box, path, polygon, circle
 # horology depends on date, time, timetz, timestamp, timestamptz, interval
 # ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import pg_ndistinct
 
 # ----------
 # Load huge amounts of data
diff --git a/src/test/regress/sql/pg_ndistinct.sql b/src/test/regress/sql/pg_ndistinct.sql
new file mode 100644
index 000000000000..49b513755323
--- /dev/null
+++ b/src/test/regress/sql/pg_ndistinct.sql
@@ -0,0 +1,32 @@
+-- Tests for type pg_distinct
+
+-- Invalid inputs
+SELECT '[]'::pg_ndistinct;
+SELECT '[null]'::pg_ndistinct;
+-- Invalid keys
+SELECT '[{"attributes_invalid" : [2,3], "ndistinct" : 4}]'::pg_ndistinct;
+SELECT '[{"attributes" : [2,3], "invalid" : 3, "ndistinct" : 4}]'::pg_ndistinct;
+-- Valid keys, invalid values
+SELECT '[{"attributes" : null, "ndistinct" : 4}]'::pg_ndistinct;
+SELECT '[{"attributes" : [2,null], "ndistinct" : 4}]'::pg_ndistinct;
+SELECT '[{"attributes" : [2,3], "ndistinct" : null}]'::pg_ndistinct;
+SELECT '[{"attributes" : [2,"a"], "ndistinct" : 4}]'::pg_ndistinct;
+SELECT '[{"attributes" : [2,3], "ndistinct" : "a"}]'::pg_ndistinct;
+SELECT '[{"attributes" : [2,3], "ndistinct" : []}]'::pg_ndistinct;
+SELECT '[{"attributes" : [2,3], "ndistinct" : [null]}]'::pg_ndistinct;
+SELECT '[{"attributes" : [2,3], "ndistinct" : [1,null]}]'::pg_ndistinct;
+SELECT '[{"attributes" : 1, "ndistinct" : 4}]'::pg_ndistinct;
+SELECT '[{"attributes" : "a", "ndistinct" : 4}]'::pg_ndistinct;
+-- Duplicated attributes
+SELECT '[{"attributes" : [2,2], "ndistinct" : 4}]'::pg_ndistinct;
+
+-- Valid inputs
+-- Duplicated attribute lists.
+SELECT '[{"attributes" : [2,3], "ndistinct" : 4},
+         {"attributes" : [2,3], "ndistinct" : 4}]'::pg_ndistinct;
+-- Partially-covered attribute lists.
+SELECT '[{"attributes" : [2,3], "ndistinct" : 4},
+         {"attributes" : [2,-1], "ndistinct" : 4},
+         {"attributes" : [2,3,-1], "ndistinct" : 4},
+         {"attributes" : [1,3,-1,-2], "ndistinct" : 4}]'::pg_ndistinct;
+
-- 
2.51.0

From 4d3db06dffce75ae2ad70b91f95e89b07e840e23 Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Tue, 11 Nov 2025 16:55:47 +0900
Subject: [PATCH v10 6/9] Add working input function for pg_dependencies.

This will be needed for importing extended statistics.
---
 src/backend/utils/adt/pg_dependencies.c | 461 +++++++++++++++++++++++-
 src/test/regress/expected/stats_ext.out |  22 ++
 src/test/regress/sql/stats_ext.sql      |  12 +
 3 files changed, 485 insertions(+), 10 deletions(-)

diff --git a/src/backend/utils/adt/pg_dependencies.c b/src/backend/utils/adt/pg_dependencies.c
index 05d0102c09cc..c05ad2dfc3af 100644
--- a/src/backend/utils/adt/pg_dependencies.c
+++ b/src/backend/utils/adt/pg_dependencies.c
@@ -14,29 +14,470 @@
 
 #include "postgres.h"
 
+#include "common/int.h"
 #include "statistics/statistics_format.h"
 
+typedef enum
+{
+	DEPS_EXPECT_START = 0,
+	DEPS_EXPECT_ITEM,
+	DEPS_EXPECT_KEY,

 /*
diff --git a/src/test/regress/expected/stats_ext.out b/src/test/regress/expected/stats_ext.out
index 3bf55353fe8e..ceaccd4c4da0 100644
--- a/src/test/regress/expected/stats_ext.out
+++ b/src/test/regress/expected/stats_ext.out
@@ -3766,6 +3766,28 @@ SELECT * FROM check_estimated_rows('SELECT * FROM sb_2 WHERE extstat_small(y)');
        196 |    196
 (1 row)
 
index a47cfc80eef2..3ce07212bbf5 100644
@@ -1832,6 +1832,18 @@ ANALYZE sb_2;

From 007b33d0caf64b04ca841336e94d42fe43602379 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 4 Nov 2025 23:50:01 -0500
Subject: [PATCH v10 7/9] Expose attribute statistics functions for use in
 extended_stats.

 
 
+/*
 
+/*

 /*

 
 
 		if (converted)
 		{
 
 		if (converted)
 		{

 
 		if (converted)
 		{

 	/*
 
 		if (converted)
 		{
 
 		if (converted)
 		{

From 0a653b336d06124fb3280252f81876986f0ddb38 Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Tue, 11 Nov 2025 16:58:26 +0900
Subject: [PATCH v10 8/9] Add extended statistics support functions.

index 5cf9e12fcb9a..84e9f176d192 100644

index 6f63b4f3ffbf..31a9f1cfc7c9 100644
@@ -1065,6 +1065,55 @@ clauselist_apply_dependencies(PlannerInfo *root, List *clauses,
 	return s1;
 }
 
+/*
  * dependency_is_compatible_expression
  *		Determines if the expression is compatible with functional dependencies
@@ -1248,6 +1297,18 @@ dependency_is_compatible_expression(Node *clause, Index relid, List *statlist, N
 	return false;
 }
 
+/*
  * dependencies_clauselist_selectivity
  *		Return the estimated selectivity of (a subset of) the given clauses

+/*

 
 /*

 
 	return result;
 }
+
+static HeapTuple
 
 	return s;
 }
+
+/*
index fe452f53ae4b..839bcc9af929 100644
@@ -599,6 +599,68 @@ generate_combinations_recurse(CombinationGenerator *state,
 	}
 }
 
+/*
  * generate_combinations
  *		generate all k-combinations of N elements
 
 DROP TABLE stats_temp;

 

From 34b6442534be0d77c4e30952419cfbf94b67d4d2 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Wed, 5 Nov 2025 01:13:04 -0500
Subject: [PATCH v10 9/9] Include Extended Statistics in pg_dump.

 

 	PQclear(res);
 
+/*
 
+	#

---

From: jian he <[email-masked]>
Date: Tue, 11 Nov 2025 23:14:34 +0800

hi.

If you not specify as
 SELECT stxkeys AS k, stxdndistinct AS nd
   FROM pg_statistic_ext join pg_statistic_ext_data on (oid = stxoid)
   WHERE stxname = 'stts2' \gx
then it won't look like ``-[ RECORD 1 ]``.

I tried the dummy data, the above query, the nd column data look like

[{"attributes": [1, 2], "ndistinct": 2} .....
which does not look like the above.

So I guess the query in doc/src/sgml/perform.sgml would be:

SELECT stxkeys AS k, jsonb_pretty(d.stxdndistinct::text::jsonb) AS nd
FROM pg_statistic_ext join pg_statistic_ext_data d on (oid = d.stxoid)
WHERE stxname = 'stts2' \gx

per related thread, the word "join" needs uppercase?

--
jian
https://www.enterprisedb.com/

---

From: jian he <[email-masked]>
Date: Wed, 12 Nov 2025 10:51:51 +0800

hi.

+Datum

I’m a bit confused about the part above,
item.ndistinct is double type, we just cast it to int type?

after apply 0004, the below in doc/src/sgml/perform.sgml also need to change?

<programlisting>
CREATE STATISTICS stts (dependencies) ON city, zip FROM zipcodes;

ANALYZE zipcodes;

SELECT stxname, stxkeys, stxddependencies
  FROM pg_statistic_ext join pg_statistic_ext_data on (oid = stxoid)
  WHERE stxname = 'stts';
 stxname | stxkeys |             stxddependencies
---------+---------+------------------------------------------
 stts    | 1 5     | {"1 => 5": 1.000000, "5 => 1": 0.423130}
(1 row)
</programlisting>

Do you think it's worth the trouble to have two separate
appendStringInfoChar for ``{}``?

for example in loop ``for (i = 0; i < ndist->nitems; i++)``. we can change to:
        appendStringInfoChar(&str, '{');
        appendStringInfo(&str, "\"" PG_NDISTINCT_KEY_ATTRIBUTES "\": [%d",
                         item.attributes[0]);

        for (int j = 1; j < item.nattributes; j++)
            appendStringInfo(&str, ", %d", item.attributes[j]);

        appendStringInfo(&str, "], \"" PG_NDISTINCT_KEY_NDISTINCT "\": %d",
                         (int) item.ndistinct);
        appendStringInfoChar(&str, '}');

---

From: Corey Huinker <[email-masked]>
Date: Wed, 12 Nov 2025 01:47:33 -0500

> statistics_format.h, for frontend and backend consumption.

Agreed, 0001-0004 all look good.

> SELECT '[]'::pg_ndistinct;

- I put some work into more specific error messages for invalid values for
both pg_ndistinct and pg_dependencies.
- The check for empty attribute lists and item lists now occur in the
array-end event handler.
- Also tried to standardize conventions between the two data types (switch
statements, similar utility functions, etc).

> extended statistics object.

I added tests for both duplicate attribute sequences as well as making the
first-longest attribute sequence the template by which all later and
shorter sequences are checked. I had been reluctant to add checks like
this, because so many similar validations were removed from the earlier
statistics code like histograms and the like.

> regression file, like the other one.

Did so.

0001-0004,0007,0009 unchanged.

Significant modification of the stats_import.sql regression tests in 0008
to conform to stricter datatype rules enacted in 0005, 0006.
From a817dfb5ed0e457e1ccf3a9129f6eacd78ec5133 Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Tue, 11 Nov 2025 13:27:13 +0900
Subject: [PATCH v11 1/9] Make pg_ndinstinct a proper adt.

diff --git a/src/backend/statistics/mvdistinct.c b/src/backend/statistics/mvdistinct.c
index 7e7a63405c8..fe452f53ae4 100644
--- a/src/backend/statistics/mvdistinct.c
+++ b/src/backend/statistics/mvdistinct.c
@@ -27,10 +27,7 @@

-/*
- * pg_ndistinct_in
index 00000000000..5ce655bce75

base-commit: 6956bca515e2a07a16c6d68b208766e97b275ddc
-- 
2.51.1

From dc707fc166b064e1debefeb21e4c57ddf9011152 Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Tue, 11 Nov 2025 13:40:24 +0900
Subject: [PATCH v11 2/9] Make pg_dependencies a proper adt.

index eb2fc4366b4..6f63b4f3ffb 100644

-/*
index 00000000000..a4f7c48683f

From 8b08131119ed8ca50dcdb8ac84bd7d8216963b2c Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Tue, 11 Nov 2025 14:05:49 +0900
Subject: [PATCH v11 3/9] Refactor output format of pg_ndistinct.

index 00000000000..ba97c0880be
index 5ce655bce75..56b17758ea5 100644

 
 	initStringInfo(&str);

 		if (i > 0)

 
 
index 495a1b35018..e9379afe39e 100644
 
 ANALYZE ab1;

 -- correct estimates
 
 DROP STATISTICS s10;

 
 
index fc6f152a072..fc4aee6d839 100644
 
 ANALYZE ndistinct;

 ANALYZE ndistinct;

 DROP STATISTICS s10;

 ANALYZE ndistinct;

 ANALYZE ndistinct;

From ece22b0205044bb4806d888882daa6d9aae35b8a Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Tue, 11 Nov 2025 14:15:27 +0900
Subject: [PATCH v11 4/9] Refactor output format of pg_dependencies.

index ba97c0880be..40655b9ec3b 100644
 
+
+/* ----------
index a4f7c48683f..05d0102c09c 100644

 /*
 
 	initStringInfo(&str);

 		if (i > 0)

 
 
index e9379afe39e..5a4077f8ed5 100644
 
 ANALYZE ab1;

index fc4aee6d839..94e2139c504 100644

 

 
-- 
2.51.1

From 20210adfe9b9e78a5e44c120bc317737fb7fa783 Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Tue, 11 Nov 2025 16:47:00 +0900
Subject: [PATCH v11 5/9] Add working input function for pg_ndistinct.

This will be needed for importing extended statistics.
---
 src/backend/utils/adt/pg_ndistinct.c       | 595 ++++++++++++++++++++-
 src/test/regress/expected/pg_ndistinct.out | 109 ++++
 src/test/regress/parallel_schedule         |   2 +-
 src/test/regress/sql/pg_ndistinct.sql      |  34 ++
 4 files changed, 732 insertions(+), 8 deletions(-)
 create mode 100644 src/test/regress/expected/pg_ndistinct.out
 create mode 100644 src/test/regress/sql/pg_ndistinct.sql

diff --git a/src/backend/utils/adt/pg_ndistinct.c b/src/backend/utils/adt/pg_ndistinct.c
index 56b17758ea5..0bed344f41d 100644
--- a/src/backend/utils/adt/pg_ndistinct.c
+++ b/src/backend/utils/adt/pg_ndistinct.c
@@ -14,34 +14,615 @@
 
 #include "postgres.h"
 
+#include "common/int.h"
 
+typedef enum
+{
+	NDIST_EXPECT_START = 0,
+	NDIST_EXPECT_ITEM,
+	NDIST_EXPECT_KEY,
+	NDIST_EXPECT_ATTNUM_LIST,
+	NDIST_EXPECT_ATTNUM,
+	NDIST_EXPECT_NDISTINCT,
+	NDIST_EXPECT_COMPLETE
+} NDistinctSemanticState;
+
+typedef struct
+{
+	const char *str;
+	NDistinctSemanticState state;
+
+	List	   *distinct_items; /* Accumulated complete MVNDistinctItems */
+	Node	   *escontext;
+
+	bool		found_attributes;	/* Item has an attributes key */
+	bool		found_ndistinct;	/* Item has ndistinct key */
+	List	   *attnum_list;	/* Accumulated attributes attnums */
+	int64		ndistinct;
+} NDistinctParseState;
+
+/*
+ * Invoked at the start of each MVNDistinctItem.
+ *
+ * The entire JSON document shoul be one array of MVNDistinctItem objects.
+ *
+ * If we're anywhere else in the document, it's an error.
+ */
+static JsonParseErrorType
+ndistinct_object_start(void *state)
+{
+	NDistinctParseState *parse = state;
+
+	switch(parse->state)
+	{
+		case NDIST_EXPECT_ITEM:
+			/* Now we expect to see attributes/ndistinct keys */
+			parse->state = NDIST_EXPECT_KEY;
+			return JSON_SUCCESS;
+			break;
+
+		default:
+			ereturn(parse->escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("Expected Item object.")));
+	}
+
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ * Routine to allow qsorting of AttNumbers
+ */
+static int
+attnum_compare(const void *aptr, const void *bptr)
+{
+	AttrNumber	a = *(const AttrNumber *) aptr;
+	AttrNumber	b = *(const AttrNumber *) bptr;
+
+	return pg_cmp_s16(a, b);
+}
+
+
+/*
+ * Invoked at the end of an object.
+ *
+ * Check to ensure that it was a complete MVNDistinctItem
+ *
+ */
+static JsonParseErrorType
+ndistinct_object_end(void *state)
+{
+	NDistinctParseState *parse = state;
+
+	int			natts = 0;
+	AttrNumber *attrsort;
+
+	MVNDistinctItem *item;
+
+	if (!parse->found_attributes)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+				 errdetail("Item must contain \"" PG_NDISTINCT_KEY_ATTRIBUTES "\" key.")));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	if (!parse->found_ndistinct)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+				 errdetail("Item must contain \"" PG_NDISTINCT_KEY_NDISTINCT "\" key.")));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	/*
+	 * We need at least 2 attnums for a ndistinct item, anything less is
+	 * malformed.
+	 */
+	natts = parse->attnum_list->length;
+	if (natts < 2)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+				 errdetail("The \"" PG_NDISTINCT_KEY_ATTRIBUTES
+						   "\" key must contain an array of at least two attnums.")));
+
+		return JSON_SEM_ACTION_FAILED;
+	}
+	attrsort = palloc0(natts * sizeof(AttrNumber));
+
+	/* Create the MVNDistinctItem */
+	item = palloc(sizeof(MVNDistinctItem));
+	item->nattributes = natts;
+	item->attributes = palloc0(natts * sizeof(AttrNumber));
+	item->ndistinct = (double) parse->ndistinct;
+
+	/* fill out both attnum list and sortable list */
+	for (int i = 0; i < natts; i++)
+	{
+		attrsort[i] = (AttrNumber) parse->attnum_list->elements[i].int_value;
+		item->attributes[i] = attrsort[i];
+	}
+
+	/* Check attrsort for uniqueness */
+	qsort(attrsort, natts, sizeof(AttrNumber), attnum_compare);
+	for (int i = 1; i < natts; i++)
+	{
+		if (attrsort[i] == attrsort[i - 1])
+		{
+			ereturn(parse->escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					 errdetail("attnum list duplicate value found: %d.", attrsort[i])));
+
+			return JSON_SEM_ACTION_FAILED;
+		}
+	}
+	pfree(attrsort);
+
+	parse->distinct_items = lappend(parse->distinct_items, (void *) item);
+
+	/* reset item state vars */
+	list_free(parse->attnum_list);
+	parse->attnum_list = NIL;
+	parse->ndistinct = 0;
+	parse->found_attributes = false;
+	parse->found_ndistinct = false;
+
+	/* Now we are looking for the next MVNDistinctItem */
+	parse->state = NDIST_EXPECT_ITEM;
+	return JSON_SUCCESS;
+}
+
+
+/*
+ * ndsitinct input format has two types of arrays, the outer MVNDistinctItem
+ * array, and the attnum list array within each MVNDistinctItem.
+ */
+static JsonParseErrorType
+ndistinct_array_start(void *state)
+{
+	NDistinctParseState *parse = state;
+
+	switch (parse->state)
+	{
+		case NDIST_EXPECT_ATTNUM_LIST:
+			parse->state = NDIST_EXPECT_ATTNUM;
+			break;
+
+		case NDIST_EXPECT_START:
+			parse->state = NDIST_EXPECT_ITEM;
+			break;
+
+		default:
+			ereturn(parse->escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					 errdetail("Array found in unexpected place.")));
+			return JSON_SEM_ACTION_FAILED;
+	}
+
+	return JSON_SUCCESS;
+}
+
+
+static JsonParseErrorType
+ndistinct_array_end(void *state)
+{
+	NDistinctParseState *parse = state;
+
+	switch (parse->state)
+	{
+		case NDIST_EXPECT_ATTNUM:
+			if (parse->attnum_list != NIL)
+			{
+				/* The attnum list is complete, look for more MVNDistinctItem keys */
+				parse->state = NDIST_EXPECT_KEY;
+				return JSON_SUCCESS;
+			}
+
+			ereturn(parse->escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("The \"" PG_NDISTINCT_KEY_ATTRIBUTES
+							"\" key must be an non-empty array.")));
+			return JSON_SEM_ACTION_FAILED;
+			break;
+
+		case NDIST_EXPECT_ITEM:
+			if (parse->distinct_items != NIL)
+			{
+				/* Item list is complete, we're done. */
+				parse->state = NDIST_EXPECT_COMPLETE;
+				return JSON_SUCCESS;
+			}
+
+			ereturn(parse->escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("Item array cannot be empty.")));
+
+			return JSON_SEM_ACTION_FAILED;
+			break;
+		default:
+			ereturn(parse->escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("Array found in unexpected place.")));
+	}
+	return JSON_SEM_ACTION_FAILED;
+}
+
+
+/*
+ * The valid keys for the MVNDistinctItem object are:
+ *   - attributes
+ *   - ndistinct
+ */
+static JsonParseErrorType
+ndistinct_object_field_start(void *state, char *fname, bool isnull)
+{
+	NDistinctParseState *parse = state;
+
+	if (strcmp(fname, PG_NDISTINCT_KEY_ATTRIBUTES) == 0)
+	{
+		parse->found_attributes = true;
+		parse->state = NDIST_EXPECT_ATTNUM_LIST;
+		return JSON_SUCCESS;
+	}
+
+	if (strcmp(fname, PG_NDISTINCT_KEY_NDISTINCT) == 0)
+	{
+		parse->found_ndistinct = true;
+		parse->state = NDIST_EXPECT_NDISTINCT;
+		return JSON_SUCCESS;
+	}
+
+	ereturn(parse->escontext, (Datum) 0,
+			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+			 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+			 errdetail("Invalid key \"%s\". Only allowed keys are \""
+					   PG_NDISTINCT_KEY_ATTRIBUTES "\" and \""
+					   PG_NDISTINCT_KEY_NDISTINCT "\".", fname)));
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ *
+ */
+static JsonParseErrorType
+ndistinct_array_element_start(void *state, bool isnull)
+{
+	NDistinctParseState *parse = state;
+
+	switch(parse->state)
+	{
+		case NDIST_EXPECT_ATTNUM:
+			if (!isnull)
+				return JSON_SUCCESS;
+
+			ereturn(parse->escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					 errdetail("Attnum list elements cannot be null.")));
+
+			break;
+
+		case NDIST_EXPECT_ITEM:
+			if (!isnull)
+				return JSON_SUCCESS;
+
+			ereturn(parse->escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					 errdetail("Item list elements cannot be null.")));
+
+			break;
+
+		default:
+			ereturn(parse->escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("Unexpected array element.")));
+	}
+
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ * Handle scalar events from the ndistinct input parser.
+ *
+ */
+static JsonParseErrorType
+ndistinct_scalar(void *state, char *token, JsonTokenType tokentype)
+{
+	NDistinctParseState *parse = state;
+	AttrNumber	attnum;
+
+	switch(parse->state)
+	{
+		case NDIST_EXPECT_ATTNUM:
+			attnum = pg_strtoint16_safe(token, parse->escontext);
+
+			if (SOFT_ERROR_OCCURRED(parse->escontext))
+				return JSON_SEM_ACTION_FAILED;
+
+			parse->attnum_list = lappend_int(parse->attnum_list, (int) attnum);
+			return JSON_SUCCESS;
+			break;
+
+		case NDIST_EXPECT_NDISTINCT:
+			/*
+			* While the structure dictates that ndistinct in a double precision
+			* floating point, in practice it has always been an integer, and it
+			* is output as such. Therefore, we follow usage precendent over the
+			* actual storage structure, and read it in as an integer.
+			*/
+			parse->ndistinct = pg_strtoint64_safe(token, parse->escontext);
+
+			if (SOFT_ERROR_OCCURRED(parse->escontext))
+				return JSON_SEM_ACTION_FAILED;
+
+			parse->state = NDIST_EXPECT_KEY;
+			return JSON_SUCCESS;
+			break;
+
+		default:
+			ereturn(parse->escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("Unexpected scalar.")));
+	}
+
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ * Compare the attribute arrays of two MVNDistinctItem values,
+ * looking for duplicate sets.
+ */
+static
+bool has_duplicate_attributes(const MVNDistinctItem *a,
+							  const MVNDistinctItem *b)
+{
+	int		i;
+
+	if (a->nattributes != b->nattributes)
+		return false;
+
+	for (i = 0; i < a->nattributes; i++)
+	{
+		if (a->attributes[i] != b->attributes[i])
+			return false;
+	}
+
+	return true;
+}
+
+/*
+ * Ensure that an attnum appears as one of the attnums in a given
+ * MVNDistinctItem.
+ */
+static
+bool item_has_attnum(const MVNDistinctItem *item, AttrNumber attnum)
+{
+	for (int i = 0; i < item->nattributes; i++)
+	{
+		if (attnum == item->attributes[i])
+			return true;
+	}
+	return false;
+}
+
+/*
+ * Ensure that the attributes of one MVNDistinctItem A are a proper subset
+ * of the reference MVNDistinctItem B.
+ */
+static
+bool item_is_attnum_subset(const MVNDistinctItem *item,
+						   const MVNDistinctItem *refitem)
+{
+	for (int i = 0; i < item->nattributes; i++)
+	{
+		if (!item_has_attnum(refitem,item->attributes[i]))
+			return false;
+	}
+	return true;
+}
+
+/*
+ * Generate a string representing an array of attnum.
+ *
+ * Freeing the allocated string is responsibility of the caller.
+ */
+static
+const char *item_attnum_list(const MVNDistinctItem *item)
+{
+	StringInfoData	str;
+
+	initStringInfo(&str);
+
+	appendStringInfo(&str, "%d", item->attributes[0]);
+
+	for (int i = 1; i < item->nattributes; i++)
+		appendStringInfo(&str, ", %d", item->attributes[i]);
+
+	return str.data;
+}
 
 /*
  * pg_ndistinct_in
 
-	PG_RETURN_VOID();			/* keep compiler quiet */
+	NDistinctParseState parse_state;
+	JsonParseErrorType result;
+	JsonLexContext *lex;
+	JsonSemAction sem_action;
+
+	int		item_most_attrs = 0;
+	int		item_most_attrs_idx = 0;
+
+	/* initialize semantic state */
+	parse_state.str = str;
+	parse_state.state = NDIST_EXPECT_START;
+	parse_state.distinct_items = NIL;
+	parse_state.escontext = fcinfo->context;
+	parse_state.found_attributes = false;
+	parse_state.found_ndistinct = false;
+	parse_state.attnum_list = NIL;
+	parse_state.ndistinct = 0;
+
+	/* set callbacks */
+	sem_action.semstate = (void *) &parse_state;
+	sem_action.object_start = ndistinct_object_start;
+	sem_action.object_end = ndistinct_object_end;
+	sem_action.array_start = ndistinct_array_start;
+	sem_action.array_end = ndistinct_array_end;
+	sem_action.object_field_start = ndistinct_object_field_start;
+	sem_action.object_field_end = NULL;
+	sem_action.array_element_start = ndistinct_array_element_start;
+	sem_action.array_element_end = NULL;
+	sem_action.scalar = ndistinct_scalar;
+
+	lex = makeJsonLexContextCstringLen(NULL, str, strlen(str),
+									   PG_UTF8, true);
+	result = pg_parse_json(lex, &sem_action);
+	freeJsonLexContext(lex);
+
+	if (result == JSON_SUCCESS &&
+		parse_state.distinct_items != NIL)
+	{
+		MVNDistinct *ndistinct;
+		int			nitems = parse_state.distinct_items->length;
+		bytea	   *bytes;
+
+
+		ndistinct = palloc(offsetof(MVNDistinct, items) +
+						   nitems * sizeof(MVNDistinctItem));
+
+		ndistinct->magic = STATS_NDISTINCT_MAGIC;
+		ndistinct->type = STATS_NDISTINCT_TYPE_BASIC;
+		ndistinct->nitems = nitems;
+
+		for (int i = 0; i < nitems; i++)
+		{
+			MVNDistinctItem *item = parse_state.distinct_items->elements[i].ptr_value;
+
+			/*
+			 * Ensure that this item does not duplicate the attributes of any
+			 * pre-existing item.
+			 */
+			for (int j = 0; j < i; j++)
+			{
+				if (has_duplicate_attributes(item, &ndistinct->items[j]))
+				{
+					ereturn(parse_state.escontext, (Datum) 0,
+							(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+							errmsg("malformed pg_ndistinct: \"%s\"", str),
+							errdetail("Duplicate \"" PG_NDISTINCT_KEY_ATTRIBUTES "\" array : [%s]",
+										item_attnum_list(item))));
+					PG_RETURN_NULL();
+				}
+			}
+
+			ndistinct->items[i].ndistinct = item->ndistinct;
+			ndistinct->items[i].nattributes = item->nattributes;
+			ndistinct->items[i].attributes = item->attributes;
+
+			/*
+			 * Keep track of the first longest attribute list. All other attribute
+			 * lists must be a subset of this list.
+			 */
+			if (item->nattributes > item_most_attrs)
+			{
+				item_most_attrs = item->nattributes;
+				item_most_attrs_idx = i;
+			}
+
+			/*
+			 * Free the MVNDistinctItem, but not the attributes we're still
+			 * using.
+			 */
+			pfree(item);
+		}
+
+		/*
+		 * Verify that all attnum sets are a proper subset of the first longest
+		 * attnum set.
+		 */
+		for (int i = 0; i < nitems; i++)
+		{
+			if (i == item_most_attrs_idx)
+				continue;
+
+			if (!item_is_attnum_subset(&ndistinct->items[i],
+									   &ndistinct->items[item_most_attrs_idx]))
+			{
+				const MVNDistinctItem  *item = &ndistinct->items[i];
+				const MVNDistinctItem  *refitem = &ndistinct->items[item_most_attrs_idx];
+				const char *item_list = item_attnum_list(item);
+				const char *refitem_list = item_attnum_list(refitem);
+
+				ereturn(parse_state.escontext, (Datum) 0,
+						(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+						errmsg("malformed pg_ndistinct: \"%s\"", str),
+						errdetail("\"" PG_NDISTINCT_KEY_ATTRIBUTES "\" array: [%s]"
+								  "must be a subset of array: [%s]",
+								  item_list, refitem_list)));
+				PG_RETURN_NULL();
+			}
+		}
+
+		bytes = statext_ndistinct_serialize(ndistinct);
+
+		list_free(parse_state.distinct_items);
+		for (int i = 0; i < nitems; i++)
+			pfree(ndistinct->items[i].attributes);
+		pfree(ndistinct);
+
+		PG_RETURN_BYTEA_P(bytes);
+	}
+	else if (result == JSON_SEM_ACTION_FAILED)
+		PG_RETURN_NULL();		/* escontext already set */
+
+	/* Anything else is a generic JSON parse error */
+	ereturn(parse_state.escontext, (Datum) 0,
+			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+			 errmsg("malformed pg_ndistinct: \"%s\"", str),
+			 errdetail("Must be valid JSON.")));
+	PG_RETURN_NULL();
 }
 
 /*
  * pg_ndistinct
index 00000000000..d99e84a2bce
@@ -0,0 +1,109 @@
+DETAIL:  Item array cannot be empty.
+DETAIL:  Invalid key "attributes_invalid". Only allowed keys are "attributes" and "ndistinct".
+DETAIL:  Invalid key "invalid". Only allowed keys are "attributes" and "ndistinct".
+-- Missing key
+SELECT '[{"attributes" : [2,3]}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : [2,3]}]"
+LINE 1: SELECT '[{"attributes" : [2,3]}]'::pg_ndistinct;
+DETAIL:  Item must contain "ndistinct" key.
+SELECT '[{"ndistinct" : 4}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"ndistinct" : 4}]"
+LINE 1: SELECT '[{"ndistinct" : 4}]'::pg_ndistinct;
+DETAIL:  Item must contain "attributes" key.
+DETAIL:  Array found in unexpected place.
+DETAIL:  Array found in unexpected place.
+DETAIL:  Array found in unexpected place.
+DETAIL:  attnum list duplicate value found: 2.
+         {"attributes" : [2,3], "ndistinct" : 4}]"
+DETAIL:  Duplicate "attributes" array : [2, 3]
+DETAIL:  "attributes" array: [2, 3]must be a subset of array: [1, 3, -1, -2]
index f56482fb9f1..f3f0b5f2f31 100644
 
 # ----------
index 00000000000..ca89fed6fe2
@@ -0,0 +1,34 @@
+-- Missing key
+SELECT '[{"attributes" : [2,3]}]'::pg_ndistinct;
+SELECT '[{"ndistinct" : 4}]'::pg_ndistinct;

From ebdf63185d40a41f5e83bad36e50f795631c15c8 Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Tue, 11 Nov 2025 16:55:47 +0900
Subject: [PATCH v11 6/9] Add working input function for pg_dependencies.

This will be needed for importing extended statistics.
---
 src/backend/utils/adt/pg_dependencies.c       | 620 +++++++++++++++++-
 src/test/regress/expected/pg_dependencies.out | 128 ++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/pg_dependencies.sql      |  39 ++
 4 files changed, 778 insertions(+), 11 deletions(-)
 create mode 100644 src/test/regress/expected/pg_dependencies.out
 create mode 100644 src/test/regress/sql/pg_dependencies.sql

diff --git a/src/backend/utils/adt/pg_dependencies.c b/src/backend/utils/adt/pg_dependencies.c
index 05d0102c09c..5e2a65c5db0 100644
--- a/src/backend/utils/adt/pg_dependencies.c
+++ b/src/backend/utils/adt/pg_dependencies.c
@@ -14,29 +14,629 @@
 
 #include "postgres.h"
 
+#include "common/int.h"
 
+typedef enum
+{
+	DEPS_EXPECT_START = 0,
+	DEPS_EXPECT_ITEM,
+	DEPS_EXPECT_KEY,
+	DEPS_EXPECT_ATTNUM_LIST,
+	DEPS_EXPECT_ATTNUM,
+	DEPS_EXPECT_DEPENDENCY,
+	DEPS_EXPECT_DEGREE,
+	DEPS_PARSE_COMPLETE
+} DepsParseSemanticState;
+
+typedef struct
+{
+	const char *str;
+	DepsParseSemanticState state;
+
+	List	   *dependency_list;
+	Node	   *escontext;
+
+	bool		found_attributes;	/* Item has an attributes key */
+	bool		found_dependency;	/* Item has an dependency key */
+	bool		found_degree;	/* Item has degree key */
+	List	   *attnum_list;	/* Accumulated attributes attnums */
+	AttrNumber	dependency;
+	double		degree;
+} DependenciesParseState;
+
+/*
+ * Invoked at the start of each MVDependency object.
+ *
+ * The entire JSON document shoul be one array of MVDependency objects.
+ *
+ * If we're anywhere else in the document, it's an error.
+ */
+static JsonParseErrorType
+dependencies_object_start(void *state)
+{
+	DependenciesParseState *parse = state;
+
+	if (parse->state != DEPS_EXPECT_ITEM)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				 errdetail("Expected Item object.")));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	/* Now we expect to see attributes/dependency/degree keys */
+	parse->state = DEPS_EXPECT_KEY;
+	return JSON_SUCCESS;
+}
+
+static int
+attnum_compare(const void *aptr, const void *bptr)
+{
+	AttrNumber	a = *(const AttrNumber *) aptr;
+	AttrNumber	b = *(const AttrNumber *) bptr;
+
+	return pg_cmp_s16(a, b);
+}
+
+static JsonParseErrorType
+dependencies_object_end(void *state)
+{
+	DependenciesParseState *parse = state;
+
+	MVDependency *dep;
+	AttrNumber *attrsort;
+
+	int			natts = 0;
+
+	if (!parse->found_attributes)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				 errdetail("Item must contain \"" PG_DEPENDENCIES_KEY_ATTRIBUTES "\" key.")));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	if (!parse->found_dependency)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				 errdetail("Item must contain \"" PG_DEPENDENCIES_KEY_DEPENDENCY "\" key.")));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	if (!parse->found_degree)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				 errdetail("Item must contain \"" PG_DEPENDENCIES_KEY_DEGREE "\" key.")));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	/*
+	 * We need at least 1 attnum for a dependencies item, anything less is
+	 * malformed.
+	 */
+	natts = parse->attnum_list->length;
+	if (natts < 1)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				 errdetail("The \"" PG_DEPENDENCIES_KEY_ATTRIBUTES "\" key must contain an array of at least one attnum.")));
+
+		return JSON_SEM_ACTION_FAILED;
+	}
+	attrsort = palloc0(natts * sizeof(AttrNumber));
+
+	/*
+	 * Allocate enough space for the dependency, the attnums in the list, plus
+	 * the final attnum
+	 */
+	dep = palloc0(offsetof(MVDependency, attributes) + ((natts + 1) * sizeof(AttrNumber)));
+	dep->nattributes = natts + 1;
+
+	dep->attributes[natts] = parse->dependency;
+	dep->degree = parse->degree;
+
+	attrsort = palloc0(dep->nattributes * sizeof(AttrNumber));
+	attrsort[natts] = parse->dependency;
+
+	for (int i = 0; i < natts; i++)
+	{
+		attrsort[i] = (AttrNumber) parse->attnum_list->elements[i].int_value;
+		dep->attributes[i] = attrsort[i];
+	}
+
+	/* Check attrsort for uniqueness */
+	qsort(attrsort, natts + 1, sizeof(AttrNumber), attnum_compare);
+	for (int i = 1; i < dep->nattributes; i++)
+		if (attrsort[i] == attrsort[i - 1])
+		{
+			ereturn(parse->escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					 errdetail("\"" PG_DEPENDENCIES_KEY_ATTRIBUTES "\" list duplicate value found: %d.", attrsort[i])));
+
+			return JSON_SEM_ACTION_FAILED;
+		}
+	pfree(attrsort);
+
+	parse->dependency_list = lappend(parse->dependency_list, (void *) dep);
+
+	/* reset dep item state vars */
+	list_free(parse->attnum_list);
+	parse->attnum_list = NIL;
+	parse->dependency = 0;
+	parse->degree = 0.0;
+	parse->found_attributes = false;
+	parse->found_dependency = false;
+	parse->found_degree = false;
+
+	/* Now we are looking for the next MVDependency */
+	parse->state = DEPS_EXPECT_ITEM;
+	return JSON_SUCCESS;
+}
+
+/*
+ * dependencies input format does not have arrays, so any array elements encountered
+ * are an error.
+ */
+static JsonParseErrorType
+dependencies_array_start(void *state)
+{
+	DependenciesParseState *parse = state;
+
+	switch (parse->state)
+	{
+		case DEPS_EXPECT_ATTNUM_LIST:
+			parse->state = DEPS_EXPECT_ATTNUM;
+			break;
+		case DEPS_EXPECT_START:
+			parse->state = DEPS_EXPECT_ITEM;
+			break;
+		default:
+			ereturn(parse->escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					 errdetail("Array found in unexpected place.")));
+			return JSON_SEM_ACTION_FAILED;
+	}
+
+	return JSON_SUCCESS;
+}
+
+/*
+ * Either the end of an attnum list or the whole object
+ */
+static JsonParseErrorType
+dependencies_array_end(void *state)
+{
+	DependenciesParseState *parse = state;
+
+	switch (parse->state)
+	{
+		case DEPS_EXPECT_ATTNUM:
+			if (parse->attnum_list == NIL)
+			{
+				ereturn(parse->escontext, (Datum) 0,
+						(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+						errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+						errdetail("The \"" PG_DEPENDENCIES_KEY_ATTRIBUTES
+								  "\" key must be an non-empty array.")));
+				return JSON_SEM_ACTION_FAILED;
+			}
+
+			parse->state = DEPS_EXPECT_KEY;
+			break;
+
+		case DEPS_EXPECT_ITEM:
+			if (parse->dependency_list == NIL)
+			{
+				ereturn(parse->escontext, (Datum) 0,
+						(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+						errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+						errdetail("The dependency list must be an non-empty array.")));
+				return JSON_SEM_ACTION_FAILED;
+			}
+			parse->state = DEPS_PARSE_COMPLETE;
+			break;
+
+		default:
+			ereturn(parse->escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					 errdetail("Array found in unexpected place.")));
+			return JSON_SEM_ACTION_FAILED;
+	}
+	return JSON_SUCCESS;
+}
+
+/*
+ * The valid keys for the MVDependency object are:
+ *   - attributes
+ *   - depeendency
+ *   - degree
+ */
+static JsonParseErrorType
+dependencies_object_field_start(void *state, char *fname, bool isnull)
+{
+	DependenciesParseState *parse = state;
+
+	if (strcmp(fname, PG_DEPENDENCIES_KEY_ATTRIBUTES) == 0)
+	{
+		parse->found_attributes = true;
+		parse->state = DEPS_EXPECT_ATTNUM_LIST;
+		return JSON_SUCCESS;
+	}
+
+	if (strcmp(fname, PG_DEPENDENCIES_KEY_DEPENDENCY) == 0)
+	{
+		parse->found_dependency = true;
+		parse->state = DEPS_EXPECT_DEPENDENCY;
+		return JSON_SUCCESS;
+	}
+
+	if (strcmp(fname, PG_DEPENDENCIES_KEY_DEGREE) == 0)
+	{
+		parse->found_degree = true;
+		parse->state = DEPS_EXPECT_DEGREE;
+		return JSON_SUCCESS;
+	}
+
+	ereturn(parse->escontext, (Datum) 0,
+			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+			 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+			 errdetail("Invalid key \"%s\". Only allowed keys are \""
+					   PG_DEPENDENCIES_KEY_ATTRIBUTES "\", \""
+					   PG_DEPENDENCIES_KEY_DEPENDENCY "\" and \""
+					   PG_DEPENDENCIES_KEY_DEGREE "\".", fname)));
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ * pg_dependencies input format does not have arrays, so any array elements
+ * encountered are an error.
+ */
+static JsonParseErrorType
+dependencies_array_element_start(void *state, bool isnull)
+{
+	DependenciesParseState *parse = state;
+
+	switch(parse->state)
+	{
+		case DEPS_EXPECT_ATTNUM:
+			if (!isnull)
+				return JSON_SUCCESS;
+
+			ereturn(parse->escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Attnum list elements cannot be null.")));
+
+			return JSON_SEM_ACTION_FAILED;
+			break;
+
+		case DEPS_EXPECT_ITEM:
+			if (!isnull)
+				return JSON_SUCCESS;
+			ereturn(parse->escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Item list elements cannot be null.")));
+
+			return JSON_SEM_ACTION_FAILED;
+			break;
+
+		default:
+			ereturn(parse->escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Unexpected array element.")));
+	}
+
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ * Handle scalar events from the dependencies input parser.
+ *
+ * There is only one case where we will encounter a scalar, and that is the
+ * dependency degree for the previous object key.
+ */
+static JsonParseErrorType
+dependencies_scalar(void *state, char *token, JsonTokenType tokentype)
+{
+	DependenciesParseState *parse = state;
+	AttrNumber	attnum;
+
+	switch(parse->state)
+	{
+		case DEPS_EXPECT_ATTNUM:
+			attnum = pg_strtoint16_safe(token, parse->escontext);
+
+			if (SOFT_ERROR_OCCURRED(parse->escontext))
+				return JSON_SEM_ACTION_FAILED;
+
+			parse->attnum_list = lappend_int(parse->attnum_list, (int) attnum);
+			return JSON_SUCCESS;
+			break;
+
+		case DEPS_EXPECT_DEPENDENCY:
+			parse->dependency = (AttrNumber) pg_strtoint16_safe(token, parse->escontext);
+
+			if (SOFT_ERROR_OCCURRED(parse->escontext))
+				return JSON_SEM_ACTION_FAILED;
+
+			return JSON_SUCCESS;
+			break;
+
+		case DEPS_EXPECT_DEGREE:
+			parse->degree = float8in_internal(token, NULL, "double",
+											token, parse->escontext);
+
+			if (SOFT_ERROR_OCCURRED(parse->escontext))
+				return JSON_SEM_ACTION_FAILED;
+
+			return JSON_SUCCESS;
+			break;
+
+		default:
+			ereturn(parse->escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Unexpected scalar.")));
+	}
+
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*************************************************** BLAH *********************************************/
+/*
+ * Compare the attribute arrays of two MVDependency values,
+ * looking for duplicate sets.
+ */
+static
+bool has_duplicate_attributes(const MVDependency *a, const MVDependency *b)
+{
+	int		i;
+
+	if (a->nattributes != b->nattributes)
+		return false;
+
+	for (i = 0; i < a->nattributes; i++)
+	{
+		if (a->attributes[i] != b->attributes[i])
+			return false;
+	}
+
+	return true;
+}
+
+/*
+ * Ensure that an attnum appears as one of the attnums in a given
+ * MVDependency.
+ */
+static
+bool dep_has_attnum(const MVDependency *item, AttrNumber attnum)
+{
+	for (int i = 0; i < item->nattributes; i++)
+	{
+		if (attnum == item->attributes[i])
+			return true;
+	}
+	return false;
+}
+
+/*
+ * Ensure that the attributes of one MVDependency A are a proper subset
+ * of the reference MVDependency B.
+ */
+static
+bool dep_is_attnum_subset(const MVDependency *item,
+						  const MVDependency *refitem)
+{
+	for (int i = 0; i < item->nattributes; i++)
+	{
+		if (!dep_has_attnum(refitem,item->attributes[i]))
+			return false;
+	}
+	return true;
+}
+
+/*
+ * Generate a string representing an array of attnums. Internally, the
+ * dependency attribute is the last element, so we leave that off.
+ *
+ *
+ * Freeing the allocated string is responsibility of the caller.
+ */
+static
+const char *dep_attnum_list(const MVDependency *item)
+{
+	StringInfoData	str;
+
+	initStringInfo(&str);
+
+	appendStringInfo(&str, "%d", item->attributes[0]);
+
+	for (int i = 1; i < item->nattributes - 1; i++)
+		appendStringInfo(&str, ", %d", item->attributes[i]);
+
+	return str.data;
+}
+
+/*
+ * Return the dependency, which is the last attribute element.
+ */
+static
+const AttrNumber dep_attnum_dependency(const MVDependency *item)
+{
+	return item->attributes[item->nattributes - 1];
+}
+
+
+
+
+/*************************************************** BLAH *********************************************/
 /*
  * pg_dependencies_in		- input routine for type pg_dependencies.
  *
- * pg_dependencies is real enough to be a table column, but it has no operations
- * of its own, and disallows input too
+ * This format is valid JSON, with the expected format:
+ *    [{"attributes": [1,2], "dependency": -1, "degree": 1.0000},
+ *     {"attributes": [1,-1], "dependency": 2, "degree": 0.0000},
+ *     {"attributes": [2,-1], "dependency": 1, "degree": 1.0000}]
+ *
  */
 Datum
 pg_dependencies_in(PG_FUNCTION_ARGS)
 {
-	/*
-	 * pg_node_list stores the data in binary form and parsing text input is
-	 * not needed, so disallow this.
-	 */
-	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot accept a value of type %s", "pg_dependencies")));
+	char	   *str = PG_GETARG_CSTRING(0);
 
-	PG_RETURN_VOID();			/* keep compiler quiet */
+	DependenciesParseState parse_state;
+	JsonParseErrorType result;
+	JsonLexContext *lex;
+	JsonSemAction sem_action;
+
+	/* initialize the semantic state */
+	parse_state.str = str;
+	parse_state.state = DEPS_EXPECT_START;
+	parse_state.dependency_list = NIL;
+	parse_state.attnum_list = NIL;
+	parse_state.dependency = 0;
+	parse_state.degree = 0.0;
+	parse_state.found_attributes = false;
+	parse_state.found_dependency = false;
+	parse_state.found_degree = false;
+	parse_state.escontext = fcinfo->context;
+
+	/* set callbacks */
+	sem_action.semstate = (void *) &parse_state;
+	sem_action.object_start = dependencies_object_start;
+	sem_action.object_end = dependencies_object_end;
+	sem_action.array_start = dependencies_array_start;
+	sem_action.array_end = dependencies_array_end;
+	sem_action.array_element_start = dependencies_array_element_start;
+	sem_action.array_element_end = NULL;
+	sem_action.object_field_start = dependencies_object_field_start;
+	sem_action.object_field_end = NULL;
+	sem_action.scalar = dependencies_scalar;
+
+	lex = makeJsonLexContextCstringLen(NULL, str, strlen(str), PG_UTF8, true);
+
+	result = pg_parse_json(lex, &sem_action);
+	freeJsonLexContext(lex);
+
+	if (result == JSON_SUCCESS)
+	{
+		List	   *list = parse_state.dependency_list;
+		int			ndeps = list->length;
+		MVDependencies *mvdeps;
+		bytea	   *bytes;
+
+		int		dep_most_attrs = 0;
+		int		dep_most_attrs_idx = 0;
+
+		mvdeps = palloc0(offsetof(MVDependencies, deps) + ndeps * sizeof(MVDependency));
+		mvdeps->magic = STATS_DEPS_MAGIC;
+		mvdeps->type = STATS_DEPS_TYPE_BASIC;
+		mvdeps->ndeps = ndeps;
+
+		/* copy MVDependency structs out of the list into the MVDependencies */
+		for (int i = 0; i < ndeps; i++)
+		{
+			mvdeps->deps[i] = list->elements[i].ptr_value;
+
+			/*
+			 * Ensure that this item does not duplicate the attributes of any
+			 * pre-existing item.
+			 */
+			for (int j = 0; j < i; j++)
+			{
+				if (has_duplicate_attributes(mvdeps->deps[i], mvdeps->deps[j]))
+				{
+					MVDependency   *dep = mvdeps->deps[i];
+
+					ereturn(parse_state.escontext, (Datum) 0,
+							(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+							errmsg("malformed pg_dependencies: \"%s\"", str),
+							errdetail("Duplicate \"" PG_DEPENDENCIES_KEY_ATTRIBUTES "\" array: [%s]"
+									  " with \"" PG_DEPENDENCIES_KEY_DEPENDENCY "\": %d.",
+									  dep_attnum_list(dep), dep_attnum_dependency(dep))));
+					PG_RETURN_NULL();
+				}
+			}
+
+			/*
+			 * Keep track of the first longest attribute list. All other attribute
+			 * lists must be a subset of this list.
+			 */
+			if (mvdeps->deps[i]->nattributes > dep_most_attrs)
+			{
+				dep_most_attrs = mvdeps->deps[i]->nattributes;
+				dep_most_attrs_idx = i;
+			}
+		}
+
+		/*
+		 * Verify that all attnum sets are a proper subset of the first longest
+		 * attnum set.
+		 */
+		for (int i = 0; i < ndeps; i++)
+		{
+			if (i == dep_most_attrs_idx)
+				continue;
+
+			if (!dep_is_attnum_subset(mvdeps->deps[i],
+									  mvdeps->deps[dep_most_attrs_idx]))
+			{
+				MVDependency   *dep = mvdeps->deps[i];
+				MVDependency   *refdep = mvdeps->deps[dep_most_attrs_idx];
+	   			const char *dep_list = dep_attnum_list(dep);
+	   			const char *refdep_list = dep_attnum_list(refdep);
+
+				ereturn(parse_state.escontext, (Datum) 0,
+						(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+						errmsg("malformed pg_dependencies: \"%s\"", str),
+						errdetail("\"" PG_DEPENDENCIES_KEY_ATTRIBUTES "\" array: [%s]"
+								  " with dependency %d must be a subset of array: [%s]"
+								  " with dependency %d.",
+								  dep_list, dep_attnum_dependency(dep),
+								  refdep_list, dep_attnum_dependency(refdep))));
+				PG_RETURN_NULL();
+			}
+		}
+		bytes = statext_dependencies_serialize(mvdeps);
+
+		list_free(list);
+		for (int i = 0; i < ndeps; i++)
+			pfree(mvdeps->deps[i]);
+		pfree(mvdeps);
+
+		PG_RETURN_BYTEA_P(bytes);
+	}
+	else if (result == JSON_SEM_ACTION_FAILED)
+		PG_RETURN_NULL();
+
+	/* Anything else is a generic JSON parse error */
+	ereturn(parse_state.escontext, (Datum) 0,
+			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+			 errmsg("malformed pg_dependencies: \"%s\"", str),
+			 errdetail("Must be valid JSON.")));
+
+	PG_RETURN_NULL();			/* keep compiler quiet */
 }
 
 /*
diff --git a/src/test/regress/expected/pg_dependencies.out b/src/test/regress/expected/pg_dependencies.out
new file mode 100644
index 00000000000..9aa2df24278
--- /dev/null
+++ b/src/test/regress/expected/pg_dependencies.out
@@ -0,0 +1,128 @@
+-- Tests for type pg_distinct
+-- Invalid inputs
+SELECT '[]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[]"
+LINE 1: SELECT '[]'::pg_dependencies;
+               ^
+DETAIL:  The dependency list must be an non-empty array.
+SELECT '[null]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[null]"
+LINE 1: SELECT '[null]'::pg_dependencies;
+               ^
+DETAIL:  Item list elements cannot be null.
+-- Invalid keys
+SELECT '[{"attributes_invalid" : [2,3], "dependency" : 4}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes_invalid" : [2,3], "dependency" : 4}]"
+LINE 1: SELECT '[{"attributes_invalid" : [2,3], "dependency" : 4}]':...
+               ^
+DETAIL:  Invalid key "attributes_invalid". Only allowed keys are "attributes", "dependency" and "degree".
+SELECT '[{"attributes" : [2,3], "invalid" : 3, "dependency" : 4}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "invalid" : 3, "dependency" : 4}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "invalid" : 3, "dependency" ...
+               ^
+DETAIL:  Invalid key "invalid". Only allowed keys are "attributes", "dependency" and "degree".
+-- Missing keys
+SELECT '[{"attributes" : [2,3], "dependency" : 4}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : 4}]'::pg_depe...
+               ^
+DETAIL:  Item must contain "degree" key.
+SELECT '[{"attributes" : [2,3], "degree" : 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "degree" : 1.000}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "degree" : 1.000}]'::pg_depe...
+               ^
+DETAIL:  Item must contain "dependency" key.
+SELECT '[{"attributes" : [2,3], "dependency" : 4}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : 4}]'::pg_depe...
+               ^
+DETAIL:  Item must contain "degree" key.
+-- Valid keys, invalid values
+SELECT '[{"attributes" : null, "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : null, "dependency" : 4, "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : null, "dependency" : 4, "degree": 1...
+               ^
+DETAIL:  Unexpected scalar.
+SELECT '[{"attributes" : [2,null], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,null], "dependency" : 4, "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : [2,null], "dependency" : 4, "degree...
+               ^
+DETAIL:  Attnum list elements cannot be null.
+SELECT '[{"attributes" : [2,3], "dependency" : null, "degree": 1.000}]'::pg_dependencies;
+ERROR:  invalid input syntax for type smallint: "null"
+LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : null, "degree...
+               ^
+SELECT '[{"attributes" : [2,"a"], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+ERROR:  invalid input syntax for type smallint: "a"
+LINE 1: SELECT '[{"attributes" : [2,"a"], "dependency" : 4, "degree"...
+               ^
+SELECT '[{"attributes" : [2,3], "dependency" : "a", "degree": 1.000}]'::pg_dependencies;
+ERROR:  invalid input syntax for type smallint: "a"
+LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : "a", "degree"...
+               ^
+SELECT '[{"attributes" : [2,3], "dependency" : [], "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : [], "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : [], "degree":...
+               ^
+DETAIL:  Array found in unexpected place.
+SELECT '[{"attributes" : [2,3], "dependency" : [null], "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : [null], "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : [null], "degr...
+               ^
+DETAIL:  Array found in unexpected place.
+SELECT '[{"attributes" : [2,3], "dependency" : [1,null], "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : [1,null], "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : [1,null], "de...
+               ^
+DETAIL:  Array found in unexpected place.
+SELECT '[{"attributes" : 1, "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : 1, "dependency" : 4, "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : 1, "dependency" : 4, "degree": 1.00...
+               ^
+DETAIL:  Unexpected scalar.
+SELECT '[{"attributes" : "a", "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : "a", "dependency" : 4, "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : "a", "dependency" : 4, "degree": 1....
+               ^
+DETAIL:  Unexpected scalar.
+SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": NaN}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4, "degree": NaN}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": ...
+               ^
+DETAIL:  Must be valid JSON.
+-- Duplicated attributes
+SELECT '[{"attributes" : [2,2], "dependency" : 4, "degree": 0.500}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,2], "dependency" : 4, "degree": 0.500}]"
+LINE 1: SELECT '[{"attributes" : [2,2], "dependency" : 4, "degree": ...
+               ^
+DETAIL:  "attributes" list duplicate value found: 2.
+-- Duplicated attribute lists.
+SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [2,3], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [2,3], "dependency" : 4, "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": ...
+               ^
+DETAIL:  Duplicate "attributes" array: [2, 3] with "dependency": 4.
+-- Partially-covered attribute lists.
+SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [1,-1], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [2,3,-1], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [2,3,-1,-2], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [1,-1], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [2,3,-1], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [2,3,-1,-2], "dependency" : 4, "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": ...
+               ^
+DETAIL:  "attributes" array: [1, -1] with dependency 4 must be a subset of array: [2, 3, -1, -2] with dependency 4.
+-- Valid inputs
+SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": 0.250},
+         {"attributes" : [2,-1], "dependency" : 4, "degree": 0.500},
+         {"attributes" : [2,3,-1], "dependency" : 4, "degree": 0.750},
+         {"attributes" : [2,3,-1,-2], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+                                                                                                                          pg_dependencies                                                                                                                          
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ [{"attributes": [2, 3], "dependency": 4, "degree": 0.250000}, {"attributes": [2, -1], "dependency": 4, "degree": 0.500000}, {"attributes": [2, 3, -1], "dependency": 4, "degree": 0.750000}, {"attributes": [2, 3, -1, -2], "dependency": 4, "degree": 1.000000}]
+(1 row)
+
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index f3f0b5f2f31..cc6d799bcea 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,7 @@ test: strings md5 numerology point lseg line box path polygon circle date time t
 # geometry depends on point, lseg, line, box, path, polygon, circle
 # horology depends on date, time, timetz, timestamp, timestamptz, interval
 # ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import pg_ndistinct
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import pg_ndistinct pg_dependencies
 
 # ----------
 # Load huge amounts of data
diff --git a/src/test/regress/sql/pg_dependencies.sql b/src/test/regress/sql/pg_dependencies.sql
new file mode 100644
index 00000000000..116f6c924cd
--- /dev/null
+++ b/src/test/regress/sql/pg_dependencies.sql
@@ -0,0 +1,39 @@
+-- Tests for type pg_distinct
+
+-- Invalid inputs
+SELECT '[]'::pg_dependencies;
+SELECT '[null]'::pg_dependencies;
+-- Invalid keys
+SELECT '[{"attributes_invalid" : [2,3], "dependency" : 4}]'::pg_dependencies;
+SELECT '[{"attributes" : [2,3], "invalid" : 3, "dependency" : 4}]'::pg_dependencies;
+-- Missing keys
+SELECT '[{"attributes" : [2,3], "dependency" : 4}]'::pg_dependencies;
+SELECT '[{"attributes" : [2,3], "degree" : 1.000}]'::pg_dependencies;
+SELECT '[{"attributes" : [2,3], "dependency" : 4}]'::pg_dependencies;
+-- Valid keys, invalid values
+SELECT '[{"attributes" : null, "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+SELECT '[{"attributes" : [2,null], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+SELECT '[{"attributes" : [2,3], "dependency" : null, "degree": 1.000}]'::pg_dependencies;
+SELECT '[{"attributes" : [2,"a"], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+SELECT '[{"attributes" : [2,3], "dependency" : "a", "degree": 1.000}]'::pg_dependencies;
+SELECT '[{"attributes" : [2,3], "dependency" : [], "degree": 1.000}]'::pg_dependencies;
+SELECT '[{"attributes" : [2,3], "dependency" : [null], "degree": 1.000}]'::pg_dependencies;
+SELECT '[{"attributes" : [2,3], "dependency" : [1,null], "degree": 1.000}]'::pg_dependencies;
+SELECT '[{"attributes" : 1, "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+SELECT '[{"attributes" : "a", "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": NaN}]'::pg_dependencies;
+-- Duplicated attributes
+SELECT '[{"attributes" : [2,2], "dependency" : 4, "degree": 0.500}]'::pg_dependencies;
+-- Duplicated attribute lists.
+SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [2,3], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+-- Partially-covered attribute lists.
+SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [1,-1], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [2,3,-1], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [2,3,-1,-2], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+-- Valid inputs
+SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": 0.250},
+         {"attributes" : [2,-1], "dependency" : 4, "degree": 0.500},
+         {"attributes" : [2,3,-1], "dependency" : 4, "degree": 0.750},
+         {"attributes" : [2,3,-1,-2], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
-- 
2.51.1

From e339b98b14a9afa0653301dc0fd5348b7f4b3b15 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 4 Nov 2025 23:50:01 -0500
Subject: [PATCH v11 7/9] Expose attribute statistics functions for use in
 extended_stats.

 
 
+/*
 
+/*

 /*

 
 
 		if (converted)
 		{
 
 		if (converted)
 		{

 
 		if (converted)
 		{

 	/*
 
 		if (converted)
 		{
 
 		if (converted)
 		{

From 3469f4e94546bb792b707b0f9185f16de813696f Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Tue, 11 Nov 2025 16:58:26 +0900
Subject: [PATCH v11 8/9] Add extended statistics support functions.

Add pg_restore_extended_stats() and pg_clear_extended_stats(). These
functions closely mirror their relation and attribute counterparts,
but for extended statistics (i.e.  CREATE STATISTICS) objects.
---
 src/include/catalog/pg_proc.dat               |   18 +
 .../statistics/extended_stats_internal.h      |   17 +
 src/backend/statistics/dependencies.c         |   61 +
 src/backend/statistics/extended_stats.c       | 1141 ++++++++++++++++-
 src/backend/statistics/mcv.c                  |  144 +++
 src/backend/statistics/mvdistinct.c           |   62 +
 src/test/regress/expected/stats_import.out    | 1123 ++++++++++++++++
 src/test/regress/sql/stats_import.sql         |  364 ++++++
 doc/src/sgml/func/func-admin.sgml             |   98 ++
 9 files changed, 3027 insertions(+), 1 deletion(-)

 
index 6f63b4f3ffb..31a9f1cfc7c 100644
 
+/*
 
+/*

+/*

 
 /*

 
 	return result;
 }
+
+static HeapTuple
 
 	return s;
 }
+
+/*
index fe452f53ae4..839bcc9af92 100644
 
+/*
index 98ce7dc2841..970e9bd0983 100644
@@ -1342,6 +1346,1125 @@ AND attname = 'i';
 (1 row)
 
 DROP TABLE stats_temp;
+        'n_distinct', '[{"attributes" : [3,-1], "ndistinct" : 4},
+                        {"attributes" : [2,3,-1,-2,1], "ndistinct" : 4}]'::pg_ndistinct
+        'n_distinct', '[{"attributes" : [3,-1], "ndistinct" : 4},
+                        {"attributes" : [2,3,-1,-2,0], "ndistinct" : 4}]'::pg_ndistinct
+        'n_distinct', '[{"attributes" : [3,-1], "ndistinct" : 4},
+                        {"attributes" : [-4,3,-1], "ndistinct" : 4},
+                        {"attributes" : [-4,3,-2], "ndistinct" : 4},
+                        {"attributes" : [-4,-1,-2], "ndistinct" : 4},
+                        {"attributes" : [3,-1,-2,-4], "ndistinct" : 4}]'::pg_ndistinct
+                        {"attributes" : [2,3,-1,-2], "ndistinct" : 4}]'::pg_ndistinct
+    jsonb_pretty(e.n_distinct::text::jsonb) AS n_distinct,
+    e.dependencies,
+    e.most_common_vals, e.most_common_val_nulls,
+-[ RECORD 1 ]----------+------------------------
+n_distinct             | [                      +
+                       |         "ndistinct": 4,+
+                       |         "attributes": [+
+                       |             3          +
+                       |         "ndistinct": 4,+
+                       |         "attributes": [+
+                       |         "ndistinct": 4,+
+                       |         "attributes": [+
+                       |         "ndistinct": 4,+
+                       |         "attributes": [+
+                       |         "ndistinct": 3,+
+                       |         "attributes": [+
+                       |         "ndistinct": 4,+
+                       |         "attributes": [+
+                       |         "ndistinct": 4,+
+                       |         "attributes": [+
+                       |         "ndistinct": 4,+
+                       |         "attributes": [+
+                       |         "ndistinct": 4,+
+                       |         "attributes": [+
+        'dependencies', '[{"attributes": [2], "dependency": 1, "degree": 1.000000}]'::pg_dependencies
+        'dependencies', '[{"attributes": [0], "dependency": -1, "degree": 1.000000}]'::pg_dependencies
+        'dependencies', '[{"attributes": [2], "dependency": -3, "degree": 1.000000}]'::pg_dependencies
+    jsonb_pretty(e.n_distinct::text::jsonb) AS n_distinct,
+    jsonb_pretty(e.dependencies::text::jsonb) AS dependencies,
+    e.most_common_vals, e.most_common_val_nulls,
+-[ RECORD 1 ]----------+----------------------------
+n_distinct             | [                          +
+                       |         "ndistinct": 4,    +
+                       |             3              +
+                       |         "ndistinct": 4,    +
+                       |         "ndistinct": 4,    +
+                       |         "ndistinct": 4,    +
+                       |         "ndistinct": 3,    +
+                       |         "ndistinct": 4,    +
+                       |         "ndistinct": 4,    +
+                       |         "ndistinct": 4,    +
+                       |         "ndistinct": 4,    +
+dependencies           | [                          +
+                       |         "degree": 1.000000,+
+                       |         ],                 +
+                       |         "dependency": 3    +
+                       |         "degree": 1.000000,+
+                       |         ],                 +
+                       |         "dependency": -1   +
+                       |         "degree": 1.000000,+
+                       |         ],                 +
+                       |         "dependency": -2   +
+                       |         "degree": 1.000000,+
+                       |             3              +
+                       |         ],                 +
+                       |         "dependency": 2    +
+                       |         "degree": 1.000000,+
+                       |             3              +
+                       |         ],                 +
+                       |         "dependency": -1   +
+                       |         "degree": 1.000000,+
+                       |             3              +
+                       |         ],                 +
+                       |         "dependency": -2   +
+                       |         "degree": 0.500000,+
+                       |         ],                 +
+                       |         "dependency": 2    +
+                       |         "degree": 0.500000,+
+                       |         ],                 +
+                       |         "dependency": 3    +
+                       |         "degree": 1.000000,+
+                       |         ],                 +
+                       |         "dependency": -2   +
+                       |         "degree": 0.500000,+
+                       |         ],                 +
+                       |         "dependency": 2    +
+                       |         "degree": 0.500000,+
+                       |         ],                 +
+                       |         "dependency": 3    +
+                       |         "degree": 1.000000,+
+                       |         ],                 +
+                       |         "dependency": -1   +
+                       |         "degree": 1.000000,+
+                       |             3              +
+                       |         ],                 +
+                       |         "dependency": -1   +
+                       |         "degree": 1.000000,+
+                       |             3              +
+                       |         ],                 +
+                       |         "dependency": -2   +
+                       |         "degree": 1.000000,+
+                       |         ],                 +
+                       |         "dependency": 3    +
+                       |         "degree": 1.000000,+
+                       |         ],                 +
+                       |         "dependency": -2   +
+                       |         "degree": 1.000000,+
+                       |         ],                 +
+                       |         "dependency": 3    +
+                       |         "degree": 1.000000,+
+                       |         ],                 +
+                       |         "dependency": -1   +
+                       |         "degree": 1.000000,+
+                       |         ],                 +
+                       |         "dependency": 2    +
+                       |         "degree": 1.000000,+
+                       |         ],                 +
+                       |         "dependency": -2   +
+                       |         "degree": 1.000000,+
+                       |         ],                 +
+                       |         "dependency": 2    +
+                       |         "degree": 1.000000,+
+                       |         ],                 +
+                       |         "dependency": -1   +
+                       |         "degree": 0.500000,+
+                       |         ],                 +
+                       |         "dependency": 2    +
+                       |         "degree": 0.500000,+
+                       |         ],                 +
+                       |         "dependency": 3    +
+                       |         "degree": 1.000000,+
+                       |         ],                 +
+                       |         "dependency": -1   +
+                       |         "degree": 1.000000,+
+                       |         ],                 +
+                       |         "dependency": 3    +
+                       |         "degree": 1.000000,+
+                       |         ],                 +
+                       |         "dependency": 2    +
+    jsonb_pretty(e.n_distinct::text::jsonb) AS n_distinct,
+    jsonb_pretty(e.dependencies::text::jsonb) AS dependencies,
+    e.most_common_vals, e.most_common_val_nulls,
+-[ RECORD 1 ]----------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+n_distinct             | [                                                                                                                                                                                          +
+                       |         "ndistinct": 4,                                                                                                                                                                    +
+                       |             3                                                                                                                                                                              +
+                       |         "ndistinct": 4,                                                                                                                                                                    +
+                       |         "ndistinct": 4,                                                                                                                                                                    +
+                       |         "ndistinct": 4,                                                                                                                                                                    +
+                       |         "ndistinct": 3,                                                                                                                                                                    +
+                       |         "ndistinct": 4,                                                                                                                                                                    +
+                       |         "ndistinct": 4,                                                                                                                                                                    +
+                       |         "ndistinct": 4,                                                                                                                                                                    +
+                       |         "ndistinct": 4,                                                                                                                                                                    +
+dependencies           | [                                                                                                                                                                                          +
+                       |         "degree": 1.000000,                                                                                                                                                                +
+                       |         ],                                                                                                                                                                                 +
+                       |         "dependency": 3                                                                                                                                                                    +
+                       |         "degree": 1.000000,                                                                                                                                                                +
+                       |         ],                                                                                                                                                                                 +
+                       |         "dependency": -1                                                                                                                                                                   +
+                       |         "degree": 1.000000,                                                                                                                                                                +
+                       |         ],                                                                                                                                                                                 +
+                       |         "dependency": -2                                                                                                                                                                   +
+                       |         "degree": 1.000000,                                                                                                                                                                +
+                       |             3                                                                                                                                                                              +
+                       |         ],                                                                                                                                                                                 +
+                       |         "dependency": 2                                                                                                                                                                    +
+                       |         "degree": 1.000000,                                                                                                                                                                +
+                       |             3                                                                                                                                                                              +
+                       |         ],                                                                                                                                                                                 +
+                       |         "dependency": -1                                                                                                                                                                   +
+                       |         "degree": 1.000000,                                                                                                                                                                +
+                       |             3                                                                                                                                                                              +
+                       |         ],                                                                                                                                                                                 +
+                       |         "dependency": -2                                                                                                                                                                   +
+                       |         "degree": 0.500000,                                                                                                                                                                +
+                       |         ],                                                                                                                                                                                 +
+                       |         "dependency": 2                                                                                                                                                                    +
+                       |         "degree": 0.500000,                                                                                                                                                                +
+                       |         ],                                                                                                                                                                                 +
+                       |         "dependency": 3                                                                                                                                                                    +
+                       |         "degree": 1.000000,                                                                                                                                                                +
+                       |         ],                                                                                                                                                                                 +
+                       |         "dependency": -2                                                                                                                                                                   +
+                       |         "degree": 0.500000,                                                                                                                                                                +
+                       |         ],                                                                                                                                                                                 +
+                       |         "dependency": 2                                                                                                                                                                    +
+                       |         "degree": 0.500000,                                                                                                                                                                +
+                       |         ],                                                                                                                                                                                 +
+                       |         "dependency": 3                                                                                                                                                                    +
+                       |         "degree": 1.000000,                                                                                                                                                                +
+                       |         ],                                                                                                                                                                                 +
+                       |         "dependency": -1                                                                                                                                                                   +
+                       |         "degree": 1.000000,                                                                                                                                                                +
+                       |             3                                                                                                                                                                              +
+                       |         ],                                                                                                                                                                                 +
+                       |         "dependency": -1                                                                                                                                                                   +
+                       |         "degree": 1.000000,                                                                                                                                                                +
+                       |             3                                                                                                                                                                              +
+                       |         ],                                                                                                                                                                                 +
+                       |         "dependency": -2                                                                                                                                                                   +
+                       |         "degree": 1.000000,                                                                                                                                                                +
+                       |         ],                                                                                                                                                                                 +
+                       |         "dependency": 3                                                                                                                                                                    +
+                       |         "degree": 1.000000,                                                                                                                                                                +
+                       |         ],                                                                                                                                                                                 +
+                       |         "dependency": -2                                                                                                                                                                   +
+                       |         "degree": 1.000000,                                                                                                                                                                +
+                       |         ],                                                                                                                                                                                 +
+                       |         "dependency": 3                                                                                                                                                                    +
+                       |         "degree": 1.000000,                                                                                                                                                                +
+                       |         ],                                                                                                                                                                                 +
+                       |         "dependency": -1                                                                                                                                                                   +
+                       |         "degree": 1.000000,                                                                                                                                                                +
+                       |         ],                                                                                                                                                                                 +
+                       |         "dependency": 2                                                                                                                                                                    +
+                       |         "degree": 1.000000,                                                                                                                                                                +
+                       |         ],                                                                                                                                                                                 +
+                       |         "dependency": -2                                                                                                                                                                   +
+                       |         "degree": 1.000000,                                                                                                                                                                +
+                       |         ],                                                                                                                                                                                 +
+                       |         "dependency": 2                                                                                                                                                                    +
+                       |         "degree": 1.000000,                                                                                                                                                                +
+                       |         ],                                                                                                                                                                                 +
+                       |         "dependency": -1                                                                                                                                                                   +
+                       |         "degree": 0.500000,                                                                                                                                                                +
+                       |         ],                                                                                                                                                                                 +
+                       |         "dependency": 2                                                                                                                                                                    +
+                       |         "degree": 0.500000,                                                                                                                                                                +
+                       |         ],                                                                                                                                                                                 +
+                       |         "dependency": 3                                                                                                                                                                    +
+                       |         "degree": 1.000000,                                                                                                                                                                +
+                       |         ],                                                                                                                                                                                 +
+                       |         "dependency": -1                                                                                                                                                                   +
+                       |         "degree": 1.000000,                                                                                                                                                                +
+                       |         ],                                                                                                                                                                                 +
+                       |         "dependency": 3                                                                                                                                                                    +
+                       |         "degree": 1.000000,                                                                                                                                                                +
+                       |         ],                                                                                                                                                                                 +
+                       |         "dependency": 2                                                                                                                                                                    +
index d140733a750..48a03f5b803 100644

 

@@ -970,4 +976,362 @@ AND tablename = 'stats_temp'
+        'n_distinct', '[{"attributes" : [3,-1], "ndistinct" : 4},
+                        {"attributes" : [2,3,-1,-2,1], "ndistinct" : 4}]'::pg_ndistinct
+        'n_distinct', '[{"attributes" : [3,-1], "ndistinct" : 4},
+                        {"attributes" : [2,3,-1,-2,0], "ndistinct" : 4}]'::pg_ndistinct
+        'n_distinct', '[{"attributes" : [3,-1], "ndistinct" : 4},
+                        {"attributes" : [-4,3,-1], "ndistinct" : 4},
+                        {"attributes" : [-4,3,-2], "ndistinct" : 4},
+                        {"attributes" : [-4,-1,-2], "ndistinct" : 4},
+                        {"attributes" : [3,-1,-2,-4], "ndistinct" : 4}]'::pg_ndistinct
+                        {"attributes" : [2,3,-1,-2], "ndistinct" : 4}]'::pg_ndistinct
+    jsonb_pretty(e.n_distinct::text::jsonb) AS n_distinct,
+    e.dependencies,
+    e.most_common_vals, e.most_common_val_nulls,
+        'dependencies', '[{"attributes": [2], "dependency": 1, "degree": 1.000000}]'::pg_dependencies
+        'dependencies', '[{"attributes": [0], "dependency": -1, "degree": 1.000000}]'::pg_dependencies
+        'dependencies', '[{"attributes": [2], "dependency": -3, "degree": 1.000000}]'::pg_dependencies
+    jsonb_pretty(e.n_distinct::text::jsonb) AS n_distinct,
+    jsonb_pretty(e.dependencies::text::jsonb) AS dependencies,
+    e.most_common_vals, e.most_common_val_nulls,
+    jsonb_pretty(e.n_distinct::text::jsonb) AS n_distinct,
+    jsonb_pretty(e.dependencies::text::jsonb) AS dependencies,
+    e.most_common_vals, e.most_common_val_nulls,

From c726bc2486b64298688eadeb11935f3e7637398e Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Wed, 5 Nov 2025 01:13:04 -0500
Subject: [PATCH v11 9/9] Include Extended Statistics in pg_dump.

 

 	PQclear(res);
 
+/*
 
+	#

---

From: Michael Paquier <[email-masked]>
Date: Wed, 12 Nov 2025 16:53:50 +0900

> Agreed, 0001-0004 all look good.

Applied 0001 and 0002 for now.
--
Michael

---

From: Man Zeng <[email-masked]>
Date: Wed, 12 Nov 2025 08:45:16 +0000

Hey, I saw that some comments in the recent commit have inconsistent styles—maybe we can tweak them to make them uniform!

```c
// pg_dependencies    - output routine for type pg_dependencies.
pg_dependencies_out   - output routine for type pg_dependencies.

// pg_ndistinct
//    output routine for type pg_ndistinct
pg_ndistinct_out
    output routine for type pg_ndistinct
```

Oh, I didn’t operate it properly, which resulted in the creation of a duplicate theme.
https://www.postgresql.org/message-id/tencent_6EC50DD07D5D567C15E65C46%40qq.com
--
Man Zeng

---

From: Michael Paquier <[email-masked]>
Date: Wed, 12 Nov 2025 20:25:43 +0900

> https://www.postgresql.org/message-id/tencent_6EC50DD07D5D567C15E65C46%40qq.com

No problem.  I've just fixed both comments, thanks.  These were older
than the recent commit, but let's fix things when these are in sight.
--
Michael

---

From: Corey Huinker <[email-masked]>
Date: Wed, 12 Nov 2025 17:21:36 -0500

> item.ndistinct is double type, we just cast it to int type?

It's a historical quirk. That's what the original output function did in
mvdistinct.c, so we maintain compatibility with that. Altering the internal
storage type would affect the bytea serialization, which would break binary
compatibility.

> after apply 0004, the below in doc/src/sgml/perform.sgml also need to

Yes it does, good catch.

> for example in loop ``for (i = 0; i < ndist->nitems; i++)``. we can change

I agree that that feels more symmetrical. However, it seems the prevailing
wisdom is that we're already paying for a string interpolation in the very
next appendStringInfo(), we might as well save ourselves a function call.
Hence, I left that one as-is.

The sgml change has been worked into a rebased and reduced patch set
(thanks for the commits Michael!)
From efb7965e113e63be2c2b3847cb3a8a9cef98b185 Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Tue, 11 Nov 2025 14:05:49 +0900
Subject: [PATCH v12 1/7] Refactor output format of pg_ndistinct.

The change in format is described from the changes to
src/test/regress/expected/stats_ext.out.
---
 src/include/statistics/statistics_format.h |  30 ++++
 src/backend/utils/adt/pg_ndistinct.c       |  22 +--
 src/test/regress/expected/stats_ext.out    | 156 ++++++++++++++++++---
 src/test/regress/sql/stats_ext.sql         |  12 +-
 doc/src/sgml/perform.sgml                  |  36 ++++-
 5 files changed, 220 insertions(+), 36 deletions(-)
 create mode 100644 src/include/statistics/statistics_format.h

index 667ada9c3b4..97efc290ef5 100644

 
 	initStringInfo(&str);

 		if (i > 0)

 

 ANALYZE ab1;

 -- correct estimates
 
 DROP STATISTICS s10;

 

 ANALYZE ndistinct;

 ANALYZE ndistinct;

 DROP STATISTICS s10;

 ANALYZE ndistinct;

 ANALYZE ndistinct;
 
-SELECT s.stxkind, d.stxdndistinct
+SELECT s.stxkind, jsonb_pretty(d.stxdndistinct::text::jsonb) AS stxdndistinct
   FROM pg_statistic_ext s, pg_statistic_ext_data d
  WHERE s.stxrelid = 'ndistinct'::regclass
    AND d.stxoid = s.oid;
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index 106583fb296..b2dc2d27a77 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -1576,12 +1576,42 @@ CREATE STATISTICS stts2 (ndistinct) ON city, state, zip FROM zipcodes;
 
 ANALYZE zipcodes;
 
-SELECT stxkeys AS k, stxdndistinct AS nd
+SELECT stxkeys AS k, jsonb_pretty(stxdndistinct::text::jsonb) AS nd
   FROM pg_statistic_ext join pg_statistic_ext_data on (oid = stxoid)
   WHERE stxname = 'stts2';
--[ RECORD 1 ]------------------------------------------------------&zwsp;--
+-[ RECORD 1 ]-------------------
 k  | 1 2 5
-nd | {"1, 2": 33178, "1, 5": 33178, "2, 5": 27435, "1, 2, 5": 33178}
+nd | [                          +
+   |     {                      +
+   |         "ndistinct": 33178,+
+   |         "attributes": [    +
+   |             1,             +
+   |             2              +
+   |         ]                  +
+   |     },                     +
+   |     {                      +
+   |         "ndistinct": 33178,+
+   |         "attributes": [    +
+   |             1,             +
+   |             5              +
+   |         ]                  +
+   |     },                     +
+   |     {                      +
+   |         "ndistinct": 27435,+
+   |         "attributes": [    +
+   |             2,             +
+   |             5              +
+   |         ]                  +
+   |     },                     +
+   |     {                      +
+   |         "ndistinct": 33178,+
+   |         "attributes": [    +
+   |             1,             +
+   |             2,             +
+   |             5              +
+   |         ]                  +
+   |     }                      +
+   | ]
 (1 row)
 </programlisting>
      This indicates that there are three combinations of columns that

base-commit: d36acd6f5c924bfe6a8618df49512cf05f898324
-- 
2.51.1

From 5bb23d3430bcb654efb8ae76c59c4b6f33689c45 Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Tue, 11 Nov 2025 14:15:27 +0900
Subject: [PATCH v12 2/7] Refactor output format of pg_dependencies.

The change in format is adequately described from the changes to
src/test/regress/expected/stats_ext.out so description here is
redundant.
---
 src/include/statistics/statistics_format.h | 20 ++++-
 src/backend/utils/adt/pg_dependencies.c    | 31 +++----
 src/test/regress/expected/stats_ext.out    | 95 ++++++++++++++++++++--
 src/test/regress/sql/stats_ext.sql         |  7 +-
 doc/src/sgml/perform.sgml                  |  6 +-
 5 files changed, 127 insertions(+), 32 deletions(-)

+
+/* ----------
index a0a9440fd5c..87181aa00e9 100644

 /*
 
 	initStringInfo(&str);

 		if (i > 0)

 

 ANALYZE ab1;

 

 

diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index b2dc2d27a77..014a542daf5 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -1488,9 +1488,9 @@ ANALYZE zipcodes;
 SELECT stxname, stxkeys, stxddependencies
   FROM pg_statistic_ext join pg_statistic_ext_data on (oid = stxoid)
   WHERE stxname = 'stts';
- stxname | stxkeys |             stxddependencies
----------+---------+------------------------------------------
- stts    | 1 5     | {"1 => 5": 1.000000, "5 => 1": 0.423130}
+ stxname | stxkeys |                                                stxddependencies
+---------+---------+----------------------------------------------------------------------------------------------------------------------
+ stts    | 1 5     | [{"attributes": [1], "dependency": 5, "degree": 1.000000}, {"attributes": [5], "dependency": 1, "degree": 0.423130}]
 (1 row)
 </programlisting>
      Here it can be seen that column 1 (zip code) fully determines column
-- 
2.51.1

From 27fa60cf482bcf98253ee488f34f935e2211d728 Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Tue, 11 Nov 2025 16:47:00 +0900
Subject: [PATCH v12 3/7] Add working input function for pg_ndistinct.

diff --git a/src/backend/utils/adt/pg_ndistinct.c b/src/backend/utils/adt/pg_ndistinct.c
index 97efc290ef5..96eaa09b4ed 100644
--- a/src/backend/utils/adt/pg_ndistinct.c
+++ b/src/backend/utils/adt/pg_ndistinct.c
@@ -14,34 +14,615 @@
 
 #include "postgres.h"
 
+#include "common/int.h"
 
+typedef enum
+{
 
 /*
  * pg_ndistinct_in

 /*
  * pg_ndistinct_out
 
 # ----------

From 6e50b2c66b30c1fed386412493010a268ec6bed5 Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Tue, 11 Nov 2025 16:55:47 +0900
Subject: [PATCH v12 4/7] Add working input function for pg_dependencies.

diff --git a/src/backend/utils/adt/pg_dependencies.c b/src/backend/utils/adt/pg_dependencies.c
index 87181aa00e9..f40394fc359 100644
--- a/src/backend/utils/adt/pg_dependencies.c
+++ b/src/backend/utils/adt/pg_dependencies.c
@@ -14,29 +14,629 @@
 
 #include "postgres.h"
 
+#include "common/int.h"
 
+typedef enum
+{
+	DEPS_EXPECT_START = 0,
+	DEPS_EXPECT_ITEM,
+	DEPS_EXPECT_KEY,

 /*
 
 # ----------

From e14eba2804438d3910611d7ba7343d4cec47d460 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 4 Nov 2025 23:50:01 -0500
Subject: [PATCH v12 5/7] Expose attribute statistics functions for use in
 extended_stats.

 
 
+/*
 
+/*

 /*

 
 
 		if (converted)
 		{
 
 		if (converted)
 		{

 
 		if (converted)
 		{

 	/*
 
 		if (converted)
 		{
 
 		if (converted)
 		{

From e0e73d1521c4d36630856d6f082d04b8e1212b82 Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Tue, 11 Nov 2025 16:58:26 +0900
Subject: [PATCH v12 6/7] Add extended statistics support functions.

 

+/*
 
+/*

+/*

 
 /*

 
 	return result;
 }
+
+static HeapTuple
 
 	return s;
 }
+
+/*
 
+/*
 
 DROP TABLE stats_temp;

 

From 22709b3677244df40096a2cc959a89a00313496e Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Wed, 5 Nov 2025 01:13:04 -0500
Subject: [PATCH v12 7/7] Include Extended Statistics in pg_dump.

 

 	PQclear(res);
 
+/*
 
+	#

---

From: jian he <[email-masked]>
Date: Thu, 13 Nov 2025 15:01:42 +0800

hi.

v12-0001 and v12-0002 overall look good to me.

        if (dependency->nattributes <= 1)
            elog(ERROR, "invalid zero-length nattributes array in
MVDependencies");
This is an unlikely-to-happen error message,  but still, “nattributes”
seems confusing?

in  doc/src/sgml/func/func-json.sgml, we have \gset
<programlisting>
SELECT '{
.......
  }
}' AS json \gset
</programlisting>

similarly, in doc/src/sgml/perform.sgml, I think the query should be:

SELECT stxkeys AS k, jsonb_pretty(stxdndistinct::text::jsonb) AS nd
  FROM pg_statistic_ext join pg_statistic_ext_data on (oid = stxoid)
  WHERE stxname = 'stts2'    \gx

---

From: Corey Huinker <[email-masked]>
Date: Thu, 13 Nov 2025 02:32:17 -0500

> This is an unlikely-to-happen error message,  but still, “nattributes”

Agreed the error message should be changed if it's kept at all. That error
may never occur now that we test for empty arrays in the array close event
handler. So maybe this becomes a plain assert().

>   WHERE stxname = 'stts2'    \gx

The example almost certainly predates \gx, so that's a good suggestion.

---

From: jian he <[email-masked]>
Date: Thu, 13 Nov 2025 19:51:00 +0800

hi.
now looking at v12-0003-Add-working-input-function-for-pg_ndistinct.patch
again.

+ * example input:

extenssted statistics surely won't work on system columns,
how should we deal with case like:
```
{"attributes": [6, -1], "ndistinct": 14}
{"attributes": [6, -7], "ndistinct": 14},
```
issue a warning or error out saying that your attribute number is invalid?
Should we discourage using system columns as examples in comments here?

I have added more test code in src/test/regress/sql/pg_ndistinct.sql,
to improve the code coverage.

as mentioned before in
https://postgr.es/m/CACJufxEZYqocFdgn-x-bJMRBSk_zkS=[email-masked]
I think it's a good thing to change
``(errcode....``
to
``errcode``.
So I did the change.

this (and many other places) looks wrong, because
ereturn would really return ``(Datum) 0``, and this function returns
JsonParseErrorType.
so we have to errsave here.

+typedef struct
+{
+ const char *str;

NDistinctParseState.ndistinct should be integer,
otherwise pg_ndistinct_out will not be consistent with  pg_ndistinct_in?

SELECT '[{"attributes" : [1, 2], "ndistinct" :
2147483648}]'::pg_ndistinct; --error
                    pg_ndistinct
----------------------------------------------------
 [{"attributes": [1, 2], "ndistinct": -2147483648}]
(1 row)

The result seems not what we expected.

---

From: Corey Huinker <[email-masked]>
Date: Fri, 14 Nov 2025 00:49:23 -0500

> Should we discourage using system columns as examples in comments here?

Negative numbers represent the Nth expression defined in the extended
statistics object. So if you have extended statistics on a, b, length(a),
length(b) then you can legally have -1 and -2 in the attributes, but
nothing lower than that.
See functions pg_ndistinct_validate_items() and
pg_depdendencies_validate_deps() as these check the attributes in the value
against the definition of the extended stats object.

Though this does bring up a small naming problem: Elements of a
pg_dependencies are Dependency, abbreviated to dep, but are also called
Items like the elements in a pg_ndistinct. We should pick a standard name
for such things (probably item) and use it everywhere.

> to improve the code coverage.

I'm trying to implement those test cases, but I may have missed some.

> so we have to errsave here.

Good point. Implemented.

+1

Implemented many, but not all of these suggestions.
From c1967c4552997288f4342af8ed0215d967ea8c66 Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Tue, 11 Nov 2025 14:05:49 +0900
Subject: [PATCH v13 1/7] Refactor output format of pg_ndistinct.

 

 	initStringInfo(&str);

 		if (i > 0)

 

 ANALYZE ab1;

 -- correct estimates
 
 DROP STATISTICS s10;

 

 ANALYZE ndistinct;

 ANALYZE ndistinct;

 DROP STATISTICS s10;

 ANALYZE ndistinct;

 ANALYZE ndistinct;

 ANALYZE zipcodes;

base-commit: 910690415b661186ae44e3b5e538e23eaa48de1b
-- 
2.51.1

From 1a29c0948cb9f1d261b96c71c820c5e113d09599 Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Tue, 11 Nov 2025 14:15:27 +0900
Subject: [PATCH v13 2/7] Refactor output format of pg_dependencies.

 
+
+/* ----------

 /*
 
 	initStringInfo(&str);

 		if (i > 0)

 

 ANALYZE ab1;

 

 

From d7db153b42945ea3d29a82437bd1cbee77057b84 Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Tue, 11 Nov 2025 16:47:00 +0900
Subject: [PATCH v13 3/7] Add working input function for pg_ndistinct.

This will be needed for importing extended statistics.
---
 src/backend/utils/adt/pg_ndistinct.c       | 701 ++++++++++++++++++++-
 src/test/regress/expected/pg_ndistinct.out | 109 ++++
 src/test/regress/parallel_schedule         |   2 +-
 src/test/regress/sql/pg_ndistinct.sql      |  34 +
 4 files changed, 834 insertions(+), 12 deletions(-)
 create mode 100644 src/test/regress/expected/pg_ndistinct.out
 create mode 100644 src/test/regress/sql/pg_ndistinct.sql

diff --git a/src/backend/utils/adt/pg_ndistinct.c b/src/backend/utils/adt/pg_ndistinct.c
index 97efc290ef5..86460db86ee 100644
--- a/src/backend/utils/adt/pg_ndistinct.c
+++ b/src/backend/utils/adt/pg_ndistinct.c
@@ -14,42 +14,721 @@
 
 #include "postgres.h"
 
+#include "common/int.h"
 
+typedef enum
+{
+	NDIST_EXPECT_START = 0,
+	NDIST_EXPECT_ITEM,
+	NDIST_EXPECT_KEY,
+	NDIST_EXPECT_ATTNUM_LIST,
+	NDIST_EXPECT_ATTNUM,
+	NDIST_EXPECT_NDISTINCT,
+	NDIST_EXPECT_COMPLETE
+} NDistinctSemanticState;
+
+typedef struct
+{
+	const char *str;
+	NDistinctSemanticState state;
+
+	List	   *distinct_items; /* Accumulated complete MVNDistinctItems */
+	Node	   *escontext;
+
+	bool		found_attributes;	/* Item has an attributes key */
+	bool		found_ndistinct;	/* Item has ndistinct key */
+	List	   *attnum_list;	/* Accumulated attributes attnums */
+	int32		ndistinct;
+} NDistinctParseState;
+
+/*
+ * Invoked at the start of each MVNDistinctItem.
+ *
+ * The entire JSON document shoul be one array of MVNDistinctItem objects.
+ *
+ * If we're anywhere else in the document, it's an error.
+ */
+static JsonParseErrorType
+ndistinct_object_start(void *state)
+{
+	NDistinctParseState *parse = state;
+
+	switch(parse->state)
+	{
+		case NDIST_EXPECT_ITEM:
+			/* Now we expect to see attributes/ndistinct keys */
+			parse->state = NDIST_EXPECT_KEY;
+			return JSON_SUCCESS;
+			break;
+
+		case NDIST_EXPECT_START:
+			/* pg_ndistinct must begin with a '[' */
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("Initial element must be an array."));
+			break;
+
+		case NDIST_EXPECT_KEY:
+			/* In an object, expecting key */
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("Expected an object key."));
+			break;
+
+		case NDIST_EXPECT_ATTNUM_LIST:
+			/* Just followed an "attributes": key */
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("Value of \"" PG_NDISTINCT_KEY_ATTRIBUTES
+							  "\" must be an array of attribute numbers."));
+			break;
+
+		case NDIST_EXPECT_ATTNUM:
+			/* In an attnum list, expect only scalar integers */
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("Attribute lists can only contain attribute numbers."));
+			break;
+
+		case NDIST_EXPECT_NDISTINCT:
+			/* Just followed an "ndistinct": key */
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("Value of \"" PG_NDISTINCT_KEY_NDISTINCT
+							  "\" must be an integer."));
+			break;
+
+		default:
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("Unexpected parse state: %d", (int) parse->state));
+	}
+
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ * Routine to allow qsorting of AttNumbers
+ */
+static int
+attnum_compare(const void *aptr, const void *bptr)
+{
+	AttrNumber	a = *(const AttrNumber *) aptr;
+	AttrNumber	b = *(const AttrNumber *) bptr;
+
+	return pg_cmp_s16(a, b);
+}
+
+
+/*
+ * Invoked at the end of an object.
+ *
+ * Check to ensure that it was a complete MVNDistinctItem
+ *
+ */
+static JsonParseErrorType
+ndistinct_object_end(void *state)
+{
+	NDistinctParseState *parse = state;
+
+	int			natts = 0;
+	AttrNumber *attrsort;
+
+	MVNDistinctItem *item;
+
+	if (parse->state != NDIST_EXPECT_KEY)
+	{
+		errsave(parse->escontext,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+				errdetail("Unexpected parse state: %d", (int) parse->state));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	if (!parse->found_attributes)
+	{
+		errsave(parse->escontext,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+				errdetail("Item must contain \"" PG_NDISTINCT_KEY_ATTRIBUTES "\" key."));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	if (!parse->found_ndistinct)
+	{
+		errsave(parse->escontext,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+				errdetail("Item must contain \"" PG_NDISTINCT_KEY_NDISTINCT "\" key."));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	/*
+	 * We need at least 2 attnums for a ndistinct item, anything less is
+	 * malformed.
+	 */
+	natts = parse->attnum_list->length;
+	if (natts < 2)
+	{
+		errsave(parse->escontext,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+				errdetail("The \"" PG_NDISTINCT_KEY_ATTRIBUTES
+						   "\" key must contain an array of at least two attnums."));
+		return JSON_SEM_ACTION_FAILED;
+	}
+	attrsort = palloc0(natts * sizeof(AttrNumber));
+
+	/* Create the MVNDistinctItem */
+	item = palloc(sizeof(MVNDistinctItem));
+	item->nattributes = natts;
+	item->attributes = palloc0(natts * sizeof(AttrNumber));
+	item->ndistinct = (double) parse->ndistinct;
+
+	/* fill out both attnum list and sortable list */
+	for (int i = 0; i < natts; i++)
+	{
+		attrsort[i] = (AttrNumber) parse->attnum_list->elements[i].int_value;
+		item->attributes[i] = attrsort[i];
+	}
+
+	/* Check attrsort for uniqueness */
+	qsort(attrsort, natts, sizeof(AttrNumber), attnum_compare);
+	for (int i = 1; i < natts; i++)
+	{
+		if (attrsort[i] == attrsort[i - 1])
+		{
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("attnum list duplicate value found: %d.", attrsort[i]));
+			pfree(attrsort);
+			pfree(item->attributes);
+			pfree(item);
+			return JSON_SEM_ACTION_FAILED;
+		}
+	}
+	pfree(attrsort);
+
+	parse->distinct_items = lappend(parse->distinct_items, (void *) item);
+
+	/* reset item state vars */
+	list_free(parse->attnum_list);
+	parse->attnum_list = NIL;
+	parse->ndistinct = 0;
+	parse->found_attributes = false;
+	parse->found_ndistinct = false;
+
+	/* Now we are looking for the next MVNDistinctItem */
+	parse->state = NDIST_EXPECT_ITEM;
+	return JSON_SUCCESS;
+}
+
+
+/*
+ * ndsitinct input format has two types of arrays, the outer MVNDistinctItem
+ * array, and the attnum list array within each MVNDistinctItem.
+ */
+static JsonParseErrorType
+ndistinct_array_start(void *state)
+{
+	NDistinctParseState *parse = state;
+
+	switch (parse->state)
+	{
+		case NDIST_EXPECT_ATTNUM_LIST:
+			parse->state = NDIST_EXPECT_ATTNUM;
+			break;
+
+		case NDIST_EXPECT_START:
+			parse->state = NDIST_EXPECT_ITEM;
+			break;
+
+		default:
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("Array found in unexpected place."));
+			return JSON_SEM_ACTION_FAILED;
+	}
+
+	return JSON_SUCCESS;
+}
+
+
+/*
+ * Arrays can never be empty.
+ */
+static JsonParseErrorType
+ndistinct_array_end(void *state)
+{
+	NDistinctParseState *parse = state;
+
+	switch (parse->state)
+	{
+		case NDIST_EXPECT_ATTNUM:
+			if (parse->attnum_list != NIL)
+			{
+				/* The attnum list is complete, look for more MVNDistinctItem keys */
+				parse->state = NDIST_EXPECT_KEY;
+				return JSON_SUCCESS;
+			}
+
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("The \"" PG_NDISTINCT_KEY_ATTRIBUTES
+							"\" key must be an non-empty array."));
+			break;
+
+		case NDIST_EXPECT_ITEM:
+			if (parse->distinct_items != NIL)
+			{
+				/* Item list is complete, we're done. */
+				parse->state = NDIST_EXPECT_COMPLETE;
+				return JSON_SUCCESS;
+			}
+
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("Item array cannot be empty."));
+			break;
+		default:
+			/*
+			 * This can only happen if a case was missed in ndistinct_array_start()
+			 */
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("Array found in unexpected place."));
+	}
+
+	return JSON_SEM_ACTION_FAILED;
+}
+
+
+/*
+ * The valid keys for the MVNDistinctItem object are:
+ *   - attributes
+ *   - ndistinct
+ */
+static JsonParseErrorType
+ndistinct_object_field_start(void *state, char *fname, bool isnull)
+{
+	NDistinctParseState *parse = state;
+
+	if (strcmp(fname, PG_NDISTINCT_KEY_ATTRIBUTES) == 0)
+	{
+		parse->found_attributes = true;
+		parse->state = NDIST_EXPECT_ATTNUM_LIST;
+		return JSON_SUCCESS;
+	}
+
+	if (strcmp(fname, PG_NDISTINCT_KEY_NDISTINCT) == 0)
+	{
+		parse->found_ndistinct = true;
+		parse->state = NDIST_EXPECT_NDISTINCT;
+		return JSON_SUCCESS;
+	}
+
+	errsave(parse->escontext,
+			errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+			errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+			errdetail("Invalid key \"%s\". Only allowed keys are \""
+					  PG_NDISTINCT_KEY_ATTRIBUTES "\" and \""
+					  PG_NDISTINCT_KEY_NDISTINCT "\".", fname));
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ * Handle any array element.
+ */
+static JsonParseErrorType
+ndistinct_array_element_start(void *state, bool isnull)
+{
+	NDistinctParseState *parse = state;
+
+	switch(parse->state)
+	{
+		case NDIST_EXPECT_ATTNUM:
+			if (!isnull)
+				return JSON_SUCCESS;
+
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("Attnum list elements cannot be null."));
+
+			break;
+
+		case NDIST_EXPECT_ITEM:
+			if (!isnull)
+				return JSON_SUCCESS;
+
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("Item list elements cannot be null."));
+
+			break;
+
+		default:
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("Unexpected array element."));
+	}
+
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ * Handle scalar events from the ndistinct input parser.
+ *
+ * Override integer parse error messages and replace them with errors
+ * specific to the context.
+ */
+static JsonParseErrorType
+ndistinct_scalar(void *state, char *token, JsonTokenType tokentype)
+{
+	NDistinctParseState *parse = state;
+	AttrNumber	attnum;
+
+	switch(parse->state)
+	{
+		case NDIST_EXPECT_ATTNUM:
+			attnum = pg_strtoint16_safe(token, parse->escontext);
+
+			if (!SOFT_ERROR_OCCURRED(parse->escontext))
+			{
+				parse->attnum_list = lappend_int(parse->attnum_list, (int) attnum);
+				return JSON_SUCCESS;
+			}
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("Invalid attribute number: \"%s\".", token));
+			break;
+
+		case NDIST_EXPECT_NDISTINCT:
+			/*
+			* While the structure dictates that ndistinct in a double precision
+			* floating point, in practice it has always been an integer, and it
+			* is output as such. Therefore, we follow usage precendent over the
+			* actual storage structure, and read it in as an integer.
+			*/
+			parse->ndistinct = pg_strtoint32_safe(token, parse->escontext);
+
+			if (!SOFT_ERROR_OCCURRED(parse->escontext))
+			{
+				parse->state = NDIST_EXPECT_KEY;
+				return JSON_SUCCESS;
+			}
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("Invalid " PG_NDISTINCT_KEY_NDISTINCT " value: \"%s\".",
+							  token));
+			break;
+
+		default:
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("Unexpected scalar \"%s\".", token));
+	}
+
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ * Compare the attribute arrays of two MVNDistinctItem values,
+ * looking for duplicate sets.
+ */
+static
+bool has_duplicate_attributes(const MVNDistinctItem *a,
+							  const MVNDistinctItem *b)
+{
+	if (a->nattributes != b->nattributes)
+		return false;
+
+	for (int i = 0; i < a->nattributes; i++)
+	{
+		if (a->attributes[i] != b->attributes[i])
+			return false;
+	}
+
+	return true;
+}
+
+/*
+ * Ensure that an attnum appears as one of the attnums in a given
+ * MVNDistinctItem.
+ */
+static
+bool item_has_attnum(const MVNDistinctItem *item, AttrNumber attnum)
+{
+	for (int i = 0; i < item->nattributes; i++)
+	{
+		if (attnum == item->attributes[i])
+			return true;
+	}
+	return false;
+}
+
+/*
+ * Ensure that the attributes of one MVNDistinctItem A are a proper subset
+ * of the reference MVNDistinctItem B.
+ */
+static
+bool item_is_attnum_subset(const MVNDistinctItem *item,
+						   const MVNDistinctItem *refitem)
+{
+	for (int i = 0; i < item->nattributes; i++)
+	{
+		if (!item_has_attnum(refitem,item->attributes[i]))
+			return false;
+	}
+	return true;
+}
+
+/*
+ * Generate a string representing an array of attnum.
+ *
+ * Freeing the allocated string is responsibility of the caller.
+ */
+static
+const char *item_attnum_list(const MVNDistinctItem *item)
+{
+	StringInfoData	str;
+
+	initStringInfo(&str);
+
+	appendStringInfo(&str, "%d", item->attributes[0]);
+
+	for (int i = 1; i < item->nattributes; i++)
+		appendStringInfo(&str, ", %d", item->attributes[i]);
+
+	return str.data;
+}
 
 /*
  * pg_ndistinct_in
 
-	PG_RETURN_VOID();			/* keep compiler quiet */
+	NDistinctParseState parse_state;
+	JsonParseErrorType result;
+	JsonLexContext *lex;
+	JsonSemAction sem_action;
+
+	int		item_most_attrs = 0;
+	int		item_most_attrs_idx = 0;
+
+	/* initialize semantic state */
+	parse_state.str = str;
+	parse_state.state = NDIST_EXPECT_START;
+	parse_state.distinct_items = NIL;
+	parse_state.escontext = fcinfo->context;
+	parse_state.found_attributes = false;
+	parse_state.found_ndistinct = false;
+	parse_state.attnum_list = NIL;
+	parse_state.ndistinct = 0;
+
+	/* set callbacks */
+	sem_action.semstate = (void *) &parse_state;
+	sem_action.object_start = ndistinct_object_start;
+	sem_action.object_end = ndistinct_object_end;
+	sem_action.array_start = ndistinct_array_start;
+	sem_action.array_end = ndistinct_array_end;
+	sem_action.object_field_start = ndistinct_object_field_start;
+	sem_action.object_field_end = NULL;
+	sem_action.array_element_start = ndistinct_array_element_start;
+	sem_action.array_element_end = NULL;
+	sem_action.scalar = ndistinct_scalar;
+
+	lex = makeJsonLexContextCstringLen(NULL, str, strlen(str),
+									   PG_UTF8, true);
+	result = pg_parse_json(lex, &sem_action);
+	freeJsonLexContext(lex);
+
+	if (result == JSON_SUCCESS)
+	{
+		MVNDistinct *ndistinct;
+		int			nitems = parse_state.distinct_items->length;
+		bytea	   *bytes;
+
+		switch(parse_state.state)
+		{
+			case NDIST_EXPECT_COMPLETE:
+				/*
+				 * Parse ended in the expected place. We should have a list of items,
+				 * but if we don't it is because there are bugs in other parse steps.
+				 */
+				if (parse_state.distinct_items == NIL)
+					elog(ERROR,
+						 "pg_ndistinct parssing claims success with an empty item list.");
+
+				break;
+
+			case NDIST_EXPECT_START:
+				/* blank */
+				errsave(parse_state.escontext,
+						errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+						errmsg("malformed pg_ndistinct: \"%s\"", str),
+						errdetail("Value cannot be empty."));
+				PG_RETURN_NULL();
+				break;
+
+			default:
+				/* Unexpected end-state. TODO: Is this an elog()? */
+				errsave(parse_state.escontext,
+						errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+						errmsg("malformed pg_ndistinct: \"%s\"", str),
+						errdetail("Unexpected end state %d.", parse_state.state));
+				PG_RETURN_NULL();
+				break;
+		}
+
+		ndistinct = palloc(offsetof(MVNDistinct, items) +
+						   nitems * sizeof(MVNDistinctItem));
+
+		ndistinct->magic = STATS_NDISTINCT_MAGIC;
+		ndistinct->type = STATS_NDISTINCT_TYPE_BASIC;
+		ndistinct->nitems = nitems;
+
+		for (int i = 0; i < nitems; i++)
+		{
+			MVNDistinctItem *item = parse_state.distinct_items->elements[i].ptr_value;
+
+			/*
+			 * Ensure that this item does not duplicate the attributes of any
+			 * pre-existing item.
+			 */
+			for (int j = 0; j < i; j++)
+			{
+				if (has_duplicate_attributes(item, &ndistinct->items[j]))
+				{
+					errsave(parse_state.escontext,
+							errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+							errmsg("malformed pg_ndistinct: \"%s\"", str),
+							errdetail("Duplicate \"" PG_NDISTINCT_KEY_ATTRIBUTES "\" array : [%s]",
+										item_attnum_list(item)));
+					PG_RETURN_NULL();
+				}
+			}
+
+			ndistinct->items[i].ndistinct = item->ndistinct;
+			ndistinct->items[i].nattributes = item->nattributes;
+			ndistinct->items[i].attributes = item->attributes;
+
+			/*
+			 * Keep track of the first longest attribute list. All other attribute
+			 * lists must be a subset of this list.
+			 */
+			if (item->nattributes > item_most_attrs)
+			{
+				item_most_attrs = item->nattributes;
+				item_most_attrs_idx = i;
+			}
+
+			/*
+			 * Free the MVNDistinctItem, but not the attributes we're still
+			 * using.
+			 */
+			pfree(item);
+		}
+
+		/*
+		 * Verify that all attnum sets are a proper subset of the first longest
+		 * attnum set.
+		 */
+		for (int i = 0; i < nitems; i++)
+		{
+			if (i == item_most_attrs_idx)
+				continue;
+
+			if (!item_is_attnum_subset(&ndistinct->items[i],
+									   &ndistinct->items[item_most_attrs_idx]))
+			{
+				const MVNDistinctItem  *item = &ndistinct->items[i];
+				const MVNDistinctItem  *refitem = &ndistinct->items[item_most_attrs_idx];
+				const char *item_list = item_attnum_list(item);
+				const char *refitem_list = item_attnum_list(refitem);
+
+				errsave(parse_state.escontext,
+						errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+						errmsg("malformed pg_ndistinct: \"%s\"", str),
+						errdetail("\"" PG_NDISTINCT_KEY_ATTRIBUTES "\" array: [%s]"
+								  "must be a subset of array: [%s]",
+								  item_list, refitem_list));
+				PG_RETURN_NULL();
+			}
+		}
+
+		bytes = statext_ndistinct_serialize(ndistinct);
+
+		list_free(parse_state.distinct_items);
+		for (int i = 0; i < nitems; i++)
+			pfree(ndistinct->items[i].attributes);
+		pfree(ndistinct);
+
+		PG_RETURN_BYTEA_P(bytes);
+	}
+
+	/*
+	 * If escontext already set, just use that.
+	 * Anything else is a generic JSON parse error.
+	 */
+	if (!SOFT_ERROR_OCCURRED(parse_state.escontext))
+		errsave(parse_state.escontext,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("malformed pg_ndistinct: \"%s\"", str),
+				errdetail("Must be valid JSON."));
+
+	PG_RETURN_NULL();
 }
 
 /*
  * pg_ndistinct_out
+	int32			i;
+	StringInfoData	str;
 
 	initStringInfo(&str);
 	appendStringInfoChar(&str, '[');
index 00000000000..975a9eff09d
+DETAIL:  Unexpected scalar "null".
+ERROR:  invalid input syntax for type integer: "null"
+ERROR:  invalid input syntax for type integer: "a"
+DETAIL:  Unexpected scalar "1".
+DETAIL:  Unexpected scalar "a".
 
 # ----------

From 5d7dc0ca19ea2a546886eb98f7f29bb1fec0e5ab Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Tue, 11 Nov 2025 16:55:47 +0900
Subject: [PATCH v13 4/7] Add working input function for pg_dependencies.

This will be needed for importing extended statistics.
---
 src/backend/utils/adt/pg_dependencies.c       | 746 +++++++++++++++++-
 src/test/regress/expected/pg_dependencies.out | 128 +++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/pg_dependencies.sql      |  39 +
 4 files changed, 904 insertions(+), 11 deletions(-)
 create mode 100644 src/test/regress/expected/pg_dependencies.out
 create mode 100644 src/test/regress/sql/pg_dependencies.sql

diff --git a/src/backend/utils/adt/pg_dependencies.c b/src/backend/utils/adt/pg_dependencies.c
index 87181aa00e9..fe4aecb2fd7 100644
--- a/src/backend/utils/adt/pg_dependencies.c
+++ b/src/backend/utils/adt/pg_dependencies.c
@@ -14,29 +14,755 @@
 
 #include "postgres.h"
 
+#include "common/int.h"
 
+typedef enum
+{
+	DEPS_EXPECT_START = 0,
+	DEPS_EXPECT_ITEM,
+	DEPS_EXPECT_KEY,
+	DEPS_EXPECT_ATTNUM_LIST,
+	DEPS_EXPECT_ATTNUM,
+	DEPS_EXPECT_DEPENDENCY,
+	DEPS_EXPECT_DEGREE,
+	DEPS_PARSE_COMPLETE
+} DepsParseSemanticState;
+
+typedef struct
+{
+	const char *str;
+	DepsParseSemanticState state;
+
+	List	   *dependency_list;
+	Node	   *escontext;
+
+	bool		found_attributes;	/* Item has an attributes key */
+	bool		found_dependency;	/* Item has an dependency key */
+	bool		found_degree;	/* Item has degree key */
+	List	   *attnum_list;	/* Accumulated attributes attnums */
+	AttrNumber	dependency;
+	double		degree;
+} DependenciesParseState;
+
+/*
+ * Invoked at the start of each MVDependency object.
+ *
+ * The entire JSON document shoul be one array of MVDependency objects.
+ *
+ * If we're anywhere else in the document, it's an error.
+ */
+static JsonParseErrorType
+dependencies_object_start(void *state)
+{
+	DependenciesParseState *parse = state;
+
+	switch(parse->state)
+	{
+		case DEPS_EXPECT_ITEM:
+			/* Now we expect to see attributes/dependency/degree keys */
+			parse->state = DEPS_EXPECT_KEY;
+			return JSON_SUCCESS;
+			break;
+
+		case DEPS_EXPECT_START:
+			/* pg_dependencies must begin with a '[' */
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Initial element must be an array."));
+			break;
+
+		case DEPS_EXPECT_KEY:
+			/* In an object, expecting key */
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Expected an object key."));
+			break;
+
+		case DEPS_EXPECT_ATTNUM_LIST:
+			/* Just followed an "attributes": key */
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Value of \"" PG_DEPENDENCIES_KEY_ATTRIBUTES
+							  "\" must be an array of attribute numbers."));
+			break;
+
+		case DEPS_EXPECT_ATTNUM:
+			/* In an attnum list, expect only scalar integers */
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Attribute lists can only contain attribute numbers."));
+			break;
+
+		case DEPS_EXPECT_DEPENDENCY:
+			/* Just followed an "dependency": key */
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Value of \"" PG_DEPENDENCIES_KEY_DEPENDENCY
+							  "\" must be an attribute number."));
+			break;
+
+		case DEPS_EXPECT_DEGREE:
+			/* Just followed an "degree": key */
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Value of \"" PG_DEPENDENCIES_KEY_DEGREE
+							  "\" must be a number."));
+			break;
+
+		default:
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Unexpected parse state: %d", (int) parse->state));
+	}
+
+	return JSON_SEM_ACTION_FAILED;
+}
+
+static int
+attnum_compare(const void *aptr, const void *bptr)
+{
+	AttrNumber	a = *(const AttrNumber *) aptr;
+	AttrNumber	b = *(const AttrNumber *) bptr;
+
+	return pg_cmp_s16(a, b);
+}
+
+static JsonParseErrorType
+dependencies_object_end(void *state)
+{
+	DependenciesParseState *parse = state;
+
+	MVDependency *dep;
+	AttrNumber *attrsort;
+
+	int			natts = 0;
+
+	if (parse->state != DEPS_EXPECT_KEY)
+	{
+		errsave(parse->escontext,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				errdetail("eUnexpected parse state: %d", (int) parse->state));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	if (!parse->found_attributes)
+	{
+		errsave(parse->escontext,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				errdetail("Item must contain \"" PG_DEPENDENCIES_KEY_ATTRIBUTES "\" key."));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	if (!parse->found_dependency)
+	{
+		errsave(parse->escontext,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				errdetail("Item must contain \"" PG_DEPENDENCIES_KEY_DEPENDENCY "\" key."));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	if (!parse->found_degree)
+	{
+		errsave(parse->escontext,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				errdetail("Item must contain \"" PG_DEPENDENCIES_KEY_DEGREE "\" key."));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	/*
+	 * We need at least 1 attnum for a dependencies item, anything less is
+	 * malformed.
+	 */
+	natts = parse->attnum_list->length;
+	if (natts < 1)
+	{
+		errsave(parse->escontext,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				errdetail("The \"" PG_DEPENDENCIES_KEY_ATTRIBUTES "\" key must contain an array of at least one attnum."));
+		return JSON_SEM_ACTION_FAILED;
+	}
+	attrsort = palloc0(natts * sizeof(AttrNumber));
+
+	/*
+	 * Allocate enough space for the dependency, the attnums in the list, plus
+	 * the final attnum
+	 */
+	dep = palloc0(offsetof(MVDependency, attributes) + ((natts + 1) * sizeof(AttrNumber)));
+	dep->nattributes = natts + 1;
+
+	dep->attributes[natts] = parse->dependency;
+	dep->degree = parse->degree;
+
+	attrsort = palloc0(dep->nattributes * sizeof(AttrNumber));
+	attrsort[natts] = parse->dependency;
+
+	for (int i = 0; i < natts; i++)
+	{
+		attrsort[i] = (AttrNumber) parse->attnum_list->elements[i].int_value;
+		dep->attributes[i] = attrsort[i];
+	}
+
+	/* Check attrsort for uniqueness */
+	qsort(attrsort, natts + 1, sizeof(AttrNumber), attnum_compare);
+	for (int i = 1; i < dep->nattributes; i++)
+		if (attrsort[i] == attrsort[i - 1])
+		{
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("\"" PG_DEPENDENCIES_KEY_ATTRIBUTES "\" list duplicate value found: %d.", attrsort[i]));
+			pfree(attrsort);
+			pfree(dep);
+			return JSON_SEM_ACTION_FAILED;
+		}
+	pfree(attrsort);
+
+	parse->dependency_list = lappend(parse->dependency_list, (void *) dep);
+
+	/* reset dep item state vars */
+	list_free(parse->attnum_list);
+	parse->attnum_list = NIL;
+	parse->dependency = 0;
+	parse->degree = 0.0;
+	parse->found_attributes = false;
+	parse->found_dependency = false;
+	parse->found_degree = false;
+
+	/* Now we are looking for the next MVDependency */
+	parse->state = DEPS_EXPECT_ITEM;
+	return JSON_SUCCESS;
+}
+
+/*
+ * dependencies input format does not have arrays, so any array elements encountered
+ * are an error.
+ */
+static JsonParseErrorType
+dependencies_array_start(void *state)
+{
+	DependenciesParseState *parse = state;
+
+	switch (parse->state)
+	{
+		case DEPS_EXPECT_ATTNUM_LIST:
+			parse->state = DEPS_EXPECT_ATTNUM;
+			break;
+		case DEPS_EXPECT_START:
+			parse->state = DEPS_EXPECT_ITEM;
+			break;
+		default:
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Array found in unexpected place."));
+			return JSON_SEM_ACTION_FAILED;
+	}
+
+	return JSON_SUCCESS;
+}
+
+/*
+ * Either the end of an attnum list or the whole object
+ */
+static JsonParseErrorType
+dependencies_array_end(void *state)
+{
+	DependenciesParseState *parse = state;
+
+	switch (parse->state)
+	{
+		case DEPS_EXPECT_ATTNUM:
+			if (parse->attnum_list != NIL)
+			{
+				parse->state = DEPS_EXPECT_KEY;
+				return JSON_SUCCESS;
+			}
+
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("The \"" PG_DEPENDENCIES_KEY_ATTRIBUTES
+								"\" key must be an non-empty array."));
+			break;
+
+		case DEPS_EXPECT_ITEM:
+			if (parse->dependency_list != NIL)
+			{
+				parse->state = DEPS_PARSE_COMPLETE;
+				return JSON_SUCCESS;
+			}
+
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("The dependency list must be an non-empty array."));
+			break;
+
+		default:
+			/*
+			 * This can only happen if a case was missed in depenenceies_array_start()
+			 */
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Array found in unexpected place. %d", parse->state));
+	}
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ * The valid keys for the MVDependency object are:
+ *   - attributes
+ *   - depeendency
+ *   - degree
+ */
+static JsonParseErrorType
+dependencies_object_field_start(void *state, char *fname, bool isnull)
+{
+	DependenciesParseState *parse = state;
+
+	if (strcmp(fname, PG_DEPENDENCIES_KEY_ATTRIBUTES) == 0)
+	{
+		parse->found_attributes = true;
+		parse->state = DEPS_EXPECT_ATTNUM_LIST;
+		return JSON_SUCCESS;
+	}
+
+	if (strcmp(fname, PG_DEPENDENCIES_KEY_DEPENDENCY) == 0)
+	{
+		parse->found_dependency = true;
+		parse->state = DEPS_EXPECT_DEPENDENCY;
+		return JSON_SUCCESS;
+	}
+
+	if (strcmp(fname, PG_DEPENDENCIES_KEY_DEGREE) == 0)
+	{
+		parse->found_degree = true;
+		parse->state = DEPS_EXPECT_DEGREE;
+		return JSON_SUCCESS;
+	}
+
+	errsave(parse->escontext,
+			errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+			errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+			errdetail("Invalid key \"%s\". Only allowed keys are \""
+					  PG_DEPENDENCIES_KEY_ATTRIBUTES "\", \""
+					  PG_DEPENDENCIES_KEY_DEPENDENCY "\" and \""
+					  PG_DEPENDENCIES_KEY_DEGREE "\".", fname));
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ * pg_dependencies input format does not have arrays, so any array elements
+ * encountered are an error.
+ */
+static JsonParseErrorType
+dependencies_array_element_start(void *state, bool isnull)
+{
+	DependenciesParseState *parse = state;
+
+	switch(parse->state)
+	{
+		case DEPS_EXPECT_ATTNUM:
+			if (!isnull)
+				return JSON_SUCCESS;
+
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Attnum list elements cannot be null."));
+
+			return JSON_SEM_ACTION_FAILED;
+			break;
+
+		case DEPS_EXPECT_ITEM:
+			if (!isnull)
+				return JSON_SUCCESS;
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Item list elements cannot be null."));
+
+			return JSON_SEM_ACTION_FAILED;
+			break;
+
+		default:
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Unexpected array element."));
+	}
+
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ * Handle scalar events from the dependencies input parser.
+ *
+ * There is only one case where we will encounter a scalar, and that is the
+ * dependency degree for the previous object key.
+ */
+static JsonParseErrorType
+dependencies_scalar(void *state, char *token, JsonTokenType tokentype)
+{
+	DependenciesParseState *parse = state;
+	AttrNumber	attnum;
+
+	switch(parse->state)
+	{
+		case DEPS_EXPECT_ATTNUM:
+			attnum = pg_strtoint16_safe(token, parse->escontext);
+
+			if (!SOFT_ERROR_OCCURRED(parse->escontext))
+			{
+				parse->attnum_list = lappend_int(parse->attnum_list, (int) attnum);
+				/* No state change, we expect more attnums */
+				return JSON_SUCCESS;
+			}
+
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Invalid attribute number: \"%s\".", token));
+
+			return JSON_SEM_ACTION_FAILED;
+			break;
+
+		case DEPS_EXPECT_DEPENDENCY:
+			parse->dependency = (AttrNumber) pg_strtoint16_safe(token, parse->escontext);
+
+			if (!SOFT_ERROR_OCCURRED(parse->escontext))
+			{
+				parse->state = DEPS_EXPECT_KEY;
+				return JSON_SUCCESS;
+			}
+
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Invalid " PG_DEPENDENCIES_KEY_DEPENDENCY
+			   				  " attribute number: \"%s\".", token));
+			return JSON_SEM_ACTION_FAILED;
+			break;
+
+		case DEPS_EXPECT_DEGREE:
+			parse->degree = float8in_internal(token, NULL, "double",
+											token, parse->escontext);
+
+			if (!SOFT_ERROR_OCCURRED(parse->escontext))
+			{
+				parse->state = DEPS_EXPECT_KEY;
+				return JSON_SUCCESS;
+			}
+
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Invalid " PG_DEPENDENCIES_KEY_DEGREE
+							  " value: \"%s\".", token));
+
+			return JSON_SEM_ACTION_FAILED;
+			break;
+
+		default:
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Unexpected scalar."));
+	}
+
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*************************************************** BLAH *********************************************/
+/*
+ * Compare the attribute arrays of two MVDependency values,
+ * looking for duplicate sets.
+ */
+static
+bool has_duplicate_attributes(const MVDependency *a, const MVDependency *b)
+{
+	int		i;
+
+	if (a->nattributes != b->nattributes)
+		return false;
+
+	for (i = 0; i < a->nattributes; i++)
+	{
+		if (a->attributes[i] != b->attributes[i])
+			return false;
+	}
+
+	return true;
+}
+
+/*
+ * Ensure that an attnum appears as one of the attnums in a given
+ * MVDependency.
+ */
+static
+bool dep_has_attnum(const MVDependency *item, AttrNumber attnum)
+{
+	for (int i = 0; i < item->nattributes; i++)
+	{
+		if (attnum == item->attributes[i])
+			return true;
+	}
+	return false;
+}
+
+/*
+ * Ensure that the attributes of one MVDependency A are a proper subset
+ * of the reference MVDependency B.
+ */
+static
+bool dep_is_attnum_subset(const MVDependency *item,
+						  const MVDependency *refitem)
+{
+	for (int i = 0; i < item->nattributes; i++)
+	{
+		if (!dep_has_attnum(refitem,item->attributes[i]))
+			return false;
+	}
+	return true;
+}
+
+/*
+ * Generate a string representing an array of attnums. Internally, the
+ * dependency attribute is the last element, so we leave that off.
+ *
+ *
+ * Freeing the allocated string is responsibility of the caller.
+ */
+static
+const char *dep_attnum_list(const MVDependency *item)
+{
+	StringInfoData	str;
+
+	initStringInfo(&str);
+
+	appendStringInfo(&str, "%d", item->attributes[0]);
+
+	for (int i = 1; i < item->nattributes - 1; i++)
+		appendStringInfo(&str, ", %d", item->attributes[i]);
+
+	return str.data;
+}
+
+/*
+ * Return the dependency, which is the last attribute element.
+ */
+static
+const AttrNumber dep_attnum_dependency(const MVDependency *item)
+{
+	return item->attributes[item->nattributes - 1];
+}
+
+
+
+
+/*************************************************** BLAH *********************************************/
 /*
  * pg_dependencies_in		- input routine for type pg_dependencies.
  *
- * pg_dependencies is real enough to be a table column, but it has no operations
- * of its own, and disallows input too
+ * This format is valid JSON, with the expected format:
+ *    [{"attributes": [1,2], "dependency": -1, "degree": 1.0000},
+ *     {"attributes": [1,-1], "dependency": 2, "degree": 0.0000},
+ *     {"attributes": [2,-1], "dependency": 1, "degree": 1.0000}]
+ *
  */
 Datum
 pg_dependencies_in(PG_FUNCTION_ARGS)
 {
-	/*
-	 * pg_node_list stores the data in binary form and parsing text input is
-	 * not needed, so disallow this.
-	 */
-	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot accept a value of type %s", "pg_dependencies")));
+	char	   *str = PG_GETARG_CSTRING(0);
 
-	PG_RETURN_VOID();			/* keep compiler quiet */
+	DependenciesParseState parse_state;
+	JsonParseErrorType result;
+	JsonLexContext *lex;
+	JsonSemAction sem_action;
+
+	/* initialize the semantic state */
+	parse_state.str = str;
+	parse_state.state = DEPS_EXPECT_START;
+	parse_state.dependency_list = NIL;
+	parse_state.attnum_list = NIL;
+	parse_state.dependency = 0;
+	parse_state.degree = 0.0;
+	parse_state.found_attributes = false;
+	parse_state.found_dependency = false;
+	parse_state.found_degree = false;
+	parse_state.escontext = fcinfo->context;
+
+	/* set callbacks */
+	sem_action.semstate = (void *) &parse_state;
+	sem_action.object_start = dependencies_object_start;
+	sem_action.object_end = dependencies_object_end;
+	sem_action.array_start = dependencies_array_start;
+	sem_action.array_end = dependencies_array_end;
+	sem_action.array_element_start = dependencies_array_element_start;
+	sem_action.array_element_end = NULL;
+	sem_action.object_field_start = dependencies_object_field_start;
+	sem_action.object_field_end = NULL;
+	sem_action.scalar = dependencies_scalar;
+
+	lex = makeJsonLexContextCstringLen(NULL, str, strlen(str), PG_UTF8, true);
+
+	result = pg_parse_json(lex, &sem_action);
+	freeJsonLexContext(lex);
+
+	if (result == JSON_SUCCESS)
+	{
+		List	   *list = parse_state.dependency_list;
+		int			ndeps = list->length;
+		MVDependencies *mvdeps;
+		bytea	   *bytes;
+
+		int		dep_most_attrs = 0;
+		int		dep_most_attrs_idx = 0;
+
+		switch(parse_state.state)
+		{
+			case DEPS_PARSE_COMPLETE:
+				/*
+				 * Parse ended in the expected place. We should have a list of items,
+				 * but if we don't it is because there are bugs in other parse steps.
+				 */
+				if (parse_state.dependency_list == NIL)
+					elog(ERROR,
+						 "pg_dependencies parssing claims success with an empty item list.");
+
+				break;
+
+			case DEPS_EXPECT_START:
+				/* blank */
+				errsave(parse_state.escontext,
+						errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+						errmsg("malformed pg_dependencies: \"%s\"", str),
+						errdetail("Value cannot be empty."));
+				PG_RETURN_NULL();
+				break;
+
+			default:
+				/* Unexpected end-state. TODO: Is this an elog()? */
+				errsave(parse_state.escontext,
+						errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+						errmsg("malformed pg_dependencies: \"%s\"", str),
+						errdetail("Unexpected end state %d.", parse_state.state));
+				PG_RETURN_NULL();
+				break;
+		}
+
+		mvdeps = palloc0(offsetof(MVDependencies, deps) + ndeps * sizeof(MVDependency));
+		mvdeps->magic = STATS_DEPS_MAGIC;
+		mvdeps->type = STATS_DEPS_TYPE_BASIC;
+		mvdeps->ndeps = ndeps;
+
+		/* copy MVDependency structs out of the list into the MVDependencies */
+		for (int i = 0; i < ndeps; i++)
+		{
+			mvdeps->deps[i] = list->elements[i].ptr_value;
+
+			/*
+			 * Ensure that this item does not duplicate the attributes of any
+			 * pre-existing item.
+			 */
+			for (int j = 0; j < i; j++)
+			{
+				if (has_duplicate_attributes(mvdeps->deps[i], mvdeps->deps[j]))
+				{
+					MVDependency   *dep = mvdeps->deps[i];
+
+					errsave(parse_state.escontext,
+							errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+							errmsg("malformed pg_dependencies: \"%s\"", str),
+							errdetail("Duplicate \"" PG_DEPENDENCIES_KEY_ATTRIBUTES "\" array: [%s]"
+									  " with \"" PG_DEPENDENCIES_KEY_DEPENDENCY "\": %d.",
+									  dep_attnum_list(dep), dep_attnum_dependency(dep)));
+					PG_RETURN_NULL();
+				}
+			}
+
+			/*
+			 * Keep track of the first longest attribute list. All other attribute
+			 * lists must be a subset of this list.
+			 */
+			if (mvdeps->deps[i]->nattributes > dep_most_attrs)
+			{
+				dep_most_attrs = mvdeps->deps[i]->nattributes;
+				dep_most_attrs_idx = i;
+			}
+		}
+
+		/*
+		 * Verify that all attnum sets are a proper subset of the first longest
+		 * attnum set.
+		 */
+		for (int i = 0; i < ndeps; i++)
+		{
+			if (i == dep_most_attrs_idx)
+				continue;
+
+			if (!dep_is_attnum_subset(mvdeps->deps[i],
+									  mvdeps->deps[dep_most_attrs_idx]))
+			{
+				MVDependency   *dep = mvdeps->deps[i];
+				MVDependency   *refdep = mvdeps->deps[dep_most_attrs_idx];
+	   			const char *dep_list = dep_attnum_list(dep);
+	   			const char *refdep_list = dep_attnum_list(refdep);
+
+				errsave(parse_state.escontext,
+						errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+						errmsg("malformed pg_dependencies: \"%s\"", str),
+						errdetail("\"" PG_DEPENDENCIES_KEY_ATTRIBUTES "\" array: [%s]"
+								  " with dependency %d must be a subset of array: [%s]"
+								  " with dependency %d.",
+								  dep_list, dep_attnum_dependency(dep),
+								  refdep_list, dep_attnum_dependency(refdep)));
+				PG_RETURN_NULL();
+			}
+		}
+		bytes = statext_dependencies_serialize(mvdeps);
+
+		list_free(list);
+		for (int i = 0; i < ndeps; i++)
+			pfree(mvdeps->deps[i]);
+		pfree(mvdeps);
+
+		PG_RETURN_BYTEA_P(bytes);
+	}
+
+	/*
+	 * If escontext already set, just use that.
+	 * Anything else is a generic JSON parse error.
+	 */
+	if (!SOFT_ERROR_OCCURRED(parse_state.escontext))
+		errsave(parse_state.escontext,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("malformed pg_dependencies: \"%s\"", str),
+				errdetail("Must be valid JSON."));
+
+	PG_RETURN_NULL();			/* keep compiler quiet */
 }
 
 /*
 
 # ----------

From c9fd2173ee87bb5e6abc07318c9961d8be4af8f2 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 4 Nov 2025 23:50:01 -0500
Subject: [PATCH v13 5/7] Expose attribute statistics functions for use in
 extended_stats.

 
 
+/*
 
+/*

 /*

 
 
 		if (converted)
 		{
 
 		if (converted)
 		{

 
 		if (converted)
 		{

 	/*
 
 		if (converted)
 		{
 
 		if (converted)
 		{

From ba378a74a96b940dbff5591204db256727103772 Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Tue, 11 Nov 2025 16:58:26 +0900
Subject: [PATCH v13 6/7] Add extended statistics support functions.

 

+/*
 
+/*

+/*

 
 /*

 
 	return result;
 }
+
+static HeapTuple
 
 	return s;
 }
+
+/*
 
+/*
 
 DROP TABLE stats_temp;

 

From 9fed09459010c975ee91b2f991648d82f8c0eaf8 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Wed, 5 Nov 2025 01:13:04 -0500
Subject: [PATCH v13 7/7] Include Extended Statistics in pg_dump.

index c84b017f21b..ab9cb75a86f 100644

 
 	PQclear(res);
 
+/*
 
+	#

---

From: Michael Paquier <[email-masked]>
Date: Fri, 14 Nov 2025 15:25:27 +0900

> against the definition of the extended stats object.

Exactly.  Extended stats on system columns don't work because they
don't really make sense as we want to track correlations between the
attributes defined, and these reflect internal states:
create table poo (a int, b int);
create statistics poos (ndistinct ) ON cmax, a from poo;
ERROR:  0A000: statistics creation on system columns is not supported

Note that the expressions are also stored in pg_stats_ext_exprs.

> I'm trying to implement those test cases, but I may have missed some.

I've found a lot of them with coverage-html during a previous lookup.
I'd like to think that we should aim for something close to 100%
coverage for the two input functions.

> Implemented many, but not all of these suggestions.

Thanks for the new versions, I'll also look at all these across the
next couple of days.  Probably not at 0005~ for now.
--
Michael

---

From: Michael Paquier <[email-masked]>
Date: Mon, 17 Nov 2025 15:56:06 +0900

> next couple of days.  Probably not at 0005~ for now.

0001 and 0002 from series v13 have been applied to change the output
functions.

And I have looked at 0003 in details for now.  Attached is a revised
version for it, with many adjustments.  Some notes:
- Many portions of the coverage were missing.  I have measured the
coverage at 91% with the updated version attached.  This includes
coverage for some error reporting, something that we rely a lot on for
this code.
- The error reports are made simpler, with the token values getting
hidden.  While testing with some fancy values, I have actually noticed
that the error handlings for the parsing of the int16 and int32 values
were incorrect, the error reports used what the safe functions
generated, not the reports from the data type.
- Passing down arbitrary bytes sequences was leading to these bytes
reported in the error outputs because we cared about the token values.
I have added a few tests based on that for the code paths involved.

There is an extra thing that bugs me as incorrect for the pg_ndistinct
input, something I have not tackled myself yet.  Your patch checks
that subsets of attributes are included in the longest set found, but
it does not match the guarantees we have in mvndistinct.c: we have to
check that *all* the combinations generated by generator_init() are
satisfied based on the longest of attributes detected.  For example,
this is thought as correct in the input function:
SELECT '[{"attributes" : [-1,2], "ndistinct" : 1},
         {"attributes" : [-1,2,3], "ndistinct" : 3}]'::pg_ndistinct;

However it is obviously not correct as we are missing an element for
the attributes [-1, 3].  The simplest solution would be to export the
routines that generate the groups now in mvndistinct.c.  Also we
should make sure that the number of elements in the arrays match with
the number of groups we expect, not only the elements.  I don't think
that we need to care much about the values, but we ought to provide
stronger guarantees for the attributes listed in these elements.

Except for this argument, the input of pg_ndistinct feels OK in terms
of the guarantees that we'd want to enforce on an import.  The same
argument applies in terms of attribute number guarantees for
pg_dependencies, based on DependencyGenerator_init() & friends in
dependencies.c.  Could you look at that?

For pg_dependencies, we also require some checks on the value for
"dependency", of course, making sure that this matches with what's
expected with the "largest" sets of attributes.  In this case, we need
to track the union of "dependency" and "attributes", with "attributes"
having at least one element.

The tests of pg_dependencies need also to be extended more (begun that
a bit, far from being complete and I'm lacking of time this week due
to a conference).  One thing that I would add are nested JSON objects
in the paths where we expect values, for example.  Please note that I
have done a brush of 0004, while on it, cleaning up typos,
inconsistencies and making the error codes consistent with the
ndistinct case where possible.  This is not ready, but that's at least
it's a start to rely on.

In terms of committable bits, it would be better to apply the input
functions once both parts are ready to go.  For now I am attached a
v14 with the work I've put into them.  0005~ are not reviewed yet, as
mentioned previously.  The changes in pg_dependencies are actually
straight-forward to figure out (well, mostly) once the pg_ndistinct
changes are OK in shape.
--
Michael
From 5bfc7c74ed90c122bc39874d99f6af38f2f944fa Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Mon, 17 Nov 2025 14:52:22 +0900
Subject: [PATCH v14 1/5] Add working input function for pg_ndistinct.

This will be needed for importing extended statistics.  With these
changes in place, coverage of pg_ndistinct.c reaches 91%.
---
 src/backend/utils/adt/pg_ndistinct.c       | 695 ++++++++++++++++++++-
 src/test/regress/expected/pg_ndistinct.out | 359 +++++++++++
 src/test/regress/parallel_schedule         |   2 +-
 src/test/regress/sql/pg_ndistinct.sql      |  87 +++
 src/tools/pgindent/typedefs.list           |   2 +
 5 files changed, 1136 insertions(+), 9 deletions(-)
 create mode 100644 src/test/regress/expected/pg_ndistinct.out
 create mode 100644 src/test/regress/sql/pg_ndistinct.sql

diff --git a/src/backend/utils/adt/pg_ndistinct.c b/src/backend/utils/adt/pg_ndistinct.c
index 97efc290ef5e..4c13b2e62885 100644
--- a/src/backend/utils/adt/pg_ndistinct.c
+++ b/src/backend/utils/adt/pg_ndistinct.c
@@ -14,27 +14,706 @@
 
 #include "postgres.h"
 
+#include "common/int.h"
 
+/* Parsing state data */
+typedef enum
+{
+	NDIST_EXPECT_START = 0,
+	NDIST_EXPECT_ITEM,
+	NDIST_EXPECT_KEY,
+	NDIST_EXPECT_ATTNUM_LIST,
+	NDIST_EXPECT_ATTNUM,
+	NDIST_EXPECT_NDISTINCT,
+	NDIST_EXPECT_COMPLETE
+} NDistinctSemanticState;
+
+typedef struct
+{
+	const char *str;
+	NDistinctSemanticState state;
+
+	List	   *distinct_items; /* Accumulated complete MVNDistinctItems */
+	Node	   *escontext;
+
+	bool		found_attributes;	/* Item has "attributes" key */
+	bool		found_ndistinct;	/* Item has "ndistinct" key */
+	List	   *attnum_list;	/* Accumulated attributes attnums */
+	int32		ndistinct;
+} NDistinctParseState;
+
+/*
+ * Invoked at the start of each MVNDistinctItem.
+ *
+ * The entire JSON document should be one array of MVNDistinctItem objects.
+ * If we are anywhere else in the document, it is an error.
+ */
+static JsonParseErrorType
+ndistinct_object_start(void *state)
+{
+	NDistinctParseState *parse = state;
+
+	switch (parse->state)
+	{
+		case NDIST_EXPECT_ITEM:
+			/* Now we expect to see attributes/ndistinct keys */
+			parse->state = NDIST_EXPECT_KEY;
+			return JSON_SUCCESS;
+			break;
+
+		case NDIST_EXPECT_START:
+			/* pg_ndistinct must begin with a '[' */
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("Initial element must be an array."));
+			break;
+
+		case NDIST_EXPECT_KEY:
+			/* In an object, expecting key */
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("Expected an object key."));
+			break;
+
+		case NDIST_EXPECT_ATTNUM_LIST:
+			/* Just followed an "attributes" key */
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("Value of \"%s\" must be an array of attribute numbers.",
+							  PG_NDISTINCT_KEY_ATTRIBUTES));
+			break;
+
+		case NDIST_EXPECT_ATTNUM:
+			/* In an attnum list, expect only scalar integers */
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("Attribute lists can only contain attribute numbers."));
+			break;
+
+		case NDIST_EXPECT_NDISTINCT:
+			/* Just followed an "ndistinct" key */
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("Value of \"%s\" must be an integer.",
+							  PG_NDISTINCT_KEY_NDISTINCT));
+			break;
+
+		default:
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("Unexpected parse state: %d", (int) parse->state));
+	}
+
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ * Routine to allow qsorting of AttNumbers
+ */
+static int
+attnum_compare(const void *aptr, const void *bptr)
+{
+	AttrNumber	a = *(const AttrNumber *) aptr;
+	AttrNumber	b = *(const AttrNumber *) bptr;
+
+	return pg_cmp_s16(a, b);
+}
+
 
 /*
- * pg_ndistinct_in
- *		input routine for type pg_ndistinct
+ * Invoked at the end of an object.
  *
- * pg_ndistinct is real enough to be a table column, but it has no
- * operations of its own, and disallows input (just like pg_node_tree).
+ * Check to ensure that it was a complete MVNDistinctItem
+ */
+static JsonParseErrorType
+ndistinct_object_end(void *state)
+{
+	NDistinctParseState *parse = state;
+
+	int			natts = 0;
+	AttrNumber *attrsort;
+
+	MVNDistinctItem *item;
+
+	if (parse->state != NDIST_EXPECT_KEY)
+	{
+		errsave(parse->escontext,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+				errdetail("Unexpected parse state: %d", (int) parse->state));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	if (!parse->found_attributes)
+	{
+		errsave(parse->escontext,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+				errdetail("Item must contain \"%s\" key.",
+						  PG_NDISTINCT_KEY_ATTRIBUTES));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	if (!parse->found_ndistinct)
+	{
+		errsave(parse->escontext,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+				errdetail("Item must contain \"%s\" key.",
+						  PG_NDISTINCT_KEY_NDISTINCT));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	/*
+	 * We need at least two attribute numbers for a ndistinct item, anything
+	 * less is malformed.
+	 */
+	natts = parse->attnum_list->length;
+	if (natts < 2)
+	{
+		errsave(parse->escontext,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+				errdetail("The \"%s\" key must contain an array of at least two attributes.",
+						  PG_NDISTINCT_KEY_NDISTINCT));
+		return JSON_SEM_ACTION_FAILED;
+	}
+	attrsort = palloc0(natts * sizeof(AttrNumber));
+
+	/* Create the MVNDistinctItem */
+	item = palloc(sizeof(MVNDistinctItem));
+	item->nattributes = natts;
+	item->attributes = palloc0(natts * sizeof(AttrNumber));
+	item->ndistinct = (double) parse->ndistinct;
+
+	/* fill out both attnum list and sortable list */
+	for (int i = 0; i < natts; i++)
+	{
+		attrsort[i] = (AttrNumber) parse->attnum_list->elements[i].int_value;
+		item->attributes[i] = attrsort[i];
+	}
+
+	/* Check attrsort for uniqueness */
+	qsort(attrsort, natts, sizeof(AttrNumber), attnum_compare);
+	for (int i = 1; i < natts; i++)
+	{
+		if (attrsort[i] == attrsort[i - 1])
+		{
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("Duplicated attribute number found: %d.", attrsort[i]));
+			pfree(attrsort);
+			pfree(item->attributes);
+			pfree(item);
+			return JSON_SEM_ACTION_FAILED;
+		}
+	}
+	pfree(attrsort);
+
+	parse->distinct_items = lappend(parse->distinct_items, (void *) item);
+
+	/* reset item state vars */
+	list_free(parse->attnum_list);
+	parse->attnum_list = NIL;
+	parse->ndistinct = 0;
+	parse->found_attributes = false;
+	parse->found_ndistinct = false;
+
+	/* Now we are looking for the next MVNDistinctItem */
+	parse->state = NDIST_EXPECT_ITEM;
+	return JSON_SUCCESS;
+}
+
+
+/*
+ * ndistinct input format has two types of arrays, the outer MVNDistinctItem
+ * array and the attribute number array within each MVNDistinctItem.
+ */
+static JsonParseErrorType
+ndistinct_array_start(void *state)
+{
+	NDistinctParseState *parse = state;
+
+	switch (parse->state)
+	{
+		case NDIST_EXPECT_ATTNUM_LIST:
+			parse->state = NDIST_EXPECT_ATTNUM;
+			break;
+
+		case NDIST_EXPECT_START:
+			parse->state = NDIST_EXPECT_ITEM;
+			break;
+
+		default:
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("Array found in unexpected place."));
+			return JSON_SEM_ACTION_FAILED;
+	}
+
+	return JSON_SUCCESS;
+}
+
+
+/*
+ * Arrays can never be empty.
+ */
+static JsonParseErrorType
+ndistinct_array_end(void *state)
+{
+	NDistinctParseState *parse = state;
+
+	switch (parse->state)
+	{
+		case NDIST_EXPECT_ATTNUM:
+			if (parse->attnum_list != NIL)
+			{
+				/*
+				 * The attribute number list is complete, look for more
+				 * MVNDistinctItem keys.
+				 */
+				parse->state = NDIST_EXPECT_KEY;
+				return JSON_SUCCESS;
+			}
+
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("The \"%s\" key must be an non-empty array.",
+							  PG_NDISTINCT_KEY_ATTRIBUTES));
+			break;
+
+		case NDIST_EXPECT_ITEM:
+			if (parse->distinct_items != NIL)
+			{
+				/* Item list is complete, we are done. */
+				parse->state = NDIST_EXPECT_COMPLETE;
+				return JSON_SUCCESS;
+			}
+
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("Item array cannot be empty."));
+			break;
+		default:
+
+			/*
+			 * This can only happen if a case was missed in
+			 * ndistinct_array_start().
+			 */
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("Array found in unexpected place."));
+	}
+
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ * The valid keys for the MVNDistinctItem object are:
+ *   - attributes
+ *   - ndistinct
+ */
+static JsonParseErrorType
+ndistinct_object_field_start(void *state, char *fname, bool isnull)
+{
+	NDistinctParseState *parse = state;
+
+	if (strcmp(fname, PG_NDISTINCT_KEY_ATTRIBUTES) == 0)
+	{
+		parse->found_attributes = true;
+		parse->state = NDIST_EXPECT_ATTNUM_LIST;
+		return JSON_SUCCESS;
+	}
+
+	if (strcmp(fname, PG_NDISTINCT_KEY_NDISTINCT) == 0)
+	{
+		parse->found_ndistinct = true;
+		parse->state = NDIST_EXPECT_NDISTINCT;
+		return JSON_SUCCESS;
+	}
+
+	errsave(parse->escontext,
+			errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+			errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+			errdetail("Only allowed keys are \"%s\" and \"%s\".",
+					  PG_NDISTINCT_KEY_ATTRIBUTES,
+					  PG_NDISTINCT_KEY_NDISTINCT));
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ * Handle any array element.
+ */
+static JsonParseErrorType
+ndistinct_array_element_start(void *state, bool isnull)
+{
+	NDistinctParseState *parse = state;
+
+	switch (parse->state)
+	{
+		case NDIST_EXPECT_ATTNUM:
+			if (!isnull)
+				return JSON_SUCCESS;
+
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("Attribute number array cannot be null."));
+			break;
+
+		case NDIST_EXPECT_ITEM:
+			if (!isnull)
+				return JSON_SUCCESS;
+
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("Item list elements cannot be null."));
+
+			break;
+
+		default:
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("Unexpected array element."));
+	}
+
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ * Handle scalar events from the ndistinct input parser.
+ *
+ * Override integer parse error messages and replace them with errors
+ * specific to the context.
+ */
+static JsonParseErrorType
+ndistinct_scalar(void *state, char *token, JsonTokenType tokentype)
+{
+	NDistinctParseState *parse = state;
+	AttrNumber	attnum;
+	ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+	switch (parse->state)
+	{
+		case NDIST_EXPECT_ATTNUM:
+			attnum = pg_strtoint16_safe(token, (Node *) &escontext);
+
+			if (!SOFT_ERROR_OCCURRED(&escontext))
+			{
+				parse->attnum_list = lappend_int(parse->attnum_list, (int) attnum);
+				return JSON_SUCCESS;
+			}
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("Invalid \"%s\" value.", PG_NDISTINCT_KEY_ATTRIBUTES));
+			break;
+
+		case NDIST_EXPECT_NDISTINCT:
+
+			/*
+			 * While the structure dictates that ndistinct is a double
+			 * precision floating point, it has always been an integer in the
+			 * output generated.  Therefore, we parse it as an integer here.
+			 */
+			parse->ndistinct = pg_strtoint32_safe(token, (Node *) &escontext);
+
+			if (!SOFT_ERROR_OCCURRED(&escontext))
+			{
+				parse->state = NDIST_EXPECT_KEY;
+				return JSON_SUCCESS;
+			}
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("Invalid \"%s\" value.",
+							  PG_NDISTINCT_KEY_NDISTINCT));
+			break;
+
+		default:
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+					errdetail("Unexpected scalar."));
+	}
+
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ * Compare the attribute arrays of two MVNDistinctItem values,
+ * looking for duplicate sets.
+ */
+static bool
+has_duplicate_attributes(const MVNDistinctItem *a,
+						 const MVNDistinctItem *b)
+{
+	if (a->nattributes != b->nattributes)
+		return false;
+
+	for (int i = 0; i < a->nattributes; i++)
+	{
+		if (a->attributes[i] != b->attributes[i])
+			return false;
+	}
+
+	return true;
+}
+
+/*
+ * Ensure that an attribute number appears as one of the attribute numbers
+ * in a MVNDistinctItem.
+ */
+static bool
+item_has_attnum(const MVNDistinctItem *item, AttrNumber attnum)
+{
+	for (int i = 0; i < item->nattributes; i++)
+	{
+		if (attnum == item->attributes[i])
+			return true;
+	}
+	return false;
+}
+
+/*
+ * Ensure that the attributes in MVNDistinctItem A are a subset of the
+ * reference MVNDistinctItem B.
+ */
+static bool
+item_is_attnum_subset(const MVNDistinctItem *item,
+					  const MVNDistinctItem *refitem)
+{
+	for (int i = 0; i < item->nattributes; i++)
+	{
+		if (!item_has_attnum(refitem, item->attributes[i]))
+			return false;
+	}
+	return true;
+}
+
+/*
+ * Generate a string representing an array of attnum.
+ *
+ * Freeing the allocated string is the responsibility of the caller.
+ */
+static const char *
+item_attnum_list(const MVNDistinctItem *item)
+{
+	StringInfoData str;
+
+	initStringInfo(&str);
+
+	appendStringInfo(&str, "%d", item->attributes[0]);
+
+	for (int i = 1; i < item->nattributes; i++)
+		appendStringInfo(&str, ", %d", item->attributes[i]);
+
+	return str.data;
+}
+
+/*
+ * pg_ndistinct_in
+ *		input routine for type pg_ndistinct.
  */
 Datum
 pg_ndistinct_in(PG_FUNCTION_ARGS)
 {
-	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot accept a value of type %s", "pg_ndistinct")));
+	char	   *str = PG_GETARG_CSTRING(0);
+	NDistinctParseState parse_state;
+	JsonParseErrorType result;
+	JsonLexContext *lex;
+	JsonSemAction sem_action;
+	int			item_most_attrs = 0;
+	int			item_most_attrs_idx = 0;
 
-	PG_RETURN_VOID();			/* keep compiler quiet */
+	/* initialize semantic state */
+	parse_state.str = str;
+	parse_state.state = NDIST_EXPECT_START;
+	parse_state.distinct_items = NIL;
+	parse_state.escontext = fcinfo->context;
+	parse_state.found_attributes = false;
+	parse_state.found_ndistinct = false;
+	parse_state.attnum_list = NIL;
+	parse_state.ndistinct = 0;
+
+	/* set callbacks */
+	sem_action.semstate = (void *) &parse_state;
+	sem_action.object_start = ndistinct_object_start;
+	sem_action.object_end = ndistinct_object_end;
+	sem_action.array_start = ndistinct_array_start;
+	sem_action.array_end = ndistinct_array_end;
+	sem_action.object_field_start = ndistinct_object_field_start;
+	sem_action.object_field_end = NULL;
+	sem_action.array_element_start = ndistinct_array_element_start;
+	sem_action.array_element_end = NULL;
+	sem_action.scalar = ndistinct_scalar;
+
+	lex = makeJsonLexContextCstringLen(NULL, str, strlen(str),
+									   PG_UTF8, true);
+	result = pg_parse_json(lex, &sem_action);
+	freeJsonLexContext(lex);
+
+	if (result == JSON_SUCCESS)
+	{
+		MVNDistinct *ndistinct;
+		int			nitems = parse_state.distinct_items->length;
+		bytea	   *bytes;
+
+		switch (parse_state.state)
+		{
+			case NDIST_EXPECT_COMPLETE:
+
+				/*
+				 * Parsing has ended correctly and we should have a list of
+				 * items.  If we don't, something has been done wrong in one
+				 * of the earlier parsing steps.
+				 */
+				if (parse_state.distinct_items == NIL)
+					elog(ERROR,
+						 "cannot have empty item list after parsing success.");
+				break;
+
+			case NDIST_EXPECT_START:
+				/* blank */
+				errsave(parse_state.escontext,
+						errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+						errmsg("malformed pg_ndistinct: \"%s\"", str),
+						errdetail("Value cannot be empty."));
+				PG_RETURN_NULL();
+				break;
+
+			default:
+				/* Unexpected end-state. */
+				errsave(parse_state.escontext,
+						errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+						errmsg("malformed pg_ndistinct: \"%s\"", str),
+						errdetail("Unexpected end state %d.", parse_state.state));
+				PG_RETURN_NULL();
+				break;
+		}
+
+		ndistinct = palloc(offsetof(MVNDistinct, items) +
+						   nitems * sizeof(MVNDistinctItem));
+
+		ndistinct->magic = STATS_NDISTINCT_MAGIC;
+		ndistinct->type = STATS_NDISTINCT_TYPE_BASIC;
+		ndistinct->nitems = nitems;
+
+		for (int i = 0; i < nitems; i++)
+		{
+			MVNDistinctItem *item = parse_state.distinct_items->elements[i].ptr_value;
+
+			/*
+			 * Ensure that this item does not duplicate the attributes of any
+			 * pre-existing item.
+			 */
+			for (int j = 0; j < i; j++)
+			{
+				if (has_duplicate_attributes(item, &ndistinct->items[j]))
+				{
+					errsave(parse_state.escontext,
+							errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+							errmsg("malformed pg_ndistinct: \"%s\"", str),
+							errdetail("Duplicated \"%s\" array found: [%s]",
+									  PG_NDISTINCT_KEY_ATTRIBUTES,
+									  item_attnum_list(item)));
+					PG_RETURN_NULL();
+				}
+			}
+
+			ndistinct->items[i].ndistinct = item->ndistinct;
+			ndistinct->items[i].nattributes = item->nattributes;
+			ndistinct->items[i].attributes = item->attributes;
+
+			/*
+			 * Keep track of the first longest attribute list. All other
+			 * attribute lists must be a subset of this list.
+			 */
+			if (item->nattributes > item_most_attrs)
+			{
+				item_most_attrs = item->nattributes;
+				item_most_attrs_idx = i;
+			}
+
+			/*
+			 * Free the MVNDistinctItem, but not the attributes we're still
+			 * using.
+			 */
+			pfree(item);
+		}
+
+		/*
+		 * Verify that all the sets of attribute numbers are a proper subset
+		 * of the longest set recorded.  This acts as an extra sanity check
+		 * based on the input given.  Note that this still needs to be
+		 * cross-checked with the extended statistics objects this would be
+		 * assigned to, but it provides one extra layer of protection.
+		 */
+		for (int i = 0; i < nitems; i++)
+		{
+			if (i == item_most_attrs_idx)
+				continue;
+
+			if (!item_is_attnum_subset(&ndistinct->items[i],
+									   &ndistinct->items[item_most_attrs_idx]))
+			{
+				const MVNDistinctItem *item = &ndistinct->items[i];
+				const MVNDistinctItem *refitem = &ndistinct->items[item_most_attrs_idx];
+				const char *item_list = item_attnum_list(item);
+				const char *refitem_list = item_attnum_list(refitem);
+
+				errsave(parse_state.escontext,
+						errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+						errmsg("malformed pg_ndistinct: \"%s\"", str),
+						errdetail("\"%s\" array: [%s] must be a subset of array: [%s]",
+								  PG_NDISTINCT_KEY_ATTRIBUTES,
+								  item_list, refitem_list));
+				PG_RETURN_NULL();
+			}
+		}
+
+		bytes = statext_ndistinct_serialize(ndistinct);
+
+		list_free(parse_state.distinct_items);
+		for (int i = 0; i < nitems; i++)
+			pfree(ndistinct->items[i].attributes);
+		pfree(ndistinct);
+
+		PG_RETURN_BYTEA_P(bytes);
+	}
+
+	/*
+	 * If escontext already set, just use that. Anything else is a generic
+	 * JSON parse error.
+	 */
+	if (!SOFT_ERROR_OCCURRED(parse_state.escontext))
+		errsave(parse_state.escontext,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("malformed pg_ndistinct: \"%s\"", str),
+				errdetail("Must be valid JSON."));
+
+	PG_RETURN_NULL();
 }
 
 /*
diff --git a/src/test/regress/expected/pg_ndistinct.out b/src/test/regress/expected/pg_ndistinct.out
new file mode 100644
index 000000000000..705900585c76
--- /dev/null
+++ b/src/test/regress/expected/pg_ndistinct.out
@@ -0,0 +1,359 @@
+-- Tests for type pg_ndistinct
+-- Invalid inputs
+SELECT 'null'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "null"
+LINE 1: SELECT 'null'::pg_ndistinct;
+               ^
+DETAIL:  Unexpected scalar.
+SELECT '{"a": 1}'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "{"a": 1}"
+LINE 1: SELECT '{"a": 1}'::pg_ndistinct;
+               ^
+DETAIL:  Initial element must be an array.
+SELECT '[]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[]"
+LINE 1: SELECT '[]'::pg_ndistinct;
+               ^
+DETAIL:  Item array cannot be empty.
+SELECT '{}'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "{}"
+LINE 1: SELECT '{}'::pg_ndistinct;
+               ^
+DETAIL:  Initial element must be an array.
+SELECT '[null]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[null]"
+LINE 1: SELECT '[null]'::pg_ndistinct;
+               ^
+DETAIL:  Item list elements cannot be null.
+SELECT * FROM pg_input_error_info('null', 'pg_ndistinct');
+            message             |       detail       | hint | sql_error_code 
+--------------------------------+--------------------+------+----------------
+ malformed pg_ndistinct: "null" | Unexpected scalar. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('{"a": 1}', 'pg_ndistinct');
+              message               |              detail               | hint | sql_error_code 
+------------------------------------+-----------------------------------+------+----------------
+ malformed pg_ndistinct: "{"a": 1}" | Initial element must be an array. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[]', 'pg_ndistinct');
+           message            |           detail            | hint | sql_error_code 
+------------------------------+-----------------------------+------+----------------
+ malformed pg_ndistinct: "[]" | Item array cannot be empty. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('{}', 'pg_ndistinct');
+           message            |              detail               | hint | sql_error_code 
+------------------------------+-----------------------------------+------+----------------
+ malformed pg_ndistinct: "{}" | Initial element must be an array. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[null]', 'pg_ndistinct');
+             message              |               detail               | hint | sql_error_code 
+----------------------------------+------------------------------------+------+----------------
+ malformed pg_ndistinct: "[null]" | Item list elements cannot be null. |      | 22P02
+(1 row)
+
+-- Invalid keys
+SELECT '[{"attributes_invalid" : [2,3], "ndistinct" : 4}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes_invalid" : [2,3], "ndistinct" : 4}]"
+LINE 1: SELECT '[{"attributes_invalid" : [2,3], "ndistinct" : 4}]'::...
+               ^
+DETAIL:  Only allowed keys are "attributes" and "ndistinct".
+SELECT '[{"attributes" : [2,3], "invalid" : 3, "ndistinct" : 4}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : [2,3], "invalid" : 3, "ndistinct" : 4}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "invalid" : 3, "ndistinct" :...
+               ^
+DETAIL:  Only allowed keys are "attributes" and "ndistinct".
+SELECT * FROM pg_input_error_info('[{"attributes_invalid" : [2,3], "ndistinct" : 4}]', 'pg_ndistinct');
+                                   message                                   |                       detail                        | hint | sql_error_code 
+-----------------------------------------------------------------------------+-----------------------------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes_invalid" : [2,3], "ndistinct" : 4}]" | Only allowed keys are "attributes" and "ndistinct". |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "invalid" : 3, "ndistinct" : 4}]', 'pg_ndistinct');
+                                      message                                       |                       detail                        | hint | sql_error_code 
+------------------------------------------------------------------------------------+-----------------------------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [2,3], "invalid" : 3, "ndistinct" : 4}]" | Only allowed keys are "attributes" and "ndistinct". |      | 22P02
+(1 row)
+
+-- Missing key
+SELECT '[{"attributes" : [2,3]}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : [2,3]}]"
+LINE 1: SELECT '[{"attributes" : [2,3]}]'::pg_ndistinct;
+               ^
+DETAIL:  Item must contain "ndistinct" key.
+SELECT '[{"ndistinct" : 4}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"ndistinct" : 4}]"
+LINE 1: SELECT '[{"ndistinct" : 4}]'::pg_ndistinct;
+               ^
+DETAIL:  Item must contain "attributes" key.
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3]}]', 'pg_ndistinct');
+                      message                       |               detail               | hint | sql_error_code 
+----------------------------------------------------+------------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [2,3]}]" | Item must contain "ndistinct" key. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[{"ndistinct" : 4}]', 'pg_ndistinct');
+                    message                    |               detail                | hint | sql_error_code 
+-----------------------------------------------+-------------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"ndistinct" : 4}]" | Item must contain "attributes" key. |      | 22P02
+(1 row)
+
+-- Special characters
+SELECT '[{"\ud83d\ude04\ud83d\udc36" : [1, 2], "ndistinct" : 4}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"\ud83d\ude04\ud83d\udc36" : [1, 2], "ndistinct" : 4}]"
+LINE 1: SELECT '[{"\ud83d\ude04\ud83d\udc36" : [1, 2], "ndistinct" :...
+               ^
+DETAIL:  Only allowed keys are "attributes" and "ndistinct".
+SELECT '[{"attributes" : [1, 2], "\ud83d\ude04\ud83d\udc36" : 4}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : [1, 2], "\ud83d\ude04\ud83d\udc36" : 4}]"
+LINE 1: SELECT '[{"attributes" : [1, 2], "\ud83d\ude04\ud83d\udc36" ...
+               ^
+DETAIL:  Only allowed keys are "attributes" and "ndistinct".
+SELECT '[{"attributes" : [1, 2], "ndistinct" : "\ud83d\ude04\ud83d\udc36"}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : [1, 2], "ndistinct" : "\ud83d\ude04\ud83d\udc36"}]"
+LINE 1: SELECT '[{"attributes" : [1, 2], "ndistinct" : "\ud83d\ude04...
+               ^
+DETAIL:  Invalid "ndistinct" value.
+SELECT '[{"attributes" : ["\ud83d\ude04\ud83d\udc36", 2], "ndistinct" : 1}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : ["\ud83d\ude04\ud83d\udc36", 2], "ndistinct" : 1}]"
+LINE 1: SELECT '[{"attributes" : ["\ud83d\ude04\ud83d\udc36", 2], "n...
+               ^
+DETAIL:  Invalid "attributes" value.
+-- Valid keys, invalid values
+SELECT '[{"attributes" : null, "ndistinct" : 4}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : null, "ndistinct" : 4}]"
+LINE 1: SELECT '[{"attributes" : null, "ndistinct" : 4}]'::pg_ndisti...
+               ^
+DETAIL:  Unexpected scalar.
+SELECT '[{"attributes" : [], "ndistinct" : 1}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : [], "ndistinct" : 1}]"
+LINE 1: SELECT '[{"attributes" : [], "ndistinct" : 1}]'::pg_ndistinc...
+               ^
+DETAIL:  The "attributes" key must be an non-empty array.
+SELECT '[{"attributes" : [2], "ndistinct" : 4}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : [2], "ndistinct" : 4}]"
+LINE 1: SELECT '[{"attributes" : [2], "ndistinct" : 4}]'::pg_ndistin...
+               ^
+DETAIL:  The "ndistinct" key must contain an array of at least two attributes.
+SELECT '[{"attributes" : [2,null], "ndistinct" : 4}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : [2,null], "ndistinct" : 4}]"
+LINE 1: SELECT '[{"attributes" : [2,null], "ndistinct" : 4}]'::pg_nd...
+               ^
+DETAIL:  Attribute number array cannot be null.
+SELECT '[{"attributes" : [2,3], "ndistinct" : null}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : null}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "ndistinct" : null}]'::pg_nd...
+               ^
+DETAIL:  Invalid "ndistinct" value.
+SELECT '[{"attributes" : [2,"a"], "ndistinct" : 4}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : [2,"a"], "ndistinct" : 4}]"
+LINE 1: SELECT '[{"attributes" : [2,"a"], "ndistinct" : 4}]'::pg_ndi...
+               ^
+DETAIL:  Invalid "attributes" value.
+SELECT '[{"attributes" : [2,3], "ndistinct" : "a"}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : "a"}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "ndistinct" : "a"}]'::pg_ndi...
+               ^
+DETAIL:  Invalid "ndistinct" value.
+SELECT '[{"attributes" : [2,3], "ndistinct" : []}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : []}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "ndistinct" : []}]'::pg_ndis...
+               ^
+DETAIL:  Array found in unexpected place.
+SELECT '[{"attributes" : [2,3], "ndistinct" : [null]}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : [null]}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "ndistinct" : [null]}]'::pg_...
+               ^
+DETAIL:  Array found in unexpected place.
+SELECT '[{"attributes" : [2,3], "ndistinct" : [1,null]}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : [1,null]}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "ndistinct" : [1,null]}]'::p...
+               ^
+DETAIL:  Array found in unexpected place.
+SELECT '[{"attributes" : [2,3], "ndistinct" : {"a": 1}}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : {"a": 1}}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "ndistinct" : {"a": 1}}]'::p...
+               ^
+DETAIL:  Value of "ndistinct" must be an integer.
+SELECT '[{"attributes" : 1, "ndistinct" : 4}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : 1, "ndistinct" : 4}]"
+LINE 1: SELECT '[{"attributes" : 1, "ndistinct" : 4}]'::pg_ndistinct...
+               ^
+DETAIL:  Unexpected scalar.
+SELECT '[{"attributes" : "a", "ndistinct" : 4}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : "a", "ndistinct" : 4}]"
+LINE 1: SELECT '[{"attributes" : "a", "ndistinct" : 4}]'::pg_ndistin...
+               ^
+DETAIL:  Unexpected scalar.
+SELECT '[{"attributes" : {"a": 1}, "ndistinct" : 1}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : {"a": 1}, "ndistinct" : 1}]"
+LINE 1: SELECT '[{"attributes" : {"a": 1}, "ndistinct" : 1}]'::pg_nd...
+               ^
+DETAIL:  Value of "attributes" must be an array of attribute numbers.
+SELECT '[{"attributes" : [1, {"a": 1}], "ndistinct" : 1}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : [1, {"a": 1}], "ndistinct" : 1}]"
+LINE 1: SELECT '[{"attributes" : [1, {"a": 1}], "ndistinct" : 1}]'::...
+               ^
+DETAIL:  Attribute lists can only contain attribute numbers.
+SELECT * FROM pg_input_error_info('[{"attributes" : null, "ndistinct" : 4}]', 'pg_ndistinct');
+                              message                               |       detail       | hint | sql_error_code 
+--------------------------------------------------------------------+--------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : null, "ndistinct" : 4}]" | Unexpected scalar. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[{"attributes" : [], "ndistinct" : 1}]', 'pg_ndistinct');
+                             message                              |                      detail                      | hint | sql_error_code 
+------------------------------------------------------------------+--------------------------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [], "ndistinct" : 1}]" | The "attributes" key must be an non-empty array. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[{"attributes" : [2], "ndistinct" : 4}]', 'pg_ndistinct');
+                              message                              |                                detail                                 | hint | sql_error_code 
+-------------------------------------------------------------------+-----------------------------------------------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [2], "ndistinct" : 4}]" | The "ndistinct" key must contain an array of at least two attributes. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,null], "ndistinct" : 4}]', 'pg_ndistinct');
+                                message                                 |                 detail                 | hint | sql_error_code 
+------------------------------------------------------------------------+----------------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [2,null], "ndistinct" : 4}]" | Attribute number array cannot be null. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : null}]', 'pg_ndistinct');
+                                message                                 |           detail           | hint | sql_error_code 
+------------------------------------------------------------------------+----------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : null}]" | Invalid "ndistinct" value. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,"a"], "ndistinct" : 4}]', 'pg_ndistinct');
+                                message                                |           detail            | hint | sql_error_code 
+-----------------------------------------------------------------------+-----------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [2,"a"], "ndistinct" : 4}]" | Invalid "attributes" value. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : "a"}]', 'pg_ndistinct');
+                                message                                |           detail           | hint | sql_error_code 
+-----------------------------------------------------------------------+----------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : "a"}]" | Invalid "ndistinct" value. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : []}]', 'pg_ndistinct');
+                               message                                |              detail              | hint | sql_error_code 
+----------------------------------------------------------------------+----------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : []}]" | Array found in unexpected place. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : [null]}]', 'pg_ndistinct');
+                                 message                                  |              detail              | hint | sql_error_code 
+--------------------------------------------------------------------------+----------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : [null]}]" | Array found in unexpected place. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : [1,null]}]', 'pg_ndistinct');
+                                  message                                   |              detail              | hint | sql_error_code 
+----------------------------------------------------------------------------+----------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : [1,null]}]" | Array found in unexpected place. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : {"a": 1}}]', 'pg_ndistinct');
+                                  message                                   |                  detail                  | hint | sql_error_code 
+----------------------------------------------------------------------------+------------------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : {"a": 1}}]" | Value of "ndistinct" must be an integer. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[{"attributes" : 1, "ndistinct" : 4}]', 'pg_ndistinct');
+                             message                             |       detail       | hint | sql_error_code 
+-----------------------------------------------------------------+--------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : 1, "ndistinct" : 4}]" | Unexpected scalar. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[{"attributes" : "a", "ndistinct" : 4}]', 'pg_ndistinct');
+                              message                              |       detail       | hint | sql_error_code 
+-------------------------------------------------------------------+--------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : "a", "ndistinct" : 4}]" | Unexpected scalar. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[{"attributes" : {"a": 1}, "ndistinct" : 1}]', 'pg_ndistinct');
+                                message                                 |                            detail                            | hint | sql_error_code 
+------------------------------------------------------------------------+--------------------------------------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : {"a": 1}, "ndistinct" : 1}]" | Value of "attributes" must be an array of attribute numbers. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[{"attributes" : [1, {"a": 1}], "ndistinct" : 1}]', 'pg_ndistinct');
+                                   message                                   |                       detail                        | hint | sql_error_code 
+-----------------------------------------------------------------------------+-----------------------------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [1, {"a": 1}], "ndistinct" : 1}]" | Attribute lists can only contain attribute numbers. |      | 22P02
+(1 row)
+
+-- Duplicated attributes
+SELECT '[{"attributes" : [2,2], "ndistinct" : 4}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : [2,2], "ndistinct" : 4}]"
+LINE 1: SELECT '[{"attributes" : [2,2], "ndistinct" : 4}]'::pg_ndist...
+               ^
+DETAIL:  Duplicated attribute number found: 2.
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,2], "ndistinct" : 4}]', 'pg_ndistinct');
+                               message                               |                detail                 | hint | sql_error_code 
+---------------------------------------------------------------------+---------------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [2,2], "ndistinct" : 4}]" | Duplicated attribute number found: 2. |      | 22P02
+(1 row)
+
+-- Duplicated attribute lists.
+SELECT '[{"attributes" : [2,3], "ndistinct" : 4},
+         {"attributes" : [2,3], "ndistinct" : 4}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : 4},
+         {"attributes" : [2,3], "ndistinct" : 4}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "ndistinct" : 4},
+               ^
+DETAIL:  Duplicated "attributes" array found: [2, 3]
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : 4},
+         {"attributes" : [2,3], "ndistinct" : 4}]', 'pg_ndistinct');
+                              message                               |                   detail                    | hint | sql_error_code 
+--------------------------------------------------------------------+---------------------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : 4},+| Duplicated "attributes" array found: [2, 3] |      | 22P02
+          {"attributes" : [2,3], "ndistinct" : 4}]"                 |                                             |      | 
+(1 row)
+
+-- Partially-covered attribute lists.
+SELECT '[{"attributes" : [2,3], "ndistinct" : 4},
+         {"attributes" : [2,-1], "ndistinct" : 4},
+         {"attributes" : [2,3,-1], "ndistinct" : 4},
+         {"attributes" : [1,3,-1,-2], "ndistinct" : 4}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : 4},
+         {"attributes" : [2,-1], "ndistinct" : 4},
+         {"attributes" : [2,3,-1], "ndistinct" : 4},
+         {"attributes" : [1,3,-1,-2], "ndistinct" : 4}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "ndistinct" : 4},
+               ^
+DETAIL:  "attributes" array: [2, 3] must be a subset of array: [1, 3, -1, -2]
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : 4},
+         {"attributes" : [2,-1], "ndistinct" : 4},
+         {"attributes" : [2,3,-1], "ndistinct" : 4},
+         {"attributes" : [1,3,-1,-2], "ndistinct" : 4}]', 'pg_ndistinct');
+                              message                               |                                detail                                | hint | sql_error_code 
+--------------------------------------------------------------------+----------------------------------------------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : 4},+| "attributes" array: [2, 3] must be a subset of array: [1, 3, -1, -2] |      | 22P02
+          {"attributes" : [2,-1], "ndistinct" : 4},                +|                                                                      |      | 
+          {"attributes" : [2,3,-1], "ndistinct" : 4},              +|                                                                      |      | 
+          {"attributes" : [1,3,-1,-2], "ndistinct" : 4}]"           |                                                                      |      | 
+(1 row)
+
+-- Valid inputs
+-- Two attributes.
+SELECT '[{"attributes" : [1,2], "ndistinct" : 4}]'::pg_ndistinct;
+               pg_ndistinct               
+------------------------------------------
+ [{"attributes": [1, 2], "ndistinct": 4}]
+(1 row)
+
+-- Three attributes.
+SELECT '[{"attributes" : [-1,2], "ndistinct" : 1},
+         {"attributes" : [-1,3], "ndistinct" : 2},
+	 {"attributes" : [-1,2,3], "ndistinct" : 3}]'::pg_ndistinct;
+                                                          pg_ndistinct                                                          
+--------------------------------------------------------------------------------------------------------------------------------
+ [{"attributes": [-1, 2], "ndistinct": 1}, {"attributes": [-1, 3], "ndistinct": 2}, {"attributes": [-1, 2, 3], "ndistinct": 3}]
+(1 row)
+
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index f56482fb9f12..f3f0b5f2f317 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,7 @@ test: strings md5 numerology point lseg line box path polygon circle date time t
 # geometry depends on point, lseg, line, box, path, polygon, circle
 # horology depends on date, time, timetz, timestamp, timestamptz, interval
 # ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import pg_ndistinct
 
 # ----------
 # Load huge amounts of data
diff --git a/src/test/regress/sql/pg_ndistinct.sql b/src/test/regress/sql/pg_ndistinct.sql
new file mode 100644
index 000000000000..dde508a31f50
--- /dev/null
+++ b/src/test/regress/sql/pg_ndistinct.sql
@@ -0,0 +1,87 @@
+-- Tests for type pg_ndistinct
+
+-- Invalid inputs
+SELECT 'null'::pg_ndistinct;
+SELECT '{"a": 1}'::pg_ndistinct;
+SELECT '[]'::pg_ndistinct;
+SELECT '{}'::pg_ndistinct;
+SELECT '[null]'::pg_ndistinct;
+SELECT * FROM pg_input_error_info('null', 'pg_ndistinct');
+SELECT * FROM pg_input_error_info('{"a": 1}', 'pg_ndistinct');
+SELECT * FROM pg_input_error_info('[]', 'pg_ndistinct');
+SELECT * FROM pg_input_error_info('{}', 'pg_ndistinct');
+SELECT * FROM pg_input_error_info('[null]', 'pg_ndistinct');
+-- Invalid keys
+SELECT '[{"attributes_invalid" : [2,3], "ndistinct" : 4}]'::pg_ndistinct;
+SELECT '[{"attributes" : [2,3], "invalid" : 3, "ndistinct" : 4}]'::pg_ndistinct;
+SELECT * FROM pg_input_error_info('[{"attributes_invalid" : [2,3], "ndistinct" : 4}]', 'pg_ndistinct');
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "invalid" : 3, "ndistinct" : 4}]', 'pg_ndistinct');
+
+-- Missing key
+SELECT '[{"attributes" : [2,3]}]'::pg_ndistinct;
+SELECT '[{"ndistinct" : 4}]'::pg_ndistinct;
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3]}]', 'pg_ndistinct');
+SELECT * FROM pg_input_error_info('[{"ndistinct" : 4}]', 'pg_ndistinct');
+
+-- Special characters
+SELECT '[{"\ud83d\ude04\ud83d\udc36" : [1, 2], "ndistinct" : 4}]'::pg_ndistinct;
+SELECT '[{"attributes" : [1, 2], "\ud83d\ude04\ud83d\udc36" : 4}]'::pg_ndistinct;
+SELECT '[{"attributes" : [1, 2], "ndistinct" : "\ud83d\ude04\ud83d\udc36"}]'::pg_ndistinct;
+SELECT '[{"attributes" : ["\ud83d\ude04\ud83d\udc36", 2], "ndistinct" : 1}]'::pg_ndistinct;
+
+-- Valid keys, invalid values
+SELECT '[{"attributes" : null, "ndistinct" : 4}]'::pg_ndistinct;
+SELECT '[{"attributes" : [], "ndistinct" : 1}]'::pg_ndistinct;
+SELECT '[{"attributes" : [2], "ndistinct" : 4}]'::pg_ndistinct;
+SELECT '[{"attributes" : [2,null], "ndistinct" : 4}]'::pg_ndistinct;
+SELECT '[{"attributes" : [2,3], "ndistinct" : null}]'::pg_ndistinct;
+SELECT '[{"attributes" : [2,"a"], "ndistinct" : 4}]'::pg_ndistinct;
+SELECT '[{"attributes" : [2,3], "ndistinct" : "a"}]'::pg_ndistinct;
+SELECT '[{"attributes" : [2,3], "ndistinct" : []}]'::pg_ndistinct;
+SELECT '[{"attributes" : [2,3], "ndistinct" : [null]}]'::pg_ndistinct;
+SELECT '[{"attributes" : [2,3], "ndistinct" : [1,null]}]'::pg_ndistinct;
+SELECT '[{"attributes" : [2,3], "ndistinct" : {"a": 1}}]'::pg_ndistinct;
+SELECT '[{"attributes" : 1, "ndistinct" : 4}]'::pg_ndistinct;
+SELECT '[{"attributes" : "a", "ndistinct" : 4}]'::pg_ndistinct;
+SELECT '[{"attributes" : {"a": 1}, "ndistinct" : 1}]'::pg_ndistinct;
+SELECT '[{"attributes" : [1, {"a": 1}], "ndistinct" : 1}]'::pg_ndistinct;
+SELECT * FROM pg_input_error_info('[{"attributes" : null, "ndistinct" : 4}]', 'pg_ndistinct');
+SELECT * FROM pg_input_error_info('[{"attributes" : [], "ndistinct" : 1}]', 'pg_ndistinct');
+SELECT * FROM pg_input_error_info('[{"attributes" : [2], "ndistinct" : 4}]', 'pg_ndistinct');
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,null], "ndistinct" : 4}]', 'pg_ndistinct');
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : null}]', 'pg_ndistinct');
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,"a"], "ndistinct" : 4}]', 'pg_ndistinct');
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : "a"}]', 'pg_ndistinct');
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : []}]', 'pg_ndistinct');
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : [null]}]', 'pg_ndistinct');
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : [1,null]}]', 'pg_ndistinct');
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : {"a": 1}}]', 'pg_ndistinct');
+SELECT * FROM pg_input_error_info('[{"attributes" : 1, "ndistinct" : 4}]', 'pg_ndistinct');
+SELECT * FROM pg_input_error_info('[{"attributes" : "a", "ndistinct" : 4}]', 'pg_ndistinct');
+SELECT * FROM pg_input_error_info('[{"attributes" : {"a": 1}, "ndistinct" : 1}]', 'pg_ndistinct');
+SELECT * FROM pg_input_error_info('[{"attributes" : [1, {"a": 1}], "ndistinct" : 1}]', 'pg_ndistinct');
+-- Duplicated attributes
+SELECT '[{"attributes" : [2,2], "ndistinct" : 4}]'::pg_ndistinct;
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,2], "ndistinct" : 4}]', 'pg_ndistinct');
+-- Duplicated attribute lists.
+SELECT '[{"attributes" : [2,3], "ndistinct" : 4},
+         {"attributes" : [2,3], "ndistinct" : 4}]'::pg_ndistinct;
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : 4},
+         {"attributes" : [2,3], "ndistinct" : 4}]', 'pg_ndistinct');
+-- Partially-covered attribute lists.
+SELECT '[{"attributes" : [2,3], "ndistinct" : 4},
+         {"attributes" : [2,-1], "ndistinct" : 4},
+         {"attributes" : [2,3,-1], "ndistinct" : 4},
+         {"attributes" : [1,3,-1,-2], "ndistinct" : 4}]'::pg_ndistinct;
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : 4},
+         {"attributes" : [2,-1], "ndistinct" : 4},
+         {"attributes" : [2,3,-1], "ndistinct" : 4},
+         {"attributes" : [1,3,-1,-2], "ndistinct" : 4}]', 'pg_ndistinct');
+
+-- Valid inputs
+-- Two attributes.
+SELECT '[{"attributes" : [1,2], "ndistinct" : 4}]'::pg_ndistinct;
+-- Three attributes.
+SELECT '[{"attributes" : [-1,2], "ndistinct" : 1},
+         {"attributes" : [-1,3], "ndistinct" : 2},
+	 {"attributes" : [-1,2,3], "ndistinct" : 3}]'::pg_ndistinct;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 23bce72ae64b..03a8d74114d0 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1729,6 +1729,8 @@ MultirangeIOData
 MultirangeParseState
 MultirangeType
 NDBOX
+NDistinctParseState
+NDistinctSemanticState
 NLSVERSIONINFOEX
 NODE
 NTSTATUS
-- 
2.51.0

From 9f60d150fc8449601edfaf9f9fcdfb041963b607 Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Mon, 17 Nov 2025 15:49:36 +0900
Subject: [PATCH v14 2/5] Add working input function for pg_dependencies.

This will be needed for importing extended statistics.
---
 src/backend/utils/adt/pg_dependencies.c       | 742 +++++++++++++++++-
 src/backend/utils/adt/pg_ndistinct.c          |   3 +-
 src/test/regress/expected/pg_dependencies.out | 176 +++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/pg_dependencies.sql      |  47 ++
 5 files changed, 957 insertions(+), 13 deletions(-)
 create mode 100644 src/test/regress/expected/pg_dependencies.out
 create mode 100644 src/test/regress/sql/pg_dependencies.sql

diff --git a/src/backend/utils/adt/pg_dependencies.c b/src/backend/utils/adt/pg_dependencies.c
index 87181aa00e9a..723e322a491f 100644
--- a/src/backend/utils/adt/pg_dependencies.c
+++ b/src/backend/utils/adt/pg_dependencies.c
@@ -14,29 +14,751 @@
 
 #include "postgres.h"
 
+#include "common/int.h"
 
+typedef enum
+{
+	DEPS_EXPECT_START = 0,
+	DEPS_EXPECT_ITEM,
+	DEPS_EXPECT_KEY,
+	DEPS_EXPECT_ATTNUM_LIST,
+	DEPS_EXPECT_ATTNUM,
+	DEPS_EXPECT_DEPENDENCY,
+	DEPS_EXPECT_DEGREE,
+	DEPS_PARSE_COMPLETE
+} DepsParseSemanticState;
+
+typedef struct
+{
+	const char *str;
+	DepsParseSemanticState state;
+
+	List	   *dependency_list;
+	Node	   *escontext;
+
+	bool		found_attributes;	/* Item has an attributes key */
+	bool		found_dependency;	/* Item has an dependency key */
+	bool		found_degree;	/* Item has degree key */
+	List	   *attnum_list;	/* Accumulated attributes attnums */
+	AttrNumber	dependency;
+	double		degree;
+} DependenciesParseState;
+
+/*
+ * Invoked at the start of each MVDependency object.
+ *
+ * The entire JSON document should be one array of MVDependency objects.
+ *
+ * If we are anywhere else in the document, it's an error.
+ */
+static JsonParseErrorType
+dependencies_object_start(void *state)
+{
+	DependenciesParseState *parse = state;
+
+	switch(parse->state)
+	{
+		case DEPS_EXPECT_ITEM:
+			/* Now we expect to see attributes/dependency/degree keys */
+			parse->state = DEPS_EXPECT_KEY;
+			return JSON_SUCCESS;
+			break;
+
+		case DEPS_EXPECT_START:
+			/* pg_dependencies must begin with a '[' */
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Initial element must be an array."));
+			break;
+
+		case DEPS_EXPECT_KEY:
+			/* In an object, expecting key */
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Expected an object key."));
+			break;
+
+		case DEPS_EXPECT_ATTNUM_LIST:
+			/* Just followed an "attributes": key */
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Value of \"%s\" must be an array of attribute numbers.",
+							  PG_DEPENDENCIES_KEY_ATTRIBUTES));
+			break;
+
+		case DEPS_EXPECT_ATTNUM:
+			/* In an attnum list, expect only scalar integers */
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Attribute lists can only contain attribute numbers."));
+			break;
+
+		case DEPS_EXPECT_DEPENDENCY:
+			/* Just followed a "dependency" key */
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Value of \"%s\" must be an integer.",
+							  PG_DEPENDENCIES_KEY_DEPENDENCY));
+			break;
+
+		case DEPS_EXPECT_DEGREE:
+			/* Just followed a "degree" key */
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Value of \"%s\" must be an integer.",
+							  PG_DEPENDENCIES_KEY_DEGREE));
+			break;
+
+		default:
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Unexpected parse state: %d", (int) parse->state));
+	}
+
+	return JSON_SEM_ACTION_FAILED;
+}
+
+static int
+attnum_compare(const void *aptr, const void *bptr)
+{
+	AttrNumber	a = *(const AttrNumber *) aptr;
+	AttrNumber	b = *(const AttrNumber *) bptr;
+
+	return pg_cmp_s16(a, b);
+}
+
+static JsonParseErrorType
+dependencies_object_end(void *state)
+{
+	DependenciesParseState *parse = state;
+
+	MVDependency *dep;
+	AttrNumber *attrsort;
+
+	int			natts = 0;
+
+	if (parse->state != DEPS_EXPECT_KEY)
+	{
+		errsave(parse->escontext,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				errdetail("Unexpected parse state: %d", (int) parse->state));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	if (!parse->found_attributes)
+	{
+		errsave(parse->escontext,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				errdetail("Item must contain \"%s\" key",
+						  PG_DEPENDENCIES_KEY_ATTRIBUTES));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	if (!parse->found_dependency)
+	{
+		errsave(parse->escontext,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				errdetail("Item must contain \"%s\" key.",
+						  PG_DEPENDENCIES_KEY_DEPENDENCY));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	if (!parse->found_degree)
+	{
+		errsave(parse->escontext,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				errdetail("Item must contain \"%s\" key.",
+						  PG_DEPENDENCIES_KEY_DEGREE));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	/*
+	 * We need at least one attribute number a dependencies item, anything
+	 * less is malformed.
+	 */
+	natts = parse->attnum_list->length;
+	if (natts < 1)
+	{
+		errsave(parse->escontext,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				errdetail("The \"%s\" key must contain an array of at least one element.",
+						  PG_DEPENDENCIES_KEY_ATTRIBUTES));
+		return JSON_SEM_ACTION_FAILED;
+	}
+	attrsort = palloc0(natts * sizeof(AttrNumber));
+
+	/*
+	 * Allocate enough space for the dependency, the attnums in the list, plus
+	 * the final attnum
+	 */
+	dep = palloc0(offsetof(MVDependency, attributes) + ((natts + 1) * sizeof(AttrNumber)));
+	dep->nattributes = natts + 1;
+
+	dep->attributes[natts] = parse->dependency;
+	dep->degree = parse->degree;
+
+	attrsort = palloc0(dep->nattributes * sizeof(AttrNumber));
+	attrsort[natts] = parse->dependency;
+
+	for (int i = 0; i < natts; i++)
+	{
+		attrsort[i] = (AttrNumber) parse->attnum_list->elements[i].int_value;
+		dep->attributes[i] = attrsort[i];
+	}
+
+	/* Check attrsort for uniqueness */
+	qsort(attrsort, natts + 1, sizeof(AttrNumber), attnum_compare);
+	for (int i = 1; i < dep->nattributes; i++)
+	{
+		if (attrsort[i] == attrsort[i - 1])
+		{
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Duplicated attribute number found: %d.", attrsort[i]));
+			pfree(attrsort);
+			pfree(dep);
+			return JSON_SEM_ACTION_FAILED;
+		}
+	}
+	pfree(attrsort);
+
+	parse->dependency_list = lappend(parse->dependency_list, (void *) dep);
+
+	/* Reset dependency item state variables */
+	list_free(parse->attnum_list);
+	parse->attnum_list = NIL;
+	parse->dependency = 0;
+	parse->degree = 0.0;
+	parse->found_attributes = false;
+	parse->found_dependency = false;
+	parse->found_degree = false;
+
+	/* Now we are looking for the next MVDependency */
+	parse->state = DEPS_EXPECT_ITEM;
+	return JSON_SUCCESS;
+}
+
+/*
+ * Dependency input format does not have arrays, so any array elements
+ * encountered are an error.
+ */
+static JsonParseErrorType
+dependencies_array_start(void *state)
+{
+	DependenciesParseState *parse = state;
+
+	switch (parse->state)
+	{
+		case DEPS_EXPECT_ATTNUM_LIST:
+			parse->state = DEPS_EXPECT_ATTNUM;
+			break;
+		case DEPS_EXPECT_START:
+			parse->state = DEPS_EXPECT_ITEM;
+			break;
+		default:
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Array found in unexpected place."));
+			return JSON_SEM_ACTION_FAILED;
+	}
+
+	return JSON_SUCCESS;
+}
+
+/*
+ * Either the end of an attnum list or the whole object.
+ */
+static JsonParseErrorType
+dependencies_array_end(void *state)
+{
+	DependenciesParseState *parse = state;
+
+	switch (parse->state)
+	{
+		case DEPS_EXPECT_ATTNUM:
+			if (parse->attnum_list != NIL)
+			{
+				parse->state = DEPS_EXPECT_KEY;
+				return JSON_SUCCESS;
+			}
+
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("The \"%s\" key must be an non-empty array.",
+							  PG_DEPENDENCIES_KEY_ATTRIBUTES));
+			break;
+
+		case DEPS_EXPECT_ITEM:
+			if (parse->dependency_list != NIL)
+			{
+				parse->state = DEPS_PARSE_COMPLETE;
+				return JSON_SUCCESS;
+			}
+
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Item array cannot be empty."));
+			break;
+
+		default:
+			/*
+			 * This can only happen if a case was missed in depenenceies_array_start()
+			 */
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Array found in unexpected place."));
+	}
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ * The valid keys for the MVDependency object are:
+ *   - attributes
+ *   - depeendency
+ *   - degree
+ */
+static JsonParseErrorType
+dependencies_object_field_start(void *state, char *fname, bool isnull)
+{
+	DependenciesParseState *parse = state;
+
+	if (strcmp(fname, PG_DEPENDENCIES_KEY_ATTRIBUTES) == 0)
+	{
+		parse->found_attributes = true;
+		parse->state = DEPS_EXPECT_ATTNUM_LIST;
+		return JSON_SUCCESS;
+	}
+
+	if (strcmp(fname, PG_DEPENDENCIES_KEY_DEPENDENCY) == 0)
+	{
+		parse->found_dependency = true;
+		parse->state = DEPS_EXPECT_DEPENDENCY;
+		return JSON_SUCCESS;
+	}
+
+	if (strcmp(fname, PG_DEPENDENCIES_KEY_DEGREE) == 0)
+	{
+		parse->found_degree = true;
+		parse->state = DEPS_EXPECT_DEGREE;
+		return JSON_SUCCESS;
+	}
+
+	errsave(parse->escontext,
+			errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+			errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+			errdetail("Only allowed keys are \"%s\", \"%s\" and \"%s\".",
+					  PG_DEPENDENCIES_KEY_ATTRIBUTES,
+					  PG_DEPENDENCIES_KEY_DEPENDENCY,
+					  PG_DEPENDENCIES_KEY_DEGREE));
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ * pg_dependencies input format does not have arrays, so any array elements
+ * encountered are an error.
+ */
+static JsonParseErrorType
+dependencies_array_element_start(void *state, bool isnull)
+{
+	DependenciesParseState *parse = state;
+
+	switch(parse->state)
+	{
+		case DEPS_EXPECT_ATTNUM:
+			if (!isnull)
+				return JSON_SUCCESS;
+
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Attribute number array cannot be null."));
+
+			return JSON_SEM_ACTION_FAILED;
+			break;
+
+		case DEPS_EXPECT_ITEM:
+			if (!isnull)
+				return JSON_SUCCESS;
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Item list elements cannot be null."));
+
+			return JSON_SEM_ACTION_FAILED;
+			break;
+
+		default:
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Unexpected array element."));
+	}
+
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ * Handle scalar events from the dependencies input parser.
+ *
+ * There is only one case where we will encounter a scalar, and that is the
+ * dependency degree for the previous object key.
+ */
+static JsonParseErrorType
+dependencies_scalar(void *state, char *token, JsonTokenType tokentype)
+{
+	DependenciesParseState *parse = state;
+	AttrNumber	attnum;
+	ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+	switch(parse->state)
+	{
+		case DEPS_EXPECT_ATTNUM:
+			attnum = pg_strtoint16_safe(token, (Node *) &escontext);
+
+			if (!SOFT_ERROR_OCCURRED(&escontext))
+			{
+				parse->attnum_list = lappend_int(parse->attnum_list, (int) attnum);
+				/* No state change, we expect more attnums */
+				return JSON_SUCCESS;
+			}
+
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Invalid \"%s\" value.", PG_DEPENDENCIES_KEY_ATTRIBUTES));
+			break;
+
+		case DEPS_EXPECT_DEPENDENCY:
+			parse->dependency = (AttrNumber)
+				pg_strtoint16_safe(token, (Node *) &escontext);
+
+			if (!SOFT_ERROR_OCCURRED(&escontext))
+			{
+				parse->state = DEPS_EXPECT_KEY;
+				return JSON_SUCCESS;
+			}
+
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Invalid \"%s\" value.", PG_DEPENDENCIES_KEY_DEPENDENCY));
+			break;
+
+		case DEPS_EXPECT_DEGREE:
+			parse->degree = float8in_internal(token, NULL, "double",
+											  token, (Node *) &escontext);
+
+			if (!SOFT_ERROR_OCCURRED(&escontext))
+			{
+				parse->state = DEPS_EXPECT_KEY;
+				return JSON_SUCCESS;
+			}
+
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Invalid \"%s\" value.", PG_DEPENDENCIES_KEY_DEGREE));
+			break;
+
+		default:
+			errsave(parse->escontext,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+					errdetail("Unexpected scalar."));
+	}
+
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ * Compare the attribute arrays of two MVDependency values,
+ * looking for duplicate sets.
+ */
+static bool
+has_duplicate_attributes(const MVDependency *a, const MVDependency *b)
+{
+	int		i;
+
+	if (a->nattributes != b->nattributes)
+		return false;
+
+	for (i = 0; i < a->nattributes; i++)
+	{
+		if (a->attributes[i] != b->attributes[i])
+			return false;
+	}
+
+	return true;
+}
+
+/*
+ * Ensure that an attnum appears as one of the attnums in a given
+ * MVDependency.
+ */
+static bool
+dep_has_attnum(const MVDependency *item, AttrNumber attnum)
+{
+	for (int i = 0; i < item->nattributes; i++)
+	{
+		if (attnum == item->attributes[i])
+			return true;
+	}
+	return false;
+}
+
+/*
+ * Ensure that the attributes of one MVDependency A are a proper subset
+ * of the reference MVDependency B.
+ */
+static bool
+dep_is_attnum_subset(const MVDependency *item,
+					 const MVDependency *refitem)
+{
+	for (int i = 0; i < item->nattributes; i++)
+	{
+		if (!dep_has_attnum(refitem,item->attributes[i]))
+			return false;
+	}
+	return true;
+}
+
+/*
+ * Generate a string representing an array of attnums. Internally, the
+ * dependency attribute is the last element, so we leave that off.
+ *
+ *
+ * Freeing the allocated string is responsibility of the caller.
+ */
+static const char *
+dep_attnum_list(const MVDependency *item)
+{
+	StringInfoData	str;
+
+	initStringInfo(&str);
+
+	appendStringInfo(&str, "%d", item->attributes[0]);
+
+	for (int i = 1; i < item->nattributes - 1; i++)
+		appendStringInfo(&str, ", %d", item->attributes[i]);
+
+	return str.data;
+}
+
+/*
+ * Return the dependency, which is the last attribute element.
+ */
+static const AttrNumber
+dep_attnum_dependency(const MVDependency *item)
+{
+	return item->attributes[item->nattributes - 1];
+}
+
 /*
  * pg_dependencies_in		- input routine for type pg_dependencies.
  *
- * pg_dependencies is real enough to be a table column, but it has no operations
- * of its own, and disallows input too
+ * This format is valid JSON, with the expected format:
+ *    [{"attributes": [1,2], "dependency": -1, "degree": 1.0000},
+ *     {"attributes": [1,-1], "dependency": 2, "degree": 0.0000},
+ *     {"attributes": [2,-1], "dependency": 1, "degree": 1.0000}]
+ *
  */
 Datum
 pg_dependencies_in(PG_FUNCTION_ARGS)
 {
-	/*
-	 * pg_node_list stores the data in binary form and parsing text input is
-	 * not needed, so disallow this.
-	 */
-	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot accept a value of type %s", "pg_dependencies")));
+	char	   *str = PG_GETARG_CSTRING(0);

 /*
diff --git a/src/backend/utils/adt/pg_ndistinct.c b/src/backend/utils/adt/pg_ndistinct.c
index 4c13b2e62885..9f83f3373527 100644
--- a/src/backend/utils/adt/pg_ndistinct.c
+++ b/src/backend/utils/adt/pg_ndistinct.c
@@ -447,8 +447,7 @@ ndistinct_scalar(void *state, char *token, JsonTokenType tokentype)
 			errsave(parse->escontext,
 					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
-					errdetail("Invalid \"%s\" value.",
-							  PG_NDISTINCT_KEY_NDISTINCT));
+					errdetail("Invalid \"%s\" value.", PG_NDISTINCT_KEY_NDISTINCT));
 			break;
 
 		default:
diff --git a/src/test/regress/expected/pg_dependencies.out b/src/test/regress/expected/pg_dependencies.out
new file mode 100644
index 000000000000..120623717999
--- /dev/null
+++ b/src/test/regress/expected/pg_dependencies.out
@@ -0,0 +1,176 @@
+-- Tests for type pg_distinct
+-- Invalid inputs
+SELECT 'null'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "null"
+LINE 1: SELECT 'null'::pg_dependencies;
+               ^
+DETAIL:  Unexpected scalar.
+SELECT '{"a": 1}'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "{"a": 1}"
+LINE 1: SELECT '{"a": 1}'::pg_dependencies;
+               ^
+DETAIL:  Initial element must be an array.
+SELECT '[]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[]"
+LINE 1: SELECT '[]'::pg_dependencies;
+               ^
+DETAIL:  Item array cannot be empty.
+SELECT '{}'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "{}"
+LINE 1: SELECT '{}'::pg_dependencies;
+               ^
+DETAIL:  Initial element must be an array.
+SELECT '[null]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[null]"
+LINE 1: SELECT '[null]'::pg_dependencies;
+               ^
+DETAIL:  Item list elements cannot be null.
+SELECT * FROM pg_input_error_info('null', 'pg_dependencies');
+              message              |       detail       | hint | sql_error_code 
+-----------------------------------+--------------------+------+----------------
+ malformed pg_dependencies: "null" | Unexpected scalar. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('{"a": 1}', 'pg_dependencies');
+                message                |              detail               | hint | sql_error_code 
+---------------------------------------+-----------------------------------+------+----------------
+ malformed pg_dependencies: "{"a": 1}" | Initial element must be an array. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[]', 'pg_dependencies');
+             message             |           detail            | hint | sql_error_code 
+---------------------------------+-----------------------------+------+----------------
+ malformed pg_dependencies: "[]" | Item array cannot be empty. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('{}', 'pg_dependencies');
+             message             |              detail               | hint | sql_error_code 
+---------------------------------+-----------------------------------+------+----------------
+ malformed pg_dependencies: "{}" | Initial element must be an array. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[null]', 'pg_dependencies');
+               message               |               detail               | hint | sql_error_code 
+-------------------------------------+------------------------------------+------+----------------
+ malformed pg_dependencies: "[null]" | Item list elements cannot be null. |      | 22P02
+(1 row)
+
+-- Invalid keys
+SELECT '[{"attributes_invalid" : [2,3], "dependency" : 4}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes_invalid" : [2,3], "dependency" : 4}]"
+LINE 1: SELECT '[{"attributes_invalid" : [2,3], "dependency" : 4}]':...
+               ^
+DETAIL:  Only allowed keys are "attributes", "dependency" and "degree".
+SELECT '[{"attributes" : [2,3], "invalid" : 3, "dependency" : 4}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "invalid" : 3, "dependency" : 4}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "invalid" : 3, "dependency" ...
+               ^
+DETAIL:  Only allowed keys are "attributes", "dependency" and "degree".
+-- Missing keys
+SELECT '[{"attributes" : [2,3], "dependency" : 4}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : 4}]'::pg_depe...
+               ^
+DETAIL:  Item must contain "degree" key.
+SELECT '[{"attributes" : [2,3], "degree" : 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "degree" : 1.000}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "degree" : 1.000}]'::pg_depe...
+               ^
+DETAIL:  Item must contain "dependency" key.
+SELECT '[{"attributes" : [2,3], "dependency" : 4}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : 4}]'::pg_depe...
+               ^
+DETAIL:  Item must contain "degree" key.
+-- Valid keys, invalid values
+SELECT '[{"attributes" : null, "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : null, "dependency" : 4, "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : null, "dependency" : 4, "degree": 1...
+               ^
+DETAIL:  Unexpected scalar.
+SELECT '[{"attributes" : [2,null], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,null], "dependency" : 4, "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : [2,null], "dependency" : 4, "degree...
+               ^
+DETAIL:  Attribute number array cannot be null.
+SELECT '[{"attributes" : [2,3], "dependency" : null, "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : null, "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : null, "degree...
+               ^
+DETAIL:  Invalid "dependency" value.
+SELECT '[{"attributes" : [2,"a"], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,"a"], "dependency" : 4, "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : [2,"a"], "dependency" : 4, "degree"...
+               ^
+DETAIL:  Invalid "attributes" value.
+SELECT '[{"attributes" : [2,3], "dependency" : "a", "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : "a", "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : "a", "degree"...
+               ^
+DETAIL:  Invalid "dependency" value.
+SELECT '[{"attributes" : [2,3], "dependency" : [], "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : [], "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : [], "degree":...
+               ^
+DETAIL:  Array found in unexpected place.
+SELECT '[{"attributes" : [2,3], "dependency" : [null], "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : [null], "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : [null], "degr...
+               ^
+DETAIL:  Array found in unexpected place.
+SELECT '[{"attributes" : [2,3], "dependency" : [1,null], "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : [1,null], "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : [1,null], "de...
+               ^
+DETAIL:  Array found in unexpected place.
+SELECT '[{"attributes" : 1, "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : 1, "dependency" : 4, "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : 1, "dependency" : 4, "degree": 1.00...
+               ^
+DETAIL:  Unexpected scalar.
+SELECT '[{"attributes" : "a", "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : "a", "dependency" : 4, "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : "a", "dependency" : 4, "degree": 1....
+               ^
+DETAIL:  Unexpected scalar.
+SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": NaN}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4, "degree": NaN}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": ...
+               ^
+DETAIL:  Must be valid JSON.
+-- Duplicated attributes
+SELECT '[{"attributes" : [2,2], "dependency" : 4, "degree": 0.500}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,2], "dependency" : 4, "degree": 0.500}]"
+LINE 1: SELECT '[{"attributes" : [2,2], "dependency" : 4, "degree": ...
+               ^
+DETAIL:  Duplicated attribute number found: 2.
+-- Duplicated attribute lists.
+SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [2,3], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [2,3], "dependency" : 4, "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": ...
+               ^
+DETAIL:  Duplicate "attributes" array: [2, 3] with "dependency": 4.
+-- Partially-covered attribute lists.
+SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [1,-1], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [2,3,-1], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [2,3,-1,-2], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [1,-1], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [2,3,-1], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [2,3,-1,-2], "dependency" : 4, "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": ...
+               ^
+DETAIL:  "attributes" array: [1, -1] with dependency 4 must be a subset of array: [2, 3, -1, -2] with dependency 4.
+-- Valid inputs
+SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": 0.250},
+         {"attributes" : [2,-1], "dependency" : 4, "degree": 0.500},
+         {"attributes" : [2,3,-1], "dependency" : 4, "degree": 0.750},
+         {"attributes" : [2,3,-1,-2], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+                                                                                                                          pg_dependencies                                                                                                                          
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ [{"attributes": [2, 3], "dependency": 4, "degree": 0.250000}, {"attributes": [2, -1], "dependency": 4, "degree": 0.500000}, {"attributes": [2, 3, -1], "dependency": 4, "degree": 0.750000}, {"attributes": [2, 3, -1, -2], "dependency": 4, "degree": 1.000000}]
+(1 row)
+
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index f3f0b5f2f317..cc6d799bceaf 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,7 @@ test: strings md5 numerology point lseg line box path polygon circle date time t
 # geometry depends on point, lseg, line, box, path, polygon, circle
 # horology depends on date, time, timetz, timestamp, timestamptz, interval
 # ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import pg_ndistinct
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import pg_ndistinct pg_dependencies
 
 # ----------
 # Load huge amounts of data
diff --git a/src/test/regress/sql/pg_dependencies.sql b/src/test/regress/sql/pg_dependencies.sql
new file mode 100644
index 000000000000..e94927f5d09d
--- /dev/null
+++ b/src/test/regress/sql/pg_dependencies.sql
@@ -0,0 +1,47 @@
+-- Tests for type pg_distinct
+
+-- Invalid inputs
+SELECT 'null'::pg_dependencies;
+SELECT '{"a": 1}'::pg_dependencies;
+SELECT '[]'::pg_dependencies;
+SELECT '{}'::pg_dependencies;
+SELECT '[null]'::pg_dependencies;
+SELECT * FROM pg_input_error_info('null', 'pg_dependencies');
+SELECT * FROM pg_input_error_info('{"a": 1}', 'pg_dependencies');
+SELECT * FROM pg_input_error_info('[]', 'pg_dependencies');
+SELECT * FROM pg_input_error_info('{}', 'pg_dependencies');
+SELECT * FROM pg_input_error_info('[null]', 'pg_dependencies');
+-- Invalid keys
+SELECT '[{"attributes_invalid" : [2,3], "dependency" : 4}]'::pg_dependencies;
+SELECT '[{"attributes" : [2,3], "invalid" : 3, "dependency" : 4}]'::pg_dependencies;
+-- Missing keys
+SELECT '[{"attributes" : [2,3], "dependency" : 4}]'::pg_dependencies;
+SELECT '[{"attributes" : [2,3], "degree" : 1.000}]'::pg_dependencies;
+SELECT '[{"attributes" : [2,3], "dependency" : 4}]'::pg_dependencies;
+-- Valid keys, invalid values
+SELECT '[{"attributes" : null, "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+SELECT '[{"attributes" : [2,null], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+SELECT '[{"attributes" : [2,3], "dependency" : null, "degree": 1.000}]'::pg_dependencies;
+SELECT '[{"attributes" : [2,"a"], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+SELECT '[{"attributes" : [2,3], "dependency" : "a", "degree": 1.000}]'::pg_dependencies;
+SELECT '[{"attributes" : [2,3], "dependency" : [], "degree": 1.000}]'::pg_dependencies;
+SELECT '[{"attributes" : [2,3], "dependency" : [null], "degree": 1.000}]'::pg_dependencies;
+SELECT '[{"attributes" : [2,3], "dependency" : [1,null], "degree": 1.000}]'::pg_dependencies;
+SELECT '[{"attributes" : 1, "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+SELECT '[{"attributes" : "a", "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": NaN}]'::pg_dependencies;
+-- Duplicated attributes
+SELECT '[{"attributes" : [2,2], "dependency" : 4, "degree": 0.500}]'::pg_dependencies;
+-- Duplicated attribute lists.
+SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [2,3], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+-- Partially-covered attribute lists.
+SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [1,-1], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [2,3,-1], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [2,3,-1,-2], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+-- Valid inputs
+SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": 0.250},
+         {"attributes" : [2,-1], "dependency" : 4, "degree": 0.500},
+         {"attributes" : [2,3,-1], "dependency" : 4, "degree": 0.750},
+         {"attributes" : [2,3,-1,-2], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
-- 
2.51.0

From 34c8c39116e8ef854832419b2b287cc782f79421 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 4 Nov 2025 23:50:01 -0500
Subject: [PATCH v14 3/5] Expose attribute statistics functions for use in
 extended_stats.

 
 
+/*
 
+/*

 /*

 
 
 		if (converted)
 		{
 
 		if (converted)
 		{

 
 		if (converted)
 		{

 	/*
 
 		if (converted)
 		{
 
 		if (converted)
 		{

From 4f0a72a0a052dd337315db63839acd25544fbbf4 Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Tue, 11 Nov 2025 16:58:26 +0900
Subject: [PATCH v14 4/5] Add extended statistics support functions.

 

+/*
 
+/*

+/*

 
 /*

 
 	return result;
 }
+
+static HeapTuple
 
 	return s;
 }
+
+/*
 
+/*
index 98ce7dc28410..970e9bd09833 100644
 
 DROP TABLE stats_temp;
index d140733a7502..48a03f5b8031 100644

 

From 8930510e729fff8c99cae4ea50de1710583f0dfa Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Wed, 5 Nov 2025 01:13:04 -0500
Subject: [PATCH v14 5/5] Include Extended Statistics in pg_dump.

index c84b017f21b8..ab9cb75a86f8 100644

 
 	PQclear(res);
 
+/*
 
+	#

---

From: jian he <[email-masked]>
Date: Mon, 17 Nov 2025 20:53:35 +0800

hi.

NDistinctSemanticState, last element
NDIST_EXPECT_COMPLETE should follow with a comma, like:
``NDIST_EXPECT_COMPLETE, ``

Lots of tests use pg_input_error_info, which is good.
since currently only pg_input_error_info, pg_input_is_valid
NDistinctParseState.escontext is not NULL.

+ /*
seems no coverage for this.

segfault:
SELECT '[{"attributes" : [1,2,3,4,5,67,6,7,8], "ndistinct" : 4}]'::pg_ndistinct;

because src/backend/statistics/mvdistinct.c line: 310, Assert
Assert((item->nattributes >= 2) && (item->nattributes <= STATS_MAX_DIMENSIONS));

here, it should be
+DETAIL:  The "attributes" key must contain an array of at least two attributes.
?

---

From: Corey Huinker <[email-masked]>
Date: Mon, 17 Nov 2025 12:18:55 -0500

> 0001 and 0002 from series v13 have been applied to change the output

Thanks!

Though, I was thinking some more about the output format. Using
jsonb_pretty() makes it readable in one way, and very clumsy in other ways.
Instead, I'm going to try doing the following:

replace (ndist_string_value, '},', E'}\n,')

This will result in the output value being formatted in exactly the way
described in the commit messages.

Of course, we could make the the actual default format by changing
appendStringInfoString(&str, ", ") instead.

One might argue that the output shouldn't get too flowery, but we're
already adding spaces between items and array elements, and we've already
made extensive changes favoring readability over compactness.

Review of changes to input function patches:

I'm curious about the re-parameterization of error messages involving
PG_NDISTINCT_KEY_ATTRIBUTES, PG_NDISTINCT_KEY_NDISTINCT, and similar keys
in dependencies. I like the parameterized version better, and was confused
as to why it was removed. Did you change your mind, or was it done for ease
of translation?

Wording changes all look good. Use of pg_input_error_info() makes sense.

> stronger guarantees for the attributes listed in these elements.

I had a feeling that was going to be requested. My question would be if
that we want to stick to modeling the other combinations after the first
longest combination, last longest, or if we want to defer those checks
altogether until we have to validate against an actual stats object?

> dependencies.c.  Could you look at that?

Yes. I had already looked at it to verify that _all_ combinations were
always generated (they are), because I had some vague memory of the
generator dropping combinations that were statistically insignificant. In
retrospect, I have no idea where I got that idea.

> having at least one element.

This is fairly simple to do. The dependency attnum is just appended to the
list of attnums, and the combinations are generated the same as ndistinct,
though obviously there are no single elements.

There's probably some common code between the lists to be shared, differing
only in how they report missing combinations.

+1

---

From: Michael Paquier <[email-masked]>
Date: Tue, 18 Nov 2025 08:19:58 +0900

> appendStringInfoString(&str, ", ") instead.

This feels like a different pretty still compressed output for json.
I don't think we should change the output functions to do that, but if
you want to add a function that filters these contents a bit in the
tests for the input functions, sure, why not.

> made extensive changes favoring readability over compactness.

I'd still keep it without newlines, FWIW.  So what we have in the
output functions is OK for me.  The key names could be updated to
something else for this release, I'm open for suggestions and we have
time for this release.  It would be nice to not do rename tweaks
several times.

> as to why it was removed. Did you change your mind, or was it done for ease

Yes, this one is to reduce the translation work, and because the
messages are quite the same across the board and deal with the same
requirements:
- Single integer expected after a key (attnum or actual value).
- Array of attribute expected after a key.
- For the degree key, float value.

> altogether until we have to validate against an actual stats object?

I would tend to think that performing one round of validation once the
whole set of objects has been parsed is going to be cheaper than
periodic checks.

One other thing would be to force a sort of the elements in the array
to match with the order these are generated when creating the stats.
We cannot do that in the input functions because we have no idea about
the order of the attributes in the statistics object yet.  Applying a
sort sounds also important to me to make sure that we order the stats
based on what the group generation functions (aka
generate_combinations(), etc.) think on the matter, which would
enforce a stronger binary compatibility after we are sure that we have
a full set of attributes listed in an array with the input function of
course.  I have briefly looked at the planner code where extended
stats are used, like selfuncs.c, and the ordering does not completely
matter, it seems, but it's cheap enough to enforce a stricter ordering
based on the K groups of N elements generated in the import function.

> retrospect, I have no idea where I got that idea.

Hmm.  I would need to double-check the code to be sure, but I don't
think that we drop combinations, because the code prevents duplicates
to begin with, even for expressions:
create table aa (a int, b int);
create statistics stats (ndistinct) ON a, a, b, b from aa;
ERROR:  42701: duplicate column name in statistics definition
create statistics stats (ndistinct) ON (a + a), ((a + a)) from aa;
ERROR:  42701: duplicate expression in statistics definition

These don't make sense anyway because they have a predictible and
perfectly matching correlation relationship.

> though obviously there are no single elements.

Yeah.  That should be not be bad, I hope.

> only in how they report missing combinations.

I would like to agree on that, but it did not look that obvious to me
yesterday.  If you think that something could be refactored, I'd
suggest a refactoring patch that applies on top of the rest of the
patch set, with new generic facilities in stat_util.c, or even a
new separate file, if that leads to a cleaner result (okay, a
definition of "clean" is up to one's taste).
--
Michael

---

From: Michael Paquier <[email-masked]>
Date: Tue, 18 Nov 2025 08:35:00 +0900

> Assert((item->nattributes >= 2) && (item->nattributes <= STATS_MAX_DIMENSIONS));

Ahah, good one here.  Perhaps we had better switch this assertion to
become an elog() with the import functions in mind, as well.  It may
be worth double-checking the other deserialization paths, as well.  We
need to be super careful about this stuff for data imports.
--
Michael

---

From: Corey Huinker <[email-masked]>
Date: Mon, 17 Nov 2025 21:32:37 -0500

> tests for the input functions, sure, why not.

+1, I'll probably just use the replaces rather than define a function and
give someone the false impression that the function exists elsewhere.

> ERROR:  42701: duplicate expression in statistics definition

So I looked at the generator functions, hoping they'd have enough in common
that they could be made generic. And they're just different enough that I
think it's not worth it to try.

But, if we don't care about the order of the combinations, I also don't
think we need to expose the functions at all. We know exactly how many
combinations there should be for any N attributes as each attribute must be
unique. So if we have the right number of unique combinations, and they're
all subsets of the first-longest, then we must have a complete set.
Thoughts on that?

Getting _too_ tight with the ordering and contents makes me concerned for
the day when the format might change. We don't want to _fail_ an upgrade
because some of the combinations were in the wrong order.

> perfectly matching correlation relationship.

They do, for now, but are we willing to lock ourselves into that forever?

> definition of "clean" is up to one's taste).

Looking over those functions, they both could have use the same generator,
but the dependencies-side decided that dependency order doesn't matter,
which puts doubt in my head that the order is perfectly the same for both,
so we'd better follow each individually IF we want to enforce order.

---

From: Corey Huinker <[email-masked]>
Date: Mon, 17 Nov 2025 21:34:14 -0500

> need to be super careful about this stuff for data imports.

Yeah, I liked that one too, and I can act on that while we're debating how
tightly to turn the screws on combinatorics.

---

From: Michael Paquier <[email-masked]>
Date: Tue, 18 Nov 2025 12:34:29 +0900

> because some of the combinations were in the wrong order.

That's fair.  The planner costing code pulling the stats numbers based
on the attributes was smart enough to not care much about the ordering
as far as I recall, but I'd rather make sure of that first.  This
needs some careful lookup.

> They do, for now, but are we willing to lock ourselves into that forever?

Perhaps not.  I cannot say for sure what's the future is going to be
made of.

> so we'd better follow each individually IF we want to enforce order.

I'd try to look at the bits related to pg_dependencies and
pg_ndistinct as two separate concepts, at the end.  They're sort of
alike, but have too many differences already.
--
Michael

---

From: Corey Huinker <[email-masked]>
Date: Mon, 17 Nov 2025 23:39:50 -0500

> needs some careful lookup.

I've done some experiments, creating extended stats objects up to the 8
attribute limit.

The big takeaway is that I wasn't imagining that he number of dependencies
combinations is NOT deterministic:

/*
 * if the dependency seems entirely invalid, don't store it
 */
if (degree == 0.0)
     continue;

So, in theory, an empty (i.e. '[]') pg_dependencies is valid.

The number of pg_ndistinct is deterministic, now, but I'm even less sure
that'll be true in the future.

We can definitely rely on the attnums being all the positive numbers in
ascending order first, followed by the negative numbers in descending
order, but that's about it. Which raises the question of how we describe
the error when attnums are out of order.

We know that the deserialize functions take the data's word for it as to
how many items to unpack, so I don't see the impact of not caring how many
might be missing. That even sort of feeds into Tom's idea that stats import
was in some sense a fuzzing tool.

> alike, but have too many differences already.

Based on the above, I think we can't really add anything beyond the attnum
order, and we have to relax some existing restrictions on pg_dependencies...

---

From: jian he <[email-masked]>
Date: Tue, 18 Nov 2025 13:07:23 +0800

hi.

in src/backend/statistics/mvdistinct.c, we have:
Assert(AttributeNumberIsValid(item->attributes[j]));

should we disallow 0 in key attributes?
SELECT '[{"attributes" : [0,1], "ndistinct" : 4}]'::pg_ndistinct;
I didn't find a way to trigger this Assert yet.

the errdetail is way too generic?
similar to ``select 'a'::int;``
we can
DETAIL:  Invalid input syntax for type integer: "a"
HINT: "ndistinct" value expected to be a type of integer.

what do you think?

we already have "fname" in ndistinct_object_field_start,
we can also print out the "fname", like:
    errsave(parse->escontext,
            errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
            errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
            errdetail("Unexpected key \"%s\"", fname),
            errhint("Only allowed keys are \"%s\" and \"%s\".",
                      PG_NDISTINCT_KEY_ATTRIBUTES,
                      PG_NDISTINCT_KEY_NDISTINCT));

SELECT '[{"attributes" : [2,3], "ndistinct" : 4, "ndistinct" :
14}]'::pg_ndistinct;
               pg_ndistinct
-------------------------------------------
 [{"attributes": [2, 3], "ndistinct": 14}]

SELECT '[{"attributes" : [2,3], "ndistinct" : 4, "attributes" :
[]}]'::pg_ndistinct;
               pg_ndistinct
------------------------------------------
 [{"attributes": [2, 3], "ndistinct": 4}]

Is the above output what we expected?

+ /*
+ * We need at least two attribute numbers for a ndistinct item, anything
+ * less is malformed.
+ */
+ natts = parse->attnum_list->length;
here, we can use list_length.

+ if (parse->attnum_list != NIL)
+ if (parse->distinct_items != NIL)
here, we can also use list_length.

---

From: Michael Paquier <[email-masked]>
Date: Tue, 18 Nov 2025 14:39:04 +0900

> HINT: "ndistinct" value expected to be a type of integer.

That is intentional, as it is intentional to not show the value of the
token in such cases.  This will be mostly used for the import
functions, so I am not sure that it is worth spending time on tuning
all these error cases that one is most likely not going to see as the
main scenarios are going to be through pg_dump/restore, most of the
time in the scope of an upgrade.

> SELECT '[{"attributes" : [2,3], "ndistinct" : 4, "ndistinct" :

Interesting one.  The extra "attributes" should not be required once
we have one set, indeed.
--
Michael

---

From: Corey Huinker <[email-masked]>
Date: Tue, 18 Nov 2025 03:52:31 -0500

> Interesting one.  The extra "attributes" should not be required once

v15:

- catches duplicate object keys cited above
- enforces attnum ordering (ascending positive numbers followed by
descending negative numbers, no zeros allowed), which means we get
duplicate attnum detection for free
- attnum validation is now done as soon as the attnum is parsed
- tests refactored to put attnums in proper order
- unfortunately, this means that one of the error cases from
stats_import.sql (attnum = 0) is now an error rather than something that
can be soft-excluded.
- didn't enforce combinatorical completeness for dependencies because not
all combinations are guaranteed to be there.
- didn't enforce combinatorical completeness for ndistinct because I'm not
convinced we should.
From f569b17679796a197b22f147bba9c51d8132e6bc Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Mon, 17 Nov 2025 14:52:22 +0900
Subject: [PATCH v15 1/5] Add working input function for pg_ndistinct.

This will be needed for importing extended statistics.  With these
changes in place, coverage of pg_ndistinct.c reaches 91%.
---
 src/backend/utils/adt/pg_ndistinct.c       | 732 ++++++++++++++++++++-
 src/test/regress/expected/pg_ndistinct.out | 376 +++++++++++
 src/test/regress/parallel_schedule         |   2 +-
 src/test/regress/sql/pg_ndistinct.sql      |  92 +++
 src/tools/pgindent/typedefs.list           |   2 +
 5 files changed, 1195 insertions(+), 9 deletions(-)
 create mode 100644 src/test/regress/expected/pg_ndistinct.out
 create mode 100644 src/test/regress/sql/pg_ndistinct.sql

diff --git a/src/backend/utils/adt/pg_ndistinct.c b/src/backend/utils/adt/pg_ndistinct.c
index 97efc290ef5..87bc3cf41ae 100644
--- a/src/backend/utils/adt/pg_ndistinct.c
+++ b/src/backend/utils/adt/pg_ndistinct.c
@@ -14,27 +14,743 @@
 
 #include "postgres.h"
 
+#include "common/int.h"
 
+	if ((natts < 2) || (natts > STATS_MAX_DIMENSIONS))
+				errdetail("The \"%s\" key must contain an array of at least %d "
+						  "and no more than %d attributes.",
+						  PG_NDISTINCT_KEY_NDISTINCT, 2, STATS_MAX_DIMENSIONS));
+	item->attributes[0] = (AttrNumber) parse->attnum_list->elements[0].int_value;
+		item->attributes[i] = (AttrNumber) parse->attnum_list->elements[i].int_value;
 
 /*
- * pg_ndistinct_in
+		if (parse->found_attributes)
+					errdetail("Multiple \"%s\" keys are not allowed.",
+		if (parse->found_ndistinct)
+					errdetail("Multiple \"%s\" keys are not allowed.",
+ * Test for valid subsequent attribute number.
+ * If the previous value is positive, then current value must either be
+ * greater than the previous value, or negative.
+ * If the previous value is negative, then the value must be less than
+ * the previous value.
+ * Duplicate values are obviously not allowed, but that is already covered
+ * by the rules listed above.
+valid_subsequent_attnum(const AttrNumber prev, const AttrNumber cur)
+	Assert(prev != 0);
+	if (prev > 0)
+		return ((cur > prev) || (cur < 0));
+	return (cur < prev);
+			if (SOFT_ERROR_OCCURRED(&escontext))
+			 * The attnum cannot be zero a negative number beyond the number of the
+			 * possible expressions.
+			if (attnum == 0 || attnum < (0-STATS_MAX_DIMENSIONS))
+						errdetail("Invalid \"%s\" element: %d.",
+									PG_NDISTINCT_KEY_ATTRIBUTES, attnum));
+				const AttrNumber prev = llast_int(parse->attnum_list);
+				if (!valid_subsequent_attnum(prev, attnum))
+							errdetail("Invalid \"%s\" element: %d cannot follow %d.",
+									PG_NDISTINCT_KEY_ATTRIBUTES, attnum, prev));

 /*
index 00000000000..eb3f74a3fff
@@ -0,0 +1,376 @@
+SELECT '[{"attributes" : [2,3], "attributes" : [1,3], "ndistinct" : 4}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : [2,3], "attributes" : [1,3], "ndistinct" : 4}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "attributes" : [1,3], "ndist...
+DETAIL:  Multiple "attributes" keys are not allowed.
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "attributes" : [1,3], "ndistinct" : 4}]', 'pg_ndistinct');
+-------------------------------------------------------------------------------------------+---------------------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [2,3], "attributes" : [1,3], "ndistinct" : 4}]" | Multiple "attributes" keys are not allowed. |      | 22P02
+-- Valid keys, too many attributes
+SELECT '[{"attributes" : [1,2,3,4,5,6,7,8,9], "ndistinct" : 4}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : [1,2,3,4,5,6,7,8,9], "ndistinct" : 4}]"
+LINE 1: SELECT '[{"attributes" : [1,2,3,4,5,6,7,8,9], "ndistinct" : ...
+DETAIL:  The "ndistinct" key must contain an array of at least 2 and no more than 8 attributes.
+DETAIL:  The "ndistinct" key must contain an array of at least 2 and no more than 8 attributes.
+-------------------------------------------------------------------+----------------------------------------------------------------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [2], "ndistinct" : 4}]" | The "ndistinct" key must contain an array of at least 2 and no more than 8 attributes. |      | 22P02
+DETAIL:  Invalid "attributes" element: 2 cannot follow 2.
+---------------------------------------------------------------------+--------------------------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [2,2], "ndistinct" : 4}]" | Invalid "attributes" element: 2 cannot follow 2. |      | 22P02
+SELECT '[{"attributes" : [2,-1], "ndistinct" : 1},
+         {"attributes" : [3,-1], "ndistinct" : 2},
+	 {"attributes" : [2,3,-1], "ndistinct" : 3}]'::pg_ndistinct;
+ [{"attributes": [2, -1], "ndistinct": 1}, {"attributes": [3, -1], "ndistinct": 2}, {"attributes": [2, 3, -1], "ndistinct": 3}]
 
 # ----------
index 00000000000..7646dedc2d0
@@ -0,0 +1,92 @@
+SELECT '[{"attributes" : [2,3], "attributes" : [1,3], "ndistinct" : 4}]'::pg_ndistinct;
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "attributes" : [1,3], "ndistinct" : 4}]', 'pg_ndistinct');
+-- Valid keys, too many attributes
+SELECT '[{"attributes" : [1,2,3,4,5,6,7,8,9], "ndistinct" : 4}]'::pg_ndistinct;
+SELECT '[{"attributes" : [2,-1], "ndistinct" : 1},
+         {"attributes" : [3,-1], "ndistinct" : 2},
+	 {"attributes" : [2,3,-1], "ndistinct" : 3}]'::pg_ndistinct;
index 23bce72ae64..03a8d74114d 100644

base-commit: 75e82b2f5a6f5de6b42dbd9ea73be5ff36a931b1
-- 
2.51.1

From d7b8d81927cd66915d2f6c07d3443414adbe10d7 Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Mon, 17 Nov 2025 15:49:36 +0900
Subject: [PATCH v15 2/5] Add working input function for pg_dependencies.

This will be needed for importing extended statistics.
---
 src/backend/utils/adt/pg_dependencies.c       | 805 +++++++++++++++++-
 src/test/regress/expected/pg_dependencies.out | 182 ++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/pg_dependencies.sql      |  49 ++
 4 files changed, 1027 insertions(+), 11 deletions(-)
 create mode 100644 src/test/regress/expected/pg_dependencies.out
 create mode 100644 src/test/regress/sql/pg_dependencies.sql

diff --git a/src/backend/utils/adt/pg_dependencies.c b/src/backend/utils/adt/pg_dependencies.c
index 87181aa00e9..5870972b77a 100644
--- a/src/backend/utils/adt/pg_dependencies.c
+++ b/src/backend/utils/adt/pg_dependencies.c
@@ -14,29 +14,814 @@
 
 #include "postgres.h"
 
+#include "common/int.h"
 
+typedef enum
+{
+	DEPS_EXPECT_START = 0,
+	DEPS_EXPECT_ITEM,
+	DEPS_EXPECT_KEY,
+	if ((natts < 1) || (natts > (STATS_MAX_DIMENSIONS - 1)))
+				errdetail("The \"%s\" key must contain an array of at least %d "
+						  " and no than %d elements.",
+						  PG_DEPENDENCIES_KEY_ATTRIBUTES, 1, STATS_MAX_DIMENSIONS - 1));
+	 * the final attnum.
+		dep->attributes[i] = (AttrNumber) parse->attnum_list->elements[i].int_value;
+		if (dep->attributes[i] == parse->dependency)
+					errdetail("Item \"%s\" value %d found in the \"%s\" list.",
+							  PG_DEPENDENCIES_KEY_DEPENDENCY, parse->dependency,
+		if (parse->found_attributes)
+					errdetail("Multiple \"%s\" keys are not allowed.",
+		if (parse->found_dependency)
+					errdetail("Multiple \"%s\" keys are not allowed.",
+		if (parse->found_degree)
+					errdetail("Multiple \"%s\" keys are not allowed.",
+ * Test for valid subsequent attribute number.
+ * If the previous value is positive, then current value must either be
+ * greater than the previous value, or negative.
+ * If the previous value is negative, then the value must be less than
+ * the previous value.
+ * Duplicate values are obviously not allowed, but that is already covered
+ * by the rules listed above.
+valid_subsequent_attnum(const AttrNumber prev, const AttrNumber cur)
+	Assert(prev != 0);
+	if (prev > 0)
+		return ((cur > prev) || (cur < 0));
+	return (cur < prev);
+			if (SOFT_ERROR_OCCURRED(&escontext))
+			 * The attnum cannot be zero a negative number beyond the number of the
+			 * possible expressions.
+			if (attnum == 0 || attnum < (0-STATS_MAX_DIMENSIONS))
+						errdetail("Invalid \"%s\" element: %d.",
+								  PG_DEPENDENCIES_KEY_ATTRIBUTES, attnum));
+				const AttrNumber prev = llast_int(parse->attnum_list);
+				if (!valid_subsequent_attnum(prev, attnum))
+							errdetail("Invalid \"%s\" element: %d cannot follow %d.",
+									PG_DEPENDENCIES_KEY_ATTRIBUTES, attnum, prev));
+			if (SOFT_ERROR_OCCURRED(&escontext))
+			if (SOFT_ERROR_OCCURRED(&escontext))

 /*
index 00000000000..5c3873c8fee
@@ -0,0 +1,182 @@
+-- Valid keys, too many attributes
+SELECT '[{"attributes" : [1,2,3,4,5,6,7,8], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [1,2,3,4,5,6,7,8], "dependency" : 4, "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : [1,2,3,4,5,6,7,8], "dependency" : 4...
+DETAIL:  The "attributes" key must contain an array of at least 1  and no than 7 elements.
+DETAIL:  Invalid "attributes" element: 2 cannot follow 2.
 
 # ----------
index 00000000000..bd09ac98332
@@ -0,0 +1,49 @@
+-- Valid keys, too many attributes
+SELECT '[{"attributes" : [1,2,3,4,5,6,7,8], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;

From bebf1b1dcb542b0542575364ee0e27d774f26ce1 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 4 Nov 2025 23:50:01 -0500
Subject: [PATCH v15 3/5] Expose attribute statistics functions for use in
 extended_stats.

 
 
+/*
 
+/*

 /*

 
 
 		if (converted)
 		{
 
 		if (converted)
 		{

 
 		if (converted)
 		{

 	/*
 
 		if (converted)
 		{
 
 		if (converted)
 		{

From a55cc8e8ab956d8e7729b9eeac85d94004a906cf Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Tue, 11 Nov 2025 16:58:26 +0900
Subject: [PATCH v15 4/5] Add extended statistics support functions.

 src/test/regress/expected/stats_import.out    | 1125 ++++++++++++++++
 9 files changed, 3029 insertions(+), 1 deletion(-)

index aaadfd8c748..cb40eb5d449 100644

 
+/*
 
+/*

+/*

 
 /*

 
 	return result;
 }
+
+static HeapTuple
 
 	return s;
 }
+
+/*
 
+/*
index 98ce7dc2841..8af47821d22 100644
@@ -1342,6 +1346,1127 @@ AND attname = 'i';
 (1 row)
 
 DROP TABLE stats_temp;
+                        {"attributes" : [1,2,3,-1,-2], "ndistinct" : 4}]'::pg_ndistinct
+                        {"attributes" : [0,2,3,-1,-2], "ndistinct" : 4}]'::pg_ndistinct
+ERROR:  malformed pg_ndistinct: "[{"attributes" : [3,-1], "ndistinct" : 4},
+                        {"attributes" : [0,2,3,-1,-2], "ndistinct" : 4}]"
+LINE 6:         'n_distinct', '[{"attributes" : [3,-1], "ndistinct" ...
+DETAIL:  Invalid "attributes" element: 0.
+                        {"attributes" : [3,-1,-4], "ndistinct" : 4},
+                        {"attributes" : [3,-2,-4], "ndistinct" : 4},
+                        {"attributes" : [-1,-2,-4], "ndistinct" : 4},
+ERROR:  malformed pg_dependencies: "[{"attributes": [0], "dependency": -1, "degree": 1.000000}]"
+LINE 6:         'dependencies', '[{"attributes": [0], "dependency": ...
+DETAIL:  Invalid "attributes" element: 0.
index d140733a750..86194519000 100644

 

+                        {"attributes" : [1,2,3,-1,-2], "ndistinct" : 4}]'::pg_ndistinct
+                        {"attributes" : [0,2,3,-1,-2], "ndistinct" : 4}]'::pg_ndistinct
+                        {"attributes" : [3,-1,-4], "ndistinct" : 4},
+                        {"attributes" : [3,-2,-4], "ndistinct" : 4},
+                        {"attributes" : [-1,-2,-4], "ndistinct" : 4},

From 62ed2a1a6d7b894d1ce2b6e9df55853e1f338ec3 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Wed, 5 Nov 2025 01:13:04 -0500
Subject: [PATCH v15 5/5] Include Extended Statistics in pg_dump.

 

 	PQclear(res);
 
+/*
 
+	#

---

From: jian he <[email-masked]>
Date: Wed, 19 Nov 2025 15:49:56 +0800

On Tue, Nov 18, 2025 at 4:52 PM Corey Huinker <[email-masked]> wrote:
> - enforces attnum ordering (ascending positive numbers followed by descending negative numbers, no zeros allowed), which means we get duplicate attnum detection for free
> - unfortunately, this means that one of the error cases from stats_import.sql (attnum = 0) is now an error rather than something that can be soft-excluded.
> - didn't enforce combinatorical completeness for dependencies because not all combinations are guaranteed to be there.
> - didn't enforce combinatorical completeness for ndistinct because I'm not convinced we should.
>

hi.

some of the switch->default, default don't have ``break``.

exposing the ptr_value seems not a good idea, we can foreach_ptr
the attached patch using foreach_ptr.

in function pg_ndistinct_in some errsave can change to ereturn.
(I didn't do this part, though).

+ /*
This part had no coverage tests, so I added a few.

as mentioned before
here PG_NDISTINCT_KEY_NDISTINCT, should be PG_NDISTINCT_KEY_ATTRIBUTES.

Please check the attached minor miscellaneous changes.

---

From: Corey Huinker <[email-masked]>
Date: Thu, 20 Nov 2025 18:08:02 -0500

> some of the switch->default, default don't have ``break``.

The compiler doesn't require them, but I see that we do use them in a lot
of places, so I'll incorporate this.

> + MVNDistinctItem *item =
> parse_state.distinct_items->elements[i].ptr_value;
>

I didn't like this because it makes getting the index value harder, and the
index value is needed in lots of places. Instead I used list_nth() and
list_nth_int().

> (I didn't do this part, though).

-0.25

There's something that I like about the consistency of errsave() followed
by a return that makes it clear that "the function ends here" that I don't
get from ereturn().

What I would really like, is a way to generate unique (but translated)
errdetail values, and move the errsave/ereturn to after the switch
statement.

+1

+1

Did similar things to pg_dependencies.
From 40242563c29a098fe99cba8846a32eb5a780c3d7 Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Mon, 17 Nov 2025 14:52:22 +0900
Subject: [PATCH v16 1/5] Add working input function for pg_ndistinct.

This will be needed for importing extended statistics.  With these
changes in place, coverage of pg_ndistinct.c reaches 91%.
---
 src/backend/utils/adt/pg_ndistinct.c       | 739 ++++++++++++++++++++-
 src/test/regress/expected/pg_ndistinct.out | 409 ++++++++++++
 src/test/regress/parallel_schedule         |   2 +-
 src/test/regress/sql/pg_ndistinct.sql      |  98 +++
 src/tools/pgindent/typedefs.list           |   2 +
 5 files changed, 1241 insertions(+), 9 deletions(-)
 create mode 100644 src/test/regress/expected/pg_ndistinct.out
 create mode 100644 src/test/regress/sql/pg_ndistinct.sql

diff --git a/src/backend/utils/adt/pg_ndistinct.c b/src/backend/utils/adt/pg_ndistinct.c
index 97efc290ef5..fb76bc42395 100644
--- a/src/backend/utils/adt/pg_ndistinct.c
+++ b/src/backend/utils/adt/pg_ndistinct.c
@@ -14,27 +14,750 @@
 
 #include "postgres.h"
 
+#include "common/int.h"
 
+	NDIST_EXPECT_COMPLETE,
+	natts = list_length(parse->attnum_list);
+						  PG_NDISTINCT_KEY_ATTRIBUTES, 2, STATS_MAX_DIMENSIONS));
+		item->attributes[i] = (AttrNumber) list_nth_int(parse->attnum_list, i);
 
 /*
- * pg_ndistinct_in
+			if (list_length(parse->attnum_list) > 0)
+			if (list_length(parse->distinct_items) > 0)
+			if (list_length(parse->attnum_list) > 0)
+ * While the lists must be in canonical order, and this should theoretically
+ * allow for short-circuiting the search, these lists are also never more than
+ * STATS_MAX_DIMENSIONS elements, such cleverness would be wasted.
 
+		int			nitems = list_length(parse_state.distinct_items);
+				if (nitems == 0)
+			MVNDistinctItem *item = list_nth(parse_state.distinct_items, i);
 
 /*
index 00000000000..b057b3b46b9
@@ -0,0 +1,409 @@
+SELECT '[{"attributes" : [2,3], "ndistinct" : 4, "ndistinct" : 4}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : 4, "ndistinct" : 4}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "ndistinct" : 4, "ndistinct"...
+DETAIL:  Multiple "ndistinct" keys are not allowed.
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : 4, "ndistinct" : 4}]', 'pg_ndistinct');
+--------------------------------------------------------------------------------------+--------------------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : 4, "ndistinct" : 4}]" | Multiple "ndistinct" keys are not allowed. |      | 22P02
+DETAIL:  The "attributes" key must contain an array of at least 2 and no more than 8 attributes.
+DETAIL:  The "attributes" key must contain an array of at least 2 and no more than 8 attributes.
+SELECT '[{"attributes" : [0,1], "ndistinct" : 1}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : [0,1], "ndistinct" : 1}]"
+LINE 1: SELECT '[{"attributes" : [0,1], "ndistinct" : 1}]'::pg_ndist...
+SELECT '[{"attributes" : [-7,-9], "ndistinct" : 1}]'::pg_ndistinct;
+ERROR:  malformed pg_ndistinct: "[{"attributes" : [-7,-9], "ndistinct" : 1}]"
+LINE 1: SELECT '[{"attributes" : [-7,-9], "ndistinct" : 1}]'::pg_ndi...
+DETAIL:  Invalid "attributes" element: -9.
+-------------------------------------------------------------------+-----------------------------------------------------------------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [2], "ndistinct" : 4}]" | The "attributes" key must contain an array of at least 2 and no more than 8 attributes. |      | 22P02
+SELECT * FROM pg_input_error_info('[{"attributes" : [-7,-9], "ndistinct" : 1}]', 'pg_ndistinct');
+-----------------------------------------------------------------------+-----------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [-7,-9], "ndistinct" : 1}]" | Invalid "attributes" element: -9. |      | 22P02
 
 # ----------
index 00000000000..104a17da948
@@ -0,0 +1,98 @@
+SELECT '[{"attributes" : [2,3], "ndistinct" : 4, "ndistinct" : 4}]'::pg_ndistinct;
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : 4, "ndistinct" : 4}]', 'pg_ndistinct');
+SELECT '[{"attributes" : [0,1], "ndistinct" : 1}]'::pg_ndistinct;
+SELECT '[{"attributes" : [-7,-9], "ndistinct" : 1}]'::pg_ndistinct;
+SELECT * FROM pg_input_error_info('[{"attributes" : [-7,-9], "ndistinct" : 1}]', 'pg_ndistinct');
index c751c25a04d..7a2195cf25e 100644
@@ -1732,6 +1732,8 @@ MultirangeIOData
 MultirangeParseState
 MultirangeType
 NDBOX
+NDistinctParseState
+NDistinctSemanticState
 NLSVERSIONINFOEX
 NODE
 NTSTATUS

base-commit: 5d4dc112c7c6c10be739d61e8be012f20e9fdbbf
-- 
2.51.1

From 29bc27036a1966dbc29c414d1dd6a1512e0ad524 Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Mon, 17 Nov 2025 15:49:36 +0900
Subject: [PATCH v16 2/5] Add working input function for pg_dependencies.

This will be needed for importing extended statistics.
---
 src/backend/utils/adt/pg_dependencies.c       | 810 +++++++++++++++++-
 src/test/regress/expected/pg_dependencies.out | 376 ++++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/pg_dependencies.sql      |  99 +++
 4 files changed, 1276 insertions(+), 11 deletions(-)
 create mode 100644 src/test/regress/expected/pg_dependencies.out
 create mode 100644 src/test/regress/sql/pg_dependencies.sql

diff --git a/src/backend/utils/adt/pg_dependencies.c b/src/backend/utils/adt/pg_dependencies.c
index 87181aa00e9..bc8795448b2 100644
--- a/src/backend/utils/adt/pg_dependencies.c
+++ b/src/backend/utils/adt/pg_dependencies.c
@@ -14,29 +14,819 @@
 
 #include "postgres.h"
 
+#include "common/int.h"
 
+typedef enum
+{
+	DEPS_EXPECT_START = 0,
+	DEPS_EXPECT_ITEM,
+	DEPS_EXPECT_KEY,
+		dep->attributes[i] = (AttrNumber) list_nth_int(parse->attnum_list, i);
 
+			mvdeps->deps[i] = (MVDependency *) list_nth(list, i);
 
 /*
diff --git a/src/test/regress/expected/pg_dependencies.out b/src/test/regress/expected/pg_dependencies.out
new file mode 100644
index 00000000000..c263c133f08
--- /dev/null
+++ b/src/test/regress/expected/pg_dependencies.out
@@ -0,0 +1,376 @@
+-- Tests for type pg_distinct
+-- Invalid inputs
+SELECT 'null'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "null"
+LINE 1: SELECT 'null'::pg_dependencies;
+               ^
+DETAIL:  Unexpected scalar.
+SELECT '{"a": 1}'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "{"a": 1}"
+LINE 1: SELECT '{"a": 1}'::pg_dependencies;
+               ^
+DETAIL:  Initial element must be an array.
+SELECT '[]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[]"
+LINE 1: SELECT '[]'::pg_dependencies;
+               ^
+DETAIL:  Item array cannot be empty.
+SELECT '{}'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "{}"
+LINE 1: SELECT '{}'::pg_dependencies;
+               ^
+DETAIL:  Initial element must be an array.
+SELECT '[null]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[null]"
+LINE 1: SELECT '[null]'::pg_dependencies;
+               ^
+DETAIL:  Item list elements cannot be null.
+SELECT * FROM pg_input_error_info('null', 'pg_dependencies');
+              message              |       detail       | hint | sql_error_code 
+-----------------------------------+--------------------+------+----------------
+ malformed pg_dependencies: "null" | Unexpected scalar. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('{"a": 1}', 'pg_dependencies');
+                message                |              detail               | hint | sql_error_code 
+---------------------------------------+-----------------------------------+------+----------------
+ malformed pg_dependencies: "{"a": 1}" | Initial element must be an array. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[]', 'pg_dependencies');
+             message             |           detail            | hint | sql_error_code 
+---------------------------------+-----------------------------+------+----------------
+ malformed pg_dependencies: "[]" | Item array cannot be empty. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('{}', 'pg_dependencies');
+             message             |              detail               | hint | sql_error_code 
+---------------------------------+-----------------------------------+------+----------------
+ malformed pg_dependencies: "{}" | Initial element must be an array. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[null]', 'pg_dependencies');
+               message               |               detail               | hint | sql_error_code 
+-------------------------------------+------------------------------------+------+----------------
+ malformed pg_dependencies: "[null]" | Item list elements cannot be null. |      | 22P02
+(1 row)
+
+-- Invalid keys
+SELECT '[{"attributes_invalid" : [2,3], "dependency" : 4}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes_invalid" : [2,3], "dependency" : 4}]"
+LINE 1: SELECT '[{"attributes_invalid" : [2,3], "dependency" : 4}]':...
+               ^
+DETAIL:  Only allowed keys are "attributes", "dependency" and "degree".
+SELECT '[{"attributes" : [2,3], "invalid" : 3, "dependency" : 4}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "invalid" : 3, "dependency" : 4}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "invalid" : 3, "dependency" ...
+               ^
+DETAIL:  Only allowed keys are "attributes", "dependency" and "degree".
+SELECT * FROM pg_input_error_info('[{"attributes_invalid" : [2,3], "dependency" : 4}]', 'pg_dependencies');
+                                     message                                     |                             detail                             | hint | sql_error_code 
+---------------------------------------------------------------------------------+----------------------------------------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes_invalid" : [2,3], "dependency" : 4}]" | Only allowed keys are "attributes", "dependency" and "degree". |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "invalid" : 3, "dependency" : 4}]', 'pg_dependencies');
+                                        message                                         |                             detail                             | hint | sql_error_code 
+----------------------------------------------------------------------------------------+----------------------------------------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [2,3], "invalid" : 3, "dependency" : 4}]" | Only allowed keys are "attributes", "dependency" and "degree". |      | 22P02
+(1 row)
+
+-- Missing keys
+SELECT '[{"attributes" : [2,3], "dependency" : 4}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : 4}]'::pg_depe...
+               ^
+DETAIL:  Item must contain "degree" key.
+SELECT '[{"attributes" : [2,3], "degree" : 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "degree" : 1.000}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "degree" : 1.000}]'::pg_depe...
+               ^
+DETAIL:  Item must contain "dependency" key.
+SELECT '[{"attributes" : [2,3], "dependency" : 4}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : 4}]'::pg_depe...
+               ^
+DETAIL:  Item must contain "degree" key.
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : 4}]', 'pg_dependencies');
+                                 message                                 |             detail              | hint | sql_error_code 
+-------------------------------------------------------------------------+---------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4}]" | Item must contain "degree" key. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "degree" : 1.000}]', 'pg_dependencies');
+                                 message                                 |               detail                | hint | sql_error_code 
+-------------------------------------------------------------------------+-------------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [2,3], "degree" : 1.000}]" | Item must contain "dependency" key. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : 4}]', 'pg_dependencies');
+                                 message                                 |             detail              | hint | sql_error_code 
+-------------------------------------------------------------------------+---------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4}]" | Item must contain "degree" key. |      | 22P02
+(1 row)
+
+-- Valid keys, too many attributes
+SELECT '[{"attributes" : [1,2,3,4,5,6,7,8], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [1,2,3,4,5,6,7,8], "dependency" : 4, "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : [1,2,3,4,5,6,7,8], "dependency" : 4...
+               ^
+DETAIL:  The "attributes" key must contain an array of at least 1  and no than 7 elements.
+SELECT * FROM pg_input_error_info('[{"attributes" : [1,2,3,4,5,6,7,8], "dependency" : 4, "degree": 1.000}]', 'pg_dependencies');
+                                               message                                                |                                      detail                                       | hint | sql_error_code 
+------------------------------------------------------------------------------------------------------+-----------------------------------------------------------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [1,2,3,4,5,6,7,8], "dependency" : 4, "degree": 1.000}]" | The "attributes" key must contain an array of at least 1  and no than 7 elements. |      | 22P02
+(1 row)
+
+-- Valid keys, invalid values
+SELECT '[{"attributes" : null, "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : null, "dependency" : 4, "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : null, "dependency" : 4, "degree": 1...
+               ^
+DETAIL:  Unexpected scalar.
+SELECT '[{"attributes" : [2,null], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,null], "dependency" : 4, "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : [2,null], "dependency" : 4, "degree...
+               ^
+DETAIL:  Attribute number array cannot be null.
+SELECT '[{"attributes" : [2,3], "dependency" : null, "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : null, "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : null, "degree...
+               ^
+DETAIL:  Invalid "dependency" value.
+SELECT '[{"attributes" : [2,"a"], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,"a"], "dependency" : 4, "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : [2,"a"], "dependency" : 4, "degree"...
+               ^
+DETAIL:  Invalid "attributes" value.
+SELECT '[{"attributes" : [2,3], "dependency" : "a", "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : "a", "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : "a", "degree"...
+               ^
+DETAIL:  Invalid "dependency" value.
+SELECT '[{"attributes" : [2,3], "dependency" : [], "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : [], "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : [], "degree":...
+               ^
+DETAIL:  Array found in unexpected place.
+SELECT '[{"attributes" : [2,3], "dependency" : [null], "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : [null], "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : [null], "degr...
+               ^
+DETAIL:  Array found in unexpected place.
+SELECT '[{"attributes" : [2,3], "dependency" : [1,null], "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : [1,null], "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : [1,null], "de...
+               ^
+DETAIL:  Array found in unexpected place.
+SELECT '[{"attributes" : 1, "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : 1, "dependency" : 4, "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : 1, "dependency" : 4, "degree": 1.00...
+               ^
+DETAIL:  Unexpected scalar.
+SELECT '[{"attributes" : "a", "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : "a", "dependency" : 4, "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : "a", "dependency" : 4, "degree": 1....
+               ^
+DETAIL:  Unexpected scalar.
+SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": NaN}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4, "degree": NaN}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": ...
+               ^
+DETAIL:  Must be valid JSON.
+SELECT * FROM pg_input_error_info('[{"attributes" : null, "dependency" : 4, "degree": 1.000}]', 'pg_dependencies');
+                                         message                                         |       detail       | hint | sql_error_code 
+-----------------------------------------------------------------------------------------+--------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : null, "dependency" : 4, "degree": 1.000}]" | Unexpected scalar. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,null], "dependency" : 4, "degree": 1.000}]', 'pg_dependencies');
+                                           message                                           |                 detail                 | hint | sql_error_code 
+---------------------------------------------------------------------------------------------+----------------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [2,null], "dependency" : 4, "degree": 1.000}]" | Attribute number array cannot be null. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : null, "degree": 1.000}]', 'pg_dependencies');
+                                           message                                           |           detail            | hint | sql_error_code 
+---------------------------------------------------------------------------------------------+-----------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : null, "degree": 1.000}]" | Invalid "dependency" value. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,"a"], "dependency" : 4, "degree": 1.000}]', 'pg_dependencies');
+                                          message                                           |           detail            | hint | sql_error_code 
+--------------------------------------------------------------------------------------------+-----------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [2,"a"], "dependency" : 4, "degree": 1.000}]" | Invalid "attributes" value. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : "a", "degree": 1.000}]', 'pg_dependencies');
+                                          message                                           |           detail            | hint | sql_error_code 
+--------------------------------------------------------------------------------------------+-----------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : "a", "degree": 1.000}]" | Invalid "dependency" value. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : [], "degree": 1.000}]', 'pg_dependencies');
+                                          message                                          |              detail              | hint | sql_error_code 
+-------------------------------------------------------------------------------------------+----------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : [], "degree": 1.000}]" | Array found in unexpected place. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : [null], "degree": 1.000}]', 'pg_dependencies');
+                                            message                                            |              detail              | hint | sql_error_code 
+-----------------------------------------------------------------------------------------------+----------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : [null], "degree": 1.000}]" | Array found in unexpected place. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : [1,null], "degree": 1.000}]', 'pg_dependencies');
+                                             message                                             |              detail              | hint | sql_error_code 
+-------------------------------------------------------------------------------------------------+----------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : [1,null], "degree": 1.000}]" | Array found in unexpected place. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[{"attributes" : 1, "dependency" : 4, "degree": 1.000}]', 'pg_dependencies');
+                                       message                                        |       detail       | hint | sql_error_code 
+--------------------------------------------------------------------------------------+--------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : 1, "dependency" : 4, "degree": 1.000}]" | Unexpected scalar. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[{"attributes" : "a", "dependency" : 4, "degree": 1.000}]', 'pg_dependencies');
+                                        message                                         |       detail       | hint | sql_error_code 
+----------------------------------------------------------------------------------------+--------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : "a", "dependency" : 4, "degree": 1.000}]" | Unexpected scalar. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : 4, "degree": NaN}]', 'pg_dependencies');
+                                        message                                         |       detail        | hint | sql_error_code 
+----------------------------------------------------------------------------------------+---------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4, "degree": NaN}]" | Must be valid JSON. |      | 22P02
+(1 row)
+
+-- Duplicated keys
+SELECT '[{"attributes" : [2,3], "attributes": [1,2], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "attributes": [1,2], "dependency" : 4, "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "attributes": [1,2], "depend...
+               ^
+DETAIL:  Multiple "attributes" keys are not allowed.
+SELECT '[{"attributes" : [2,3], "dependency" : 4, "dependency": 4, "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4, "dependency": 4, "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : 4, "dependenc...
+               ^
+DETAIL:  Multiple "dependency" keys are not allowed.
+SELECT '[{"attributes" : [2,3], "dependency": 4, "degree": 1.000, "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency": 4, "degree": 1.000, "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "dependency": 4, "degree": 1...
+               ^
+DETAIL:  Multiple "degree" keys are not allowed.
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "attributes": [1,2], "dependency" : 4, "degree": 1.000}]', 'pg_dependencies');
+                                                    message                                                    |                   detail                    | hint | sql_error_code 
+---------------------------------------------------------------------------------------------------------------+---------------------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [2,3], "attributes": [1,2], "dependency" : 4, "degree": 1.000}]" | Multiple "attributes" keys are not allowed. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : 4, "dependency": 4, "degree": 1.000}]', 'pg_dependencies');
+                                                  message                                                  |                   detail                    | hint | sql_error_code 
+-----------------------------------------------------------------------------------------------------------+---------------------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4, "dependency": 4, "degree": 1.000}]" | Multiple "dependency" keys are not allowed. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency": 4, "degree": 1.000, "degree": 1.000}]', 'pg_dependencies');
+                                                 message                                                  |                 detail                  | hint | sql_error_code 
+----------------------------------------------------------------------------------------------------------+-----------------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [2,3], "dependency": 4, "degree": 1.000, "degree": 1.000}]" | Multiple "degree" keys are not allowed. |      | 22P02
+(1 row)
+
+-- Invalid attnums
+SELECT '[{"attributes" : [0,2], "dependency" : 4, "degree": 0.500}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [0,2], "dependency" : 4, "degree": 0.500}]"
+LINE 1: SELECT '[{"attributes" : [0,2], "dependency" : 4, "degree": ...
+               ^
+DETAIL:  Invalid "attributes" element: 0.
+SELECT '[{"attributes" : [-7,-9], "dependency" : 4, "degree": 0.500}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [-7,-9], "dependency" : 4, "degree": 0.500}]"
+LINE 1: SELECT '[{"attributes" : [-7,-9], "dependency" : 4, "degree"...
+               ^
+DETAIL:  Invalid "attributes" element: -9.
+SELECT * FROM pg_input_error_info('[{"attributes" : [0,2], "dependency" : 4, "degree": 0.500}]', 'pg_dependencies');
+                                         message                                          |              detail              | hint | sql_error_code 
+------------------------------------------------------------------------------------------+----------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [0,2], "dependency" : 4, "degree": 0.500}]" | Invalid "attributes" element: 0. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[{"attributes" : [-7,-9], "dependency" : 4, "degree": 0.500}]', 'pg_dependencies');
+                                          message                                           |              detail               | hint | sql_error_code 
+--------------------------------------------------------------------------------------------+-----------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [-7,-9], "dependency" : 4, "degree": 0.500}]" | Invalid "attributes" element: -9. |      | 22P02
+(1 row)
+
+-- Duplicated attributes
+SELECT '[{"attributes" : [2,2], "dependency" : 4, "degree": 0.500}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,2], "dependency" : 4, "degree": 0.500}]"
+LINE 1: SELECT '[{"attributes" : [2,2], "dependency" : 4, "degree": ...
+               ^
+DETAIL:  Invalid "attributes" element: 2 cannot follow 2.
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,2], "dependency" : 4, "degree": 0.500}]', 'pg_dependencies');
+                                         message                                          |                      detail                      | hint | sql_error_code 
+------------------------------------------------------------------------------------------+--------------------------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [2,2], "dependency" : 4, "degree": 0.500}]" | Invalid "attributes" element: 2 cannot follow 2. |      | 22P02
+(1 row)
+
+-- Duplicated attribute lists.
+SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [2,3], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [2,3], "dependency" : 4, "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": ...
+               ^
+DETAIL:  Duplicate "attributes" array: [2, 3] with "dependency": 4.
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [2,3], "dependency" : 4, "degree": 1.000}]', 'pg_dependencies');
+                                         message                                         |                           detail                           | hint | sql_error_code 
+-----------------------------------------------------------------------------------------+------------------------------------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4, "degree": 1.000},+| Duplicate "attributes" array: [2, 3] with "dependency": 4. |      | 22P02
+          {"attributes" : [2,3], "dependency" : 4, "degree": 1.000}]"                    |                                                            |      | 
+(1 row)
+
+-- Partially-covered attribute lists.
+SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [1,-1], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [2,3,-1], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [2,3,-1,-2], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [1,-1], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [2,3,-1], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [2,3,-1,-2], "dependency" : 4, "degree": 1.000}]"
+LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": ...
+               ^
+DETAIL:  "attributes" array: [1, -1] with dependency 4 must be a subset of array: [2, 3, -1, -2] with dependency 4.
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [1,-1], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [2,3,-1], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [2,3,-1,-2], "dependency" : 4, "degree": 1.000}]', 'pg_dependencies');
+                                         message                                         |                                                   detail                                                   | hint | sql_error_code 
+-----------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4, "degree": 1.000},+| "attributes" array: [1, -1] with dependency 4 must be a subset of array: [2, 3, -1, -2] with dependency 4. |      | 22P02
+          {"attributes" : [1,-1], "dependency" : 4, "degree": 1.000},                   +|                                                                                                            |      | 
+          {"attributes" : [2,3,-1], "dependency" : 4, "degree": 1.000},                 +|                                                                                                            |      | 
+          {"attributes" : [2,3,-1,-2], "dependency" : 4, "degree": 1.000}]"              |                                                                                                            |      | 
+(1 row)
+
+-- Valid inputs
+SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": 0.250},
+         {"attributes" : [2,-1], "dependency" : 4, "degree": 0.500},
+         {"attributes" : [2,3,-1], "dependency" : 4, "degree": 0.750},
+         {"attributes" : [2,3,-1,-2], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+                                                                                                                          pg_dependencies                                                                                                                          
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ [{"attributes": [2, 3], "dependency": 4, "degree": 0.250000}, {"attributes": [2, -1], "dependency": 4, "degree": 0.500000}, {"attributes": [2, 3, -1], "dependency": 4, "degree": 0.750000}, {"attributes": [2, 3, -1, -2], "dependency": 4, "degree": 1.000000}]
+(1 row)
+
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : 4, "degree": 0.250},
+         {"attributes" : [2,-1], "dependency" : 4, "degree": 0.500},
+         {"attributes" : [2,3,-1], "dependency" : 4, "degree": 0.750},
+         {"attributes" : [2,3,-1,-2], "dependency" : 4, "degree": 1.000}]', 'pg_dependencies');
+ message | detail | hint | sql_error_code 
+---------+--------+------+----------------
+         |        |      | 
+(1 row)
+
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index f3f0b5f2f31..cc6d799bcea 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,7 @@ test: strings md5 numerology point lseg line box path polygon circle date time t
 # geometry depends on point, lseg, line, box, path, polygon, circle
 # horology depends on date, time, timetz, timestamp, timestamptz, interval
 # ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import pg_ndistinct
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import pg_ndistinct pg_dependencies
 
 # ----------
 # Load huge amounts of data
diff --git a/src/test/regress/sql/pg_dependencies.sql b/src/test/regress/sql/pg_dependencies.sql
new file mode 100644
index 00000000000..0dda9f76b1c
--- /dev/null
+++ b/src/test/regress/sql/pg_dependencies.sql
@@ -0,0 +1,99 @@
+-- Tests for type pg_distinct
+
+-- Invalid inputs
+SELECT 'null'::pg_dependencies;
+SELECT '{"a": 1}'::pg_dependencies;
+SELECT '[]'::pg_dependencies;
+SELECT '{}'::pg_dependencies;
+SELECT '[null]'::pg_dependencies;
+SELECT * FROM pg_input_error_info('null', 'pg_dependencies');
+SELECT * FROM pg_input_error_info('{"a": 1}', 'pg_dependencies');
+SELECT * FROM pg_input_error_info('[]', 'pg_dependencies');
+SELECT * FROM pg_input_error_info('{}', 'pg_dependencies');
+SELECT * FROM pg_input_error_info('[null]', 'pg_dependencies');
+
+-- Invalid keys
+SELECT '[{"attributes_invalid" : [2,3], "dependency" : 4}]'::pg_dependencies;
+SELECT '[{"attributes" : [2,3], "invalid" : 3, "dependency" : 4}]'::pg_dependencies;
+SELECT * FROM pg_input_error_info('[{"attributes_invalid" : [2,3], "dependency" : 4}]', 'pg_dependencies');
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "invalid" : 3, "dependency" : 4}]', 'pg_dependencies');
+
+-- Missing keys
+SELECT '[{"attributes" : [2,3], "dependency" : 4}]'::pg_dependencies;
+SELECT '[{"attributes" : [2,3], "degree" : 1.000}]'::pg_dependencies;
+SELECT '[{"attributes" : [2,3], "dependency" : 4}]'::pg_dependencies;
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : 4}]', 'pg_dependencies');
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "degree" : 1.000}]', 'pg_dependencies');
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : 4}]', 'pg_dependencies');
+
+-- Valid keys, too many attributes
+SELECT '[{"attributes" : [1,2,3,4,5,6,7,8], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+SELECT * FROM pg_input_error_info('[{"attributes" : [1,2,3,4,5,6,7,8], "dependency" : 4, "degree": 1.000}]', 'pg_dependencies');
+
+-- Valid keys, invalid values
+SELECT '[{"attributes" : null, "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+SELECT '[{"attributes" : [2,null], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+SELECT '[{"attributes" : [2,3], "dependency" : null, "degree": 1.000}]'::pg_dependencies;
+SELECT '[{"attributes" : [2,"a"], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+SELECT '[{"attributes" : [2,3], "dependency" : "a", "degree": 1.000}]'::pg_dependencies;
+SELECT '[{"attributes" : [2,3], "dependency" : [], "degree": 1.000}]'::pg_dependencies;
+SELECT '[{"attributes" : [2,3], "dependency" : [null], "degree": 1.000}]'::pg_dependencies;
+SELECT '[{"attributes" : [2,3], "dependency" : [1,null], "degree": 1.000}]'::pg_dependencies;
+SELECT '[{"attributes" : 1, "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+SELECT '[{"attributes" : "a", "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": NaN}]'::pg_dependencies;
+SELECT * FROM pg_input_error_info('[{"attributes" : null, "dependency" : 4, "degree": 1.000}]', 'pg_dependencies');
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,null], "dependency" : 4, "degree": 1.000}]', 'pg_dependencies');
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : null, "degree": 1.000}]', 'pg_dependencies');
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,"a"], "dependency" : 4, "degree": 1.000}]', 'pg_dependencies');
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : "a", "degree": 1.000}]', 'pg_dependencies');
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : [], "degree": 1.000}]', 'pg_dependencies');
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : [null], "degree": 1.000}]', 'pg_dependencies');
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : [1,null], "degree": 1.000}]', 'pg_dependencies');
+SELECT * FROM pg_input_error_info('[{"attributes" : 1, "dependency" : 4, "degree": 1.000}]', 'pg_dependencies');
+SELECT * FROM pg_input_error_info('[{"attributes" : "a", "dependency" : 4, "degree": 1.000}]', 'pg_dependencies');
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : 4, "degree": NaN}]', 'pg_dependencies');
+
+-- Duplicated keys
+SELECT '[{"attributes" : [2,3], "attributes": [1,2], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+SELECT '[{"attributes" : [2,3], "dependency" : 4, "dependency": 4, "degree": 1.000}]'::pg_dependencies;
+SELECT '[{"attributes" : [2,3], "dependency": 4, "degree": 1.000, "degree": 1.000}]'::pg_dependencies;
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "attributes": [1,2], "dependency" : 4, "degree": 1.000}]', 'pg_dependencies');
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : 4, "dependency": 4, "degree": 1.000}]', 'pg_dependencies');
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency": 4, "degree": 1.000, "degree": 1.000}]', 'pg_dependencies');
+
+-- Invalid attnums
+SELECT '[{"attributes" : [0,2], "dependency" : 4, "degree": 0.500}]'::pg_dependencies;
+SELECT '[{"attributes" : [-7,-9], "dependency" : 4, "degree": 0.500}]'::pg_dependencies;
+SELECT * FROM pg_input_error_info('[{"attributes" : [0,2], "dependency" : 4, "degree": 0.500}]', 'pg_dependencies');
+SELECT * FROM pg_input_error_info('[{"attributes" : [-7,-9], "dependency" : 4, "degree": 0.500}]', 'pg_dependencies');
+
+-- Duplicated attributes
+SELECT '[{"attributes" : [2,2], "dependency" : 4, "degree": 0.500}]'::pg_dependencies;
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,2], "dependency" : 4, "degree": 0.500}]', 'pg_dependencies');
+
+-- Duplicated attribute lists.
+SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [2,3], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [2,3], "dependency" : 4, "degree": 1.000}]', 'pg_dependencies');
+
+-- Partially-covered attribute lists.
+SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [1,-1], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [2,3,-1], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [2,3,-1,-2], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [1,-1], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [2,3,-1], "dependency" : 4, "degree": 1.000},
+         {"attributes" : [2,3,-1,-2], "dependency" : 4, "degree": 1.000}]', 'pg_dependencies');
+
+-- Valid inputs
+SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": 0.250},
+         {"attributes" : [2,-1], "dependency" : 4, "degree": 0.500},
+         {"attributes" : [2,3,-1], "dependency" : 4, "degree": 0.750},
+         {"attributes" : [2,3,-1,-2], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : 4, "degree": 0.250},
+         {"attributes" : [2,-1], "dependency" : 4, "degree": 0.500},
+         {"attributes" : [2,3,-1], "dependency" : 4, "degree": 0.750},
+         {"attributes" : [2,3,-1,-2], "dependency" : 4, "degree": 1.000}]', 'pg_dependencies');
-- 
2.51.1

From e0c7f3fdf2a82c39aaa6e3341384db1293a37449 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 4 Nov 2025 23:50:01 -0500
Subject: [PATCH v16 3/5] Expose attribute statistics functions for use in
 extended_stats.

 
 
+/*
 
+/*

 /*

 
 
 		if (converted)
 		{
 
 		if (converted)
 		{

 
 		if (converted)
 		{

 	/*
 
 		if (converted)
 		{
 
 		if (converted)
 		{

From 2eaf2c81acd0e570df7b14a3625ebebb1c85ed9b Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Tue, 11 Nov 2025 16:58:26 +0900
Subject: [PATCH v16 4/5] Add extended statistics support functions.

 

+/*
 
+/*

+/*

 
 /*

 
 	return result;
 }
+
+static HeapTuple
 
 	return s;
 }
+
+/*
 
+/*
 
 DROP TABLE stats_temp;

 

From df69424f8de71e7c70b6bea2287c39c652a3866d Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Wed, 5 Nov 2025 01:13:04 -0500
Subject: [PATCH v16 5/5] Include Extended Statistics in pg_dump.

 

 	PQclear(res);
 
+/*
 
+	#

---

From: Chao Li <[email-masked]>
Date: Fri, 21 Nov 2025 11:36:23 +0800

After 16 rounds of revisions, v16 looks solid and robust. Only got a few small comments:

> On Nov 21, 2025, at 07:08, Corey Huinker <[email-masked]> wrote:
> 
> <v16-0001-Add-working-input-function-for-pg_ndistinct.patch><v16-0002-Add-working-input-function-for-pg_dependencies.patch><v16-0003-Expose-attribute-statistics-functions-for-use-in.patch><v16-0004-Add-extended-statistics-support-functions.patch><v16-0005-Include-Extended-Statistics-in-pg_dump.patch>

1 - 0001
```
+	/*

Do we also need to reset item state vars similar to the success case?

2 - 0001
```
*

This function comment is too simple, and doesn’t match to the function name. Reading the function body, the logic is clear, so I would suggest either just remove the comment or enhance it.

3  - 0001
```
static JsonParseErrorType
+ndistinct_array_element_start(void *state, bool isnull)
+{
+	NDistinctParseState *parse = state;
```

Nit: NDistinctParseState * can be const.

4 - 0001
```
+static bool

AttrNumber is int16, thus “const” here is unnecessary.

5 - 0001
```
+			return JSON_SUCCESS;
+			break;
```

“Break” after “return” is unnecessary. I tried to delete the “break” and didn’t see a compile warning, it should be safe to delete the “break”.

6 - 0001
```
+/*

+ */
+static bool

The function comment says “looking for duplicate sets”, seems to imply the function would return the set, but the function returns a bool, which is a little confusing.

7 - 0002
```

Similar to comment 1, do we need to reset state vars upon failure?

8 - 0002
```

I believe “more” is missed between “no than”.

9 - 0002
```

Similar to commit 3, DependenciesParseState * can be const.

10 - 0002
```
static bool
valid_subsequent_attnum(const AttrNumber prev, const AttrNumber cur)
```

This function is a dup to the one in 0001, can we put it to a common place?

11 - 0003
```

Several functions are made external visible, they are all renamed with adding a prefix “statatt_”, why text_to_stavalues is an exception?

11 - 0004
```
+bool

This MVDependency * can be const.

12 - 0004
```
static void
upsert_pg_statistic_ext_data(Datum *values, bool *nulls, bool *replaces)
{
```

This function pass values, nulls and replaces to heap_modify_tuple() and heap_form_tuple(), the both functions take all const pointers as parameters. So, here values, nulls and replaces can all be const.

13 - 0004
```

Here: char    *s = TextDatumGetCString(exprs_elems[offset + NULL_FRAC_ELEM]);
Should NULL_FRAC_ELEM be AVG_WIDTH_ELEM?

14 - 0004
```

Similar to comment 13, should NULL_FRAC_ELEM be N_DISTINCT_ELEM?

15 - 0004
```

import_mcvlist is declared twice, looks like a copy-paste mistake.

16 - 0005
```

“statsstats” looks weird, I’d suggest PREPQUERY_DUMPEXTSTATSDATA.

Best regards,
—
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/

---

From: Yuefei Shi <[email-masked]>
Date: Fri, 21 Nov 2025 13:33:56 +0800

On Fri, Nov 21, 2025 at 10:54 AM Corey Huinker <[email-masked]>
wrote:

> I didn't like this because it makes getting the index value harder, and
> the index value is needed in lots of places. Instead I used list_nth() and
> list_nth_int().
>
>
>
A few small comments.

1. Minor typo fixes:
In pg_dependencies_in: "pg_dependencies parssing claims..." could be
corrected to "pg_dependencies parsing claims..."
In ndistinct_array_end: "must be an non-empty array" might be better as
"must be a non-empty array"
In the comment for dependencies_object_field_start: "depeendency" appears
to be a typo for "dependency"

2.Code maintainability suggestion:
I noticed the string "malformed pg_dependencies: "%s"" is used repeatedly
throughout the code. Would you consider defining this as a macro? This
could reduce duplication and make future updates easier.

3.Memory management observation:
Regarding item_attnum_list, while PostgreSQL's memory context mechanism
handles cleanup, explicitly freeing the allocated memory after use might
improve code clarity.

These are all minor points - the implementation looks solid overall. Thank
you for your work on this feature!

---

From: jian he <[email-masked]>
Date: Fri, 21 Nov 2025 15:16:26 +0800

hi.

makeJsonLexContextCstringLen in v16-0001 and v16-0002
should use GetDatabaseEncoding()?

wondering, should we check the value of "degree" is between 0 to 1?

DETAIL:  The "attributes" key must contain an array of at least 1  and
no than 7 elements.
if you check the error message carefully, there is one more extra
white space after "at least 1".

typedef struct MVDependency
{
    double        degree;            /* degree of validity (0-1) */
    AttrNumber    nattributes;    /* number of attributes */
    AttrNumber    attributes[FLEXIBLE_ARRAY_MEMBER];    /* attribute numbers */
} MVDependency;

SELECT '[{"attributes" : [2], "dependency" : 4, "degree":
"NaN"}]'::pg_dependencies;
SELECT '[{"attributes" : [2], "dependency" : 4, "degree":
"-inf"}]'::pg_dependencies;
SELECT '[{"attributes" : [2], "dependency" : 4, "degree":
"inf"}]'::pg_dependencies;

either error out or not, pg_dependencies.sql needs tests like the
above to test these special values.
json does not support special value like, "inf", "NaN", so we can
SELECT '[{"attributes" : [2], "dependency" : 4, "degree":
"-inf"}]'::pg_dependencies;
but can not
SELECT '[{"attributes" : [2], "dependency" : 4, "degree":
"-inf"}]'::pg_dependencies::text::pg_dependencies;

I found that numeric values behave the same with or without double quotes.
SELECT '[{"ndistinct" : "1", "attributes" : ["2","3"]}]'::pg_ndistinct;
SELECT '[{"ndistinct" : 1, "attributes" : [2,3]}]'::pg_ndistinct;
SELECT '[{"attributes" : [1, 2], "dependency" : 4, "degree":
"1"}]'::pg_dependencies;
SELECT '[{"attributes" : [1, "2"], "dependency" : "4", "degree":
"1"}]'::pg_dependencies;
i guess the reason is in makeJsonLexContextCstringLen, we set
need_escapes to true.

I added more regress tests to improve coverage for
src/test/regress/sql/pg_dependencies.sql

SELECT '[{"attributes" : [-11,1], "dependency" : -1116, "degree":
"1.2"}]'::pg_dependencies;
DETAIL:  Invalid "attributes" element: -11.
we already check the key "attributes", we can also check "dependency",
IF the value of ""dependency"
does not meet
``(if (parse->dependency == 0 || parse->dependency < (0-STATS_MAX_DIMENSIONS))``
then error out.
see the attached patch.

---

From: Corey Huinker <[email-masked]>
Date: Fri, 21 Nov 2025 17:41:15 -0500

> 1. Minor typo fixes:

+1 Thanks.

> could reduce duplication and make future updates easier.

It seems to be the way of things. As I stated earlier, what I'd really like
is the ability to form different errdetails and have them feed into one
errsave(), and still have the strings go through translation.

+1

---

From: Corey Huinker <[email-masked]>
Date: Sat, 22 Nov 2025 03:26:19 -0500

> wondering, should we check the value of "degree" is between 0 to 1?

I'm open to it, but there was significant pushback to having tight validity
checks on other stats types.

The more things that we make errors, the more chances we have to breaking a
dump/restore. Granted, such values should never happen, but odd things do
happen.

> (0-STATS_MAX_DIMENSIONS))``

Good catch.

Incorporated these patches as well as Yuefei's feedback especially around
memory management.

I added a comment debating the feasibility of testing for subsets of
attribute sets in pg_dependencies. Basically, I think we can't have the
test at all, but I haven't removed it just yet pending consensus.
From 92e344edf02ddc05a9ef29e89900f63357125331 Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Mon, 17 Nov 2025 14:52:22 +0900
Subject: [PATCH v17 1/5] Add working input function for pg_ndistinct.

This will be needed for importing extended statistics.  With these
changes in place, coverage of pg_ndistinct.c reaches 91%.
---
 src/backend/utils/adt/pg_ndistinct.c       | 761 ++++++++++++++++++++-
 src/test/regress/expected/pg_ndistinct.out | 409 +++++++++++
 src/test/regress/parallel_schedule         |   2 +-
 src/test/regress/sql/pg_ndistinct.sql      |  98 +++
 src/tools/pgindent/typedefs.list           |   2 +
 5 files changed, 1263 insertions(+), 9 deletions(-)
 create mode 100644 src/test/regress/expected/pg_ndistinct.out
 create mode 100644 src/test/regress/sql/pg_ndistinct.sql

diff --git a/src/backend/utils/adt/pg_ndistinct.c b/src/backend/utils/adt/pg_ndistinct.c
index 97efc290ef5..443e0828655 100644
--- a/src/backend/utils/adt/pg_ndistinct.c
+++ b/src/backend/utils/adt/pg_ndistinct.c
@@ -14,29 +14,774 @@
 
 #include "postgres.h"
 
+#include "common/int.h"

 /*
- * pg_ndistinct_in
+					errdetail("The \"%s\" key must be a non-empty array.",
+ * The overall structure of the datatype is an array, but there are also
+ * arrays as the value of every attributes key.
+	const NDistinctParseState *parse = state;
+valid_subsequent_attnum(AttrNumber prev, AttrNumber cur)
+ * looking for duplicate sets. Return true if a duplicate set is found.
+ * The arrays are required to be in canonical order (all positive numbers
+ * in ascending order first, followed by all negative numbers in descending
+ * order) so it's safe to compare the attrnums in order, stopping at the
+ * first difference.
+item_attrbutes_eq(const MVNDistinctItem *a, const MVNDistinctItem *b)
+static char *
+ * Attempt to build and serialize the MVNDistinct object.
+ * This can only be executed after the completion of the JSON parsing.
+ * In the event of an error, set the error context and return NULL.
+static bytea *
+build_mvndistinct(NDistinctParseState *parse, char *str)
+	int			nitems = list_length(parse->distinct_items);
+					errdetail("Unexpected end state %d.", parse->state));
+		MVNDistinctItem *item = list_nth(parse->distinct_items, i);
+			if (item_attrbutes_eq(item, &ndistinct->items[j]))
+				char *s = item_attnum_list(item);
+								  PG_NDISTINCT_KEY_ATTRIBUTES, s));
+		 * This transfers free-ing responsibility from the distinct_items
+		 * list to the ndistinct object.
+			char *item_list = item_attnum_list(item);
+			char *refitem_list = item_attnum_list(refitem);
+			pfree(item_list);
+			pfree(refitem_list);
+	 * Free the attribute lists, before the ndistinct itself.
+	return bytes;
+	bytea	   *bytes = NULL;
 
+		bytes = build_mvndistinct(&parse_state, str);
+	list_free(parse_state.attnum_list);
+	list_free_deep(parse_state.distinct_items);
+	if (bytes)
 
+
 /*
  * pg_ndistinct_out
index 00000000000..10adebbdff6
+DETAIL:  The "attributes" key must be a non-empty array.
+------------------------------------------------------------------+-------------------------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [], "ndistinct" : 1}]" | The "attributes" key must be a non-empty array. |      | 22P02
 
 # ----------
index 27a4d131897..d422f2c070c 100644

base-commit: 51da766494dcc84b6f8d793ecaa064363a9243c2
-- 
2.51.1

From c47a55386cafbb95b01dd338ea0898bd0395486d Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Mon, 17 Nov 2025 15:49:36 +0900
Subject: [PATCH v17 2/5] Add working input function for pg_dependencies.

This will be needed for importing extended statistics.
---
 src/backend/utils/adt/pg_dependencies.c       | 869 +++++++++++++++++-
 src/test/regress/expected/pg_dependencies.out | 447 +++++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/pg_dependencies.sql      | 116 +++
 4 files changed, 1423 insertions(+), 11 deletions(-)
 create mode 100644 src/test/regress/expected/pg_dependencies.out
 create mode 100644 src/test/regress/sql/pg_dependencies.sql

diff --git a/src/backend/utils/adt/pg_dependencies.c b/src/backend/utils/adt/pg_dependencies.c
index 87181aa00e9..baa4270a7c8 100644
--- a/src/backend/utils/adt/pg_dependencies.c
+++ b/src/backend/utils/adt/pg_dependencies.c
@@ -14,31 +14,880 @@
 
 #include "postgres.h"
 
+#include "common/int.h"
 
+typedef enum
+{
+	DEPS_EXPECT_START = 0,
+	DEPS_EXPECT_ITEM,
+	DEPS_EXPECT_KEY,
+	DEPS_PARSE_COMPLETE,
+ * Handle the end of an MVDependency object's JSON representation.
+						  "and no more than %d elements.",
+	 * Assign attnums to the attributes array, comparing each one against the
+	 * dependency attributes to ensure that there there are no matches.
+	 * Reset dependency item state variables to look for the next MVDependency.
+			if (list_length(parse->dependency_list) > 0)
+ *   - dependency
+			 * The dependency attnum cannot be zero a negative number beyond the number
+			 * of the possible expressions.
+			if (parse->dependency == 0 || parse->dependency < (0-STATS_MAX_DIMENSIONS))
+						errdetail("Invalid \"%s\" value: %d.",
+								  PG_DEPENDENCIES_KEY_DEPENDENCY, parse->dependency));
+dep_attrbutes_eq(const MVDependency *a, const MVDependency *b)
+static char *
+static AttrNumber
+ * Attempt to build and serialize the MVDependencies object.
+ * This can only be executed after the completion of the JSON parsing.
+ * In the event of an error, set the error context and return NULL.
+static bytea *
+build_mvdependencies(DependenciesParseState *parse, char *str)
+	int		ndeps = list_length(parse->dependency_list);
+			if (ndeps == 0)
+					 "pg_dependencies parsing claims success with an empty item list.");
+					errdetail("Unexpected end state %d.", parse->state));
+	mvdeps = palloc0(offsetof(MVDependencies, deps)
+					 + (ndeps * sizeof(MVDependency *)));
+		 * Use the MVDependency objects in the dependency_list.
+		 * Because we free the dependency_list after parsing is done, we cannot
+		 * free it here.
+		mvdeps->deps[i] = list_nth(parse->dependency_list, i);
+			if (dep_attrbutes_eq(mvdeps->deps[i], mvdeps->deps[j]))
+				char		   *attnum_list = dep_attnum_list(dep);
+				AttrNumber		attnum_dep = dep_attnum_dependency(dep);
+								  attnum_list, attnum_dep));
+	 * TODO:
+	 * I'm fairly certain that because statisticsally insignificant dependency
+	 * combinations are not stored, there is a chance that the longest dependency
+	 * does not exist, and therefore this test cannot be done. I have left the
+	 * test in place for the time being until the issue can be definitively
+	 * settled.
+			char *dep_list = dep_attnum_list(dep);
+			char *refdep_list = dep_attnum_list(refdep);
+	 * No need to free the individual MVDependency objects, because they are still
+	 * in the dependency_list, and will be freed with that.
+	return bytes;
+	bytea	   *bytes = NULL;
 
+		bytes = build_mvdependencies(&parse_state, str);
+	list_free_deep(parse_state.dependency_list);
+	list_free(parse_state.attnum_list);
+	if (bytes)
 
+
 /*
  * pg_dependencies_out		- output routine for type pg_dependencies.
  */
index 00000000000..fcf5144a5ea
@@ -0,0 +1,447 @@
+DETAIL:  The "attributes" key must contain an array of at least 1 and no more than 7 elements.
+------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [1,2,3,4,5,6,7,8], "dependency" : 4, "degree": 1.000}]" | The "attributes" key must contain an array of at least 1 and no more than 7 elements. |      | 22P02
+SELECT '[{"attributes": [], "dependency": 2, "degree": 1}]' ::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes": [], "dependency": 2, "degree": 1}]"
+LINE 1: SELECT '[{"attributes": [], "dependency": 2, "degree": 1}]' ...
+SELECT '[{"attributes" : {"a": 1}, "dependency" : 4, "degree": "1.2"}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : {"a": 1}, "dependency" : 4, "degree": "1.2"}]"
+LINE 1: SELECT '[{"attributes" : {"a": 1}, "dependency" : 4, "degree...
+SELECT '[{"dependency" : 4, "degree": "1.2"}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"dependency" : 4, "degree": "1.2"}]"
+LINE 1: SELECT '[{"dependency" : 4, "degree": "1.2"}]'::pg_dependenc...
+DETAIL:  Item must contain "attributes" key
+SELECT '[{"attributes" : [1,2,3,4,5,6,7], "dependency" : 0, "degree": "1.2"}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [1,2,3,4,5,6,7], "dependency" : 0, "degree": "1.2"}]"
+LINE 1: SELECT '[{"attributes" : [1,2,3,4,5,6,7], "dependency" : 0, ...
+DETAIL:  Invalid "dependency" value: 0.
+SELECT '[{"attributes" : [1,2,3,4,5,6,7], "dependency" : -9, "degree": "1.2"}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [1,2,3,4,5,6,7], "dependency" : -9, "degree": "1.2"}]"
+LINE 1: SELECT '[{"attributes" : [1,2,3,4,5,6,7], "dependency" : -9,...
+DETAIL:  Invalid "dependency" value: -9.
+SELECT '[{"attributes": [1,2], "dependency": 2, "degree": 1}]' ::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes": [1,2], "dependency": 2, "degree": 1}]"
+LINE 1: SELECT '[{"attributes": [1,2], "dependency": 2, "degree": 1}...
+DETAIL:  Item "dependency" value 2 found in the "attributes" list.
+SELECT '[{"attributes" : [1, {}], "dependency" : 1, "degree": "1.2"}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [1, {}], "dependency" : 1, "degree": "1.2"}]"
+LINE 1: SELECT '[{"attributes" : [1, {}], "dependency" : 1, "degree"...
+SELECT '[{"attributes" : [1,2], "dependency" : {}, "degree": 1.0}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [1,2], "dependency" : {}, "degree": 1.0}]"
+LINE 1: SELECT '[{"attributes" : [1,2], "dependency" : {}, "degree":...
+DETAIL:  Value of "dependency" must be an integer.
+SELECT '[{"attributes" : [1,2], "dependency" : 3, "degree": {}}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [1,2], "dependency" : 3, "degree": {}}]"
+LINE 1: SELECT '[{"attributes" : [1,2], "dependency" : 3, "degree": ...
+DETAIL:  Value of "degree" must be an integer.
+SELECT '[{"attributes" : [1,2], "dependency" : 1, "degree": "a"}]'::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes" : [1,2], "dependency" : 1, "degree": "a"}]"
+LINE 1: SELECT '[{"attributes" : [1,2], "dependency" : 1, "degree": ...
+DETAIL:  Invalid "degree" value.
+SELECT '[{"attributes" : [2], "dependency" : 4, "degree": "NaN"}]'::pg_dependencies;
+-------------------------------------------------------
+ [{"attributes": [2], "dependency": 4, "degree": NaN}]
+SELECT '[{"attributes" : [2], "dependency" : 4, "degree": "-inf"}]'::pg_dependencies;
+-------------------------------------------------------------
+ [{"attributes": [2], "dependency": 4, "degree": -Infinity}]
+SELECT '[{"attributes" : [2], "dependency" : 4, "degree": "inf"}]'::pg_dependencies;
+------------------------------------------------------------
+ [{"attributes": [2], "dependency": 4, "degree": Infinity}]
+SELECT '[{"attributes" : [2], "dependency" : 4, "degree": "-inf"}]'::pg_dependencies::text::pg_dependencies;
+ERROR:  malformed pg_dependencies: "[{"attributes": [2], "dependency": 4, "degree": -Infinity}]"
 
 # ----------
index 00000000000..ad91df99110
@@ -0,0 +1,116 @@
+SELECT '[{"attributes": [], "dependency": 2, "degree": 1}]' ::pg_dependencies;
+SELECT '[{"attributes" : {"a": 1}, "dependency" : 4, "degree": "1.2"}]'::pg_dependencies;
+SELECT '[{"dependency" : 4, "degree": "1.2"}]'::pg_dependencies;
+SELECT '[{"attributes" : [1,2,3,4,5,6,7], "dependency" : 0, "degree": "1.2"}]'::pg_dependencies;
+SELECT '[{"attributes" : [1,2,3,4,5,6,7], "dependency" : -9, "degree": "1.2"}]'::pg_dependencies;
+SELECT '[{"attributes": [1,2], "dependency": 2, "degree": 1}]' ::pg_dependencies;
+SELECT '[{"attributes" : [1, {}], "dependency" : 1, "degree": "1.2"}]'::pg_dependencies;
+SELECT '[{"attributes" : [1,2], "dependency" : {}, "degree": 1.0}]'::pg_dependencies;
+SELECT '[{"attributes" : [1,2], "dependency" : 3, "degree": {}}]'::pg_dependencies;
+SELECT '[{"attributes" : [1,2], "dependency" : 1, "degree": "a"}]'::pg_dependencies;
+SELECT '[{"attributes" : [2], "dependency" : 4, "degree": "NaN"}]'::pg_dependencies;
+SELECT '[{"attributes" : [2], "dependency" : 4, "degree": "-inf"}]'::pg_dependencies;
+SELECT '[{"attributes" : [2], "dependency" : 4, "degree": "inf"}]'::pg_dependencies;
+SELECT '[{"attributes" : [2], "dependency" : 4, "degree": "-inf"}]'::pg_dependencies::text::pg_dependencies;

From 9735f95d5bcaeb89fcde6c2ae1b1402f8904d781 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 4 Nov 2025 23:50:01 -0500
Subject: [PATCH v17 3/5] Expose attribute statistics functions for use in
 extended_stats.

 
 
+/*
 
+/*

 /*

 
 
 		if (converted)
 		{
 
 		if (converted)
 		{

 
 		if (converted)
 		{

 	/*
 
 		if (converted)
 		{
 
 		if (converted)
 		{

From 0a77767474a80be32a91ff6ad779b52b34341641 Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Tue, 11 Nov 2025 16:58:26 +0900
Subject: [PATCH v17 4/5] Add extended statistics support functions.

 

+/*
 
+/*

+/*

 
 /*

 
 	return result;
 }
+
+static HeapTuple
 
 	return s;
 }
+
+/*
 
+/*
 
 DROP TABLE stats_temp;

 

From e5a1a3244f4a6836c782adc94886bdc6b2ee5e05 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Wed, 5 Nov 2025 01:13:04 -0500
Subject: [PATCH v17 5/5] Include Extended Statistics in pg_dump.

 

 	PQclear(res);
 
+/*
 
+	#

---

From: Michael Paquier <[email-masked]>
Date: Sun, 23 Nov 2025 15:57:31 +0900

> checks on other stats types.

Yeah, I am not sure that there is much value in having strict checks
for the values.  Some control over the arguments listed in a single
object is much more valuable as these are easy to check in the input
functions. 

> dump/restore. Granted, such values should never happen, but odd things do

Indeed.  Like the existing import functions, these should not lead to
hard failures, but WARNING-level reports.
--
Michael

---

From: Michael Paquier <[email-masked]>
Date: Tue, 25 Nov 2025 16:28:51 +0900

As you have already quoted upthread, statext_dependencies_build()
settles the issue on this one, I think.  It is entirely possible that
any group returned by DependencyGenerator generates a degree value
that would prevent a given group to be stored, and this could as well
be the largest possible group there could be in the set.  So we cannot
do any of that for dependencies, unfortunately.  We can always rely on
the list of attributes when assigning the json blob to the stats
object, at least, cross-checking that each attribute list matches with
the numbers of the stats object.  At least we can check for
duplicates, which is better than nothing at all.

Regarding the suggested check where we'd want to enforce all the
groups of attributes to be listed depending on the longest set we have
found, at the end estimate_multivariate_ndistinct() checks the items
listed one-by-one, giving up if we cannot find something in the list
of items.  I think that I am going to be content with the patch as it
is, without this piece.  Let's add an extra SQL test to treat that as
valid input, though.  So I am feeling OK with the input for ndistinct
at this stage.  I have noticed a couple of issues in passing,
adjusting them.  We are reaching more than 90% of coverage with the
tests, and I am not sure that we can actually reach the rest except if
one of the previous steps failed.

So That's one.  Now into the second patch for the input of the
dependencies.

Okay, I have to admit that these ones are fun.  I doubt that anybody
would actually do that, and these do not produce valid json objects,
which is what the last case shows.  Hmm, it makes sense to keep these,
and I'm still siding that we should not care too much about applying
checks on the values and complicate the input function more than that,
so fine by me.

There were a couple of things in the tests, missing quite a few soft
errors.  Many typos, grammar mistakes in the whole.  Also, please do
not split the error strings into multiple lines to make these
greppable.  There is also no need for a break after a return.  In some
cases, a return was used where a break made more sense as the default
path returned a failure.. 

The TODO in build_mvdependencies() could be an elog(), but I have left
it untouched for the errdetail().

We're reaching 91% of coverage here, not bad.  The rest does not seem
reachable, as far as I can see.

With that said, a v18 for the first two patches with the input
functions.  Comments and/or opinions?
--
Michael
From b19df83c7be3c9de6174f3d971aac581b45eb440 Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Tue, 25 Nov 2025 15:20:49 +0900
Subject: [PATCH v18 1/2] Add working input function for pg_ndistinct.

This will be needed for importing extended statistics.  With these
changes in place, coverage of pg_ndistinct.c reaches 91%.
---
 src/backend/utils/adt/pg_ndistinct.c       | 760 ++++++++++++++++++++-
 src/test/regress/expected/pg_ndistinct.out | 417 +++++++++++
 src/test/regress/parallel_schedule         |   2 +-
 src/test/regress/sql/pg_ndistinct.sql      | 101 +++
 src/tools/pgindent/typedefs.list           |   2 +
 5 files changed, 1273 insertions(+), 9 deletions(-)
 create mode 100644 src/test/regress/expected/pg_ndistinct.out
 create mode 100644 src/test/regress/sql/pg_ndistinct.sql

diff --git a/src/backend/utils/adt/pg_ndistinct.c b/src/backend/utils/adt/pg_ndistinct.c
index 97efc290ef5e..a730ea6ef2db 100644
--- a/src/backend/utils/adt/pg_ndistinct.c
+++ b/src/backend/utils/adt/pg_ndistinct.c
@@ -14,29 +14,773 @@
 
 #include "postgres.h"
 
+#include "common/int.h"
 
+	List	   *attnum_list;	/* Accumulated attribute numbers */
+			/* In an attribute number list, expect only scalar integers */
 
 /*
- * pg_ndistinct_in
+			 * The attribute number cannot be zero a negative number beyond
+			 * the number of the possible expressions.
+			if (attnum == 0 || attnum < (0 - STATS_MAX_DIMENSIONS))
+item_attributes_eq(const MVNDistinctItem *a, const MVNDistinctItem *b)
+ * Generate a string representing an array of attribute numbers.
+			 * Parsing has ended correctly and we should have a list of items.
+			 * If we don't, something has been done wrong in one of the
+			 * earlier parsing steps.
+			if (item_attributes_eq(item, &ndistinct->items[j]))
+		 * This transfers free-ing responsibility from the distinct_items list
+		 * to the ndistinct object.
+	 * Verify that all the sets of attribute numbers are a proper subset of
+	 * the longest set recorded.  This acts as an extra sanity check based on
+	 * the input given.  Note that this still needs to be cross-checked with
+	 * the extended statistics objects this would be assigned to, but it
+	 * provides one extra layer of protection.

+
 /*
  * pg_ndistinct_out
index 000000000000..6e8c94e4fa5b
@@ -0,0 +1,417 @@
+-- Three attributes with only two items.
+---------------------------------------------------------------------------------------
+ [{"attributes": [2, -1], "ndistinct": 1}, {"attributes": [2, 3, -1], "ndistinct": 3}]
 
 # ----------
index 000000000000..e2bd19eaa5ef
@@ -0,0 +1,101 @@
+-- Three attributes with only two items.
index 57a8f0366a55..17e2b40b9cb0 100644

From 29f98ba149aeb006f61bb2c8a562732c264875c0 Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Tue, 25 Nov 2025 16:13:44 +0900
Subject: [PATCH v18 2/2] Add working input function for pg_dependencies.

This will be needed for importing extended statistics.
---
 src/backend/utils/adt/pg_dependencies.c       | 785 +++++++++++++++++-
 src/test/regress/expected/pg_dependencies.out | 495 +++++++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/pg_dependencies.sql      | 123 +++
 src/tools/pgindent/typedefs.list              |   2 +
 5 files changed, 1396 insertions(+), 11 deletions(-)
 create mode 100644 src/test/regress/expected/pg_dependencies.out
 create mode 100644 src/test/regress/sql/pg_dependencies.sql

diff --git a/src/backend/utils/adt/pg_dependencies.c b/src/backend/utils/adt/pg_dependencies.c
index 87181aa00e9a..0fd8a2476459 100644
--- a/src/backend/utils/adt/pg_dependencies.c
+++ b/src/backend/utils/adt/pg_dependencies.c
@@ -14,31 +14,796 @@
 
 #include "postgres.h"
 
+#include "common/int.h"
 
+typedef enum
+{
+	DEPS_EXPECT_START = 0,
+	DEPS_EXPECT_ITEM,
+	DEPS_EXPECT_KEY,
+} DependenciesSemanticState;
+	DependenciesSemanticState state;
+	List	   *attnum_list;	/* Accumulated attribute numbers */
+			/* In an attribute number list, expect only scalar integers */
+	 * We need at least one attribute number in a dependencies item, anything
+				errdetail("The \"%s\" key must contain an array of at least %d and no more than %d elements.",
+						  PG_DEPENDENCIES_KEY_ATTRIBUTES, 1,
+						  STATS_MAX_DIMENSIONS - 1));
+	 * Allocate enough space for the dependency, the attribute numbers in the
+	 * list and the final attribute number for the dependency.
+	 * Assign attribute numbers to the attributes array, comparing each one
+	 * against the dependency attribute to ensure that there there are no
+	 * matches.
+	 * Reset dependency item state variables to look for the next
+ * Either the end of an attribute number list or the whole object.
+			 * dependencies_array_start().
+ * Duplicate values are not allowed; that is already covered by the rules
+ * described above.
+			 * An attribute number cannot be zero or a negative number beyond
+			 * the number of the possible expressions.
+			if (attnum == 0 || attnum < (0 - STATS_MAX_DIMENSIONS))
+			 * The dependency attribute number cannot be zero or a negative
+			 * number beyond the number of the possible expressions.
+			if (parse->dependency == 0 || parse->dependency < (0 - STATS_MAX_DIMENSIONS))
+ * looking for duplicated sets.
+dep_attributes_eq(const MVDependency *a, const MVDependency *b)
+ * Generate a string representing an array of attribute numbers.
+ * Internally, the dependency attribute is the last element, so we
+ * leave that off.
+			 * Parse ended in the expected place.  We should have a list of
+			 * items, but if we do not there is an issue with one of the
+			 * earlier parse steps.
+		 * Because we free the dependency_list after parsing is done, we
+		 * cannot free it here.
+			if (dep_attributes_eq(mvdeps->deps[i], mvdeps->deps[j]))
+						errdetail("Duplicate \"%s\" array: [%s] with \"%s\": %d.",
+								  PG_DEPENDENCIES_KEY_ATTRIBUTES, attnum_list,
+								  PG_DEPENDENCIES_KEY_DEPENDENCY, attnum_dep));
+	 * No need to free the individual MVDependency objects, because they are
+	 * still in the dependency_list, and will be freed with that.

+
 /*
index 000000000000..556e3dad00fb
@@ -0,0 +1,495 @@
+SELECT * FROM pg_input_error_info('[{"attributes": [], "dependency": 2, "degree": 1}]', 'pg_dependencies');
+---------------------------------------------------------------------------------+--------------------------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes": [], "dependency": 2, "degree": 1}]" | The "attributes" key must be an non-empty array. |      | 22P02
+SELECT * FROM pg_input_error_info('[{"attributes" : {"a": 1}, "dependency" : 4, "degree": "1.2"}]', 'pg_dependencies');
+---------------------------------------------------------------------------------------------+--------------------------------------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : {"a": 1}, "dependency" : 4, "degree": "1.2"}]" | Value of "attributes" must be an array of attribute numbers. |      | 22P02
+SELECT * FROM pg_input_error_info('[{"dependency" : 4, "degree": "1.2"}]', 'pg_dependencies');
+--------------------------------------------------------------------+------------------------------------+------+----------------
+ malformed pg_dependencies: "[{"dependency" : 4, "degree": "1.2"}]" | Item must contain "attributes" key |      | 22P02
+SELECT * FROM pg_input_error_info('[{"attributes" : [1,2,3,4,5,6,7], "dependency" : 0, "degree": "1.2"}]', 'pg_dependencies');
+----------------------------------------------------------------------------------------------------+--------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [1,2,3,4,5,6,7], "dependency" : 0, "degree": "1.2"}]" | Invalid "dependency" value: 0. |      | 22P02
+SELECT * FROM pg_input_error_info('[{"attributes" : [1,2,3,4,5,6,7], "dependency" : -9, "degree": "1.2"}]', 'pg_dependencies');
+-----------------------------------------------------------------------------------------------------+---------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [1,2,3,4,5,6,7], "dependency" : -9, "degree": "1.2"}]" | Invalid "dependency" value: -9. |      | 22P02
+SELECT * FROM pg_input_error_info('[{"attributes": [1,2], "dependency": 2, "degree": 1}]' , 'pg_dependencies');
+------------------------------------------------------------------------------------+-----------------------------------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes": [1,2], "dependency": 2, "degree": 1}]" | Item "dependency" value 2 found in the "attributes" list. |      | 22P02
+SELECT * FROM pg_input_error_info('[{"attributes" : [1, {}], "dependency" : 1, "degree": "1.2"}]', 'pg_dependencies');
+--------------------------------------------------------------------------------------------+-----------------------------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [1, {}], "dependency" : 1, "degree": "1.2"}]" | Attribute lists can only contain attribute numbers. |      | 22P02
+SELECT * FROM pg_input_error_info('[{"attributes" : [1,2], "dependency" : {}, "degree": 1.0}]', 'pg_dependencies');
+-----------------------------------------------------------------------------------------+-------------------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [1,2], "dependency" : {}, "degree": 1.0}]" | Value of "dependency" must be an integer. |      | 22P02
+SELECT * FROM pg_input_error_info('[{"attributes" : [1,2], "dependency" : 3, "degree": {}}]', 'pg_dependencies');
+---------------------------------------------------------------------------------------+---------------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [1,2], "dependency" : 3, "degree": {}}]" | Value of "degree" must be an integer. |      | 22P02
+SELECT * FROM pg_input_error_info('[{"attributes" : [1,2], "dependency" : 1, "degree": "a"}]', 'pg_dependencies');
+----------------------------------------------------------------------------------------+-------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [1,2], "dependency" : 1, "degree": "a"}]" | Invalid "degree" value. |      | 22P02
+-- Funky degree values, which do not fail.
+-- Partially-covered attribute lists, possible as items with a degree of 0
+-- are discarded.
+ [{"attributes": [2, 3], "dependency": 4, "degree": 1.000000}, {"attributes": [1, -1], "dependency": 4, "degree": 1.000000}, {"attributes": [2, 3, -1], "dependency": 4, "degree": 1.000000}, {"attributes": [2, 3, -1, -2], "dependency": 4, "degree": 1.000000}]
 
 # ----------
index 000000000000..ccc8ee277126
@@ -0,0 +1,123 @@
+SELECT * FROM pg_input_error_info('[{"attributes": [], "dependency": 2, "degree": 1}]', 'pg_dependencies');
+SELECT * FROM pg_input_error_info('[{"attributes" : {"a": 1}, "dependency" : 4, "degree": "1.2"}]', 'pg_dependencies');
+SELECT * FROM pg_input_error_info('[{"dependency" : 4, "degree": "1.2"}]', 'pg_dependencies');
+SELECT * FROM pg_input_error_info('[{"attributes" : [1,2,3,4,5,6,7], "dependency" : 0, "degree": "1.2"}]', 'pg_dependencies');
+SELECT * FROM pg_input_error_info('[{"attributes" : [1,2,3,4,5,6,7], "dependency" : -9, "degree": "1.2"}]', 'pg_dependencies');
+SELECT * FROM pg_input_error_info('[{"attributes": [1,2], "dependency": 2, "degree": 1}]' , 'pg_dependencies');
+SELECT * FROM pg_input_error_info('[{"attributes" : [1, {}], "dependency" : 1, "degree": "1.2"}]', 'pg_dependencies');
+SELECT * FROM pg_input_error_info('[{"attributes" : [1,2], "dependency" : {}, "degree": 1.0}]', 'pg_dependencies');
+SELECT * FROM pg_input_error_info('[{"attributes" : [1,2], "dependency" : 3, "degree": {}}]', 'pg_dependencies');
+SELECT * FROM pg_input_error_info('[{"attributes" : [1,2], "dependency" : 1, "degree": "a"}]', 'pg_dependencies');
+-- Funky degree values, which do not fail.
+-- Partially-covered attribute lists, possible as items with a degree of 0
+-- are discarded.
index 17e2b40b9cb0..dfcd619bfee3 100644
@@ -631,6 +631,8 @@ DefaultACLInfo
 DefineStmt
 DefnDumperPtr
 DeleteStmt
+DependenciesParseState
+DependenciesSemanticState
 DependencyGenerator
 DependencyGeneratorData
 DependencyType
-- 
2.51.0

---

From: Chao Li <[email-masked]>
Date: Tue, 25 Nov 2025 16:59:28 +0800

> On Nov 25, 2025, at 15:28, Michael Paquier <[email-masked]> wrote:
> 
> <v18-0001-Add-working-input-function-for-pg_ndistinct.patch><v18-0002-Add-working-input-function-for-pg_dependencies.patch>

I don’t see any of my comments are addressed in v18.

Best regards,

---

From: Michael Paquier <[email-masked]>
Date: Tue, 25 Nov 2025 19:06:09 +0900

> I don’t see any of my comments are addressed in v18.

Sorry about that, I thought that v17 was addressing these.  I have
reviewed your latest email posted here:
https://www.postgresql.org/message-id/[email-masked]

And well, this presents a bunch of issues I have already fixed (break
after return, one error message incorrect, some descriptions).  The
comments on top of the functions are actually true as they stand (aka
ndistinct_array_end() fails if we don't have an array).  There is no
need to reset state vars upon a JSON_SEM_ACTION_FAILED AFAIK.
Finally, the consts are non-appealing for me for functions that are
internally used in these files.  And forgot one:
valid_subsequent_attnum()'s duplication is actually adapted IMO, the
argument lists could have different assumptions depending on the type
of extended stats we are dealing with.  I did not check the comments
for 0003~0005.  Perhaps you are right that these need adjustments.

Thanks,
--
Michael

---

From: Michael Paquier <[email-masked]>
Date: Wed, 26 Nov 2025 11:06:10 +0900

> functions.  Comments and/or opinions?

Okay.  I have gone other these two and after an extra round of
polishing, mainly around comments (some suggested by Chao, actually),
style, a fix for the test with valid UTF8 sequences failing in
non-UTF8 databases, plus a few more tests that were missing, I have
applied the two patches for the input functions.

Perhaps these will need some adjustments, but I also see little point
in moving on with these based on the coverage we have and how the code
is shaped.  Could you rebase the rest of the patch set for the three
remaining pieces, Corey?  You will also need to address some of the
comments from Chao based on a review of v16, that were not addressed
as of v17. 
--
Michael

---

From: Corey Huinker <[email-masked]>
Date: Tue, 25 Nov 2025 23:14:26 -0600

> I don’t see any of my comments are addressed in v18.
My apologies. My v17 focused entirely on the input functions, as those were
receiving the vast majority of the attention. Now that those are out of the
way (Thanks Michael!) I can address those issues.

---

From: Michael Paquier <[email-masked]>
Date: Wed, 26 Nov 2025 14:24:30 +0900

> way (Thanks Michael!) I can address those issues.

No problem, thanks.  Please note that I am not sure if I will be able
to look at anything else posted on this thread until next week, so
please feel free to continue the discussion for the time being.
--
Michael

---

From: Corey Huinker <[email-masked]>
Date: Tue, 25 Nov 2025 23:32:49 -0600

> internally used in these files.

I'm curious why you find them non-appealing for non-exposed functions, if
you have time to elaborate.

>   And forgot one:
> of extended stats we are dealing with.

Yeah, I saw that, reviewing the diff-of-diffs just now.

---

From: Corey Huinker <[email-masked]>
Date: Tue, 25 Nov 2025 23:34:56 -0600

> please feel free to continue the discussion for the time being.
It's holiday season here too. I'm going to get the rebase out and see if
any lessons learned can be applied to the remaining patches, but my own
availability may be limited.

---

From: Corey Huinker <[email-masked]>
Date: Tue, 25 Nov 2025 23:37:09 -0600

> applied the two patches for the input functions.

I've reviewed what you committed and it looks good to me aside from the one
nitpick I already made.

+1

---

From: Michael Paquier <[email-masked]>
Date: Wed, 26 Nov 2025 14:44:23 +0900

> I've reviewed what you committed and it looks good to me aside from the one

I was not really excited about these as it concerns only code local to
each file where the input functions are located, no routines that are
actually published for consumption by other parts of the backend.
--
Michael

---

From: Corey Huinker <[email-masked]>
Date: Tue, 25 Nov 2025 23:50:14 -0600

> actually published for consumption by other parts of the backend.

So if a function were previously static to one page, but now had utility
outside of that file, you would then consider changing the function
signature to add the consts? I ask because this may become relevant with
the statatt_* functions being moved in the remaining patches.

---

From: Michael Paquier <[email-masked]>
Date: Wed, 26 Nov 2025 15:16:54 +0900

> the statatt_* functions being moved in the remaining patches.

That's the kind of analysis that requires a case-by-case lookup.  The
input functions were not really that worth bothering to me based on
their logic being very isolated.  The refactoring pieces with
attribute stats that you are suggesting in 0003 may be indeed more
relevant if done this way.
--
Michael

---

From: Corey Huinker <[email-masked]>
Date: Wed, 26 Nov 2025 03:21:46 -0600

On Tue, Nov 25, 2025 at 11:14 PM Corey Huinker <[email-masked]>
wrote:

> My apologies. My v17 focused entirely on the input functions, as those
> were receiving the vast majority of the attention. Now that those are out
> of the way (Thanks Michael!) I can address those issues.
>

Paraphrasing the "quotes" here for brevity...

> Several functions are made external visible, they are all renamed with
adding a prefix “statatt_”, why text_to_stavalues is an exception?

Michael had specifically said that one didn't need to be renamed. I suppose
statatt_import_stavalues() might be a good name for it. It *is* specific to
attribute stats, though that definition also applies to the attribute stats
nested in the stxdexprs of extended stats. I have no strong opinion on the
matter.

+1

> This function pass values, nulls and replaces to heap_modify_tuple() and
heap_form_tuple(),
> the both functions take all const pointers as parameters.
> So, here values, nulls and replaces can all be const.
I find your argument here persuasive enough to override Michael's
previously stated non-excitement.

> ...the two NULL_FRAC questions

Yes, fixed.

> import_mcvlist is declared twice, looks like a copy-paste mistake.

That or a rebase/apply gone wrong. Fixed.

> PREPQUERY_DUMPEXTSTATSSTATS weird, suggest PREPQUERY_DUMPEXTSTATSDATA

Awkward names are almost inevitable when talking about the statistics
associated with an object of type "statistics".

There was a debate about whether statistics were data or not, and I'd
rather not restart that, so I went with PREPQUERY_DUMPEXTSTATSOBJSTATS for
now.

 Incorporated these fixes, and some other lessons learned.

---

From: Michael Paquier <[email-masked]>
Date: Wed, 3 Dec 2025 14:00:31 +0900

>  Incorporated these fixes, and some other lessons learned.

I've managed to miss the update you have sent here with the three
remaining patches.  I am going to looking at the remaining pieces
now as this patch set was still on my stack of patches to look at..
--
Michael

---

From: WangYu <[email-masked]>
Date: Thu, 4 Dec 2025 08:52:16 +0800 (CST)

Hi Corey
I was reviewing the recent patch v19-0003-Include-Extended-Statistics-in-pg_dump.patch and noticed a couple of small typo issues in the explanatory comments — nothing that affects the functionality.

Here are the two minor fixes I’d suggest:
1. “ndistintinct” should be “ndistinct”.
2. “depdendencies” should be “dependencies”.

Best regards,
Yu

在 2025-12-04 08:46:29，"Corey Huinker" <[email-masked]> 写道：

On Tue, Nov 25, 2025 at 11:14 PM Corey Huinker <[email-masked]> wrote:

My apologies. My v17 focused entirely on the input functions, as those were receiving the vast majority of the attention. Now that those are out of the way (Thanks Michael!) I can address those issues.

> Several functions are made external visible, they are all renamed with adding a prefix “statatt_”, why text_to_stavalues is an exception?

Michael had specifically said that one didn't need to be renamed. I suppose statatt_import_stavalues() might be a good name for it. It *is* specific to attribute stats, though that definition also applies to the attribute stats nested in the stxdexprs of extended stats. I have no strong opinion on the matter.

+1

> This function pass values, nulls and replaces to heap_modify_tuple() and heap_form_tuple(),
I find your argument here persuasive enough to override Michael's previously stated non-excitement.

Yes, fixed.

> PREPQUERY_DUMPEXTSTATSSTATS weird, suggest PREPQUERY_DUMPEXTSTATSDATA

Awkward names are almost inevitable when talking about the statistics associated with an object of type "statistics".

There was a debate about whether statistics were data or not, and I'd rather not restart that, so I went with PREPQUERY_DUMPEXTSTATSOBJSTATS for now.

---

From: Michael Paquier <[email-masked]>
Date: Thu, 4 Dec 2025 16:28:00 +0900

On Thu, Dec 04, 2025 at 08:52:16AM +0800, WangYu wrote:
> I was reviewing the recent patch
> v19-0003-Include-Extended-Statistics-in-pg_dump.patch and noticed a
> couple of small typo issues in the explanatory comments — nothing
> that affects the functionality.

Please be careful that the mailing lists of Postgres use
bottom-posting for thread discussions.  Please see here for details:
https://en.wikipedia.org/wiki/Posting_style#Bottom-posting

> 2. “depdendencies” should be “dependencies”.

Indeed.  These exact typos existed in some of the previous iterations
of the patch posted on this thread.

I have begun looking at 0001 and 0002 more seriously, and can now
share a couple of comments about them. 

My biggest issue with 0001 is the severe lack of documentation for all
the routines that are now in attribute_stats.c which are published so
as they can be used for the import functions of extended stats:

1) statatt_get_type(), that retrieves a set of information from a
relation and one of its attributes.  This is lightly documented in
extended_statistics_update() from 0002, with an argument claiming that
we do the same as attribute statistics.  If one then looks at
attribute_stats.c, we have a not-really-helpful "derive information
from attribute".  The trick is that we have an implied dependency
here: statatt_get_type() needs to be called *before*
statatt_get_elem_type() for attribute *and* extended stats,
statatt_get_elem_type() being fed data from statatt_get_type().

2) statatt_get_elem_type(), interesting piece of the puzzle that acts
as a wrapper for the type cache.  An important part here, it seems to
me, would be to at least tell that this relies on data retrieved from
statatt_get_type(), initially.  Once I've noticed this dependency the
logic felt a bit cleaner to understand, but we have no docs explaining
any of that..

3) statatt_init_empty_tuple(), that initializes a set of data to be
used for updates and/or insertion.  My first thought here is about 
"inherited", which turns around an assumption in
attribute_statistics_update().  How should callers assign its value?
For attribute_statistics_update() it comes as an input argument.
Also why are these values initialized as they are and why should these
make sense when applied to pg_statistic.  Extended stats set the
"inherited" argument to false in import_expressions().  That seems
equally important to document, both in a comment in extended_stats.c
and at the top of the function.  Of course it means that this refers
to the fact that the stats include values from stats tables, which do
not apply to extended stats.  But one would have to guess this fact
after knowing that this relates to the catalog pg_statistic..

4) statatt_set_slot is slightly simpler.  Still here, it is really easy
to miss that this routine should be fed data retrieved by
statatt_get_type().  staop can also be an eq or an lt operator.
stakind is one of the STATISTIC_KIND_* values, etc, etc.

5) text_to_stavalues() has a bit more explanation, with some data fed
from statatt_get_type() for the atttypmod and atttypid.  We have also
some fix of input coming from statatt_get_elem_type(), for elemtypid.

As far as I can see, there is nothing in these functions that require
them to be located in attribute_stats.c anymore.  Let's move that to
stats_utils.c, common place for all the shared facilities used by
these stats modules.  Or it could also be a new, separate patch, for
the manipulation and extraction of the tuple data related to
the pg_statistic tuples.

I am not OK with these being in attribute_stats.c, and as far as I can
see they have no contents that force these routines to 

Perhaps you know what these imply because you have written this code
originally in ce207d2a7901, but exposing them also means that it is
very important to document at least some concepts and assumptions
around them so as it is possible to guide somebody that may want to
use this code:
- What are the input arguments from?
- And what are the results?
- In which circumstances should these be used?

Then about 0002.

There are a bunch of assumptions embedded inside import_expressions()
that are interdependent with the calls of these attribute routines
which are a bit hard to evaluate by themselves, at least it's not
clear why some of these operations are done and why they are done the
way they are presented in the patch.  The code processes an array of
expressions and processes them in a sequential fashion, repeating
checks based on extended_stats_exprs_element.  Each step is going to
need more explanation to connect the dots.  For example, why the first
checks on exprs_nulls are important, with a second step of checks for
each STATISTIC_KIND_*.  Too much is let to the reader to guess, making
the code complicated to maintain as written, at least IMHO.

Is there any need to locate these new functions and code in
extended_stats.c, actually?  A separation into a new file seems like
it would be a cleaner result, leaving extended_stats.c to deal with
the import of the new data, including the fetch and deletion of any
existing data in pg_stat_ext that would be updated or inserted.
Perhaps name that extended_stats_funcs.c, as these are about the
direct SQL functions, import and deletion.

For the tests, it looks like it would be better to have everything in
a new file, like a stats_ext_import.sql for the clear and restore
bits.

A lot of the tests are copy-pastes of surrounding queries.  Perhaps it
would be better to use a SQL function wrapper or an IN clause with
multiple relations?  The patch has a lot of bloat with these repeated
queries..  Perhaps we should use a split for the elements in the
extended stats array instead of jsonb_pretty().  The results get quite
long here.

As of 0002, there are actually two independent pieces dealt with: the
deletion of extended stats with pg_clear_extended_stats(), and the
insert/update of extended stats with pg_restore_extended_stats().  I'd
suggest to split that into two parts, with the clear being first in
rank.  The deletion is simpler, and getting that in first simplifies
the review of the import part with less input to deal with.
--
Michael

---

From: Corey Huinker <[email-masked]>
Date: Thu, 4 Dec 2025 18:12:16 -0500

+1

> statatt_get_elem_type() being fed data from statatt_get_type().

I've added comments to that effect.

> logic felt a bit cleaner to understand, but we have no docs explaining

I've added comments that will hopefully connect those dots.

> ...

The comments didn't seem quite so illuminating, but I added them just the
same.

> stakind is one of the STATISTIC_KIND_* values, etc, etc.

Some here too.

> the manipulation and extraction of the tuple data related to

They've been moved to stats_utils.c, and impact was fairly minimal. I was
worried about an explosion of #includes, but that ended up not being the
case.

> - In which circumstances should these be used?

An attempt was made.

> ...

I tried to expand on these things, but I haven't yet moved them to another
file for reasons I'll explain later.

> direct SQL functions, import and deletion.

We can move them to another file, but I'm not sure if that will actually
make things clearer. What is extended_stats.c if not functions about
extended stats? Perhaps it makes more sense to try harder to find the
commonality in the building of the pg_statistic tuples, move those
functions to stats_utils, and get things decluttered that way?

> a new file, like a stats_ext_import.sql for the clear and restore

Those tests very much build upon the tables and data already created for
regular relation/attribute imports, so we save quite a bit of boilerplate
in keeping them there.

> multiple relations?

I'd say "no", because we're doing a lot of things that *could* generate
ERRORs, and we're verifying that they generate WARNINGs instead. If we did
batch up those commands, and any one of them generated an error, we'd be at
loss for which one was the culprit, and we'd also lose any insight into
what else got busted in the other rows of the batch.

>   The patch has a lot of bloat with these repeated

I replaced '}, ' with '},\n' and removed the jsonb_pretty entirely, which
is something I had proposed doing earlier, as it's actually more pretty
than jsonb_pretty(). It's still in the same file, though.

I like the net result of this, and it might make sense to search other
regression tests for places where we can employ this mini-pretty pattern.

> the review of the import part with less input to deal with.

The pg_clear function is quite trivial, as it's basically just marshalling
and locking before a call to delete_pg_statistic_ext_data(). So while I can
split them out, I'd also have to split the pg_proc.dat changes into two
separate ones, which makes it hard to see how similar they are. I have a
similar feeling about the changes to func-admin.sgml.

All of that, plus a rebase.
From f0da95a88e43e2046bc1f5287b9ad7ddbb251e13 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 4 Nov 2025 23:50:01 -0500
Subject: [PATCH v20 1/3] Expose attribute statistics functions for use in
 extended_stats.

Also, add comments explaining the function argument index enums, and the
arrays that are indexed by those enums.
---
 src/include/statistics/stat_utils.h      |  21 +-
 src/backend/statistics/attribute_stats.c | 424 +++--------------------
 src/backend/statistics/stat_utils.c      | 372 ++++++++++++++++++++
 3 files changed, 434 insertions(+), 383 deletions(-)

diff --git a/src/include/statistics/stat_utils.h b/src/include/statistics/stat_utils.h
index f41b181d4d3..e57a01043b7 100644
--- a/src/include/statistics/stat_utils.h
+++ b/src/include/statistics/stat_utils.h
@@ -14,9 +14,7 @@
 #define STATS_UTILS_H
 
 #include "fmgr.h"
-
-/* avoid including primnodes.h here */
-typedef struct RangeVar RangeVar;
+#include "nodes/pathnodes.h"
 
 struct StatsArgInfo
 {
@@ -40,4 +38,21 @@ extern bool stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo,
 											 FunctionCallInfo positional_fcinfo,
 											 struct StatsArgInfo *arginfo);
 
+extern void statatt_get_type(Oid reloid, AttrNumber attnum,
+							 Oid *atttypid, int32 *atttypmod,
+							 char *atttyptype, Oid *atttypcoll,
+							 Oid *eq_opr, Oid *lt_opr);
+extern void statatt_init_empty_tuple(Oid reloid, int16 attnum, bool inherited,
+									 Datum *values, bool *nulls, bool *replaces);
+
+extern void statatt_set_slot(Datum *values, bool *nulls, bool *replaces,
+							 int16 stakind, Oid staop, Oid stacoll,
+							 Datum stanumbers, bool stanumbers_isnull,
+							 Datum stavalues, bool stavalues_isnull);
+
+extern Datum text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d,
+							   Oid typid, int32 typmod, bool *ok);
+extern bool statatt_get_elem_type(Oid atttypid, char atttyptype,
+								  Oid *elemtypid, Oid *elem_eq_opr);
+
 #endif							/* STATS_UTILS_H */
diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index ef4d768feab..9b289129fcc 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -20,10 +20,8 @@
 #include "access/heapam.h"
 #include "catalog/indexing.h"
 #include "catalog/namespace.h"
-#include "catalog/pg_collation.h"
 #include "catalog/pg_operator.h"
 #include "nodes/makefuncs.h"
-#include "nodes/nodeFuncs.h"
 #include "statistics/statistics.h"
 #include "statistics/stat_utils.h"
 #include "utils/array.h"
@@ -32,10 +30,6 @@
 #include "utils/lsyscache.h"
 #include "utils/syscache.h"
 
-#define DEFAULT_NULL_FRAC      Float4GetDatum(0.0)
-#define DEFAULT_AVG_WIDTH      Int32GetDatum(0) /* unknown */
-#define DEFAULT_N_DISTINCT     Float4GetDatum(0.0)	/* unknown */
-
 /*
  * Positional argument numbers, names, and types for
  * attribute_statistics_update() and pg_restore_attribute_stats().
@@ -64,6 +58,10 @@ enum attribute_stats_argnum
 	NUM_ATTRIBUTE_STATS_ARGS
 };
 
+/*
@@ -101,6 +99,10 @@ enum clear_attribute_stats_argnum
 
+/*
@@ -111,24 +113,9 @@ static struct StatsArgInfo cleararginfo[] =
 };
 
-static Node *get_attr_expr(Relation rel, int attnum);
 
 /*
  * Insert or Update Attribute Statistics
@@ -298,16 +285,16 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	}

@@ -361,8 +348,8 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
-							   replaces);
 
 	/* if specified, set to argument values */
 	if (!PG_ARGISNULL(NULL_FRAC_ARG))
@@ -394,10 +381,10 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 		if (converted)
 		{
@@ -417,10 +404,10 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 		if (converted)
 		{
@@ -433,10 +420,10 @@ attribute_statistics_update(FunctionCallInfo fcinfo)

 	/* STATISTIC_KIND_MCELEM */
@@ -454,10 +441,10 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 		if (converted)
 		{
@@ -468,10 +455,10 @@ attribute_statistics_update(FunctionCallInfo fcinfo)

 	/*
@@ -494,10 +481,10 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 		if (converted)
 		{
@@ -521,10 +508,10 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 		if (converted)
 		{
@@ -539,291 +526,6 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	return result;
 }
 
-/*
- * If this relation is an index and that index has expressions in it, and
- * the attnum specified is known to be an expression, then we must walk
- * the list attributes up to the specified attnum to get the right
- * expression.
- */
-static Node *
-get_attr_expr(Relation rel, int attnum)
-{
-	List	   *index_exprs;
-	ListCell   *indexpr_item;
-
-	/* relation is not an index */
-	if (rel->rd_rel->relkind != RELKIND_INDEX &&
-		rel->rd_rel->relkind != RELKIND_PARTITIONED_INDEX)
-		return NULL;
-
-	index_exprs = RelationGetIndexExpressions(rel);
-
-	/* index has no expressions to give */
-	if (index_exprs == NIL)
-		return NULL;
-
-	/*
-	 * The index attnum points directly to a relation attnum, then it's not an
-	 * expression attribute.
-	 */
-	if (rel->rd_index->indkey.values[attnum - 1] != 0)
-		return NULL;
-
-	indexpr_item = list_head(rel->rd_indexprs);
-
-	for (int i = 0; i < attnum - 1; i++)
-		if (rel->rd_index->indkey.values[i] == 0)
-			indexpr_item = lnext(rel->rd_indexprs, indexpr_item);
-
-	if (indexpr_item == NULL)	/* shouldn't happen */
-		elog(ERROR, "too few entries in indexprs list");
-
-	return (Node *) lfirst(indexpr_item);
-}
-
-/*
- * Derive type information from the attribute.
- */
-static void
-get_attr_stat_type(Oid reloid, AttrNumber attnum,
-				   Oid *atttypid, int32 *atttypmod,
-				   char *atttyptype, Oid *atttypcoll,
-				   Oid *eq_opr, Oid *lt_opr)
-{
-	Relation	rel = relation_open(reloid, AccessShareLock);
-	Form_pg_attribute attr;
-	HeapTuple	atup;
-	Node	   *expr;
-	TypeCacheEntry *typcache;
-
-	atup = SearchSysCache2(ATTNUM, ObjectIdGetDatum(reloid),
-						   Int16GetDatum(attnum));
-
-	/* Attribute not found */
-	if (!HeapTupleIsValid(atup))
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_COLUMN),
-				 errmsg("column %d of relation \"%s\" does not exist",
-						attnum, RelationGetRelationName(rel))));
-
-	attr = (Form_pg_attribute) GETSTRUCT(atup);
-
-	if (attr->attisdropped)
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_COLUMN),
-				 errmsg("column %d of relation \"%s\" does not exist",
-						attnum, RelationGetRelationName(rel))));
-
-	expr = get_attr_expr(rel, attr->attnum);
-
-	/*
-	 * When analyzing an expression index, believe the expression tree's type
-	 * not the column datatype --- the latter might be the opckeytype storage
-	 * type of the opclass, which is not interesting for our purposes. This
-	 * mimics the behavior of examine_attribute().
-	 */
-	if (expr == NULL)
-	{
-		*atttypid = attr->atttypid;
-		*atttypmod = attr->atttypmod;
-		*atttypcoll = attr->attcollation;
-	}
-	else
-	{
-		*atttypid = exprType(expr);
-		*atttypmod = exprTypmod(expr);
-
-		if (OidIsValid(attr->attcollation))
-			*atttypcoll = attr->attcollation;
-		else
-			*atttypcoll = exprCollation(expr);
-	}
-	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;
-	*eq_opr = typcache->eq_opr;
-	*lt_opr = typcache->lt_opr;
-
-	/*
-	 * Special case: collation for tsvector is DEFAULT_COLLATION_OID. See
-	 * compute_tsvector_stats().
-	 */
-	if (*atttypid == TSVECTOROID)
-		*atttypcoll = DEFAULT_COLLATION_OID;
-
-	relation_close(rel, NoLock);
-}
-
-/*
- * Derive element type information from the attribute type.
- */
-static bool
-get_elem_stat_type(Oid atttypid, char atttyptype,
-				   Oid *elemtypid, Oid *elem_eq_opr)
-{
-	TypeCacheEntry *elemtypcache;
-
-	if (atttypid == TSVECTOROID)
-	{
-		/*
-		 * Special case: element type for tsvector is text. See
-		 * compute_tsvector_stats().
-		 */
-		*elemtypid = TEXTOID;
-	}
-	else
-	{
-		/* find underlying element type through any domain */
-		*elemtypid = get_base_element_type(atttypid);
-	}
-
-	if (!OidIsValid(*elemtypid))
-		return false;
-
-	/* finds the right operator even if elemtypid is a domain */
-	elemtypcache = lookup_type_cache(*elemtypid, TYPECACHE_EQ_OPR);
-	if (!OidIsValid(elemtypcache->eq_opr))
-		return false;
-
-	*elem_eq_opr = elemtypcache->eq_opr;
-
-	return true;
-}
-
-/*
- * Cast a text datum into an array with element type elemtypid.
- *
- * If an error is encountered, capture it and re-throw a WARNING, and set ok
- * to false. If the resulting array contains NULLs, raise a WARNING and set ok
- * to false. Otherwise, set ok to true.
- */
-static Datum
-text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d, Oid typid,
-				  int32 typmod, bool *ok)
-{
-	LOCAL_FCINFO(fcinfo, 8);
-	char	   *s;
-	Datum		result;
-	ErrorSaveContext escontext = {T_ErrorSaveContext};
-
-	escontext.details_wanted = true;
-
-	s = TextDatumGetCString(d);
-
-	InitFunctionCallInfoData(*fcinfo, array_in, 3, InvalidOid,
-							 (Node *) &escontext, NULL);
-
-	fcinfo->args[0].value = CStringGetDatum(s);
-	fcinfo->args[0].isnull = false;
-	fcinfo->args[1].value = ObjectIdGetDatum(typid);
-	fcinfo->args[1].isnull = false;
-	fcinfo->args[2].value = Int32GetDatum(typmod);
-	fcinfo->args[2].isnull = false;
-
-	result = FunctionCallInvoke(fcinfo);
-
-	pfree(s);
-
-	if (escontext.error_occurred)
-	{
-		escontext.error_data->elevel = WARNING;
-		ThrowErrorData(escontext.error_data);
-		*ok = false;
-		return (Datum) 0;
-	}
-
-	if (array_contains_nulls(DatumGetArrayTypeP(result)))
-	{
-		ereport(WARNING,
-				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				 errmsg("\"%s\" array must not contain null values", staname)));
-		*ok = false;
-		return (Datum) 0;
-	}
-
-	*ok = true;
-
-	return result;
-}
-
-/*
- * Find and update the slot with the given stakind, or use the first empty
- * slot.
- */
-static void
-set_stats_slot(Datum *values, bool *nulls, bool *replaces,
-			   int16 stakind, Oid staop, Oid stacoll,
-			   Datum stanumbers, bool stanumbers_isnull,
-			   Datum stavalues, bool stavalues_isnull)
-{
-	int			slotidx;
-	int			first_empty = -1;
-	AttrNumber	stakind_attnum;
-	AttrNumber	staop_attnum;
-	AttrNumber	stacoll_attnum;
-
-	/* find existing slot with given stakind */
-	for (slotidx = 0; slotidx < STATISTIC_NUM_SLOTS; slotidx++)
-	{
-		stakind_attnum = Anum_pg_statistic_stakind1 - 1 + slotidx;
-
-		if (first_empty < 0 &&
-			DatumGetInt16(values[stakind_attnum]) == 0)
-			first_empty = slotidx;
-		if (DatumGetInt16(values[stakind_attnum]) == stakind)
-			break;
-	}
-
-	if (slotidx >= STATISTIC_NUM_SLOTS && first_empty >= 0)
-		slotidx = first_empty;
-
-	if (slotidx >= STATISTIC_NUM_SLOTS)
-		ereport(ERROR,
-				(errmsg("maximum number of statistics slots exceeded: %d",
-						slotidx + 1)));
-
-	stakind_attnum = Anum_pg_statistic_stakind1 - 1 + slotidx;
-	staop_attnum = Anum_pg_statistic_staop1 - 1 + slotidx;
-	stacoll_attnum = Anum_pg_statistic_stacoll1 - 1 + slotidx;
-
-	if (DatumGetInt16(values[stakind_attnum]) != stakind)
-	{
-		values[stakind_attnum] = Int16GetDatum(stakind);
-		replaces[stakind_attnum] = true;
-	}
-	if (DatumGetObjectId(values[staop_attnum]) != staop)
-	{
-		values[staop_attnum] = ObjectIdGetDatum(staop);
-		replaces[staop_attnum] = true;
-	}
-	if (DatumGetObjectId(values[stacoll_attnum]) != stacoll)
-	{
-		values[stacoll_attnum] = ObjectIdGetDatum(stacoll);
-		replaces[stacoll_attnum] = true;
-	}
-	if (!stanumbers_isnull)
-	{
-		values[Anum_pg_statistic_stanumbers1 - 1 + slotidx] = stanumbers;
-		nulls[Anum_pg_statistic_stanumbers1 - 1 + slotidx] = false;
-		replaces[Anum_pg_statistic_stanumbers1 - 1 + slotidx] = true;
-	}
-	if (!stavalues_isnull)
-	{
-		values[Anum_pg_statistic_stavalues1 - 1 + slotidx] = stavalues;
-		nulls[Anum_pg_statistic_stavalues1 - 1 + slotidx] = false;
-		replaces[Anum_pg_statistic_stavalues1 - 1 + slotidx] = true;
-	}
-}
-
 /*
  * Upsert the pg_statistic record.
  */
@@ -880,44 +582,6 @@ delete_pg_statistic(Oid reloid, AttrNumber attnum, bool stainherit)
 	return result;
 }
 
-/*
- * Initialize values and nulls for a new stats tuple.
- */
-static void
-init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
-					   Datum *values, bool *nulls, bool *replaces)
-{
-	memset(nulls, true, sizeof(bool) * Natts_pg_statistic);
-	memset(replaces, true, sizeof(bool) * Natts_pg_statistic);
-
-	/* must initialize non-NULL attributes */
-
-	values[Anum_pg_statistic_starelid - 1] = ObjectIdGetDatum(reloid);
-	nulls[Anum_pg_statistic_starelid - 1] = false;
-	values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(attnum);
-	nulls[Anum_pg_statistic_staattnum - 1] = false;
-	values[Anum_pg_statistic_stainherit - 1] = BoolGetDatum(inherited);
-	nulls[Anum_pg_statistic_stainherit - 1] = false;
-
-	values[Anum_pg_statistic_stanullfrac - 1] = DEFAULT_NULL_FRAC;
-	nulls[Anum_pg_statistic_stanullfrac - 1] = false;
-	values[Anum_pg_statistic_stawidth - 1] = DEFAULT_AVG_WIDTH;
-	nulls[Anum_pg_statistic_stawidth - 1] = false;
-	values[Anum_pg_statistic_stadistinct - 1] = DEFAULT_N_DISTINCT;
-	nulls[Anum_pg_statistic_stadistinct - 1] = false;
-
-	/* initialize stakind, staop, and stacoll slots */
-	for (int slotnum = 0; slotnum < STATISTIC_NUM_SLOTS; slotnum++)
-	{
-		values[Anum_pg_statistic_stakind1 + slotnum - 1] = (Datum) 0;
-		nulls[Anum_pg_statistic_stakind1 + slotnum - 1] = false;
-		values[Anum_pg_statistic_staop1 + slotnum - 1] = ObjectIdGetDatum(InvalidOid);
-		nulls[Anum_pg_statistic_staop1 + slotnum - 1] = false;
-		values[Anum_pg_statistic_stacoll1 + slotnum - 1] = ObjectIdGetDatum(InvalidOid);
-		nulls[Anum_pg_statistic_stacoll1 + slotnum - 1] = false;
-	}
-}
-
 /*
  * Delete statistics for the given attribute.
  */
diff --git a/src/backend/statistics/stat_utils.c b/src/backend/statistics/stat_utils.c
index 0c139bf43a7..1a7c6b024a1 100644
--- a/src/backend/statistics/stat_utils.c
+++ b/src/backend/statistics/stat_utils.c
@@ -21,9 +21,12 @@
 #include "catalog/index.h"
 #include "catalog/namespace.h"
 #include "catalog/pg_class.h"
+#include "catalog/pg_collation.h"
 #include "catalog/pg_database.h"
+#include "catalog/pg_statistic.h"
 #include "funcapi.h"
 #include "miscadmin.h"
+#include "nodes/nodeFuncs.h"
 #include "statistics/stat_utils.h"
 #include "storage/lmgr.h"
 #include "utils/acl.h"
@@ -33,6 +36,13 @@
 #include "utils/rel.h"
 #include "utils/syscache.h"
 
+
+#define DEFAULT_STATATT_NULL_FRAC      Float4GetDatum(0.0)
+#define DEFAULT_STATATT_AVG_WIDTH      Int32GetDatum(0) /* unknown */
+#define DEFAULT_STATATT_N_DISTINCT     Float4GetDatum(0.0)	/* unknown */
+
+static Node *get_attr_expr(Relation rel, int attnum);
+
 /*
  * Ensure that a given argument is not null.
  */
@@ -365,3 +375,365 @@ stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo,
 
 	return result;
 }
+
+/*
+ * If this relation is an index and that index has expressions in it, and
+ * the attnum specified is known to be an expression, then we must walk
+ * the list attributes up to the specified attnum to get the right
+ * expression.
+ */
+static Node *
+get_attr_expr(Relation rel, int attnum)
+{
+	List	   *index_exprs;
+	ListCell   *indexpr_item;
+
+	/* relation is not an index */
+	if (rel->rd_rel->relkind != RELKIND_INDEX &&
+		rel->rd_rel->relkind != RELKIND_PARTITIONED_INDEX)
+		return NULL;
+
+	index_exprs = RelationGetIndexExpressions(rel);
+
+	/* index has no expressions to give */
+	if (index_exprs == NIL)
+		return NULL;
+
+	/*
+	 * The index attnum points directly to a relation attnum, then it's not an
+	 * expression attribute.
+	 */
+	if (rel->rd_index->indkey.values[attnum - 1] != 0)
+		return NULL;
+
+	indexpr_item = list_head(rel->rd_indexprs);
+
+	for (int i = 0; i < attnum - 1; i++)
+		if (rel->rd_index->indkey.values[i] == 0)
+			indexpr_item = lnext(rel->rd_indexprs, indexpr_item);
+
+	if (indexpr_item == NULL)	/* shouldn't happen */
+		elog(ERROR, "too few entries in indexprs list");
+
+	return (Node *) lfirst(indexpr_item);
+}
+
+/*
+ * Derive type information from the attribute.
+ *
+ * This is needed for setting most slot statistics for all data types.
+ *
+ * This duplicates the logic in examine_attribute() but it will not skip the
+ * attribute if the attstattarget is 0.
+ *
+ * The information fetched here is a prerequisite to calling
+ * the other statatt_*() functions.
+ */
+void
+statatt_get_type(Oid reloid, AttrNumber attnum,
+				 Oid *atttypid, int32 *atttypmod,
+				 char *atttyptype, Oid *atttypcoll,
+				 Oid *eq_opr, Oid *lt_opr)
+{
+	Relation	rel = relation_open(reloid, AccessShareLock);
+	Form_pg_attribute attr;
+	HeapTuple	atup;
+	Node	   *expr;
+	TypeCacheEntry *typcache;
+
+	atup = SearchSysCache2(ATTNUM, ObjectIdGetDatum(reloid),
+						   Int16GetDatum(attnum));
+
+	/* Attribute not found */
+	if (!HeapTupleIsValid(atup))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_COLUMN),
+				 errmsg("column %d of relation \"%s\" does not exist",
+						attnum, RelationGetRelationName(rel))));
+
+	attr = (Form_pg_attribute) GETSTRUCT(atup);
+
+	if (attr->attisdropped)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_COLUMN),
+				 errmsg("column %d of relation \"%s\" does not exist",
+						attnum, RelationGetRelationName(rel))));
+
+	expr = get_attr_expr(rel, attr->attnum);
+
+	/*
+	 * When analyzing an expression index, believe the expression tree's type
+	 * not the column datatype --- the latter might be the opckeytype storage
+	 * type of the opclass, which is not interesting for our purposes. This
+	 * mimics the behavior of examine_attribute().
+	 */
+	if (expr == NULL)
+	{
+		*atttypid = attr->atttypid;
+		*atttypmod = attr->atttypmod;
+		*atttypcoll = attr->attcollation;
+	}
+	else
+	{
+		*atttypid = exprType(expr);
+		*atttypmod = exprTypmod(expr);
+
+		if (OidIsValid(attr->attcollation))
+			*atttypcoll = attr->attcollation;
+		else
+			*atttypcoll = exprCollation(expr);
+	}
+	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;
+	*eq_opr = typcache->eq_opr;
+	*lt_opr = typcache->lt_opr;
+
+	/*
+	 * Special case: collation for tsvector is DEFAULT_COLLATION_OID. See
+	 * compute_tsvector_stats().
+	 */
+	if (*atttypid == TSVECTOROID)
+		*atttypcoll = DEFAULT_COLLATION_OID;
+
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Derive element type information from the attribute type. This information
+ * is needed when the given type is one that contains elements of other types.
+ *
+ * The atttypid and atttyptype should be derived from a previous call to
+ * statatt_get_type().
+ */
+bool
+statatt_get_elem_type(Oid atttypid, char atttyptype,
+					  Oid *elemtypid, Oid *elem_eq_opr)
+{
+	TypeCacheEntry *elemtypcache;
+
+	if (atttypid == TSVECTOROID)
+	{
+		/*
+		 * Special case: element type for tsvector is text. See
+		 * compute_tsvector_stats().
+		 */
+		*elemtypid = TEXTOID;
+	}
+	else
+	{
+		/* find underlying element type through any domain */
+		*elemtypid = get_base_element_type(atttypid);
+	}
+
+	if (!OidIsValid(*elemtypid))
+		return false;
+
+	/* finds the right operator even if elemtypid is a domain */
+	elemtypcache = lookup_type_cache(*elemtypid, TYPECACHE_EQ_OPR);
+	if (!OidIsValid(elemtypcache->eq_opr))
+		return false;
+
+	*elem_eq_opr = elemtypcache->eq_opr;
+
+	return true;
+}
+
+/*
+ * Cast a text datum into an array with element type elemtypid.
+ *
+ * The typid and typmod should be derived from a previous call to
+ * statatt_get_type().
+ *
+ * If an error is encountered, capture it and re-throw a WARNING, and set ok
+ * to false. If the resulting array contains NULLs, raise a WARNING and set ok
+ * to false. Otherwise, set ok to true.
+ */
+Datum
+text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d, Oid typid,
+				  int32 typmod, bool *ok)
+{
+	LOCAL_FCINFO(fcinfo, 8);
+	char	   *s;
+	Datum		result;
+	ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+	escontext.details_wanted = true;
+
+	s = TextDatumGetCString(d);
+
+	InitFunctionCallInfoData(*fcinfo, array_in, 3, InvalidOid,
+							 (Node *) &escontext, NULL);
+
+	fcinfo->args[0].value = CStringGetDatum(s);
+	fcinfo->args[0].isnull = false;
+	fcinfo->args[1].value = ObjectIdGetDatum(typid);
+	fcinfo->args[1].isnull = false;
+	fcinfo->args[2].value = Int32GetDatum(typmod);
+	fcinfo->args[2].isnull = false;
+
+	result = FunctionCallInvoke(fcinfo);
+
+	pfree(s);
+
+	if (escontext.error_occurred)
+	{
+		escontext.error_data->elevel = WARNING;
+		ThrowErrorData(escontext.error_data);
+		*ok = false;
+		return (Datum) 0;
+	}
+
+	if (array_contains_nulls(DatumGetArrayTypeP(result)))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("\"%s\" array must not contain null values", staname)));
+		*ok = false;
+		return (Datum) 0;
+	}
+
+	*ok = true;
+
+	return result;
+}
+
+/*
+ * Find and update the slot with the given stakind, or use the first empty
+ * slot.
+ *
+ * Core statistics types expect the stakind value must be one of the
+ * STATISTIC_KIND_* constants defined in pg_statistic.h, but types defined by
+ * extensions are not restricted to those values.
+ *
+ * In the case of core statistics, the required staop is determined by the
+ * stakind given and will either be a hardcoded oid, or will be the eq/lt
+ * operator derived from statatt_get_type(). Likewise, types defined by
+ * extensions have no such restriction.
+ *
+ * The stacoll value will either be the atttypcoll derived from
+ * statatt_get_type() or a harcoded value required by that particular stakind.
+ *
+ * The value/null pairs for stanumbers and stavalues will have been calculated
+ * based on the stakind given.
+ */
+void
+statatt_set_slot(Datum *values, bool *nulls, bool *replaces,
+				 int16 stakind, Oid staop, Oid stacoll,
+				 Datum stanumbers, bool stanumbers_isnull,
+				 Datum stavalues, bool stavalues_isnull)
+{
+	int			slotidx;
+	int			first_empty = -1;
+	AttrNumber	stakind_attnum;
+	AttrNumber	staop_attnum;
+	AttrNumber	stacoll_attnum;
+
+	/* find existing slot with given stakind */
+	for (slotidx = 0; slotidx < STATISTIC_NUM_SLOTS; slotidx++)
+	{
+		stakind_attnum = Anum_pg_statistic_stakind1 - 1 + slotidx;
+
+		if (first_empty < 0 &&
+			DatumGetInt16(values[stakind_attnum]) == 0)
+			first_empty = slotidx;
+		if (DatumGetInt16(values[stakind_attnum]) == stakind)
+			break;
+	}
+
+	if (slotidx >= STATISTIC_NUM_SLOTS && first_empty >= 0)
+		slotidx = first_empty;
+
+	if (slotidx >= STATISTIC_NUM_SLOTS)
+		ereport(ERROR,
+				(errmsg("maximum number of statistics slots exceeded: %d",
+						slotidx + 1)));
+
+	stakind_attnum = Anum_pg_statistic_stakind1 - 1 + slotidx;
+	staop_attnum = Anum_pg_statistic_staop1 - 1 + slotidx;
+	stacoll_attnum = Anum_pg_statistic_stacoll1 - 1 + slotidx;
+
+	if (DatumGetInt16(values[stakind_attnum]) != stakind)
+	{
+		values[stakind_attnum] = Int16GetDatum(stakind);
+		replaces[stakind_attnum] = true;
+	}
+	if (DatumGetObjectId(values[staop_attnum]) != staop)
+	{
+		values[staop_attnum] = ObjectIdGetDatum(staop);
+		replaces[staop_attnum] = true;
+	}
+	if (DatumGetObjectId(values[stacoll_attnum]) != stacoll)
+	{
+		values[stacoll_attnum] = ObjectIdGetDatum(stacoll);
+		replaces[stacoll_attnum] = true;
+	}
+	if (!stanumbers_isnull)
+	{
+		values[Anum_pg_statistic_stanumbers1 - 1 + slotidx] = stanumbers;
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + slotidx] = false;
+		replaces[Anum_pg_statistic_stanumbers1 - 1 + slotidx] = true;
+	}
+	if (!stavalues_isnull)
+	{
+		values[Anum_pg_statistic_stavalues1 - 1 + slotidx] = stavalues;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + slotidx] = false;
+		replaces[Anum_pg_statistic_stavalues1 - 1 + slotidx] = true;
+	}
+}
+
+/*
+ * Initialize values and nulls for a new pg_statistic tuple.
+ *
+ * There are two possible destinations for the tuple created.
+ *
+ * The first is the pg_statistic table, in which case the reloid, attnum,
+ * and inherited flags should all be set.
+ *
+ * The second case is as an element of the stxdexpr array of a
+ * pg_statistic_ext_data tuple, in which case (reloid, attnum, inherited)
+ * should be set to (InvalidOid, InvalidAttrNumber, false).
+ */
+void
+statatt_init_empty_tuple(Oid reloid, int16 attnum, bool inherited,
+						 Datum *values, bool *nulls, bool *replaces)
+{
+	memset(nulls, true, sizeof(bool) * Natts_pg_statistic);
+	memset(replaces, true, sizeof(bool) * Natts_pg_statistic);
+
+	/* must initialize non-NULL attributes */
+
+	values[Anum_pg_statistic_starelid - 1] = ObjectIdGetDatum(reloid);
+	nulls[Anum_pg_statistic_starelid - 1] = false;
+	values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(attnum);
+	nulls[Anum_pg_statistic_staattnum - 1] = false;
+	values[Anum_pg_statistic_stainherit - 1] = BoolGetDatum(inherited);
+	nulls[Anum_pg_statistic_stainherit - 1] = false;
+
+	values[Anum_pg_statistic_stanullfrac - 1] = DEFAULT_STATATT_NULL_FRAC;
+	nulls[Anum_pg_statistic_stanullfrac - 1] = false;
+	values[Anum_pg_statistic_stawidth - 1] = DEFAULT_STATATT_AVG_WIDTH;
+	nulls[Anum_pg_statistic_stawidth - 1] = false;
+	values[Anum_pg_statistic_stadistinct - 1] = DEFAULT_STATATT_N_DISTINCT;
+	nulls[Anum_pg_statistic_stadistinct - 1] = false;
+
+	/* initialize stakind, staop, and stacoll slots */
+	for (int slotnum = 0; slotnum < STATISTIC_NUM_SLOTS; slotnum++)
+	{
+		values[Anum_pg_statistic_stakind1 + slotnum - 1] = (Datum) 0;
+		nulls[Anum_pg_statistic_stakind1 + slotnum - 1] = false;
+		values[Anum_pg_statistic_staop1 + slotnum - 1] = ObjectIdGetDatum(InvalidOid);
+		nulls[Anum_pg_statistic_staop1 + slotnum - 1] = false;
+		values[Anum_pg_statistic_stacoll1 + slotnum - 1] = ObjectIdGetDatum(InvalidOid);
+		nulls[Anum_pg_statistic_stacoll1 + slotnum - 1] = false;
+	}
+}

base-commit: 80f6e2fb4addb03e2e163a380b5e6e1f4b321286
-- 
2.52.0

From 933564c84cee2380faeac2c6e1bd368cb74fc52c Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Tue, 11 Nov 2025 16:58:26 +0900
Subject: [PATCH v20 2/3] Add extended statistics support functions.

Add pg_restore_extended_stats() and pg_clear_extended_stats(). These
functions closely mirror their relation and attribute counterparts,
but for extended statistics (i.e.  CREATE STATISTICS) objects.
---
 src/include/catalog/pg_proc.dat               |   18 +
 .../statistics/extended_stats_internal.h      |   12 +
 src/backend/statistics/dependencies.c         |   63 +
 src/backend/statistics/extended_stats.c       | 1205 +++++++++++++++++
 src/backend/statistics/mcv.c                  |  144 ++
 src/backend/statistics/mvdistinct.c           |   62 +
 src/test/regress/expected/stats_import.out    |  578 ++++++++
 src/test/regress/sql/stats_import.sql         |  364 +++++
 doc/src/sgml/func/func-admin.sgml             |   98 ++
 9 files changed, 2544 insertions(+)

index 66af2d96d67..38970930f95 100644
@@ -12602,6 +12602,24 @@
 
index efcb7dc3546..042f07a7602 100644
@@ -127,4 +127,16 @@ extern Selectivity mcv_clause_selectivity_or(PlannerInfo *root,
 
+extern Datum import_mcvlist(HeapTuple tup, int elevel, int numattrs,
+							Oid *atttypids, int32 *atttypmods, Oid *atttypcolls,
+							int nitems, Datum *mcv_elems, bool *mcv_nulls,
+							bool *mcv_elem_nulls, float8 *freqs, float8 *base_freqs);
+extern bool pg_ndistinct_validate_items(MVNDistinct *ndistinct, int2vector *stxkeys,
+										int numexprs, int elevel);
+extern void free_pg_ndistinct(MVNDistinct *ndistinct);
+extern bool pg_dependencies_validate_deps(const MVDependencies *dependencies,
+										  const int2vector *stxkeys, int numexprs,
+										  int elevel);
+extern void free_pg_dependencies(MVDependencies *dependencies);
+
 #endif							/* EXTENDED_STATS_INTERNAL_H */
diff --git a/src/backend/statistics/dependencies.c b/src/backend/statistics/dependencies.c
index d7bf6b7e846..0b64227223c 100644
--- a/src/backend/statistics/dependencies.c
+++ b/src/backend/statistics/dependencies.c
@@ -1065,6 +1065,57 @@ clauselist_apply_dependencies(PlannerInfo *root, List *clauses,
 	return s1;
 }
 
+/*
+ * Validate an MVDependencies against the extended statistics object definition.
+ *
+ * Every MVDependencies must be checked to ensure that the attnums in the
+ * attributes list correspond to attnums/expressions defined by the
+ * extended statistics object.
+ *
+ * Positive attnums are attributes which must be found in the stxkeys,
+ * while negative attnums correspond to an expr number, so the attnum
+ * can't be below (0 - numexprs).
+ */
+bool
+pg_dependencies_validate_deps(const MVDependencies *dependencies,
+							  const int2vector *stxkeys,
+							  int numexprs, int elevel)
+{
+	int			attnum_expr_lowbound = 0 - numexprs;
+
+	for (int i = 0; i < dependencies->ndeps; i++)
+	{
+		const MVDependency *dep = dependencies->deps[i];
+
+		for (int j = 0; j < dep->nattributes; j++)
+		{
+			AttrNumber	attnum = dep->attributes[j];
+			bool		ok = false;
+
+			if (attnum > 0)
+			{
+				for (int k = 0; k < stxkeys->dim1; k++)
+					if (attnum == stxkeys->values[k])
+					{
+						ok = true;
+						break;
+					}
+			}
+			else if ((attnum < 0) && (attnum >= attnum_expr_lowbound))
+				ok = true;
+
+			if (!ok)
+			{
+				ereport(elevel,
+						(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+						 errmsg("pg_dependencies: invalid attnum for this statistics object: %d", attnum)));
+				return false;
+			}
+		}
+	}
+	return true;
+}
+
 /*
  * dependency_is_compatible_expression
  *		Determines if the expression is compatible with functional dependencies
@@ -1248,6 +1299,18 @@ dependency_is_compatible_expression(Node *clause, Index relid, List *statlist, N
 	return false;
 }
 
+/*
index f003d7b4a73..7f6728195df 100644

+/*
 
@@ -99,6 +184,30 @@ static StatsBuildData *make_build_data(Relation rel, StatExtEntry *stat,
 
+static void upsert_pg_statistic_ext_data(const Datum *values,
+										 const bool *nulls,
+										 const bool *replaces);
 
 /*
  * Compute requested extended stats, using the rows sampled for the plain
@@ -2612,3 +2721,1099 @@ make_build_data(Relation rel, StatExtEntry *stat, int numrows, HeapTuple *rows,
 
 	return result;
 }
+
+/*
+ * Fetch a pg_statistic_ext row by name+nspoid.
+ */
+static HeapTuple
+get_pg_statistic_ext(Relation pg_stext, Oid nspoid, const char *stxname)
+{
+	ScanKeyData key[2];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	Oid			stxoid = InvalidOid;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_statistic_ext_stxname,
+				BTEqualStrategyNumber,
+				F_NAMEEQ,
+				CStringGetDatum(stxname));
+	ScanKeyInit(&key[1],
+				Anum_pg_statistic_ext_stxnamespace,
+				BTEqualStrategyNumber,
+				F_OIDEQ,
+				ObjectIdGetDatum(nspoid));
+
+	/*
+	 * Try to find matching pg_statistic_ext row.
+	 */
+	scan = systable_beginscan(pg_stext,
+							  StatisticExtNameIndexId,
+							  true,
+							  NULL,
+							  2,
+							  key);
+
+	/* Unique index, so we get 0 or 1 tuples. */
+	tup = systable_getnext(scan);
+
+	if (HeapTupleIsValid(tup))
+		stxoid = ((Form_pg_statistic_ext) GETSTRUCT(tup))->oid;
+
+	systable_endscan(scan);
+
+	if (!OidIsValid(stxoid))
+		return NULL;
+
+	return SearchSysCacheCopy1(STATEXTOID, ObjectIdGetDatum(stxoid));
+}
+
+/*
+ * Decode the stxkind column so that we know which stats types to expect.
+ */
+static void
+expand_stxkind(HeapTuple tup, stakindFlags * enabled)
+{
+	Datum		datum;
+	ArrayType  *arr;
+	char	   *kinds;
+
+	datum = SysCacheGetAttrNotNull(STATEXTOID,
+								   tup,
+								   Anum_pg_statistic_ext_stxkind);
+	arr = DatumGetArrayTypeP(datum);
+	if (ARR_NDIM(arr) != 1 || ARR_HASNULL(arr) || ARR_ELEMTYPE(arr) != CHAROID)
+		elog(ERROR, "stxkind is not a 1-D char array");
+
+	kinds = (char *) ARR_DATA_PTR(arr);
+
+	for (int i = 0; i < ARR_DIMS(arr)[0]; i++)
+		if (kinds[i] == STATS_EXT_NDISTINCT)
+			enabled->ndistinct = true;
+		else if (kinds[i] == STATS_EXT_DEPENDENCIES)
+			enabled->dependencies = true;
+		else if (kinds[i] == STATS_EXT_MCV)
+			enabled->mcv = true;
+		else if (kinds[i] == STATS_EXT_EXPRESSIONS)
+			enabled->expressions = true;
+}
+
+/*
+ * Perform the actual storage of a pg_statistic_ext_data tuple.
+ */
+static void
+upsert_pg_statistic_ext_data(const Datum *values, const bool *nulls,
+							 const bool *replaces)
+{
+	Relation	pg_stextdata;
+	HeapTuple	stxdtup;
+	HeapTuple	newtup;
+
+	pg_stextdata = table_open(StatisticExtDataRelationId, RowExclusiveLock);
+
+	stxdtup = SearchSysCache2(STATEXTDATASTXOID,
+							  values[Anum_pg_statistic_ext_data_stxoid - 1],
+							  values[Anum_pg_statistic_ext_data_stxdinherit - 1]);
+
+	if (HeapTupleIsValid(stxdtup))
+	{
+		newtup = heap_modify_tuple(stxdtup,
+								   RelationGetDescr(pg_stextdata),
+								   values,
+								   nulls,
+								   replaces);
+		CatalogTupleUpdate(pg_stextdata, &newtup->t_self, newtup);
+		ReleaseSysCache(stxdtup);
+	}
+	else
+	{
+		newtup = heap_form_tuple(RelationGetDescr(pg_stextdata), values, nulls);
+		CatalogTupleInsert(pg_stextdata, newtup);
+	}
+
+	heap_freetuple(newtup);
+
+	CommandCounterIncrement();
+
+	table_close(pg_stextdata, RowExclusiveLock);
+}
+
+/*
+ * Insert or Update Extended Statistics
+ *
+ * Major errors, such as the table not existing, the statistics object not
+ * existing, or a permissions failure are always reported at ERROR. Other
+ * errors, such as a conversion failure on one statistic kind, are reported
+ * as WARNINGs, and other statistic kinds may still be updated.
+ */
+static bool
+extended_statistics_update(FunctionCallInfo fcinfo)
+{
+	Oid			nspoid;
+	char	   *nspname;
+	char	   *stxname;
+	bool		inherited;
+	Relation	pg_stext;
+	HeapTuple	tup = NULL;
+
+	stakindFlags enabled;
+	stakindFlags has;
+
+	Form_pg_statistic_ext stxform;
+
+	Datum		values[Natts_pg_statistic_ext_data];
+	bool		nulls[Natts_pg_statistic_ext_data];
+	bool		replaces[Natts_pg_statistic_ext_data];
+
+	bool		success = true;
+
+	Datum		exprdatum;
+	bool		isnull;
+	List	   *exprs = NIL;
+	int			numattnums = 0;
+	int			numexprs = 0;
+	int			numattrs = 0;
+
+	/* arrays of type info, if we need them */
+	Oid		   *atttypids = NULL;
+	int32	   *atttypmods = NULL;
+	Oid		   *atttypcolls = NULL;
+	Relation	rel;
+	Oid			locked_table = InvalidOid;
+
+	memset(nulls, false, sizeof(nulls));
+	memset(values, 0, sizeof(values));
+	memset(replaces, 0, sizeof(replaces));
+	memset(&enabled, 0, sizeof(enabled));
+
+	has.mcv = (!PG_ARGISNULL(MOST_COMMON_VALS_ARG) &&
+			   !PG_ARGISNULL(MOST_COMMON_VAL_NULLS_ARG) &&
+			   !PG_ARGISNULL(MOST_COMMON_FREQS_ARG) &&
+			   !PG_ARGISNULL(MOST_COMMON_BASE_FREQS_ARG));
+	has.ndistinct = !PG_ARGISNULL(NDISTINCT_ARG);
+	has.dependencies = !PG_ARGISNULL(DEPENDENCIES_ARG);
+	has.expressions = !PG_ARGISNULL(EXPRESSIONS_ARG);
+
+	if (RecoveryInProgress())
+	{
+		ereport(WARNING,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("recovery is in progress"),
+				errhint("Statistics cannot be modified during recovery."));
+		PG_RETURN_BOOL(false);
+	}
+
+	stats_check_required_arg(fcinfo, extarginfo, STATSCHEMA_ARG);
+	nspname = TextDatumGetCString(PG_GETARG_DATUM(STATSCHEMA_ARG));
+	stats_check_required_arg(fcinfo, extarginfo, STATNAME_ARG);
+	stxname = TextDatumGetCString(PG_GETARG_DATUM(STATNAME_ARG));
+	stats_check_required_arg(fcinfo, extarginfo, INHERITED_ARG);
+	inherited = PG_GETARG_NAME(INHERITED_ARG);
+
+	nspoid = get_namespace_oid(nspname, true);
+	if (nspoid == InvalidOid)
+	{
+		ereport(WARNING,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("Namespace \"%s\" not found.", stxname));
+		PG_RETURN_BOOL(false);
+	}
+
+	pg_stext = table_open(StatisticExtRelationId, RowExclusiveLock);
+	tup = get_pg_statistic_ext(pg_stext, nspoid, stxname);
+
+	if (!HeapTupleIsValid(tup))
+	{
+		table_close(pg_stext, RowExclusiveLock);
+		ereport(WARNING,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("Extended Statistics Object \"%s\".\"%s\" not found.",
+					   get_namespace_name(nspoid), stxname));
+		PG_RETURN_BOOL(false);
+	}
+
+	stxform = (Form_pg_statistic_ext) GETSTRUCT(tup);
+	expand_stxkind(tup, &enabled);
+	numattnums = stxform->stxkeys.dim1;
+
+	/* decode expression (if any) */
+	exprdatum = SysCacheGetAttr(STATEXTOID,
+								tup,
+								Anum_pg_statistic_ext_stxexprs,
+								&isnull);
+
+	if (!isnull)
+	{
+		char	   *s;
+
+		s = TextDatumGetCString(exprdatum);
+		exprs = (List *) stringToNode(s);
+		pfree(s);
+
+		/*
+		 * Run the expressions through eval_const_expressions. This is not
+		 * just an optimization, but is necessary, because the planner will be
+		 * comparing them to similarly-processed qual clauses, and may fail to
+		 * detect valid matches without this.  We must not use
+		 * canonicalize_qual, however, since these aren't qual expressions.
+		 */
+		exprs = (List *) eval_const_expressions(NULL, (Node *) exprs);
+
+		/* May as well fix opfuncids too */
+		fix_opfuncids((Node *) exprs);
+	}
+	numexprs = list_length(exprs);
+	numattrs = numattnums + numexprs;
+
+	/* Roundabout way of getting a RangeVar on the underlying table */
+	rel = relation_open(stxform->stxrelid, AccessShareLock);
+
+	/* no need to fetch reloid, we already have it */
+	RangeVarGetRelidExtended(makeRangeVar(nspname,
+										  RelationGetRelationName(rel), -1),
+							 ShareUpdateExclusiveLock, 0,
+							 RangeVarCallbackForStats, &locked_table);
+
+	relation_close(rel, AccessShareLock);
+
+	if (has.mcv)
+	{
+		if (!enabled.mcv)
+		{
+			ereport(WARNING,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("MCV parameters \"%s\", \"%s\", \"%s\", and \"%s\" were all "
+						   "specified for extended statistics object that does not expect MCV ",
+						   extarginfo[MOST_COMMON_VALS_ARG].argname,
+						   extarginfo[MOST_COMMON_VAL_NULLS_ARG].argname,
+						   extarginfo[MOST_COMMON_FREQS_ARG].argname,
+						   extarginfo[MOST_COMMON_BASE_FREQS_ARG].argname));
+			has.mcv = false;
+			success = false;
+		}
+	}
+	else
+	{
+		/* The MCV args must all be NULL. */
+		if (!PG_ARGISNULL(MOST_COMMON_VALS_ARG) ||
+			!PG_ARGISNULL(MOST_COMMON_VAL_NULLS_ARG) ||
+			!PG_ARGISNULL(MOST_COMMON_FREQS_ARG) ||
+			!PG_ARGISNULL(MOST_COMMON_BASE_FREQS_ARG))
+		{
+			ereport(WARNING,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("MCV parameters \"%s\", \"%s\", \"%s\", and \"%s\" must be all specified if any are specified",
+						   extarginfo[MOST_COMMON_VALS_ARG].argname,
+						   extarginfo[MOST_COMMON_VAL_NULLS_ARG].argname,
+						   extarginfo[MOST_COMMON_FREQS_ARG].argname,
+						   extarginfo[MOST_COMMON_BASE_FREQS_ARG].argname));
+			success = false;
+		}
+	}
+
+	if (has.ndistinct && !enabled.ndistinct)
+	{
+		ereport(WARNING,
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("Parameters \"%s\" was specified for extended statistics object "
+					   "that does not expect \"%s\"",
+					   extarginfo[NDISTINCT_ARG].argname,
+					   extarginfo[NDISTINCT_ARG].argname));
+		has.ndistinct = false;
+		success = false;
+	}
+
+	if (has.dependencies && !enabled.dependencies)
+	{
+		ereport(WARNING,
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("Parameters \"%s\" was specified for extended statistics object "
+					   "that does not expect \"%s\"",
+					   extarginfo[DEPENDENCIES_ARG].argname,
+					   extarginfo[DEPENDENCIES_ARG].argname));
+		has.dependencies = false;
+		success = false;
+	}
+
+	if (has.expressions && !enabled.expressions)
+	{
+		ereport(WARNING,
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("Parameters \"%s\" was specified for extended statistics object "
+					   "that does not expect \"%s\"",
+					   extarginfo[DEPENDENCIES_ARG].argname,
+					   extarginfo[DEPENDENCIES_ARG].argname));
+		has.expressions = false;
+		success = false;
+	}
+
+	/*
+	 * Either of these statsistic types requires that we supply
+	 * semi-filled-out VacAttrStatP array.
+	 *
+	 *
+	 * It is not possible to use the existing lookup_var_attr_stats() and
+	 * examine_attribute() because these functions will skip attributes for
+	 * which attstattarget is 0, and we may have stats to import for those
+	 * attributes.
+	 */
+	if (has.mcv || has.expressions)
+	{
+		atttypids = palloc0(numattrs * sizeof(Oid));
+		atttypmods = palloc0(numattrs * sizeof(int32));
+		atttypcolls = palloc0(numattrs * sizeof(Oid));
+
+		for (int i = 0; i < numattnums; i++)
+		{
+			AttrNumber	attnum = stxform->stxkeys.values[i];
+
+			Oid			lt_opr;
+			Oid			eq_opr;
+			char		typetype;
+
+			/*
+			 * fetch attribute entries the same as are done for attribute
+			 * stats
+			 */
+			statatt_get_type(stxform->stxrelid,
+							 attnum,
+							 &atttypids[i],
+							 &atttypmods[i],
+							 &typetype,
+							 &atttypcolls[i],
+							 &lt_opr,
+							 &eq_opr);
+		}
+
+		for (int i = numattnums; i < numattrs; i++)
+		{
+			Node	   *expr = list_nth(exprs, i - numattnums);
+
+			atttypids[i] = exprType(expr);
+			atttypmods[i] = exprTypmod(expr);
+			atttypcolls[i] = exprCollation(expr);
+
+			/*
+			 * Duplicate logic from get_attr_stat_type
+			 */
+
+			/*
+			 * If it's a multirange, step down to the range type, as is done
+			 * by multirange_typanalyze().
+			 */
+			if (type_is_multirange(atttypids[i]))
+				atttypids[i] = get_multirange_range(atttypids[i]);
+
+			/*
+			 * Special case: collation for tsvector is DEFAULT_COLLATION_OID.
+			 * See compute_tsvector_stats().
+			 */
+			if (atttypids[i] == TSVECTOROID)
+				atttypcolls[i] = DEFAULT_COLLATION_OID;
+
+		}
+	}
+
+	/* Primary Key: cannot be NULL or replaced. */
+	values[Anum_pg_statistic_ext_data_stxoid - 1] = ObjectIdGetDatum(stxform->oid);
+	values[Anum_pg_statistic_ext_data_stxdinherit - 1] = BoolGetDatum(inherited);
+
+	if (has.ndistinct)
+	{
+		Datum		ndistinct_datum = PG_GETARG_DATUM(NDISTINCT_ARG);
+		bytea	   *data = DatumGetByteaPP(ndistinct_datum);
+		MVNDistinct *ndistinct = statext_ndistinct_deserialize(data);
+
+		if (pg_ndistinct_validate_items(ndistinct, &stxform->stxkeys, numexprs, WARNING))
+		{
+			values[Anum_pg_statistic_ext_data_stxdndistinct - 1] = ndistinct_datum;
+			replaces[Anum_pg_statistic_ext_data_stxdndistinct - 1] = true;
+		}
+		else
+		{
+			nulls[Anum_pg_statistic_ext_data_stxdndistinct - 1] = true;
+			success = false;
+		}
+
+		free_pg_ndistinct(ndistinct);
+	}
+	else
+		nulls[Anum_pg_statistic_ext_data_stxdndistinct - 1] = true;
+
+	if (has.dependencies)
+	{
+		Datum		dependencies_datum = PG_GETARG_DATUM(DEPENDENCIES_ARG);
+		bytea	   *data = DatumGetByteaPP(dependencies_datum);
+		MVDependencies *dependencies = statext_dependencies_deserialize(data);
+
+		if (pg_dependencies_validate_deps(dependencies, &stxform->stxkeys, numexprs, WARNING))
+		{
+			values[Anum_pg_statistic_ext_data_stxddependencies - 1] = dependencies_datum;
+			replaces[Anum_pg_statistic_ext_data_stxddependencies - 1] = true;
+		}
+		else
+		{
+			nulls[Anum_pg_statistic_ext_data_stxddependencies - 1] = true;
+			success = false;
+		}
+
+		free_pg_dependencies(dependencies);
+	}
+	else
+		nulls[Anum_pg_statistic_ext_data_stxddependencies - 1] = true;
+
+	if (has.mcv)
+	{
+		Datum		datum;
+		ArrayType  *mcv_arr = PG_GETARG_ARRAYTYPE_P(MOST_COMMON_VALS_ARG);
+		ArrayType  *nulls_arr = PG_GETARG_ARRAYTYPE_P(MOST_COMMON_VAL_NULLS_ARG);
+		ArrayType  *freqs_arr = PG_GETARG_ARRAYTYPE_P(MOST_COMMON_FREQS_ARG);
+		ArrayType  *base_freqs_arr = PG_GETARG_ARRAYTYPE_P(MOST_COMMON_BASE_FREQS_ARG);
+		int			nitems;
+		Datum	   *mcv_elems;
+		bool	   *mcv_nulls;
+		int			check_nummcv;
+
+		/*
+		 * The mcv_arr is an array of arrays of text, and we use it as the
+		 * reference array for checking the lengths of the other 3 arrays.
+		 */
+		if (ARR_NDIM(mcv_arr) != 2)
+		{
+			ereport(WARNING,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("Parameters \"%s\" must be a text array of 2 dimensions.",
+						   extarginfo[MOST_COMMON_VALS_ARG].argname));
+			return (Datum) 0;
+		}
+
+		nitems = ARR_DIMS(mcv_arr)[0];
+
+		/* Fixed length arrays that cannot contain NULLs. */
+		if (!check_mcvlist_array(nulls_arr, MOST_COMMON_VAL_NULLS_ARG,
+								 2, nitems) ||
+			!check_mcvlist_array(freqs_arr, MOST_COMMON_FREQS_ARG,
+								 1, nitems) ||
+			!check_mcvlist_array(base_freqs_arr, MOST_COMMON_BASE_FREQS_ARG,
+								 1, nitems))
+			return (Datum) 0;
+
+
+		deconstruct_array_builtin(mcv_arr, TEXTOID, &mcv_elems,
+								  &mcv_nulls, &check_nummcv);
+
+		Assert(check_nummcv == (nitems * numattrs));
+
+		datum = import_mcvlist(tup, WARNING, numattrs,
+							   atttypids, atttypmods, atttypcolls,
+							   nitems, mcv_elems, mcv_nulls,
+							   (bool *) ARR_DATA_PTR(nulls_arr),
+							   (float8 *) ARR_DATA_PTR(freqs_arr),
+							   (float8 *) ARR_DATA_PTR(base_freqs_arr));
+
+		values[Anum_pg_statistic_ext_data_stxdmcv - 1] = datum;
+		replaces[Anum_pg_statistic_ext_data_stxdmcv - 1] = true;
+	}
+	else
+		nulls[Anum_pg_statistic_ext_data_stxdmcv - 1] = true;
+
+	if (has.expressions)
+	{
+		Datum		datum;
+		Relation	pgsd;
+
+		pgsd = table_open(StatisticRelationId, RowExclusiveLock);
+
+		/*
+		 * Generate the expressions array.
+		 *
+		 * The attytypids, attytypmods, and atttypcols arrays have all the regular
+		 * attributes listed first, so we can pass those arrays with a start point
+		 * after the last regular attribute, and there should be numexprs elements
+		 * remaining.
+		 */
+		datum = import_expressions(pgsd, numexprs,
+								   &atttypids[numattnums], &atttypmods[numattnums],
+								   &atttypcolls[numattnums],
+								   PG_GETARG_ARRAYTYPE_P(EXPRESSIONS_ARG));
+
+		table_close(pgsd, RowExclusiveLock);
+
+		values[Anum_pg_statistic_ext_data_stxdexpr - 1] = datum;
+		replaces[Anum_pg_statistic_ext_data_stxdexpr - 1] = true;
+	}
+	else
+		nulls[Anum_pg_statistic_ext_data_stxdexpr - 1] = true;
+
+	upsert_pg_statistic_ext_data(values, nulls, replaces);
+
+	heap_freetuple(tup);
+	table_close(pg_stext, RowExclusiveLock);
+
+	if (atttypids != NULL)
+		pfree(atttypids);
+	if (atttypmods != NULL)
+		pfree(atttypmods);
+	if (atttypcolls != NULL)
+		pfree(atttypcolls);
+	return success;
+}
+
+/*
+ * Consistency checks to ensure that other mcvlist arrays are in alignment
+ * with the mcv array.
+ */
+static bool
+check_mcvlist_array(ArrayType *arr, int argindex, int required_ndims,
+					int mcv_length)
+{
+	if (ARR_NDIM(arr) != required_ndims)
+	{
+		ereport(WARNING,
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("Parameter \"%s\" must be an array of %d dimensions.",
+					   extarginfo[argindex].argname, required_ndims));
+		return false;
+	}
+
+	if (array_contains_nulls(arr))
+	{
+		ereport(WARNING,
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("Array \"%s\" cannot contain NULLs.",
+					   extarginfo[argindex].argname));
+		return false;
+	}
+
+	if (ARR_DIMS(arr)[0] != mcv_length)
+	{
+		ereport(WARNING,
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("Parameters \"%s\" must have the same number of elements as \"%s\"",
+					   extarginfo[argindex].argname,
+					   extarginfo[MOST_COMMON_VALS_ARG].argname));
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Warn of type mismatch. Common pattern.
+ */
+static Datum
+warn_type_mismatch(Datum d, const char *argname)
+{
+	char	   *s = TextDatumGetCString(d);
+
+	ereport(WARNING,
+			errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			errmsg("Expression %s element \"%s\" does not match expected input type.",
+				   argname, s));
+	return (Datum) 0;
+}
+
+/*
+ * Create the stxdexprs datum using the user input in an array of array of
+ * text, referenced against the datatypes for the expressions.
+ *
+ * This datum is needed to fill out a complete pg_statistic_ext_data tuple.
+ *
+ * The input arrays should each have numexprs elements in them and they should
+ * be the in the order that the expressions appear in the statistics object.
+ */
+static Datum
+import_expressions(Relation pgsd, int numexprs,
+				   Oid *atttypids, int32 *atttypmods,
+				   Oid *atttypcolls, ArrayType *exprs_arr)
+{
+	Datum	   *exprs_elems;
+	bool	   *exprs_nulls;
+	int			check_numexprs;
+	int			offset = 0;
+
+	FmgrInfo	array_in_fn;
+
+	Oid			pgstypoid = get_rel_type_id(StatisticRelationId);
+
+	ArrayBuildState *astate = NULL;
+
+	/*
+	 * Verify that the exprs_array is something that matches the expectations
+	 * set by stxdexprs generally and the specific statistics object definition.
+	 */
+	if (ARR_NDIM(exprs_arr) != 2)
+	{
+		ereport(WARNING,
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("Parameter \"%s\" must be a text array of 2 dimensions.",
+					   extarginfo[EXPRESSIONS_ARG].argname));
+		return (Datum) 0;
+	}
+
+	if (ARR_DIMS(exprs_arr)[0] != numexprs)
+	{
+		ereport(WARNING,
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("Parameter \"%s\" must have an outer dimension of %d elements.",
+					   extarginfo[EXPRESSIONS_ARG].argname, numexprs));
+		return (Datum) 0;
+	}
+	if (ARR_DIMS(exprs_arr)[1] != NUM_ATTRIBUTE_STATS_ELEMS)
+	{
+		ereport(WARNING,
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("Parameter \"%s\" must have an inner dimension of %d elements.",
+					   extarginfo[EXPRESSIONS_ARG].argname,
+					   NUM_ATTRIBUTE_STATS_ELEMS));
+		return (Datum) 0;
+	}
+
+	fmgr_info(F_ARRAY_IN, &array_in_fn);
+
+	deconstruct_array_builtin(exprs_arr, TEXTOID, &exprs_elems,
+							  &exprs_nulls, &check_numexprs);
+
+	/*
+	 * Iterate over each expected expression.
+	 *
+	 * The values/nulls/replaces arrays are deconstructed into a 1-D arrays, so
+	 * we have to advance an offset by NUM_ATTRIBUTE_STATS_ELEMS to get to the
+	 * next row of the 2-D array.
+	 */
+	for (int i = 0; i < numexprs; i++)
+	{
+		Oid			typid = atttypids[i];
+		int32		typmod = atttypmods[i];
+		Oid			stacoll = atttypcolls[i];
+		TypeCacheEntry *typcache;
+
+		Oid			elemtypid = InvalidOid;
+		Oid			elem_eq_opr = InvalidOid;
+
+		bool		ok;
+
+		Datum		values[Natts_pg_statistic];
+		bool		nulls[Natts_pg_statistic];
+		bool		replaces[Natts_pg_statistic];
+
+		HeapTuple	pgstup;
+		Datum		pgstdat;
+
+		/* Advance the indexes to the next offset. */
+		const int	null_frac_idx = offset + NULL_FRAC_ELEM;
+		const int	avg_width_idx = offset + AVG_WIDTH_ELEM;
+		const int	n_distinct_idx = offset + N_DISTINCT_ELEM;
+		const int	most_common_vals_idx = offset + MOST_COMMON_VALS_ELEM;
+		const int	most_common_freqs_idx = offset + MOST_COMMON_FREQS_ELEM;
+		const int	histogram_bounds_idx = offset + HISTOGRAM_BOUNDS_ELEM;
+		const int	correlation_idx = offset + CORRELATION_ELEM;
+		const int	most_common_elems_idx = offset + MOST_COMMON_ELEMS_ELEM;
+		const int	most_common_elems_freqs_idx = offset + MOST_COMMON_ELEM_FREQS_ELEM;
+		const int	elem_count_histogram_idx = offset + ELEM_COUNT_HISTOGRAM_ELEM;
+
+		/* This finds the right operators even if atttypid is a domain */
+		typcache = lookup_type_cache(typid, TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR);
+
+		statatt_init_empty_tuple(InvalidOid, InvalidAttrNumber, false,
+								 values, nulls, replaces);
+
+		/*
+		 * Check each of the fixed attributes to see if they have values set. If not
+		 * set, then just let them stay with the default values set in
+		 * statatt_init_empty_tuple().
+		 */
+		if (!exprs_nulls[null_frac_idx])
+		{
+			ok = text_to_float4(exprs_elems[null_frac_idx],
+								&values[Anum_pg_statistic_stanullfrac - 1]);
+
+			if (!ok)
+				return warn_type_mismatch(exprs_elems[null_frac_idx],
+										  extexprarginfo[NULL_FRAC_ELEM].argname);
+		}
+
+		if (!exprs_nulls[avg_width_idx])
+		{
+			ok = text_to_int4(exprs_elems[avg_width_idx],
+							  &values[Anum_pg_statistic_stawidth - 1]);
+
+			if (!ok)
+				return warn_type_mismatch(exprs_elems[avg_width_idx],
+										  extexprarginfo[AVG_WIDTH_ELEM].argname);
+		}
+
+		if (!exprs_nulls[n_distinct_idx])
+		{
+			ok = text_to_float4(exprs_elems[n_distinct_idx],
+								&values[Anum_pg_statistic_stadistinct - 1]);
+
+			if (!ok)
+				return warn_type_mismatch(exprs_elems[n_distinct_idx],
+										  extexprarginfo[N_DISTINCT_ELEM].argname);
+		}
+
+		/*
+		 * The STAKIND statistics are the same as the ones found in attribute
+		 * stats. However, these are all derived from text columns, whereas
+		 * the ones derived for attribute stats are a mix of datatypes. This
+		 * limits the opportunities for code sharing between the two.
+		 *
+		 * Some statistic kinds have both a stanumbers and a stavalues components.
+		 * In those cases, both values must either be NOT NULL or both NULL, and
+		 * if they aren't then we need to reject that stakind completely. Currently
+		 * we go a step further and reject the expression array completely.
+		 *
+		 * Once it is established that the pairs are in NULL/NOT-NULL alignment,
+		 * we can test either expr_nulls[] value to see if the stakind has value(s)
+		 * that we can set or not.
+		 */
+
+		/* STATISTIC_KIND_MCV */
+		if (exprs_nulls[most_common_vals_idx] !=
+			exprs_nulls[most_common_freqs_idx])
+		{
+			ereport(WARNING,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("Expression %s and %s must both be NOT NULL or both NULL.",
+						   extexprarginfo[MOST_COMMON_VALS_ELEM].argname,
+						   extexprarginfo[MOST_COMMON_FREQS_ELEM].argname));
+			return (Datum) 0;
+		}
+
+		if (!exprs_nulls[most_common_vals_idx])
+		{
+			Datum		stavalues;
+			Datum		stanumbers;
+
+			stavalues = text_to_stavalues(extexprarginfo[MOST_COMMON_VALS_ELEM].argname,
+										  &array_in_fn, exprs_elems[most_common_vals_idx],
+										  typid, typmod, &ok);
+
+			if (!ok)
+				return (Datum) 0;
+
+			stanumbers = text_to_stavalues(extexprarginfo[MOST_COMMON_FREQS_ELEM].argname,
+										   &array_in_fn, exprs_elems[most_common_freqs_idx],
+										   FLOAT4OID, -1, &ok);
+
+			if (!ok)
+				return (Datum) 0;
+
+			statatt_set_slot(values, nulls, replaces,
+							 STATISTIC_KIND_MCV,
+							 typcache->eq_opr, stacoll,
+							 stanumbers, false, stavalues, false);
+		}
+
+		/* STATISTIC_KIND_HISTOGRAM */
+		if (!exprs_nulls[histogram_bounds_idx])
+		{
+			Datum		stavalues;
+
+			stavalues = text_to_stavalues(extexprarginfo[HISTOGRAM_BOUNDS_ELEM].argname,
+										  &array_in_fn, exprs_elems[histogram_bounds_idx],
+										  typid, typmod, &ok);
+
+			if (!ok)
+				return (Datum) 0;
+
+			statatt_set_slot(values, nulls, replaces,
+							 STATISTIC_KIND_HISTOGRAM,
+							 typcache->lt_opr, stacoll,
+							 0, true, stavalues, false);
+		}
+
+		/* STATISTIC_KIND_CORRELATION */
+		if (!exprs_nulls[correlation_idx])
+		{
+			Datum		corr[] = {(Datum) 0};
+			ArrayType  *arry;
+			Datum		stanumbers;
+
+			ok = text_to_float4(exprs_elems[correlation_idx], &corr[0]);
+
+			if (!ok)
+			{
+				char	   *s = TextDatumGetCString(exprs_elems[correlation_idx]);
+
+				ereport(WARNING,
+						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						errmsg("Expression %s element \"%s\" does not match expected input type.",
+							   extexprarginfo[CORRELATION_ELEM].argname, s));
+				return (Datum) 0;
+			}
+
+			arry = construct_array_builtin(corr, 1, FLOAT4OID);
+
+			stanumbers = PointerGetDatum(arry);
+
+			statatt_set_slot(values, nulls, replaces,
+							 STATISTIC_KIND_CORRELATION,
+							 typcache->lt_opr, stacoll,
+							 stanumbers, false, 0, true);
+		}
+
+		/* STATISTIC_KIND_MCELEM */
+		if (exprs_nulls[most_common_elems_idx] !=
+			exprs_nulls[most_common_elems_freqs_idx])
+		{
+			ereport(WARNING,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("Expression %s and %s must both be NOT NULL or both NULL.",
+						   extexprarginfo[MOST_COMMON_ELEMS_ELEM].argname,
+						   extexprarginfo[MOST_COMMON_ELEM_FREQS_ELEM].argname));
+			return (Datum) 0;
+		}
+
+		/*
+		 * We only need to fetch element type and eq operator if we have a
+		 * stat of type MCELEM or DECHIST, otherwise the values are unnecessary
+		 * and not meaningful.
+		 */
+		if (!exprs_nulls[most_common_elems_idx] ||
+			!exprs_nulls[elem_count_histogram_idx])
+		{
+			if (!statatt_get_elem_type(typid, typcache->typtype,
+									   &elemtypid, &elem_eq_opr))
+			{
+				ereport(WARNING,
+						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						errmsg("unable to determine element type of expression"));
+				return (Datum) 0;
+			}
+		}
+
+		if (!exprs_nulls[most_common_elems_idx])
+		{
+			Datum		stavalues;
+			Datum		stanumbers;
+
+			stavalues = text_to_stavalues(extexprarginfo[MOST_COMMON_ELEMS_ELEM].argname,
+										  &array_in_fn,
+										  exprs_elems[most_common_elems_idx],
+										  elemtypid, typmod, &ok);
+
+			if (!ok)
+				return (Datum) 0;
+
+			stanumbers = text_to_stavalues(extexprarginfo[MOST_COMMON_ELEM_FREQS_ELEM].argname,
+										   &array_in_fn,
+										   exprs_elems[most_common_elems_freqs_idx],
+										   FLOAT4OID, -1, &ok);
+
+			if (!ok)
+				return (Datum) 0;
+
+			statatt_set_slot(values, nulls, replaces,
+							 STATISTIC_KIND_MCELEM,
+							 elem_eq_opr, stacoll,
+							 stanumbers, false, stavalues, false);
+		}
+
+		if (!exprs_nulls[elem_count_histogram_idx])
+		{
+			Datum		stanumbers;
+
+			stanumbers = text_to_stavalues(extexprarginfo[ELEM_COUNT_HISTOGRAM_ELEM].argname,
+										   &array_in_fn,
+										   exprs_elems[elem_count_histogram_idx],
+										   FLOAT4OID, -1, &ok);
+
+			if (!ok)
+				return (Datum) 0;
+
+			statatt_set_slot(values, nulls, replaces, STATISTIC_KIND_DECHIST,
+							 elem_eq_opr, stacoll,
+							 stanumbers, false, 0, true);
+		}
+
+		/*
+		 * Currently there are no extended stats exports of the statistic
+		 * kinds STATISTIC_KIND_BOUNDS_HISTOGRAM or
+		 * STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM so these cannot be imported.
+		 * These may be added in the future.
+		 */
+
+		pgstup = heap_form_tuple(RelationGetDescr(pgsd), values, nulls);
+		pgstdat = heap_copy_tuple_as_datum(pgstup, RelationGetDescr(pgsd));
+		astate = accumArrayResult(astate, pgstdat, false, pgstypoid,
+								  CurrentMemoryContext);
+
+		offset += NUM_ATTRIBUTE_STATS_ELEMS;
+	}
+
+	pfree(exprs_elems);
+	pfree(exprs_nulls);
+
+	return makeArrayResult(astate, CurrentMemoryContext);
+}
+
+/*
+ * Safe conversion of text to float4.
+ *
+ * There is no need for the specific error message.
+ */
+static bool
+text_to_float4(Datum input, Datum *output)
+{
+	ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+	char	   *s;
+	bool		ok;
+
+	s = TextDatumGetCString(input);
+	ok = DirectInputFunctionCallSafe(float4in, s, InvalidOid, -1,
+									 (Node *) &escontext, output);
+
+	pfree(s);
+	return ok;
+}
+
+
+/*
+ * Safe conversion of text to int4.
+ *
+ * There is no need for the specific error message.
+ */
+static bool
+text_to_int4(Datum input, Datum *output)
+{
+	ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+	char	   *s;
+	bool		ok;
+
+	s = TextDatumGetCString(input);
+	ok = DirectInputFunctionCallSafe(int4in, s, InvalidOid, -1,
+									 (Node *) &escontext, output);
+
+	pfree(s);
+	return ok;
+}
+
+/*
+ * Remove an existing pg_statistic_ext_data row for a given pg_statistic_ext
+ * row + inherited pair.
+ */
+static bool
+delete_pg_statistic_ext_data(Oid stxoid, bool inherited)
+{
+	Relation	sed = table_open(StatisticExtDataRelationId, RowExclusiveLock);
+	HeapTuple	oldtup;
+	bool		result = false;
+
+	/* Is there already a pg_statistic tuple for this attribute? */
+	oldtup = SearchSysCache2(STATEXTDATASTXOID,
+							 ObjectIdGetDatum(stxoid),
+							 BoolGetDatum(inherited));
+
+	if (HeapTupleIsValid(oldtup))
+	{
+		CatalogTupleDelete(sed, &oldtup->t_self);
+		ReleaseSysCache(oldtup);
+		result = true;
+	}
+
+	table_close(sed, RowExclusiveLock);
+
+	CommandCounterIncrement();
+
+	return result;
+}
+
+/*
+ * Restore (insert or replace) statistics for the given statistics object.
+ */
+Datum
+pg_restore_extended_stats(PG_FUNCTION_ARGS)
+{
+	LOCAL_FCINFO(positional_fcinfo, NUM_EXTENDED_STATS_ARGS);
+	bool		result = true;
+
+	InitFunctionCallInfoData(*positional_fcinfo, NULL, NUM_EXTENDED_STATS_ARGS,
+							 InvalidOid, NULL, NULL);
+
+	if (!stats_fill_fcinfo_from_arg_pairs(fcinfo, positional_fcinfo, extarginfo))
+		result = false;
+
+	if (!extended_statistics_update(positional_fcinfo))
+		result = false;
+
+	PG_RETURN_BOOL(result);
+}
+
+/*
+ * Delete statistics for the given statistics object.
+ */
+Datum
+pg_clear_extended_stats(PG_FUNCTION_ARGS)
+{
+	char	   *nspname;
+	Oid			nspoid;
+	char	   *stxname;
+	bool		inherited;
+	Relation	pg_stext;
+	HeapTuple	tup;
+	Relation	rel;
+	Oid			locked_table = InvalidOid;
+
+	Form_pg_statistic_ext stxform;
+
+	stats_check_required_arg(fcinfo, extarginfo, STATSCHEMA_ARG);
+	nspname = TextDatumGetCString(PG_GETARG_DATUM(STATSCHEMA_ARG));
+	stats_check_required_arg(fcinfo, extarginfo, STATNAME_ARG);
+	stxname = TextDatumGetCString(PG_GETARG_DATUM(STATNAME_ARG));
+	stats_check_required_arg(fcinfo, extarginfo, INHERITED_ARG);
+	inherited = PG_GETARG_NAME(INHERITED_ARG);
+
+	if (RecoveryInProgress())
+	{
+		ereport(WARNING,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("recovery is in progress"),
+				errhint("Statistics cannot be modified during recovery."));
+		PG_RETURN_VOID();
+	}
+
+	nspoid = get_namespace_oid(nspname, true);
+	if (nspoid == InvalidOid)
+	{
+		ereport(WARNING,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("Namespace \"%s\" not found.", stxname));
+		PG_RETURN_VOID();
+	}
+
+	pg_stext = table_open(StatisticExtRelationId, RowExclusiveLock);
+	tup = get_pg_statistic_ext(pg_stext, nspoid, stxname);
+
+	if (!HeapTupleIsValid(tup))
+	{
+		table_close(pg_stext, RowExclusiveLock);
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("Extended Statistics Object \"%s\".\"%s\" not found.",
+					   nspname, stxname));
+		PG_RETURN_VOID();
+	}
+
+	stxform = (Form_pg_statistic_ext) GETSTRUCT(tup);
+
+	/* Roundabout way of getting a RangeVar on the underlying table */
+	rel = relation_open(stxform->stxrelid, AccessShareLock);
+
+	/* no need to fetch reloid, we already have it */
+	RangeVarGetRelidExtended(makeRangeVar(nspname,
+										  RelationGetRelationName(rel), -1),
+							 ShareUpdateExclusiveLock, 0,
+							 RangeVarCallbackForStats, &locked_table);
+
+	relation_close(rel, AccessShareLock);
+
+	delete_pg_statistic_ext_data(stxform->oid, inherited);
+	heap_freetuple(tup);
+	table_close(pg_stext, RowExclusiveLock);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/statistics/mcv.c b/src/backend/statistics/mcv.c
index 28f925f397e..fe2f04ccb47 100644
--- a/src/backend/statistics/mcv.c
+++ b/src/backend/statistics/mcv.c
@@ -2173,3 +2173,147 @@ mcv_clause_selectivity_or(PlannerInfo *root, StatisticExtInfo *stat,
 
 	return s;
 }
+
+/*
index fe452f53ae4..0287ed7ecb0 100644
 
+/*
index 98ce7dc2841..d69771add12 100644
@@ -1342,6 +1346,580 @@ AND attname = 'i';
 (1 row)
 
 DROP TABLE stats_temp;
+-- set n_distinct using at attnum (1) that is not in the statistics object
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'n_distinct', '[{"attributes" : [3,-1], "ndistinct" : 4},
+                        {"attributes" : [3,-2], "ndistinct" : 4},
+                        {"attributes" : [-1,-2], "ndistinct" : 3},
+                        {"attributes" : [2,3,-1], "ndistinct" : 4},
+                        {"attributes" : [2,3,-2], "ndistinct" : 4},
+                        {"attributes" : [2,-1,-2], "ndistinct" : 4},
+                        {"attributes" : [3,-1,-2], "ndistinct" : 4},
+                        {"attributes" : [1,2,3,-1,-2], "ndistinct" : 4}]'::pg_ndistinct
+      );
+WARNING:  pg_ndistinct: invalid attnum for this statistics object: 1
+ pg_restore_extended_stats 
+---------------------------
+ f
+(1 row)
+
+-- set n_distinct using at attnum that is 0
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'n_distinct', '[{"attributes" : [3,-1], "ndistinct" : 4},
+                        {"attributes" : [3,-2], "ndistinct" : 4},
+                        {"attributes" : [-1,-2], "ndistinct" : 3},
+                        {"attributes" : [2,3,-1], "ndistinct" : 4},
+                        {"attributes" : [2,3,-2], "ndistinct" : 4},
+                        {"attributes" : [2,-1,-2], "ndistinct" : 4},
+                        {"attributes" : [0,2,3,-1,-2], "ndistinct" : 4}]'::pg_ndistinct
+      );
+ERROR:  malformed pg_ndistinct: "[{"attributes" : [3,-1], "ndistinct" : 4},
+                        {"attributes" : [3,-2], "ndistinct" : 4},
+                        {"attributes" : [-1,-2], "ndistinct" : 3},
+                        {"attributes" : [2,3,-1], "ndistinct" : 4},
+                        {"attributes" : [2,3,-2], "ndistinct" : 4},
+                        {"attributes" : [2,-1,-2], "ndistinct" : 4},
+                        {"attributes" : [0,2,3,-1,-2], "ndistinct" : 4}]"
+LINE 6:         'n_distinct', '[{"attributes" : [3,-1], "ndistinct" ...
+                              ^
+DETAIL:  Invalid "attributes" element: 0.
+-- set n_distinct using at attnum that is outside the expression bounds(below -2)
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'n_distinct', '[{"attributes" : [3,-1], "ndistinct" : 4},
+                        {"attributes" : [3,-2], "ndistinct" : 4},
+                        {"attributes" : [-1,-2], "ndistinct" : 3},
+                        {"attributes" : [3,-1,-4], "ndistinct" : 4},
+                        {"attributes" : [3,-2,-4], "ndistinct" : 4},
+                        {"attributes" : [-1,-2,-4], "ndistinct" : 4},
+                        {"attributes" : [3,-1,-2,-4], "ndistinct" : 4}]'::pg_ndistinct
+      );
+WARNING:  pg_ndistinct: invalid attnum for this statistics object: -4
+ pg_restore_extended_stats 
+---------------------------
+ f
+(1 row)
+
+-- ok
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'n_distinct', '[{"attributes" : [2,3], "ndistinct" : 4},
+                        {"attributes" : [2,-1], "ndistinct" : 4},
+                        {"attributes" : [3,-1], "ndistinct" : 4},
+                        {"attributes" : [3,-2], "ndistinct" : 4},
+                        {"attributes" : [-1,-2], "ndistinct" : 3},
+                        {"attributes" : [2,3,-1], "ndistinct" : 4},
+                        {"attributes" : [2,3,-2], "ndistinct" : 4},
+                        {"attributes" : [2,-1,-2], "ndistinct" : 4},
+                        {"attributes" : [2,3,-1,-2], "ndistinct" : 4}]'::pg_ndistinct
+      );
+ pg_restore_extended_stats 
+---------------------------
+ t
+(1 row)
+
+SELECT
+    replace(e.n_distinct,   '}, ', E'},\n') AS n_distinct,
+    e.dependencies,
+    e.most_common_vals, e.most_common_val_nulls,
+    e.most_common_freqs, e.most_common_base_freqs
+FROM pg_stats_ext AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false
+\gx
+-[ RECORD 1 ]----------+------------------------------------------------
+n_distinct             | [{"attributes": [2, 3], "ndistinct": 4},       +
+                       | {"attributes": [2, -1], "ndistinct": 4},       +
+                       | {"attributes": [3, -1], "ndistinct": 4},       +
+                       | {"attributes": [3, -2], "ndistinct": 4},       +
+                       | {"attributes": [-1, -2], "ndistinct": 3},      +
+                       | {"attributes": [2, 3, -1], "ndistinct": 4},    +
+                       | {"attributes": [2, 3, -2], "ndistinct": 4},    +
+                       | {"attributes": [2, -1, -2], "ndistinct": 4},   +
+                       | {"attributes": [2, 3, -1, -2], "ndistinct": 4}]
+dependencies           | 
+most_common_vals       | 
+most_common_val_nulls  | 
+most_common_freqs      | 
+most_common_base_freqs | 
+
+-- set dependencies using at attnum (1) that is not in the statistics object
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'dependencies', '[{"attributes": [2], "dependency": 1, "degree": 1.000000}]'::pg_dependencies
+      );
+WARNING:  pg_dependencies: invalid attnum for this statistics object: 1
+ pg_restore_extended_stats 
+---------------------------
+ f
+(1 row)
+
+-- set dependencies using at attnum that is 0
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'dependencies', '[{"attributes": [0], "dependency": -1, "degree": 1.000000}]'::pg_dependencies
+      );
+ERROR:  malformed pg_dependencies: "[{"attributes": [0], "dependency": -1, "degree": 1.000000}]"
+LINE 6:         'dependencies', '[{"attributes": [0], "dependency": ...
+                                ^
+DETAIL:  Invalid "attributes" element: 0.
+-- set dependencies using at attnum that is outside the expression bounds(below -2)
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'dependencies', '[{"attributes": [2], "dependency": -3, "degree": 1.000000}]'::pg_dependencies
+      );
+WARNING:  pg_dependencies: invalid attnum for this statistics object: -3
+ pg_restore_extended_stats 
+---------------------------
+ f
+(1 row)
+
+-- ok
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'dependencies', '[{"attributes": [2], "dependency": 3, "degree": 1.000000},
+                          {"attributes": [2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [3], "dependency": 2, "degree": 1.000000},
+                          {"attributes": [3], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [3], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [-1], "dependency": 2, "degree": 0.500000},
+                          {"attributes": [-1], "dependency": 3, "degree": 0.500000},
+                          {"attributes": [-1], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [-2], "dependency": 2, "degree": 0.500000},
+                          {"attributes": [-2], "dependency": 3, "degree": 0.500000},
+                          {"attributes": [-2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2,3], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2,3], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [2,-1], "dependency": 3, "degree": 1.000000},
+                          {"attributes": [2,-1], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [2,-2], "dependency": 3, "degree": 1.000000},
+                          {"attributes": [2,-2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [3,-1], "dependency": 2, "degree": 1.000000},
+                          {"attributes": [3,-1], "dependency": -2, "degree": 1.000000},
+                          {"attributes": [3,-2], "dependency": 2, "degree": 1.000000},
+                          {"attributes": [3,-2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [-1,-2], "dependency": 2, "degree": 0.500000},
+                          {"attributes": [-1,-2], "dependency": 3, "degree": 0.500000},
+                          {"attributes": [2,3,-2], "dependency": -1, "degree": 1.000000},
+                          {"attributes": [2,-1,-2], "dependency": 3, "degree": 1.000000},
+                          {"attributes": [3,-1,-2], "dependency": 2, "degree": 1.000000}]'::pg_dependencies
+      );
+ pg_restore_extended_stats 
+---------------------------
+ t
+(1 row)
+
+SELECT
+    replace(e.n_distinct,   '}, ', E'},\n') AS n_distinct,
+    replace(e.dependencies, '}, ', E'},\n') AS dependencies,
+    e.most_common_vals, e.most_common_val_nulls,
+    e.most_common_freqs, e.most_common_base_freqs
+FROM pg_stats_ext AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false
+\gx
+-[ RECORD 1 ]----------+------------------------------------------------------------------
+n_distinct             | [{"attributes": [2, 3], "ndistinct": 4},                         +
+                       | {"attributes": [2, -1], "ndistinct": 4},                         +
+                       | {"attributes": [3, -1], "ndistinct": 4},                         +
+                       | {"attributes": [3, -2], "ndistinct": 4},                         +
+                       | {"attributes": [-1, -2], "ndistinct": 3},                        +
+                       | {"attributes": [2, 3, -1], "ndistinct": 4},                      +
+                       | {"attributes": [2, 3, -2], "ndistinct": 4},                      +
+                       | {"attributes": [2, -1, -2], "ndistinct": 4},                     +
+                       | {"attributes": [2, 3, -1, -2], "ndistinct": 4}]
+dependencies           | [{"attributes": [2], "dependency": 3, "degree": 1.000000},       +
+                       | {"attributes": [2], "dependency": -1, "degree": 1.000000},       +
+                       | {"attributes": [2], "dependency": -2, "degree": 1.000000},       +
+                       | {"attributes": [3], "dependency": 2, "degree": 1.000000},        +
+                       | {"attributes": [3], "dependency": -1, "degree": 1.000000},       +
+                       | {"attributes": [3], "dependency": -2, "degree": 1.000000},       +
+                       | {"attributes": [-1], "dependency": 2, "degree": 0.500000},       +
+                       | {"attributes": [-1], "dependency": 3, "degree": 0.500000},       +
+                       | {"attributes": [-1], "dependency": -2, "degree": 1.000000},      +
+                       | {"attributes": [-2], "dependency": 2, "degree": 0.500000},       +
+                       | {"attributes": [-2], "dependency": 3, "degree": 0.500000},       +
+                       | {"attributes": [-2], "dependency": -1, "degree": 1.000000},      +
+                       | {"attributes": [2, 3], "dependency": -1, "degree": 1.000000},    +
+                       | {"attributes": [2, 3], "dependency": -2, "degree": 1.000000},    +
+                       | {"attributes": [2, -1], "dependency": 3, "degree": 1.000000},    +
+                       | {"attributes": [2, -1], "dependency": -2, "degree": 1.000000},   +
+                       | {"attributes": [2, -2], "dependency": 3, "degree": 1.000000},    +
+                       | {"attributes": [2, -2], "dependency": -1, "degree": 1.000000},   +
+                       | {"attributes": [3, -1], "dependency": 2, "degree": 1.000000},    +
+                       | {"attributes": [3, -1], "dependency": -2, "degree": 1.000000},   +
+                       | {"attributes": [3, -2], "dependency": 2, "degree": 1.000000},    +
+                       | {"attributes": [3, -2], "dependency": -1, "degree": 1.000000},   +
+                       | {"attributes": [-1, -2], "dependency": 2, "degree": 0.500000},   +
+                       | {"attributes": [-1, -2], "dependency": 3, "degree": 0.500000},   +
+                       | {"attributes": [2, 3, -2], "dependency": -1, "degree": 1.000000},+
+                       | {"attributes": [2, -1, -2], "dependency": 3, "degree": 1.000000},+
+                       | {"attributes": [3, -1, -2], "dependency": 2, "degree": 1.000000}]
+most_common_vals       | 
+most_common_val_nulls  | 
+most_common_freqs      | 
+most_common_base_freqs | 
+
+-- if any one mcv param specified, all four must be specified (part 1)
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'most_common_vals', '{{four,NULL,0,NULL},{one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")",1,2},{tre,"(3,3.3,TRE,03-03-2003,)",-1,3},{two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")",1,2}}'::text[]
+      );
+WARNING:  MCV parameters "most_common_vals", "most_common_val_nulls", "most_common_freqs", and "most_common_base_freqs" must be all specified if any are specified
+ pg_restore_extended_stats 
+---------------------------
+ f
+(1 row)
+
+-- if any one mcv param specified, all four must be specified (part 2)
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'most_common_val_nulls', '{{f,t,f,t},{f,f,f,f},{f,f,f,f},{f,f,f,f}}'::boolean[]
+      );
+WARNING:  MCV parameters "most_common_vals", "most_common_val_nulls", "most_common_freqs", and "most_common_base_freqs" must be all specified if any are specified
+ pg_restore_extended_stats 
+---------------------------
+ f
+(1 row)
+
+-- if any one mcv param specified, all four must be specified (part 3)
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'most_common_freqs', '{0.25,0.25,0.25,0.25}'::double precision[]
+      );
+WARNING:  MCV parameters "most_common_vals", "most_common_val_nulls", "most_common_freqs", and "most_common_base_freqs" must be all specified if any are specified
+ pg_restore_extended_stats 
+---------------------------
+ f
+(1 row)
+
+-- if any one mcv param specified, all four must be specified (part 4)
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'most_common_base_freqs', '{0.00390625,0.015625,0.00390625,0.015625}'::double precision[]
+      );
+WARNING:  MCV parameters "most_common_vals", "most_common_val_nulls", "most_common_freqs", and "most_common_base_freqs" must be all specified if any are specified
+ pg_restore_extended_stats 
+---------------------------
+ f
+(1 row)
+
+-- ok
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'most_common_vals', '{{four,NULL,0,NULL},{one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")",1,2},{tre,"(3,3.3,TRE,03-03-2003,)",-1,3},{two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")",1,2}}'::text[],
+        'most_common_val_nulls', '{{f,t,f,t},{f,f,f,f},{f,f,f,f},{f,f,f,f}}'::boolean[],
+        'most_common_freqs', '{0.25,0.25,0.25,0.25}'::double precision[],
+        'most_common_base_freqs', '{0.00390625,0.015625,0.00390625,0.015625}'::double precision[]
+      );
+ pg_restore_extended_stats 
+---------------------------
+ t
+(1 row)
+
+SELECT
+    replace(e.n_distinct,   '}, ', E'},\n') AS n_distinct,
+    replace(e.dependencies, '}, ', E'},\n') AS dependencies,
+    e.most_common_vals, e.most_common_val_nulls,
+    e.most_common_freqs, e.most_common_base_freqs
+FROM pg_stats_ext AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false
+\gx
+-[ RECORD 1 ]----------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+n_distinct             | [{"attributes": [2, 3], "ndistinct": 4},                                                                                                                                                   +
+                       | {"attributes": [2, -1], "ndistinct": 4},                                                                                                                                                   +
+                       | {"attributes": [3, -1], "ndistinct": 4},                                                                                                                                                   +
+                       | {"attributes": [3, -2], "ndistinct": 4},                                                                                                                                                   +
+                       | {"attributes": [-1, -2], "ndistinct": 3},                                                                                                                                                  +
+                       | {"attributes": [2, 3, -1], "ndistinct": 4},                                                                                                                                                +
+                       | {"attributes": [2, 3, -2], "ndistinct": 4},                                                                                                                                                +
+                       | {"attributes": [2, -1, -2], "ndistinct": 4},                                                                                                                                               +
+                       | {"attributes": [2, 3, -1, -2], "ndistinct": 4}]
+dependencies           | [{"attributes": [2], "dependency": 3, "degree": 1.000000},                                                                                                                                 +
+                       | {"attributes": [2], "dependency": -1, "degree": 1.000000},                                                                                                                                 +
+                       | {"attributes": [2], "dependency": -2, "degree": 1.000000},                                                                                                                                 +
+                       | {"attributes": [3], "dependency": 2, "degree": 1.000000},                                                                                                                                  +
+                       | {"attributes": [3], "dependency": -1, "degree": 1.000000},                                                                                                                                 +
+                       | {"attributes": [3], "dependency": -2, "degree": 1.000000},                                                                                                                                 +
+                       | {"attributes": [-1], "dependency": 2, "degree": 0.500000},                                                                                                                                 +
+                       | {"attributes": [-1], "dependency": 3, "degree": 0.500000},                                                                                                                                 +
+                       | {"attributes": [-1], "dependency": -2, "degree": 1.000000},                                                                                                                                +
+                       | {"attributes": [-2], "dependency": 2, "degree": 0.500000},                                                                                                                                 +
+                       | {"attributes": [-2], "dependency": 3, "degree": 0.500000},                                                                                                                                 +
+                       | {"attributes": [-2], "dependency": -1, "degree": 1.000000},                                                                                                                                +
+                       | {"attributes": [2, 3], "dependency": -1, "degree": 1.000000},                                                                                                                              +
+                       | {"attributes": [2, 3], "dependency": -2, "degree": 1.000000},                                                                                                                              +
+                       | {"attributes": [2, -1], "dependency": 3, "degree": 1.000000},                                                                                                                              +
+                       | {"attributes": [2, -1], "dependency": -2, "degree": 1.000000},                                                                                                                             +
+                       | {"attributes": [2, -2], "dependency": 3, "degree": 1.000000},                                                                                                                              +
+                       | {"attributes": [2, -2], "dependency": -1, "degree": 1.000000},                                                                                                                             +
+                       | {"attributes": [3, -1], "dependency": 2, "degree": 1.000000},                                                                                                                              +
+                       | {"attributes": [3, -1], "dependency": -2, "degree": 1.000000},                                                                                                                             +
+                       | {"attributes": [3, -2], "dependency": 2, "degree": 1.000000},                                                                                                                              +
+                       | {"attributes": [3, -2], "dependency": -1, "degree": 1.000000},                                                                                                                             +
+                       | {"attributes": [-1, -2], "dependency": 2, "degree": 0.500000},                                                                                                                             +
+                       | {"attributes": [-1, -2], "dependency": 3, "degree": 0.500000},                                                                                                                             +
+                       | {"attributes": [2, 3, -2], "dependency": -1, "degree": 1.000000},                                                                                                                          +
+                       | {"attributes": [2, -1, -2], "dependency": 3, "degree": 1.000000},                                                                                                                          +
+                       | {"attributes": [3, -1, -2], "dependency": 2, "degree": 1.000000}]
+most_common_vals       | {{four,NULL,0,NULL},{one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")",1,2},{tre,"(3,3.3,TRE,03-03-2003,)",-1,3},{two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")",1,2}}
+most_common_val_nulls  | {{f,t,f,t},{f,f,f,f},{f,f,f,f},{f,f,f,f}}
+most_common_freqs      | {0.25,0.25,0.25,0.25}
+most_common_base_freqs | {0.00390625,0.015625,0.00390625,0.015625}
+
+SELECT
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', 'stats_import',
+        'statistics_name', 'test_stat_clone',
+        'inherited', false,
+        'exprs', '{{0,4,-0.75,"{1}","{0.5}","{-1,0}",-0.6,NULL,NULL,NULL},{0.25,4,-0.5,"{2}","{0.5}",NULL,1,NULL,NULL,NULL}}'::text[]
+      );
+ pg_restore_extended_stats 
+---------------------------
+ t
+(1 row)
+
+SELECT
+    e.inherited, e.null_frac, e.avg_width, e.n_distinct, e.most_common_vals,
+    e.most_common_freqs, e.histogram_bounds, e.correlation,
+    e.most_common_elems, e.most_common_elem_freqs, e.elem_count_histogram
+FROM pg_stats_ext_exprs AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+and e.inherited = false
+\gx
+-[ RECORD 1 ]----------+-------
+inherited              | f
+null_frac              | 0
+avg_width              | 4
+n_distinct             | -0.75
+most_common_vals       | {1}
+most_common_freqs      | {0.5}
+histogram_bounds       | {-1,0}
+correlation            | -0.6
+most_common_elems      | 
+most_common_elem_freqs | 
+elem_count_histogram   | 
+-[ RECORD 2 ]----------+-------
+inherited              | f
+null_frac              | 0.25
+avg_width              | 4
+n_distinct             | -0.5
+most_common_vals       | {2}
+most_common_freqs      | {0.5}
+histogram_bounds       | 
+correlation            | 1
+most_common_elems      | 
+most_common_elem_freqs | 
+elem_count_histogram   | 
+
+SELECT
+    pg_catalog.pg_clear_extended_stats(
+        statistics_schemaname => 'stats_import',
+        statistics_name => 'test_stat_clone',
+        inherited => false);
+ pg_clear_extended_stats 
+-------------------------
+ 
+(1 row)
+
+SELECT COUNT(*)
+FROM pg_stats_ext AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false;
+ count 
+-------
+     0
+(1 row)
+
+SELECT COUNT(*)
+FROM pg_stats_ext_exprs AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false;
+ count 
+-------
+     0
+(1 row)
+
+--
+-- Copy stats from test_stat to test_stat_clone
+--
+SELECT
+    e.statistics_name,
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', e.statistics_schemaname::text,
+        'statistics_name', 'test_stat_clone',
+        'inherited', e.inherited,
+        'n_distinct', e.n_distinct,
+        'dependencies', e.dependencies,
+        'most_common_vals', e.most_common_vals,
+        'most_common_val_nulls', e.most_common_val_nulls,
+        '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
+        array_agg(
+            ARRAY[ee.null_frac::text, ee.avg_width::text,
+                  ee.n_distinct::text, ee.most_common_vals::text,
+                  ee.most_common_freqs::text, ee.histogram_bounds::text,
+                  ee.correlation::text, ee.most_common_elems::text,
+                  ee.most_common_elem_freqs::text,
+                  ee.elem_count_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 = 'test_stat';
+ statistics_name | pg_restore_extended_stats 
+-----------------+---------------------------
+ test_stat       | t
+(1 row)
+
+SELECT o.inherited,
+       o.n_distinct, o.dependencies, o.most_common_vals,
+       o.most_common_val_nulls, o.most_common_freqs,
+       o.most_common_base_freqs
+FROM pg_stats_ext AS o
+WHERE o.statistics_schemaname = 'stats_import'
+AND o.statistics_name = 'test_stat'
+EXCEPT
+SELECT n.inherited,
+       n.n_distinct, n.dependencies, n.most_common_vals,
+       n.most_common_val_nulls, n.most_common_freqs,
+       n.most_common_base_freqs
+FROM pg_stats_ext AS n
+WHERE n.statistics_schemaname = 'stats_import'
+AND n.statistics_name = 'test_stat_clone';
+ inherited | n_distinct | dependencies | most_common_vals | most_common_val_nulls | most_common_freqs | most_common_base_freqs 
+-----------+------------+--------------+------------------+-----------------------+-------------------+------------------------
+(0 rows)
+
+SELECT n.inherited,
+       n.n_distinct, n.dependencies, n.most_common_vals,
+       n.most_common_val_nulls, n.most_common_freqs,
+       n.most_common_base_freqs
+FROM pg_stats_ext AS n
+WHERE n.statistics_schemaname = 'stats_import'
+AND n.statistics_name = 'test_stat_clone'
+EXCEPT
+SELECT o.inherited,
+       o.n_distinct, o.dependencies, o.most_common_vals,
+       o.most_common_val_nulls, o.most_common_freqs,
+       o.most_common_base_freqs
+FROM pg_stats_ext AS o
+WHERE o.statistics_schemaname = 'stats_import'
+AND o.statistics_name = 'test_stat';
+ inherited | n_distinct | dependencies | most_common_vals | most_common_val_nulls | most_common_freqs | most_common_base_freqs 
+-----------+------------+--------------+------------------+-----------------------+-------------------+------------------------
+(0 rows)
+
+SELECT o.inherited,
+       o.null_frac, o.avg_width, o.n_distinct,
+       o.most_common_vals::text AS most_common_vals,
+       o.most_common_freqs,
+       o.histogram_bounds::text AS histogram_bounds,
+       o.correlation,
+       o.most_common_elems::text AS most_common_elems,
+       o.most_common_elem_freqs, o.elem_count_histogram
+FROM pg_stats_ext_exprs AS o
+WHERE o.statistics_schemaname = 'stats_import'
+AND o.statistics_name = 'test_stat'
+EXCEPT
+SELECT n.inherited,
+       n.null_frac, n.avg_width, n.n_distinct,
+       n.most_common_vals::text AS most_common_vals,
+       n.most_common_freqs,
+       n.histogram_bounds::text AS histogram_bounds,
+       n.correlation,
+       n.most_common_elems::text AS most_common_elems,
+       n.most_common_elem_freqs, n.elem_count_histogram
+FROM pg_stats_ext_exprs AS n
+WHERE n.statistics_schemaname = 'stats_import'
+AND n.statistics_name = 'test_stat_clone';
+ inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram 
+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------
+(0 rows)
+
+SELECT n.inherited,
+       n.null_frac, n.avg_width, n.n_distinct,
+       n.most_common_vals::text AS most_common_vals,
+       n.most_common_freqs,
+       n.histogram_bounds::text AS histogram_bounds,
+       n.correlation,
+       n.most_common_elems::text AS most_common_elems,
+       n.most_common_elem_freqs, n.elem_count_histogram
+FROM pg_stats_ext_exprs AS n
+WHERE n.statistics_schemaname = 'stats_import'
+AND n.statistics_name = 'test_stat_clone'
+EXCEPT
+SELECT o.inherited,
+       o.null_frac, o.avg_width, o.n_distinct,
+       o.most_common_vals::text AS most_common_vals,
+       o.most_common_freqs,
+       o.histogram_bounds::text AS histogram_bounds,
+       o.correlation,
+       o.most_common_elems::text AS most_common_elems,
+       o.most_common_elem_freqs, o.elem_count_histogram
+FROM pg_stats_ext_exprs AS o
+WHERE o.statistics_schemaname = 'stats_import'
+AND o.statistics_name = 'test_stat';
+ inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram 
+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------
+(0 rows)
+
 DROP SCHEMA stats_import CASCADE;
 NOTICE:  drop cascades to 6 other objects
 DETAIL:  drop cascades to type stats_import.complex_type
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index d140733a750..74700554c9c 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -766,6 +766,9 @@ SELECT 4, 'four', NULL, int4range(0,100), NULL;

 

+    replace(e.n_distinct,   '}, ', E'},\n') AS n_distinct,
+    replace(e.n_distinct,   '}, ', E'},\n') AS n_distinct,
+    replace(e.dependencies, '}, ', E'},\n') AS dependencies,
+    replace(e.n_distinct,   '}, ', E'},\n') AS n_distinct,
+    replace(e.dependencies, '}, ', E'},\n') AS dependencies,
2.52.0

From cfd04fb59a80f43e999b255df32952c0689f622e Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Wed, 5 Nov 2025 01:13:04 -0500
Subject: [PATCH v20 3/3] Include Extended Statistics in pg_dump.

* Extended Statistics were introduced in v10, so there is no support for
  prior versions necessary.
---
 src/bin/pg_dump/pg_backup.h          |   1 +
 src/bin/pg_dump/pg_backup_archiver.c |   3 +-
 src/bin/pg_dump/pg_dump.c            | 254 +++++++++++++++++++++++++++
 src/bin/pg_dump/t/002_pg_dump.pl     |  28 +++
 4 files changed, 285 insertions(+), 1 deletion(-)

index d9041dad720..2f8d9799c30 100644
+	PREPQUERY_DUMPEXTSTATSOBJSTATS,
 
index 2445085dbbd..207f47cc498 100644

 	PQclear(res);
@@ -18514,6 +18519,255 @@ dumpStatisticsExt(Archive *fout, const StatsExtInfo *statsextinfo)
 	free(qstatsextname);
 }
 
+/*
+	if (!fout->is_prepared[PREPQUERY_DUMPEXTSTATSOBJSTATS])
+		 * pg_stats_ext translating the ndistinct and dependencies, 14:
+		 * Create this column in all version because we need to order by it
+		 * later.
+		 * The ndistinnct and depdendencies formats changed in v19, so
+		 * everything before that needs to be translated.
+		 * The ndistinct translation converts this:
+		 * to this:
+		 * [ {"attributes": [3,4], "ndistinct": 11}, {"attributes": [3,6],
+		 * "ndistinct": 11}, {"attributes": [4,6], "ndistinct": 11},
+		 * {"attributes": [3,4,6], "ndistinct": 11} ]
+		 * and the dependencies translation converts this:
+		 * {"3 => 4": 1.000000, "3 => 6": 1.000000, "4 => 6": 1.000000, "3, 4
+		 * => 6": 1.000000, "3, 6 => 4": 1.000000}
+		 * to this:
+		 * [ {"attributes": [3], "dependency": 4, "degree": 1.000000},
+		 * {"attributes": [3], "dependency": 6, "degree": 1.000000},
+		 * {"attributes": [4], "dependency": 6, "degree": 1.000000},
+		 * {"attributes": [3,4], "dependency": 6, "degree": 1.000000},
+		 * {"attributes": [3,6], "dependency": 4, "degree": 1.000000} ]
+		/* MCV was introduced v13 */
+		fout->is_prepared[PREPQUERY_DUMPEXTSTATSOBJSTATS] = true;
index e33aa95f6ff..9ab82f97277 100644
 
+	#
2.52.0

---

From: Tom Lane <[email-masked]>
Date: Thu, 04 Dec 2025 19:37:32 -0500

> applied the two patches for the input functions.

I notice that since e1405aa5e went in, a number of older buildfarm
animals are issuing warnings about it:

In file included from pg_dependencies.c:21:0:
pg_dependencies.c: In function "dependencies_scalar":
../../../../src/include/nodes/miscnodes.h:54:15: warning: the comparison will always evaluate as "true" for the address of "escontext" will never be NULL [-Waddress]
  ((escontext) != NULL && IsA(escontext, ErrorSaveContext) && \\
               ^
pg_dependencies.c:497:8: note: in expansion of macro "SOFT_ERROR_OCCURRED"
    if (SOFT_ERROR_OCCURRED(&escontext))
        ^
../../../../src/include/nodes/miscnodes.h:54:15: warning: the comparison will always evaluate as "true" for the address of "escontext" will never be NULL [-Waddress]
  ((escontext) != NULL && IsA(escontext, ErrorSaveContext) && \\
               ^
pg_dependencies.c:542:8: note: in expansion of macro "SOFT_ERROR_OCCURRED"
    if (SOFT_ERROR_OCCURRED(&escontext))
        ^
../../../../src/include/nodes/miscnodes.h:54:15: warning: the comparison will always evaluate as "true" for the address of "escontext" will never be NULL [-Waddress]
  ((escontext) != NULL && IsA(escontext, ErrorSaveContext) && \\
               ^
pg_dependencies.c:572:8: note: in expansion of macro "SOFT_ERROR_OCCURRED"
    if (SOFT_ERROR_OCCURRED(&escontext))
        ^
In file included from pg_ndistinct.c:21:0:
pg_ndistinct.c: In function "ndistinct_scalar":
../../../../src/include/nodes/miscnodes.h:54:15: warning: the comparison will always evaluate as "true" for the address of "escontext" will never be NULL [-Waddress]
  ((escontext) != NULL && IsA(escontext, ErrorSaveContext) && \\
               ^
pg_ndistinct.c:438:8: note: in expansion of macro "SOFT_ERROR_OCCURRED"
    if (SOFT_ERROR_OCCURRED(&escontext))
        ^
../../../../src/include/nodes/miscnodes.h:54:15: warning: the comparison will always evaluate as "true" for the address of "escontext" will never be NULL [-Waddress]
  ((escontext) != NULL && IsA(escontext, ErrorSaveContext) && \\
               ^
pg_ndistinct.c:488:9: note: in expansion of macro "SOFT_ERROR_OCCURRED"
    if (!SOFT_ERROR_OCCURRED(&escontext))
         ^

The above is from rhinoceros, but about half a dozen other animals
are reporting the same.  They are all running very old gcc versions,
mostly from RHEL7 or CentOS 7.

This of course arises because the source code is passing the address
of a local ErrorSaveContext variable to that macro.  We don't have
any other instances of that coding pattern AFAICS, so I wonder if
that was really the most adapted way to do it.  Looking at the
code, it seems to be turning around and stuffing a different error
message into a passed-in ErrorSaveContext, which feels a little
weird.

It may be that there's not a better way, in which case I'll probably
just teach my buildfarm-warning-scraper script to ignore these
warnings, as it's already doing for some other warnings from these
same animals (-Wmissing-braces mostly).  But I thought I'd raise a
question about it.

Also, I don't think these errdetail messages meet our style
guidelines:

                        errdetail("Invalid \"%s\" value.", PG_DEPENDENCIES_KEY_ATTRIBUTES));

They're supposed to be complete sentences.

			regards, tom lane

---

From: Michael Paquier <[email-masked]>
Date: Fri, 5 Dec 2025 10:19:27 +0900

> message into a passed-in ErrorSaveContext, which feels a little

Thanks for the report.  Right.  There are three instances of that in
pg_dependencies.c, two in pg_ndistinct.c.  I would not mind doing the
attached to calm down these warnings, matching with the other areas of
the code, that simply removes the macro and checks the state value
directly.

> They're supposed to be complete sentences.

Yeah, I should have been more careful here.  I have spent more time on
all of these, and adjusted all the errdetails for pg_ndistinct.c and
pg_dependencies.c to use full sentences.

I am just grouping everything with the attached.  They'd be better
if handled separately, obviously.  Comments?
--
Michael
From bb8f5e97e7cc7c435cbf1ec5bc16f1d5a46cc9e1 Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Fri, 5 Dec 2025 10:14:39 +0900
Subject: [PATCH] Improve pg_dependencies.c and pg_ndistinct.c

---
 src/backend/utils/adt/pg_dependencies.c       |  42 ++--
 src/backend/utils/adt/pg_ndistinct.c          |  34 +--
 src/test/regress/expected/pg_dependencies.out | 196 +++++++++---------
 src/test/regress/expected/pg_ndistinct.out    | 160 +++++++-------
 4 files changed, 216 insertions(+), 216 deletions(-)

diff --git a/src/backend/utils/adt/pg_dependencies.c b/src/backend/utils/adt/pg_dependencies.c
index 56abb7441775..1552e03d1c1e 100644
--- a/src/backend/utils/adt/pg_dependencies.c
+++ b/src/backend/utils/adt/pg_dependencies.c
@@ -85,7 +85,7 @@ dependencies_object_start(void *state)
 			errsave(parse->escontext,
 					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
-					errdetail("Expected an object key."));
+					errdetail("A key was expected."));
 			break;
 
 		case DEPS_EXPECT_ATTNUM_LIST:
@@ -127,7 +127,7 @@ dependencies_object_start(void *state)
 			errsave(parse->escontext,
 					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
-					errdetail("Unexpected parse state: %d", (int) parse->state));
+					errdetail("Unexpected parse state has been found: %d.", (int) parse->state));
 			break;
 	}
 
@@ -153,7 +153,7 @@ dependencies_object_end(void *state)
 		errsave(parse->escontext,
 				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 				errmsg("malformed pg_dependencies: \"%s\"", parse->str),
-				errdetail("Unexpected parse state: %d", (int) parse->state));
+				errdetail("Unexpected parse state has been found: %d.", (int) parse->state));
 		return JSON_SEM_ACTION_FAILED;
 	}
 
@@ -162,7 +162,7 @@ dependencies_object_end(void *state)
 		errsave(parse->escontext,
 				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 				errmsg("malformed pg_dependencies: \"%s\"", parse->str),
-				errdetail("Item must contain \"%s\" key",
+				errdetail("Item must contain \"%s\" key.",
 						  PG_DEPENDENCIES_KEY_ATTRIBUTES));
 		return JSON_SEM_ACTION_FAILED;
 	}
@@ -226,7 +226,7 @@ dependencies_object_end(void *state)
 			errsave(parse->escontext,
 					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
-					errdetail("Item \"%s\" value %d found in the \"%s\" list.",
+					errdetail("Item \"%s\" with value %d has been found in the \"%s\" list.",
 							  PG_DEPENDENCIES_KEY_DEPENDENCY, parse->dependency,
 							  PG_DEPENDENCIES_KEY_ATTRIBUTES));
 			return JSON_SEM_ACTION_FAILED;
@@ -274,7 +274,7 @@ dependencies_array_start(void *state)
 			errsave(parse->escontext,
 					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
-					errdetail("Array found in unexpected place."));
+					errdetail("Array has been found at an unexpected location."));
 			return JSON_SEM_ACTION_FAILED;
 	}
 
@@ -329,7 +329,7 @@ dependencies_array_end(void *state)
 			errsave(parse->escontext,
 					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
-					errdetail("Array found in unexpected place."));
+					errdetail("Array has been found at an unexpected location."));
 			break;
 	}
 	return JSON_SEM_ACTION_FAILED;
@@ -446,7 +446,7 @@ dependencies_array_element_start(void *state, bool isnull)
 			errsave(parse->escontext,
 					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
-					errdetail("Unexpected array element."));
+					errdetail("Unexpected array element has been found."));
 			break;
 	}
 
@@ -494,12 +494,12 @@ dependencies_scalar(void *state, char *token, JsonTokenType tokentype)
 		case DEPS_EXPECT_ATTNUM:
 			attnum = pg_strtoint16_safe(token, (Node *) &escontext);
 
-			if (SOFT_ERROR_OCCURRED(&escontext))
+			if (escontext.error_occurred)
 			{
 				errsave(parse->escontext,
 						errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 						errmsg("malformed pg_dependencies: \"%s\"", parse->str),
-						errdetail("Invalid \"%s\" value.", PG_DEPENDENCIES_KEY_ATTRIBUTES));
+						errdetail("Key \"%s\" has an incorrect value.", PG_DEPENDENCIES_KEY_ATTRIBUTES));
 				return JSON_SEM_ACTION_FAILED;
 			}
 
@@ -512,7 +512,7 @@ dependencies_scalar(void *state, char *token, JsonTokenType tokentype)
 				errsave(parse->escontext,
 						errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 						errmsg("malformed pg_dependencies: \"%s\"", parse->str),
-						errdetail("Invalid \"%s\" element: %d.",
+						errdetail("Invalid \"%s\" element has been found: %d.",
 								  PG_DEPENDENCIES_KEY_ATTRIBUTES, attnum));
 				return JSON_SEM_ACTION_FAILED;
 			}
@@ -526,7 +526,7 @@ dependencies_scalar(void *state, char *token, JsonTokenType tokentype)
 					errsave(parse->escontext,
 							errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 							errmsg("malformed pg_dependencies: \"%s\"", parse->str),
-							errdetail("Invalid \"%s\" element: %d cannot follow %d.",
+							errdetail("Invalid \"%s\" element has been found: %d cannot follow %d.",
 									  PG_DEPENDENCIES_KEY_ATTRIBUTES, attnum, prev));
 					return JSON_SEM_ACTION_FAILED;
 				}
@@ -539,12 +539,12 @@ dependencies_scalar(void *state, char *token, JsonTokenType tokentype)
 			parse->dependency = (AttrNumber)
 				pg_strtoint16_safe(token, (Node *) &escontext);
 
-			if (SOFT_ERROR_OCCURRED(&escontext))
+			if (escontext.error_occurred)
 			{
 				errsave(parse->escontext,
 						errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 						errmsg("malformed pg_dependencies: \"%s\"", parse->str),
-						errdetail("Invalid \"%s\" value.", PG_DEPENDENCIES_KEY_DEPENDENCY));
+						errdetail("Key \"%s\" has an incorrect value.", PG_DEPENDENCIES_KEY_DEPENDENCY));
 				return JSON_SEM_ACTION_FAILED;
 			}
 
@@ -557,7 +557,7 @@ dependencies_scalar(void *state, char *token, JsonTokenType tokentype)
 				errsave(parse->escontext,
 						errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 						errmsg("malformed pg_dependencies: \"%s\"", parse->str),
-						errdetail("Invalid \"%s\" value: %d.",
+						errdetail("Key \"%s\" has an incorrect value: %d.",
 								  PG_DEPENDENCIES_KEY_DEPENDENCY, parse->dependency));
 				return JSON_SEM_ACTION_FAILED;
 			}
@@ -569,12 +569,12 @@ dependencies_scalar(void *state, char *token, JsonTokenType tokentype)
 			parse->degree = float8in_internal(token, NULL, "double",
 											  token, (Node *) &escontext);
 
-			if (SOFT_ERROR_OCCURRED(&escontext))
+			if (escontext.error_occurred)
 			{
 				errsave(parse->escontext,
 						errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 						errmsg("malformed pg_dependencies: \"%s\"", parse->str),
-						errdetail("Invalid \"%s\" value.", PG_DEPENDENCIES_KEY_DEGREE));
+						errdetail("Key \"%s\" has an incorrect value.", PG_DEPENDENCIES_KEY_DEGREE));
 				return JSON_SEM_ACTION_FAILED;
 			}
 
@@ -585,7 +585,7 @@ dependencies_scalar(void *state, char *token, JsonTokenType tokentype)
 			errsave(parse->escontext,
 					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
-					errdetail("Unexpected scalar."));
+					errdetail("Unexpected scalar has been found."));
 			break;
 	}
 
@@ -686,7 +686,7 @@ build_mvdependencies(DependenciesParseState *parse, char *str)
 			errsave(parse->escontext,
 					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 					errmsg("malformed pg_dependencies: \"%s\"", str),
-					errdetail("Unexpected end state %d.", parse->state));
+					errdetail("Unexpected end state has been found: %d.", parse->state));
 			return NULL;
 	}
 
@@ -721,7 +721,7 @@ build_mvdependencies(DependenciesParseState *parse, char *str)
 				errsave(parse->escontext,
 						errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 						errmsg("malformed pg_dependencies: \"%s\"", str),
-						errdetail("Duplicate \"%s\" array: [%s] with \"%s\": %d.",
+						errdetail("Duplicated \"%s\" array has been found: [%s] for key \"%s\" and value %d.",
 								  PG_DEPENDENCIES_KEY_ATTRIBUTES, attnum_list,
 								  PG_DEPENDENCIES_KEY_DEPENDENCY, attnum_dep));
 				pfree(mvdeps);
@@ -808,7 +808,7 @@ pg_dependencies_in(PG_FUNCTION_ARGS)
 		errsave(parse_state.escontext,
 				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 				errmsg("malformed pg_dependencies: \"%s\"", str),
-				errdetail("Must be valid JSON."));
+				errdetail("Input data must be valid JSON."));
 
 	PG_RETURN_NULL();
 }
diff --git a/src/backend/utils/adt/pg_ndistinct.c b/src/backend/utils/adt/pg_ndistinct.c
index 5292521b1695..9bf1546c8030 100644
--- a/src/backend/utils/adt/pg_ndistinct.c
+++ b/src/backend/utils/adt/pg_ndistinct.c
@@ -81,7 +81,7 @@ ndistinct_object_start(void *state)
 			errsave(parse->escontext,
 					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
-					errdetail("Expected an object key."));
+					errdetail("A key was expected."));
 			break;
 
 		case NDIST_EXPECT_ATTNUM_LIST:
@@ -114,7 +114,7 @@ ndistinct_object_start(void *state)
 			errsave(parse->escontext,
 					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
-					errdetail("Unexpected parse state: %d", (int) parse->state));
+					errdetail("Unexpected parse state has been found: %d.", (int) parse->state));
 			break;
 	}
 
@@ -140,7 +140,7 @@ ndistinct_object_end(void *state)
 		errsave(parse->escontext,
 				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 				errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
-				errdetail("Unexpected parse state: %d", (int) parse->state));
+				errdetail("Unexpected parse state has been found: %d.", (int) parse->state));
 		return JSON_SEM_ACTION_FAILED;
 	}
 
@@ -228,7 +228,7 @@ ndistinct_array_start(void *state)
 			errsave(parse->escontext,
 					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
-					errdetail("Array found in unexpected place."));
+					errdetail("Array has been found at an unexpected location."));
 			return JSON_SEM_ACTION_FAILED;
 	}
 
@@ -289,7 +289,7 @@ ndistinct_array_end(void *state)
 			errsave(parse->escontext,
 					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
-					errdetail("Array found in unexpected place."));
+					errdetail("Array has been found at an unexpected location."));
 			break;
 	}
 
@@ -387,7 +387,7 @@ ndistinct_array_element_start(void *state, bool isnull)
 			errsave(parse->escontext,
 					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
-					errdetail("Unexpected array element."));
+					errdetail("Unexpected array element has been found."));
 			break;
 	}
 
@@ -435,12 +435,12 @@ ndistinct_scalar(void *state, char *token, JsonTokenType tokentype)
 		case NDIST_EXPECT_ATTNUM:
 			attnum = pg_strtoint16_safe(token, (Node *) &escontext);
 
-			if (SOFT_ERROR_OCCURRED(&escontext))
+			if (escontext.error_occurred)
 			{
 				errsave(parse->escontext,
 						errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 						errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
-						errdetail("Invalid \"%s\" value.", PG_NDISTINCT_KEY_ATTRIBUTES));
+						errdetail("Key \"%s\" has an incorrect value.", PG_NDISTINCT_KEY_ATTRIBUTES));
 				return JSON_SEM_ACTION_FAILED;
 			}
 
@@ -453,7 +453,7 @@ ndistinct_scalar(void *state, char *token, JsonTokenType tokentype)
 				errsave(parse->escontext,
 						errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 						errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
-						errdetail("Invalid \"%s\" element: %d.",
+						errdetail("Invalid \"%s\" element has been found: %d.",
 								  PG_NDISTINCT_KEY_ATTRIBUTES, attnum));
 				return JSON_SEM_ACTION_FAILED;
 			}
@@ -467,7 +467,7 @@ ndistinct_scalar(void *state, char *token, JsonTokenType tokentype)
 					errsave(parse->escontext,
 							errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 							errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
-							errdetail("Invalid \"%s\" element: %d cannot follow %d.",
+							errdetail("Invalid \"%s\" element has been found: %d cannot follow %d.",
 									  PG_NDISTINCT_KEY_ATTRIBUTES, attnum, prev));
 					return JSON_SEM_ACTION_FAILED;
 				}
@@ -485,7 +485,7 @@ ndistinct_scalar(void *state, char *token, JsonTokenType tokentype)
 			 */
 			parse->ndistinct = pg_strtoint32_safe(token, (Node *) &escontext);
 
-			if (!SOFT_ERROR_OCCURRED(&escontext))
+			if (!escontext.error_occurred)
 			{
 				parse->state = NDIST_EXPECT_KEY;
 				return JSON_SUCCESS;
@@ -494,7 +494,7 @@ ndistinct_scalar(void *state, char *token, JsonTokenType tokentype)
 			errsave(parse->escontext,
 					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
-					errdetail("Invalid \"%s\" value.",
+					errdetail("Key \"%s\" has an incorrect value.",
 							  PG_NDISTINCT_KEY_NDISTINCT));
 			break;
 
@@ -502,7 +502,7 @@ ndistinct_scalar(void *state, char *token, JsonTokenType tokentype)
 			errsave(parse->escontext,
 					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
-					errdetail("Unexpected scalar."));
+					errdetail("Unexpected scalar has been found."));
 			break;
 	}
 
@@ -627,7 +627,7 @@ build_mvndistinct(NDistinctParseState *parse, char *str)
 			errsave(parse->escontext,
 					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 					errmsg("malformed pg_ndistinct: \"%s\"", str),
-					errdetail("Unexpected end state %d.", parse->state));
+					errdetail("Unexpected end state has been found: %d.", parse->state));
 			return NULL;
 	}
 
@@ -655,7 +655,7 @@ build_mvndistinct(NDistinctParseState *parse, char *str)
 				errsave(parse->escontext,
 						errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 						errmsg("malformed pg_ndistinct: \"%s\"", str),
-						errdetail("Duplicated \"%s\" array found: [%s]",
+						errdetail("Duplicated \"%s\" array has been found: [%s]",
 								  PG_NDISTINCT_KEY_ATTRIBUTES, s));
 				pfree(s);
 				return NULL;
@@ -705,7 +705,7 @@ build_mvndistinct(NDistinctParseState *parse, char *str)
 			errsave(parse->escontext,
 					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 					errmsg("malformed pg_ndistinct: \"%s\"", str),
-					errdetail("\"%s\" array: [%s] must be a subset of array: [%s]",
+					errdetail("\"%s\" array [%s] must be a subset of array [%s].",
 							  PG_NDISTINCT_KEY_ATTRIBUTES,
 							  item_list, refitem_list));
 			pfree(item_list);
@@ -784,7 +784,7 @@ pg_ndistinct_in(PG_FUNCTION_ARGS)
 		errsave(parse_state.escontext,
 				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 				errmsg("malformed pg_ndistinct: \"%s\"", str),
-				errdetail("Must be valid JSON."));
+				errdetail("Input data must be valid JSON."));
 
 	PG_RETURN_NULL();
 }
diff --git a/src/test/regress/expected/pg_dependencies.out b/src/test/regress/expected/pg_dependencies.out
index 5c6fe6675173..04641f81d13f 100644
--- a/src/test/regress/expected/pg_dependencies.out
+++ b/src/test/regress/expected/pg_dependencies.out
@@ -4,7 +4,7 @@ SELECT 'null'::pg_dependencies;
 ERROR:  malformed pg_dependencies: "null"
 LINE 1: SELECT 'null'::pg_dependencies;
                ^
-DETAIL:  Unexpected scalar.
+DETAIL:  Unexpected scalar has been found.
 SELECT '{"a": 1}'::pg_dependencies;
 ERROR:  malformed pg_dependencies: "{"a": 1}"
 LINE 1: SELECT '{"a": 1}'::pg_dependencies;
@@ -26,9 +26,9 @@ LINE 1: SELECT '[null]'::pg_dependencies;
                ^
 DETAIL:  Item list elements cannot be null.
 SELECT * FROM pg_input_error_info('null', 'pg_dependencies');
-              message              |       detail       | hint | sql_error_code 
------------------------------------+--------------------+------+----------------
- malformed pg_dependencies: "null" | Unexpected scalar. |      | 22P02
+              message              |              detail               | hint | sql_error_code 
+-----------------------------------+-----------------------------------+------+----------------
+ malformed pg_dependencies: "null" | Unexpected scalar has been found. |      | 22P02
 (1 row)
 
 SELECT * FROM pg_input_error_info('{"a": 1}', 'pg_dependencies');
@@ -129,44 +129,44 @@ SELECT '[{"attributes" : ["\ud83d",3], "dependency" : 4, "degree": 0.250}]'::pg_
 ERROR:  malformed pg_dependencies: "[{"attributes" : ["\ud83d",3], "dependency" : 4, "degree": 0.250}]"
 LINE 1: SELECT '[{"attributes" : ["\ud83d",3], "dependency" : 4, "de...
                ^
-DETAIL:  Must be valid JSON.
+DETAIL:  Input data must be valid JSON.
 SELECT '[{"attributes" : [2,3], "dependency" : "\ud83d", "degree": 0.250}]'::pg_dependencies;
 ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : "\ud83d", "degree": 0.250}]"
 LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : "\ud83d", "de...
                ^
-DETAIL:  Must be valid JSON.
+DETAIL:  Input data must be valid JSON.
 SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": "\ud83d"}]'::pg_dependencies;
 ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4, "degree": "\ud83d"}]"
 LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": ...
                ^
-DETAIL:  Must be valid JSON.
+DETAIL:  Input data must be valid JSON.
 SELECT '[{"\ud83d" : [2,3], "dependency" : 4, "degree": 0.250}]'::pg_dependencies;
 ERROR:  malformed pg_dependencies: "[{"\ud83d" : [2,3], "dependency" : 4, "degree": 0.250}]"
 LINE 1: SELECT '[{"\ud83d" : [2,3], "dependency" : 4, "degree": 0.25...
                ^
-DETAIL:  Must be valid JSON.
+DETAIL:  Input data must be valid JSON.
 SELECT * FROM pg_input_error_info('[{"attributes" : ["\ud83d",3], "dependency" : 4, "degree": 0.250}]', 'pg_dependencies');
-                                             message                                             |       detail        | hint | sql_error_code 
--------------------------------------------------------------------------------------------------+---------------------+------+----------------
- malformed pg_dependencies: "[{"attributes" : ["\ud83d",3], "dependency" : 4, "degree": 0.250}]" | Must be valid JSON. |      | 22P02
+                                             message                                             |             detail             | hint | sql_error_code 
+-------------------------------------------------------------------------------------------------+--------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : ["\ud83d",3], "dependency" : 4, "degree": 0.250}]" | Input data must be valid JSON. |      | 22P02
 (1 row)
 
 SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : "\ud83d", "degree": 0.250}]', 'pg_dependencies');
-                                             message                                             |       detail        | hint | sql_error_code 
--------------------------------------------------------------------------------------------------+---------------------+------+----------------
- malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : "\ud83d", "degree": 0.250}]" | Must be valid JSON. |      | 22P02
+                                             message                                             |             detail             | hint | sql_error_code 
+-------------------------------------------------------------------------------------------------+--------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : "\ud83d", "degree": 0.250}]" | Input data must be valid JSON. |      | 22P02
 (1 row)
 
 SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : 4, "degree": "\ud83d"}]', 'pg_dependencies');
-                                           message                                           |       detail        | hint | sql_error_code 
----------------------------------------------------------------------------------------------+---------------------+------+----------------
- malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4, "degree": "\ud83d"}]" | Must be valid JSON. |      | 22P02
+                                           message                                           |             detail             | hint | sql_error_code 
+---------------------------------------------------------------------------------------------+--------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4, "degree": "\ud83d"}]" | Input data must be valid JSON. |      | 22P02
 (1 row)
 
 SELECT * FROM pg_input_error_info('[{"\ud83d" : [2,3], "dependency" : 4, "degree": 0.250}]', 'pg_dependencies');
-                                       message                                        |       detail        | hint | sql_error_code 
---------------------------------------------------------------------------------------+---------------------+------+----------------
- malformed pg_dependencies: "[{"\ud83d" : [2,3], "dependency" : 4, "degree": 0.250}]" | Must be valid JSON. |      | 22P02
+                                       message                                        |             detail             | hint | sql_error_code 
+--------------------------------------------------------------------------------------+--------------------------------+------+----------------
+ malformed pg_dependencies: "[{"\ud83d" : [2,3], "dependency" : 4, "degree": 0.250}]" | Input data must be valid JSON. |      | 22P02
 (1 row)
 
 -- Valid keys, invalid values
@@ -174,7 +174,7 @@ SELECT '[{"attributes" : null, "dependency" : 4, "degree": 1.000}]'::pg_dependen
 ERROR:  malformed pg_dependencies: "[{"attributes" : null, "dependency" : 4, "degree": 1.000}]"
 LINE 1: SELECT '[{"attributes" : null, "dependency" : 4, "degree": 1...
                ^
-DETAIL:  Unexpected scalar.
+DETAIL:  Unexpected scalar has been found.
 SELECT '[{"attributes" : [2,null], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
 ERROR:  malformed pg_dependencies: "[{"attributes" : [2,null], "dependency" : 4, "degree": 1.000}]"
 LINE 1: SELECT '[{"attributes" : [2,null], "dependency" : 4, "degree...
@@ -184,51 +184,51 @@ SELECT '[{"attributes" : [2,3], "dependency" : null, "degree": 1.000}]'::pg_depe
 ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : null, "degree": 1.000}]"
 LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : null, "degree...
                ^
-DETAIL:  Invalid "dependency" value.
+DETAIL:  Key "dependency" has an incorrect value.
 SELECT '[{"attributes" : [2,"a"], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
 ERROR:  malformed pg_dependencies: "[{"attributes" : [2,"a"], "dependency" : 4, "degree": 1.000}]"
 LINE 1: SELECT '[{"attributes" : [2,"a"], "dependency" : 4, "degree"...
                ^
-DETAIL:  Invalid "attributes" value.
+DETAIL:  Key "attributes" has an incorrect value.
 SELECT '[{"attributes" : [2,3], "dependency" : "a", "degree": 1.000}]'::pg_dependencies;
 ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : "a", "degree": 1.000}]"
 LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : "a", "degree"...
                ^
-DETAIL:  Invalid "dependency" value.
+DETAIL:  Key "dependency" has an incorrect value.
 SELECT '[{"attributes" : [2,3], "dependency" : [], "degree": 1.000}]'::pg_dependencies;
 ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : [], "degree": 1.000}]"
 LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : [], "degree":...
                ^
-DETAIL:  Array found in unexpected place.
+DETAIL:  Array has been found at an unexpected location.
 SELECT '[{"attributes" : [2,3], "dependency" : [null], "degree": 1.000}]'::pg_dependencies;
 ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : [null], "degree": 1.000}]"
 LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : [null], "degr...
                ^
-DETAIL:  Array found in unexpected place.
+DETAIL:  Array has been found at an unexpected location.
 SELECT '[{"attributes" : [2,3], "dependency" : [1,null], "degree": 1.000}]'::pg_dependencies;
 ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : [1,null], "degree": 1.000}]"
 LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : [1,null], "de...
                ^
-DETAIL:  Array found in unexpected place.
+DETAIL:  Array has been found at an unexpected location.
 SELECT '[{"attributes" : 1, "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
 ERROR:  malformed pg_dependencies: "[{"attributes" : 1, "dependency" : 4, "degree": 1.000}]"
 LINE 1: SELECT '[{"attributes" : 1, "dependency" : 4, "degree": 1.00...
                ^
-DETAIL:  Unexpected scalar.
+DETAIL:  Unexpected scalar has been found.
 SELECT '[{"attributes" : "a", "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
 ERROR:  malformed pg_dependencies: "[{"attributes" : "a", "dependency" : 4, "degree": 1.000}]"
 LINE 1: SELECT '[{"attributes" : "a", "dependency" : 4, "degree": 1....
                ^
-DETAIL:  Unexpected scalar.
+DETAIL:  Unexpected scalar has been found.
 SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": NaN}]'::pg_dependencies;
 ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4, "degree": NaN}]"
 LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": ...
                ^
-DETAIL:  Must be valid JSON.
+DETAIL:  Input data must be valid JSON.
 SELECT * FROM pg_input_error_info('[{"attributes" : null, "dependency" : 4, "degree": 1.000}]', 'pg_dependencies');
-                                         message                                         |       detail       | hint | sql_error_code 
------------------------------------------------------------------------------------------+--------------------+------+----------------
- malformed pg_dependencies: "[{"attributes" : null, "dependency" : 4, "degree": 1.000}]" | Unexpected scalar. |      | 22P02
+                                         message                                         |              detail               | hint | sql_error_code 
+-----------------------------------------------------------------------------------------+-----------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : null, "dependency" : 4, "degree": 1.000}]" | Unexpected scalar has been found. |      | 22P02
 (1 row)
 
 SELECT * FROM pg_input_error_info('[{"attributes" : [2,null], "dependency" : 4, "degree": 1.000}]', 'pg_dependencies');
@@ -238,57 +238,57 @@ SELECT * FROM pg_input_error_info('[{"attributes" : [2,null], "dependency" : 4,
 (1 row)
 
 SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : null, "degree": 1.000}]', 'pg_dependencies');
-                                           message                                           |           detail            | hint | sql_error_code 
----------------------------------------------------------------------------------------------+-----------------------------+------+----------------
- malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : null, "degree": 1.000}]" | Invalid "dependency" value. |      | 22P02
+                                           message                                           |                  detail                  | hint | sql_error_code 
+---------------------------------------------------------------------------------------------+------------------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : null, "degree": 1.000}]" | Key "dependency" has an incorrect value. |      | 22P02
 (1 row)
 
 SELECT * FROM pg_input_error_info('[{"attributes" : [2,"a"], "dependency" : 4, "degree": 1.000}]', 'pg_dependencies');
-                                          message                                           |           detail            | hint | sql_error_code 
---------------------------------------------------------------------------------------------+-----------------------------+------+----------------
- malformed pg_dependencies: "[{"attributes" : [2,"a"], "dependency" : 4, "degree": 1.000}]" | Invalid "attributes" value. |      | 22P02
+                                          message                                           |                  detail                  | hint | sql_error_code 
+--------------------------------------------------------------------------------------------+------------------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [2,"a"], "dependency" : 4, "degree": 1.000}]" | Key "attributes" has an incorrect value. |      | 22P02
 (1 row)
 
 SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : "a", "degree": 1.000}]', 'pg_dependencies');
-                                          message                                           |           detail            | hint | sql_error_code 
---------------------------------------------------------------------------------------------+-----------------------------+------+----------------
- malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : "a", "degree": 1.000}]" | Invalid "dependency" value. |      | 22P02
+                                          message                                           |                  detail                  | hint | sql_error_code 
+--------------------------------------------------------------------------------------------+------------------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : "a", "degree": 1.000}]" | Key "dependency" has an incorrect value. |      | 22P02
 (1 row)
 
 SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : [], "degree": 1.000}]', 'pg_dependencies');
-                                          message                                          |              detail              | hint | sql_error_code 
--------------------------------------------------------------------------------------------+----------------------------------+------+----------------
- malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : [], "degree": 1.000}]" | Array found in unexpected place. |      | 22P02
+                                          message                                          |                     detail                      | hint | sql_error_code 
+-------------------------------------------------------------------------------------------+-------------------------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : [], "degree": 1.000}]" | Array has been found at an unexpected location. |      | 22P02
 (1 row)
 
 SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : [null], "degree": 1.000}]', 'pg_dependencies');
-                                            message                                            |              detail              | hint | sql_error_code 
------------------------------------------------------------------------------------------------+----------------------------------+------+----------------
- malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : [null], "degree": 1.000}]" | Array found in unexpected place. |      | 22P02
+                                            message                                            |                     detail                      | hint | sql_error_code 
+-----------------------------------------------------------------------------------------------+-------------------------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : [null], "degree": 1.000}]" | Array has been found at an unexpected location. |      | 22P02
 (1 row)
 
 SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : [1,null], "degree": 1.000}]', 'pg_dependencies');
-                                             message                                             |              detail              | hint | sql_error_code 
--------------------------------------------------------------------------------------------------+----------------------------------+------+----------------
- malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : [1,null], "degree": 1.000}]" | Array found in unexpected place. |      | 22P02
+                                             message                                             |                     detail                      | hint | sql_error_code 
+-------------------------------------------------------------------------------------------------+-------------------------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : [1,null], "degree": 1.000}]" | Array has been found at an unexpected location. |      | 22P02
 (1 row)
 
 SELECT * FROM pg_input_error_info('[{"attributes" : 1, "dependency" : 4, "degree": 1.000}]', 'pg_dependencies');
-                                       message                                        |       detail       | hint | sql_error_code 
---------------------------------------------------------------------------------------+--------------------+------+----------------
- malformed pg_dependencies: "[{"attributes" : 1, "dependency" : 4, "degree": 1.000}]" | Unexpected scalar. |      | 22P02
+                                       message                                        |              detail               | hint | sql_error_code 
+--------------------------------------------------------------------------------------+-----------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : 1, "dependency" : 4, "degree": 1.000}]" | Unexpected scalar has been found. |      | 22P02
 (1 row)
 
 SELECT * FROM pg_input_error_info('[{"attributes" : "a", "dependency" : 4, "degree": 1.000}]', 'pg_dependencies');
-                                        message                                         |       detail       | hint | sql_error_code 
-----------------------------------------------------------------------------------------+--------------------+------+----------------
- malformed pg_dependencies: "[{"attributes" : "a", "dependency" : 4, "degree": 1.000}]" | Unexpected scalar. |      | 22P02
+                                        message                                         |              detail               | hint | sql_error_code 
+----------------------------------------------------------------------------------------+-----------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : "a", "dependency" : 4, "degree": 1.000}]" | Unexpected scalar has been found. |      | 22P02
 (1 row)
 
 SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : 4, "degree": NaN}]', 'pg_dependencies');
-                                        message                                         |       detail        | hint | sql_error_code 
-----------------------------------------------------------------------------------------+---------------------+------+----------------
- malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4, "degree": NaN}]" | Must be valid JSON. |      | 22P02
+                                        message                                         |             detail             | hint | sql_error_code 
+----------------------------------------------------------------------------------------+--------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4, "degree": NaN}]" | Input data must be valid JSON. |      | 22P02
 (1 row)
 
 SELECT '[{"attributes": [], "dependency": 2, "degree": 1}]' ::pg_dependencies;
@@ -317,22 +317,22 @@ SELECT '[{"dependency" : 4, "degree": "1.2"}]'::pg_dependencies;
 ERROR:  malformed pg_dependencies: "[{"dependency" : 4, "degree": "1.2"}]"
 LINE 1: SELECT '[{"dependency" : 4, "degree": "1.2"}]'::pg_dependenc...
                ^
-DETAIL:  Item must contain "attributes" key
+DETAIL:  Item must contain "attributes" key.
 SELECT '[{"attributes" : [1,2,3,4,5,6,7], "dependency" : 0, "degree": "1.2"}]'::pg_dependencies;
 ERROR:  malformed pg_dependencies: "[{"attributes" : [1,2,3,4,5,6,7], "dependency" : 0, "degree": "1.2"}]"
 LINE 1: SELECT '[{"attributes" : [1,2,3,4,5,6,7], "dependency" : 0, ...
                ^
-DETAIL:  Invalid "dependency" value: 0.
+DETAIL:  Key "dependency" has an incorrect value: 0.
 SELECT '[{"attributes" : [1,2,3,4,5,6,7], "dependency" : -9, "degree": "1.2"}]'::pg_dependencies;
 ERROR:  malformed pg_dependencies: "[{"attributes" : [1,2,3,4,5,6,7], "dependency" : -9, "degree": "1.2"}]"
 LINE 1: SELECT '[{"attributes" : [1,2,3,4,5,6,7], "dependency" : -9,...
                ^
-DETAIL:  Invalid "dependency" value: -9.
+DETAIL:  Key "dependency" has an incorrect value: -9.
 SELECT '[{"attributes": [1,2], "dependency": 2, "degree": 1}]' ::pg_dependencies;
 ERROR:  malformed pg_dependencies: "[{"attributes": [1,2], "dependency": 2, "degree": 1}]"
 LINE 1: SELECT '[{"attributes": [1,2], "dependency": 2, "degree": 1}...
                ^
-DETAIL:  Item "dependency" value 2 found in the "attributes" list.
+DETAIL:  Item "dependency" with value 2 has been found in the "attributes" list.
 SELECT '[{"attributes" : [1, {}], "dependency" : 1, "degree": "1.2"}]'::pg_dependencies;
 ERROR:  malformed pg_dependencies: "[{"attributes" : [1, {}], "dependency" : 1, "degree": "1.2"}]"
 LINE 1: SELECT '[{"attributes" : [1, {}], "dependency" : 1, "degree"...
@@ -352,29 +352,29 @@ SELECT '[{"attributes" : [1,2], "dependency" : 1, "degree": "a"}]'::pg_dependenc
 ERROR:  malformed pg_dependencies: "[{"attributes" : [1,2], "dependency" : 1, "degree": "a"}]"
 LINE 1: SELECT '[{"attributes" : [1,2], "dependency" : 1, "degree": ...
                ^
-DETAIL:  Invalid "degree" value.
+DETAIL:  Key "degree" has an incorrect value.
 SELECT * FROM pg_input_error_info('[{"dependency" : 4, "degree": "1.2"}]', 'pg_dependencies');
-                              message                               |               detail               | hint | sql_error_code 
---------------------------------------------------------------------+------------------------------------+------+----------------
- malformed pg_dependencies: "[{"dependency" : 4, "degree": "1.2"}]" | Item must contain "attributes" key |      | 22P02
+                              message                               |               detail                | hint | sql_error_code 
+--------------------------------------------------------------------+-------------------------------------+------+----------------
+ malformed pg_dependencies: "[{"dependency" : 4, "degree": "1.2"}]" | Item must contain "attributes" key. |      | 22P02
 (1 row)
 
 SELECT * FROM pg_input_error_info('[{"attributes" : [1,2,3,4,5,6,7], "dependency" : 0, "degree": "1.2"}]', 'pg_dependencies');
-                                              message                                               |             detail             | hint | sql_error_code 
-----------------------------------------------------------------------------------------------------+--------------------------------+------+----------------
- malformed pg_dependencies: "[{"attributes" : [1,2,3,4,5,6,7], "dependency" : 0, "degree": "1.2"}]" | Invalid "dependency" value: 0. |      | 22P02
+                                              message                                               |                   detail                    | hint | sql_error_code 
+----------------------------------------------------------------------------------------------------+---------------------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [1,2,3,4,5,6,7], "dependency" : 0, "degree": "1.2"}]" | Key "dependency" has an incorrect value: 0. |      | 22P02
 (1 row)
 
 SELECT * FROM pg_input_error_info('[{"attributes" : [1,2,3,4,5,6,7], "dependency" : -9, "degree": "1.2"}]', 'pg_dependencies');
-                                               message                                               |             detail              | hint | sql_error_code 
------------------------------------------------------------------------------------------------------+---------------------------------+------+----------------
- malformed pg_dependencies: "[{"attributes" : [1,2,3,4,5,6,7], "dependency" : -9, "degree": "1.2"}]" | Invalid "dependency" value: -9. |      | 22P02
+                                               message                                               |                    detail                    | hint | sql_error_code 
+-----------------------------------------------------------------------------------------------------+----------------------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [1,2,3,4,5,6,7], "dependency" : -9, "degree": "1.2"}]" | Key "dependency" has an incorrect value: -9. |      | 22P02
 (1 row)
 
 SELECT * FROM pg_input_error_info('[{"attributes": [1,2], "dependency": 2, "degree": 1}]' , 'pg_dependencies');
-                                      message                                       |                          detail                           | hint | sql_error_code 
-------------------------------------------------------------------------------------+-----------------------------------------------------------+------+----------------
- malformed pg_dependencies: "[{"attributes": [1,2], "dependency": 2, "degree": 1}]" | Item "dependency" value 2 found in the "attributes" list. |      | 22P02
+                                      message                                       |                                 detail                                  | hint | sql_error_code 
+------------------------------------------------------------------------------------+-------------------------------------------------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes": [1,2], "dependency": 2, "degree": 1}]" | Item "dependency" with value 2 has been found in the "attributes" list. |      | 22P02
 (1 row)
 
 SELECT * FROM pg_input_error_info('[{"attributes" : [1, {}], "dependency" : 1, "degree": "1.2"}]', 'pg_dependencies');
@@ -396,9 +396,9 @@ SELECT * FROM pg_input_error_info('[{"attributes" : [1,2], "dependency" : 3, "de
 (1 row)
 
 SELECT * FROM pg_input_error_info('[{"attributes" : [1,2], "dependency" : 1, "degree": "a"}]', 'pg_dependencies');
-                                        message                                         |         detail          | hint | sql_error_code 
-----------------------------------------------------------------------------------------+-------------------------+------+----------------
- malformed pg_dependencies: "[{"attributes" : [1,2], "dependency" : 1, "degree": "a"}]" | Invalid "degree" value. |      | 22P02
+                                        message                                         |                detail                | hint | sql_error_code 
+----------------------------------------------------------------------------------------+--------------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [1,2], "dependency" : 1, "degree": "a"}]" | Key "degree" has an incorrect value. |      | 22P02
 (1 row)
 
 -- Funky degree values, which do not fail.
@@ -422,7 +422,7 @@ SELECT '[{"attributes" : [2], "dependency" : 4, "degree": "inf"}]'::pg_dependenc
 
 SELECT '[{"attributes" : [2], "dependency" : 4, "degree": "-inf"}]'::pg_dependencies::text::pg_dependencies;
 ERROR:  malformed pg_dependencies: "[{"attributes": [2], "dependency": 4, "degree": -Infinity}]"
-DETAIL:  Must be valid JSON.
+DETAIL:  Input data must be valid JSON.
 -- Duplicated keys
 SELECT '[{"attributes" : [2,3], "attributes": [1,2], "dependency" : 4, "degree": 1.000}]'::pg_dependencies;
 ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "attributes": [1,2], "dependency" : 4, "degree": 1.000}]"
@@ -462,22 +462,22 @@ SELECT '[{"attributes" : [0,2], "dependency" : 4, "degree": 0.500}]'::pg_depende
 ERROR:  malformed pg_dependencies: "[{"attributes" : [0,2], "dependency" : 4, "degree": 0.500}]"
 LINE 1: SELECT '[{"attributes" : [0,2], "dependency" : 4, "degree": ...
                ^
-DETAIL:  Invalid "attributes" element: 0.
+DETAIL:  Invalid "attributes" element has been found: 0.
 SELECT '[{"attributes" : [-7,-9], "dependency" : 4, "degree": 0.500}]'::pg_dependencies;
 ERROR:  malformed pg_dependencies: "[{"attributes" : [-7,-9], "dependency" : 4, "degree": 0.500}]"
 LINE 1: SELECT '[{"attributes" : [-7,-9], "dependency" : 4, "degree"...
                ^
-DETAIL:  Invalid "attributes" element: -9.
+DETAIL:  Invalid "attributes" element has been found: -9.
 SELECT * FROM pg_input_error_info('[{"attributes" : [0,2], "dependency" : 4, "degree": 0.500}]', 'pg_dependencies');
-                                         message                                          |              detail              | hint | sql_error_code 
-------------------------------------------------------------------------------------------+----------------------------------+------+----------------
- malformed pg_dependencies: "[{"attributes" : [0,2], "dependency" : 4, "degree": 0.500}]" | Invalid "attributes" element: 0. |      | 22P02
+                                         message                                          |                     detail                      | hint | sql_error_code 
+------------------------------------------------------------------------------------------+-------------------------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [0,2], "dependency" : 4, "degree": 0.500}]" | Invalid "attributes" element has been found: 0. |      | 22P02
 (1 row)
 
 SELECT * FROM pg_input_error_info('[{"attributes" : [-7,-9], "dependency" : 4, "degree": 0.500}]', 'pg_dependencies');
-                                          message                                           |              detail               | hint | sql_error_code 
---------------------------------------------------------------------------------------------+-----------------------------------+------+----------------
- malformed pg_dependencies: "[{"attributes" : [-7,-9], "dependency" : 4, "degree": 0.500}]" | Invalid "attributes" element: -9. |      | 22P02
+                                          message                                           |                      detail                      | hint | sql_error_code 
+--------------------------------------------------------------------------------------------+--------------------------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [-7,-9], "dependency" : 4, "degree": 0.500}]" | Invalid "attributes" element has been found: -9. |      | 22P02
 (1 row)
 
 -- Duplicated attributes
@@ -485,11 +485,11 @@ SELECT '[{"attributes" : [2,2], "dependency" : 4, "degree": 0.500}]'::pg_depende
 ERROR:  malformed pg_dependencies: "[{"attributes" : [2,2], "dependency" : 4, "degree": 0.500}]"
 LINE 1: SELECT '[{"attributes" : [2,2], "dependency" : 4, "degree": ...
                ^
-DETAIL:  Invalid "attributes" element: 2 cannot follow 2.
+DETAIL:  Invalid "attributes" element has been found: 2 cannot follow 2.
 SELECT * FROM pg_input_error_info('[{"attributes" : [2,2], "dependency" : 4, "degree": 0.500}]', 'pg_dependencies');
-                                         message                                          |                      detail                      | hint | sql_error_code 
-------------------------------------------------------------------------------------------+--------------------------------------------------+------+----------------
- malformed pg_dependencies: "[{"attributes" : [2,2], "dependency" : 4, "degree": 0.500}]" | Invalid "attributes" element: 2 cannot follow 2. |      | 22P02
+                                         message                                          |                             detail                              | hint | sql_error_code 
+------------------------------------------------------------------------------------------+-----------------------------------------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [2,2], "dependency" : 4, "degree": 0.500}]" | Invalid "attributes" element has been found: 2 cannot follow 2. |      | 22P02
 (1 row)
 
 -- Duplicated attribute lists.
@@ -499,13 +499,13 @@ ERROR:  malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4, "d
          {"attributes" : [2,3], "dependency" : 4, "degree": 1.000}]"
 LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": ...
                ^
-DETAIL:  Duplicate "attributes" array: [2, 3] with "dependency": 4.
+DETAIL:  Duplicated "attributes" array has been found: [2, 3] for key "dependency" and value 4.
 SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : 4, "degree": 1.000},
          {"attributes" : [2,3], "dependency" : 4, "degree": 1.000}]', 'pg_dependencies');
-                                         message                                         |                           detail                           | hint | sql_error_code 
------------------------------------------------------------------------------------------+------------------------------------------------------------+------+----------------
- malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4, "degree": 1.000},+| Duplicate "attributes" array: [2, 3] with "dependency": 4. |      | 22P02
-          {"attributes" : [2,3], "dependency" : 4, "degree": 1.000}]"                    |                                                            |      | 
+                                         message                                         |                                         detail                                         | hint | sql_error_code 
+-----------------------------------------------------------------------------------------+----------------------------------------------------------------------------------------+------+----------------
+ malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4, "degree": 1.000},+| Duplicated "attributes" array has been found: [2, 3] for key "dependency" and value 4. |      | 22P02
+          {"attributes" : [2,3], "dependency" : 4, "degree": 1.000}]"                    |                                                                                        |      | 
 (1 row)
 
 -- Valid inputs
diff --git a/src/test/regress/expected/pg_ndistinct.out b/src/test/regress/expected/pg_ndistinct.out
index 736954b9301c..e03cacd25c24 100644
--- a/src/test/regress/expected/pg_ndistinct.out
+++ b/src/test/regress/expected/pg_ndistinct.out
@@ -4,7 +4,7 @@ SELECT 'null'::pg_ndistinct;
 ERROR:  malformed pg_ndistinct: "null"
 LINE 1: SELECT 'null'::pg_ndistinct;
                ^
-DETAIL:  Unexpected scalar.
+DETAIL:  Unexpected scalar has been found.
 SELECT '{"a": 1}'::pg_ndistinct;
 ERROR:  malformed pg_ndistinct: "{"a": 1}"
 LINE 1: SELECT '{"a": 1}'::pg_ndistinct;
@@ -26,9 +26,9 @@ LINE 1: SELECT '[null]'::pg_ndistinct;
                ^
 DETAIL:  Item list elements cannot be null.
 SELECT * FROM pg_input_error_info('null', 'pg_ndistinct');
-            message             |       detail       | hint | sql_error_code 
---------------------------------+--------------------+------+----------------
- malformed pg_ndistinct: "null" | Unexpected scalar. |      | 22P02
+            message             |              detail               | hint | sql_error_code 
+--------------------------------+-----------------------------------+------+----------------
+ malformed pg_ndistinct: "null" | Unexpected scalar has been found. |      | 22P02
 (1 row)
 
 SELECT * FROM pg_input_error_info('{"a": 1}', 'pg_ndistinct');
@@ -140,44 +140,44 @@ SELECT '[{"\ud83d" : [1, 2], "ndistinct" : 4}]'::pg_ndistinct;
 ERROR:  malformed pg_ndistinct: "[{"\ud83d" : [1, 2], "ndistinct" : 4}]"
 LINE 1: SELECT '[{"\ud83d" : [1, 2], "ndistinct" : 4}]'::pg_ndistinc...
                ^
-DETAIL:  Must be valid JSON.
+DETAIL:  Input data must be valid JSON.
 SELECT '[{"attributes" : [1, 2], "\ud83d" : 4}]'::pg_ndistinct;
 ERROR:  malformed pg_ndistinct: "[{"attributes" : [1, 2], "\ud83d" : 4}]"
 LINE 1: SELECT '[{"attributes" : [1, 2], "\ud83d" : 4}]'::pg_ndistin...
                ^
-DETAIL:  Must be valid JSON.
+DETAIL:  Input data must be valid JSON.
 SELECT '[{"attributes" : [1, 2], "ndistinct" : "\ud83d"}]'::pg_ndistinct;
 ERROR:  malformed pg_ndistinct: "[{"attributes" : [1, 2], "ndistinct" : "\ud83d"}]"
 LINE 1: SELECT '[{"attributes" : [1, 2], "ndistinct" : "\ud83d"}]'::...
                ^
-DETAIL:  Must be valid JSON.
+DETAIL:  Input data must be valid JSON.
 SELECT '[{"attributes" : ["\ud83d", 2], "ndistinct" : 1}]'::pg_ndistinct;
 ERROR:  malformed pg_ndistinct: "[{"attributes" : ["\ud83d", 2], "ndistinct" : 1}]"
 LINE 1: SELECT '[{"attributes" : ["\ud83d", 2], "ndistinct" : 1}]'::...
                ^
-DETAIL:  Must be valid JSON.
+DETAIL:  Input data must be valid JSON.
 SELECT * FROM pg_input_error_info('[{"\ud83d" : [1, 2], "ndistinct" : 4}]', 'pg_ndistinct');
-                             message                              |       detail        | hint | sql_error_code 
-------------------------------------------------------------------+---------------------+------+----------------
- malformed pg_ndistinct: "[{"\ud83d" : [1, 2], "ndistinct" : 4}]" | Must be valid JSON. |      | 22P02
+                             message                              |             detail             | hint | sql_error_code 
+------------------------------------------------------------------+--------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"\ud83d" : [1, 2], "ndistinct" : 4}]" | Input data must be valid JSON. |      | 22P02
 (1 row)
 
 SELECT * FROM pg_input_error_info('[{"attributes" : [1, 2], "\ud83d" : 4}]', 'pg_ndistinct');
-                              message                              |       detail        | hint | sql_error_code 
--------------------------------------------------------------------+---------------------+------+----------------
- malformed pg_ndistinct: "[{"attributes" : [1, 2], "\ud83d" : 4}]" | Must be valid JSON. |      | 22P02
+                              message                              |             detail             | hint | sql_error_code 
+-------------------------------------------------------------------+--------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [1, 2], "\ud83d" : 4}]" | Input data must be valid JSON. |      | 22P02
 (1 row)
 
 SELECT * FROM pg_input_error_info('[{"attributes" : [1, 2], "ndistinct" : "\ud83d"}]', 'pg_ndistinct');
-                                   message                                   |       detail        | hint | sql_error_code 
------------------------------------------------------------------------------+---------------------+------+----------------
- malformed pg_ndistinct: "[{"attributes" : [1, 2], "ndistinct" : "\ud83d"}]" | Must be valid JSON. |      | 22P02
+                                   message                                   |             detail             | hint | sql_error_code 
+-----------------------------------------------------------------------------+--------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [1, 2], "ndistinct" : "\ud83d"}]" | Input data must be valid JSON. |      | 22P02
 (1 row)
 
 SELECT * FROM pg_input_error_info('[{"attributes" : ["\ud83d", 2], "ndistinct" : 1}]', 'pg_ndistinct');
-                                   message                                   |       detail        | hint | sql_error_code 
------------------------------------------------------------------------------+---------------------+------+----------------
- malformed pg_ndistinct: "[{"attributes" : ["\ud83d", 2], "ndistinct" : 1}]" | Must be valid JSON. |      | 22P02
+                                   message                                   |             detail             | hint | sql_error_code 
+-----------------------------------------------------------------------------+--------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : ["\ud83d", 2], "ndistinct" : 1}]" | Input data must be valid JSON. |      | 22P02
 (1 row)
 
 -- Valid keys, invalid values
@@ -185,7 +185,7 @@ SELECT '[{"attributes" : null, "ndistinct" : 4}]'::pg_ndistinct;
 ERROR:  malformed pg_ndistinct: "[{"attributes" : null, "ndistinct" : 4}]"
 LINE 1: SELECT '[{"attributes" : null, "ndistinct" : 4}]'::pg_ndisti...
                ^
-DETAIL:  Unexpected scalar.
+DETAIL:  Unexpected scalar has been found.
 SELECT '[{"attributes" : [], "ndistinct" : 1}]'::pg_ndistinct;
 ERROR:  malformed pg_ndistinct: "[{"attributes" : [], "ndistinct" : 1}]"
 LINE 1: SELECT '[{"attributes" : [], "ndistinct" : 1}]'::pg_ndistinc...
@@ -205,32 +205,32 @@ SELECT '[{"attributes" : [2,3], "ndistinct" : null}]'::pg_ndistinct;
 ERROR:  malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : null}]"
 LINE 1: SELECT '[{"attributes" : [2,3], "ndistinct" : null}]'::pg_nd...
                ^
-DETAIL:  Invalid "ndistinct" value.
+DETAIL:  Key "ndistinct" has an incorrect value.
 SELECT '[{"attributes" : [2,"a"], "ndistinct" : 4}]'::pg_ndistinct;
 ERROR:  malformed pg_ndistinct: "[{"attributes" : [2,"a"], "ndistinct" : 4}]"
 LINE 1: SELECT '[{"attributes" : [2,"a"], "ndistinct" : 4}]'::pg_ndi...
                ^
-DETAIL:  Invalid "attributes" value.
+DETAIL:  Key "attributes" has an incorrect value.
 SELECT '[{"attributes" : [2,3], "ndistinct" : "a"}]'::pg_ndistinct;
 ERROR:  malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : "a"}]"
 LINE 1: SELECT '[{"attributes" : [2,3], "ndistinct" : "a"}]'::pg_ndi...
                ^
-DETAIL:  Invalid "ndistinct" value.
+DETAIL:  Key "ndistinct" has an incorrect value.
 SELECT '[{"attributes" : [2,3], "ndistinct" : []}]'::pg_ndistinct;
 ERROR:  malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : []}]"
 LINE 1: SELECT '[{"attributes" : [2,3], "ndistinct" : []}]'::pg_ndis...
                ^
-DETAIL:  Array found in unexpected place.
+DETAIL:  Array has been found at an unexpected location.
 SELECT '[{"attributes" : [2,3], "ndistinct" : [null]}]'::pg_ndistinct;
 ERROR:  malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : [null]}]"
 LINE 1: SELECT '[{"attributes" : [2,3], "ndistinct" : [null]}]'::pg_...
                ^
-DETAIL:  Array found in unexpected place.
+DETAIL:  Array has been found at an unexpected location.
 SELECT '[{"attributes" : [2,3], "ndistinct" : [1,null]}]'::pg_ndistinct;
 ERROR:  malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : [1,null]}]"
 LINE 1: SELECT '[{"attributes" : [2,3], "ndistinct" : [1,null]}]'::p...
                ^
-DETAIL:  Array found in unexpected place.
+DETAIL:  Array has been found at an unexpected location.
 SELECT '[{"attributes" : [2,3], "ndistinct" : {"a": 1}}]'::pg_ndistinct;
 ERROR:  malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : {"a": 1}}]"
 LINE 1: SELECT '[{"attributes" : [2,3], "ndistinct" : {"a": 1}}]'::p...
@@ -240,22 +240,22 @@ SELECT '[{"attributes" : [0,1], "ndistinct" : 1}]'::pg_ndistinct;
 ERROR:  malformed pg_ndistinct: "[{"attributes" : [0,1], "ndistinct" : 1}]"
 LINE 1: SELECT '[{"attributes" : [0,1], "ndistinct" : 1}]'::pg_ndist...
                ^
-DETAIL:  Invalid "attributes" element: 0.
+DETAIL:  Invalid "attributes" element has been found: 0.
 SELECT '[{"attributes" : [-7,-9], "ndistinct" : 1}]'::pg_ndistinct;
 ERROR:  malformed pg_ndistinct: "[{"attributes" : [-7,-9], "ndistinct" : 1}]"
 LINE 1: SELECT '[{"attributes" : [-7,-9], "ndistinct" : 1}]'::pg_ndi...
                ^
-DETAIL:  Invalid "attributes" element: -9.
+DETAIL:  Invalid "attributes" element has been found: -9.
 SELECT '[{"attributes" : 1, "ndistinct" : 4}]'::pg_ndistinct;
 ERROR:  malformed pg_ndistinct: "[{"attributes" : 1, "ndistinct" : 4}]"
 LINE 1: SELECT '[{"attributes" : 1, "ndistinct" : 4}]'::pg_ndistinct...
                ^
-DETAIL:  Unexpected scalar.
+DETAIL:  Unexpected scalar has been found.
 SELECT '[{"attributes" : "a", "ndistinct" : 4}]'::pg_ndistinct;
 ERROR:  malformed pg_ndistinct: "[{"attributes" : "a", "ndistinct" : 4}]"
 LINE 1: SELECT '[{"attributes" : "a", "ndistinct" : 4}]'::pg_ndistin...
                ^
-DETAIL:  Unexpected scalar.
+DETAIL:  Unexpected scalar has been found.
 SELECT '[{"attributes" : {"a": 1}, "ndistinct" : 1}]'::pg_ndistinct;
 ERROR:  malformed pg_ndistinct: "[{"attributes" : {"a": 1}, "ndistinct" : 1}]"
 LINE 1: SELECT '[{"attributes" : {"a": 1}, "ndistinct" : 1}]'::pg_nd...
@@ -267,9 +267,9 @@ LINE 1: SELECT '[{"attributes" : [1, {"a": 1}], "ndistinct" : 1}]'::...
                ^
 DETAIL:  Attribute lists can only contain attribute numbers.
 SELECT * FROM pg_input_error_info('[{"attributes" : null, "ndistinct" : 4}]', 'pg_ndistinct');
-                              message                               |       detail       | hint | sql_error_code 
---------------------------------------------------------------------+--------------------+------+----------------
- malformed pg_ndistinct: "[{"attributes" : null, "ndistinct" : 4}]" | Unexpected scalar. |      | 22P02
+                              message                               |              detail               | hint | sql_error_code 
+--------------------------------------------------------------------+-----------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : null, "ndistinct" : 4}]" | Unexpected scalar has been found. |      | 22P02
 (1 row)
 
 SELECT * FROM pg_input_error_info('[{"attributes" : [], "ndistinct" : 1}]', 'pg_ndistinct');
@@ -291,39 +291,39 @@ SELECT * FROM pg_input_error_info('[{"attributes" : [2,null], "ndistinct" : 4}]'
 (1 row)
 
 SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : null}]', 'pg_ndistinct');
-                                message                                 |           detail           | hint | sql_error_code 
-------------------------------------------------------------------------+----------------------------+------+----------------
- malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : null}]" | Invalid "ndistinct" value. |      | 22P02
+                                message                                 |                 detail                  | hint | sql_error_code 
+------------------------------------------------------------------------+-----------------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : null}]" | Key "ndistinct" has an incorrect value. |      | 22P02
 (1 row)
 
 SELECT * FROM pg_input_error_info('[{"attributes" : [2,"a"], "ndistinct" : 4}]', 'pg_ndistinct');
-                                message                                |           detail            | hint | sql_error_code 
------------------------------------------------------------------------+-----------------------------+------+----------------
- malformed pg_ndistinct: "[{"attributes" : [2,"a"], "ndistinct" : 4}]" | Invalid "attributes" value. |      | 22P02
+                                message                                |                  detail                  | hint | sql_error_code 
+-----------------------------------------------------------------------+------------------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [2,"a"], "ndistinct" : 4}]" | Key "attributes" has an incorrect value. |      | 22P02
 (1 row)
 
 SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : "a"}]', 'pg_ndistinct');
-                                message                                |           detail           | hint | sql_error_code 
------------------------------------------------------------------------+----------------------------+------+----------------
- malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : "a"}]" | Invalid "ndistinct" value. |      | 22P02
+                                message                                |                 detail                  | hint | sql_error_code 
+-----------------------------------------------------------------------+-----------------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : "a"}]" | Key "ndistinct" has an incorrect value. |      | 22P02
 (1 row)
 
 SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : []}]', 'pg_ndistinct');
-                               message                                |              detail              | hint | sql_error_code 
-----------------------------------------------------------------------+----------------------------------+------+----------------
- malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : []}]" | Array found in unexpected place. |      | 22P02
+                               message                                |                     detail                      | hint | sql_error_code 
+----------------------------------------------------------------------+-------------------------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : []}]" | Array has been found at an unexpected location. |      | 22P02
 (1 row)
 
 SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : [null]}]', 'pg_ndistinct');
-                                 message                                  |              detail              | hint | sql_error_code 
---------------------------------------------------------------------------+----------------------------------+------+----------------
- malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : [null]}]" | Array found in unexpected place. |      | 22P02
+                                 message                                  |                     detail                      | hint | sql_error_code 
+--------------------------------------------------------------------------+-------------------------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : [null]}]" | Array has been found at an unexpected location. |      | 22P02
 (1 row)
 
 SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : [1,null]}]', 'pg_ndistinct');
-                                  message                                   |              detail              | hint | sql_error_code 
-----------------------------------------------------------------------------+----------------------------------+------+----------------
- malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : [1,null]}]" | Array found in unexpected place. |      | 22P02
+                                  message                                   |                     detail                      | hint | sql_error_code 
+----------------------------------------------------------------------------+-------------------------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : [1,null]}]" | Array has been found at an unexpected location. |      | 22P02
 (1 row)
 
 SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : {"a": 1}}]', 'pg_ndistinct');
@@ -333,27 +333,27 @@ SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : {"a": 1
 (1 row)
 
 SELECT * FROM pg_input_error_info('[{"attributes" : 1, "ndistinct" : 4}]', 'pg_ndistinct');
-                             message                             |       detail       | hint | sql_error_code 
------------------------------------------------------------------+--------------------+------+----------------
- malformed pg_ndistinct: "[{"attributes" : 1, "ndistinct" : 4}]" | Unexpected scalar. |      | 22P02
+                             message                             |              detail               | hint | sql_error_code 
+-----------------------------------------------------------------+-----------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : 1, "ndistinct" : 4}]" | Unexpected scalar has been found. |      | 22P02
 (1 row)
 
 SELECT * FROM pg_input_error_info('[{"attributes" : [-7,-9], "ndistinct" : 1}]', 'pg_ndistinct');
-                                message                                |              detail               | hint | sql_error_code 
------------------------------------------------------------------------+-----------------------------------+------+----------------
- malformed pg_ndistinct: "[{"attributes" : [-7,-9], "ndistinct" : 1}]" | Invalid "attributes" element: -9. |      | 22P02
+                                message                                |                      detail                      | hint | sql_error_code 
+-----------------------------------------------------------------------+--------------------------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [-7,-9], "ndistinct" : 1}]" | Invalid "attributes" element has been found: -9. |      | 22P02
 (1 row)
 
 SELECT * FROM pg_input_error_info('[{"attributes" : 1, "ndistinct" : 4}]', 'pg_ndistinct');
-                             message                             |       detail       | hint | sql_error_code 
------------------------------------------------------------------+--------------------+------+----------------
- malformed pg_ndistinct: "[{"attributes" : 1, "ndistinct" : 4}]" | Unexpected scalar. |      | 22P02
+                             message                             |              detail               | hint | sql_error_code 
+-----------------------------------------------------------------+-----------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : 1, "ndistinct" : 4}]" | Unexpected scalar has been found. |      | 22P02
 (1 row)
 
 SELECT * FROM pg_input_error_info('[{"attributes" : "a", "ndistinct" : 4}]', 'pg_ndistinct');
-                              message                              |       detail       | hint | sql_error_code 
--------------------------------------------------------------------+--------------------+------+----------------
- malformed pg_ndistinct: "[{"attributes" : "a", "ndistinct" : 4}]" | Unexpected scalar. |      | 22P02
+                              message                              |              detail               | hint | sql_error_code 
+-------------------------------------------------------------------+-----------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : "a", "ndistinct" : 4}]" | Unexpected scalar has been found. |      | 22P02
 (1 row)
 
 SELECT * FROM pg_input_error_info('[{"attributes" : {"a": 1}, "ndistinct" : 1}]', 'pg_ndistinct');
@@ -373,11 +373,11 @@ SELECT '[{"attributes" : [2,2], "ndistinct" : 4}]'::pg_ndistinct;
 ERROR:  malformed pg_ndistinct: "[{"attributes" : [2,2], "ndistinct" : 4}]"
 LINE 1: SELECT '[{"attributes" : [2,2], "ndistinct" : 4}]'::pg_ndist...
                ^
-DETAIL:  Invalid "attributes" element: 2 cannot follow 2.
+DETAIL:  Invalid "attributes" element has been found: 2 cannot follow 2.
 SELECT * FROM pg_input_error_info('[{"attributes" : [2,2], "ndistinct" : 4}]', 'pg_ndistinct');
-                               message                               |                      detail                      | hint | sql_error_code 
----------------------------------------------------------------------+--------------------------------------------------+------+----------------
- malformed pg_ndistinct: "[{"attributes" : [2,2], "ndistinct" : 4}]" | Invalid "attributes" element: 2 cannot follow 2. |      | 22P02
+                               message                               |                             detail                              | hint | sql_error_code 
+---------------------------------------------------------------------+-----------------------------------------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [2,2], "ndistinct" : 4}]" | Invalid "attributes" element has been found: 2 cannot follow 2. |      | 22P02
 (1 row)
 
 -- Duplicated attribute lists.
@@ -387,13 +387,13 @@ ERROR:  malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : 4},
          {"attributes" : [2,3], "ndistinct" : 4}]"
 LINE 1: SELECT '[{"attributes" : [2,3], "ndistinct" : 4},
                ^
-DETAIL:  Duplicated "attributes" array found: [2, 3]
+DETAIL:  Duplicated "attributes" array has been found: [2, 3]
 SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : 4},
          {"attributes" : [2,3], "ndistinct" : 4}]', 'pg_ndistinct');
-                              message                               |                   detail                    | hint | sql_error_code 
---------------------------------------------------------------------+---------------------------------------------+------+----------------
- malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : 4},+| Duplicated "attributes" array found: [2, 3] |      | 22P02
-          {"attributes" : [2,3], "ndistinct" : 4}]"                 |                                             |      | 
+                              message                               |                        detail                        | hint | sql_error_code 
+--------------------------------------------------------------------+------------------------------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : 4},+| Duplicated "attributes" array has been found: [2, 3] |      | 22P02
+          {"attributes" : [2,3], "ndistinct" : 4}]"                 |                                                      |      | 
 (1 row)
 
 -- Partially-covered attribute lists.
@@ -407,17 +407,17 @@ ERROR:  malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : 4},
          {"attributes" : [1,3,-1,-2], "ndistinct" : 4}]"
 LINE 1: SELECT '[{"attributes" : [2,3], "ndistinct" : 4},
                ^
-DETAIL:  "attributes" array: [2, 3] must be a subset of array: [1, 3, -1, -2]
+DETAIL:  "attributes" array [2, 3] must be a subset of array [1, 3, -1, -2].
 SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : 4},
          {"attributes" : [2,-1], "ndistinct" : 4},
          {"attributes" : [2,3,-1], "ndistinct" : 4},
          {"attributes" : [1,3,-1,-2], "ndistinct" : 4}]', 'pg_ndistinct');
-                              message                               |                                detail                                | hint | sql_error_code 
---------------------------------------------------------------------+----------------------------------------------------------------------+------+----------------
- malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : 4},+| "attributes" array: [2, 3] must be a subset of array: [1, 3, -1, -2] |      | 22P02
-          {"attributes" : [2,-1], "ndistinct" : 4},                +|                                                                      |      | 
-          {"attributes" : [2,3,-1], "ndistinct" : 4},              +|                                                                      |      | 
-          {"attributes" : [1,3,-1,-2], "ndistinct" : 4}]"           |                                                                      |      | 
+                              message                               |                               detail                                | hint | sql_error_code 
+--------------------------------------------------------------------+---------------------------------------------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : 4},+| "attributes" array [2, 3] must be a subset of array [1, 3, -1, -2]. |      | 22P02
+          {"attributes" : [2,-1], "ndistinct" : 4},                +|                                                                     |      | 
+          {"attributes" : [2,3,-1], "ndistinct" : 4},              +|                                                                     |      | 
+          {"attributes" : [1,3,-1,-2], "ndistinct" : 4}]"           |                                                                     |      | 
 (1 row)
 
 -- Valid inputs
-- 
2.51.0

---

From: Tom Lane <[email-masked]>
Date: Thu, 04 Dec 2025 21:29:50 -0500

Michael Paquier <[email-masked]> writes:
>> that was really the most adapted way to do it.

> the code, that simply removes the macro and checks the state value

Ah, right, now that you mention it, 56b1e88c8 did this same fix
before.  This way is fine with me.

I didn't review the error message changes closely.  I do wonder if
some of them are can't-happen cases that could just use an elog()
instead of a translatable message.

			regards, tom lane

---

From: Michael Paquier <[email-masked]>
Date: Fri, 5 Dec 2025 12:39:05 +0900

> before.  This way is fine with me.

Oh, I haven't noticed this one.  Pushed the bits to take care of the
compiler warnings, for now.

> instead of a translatable message.

Some of them are, yes.  However, they are also worded with the same
format as some of the legit cases.  So they don't add any extra
workload on the translation side as far as I recall, and I've been
fond of the errdetail part to get a consistent style across the board.
I'll double-check the whole a bit later, attached is the rest of them.

Corey, any comments about these?
--
Michael
From 8efe5bb10d7863181971e059eda42ec045c6cd09 Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Fri, 5 Dec 2025 10:14:39 +0900
Subject: [PATCH v2] Improve pg_dependencies.c and pg_ndistinct.c

---
 src/backend/utils/adt/pg_dependencies.c       |  36 ++--
 src/backend/utils/adt/pg_ndistinct.c          |  30 +--
 src/test/regress/expected/pg_dependencies.out | 196 +++++++++---------
 src/test/regress/expected/pg_ndistinct.out    | 160 +++++++-------
 4 files changed, 211 insertions(+), 211 deletions(-)

index 236790b14399..1552e03d1c1e 100644

 

@@ -499,7 +499,7 @@ dependencies_scalar(void *state, char *token, JsonTokenType tokentype)
 
@@ -544,7 +544,7 @@ dependencies_scalar(void *state, char *token, JsonTokenType tokentype)
 
@@ -574,7 +574,7 @@ dependencies_scalar(void *state, char *token, JsonTokenType tokentype)

 
 
 	PG_RETURN_NULL();
 }
index a2bce48fda07..9bf1546c8030 100644

 

 
@@ -440,7 +440,7 @@ ndistinct_scalar(void *state, char *token, JsonTokenType tokentype)

 

 	PG_RETURN_NULL();
 }

 

 

 

 

 

 

 

 
 
 -- Duplicated attributes

 -- Valid inputs

 

 

 

 

 

 

 
 -- Valid inputs
-- 
2.51.0

---

From: Corey Huinker <[email-masked]>
Date: Fri, 5 Dec 2025 00:30:38 -0500

> Corey, any comments about these?

The wordings are fine, and I'm sorry I didn't word them as complete
sentences from the get-go.

Attached is a follow-on to Michael's most recent uncommitted patch,
changing the errors that I see as "impossible" to elogs. However, I agree
that they don't add significant workload to the translations, and most
input functions need to avoid any hard error returns lest they be called in
a soft-error context.
From 9e7c703eb0ed04dde25a2a6e189f21e916e581bc Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Fri, 5 Dec 2025 00:24:02 -0500
Subject: [PATCH v1 2/2] Change impossible conditions to elogs.

---
 src/backend/utils/adt/pg_dependencies.c | 14 ++++++--------
 src/backend/utils/adt/pg_ndistinct.c    |  7 +++----
 2 files changed, 9 insertions(+), 12 deletions(-)

diff --git a/src/backend/utils/adt/pg_dependencies.c b/src/backend/utils/adt/pg_dependencies.c
index 1552e03d1c1..7375e8498c5 100644
--- a/src/backend/utils/adt/pg_dependencies.c
+++ b/src/backend/utils/adt/pg_dependencies.c
@@ -326,10 +326,9 @@ dependencies_array_end(void *state)
 			 * This can only happen if a case was missed in
 			 * dependencies_array_start().
 			 */
-			errsave(parse->escontext,
-					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
-					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
-					errdetail("Array has been found at an unexpected location."));
+			elog(ERROR,
+				 "pg_dependencies array end encountered in unexpected parse state: %d.",
+				 (int) parse->state);
 			break;
 	}
 	return JSON_SEM_ACTION_FAILED;
@@ -443,10 +442,9 @@ dependencies_array_element_start(void *state, bool isnull)
 			break;
 
 		default:
-			errsave(parse->escontext,
-					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
-					errmsg("malformed pg_dependencies: \"%s\"", parse->str),
-					errdetail("Unexpected array element has been found."));
+			elog(ERROR,
+				 "pg_dependencies array element encountered in unexpected parse state: %d.",
+				 (int) parse->state);
 			break;
 	}
 
diff --git a/src/backend/utils/adt/pg_ndistinct.c b/src/backend/utils/adt/pg_ndistinct.c
index 9bf1546c803..718d161ec41 100644
--- a/src/backend/utils/adt/pg_ndistinct.c
+++ b/src/backend/utils/adt/pg_ndistinct.c
@@ -286,10 +286,9 @@ ndistinct_array_end(void *state)
 			 * This can only happen if a case was missed in
 			 * ndistinct_array_start().
 			 */
-			errsave(parse->escontext,
-					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
-					errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
-					errdetail("Array has been found at an unexpected location."));
+			elog(ERROR,
+				 "pg_ndistinct array element encountered in unexpected parse state: %d.",
+				 (int) parse->state);
 			break;
 	}
 
-- 
2.52.0

---

From: Michael Paquier <[email-masked]>
Date: Mon, 8 Dec 2025 10:25:45 +0900

> input functions need to avoid any hard error returns lest they be called in

Reporting the state of the parser in these new elogs is OK for me, but
we had more cases that what your patch has been updating.  I have
included all these cases, making the set of error messages a bit more
consistent across the board, and applied the result.
--
Michael

---

From: Corey Huinker <[email-masked]>
Date: Thu, 11 Dec 2025 16:52:26 -0500

> consistent across the board, and applied the result.

Rebased. Used new palloc0_array() in a few places, and fixed an expected
output message that wasn't updated to reflect the changes.
From 9c94fd9e5822867334a2b6f9e208e15be589474a Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 4 Nov 2025 23:50:01 -0500
Subject: [PATCH v21 1/3] Expose attribute statistics functions for use in
 extended_stats.

 
 #include "fmgr.h"
-
 
 struct StatsArgInfo
 {

 
+/*
 
+/*

 /*

 
 
 		if (converted)
 		{
 
 		if (converted)
 		{

 
 		if (converted)
 		{

 	/*
 
 		if (converted)
 		{
 
 		if (converted)
 		{
 
-/*
 
-/*
 
+
 
 	return result;
 }
+
+/*

base-commit: b65f1ad9b12767dbd45d9588ce8ed2e593dbddbf
-- 
2.52.0

From 9c5bcf05cbd46c08e9d146c37b2188532998f66b Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Tue, 11 Nov 2025 16:58:26 +0900
Subject: [PATCH v21 2/3] Add extended statistics support functions.

index fd9448ec7b9..22fe1f0a9f8 100644

index 2aed867d5e7..215b7b690f0 100644
@@ -1064,6 +1064,57 @@ clauselist_apply_dependencies(PlannerInfo *root, List *clauses,
 	return s1;
 }
 
+/*
@@ -1247,6 +1298,18 @@ dependency_is_compatible_expression(Node *clause, Index relid, List *statlist, N
 	return false;
 }
 
+/*
index 19778b773d2..2969a667cb8 100644

+/*

 
 /*
  * Compute requested extended stats, using the rows sampled for the plain
@@ -2611,3 +2720,1099 @@ make_build_data(Relation rel, StatExtEntry *stat, int numrows, HeapTuple *rows,
 
 	return result;
 }
+
+/*
index ec650ba029f..627388f2125 100644
 
 	return s;
 }
+
+/*
index 58046d2bd62..d189602bb35 100644
 
+/*
 
 DROP TABLE stats_temp;

 

From ab6fbcec808b61bc2bec9839d431355371368204 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Wed, 5 Nov 2025 01:13:04 -0500
Subject: [PATCH v21 3/3] Include Extended Statistics in pg_dump.

index 4a63f7392ae..1d42b0343eb 100644
@@ -3007,7 +3007,8 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 
index 24ad201af2f..b5ecf463db6 100644
@@ -8272,6 +8273,9 @@ getExtendedStatistics(Archive *fout)

 	PQclear(res);
@@ -11726,6 +11730,7 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 			break;
 		case DO_STATSEXT:
 			dumpStatisticsExt(fout, (const StatsExtInfo *) dobj);
+			dumpStatisticsExtStats(fout, (const StatsExtInfo *) dobj);
 			break;
 		case DO_REFRESH_MATVIEW:
 			refreshMatViewData(fout, (const TableDataInfo *) dobj);
@@ -18528,6 +18533,255 @@ dumpStatisticsExt(Archive *fout, const StatsExtInfo *statsextinfo)
 	free(qstatsextname);
 }
 
+/*
 
+	#

---

From: Corey Huinker <[email-masked]>
Date: Thu, 11 Dec 2025 16:55:52 -0500

> output message that wasn't updated to reflect the changes.

Disregard v21 (uncommitted changes). Everything I said about 21 applies to
22.
From 9c94fd9e5822867334a2b6f9e208e15be589474a Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 4 Nov 2025 23:50:01 -0500
Subject: [PATCH v22 1/3] Expose attribute statistics functions for use in
 extended_stats.

 
 #include "fmgr.h"
-
 
 struct StatsArgInfo
 {

 
+/*
 
+/*

 /*

 
 
 		if (converted)
 		{
 
 		if (converted)
 		{

 
 		if (converted)
 		{

 	/*
 
 		if (converted)
 		{
 
 		if (converted)
 		{
 
-/*
 
-/*
 
+
 
 	return result;
 }
+
+/*

From 9719d44eae8fbc96872cf238c2af0fd7d73b293a Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Tue, 11 Nov 2025 16:58:26 +0900
Subject: [PATCH v22 2/3] Add extended statistics support functions.

 

+/*
 
+/*
index 19778b773d2..f4863502db3 100644

+/*

 
 /*
 
 	return result;
 }
+
+/*
+		atttypids = palloc0_array(Oid, numattrs);
+		atttypmods = palloc0_array(int32, numattrs);
+		atttypcolls = palloc0_array(Oid, numattrs);
index ec650ba029f..d0113dceda0 100644
 
 	return s;
 }
+
+/*
+		item->values = (Datum *) palloc0_array(Datum, numattrs);
+		item->isnull = (bool *) palloc0_array(bool, numattrs);
+	vastats = (VacAttrStats **) palloc0_array(VacAttrStats *, numattrs);
+	vatuples = (HeapTuple *) palloc0_array(HeapTuple, numattrs);
+		vastats[i] = palloc0_object(VacAttrStats);
 
+/*
index 98ce7dc2841..4d7dcfca093 100644
 
 DROP TABLE stats_temp;

 

From 1ca442a2021e5aa4a06f40bea1827c87215b3a02 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Wed, 5 Nov 2025 01:13:04 -0500
Subject: [PATCH v22 3/3] Include Extended Statistics in pg_dump.

 

 	PQclear(res);
 
+/*
 
+	#

---

From: Michael Paquier <[email-masked]>
Date: Thu, 25 Dec 2025 15:23:58 +0900

> Disregard v21 (uncommitted changes). Everything I said about 21 applies to

A couple of weeks later, I have locked a couple of hours to look at
v22-0001.  And after a couple of rounds of self-fixing and adjustments
in the documentation added, the result looks OK so I have applied it.
Attached are the remaining pieces, labelled v23.
--
Michael
From 073ebafcaadef0afdd1444c8bfca13ae4665b68b Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Thu, 25 Dec 2025 15:20:56 +0900
Subject: [PATCH v23 1/2] Add extended statistics support functions.

index fd9448ec7b98..22fe1f0a9f8e 100644
 
index efcb7dc35461..042f07a76029 100644
 
index 2aed867d5e7c..215b7b690f0f 100644
 
+/*
 
+/*
index 19778b773d20..715975f36ac8 100644

+/*

 
 /*
 
 	return result;
 }
+
+/*
+			stavalues = statatt_build_stavalues(extexprarginfo[MOST_COMMON_VALS_ELEM].argname,
+			stanumbers = statatt_build_stavalues(extexprarginfo[MOST_COMMON_FREQS_ELEM].argname,
+			stavalues = statatt_build_stavalues(extexprarginfo[HISTOGRAM_BOUNDS_ELEM].argname,
+			stavalues = statatt_build_stavalues(extexprarginfo[MOST_COMMON_ELEMS_ELEM].argname,
+			stanumbers = statatt_build_stavalues(extexprarginfo[MOST_COMMON_ELEM_FREQS_ELEM].argname,
+			stanumbers = statatt_build_stavalues(extexprarginfo[ELEM_COUNT_HISTOGRAM_ELEM].argname,
index ec650ba029f5..d0113dceda05 100644
 
 	return s;
 }
+
+/*
index 58046d2bd625..d189602bb35b 100644
 
+/*
index 98ce7dc28410..4d7dcfca093c 100644
 
 DROP TABLE stats_temp;
index d140733a7502..74700554c9c6 100644

 

index 2896cd9e4290..7fba7a833bdd 100644
@@ -2165,6 +2165,104 @@ SELECT pg_restore_attribute_stats(

From 8574e374cec4dd30e345f23a07633eab6160f6c4 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Wed, 5 Nov 2025 01:13:04 -0500
Subject: [PATCH v23 2/2] Include Extended Statistics in pg_dump.

index d9041dad7206..2f8d9799c30c 100644
index 4a63f7392ae8..1d42b0343eb3 100644
 
index 27f6be3f0f82..d7e6c71f140d 100644

 	PQclear(res);
 
+/*
index e33aa95f6ffc..9ab82f972773 100644
 
+	#

---

From: Tender Wang <[email-masked]>
Date: Thu, 25 Dec 2025 19:00:02 +0800

Tender Wang <[email-masked]> 于2025年12月25日周四 18:58写道：

> Hi Michael,
>
> I found a typo in recent commit :
> commit 213a1b89527049cb27bbcd6871fdb0c0916b43e1 (origin/master,
> origin/HEAD, master)
> Author: Michael Paquier <[email-masked]>
> Date:   Thu Dec 25 15:13:39 2025 +0900
>  Move attribute statistics functions to stat_utils.c
> ....
>  * The stacoll value should be either the atttypcoll derived from
>  * statatt_get_type(), or a harcoded value required by that particular
>  * stakind.
> ....
>
> "harcoded" should be "hardcoded".
>
> Please check the attached patch.
>
> --
> Thanks,
> Tender Wang
>

Sorry, I forgot to cc the pgsql-hackers mail in my last email.
-- 
Thanks,
Tender Wang

---

From: Michael Paquier <[email-masked]>
Date: Fri, 26 Dec 2025 07:47:54 +0900

> Sorry, I forgot to cc the pgsql-hackers mail in my last email.

Missed this one from v22-0001.  Thanks.
--
Michael