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