2018-03-24 8:15 GMT+01:00 Fabien COELHO <coe...@cri.ensmp.fr>:

>
> Hello Pavel,
>
> The patch adds a simple way to generate csv output from "psql" queries,
>>> much simpler than playing around with COPY or \copy. It allows to
>>> generate
>>> a clean CSV dump from something as short as:
>>>
>>>   sh> psql --csv -c 'TABLE foo' > foo.csv
>>>
>>> Documentation is clear.
>>>
>>> Test cover a significant number of cases (fieldsep, expanded,
>>> tuples-only).
>>> Although recordsep changes are not actually tested, it worked
>>> interactively
>>> and I think that tests are sufficient as is.
>>>
>>> There are somehow remaining point about which a committer/other people
>>> input would be nice:
>>>
>>> (1) There are some mild disagreement whether the fieldsep should be
>>> format specific shared with other format. I do not think that a specific
>>> fieldsep is worth it, but this is a marginal preference, and other people
>>> opinion differ. What is best is not obvious.
>>>
>>> Pavel also suggested to have a special handling based on whether the
>>> fieldsep is explicitely set or not. I'm not too keen on that because it
>>> departs significantly from the way psql formatting is currently handled,
>>> and what is happening becomes unclear to the user.
>>>
>>> (2) For interactive use, two commands are required: \pset format csv +
>>> \pset fieldsep ',' (or ';' or '\t' or whatever...). Maybe some \csv command
>>> similar to \H would be appropriate, or not, to set both values more
>>> efficiently. Could be something for another patch.
>>>
>>> Not sure what is the status of the patch if we do not have a clear
>>> consensus.
>>>
>>
>> I am sorry, but I don't think so this interface is good enough. Using | as
>> default CSV separator is just wrong. It and only it is a problem. Any
>> other
>> is perfect.
>>
>
> I do not think that there is a perfect solution, so some compromise will
> be needed or we won't get it.
>
> (1) patch v4:
>
>     "\pset format csv" retains the current fieldsep value, so fields are
>     separated by whatever is in the variable, which means that for getting
>     a standard csv two commands are needed, which is clearly documented,
>     but may be considered as surprising. ISTM that the underlying point is
>     that "format" is really about string escaping, not about the full
> output
>     format, but this is a pre-existing situation.
>
>     I'm suggesting to add \csv which would behave like \H to toggle CSV
>     mode so as to improve this situation, with a caveat which is that
>     toggling back \csv would have forgotted the previous settings (just
>     like \H does, though, so would for instance reset to aligned with |),
>     so it would not be perfect.
>

this doesn't solve usual format settings by \pset format csv


>
> (2) your proposal as I understand it:
>
>     "\pset format csv" may or may not use the fieldsep, depending on
>     whether it was explicitely set, an information which is not shown,
> i.e.:
>
>       \pset fieldsep # fieldsep separator is "|"
>       \pset format csv # would output a,b,c or a|b|c...
>
>     Because it depends on whether fieldsep was set explicitely to '|' or
>     whether it has this value but it was due to the default.
>
>     This kind of unclear behavioral determinism does not seem desirable.
>

please, check and test attached patch. It is very simply for usage - and
there is not any unclear behave. Just you should to accept so formats can
have own defaults for separators.


>
> (3) other option, always use a comma:
>
>     this was rejected because some people like their comma separated
>     values to be separated by semi-colons or tabs (aka tsv).
>
> (4) other option, Daniel v3 or v2:
>
>     use a distinct "fieldsep_csv" variable initially set to ','. This adds
>     yet another specific variable that has to be remembered, some styles
>     would use fieldsep but csv would not so it is some kind of exception
>     that I would wish to avoid.
>
> My current preference order in the suggested solutions is 1, 4, 2, 3, with
> a significant preference for 1.
>

I am thinking so @1 solves nothing - people are using \pset format ...

@3 is clearly bad - there are not any discussion

@4 can be compromise solution, but then there should be renamed fieldsep.
Now, fieldsep is used just for unaligned format - for nothing else. If we
introduce fieldsep_csv, then fieldsep should be renamed to
fieldsep_unaligned. I can live with it.

But I think so default fieldsep is better option. Please, try my patch and
comment it.

Regards

Pavel

>
> --
> Fabien.
>
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
       </listitem>
     </varlistentry>
 
+    <varlistentry>
+      <term><option>--csv</option></term>
+      <listitem>
+      <para>
+      Switches to csv output mode. This is equivalent to <command>\pset format
+      csv</command> followed by <command>\pset fieldsep ','</command>.
+      </para>
+      </listitem>
+    </varlistentry>
+
     <varlistentry>
       <term><option>-d <replaceable class="parameter">dbname</replaceable></option></term>
       <term><option>--dbname=<replaceable class="parameter">dbname</replaceable></option></term>
