psql: Show all query results by default
authorPeter Eisentraut <peter@eisentraut.org>
Tue, 6 Apr 2021 14:58:10 +0000 (16:58 +0200)
committerPeter Eisentraut <peter@eisentraut.org>
Tue, 6 Apr 2021 15:10:24 +0000 (17:10 +0200)
Previously, psql printed only the last result if a command string
returned multiple result sets.  Now it prints all of them.  The
previous behavior can be obtained by setting the psql variable
SHOW_ALL_RESULTS to off.

Author: Fabien COELHO <coelho@cri.ensmp.fr>
Reviewed-by: "Iwata, Aya" <iwata.aya@jp.fujitsu.com>
Reviewed-by: Daniel Verite <daniel@manitou-mail.org>
Reviewed-by: Peter Eisentraut <peter.eisentraut@2ndquadrant.com>
Reviewed-by: Kyotaro Horiguchi <horikyota.ntt@gmail.com>
Reviewed-by: vignesh C <vignesh21@gmail.com>
Discussion: https://www.postgresql.org/message-id/flat/alpine.DEB.2.21.1904132231510.8961@lancre

13 files changed:
contrib/pg_stat_statements/expected/pg_stat_statements.out
doc/src/sgml/ref/psql-ref.sgml
src/bin/psql/common.c
src/bin/psql/help.c
src/bin/psql/settings.h
src/bin/psql/startup.c
src/bin/psql/tab-complete.c
src/test/regress/expected/copyselect.out
src/test/regress/expected/psql.out
src/test/regress/expected/transactions.out
src/test/regress/sql/copyselect.sql
src/test/regress/sql/psql.sql
src/test/regress/sql/transactions.sql

index 16158525cac06a43c10d492edb6859ef02f8a231..4ffb7e0076a10d13ca99e6c83e069499419bc8bf 100644 (file)
@@ -50,8 +50,28 @@ BEGIN \;
 SELECT 2.0 AS "float" \;
 SELECT 'world' AS "text" \;
 COMMIT;
+ float 
+-------
+   2.0
+(1 row)
+
+ text  
+-------
+ world
+(1 row)
+
 -- compound with empty statements and spurious leading spacing
 \;\;   SELECT 3 + 3 \;\;\;   SELECT ' ' || ' !' \;\;   SELECT 1 + 4 \;;
+ ?column? 
+----------
+        6
+(1 row)
+
+ ?column? 
+----------
+   !
+(1 row)
+
  ?column? 
 ----------
         5
@@ -61,6 +81,11 @@ COMMIT;
 SELECT 1 + 1 + 1 AS "add" \gset
 SELECT :add + 1 + 1 AS "add" \;
 SELECT :add + 1 + 1 AS "add" \gset
+ add 
+-----
+   5
+(1 row)
+
 -- set operator
 SELECT 1 AS i UNION SELECT 2 ORDER BY i;
  i 
index 0ba1e2d44d47fe49be6a03db62fcf07cc2565c7d..c1451c16727ebfc242b2a1b4f087fce113e92c32 100644 (file)
@@ -127,18 +127,11 @@ echo '\x \\ SELECT * FROM foo;' | psql
        commands included in the string to divide it into multiple
        transactions.  (See <xref linkend="protocol-flow-multi-statement"/>
        for more details about how the server handles multi-query strings.)
-       Also, <application>psql</application> only prints the
-       result of the last <acronym>SQL</acronym> command in the string.
-       This is different from the behavior when the same string is read from
-       a file or fed to <application>psql</application>'s standard input,
-       because then <application>psql</application> sends
-       each <acronym>SQL</acronym> command separately.
       </para>
       <para>
-       Because of this behavior, putting more than one SQL command in a
-       single <option>-c</option> string often has unexpected results.
-       It's better to use repeated <option>-c</option> commands or feed
-       multiple commands to <application>psql</application>'s standard input,
+       If having several commands executed in one transaction is not desired, 
+       use repeated <option>-c</option> commands or feed multiple commands to
+       <application>psql</application>'s standard input,
        either using <application>echo</application> as illustrated above, or
        via a shell here-document, for example:
 <programlisting>
@@ -3527,10 +3520,6 @@ select 1\; select 2\; select 3;
         commands included in the string to divide it into multiple
         transactions.  (See <xref linkend="protocol-flow-multi-statement"/>
         for more details about how the server handles multi-query strings.)
-        <application>psql</application> prints only the last query result
-        it receives for each request; in this example, although all
-        three <command>SELECT</command>s are indeed executed, <application>psql</application>
-        only prints the <literal>3</literal>.
         </para>
         </listitem>
       </varlistentry>
@@ -4117,6 +4106,18 @@ bar
       </varlistentry>
 
       <varlistentry>
+        <term><varname>SHOW_ALL_RESULTS</varname></term>
+        <listitem>
+        <para>
+        When this variable is set to <literal>off</literal>, only the last
+        result of a combined query (<literal>\;</literal>) is shown instead of
+        all of them.  The default is <literal>on</literal>.  The off behavior
+        is for compatibility with older versions of psql.
+        </para>
+        </listitem>
+      </varlistentry>
+
+       <varlistentry>
         <term><varname>SHOW_CONTEXT</varname></term>
         <listitem>
         <para>
index 7a95465111ad1f1e3485350f680053ecb28df820..028a357991fd2d0d18bb7a43d57f186b7f4e5330 100644 (file)
@@ -33,6 +33,7 @@ static bool DescribeQuery(const char *query, double *elapsed_msec);
 static bool ExecQueryUsingCursor(const char *query, double *elapsed_msec);
 static bool command_no_begin(const char *query);
 static bool is_select_command(const char *query);
+static int SendQueryAndProcessResults(const char *query, double *pelapsed_msec, bool is_watch);
 
 
 /*
@@ -353,7 +354,7 @@ CheckConnection(void)
  * Returns true for valid result, false for error state.
  */
 static bool
