]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Add input function for data type pg_ndistinct
authorMichael Paquier <michael@paquier.xyz>
Wed, 26 Nov 2025 01:13:18 +0000 (10:13 +0900)
committerMichael Paquier <michael@paquier.xyz>
Wed, 26 Nov 2025 01:13:18 +0000 (10:13 +0900)
pg_ndistinct is used as data type for the contents of ndistinct extended
statistics.  This new input function consumes the format that has been
established by 1f927cce4498 for the output function of pg_ndistinct,
enforcing some sanity checks for:
- Checks for the input object, which should be a one-dimension array
with correct attributes and values.
- The key names: "attributes", "ndistinct".  Both are required, other
key names are blocked.
- Value types for each key: "attributes" requires an array of integers,
and "ndistinct" an integer.
- List of attributes.  Note that this enforces a check so as an
attribute list has to be a subset of the longest attribute list found.
This does not enforce that a full group of attribute sets exist, based
on how the groups are generated when the ndistinct objects are
generated, making the list of ndistinct items a bit loose.  Note a check
would still be required at import to see if the attributes listed match
with the attribute numbers set in the definition of a statistics object.
- Based on the discussion, the checks on the values are loose, as there
is also an argument for potentially stats injection.  The relation and
attribute level stats follow the same line of argument for the values.

This is required for a follow-up patch that aims to implement the import
of extended statistics.  Some tests are added to check the code paths of
the JSON parser checking the shape of the pg_ndistinct inputs, with 90%
of code coverage reached.  The tests are located in their own new test
file, for clarity.

Author: Corey Huinker <corey.huinker@gmail.com>
Reviewed-by: Jian He <jian.universality@gmail.com>
Reviewed-by: Chao Li <li.evan.chao@gmail.com>
Reviewed-by: Michael Paquier <michael@paquier.xyz>
Reviewed-by: Yuefei Shi <shiyuefei1004@gmail.com>
Discussion: https://postgr.es/m/CADkLM=dpz3KFnqP-dgJ-zvRvtjsa8UZv8wDAQdqho=qN3kX0Zg@mail.gmail.com

src/backend/utils/adt/pg_ndistinct.c
src/test/regress/expected/pg_ndistinct.out [new file with mode: 0644]
src/test/regress/parallel_schedule
src/test/regress/sql/pg_ndistinct.sql [new file with mode: 0644]
src/tools/pgindent/typedefs.list

index 97efc290ef5e2b3cf6f9746842e01c420d0df983..5292521b1695b9277682dfae272b66e97f3c4f26 100644 (file)
 
 #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/fmgrprotos.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 attribute numbers */
