diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 10b97950ec..c984a9cbaa 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -151,6 +151,16 @@ EOF
+
+
+
+
+ Switches to csv output mode. This is equivalent to \pset format
+ csv followed by \pset fieldsep ','.
+
+
+
+
@@ -246,7 +256,7 @@ EOF
Use separator as the
- field separator for unaligned output. This is equivalent to
+ field separator for unaligned and csv outputs. This is equivalent to
\pset fieldsep or \f.
@@ -382,7 +392,7 @@ EOF
Use separator as the
- record separator for unaligned output. This is equivalent to
+ record separator for unaligned and csv outputs. This is equivalent to
\pset recordsep.
@@ -558,7 +568,7 @@ EOF
Set the field separator for unaligned output to a zero byte. This is
- equvalent to \pset fieldsep_zero.
+ equivalent to \pset fieldsep_zero.
@@ -1937,9 +1947,9 @@ Tue Oct 26 21:40:57 CEST 1999
- Sets the field separator for unaligned query output. The default
- is the vertical bar (|). It is equivalent to
- \pset fieldsep.
+ Sets the field separator for unaligned and csv query outputs. The
+ default is the vertical bar (|). It is equivalent
+ to \pset fieldsep.
@@ -2546,8 +2556,8 @@ lo_import 152801
fieldsep
- Specifies the field separator to be used in unaligned output
- format. That way one can create, for example, tab- or
+ Specifies the field separator to be used in unaligned and csv output
+ formats. That way one can create, for example, tab- or
comma-separated output, which other programs might prefer. To
set a tab as field separator, type \pset fieldsep
'\t'. The default field separator is
@@ -2584,9 +2594,13 @@ lo_import 152801
format
- Sets the output format to one of unaligned,
- aligned, wrapped,
- html, asciidoc,
+ Sets the output format to one of
+ unaligned,
+ aligned,
+ csv,
+ wrapped,
+ html,
+ asciidoc,
latex (uses tabular),
latex-longtable, or
troff-ms.
@@ -2601,6 +2615,15 @@ lo_import 152801
format).
+ csv format writes columns separated
+ by fieldsep, applying the CSV quoting rules
+ described in RFC-4180 and compatible with the CSV format
+ of the COPY command.
+ The header with column names is output unless the
+ tuples_only parameter is on.
+ Title and footers are not printed.
+
+
aligned format is the standard, human-readable,
nicely formatted text output; this is the default.
@@ -2747,8 +2770,8 @@ lo_import 152801
recordsep
- Specifies the record (line) separator to use in unaligned
- output format. The default is a newline character.
+ Specifies the record (line) separator to use in unaligned or
+ csv output formats. The default is a newline character.
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 3560318749..f1daf3a88b 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -3603,6 +3603,9 @@ _align2string(enum printFormat in)
case PRINT_TROFF_MS:
return "troff-ms";
break;
+ case PRINT_CSV:
+ return "csv";
+ break;
}
return "unknown";
}
@@ -3658,27 +3661,37 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
{
if (!value)
;
- else if (pg_strncasecmp("unaligned", value, vallen) == 0)
- popt->topt.format = PRINT_UNALIGNED;
else if (pg_strncasecmp("aligned", value, vallen) == 0)
popt->topt.format = PRINT_ALIGNED;
- else if (pg_strncasecmp("wrapped", value, vallen) == 0)
- popt->topt.format = PRINT_WRAPPED;
- else if (pg_strncasecmp("html", value, vallen) == 0)
- popt->topt.format = PRINT_HTML;
else if (pg_strncasecmp("asciidoc", value, vallen) == 0)
popt->topt.format = PRINT_ASCIIDOC;
+ else if (pg_strncasecmp("csv", value, vallen) == 0)
+ popt->topt.format = PRINT_CSV;
+ else if (pg_strncasecmp("html", value, vallen) == 0)
+ popt->topt.format = PRINT_HTML;
else if (pg_strncasecmp("latex", value, vallen) == 0)
popt->topt.format = PRINT_LATEX;
else if (pg_strncasecmp("latex-longtable", value, vallen) == 0)
popt->topt.format = PRINT_LATEX_LONGTABLE;
else if (pg_strncasecmp("troff-ms", value, vallen) == 0)
popt->topt.format = PRINT_TROFF_MS;
+ else if (pg_strncasecmp("unaligned", value, vallen) == 0)
+ popt->topt.format = PRINT_UNALIGNED;
+ else if (pg_strncasecmp("wrapped", value, vallen) == 0)
+ popt->topt.format = PRINT_WRAPPED;
else
{
- psql_error("\\pset: allowed formats are unaligned, aligned, wrapped, html, asciidoc, latex, latex-longtable, troff-ms\n");
+ psql_error("\\pset: allowed formats are aligned, asciidoc, csv, html, latex, latex-longtable, troff-ms, unaligned, wrapped\n");
return false;
}
+
+ if (!popt->topt.fieldSep.is_custom)
+ {
+ if (popt->topt.fieldSep.separator)
+ free(popt->topt.fieldSep.separator);
+ popt->topt.fieldSep.separator =
+ pg_strdup(get_format_fieldsep(popt->topt.format));
+ }
}
/* set table line style */
@@ -3801,6 +3814,30 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
free(popt->topt.fieldSep.separator);
popt->topt.fieldSep.separator = pg_strdup(value);
popt->topt.fieldSep.separator_zero = false;
+ popt->topt.fieldSep.is_custom = true;
+ }
+ }
+
+ else if (strcmp(param, "reset") == 0)
+ {
+ if (!value)
+ {
+ psql_error("\\pset: reset missing object\n");
+ return false;
+ }
+
+ if (strcmp(value, "fieldsep") == 0)
+ {
+ free(popt->topt.fieldSep.separator);
+ popt->topt.fieldSep.separator =
+ pg_strdup(get_format_fieldsep(popt->topt.format));
+ popt->topt.fieldSep.separator_zero = false;
+ popt->topt.fieldSep.is_custom = false;
+ }
+ else
+ {
+ psql_error("\\pset: only fieldsep option can be reseted\n");
+ return false;
}
}
@@ -3920,6 +3957,10 @@ printPsetInfo(const char *param, struct printQueryOpt *popt)
{
Assert(param != NULL);
+ /* do nothing if option was reseted */
+ if (strcmp(param, "reset") == 0)
+ return true;
+
/* show border style/width */
if (strcmp(param, "border") == 0)
printf(_("Border style is %d.\n"), popt->topt.border);
@@ -3949,6 +3990,17 @@ printPsetInfo(const char *param, struct printQueryOpt *popt)
{
if (popt->topt.fieldSep.separator_zero)
printf(_("Field separator is zero byte.\n"));
+ else if (!popt->topt.fieldSep.is_custom)
+ {
+ if (strcmp(popt->topt.fieldSep.separator, "not used") == 0)
+ {
+ printf("User didn't specified field separator.\n");
+ printf("Current format doesn't specify default field separator.\n");
+ }
+ else
+ printf(_("Field separator is format default (\"%s\").\n"),
+ popt->topt.fieldSep.separator);
+ }
else
printf(_("Field separator is \"%s\".\n"),
popt->topt.fieldSep.separator);
@@ -4150,9 +4202,16 @@ pset_value_string(const char *param, struct printQueryOpt *popt)
? "auto"
: pset_bool_string(popt->topt.expanded));
else if (strcmp(param, "fieldsep") == 0)
- return pset_quoted_string(popt->topt.fieldSep.separator
+ {
+ if (!popt->topt.fieldSep.is_custom &&
+ popt->topt.fieldSep.separator &&
+ strcmp(popt->topt.fieldSep.separator, "not used") == 0)
+ return pstrdup("not used");
+ else
+ return pset_quoted_string(popt->topt.fieldSep.separator
? popt->topt.fieldSep.separator
: "");
+ }
else if (strcmp(param, "fieldsep_zero") == 0)
return pstrdup(pset_bool_string(popt->topt.fieldSep.separator_zero));
else if (strcmp(param, "footer") == 0)
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index 702e742af4..50a07deccd 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -108,13 +108,14 @@ usage(unsigned short int pager)
fprintf(output, _("\nOutput format options:\n"));
fprintf(output, _(" -A, --no-align unaligned table output mode\n"));
+ fprintf(output, _(" --csv Comma-Separated-Values output mode\n"));
fprintf(output, _(" -F, --field-separator=STRING\n"
- " field separator for unaligned output (default: \"%s\")\n"),
- DEFAULT_FIELD_SEP);
+ " field separator for unaligned (default: \"%s\") or csv (default \"%s\") output\n"),
+ DEFAULT_FIELD_SEP, DEFAULT_FIELD_SEP_CSV);
fprintf(output, _(" -H, --html HTML table output mode\n"));
fprintf(output, _(" -P, --pset=VAR[=ARG] set printing option VAR to ARG (see \\pset command)\n"));
fprintf(output, _(" -R, --record-separator=STRING\n"
- " record separator for unaligned output (default: newline)\n"));
+ " record separator for unaligned or csv output (default: newline)\n"));
fprintf(output, _(" -t, --tuples-only print rows only\n"));
fprintf(output, _(" -T, --table-attr=TEXT set HTML table tag attributes (e.g., width, border)\n"));
fprintf(output, _(" -x, --expanded turn on expanded table output\n"));
@@ -268,14 +269,14 @@ slashUsage(unsigned short int pager)
fprintf(output, _("Formatting\n"));
fprintf(output, _(" \\a toggle between unaligned and aligned output mode\n"));
fprintf(output, _(" \\C [STRING] set table title, or unset if none\n"));
- fprintf(output, _(" \\f [STRING] show or set field separator for unaligned query output\n"));
+ fprintf(output, _(" \\f [STRING] show or set field separator for unaligned or csv output\n"));
fprintf(output, _(" \\H toggle HTML output mode (currently %s)\n"),
ON(pset.popt.topt.format == PRINT_HTML));
fprintf(output, _(" \\pset [NAME [VALUE]] set table output option\n"
- " (NAME := {border|columns|expanded|fieldsep|fieldsep_zero|\n"
+ " (NAME := {border|columns|expanded|fieldsep|fieldsep_zero\n"
" footer|format|linestyle|null|numericlocale|pager|\n"
" pager_min_lines|recordsep|recordsep_zero|tableattr|title|\n"
- " tuples_only|unicode_border_linestyle|\n"
+ " tuples_only|unicode_border_linestyle|reset|\n"
" unicode_column_linestyle|unicode_header_linestyle})\n"));
fprintf(output, _(" \\t [on|off] show only rows (currently %s)\n"),
ON(pset.popt.topt.tuples_only));
@@ -426,8 +427,8 @@ helpVariables(unsigned short int pager)
fprintf(output, _(" expanded (or x)\n"
" expanded output [on, off, auto]\n"));
fprintf(output, _(" fieldsep\n"
- " field separator for unaligned output (default \"%s\")\n"),
- DEFAULT_FIELD_SEP);
+ " field separator for unaligned (default: \"%s\") and csv (default \"%s\") output\n"),
+ DEFAULT_FIELD_SEP, DEFAULT_FIELD_SEP_CSV);
fprintf(output, _(" fieldsep_zero\n"
" set field separator for unaligned output to a zero byte\n"));
fprintf(output, _(" footer\n"
@@ -443,7 +444,7 @@ helpVariables(unsigned short int pager)
fprintf(output, _(" pager\n"
" control when an external pager is used [yes, no, always]\n"));
fprintf(output, _(" recordsep\n"
- " record (line) separator for unaligned output\n"));
+ " record (line) separator for unaligned and csv output\n"));
fprintf(output, _(" recordsep_zero\n"
" set record separator for unaligned output to a zero byte\n"));
fprintf(output, _(" tableattr (or T)\n"
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index 69e617e6b5..73314dbca1 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -12,7 +12,6 @@
#include "variables.h"
#include "fe_utils/print.h"
-#define DEFAULT_FIELD_SEP "|"
#define DEFAULT_RECORD_SEP "\n"
#if defined(WIN32) || defined(__CYGWIN__)
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index be57574cd3..4dd8d8f45e 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -194,7 +194,8 @@ main(int argc, char *argv[])
if (!pset.popt.topt.fieldSep.separator &&
!pset.popt.topt.fieldSep.separator_zero)
{
- pset.popt.topt.fieldSep.separator = pg_strdup(DEFAULT_FIELD_SEP);
+ pset.popt.topt.fieldSep.separator = pg_strdup(get_format_fieldsep(pset.popt.topt.format));
+ pset.popt.topt.fieldSep.is_custom = false;
pset.popt.topt.fieldSep.separator_zero = false;
}
if (!pset.popt.topt.recordSep.separator &&
@@ -436,6 +437,7 @@ parse_psql_options(int argc, char *argv[], struct adhoc_opts *options)
{"echo-all", no_argument, NULL, 'a'},
{"no-align", no_argument, NULL, 'A'},
{"command", required_argument, NULL, 'c'},
+ {"csv", no_argument, NULL, 2}, /* no single-letter (leave -C for future use) */
{"dbname", required_argument, NULL, 'd'},
{"echo-queries", no_argument, NULL, 'e'},
{"echo-errors", no_argument, NULL, 'b'},
@@ -485,7 +487,7 @@ parse_psql_options(int argc, char *argv[], struct adhoc_opts *options)
SetVariable(pset.vars, "ECHO", "all");
break;
case 'A':
- pset.popt.topt.format = PRINT_UNALIGNED;
+ do_pset("format", "unaligned", &pset.popt, true);
break;
case 'b':
SetVariable(pset.vars, "ECHO", "errors");
@@ -515,14 +517,13 @@ parse_psql_options(int argc, char *argv[], struct adhoc_opts *options)
optarg);
break;
case 'F':
- pset.popt.topt.fieldSep.separator = pg_strdup(optarg);
- pset.popt.topt.fieldSep.separator_zero = false;
+ do_pset("fieldsep", optarg, &pset.popt, true);
break;
case 'h':
options->host = pg_strdup(optarg);
break;
case 'H':
- pset.popt.topt.format = PRINT_HTML;
+ do_pset("format", "html", &pset.popt, true);
break;
case 'l':
options->list_dbs = true;
@@ -569,8 +570,7 @@ parse_psql_options(int argc, char *argv[], struct adhoc_opts *options)
SetVariableBool(pset.vars, "QUIET");
break;
case 'R':
- pset.popt.topt.recordSep.separator = pg_strdup(optarg);
- pset.popt.topt.recordSep.separator_zero = false;
+ do_pset("recordsep", optarg, &pset.popt, true);
break;
case 's':
SetVariableBool(pset.vars, "SINGLESTEP");
@@ -625,10 +625,10 @@ parse_psql_options(int argc, char *argv[], struct adhoc_opts *options)
options->no_psqlrc = true;
break;
case 'z':
- pset.popt.topt.fieldSep.separator_zero = true;
+ do_pset("fieldsep_zero", NULL, &pset.popt, true);
break;
case '0':
- pset.popt.topt.recordSep.separator_zero = true;
+ do_pset("recordsep_zero", NULL, &pset.popt, true);
break;
case '1':
options->single_txn = true;
@@ -658,6 +658,10 @@ parse_psql_options(int argc, char *argv[], struct adhoc_opts *options)
exit(EXIT_SUCCESS);
}
break;
+ case 2:
+ /* --csv: set both format and field separator */
+ do_pset("format", "csv", &pset.popt, true);
+ break;
default:
unknown_option:
fprintf(stderr, _("Try \"%s --help\" for more information.\n"),
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 08d8ef09a4..735670e0f5 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3743,8 +3743,9 @@ psql_completion(const char *text, int start, int end)
{"border", "columns", "expanded", "fieldsep", "fieldsep_zero",
"footer", "format", "linestyle", "null", "numericlocale",
"pager", "pager_min_lines", "recordsep", "recordsep_zero",
- "tableattr", "title", "tuples_only", "unicode_border_linestyle",
- "unicode_column_linestyle", "unicode_header_linestyle", NULL};
+ "reset", "tableattr", "title", "tuples_only",
+ "unicode_border_linestyle", "unicode_column_linestyle",
+ "unicode_header_linestyle", NULL};
COMPLETE_WITH_LIST_CS(my_list);
}
@@ -3753,8 +3754,8 @@ psql_completion(const char *text, int start, int end)
if (TailMatchesCS1("format"))
{
static const char *const my_list[] =
- {"unaligned", "aligned", "wrapped", "html", "asciidoc",
- "latex", "latex-longtable", "troff-ms", NULL};
+ {"unaligned", "aligned", "csv", "wrapped", "html", "asciidoc",
+ "latex", "latex-longtable", "troff-ms", NULL};
COMPLETE_WITH_LIST_CS(my_list);
}
diff --git a/src/fe_utils/print.c b/src/fe_utils/print.c
index ec5ad45a30..954f448400 100644
--- a/src/fe_utils/print.c
+++ b/src/fe_utils/print.c
@@ -35,7 +35,6 @@
#include "catalog/pg_type.h"
#include "fe_utils/mbprint.h"
-
/*
* If the calling program doesn't have any mechanism for setting
* cancel_pressed, it will have no effect.
@@ -2783,6 +2782,109 @@ print_troff_ms_vertical(const printTableContent *cont, FILE *fout)
}
}
+/*************************/
+/* CSV */
+/*************************/
+static void
+csv_escaped_print(const char *text, FILE *fout)
+{
+ const char *p;
+
+ fputc('"', fout);
+ for (p = text; *p; p++)
+ {
+ if (*p == '"')
+ fputc('"', fout); /* double quotes are doubled */
+ fputc(*p, fout);
+ }
+ fputc('"', fout);
+}
+
+static void
+csv_print_field(const char *text, FILE *fout, const char *sep)
+{
+ /*
+ * Enclose and escape field contents when one of these conditions is
+ * met:
+ * - the field separator is found in the contents
+ * - the field contains a CR or LF
+ * - the field contains a double quote
+ */
+ if ((sep != NULL && *sep != '\0' && strstr(text, sep) != NULL) ||
+ strcspn(text, "\r\n\"") != strlen(text))
+ {
+ csv_escaped_print(text, fout);
+ }
+ else
+ fputs(text, fout);
+}
+
+static void
+print_csv_text(const printTableContent *cont, FILE *fout)
+{
+ const char *const *ptr;
+ const char* fieldsep = cont->opt->fieldSep.separator;
+ const char* const recordsep = cont->opt->recordSep.separator;
+ int i;
+
+ if (cancel_pressed)
+ return;
+
+ /*
+ * The title and footer are never printed in csv format.
+ * The header is printed if opt_tuples_only is false.
+ */
+
+ if (cont->opt->start_table && !cont->opt->tuples_only)
+ {
+ /* print headers */
+ for (ptr = cont->headers; *ptr; ptr++)
+ {
+ if (ptr != cont->headers)
+ fputs(fieldsep, fout);
+ csv_print_field(*ptr, fout, fieldsep);
+ }
+ fputs(recordsep, fout);
+ }
+
+ /* print cells */
+ for (i = 0, ptr = cont->cells; *ptr; i++, ptr++)
+ {
+ if (cancel_pressed)
+ break;
+
+ csv_print_field(*ptr, fout, fieldsep);
+
+ if ((i + 1) % cont->ncolumns)
+ fputs(fieldsep, fout);
+ else
+ fputs(recordsep, fout);
+ }
+}
+
+static void
+print_csv_vertical(const printTableContent *cont, FILE *fout)
+{
+ unsigned int i;
+ const char *const *ptr;
+
+ /* Print records */
+ for (i = 0, ptr = cont->cells; *ptr; i++, ptr++)
+ {
+ if (cancel_pressed)
+ break;
+
+ /* Field name */
+ csv_print_field(cont->headers[i % cont->ncolumns], fout,
+ cont->opt->fieldSep.separator);
+ fputs(cont->opt->fieldSep.separator, fout);
+
+ /* Field value followed by record separator */
+ csv_print_field(*ptr, fout, cont->opt->fieldSep.separator);
+ fputs(cont->opt->recordSep.separator, fout);
+ }
+}
+
/********************************/
/* Public functions */
@@ -3234,6 +3336,12 @@ printTable(const printTableContent *cont,
else
print_aligned_text(cont, fout, is_pager);
break;
+ case PRINT_CSV:
+ if (cont->opt->expanded == 1)
+ print_csv_vertical(cont, fout);
+ else
+ print_csv_text(cont, fout);
+ break;
case PRINT_HTML:
if (cont->opt->expanded == 1)
print_html_vertical(cont, fout);
@@ -3424,6 +3532,21 @@ get_line_style(const printTableOpt *opt)
return &pg_asciiformat;
}
+/* returns default fieldSep used by formats */
+const char *
+get_format_fieldsep(enum printFormat format)
+{
+ switch (format)
+ {
+ case PRINT_UNALIGNED:
+ return DEFAULT_FIELD_SEP;
+ case PRINT_CSV:
+ return DEFAULT_FIELD_SEP_CSV;
+ default:
+ return "not used";
+ }
+}
+
void
refresh_utf8format(const printTableOpt *opt)
{
diff --git a/src/include/fe_utils/print.h b/src/include/fe_utils/print.h
index 83320d06bd..6ca04e7ea6 100644
--- a/src/include/fe_utils/print.h
+++ b/src/include/fe_utils/print.h
@@ -23,6 +23,9 @@
#define DEFAULT_PAGER "less"
#endif
+#define DEFAULT_FIELD_SEP "|"
+#define DEFAULT_FIELD_SEP_CSV ","
+
enum printFormat
{
PRINT_NOTHING = 0, /* to make sure someone initializes this */
@@ -33,7 +36,8 @@ enum printFormat
PRINT_ASCIIDOC,
PRINT_LATEX,
PRINT_LATEX_LONGTABLE,
- PRINT_TROFF_MS
+ PRINT_TROFF_MS,
+ PRINT_CSV
/* add your favourite output format here ... */
};
@@ -91,6 +95,7 @@ struct separator
{
char *separator;
bool separator_zero;
+ bool is_custom; /* true, when user set this option */
};
typedef struct printTableOpt
@@ -158,8 +163,8 @@ typedef struct printTableContent
char *aligns; /* Array of alignment specifiers; 'l' or 'r',
* one per column */
char *align; /* Pointer to the last added alignment */
-} printTableContent;
+} printTableContent;
typedef struct printQueryOpt
{
printTableOpt topt; /* the options above */
@@ -210,6 +215,7 @@ extern char column_type_alignment(Oid);
extern void setDecimalLocale(void);
extern const printTextFormat *get_line_style(const printTableOpt *opt);
+extern const char *get_format_fieldsep(enum printFormat format);
extern void refresh_utf8format(const printTableOpt *opt);
#endif /* PRINT_H */
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 3818cfea7e..04f7ee11f7 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -261,7 +261,7 @@ select '2000-01-01'::date as party_over
border 1
columns 0
expanded off
-fieldsep '|'
+fieldsep ''
fieldsep_zero off
footer on
format aligned
@@ -3243,3 +3243,106 @@ last error message: division by zero
\echo 'last error code:' :LAST_ERROR_SQLSTATE
last error code: 22012
\unset FETCH_COUNT
+-- test csv format
+prepare q as select 'ab,cd' as col1, 'ab' as "col,2", E'a\tb' as col3, '"' as col4,
+ '""' as col5, 'a"b' as "col""6", E'a\nb' as col7, NULL as col8, 'ab' as "col
+ 9", array['ab', E'cd\nef'] as col10,
+ '{"a":"a,b", "a,b":null, "c":"a,\"b"}'::json as col11
+ from generate_series(1,2);
+\pset format csv
+\pset fieldsep ','
+\pset expanded off
+\t off
+execute q;
+col1,"col,2",col3,col4,col5,"col""6",col7,col8,"col
+ 9",col10,col11
+"ab,cd",ab,a b,"""","""""","a""b","a
+b",,ab,"{ab,""cd
+ef""}","{""a"":""a,b"", ""a,b"":null, ""c"":""a,\""b""}"
+"ab,cd",ab,a b,"""","""""","a""b","a
+b",,ab,"{ab,""cd
+ef""}","{""a"":""a,b"", ""a,b"":null, ""c"":""a,\""b""}"
+\pset fieldsep '\t'
+execute q;
+col1 col,2 col3 col4 col5 "col""6" col7 col8 "col
+ 9" col10 col11
+ab,cd ab "a b" """" """""" "a""b" "a
+b" ab "{ab,""cd
+ef""}" "{""a"":""a,b"", ""a,b"":null, ""c"":""a,\""b""}"
+ab,cd ab "a b" """" """""" "a""b" "a
+b" ab "{ab,""cd
+ef""}" "{""a"":""a,b"", ""a,b"":null, ""c"":""a,\""b""}"
+\t on
+execute q;
+ab,cd ab "a b" """" """""" "a""b" "a
+b" ab "{ab,""cd
+ef""}" "{""a"":""a,b"", ""a,b"":null, ""c"":""a,\""b""}"
+ab,cd ab "a b" """" """""" "a""b" "a
+b" ab "{ab,""cd
+ef""}" "{""a"":""a,b"", ""a,b"":null, ""c"":""a,\""b""}"
+\t off
+\pset expanded on
+execute q;
+col1 ab,cd
+col,2 ab
+col3 "a b"
+col4 """"
+col5 """"""
+"col""6" "a""b"
+col7 "a
+b"
+col8
+"col
+ 9" ab
+col10 "{ab,""cd
+ef""}"
+col11 "{""a"":""a,b"", ""a,b"":null, ""c"":""a,\""b""}"
+col1 ab,cd
+col,2 ab
+col3 "a b"
+col4 """"
+col5 """"""
+"col""6" "a""b"
+col7 "a
+b"
+col8
+"col
+ 9" ab
+col10 "{ab,""cd
+ef""}"
+col11 "{""a"":""a,b"", ""a,b"":null, ""c"":""a,\""b""}"
+\pset fieldsep ','
+execute q;
+col1,"ab,cd"
+"col,2",ab
+col3,a b
+col4,""""
+col5,""""""
+"col""6","a""b"
+col7,"a
+b"
+col8,
+"col
+ 9",ab
+col10,"{ab,""cd
+ef""}"
+col11,"{""a"":""a,b"", ""a,b"":null, ""c"":""a,\""b""}"
+col1,"ab,cd"
+"col,2",ab
+col3,a b
+col4,""""
+col5,""""""
+"col""6","a""b"
+col7,"a
+b"
+col8,
+"col
+ 9",ab
+col10,"{ab,""cd
+ef""}"
+col11,"{""a"":""a,b"", ""a,b"":null, ""c"":""a,\""b""}"
+deallocate q;
+\pset format aligned
+\pset expanded off
+\pset fieldsep '|'
+\t off
diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql
index b45da9bb8d..6891a9b54c 100644
--- a/src/test/regress/sql/psql.sql
+++ b/src/test/regress/sql/psql.sql
@@ -688,3 +688,31 @@ select 1/(15-unique2) from tenk1 order by unique2 limit 19;
\echo 'last error code:' :LAST_ERROR_SQLSTATE
\unset FETCH_COUNT
+
+-- test csv format
+prepare q as select 'ab,cd' as col1, 'ab' as "col,2", E'a\tb' as col3, '"' as col4,
+ '""' as col5, 'a"b' as "col""6", E'a\nb' as col7, NULL as col8, 'ab' as "col
+ 9", array['ab', E'cd\nef'] as col10,
+ '{"a":"a,b", "a,b":null, "c":"a,\"b"}'::json as col11
+ from generate_series(1,2);
+
+\pset format csv
+\pset fieldsep ','
+\pset expanded off
+\t off
+execute q;
+\pset fieldsep '\t'
+execute q;
+\t on
+execute q;
+\t off
+\pset expanded on
+execute q;
+\pset fieldsep ','
+execute q;
+
+deallocate q;
+\pset format aligned
+\pset expanded off
+\pset fieldsep '|'
+\t off