-AcceptResult(const PGresult *result)
+AcceptResult(const PGresult *result, bool show_error)
 {
    bool        OK;
 
@@ -384,7 +385,7 @@ AcceptResult(const PGresult *result)
                break;
        }
 
-   if (!OK)
+   if (!OK && show_error)
    {
        const char *error = PQerrorMessage(pset.db);
 
@@ -472,6 +473,18 @@ ClearOrSaveResult(PGresult *result)
    }
 }
 
+/*
+ * Consume all results
+ */
+static void
+ClearOrSaveAllResults()
+{
+   PGresult    *result;
+
+   while ((result = PQgetResult(pset.db)) != NULL)
+       ClearOrSaveResult(result);
+}
+
 
 /*
  * Print microtiming output.  Always print raw milliseconds; if the interval
@@ -572,7 +585,7 @@ PSQLexec(const char *query)
 
    ResetCancelConn();
 
-   if (!AcceptResult(res))
+   if (!AcceptResult(res, true))
    {
        ClearOrSaveResult(res);
        res = NULL;
@@ -594,10 +607,8 @@ PSQLexec(const char *query)
 int
 PSQLexecWatch(const char *query, const printQueryOpt *opt)
 {
-   PGresult   *res;
    double      elapsed_msec = 0;
-   instr_time  before;
-   instr_time  after;
+   int         res;
 
    if (!pset.db)
    {
@@ -606,75 +617,16 @@ PSQLexecWatch(const char *query, const printQueryOpt *opt)
    }
 
    SetCancelConn(pset.db);
-
-   if (pset.timing)
-       INSTR_TIME_SET_CURRENT(before);
-
-   res = PQexec(pset.db, query);
-
+   res = SendQueryAndProcessResults(query, &elapsed_msec, true);
    ResetCancelConn();
 
-   if (!AcceptResult(res))
-   {
-       ClearOrSaveResult(res);
-       return 0;
-   }
-
-   if (pset.timing)
-   {
-       INSTR_TIME_SET_CURRENT(after);
-       INSTR_TIME_SUBTRACT(after, before);
-       elapsed_msec = INSTR_TIME_GET_MILLISEC(after);
-   }
-
-   /*
-    * If SIGINT is sent while the query is processing, the interrupt will be
-    * consumed.  The user's intention, though, is to cancel the entire watch
-    * process, so detect a sent cancellation request and exit in this case.
-    */
-   if (cancel_pressed)
-   {
-       PQclear(res);
-       return 0;
-   }
-
-   switch (PQresultStatus(res))
-   {
-       case PGRES_TUPLES_OK:
-           printQuery(res, opt, pset.queryFout, false, pset.logfile);
-           break;
-
-       case PGRES_COMMAND_OK:
-           fprintf(pset.queryFout, "%s\n%s\n\n", opt->title, PQcmdStatus(res));
-           break;
-
-       case PGRES_EMPTY_QUERY:
-           pg_log_error("\\watch cannot be used with an empty query");
-           PQclear(res);
-           return -1;
-
-       case PGRES_COPY_OUT:
-       case PGRES_COPY_IN:
-       case PGRES_COPY_BOTH:
-           pg_log_error("\\watch cannot be used with COPY");
-           PQclear(res);
-           return -1;
-
-       default:
-           pg_log_error("unexpected result status for \\watch");
-           PQclear(res);
-           return -1;
-   }
-
-   PQclear(res);
-
    fflush(pset.queryFout);
 
    /* Possible microtiming output */
    if (pset.timing)
        PrintTiming(elapsed_msec);
 
-   return 1;
+   return res;
 }
 
 
@@ -887,197 +839,114 @@ loop_exit:
 
 
 /*
- * ProcessResult: utility function for use by SendQuery() only
- *
- * When our command string contained a COPY FROM STDIN or COPY TO STDOUT,
- * PQexec() has stopped at the PGresult associated with the first such
- * command.  In that event, we'll marshal data for the COPY and then cycle
- * through any subsequent PGresult objects.
+ * Marshal the COPY data.  Either subroutine will get the
+ * connection out of its COPY state, then call PQresultStatus()
+ * once and report any error. Return whether all was ok.
  *
- * When the command string contained no such COPY command, this function
- * degenerates to an AcceptResult() call.
+ * For COPY OUT, direct the output to pset.copyStream if it's set,
+ * otherwise to pset.gfname if it's set, otherwise to queryFout.
+ * For COPY IN, use pset.copyStream as data source if it's set,
+ * otherwise cur_cmd_source.
  *
- * Changes its argument to point to the last PGresult of the command string,
- * or NULL if that result was for a COPY TO STDOUT.  (Returning NULL prevents
- * the command status from being printed, which we want in that case so that
- * the status line doesn't get taken as part of the COPY data.)
- *
- * Returns true on complete success, false otherwise.  Possible failure modes
- * include purely client-side problems; check the transaction status for the
- * server-side opinion.
+ * Update result if further processing is necessary, or NULL otherwise.
+ * Return a result when queryFout can safely output a result status:
+ * on COPY IN, or on COPY OUT if written to something other than pset.queryFout.
+ * Returning NULL prevents the command status from being printed, which
+ * we want if the status line doesn't get taken as part of the COPY data.
  */
 static bool