@@ -246,7 +256,7 @@ EOF
       <listitem>
       <para>
       Use <replaceable class="parameter">separator</replaceable> as the
-      field separator for unaligned output. This is equivalent to
+      field separator for unaligned and csv outputs. This is equivalent to
       <command>\pset fieldsep</command> or <command>\f</command>.
       </para>
       </listitem>
@@ -382,7 +392,7 @@ EOF
       <listitem>
       <para>
       Use <replaceable class="parameter">separator</replaceable> as the
-      record separator for unaligned output. This is equivalent to
+      record separator for unaligned and csv outputs. This is equivalent to
       <command>\pset recordsep</command>.
       </para>
       </listitem>
@@ -558,7 +568,7 @@ EOF
       <listitem>
       <para>
       Set the field separator for unaligned output to a zero byte.  This is
-      equvalent to <command>\pset fieldsep_zero</command>.
+      equivalent to <command>\pset fieldsep_zero</command>.
       </para>
       </listitem>
     </varlistentry>
@@ -1937,9 +1947,9 @@ Tue Oct 26 21:40:57 CEST 1999
 
         <listitem>
         <para>
-        Sets the field separator for unaligned query output. The default
-        is the vertical bar (<literal>|</literal>). It is equivalent to
-        <command>\pset fieldsep</command>.
+        Sets the field separator for unaligned and csv query outputs. The
+        default is the vertical bar (<literal>|</literal>). It is equivalent
+        to <command>\pset fieldsep</command>.
         </para>
         </listitem>
       </varlistentry>
@@ -2546,8 +2556,8 @@ lo_import 152801
           <term><literal>fieldsep</literal></term>
           <listitem>
           <para>
-          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 <literal>\pset fieldsep
           '\t'</literal>. The default field separator is
@@ -2584,9 +2594,13 @@ lo_import 152801
           <term><literal>format</literal></term>
           <listitem>
           <para>
-          Sets the output format to one of <literal>unaligned</literal>,
-          <literal>aligned</literal>, <literal>wrapped</literal>,
-          <literal>html</literal>, <literal>asciidoc</literal>,
+          Sets the output format to one of
+          <literal>unaligned</literal>,
+          <literal>aligned</literal>,
+          <literal>csv</literal>,
+          <literal>wrapped</literal>,
+          <literal>html</literal>,
+          <literal>asciidoc</literal>,
           <literal>latex</literal> (uses <literal>tabular</literal>),
           <literal>latex-longtable</literal>, or
           <literal>troff-ms</literal>.
@@ -2601,6 +2615,15 @@ lo_import 152801
           format).
           </para>
 
+          <para><literal>csv</literal> format writes columns separated
+          by <literal>fieldsep</literal>, applying the CSV quoting rules
+          described in RFC-4180 and compatible with the CSV format
+          of the <command>COPY</command> command.
+          The header with column names is output unless the
+          <literal>tuples_only</literal> parameter is <literal>on</literal>.
+          Title and footers are not printed.
+          </para>
+
           <para><literal>aligned</literal> format is the standard, human-readable,
           nicely formatted text output;  this is the default.
           </para>
@@ -2747,8 +2770,8 @@ lo_import 152801
           <term><literal>recordsep</literal></term>
           <listitem>
           <para>
-          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.
           </para>
           </listitem>
           </varlistentry>
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 3560318749..a8e7949160 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,7 @@ 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;
 		}
 	}
 
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index 702e742af4..c8ce463549 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"));
@@ -426,7 +427,7 @@ 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"),
+					  "    field separator for unaligned and csv output (default \"%s\")\n"),
 			DEFAULT_FIELD_SEP);
 	fprintf(output, _("  fieldsep_zero\n"
 					  "    set field separator for unaligned output to a zero byte\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..501af67aa6 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -191,10 +191,13 @@ main(int argc, char *argv[])
 		exit(EXIT_FAILURE);
 	}
 
+	pset.popt.topt.fieldSep.is_custom = false;
+	pset.popt.topt.fieldSep.separator = NULL;
+
 	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.separator_zero = false;
 	}
 	if (!pset.popt.topt.recordSep.separator &&
@@ -436,6 +439,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 +489,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 +519,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 +572,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 +627,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 +660,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..b9fc423526 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3753,8 +3753,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..3afc89f7fe 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 "";
+	}
+}
+
 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

Reply via email to