From d3be52fe965bb5cdee20f3839d93e108c7a16003 Mon Sep 17 00:00:00 2001 From: Joel Jacobson Date: Thu, 7 Nov 2024 14:35:40 +0100 Subject: [PATCH 2/3] Add COPY format 'list' --- doc/src/sgml/ref/copy.sgml | 57 ++++++++- src/backend/commands/copy.c | 86 +++++++++----- src/backend/commands/copyfrom.c | 7 ++ src/backend/commands/copyfromparse.c | 172 +++++++++++++++++++++++++-- src/backend/commands/copyto.c | 62 +++++++++- src/bin/psql/tab-complete.in.c | 2 +- src/include/commands/copy.h | 1 + src/test/regress/expected/copy.out | 33 +++++ src/test/regress/expected/copy2.out | 33 ++++- src/test/regress/sql/copy.sql | 18 +++ src/test/regress/sql/copy2.sql | 19 ++- 11 files changed, 442 insertions(+), 48 deletions(-) diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml index 8394402f096..f71119ba6f5 100644 --- a/doc/src/sgml/ref/copy.sgml +++ b/doc/src/sgml/ref/copy.sgml @@ -218,8 +218,9 @@ COPY { table_name [ ( Selects the data format to be read or written: text, - csv (Comma Separated Values), - or binary. + CSV (Comma Separated Values), + binary, + or list The default is text. See below for details. @@ -257,7 +258,8 @@ COPY { table_name [ ( CSV format. This must be a single one-byte character. - This option is not allowed when using binary format. + This option is allowed only when using text or + CSV format. @@ -271,7 +273,8 @@ COPY { table_name [ ( CSV format. You might prefer an empty string even in text format for cases where you don't want to distinguish nulls from empty strings. - This option is not allowed when using binary format. + This option is allowed only when using text or + CSV format. @@ -294,7 +297,7 @@ COPY { table_name [ ( COPY FROM, and only when - not using binary format. + using text or CSV format. @@ -400,7 +403,8 @@ COPY { table_name [ ( The ignore option is applicable only for COPY FROM - when the FORMAT is text or csv. + when the FORMAT is text, + CSV or list. A NOTICE message containing the ignored row count is @@ -893,6 +897,47 @@ COPY count + + List Format + + + This format option is used for importing and exporting files containing + unstructured text, where each line is treated as a single field. It is + useful for data that does not conform to a structured, tabular format and + lacks delimiters. + + + + In the list format, each line of the input or output is + considered a complete value without any field separation. There are no + field delimiters, and all characters are taken literally. There is no + special handling for quotes, backslashes, or escape sequences. All + characters, including whitespace and special characters, are preserved + exactly as they appear in the file. However, it's important to note that + the text is still interpreted according to the specified ENCODING + option or the current client encoding for input, and encoded using the + specified ENCODING or the current client encoding for output. + + + + When using this format, the COPY command must specify + exactly one column. Specifying multiple columns will result in an error. + If the table has multiple columns and no column list is provided, an error + will occur. + + + + The list format does not distinguish a NULL + value from an empty string. Empty lines are imported as empty strings, not + as NULL values. + + + + Encoding works the same as in the text and CSV formats. + + + + Binary Format diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c index b7e819de408..3b98a8e7db1 100644 --- a/src/backend/commands/copy.c +++ b/src/backend/commands/copy.c @@ -516,6 +516,8 @@ ProcessCopyOptions(ParseState *pstate, opts_out->format = COPY_FORMAT_CSV; else if (strcmp(fmt, "binary") == 0) opts_out->format = COPY_FORMAT_BINARY; + else if (strcmp(fmt, "list") == 0) + opts_out->format = COPY_FORMAT_LIST; else ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), @@ -681,23 +683,69 @@ ProcessCopyOptions(ParseState *pstate, /*- translator: %s is the name of a COPY option, e.g. ON_ERROR */ errmsg("cannot specify %s in BINARY mode", "DELIMITER"))); + if (opts_out->format == COPY_FORMAT_LIST && opts_out->delim) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + /*- translator: %s is the name of a COPY option, e.g. ON_ERROR */ + errmsg("cannot specify %s in LIST mode", "DELIMITER"))); + if (opts_out->format == COPY_FORMAT_BINARY && opts_out->null_print) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("cannot specify %s in BINARY mode", "NULL"))); + if (opts_out->format == COPY_FORMAT_LIST && opts_out->null_print) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot specify %s in LIST mode", "NULL"))); + if (opts_out->format == COPY_FORMAT_BINARY && opts_out->default_print) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("cannot specify %s in BINARY mode", "DEFAULT"))); + if (opts_out->format == COPY_FORMAT_LIST && opts_out->default_print) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot specify %s in LIST mode", "DEFAULT"))); + + if (opts_out->delim) + { + /* Only single-byte delimiter strings are supported. */ + if (strlen(opts_out->delim) != 1) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("COPY delimiter must be a single one-byte character"))); + + /* Disallow end-of-line characters */ + if (strchr(opts_out->delim, '\r') != NULL || + strchr(opts_out->delim, '\n') != NULL) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("COPY delimiter cannot be newline or carriage return"))); + } /* Set defaults for omitted options */ - if (!opts_out->delim) - opts_out->delim = opts_out->format == COPY_FORMAT_CSV ? "," : "\t"; + else if (opts_out->format == COPY_FORMAT_CSV) + opts_out->delim = ","; + else if (opts_out->format == COPY_FORMAT_TEXT) + opts_out->delim = "\t"; - if (!opts_out->null_print) - opts_out->null_print = opts_out->format == COPY_FORMAT_CSV ? "" : "\\N"; - opts_out->null_print_len = strlen(opts_out->null_print); + if (opts_out->null_print) + { + if (strchr(opts_out->null_print, '\r') != NULL || + strchr(opts_out->null_print, '\n') != NULL) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("COPY null representation cannot use newline or carriage return"))); + + } + else if (opts_out->format == COPY_FORMAT_CSV) + opts_out->null_print = ""; + else if (opts_out->format == COPY_FORMAT_TEXT) + opts_out->null_print = "\\N"; + + if (opts_out->null_print) + opts_out->null_print_len = strlen(opts_out->null_print); if (opts_out->format == COPY_FORMAT_CSV) { @@ -707,25 +755,6 @@ ProcessCopyOptions(ParseState *pstate, opts_out->escape = opts_out->quote; } - /* Only single-byte delimiter strings are supported. */ - if (strlen(opts_out->delim) != 1) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("COPY delimiter must be a single one-byte character"))); - - /* Disallow end-of-line characters */ - if (strchr(opts_out->delim, '\r') != NULL || - strchr(opts_out->delim, '\n') != NULL) - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("COPY delimiter cannot be newline or carriage return"))); - - if (strchr(opts_out->null_print, '\r') != NULL || - strchr(opts_out->null_print, '\n') != NULL) - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("COPY null representation cannot use newline or carriage return"))); - if (opts_out->default_print) { opts_out->default_print_len = strlen(opts_out->default_print); @@ -738,7 +767,7 @@ ProcessCopyOptions(ParseState *pstate, } /* - * Disallow unsafe delimiter characters in non-CSV mode. We can't allow + * Disallow unsafe delimiter characters in text mode. We can't allow * backslash because it would be ambiguous. We can't allow the other * cases because data characters matching the delimiter must be * backslashed, and certain backslash combinations are interpreted @@ -747,7 +776,7 @@ ProcessCopyOptions(ParseState *pstate, * future-proofing. Likewise we disallow all digits though only octal * digits are actually dangerous. */ - if (opts_out->format != COPY_FORMAT_CSV && + if (opts_out->format == COPY_FORMAT_TEXT && strchr("\\.abcdefghijklmnopqrstuvwxyz0123456789", opts_out->delim[0]) != NULL) ereport(ERROR, @@ -839,7 +868,8 @@ ProcessCopyOptions(ParseState *pstate, "COPY TO"))); /* Don't allow the delimiter to appear in the null string. */ - if (strchr(opts_out->null_print, opts_out->delim[0]) != NULL) + if (opts_out->delim && opts_out->null_print && + strchr(opts_out->null_print, opts_out->delim[0]) != NULL) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), /*- translator: %s is the name of a COPY option, e.g. NULL */ @@ -875,7 +905,7 @@ ProcessCopyOptions(ParseState *pstate, "COPY TO"))); /* Don't allow the delimiter to appear in the default string. */ - if (strchr(opts_out->default_print, opts_out->delim[0]) != NULL) + if (opts_out->delim && strchr(opts_out->default_print, opts_out->delim[0]) != NULL) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), /*- translator: %s is the name of a COPY option, e.g. NULL */ diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c index f350a4ff976..af2b3f3d11f 100644 --- a/src/backend/commands/copyfrom.c +++ b/src/backend/commands/copyfrom.c @@ -1438,6 +1438,13 @@ BeginCopyFrom(ParseState *pstate, /* Generate or convert list of attributes to process */ cstate->attnumlist = CopyGetAttnums(tupDesc, cstate->rel, attnamelist); + /* Enforce single column requirement for 'list' format */ + if (cstate->opts.format == COPY_FORMAT_LIST && + list_length(cstate->attnumlist) != 1) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("COPY with format 'list' must specify exactly one column"))); + num_phys_attrs = tupDesc->natts; /* Convert FORCE_NOT_NULL name list to per-column flags, check validity */ diff --git a/src/backend/commands/copyfromparse.c b/src/backend/commands/copyfromparse.c index 51eb14d7432..f82fd4c1ed4 100644 --- a/src/backend/commands/copyfromparse.c +++ b/src/backend/commands/copyfromparse.c @@ -7,7 +7,7 @@ * formats. The main entry point is NextCopyFrom(), which parses the * next input line and returns it as Datums. * - * In text/CSV mode, the parsing happens in multiple stages: + * In text/CSV/list mode, the parsing happens in multiple stages: * * [data source] --> raw_buf --> input_buf --> line_buf --> attribute_buf * 1. 2. 3. 4. @@ -28,7 +28,10 @@ * 4. CopyReadAttributesText/CSV() function takes the input line from * 'line_buf', and splits it into fields, unescaping the data as required. * The fields are stored in 'attribute_buf', and 'raw_fields' array holds - * pointers to each field. + * pointers to each field. (text/csv modes only) + * + * In list mode, the fourth stage is skipped because the entire line is + * treated as a list field, making field splitting unnecessary. * * If encoding conversion is not required, a shortcut is taken in step 2 to * avoid copying the data unnecessarily. The 'input_buf' pointer is set to @@ -142,6 +145,7 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0"; /* non-export function prototypes */ static bool CopyReadLine(CopyFromState cstate); static bool CopyReadLineText(CopyFromState cstate); +static bool CopyReadLineList(CopyFromState cstate); static int CopyReadAttributesText(CopyFromState cstate); static int CopyReadAttributesCSV(CopyFromState cstate); static Datum CopyReadBinaryAttribute(CopyFromState cstate, FmgrInfo *flinfo, @@ -731,7 +735,7 @@ CopyReadBinaryData(CopyFromState cstate, char *dest, int nbytes) } /* - * Read raw fields in the next line for COPY FROM in text or csv mode. + * Read raw fields in the next line for COPY FROM in text, csv, or list mode. * Return false if no more lines. * * An internal temporary buffer is returned via 'fields'. It is valid until @@ -747,7 +751,7 @@ NextCopyFromRawFields(CopyFromState cstate, char ***fields, int *nfields) int fldct; bool done; - /* only available for text or csv input */ + /* only available for text, csv, or list input */ Assert(cstate->opts.format != COPY_FORMAT_BINARY); /* on input check that the header line is correct if needed */ @@ -767,8 +771,16 @@ NextCopyFromRawFields(CopyFromState cstate, char ***fields, int *nfields) if (cstate->opts.format == COPY_FORMAT_CSV) fldct = CopyReadAttributesCSV(cstate); - else + else if (cstate->opts.format == COPY_FORMAT_TEXT) fldct = CopyReadAttributesText(cstate); + else + { + Assert(cstate->opts.format == COPY_FORMAT_LIST); + Assert(cstate->max_fields == 1); + /* Point raw_fields directly to line_buf data */ + cstate->raw_fields[0] = cstate->line_buf.data; + fldct = 1; + } if (fldct != list_length(cstate->attnumlist)) ereport(ERROR, @@ -822,8 +834,16 @@ NextCopyFromRawFields(CopyFromState cstate, char ***fields, int *nfields) /* Parse the line into de-escaped field values */ if (cstate->opts.format == COPY_FORMAT_CSV) fldct = CopyReadAttributesCSV(cstate); - else + else if (cstate->opts.format == COPY_FORMAT_TEXT) fldct = CopyReadAttributesText(cstate); + else + { + Assert(cstate->opts.format == COPY_FORMAT_LIST); + Assert(cstate->max_fields == 1); + /* Point raw_fields directly to line_buf data */ + cstate->raw_fields[0] = cstate->line_buf.data; + fldct = 1; + } *fields = cstate->raw_fields; *nfields = fldct; @@ -1095,7 +1115,10 @@ CopyReadLine(CopyFromState cstate) cstate->line_buf_valid = false; /* Parse data and transfer into line_buf */ - result = CopyReadLineText(cstate); + if (cstate->opts.format == COPY_FORMAT_LIST) + result = CopyReadLineList(cstate); + else + result = CopyReadLineText(cstate); if (result) { @@ -1461,6 +1484,140 @@ CopyReadLineText(CopyFromState cstate) return result; } +/* + * CopyReadLineList - inner loop of CopyReadLine for list text mode + */ +static bool +CopyReadLineList(CopyFromState cstate) +{ + char *copy_input_buf; + int input_buf_ptr; + int copy_buf_len; + bool need_data = false; + bool hit_eof = false; + bool result = false; + + /* + * The objective of this loop is to transfer the entire next input line + * into line_buf. We only care for detecting newlines (\r and/or \n). All + * other characters are treated as regular data. + * + * For speed, we try to move data from input_buf to line_buf in chunks + * rather than one character at a time. input_buf_ptr points to the next + * character to examine; any characters from input_buf_index to + * input_buf_ptr have been determined to be part of the line, but not yet + * transferred to line_buf. + * + * For a little extra speed within the loop, we copy input_buf and + * input_buf_len into local variables. + */ + copy_input_buf = cstate->input_buf; + input_buf_ptr = cstate->input_buf_index; + copy_buf_len = cstate->input_buf_len; + + for (;;) + { + int prev_raw_ptr; + char c; + + /* + * Load more data if needed. + */ + if (input_buf_ptr >= copy_buf_len || need_data) + { + REFILL_LINEBUF; + + CopyLoadInputBuf(cstate); + /* update our local variables */ + hit_eof = cstate->input_reached_eof; + input_buf_ptr = cstate->input_buf_index; + copy_buf_len = cstate->input_buf_len; + + /* + * If we are completely out of data, break out of the loop, + * reporting EOF. + */ + if (INPUT_BUF_BYTES(cstate) <= 0) + { + result = true; + break; + } + need_data = false; + } + + /* OK to fetch a character */ + prev_raw_ptr = input_buf_ptr; + c = copy_input_buf[input_buf_ptr++]; + + /* Process \r */ + if (c == '\r') + { + /* Check for \r\n on first line, _and_ handle \r\n. */ + if (cstate->eol_type == EOL_UNKNOWN || + cstate->eol_type == EOL_CRNL) + { + /* + * If need more data, go back to loop top to load it. + * + * Note that if we are at EOF, c will wind up as '\0' because + * of the guaranteed pad of input_buf. + */ + IF_NEED_REFILL_AND_NOT_EOF_CONTINUE(0); + + /* get next char */ + c = copy_input_buf[input_buf_ptr]; + + if (c == '\n') + { + input_buf_ptr++; /* eat newline */ + cstate->eol_type = EOL_CRNL; /* in case not set yet */ + } + else + { + if (cstate->eol_type == EOL_CRNL) + ereport(ERROR, + (errcode(ERRCODE_BAD_COPY_FILE_FORMAT), + errmsg("end-of-copy marker does not match previous newline style"))); + + /* + * if we got here, it is the first line and we didn't find + * \n, so don't consume the peeked character + */ + cstate->eol_type = EOL_CR; + } + } + else if (cstate->eol_type == EOL_NL) + ereport(ERROR, + (errcode(ERRCODE_BAD_COPY_FILE_FORMAT), + errmsg("end-of-copy marker does not match previous newline style"))); + /* If reach here, we have found the line terminator */ + break; + } + + /* Process \n */ + if (c == '\n') + { + if (cstate->eol_type == EOL_CR || cstate->eol_type == EOL_CRNL) + ereport(ERROR, + (errcode(ERRCODE_BAD_COPY_FILE_FORMAT), + errmsg("end-of-copy marker does not match previous newline style"))); + cstate->eol_type = EOL_NL; /* in case not set yet */ + /* If reach here, we have found the line terminator */ + break; + } + + /* All other characters are treated as regular data */ + } /* end of outer loop */ + + /* + * Transfer any still-uncopied data to line_buf. + */ + REFILL_LINEBUF; + + return result; +} + + /* * Return decimal value for a hexadecimal digit */ @@ -1937,7 +2094,6 @@ endfield: return fieldno; } - /* * Read a binary attribute */ diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c index 03c9d71d34a..6779e7b1394 100644 --- a/src/backend/commands/copyto.c +++ b/src/backend/commands/copyto.c @@ -113,6 +113,7 @@ static void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot); static void CopyAttributeOutText(CopyToState cstate, const char *string); static void CopyAttributeOutCSV(CopyToState cstate, const char *string, bool use_quote); +static void CopyAttributeOutList(CopyToState cstate, const char *string); /* Low-level communications functions */ static void SendCopyBegin(CopyToState cstate); @@ -574,6 +575,13 @@ BeginCopyTo(ParseState *pstate, /* Generate or convert list of attributes to process */ cstate->attnumlist = CopyGetAttnums(tupDesc, cstate->rel, attnamelist); + /* Enforce single column requirement for 'list' format */ + if (cstate->opts.format == COPY_FORMAT_LIST && + list_length(cstate->attnumlist) != 1) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("COPY with format 'list' must specify exactly one column"))); + num_phys_attrs = tupDesc->natts; /* Convert FORCE_QUOTE name list to per-column flags, check validity */ @@ -839,8 +847,10 @@ DoCopyTo(CopyToState cstate) if (cstate->opts.format == COPY_FORMAT_CSV) CopyAttributeOutCSV(cstate, colname, false); - else + else if (cstate->opts.format == COPY_FORMAT_TEXT) CopyAttributeOutText(cstate, colname); + else if (cstate->opts.format == COPY_FORMAT_LIST) + CopyAttributeOutList(cstate, colname); } CopySendEndOfRow(cstate); @@ -921,7 +931,8 @@ CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot) /* Make sure the tuple is fully deconstructed */ slot_getallattrs(slot); - if (cstate->opts.format != COPY_FORMAT_BINARY) + if (cstate->opts.format == COPY_FORMAT_TEXT || + cstate->opts.format == COPY_FORMAT_CSV) { bool need_delim = false; @@ -949,7 +960,7 @@ CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot) } } } - else + else if (cstate->opts.format == COPY_FORMAT_BINARY) { foreach_int(attnum, cstate->attnumlist) { @@ -969,6 +980,35 @@ CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot) } } } + else if (cstate->opts.format == COPY_FORMAT_LIST) + { + int attnum; + Datum value; + bool isnull; + + /* Assert only one column is being copied */ + Assert(list_length(cstate->attnumlist) == 1); + + attnum = linitial_int(cstate->attnumlist); + value = slot->tts_values[attnum - 1]; + isnull = slot->tts_isnull[attnum - 1]; + + if (!isnull) + { + char *string = OutputFunctionCall(&out_functions[attnum - 1], + value); + + CopyAttributeOutList(cstate, string); + } + /* For 'list' format, we don't send anything for NULL values */ + } + else + { + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("Unsupported COPY format"))); + } + CopySendEndOfRow(cstate); @@ -1223,6 +1263,22 @@ CopyAttributeOutCSV(CopyToState cstate, const char *string, } } +/* + * Send text representation of one attribute for 'list' format. + */ +static void +CopyAttributeOutList(CopyToState cstate, const char *string) +{ + const char *ptr; + + if (cstate->need_transcoding) + ptr = pg_server_to_any(string, strlen(string), cstate->file_encoding); + else + ptr = string; + + CopySendString(cstate, ptr); +} + /* * copy_dest_startup --- executor startup */ diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index fad2277991d..75f312a9ac5 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -3239,7 +3239,7 @@ match_previous_words(int pattern_id, /* Complete COPY FROM|TO filename WITH (FORMAT */ else if (Matches("COPY|\\copy", MatchAny, "FROM|TO", MatchAny, "WITH", "(", "FORMAT")) - COMPLETE_WITH("binary", "csv", "text"); + COMPLETE_WITH("binary", "csv", "text", "single"); /* Complete COPY FROM filename WITH (ON_ERROR */ else if (Matches("COPY|\\copy", MatchAny, "FROM|TO", MatchAny, "WITH", "(", "ON_ERROR")) diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h index c3d1df267f0..44e9934d630 100644 --- a/src/include/commands/copy.h +++ b/src/include/commands/copy.h @@ -59,6 +59,7 @@ typedef enum CopyFormat COPY_FORMAT_TEXT = 0, COPY_FORMAT_BINARY, COPY_FORMAT_CSV, + COPY_FORMAT_LIST, } CopyFormat; /* diff --git a/src/test/regress/expected/copy.out b/src/test/regress/expected/copy.out index f554d42c84c..f92775dd573 100644 --- a/src/test/regress/expected/copy.out +++ b/src/test/regress/expected/copy.out @@ -325,3 +325,36 @@ SELECT tableoid::regclass, id % 2 = 0 is_even, count(*) from parted_si GROUP BY (2 rows) DROP TABLE parted_si; +-- Test 'list' format +\set filename :abs_srcdir '/data/emp.data' +create temp table single_copytest (col text); +copy single_copytest from :'filename' (format list); +select col from single_copytest order by col collate "C"; + col +---------------------------------------- + bill 20 (11,10) 1000 sharon + sam 30 (10,5) 2000 bill + sharon 25 (15,12) 1000 sam +(3 rows) + +copy single_copytest to stdout (format list); +sharon 25 (15,12) 1000 sam +sam 30 (10,5) 2000 bill +bill 20 (11,10) 1000 sharon +truncate single_copytest; +copy single_copytest (col) from stdin (format list, header match); +select col from single_copytest order by col collate "C"; + col +-------- + "def", + abc\. + ghi +(3 rows) + +copy single_copytest (col) to stdout (format list, header); +col +abc\. +"def", +ghi +truncate single_copytest; +drop table single_copytest; diff --git a/src/test/regress/expected/copy2.out b/src/test/regress/expected/copy2.out index 64ea33aeae8..fde63fe4eac 100644 --- a/src/test/regress/expected/copy2.out +++ b/src/test/regress/expected/copy2.out @@ -90,6 +90,20 @@ COPY x from stdin (format BINARY, delimiter ','); ERROR: cannot specify DELIMITER in BINARY mode COPY x from stdin (format BINARY, null 'x'); ERROR: cannot specify NULL in BINARY mode +COPY x (c) from stdin (format LIST, null 'x'); +ERROR: cannot specify NULL in LIST mode +COPY x from stdin (format TEXT, escape 'x'); +ERROR: COPY ESCAPE requires CSV mode +COPY x from stdin (format BINARY, escape 'x'); +ERROR: COPY ESCAPE requires CSV mode +COPY x (c) from stdin (format LIST, escape 'x'); +ERROR: COPY ESCAPE requires CSV mode +COPY x from stdin (format TEXT, quote 'x'); +ERROR: COPY QUOTE requires CSV mode +COPY x from stdin (format BINARY, quote 'x'); +ERROR: COPY QUOTE requires CSV mode +COPY x (c) from stdin (format LIST, quote 'x'); +ERROR: COPY QUOTE requires CSV mode COPY x from stdin (format BINARY, on_error ignore); ERROR: only ON_ERROR STOP is allowed in BINARY mode COPY x from stdin (on_error unsupported); @@ -100,6 +114,10 @@ COPY x from stdin (format TEXT, force_quote(a)); ERROR: COPY FORCE_QUOTE requires CSV mode COPY x from stdin (format TEXT, force_quote *); ERROR: COPY FORCE_QUOTE requires CSV mode +COPY x (c) from stdin (format LIST, force_quote(a)); +ERROR: COPY FORCE_QUOTE requires CSV mode +COPY x (c) from stdin (format LIST, force_quote *); +ERROR: COPY FORCE_QUOTE requires CSV mode COPY x from stdin (format CSV, force_quote(a)); ERROR: COPY FORCE_QUOTE cannot be used with COPY FROM COPY x from stdin (format CSV, force_quote *); @@ -108,6 +126,10 @@ COPY x from stdin (format TEXT, force_not_null(a)); ERROR: COPY FORCE_NOT_NULL requires CSV mode COPY x from stdin (format TEXT, force_not_null *); ERROR: COPY FORCE_NOT_NULL requires CSV mode +COPY x (c) from stdin (format LIST, force_not_null(a)); +ERROR: COPY FORCE_NOT_NULL requires CSV mode +COPY x (c) from stdin (format LIST, force_not_null *); +ERROR: COPY FORCE_NOT_NULL requires CSV mode COPY x to stdout (format CSV, force_not_null(a)); ERROR: COPY FORCE_NOT_NULL cannot be used with COPY TO COPY x to stdout (format CSV, force_not_null *); @@ -116,6 +138,10 @@ COPY x from stdin (format TEXT, force_null(a)); ERROR: COPY FORCE_NULL requires CSV mode COPY x from stdin (format TEXT, force_null *); ERROR: COPY FORCE_NULL requires CSV mode +COPY x (c) from stdin (format LIST, force_null(a)); +ERROR: COPY FORCE_NULL requires CSV mode +COPY x (c) from stdin (format LIST, force_null *); +ERROR: COPY FORCE_NULL requires CSV mode COPY x to stdout (format CSV, force_null(a)); ERROR: COPY FORCE_NULL cannot be used with COPY TO COPY x to stdout (format CSV, force_null *); @@ -858,9 +884,11 @@ select id, text_value, ts_value from copy_default; (2 rows) truncate copy_default; --- DEFAULT cannot be used in binary mode +-- DEFAULT cannot be used in binary or list mode copy copy_default from stdin with (format binary, default '\D'); ERROR: cannot specify DEFAULT in BINARY mode +copy copy_default (text_value) from stdin with (format list, default '\D'); +ERROR: cannot specify DEFAULT in LIST mode -- DEFAULT cannot be new line nor carriage return copy copy_default from stdin with (default E'\n'); ERROR: COPY default representation cannot use newline or carriage return @@ -929,3 +957,6 @@ truncate copy_default; -- DEFAULT cannot be used in COPY TO copy (select 1 as test) TO stdout with (default '\D'); ERROR: COPY DEFAULT cannot be used with COPY TO +-- Test list column requirement +copy copy_default from stdin with (format list); +ERROR: COPY with format 'list' must specify exactly one column diff --git a/src/test/regress/sql/copy.sql b/src/test/regress/sql/copy.sql index f1699b66b04..4e40c974f29 100644 --- a/src/test/regress/sql/copy.sql +++ b/src/test/regress/sql/copy.sql @@ -348,3 +348,21 @@ COPY parted_si(id, data) FROM :'filename'; SELECT tableoid::regclass, id % 2 = 0 is_even, count(*) from parted_si GROUP BY 1, 2 ORDER BY 1; DROP TABLE parted_si; + +-- Test 'list' format +\set filename :abs_srcdir '/data/emp.data' +create temp table single_copytest (col text); +copy single_copytest from :'filename' (format list); +select col from single_copytest order by col collate "C"; +copy single_copytest to stdout (format list); +truncate single_copytest; +copy single_copytest (col) from stdin (format list, header match); +col +abc\. +"def", +ghi +\. +select col from single_copytest order by col collate "C"; +copy single_copytest (col) to stdout (format list, header); +truncate single_copytest; +drop table single_copytest; diff --git a/src/test/regress/sql/copy2.sql b/src/test/regress/sql/copy2.sql index 45273557ce0..c7d2ba78565 100644 --- a/src/test/regress/sql/copy2.sql +++ b/src/test/regress/sql/copy2.sql @@ -72,18 +72,31 @@ COPY x from stdin (log_verbosity default, log_verbosity verbose); -- incorrect options COPY x from stdin (format BINARY, delimiter ','); COPY x from stdin (format BINARY, null 'x'); +COPY x (c) from stdin (format LIST, null 'x'); +COPY x from stdin (format TEXT, escape 'x'); +COPY x from stdin (format BINARY, escape 'x'); +COPY x (c) from stdin (format LIST, escape 'x'); +COPY x from stdin (format TEXT, quote 'x'); +COPY x from stdin (format BINARY, quote 'x'); +COPY x (c) from stdin (format LIST, quote 'x'); COPY x from stdin (format BINARY, on_error ignore); COPY x from stdin (on_error unsupported); COPY x from stdin (format TEXT, force_quote(a)); COPY x from stdin (format TEXT, force_quote *); +COPY x (c) from stdin (format LIST, force_quote(a)); +COPY x (c) from stdin (format LIST, force_quote *); COPY x from stdin (format CSV, force_quote(a)); COPY x from stdin (format CSV, force_quote *); COPY x from stdin (format TEXT, force_not_null(a)); COPY x from stdin (format TEXT, force_not_null *); +COPY x (c) from stdin (format LIST, force_not_null(a)); +COPY x (c) from stdin (format LIST, force_not_null *); COPY x to stdout (format CSV, force_not_null(a)); COPY x to stdout (format CSV, force_not_null *); COPY x from stdin (format TEXT, force_null(a)); COPY x from stdin (format TEXT, force_null *); +COPY x (c) from stdin (format LIST, force_null(a)); +COPY x (c) from stdin (format LIST, force_null *); COPY x to stdout (format CSV, force_null(a)); COPY x to stdout (format CSV, force_null *); COPY x to stdout (format BINARY, on_error unsupported); @@ -636,8 +649,9 @@ select id, text_value, ts_value from copy_default; truncate copy_default; --- DEFAULT cannot be used in binary mode +-- DEFAULT cannot be used in binary or list mode copy copy_default from stdin with (format binary, default '\D'); +copy copy_default (text_value) from stdin with (format list, default '\D'); -- DEFAULT cannot be new line nor carriage return copy copy_default from stdin with (default E'\n'); @@ -707,3 +721,6 @@ truncate copy_default; -- DEFAULT cannot be used in COPY TO copy (select 1 as test) TO stdout with (default '\D'); + +-- Test list column requirement +copy copy_default from stdin with (format list); -- 2.45.1