-ProcessResult(PGresult **results)
+HandleCopyResult(PGresult **result)
 {
-   bool        success = true;
-   bool        first_cycle = true;
+   bool        success;
+   FILE       *copystream;
+   PGresult   *copy_result;
+   ExecStatusType result_status = PQresultStatus(*result);
 
-   for (;;)
+   Assert(result_status == PGRES_COPY_OUT ||
+          result_status == PGRES_COPY_IN);
+
+   SetCancelConn(pset.db);
+
+   if (result_status == PGRES_COPY_OUT)
    {
-       ExecStatusType result_status;
-       bool        is_copy;
-       PGresult   *next_result;
+       bool        need_close = false;
+       bool        is_pipe = false;
 
-       if (!AcceptResult(*results))
+       if (pset.copyStream)
        {
-           /*
-            * Failure at this point is always a server-side failure or a
-            * failure to submit the command string.  Either way, we're
-            * finished with this command string.
-            */
-           success = false;
-           break;
+           /* invoked by \copy */
+           copystream = pset.copyStream;
        }
-
-       result_status = PQresultStatus(*results);
-       switch (result_status)
+       else if (pset.gfname)
        {
-           case PGRES_EMPTY_QUERY:
-           case PGRES_COMMAND_OK:
-           case PGRES_TUPLES_OK:
-               is_copy = false;
-               break;
+           /* invoked by \g */
+           if (openQueryOutputFile(pset.gfname,
+                                   &copystream, &is_pipe))
+           {
+               need_close = true;
+               if (is_pipe)
+                   disable_sigpipe_trap();
+           }
+           else
+               copystream = NULL;  /* discard COPY data entirely */
+       }
+       else
+       {
+           /* fall back to the generic query output stream */
+           copystream = pset.queryFout;
+       }
 
-           case PGRES_COPY_OUT:
-           case PGRES_COPY_IN:
-               is_copy = true;
-               break;
+       success = handleCopyOut(pset.db,
+                               copystream,
+                               &copy_result)
+           && (copystream != NULL);
 
-           default:
-               /* AcceptResult() should have caught anything else. */
-               is_copy = false;
-               pg_log_error("unexpected PQresultStatus: %d", result_status);
-               break;
+       /*
+        * Suppress status printing if the report would go to the same
+        * place as the COPY data just went.  Note this doesn't
+        * prevent error reporting, since handleCopyOut did that.
+        */
+       if (copystream == pset.queryFout)
+       {
+           PQclear(copy_result);
+           copy_result = NULL;
        }
 
-       if (is_copy)
+       if (need_close)
        {
-           /*
-            * Marshal the COPY data.  Either subroutine will get the
-            * connection out of its COPY state, then call PQresultStatus()
-            * once and report any error.
-            *
-            * For COPY OUT, direct the output to pset.copyStream if it's set,
-            * otherwise to pset.gfname if it's set, otherwise to queryFout.
-            * For COPY IN, use pset.copyStream as data source if it's set,
-            * otherwise cur_cmd_source.
-            */
-           FILE       *copystream;
-           PGresult   *copy_result;
-
-           SetCancelConn(pset.db);
-           if (result_status == PGRES_COPY_OUT)
+           /* close \g argument file/pipe */
+           if (is_pipe)
            {
-               bool        need_close = false;
-               bool        is_pipe = false;
-
-               if (pset.copyStream)
-               {
-                   /* invoked by \copy */
-                   copystream = pset.copyStream;
-               }
-               else if (pset.gfname)
-               {
-                   /* invoked by \g */
-                   if (openQueryOutputFile(pset.gfname,
-                                           &copystream, &is_pipe))
-                   {
-                       need_close = true;
-                       if (is_pipe)
-                           disable_sigpipe_trap();
-                   }
-                   else
-                       copystream = NULL;  /* discard COPY data entirely */
-               }
-               else
-               {
-                   /* fall back to the generic query output stream */
-                   copystream = pset.queryFout;
-               }
-
-               success = handleCopyOut(pset.db,
-                                       copystream,
-                                       &copy_result)
-                   && success
-                   && (copystream != NULL);
-
-               /*
-                * Suppress status printing if the report would go to the same
-                * place as the COPY data just went.  Note this doesn't
-                * prevent error reporting, since handleCopyOut did that.
-                */
-               if (copystream == pset.queryFout)
-               {
-                   PQclear(copy_result);
-                   copy_result = NULL;
-               }
-
-               if (need_close)
-               {
-                   /* close \g argument file/pipe */
-                   if (is_pipe)
-                   {
-                       pclose(copystream);
-                       restore_sigpipe_trap();
-                   }
-                   else
-                   {
-                       fclose(copystream);
-                   }
-               }
+               pclose(copystream);
+               restore_sigpipe_trap();
            }
            else
            {
-               /* COPY IN */
-               copystream = pset.copyStream ? pset.copyStream : pset.cur_cmd_source;
-               success = handleCopyIn(pset.db,
-                                      copystream,
-                                      PQbinaryTuples(*results),
-                                      &copy_result) && success;
+               fclose(copystream);
            }
-           ResetCancelConn();
-
-           /*
-            * Replace the PGRES_COPY_OUT/IN result with COPY command's exit
-            * status, or with NULL if we want to suppress printing anything.
-            */
-           PQclear(*results);
-           *results = copy_result;
-       }
-       else if (first_cycle)
-       {
-           /* fast path: no COPY commands; PQexec visited all results */
-           break;
        }
-
-       /*
-        * Check PQgetResult() again.  In the typical case of a single-command
-        * string, it will return NULL.  Otherwise, we'll have other results
-        * to process that may include other COPYs.  We keep the last result.
-        */
-       next_result = PQgetResult(pset.db);
-       if (!next_result)
-           break;
-
-       PQclear(*results);
-       *results = next_result;
-       first_cycle = false;
+   }
+   else
+   {
+       /* COPY IN */
+       copystream = pset.copyStream ? pset.copyStream : pset.cur_cmd_source;
+       success = handleCopyIn(pset.db,
+                              copystream,
+                              PQbinaryTuples(*result),
+                              &copy_result);
    }
 
-   SetResultVariables(*results, success);
-
-   /* may need this to recover from conn loss during COPY */
-   if (!first_cycle && !CheckConnection())
-       return false;
+   ResetCancelConn();
+   PQclear(*result);
+   *result = copy_result;
 
    return success;
 }
 