+       int32           ndistinct;
+} NDistinctParseState;
 
 /*
- * pg_ndistinct_in
- *             input routine for type pg_ndistinct
+ * 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;
+
+               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 attribute number 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));
+                       break;
+       }
+
+       return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ * 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;
+
+       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 = list_length(parse->attnum_list);
+       if ((natts < 2) || (natts > STATS_MAX_DIMENSIONS))
+       {
+               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 %d and no more than %d attributes.",
+                                                 PG_NDISTINCT_KEY_ATTRIBUTES, 2, STATS_MAX_DIMENSIONS));
+               return JSON_SEM_ACTION_FAILED;
+       }
+
+       /* Create the MVNDistinctItem */
+       item = palloc(sizeof(MVNDistinctItem));
+       item->nattributes = natts;
+       item->attributes = palloc0(natts * sizeof(AttrNumber));
+       item->ndistinct = (double) parse->ndistinct;
+
+       for (int i = 0; i < natts; i++)
+               item->attributes[i] = (AttrNumber) list_nth_int(parse->attnum_list, i);
+
+       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;
+}
+
+
+/*
+ * Invoked at the start of an array.
+ *
+ * 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;
+}
+
+
+/*
+ * Invoked at the end of an array.
+ *
+ * Arrays can never be empty.
+ */
+static JsonParseErrorType
+ndistinct_array_end(void *state)
+{
+       NDistinctParseState *parse = state;
+
+       switch (parse->state)
+       {
+               case NDIST_EXPECT_ATTNUM:
+                       if (list_length(parse->attnum_list) > 0)
+                       {
+                               /*
+                                * 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 a non-empty array.",
+                                                         PG_NDISTINCT_KEY_ATTRIBUTES));
+                       break;
+
+               case NDIST_EXPECT_ITEM:
+                       if (list_length(parse->distinct_items) > 0)
+                       {
+                               /* 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."));
+                       break;
+       }
+
+       return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ * Invoked at the start of a key/value field.
+ *
+ * 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)
+       {
+               if (parse->found_attributes)
+               {
+                       errsave(parse->escontext,
+                                       errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+                                       errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+                                       errdetail("Multiple \"%s\" keys are not allowed.",
+                                                         PG_NDISTINCT_KEY_ATTRIBUTES));
+                       return JSON_SEM_ACTION_FAILED;
+               }
+               parse->found_attributes = true;
+               parse->state = NDIST_EXPECT_ATTNUM_LIST;
+               return JSON_SUCCESS;
+       }
+
+       if (strcmp(fname, PG_NDISTINCT_KEY_NDISTINCT) == 0)
+       {
+               if (parse->found_ndistinct)
+               {
+                       errsave(parse->escontext,
+                                       errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+                                       errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+                                       errdetail("Multiple \"%s\" keys are not allowed.",
+                                                         PG_NDISTINCT_KEY_NDISTINCT));
+                       return JSON_SEM_ACTION_FAILED;
+               }
+               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;
+}
+
+/*
+ * Invoked at the start of an array element.
+ *
+ * The overall structure of the datatype is an array, but there are also
+ * arrays as the value of every attributes key.
+ */
+static JsonParseErrorType
+ndistinct_array_element_start(void *state, bool isnull)
+{
+       const 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."));
+                       break;
+       }
+
+       return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ * 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.
  *
- * 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).
+ * Duplicate values are obviously not allowed, but that is already covered
+ * by the rules listed above.
+ */
+static bool
+valid_subsequent_attnum(AttrNumber prev, AttrNumber cur)
+{
+       Assert(prev != 0);
+
+       if (prev > 0)
+               return ((cur > prev) || (cur < 0));
+
+       return (cur < prev);
+}
+
+/*
+ * 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))
+                       {
+                               errsave(parse->escontext,
+                                               errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+                                               errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+                                               errdetail("Invalid \"%s\" value.", PG_NDISTINCT_KEY_ATTRIBUTES));
+                               return JSON_SEM_ACTION_FAILED;
+                       }
+
+                       /*
+                        * The attribute number cannot be zero a negative number beyond
+                        * the number of the possible expressions.
+                        */
+                       if (attnum == 0 || attnum < (0 - STATS_MAX_DIMENSIONS))
+                       {
+                               errsave(parse->escontext,
+                                               errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+                                               errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+                                               errdetail("Invalid \"%s\" element: %d.",
+                                                                 PG_NDISTINCT_KEY_ATTRIBUTES, attnum));
+                               return JSON_SEM_ACTION_FAILED;
+                       }
+
+                       if (list_length(parse->attnum_list) > 0)
+                       {
+                               const AttrNumber prev = llast_int(parse->attnum_list);
+
+                               if (!valid_subsequent_attnum(prev, attnum))
+                               {
+                                       errsave(parse->escontext,
+                                                       errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+                                                       errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+                                                       errdetail("Invalid \"%s\" element: %d cannot follow %d.",
+                                                                         PG_NDISTINCT_KEY_ATTRIBUTES, attnum, prev));
+                                       return JSON_SEM_ACTION_FAILED;
+                               }
+                       }
+
+                       parse->attnum_list = lappend_int(parse->attnum_list, (int) attnum);
+                       return JSON_SUCCESS;
+
+               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."));
+                       break;
+       }
+
+       return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ * Compare the attribute arrays of two MVNDistinctItem values,
+ * 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.
+ */
+static bool
+item_attributes_eq(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 attribute numbers.
+ *
+ * Freeing the allocated string is the responsibility of the caller.
+ */
+static 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;
+}
+
+/*
+ * 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)
+{
+       MVNDistinct *ndistinct;
+       int                     nitems = list_length(parse->distinct_items);
+       bytea      *bytes;
+       int                     item_most_attrs = 0;
+       int                     item_most_attrs_idx = 0;
+
+       switch (parse->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 (nitems == 0)
+                               elog(ERROR,
+                                        "cannot have empty item list after parsing success.");
+                       break;
+
+               case NDIST_EXPECT_START:
+                       /* blank */
+                       errsave(parse->escontext,
+                                       errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+                                       errmsg("malformed pg_ndistinct: \"%s\"", str),
+                                       errdetail("Value cannot be empty."));
+                       return NULL;
+
+               default:
+                       /* Unexpected end-state. */
+                       errsave(parse->escontext,
+                                       errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+                                       errmsg("malformed pg_ndistinct: \"%s\"", str),
+                                       errdetail("Unexpected end state %d.", parse->state));
+                       return NULL;
+       }
+
+       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 = list_nth(parse->distinct_items, i);
+
+               /*
+                * Ensure that this item does not duplicate the attributes of any
+                * pre-existing item.
+                */
+               for (int j = 0; j < i; j++)
+               {
+                       if (item_attributes_eq(item, &ndistinct->items[j]))
+                       {
+                               char       *s = item_attnum_list(item);
+
+                               errsave(parse->escontext,
+                                               errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+                                               errmsg("malformed pg_ndistinct: \"%s\"", str),
+                                               errdetail("Duplicated \"%s\" array found: [%s]",
+                                                                 PG_NDISTINCT_KEY_ATTRIBUTES, s));
+                               pfree(s);
+                               return NULL;
+                       }
+               }
+
+               ndistinct->items[i].ndistinct = item->ndistinct;
+               ndistinct->items[i].nattributes = item->nattributes;
+
+               /*
+                * This transfers free-ing responsibility from the distinct_items list
+                * to the ndistinct object.
+                */
+               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;
+               }
+       }
+
+       /*
+        * 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];
+                       char       *item_list = item_attnum_list(item);
+                       char       *refitem_list = item_attnum_list(refitem);
+
+                       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]",
+                                                         PG_NDISTINCT_KEY_ATTRIBUTES,
+                                                         item_list, refitem_list));
+                       pfree(item_list);
+                       pfree(refitem_list);
+                       return NULL;
+               }
+       }
+
+       bytes = statext_ndistinct_serialize(ndistinct);
+
+       /*
+        * Free the attribute lists, before the ndistinct itself.
+        */
+       for (int i = 0; i < nitems; i++)
+               pfree(ndistinct->items[i].attributes);
+       pfree(ndistinct);
+
+       return bytes;
+}
+
+/*
+ * 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;
+       bytea      *bytes = NULL;
 
-       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)
+               bytes = build_mvndistinct(&parse_state, str);
+
+       list_free(parse_state.attnum_list);
+       list_free_deep(parse_state.distinct_items);
+
+       if (bytes)
+               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 (file)
index 0000000..736954b
--- /dev/null
@@ -0,0 +1,447 @@
+-- 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 '[{"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 '[{"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_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)
+
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "attributes" : [1,3], "ndistinct" : 4}]', 'pg_ndistinct');
+                                          message                                          |                   detail                    | hint | sql_error_code 
+-------------------------------------------------------------------------------------------+---------------------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [2,3], "attributes" : [1,3], "ndistinct" : 4}]" | Multiple "attributes" keys are not allowed. |      | 22P02
+(1 row)
+
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : 4, "ndistinct" : 4}]', 'pg_ndistinct');
+                                       message                                        |                   detail                   | hint | sql_error_code 
+--------------------------------------------------------------------------------------+--------------------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : 4, "ndistinct" : 4}]" | Multiple "ndistinct" keys are not allowed. |      | 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)
+
+-- 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 "attributes" key must contain an array of at least 2 and no more than 8 attributes.
+SELECT * FROM pg_input_error_info('[{"attributes" : [1,2,3,4,5,6,7,8,9], "ndistinct" : 4}]', 'pg_ndistinct');
+                                      message                                      |                                         detail                                          | hint | sql_error_code 
+-----------------------------------------------------------------------------------+-----------------------------------------------------------------------------------------+------+----------------
+ malformed pg_ndistinct: "[{"attributes" : [1,2,3,4,5,6,7,8,9], "ndistinct" : 4}]" | The "attributes" key must contain an array of at least 2 and no more than 8 attributes. |      | 22P02
+(1 row)
+
+-- Special characters
+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.
+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.
+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.
+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.
+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
+(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
+(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
+(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
+(1 row)
+
+-- 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 a 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 "attributes" key must contain an array of at least 2 and no more than 8 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" : [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.
+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.
+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 a 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 "attributes" key must contain an array of at least 2 and no more than 8 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" : [-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
+(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:  Invalid "attributes" element: 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
+(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" : [2,-1], "ndistinct" : 1},
+         {"attributes" : [3,-1], "ndistinct" : 2},
+         {"attributes" : [2,3,-1], "ndistinct" : 3}]'::pg_ndistinct;
+                                                          pg_ndistinct                                                          
+--------------------------------------------------------------------------------------------------------------------------------
+ [{"attributes": [2, -1], "ndistinct": 1}, {"attributes": [3, -1], "ndistinct": 2}, {"attributes": [2, 3, -1], "ndistinct": 3}]
+(1 row)
+
+-- Three attributes with only two items.
+SELECT '[{"attributes" : [2,-1], "ndistinct" : 1},
+         {"attributes" : [2,3,-1], "ndistinct" : 3}]'::pg_ndistinct;
+                                     pg_ndistinct                                      
+---------------------------------------------------------------------------------------
+ [{"attributes": [2, -1], "ndistinct": 1}, {"attributes": [2, 3, -1], "ndistinct": 3}]
+(1 row)
+
index f56482fb9f1211edc8a1d4b0011ff6a2778c8d95..f3f0b5f2f317b485a89a134cd79f682d9cd072f8 100644 (file)
@@ -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 (file)
index 0000000..319c7d4
--- /dev/null
@@ -0,0 +1,106 @@
+-- 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 '[{"attributes" : [2,3], "attributes" : [1,3], "ndistinct" : 4}]'::pg_ndistinct;
+SELECT '[{"attributes" : [2,3], "ndistinct" : 4, "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');
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "attributes" : [1,3], "ndistinct" : 4}]', 'pg_ndistinct');
+SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : 4, "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');
+
+-- Valid keys, too many attributes
+SELECT '[{"attributes" : [1,2,3,4,5,6,7,8,9], "ndistinct" : 4}]'::pg_ndistinct;
+SELECT * FROM pg_input_error_info('[{"attributes" : [1,2,3,4,5,6,7,8,9], "ndistinct" : 4}]', 'pg_ndistinct');
+
+-- Special characters
+SELECT '[{"\ud83d" : [1, 2], "ndistinct" : 4}]'::pg_ndistinct;
+SELECT '[{"attributes" : [1, 2], "\ud83d" : 4}]'::pg_ndistinct;
+SELECT '[{"attributes" : [1, 2], "ndistinct" : "\ud83d"}]'::pg_ndistinct;
+SELECT '[{"attributes" : ["\ud83d", 2], "ndistinct" : 1}]'::pg_ndistinct;
+SELECT * FROM pg_input_error_info('[{"\ud83d" : [1, 2], "ndistinct" : 4}]', 'pg_ndistinct');
+SELECT * FROM pg_input_error_info('[{"attributes" : [1, 2], "\ud83d" : 4}]', 'pg_ndistinct');
+SELECT * FROM pg_input_error_info('[{"attributes" : [1, 2], "ndistinct" : "\ud83d"}]', 'pg_ndistinct');
+SELECT * FROM pg_input_error_info('[{"attributes" : ["\ud83d", 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" : [0,1], "ndistinct" : 1}]'::pg_ndistinct;
+SELECT '[{"attributes" : [-7,-9], "ndistinct" : 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" : [-7,-9], "ndistinct" : 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" : [2,-1], "ndistinct" : 1},
+         {"attributes" : [3,-1], "ndistinct" : 2},
+         {"attributes" : [2,3,-1], "ndistinct" : 3}]'::pg_ndistinct;
+-- Three attributes with only two items.
+SELECT '[{"attributes" : [2,-1], "ndistinct" : 1},
+         {"attributes" : [2,3,-1], "ndistinct" : 3}]'::pg_ndistinct;
index 57a8f0366a55dfb28176d1aae556601bbe2bb031..17e2b40b9cb0d52edf1712753b87688c962b9abe 100644 (file)
@@ -1732,6 +1732,8 @@ MultirangeIOData
 MultirangeParseState
 MultirangeType
 NDBOX
+NDistinctParseState
+NDistinctSemanticState
 NLSVERSIONINFOEX
 NODE
 NTSTATUS