From 5d39e2bae4d952205c4952e23a41220b940ccbd4 Mon Sep 17 00:00:00 2001 From: Corey Huinker 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" +#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 */ } /* diff --git a/src/test/regress/expected/stats_ext.out b/src/test/regress/expected/stats_ext.out index 8cea29de314..6219b9674b0 100644 --- a/src/test/regress/expected/stats_ext.out +++ b/src/test/regress/expected/stats_ext.out @@ -3788,6 +3788,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 e8aa6b58009..6a5fbfd31ad 100644 --- a/src/test/regress/sql/stats_ext.sql +++ b/src/test/regress/sql/stats_ext.sql @@ -1844,6 +1844,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.1