-
 /*
  * PrintQueryStatus: report command status as required
  *
- * Note: Utility function for use by PrintQueryResults() only.
+ * Note: Utility function for use by HandleQueryResult() only.
  */
 static void
 PrintQueryStatus(PGresult *results)
@@ -1105,43 +974,50 @@ PrintQueryStatus(PGresult *results)
 
 
 /*
- * PrintQueryResults: print out (or store or execute) query results as required
- *
- * Note: Utility function for use by SendQuery() only.
+ * HandleQueryResult: print out, store or execute one query result
+ * as required.
  *
  * Returns true if the query executed successfully, false otherwise.
  */
 static bool
-PrintQueryResults(PGresult *results)
+HandleQueryResult(PGresult *result, bool last)
 {
    bool        success;
    const char *cmdstatus;
 
-   if (!results)
+   if (result == NULL)
        return false;
 
-   switch (PQresultStatus(results))
+   switch (PQresultStatus(result))
    {
        case PGRES_TUPLES_OK:
            /* store or execute or print the data ... */
-           if (pset.gset_prefix)
-               success = StoreQueryTuple(results);
-           else if (pset.gexec_flag)
-               success = ExecQueryTuples(results);
-           else if (pset.crosstab_flag)
-               success = PrintResultsInCrosstab(results);
+           if (last && pset.gset_prefix)
+               success = StoreQueryTuple(result);
+           else if (last && pset.gexec_flag)
+               success = ExecQueryTuples(result);
+           else if (last && pset.crosstab_flag)
+               success = PrintResultsInCrosstab(result);
+           else if (last || pset.show_all_results)
+               success = PrintQueryTuples(result);
            else
-               success = PrintQueryTuples(results);
+               success = true;
+
            /* if it's INSERT/UPDATE/DELETE RETURNING, also print status */
-           cmdstatus = PQcmdStatus(results);
-           if (strncmp(cmdstatus, "INSERT", 6) == 0 ||
-               strncmp(cmdstatus, "UPDATE", 6) == 0 ||
-               strncmp(cmdstatus, "DELETE", 6) == 0)
-               PrintQueryStatus(results);
+           if (last || pset.show_all_results)
+           {
+               cmdstatus = PQcmdStatus(result);
+               if (strncmp(cmdstatus, "INSERT", 6) == 0 ||
+                   strncmp(cmdstatus, "UPDATE", 6) == 0 ||
+                   strncmp(cmdstatus, "DELETE", 6) == 0)
+                   PrintQueryStatus(result);
+           }
+
            break;
 
        case PGRES_COMMAND_OK:
-           PrintQueryStatus(results);
+           if (last || pset.show_all_results)
+               PrintQueryStatus(result);
            success = true;
            break;
 
@@ -1151,7 +1027,7 @@ PrintQueryResults(PGresult *results)
 
        case PGRES_COPY_OUT:
        case PGRES_COPY_IN:
-           /* nothing to do here */
+           /* nothing to do here: already processed */
            success = true;
            break;
 
@@ -1164,7 +1040,7 @@ PrintQueryResults(PGresult *results)
        default:
            success = false;
            pg_log_error("unexpected PQresultStatus: %d",
-                        PQresultStatus(results));
+                        PQresultStatus(result));
            break;
    }
 
@@ -1173,6 +1049,217 @@ PrintQueryResults(PGresult *results)
    return success;
 }
 
