From 3e7173fd578f75ddd387537890c039252e34c91c Mon Sep 17 00:00:00 2001 From: Junwang Zhao Date: Sun, 22 Mar 2026 00:04:22 +0800 Subject: [PATCH v4] Support COPY FROM with FORMAT JSON Bytes are read into raw_buf and optionally transcoded into input_buf. Instead of CopyReadLine, JSON mode uses CopyReadNextJson to fetch the next row object via a small state machine: - BEFORE_ARRAY: expect '[' or '{' - BEFORE_OBJECT: in concat mode, expect next object or EOF - IN_ARRAY: expect object, comma, or ']' - IN_OBJECT: track brace/bracket depth to find the matching row object end - IN_STRING / IN_STRING_ESC: skip braces inside strings - ARRAY_END: after ']', no more rows When a row object closes, CopyReadNextJson records its bounds in row_text_start and row_text_end, leaving parse_pos just after the object. The row is parsed in place; consumed input is discarded only when line_buf needs refilling. NextCopyFromJsonRawFieldsInternal parses that slice using JsonLexContext and field callbacks, and looks up each target column by name. Values retain their original JSON text in separately allocated raw_fields entries. CopyFromJsonOneRow then calls CopyConvertJsonAttribute for per-column conversion. json/jsonb columns and their domains receive the original JSON text. For other types, strings are unquoted and unescaped and scalars go through the input functions, while arrays and objects are converted using json_populate_type(). The existing COPY machinery for defaults and soft errors applies unchanged. The JSON input path is strict JSON input and does not treat \. as a COPY end marker; frontend COPY termination is still handled by the protocol CopyDone message. Discussion: https://www.postgresql.org/message-id/flat/CAEG8a3%2BwxMXGLcnrDzSF0qTDzH%2BK7_x0FWNAcu-H_j8gKiwFVw%40mail.gmail.com --- doc/src/sgml/ref/copy.sgml | 96 ++- src/backend/commands/copy.c | 14 +- src/backend/commands/copyfrom.c | 90 +++ src/backend/commands/copyfromparse.c | 685 +++++++++++++++++- src/backend/commands/tablecmds.c | 2 +- src/include/commands/copyfrom_internal.h | 52 ++ .../expected/test_copy_callbacks.out | 83 +++ .../sql/test_copy_callbacks.sql | 30 + .../test_copy_callbacks--1.0.sql | 5 + .../test_copy_callbacks/test_copy_callbacks.c | 84 +++ src/test/regress/expected/copy.out | 308 +++++++- src/test/regress/sql/copy.sql | 219 +++++- src/tools/pgindent/typedefs.list | 6 + 13 files changed, 1647 insertions(+), 27 deletions(-) diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml index b23433b2c41..dca499e3989 100644 --- a/doc/src/sgml/ref/copy.sgml +++ b/doc/src/sgml/ref/copy.sgml @@ -234,10 +234,6 @@ COPY { table_name [ ( text. See below for details. - - The json option is allowed only in - COPY TO. - In JSON format, SQL NULL values are output as @@ -390,7 +386,8 @@ COPY (SELECT j FROM (VALUES ('null'::json), (NULL::json)) v(j)) Force output of square brackets as array decorations at the beginning and end of output, and commas between the rows. It is allowed only in COPY TO, and only when using - json format. The default is + json format. It is not supported for + COPY FROM. The default is false. @@ -1121,6 +1118,85 @@ versions of PostgreSQL. + + + JSON Format + + + <command>COPY TO</command> + + + By default, COPY TO writes one JSON object per line + (record), in column order, using the column names as object keys. + With FORCE_ARRAY, the whole output is wrapped as a + single JSON array: an opening [, objects separated + by commas (with a line break after the opening bracket, matching + COPY TO row boundaries), and a closing + ]. + + + + + <command>COPY FROM</command> + + + COPY FROM with FORMAT JSON accepts + either of these shapes. + + + + + + A JSON array whose elements are objects, one + object per table row, e.g. + [{row1},{row2}]. + Standard JSON rules apply: a comma is required between array elements, + the array must be closed with ], and a trailing + comma after the last element is not allowed. + + + + + A stream of JSON objects concatenated back-to-back + (optionally separated by white space), with no + comma between objects, e.g. + {row1}{row2}. + This form is recognized when the document begins with + {. + + + + + + For each row object, keys are matched to the target columns by name. + Keys that do not correspond to a column in the COPY + column list are ignored. Missing keys are treated as + NULL for the corresponding column. When no column + list is given, column names are those of the table (excluding generated + columns). + + + + JSON strings are unquoted and unescaped before being passed to the + target type's input function, except for json and + jsonb columns (including domains over these types), which + receive JSON text. JSON arrays and objects are recursively converted + to SQL arrays and composite values using the same rules as + json_populate_record. Other values are passed to + the target type's input function using their JSON text representation. + In particular, JSON booleans use true and + false. + + + + Options that only apply to text or CSV line parsing (such as + DELIMITER, NULL, and + HEADER) are not supported with JSON format. + ON_ERROR applies to errors converting individual + fields to the target types; malformed JSON input always causes an error. + + + @@ -1149,6 +1225,16 @@ The output is as follows: + + To load rows from a JSON file (for example one produced by + COPY TO with FORMAT JSON): + +COPY mytable FROM '/path/to/rows.json' (FORMAT JSON); + + The file may be either a JSON array of row objects or a concatenation + of row objects; see . + + To copy data from a file into the country table: diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c index 003b70852bb..4a1240247b8 100644 --- a/src/backend/commands/copy.c +++ b/src/backend/commands/copy.c @@ -989,17 +989,19 @@ ProcessCopyOptions(ParseState *pstate, errmsg("COPY %s cannot be used with %s", "FREEZE", "COPY TO"))); - /* Check json format */ - if (opts_out->format == COPY_FORMAT_JSON && is_from) - ereport(ERROR, - errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("COPY %s is not supported for %s", "FORMAT JSON", "COPY FROM")); - if (opts_out->format != COPY_FORMAT_JSON && opts_out->force_array) ereport(ERROR, errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("COPY %s can only be used with JSON mode", "FORCE_ARRAY")); + if (is_from && opts_out->format == COPY_FORMAT_JSON && opts_out->force_array) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + /*- translator: first %s is the name of a COPY option, e.g. ON_ERROR, + second %s is a COPY with direction, e.g. COPY TO */ + errmsg("COPY %s cannot be used with %s", "FORCE_ARRAY", + "COPY FROM"))); + if (opts_out->default_print) { if (!is_from) diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c index 746b4fdf6c3..0e5fd6c10f7 100644 --- a/src/backend/commands/copyfrom.c +++ b/src/backend/commands/copyfrom.c @@ -153,6 +153,19 @@ static const CopyFromRoutine CopyFromRoutineBinary = { .CopyFromEnd = CopyFromBinaryEnd, }; +/* JSON format */ +static void CopyFromJsonInFunc(CopyFromState cstate, Oid atttypid, FmgrInfo *finfo, + Oid *typioparam); +static void CopyFromJsonStart(CopyFromState cstate, TupleDesc tupDesc); +static void CopyFromJsonEnd(CopyFromState cstate); + +static const CopyFromRoutine CopyFromRoutineJson = { + .CopyFromInFunc = CopyFromJsonInFunc, + .CopyFromStart = CopyFromJsonStart, + .CopyFromOneRow = CopyFromJsonOneRow, + .CopyFromEnd = CopyFromJsonEnd, +}; + /* Return a COPY FROM routine for the given options */ static const CopyFromRoutine * CopyFromGetRoutine(const CopyFormatOptions *opts) @@ -161,6 +174,8 @@ CopyFromGetRoutine(const CopyFormatOptions *opts) return &CopyFromRoutineCSV; else if (opts->format == COPY_FORMAT_BINARY) return &CopyFromRoutineBinary; + else if (opts->format == COPY_FORMAT_JSON) + return &CopyFromRoutineJson; /* default is text */ return &CopyFromRoutineText; @@ -247,6 +262,81 @@ CopyFromBinaryEnd(CopyFromState cstate) /* nothing to do */ } +/* Implementation of the infunc callback for JSON format */ +static void +CopyFromJsonInFunc(CopyFromState cstate, Oid atttypid, FmgrInfo *finfo, + Oid *typioparam) +{ + Oid func_oid; + + getTypeInputInfo(atttypid, &func_oid, typioparam); + fmgr_info(func_oid, finfo); +} + +/* Implementation of the start callback for JSON format */ +static void +CopyFromJsonStart(CopyFromState cstate, TupleDesc tupDesc) +{ + CopyFromJsonState *json_state; + HASHCTL ctl = {0}; + int fieldno = 0; + + /* + * Set up input_buf for encoding conversion, same as text format. + */ + if (cstate->need_transcoding) + { + cstate->input_buf = (char *) palloc(INPUT_BUF_SIZE + 1); + cstate->input_buf_index = cstate->input_buf_len = 0; + } + else + cstate->input_buf = cstate->raw_buf; + cstate->input_reached_eof = false; + + initStringInfo(&cstate->line_buf); + + /* Store state for CopyFromJsonOneRow (JSON scan uses line_buf) */ + json_state = palloc0_object(CopyFromJsonState); + /* Accept [...] or auto-detect concatenated objects {...}{...} */ + json_state->parse_state = COPY_JSON_BEFORE_ARRAY; + json_state->row_text_start = -1; + json_state->row_text_end = -1; + json_state->base_types = palloc0_array(Oid, tupDesc->natts); + json_state->conversion_cache = palloc0_array(void *, tupDesc->natts); + cstate->format_private = json_state; + + ctl.keysize = NAMEDATALEN; + ctl.entrysize = sizeof(CopyJsonAttribute); + ctl.hcxt = cstate->copycontext; + json_state->attribute_map = hash_create("COPY JSON attributes", + Max(1, list_length(cstate->attnumlist)), + &ctl, HASH_ELEM | HASH_STRINGS | HASH_CONTEXT); + foreach_int(attnum, cstate->attnumlist) + { + Form_pg_attribute att = TupleDescAttr(tupDesc, attnum - 1); + CopyJsonAttribute *entry; + + entry = hash_search(json_state->attribute_map, NameStr(att->attname), + HASH_ENTER, NULL); + entry->fieldno = fieldno++; + json_state->base_types[attnum - 1] = getBaseType(att->atttypid); + } + + /* + * Raw JSON field strings are allocated in the per-tuple context. Only + * the array of pointers needs to survive between rows. + */ + cstate->max_fields = list_length(cstate->attnumlist); + cstate->raw_fields = palloc_array(char *, cstate->max_fields); +} + +/* Implementation of the end callback for JSON format */ +static void +CopyFromJsonEnd(CopyFromState cstate) +{ + /* format_private (CopyFromJsonState) is freed with copycontext */ +} + /* * error context callback for COPY FROM * diff --git a/src/backend/commands/copyfromparse.c b/src/backend/commands/copyfromparse.c index 06450535779..c6b7c12548f 100644 --- a/src/backend/commands/copyfromparse.c +++ b/src/backend/commands/copyfromparse.c @@ -1,9 +1,9 @@ /*------------------------------------------------------------------------- * * copyfromparse.c - * Parse CSV/text/binary format for COPY FROM. + * Parse CSV/text/binary/JSON format for COPY FROM. * - * This file contains routines to parse the text, CSV and binary input + * This file contains routines to parse the text, CSV, binary and JSON input * formats. The main entry point is NextCopyFrom(), which parses the * next input line and returns it as Datums. * @@ -42,6 +42,14 @@ * but 'attribute_buf' is used as a temporary buffer to hold one attribute's * data when it's passed the receive function. * + * In JSON mode, steps 1--2 are the same as text mode. CopyReadNextJson() then + * scans line_buf (refilled via CopyLoadInputBuf, then drained into line_buf) + * for the next top-level JSON object, like CopyReadLine() gathers one text + * line. The row is parsed in place using row_text_start and row_text_end; + * parse_pos remains the scan cursor for subsequent rows. Consumed data is + * discarded only when refilling the buffer. Selected fields retain their + * JSON text until type conversion. + * * 'raw_buf' is always 64 kB in size (RAW_BUF_SIZE). 'input_buf' is also * 64 kB (INPUT_BUF_SIZE), if encoding conversion is required. 'line_buf' * and 'attribute_buf' are expanded on demand, to hold the longest line @@ -62,6 +70,7 @@ #include #include +#include "catalog/pg_type_d.h" #include "commands/copyapi.h" #include "commands/copyfrom_internal.h" #include "commands/progress.h" @@ -75,6 +84,7 @@ #include "port/pg_bswap.h" #include "port/simd.h" #include "utils/builtins.h" +#include "utils/jsonfuncs.h" #include "utils/rel.h" #include "utils/wait_event.h" @@ -160,6 +170,9 @@ static pg_always_inline bool NextCopyFromRawFieldsInternal(CopyFromState cstate, char ***fields, int *nfields, bool is_csv); +static bool NextCopyFromJsonRawFieldsInternal(CopyFromState cstate, + char ***fields, int *nfields); +static bool CopyReadNextJson(CopyFromState cstate); /* Low-level communications functions */ @@ -770,6 +783,353 @@ CopyReadBinaryData(CopyFromState cstate, char *dest, int nbytes) return copied_bytes; } +/* + * COPY FROM JSON: incremental scanner over line_buf. + * Each completed row is identified by offsets in line_buf. Parsing that + * slice in place avoids copying the unparsed suffix after every row. + * + * We support: + * - A single JSON array of objects: [ {...}, {...} ] + * - Concatenated objects (auto-detect when input starts with '{'): {...}{...} + * + * States: + * BEFORE_ARRAY — start of input; expect '[' or '{'. + * BEFORE_OBJECT — after an object in concatenated-object mode; expect '{'. + * IN_ARRAY — inside [...]; array_parse_state tracks whether to expect + * an object, a comma, or ']'. + * IN_OBJECT — brace depth count to find the matching '}' for one row; + * strings switch to IN_STRING so braces inside strings are ignored. + * IN_STRING / IN_STRING_ESC — minimal string lexer for the above. + * ARRAY_END — saw closing ']'; no more rows from this document. + * + * obj_start is the byte offset of an unfinished object in line_buf, including + * while scanning a string within that object. Refilling discards consumed + * rows, preserving any unfinished object and adjusting its offset. + * + * The state machine is implemented in CopyReadNextJson(). + */ + +/* JSON permits fewer whitespace characters than isspace(). */ +static inline bool +CopyJsonIsSpace(unsigned char c) +{ + return c == ' ' || c == '\t' || c == '\n' || c == '\r'; +} + +/* + * If the scan cursor is past buffered text, compact prefix (when safe) and + * read more via CopyLoadInputBuf (then append new input_buf bytes into + * line_buf). When line_buf is still empty afterward, + * distinguish clean EOF from truncation errors. + * + * Returns true if there is more input to scan in line_buf; false if there is + * no next JSON row (CopyReadNextJson should report EOF / end of array). + */ +static bool +CopyJsonRefillIfExhausted(CopyFromState cstate, int *obj_start) +{ + CopyFromJsonState *json_state = cstate->format_private; + StringInfo line_buf = &cstate->line_buf; + + if (json_state->parse_pos < line_buf->len) + return true; + + Assert(json_state->parse_pos == line_buf->len); + + /* + * Discard consumed rows only when we need more input. If an object is + * incomplete, keep just that object, even when its string spans buffers. + * Keeping earlier rows here would let consecutive large objects grow + * line_buf without bound. + */ + if (*obj_start < 0) + { + resetStringInfo(line_buf); + json_state->parse_pos = 0; + } + else if (*obj_start > 0) + { + line_buf->len -= *obj_start; + memmove(line_buf->data, line_buf->data + *obj_start, line_buf->len); + line_buf->data[line_buf->len] = '\0'; + json_state->parse_pos -= *obj_start; + *obj_start = 0; + } + + /* + * Same refill primitive as text COPY: fill input_buf, then move decoded + * bytes into line_buf. Always drain the full input_buf chunk (unlike + * CopyReadLine, which may leave a prefix in input_buf). + */ + { + int nbytes = INPUT_BUF_BYTES(cstate); + + CopyLoadInputBuf(cstate, false); + + if (INPUT_BUF_BYTES(cstate) > nbytes) + { + appendBinaryStringInfo(line_buf, + cstate->input_buf + cstate->input_buf_index, + INPUT_BUF_BYTES(cstate)); + cstate->input_buf_index = cstate->input_buf_len; + if (cstate->raw_buf == cstate->input_buf) + cstate->raw_buf_index = cstate->input_buf_index; + } + } + + if (json_state->parse_state == COPY_JSON_IN_OBJECT || + json_state->parse_state == COPY_JSON_IN_STRING || + json_state->parse_state == COPY_JSON_IN_STRING_ESC) + { + if (cstate->input_reached_eof) + { + cstate->line_buf_valid = false; + ereport(ERROR, + errcode(ERRCODE_BAD_COPY_FILE_FORMAT), + errmsg("unexpected end of input in COPY JSON")); + } + else if (line_buf->len > 0) + return true; + } + + if (line_buf->len > 0) + return true; + + /* + * Still inside [...] (e.g. missing closing "]", or a trailing comma after + * the last element with no "]" following). + */ + if (json_state->array_mode && + json_state->parse_state == COPY_JSON_IN_ARRAY) + { + if (cstate->cur_lineno == 0) + cstate->cur_lineno = 1; + cstate->line_buf_valid = false; + ereport(ERROR, + (errcode(ERRCODE_BAD_COPY_FILE_FORMAT), + errmsg("invalid input format for COPY JSON"), + errdetail("JSON array input was not closed with \"]\"."))); + } + + return false; +} + +/* + * Read the next JSON row from the input pipeline, analogous to CopyReadLine() + * for text format. On success, [row_text_start, row_text_end) identifies + * the row in line_buf, and parse_pos points just after its closing brace. + * The next call handles any whitespace or separator following the row. + * Does not validate the object's JSON syntax. + * + * Returns true if there is no next object (EOF / end of array). + * + * Each iteration consumes one byte (c) from line_buf, then runs the state + * machine. + */ +static bool +CopyReadNextJson(CopyFromState cstate) +{ + CopyFromJsonState *json_state = cstate->format_private; + StringInfo line_buf = &cstate->line_buf; + int obj_start = -1; + + json_state->row_text_start = -1; + json_state->row_text_end = -1; + + for (;;) + { + /* Long whitespace runs and unfinished rows must be interruptible. */ + CHECK_FOR_INTERRUPTS(); + + if (!CopyJsonRefillIfExhausted(cstate, &obj_start)) + { + cstate->line_buf_valid = false; + return true; + } + + while (json_state->parse_pos < line_buf->len) + { + const char *p = line_buf->data + json_state->parse_pos; + unsigned char c = (unsigned char) *p++; + + json_state->parse_pos = p - line_buf->data; + + switch (json_state->parse_state) + { + case COPY_JSON_BEFORE_ARRAY: + if (c == '[') + { + json_state->parse_state = COPY_JSON_IN_ARRAY; + json_state->array_parse_state = COPY_JSON_ARRAY_EXPECT_VALUE_OR_END; + json_state->array_mode = true; + continue; + } + if (c == '{') + { + /* Auto-detect concatenated objects {...}{...}. */ + cstate->cur_lineno++; + json_state->parse_state = COPY_JSON_IN_OBJECT; + json_state->object_depth = 1; + obj_start = (p - 1) - line_buf->data; + continue; + } + if (CopyJsonIsSpace(c)) + continue; + if (cstate->cur_lineno == 0) + cstate->cur_lineno = 1; + cstate->line_buf_valid = false; + ereport(ERROR, + (errcode(ERRCODE_BAD_COPY_FILE_FORMAT), + errmsg("invalid input format for COPY JSON"), + errdetail("Document must begin with \"[\" or \"{\"."))); + pg_unreachable(); + + case COPY_JSON_BEFORE_OBJECT: + if (c == '{') + { + cstate->cur_lineno++; + json_state->parse_state = COPY_JSON_IN_OBJECT; + json_state->object_depth = 1; + obj_start = (p - 1) - line_buf->data; + continue; + } + if (CopyJsonIsSpace(c)) + continue; + cstate->line_buf_valid = false; + if (c == ',') + ereport(ERROR, + errcode(ERRCODE_BAD_COPY_FILE_FORMAT), + errmsg("COPY JSON, line %" PRIu64 ": invalid input format", + cstate->cur_lineno), + errdetail("Cannot use a comma between concatenated JSON objects; use a JSON array.")); + ereport(ERROR, + (errcode(ERRCODE_BAD_COPY_FILE_FORMAT), + errmsg("COPY JSON, line %" PRIu64 ": invalid input format", + cstate->cur_lineno), + errdetail("Expected \"{\" to start the next row object."))); + pg_unreachable(); + + case COPY_JSON_IN_ARRAY: + if (CopyJsonIsSpace(c)) + continue; + + if (json_state->array_parse_state == COPY_JSON_ARRAY_EXPECT_COMMA_OR_END) + { + if (c == ',') + { + json_state->array_parse_state = COPY_JSON_ARRAY_EXPECT_VALUE; + continue; + } + if (c == ']') + { + json_state->parse_state = COPY_JSON_ARRAY_END; + continue; + } + if (c == '{') + { + cstate->line_buf_valid = false; + ereport(ERROR, + errcode(ERRCODE_BAD_COPY_FILE_FORMAT), + errmsg("COPY JSON, line %" PRIu64 ": invalid input format", + cstate->cur_lineno), + errdetail("Expected \",\" between array elements.")); + pg_unreachable(); + } + } + else + { + if (c == '{') + { + cstate->cur_lineno++; + json_state->parse_state = COPY_JSON_IN_OBJECT; + json_state->object_depth = 1; + obj_start = (p - 1) - line_buf->data; + continue; + } + if (c == ']' && + json_state->array_parse_state == COPY_JSON_ARRAY_EXPECT_VALUE_OR_END) + { + json_state->parse_state = COPY_JSON_ARRAY_END; + continue; + } + } + if (cstate->cur_lineno == 0) + cstate->cur_lineno = 1; + cstate->line_buf_valid = false; + ereport(ERROR, + (errcode(ERRCODE_BAD_COPY_FILE_FORMAT), + errmsg("COPY JSON, line %" PRIu64 ": each array element must be a JSON object", + cstate->cur_lineno))); + pg_unreachable(); + + case COPY_JSON_IN_OBJECT: + switch (c) + { + case '{': + json_state->object_depth++; + break; + case '}': + json_state->object_depth--; + if (json_state->object_depth == 0) + { + json_state->row_text_start = obj_start; + json_state->row_text_end = json_state->parse_pos; + + json_state->parse_state = (json_state->array_mode) + ? COPY_JSON_IN_ARRAY : COPY_JSON_BEFORE_OBJECT; + if (json_state->array_mode) + json_state->array_parse_state = COPY_JSON_ARRAY_EXPECT_COMMA_OR_END; + + return false; + } + break; + case '[': + json_state->object_depth++; + break; + case ']': + json_state->object_depth--; + break; + case '"': + json_state->parse_state = COPY_JSON_IN_STRING; + break; + case '\\': + cstate->line_buf_valid = false; + ereport(ERROR, + errcode(ERRCODE_BAD_COPY_FILE_FORMAT), + errmsg("invalid input syntax for type json")); + break; + default: + break; + } + break; + + case COPY_JSON_IN_STRING: + if (c == '\\') + json_state->parse_state = COPY_JSON_IN_STRING_ESC; + else if (c == '"') + json_state->parse_state = COPY_JSON_IN_OBJECT; + break; + + case COPY_JSON_IN_STRING_ESC: + json_state->parse_state = COPY_JSON_IN_STRING; + break; + + case COPY_JSON_ARRAY_END: + if (CopyJsonIsSpace(c)) + continue; + if (cstate->cur_lineno == 0) + cstate->cur_lineno = 1; + cstate->line_buf_valid = false; + ereport(ERROR, + errcode(ERRCODE_BAD_COPY_FILE_FORMAT), + errmsg("invalid input format for COPY JSON"), + errdetail("Trailing data after JSON array.")); + pg_unreachable(); + } + } + } +} + /* * This function is exposed for use by extensions that read raw fields in the * next line. See NextCopyFromRawFieldsInternal() for details. @@ -811,7 +1171,6 @@ NextCopyFromRawFieldsInternal(CopyFromState cstate, char ***fields, int *nfields /* on input check that the header line is correct if needed */ if (cstate->cur_lineno == 0 && cstate->opts.header_line != COPY_HEADER_FALSE) { - ListCell *cur; TupleDesc tupDesc; int lines_to_skip = cstate->opts.header_line; @@ -844,9 +1203,8 @@ NextCopyFromRawFieldsInternal(CopyFromState cstate, char ***fields, int *nfields fldct, list_length(cstate->attnumlist)))); fldnum = 0; - foreach(cur, cstate->attnumlist) + foreach_int(attnum, cstate->attnumlist) { - int attnum = lfirst_int(cur); char *colName; Form_pg_attribute attr = TupleDescAttr(tupDesc, attnum - 1); @@ -984,7 +1342,6 @@ CopyFromTextLikeOneRow(CopyFromState cstate, ExprContext *econtext, Oid *typioparams = cstate->typioparams; ExprState **defexprs = cstate->defexprs; char **field_strings; - ListCell *cur; int fldct; int fieldno; char *string; @@ -1006,9 +1363,8 @@ CopyFromTextLikeOneRow(CopyFromState cstate, ExprContext *econtext, fieldno = 0; /* Loop to read the user attributes on the line. */ - foreach(cur, cstate->attnumlist) + foreach_int(attnum, cstate->attnumlist) { - int attnum = lfirst_int(cur); int m = attnum - 1; Form_pg_attribute att = TupleDescAttr(tupDesc, m); @@ -1194,7 +1550,6 @@ CopyFromBinaryOneRow(CopyFromState cstate, ExprContext *econtext, Datum *values, FmgrInfo *in_functions = cstate->in_functions; Oid *typioparams = cstate->typioparams; int16 fld_count; - ListCell *cur; tupDesc = RelationGetDescr(cstate->rel); attr_count = list_length(cstate->attnumlist); @@ -1232,9 +1587,8 @@ CopyFromBinaryOneRow(CopyFromState cstate, ExprContext *econtext, Datum *values, errmsg("row field count is %d, expected %d", fld_count, attr_count))); - foreach(cur, cstate->attnumlist) + foreach_int(attnum, cstate->attnumlist) { - int attnum = lfirst_int(cur); int m = attnum - 1; Form_pg_attribute att = TupleDescAttr(tupDesc, m); @@ -1250,6 +1604,315 @@ CopyFromBinaryOneRow(CopyFromState cstate, ExprContext *econtext, Datum *values, return true; } +/* State for extracting the top-level fields of a JSON row object. */ +typedef struct CopyFromJsonParseState +{ + CopyFromState cstate; + JsonLexContext *lex; + const char *next_field; + const char *value_start; + int fieldno; +} CopyFromJsonParseState; + +/* + * Decode only top-level field names. The main lexer deliberately leaves + * string values escaped so that json columns can retain their original text, + * including Unicode escapes that cannot be represented in a SQL text value. + */ +static JsonParseErrorType +CopyJsonFieldStart(void *state, char *fname, bool isnull) +{ + CopyFromJsonParseState *st = state; + CopyFromJsonState *json_state = st->cstate->format_private; + JsonLexContext keylex; + JsonParseErrorType result; + CopyJsonAttribute *entry = NULL; + + if (st->lex->lex_level != 1) + return JSON_SUCCESS; + + makeJsonLexContextCstringLen(&keylex, st->next_field, + st->lex->token_start - st->next_field, + GetDatabaseEncoding(), true); + result = json_lex(&keylex); + if (result != JSON_SUCCESS) + json_errsave_error(result, &keylex, NULL); + Assert(keylex.token_type == JSON_TOKEN_STRING); + + /* Longer names cannot match a column; do not let the hash truncate them. */ + if (keylex.strval->len < NAMEDATALEN) + entry = hash_search(json_state->attribute_map, keylex.strval->data, + HASH_FIND, NULL); + st->fieldno = entry ? entry->fieldno : -1; + st->value_start = st->lex->token_start; + freeJsonLexContext(&keylex); + + return JSON_SUCCESS; +} + +static JsonParseErrorType +CopyJsonFieldEnd(void *state, char *fname, bool isnull) +{ + CopyFromJsonParseState *st = state; + + if (st->lex->lex_level != 1) + return JSON_SUCCESS; + + if (st->fieldno >= 0) + { + char **field = &st->cstate->raw_fields[st->fieldno]; + + /* As with json_populate_record, the last occurrence of a key wins. */ + if (*field != NULL) + pfree(*field); + *field = isnull ? NULL : + pnstrdup(st->value_start, + st->lex->prev_token_terminator - st->value_start); + } + + /* The current token is the comma (or closing brace) after the value. */ + st->next_field = st->lex->token_terminator; + return JSON_SUCCESS; +} + +/* + * Read and validate one JSON row, keeping the raw JSON text of each selected + * field. Missing keys and JSON null become SQL NULL. Field strings live in + * the caller's per-tuple context, so growing one cannot invalidate another. + */ +static bool +NextCopyFromJsonRawFieldsInternal(CopyFromState cstate, char ***fields, int *nfields) +{ + CopyFromJsonState *json_state = cstate->format_private; + CopyFromJsonParseState state; + JsonLexContext lex; + JsonSemAction sem = {0}; + const char *row; + int rowlen; + + Assert(cstate->opts.format == COPY_FORMAT_JSON); + + /* A previous row may have ended with a soft conversion error. */ + cstate->cur_attname = NULL; + cstate->cur_attval = NULL; + if (CopyReadNextJson(cstate)) + return false; + + /* line_buf also contains read-ahead data, which is not error context. */ + cstate->line_buf_valid = false; + MemSet(cstate->raw_fields, 0, cstate->max_fields * sizeof(char *)); + + Assert(json_state->row_text_start >= 0); + Assert(json_state->row_text_end > json_state->row_text_start); + Assert(json_state->row_text_end == json_state->parse_pos); + Assert(json_state->row_text_end <= cstate->line_buf.len); + row = cstate->line_buf.data + json_state->row_text_start; + rowlen = json_state->row_text_end - json_state->row_text_start; + makeJsonLexContextCstringLen(&lex, row, rowlen, GetDatabaseEncoding(), false); + state.cstate = cstate; + state.lex = &lex; + + Assert(row[0] == '{'); + state.next_field = row + 1; + state.value_start = NULL; + state.fieldno = -1; + sem.semstate = &state; + sem.object_field_start = CopyJsonFieldStart; + sem.object_field_end = CopyJsonFieldEnd; + pg_parse_json_or_ereport(&lex, &sem); + freeJsonLexContext(&lex); + + *fields = cstate->raw_fields; + *nfields = cstate->max_fields; + return true; +} + +/* Convert a field without discarding its original JSON representation. */ +static bool +CopyConvertJsonAttribute(CopyFromState cstate, Form_pg_attribute att, int m, + const char *string, Datum *value, bool *isnull) +{ + CopyFromJsonState *json_state = cstate->format_private; + Node *escontext = (Node *) cstate->escontext; + Oid base_type = json_state->base_types[m]; + JsonLexContext lex; + bool result; + + /* json/jsonb input functions, including domain inputs, need JSON text. */ + if (string == NULL || base_type == JSONOID || base_type == JSONBOID) + return InputFunctionCallSafe(&cstate->in_functions[m], string, + cstate->typioparams[m], att->atttypmod, + escontext, value); + + if (string[0] == '[' || string[0] == '{') + { + /* + * Reuse recursive conversion for arrays, composites and their + * domains. + */ + *value = json_populate_type(CStringGetTextDatum(string), JSONOID, + att->atttypid, att->atttypmod, + &json_state->conversion_cache[m], + cstate->copycontext, isnull, false, escontext); + return !SOFT_ERROR_OCCURRED(escontext); + } + + if (string[0] != '"') + return InputFunctionCallSafe(&cstate->in_functions[m], string, + cstate->typioparams[m], att->atttypmod, + escontext, value); + + /* Other input functions receive the unquoted, unescaped JSON string. */ + makeJsonLexContextCstringLen(&lex, string, strlen(string), + GetDatabaseEncoding(), true); + result = pg_parse_json_or_errsave(&lex, &nullSemAction, escontext); + if (result) + result = InputFunctionCallSafe(&cstate->in_functions[m], lex.strval->data, + cstate->typioparams[m], att->atttypmod, + escontext, value); + freeJsonLexContext(&lex); + return result; +} + +/* Implementation of the per-row callback for JSON format */ +bool +CopyFromJsonOneRow(CopyFromState cstate, ExprContext *econtext, Datum *values, + bool *nulls) +{ + TupleDesc tupDesc; + FmgrInfo *in_functions = cstate->in_functions; + Oid *typioparams = cstate->typioparams; + ExprState **defexprs = cstate->defexprs; + char **field_strings; + int fldct; + int fieldno; + char *string; + bool current_row_erroneous = false; + + /* read raw fields from the next JSON object (by column name) */ + if (!NextCopyFromJsonRawFieldsInternal(cstate, &field_strings, &fldct)) + return false; + + tupDesc = RelationGetDescr(cstate->rel); + + fieldno = 0; + + /* Loop to convert field strings to Datums for each column */ + foreach_int(attnum, cstate->attnumlist) + { + int m = attnum - 1; + Form_pg_attribute att = TupleDescAttr(tupDesc, m); + + Assert(fieldno < fldct); + string = field_strings[fieldno++]; + + if (cstate->convert_select_flags && + !cstate->convert_select_flags[m]) + { + /* ignore input field, leaving column as NULL */ + continue; + } + + cstate->cur_attname = NameStr(att->attname); + cstate->cur_attval = string; + + if (string != NULL) + nulls[m] = false; + + if (cstate->defaults[m]) + { + Assert(econtext != NULL); + Assert(CurrentMemoryContext == econtext->ecxt_per_tuple_memory); + + values[m] = ExecEvalExpr(defexprs[m], econtext, &nulls[m]); + } + else if (!CopyConvertJsonAttribute(cstate, att, m, string, + &values[m], &nulls[m])) + { + Assert(cstate->opts.on_error != COPY_ON_ERROR_STOP); + + if (cstate->opts.on_error == COPY_ON_ERROR_IGNORE) + cstate->num_errors++; + else if (cstate->opts.on_error == COPY_ON_ERROR_SET_NULL) + { + cstate->escontext->error_occurred = false; + + Assert(cstate->domain_with_constraint != NULL); + + if (!cstate->domain_with_constraint[m] || + InputFunctionCallSafe(&in_functions[m], + NULL, + typioparams[m], + att->atttypmod, + (Node *) cstate->escontext, + &values[m])) + { + nulls[m] = true; + values[m] = (Datum) 0; + } + else + ereport(ERROR, + errcode(ERRCODE_NOT_NULL_VIOLATION), + errmsg("domain %s does not allow null values", + format_type_be(typioparams[m])), + errdetail("ON_ERROR SET_NULL cannot be applied because column \"%s\" (domain %s) does not accept null values.", + cstate->cur_attname, + format_type_be(typioparams[m])), + errdatatype(typioparams[m])); + + if (!current_row_erroneous) + { + current_row_erroneous = true; + cstate->num_errors++; + } + } + + if (cstate->opts.log_verbosity == COPY_LOG_VERBOSITY_VERBOSE) + { + Assert(!cstate->relname_only); + cstate->relname_only = true; + + if (cstate->cur_attval) + { + char *attval = CopyLimitPrintoutLength(cstate->cur_attval); + + if (cstate->opts.on_error == COPY_ON_ERROR_IGNORE) + ereport(NOTICE, + errmsg("skipping row due to data type incompatibility at line %" PRIu64 " for column \"%s\": \"%s\"", + cstate->cur_lineno, + cstate->cur_attname, + attval)); + else if (cstate->opts.on_error == COPY_ON_ERROR_SET_NULL) + ereport(NOTICE, + errmsg("setting to null due to data type incompatibility at line %" PRIu64 " for column \"%s\": \"%s\"", + cstate->cur_lineno, + cstate->cur_attname, + attval)); + pfree(attval); + } + else if (cstate->opts.on_error == COPY_ON_ERROR_IGNORE) + ereport(NOTICE, + errmsg("skipping row due to data type incompatibility at line %" PRIu64 " for column \"%s\": null input", + cstate->cur_lineno, + cstate->cur_attname)); + cstate->relname_only = false; + } + + if (cstate->opts.on_error == COPY_ON_ERROR_IGNORE) + return true; + else if (cstate->opts.on_error == COPY_ON_ERROR_SET_NULL) + continue; + } + + cstate->cur_attname = NULL; + cstate->cur_attval = NULL; + } + + Assert(fieldno == fldct); + + return true; +} + /* * Read the next input line and stash it in line_buf. * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 0274d892f2e..69c2f72319b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -20624,7 +20624,7 @@ ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNu * SET EXPRESSION would need to check whether the column is * used in partition keys). Seems safer to prohibit for now. */ - if (TupleDescAttr(RelationGetDescr(rel), attno - 1)->attgenerated) + if (TupleDescCompactAttr(RelationGetDescr(rel), attno - 1)->attgenerated) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), errmsg("cannot use generated column in partition key"), diff --git a/src/include/commands/copyfrom_internal.h b/src/include/commands/copyfrom_internal.h index 9d3e244ee55..4b9d296abd5 100644 --- a/src/include/commands/copyfrom_internal.h +++ b/src/include/commands/copyfrom_internal.h @@ -17,6 +17,53 @@ #include "commands/copy.h" #include "commands/trigger.h" #include "nodes/miscnodes.h" +#include "utils/hsearch.h" + +/* + * State for COPY FROM JSON format. line_buf holds text in server encoding. + * parse_pos is the scan cursor, and [row_text_start, row_text_end) identifies + * the completed row to parse in place. Consumed rows are discarded on refill. + */ +typedef enum CopyJsonScanState +{ + COPY_JSON_BEFORE_ARRAY, /* skip whitespace, expect '[' or '{' */ + COPY_JSON_BEFORE_OBJECT, /* after {...} row when input is {...}{...} + * form */ + COPY_JSON_IN_ARRAY, /* inside [...], expect value/comma/']' */ + COPY_JSON_IN_OBJECT, /* inside {...}, track depth to find matching + * '}' */ + COPY_JSON_IN_STRING, /* inside "...", skip until unescaped '"' */ + COPY_JSON_IN_STRING_ESC, /* saw '\' in string, consume escape sequence */ + COPY_JSON_ARRAY_END /* saw ']', no more rows */ +} CopyJsonScanState; + +typedef enum CopyJsonArrayScanState +{ + COPY_JSON_ARRAY_EXPECT_VALUE_OR_END, /* start of array: expect '{' or + * ']' */ + COPY_JSON_ARRAY_EXPECT_VALUE, /* after comma: expect next object */ + COPY_JSON_ARRAY_EXPECT_COMMA_OR_END /* after object: expect ',' or ']' */ +} CopyJsonArrayScanState; + +typedef struct CopyFromJsonState +{ + CopyJsonScanState parse_state; + CopyJsonArrayScanState array_parse_state; + int object_depth; /* brace/bracket depth: 1 = in target object */ + bool array_mode; /* have we seen the opening '[' */ + int parse_pos; /* scan cursor in line_buf */ + int row_text_start; /* set while completing a row; else -1 */ + int row_text_end; /* byte offset just past row's closing '}' */ + HTAB *attribute_map; /* column name to raw_fields index */ + Oid *base_types; /* base type of each table attribute */ + void **conversion_cache; /* per-attribute JSON conversion metadata */ +} CopyFromJsonState; + +typedef struct CopyJsonAttribute +{ + char name[NAMEDATALEN]; + int fieldno; +} CopyJsonAttribute; /* * Represents the different source cases we need to worry about at @@ -189,6 +236,9 @@ typedef struct CopyFromStateData #define RAW_BUF_BYTES(cstate) ((cstate)->raw_buf_len - (cstate)->raw_buf_index) uint64 bytes_processed; /* number of bytes processed so far */ + + /* Format-specific private data */ + void *format_private; } CopyFromStateData; extern void ReceiveCopyBegin(CopyFromState cstate); @@ -201,5 +251,7 @@ extern bool CopyFromCSVOneRow(CopyFromState cstate, ExprContext *econtext, Datum *values, bool *nulls); extern bool CopyFromBinaryOneRow(CopyFromState cstate, ExprContext *econtext, Datum *values, bool *nulls); +extern bool CopyFromJsonOneRow(CopyFromState cstate, ExprContext *econtext, + Datum *values, bool *nulls); #endif /* COPYFROM_INTERNAL_H */ diff --git a/src/test/modules/test_copy_callbacks/expected/test_copy_callbacks.out b/src/test/modules/test_copy_callbacks/expected/test_copy_callbacks.out index 93ebeef1301..117c7175211 100644 --- a/src/test/modules/test_copy_callbacks/expected/test_copy_callbacks.out +++ b/src/test/modules/test_copy_callbacks/expected/test_copy_callbacks.out @@ -11,3 +11,86 @@ NOTICE: COPY TO callback has processed 3 rows (1 row) +CREATE TABLE public.test_json (a text); +-- Delimiters, nesting and string escapes can span callback reads. +SELECT test_copy_from_json_callback('test_json', + '[{"a":"brace } and quote \"","ignored":[{},[]]},{"a":"backslash \\"}]', 1); + test_copy_from_json_callback +------------------------------ + 2 +(1 row) + +SELECT * FROM test_json; + a +--------------------- + brace } and quote " + backslash \ +(2 rows) + +TRUNCATE test_json; +-- Many rows share a buffer, with a partial row at the next refill. +SELECT test_copy_from_json_callback('test_json', repeat('{"a":"x"}', 8192), 65536); + test_copy_from_json_callback +------------------------------ + 8192 +(1 row) + +SELECT count(*), bool_and(a = 'x') FROM test_json; + count | bool_and +-------+---------- + 8192 | t +(1 row) + +TRUNCATE test_json; +-- Consecutive objects span multiple buffers; consumed rows must be discarded. +SELECT test_copy_from_json_callback('test_json', + repeat('{"a":"' || repeat('x', 70000) || '"}', 4), 65536); + test_copy_from_json_callback +------------------------------ + 4 +(1 row) + +SELECT count(*), bool_and(a = repeat('x', 70000)) FROM test_json; + count | bool_and +-------+---------- + 4 | t +(1 row) + +TRUNCATE test_json; +-- Commas between concatenated objects are rejected identically for all chunks. +SELECT test_copy_from_json_callback('test_json', '{"a":1},{"a":2}', 14); +ERROR: COPY JSON, line 1: invalid input format +DETAIL: Cannot use a comma between concatenated JSON objects; use a JSON array. +CONTEXT: COPY test_json, line 1 +SELECT test_copy_from_json_callback('test_json', '{"a":1},{"a":2}', 7); +ERROR: COPY JSON, line 1: invalid input format +DETAIL: Cannot use a comma between concatenated JSON objects; use a JSON array. +CONTEXT: COPY test_json, line 1 +SELECT count(*) FROM test_json; + count +------- + 0 +(1 row) + +-- Cancellation must be checked before refilling, even without a complete row. +SELECT test_copy_from_json_callback('test_json', repeat(' ', 131072), 65536, true); +ERROR: canceling statement due to user request +CONTEXT: COPY test_json, line 0 +SELECT test_copy_from_json_callback('test_json', '[]' || repeat(' ', 131072), 65536, true); +ERROR: canceling statement due to user request +CONTEXT: COPY test_json, line 0 +SELECT test_copy_from_json_callback('test_json', '{"a":"' || repeat('x', 131072), 65536, true); +ERROR: canceling statement due to user request +CONTEXT: COPY test_json, line 1 +SELECT test_copy_from_json_callback('test_json', '{"a":"ok"}', 1); + test_copy_from_json_callback +------------------------------ + 1 +(1 row) + +SELECT * FROM test_json; + a +---- + ok +(1 row) + diff --git a/src/test/modules/test_copy_callbacks/sql/test_copy_callbacks.sql b/src/test/modules/test_copy_callbacks/sql/test_copy_callbacks.sql index 2deffba635c..887dc3c5145 100644 --- a/src/test/modules/test_copy_callbacks/sql/test_copy_callbacks.sql +++ b/src/test/modules/test_copy_callbacks/sql/test_copy_callbacks.sql @@ -2,3 +2,33 @@ CREATE EXTENSION test_copy_callbacks; CREATE TABLE public.test (a INT, b INT, c INT); INSERT INTO public.test VALUES (1, 2, 3), (12, 34, 56), (123, 456, 789); SELECT test_copy_to_callback('public.test'::pg_catalog.regclass); + +CREATE TABLE public.test_json (a text); +-- Delimiters, nesting and string escapes can span callback reads. +SELECT test_copy_from_json_callback('test_json', + '[{"a":"brace } and quote \"","ignored":[{},[]]},{"a":"backslash \\"}]', 1); +SELECT * FROM test_json; +TRUNCATE test_json; + +-- Many rows share a buffer, with a partial row at the next refill. +SELECT test_copy_from_json_callback('test_json', repeat('{"a":"x"}', 8192), 65536); +SELECT count(*), bool_and(a = 'x') FROM test_json; +TRUNCATE test_json; + +-- Consecutive objects span multiple buffers; consumed rows must be discarded. +SELECT test_copy_from_json_callback('test_json', + repeat('{"a":"' || repeat('x', 70000) || '"}', 4), 65536); +SELECT count(*), bool_and(a = repeat('x', 70000)) FROM test_json; +TRUNCATE test_json; + +-- Commas between concatenated objects are rejected identically for all chunks. +SELECT test_copy_from_json_callback('test_json', '{"a":1},{"a":2}', 14); +SELECT test_copy_from_json_callback('test_json', '{"a":1},{"a":2}', 7); +SELECT count(*) FROM test_json; + +-- Cancellation must be checked before refilling, even without a complete row. +SELECT test_copy_from_json_callback('test_json', repeat(' ', 131072), 65536, true); +SELECT test_copy_from_json_callback('test_json', '[]' || repeat(' ', 131072), 65536, true); +SELECT test_copy_from_json_callback('test_json', '{"a":"' || repeat('x', 131072), 65536, true); +SELECT test_copy_from_json_callback('test_json', '{"a":"ok"}', 1); +SELECT * FROM test_json; diff --git a/src/test/modules/test_copy_callbacks/test_copy_callbacks--1.0.sql b/src/test/modules/test_copy_callbacks/test_copy_callbacks--1.0.sql index 215cf3fad69..fa42a51726d 100644 --- a/src/test/modules/test_copy_callbacks/test_copy_callbacks--1.0.sql +++ b/src/test/modules/test_copy_callbacks/test_copy_callbacks--1.0.sql @@ -6,3 +6,8 @@ CREATE FUNCTION test_copy_to_callback(pg_catalog.regclass) RETURNS pg_catalog.void AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_copy_from_json_callback(pg_catalog.regclass, pg_catalog.text, + pg_catalog.int4, pg_catalog.bool DEFAULT false) + RETURNS pg_catalog.int8 + AS 'MODULE_PATHNAME' LANGUAGE C STRICT; diff --git a/src/test/modules/test_copy_callbacks/test_copy_callbacks.c b/src/test/modules/test_copy_callbacks/test_copy_callbacks.c index f6b113e3e98..6f0dab8acab 100644 --- a/src/test/modules/test_copy_callbacks/test_copy_callbacks.c +++ b/src/test/modules/test_copy_callbacks/test_copy_callbacks.c @@ -17,10 +17,94 @@ #include "access/table.h" #include "commands/copy.h" #include "fmgr.h" +#include "miscadmin.h" +#include "nodes/makefuncs.h" +#include "parser/parse_relation.h" #include "utils/rel.h" +#include "varatt.h" PG_MODULE_MAGIC; +typedef struct TestCopyFromJsonState +{ + const char *data; + int len; + int pos; + int chunk_size; + bool cancel; +} TestCopyFromJsonState; + +static TestCopyFromJsonState *from_json_state; + +static int +from_json_cb(void *data, int minread, int maxread) +{ + TestCopyFromJsonState *state = from_json_state; + int nbytes = Min(state->len - state->pos, + Min(state->chunk_size, maxread)); + + if (state->cancel) + { + /* Request cancellation during the first read, without a timing race. */ + if (state->pos == 0) + { + InterruptPending = true; + QueryCancelPending = true; + } + else + elog(ERROR, "COPY did not process cancellation before reading more data"); + } + + memcpy(data, state->data + state->pos, nbytes); + state->pos += nbytes; + return nbytes; +} + +PG_FUNCTION_INFO_V1(test_copy_from_json_callback); +Datum +test_copy_from_json_callback(PG_FUNCTION_ARGS) +{ + Relation rel = table_open(PG_GETARG_OID(0), RowExclusiveLock); + ParseState *pstate = make_parsestate(NULL); + text *data = PG_GETARG_TEXT_PP(1); + TestCopyFromJsonState state; + TestCopyFromJsonState *saved_state = from_json_state; + volatile uint64 processed = 0; + + state.data = VARDATA_ANY(data); + state.len = VARSIZE_ANY_EXHDR(data); + state.pos = 0; + state.chunk_size = PG_GETARG_INT32(2); + state.cancel = PG_GETARG_BOOL(3); + if (state.chunk_size <= 0) + elog(ERROR, "chunk size must be positive"); + + addRangeTableEntryForRelation(pstate, rel, RowExclusiveLock, + NULL, false, false); + + from_json_state = &state; + PG_TRY(); + { + CopyFromState cstate; + List *options = list_make1(makeDefElem("format", + (Node *) makeString("json"), -1)); + + cstate = BeginCopyFrom(pstate, rel, NULL, NULL, false, + from_json_cb, NIL, options); + processed = CopyFrom(cstate); + EndCopyFrom(cstate); + } + PG_FINALLY(); + { + from_json_state = saved_state; + } + PG_END_TRY(); + + free_parsestate(pstate); + table_close(rel, NoLock); + PG_RETURN_INT64(processed); +} + static void to_cb(void *data, int len) { diff --git a/src/test/regress/expected/copy.out b/src/test/regress/expected/copy.out index 0af0b646921..ffb9958202e 100644 --- a/src/test/regress/expected/copy.out +++ b/src/test/regress/expected/copy.out @@ -144,9 +144,313 @@ LINE 1: copy copytest to stdout (format json, on_error ignore); ^ copy copytest to stdout (format json, reject_limit 1); ERROR: COPY REJECT_LIMIT requires ON_ERROR to be set to IGNORE -copy copytest from stdin(format json); -ERROR: COPY FORMAT JSON is not supported for COPY FROM -- all of the above should yield error +-- COPY FROM JSON: each array element is a row, object keys match column names +create temp table copytest_from_json (like copytest); +copy copytest_from_json (style, test) from stdin (format json); +select * from copytest_from_json order by style collate "C"; + style | test | filler +---------+----------+-------- + DOS | abc\r +| + | def | + Mac | abc\rdef | + Unix | abc +| + | def | + esc\ape | a\r\\r\ +| + | \nb | +(4 rows) + +-- Round trip: COPY TO JSON file, then COPY FROM JSON file +\set copy_json_rt :abs_builddir '/results/copytest_roundtrip.json' +truncate copytest2; +copy copytest to :'copy_json_rt' (format json); +copy copytest2 from :'copy_json_rt' (format json); +select * from copytest except select * from copytest2; + style | test | filler +-------+------+-------- +(0 rows) + +truncate copytest2; +copy copytest to :'copy_json_rt' (format json, force_array true); +copy copytest2 from :'copy_json_rt' (format json); +select * from copytest except select * from copytest2; + style | test | filler +-------+------+-------- +(0 rows) + +-- COPY FROM JSON edge cases: invalid input, non-array, missing required field +copy copytest_from_json from stdin (format json); +ERROR: invalid input format for COPY JSON +DETAIL: Document must begin with "[" or "{". +CONTEXT: COPY copytest_from_json, line 1 +copy copytest_from_json from stdin (format json); +ERROR: invalid input syntax for type json +CONTEXT: COPY copytest_from_json, line 1 +copy copytest_from_json from stdin (format json); +ERROR: COPY JSON, line 1: each array element must be a JSON object +CONTEXT: COPY copytest_from_json, line 1 +copy copytest_from_json from stdin (format json); +ERROR: COPY JSON, line 1: each array element must be a JSON object +CONTEXT: COPY copytest_from_json, line 1 +copy copytest_from_json from stdin (format json); +ERROR: COPY JSON, line 1: invalid input format +DETAIL: Expected "," between array elements. +CONTEXT: COPY copytest_from_json, line 1 +copy copytest_from_json from stdin (format json); +ERROR: COPY JSON, line 1: invalid input format +DETAIL: Cannot use a comma between concatenated JSON objects; use a JSON array. +CONTEXT: COPY copytest_from_json, line 1 +copy copytest_from_json from stdin (format json); +ERROR: invalid input format for COPY JSON +DETAIL: JSON array input was not closed with "]". +CONTEXT: COPY copytest_from_json, line 2 +copy copytest_from_json from stdin (format json); +ERROR: invalid input format for COPY JSON +DETAIL: JSON array input was not closed with "]". +CONTEXT: COPY copytest_from_json, line 2 +copy copytest_from_json from stdin (format json); +ERROR: unexpected end of input in COPY JSON +CONTEXT: COPY copytest_from_json, line 1 +copy copytest_from_json from stdin (format json); +ERROR: unexpected end of input in COPY JSON +CONTEXT: COPY copytest_from_json, line 1 +copy copytest_from_json from stdin (format json); +ERROR: COPY JSON, line 1: each array element must be a JSON object +CONTEXT: COPY copytest_from_json, line 1 +copy copytest_from_json from stdin (format json); +ERROR: invalid input format for COPY JSON +DETAIL: Trailing data after JSON array. +CONTEXT: COPY copytest_from_json, line 1 +copy copytest_from_json from stdin (format json); +ERROR: invalid input format for COPY JSON +DETAIL: Trailing data after JSON array. +CONTEXT: COPY copytest_from_json, line 1 +create temp table copyjson_req (style text NOT NULL, test text); +copy copyjson_req from stdin (format json); +ERROR: null value in column "style" of relation "copyjson_req" violates not-null constraint +DETAIL: Failing row contains (null, only test). +CONTEXT: COPY copyjson_req, line 1 +copy copyjson_req from stdin (format json); +select * from copyjson_req; + style | test +-------+------ + ok | both +(1 row) + +truncate copytest_from_json; +copy copytest_from_json (style, test) from stdin (format json); +select style, test, filler from copytest_from_json; + style | test | filler +-------+------+-------- + a | b | +(1 row) + +\set copy_json_boundary :abs_builddir '/results/copytest_json_boundary.json' +copy (select '[{"style":"' || repeat('x', 65512) || '","test":"a"},{"style":"b","test":"c"}]') to :'copy_json_boundary'; +truncate copytest_from_json; +copy copytest_from_json (style, test) from :'copy_json_boundary' (format json); +select length(style), test from copytest_from_json order by length(style); + length | test +--------+------ + 1 | c + 65512 | a +(2 rows) + +create temp table copyjsontest_scalars (js json, jsb jsonb); +copy copyjsontest_scalars from stdin (format json); +select js, jsb from copyjsontest_scalars; + js | jsb +-------+------- + "foo" | true + false | "bar" +(2 rows) + +-- Field storage must survive large values and exact buffer-size boundaries. +create temp table copyjson_wide (a text, b text); +copy (select 'first' as a, repeat('x', 5000) as b) to :'copy_json_rt' (format json); +copy copyjson_wide from :'copy_json_rt' (format json); +select a = 'first', length(a), b = repeat('x', 5000) from copyjson_wide; + ?column? | length | ?column? +----------+--------+---------- + t | 5 | t +(1 row) + +truncate copyjson_wide; +copy (select repeat('x', 1023) as a) to :'copy_json_rt' (format json); +copy copyjson_wide (a) from :'copy_json_rt' (format json); +select a = repeat('x', 1023), length(a), b is null from copyjson_wide; + ?column? | length | ?column? +----------+--------+---------- + t | 1023 | t +(1 row) + +-- Domains need the same JSON representation as their base types. +create domain pg_temp.copyjson_json as json; +create domain pg_temp.copyjson_jsonb as jsonb; +create domain pg_temp.copyjson_string as json check (json_typeof(value) = 'string'); +create type pg_temp.copyjson_boolean_word as enum ('true', 'false'); +create temp table copyjson_domains + (j pg_temp.copyjson_json, jb pg_temp.copyjson_jsonb, + s pg_temp.copyjson_string, t text, e pg_temp.copyjson_boolean_word); +copy copyjson_domains from stdin (format json); +select j, json_typeof(j), jb, jsonb_typeof(jb), s, t, e from copyjson_domains; + j | json_typeof | jb | jsonb_typeof | s | t | e +--------+-------------+-------+--------------+--------+-------+------- + "true" | string | "123" | string | "null" | true | false + true | boolean | false | boolean | "foo" | false | true +(2 rows) + +copy copyjson_domains (s) from stdin (format json); +ERROR: value for domain copyjson_string violates check constraint "copyjson_string_check" +CONTEXT: COPY copyjson_domains, line 1, column s: "true" +-- Preserve json text, including duplicate keys and values jsonb rejects. +create temp table copyjson_preserve (id int, j json); +insert into copyjson_preserve values + (1, '{"k":1, "k":2}'), (2, '"\u0000"'), (3, '1e1000000'), + (4, '{"nested":["\u0000",{"k":1,"k":2}]}'), (5, '"\u0061"'); +copy copyjson_preserve to :'copy_json_rt' (format json); +truncate copyjson_preserve; +copy copyjson_preserve from :'copy_json_rt' (format json); +select * from copyjson_preserve order by id; + id | j +----+------------------------------------- + 1 | {"k":1, "k":2} + 2 | "\u0000" + 3 | 1e1000000 + 4 | {"nested":["\u0000",{"k":1,"k":2}]} + 5 | "\u0061" +(5 rows) + +-- Recurse into SQL arrays/composites and enforce domain constraints. +create type pg_temp.copyjson_pair as (i int, s text); +create domain pg_temp.copyjson_intarray as int[] check (cardinality(value) <= 2); +create domain pg_temp.copyjson_pair_domain as pg_temp.copyjson_pair + check ((value).i > 0); +create temp table copyjson_structured + (a int[], t text[], p pg_temp.copyjson_pair, + da pg_temp.copyjson_intarray, dp pg_temp.copyjson_pair_domain); +copy copyjson_structured from stdin (format json); +select * from copyjson_structured; + a | t | p | da | dp +---------------+------------+----------------+-------+--------- + {{1,2},{3,4}} | {a,NULL,b} | (1,one) | {1,2} | (2,two) + {} | {} | (,"missing i") | {} | (3,) +(2 rows) + +copy copyjson_structured to :'copy_json_rt' (format json, force_array); +truncate copyjson_structured; +copy copyjson_structured from :'copy_json_rt' (format json); +select * from copyjson_structured; + a | t | p | da | dp +---------------+------------+----------------+-------+--------- + {{1,2},{3,4}} | {a,NULL,b} | (1,one) | {1,2} | (2,two) + {} | {} | (,"missing i") | {} | (3,) +(2 rows) + +-- JSON strings can also contain PostgreSQL array and record literals. +copy copyjson_structured (a, p) from stdin (format json); +select a, p from copyjson_structured where a = array[5,6]; + a | p +-------+----------- + {5,6} | (7,seven) +(1 row) + +copy copyjson_structured (da) from stdin (format json); +ERROR: value for domain copyjson_intarray violates check constraint "copyjson_intarray_check" +CONTEXT: COPY copyjson_structured, line 1, column da: "[1,2,3]" +copy copyjson_structured (dp) from stdin (format json); +ERROR: value for domain copyjson_pair_domain violates check constraint "copyjson_pair_domain_check" +CONTEXT: COPY copyjson_structured, line 1, column dp: "{"i":0,"s":"invalid"}" +-- Soft conversion errors still work for strings and nested values. +truncate copyjson_structured; +copy copyjson_structured (a, p) from stdin (format json, on_error ignore); +NOTICE: 2 rows were skipped due to data type incompatibility +copy copyjson_structured (a, p) from stdin (format json, on_error set_null); +NOTICE: in 1 row, columns were set to null due to data type incompatibility +select a, p from copyjson_structured; + a | p +-------+------------- + {8,9} | (10,ten) + | (11,eleven) +(2 rows) + +create domain pg_temp.copyjson_required_array as int[] not null; +create temp table copyjson_required (a pg_temp.copyjson_required_array); +copy copyjson_required from stdin (format json, on_error set_null); +ERROR: domain copyjson_required_array does not allow null values +DETAIL: ON_ERROR SET_NULL cannot be applied because column "a" (domain copyjson_required_array) does not accept null values. +CONTEXT: COPY copyjson_required, line 1, column a: "["bad"]" +copy copyjson_required from stdin (format json); +ERROR: domain copyjson_required_array does not allow null values +CONTEXT: COPY copyjson_required, line 1, column a: null input +create temp table copyjson_soft_strings (t text, j jsonb); +copy copyjson_soft_strings from stdin (format json, on_error ignore); +NOTICE: 2 rows were skipped due to data type incompatibility +select * from copyjson_soft_strings; + t | j +---+------ + a | true +(1 row) + +copy copyjson_soft_strings from stdin (format json, on_error set_null); +NOTICE: in 1 row, columns were set to null due to data type incompatibility +select t is null, j is null from copyjson_soft_strings; + ?column? | ?column? +----------+---------- + f | f + t | t +(2 rows) + +-- Match escaped names, take the last duplicate, and ignore unselected values. +create temp table copyjson_names (ab int, other text default 'default'); +copy copyjson_names (ab) from stdin (format json); +select * from copyjson_names; + ab | other +----+--------- + 2 | default + | default +(2 rows) + +-- Only space, horizontal tab, CR and LF are JSON whitespace. +-- Use psql output to write the control characters without COPY escaping them. +select E'\v[{"ab":1}]' as bad_json \gset +\o :copy_json_rt +\qecho :bad_json +\o +copy copyjson_names from :'copy_json_rt' (format json); +ERROR: invalid input format for COPY JSON +DETAIL: Document must begin with "[" or "{". +CONTEXT: COPY copyjson_names, line 1 +select E'[\v{"ab":1}]' as bad_json \gset +\o :copy_json_rt +\qecho :bad_json +\o +copy copyjson_names from :'copy_json_rt' (format json); +ERROR: COPY JSON, line 1: each array element must be a JSON object +CONTEXT: COPY copyjson_names, line 1 +select E'{"ab":1}\f{"ab":2}' as bad_json \gset +\o :copy_json_rt +\qecho :bad_json +\o +copy copyjson_names from :'copy_json_rt' (format json); +ERROR: COPY JSON, line 1: invalid input format +DETAIL: Expected "{" to start the next row object. +CONTEXT: COPY copyjson_names, line 1 +select E'[{"ab":1}]\v' as bad_json \gset +\o :copy_json_rt +\qecho :bad_json +\o +copy copyjson_names from :'copy_json_rt' (format json); +ERROR: invalid input format for COPY JSON +DETAIL: Trailing data after JSON array. +CONTEXT: COPY copyjson_names, line 1 +select E'[{"ab":1}\f]' as bad_json \gset +\o :copy_json_rt +\qecho :bad_json +\o +copy copyjson_names from :'copy_json_rt' (format json); +ERROR: COPY JSON, line 1: each array element must be a JSON object +CONTEXT: COPY copyjson_names, line 1 -- column list with json format copy copytest (style, test, filler) to stdout (format json); {"style":"DOS","test":"abc\r\ndef","filler":1} diff --git a/src/test/regress/sql/copy.sql b/src/test/regress/sql/copy.sql index 14da3c5ec37..1628334d818 100644 --- a/src/test/regress/sql/copy.sql +++ b/src/test/regress/sql/copy.sql @@ -114,10 +114,225 @@ copy copytest to stdout (format json, force_not_null *); copy copytest to stdout (format json, force_null *); copy copytest to stdout (format json, on_error ignore); copy copytest to stdout (format json, reject_limit 1); -copy copytest from stdin(format json); -\. -- all of the above should yield error +-- COPY FROM JSON: each array element is a row, object keys match column names +create temp table copytest_from_json (like copytest); +copy copytest_from_json (style, test) from stdin (format json); +[ {"style":"DOS","test":"abc\r\ndef"} ,{"style":"Unix","test":"abc\ndef"} ,{"style":"Mac","test":"abc\rdef"} ,{"style":"esc\\ape","test":"a\\r\\\r\\\n\\nb"} ] +\. +select * from copytest_from_json order by style collate "C"; + +-- Round trip: COPY TO JSON file, then COPY FROM JSON file +\set copy_json_rt :abs_builddir '/results/copytest_roundtrip.json' +truncate copytest2; +copy copytest to :'copy_json_rt' (format json); +copy copytest2 from :'copy_json_rt' (format json); +select * from copytest except select * from copytest2; + +truncate copytest2; +copy copytest to :'copy_json_rt' (format json, force_array true); +copy copytest2 from :'copy_json_rt' (format json); +select * from copytest except select * from copytest2; + +-- COPY FROM JSON edge cases: invalid input, non-array, missing required field +copy copytest_from_json from stdin (format json); +not valid json +\. +copy copytest_from_json from stdin (format json); +{\} +\. +copy copytest_from_json from stdin (format json); +[1, 2, 3] +\. +copy copytest_from_json from stdin (format json); +[null, true, "string"] +\. +copy copytest_from_json from stdin (format json); +[{"style":"a","test":"b"} {"style":"c","test":"d"}] +\. +copy copytest_from_json from stdin (format json); +{"style":"a","test":"b"}, {"style":"c","test":"d"} +\. +copy copytest_from_json from stdin (format json); +[{"style":"a","test":"b"},{"style":"c","test":"d"} +\. +copy copytest_from_json from stdin (format json); +[{"style":"a","test":"b"},{"style":"c","test":"d"}, +\. +copy copytest_from_json from stdin (format json); +[{"style": +\. +copy copytest_from_json from stdin (format json); +{"style":"unterminated +\. +copy copytest_from_json from stdin (format json); +[{"style":"a","test":"b"},] +\. +copy copytest_from_json from stdin (format json); +[{"style":"a","test":"b"}]{"style":"c","test":"d"} +\. +copy copytest_from_json from stdin (format json); +[{"style":"a","test":"b"}] garbage +\. +create temp table copyjson_req (style text NOT NULL, test text); +copy copyjson_req from stdin (format json); +[{"test":"only test"}] +\. +copy copyjson_req from stdin (format json); +[{"style":"ok","test":"both"}] +\. +select * from copyjson_req; +truncate copytest_from_json; +copy copytest_from_json (style, test) from stdin (format json); +[{"style":"a","test":"b","extra":"ignored","filler":999}] +\. +select style, test, filler from copytest_from_json; + +\set copy_json_boundary :abs_builddir '/results/copytest_json_boundary.json' +copy (select '[{"style":"' || repeat('x', 65512) || '","test":"a"},{"style":"b","test":"c"}]') to :'copy_json_boundary'; +truncate copytest_from_json; +copy copytest_from_json (style, test) from :'copy_json_boundary' (format json); +select length(style), test from copytest_from_json order by length(style); + +create temp table copyjsontest_scalars (js json, jsb jsonb); +copy copyjsontest_scalars from stdin (format json); +[{"js":"foo","jsb":true},{"js":false,"jsb":"bar"}] +\. +select js, jsb from copyjsontest_scalars; + +-- Field storage must survive large values and exact buffer-size boundaries. +create temp table copyjson_wide (a text, b text); +copy (select 'first' as a, repeat('x', 5000) as b) to :'copy_json_rt' (format json); +copy copyjson_wide from :'copy_json_rt' (format json); +select a = 'first', length(a), b = repeat('x', 5000) from copyjson_wide; +truncate copyjson_wide; +copy (select repeat('x', 1023) as a) to :'copy_json_rt' (format json); +copy copyjson_wide (a) from :'copy_json_rt' (format json); +select a = repeat('x', 1023), length(a), b is null from copyjson_wide; + +-- Domains need the same JSON representation as their base types. +create domain pg_temp.copyjson_json as json; +create domain pg_temp.copyjson_jsonb as jsonb; +create domain pg_temp.copyjson_string as json check (json_typeof(value) = 'string'); +create type pg_temp.copyjson_boolean_word as enum ('true', 'false'); +create temp table copyjson_domains + (j pg_temp.copyjson_json, jb pg_temp.copyjson_jsonb, + s pg_temp.copyjson_string, t text, e pg_temp.copyjson_boolean_word); +copy copyjson_domains from stdin (format json); +{"j":"true","jb":"123","s":"null","t":true,"e":false} +{"j":true,"jb":false,"s":"foo","t":false,"e":true} +\. +select j, json_typeof(j), jb, jsonb_typeof(jb), s, t, e from copyjson_domains; +copy copyjson_domains (s) from stdin (format json); +{"s":true} +\. + +-- Preserve json text, including duplicate keys and values jsonb rejects. +create temp table copyjson_preserve (id int, j json); +insert into copyjson_preserve values + (1, '{"k":1, "k":2}'), (2, '"\u0000"'), (3, '1e1000000'), + (4, '{"nested":["\u0000",{"k":1,"k":2}]}'), (5, '"\u0061"'); +copy copyjson_preserve to :'copy_json_rt' (format json); +truncate copyjson_preserve; +copy copyjson_preserve from :'copy_json_rt' (format json); +select * from copyjson_preserve order by id; + +-- Recurse into SQL arrays/composites and enforce domain constraints. +create type pg_temp.copyjson_pair as (i int, s text); +create domain pg_temp.copyjson_intarray as int[] check (cardinality(value) <= 2); +create domain pg_temp.copyjson_pair_domain as pg_temp.copyjson_pair + check ((value).i > 0); +create temp table copyjson_structured + (a int[], t text[], p pg_temp.copyjson_pair, + da pg_temp.copyjson_intarray, dp pg_temp.copyjson_pair_domain); +copy copyjson_structured from stdin (format json); +{"a":[[1,2],[3,4]],"t":["a",null,"b"],"p":{"i":1,"s":"one"},"da":[1,2],"dp":{"i":2,"s":"two"}} +{"a":[],"t":[],"p":{"s":"missing i"},"da":[],"dp":{"i":3,"s":null}} +\. +select * from copyjson_structured; +copy copyjson_structured to :'copy_json_rt' (format json, force_array); +truncate copyjson_structured; +copy copyjson_structured from :'copy_json_rt' (format json); +select * from copyjson_structured; +-- JSON strings can also contain PostgreSQL array and record literals. +copy copyjson_structured (a, p) from stdin (format json); +{"a":"{5,6}","p":"(7,seven)"} +\. +select a, p from copyjson_structured where a = array[5,6]; +copy copyjson_structured (da) from stdin (format json); +{"da":[1,2,3]} +\. +copy copyjson_structured (dp) from stdin (format json); +{"dp":{"i":0,"s":"invalid"}} +\. + +-- Soft conversion errors still work for strings and nested values. +truncate copyjson_structured; +copy copyjson_structured (a, p) from stdin (format json, on_error ignore); +[{"a":[1,"bad"]},{"p":{"i":"bad"}},{"a":[8,9],"p":{"i":10,"s":"ten"}}] +\. +copy copyjson_structured (a, p) from stdin (format json, on_error set_null); +{"a":[1,"bad"],"p":{"i":11,"s":"eleven"}} +\. +select a, p from copyjson_structured; +create domain pg_temp.copyjson_required_array as int[] not null; +create temp table copyjson_required (a pg_temp.copyjson_required_array); +copy copyjson_required from stdin (format json, on_error set_null); +{"a":["bad"]} +\. +copy copyjson_required from stdin (format json); +{} +\. + +create temp table copyjson_soft_strings (t text, j jsonb); +copy copyjson_soft_strings from stdin (format json, on_error ignore); +{"t":"\u0000","j":1} +{"t":"ok","j":"\u0000"} +{"t":"\u0061","j":true} +\. +select * from copyjson_soft_strings; +copy copyjson_soft_strings from stdin (format json, on_error set_null); +{"t":"\u0000","j":1e1000000} +\. +select t is null, j is null from copyjson_soft_strings; + +-- Match escaped names, take the last duplicate, and ignore unselected values. +create temp table copyjson_names (ab int, other text default 'default'); +copy copyjson_names (ab) from stdin (format json); +{"ab":1,"a\u0062":2,"ignored":"\u0000","huge":1e1000000} +{"ab":3,"ab":null} +\. +select * from copyjson_names; + +-- Only space, horizontal tab, CR and LF are JSON whitespace. +-- Use psql output to write the control characters without COPY escaping them. +select E'\v[{"ab":1}]' as bad_json \gset +\o :copy_json_rt +\qecho :bad_json +\o +copy copyjson_names from :'copy_json_rt' (format json); +select E'[\v{"ab":1}]' as bad_json \gset +\o :copy_json_rt +\qecho :bad_json +\o +copy copyjson_names from :'copy_json_rt' (format json); +select E'{"ab":1}\f{"ab":2}' as bad_json \gset +\o :copy_json_rt +\qecho :bad_json +\o +copy copyjson_names from :'copy_json_rt' (format json); +select E'[{"ab":1}]\v' as bad_json \gset +\o :copy_json_rt +\qecho :bad_json +\o +copy copyjson_names from :'copy_json_rt' (format json); +select E'[{"ab":1}\f]' as bad_json \gset +\o :copy_json_rt +\qecho :bad_json +\o +copy copyjson_names from :'copy_json_rt' (format json); + -- column list with json format copy copytest (style, test, filler) to stdout (format json); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 656f1f60862..583a27d4579 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -544,10 +544,15 @@ CookedConstraint CopyDest CopyFormat CopyFormatOptions +CopyFromJsonParseState +CopyFromJsonState CopyFromRoutine CopyFromState CopyFromStateData CopyInsertMethod +CopyJsonArrayScanState +CopyJsonAttribute +CopyJsonScanState CopyLogVerbosityChoice CopyMethod CopyMultiInsertBuffer @@ -3161,6 +3166,7 @@ Tcl_Obj Tcl_Size Tcl_Time TempNamespaceStatus +TestCopyFromJsonState TestCustomScanState TestDSMRegistryHashEntry TestDSMRegistryStruct -- 2.39.5 (Apple Git-154)