From 9f60d150fc8449601edfaf9f9fcdfb041963b607 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Mon, 17 Nov 2025 15:49:36 +0900
Subject: [PATCH v14 2/5] 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/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"
+#include "common/jsonapi.h"
 #include "lib/stringinfo.h"
+#include "mb/pg_wchar.h"
+#include "nodes/miscnodes.h"
 #include "statistics/extended_stats_internal.h"
 #include "statistics/statistics_format.h"
+#include "utils/builtins.h"
+#include "utils/float.h"
 #include "utils/fmgrprotos.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);
 
-	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 */
 }
 
 /*
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