+/*
+ * Data structure and functions to record notices while they are
+ * emitted, so that they can be shown later.
+ *
+ * We need to know which result is last, which requires to extract
+ * one result in advance, hence two buffers are needed.
+ */
+typedef struct {
+   bool            in_flip;
+   PQExpBufferData flip;
+   PQExpBufferData flop;
+} t_notice_messages;
+
+/*
+ * Store notices in appropriate buffer, for later display.
+ */
+static void
+AppendNoticeMessage(void *arg, const char *msg)
+{
+   t_notice_messages *notes = (t_notice_messages*) arg;
+   appendPQExpBufferStr(notes->in_flip ? &notes->flip : &notes->flop, msg);
+}
+
+/*
+ * Show notices stored in buffer, which is then reset.
+ */
+static void
+ShowNoticeMessage(t_notice_messages *notes)
+{
+   PQExpBufferData *current = notes->in_flip ? &notes->flip : &notes->flop;
+   if (current->data != NULL && *current->data != '\0')
+       pg_log_info("%s", current->data);
+   resetPQExpBuffer(current);
+}
+
+/*
+ * SendQueryAndProcessResults: utility function for use by SendQuery()
+ * and PSQLexecWatch().
+ *
+ * Sends query and cycles through PGresult objects.
+ *
+ * When not under \watch and if our command string contained a COPY FROM STDIN
+ * or COPY TO STDOUT, the PGresult associated with these commands must be
+ * processed by providing an input or output stream.  In that event, we'll
+ * marshal data for the COPY.
+ *
+ * For other commands, the results are processed normally, depending on their
+ * status.
+ *
+ * Returns 1 on complete success, 0 on interrupt and -1 or errors.  Possible
+ * failure modes include purely client-side problems; check the transaction
+ * status for the server-side opinion.
+ *
+ * Note that on a combined query, failure does not mean that nothing was
+ * committed.
+ */
+static int
+SendQueryAndProcessResults(const char *query, double *pelapsed_msec, bool is_watch)
+{
+   bool                success;
+   instr_time          before;
+   PGresult           *result;
+   t_notice_messages   notes;
+
+   if (pset.timing)
+       INSTR_TIME_SET_CURRENT(before);
+
+   success = PQsendQuery(pset.db, query);
+   ResetCancelConn();
+
+   if (!success)
+   {
+       const char *error = PQerrorMessage(pset.db);
+
+       if (strlen(error))
+           pg_log_info("%s", error);
+
+       CheckConnection();
+
+       return -1;
+   }
+
+   /*
+    * If SIGINT is sent while the query is processing, the interrupt will be
+    * consumed.  The user's intention, though, is to cancel the entire watch
+    * process, so detect a sent cancellation request and exit in this case.
+    */
+   if (is_watch && cancel_pressed)
+   {
+       ClearOrSaveAllResults();
+       return 0;
+   }
+
+   /* intercept notices */
+   notes.in_flip = true;
+   initPQExpBuffer(&notes.flip);
+   initPQExpBuffer(&notes.flop);
+   PQsetNoticeProcessor(pset.db, AppendNoticeMessage, &notes);
+
+   /* first result */
+   result = PQgetResult(pset.db);
+
+   while (result != NULL)
+   {
+       ExecStatusType result_status;
+       PGresult   *next_result;
+       bool        last;
+
+       if (!AcceptResult(result, false))
+       {
+           /*
+            * Some error occured, either a server-side failure or
+            * a failure to submit the command string.  Record that.
+            */
+           const char *error = PQerrorMessage(pset.db);
+
+           ShowNoticeMessage(&notes);
+           if (strlen(error))
+               pg_log_info("%s", error);
+           CheckConnection();
+           if (!is_watch)
+               SetResultVariables(result, false);
+           ClearOrSaveResult(result);
+           success = false;
+
+           /* and switch to next result */
+           result = PQgetResult(pset.db);
+           continue;
+       }
+
+       /* must handle COPY before changing the current result */
+       result_status = PQresultStatus(result);
+       Assert(result_status != PGRES_COPY_BOTH);
+       if (result_status == PGRES_COPY_IN ||
+           result_status == PGRES_COPY_OUT)
+       {
+           ShowNoticeMessage(&notes);
+
+           if (is_watch)
+           {
+               ClearOrSaveAllResults();
+               pg_log_error("\\watch cannot be used with COPY");
+               return -1;
+           }
+
+           /* use normal notice processor during COPY */
+           PQsetNoticeProcessor(pset.db, NoticeProcessor, NULL);
+
+           success &= HandleCopyResult(&result);
+
+           PQsetNoticeProcessor(pset.db, AppendNoticeMessage, &notes);
+       }
+
+       /*
+        * Check PQgetResult() again.  In the typical case of a single-command
+        * string, it will return NULL.  Otherwise, we'll have other results
+        * to process.
+        */
+       notes.in_flip = !notes.in_flip;
+       next_result = PQgetResult(pset.db);
+       notes.in_flip = !notes.in_flip;
+       last = (next_result == NULL);
+
+       /*
+        * Get timing measure before printing the last result.
+        *
+        * It will include the display of previous results, if any.
+        * This cannot be helped because the server goes on processing
+        * further queries anyway while the previous ones are being displayed.
+        * The parallel execution of the client display hides the server time
+        * when it is shorter.
+        *
+        * With combined queries, timing must be understood as an upper bound
+        * of the time spent processing them.
+        */
+       if (last && pset.timing)
+       {
+           instr_time  now;
+           INSTR_TIME_SET_CURRENT(now);
+           INSTR_TIME_SUBTRACT(now, before);
+           *pelapsed_msec = INSTR_TIME_GET_MILLISEC(now);
+       }
+
+       /* notices already shown above for copy */
+       ShowNoticeMessage(&notes);
+
+       /* this may or may not print something depending on settings */
+       if (result != NULL)
+           success &= HandleQueryResult(result, last);
+
+       /* set variables on last result if all went well */
+       if (!is_watch && last && success)
+           SetResultVariables(result, true);
+
+       ClearOrSaveResult(result);
+       notes.in_flip = !notes.in_flip;
+       result = next_result;
+   }
+
+   /* reset notice hook */
+   PQsetNoticeProcessor(pset.db, NoticeProcessor, NULL);
+   termPQExpBuffer(&notes.flip);
+   termPQExpBuffer(&notes.flop);
+
+   /* may need this to recover from conn loss during COPY */
+   if (!CheckConnection())
+       return -1;
+
+   return success ? 1 : -1;
+}
+
 
 /*
  * SendQuery: send the query string to the backend
@@ -1294,28 +1381,9 @@ SendQuery(const char *query)
             pset.crosstab_flag || !is_select_command(query))
    {
        /* Default fetch-it-all-and-print mode */
-       instr_time  before,
-                   after;
-
-       if (pset.timing)
-           INSTR_TIME_SET_CURRENT(before);
-
-       results = PQexec(pset.db, query);
-
-       /* these operations are included in the timing result: */
-       ResetCancelConn();
-       OK = ProcessResult(&results);
-
-       if (pset.timing)
-       {
-           INSTR_TIME_SET_CURRENT(after);
-           INSTR_TIME_SUBTRACT(after, before);
-           elapsed_msec = INSTR_TIME_GET_MILLISEC(after);
-       }
-
-       /* but printing results isn't: */
-       if (OK && results)
-           OK = PrintQueryResults(results);
+       int res = SendQueryAndProcessResults(query, &elapsed_msec, false);
+       OK = (res >= 0);
+       results = NULL;
    }
    else
    {
@@ -1497,7 +1565,7 @@ DescribeQuery(const char *query, double *elapsed_msec)
    PQclear(results);
 
    results = PQdescribePrepared(pset.db, "");
-   OK = AcceptResult(results) &&
+   OK = AcceptResult(results, true) &&
        (PQresultStatus(results) == PGRES_COMMAND_OK);
    if (OK && results)
    {
@@ -1545,7 +1613,7 @@ DescribeQuery(const char *query, double *elapsed_msec)
            PQclear(results);
 
            results = PQexec(pset.db, buf.data);
-           OK = AcceptResult(results);
+           OK = AcceptResult(results, true);
 
            if (pset.timing)
            {
@@ -1555,7 +1623,7 @@ DescribeQuery(const char *query, double *elapsed_msec)
            }
 
            if (OK && results)
-               OK = PrintQueryResults(results);
+               OK = HandleQueryResult(results, true);
 
            termPQExpBuffer(&buf);
        }
@@ -1614,7 +1682,7 @@ ExecQueryUsingCursor(const char *query, double *elapsed_msec)
    if (PQtransactionStatus(pset.db) == PQTRANS_IDLE)
    {
        results = PQexec(pset.db, "BEGIN");
-       OK = AcceptResult(results) &&
+       OK = AcceptResult(results, true) &&
            (PQresultStatus(results) == PGRES_COMMAND_OK);
        ClearOrSaveResult(results);
        if (!OK)
@@ -1628,7 +1696,7 @@ ExecQueryUsingCursor(const char *query, double *elapsed_msec)
                      query);
 
    results = PQexec(pset.db, buf.data);
-   OK = AcceptResult(results) &&
+   OK = AcceptResult(results, true) &&
        (PQresultStatus(results) == PGRES_COMMAND_OK);
    if (!OK)
        SetResultVariables(results, OK);
@@ -1701,7 +1769,7 @@ ExecQueryUsingCursor(const char *query, double *elapsed_msec)
                is_pager = false;
            }
 
