From 242e68f0bd1e7fff8d31117abf8c9991398bcbbb Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
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 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 |  34 +-
 src/test/regress/sql/stats_ext.sql      |  12 +
 3 files changed, 506 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"
 
+#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"
@@ -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);
 
-	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;
+
+		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 */
 }
 
 /*
@@ -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 82d4d479ec8..6609b039453 100644
--- a/src/test/regress/expected/stats_ext.out
+++ b/src/test/regress/expected/stats_ext.out
@@ -1314,9 +1314,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''');
@@ -1656,9 +1656,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''');
@@ -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;
 
+-- 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;
+
 -- Tidy up
 DROP TABLE sb_1, sb_2 CASCADE;
 DROP FUNCTION extstat_small(x numeric);
-- 
2.51.0