-           OK = AcceptResult(results);
+           OK = AcceptResult(results, true);
            Assert(!OK);
            SetResultVariables(results, OK);
            ClearOrSaveResult(results);
@@ -1810,7 +1878,7 @@ cleanup:
    results = PQexec(pset.db, "CLOSE _psql_cursor");
    if (OK)
    {
-       OK = AcceptResult(results) &&
+       OK = AcceptResult(results, true) &&
            (PQresultStatus(results) == PGRES_COMMAND_OK);
        ClearOrSaveResult(results);
    }
@@ -1820,7 +1888,7 @@ cleanup:
    if (started_txn)
    {
        results = PQexec(pset.db, OK ? "COMMIT" : "ROLLBACK");
-       OK &= AcceptResult(results) &&
+       OK &= AcceptResult(results, true) &&
            (PQresultStatus(results) == PGRES_COMMAND_OK);
        ClearOrSaveResult(results);
    }
index 99a59470c5dc86c1e8d0f2f883cb2b319daf6061..ac9a89a889ba8abd011d14b9c1fc0d16def35ba6 100644 (file)
@@ -410,6 +410,8 @@ helpVariables(unsigned short int pager)
    fprintf(output, _("  SERVER_VERSION_NAME\n"
                      "  SERVER_VERSION_NUM\n"
                      "    server's version (in short string or numeric format)\n"));
+   fprintf(output, _("  SHOW_ALL_RESULTS\n"
+                     "    show all results of a combined query (\\;) instead of only the last\n"));
    fprintf(output, _("  SHOW_CONTEXT\n"
                      "    controls display of message context fields [never, errors, always]\n"));
    fprintf(output, _("  SINGLELINE\n"
index 83f2e6f254edd030ef7949df44eb031ff769fbc7..62583ad6ca6fc7ba46e7341684e406989b1dda90 100644 (file)
@@ -148,6 +148,7 @@ typedef struct _psqlSettings
    const char *prompt2;
    const char *prompt3;
    PGVerbosity verbosity;      /* current error verbosity level */
+   bool        show_all_results;
    PGContextVisibility show_context;   /* current context display level */
 } PsqlSettings;
 
index 110906a4e959b46d4540f6c78580bdde77d3e950..17437ce07dd13e227493c083e8a8cc4854b89148 100644 (file)
@@ -196,6 +196,7 @@ main(int argc, char *argv[])
    SetVariable(pset.vars, "PROMPT1", DEFAULT_PROMPT1);
    SetVariable(pset.vars, "PROMPT2", DEFAULT_PROMPT2);
    SetVariable(pset.vars, "PROMPT3", DEFAULT_PROMPT3);
+   SetVariableBool(pset.vars, "SHOW_ALL_RESULTS");
 
    parse_psql_options(argc, argv, &options);
 
@@ -1130,6 +1131,12 @@ verbosity_hook(const char *newval)
    return true;
 }
 
+static bool
+show_all_results_hook(const char *newval)
+{
+   return ParseVariableBool(newval, "SHOW_ALL_RESULTS", &pset.show_all_results);
+}
+
 static char *
 show_context_substitute_hook(char *newval)
 {
@@ -1231,6 +1238,9 @@ EstablishVariableSpace(void)
    SetVariableHooks(pset.vars, "VERBOSITY",
                     verbosity_substitute_hook,
                     verbosity_hook);
+   SetVariableHooks(pset.vars, "SHOW_ALL_RESULTS",
+                    bool_substitute_hook,
+                    show_all_results_hook);
    SetVariableHooks(pset.vars, "SHOW_CONTEXT",
                     show_context_substitute_hook,
                     show_context_hook);
index 832bcdfc3bf9f4f0f9a0aafa6b5a923fdfdcc252..891997ca36975cca5b6d9e7392ff8359dadff31c 100644 (file)
@@ -4122,7 +4122,7 @@ psql_completion(const char *text, int start, int end)
        matches = complete_from_variables(text, "", "", false);
    else if (TailMatchesCS("\\set", MatchAny))
    {
-       if (TailMatchesCS("AUTOCOMMIT|ON_ERROR_STOP|QUIET|"
+       if (TailMatchesCS("AUTOCOMMIT|ON_ERROR_STOP|QUIET|SHOW_ALL_RESULTS|"
                          "SINGLELINE|SINGLESTEP"))
            COMPLETE_WITH_CS("on", "off");
        else if (TailMatchesCS("COMP_KEYWORD_CASE"))
index 72865fe1ebeeac818eea2457828b3e24fb73668f..bb9e026f913aed4963f8d42ab289eaab0a73918e 100644 (file)
@@ -126,7 +126,7 @@ copy (select 1) to stdout\; select 1/0; -- row, then error
 ERROR:  division by zero
 select 1/0\; copy (select 1) to stdout; -- error only
 ERROR:  division by zero
-copy (select 1) to stdout\; copy (select 2) to stdout\; select 0\; select 3; -- 1 2 3
+copy (select 1) to stdout\; copy (select 2) to stdout\; select 3\; select 4; -- 1 2 3 4
 1
 2
  ?column? 
@@ -134,8 +134,18 @@ copy (select 1) to stdout\; copy (select 2) to stdout\; select 0\; select 3; --
         3
 (1 row)
 
+ ?column? 
+----------
+        4
+(1 row)
+
 create table test3 (c int);
-select 0\; copy test3 from stdin\; copy test3 from stdin\; select 1; -- 1
+select 0\; copy test3 from stdin\; copy test3 from stdin\; select 1; -- 0 1
+ ?column? 
+----------
+        0
+(1 row)
+
  ?column? 
 ----------
         1
index 76050af797a763f1b69a64bbbbf0fc738faa0f8d..9a51940530ab844375df68193ddf6efd1cb0a6fd 100644 (file)
@@ -5078,3 +5078,96 @@ List of access methods
  hash  | uuid_ops        | uuid                 | uuid                  |      2 | uuid_hash_extended
 (5 rows)
 
+--
+-- combined queries
+--
+CREATE FUNCTION warn(msg TEXT) RETURNS BOOLEAN LANGUAGE plpgsql
+AS $$
+  BEGIN RAISE NOTICE 'warn %', msg ; RETURN TRUE ; END
+$$;
+-- show both
+SELECT 1 AS one \; SELECT warn('1.5') \; SELECT 2 AS two ;
+ one 
+-----
+   1
+(1 row)
+
+NOTICE:  warn 1.5
+CONTEXT:  PL/pgSQL function warn(text) line 2 at RAISE
+ warn 
+------
+ t
+(1 row)
+
+ two 
+-----
+   2
+(1 row)
+
+-- \gset applies to last query only
+SELECT 3 AS three \; SELECT warn('3.5') \; SELECT 4 AS four \gset
+ three 
+-------
+     3
+(1 row)
+
+NOTICE:  warn 3.5
+CONTEXT:  PL/pgSQL function warn(text) line 2 at RAISE
+ warn 
+------
+ t
+(1 row)
+
+\echo :three :four
+:three 4
+-- syntax error stops all processing
+SELECT 5 \; SELECT 6 + \; SELECT warn('6.5') \; SELECT 7 ;
+ERROR:  syntax error at or near ";"
+LINE 1: SELECT 5 ; SELECT 6 + ; SELECT warn('6.5') ; SELECT 7 ;
+                              ^
+-- with aborted transaction, stop on first error
+BEGIN \; SELECT 8 AS eight \; SELECT 9/0 AS nine \; ROLLBACK \; SELECT 10 AS ten ;
+ eight 
+-------
+     8
+(1 row)
+
+ERROR:  division by zero
+-- close previously aborted transaction
+ROLLBACK;
+-- misc SQL commands
+-- (non SELECT output is sent to stderr, thus is not shown in expected results)
+SELECT 'ok' AS "begin" \;
+CREATE TABLE psql_comics(s TEXT) \;
+INSERT INTO psql_comics VALUES ('Calvin'), ('hobbes') \;
+COPY psql_comics FROM STDIN \;
+UPDATE psql_comics SET s = 'Hobbes' WHERE s = 'hobbes' \;
+DELETE FROM psql_comics WHERE s = 'Moe' \;
+COPY psql_comics TO STDOUT \;
+TRUNCATE psql_comics \;
+DROP TABLE psql_comics \;
+SELECT 'ok' AS "done" ;
+ begin 
+-------
+ ok
+(1 row)
+
+Calvin
+Susie
+Hobbes
+ done 
+------
+ ok
+(1 row)
+
+\set SHOW_ALL_RESULTS off
+SELECT 1 AS one \; SELECT warn('1.5') \; SELECT 2 AS two ;
+NOTICE:  warn 1.5
+CONTEXT:  PL/pgSQL function warn(text) line 2 at RAISE
+ two 
+-----
+   2
+(1 row)
+
+\set SHOW_ALL_RESULTS on
+DROP FUNCTION warn(TEXT);
index 61862d595d1abec90127bb56a4ff17f2581c750c..be1db0d5c0b5318ca88d4790ae64ac4ca67f0d12 100644 (file)
@@ -900,8 +900,18 @@ DROP TABLE abc;
 -- tests rely on the fact that psql will not break SQL commands apart at a
 -- backslash-quoted semicolon, but will send them as one Query.
 create temp table i_table (f1 int);
--- psql will show only the last result in a multi-statement Query
+-- psql will show all results of a multi-statement Query
 SELECT 1\; SELECT 2\; SELECT 3;
+ ?column? 
+----------
+        1
+(1 row)
+
+ ?column? 
+----------
+        2
+(1 row)
+
  ?column? 
 ----------
         3
@@ -916,6 +926,12 @@ insert into i_table values(1)\; select * from i_table;
 
 -- 1/0 error will cause rolling back the whole implicit transaction
 insert into i_table values(2)\; select * from i_table\; select 1/0;
+ f1 
+----
+  1
+  2
+(2 rows)
+
 ERROR:  division by zero
 select * from i_table;
  f1 
@@ -935,8 +951,18 @@ WARNING:  there is no transaction in progress
 -- begin converts implicit transaction into a regular one that
 -- can extend past the end of the Query
 select 1\; begin\; insert into i_table values(5);
+ ?column? 
+----------
+        1
+(1 row)
+
 commit;
 select 1\; begin\; insert into i_table values(6);
+ ?column? 
+----------
+        1
+(1 row)
+
 rollback;
 -- commit in implicit-transaction state commits but issues a warning.
 insert into i_table values(7)\; commit\; insert into i_table values(8)\; select 1/0;
@@ -963,22 +989,52 @@ rollback;  -- we are not in a transaction at this point
 WARNING:  there is no transaction in progress
 -- implicit transaction block is still a transaction block, for e.g. VACUUM
 SELECT 1\; VACUUM;
+ ?column? 
+----------
+        1
+(1 row)
+
 ERROR:  VACUUM cannot run inside a transaction block
 SELECT 1\; COMMIT\; VACUUM;
 WARNING:  there is no transaction in progress
+ ?column? 
+----------
+        1
+(1 row)
+
 ERROR:  VACUUM cannot run inside a transaction block
 -- we disallow savepoint-related commands in implicit-transaction state
 SELECT 1\; SAVEPOINT sp;
+ ?column? 
+----------
+        1
+(1 row)
+
 ERROR:  SAVEPOINT can only be used in transaction blocks
 SELECT 1\; COMMIT\; SAVEPOINT sp;
 WARNING:  there is no transaction in progress
+ ?column? 
+----------
+        1
+(1 row)
+
 ERROR:  SAVEPOINT can only be used in transaction blocks
 ROLLBACK TO SAVEPOINT sp\; SELECT 2;
 ERROR:  ROLLBACK TO SAVEPOINT can only be used in transaction blocks
 SELECT 2\; RELEASE SAVEPOINT sp\; SELECT 3;
+ ?column? 
+----------
+        2
+(1 row)
+
 ERROR:  RELEASE SAVEPOINT can only be used in transaction blocks
 -- but this is OK, because the BEGIN converts it to a regular xact
 SELECT 1\; BEGIN\; SAVEPOINT sp\; ROLLBACK TO SAVEPOINT sp\; COMMIT;
+ ?column? 
+----------
+        1
+(1 row)
+
 -- Tests for AND CHAIN in implicit transaction blocks
 SET TRANSACTION READ ONLY\; COMMIT AND CHAIN;  -- error
 ERROR:  COMMIT AND CHAIN can only be used in transaction blocks
index 1d98dad3c8c55517d241c147f6baa4cbd9e31e38..e32a4f8e38e52e6bb67e20cd0991a18a49cc8634 100644 (file)
@@ -84,10 +84,10 @@ drop table test1;
 -- psql handling of COPY in multi-command strings
 copy (select 1) to stdout\; select 1/0;    -- row, then error
 select 1/0\; copy (select 1) to stdout; -- error only
-copy (select 1) to stdout\; copy (select 2) to stdout\; select 0\; select 3; -- 1 2 3
+copy (select 1) to stdout\; copy (select 2) to stdout\; select 3\; select 4; -- 1 2 3 4
 
 create table test3 (c int);
-select 0\; copy test3 from stdin\; copy test3 from stdin\; select 1; -- 1
+select 0\; copy test3 from stdin\; copy test3 from stdin\; select 1; -- 1
 1
 \.
 2
index 537d5332aa90c0a0ce40b34550f5e687e5abcd9a..bf06bb87b5021d65125e23f9af43bcb17c960b0f 100644 (file)
@@ -1228,3 +1228,41 @@ drop role regress_partitioning_role;
 \dAo * pg_catalog.jsonb_path_ops
 \dAp+ btree float_ops
 \dAp * pg_catalog.uuid_ops
+
+--
+-- combined queries
+--
+CREATE FUNCTION warn(msg TEXT) RETURNS BOOLEAN LANGUAGE plpgsql
+AS $$
+  BEGIN RAISE NOTICE 'warn %', msg ; RETURN TRUE ; END
+$$;
+-- show both
+SELECT 1 AS one \; SELECT warn('1.5') \; SELECT 2 AS two ;
+-- \gset applies to last query only
+SELECT 3 AS three \; SELECT warn('3.5') \; SELECT 4 AS four \gset
+\echo :three :four
+-- syntax error stops all processing
+SELECT 5 \; SELECT 6 + \; SELECT warn('6.5') \; SELECT 7 ;
+-- with aborted transaction, stop on first error
+BEGIN \; SELECT 8 AS eight \; SELECT 9/0 AS nine \; ROLLBACK \; SELECT 10 AS ten ;
+-- close previously aborted transaction
+ROLLBACK;
+-- misc SQL commands
+-- (non SELECT output is sent to stderr, thus is not shown in expected results)
+SELECT 'ok' AS "begin" \;
+CREATE TABLE psql_comics(s TEXT) \;
+INSERT INTO psql_comics VALUES ('Calvin'), ('hobbes') \;
+COPY psql_comics FROM STDIN \;
+UPDATE psql_comics SET s = 'Hobbes' WHERE s = 'hobbes' \;
+DELETE FROM psql_comics WHERE s = 'Moe' \;
+COPY psql_comics TO STDOUT \;
+TRUNCATE psql_comics \;
+DROP TABLE psql_comics \;
+SELECT 'ok' AS "done" ;
+Moe
+Susie
+\.
+\set SHOW_ALL_RESULTS off
+SELECT 1 AS one \; SELECT warn('1.5') \; SELECT 2 AS two ;
+\set SHOW_ALL_RESULTS on
+DROP FUNCTION warn(TEXT);
index 8886280c0a628faa1afff0a95cc7920f5eea8911..7fc9f094680b454e0d32ff69e9790089f66db2a7 100644 (file)
@@ -504,7 +504,7 @@ DROP TABLE abc;
 
 create temp table i_table (f1 int);
 
--- psql will show only the last result in a multi-statement Query
+-- psql will show all results of a multi-statement Query
 SELECT 1\; SELECT 2\; SELECT 3;
 
 -- this implicitly commits: