public inbox for [email protected]  
help / color / mirror / Atom feed
[PATCH v7 3/7] Propagate changes to indisclustered to child/parents
14+ messages / 4 participants
[nested] [flat]

* [PATCH v7 3/7] Propagate changes to indisclustered to child/parents
@ 2020-10-07 03:11 Justin Pryzby <[email protected]>
  0 siblings, 0 replies; 14+ messages in thread

From: Justin Pryzby @ 2020-10-07 03:11 UTC (permalink / raw)

---
 src/backend/commands/cluster.c        | 109 ++++++++++++++++----------
 src/backend/commands/indexcmds.c      |   2 +
 src/test/regress/expected/cluster.out |  46 +++++++++++
 src/test/regress/sql/cluster.sql      |  11 +++
 4 files changed, 125 insertions(+), 43 deletions(-)

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 9b6673867c..78c2c2ba72 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -74,6 +74,7 @@ static void rebuild_relation(Relation OldHeap, Oid indexOid, bool verbose);
 static void copy_table_data(Oid OIDNewHeap, Oid OIDOldHeap, Oid OIDOldIndex,
 							bool verbose, bool *pSwapToastByContent,
 							TransactionId *pFreezeXid, MultiXactId *pCutoffMulti);
+static void set_indisclustered(Oid indexOid, bool isclustered, Relation pg_index);
 static List *get_tables_to_cluster(MemoryContext cluster_context);
 static List *get_tables_to_cluster_partitioned(MemoryContext cluster_context,
 		Oid indexOid);
@@ -510,66 +511,88 @@ check_index_is_clusterable(Relation OldHeap, Oid indexOid, bool recheck, LOCKMOD
 	index_close(OldIndex, NoLock);
 }
 
+/*
+ * Helper for mark_index_clustered
+ * Mark a single index as clustered or not.
+ * pg_index is passed by caller to avoid repeatedly re-opening it.
+ */
+static void
+set_indisclustered(Oid indexOid, bool isclustered, Relation pg_index)
+{
+	HeapTuple	indexTuple;
+	Form_pg_index indexForm;
+
+	indexTuple = SearchSysCacheCopy1(INDEXRELID,
+									ObjectIdGetDatum(indexOid));
+	if (!HeapTupleIsValid(indexTuple))
+		elog(ERROR, "cache lookup failed for index %u", indexOid);
+	indexForm = (Form_pg_index) GETSTRUCT(indexTuple);
+
+	/* this was checked earlier, but let's be real sure */
+	if (isclustered && !indexForm->indisvalid)
+		elog(ERROR, "cannot cluster on invalid index %u", indexOid);
+
+	indexForm->indisclustered = isclustered;
+	CatalogTupleUpdate(pg_index, &indexTuple->t_self, indexTuple);
+	heap_freetuple(indexTuple);
+}
+
 /*
  * mark_index_clustered: mark the specified index as the one clustered on
  *
- * With indexOid == InvalidOid, will mark all indexes of rel not-clustered.
+ * With indexOid == InvalidOid, mark all indexes of rel not-clustered.
+ * Otherwise, mark children of the clustered index as clustered, and parents of
+ * other indexes as unclustered.
+ * We wish to maintain the following properties:
+ * 1) Only one index on a relation can be marked clustered at once
+ * 2) If a partitioned index is clustered, then all its children must be
+ *     clustered.
  */
 void
 mark_index_clustered(Relation rel, Oid indexOid, bool is_internal)
 {
-	HeapTuple	indexTuple;
-	Form_pg_index indexForm;
-	Relation	pg_index;
-	ListCell   *index;
-
-	/*
-	 * If the index is already marked clustered, no need to do anything.
-	 */
-	if (OidIsValid(indexOid))
-	{
-		if (get_index_isclustered(indexOid))
-			return;
-	}
+	ListCell	*lc, *lc2;
+	List		*indexes;
+	Relation	pg_index = table_open(IndexRelationId, RowExclusiveLock);
+	List		*inh = find_all_inheritors(RelationGetRelid(rel), ShareRowExclusiveLock, NULL);
 
 	/*
 	 * Check each index of the relation and set/clear the bit as needed.
+	 * Iterate over the relation's children rather than the index's children
+	 * since we need to unset cluster for indexes on intermediate children,
+	 * too.
 	 */
-	pg_index = table_open(IndexRelationId, RowExclusiveLock);
-
-	foreach(index, RelationGetIndexList(rel))
+	foreach(lc, inh)
 	{
-		Oid			thisIndexOid = lfirst_oid(index);
-
-		indexTuple = SearchSysCacheCopy1(INDEXRELID,
-										 ObjectIdGetDatum(thisIndexOid));
-		if (!HeapTupleIsValid(indexTuple))
-			elog(ERROR, "cache lookup failed for index %u", thisIndexOid);
-		indexForm = (Form_pg_index) GETSTRUCT(indexTuple);
+		Oid			inhrelid = lfirst_oid(lc);
+		Relation	thisrel = table_open(inhrelid, ShareRowExclusiveLock);
 
-		/*
-		 * Unset the bit if set.  We know it's wrong because we checked this
-		 * earlier.
-		 */
-		if (indexForm->indisclustered)
+		indexes = RelationGetIndexList(thisrel);
+		foreach (lc2, indexes)
 		{
-			indexForm->indisclustered = false;
-			CatalogTupleUpdate(pg_index, &indexTuple->t_self, indexTuple);
-		}
-		else if (thisIndexOid == indexOid)
-		{
-			/* this was checked earlier, but let's be real sure */
-			if (!indexForm->indisvalid)
-				elog(ERROR, "cannot cluster on invalid index %u", indexOid);
-			indexForm->indisclustered = true;
-			CatalogTupleUpdate(pg_index, &indexTuple->t_self, indexTuple);
-		}
+			bool	isclustered;
+			Oid		thisIndexOid = lfirst_oid(lc2);
+			List	*parentoids = get_rel_relispartition(thisIndexOid) ?
+				get_partition_ancestors(thisIndexOid) : NIL;
 
-		InvokeObjectPostAlterHookArg(IndexRelationId, thisIndexOid, 0,
-									 InvalidOid, is_internal);
+			/*
+			 * A child of the clustered index must be set clustered;
+			 * indexes which are not children of the clustered index are
+			 * set unclustered
+			 */
+			isclustered = (thisIndexOid == indexOid) ||
+					list_member_oid(parentoids, indexOid);
+			Assert(OidIsValid(indexOid) || !isclustered);
+			set_indisclustered(thisIndexOid, isclustered, pg_index);
+
+			InvokeObjectPostAlterHookArg(IndexRelationId, thisIndexOid, 0,
+										 InvalidOid, is_internal);
+		}
 
-		heap_freetuple(indexTuple);
+		list_free(indexes);
+		table_close(thisrel, ShareRowExclusiveLock);
 	}
+	list_free(inh);
 
 	table_close(pg_index, RowExclusiveLock);
 }
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 127ba7835d..4ca1ffbfa4 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -25,6 +25,7 @@
 #include "catalog/catalog.h"
 #include "catalog/index.h"
 #include "catalog/indexing.h"
+#include "catalog/partition.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_inherits.h"
@@ -32,6 +33,7 @@
 #include "catalog/pg_opfamily.h"
 #include "catalog/pg_tablespace.h"
 #include "catalog/pg_type.h"
+#include "commands/cluster.h"
 #include "commands/comment.h"
 #include "commands/dbcommands.h"
 #include "commands/defrem.h"
diff --git a/src/test/regress/expected/cluster.out b/src/test/regress/expected/cluster.out
index c74cfa88cc..1d436dfaae 100644
--- a/src/test/regress/expected/cluster.out
+++ b/src/test/regress/expected/cluster.out
@@ -495,6 +495,52 @@ Indexes:
     "clstrpart_idx" btree (a) CLUSTER
 Number of partitions: 3 (Use \d+ to list them.)
 
+-- Test that it recurses to grandchildren:
+\d clstrpart33
+            Table "public.clstrpart33"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+Partition of: clstrpart3 DEFAULT
+Indexes:
+    "clstrpart33_a_idx" btree (a) CLUSTER
+
+ALTER TABLE clstrpart SET WITHOUT CLUSTER;
+\d clstrpart33
+            Table "public.clstrpart33"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+Partition of: clstrpart3 DEFAULT
+Indexes:
+    "clstrpart33_a_idx" btree (a)
+
+ALTER TABLE clstrpart CLUSTER ON clstrpart_idx;
+\d clstrpart33
+            Table "public.clstrpart33"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+Partition of: clstrpart3 DEFAULT
+Indexes:
+    "clstrpart33_a_idx" btree (a) CLUSTER
+
+-- Check that only one child is marked clustered after marking clustered on a different parent
+CREATE INDEX clstrpart1_idx_2 ON clstrpart1(a);
+ALTER TABLE clstrpart CLUSTER ON clstrpart_idx;
+ALTER TABLE clstrpart1 CLUSTER ON clstrpart1_idx_2;
+\d clstrpart1
+       Partitioned table "public.clstrpart1"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+Partition of: clstrpart FOR VALUES FROM (1) TO (10)
+Partition key: RANGE (a)
+Indexes:
+    "clstrpart1_a_idx" btree (a)
+    "clstrpart1_idx_2" btree (a) CLUSTER
+Number of partitions: 2 (Use \d+ to list them.)
+
 -- Test CLUSTER with external tuplesorting
 create table clstr_4 as select * from tenk1;
 create index cluster_sort on clstr_4 (hundred, thousand, tenthous);
diff --git a/src/test/regress/sql/cluster.sql b/src/test/regress/sql/cluster.sql
index 9bcc77695c..0ded2be1ca 100644
--- a/src/test/regress/sql/cluster.sql
+++ b/src/test/regress/sql/cluster.sql
@@ -220,6 +220,17 @@ CLUSTER clstrpart1 USING clstrpart1_a_idx; -- partition which is itself partitio
 CLUSTER clstrpart12 USING clstrpart12_a_idx; -- partition which is itself partitioned, no childs
 CLUSTER clstrpart2 USING clstrpart2_a_idx; -- leaf
 \d clstrpart
+-- Test that it recurses to grandchildren:
+\d clstrpart33
+ALTER TABLE clstrpart SET WITHOUT CLUSTER;
+\d clstrpart33
+ALTER TABLE clstrpart CLUSTER ON clstrpart_idx;
+\d clstrpart33
+-- Check that only one child is marked clustered after marking clustered on a different parent
+CREATE INDEX clstrpart1_idx_2 ON clstrpart1(a);
+ALTER TABLE clstrpart CLUSTER ON clstrpart_idx;
+ALTER TABLE clstrpart1 CLUSTER ON clstrpart1_idx_2;
+\d clstrpart1
 
 -- Test CLUSTER with external tuplesorting
 
-- 
2.17.0


--AA9g+nFNFPYNJKiL
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
 filename="v7-0004-Invalidate-parent-indexes.patch"



^ permalink  raw  reply  [nested|flat] 14+ messages in thread

* Re: psql - add SHOW_ALL_RESULTS option
@ 2022-01-04 07:55 Fabien COELHO <[email protected]>
  2022-01-08 18:32 ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  0 siblings, 1 reply; 14+ messages in thread

From: Fabien COELHO @ 2022-01-04 07:55 UTC (permalink / raw)
  To: Peter Eisentraut <[email protected]>; +Cc: [email protected] <[email protected]>


Hello Peter,

> With this "voluntary crash", the regression test output is now
>
>     psql                         ... ok     (test process exited with exit 
> code 2)      281 ms
>
> Normally, I'd expect this during development if there was a crash somewhere, 
> but showing this during a normal run now, and moreover still saying "ok",

Well, from a testing perspective, the crash is voluntary and it is 
indeed ok:-)

> is quite weird and confusing.  Maybe this type of test should be done in 
> the TAP framework instead.

It could. Another simpler option: add a "psql_voluntary_crash.sql" with 
just that test instead of modifying the "psql.sql" test script? That would 
keep the test exit code information, but the name of the script would make 
things clearer?

Also, if non zero status do not look so ok, should they be noted as bad?

-- 
Fabien.






^ permalink  raw  reply  [nested|flat] 14+ messages in thread

* Re: psql - add SHOW_ALL_RESULTS option
  2022-01-04 07:55 Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
@ 2022-01-08 18:32 ` Fabien COELHO <[email protected]>
  2022-01-13 05:44   ` Re: psql - add SHOW_ALL_RESULTS option Andres Freund <[email protected]>
  0 siblings, 1 reply; 14+ messages in thread

From: Fabien COELHO @ 2022-01-08 18:32 UTC (permalink / raw)
  To: Peter Eisentraut <[email protected]>; +Cc: [email protected] <[email protected]>


Hello Peter,

>> quite weird and confusing.  Maybe this type of test should be done in 
>> the TAP framework instead.

Attached v13 where the crash test is moved to tap.

-- 
Fabien.

Attachments:

  [text/x-diff] psql-show-all-results-13.patch (50.3K, ../../alpine.DEB.2.22.394.2201081931170.731324@pseudo/2-psql-show-all-results-13.patch)
  download | inline diff:
diff --git a/contrib/pg_stat_statements/expected/pg_stat_statements.out b/contrib/pg_stat_statements/expected/pg_stat_statements.out
index e0abe34bb6..8f7f93172a 100644
--- a/contrib/pg_stat_statements/expected/pg_stat_statements.out
+++ b/contrib/pg_stat_statements/expected/pg_stat_statements.out
@@ -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 
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 1ab200a4ad..0a22850912 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -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>
@@ -3570,10 +3563,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>
@@ -4160,6 +4149,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>
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
index f210ccbde8..b8e8c2b245 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -33,6 +33,8 @@ 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, const printQueryOpt *opt, FILE *printQueryFout, bool *tx_ended);
 
 
 /*
@@ -353,7 +355,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 +386,7 @@ AcceptResult(const PGresult *result)
 				break;
 		}
 
-	if (!OK)
+	if (!OK && show_error)
 	{
 		const char *error = PQerrorMessage(pset.db);
 
@@ -472,6 +474,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 +586,7 @@ PSQLexec(const char *query)
 
 	ResetCancelConn();
 
-	if (!AcceptResult(res))
+	if (!AcceptResult(res, true))
 	{
 		ClearOrSaveResult(res);
 		res = NULL;
@@ -595,11 +609,8 @@ int
 PSQLexecWatch(const char *query, const printQueryOpt *opt, FILE *printQueryFout)
 {
 	bool		timing = pset.timing;
-	PGresult   *res;
 	double		elapsed_msec = 0;
-	instr_time	before;
-	instr_time	after;
-	FILE	   *fout;
+	int			res;
 
 	if (!pset.db)
 	{
@@ -608,77 +619,14 @@ PSQLexecWatch(const char *query, const printQueryOpt *opt, FILE *printQueryFout)
 	}
 
 	SetCancelConn(pset.db);
-
-	if (timing)
-		INSTR_TIME_SET_CURRENT(before);
-
-	res = PQexec(pset.db, query);
-
+	res = SendQueryAndProcessResults(query, &elapsed_msec, true, opt, printQueryFout, NULL);
 	ResetCancelConn();
 
-	if (!AcceptResult(res))
-	{
-		ClearOrSaveResult(res);
-		return 0;
-	}
-
-	if (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;
-	}
-
-	fout = printQueryFout ? printQueryFout : pset.queryFout;
-
-	switch (PQresultStatus(res))
-	{
-		case PGRES_TUPLES_OK:
-			printQuery(res, opt, fout, false, pset.logfile);
-			break;
-
-		case PGRES_COMMAND_OK:
-			fprintf(fout, "%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(fout);
-
 	/* Possible microtiming output */
 	if (timing)
 		PrintTiming(elapsed_msec);
 
-	return 1;
+	return res;
 }
 
 
@@ -713,7 +661,7 @@ PrintNotifications(void)
  * Returns true if successful, false otherwise.
  */
 static bool
-PrintQueryTuples(const PGresult *results)
+PrintQueryTuples(const PGresult *results, const printQueryOpt *opt, FILE *printQueryFout)
 {
 	bool		result = true;
 
@@ -745,8 +693,9 @@ PrintQueryTuples(const PGresult *results)
 	}
 	else
 	{
-		printQuery(results, &pset.popt, pset.queryFout, false, pset.logfile);
-		if (ferror(pset.queryFout))
+		FILE *fout = printQueryFout ? printQueryFout : pset.queryFout;
+		printQuery(results, opt ? opt : &pset.popt, fout, false, pset.logfile);
+		if (ferror(fout))
 		{
 			pg_log_error("could not print result table: %m");
 			result = false;
@@ -891,213 +840,131 @@ 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.
- *
- * When the command string contained no such COPY command, this function
- * degenerates to an AcceptResult() call.
- *
- * 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.
+ * 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.
+ *
+ * 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.
+ *
+ * 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;
-
-			case PGRES_COPY_OUT:
-			case PGRES_COPY_IN:
-				is_copy = true;
-				break;
-
-			default:
-				/* AcceptResult() should have caught anything else. */
-				is_copy = false;
-				pg_log_error("unexpected PQresultStatus: %d", result_status);
-				break;
-		}
-
-		if (is_copy)
-		{
-			/*
-			 * 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)
+			/* invoked by \g */
+			if (openQueryOutputFile(pset.gfname,
+									&copystream, &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);
-					}
-				}
+				need_close = true;
+				if (is_pipe)
+					disable_sigpipe_trap();
 			}
 			else
-			{
-				/* COPY IN */
-				copystream = pset.copyStream ? pset.copyStream : pset.cur_cmd_source;
-				success = handleCopyIn(pset.db,
-									   copystream,
-									   PQbinaryTuples(*results),
-									   &copy_result) && success;
-			}
-			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;
+				copystream = NULL;	/* discard COPY data entirely */
 		}
-		else if (first_cycle)
+		else
 		{
-			/* fast path: no COPY commands; PQexec visited all results */
-			break;
+			/* fall back to the generic query output stream */
+			copystream = pset.queryFout;
 		}
 
+		success = handleCopyOut(pset.db,
+								copystream,
+								&copy_result)
+			&& (copystream != NULL);
+
 		/*
-		 * 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.
+		 * 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.
 		 */
-		next_result = PQgetResult(pset.db);
-		if (!next_result)
-			break;
+		if (copystream == pset.queryFout)
+		{
+			PQclear(copy_result);
+			copy_result = NULL;
+		}
 
-		PQclear(*results);
-		*results = next_result;
-		first_cycle = false;
+		if (need_close)
+		{
+			/* close \g argument file/pipe */
+			if (is_pipe)
+			{
+				pclose(copystream);
+				restore_sigpipe_trap();
+			}
+			else
+			{
+				fclose(copystream);
+			}
+		}
+	}
+	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)
+PrintQueryStatus(PGresult *results, FILE *printQueryFout)
 {
 	char		buf[16];
+	FILE	   *fout = printQueryFout ? printQueryFout : pset.queryFout;
 
 	if (!pset.quiet)
 	{
 		if (pset.popt.topt.format == PRINT_HTML)
 		{
-			fputs("<p>", pset.queryFout);
-			html_escaped_print(PQcmdStatus(results), pset.queryFout);
-			fputs("</p>\n", pset.queryFout);
+			fputs("<p>", fout);
+			html_escaped_print(PQcmdStatus(results), fout);
+			fputs("</p>\n", fout);
 		}
 		else
-			fprintf(pset.queryFout, "%s\n", PQcmdStatus(results));
+			fprintf(fout, "%s\n", PQcmdStatus(results));
 	}
 
 	if (pset.logfile)
@@ -1109,43 +976,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 is_watch, const printQueryOpt *opt, FILE *printQueryFout)
 {
 	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, opt, printQueryFout);
 			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, printQueryFout);
+			}
+
 			break;
 
 		case PGRES_COMMAND_OK:
-			PrintQueryStatus(results);
+			if (last || pset.show_all_results)
+				PrintQueryStatus(result, printQueryFout);
 			success = true;
 			break;
 
@@ -1155,7 +1029,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;
 
@@ -1168,15 +1042,261 @@ PrintQueryResults(PGresult *results)
 		default:
 			success = false;
 			pg_log_error("unexpected PQresultStatus: %d",
-						 PQresultStatus(results));
+						 PQresultStatus(result));
 			break;
 	}
 
-	fflush(pset.queryFout);
+	fflush(printQueryFout ? printQueryFout : pset.queryFout);
 
 	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 {
+	PQExpBufferData	messages[2];
+	int				current;
+} 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->messages[notes->current], msg);
+}
+
+/*
+ * Show notices stored in buffer, which is then reset.
+ */
+static void
+ShowNoticeMessage(t_notice_messages *notes)
+{
+	PQExpBufferData	*current = &notes->messages[notes->current];
+	if (*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, const printQueryOpt *opt, FILE *printQueryFout, bool *tx_ended)
+{
+	bool				timing = pset.timing;
+	bool				success;
+	instr_time			before;
+	PGresult		   *result;
+	t_notice_messages	notes;
+
+	if (timing)
+		INSTR_TIME_SET_CURRENT(before);
+
+	success = PQsendQuery(pset.db, query);
+
+	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.current = 0;
+	initPQExpBuffer(&notes.messages[0]);
+	initPQExpBuffer(&notes.messages[1]);
+	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);
+
+				/*
+				 * On connection loss another result with a message will be
+				 * generated, we do not want to see this error again.
+				 */
+				PQclearErrorMessage(pset.db);
+			}
+			CheckConnection();
+			if (!is_watch)
+				SetResultVariables(result, false);
+			ClearOrSaveResult(result);
+			success = false;
+
+			/* and switch to next result */
+			result_status = PQresultStatus(result);
+			if (result_status == PGRES_COPY_BOTH ||
+				result_status == PGRES_COPY_OUT ||
+				result_status == PGRES_COPY_IN)
+				/*
+				 * Hmmm... for some obscure reason PQgetResult does *not*
+				 * return a NULL in these cases despite the result having
+				 * been cleared, but keeps returning an "empty" result that
+				 * we have to ignore manually.
+				 */
+				result = NULL;
+			else
+				result = PQgetResult(pset.db);
+
+			continue;
+		}
+		else if (tx_ended != NULL && ! *tx_ended)
+		{
+			/* on success, tell whether the "current" transaction sequence ended */
+			const char *cmd = PQcmdStatus(result);
+			*tx_ended = strcmp(cmd, "COMMIT") == 0 || strcmp(cmd, "ROLLBACK") == 0 ||
+				strcmp(cmd, "SAVEPOINT") == 0 || strcmp(cmd, "RELEASE") == 0;
+		}
+
+		result_status = PQresultStatus(result);
+
+		/* must handle COPY before changing the current 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. We need to do that to check whether this is the last.
+		 */
+		notes.current ^= 1;
+		next_result = PQgetResult(pset.db);
+		notes.current ^= 1;
+		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 && 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, false, opt, printQueryFout);
+
+		/* set variables on last result if all went well */
+		if (!is_watch && last && success)
+			SetResultVariables(result, true);
+
+		ClearOrSaveResult(result);
+		notes.current ^= 1;
+		result = next_result;
+
+		if (cancel_pressed)
+		{
+			ClearOrSaveAllResults();
+			break;
+		}
+	}
+
+	/* reset notice hook */
+	PQsetNoticeProcessor(pset.db, NoticeProcessor, NULL);
+	termPQExpBuffer(&notes.messages[0]);
+	termPQExpBuffer(&notes.messages[1]);
+
+	/* may need this to recover from conn loss during COPY */
+	if (!CheckConnection())
+		return -1;
+
+	return cancel_pressed ? 0 : success ? 1 : -1;
+}
+
 
 /*
  * SendQuery: send the query string to the backend
@@ -1194,12 +1314,14 @@ bool
 SendQuery(const char *query)
 {
 	bool		timing = pset.timing;
-	PGresult   *results;
+	PGresult   *results = NULL;
 	PGTransactionStatusType transaction_status;
 	double		elapsed_msec = 0;
 	bool		OK = false;
+	bool		res_error;
 	int			i;
 	bool		on_error_rollback_savepoint = false;
+	bool		tx_ended = false;
 
 	if (!pset.db)
 	{
@@ -1238,82 +1360,69 @@ SendQuery(const char *query)
 		fflush(pset.logfile);
 	}
 
+	/* global query cancellation for this query */
 	SetCancelConn(pset.db);
 
 	transaction_status = PQtransactionStatus(pset.db);
 
+	/* issue a BEGIN if needed, corresponding COMMIT/ROLLBACK by user */
 	if (transaction_status == PQTRANS_IDLE &&
 		!pset.autocommit &&
 		!command_no_begin(query))
 	{
-		results = PQexec(pset.db, "BEGIN");
-		if (PQresultStatus(results) != PGRES_COMMAND_OK)
+		PGresult    *result;
+		result = PQexec(pset.db, "BEGIN");
+		res_error = PQresultStatus(result) != PGRES_COMMAND_OK;
+		ClearOrSaveResult(result);
+
+		if (res_error)
 		{
 			pg_log_info("%s", PQerrorMessage(pset.db));
-			ClearOrSaveResult(results);
-			ResetCancelConn();
 			goto sendquery_cleanup;
 		}
-		ClearOrSaveResult(results);
+
+		/* must be PQTRANS_INTRANS after a BEGIN */
 		transaction_status = PQtransactionStatus(pset.db);
 	}
 
+	/* create automatic savepoint if needed and possible */
 	if (transaction_status == PQTRANS_INTRANS &&
 		pset.on_error_rollback != PSQL_ERROR_ROLLBACK_OFF &&
 		(pset.cur_cmd_interactive ||
 		 pset.on_error_rollback == PSQL_ERROR_ROLLBACK_ON))
 	{
-		results = PQexec(pset.db, "SAVEPOINT pg_psql_temporary_savepoint");
-		if (PQresultStatus(results) != PGRES_COMMAND_OK)
+		PGresult   *result;
+		result = PQexec(pset.db, "SAVEPOINT pg_psql_temporary_savepoint");
+		res_error = PQresultStatus(result) != PGRES_COMMAND_OK;
+		ClearOrSaveResult(result);
+
+		if (res_error)
 		{
 			pg_log_info("%s", PQerrorMessage(pset.db));
-			ClearOrSaveResult(results);
-			ResetCancelConn();
 			goto sendquery_cleanup;
 		}
-		ClearOrSaveResult(results);
+
 		on_error_rollback_savepoint = true;
 	}
 
+	/* process the query, one way or the other */
 	if (pset.gdesc_flag)
 	{
 		/* Describe query's result columns, without executing it */
 		OK = DescribeQuery(query, &elapsed_msec);
-		ResetCancelConn();
 		results = NULL;			/* PQclear(NULL) does nothing */
 	}
 	else if (pset.fetch_count <= 0 || pset.gexec_flag ||
 			 pset.crosstab_flag || !is_select_command(query))
 	{
 		/* Default fetch-it-all-and-print mode */
-		instr_time	before,
-					after;
-
-		if (timing)
-			INSTR_TIME_SET_CURRENT(before);
-
-		results = PQexec(pset.db, query);
-
-		/* these operations are included in the timing result: */
-		ResetCancelConn();
-		OK = ProcessResult(&results);
-
-		if (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, NULL, NULL, &tx_ended);
+		OK = (res >= 0);
 	}
 	else
 	{
 		/* Fetch-in-segments mode */
 		OK = ExecQueryUsingCursor(query, &elapsed_msec);
-		ResetCancelConn();
 		results = NULL;			/* PQclear(NULL) does nothing */
 	}
 
@@ -1346,11 +1455,7 @@ SendQuery(const char *query)
 				 * savepoint is gone. If they issued a SAVEPOINT, releasing
 				 * ours would remove theirs.
 				 */
-				if (results &&
-					(strcmp(PQcmdStatus(results), "COMMIT") == 0 ||
-					 strcmp(PQcmdStatus(results), "SAVEPOINT") == 0 ||
-					 strcmp(PQcmdStatus(results), "RELEASE") == 0 ||
-					 strcmp(PQcmdStatus(results), "ROLLBACK") == 0))
+				if (tx_ended)
 					svptcmd = NULL;
 				else
 					svptcmd = "RELEASE pg_psql_temporary_savepoint";
@@ -1372,14 +1477,15 @@ SendQuery(const char *query)
 			PGresult   *svptres;
 
 			svptres = PQexec(pset.db, svptcmd);
-			if (PQresultStatus(svptres) != PGRES_COMMAND_OK)
+			res_error = PQresultStatus(svptres) != PGRES_COMMAND_OK;
+
+			if (res_error)
 			{
 				pg_log_info("%s", PQerrorMessage(pset.db));
 				ClearOrSaveResult(svptres);
 				OK = false;
 
 				PQclear(results);
-				ResetCancelConn();
 				goto sendquery_cleanup;
 			}
 			PQclear(svptres);
@@ -1410,6 +1516,9 @@ SendQuery(const char *query)
 
 sendquery_cleanup:
 
+	/* global cancellation reset */
+	ResetCancelConn();
+
 	/* reset \g's output-to-filename trigger */
 	if (pset.gfname)
 	{
@@ -1490,7 +1599,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)
 	{
@@ -1538,7 +1647,7 @@ DescribeQuery(const char *query, double *elapsed_msec)
 			PQclear(results);
 
 			results = PQexec(pset.db, buf.data);
-			OK = AcceptResult(results);
+			OK = AcceptResult(results, true);
 
 			if (timing)
 			{
@@ -1548,7 +1657,7 @@ DescribeQuery(const char *query, double *elapsed_msec)
 			}
 
 			if (OK && results)
-				OK = PrintQueryResults(results);
+				OK = HandleQueryResult(results, true, false, NULL, NULL);
 
 			termPQExpBuffer(&buf);
 		}
@@ -1608,7 +1717,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)
@@ -1622,7 +1731,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);
@@ -1695,7 +1804,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);
@@ -1804,7 +1913,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);
 	}
@@ -1814,7 +1923,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);
 	}
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index 937d6e9d49..82504258d0 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -413,6 +413,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"
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index f614b26e2c..57bddec5a5 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -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;
 
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index be9dec749d..d08b15886a 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -203,6 +203,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);
 
@@ -1150,6 +1151,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)
 {
@@ -1251,6 +1258,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);
diff --git a/src/bin/psql/t/001_basic.pl b/src/bin/psql/t/001_basic.pl
index 9e14dc71ff..77c60a0e7b 100644
--- a/src/bin/psql/t/001_basic.pl
+++ b/src/bin/psql/t/001_basic.pl
@@ -6,7 +6,8 @@ use warnings;
 
 use PostgreSQL::Test::Cluster;
 use PostgreSQL::Test::Utils;
-use Test::More tests => 25;
+
+use Test::More tests => 29;
 
 program_help_ok('psql');
 program_version_ok('psql');
@@ -80,3 +81,19 @@ psql_like(
 	'handling of unexpected PQresultStatus',
 	'START_REPLICATION 0/0',
 	undef, qr/unexpected PQresultStatus: 8$/);
+
+# Test voluntary crash
+my ($ret, $out, $err) = $node->psql(
+	'postgres',
+	"SELECT 'before' AS running;\n" .
+	"SELECT pg_terminate_backend(pg_backend_pid());\n" .
+	"SELECT 'AFTER' AS not_running;\n");
+
+is($ret, 2, "server stopped");
+like($out, qr/before/, "output before crash");
+ok($out !~ qr/AFTER/, "no output after crash");
+is($err, 'psql:<stdin>:2: FATAL:  terminating connection due to administrator command
+psql:<stdin>:2: server closed the connection unexpectedly
+	This probably means the server terminated abnormally
+	before or while processing the request.
+psql:<stdin>:2: fatal: connection to server was lost', "expected error message");
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 39be6f556a..fbe6b2b579 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -4347,7 +4347,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"))
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index e8bcc88370..2a3d29aee5 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -186,3 +186,4 @@ PQpipelineStatus          183
 PQsetTraceFlags           184
 PQmblenBounded            185
 PQsendFlushRequest        186
+PQclearErrorMessage	      187
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 72914116ee..099c2715c0 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -6782,6 +6782,12 @@ PQerrorMessage(const PGconn *conn)
 	return conn->errorMessage.data;
 }
 
+void
+PQclearErrorMessage(PGconn *conn)
+{
+	resetPQExpBuffer(&conn->errorMessage);
+}
+
 /*
  * In Windows, socket values are unsigned, and an invalid socket value
  * (INVALID_SOCKET) is ~0, which equals -1 in comparisons (with no compiler
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index 20eb855abc..b73b44e817 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -347,6 +347,7 @@ extern const char *PQparameterStatus(const PGconn *conn,
 extern int	PQprotocolVersion(const PGconn *conn);
 extern int	PQserverVersion(const PGconn *conn);
 extern char *PQerrorMessage(const PGconn *conn);
+extern void	PQclearErrorMessage(PGconn *conn);
 extern int	PQsocket(const PGconn *conn);
 extern int	PQbackendPID(const PGconn *conn);
 extern PGpipelineStatus PQpipelineStatus(const PGconn *conn);
diff --git a/src/test/regress/expected/copyselect.out b/src/test/regress/expected/copyselect.out
index 72865fe1eb..bb9e026f91 100644
--- a/src/test/regress/expected/copyselect.out
+++ b/src/test/regress/expected/copyselect.out
@@ -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
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 6428ebc507..428ee941b9 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5290,3 +5290,129 @@ ERROR:  relation "notexists" does not exist
 LINE 1: SELECT * FROM notexists;
                       ^
 STATEMENT:  SELECT * FROM notexists;
+ 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)
+
+ three 
+-------
+     3
+(1 row)
+
+NOTICE:  warn 3.5
+CONTEXT:  PL/pgSQL function warn(text) line 2 at RAISE
+ warn 
+------
+ t
+(1 row)
+
+:three 4
+ERROR:  syntax error at or near ";"
+LINE 1: SELECT 5 ; SELECT 6 + ; SELECT warn('6.5') ; SELECT 7 ;
+                              ^
+ eight 
+-------
+     8
+(1 row)
+
+ERROR:  division by zero
+ begin 
+-------
+ ok
+(1 row)
+
+Calvin
+Susie
+Hobbes
+ done 
+------
+ ok
+(1 row)
+
+NOTICE:  warn 1.5
+CONTEXT:  PL/pgSQL function warn(text) line 2 at RAISE
+ two 
+-----
+   2
+(1 row)
+
+# initial AUTOCOMMIT: on
+# AUTOCOMMIT: off
+   s   
+-------
+ hello
+ world
+(2 rows)
+
+# AUTOCOMMIT: on
+   s   
+-------
+ hello
+ world
+(2 rows)
+
+# final AUTOCOMMIT: on
+# initial ON_ERROR_ROLLBACK: off
+# ON_ERROR_ROLLBACK: on
+# AUTOCOMMIT: on
+ERROR:  type "no_such_type" does not exist
+LINE 1: CREATE TABLE bla(s NO_SUCH_TYPE);
+                           ^
+ERROR:  error oops!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+   s    
+--------
+ Calvin
+ Hobbes
+(2 rows)
+
+     show     
+--------------
+ before error
+(1 row)
+
+ERROR:  error boum!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+ERROR:  error bam!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+       s       
+---------------
+ Calvin
+ Hobbes
+ Miss Wormwood
+ Susie
+(4 rows)
+
+# AUTOCOMMIT: off
+ERROR:  error bad!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+ #mum 
+------
+    1
+(1 row)
+
+ERROR:  error bad!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+       s       
+---------------
+ Calvin
+ Dad
+ Hobbes
+ Miss Wormwood
+ Susie
+(5 rows)
+
+# final ON_ERROR_ROLLBACK: off
diff --git a/src/test/regress/expected/transactions.out b/src/test/regress/expected/transactions.out
index 61862d595d..be1db0d5c0 100644
--- a/src/test/regress/expected/transactions.out
+++ b/src/test/regress/expected/transactions.out
@@ -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
diff --git a/src/test/regress/sql/copyselect.sql b/src/test/regress/sql/copyselect.sql
index 1d98dad3c8..e32a4f8e38 100644
--- a/src/test/regress/sql/copyselect.sql
+++ b/src/test/regress/sql/copyselect.sql
@@ -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; -- 0 1
 1
 \.
 2
diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql
index d4e4fdbbb7..69521dc915 100644
--- a/src/test/regress/sql/psql.sql
+++ b/src/test/regress/sql/psql.sql
@@ -1316,3 +1316,123 @@ DROP TABLE oer_test;
 \set ECHO errors
 SELECT * FROM notexists;
 \set ECHO none
+
+--
+-- 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);
+
+--
+-- autocommit
+--
+\echo '# initial AUTOCOMMIT:' :AUTOCOMMIT
+\set AUTOCOMMIT off
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+-- implicit BEGIN
+CREATE TABLE foo(s TEXT);
+ROLLBACK;
+CREATE TABLE foo(s TEXT);
+INSERT INTO foo(s) VALUES ('hello'), ('world');
+COMMIT;
+DROP TABLE foo;
+ROLLBACK;
+SELECT * FROM foo ORDER BY 1;
+DROP TABLE foo;
+COMMIT;
+\set AUTOCOMMIT on
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+-- explicit BEGIN
+BEGIN;
+CREATE TABLE foo(s TEXT);
+INSERT INTO foo(s) VALUES ('hello'), ('world');
+COMMIT;
+BEGIN;
+DROP TABLE foo;
+ROLLBACK;
+-- implicit transactions
+SELECT * FROM foo ORDER BY 1;
+DROP TABLE foo;
+\echo '# final AUTOCOMMIT:' :AUTOCOMMIT
+
+--
+-- test ON_ERROR_ROLLBACK
+--
+CREATE FUNCTION psql_error(msg TEXT) RETURNS BOOLEAN AS $$
+  BEGIN
+    RAISE EXCEPTION 'error %', msg;
+  END;
+$$ LANGUAGE plpgsql;
+\echo '# initial ON_ERROR_ROLLBACK:' :ON_ERROR_ROLLBACK
+\set ON_ERROR_ROLLBACK on
+\echo '# ON_ERROR_ROLLBACK:' :ON_ERROR_ROLLBACK
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+BEGIN;
+CREATE TABLE bla(s NO_SUCH_TYPE);         -- fails
+CREATE TABLE bla(s TEXT);                 -- succeeds
+SELECT psql_error('oops!');               -- fails
+INSERT INTO bla VALUES ('Calvin'), ('Hobbes');
+COMMIT;
+SELECT * FROM bla ORDER BY 1;
+BEGIN;
+INSERT INTO bla VALUES ('Susie');         -- succeeds
+-- combined queries
+INSERT INTO bla VALUES ('Rosalyn') \;     -- will rollback
+SELECT 'before error' AS show \;          -- will show!
+  SELECT psql_error('boum!') \;           -- failure
+  SELECT 'after error' AS noshow;         -- hidden by preceeding error
+INSERT INTO bla(s) VALUES ('Moe') \;      -- will rollback
+  SELECT psql_error('bam!');
+INSERT INTO bla VALUES ('Miss Wormwood'); -- succeeds
+COMMIT;
+SELECT * FROM bla ORDER BY 1;
+-- some with autocommit off
+\set AUTOCOMMIT off
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+-- implicit BEGIN
+INSERT INTO bla VALUES ('Dad');           -- succeeds
+SELECT psql_error('bad!');                -- implicit partial rollback
+INSERT INTO bla VALUES ('Mum') \;         -- will rollback
+SELECT COUNT(*) AS "#mum"
+FROM bla WHERE s = 'Mum' \;               -- but be counted here
+SELECT psql_error('bad!');                -- implicit partial rollback
+COMMIT;
+SELECT * FROM bla ORDER BY 1;
+-- reset
+\set AUTOCOMMIT on
+\set ON_ERROR_ROLLBACK off
+\echo '# final ON_ERROR_ROLLBACK:' :ON_ERROR_ROLLBACK
+DROP TABLE bla;
+DROP FUNCTION psql_error;
diff --git a/src/test/regress/sql/transactions.sql b/src/test/regress/sql/transactions.sql
index 8886280c0a..7fc9f09468 100644
--- a/src/test/regress/sql/transactions.sql
+++ b/src/test/regress/sql/transactions.sql
@@ -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:


^ permalink  raw  reply  [nested|flat] 14+ messages in thread

* Re: psql - add SHOW_ALL_RESULTS option
  2022-01-04 07:55 Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-01-08 18:32 ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
@ 2022-01-13 05:44   ` Andres Freund <[email protected]>
  2022-01-15 09:00     ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  0 siblings, 1 reply; 14+ messages in thread

From: Andres Freund @ 2022-01-13 05:44 UTC (permalink / raw)
  To: Fabien COELHO <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; [email protected] <[email protected]>

Hi,

On 2022-01-08 19:32:36 +0100, Fabien COELHO wrote:
> Attached v13 where the crash test is moved to tap.

The reason this test constantly fails on cfbot windows is a use-after-free
bug.

I figured that out in the context of another thread, so the debugging is
there:

https://postgr.es/m/20220113054123.ib4khtafgq34lv4z%40alap3.anarazel.de
> Ah, I see the bug. It's a use-after-free introduced in the patch:
>
> SendQueryAndProcessResults(const char *query, double *pelapsed_msec,
> 	bool is_watch, const printQueryOpt *opt, FILE *printQueryFout, bool *tx_ended)
> 
> 
> ...
> 	/* first result */
> 	result = PQgetResult(pset.db);
> 
> 
> 	while (result != NULL)
> 
> 
> ...
> 		if (!AcceptResult(result, false))
> 		{
> ...
> 			ClearOrSaveResult(result);
> 			success = false;
> 
> 
> 			/* and switch to next result */
> 			result_status = PQresultStatus(result);
> 			if (result_status == PGRES_COPY_BOTH ||
> 				result_status == PGRES_COPY_OUT ||
> 				result_status == PGRES_COPY_IN)
> 
> 
> So we called ClearOrSaveResult() with did a PQclear(), and then we go and call
> PQresultStatus().

Greetings,

Andres Freund






^ permalink  raw  reply  [nested|flat] 14+ messages in thread

* Re: psql - add SHOW_ALL_RESULTS option
  2022-01-04 07:55 Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-01-08 18:32 ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-01-13 05:44   ` Re: psql - add SHOW_ALL_RESULTS option Andres Freund <[email protected]>
@ 2022-01-15 09:00     ` Fabien COELHO <[email protected]>
  2022-01-18 16:36       ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  2022-03-04 08:50       ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  0 siblings, 2 replies; 14+ messages in thread

From: Fabien COELHO @ 2022-01-15 09:00 UTC (permalink / raw)
  To: Andres Freund <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; [email protected] <[email protected]>


Hello Andres,

> The reason this test constantly fails on cfbot windows is a use-after-free
> bug.

Indeed! Thanks a lot for the catch and the debug!

The ClearOrSaveResult function is quite annoying because it may or may not 
clear the result as a side effect.

Attached v14 moves the status extraction before the possible clear. I've 
added a couple of results = NULL after such calls in the code.

-- 
Fabien.

Attachments:

  [text/x-diff] psql-show-all-results-14.patch (50.4K, ../../alpine.DEB.2.22.394.2201150938470.3378943@pseudo/2-psql-show-all-results-14.patch)
  download | inline diff:
diff --git a/contrib/pg_stat_statements/expected/pg_stat_statements.out b/contrib/pg_stat_statements/expected/pg_stat_statements.out
index e0abe34bb6..8f7f93172a 100644
--- a/contrib/pg_stat_statements/expected/pg_stat_statements.out
+++ b/contrib/pg_stat_statements/expected/pg_stat_statements.out
@@ -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 
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 1ab200a4ad..0a22850912 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -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>
@@ -3570,10 +3563,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>
@@ -4160,6 +4149,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>
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
index 3503605a7d..47eabcbb8e 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -34,6 +34,8 @@ 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, const printQueryOpt *opt, FILE *printQueryFout, bool *tx_ended);
 
 
 /*
@@ -354,7 +356,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;
 
@@ -385,7 +387,7 @@ AcceptResult(const PGresult *result)
 				break;
 		}
 
-	if (!OK)
+	if (!OK && show_error)
 	{
 		const char *error = PQerrorMessage(pset.db);
 
@@ -473,6 +475,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
@@ -573,7 +587,7 @@ PSQLexec(const char *query)
 
 	ResetCancelConn();
 
-	if (!AcceptResult(res))
+	if (!AcceptResult(res, true))
 	{
 		ClearOrSaveResult(res);
 		res = NULL;
@@ -596,11 +610,8 @@ int
 PSQLexecWatch(const char *query, const printQueryOpt *opt, FILE *printQueryFout)
 {
 	bool		timing = pset.timing;
-	PGresult   *res;
 	double		elapsed_msec = 0;
-	instr_time	before;
-	instr_time	after;
-	FILE	   *fout;
+	int			res;
 
 	if (!pset.db)
 	{
@@ -609,77 +620,14 @@ PSQLexecWatch(const char *query, const printQueryOpt *opt, FILE *printQueryFout)
 	}
 
 	SetCancelConn(pset.db);
-
-	if (timing)
-		INSTR_TIME_SET_CURRENT(before);
-
-	res = PQexec(pset.db, query);
-
+	res = SendQueryAndProcessResults(query, &elapsed_msec, true, opt, printQueryFout, NULL);
 	ResetCancelConn();
 
-	if (!AcceptResult(res))
-	{
-		ClearOrSaveResult(res);
-		return 0;
-	}
-
-	if (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;
-	}
-
-	fout = printQueryFout ? printQueryFout : pset.queryFout;
-
-	switch (PQresultStatus(res))
-	{
-		case PGRES_TUPLES_OK:
-			printQuery(res, opt, fout, false, pset.logfile);
-			break;
-
-		case PGRES_COMMAND_OK:
-			fprintf(fout, "%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(fout);
-
 	/* Possible microtiming output */
 	if (timing)
 		PrintTiming(elapsed_msec);
 
-	return 1;
+	return res;
 }
 
 
@@ -714,7 +662,7 @@ PrintNotifications(void)
  * Returns true if successful, false otherwise.
  */
 static bool
-PrintQueryTuples(const PGresult *results)
+PrintQueryTuples(const PGresult *results, const printQueryOpt *opt, FILE *printQueryFout)
 {
 	bool		result = true;
 
@@ -746,8 +694,9 @@ PrintQueryTuples(const PGresult *results)
 	}
 	else
 	{
-		printQuery(results, &pset.popt, pset.queryFout, false, pset.logfile);
-		if (ferror(pset.queryFout))
+		FILE *fout = printQueryFout ? printQueryFout : pset.queryFout;
+		printQuery(results, opt ? opt : &pset.popt, fout, false, pset.logfile);
+		if (ferror(fout))
 		{
 			pg_log_error("could not print result table: %m");
 			result = false;
@@ -892,213 +841,131 @@ 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.
- *
- * When the command string contained no such COPY command, this function
- * degenerates to an AcceptResult() call.
- *
- * 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.
+ * 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.
+ *
+ * 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.
+ *
+ * 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;
-
-			case PGRES_COPY_OUT:
-			case PGRES_COPY_IN:
-				is_copy = true;
-				break;
-
-			default:
-				/* AcceptResult() should have caught anything else. */
-				is_copy = false;
-				pg_log_error("unexpected PQresultStatus: %d", result_status);
-				break;
-		}
-
-		if (is_copy)
-		{
-			/*
-			 * 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)
+			/* invoked by \g */
+			if (openQueryOutputFile(pset.gfname,
+									&copystream, &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);
-					}
-				}
+				need_close = true;
+				if (is_pipe)
+					disable_sigpipe_trap();
 			}
 			else
-			{
-				/* COPY IN */
-				copystream = pset.copyStream ? pset.copyStream : pset.cur_cmd_source;
-				success = handleCopyIn(pset.db,
-									   copystream,
-									   PQbinaryTuples(*results),
-									   &copy_result) && success;
-			}
-			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;
+				copystream = NULL;	/* discard COPY data entirely */
 		}
-		else if (first_cycle)
+		else
 		{
-			/* fast path: no COPY commands; PQexec visited all results */
-			break;
+			/* fall back to the generic query output stream */
+			copystream = pset.queryFout;
 		}
 
+		success = handleCopyOut(pset.db,
+								copystream,
+								&copy_result)
+			&& (copystream != NULL);
+
 		/*
-		 * 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.
+		 * 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.
 		 */
-		next_result = PQgetResult(pset.db);
-		if (!next_result)
-			break;
+		if (copystream == pset.queryFout)
+		{
+			PQclear(copy_result);
+			copy_result = NULL;
+		}
 
-		PQclear(*results);
-		*results = next_result;
-		first_cycle = false;
+		if (need_close)
+		{
+			/* close \g argument file/pipe */
+			if (is_pipe)
+			{
+				pclose(copystream);
+				restore_sigpipe_trap();
+			}
+			else
+			{
+				fclose(copystream);
+			}
+		}
+	}
+	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)
+PrintQueryStatus(PGresult *results, FILE *printQueryFout)
 {
 	char		buf[16];
+	FILE	   *fout = printQueryFout ? printQueryFout : pset.queryFout;
 
 	if (!pset.quiet)
 	{
 		if (pset.popt.topt.format == PRINT_HTML)
 		{
-			fputs("<p>", pset.queryFout);
-			html_escaped_print(PQcmdStatus(results), pset.queryFout);
-			fputs("</p>\n", pset.queryFout);
+			fputs("<p>", fout);
+			html_escaped_print(PQcmdStatus(results), fout);
+			fputs("</p>\n", fout);
 		}
 		else
-			fprintf(pset.queryFout, "%s\n", PQcmdStatus(results));
+			fprintf(fout, "%s\n", PQcmdStatus(results));
 	}
 
 	if (pset.logfile)
@@ -1110,43 +977,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 is_watch, const printQueryOpt *opt, FILE *printQueryFout)
 {
 	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, opt, printQueryFout);
 			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, printQueryFout);
+			}
+
 			break;
 
 		case PGRES_COMMAND_OK:
-			PrintQueryStatus(results);
+			if (last || pset.show_all_results)
+				PrintQueryStatus(result, printQueryFout);
 			success = true;
 			break;
 
@@ -1156,7 +1030,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;
 
@@ -1169,15 +1043,263 @@ PrintQueryResults(PGresult *results)
 		default:
 			success = false;
 			pg_log_error("unexpected PQresultStatus: %d",
-						 PQresultStatus(results));
+						 PQresultStatus(result));
 			break;
 	}
 
-	fflush(pset.queryFout);
+	fflush(printQueryFout ? printQueryFout : pset.queryFout);
 
 	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 {
+	PQExpBufferData	messages[2];
+	int				current;
+} 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->messages[notes->current], msg);
+}
+
+/*
+ * Show notices stored in buffer, which is then reset.
+ */
+static void
+ShowNoticeMessage(t_notice_messages *notes)
+{
+	PQExpBufferData	*current = &notes->messages[notes->current];
+	if (*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, const printQueryOpt *opt, FILE *printQueryFout, bool *tx_ended)
+{
+	bool				timing = pset.timing;
+	bool				success;
+	instr_time			before;
+	PGresult		   *result;
+	t_notice_messages	notes;
+
+	if (timing)
+		INSTR_TIME_SET_CURRENT(before);
+
+	success = PQsendQuery(pset.db, query);
+
+	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.current = 0;
+	initPQExpBuffer(&notes.messages[0]);
+	initPQExpBuffer(&notes.messages[1]);
+	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);
+
+				/*
+				 * On connection loss another result with a message will be
+				 * generated, we do not want to see this error again.
+				 */
+				PQclearErrorMessage(pset.db);
+			}
+			CheckConnection();
+			if (!is_watch)
+				SetResultVariables(result, false);
+
+			/* keep the result status before clearing it */
+			result_status = PQresultStatus(result);
+			ClearOrSaveResult(result);
+			result = NULL;
+			success = false;
+
+			/* switch to next result */
+			if (result_status == PGRES_COPY_BOTH ||
+				result_status == PGRES_COPY_OUT ||
+				result_status == PGRES_COPY_IN)
+				/*
+				 * For some obscure reason PQgetResult does *not* return a NULL in copy
+				 * cases despite the result having been cleared, but keeps returning an
+				 * "empty" result that we have to ignore manually.
+				 */
+				;
+			else
+				result = PQgetResult(pset.db);
+
+			continue;
+		}
+		else if (tx_ended != NULL && ! *tx_ended)
+		{
+			/* on success, tell whether the "current" transaction sequence ended */
+			const char *cmd = PQcmdStatus(result);
+			*tx_ended = strcmp(cmd, "COMMIT") == 0 || strcmp(cmd, "ROLLBACK") == 0 ||
+				strcmp(cmd, "SAVEPOINT") == 0 || strcmp(cmd, "RELEASE") == 0;
+		}
+
+		result_status = PQresultStatus(result);
+
+		/* must handle COPY before changing the current 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. We need to do that to check whether this is the last.
+		 */
+		notes.current ^= 1;
+		next_result = PQgetResult(pset.db);
+		notes.current ^= 1;
+		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 && 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, false, opt, printQueryFout);
+
+		/* set variables on last result if all went well */
+		if (!is_watch && last && success)
+			SetResultVariables(result, true);
+
+		ClearOrSaveResult(result);
+		notes.current ^= 1;
+		result = next_result;
+
+		if (cancel_pressed)
+		{
+			ClearOrSaveAllResults();
+			break;
+		}
+	}
+
+	/* reset notice hook */
+	PQsetNoticeProcessor(pset.db, NoticeProcessor, NULL);
+	termPQExpBuffer(&notes.messages[0]);
+	termPQExpBuffer(&notes.messages[1]);
+
+	/* may need this to recover from conn loss during COPY */
+	if (!CheckConnection())
+		return -1;
+
+	return cancel_pressed ? 0 : success ? 1 : -1;
+}
+
 
 /*
  * SendQuery: send the query string to the backend
@@ -1195,12 +1317,14 @@ bool
 SendQuery(const char *query)
 {
 	bool		timing = pset.timing;
-	PGresult   *results;
+	PGresult   *results = NULL;
 	PGTransactionStatusType transaction_status;
 	double		elapsed_msec = 0;
 	bool		OK = false;
+	bool		res_error;
 	int			i;
 	bool		on_error_rollback_savepoint = false;
+	bool		tx_ended = false;
 
 	if (!pset.db)
 	{
@@ -1239,82 +1363,71 @@ SendQuery(const char *query)
 		fflush(pset.logfile);
 	}
 
+	/* global query cancellation for this query */
 	SetCancelConn(pset.db);
 
 	transaction_status = PQtransactionStatus(pset.db);
 
+	/* issue a BEGIN if needed, corresponding COMMIT/ROLLBACK by user */
 	if (transaction_status == PQTRANS_IDLE &&
 		!pset.autocommit &&
 		!command_no_begin(query))
 	{
-		results = PQexec(pset.db, "BEGIN");
-		if (PQresultStatus(results) != PGRES_COMMAND_OK)
+		PGresult    *result;
+		result = PQexec(pset.db, "BEGIN");
+		res_error = PQresultStatus(result) != PGRES_COMMAND_OK;
+		ClearOrSaveResult(result);
+		result = NULL;
+
+		if (res_error)
 		{
 			pg_log_info("%s", PQerrorMessage(pset.db));
-			ClearOrSaveResult(results);
-			ResetCancelConn();
 			goto sendquery_cleanup;
 		}
-		ClearOrSaveResult(results);
+
+		/* must be PQTRANS_INTRANS after a BEGIN */
 		transaction_status = PQtransactionStatus(pset.db);
 	}
 
+	/* create automatic savepoint if needed and possible */
 	if (transaction_status == PQTRANS_INTRANS &&
 		pset.on_error_rollback != PSQL_ERROR_ROLLBACK_OFF &&
 		(pset.cur_cmd_interactive ||
 		 pset.on_error_rollback == PSQL_ERROR_ROLLBACK_ON))
 	{
-		results = PQexec(pset.db, "SAVEPOINT pg_psql_temporary_savepoint");
-		if (PQresultStatus(results) != PGRES_COMMAND_OK)
+		PGresult   *result;
+		result = PQexec(pset.db, "SAVEPOINT pg_psql_temporary_savepoint");
+		res_error = PQresultStatus(result) != PGRES_COMMAND_OK;
+		ClearOrSaveResult(result);
+		result = NULL;
+
+		if (res_error)
 		{
 			pg_log_info("%s", PQerrorMessage(pset.db));
-			ClearOrSaveResult(results);
-			ResetCancelConn();
 			goto sendquery_cleanup;
 		}
-		ClearOrSaveResult(results);
+
 		on_error_rollback_savepoint = true;
 	}
 
+	/* process the query, one way or the other */
 	if (pset.gdesc_flag)
 	{
 		/* Describe query's result columns, without executing it */
 		OK = DescribeQuery(query, &elapsed_msec);
-		ResetCancelConn();
 		results = NULL;			/* PQclear(NULL) does nothing */
 	}
 	else if (pset.fetch_count <= 0 || pset.gexec_flag ||
 			 pset.crosstab_flag || !is_select_command(query))
 	{
 		/* Default fetch-it-all-and-print mode */
-		instr_time	before,
-					after;
-
-		if (timing)
-			INSTR_TIME_SET_CURRENT(before);
-
-		results = PQexec(pset.db, query);
-
-		/* these operations are included in the timing result: */
-		ResetCancelConn();
-		OK = ProcessResult(&results);
-
-		if (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, NULL, NULL, &tx_ended);
+		OK = (res >= 0);
 	}
 	else
 	{
 		/* Fetch-in-segments mode */
 		OK = ExecQueryUsingCursor(query, &elapsed_msec);
-		ResetCancelConn();
 		results = NULL;			/* PQclear(NULL) does nothing */
 	}
 
@@ -1347,11 +1460,7 @@ SendQuery(const char *query)
 				 * savepoint is gone. If they issued a SAVEPOINT, releasing
 				 * ours would remove theirs.
 				 */
-				if (results &&
-					(strcmp(PQcmdStatus(results), "COMMIT") == 0 ||
-					 strcmp(PQcmdStatus(results), "SAVEPOINT") == 0 ||
-					 strcmp(PQcmdStatus(results), "RELEASE") == 0 ||
-					 strcmp(PQcmdStatus(results), "ROLLBACK") == 0))
+				if (tx_ended)
 					svptcmd = NULL;
 				else
 					svptcmd = "RELEASE pg_psql_temporary_savepoint";
@@ -1373,14 +1482,15 @@ SendQuery(const char *query)
 			PGresult   *svptres;
 
 			svptres = PQexec(pset.db, svptcmd);
-			if (PQresultStatus(svptres) != PGRES_COMMAND_OK)
+			res_error = PQresultStatus(svptres) != PGRES_COMMAND_OK;
+
+			if (res_error)
 			{
 				pg_log_info("%s", PQerrorMessage(pset.db));
 				ClearOrSaveResult(svptres);
 				OK = false;
 
 				PQclear(results);
-				ResetCancelConn();
 				goto sendquery_cleanup;
 			}
 			PQclear(svptres);
@@ -1411,6 +1521,9 @@ SendQuery(const char *query)
 
 sendquery_cleanup:
 
+	/* global cancellation reset */
+	ResetCancelConn();
+
 	/* reset \g's output-to-filename trigger */
 	if (pset.gfname)
 	{
@@ -1491,7 +1604,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)
 	{
@@ -1539,7 +1652,7 @@ DescribeQuery(const char *query, double *elapsed_msec)
 			PQclear(results);
 
 			results = PQexec(pset.db, buf.data);
-			OK = AcceptResult(results);
+			OK = AcceptResult(results, true);
 
 			if (timing)
 			{
@@ -1549,7 +1662,7 @@ DescribeQuery(const char *query, double *elapsed_msec)
 			}
 
 			if (OK && results)
-				OK = PrintQueryResults(results);
+				OK = HandleQueryResult(results, true, false, NULL, NULL);
 
 			termPQExpBuffer(&buf);
 		}
@@ -1609,7 +1722,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)
@@ -1623,7 +1736,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);
@@ -1696,7 +1809,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);
@@ -1805,7 +1918,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);
 	}
@@ -1815,7 +1928,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);
 	}
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index 937d6e9d49..82504258d0 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -413,6 +413,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"
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index f614b26e2c..57bddec5a5 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -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;
 
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index be9dec749d..d08b15886a 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -203,6 +203,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);
 
@@ -1150,6 +1151,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)
 {
@@ -1251,6 +1258,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);
diff --git a/src/bin/psql/t/001_basic.pl b/src/bin/psql/t/001_basic.pl
index 9e14dc71ff..77c60a0e7b 100644
--- a/src/bin/psql/t/001_basic.pl
+++ b/src/bin/psql/t/001_basic.pl
@@ -6,7 +6,8 @@ use warnings;
 
 use PostgreSQL::Test::Cluster;
 use PostgreSQL::Test::Utils;
-use Test::More tests => 25;
+
+use Test::More tests => 29;
 
 program_help_ok('psql');
 program_version_ok('psql');
@@ -80,3 +81,19 @@ psql_like(
 	'handling of unexpected PQresultStatus',
 	'START_REPLICATION 0/0',
 	undef, qr/unexpected PQresultStatus: 8$/);
+
+# Test voluntary crash
+my ($ret, $out, $err) = $node->psql(
+	'postgres',
+	"SELECT 'before' AS running;\n" .
+	"SELECT pg_terminate_backend(pg_backend_pid());\n" .
+	"SELECT 'AFTER' AS not_running;\n");
+
+is($ret, 2, "server stopped");
+like($out, qr/before/, "output before crash");
+ok($out !~ qr/AFTER/, "no output after crash");
+is($err, 'psql:<stdin>:2: FATAL:  terminating connection due to administrator command
+psql:<stdin>:2: server closed the connection unexpectedly
+	This probably means the server terminated abnormally
+	before or while processing the request.
+psql:<stdin>:2: fatal: connection to server was lost', "expected error message");
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 71f1a5c00d..9252bd57a2 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -4351,7 +4351,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"))
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index e8bcc88370..2a3d29aee5 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -186,3 +186,4 @@ PQpipelineStatus          183
 PQsetTraceFlags           184
 PQmblenBounded            185
 PQsendFlushRequest        186
+PQclearErrorMessage	      187
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 5fc16be849..5bf51154cc 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -6771,6 +6771,12 @@ PQerrorMessage(const PGconn *conn)
 	return conn->errorMessage.data;
 }
 
+void
+PQclearErrorMessage(PGconn *conn)
+{
+	resetPQExpBuffer(&conn->errorMessage);
+}
+
 /*
  * In Windows, socket values are unsigned, and an invalid socket value
  * (INVALID_SOCKET) is ~0, which equals -1 in comparisons (with no compiler
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index 20eb855abc..b73b44e817 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -347,6 +347,7 @@ extern const char *PQparameterStatus(const PGconn *conn,
 extern int	PQprotocolVersion(const PGconn *conn);
 extern int	PQserverVersion(const PGconn *conn);
 extern char *PQerrorMessage(const PGconn *conn);
+extern void	PQclearErrorMessage(PGconn *conn);
 extern int	PQsocket(const PGconn *conn);
 extern int	PQbackendPID(const PGconn *conn);
 extern PGpipelineStatus PQpipelineStatus(const PGconn *conn);
diff --git a/src/test/regress/expected/copyselect.out b/src/test/regress/expected/copyselect.out
index 72865fe1eb..bb9e026f91 100644
--- a/src/test/regress/expected/copyselect.out
+++ b/src/test/regress/expected/copyselect.out
@@ -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
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 6428ebc507..428ee941b9 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5290,3 +5290,129 @@ ERROR:  relation "notexists" does not exist
 LINE 1: SELECT * FROM notexists;
                       ^
 STATEMENT:  SELECT * FROM notexists;
+ 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)
+
+ three 
+-------
+     3
+(1 row)
+
+NOTICE:  warn 3.5
+CONTEXT:  PL/pgSQL function warn(text) line 2 at RAISE
+ warn 
+------
+ t
+(1 row)
+
+:three 4
+ERROR:  syntax error at or near ";"
+LINE 1: SELECT 5 ; SELECT 6 + ; SELECT warn('6.5') ; SELECT 7 ;
+                              ^
+ eight 
+-------
+     8
+(1 row)
+
+ERROR:  division by zero
+ begin 
+-------
+ ok
+(1 row)
+
+Calvin
+Susie
+Hobbes
+ done 
+------
+ ok
+(1 row)
+
+NOTICE:  warn 1.5
+CONTEXT:  PL/pgSQL function warn(text) line 2 at RAISE
+ two 
+-----
+   2
+(1 row)
+
+# initial AUTOCOMMIT: on
+# AUTOCOMMIT: off
+   s   
+-------
+ hello
+ world
+(2 rows)
+
+# AUTOCOMMIT: on
+   s   
+-------
+ hello
+ world
+(2 rows)
+
+# final AUTOCOMMIT: on
+# initial ON_ERROR_ROLLBACK: off
+# ON_ERROR_ROLLBACK: on
+# AUTOCOMMIT: on
+ERROR:  type "no_such_type" does not exist
+LINE 1: CREATE TABLE bla(s NO_SUCH_TYPE);
+                           ^
+ERROR:  error oops!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+   s    
+--------
+ Calvin
+ Hobbes
+(2 rows)
+
+     show     
+--------------
+ before error
+(1 row)
+
+ERROR:  error boum!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+ERROR:  error bam!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+       s       
+---------------
+ Calvin
+ Hobbes
+ Miss Wormwood
+ Susie
+(4 rows)
+
+# AUTOCOMMIT: off
+ERROR:  error bad!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+ #mum 
+------
+    1
+(1 row)
+
+ERROR:  error bad!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+       s       
+---------------
+ Calvin
+ Dad
+ Hobbes
+ Miss Wormwood
+ Susie
+(5 rows)
+
+# final ON_ERROR_ROLLBACK: off
diff --git a/src/test/regress/expected/transactions.out b/src/test/regress/expected/transactions.out
index 61862d595d..be1db0d5c0 100644
--- a/src/test/regress/expected/transactions.out
+++ b/src/test/regress/expected/transactions.out
@@ -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
diff --git a/src/test/regress/sql/copyselect.sql b/src/test/regress/sql/copyselect.sql
index 1d98dad3c8..e32a4f8e38 100644
--- a/src/test/regress/sql/copyselect.sql
+++ b/src/test/regress/sql/copyselect.sql
@@ -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; -- 0 1
 1
 \.
 2
diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql
index d4e4fdbbb7..69521dc915 100644
--- a/src/test/regress/sql/psql.sql
+++ b/src/test/regress/sql/psql.sql
@@ -1316,3 +1316,123 @@ DROP TABLE oer_test;
 \set ECHO errors
 SELECT * FROM notexists;
 \set ECHO none
+
+--
+-- 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);
+
+--
+-- autocommit
+--
+\echo '# initial AUTOCOMMIT:' :AUTOCOMMIT
+\set AUTOCOMMIT off
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+-- implicit BEGIN
+CREATE TABLE foo(s TEXT);
+ROLLBACK;
+CREATE TABLE foo(s TEXT);
+INSERT INTO foo(s) VALUES ('hello'), ('world');
+COMMIT;
+DROP TABLE foo;
+ROLLBACK;
+SELECT * FROM foo ORDER BY 1;
+DROP TABLE foo;
+COMMIT;
+\set AUTOCOMMIT on
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+-- explicit BEGIN
+BEGIN;
+CREATE TABLE foo(s TEXT);
+INSERT INTO foo(s) VALUES ('hello'), ('world');
+COMMIT;
+BEGIN;
+DROP TABLE foo;
+ROLLBACK;
+-- implicit transactions
+SELECT * FROM foo ORDER BY 1;
+DROP TABLE foo;
+\echo '# final AUTOCOMMIT:' :AUTOCOMMIT
+
+--
+-- test ON_ERROR_ROLLBACK
+--
+CREATE FUNCTION psql_error(msg TEXT) RETURNS BOOLEAN AS $$
+  BEGIN
+    RAISE EXCEPTION 'error %', msg;
+  END;
+$$ LANGUAGE plpgsql;
+\echo '# initial ON_ERROR_ROLLBACK:' :ON_ERROR_ROLLBACK
+\set ON_ERROR_ROLLBACK on
+\echo '# ON_ERROR_ROLLBACK:' :ON_ERROR_ROLLBACK
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+BEGIN;
+CREATE TABLE bla(s NO_SUCH_TYPE);         -- fails
+CREATE TABLE bla(s TEXT);                 -- succeeds
+SELECT psql_error('oops!');               -- fails
+INSERT INTO bla VALUES ('Calvin'), ('Hobbes');
+COMMIT;
+SELECT * FROM bla ORDER BY 1;
+BEGIN;
+INSERT INTO bla VALUES ('Susie');         -- succeeds
+-- combined queries
+INSERT INTO bla VALUES ('Rosalyn') \;     -- will rollback
+SELECT 'before error' AS show \;          -- will show!
+  SELECT psql_error('boum!') \;           -- failure
+  SELECT 'after error' AS noshow;         -- hidden by preceeding error
+INSERT INTO bla(s) VALUES ('Moe') \;      -- will rollback
+  SELECT psql_error('bam!');
+INSERT INTO bla VALUES ('Miss Wormwood'); -- succeeds
+COMMIT;
+SELECT * FROM bla ORDER BY 1;
+-- some with autocommit off
+\set AUTOCOMMIT off
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+-- implicit BEGIN
+INSERT INTO bla VALUES ('Dad');           -- succeeds
+SELECT psql_error('bad!');                -- implicit partial rollback
+INSERT INTO bla VALUES ('Mum') \;         -- will rollback
+SELECT COUNT(*) AS "#mum"
+FROM bla WHERE s = 'Mum' \;               -- but be counted here
+SELECT psql_error('bad!');                -- implicit partial rollback
+COMMIT;
+SELECT * FROM bla ORDER BY 1;
+-- reset
+\set AUTOCOMMIT on
+\set ON_ERROR_ROLLBACK off
+\echo '# final ON_ERROR_ROLLBACK:' :ON_ERROR_ROLLBACK
+DROP TABLE bla;
+DROP FUNCTION psql_error;
diff --git a/src/test/regress/sql/transactions.sql b/src/test/regress/sql/transactions.sql
index 8886280c0a..7fc9f09468 100644
--- a/src/test/regress/sql/transactions.sql
+++ b/src/test/regress/sql/transactions.sql
@@ -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:


^ permalink  raw  reply  [nested|flat] 14+ messages in thread

* Re: psql - add SHOW_ALL_RESULTS option
  2022-01-04 07:55 Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-01-08 18:32 ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-01-13 05:44   ` Re: psql - add SHOW_ALL_RESULTS option Andres Freund <[email protected]>
  2022-01-15 09:00     ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
@ 2022-01-18 16:36       ` Peter Eisentraut <[email protected]>
  2022-01-23 17:17         ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  1 sibling, 1 reply; 14+ messages in thread

From: Peter Eisentraut @ 2022-01-18 16:36 UTC (permalink / raw)
  To: Fabien COELHO <[email protected]>; +Cc: [email protected] <[email protected]>

On 15.01.22 10:00, Fabien COELHO wrote:
>> The reason this test constantly fails on cfbot windows is a 
>> use-after-free
>> bug.
> 
> Indeed! Thanks a lot for the catch and the debug!
> 
> The ClearOrSaveResult function is quite annoying because it may or may 
> not clear the result as a side effect.
> 
> Attached v14 moves the status extraction before the possible clear. I've 
> added a couple of results = NULL after such calls in the code.

In the psql.sql test file, the test I previously added concluded with 
\set ECHO none, which was a mistake that I have now fixed.  As a result, 
the tests that you added after that point didn't show their input lines, 
which was weird and not intentional.  So the tests will now show a 
different output.

I notice that this patch has recently gained a new libpq function.  I 
gather that this is to work around the misbehaviors in libpq that we 
have discussed.  But I think if we are adding a libpq API function to 
work around a misbehavior in libpq, we might as well fix the misbehavior 
in libpq to begin with.  Adding a new public libpq function is a 
significant step, needs documentation, etc.  It would be better to do 
without.  Also, it makes one wonder how others are supposed to use this 
multiple-results API properly, if even psql can't do it without 
extending libpq.  Needs more thought.






^ permalink  raw  reply  [nested|flat] 14+ messages in thread

* Re: psql - add SHOW_ALL_RESULTS option
  2022-01-04 07:55 Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-01-08 18:32 ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-01-13 05:44   ` Re: psql - add SHOW_ALL_RESULTS option Andres Freund <[email protected]>
  2022-01-15 09:00     ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-01-18 16:36       ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
@ 2022-01-23 17:17         ` Fabien COELHO <[email protected]>
  2022-01-27 13:30           ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  0 siblings, 1 reply; 14+ messages in thread

From: Fabien COELHO @ 2022-01-23 17:17 UTC (permalink / raw)
  To: Peter Eisentraut <[email protected]>; +Cc: [email protected] <[email protected]>


Hallo Peter,

>> Attached v14 moves the status extraction before the possible clear. I've 
>> added a couple of results = NULL after such calls in the code.
>
> In the psql.sql test file, the test I previously added concluded with \set 
> ECHO none, which was a mistake that I have now fixed.  As a result, the tests 
> that you added after that point didn't show their input lines, which was 
> weird and not intentional.  So the tests will now show a different output.

Ok.

> I notice that this patch has recently gained a new libpq function.  I gather 
> that this is to work around the misbehaviors in libpq that we have discussed.

Indeed.

> But I think if we are adding a libpq API function to work around a 
> misbehavior in libpq, we might as well fix the misbehavior in libpq to 
> begin with. Adding a new public libpq function is a significant step, 
> needs documentation, etc.

I'm not so sure.

The choice is (1) change the behavior of an existing function or (2) add a 
new function. Whatever the existing function does, the usual anwer to API 
changes is "someone is going to complain because it breaks their code", so 
"Returned with feedback", hence I did not even try. The advantage of (2) 
is that it does not harm anyone to have a new function that they just do 
not need to use.

> It would be better to do without.  Also, it makes one wonder how others 
> are supposed to use this multiple-results API properly, if even psql 
> can't do it without extending libpq. Needs more thought.

Fine with me! Obviously I'm okay if libpq is repaired instead of writing 
strange code on the client to deal with strange behavior.

-- 
Fabien.






^ permalink  raw  reply  [nested|flat] 14+ messages in thread

* Re: psql - add SHOW_ALL_RESULTS option
  2022-01-04 07:55 Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-01-08 18:32 ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-01-13 05:44   ` Re: psql - add SHOW_ALL_RESULTS option Andres Freund <[email protected]>
  2022-01-15 09:00     ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-01-18 16:36       ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  2022-01-23 17:17         ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
@ 2022-01-27 13:30           ` Peter Eisentraut <[email protected]>
  0 siblings, 0 replies; 14+ messages in thread

From: Peter Eisentraut @ 2022-01-27 13:30 UTC (permalink / raw)
  To: Fabien COELHO <[email protected]>; +Cc: [email protected] <[email protected]>


On 23.01.22 18:17, Fabien COELHO wrote:
>> But I think if we are adding a libpq API function to work around a 
>> misbehavior in libpq, we might as well fix the misbehavior in libpq to 
>> begin with. Adding a new public libpq function is a significant step, 
>> needs documentation, etc.
> 
> I'm not so sure.
> 
> The choice is (1) change the behavior of an existing function or (2) add 
> a new function. Whatever the existing function does, the usual anwer to 
> API changes is "someone is going to complain because it breaks their 
> code", so "Returned with feedback", hence I did not even try. The 
> advantage of (2) is that it does not harm anyone to have a new function 
> that they just do not need to use.
> 
>> It would be better to do without.  Also, it makes one wonder how 
>> others are supposed to use this multiple-results API properly, if even 
>> psql can't do it without extending libpq. Needs more thought.
> 
> Fine with me! Obviously I'm okay if libpq is repaired instead of writing 
> strange code on the client to deal with strange behavior.

I have a new thought on this, as long as we are looking into libpq.  Why 
can't libpq provide a variant of PQexec() that returns all results, 
instead of just the last one.  It has all the information, all it has to 
do is return the results instead of throwing them away.  Then the 
changes in psql would be very limited, and we don't have to re-invent 
PQexec() from its pieces in psql.  And this would also make it easier 
for other clients and user code to make use of this functionality more 
easily.

Attached is a rough draft of what this could look like.  It basically 
works.  Thoughts?
From 947d9d98507d6c93b547ac17fef41c1870c0d577 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email protected]>
Date: Thu, 27 Jan 2022 14:09:52 +0100
Subject: [PATCH] WIP: PQexecMulti

---
 src/bin/psql/common.c            | 44 +++++++++++++++++++-------------
 src/interfaces/libpq/exports.txt |  1 +
 src/interfaces/libpq/fe-exec.c   | 37 +++++++++++++++++++++++++++
 src/interfaces/libpq/libpq-fe.h  |  1 +
 4 files changed, 65 insertions(+), 18 deletions(-)

diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
index 3503605a7d..deaaa54f10 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -1195,7 +1195,6 @@ bool
 SendQuery(const char *query)
 {
 	bool		timing = pset.timing;
-	PGresult   *results;
 	PGTransactionStatusType transaction_status;
 	double		elapsed_msec = 0;
 	bool		OK = false;
@@ -1247,15 +1246,17 @@ SendQuery(const char *query)
 		!pset.autocommit &&
 		!command_no_begin(query))
 	{
-		results = PQexec(pset.db, "BEGIN");
-		if (PQresultStatus(results) != PGRES_COMMAND_OK)
+		PGresult   *result;
+
+		result = PQexec(pset.db, "BEGIN");
+		if (PQresultStatus(result) != PGRES_COMMAND_OK)
 		{
 			pg_log_info("%s", PQerrorMessage(pset.db));
-			ClearOrSaveResult(results);
+			ClearOrSaveResult(result);
 			ResetCancelConn();
 			goto sendquery_cleanup;
 		}
-		ClearOrSaveResult(results);
+		ClearOrSaveResult(result);
 		transaction_status = PQtransactionStatus(pset.db);
 	}
 
@@ -1264,15 +1265,17 @@ SendQuery(const char *query)
 		(pset.cur_cmd_interactive ||
 		 pset.on_error_rollback == PSQL_ERROR_ROLLBACK_ON))
 	{
-		results = PQexec(pset.db, "SAVEPOINT pg_psql_temporary_savepoint");
-		if (PQresultStatus(results) != PGRES_COMMAND_OK)
+		PGresult   *result;
+
+		result = PQexec(pset.db, "SAVEPOINT pg_psql_temporary_savepoint");
+		if (PQresultStatus(result) != PGRES_COMMAND_OK)
 		{
 			pg_log_info("%s", PQerrorMessage(pset.db));
-			ClearOrSaveResult(results);
+			ClearOrSaveResult(result);
 			ResetCancelConn();
 			goto sendquery_cleanup;
 		}
-		ClearOrSaveResult(results);
+		ClearOrSaveResult(result);
 		on_error_rollback_savepoint = true;
 	}
 
@@ -1281,7 +1284,6 @@ SendQuery(const char *query)
 		/* Describe query's result columns, without executing it */
 		OK = DescribeQuery(query, &elapsed_msec);
 		ResetCancelConn();
-		results = NULL;			/* PQclear(NULL) does nothing */
 	}
 	else if (pset.fetch_count <= 0 || pset.gexec_flag ||
 			 pset.crosstab_flag || !is_select_command(query))
@@ -1289,15 +1291,19 @@ SendQuery(const char *query)
 		/* Default fetch-it-all-and-print mode */
 		instr_time	before,
 					after;
+		PGresult  **results;
+		PGresult  **res;
 
 		if (timing)
 			INSTR_TIME_SET_CURRENT(before);
 
-		results = PQexec(pset.db, query);
+		results = PQexecMulti(pset.db, query);
 
 		/* these operations are included in the timing result: */
 		ResetCancelConn();
-		OK = ProcessResult(&results);
+		OK = false;
+		for (res = results; *res; res++)
+			OK |= ProcessResult(res);
 
 		if (timing)
 		{
@@ -1307,15 +1313,18 @@ SendQuery(const char *query)
 		}
 
 		/* but printing results isn't: */
-		if (OK && results)
-			OK = PrintQueryResults(results);
+		if (OK)
+			for (res = results; *res; res++)
+				OK |= PrintQueryResults(*res);
+
+		for (res = results; *res; res++)
+			ClearOrSaveResult(*res);
 	}
 	else
 	{
 		/* Fetch-in-segments mode */
 		OK = ExecQueryUsingCursor(query, &elapsed_msec);
 		ResetCancelConn();
-		results = NULL;			/* PQclear(NULL) does nothing */
 	}
 
 	if (!OK && pset.echo == PSQL_ECHO_ERRORS)
@@ -1341,6 +1350,7 @@ SendQuery(const char *query)
 
 			case PQTRANS_INTRANS:
 
+#if FIXME
 				/*
 				 * Do nothing if they are messing with savepoints themselves:
 				 * If the user did COMMIT AND CHAIN, RELEASE or ROLLBACK, our
@@ -1354,6 +1364,7 @@ SendQuery(const char *query)
 					 strcmp(PQcmdStatus(results), "ROLLBACK") == 0))
 					svptcmd = NULL;
 				else
+#endif
 					svptcmd = "RELEASE pg_psql_temporary_savepoint";
 				break;
 
@@ -1379,7 +1390,6 @@ SendQuery(const char *query)
 				ClearOrSaveResult(svptres);
 				OK = false;
 
-				PQclear(results);
 				ResetCancelConn();
 				goto sendquery_cleanup;
 			}
@@ -1387,8 +1397,6 @@ SendQuery(const char *query)
 		}
 	}
 
-	ClearOrSaveResult(results);
-
 	/* Possible microtiming output */
 	if (timing)
 		PrintTiming(elapsed_msec);
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index e8bcc88370..878e430b92 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -186,3 +186,4 @@ PQpipelineStatus          183
 PQsetTraceFlags           184
 PQmblenBounded            185
 PQsendFlushRequest        186
+PQexecMulti               187
diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c
index 59121873d2..90f00ad401 100644
--- a/src/interfaces/libpq/fe-exec.c
+++ b/src/interfaces/libpq/fe-exec.c
@@ -70,6 +70,7 @@ static void parseInput(PGconn *conn);
 static PGresult *getCopyResult(PGconn *conn, ExecStatusType copytype);
 static bool PQexecStart(PGconn *conn);
 static PGresult *PQexecFinish(PGconn *conn);
+static PGresult **PQexecFinishMulti(PGconn *conn);
 static int	PQsendDescribe(PGconn *conn, char desc_type,
 						   const char *desc_target);
 static int	check_field_number(const PGresult *res, int field_num);
@@ -2199,6 +2200,16 @@ PQexec(PGconn *conn, const char *query)
 	return PQexecFinish(conn);
 }
 
+PGresult **
+PQexecMulti(PGconn *conn, const char *query)
+{
+	if (!PQexecStart(conn))
+		return NULL;
+	if (!PQsendQuery(conn, query))
+		return NULL;
+	return PQexecFinishMulti(conn);
+}
+
 /*
  * PQexecParams
  *		Like PQexec, but use extended query protocol so we can pass parameters
@@ -2369,6 +2380,32 @@ PQexecFinish(PGconn *conn)
 	return lastResult;
 }
 
+static PGresult **
+PQexecFinishMulti(PGconn *conn)
+{
+	int			count = 0;
+	PGresult   *result;
+	PGresult  **ret = NULL;
+
+	while ((result = PQgetResult(conn)) != NULL)
+	{
+		count++;
+		ret = realloc(ret, count * sizeof(PGresult*));
+		ret[count - 1] = result;
+
+		if (result->resultStatus == PGRES_COPY_IN ||
+			result->resultStatus == PGRES_COPY_OUT ||
+			result->resultStatus == PGRES_COPY_BOTH ||
+			conn->status == CONNECTION_BAD)
+			break;
+	}
+
+	ret = realloc(ret, (count + 1) * sizeof(PGresult*));
+	ret[count] = NULL;
+
+	return ret;
+}
+
 /*
  * PQdescribePrepared
  *	  Obtain information about a previously prepared statement
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index 20eb855abc..61b3e9bf21 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -418,6 +418,7 @@ extern void PQsetTraceFlags(PGconn *conn, int flags);
 
 /* Simple synchronous query */
 extern PGresult *PQexec(PGconn *conn, const char *query);
+extern PGresult **PQexecMulti(PGconn *conn, const char *query);
 extern PGresult *PQexecParams(PGconn *conn,
 							  const char *command,
 							  int nParams,
-- 
2.34.1



Attachments:

  [text/plain] 0001-WIP-PQexecMulti.patch (6.8K, ../../[email protected]/2-0001-WIP-PQexecMulti.patch)
  download | inline diff:
From 947d9d98507d6c93b547ac17fef41c1870c0d577 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email protected]>
Date: Thu, 27 Jan 2022 14:09:52 +0100
Subject: [PATCH] WIP: PQexecMulti

---
 src/bin/psql/common.c            | 44 +++++++++++++++++++-------------
 src/interfaces/libpq/exports.txt |  1 +
 src/interfaces/libpq/fe-exec.c   | 37 +++++++++++++++++++++++++++
 src/interfaces/libpq/libpq-fe.h  |  1 +
 4 files changed, 65 insertions(+), 18 deletions(-)

diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
index 3503605a7d..deaaa54f10 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -1195,7 +1195,6 @@ bool
 SendQuery(const char *query)
 {
 	bool		timing = pset.timing;
-	PGresult   *results;
 	PGTransactionStatusType transaction_status;
 	double		elapsed_msec = 0;
 	bool		OK = false;
@@ -1247,15 +1246,17 @@ SendQuery(const char *query)
 		!pset.autocommit &&
 		!command_no_begin(query))
 	{
-		results = PQexec(pset.db, "BEGIN");
-		if (PQresultStatus(results) != PGRES_COMMAND_OK)
+		PGresult   *result;
+
+		result = PQexec(pset.db, "BEGIN");
+		if (PQresultStatus(result) != PGRES_COMMAND_OK)
 		{
 			pg_log_info("%s", PQerrorMessage(pset.db));
-			ClearOrSaveResult(results);
+			ClearOrSaveResult(result);
 			ResetCancelConn();
 			goto sendquery_cleanup;
 		}
-		ClearOrSaveResult(results);
+		ClearOrSaveResult(result);
 		transaction_status = PQtransactionStatus(pset.db);
 	}
 
@@ -1264,15 +1265,17 @@ SendQuery(const char *query)
 		(pset.cur_cmd_interactive ||
 		 pset.on_error_rollback == PSQL_ERROR_ROLLBACK_ON))
 	{
-		results = PQexec(pset.db, "SAVEPOINT pg_psql_temporary_savepoint");
-		if (PQresultStatus(results) != PGRES_COMMAND_OK)
+		PGresult   *result;
+
+		result = PQexec(pset.db, "SAVEPOINT pg_psql_temporary_savepoint");
+		if (PQresultStatus(result) != PGRES_COMMAND_OK)
 		{
 			pg_log_info("%s", PQerrorMessage(pset.db));
-			ClearOrSaveResult(results);
+			ClearOrSaveResult(result);
 			ResetCancelConn();
 			goto sendquery_cleanup;
 		}
-		ClearOrSaveResult(results);
+		ClearOrSaveResult(result);
 		on_error_rollback_savepoint = true;
 	}
 
@@ -1281,7 +1284,6 @@ SendQuery(const char *query)
 		/* Describe query's result columns, without executing it */
 		OK = DescribeQuery(query, &elapsed_msec);
 		ResetCancelConn();
-		results = NULL;			/* PQclear(NULL) does nothing */
 	}
 	else if (pset.fetch_count <= 0 || pset.gexec_flag ||
 			 pset.crosstab_flag || !is_select_command(query))
@@ -1289,15 +1291,19 @@ SendQuery(const char *query)
 		/* Default fetch-it-all-and-print mode */
 		instr_time	before,
 					after;
+		PGresult  **results;
+		PGresult  **res;
 
 		if (timing)
 			INSTR_TIME_SET_CURRENT(before);
 
-		results = PQexec(pset.db, query);
+		results = PQexecMulti(pset.db, query);
 
 		/* these operations are included in the timing result: */
 		ResetCancelConn();
-		OK = ProcessResult(&results);
+		OK = false;
+		for (res = results; *res; res++)
+			OK |= ProcessResult(res);
 
 		if (timing)
 		{
@@ -1307,15 +1313,18 @@ SendQuery(const char *query)
 		}
 
 		/* but printing results isn't: */
-		if (OK && results)
-			OK = PrintQueryResults(results);
+		if (OK)
+			for (res = results; *res; res++)
+				OK |= PrintQueryResults(*res);
+
+		for (res = results; *res; res++)
+			ClearOrSaveResult(*res);
 	}
 	else
 	{
 		/* Fetch-in-segments mode */
 		OK = ExecQueryUsingCursor(query, &elapsed_msec);
 		ResetCancelConn();
-		results = NULL;			/* PQclear(NULL) does nothing */
 	}
 
 	if (!OK && pset.echo == PSQL_ECHO_ERRORS)
@@ -1341,6 +1350,7 @@ SendQuery(const char *query)
 
 			case PQTRANS_INTRANS:
 
+#if FIXME
 				/*
 				 * Do nothing if they are messing with savepoints themselves:
 				 * If the user did COMMIT AND CHAIN, RELEASE or ROLLBACK, our
@@ -1354,6 +1364,7 @@ SendQuery(const char *query)
 					 strcmp(PQcmdStatus(results), "ROLLBACK") == 0))
 					svptcmd = NULL;
 				else
+#endif
 					svptcmd = "RELEASE pg_psql_temporary_savepoint";
 				break;
 
@@ -1379,7 +1390,6 @@ SendQuery(const char *query)
 				ClearOrSaveResult(svptres);
 				OK = false;
 
-				PQclear(results);
 				ResetCancelConn();
 				goto sendquery_cleanup;
 			}
@@ -1387,8 +1397,6 @@ SendQuery(const char *query)
 		}
 	}
 
-	ClearOrSaveResult(results);
-
 	/* Possible microtiming output */
 	if (timing)
 		PrintTiming(elapsed_msec);
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index e8bcc88370..878e430b92 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -186,3 +186,4 @@ PQpipelineStatus          183
 PQsetTraceFlags           184
 PQmblenBounded            185
 PQsendFlushRequest        186
+PQexecMulti               187
diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c
index 59121873d2..90f00ad401 100644
--- a/src/interfaces/libpq/fe-exec.c
+++ b/src/interfaces/libpq/fe-exec.c
@@ -70,6 +70,7 @@ static void parseInput(PGconn *conn);
 static PGresult *getCopyResult(PGconn *conn, ExecStatusType copytype);
 static bool PQexecStart(PGconn *conn);
 static PGresult *PQexecFinish(PGconn *conn);
+static PGresult **PQexecFinishMulti(PGconn *conn);
 static int	PQsendDescribe(PGconn *conn, char desc_type,
 						   const char *desc_target);
 static int	check_field_number(const PGresult *res, int field_num);
@@ -2199,6 +2200,16 @@ PQexec(PGconn *conn, const char *query)
 	return PQexecFinish(conn);
 }
 
+PGresult **
+PQexecMulti(PGconn *conn, const char *query)
+{
+	if (!PQexecStart(conn))
+		return NULL;
+	if (!PQsendQuery(conn, query))
+		return NULL;
+	return PQexecFinishMulti(conn);
+}
+
 /*
  * PQexecParams
  *		Like PQexec, but use extended query protocol so we can pass parameters
@@ -2369,6 +2380,32 @@ PQexecFinish(PGconn *conn)
 	return lastResult;
 }
 
+static PGresult **
+PQexecFinishMulti(PGconn *conn)
+{
+	int			count = 0;
+	PGresult   *result;
+	PGresult  **ret = NULL;
+
+	while ((result = PQgetResult(conn)) != NULL)
+	{
+		count++;
+		ret = realloc(ret, count * sizeof(PGresult*));
+		ret[count - 1] = result;
+
+		if (result->resultStatus == PGRES_COPY_IN ||
+			result->resultStatus == PGRES_COPY_OUT ||
+			result->resultStatus == PGRES_COPY_BOTH ||
+			conn->status == CONNECTION_BAD)
+			break;
+	}
+
+	ret = realloc(ret, (count + 1) * sizeof(PGresult*));
+	ret[count] = NULL;
+
+	return ret;
+}
+
 /*
  * PQdescribePrepared
  *	  Obtain information about a previously prepared statement
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index 20eb855abc..61b3e9bf21 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -418,6 +418,7 @@ extern void PQsetTraceFlags(PGconn *conn, int flags);
 
 /* Simple synchronous query */
 extern PGresult *PQexec(PGconn *conn, const char *query);
+extern PGresult **PQexecMulti(PGconn *conn, const char *query);
 extern PGresult *PQexecParams(PGconn *conn,
 							  const char *command,
 							  int nParams,
-- 
2.34.1



^ permalink  raw  reply  [nested|flat] 14+ messages in thread

* Re: psql - add SHOW_ALL_RESULTS option
  2022-01-04 07:55 Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-01-08 18:32 ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-01-13 05:44   ` Re: psql - add SHOW_ALL_RESULTS option Andres Freund <[email protected]>
  2022-01-15 09:00     ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
@ 2022-03-04 08:50       ` Peter Eisentraut <[email protected]>
  2022-03-04 13:48         ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  1 sibling, 1 reply; 14+ messages in thread

From: Peter Eisentraut @ 2022-03-04 08:50 UTC (permalink / raw)
  To: Fabien COELHO <[email protected]>; +Cc: [email protected] <[email protected]>

On 15.01.22 10:00, Fabien COELHO wrote:
>> The reason this test constantly fails on cfbot windows is a 
>> use-after-free
>> bug.
> 
> Indeed! Thanks a lot for the catch and the debug!
> 
> The ClearOrSaveResult function is quite annoying because it may or may 
> not clear the result as a side effect.
> 
> Attached v14 moves the status extraction before the possible clear. I've 
> added a couple of results = NULL after such calls in the code.

Are you planning to send a rebased patch for this commit fest?






^ permalink  raw  reply  [nested|flat] 14+ messages in thread

* Re: psql - add SHOW_ALL_RESULTS option
  2022-01-04 07:55 Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-01-08 18:32 ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-01-13 05:44   ` Re: psql - add SHOW_ALL_RESULTS option Andres Freund <[email protected]>
  2022-01-15 09:00     ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-03-04 08:50       ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
@ 2022-03-04 13:48         ` Fabien COELHO <[email protected]>
  2022-03-12 16:27           ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  0 siblings, 1 reply; 14+ messages in thread

From: Fabien COELHO @ 2022-03-04 13:48 UTC (permalink / raw)
  To: Peter Eisentraut <[email protected]>; +Cc: [email protected] <[email protected]>



> Are you planning to send a rebased patch for this commit fest?

Argh, I did it in a reply in another thread:-( Attached v15.

So as to help moves things forward, I'd suggest that we should not to care 
too much about corner case repetition of some error messages which are due 
to libpq internals, so I could remove the ugly buffer reset from the patch 
and have the repetition, and if/when the issue is fixed later in libpq 
then the repetition will be removed, fine! The issue is that we just 
expose the strange behavior of libpq, which is libpq to solve, not psql.

What do you think?

-- 
Fabien.

Attachments:

  [text/x-diff] psql-show-all-results-15.patch (49.9K, ../../alpine.DEB.2.22.394.2203041443080.538147@pseudo/2-psql-show-all-results-15.patch)
  download | inline diff:
diff --git a/contrib/pg_stat_statements/expected/pg_stat_statements.out b/contrib/pg_stat_statements/expected/pg_stat_statements.out
index e0abe34bb6..8f7f93172a 100644
--- a/contrib/pg_stat_statements/expected/pg_stat_statements.out
+++ b/contrib/pg_stat_statements/expected/pg_stat_statements.out
@@ -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 
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index caabb06c53..f01adb1fd2 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -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>
@@ -3570,10 +3563,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>
@@ -4160,6 +4149,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>
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
index d65b9a124f..3b2f6305b4 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -34,6 +34,8 @@ 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, const printQueryOpt *opt, FILE *printQueryFout, bool *tx_ended);
 
 
 /*
@@ -354,7 +356,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;
 
@@ -385,7 +387,7 @@ AcceptResult(const PGresult *result)
 				break;
 		}
 
-	if (!OK)
+	if (!OK && show_error)
 	{
 		const char *error = PQerrorMessage(pset.db);
 
@@ -473,6 +475,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
@@ -573,7 +587,7 @@ PSQLexec(const char *query)
 
 	ResetCancelConn();
 
-	if (!AcceptResult(res))
+	if (!AcceptResult(res, true))
 	{
 		ClearOrSaveResult(res);
 		res = NULL;
@@ -596,11 +610,8 @@ int
 PSQLexecWatch(const char *query, const printQueryOpt *opt, FILE *printQueryFout)
 {
 	bool		timing = pset.timing;
-	PGresult   *res;
 	double		elapsed_msec = 0;
-	instr_time	before;
-	instr_time	after;
-	FILE	   *fout;
+	int			res;
 
 	if (!pset.db)
 	{
@@ -609,77 +620,14 @@ PSQLexecWatch(const char *query, const printQueryOpt *opt, FILE *printQueryFout)
 	}
 
 	SetCancelConn(pset.db);
-
-	if (timing)
-		INSTR_TIME_SET_CURRENT(before);
-
-	res = PQexec(pset.db, query);
-
+	res = SendQueryAndProcessResults(query, &elapsed_msec, true, opt, printQueryFout, NULL);
 	ResetCancelConn();
 
-	if (!AcceptResult(res))
-	{
-		ClearOrSaveResult(res);
-		return 0;
-	}
-
-	if (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;
-	}
-
-	fout = printQueryFout ? printQueryFout : pset.queryFout;
-
-	switch (PQresultStatus(res))
-	{
-		case PGRES_TUPLES_OK:
-			printQuery(res, opt, fout, false, pset.logfile);
-			break;
-
-		case PGRES_COMMAND_OK:
-			fprintf(fout, "%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(fout);
-
 	/* Possible microtiming output */
 	if (timing)
 		PrintTiming(elapsed_msec);
 
-	return 1;
+	return res;
 }
 
 
@@ -714,7 +662,7 @@ PrintNotifications(void)
  * Returns true if successful, false otherwise.
  */
 static bool
-PrintQueryTuples(const PGresult *result)
+PrintQueryTuples(const PGresult *result, const printQueryOpt *opt, FILE *printQueryFout)
 {
 	bool		ok = true;
 
@@ -746,8 +694,9 @@ PrintQueryTuples(const PGresult *result)
 	}
 	else
 	{
-		printQuery(result, &pset.popt, pset.queryFout, false, pset.logfile);
-		if (ferror(pset.queryFout))
+		FILE *fout = printQueryFout ? printQueryFout : pset.queryFout;
+		printQuery(result, opt ? opt : &pset.popt, fout, false, pset.logfile);
+		if (ferror(fout))
 		{
 			pg_log_error("could not print result table: %m");
 			ok = false;
@@ -892,213 +841,131 @@ 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.
- *
- * When the command string contained no such COPY command, this function
- * degenerates to an AcceptResult() call.
- *
- * 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.
+ * 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.
+ *
+ * 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.
+ *
+ * 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 **resultp)
+HandleCopyResult(PGresult **resultp)
 {
-	bool		success = true;
-	bool		first_cycle = true;
+	bool		success;
+	FILE	   *copystream;
+	PGresult   *copy_result;
+	ExecStatusType result_status = PQresultStatus(*resultp);
 
-	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(*resultp))
+		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(*resultp);
-		switch (result_status)
+		else if (pset.gfname)
 		{
-			case PGRES_EMPTY_QUERY:
-			case PGRES_COMMAND_OK:
-			case PGRES_TUPLES_OK:
-				is_copy = false;
-				break;
-
-			case PGRES_COPY_OUT:
-			case PGRES_COPY_IN:
-				is_copy = true;
-				break;
-
-			default:
-				/* AcceptResult() should have caught anything else. */
-				is_copy = false;
-				pg_log_error("unexpected PQresultStatus: %d", result_status);
-				break;
-		}
-
-		if (is_copy)
-		{
-			/*
-			 * 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)
+			/* invoked by \g */
+			if (openQueryOutputFile(pset.gfname,
+									&copystream, &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);
-					}
-				}
+				need_close = true;
+				if (is_pipe)
+					disable_sigpipe_trap();
 			}
 			else
-			{
-				/* COPY IN */
-				copystream = pset.copyStream ? pset.copyStream : pset.cur_cmd_source;
-				success = handleCopyIn(pset.db,
-									   copystream,
-									   PQbinaryTuples(*resultp),
-									   &copy_result) && success;
-			}
-			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(*resultp);
-			*resultp = copy_result;
+				copystream = NULL;	/* discard COPY data entirely */
 		}
-		else if (first_cycle)
+		else
 		{
-			/* fast path: no COPY commands; PQexec visited all results */
-			break;
+			/* fall back to the generic query output stream */
+			copystream = pset.queryFout;
 		}
 
+		success = handleCopyOut(pset.db,
+								copystream,
+								&copy_result)
+			&& (copystream != NULL);
+
 		/*
-		 * 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.
+		 * 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.
 		 */
-		next_result = PQgetResult(pset.db);
-		if (!next_result)
-			break;
+		if (copystream == pset.queryFout)
+		{
+			PQclear(copy_result);
+			copy_result = NULL;
+		}
 
-		PQclear(*resultp);
-		*resultp = next_result;
-		first_cycle = false;
+		if (need_close)
+		{
+			/* close \g argument file/pipe */
+			if (is_pipe)
+			{
+				pclose(copystream);
+				restore_sigpipe_trap();
+			}
+			else
+			{
+				fclose(copystream);
+			}
+		}
+	}
+	else
+	{
+		/* COPY IN */
+		copystream = pset.copyStream ? pset.copyStream : pset.cur_cmd_source;
+		success = handleCopyIn(pset.db,
+							   copystream,
+							   PQbinaryTuples(*resultp),
+							   &copy_result);
 	}
 
-	SetResultVariables(*resultp, success);
-
-	/* may need this to recover from conn loss during COPY */
-	if (!first_cycle && !CheckConnection())
-		return false;
+	ResetCancelConn();
+	PQclear(*resultp);
+	*resultp = copy_result;
 
 	return success;
 }
 
-
 /*
  * PrintQueryStatus: report command status as required
  *
- * Note: Utility function for use by PrintQueryResult() only.
+ * Note: Utility function for use by HandleQueryResult() only.
  */
 static void
-PrintQueryStatus(PGresult *result)
+PrintQueryStatus(PGresult *result, FILE *printQueryFout)
 {
 	char		buf[16];
+	FILE	   *fout = printQueryFout ? printQueryFout : pset.queryFout;
 
 	if (!pset.quiet)
 	{
 		if (pset.popt.topt.format == PRINT_HTML)
 		{
-			fputs("<p>", pset.queryFout);
-			html_escaped_print(PQcmdStatus(result), pset.queryFout);
-			fputs("</p>\n", pset.queryFout);
+			fputs("<p>", fout);
+			html_escaped_print(PQcmdStatus(result), fout);
+			fputs("</p>\n", fout);
 		}
 		else
-			fprintf(pset.queryFout, "%s\n", PQcmdStatus(result));
+			fprintf(fout, "%s\n", PQcmdStatus(result));
 	}
 
 	if (pset.logfile)
@@ -1110,43 +977,50 @@ PrintQueryStatus(PGresult *result)
 
 
 /*
- * PrintQueryResult: print out (or store or execute) query result 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
-PrintQueryResult(PGresult *result)
+HandleQueryResult(PGresult *result, bool last, bool is_watch, const printQueryOpt *opt, FILE *printQueryFout)
 {
 	bool		success;
 	const char *cmdstatus;
 
-	if (!result)
+	if (result == NULL)
 		return false;
 
 	switch (PQresultStatus(result))
 	{
 		case PGRES_TUPLES_OK:
 			/* store or execute or print the data ... */
-			if (pset.gset_prefix)
+			if (last && pset.gset_prefix)
 				success = StoreQueryTuple(result);
-			else if (pset.gexec_flag)
+			else if (last && pset.gexec_flag)
 				success = ExecQueryTuples(result);
-			else if (pset.crosstab_flag)
+			else if (last && pset.crosstab_flag)
 				success = PrintResultInCrosstab(result);
+			else if (last || pset.show_all_results)
+				success = PrintQueryTuples(result, opt, printQueryFout);
 			else
-				success = PrintQueryTuples(result);
+				success = true;
+
 			/* if it's INSERT/UPDATE/DELETE RETURNING, also print status */
-			cmdstatus = PQcmdStatus(result);
-			if (strncmp(cmdstatus, "INSERT", 6) == 0 ||
-				strncmp(cmdstatus, "UPDATE", 6) == 0 ||
-				strncmp(cmdstatus, "DELETE", 6) == 0)
-				PrintQueryStatus(result);
+			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, printQueryFout);
+			}
+
 			break;
 
 		case PGRES_COMMAND_OK:
-			PrintQueryStatus(result);
+			if (last || pset.show_all_results)
+				PrintQueryStatus(result, printQueryFout);
 			success = true;
 			break;
 
@@ -1156,7 +1030,7 @@ PrintQueryResult(PGresult *result)
 
 		case PGRES_COPY_OUT:
 		case PGRES_COPY_IN:
-			/* nothing to do here */
+			/* nothing to do here: already processed */
 			success = true;
 			break;
 
@@ -1173,11 +1047,259 @@ PrintQueryResult(PGresult *result)
 			break;
 	}
 
-	fflush(pset.queryFout);
+	fflush(printQueryFout ? printQueryFout : pset.queryFout);
 
 	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 {
+	PQExpBufferData	messages[2];
+	int				current;
+} 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->messages[notes->current], msg);
+}
+
+/*
+ * Show notices stored in buffer, which is then reset.
+ */
+static void
+ShowNoticeMessage(t_notice_messages *notes)
+{
+	PQExpBufferData	*current = &notes->messages[notes->current];
+	if (*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, const printQueryOpt *opt, FILE *printQueryFout, bool *tx_ended)
+{
+	bool				timing = pset.timing;
+	bool				success;
+	instr_time			before;
+	PGresult		   *result;
+	t_notice_messages	notes;
+
+	if (timing)
+		INSTR_TIME_SET_CURRENT(before);
+
+	success = PQsendQuery(pset.db, query);
+
+	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.current = 0;
+	initPQExpBuffer(&notes.messages[0]);
+	initPQExpBuffer(&notes.messages[1]);
+	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);
+
+				/*
+				 * On connection loss another result with a message will be
+				 * generated, we do not want to see this error again.
+				 */
+				PQclearErrorMessage(pset.db);
+			}
+			CheckConnection();
+			if (!is_watch)
+				SetResultVariables(result, false);
+
+			/* keep the result status before clearing it */
+			result_status = PQresultStatus(result);
+			ClearOrSaveResult(result);
+			result = NULL;
+			success = false;
+
+			/* switch to next result */
+			if (result_status == PGRES_COPY_BOTH ||
+				result_status == PGRES_COPY_OUT ||
+				result_status == PGRES_COPY_IN)
+				/*
+				 * For some obscure reason PQgetResult does *not* return a NULL in copy
+				 * cases despite the result having been cleared, but keeps returning an
+				 * "empty" result that we have to ignore manually.
+				 */
+				;
+			else
+				result = PQgetResult(pset.db);
+
+			continue;
+		}
+		else if (tx_ended != NULL && ! *tx_ended)
+		{
+			/* on success, tell whether the "current" transaction sequence ended */
+			const char *cmd = PQcmdStatus(result);
+			*tx_ended = strcmp(cmd, "COMMIT") == 0 || strcmp(cmd, "ROLLBACK") == 0 ||
+				strcmp(cmd, "SAVEPOINT") == 0 || strcmp(cmd, "RELEASE") == 0;
+		}
+
+		result_status = PQresultStatus(result);
+
+		/* must handle COPY before changing the current 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. We need to do that to check whether this is the last.
+		 */
+		notes.current ^= 1;
+		next_result = PQgetResult(pset.db);
+		notes.current ^= 1;
+		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 && 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, false, opt, printQueryFout);
+
+		/* set variables on last result if all went well */
+		if (!is_watch && last && success)
+			SetResultVariables(result, true);
+
+		ClearOrSaveResult(result);
+		notes.current ^= 1;
+		result = next_result;
+
+		if (cancel_pressed)
+		{
+			ClearOrSaveAllResults();
+			break;
+		}
+	}
+
+	/* reset notice hook */
+	PQsetNoticeProcessor(pset.db, NoticeProcessor, NULL);
+	termPQExpBuffer(&notes.messages[0]);
+	termPQExpBuffer(&notes.messages[1]);
+
+	/* may need this to recover from conn loss during COPY */
+	if (!CheckConnection())
+		return -1;
+
+	return cancel_pressed ? 0 : success ? 1 : -1;
+}
+
 
 /*
  * SendQuery: send the query string to the backend
@@ -1195,12 +1317,14 @@ bool
 SendQuery(const char *query)
 {
 	bool		timing = pset.timing;
-	PGresult   *result;
+	PGresult    *result = NULL;
 	PGTransactionStatusType transaction_status;
 	double		elapsed_msec = 0;
 	bool		OK = false;
+	bool		res_error;
 	int			i;
 	bool		on_error_rollback_savepoint = false;
+	bool		tx_ended = false;
 
 	if (!pset.db)
 	{
@@ -1239,82 +1363,71 @@ SendQuery(const char *query)
 		fflush(pset.logfile);
 	}
 
+	/* global query cancellation for this query */
 	SetCancelConn(pset.db);
 
 	transaction_status = PQtransactionStatus(pset.db);
 
+	/* issue a BEGIN if needed, corresponding COMMIT/ROLLBACK by user */
 	if (transaction_status == PQTRANS_IDLE &&
 		!pset.autocommit &&
 		!command_no_begin(query))
 	{
+		PGresult    *result;
 		result = PQexec(pset.db, "BEGIN");
-		if (PQresultStatus(result) != PGRES_COMMAND_OK)
+		res_error = PQresultStatus(result) != PGRES_COMMAND_OK;
+		ClearOrSaveResult(result);
+		result = NULL;
+
+		if (res_error)
 		{
 			pg_log_info("%s", PQerrorMessage(pset.db));
-			ClearOrSaveResult(result);
-			ResetCancelConn();
 			goto sendquery_cleanup;
 		}
-		ClearOrSaveResult(result);
+
+		/* must be PQTRANS_INTRANS after a BEGIN */
 		transaction_status = PQtransactionStatus(pset.db);
 	}
 
+	/* create automatic savepoint if needed and possible */
 	if (transaction_status == PQTRANS_INTRANS &&
 		pset.on_error_rollback != PSQL_ERROR_ROLLBACK_OFF &&
 		(pset.cur_cmd_interactive ||
 		 pset.on_error_rollback == PSQL_ERROR_ROLLBACK_ON))
 	{
+		PGresult   *result;
 		result = PQexec(pset.db, "SAVEPOINT pg_psql_temporary_savepoint");
-		if (PQresultStatus(result) != PGRES_COMMAND_OK)
+		res_error = PQresultStatus(result) != PGRES_COMMAND_OK;
+		ClearOrSaveResult(result);
+		result = NULL;
+
+		if (res_error)
 		{
 			pg_log_info("%s", PQerrorMessage(pset.db));
-			ClearOrSaveResult(result);
-			ResetCancelConn();
 			goto sendquery_cleanup;
 		}
-		ClearOrSaveResult(result);
+
 		on_error_rollback_savepoint = true;
 	}
 
+	/* process the query, one way or the other */
 	if (pset.gdesc_flag)
 	{
 		/* Describe query's result columns, without executing it */
 		OK = DescribeQuery(query, &elapsed_msec);
-		ResetCancelConn();
 		result = NULL;			/* PQclear(NULL) does nothing */
 	}
 	else if (pset.fetch_count <= 0 || pset.gexec_flag ||
 			 pset.crosstab_flag || !is_select_command(query))
 	{
 		/* Default fetch-it-all-and-print mode */
-		instr_time	before,
-					after;
-
-		if (timing)
-			INSTR_TIME_SET_CURRENT(before);
-
-		result = PQexec(pset.db, query);
-
-		/* these operations are included in the timing result: */
-		ResetCancelConn();
-		OK = ProcessResult(&result);
-
-		if (timing)
-		{
-			INSTR_TIME_SET_CURRENT(after);
-			INSTR_TIME_SUBTRACT(after, before);
-			elapsed_msec = INSTR_TIME_GET_MILLISEC(after);
-		}
-
-		/* but printing result isn't: */
-		if (OK && result)
-			OK = PrintQueryResult(result);
+		int res = SendQueryAndProcessResults(query, &elapsed_msec, false, NULL, NULL, &tx_ended);
+		OK = (res >= 0);
 	}
 	else
 	{
 		/* Fetch-in-segments mode */
 		OK = ExecQueryUsingCursor(query, &elapsed_msec);
-		ResetCancelConn();
 		result = NULL;			/* PQclear(NULL) does nothing */
 	}
 
@@ -1347,11 +1460,7 @@ SendQuery(const char *query)
 				 * savepoint is gone. If they issued a SAVEPOINT, releasing
 				 * ours would remove theirs.
 				 */
-				if (result &&
-					(strcmp(PQcmdStatus(result), "COMMIT") == 0 ||
-					 strcmp(PQcmdStatus(result), "SAVEPOINT") == 0 ||
-					 strcmp(PQcmdStatus(result), "RELEASE") == 0 ||
-					 strcmp(PQcmdStatus(result), "ROLLBACK") == 0))
+				if (tx_ended)
 					svptcmd = NULL;
 				else
 					svptcmd = "RELEASE pg_psql_temporary_savepoint";
@@ -1373,14 +1482,15 @@ SendQuery(const char *query)
 			PGresult   *svptres;
 
 			svptres = PQexec(pset.db, svptcmd);
-			if (PQresultStatus(svptres) != PGRES_COMMAND_OK)
+			res_error = PQresultStatus(svptres) != PGRES_COMMAND_OK;
+
+			if (res_error)
 			{
 				pg_log_info("%s", PQerrorMessage(pset.db));
 				ClearOrSaveResult(svptres);
 				OK = false;
 
 				PQclear(result);
-				ResetCancelConn();
 				goto sendquery_cleanup;
 			}
 			PQclear(svptres);
@@ -1411,6 +1521,9 @@ SendQuery(const char *query)
 
 sendquery_cleanup:
 
+	/* global cancellation reset */
+	ResetCancelConn();
+
 	/* reset \g's output-to-filename trigger */
 	if (pset.gfname)
 	{
@@ -1491,7 +1604,7 @@ DescribeQuery(const char *query, double *elapsed_msec)
 	PQclear(result);
 
 	result = PQdescribePrepared(pset.db, "");
-	OK = AcceptResult(result) &&
+	OK = AcceptResult(result, true) &&
 		(PQresultStatus(result) == PGRES_COMMAND_OK);
 	if (OK && result)
 	{
@@ -1539,7 +1652,7 @@ DescribeQuery(const char *query, double *elapsed_msec)
 			PQclear(result);
 
 			result = PQexec(pset.db, buf.data);
-			OK = AcceptResult(result);
+			OK = AcceptResult(result, true);
 
 			if (timing)
 			{
@@ -1549,7 +1662,7 @@ DescribeQuery(const char *query, double *elapsed_msec)
 			}
 
 			if (OK && result)
-				OK = PrintQueryResult(result);
+				OK = HandleQueryResult(result, true, false, NULL, NULL);
 
 			termPQExpBuffer(&buf);
 		}
@@ -1609,7 +1722,7 @@ ExecQueryUsingCursor(const char *query, double *elapsed_msec)
 	if (PQtransactionStatus(pset.db) == PQTRANS_IDLE)
 	{
 		result = PQexec(pset.db, "BEGIN");
-		OK = AcceptResult(result) &&
+		OK = AcceptResult(result, true) &&
 			(PQresultStatus(result) == PGRES_COMMAND_OK);
 		ClearOrSaveResult(result);
 		if (!OK)
@@ -1623,7 +1736,7 @@ ExecQueryUsingCursor(const char *query, double *elapsed_msec)
 					  query);
 
 	result = PQexec(pset.db, buf.data);
-	OK = AcceptResult(result) &&
+	OK = AcceptResult(result, true) &&
 		(PQresultStatus(result) == PGRES_COMMAND_OK);
 	if (!OK)
 		SetResultVariables(result, OK);
@@ -1696,7 +1809,7 @@ ExecQueryUsingCursor(const char *query, double *elapsed_msec)
 				is_pager = false;
 			}
 
-			OK = AcceptResult(result);
+			OK = AcceptResult(result, true);
 			Assert(!OK);
 			SetResultVariables(result, OK);
 			ClearOrSaveResult(result);
@@ -1805,7 +1918,7 @@ cleanup:
 	result = PQexec(pset.db, "CLOSE _psql_cursor");
 	if (OK)
 	{
-		OK = AcceptResult(result) &&
+		OK = AcceptResult(result, true) &&
 			(PQresultStatus(result) == PGRES_COMMAND_OK);
 		ClearOrSaveResult(result);
 	}
@@ -1815,7 +1928,7 @@ cleanup:
 	if (started_txn)
 	{
 		result = PQexec(pset.db, OK ? "COMMIT" : "ROLLBACK");
-		OK &= AcceptResult(result) &&
+		OK &= AcceptResult(result, true) &&
 			(PQresultStatus(result) == PGRES_COMMAND_OK);
 		ClearOrSaveResult(result);
 	}
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index 56afa6817e..b3971bce64 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -413,6 +413,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"
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index 80dbea9efd..2399cffa3f 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -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;
 
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index be9dec749d..d08b15886a 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -203,6 +203,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);
 
@@ -1150,6 +1151,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)
 {
@@ -1251,6 +1258,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);
diff --git a/src/bin/psql/t/001_basic.pl b/src/bin/psql/t/001_basic.pl
index 44ecd05add..f58e3365a8 100644
--- a/src/bin/psql/t/001_basic.pl
+++ b/src/bin/psql/t/001_basic.pl
@@ -6,7 +6,7 @@ use warnings;
 
 use PostgreSQL::Test::Cluster;
 use PostgreSQL::Test::Utils;
-use Test::More;
+use Test::More tests => 41;
 
 program_help_ok('psql');
 program_version_ok('psql');
@@ -115,4 +115,20 @@ NOTIFY foo, 'bar';",
 	qr/^Asynchronous notification "foo" with payload "bar" received from server process with PID \d+\.$/,
 	'notification with payload');
 
+# Test voluntary crash
+my ($ret, $out, $err) = $node->psql(
+	'postgres',
+	"SELECT 'before' AS running;\n" .
+	"SELECT pg_terminate_backend(pg_backend_pid());\n" .
+	"SELECT 'AFTER' AS not_running;\n");
+
+is($ret, 2, "server stopped");
+like($out, qr/before/, "output before crash");
+ok($out !~ qr/AFTER/, "no output after crash");
+is($err, 'psql:<stdin>:2: FATAL:  terminating connection due to administrator command
+psql:<stdin>:2: server closed the connection unexpectedly
+	This probably means the server terminated abnormally
+	before or while processing the request.
+psql:<stdin>:2: fatal: connection to server was lost', "expected error message");
+
 done_testing();
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 6957567264..a532b0d694 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -4511,7 +4511,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"))
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index e8bcc88370..2a3d29aee5 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -186,3 +186,4 @@ PQpipelineStatus          183
 PQsetTraceFlags           184
 PQmblenBounded            185
 PQsendFlushRequest        186
+PQclearErrorMessage	      187
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 1c5a2b43e9..6bd756c134 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -6916,6 +6916,12 @@ PQerrorMessage(const PGconn *conn)
 	return conn->errorMessage.data;
 }
 
+void
+PQclearErrorMessage(PGconn *conn)
+{
+	resetPQExpBuffer(&conn->errorMessage);
+}
+
 /*
  * In Windows, socket values are unsigned, and an invalid socket value
  * (INVALID_SOCKET) is ~0, which equals -1 in comparisons (with no compiler
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index 20eb855abc..b73b44e817 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -347,6 +347,7 @@ extern const char *PQparameterStatus(const PGconn *conn,
 extern int	PQprotocolVersion(const PGconn *conn);
 extern int	PQserverVersion(const PGconn *conn);
 extern char *PQerrorMessage(const PGconn *conn);
+extern void	PQclearErrorMessage(PGconn *conn);
 extern int	PQsocket(const PGconn *conn);
 extern int	PQbackendPID(const PGconn *conn);
 extern PGpipelineStatus PQpipelineStatus(const PGconn *conn);
diff --git a/src/test/regress/expected/copyselect.out b/src/test/regress/expected/copyselect.out
index 72865fe1eb..bb9e026f91 100644
--- a/src/test/regress/expected/copyselect.out
+++ b/src/test/regress/expected/copyselect.out
@@ -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
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 6428ebc507..6f468355ab 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5290,3 +5290,130 @@ ERROR:  relation "notexists" does not exist
 LINE 1: SELECT * FROM notexists;
                       ^
 STATEMENT:  SELECT * FROM notexists;
+\set ECHO none
+ 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)
+
+ three 
+-------
+     3
+(1 row)
+
+NOTICE:  warn 3.5
+CONTEXT:  PL/pgSQL function warn(text) line 2 at RAISE
+ warn 
+------
+ t
+(1 row)
+
+:three 4
+ERROR:  syntax error at or near ";"
+LINE 1: SELECT 5 ; SELECT 6 + ; SELECT warn('6.5') ; SELECT 7 ;
+                              ^
+ eight 
+-------
+     8
+(1 row)
+
+ERROR:  division by zero
+ begin 
+-------
+ ok
+(1 row)
+
+Calvin
+Susie
+Hobbes
+ done 
+------
+ ok
+(1 row)
+
+NOTICE:  warn 1.5
+CONTEXT:  PL/pgSQL function warn(text) line 2 at RAISE
+ two 
+-----
+   2
+(1 row)
+
+# initial AUTOCOMMIT: on
+# AUTOCOMMIT: off
+   s   
+-------
+ hello
+ world
+(2 rows)
+
+# AUTOCOMMIT: on
+   s   
+-------
+ hello
+ world
+(2 rows)
+
+# final AUTOCOMMIT: on
+# initial ON_ERROR_ROLLBACK: off
+# ON_ERROR_ROLLBACK: on
+# AUTOCOMMIT: on
+ERROR:  type "no_such_type" does not exist
+LINE 1: CREATE TABLE bla(s NO_SUCH_TYPE);
+                           ^
+ERROR:  error oops!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+   s    
+--------
+ Calvin
+ Hobbes
+(2 rows)
+
+     show     
+--------------
+ before error
+(1 row)
+
+ERROR:  error boum!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+ERROR:  error bam!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+       s       
+---------------
+ Calvin
+ Hobbes
+ Miss Wormwood
+ Susie
+(4 rows)
+
+# AUTOCOMMIT: off
+ERROR:  error bad!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+ #mum 
+------
+    1
+(1 row)
+
+ERROR:  error bad!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+       s       
+---------------
+ Calvin
+ Dad
+ Hobbes
+ Miss Wormwood
+ Susie
+(5 rows)
+
+# final ON_ERROR_ROLLBACK: off
diff --git a/src/test/regress/expected/transactions.out b/src/test/regress/expected/transactions.out
index 599d511a67..a46fa5d48a 100644
--- a/src/test/regress/expected/transactions.out
+++ b/src/test/regress/expected/transactions.out
@@ -904,8 +904,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
@@ -920,6 +930,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 
@@ -939,8 +955,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;
@@ -967,22 +993,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
diff --git a/src/test/regress/sql/copyselect.sql b/src/test/regress/sql/copyselect.sql
index 1d98dad3c8..e32a4f8e38 100644
--- a/src/test/regress/sql/copyselect.sql
+++ b/src/test/regress/sql/copyselect.sql
@@ -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; -- 0 1
 1
 \.
 2
diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql
index 0f5287f77b..2b06789b5a 100644
--- a/src/test/regress/sql/psql.sql
+++ b/src/test/regress/sql/psql.sql
@@ -1316,3 +1316,125 @@ DROP TABLE oer_test;
 \set ECHO errors
 SELECT * FROM notexists;
 \set ECHO all
+
+\set ECHO none
+
+--
+-- 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);
+
+--
+-- autocommit
+--
+\echo '# initial AUTOCOMMIT:' :AUTOCOMMIT
+\set AUTOCOMMIT off
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+-- implicit BEGIN
+CREATE TABLE foo(s TEXT);
+ROLLBACK;
+CREATE TABLE foo(s TEXT);
+INSERT INTO foo(s) VALUES ('hello'), ('world');
+COMMIT;
+DROP TABLE foo;
+ROLLBACK;
+SELECT * FROM foo ORDER BY 1;
+DROP TABLE foo;
+COMMIT;
+\set AUTOCOMMIT on
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+-- explicit BEGIN
+BEGIN;
+CREATE TABLE foo(s TEXT);
+INSERT INTO foo(s) VALUES ('hello'), ('world');
+COMMIT;
+BEGIN;
+DROP TABLE foo;
+ROLLBACK;
+-- implicit transactions
+SELECT * FROM foo ORDER BY 1;
+DROP TABLE foo;
+\echo '# final AUTOCOMMIT:' :AUTOCOMMIT
+
+--
+-- test ON_ERROR_ROLLBACK
+--
+CREATE FUNCTION psql_error(msg TEXT) RETURNS BOOLEAN AS $$
+  BEGIN
+    RAISE EXCEPTION 'error %', msg;
+  END;
+$$ LANGUAGE plpgsql;
+\echo '# initial ON_ERROR_ROLLBACK:' :ON_ERROR_ROLLBACK
+\set ON_ERROR_ROLLBACK on
+\echo '# ON_ERROR_ROLLBACK:' :ON_ERROR_ROLLBACK
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+BEGIN;
+CREATE TABLE bla(s NO_SUCH_TYPE);         -- fails
+CREATE TABLE bla(s TEXT);                 -- succeeds
+SELECT psql_error('oops!');               -- fails
+INSERT INTO bla VALUES ('Calvin'), ('Hobbes');
+COMMIT;
+SELECT * FROM bla ORDER BY 1;
+BEGIN;
+INSERT INTO bla VALUES ('Susie');         -- succeeds
+-- combined queries
+INSERT INTO bla VALUES ('Rosalyn') \;     -- will rollback
+SELECT 'before error' AS show \;          -- will show!
+  SELECT psql_error('boum!') \;           -- failure
+  SELECT 'after error' AS noshow;         -- hidden by preceeding error
+INSERT INTO bla(s) VALUES ('Moe') \;      -- will rollback
+  SELECT psql_error('bam!');
+INSERT INTO bla VALUES ('Miss Wormwood'); -- succeeds
+COMMIT;
+SELECT * FROM bla ORDER BY 1;
+-- some with autocommit off
+\set AUTOCOMMIT off
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+-- implicit BEGIN
+INSERT INTO bla VALUES ('Dad');           -- succeeds
+SELECT psql_error('bad!');                -- implicit partial rollback
+INSERT INTO bla VALUES ('Mum') \;         -- will rollback
+SELECT COUNT(*) AS "#mum"
+FROM bla WHERE s = 'Mum' \;               -- but be counted here
+SELECT psql_error('bad!');                -- implicit partial rollback
+COMMIT;
+SELECT * FROM bla ORDER BY 1;
+-- reset
+\set AUTOCOMMIT on
+\set ON_ERROR_ROLLBACK off
+\echo '# final ON_ERROR_ROLLBACK:' :ON_ERROR_ROLLBACK
+DROP TABLE bla;
+DROP FUNCTION psql_error;
diff --git a/src/test/regress/sql/transactions.sql b/src/test/regress/sql/transactions.sql
index 0a716b506b..d71c3ce93e 100644
--- a/src/test/regress/sql/transactions.sql
+++ b/src/test/regress/sql/transactions.sql
@@ -506,7 +506,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:


^ permalink  raw  reply  [nested|flat] 14+ messages in thread

* Re: psql - add SHOW_ALL_RESULTS option
  2022-01-04 07:55 Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-01-08 18:32 ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-01-13 05:44   ` Re: psql - add SHOW_ALL_RESULTS option Andres Freund <[email protected]>
  2022-01-15 09:00     ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-03-04 08:50       ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  2022-03-04 13:48         ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
@ 2022-03-12 16:27           ` Fabien COELHO <[email protected]>
  2022-03-17 14:51             ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  0 siblings, 1 reply; 14+ messages in thread

From: Fabien COELHO @ 2022-03-12 16:27 UTC (permalink / raw)
  To: Peter Eisentraut <[email protected]>; +Cc: [email protected] <[email protected]>


>> Are you planning to send a rebased patch for this commit fest?
>
> Argh, I did it in a reply in another thread:-( Attached v15.
>
> So as to help moves things forward, I'd suggest that we should not to care 
> too much about corner case repetition of some error messages which are due to 
> libpq internals, so I could remove the ugly buffer reset from the patch and 
> have the repetition, and if/when the issue is fixed later in libpq then the 
> repetition will be removed, fine! The issue is that we just expose the 
> strange behavior of libpq, which is libpq to solve, not psql.

See attached v16 which removes the libpq workaround.

-- 
Fabien.

Attachments:

  [text/x-diff] psql-show-all-results-16.patch (48.2K, ../../alpine.DEB.2.22.394.2203121726590.3679190@pseudo/2-psql-show-all-results-16.patch)
  download | inline diff:
diff --git a/contrib/pg_stat_statements/expected/pg_stat_statements.out b/contrib/pg_stat_statements/expected/pg_stat_statements.out
index e0abe34bb6..8f7f93172a 100644
--- a/contrib/pg_stat_statements/expected/pg_stat_statements.out
+++ b/contrib/pg_stat_statements/expected/pg_stat_statements.out
@@ -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 
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index caabb06c53..f01adb1fd2 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -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>
@@ -3570,10 +3563,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>
@@ -4160,6 +4149,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>
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
index d65b9a124f..7903075975 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -34,6 +34,8 @@ 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, const printQueryOpt *opt, FILE *printQueryFout, bool *tx_ended);
 
 
 /*
@@ -354,7 +356,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;
 
@@ -385,7 +387,7 @@ AcceptResult(const PGresult *result)
 				break;
 		}
 
-	if (!OK)
+	if (!OK && show_error)
 	{
 		const char *error = PQerrorMessage(pset.db);
 
@@ -473,6 +475,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
@@ -573,7 +587,7 @@ PSQLexec(const char *query)
 
 	ResetCancelConn();
 
-	if (!AcceptResult(res))
+	if (!AcceptResult(res, true))
 	{
 		ClearOrSaveResult(res);
 		res = NULL;
@@ -596,11 +610,8 @@ int
 PSQLexecWatch(const char *query, const printQueryOpt *opt, FILE *printQueryFout)
 {
 	bool		timing = pset.timing;
-	PGresult   *res;
 	double		elapsed_msec = 0;
-	instr_time	before;
-	instr_time	after;
-	FILE	   *fout;
+	int			res;
 
 	if (!pset.db)
 	{
@@ -609,77 +620,14 @@ PSQLexecWatch(const char *query, const printQueryOpt *opt, FILE *printQueryFout)
 	}
 
 	SetCancelConn(pset.db);
-
-	if (timing)
-		INSTR_TIME_SET_CURRENT(before);
-
-	res = PQexec(pset.db, query);
-
+	res = SendQueryAndProcessResults(query, &elapsed_msec, true, opt, printQueryFout, NULL);
 	ResetCancelConn();
 
-	if (!AcceptResult(res))
-	{
-		ClearOrSaveResult(res);
-		return 0;
-	}
-
-	if (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;
-	}
-
-	fout = printQueryFout ? printQueryFout : pset.queryFout;
-
-	switch (PQresultStatus(res))
-	{
-		case PGRES_TUPLES_OK:
-			printQuery(res, opt, fout, false, pset.logfile);
-			break;
-
-		case PGRES_COMMAND_OK:
-			fprintf(fout, "%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(fout);
-
 	/* Possible microtiming output */
 	if (timing)
 		PrintTiming(elapsed_msec);
 
-	return 1;
+	return res;
 }
 
 
@@ -714,7 +662,7 @@ PrintNotifications(void)
  * Returns true if successful, false otherwise.
  */
 static bool
-PrintQueryTuples(const PGresult *result)
+PrintQueryTuples(const PGresult *result, const printQueryOpt *opt, FILE *printQueryFout)
 {
 	bool		ok = true;
 
@@ -746,8 +694,9 @@ PrintQueryTuples(const PGresult *result)
 	}
 	else
 	{
-		printQuery(result, &pset.popt, pset.queryFout, false, pset.logfile);
-		if (ferror(pset.queryFout))
+		FILE *fout = printQueryFout ? printQueryFout : pset.queryFout;
+		printQuery(result, opt ? opt : &pset.popt, fout, false, pset.logfile);
+		if (ferror(fout))
 		{
 			pg_log_error("could not print result table: %m");
 			ok = false;
@@ -892,213 +841,131 @@ 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.
- *
- * When the command string contained no such COPY command, this function
- * degenerates to an AcceptResult() call.
- *
- * 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.
+ * Marshal the COPY data.  Either subroutine will get the
+ * connection out of its COPY state, then 
+ * once and report any error. Return whether all was ok.
+ *
+ * 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.
+ *
+ * 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 **resultp)
+HandleCopyResult(PGresult **resultp)
 {
-	bool		success = true;
-	bool		first_cycle = true;
+	bool		success;
+	FILE	   *copystream;
+	PGresult   *copy_result;
+	ExecStatusType result_status = PQresultStatus(*resultp);
 
-	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(*resultp))
+		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(*resultp);
-		switch (result_status)
+		else if (pset.gfname)
 		{
-			case PGRES_EMPTY_QUERY:
-			case PGRES_COMMAND_OK:
-			case PGRES_TUPLES_OK:
-				is_copy = false;
-				break;
-
-			case PGRES_COPY_OUT:
-			case PGRES_COPY_IN:
-				is_copy = true;
-				break;
-
-			default:
-				/* AcceptResult() should have caught anything else. */
-				is_copy = false;
-				pg_log_error("unexpected PQresultStatus: %d", result_status);
-				break;
-		}
-
-		if (is_copy)
-		{
-			/*
-			 * 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)
+			/* invoked by \g */
+			if (openQueryOutputFile(pset.gfname,
+									&copystream, &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);
-					}
-				}
+				need_close = true;
+				if (is_pipe)
+					disable_sigpipe_trap();
 			}
 			else
-			{
-				/* COPY IN */
-				copystream = pset.copyStream ? pset.copyStream : pset.cur_cmd_source;
-				success = handleCopyIn(pset.db,
-									   copystream,
-									   PQbinaryTuples(*resultp),
-									   &copy_result) && success;
-			}
-			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(*resultp);
-			*resultp = copy_result;
+				copystream = NULL;	/* discard COPY data entirely */
 		}
-		else if (first_cycle)
+		else
 		{
-			/* fast path: no COPY commands; PQexec visited all results */
-			break;
+			/* fall back to the generic query output stream */
+			copystream = pset.queryFout;
 		}
 
+		success = handleCopyOut(pset.db,
+								copystream,
+								&copy_result)
+			&& (copystream != NULL);
+
 		/*
-		 * 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.
+		 * 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.
 		 */
-		next_result = PQgetResult(pset.db);
-		if (!next_result)
-			break;
+		if (copystream == pset.queryFout)
+		{
+			PQclear(copy_result);
+			copy_result = NULL;
+		}
 
-		PQclear(*resultp);
-		*resultp = next_result;
-		first_cycle = false;
+		if (need_close)
+		{
+			/* close \g argument file/pipe */
+			if (is_pipe)
+			{
+				pclose(copystream);
+				restore_sigpipe_trap();
+			}
+			else
+			{
+				fclose(copystream);
+			}
+		}
+	}
+	else
+	{
+		/* COPY IN */
+		copystream = pset.copyStream ? pset.copyStream : pset.cur_cmd_source;
+		success = handleCopyIn(pset.db,
+							   copystream,
+							   PQbinaryTuples(*resultp),
+							   &copy_result);
 	}
 
-	SetResultVariables(*resultp, success);
-
-	/* may need this to recover from conn loss during COPY */
-	if (!first_cycle && !CheckConnection())
-		return false;
+	ResetCancelConn();
+	PQclear(*resultp);
+	*resultp = copy_result;
 
 	return success;
 }
 
-
 /*
  * PrintQueryStatus: report command status as required
  *
- * Note: Utility function for use by PrintQueryResult() only.
+ * Note: Utility function for use by HandleQueryResult() only.
  */
 static void
-PrintQueryStatus(PGresult *result)
+PrintQueryStatus(PGresult *result, FILE *printQueryFout)
 {
 	char		buf[16];
+	FILE	   *fout = printQueryFout ? printQueryFout : pset.queryFout;
 
 	if (!pset.quiet)
 	{
 		if (pset.popt.topt.format == PRINT_HTML)
 		{
-			fputs("<p>", pset.queryFout);
-			html_escaped_print(PQcmdStatus(result), pset.queryFout);
-			fputs("</p>\n", pset.queryFout);
+			fputs("<p>", fout);
+			html_escaped_print(PQcmdStatus(result), fout);
+			fputs("</p>\n", fout);
 		}
 		else
-			fprintf(pset.queryFout, "%s\n", PQcmdStatus(result));
+			fprintf(fout, "%s\n", PQcmdStatus(result));
 	}
 
 	if (pset.logfile)
@@ -1110,43 +977,50 @@ PrintQueryStatus(PGresult *result)
 
 
 /*
- * PrintQueryResult: print out (or store or execute) query result 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
-PrintQueryResult(PGresult *result)
+HandleQueryResult(PGresult *result, bool last, bool is_watch, const printQueryOpt *opt, FILE *printQueryFout)
 {
 	bool		success;
 	const char *cmdstatus;
 
-	if (!result)
+	if (result == NULL)
 		return false;
 
 	switch (PQresultStatus(result))
 	{
 		case PGRES_TUPLES_OK:
 			/* store or execute or print the data ... */
-			if (pset.gset_prefix)
+			if (last && pset.gset_prefix)
 				success = StoreQueryTuple(result);
-			else if (pset.gexec_flag)
+			else if (last && pset.gexec_flag)
 				success = ExecQueryTuples(result);
-			else if (pset.crosstab_flag)
+			else if (last && pset.crosstab_flag)
 				success = PrintResultInCrosstab(result);
+			else if (last || pset.show_all_results)
+				success = PrintQueryTuples(result, opt, printQueryFout);
 			else
-				success = PrintQueryTuples(result);
+				success = true;
+
 			/* if it's INSERT/UPDATE/DELETE RETURNING, also print status */
-			cmdstatus = PQcmdStatus(result);
-			if (strncmp(cmdstatus, "INSERT", 6) == 0 ||
-				strncmp(cmdstatus, "UPDATE", 6) == 0 ||
-				strncmp(cmdstatus, "DELETE", 6) == 0)
-				PrintQueryStatus(result);
+			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, printQueryFout);
+			}
+
 			break;
 
 		case PGRES_COMMAND_OK:
-			PrintQueryStatus(result);
+			if (last || pset.show_all_results)
+				PrintQueryStatus(result, printQueryFout);
 			success = true;
 			break;
 
@@ -1156,7 +1030,7 @@ PrintQueryResult(PGresult *result)
 
 		case PGRES_COPY_OUT:
 		case PGRES_COPY_IN:
-			/* nothing to do here */
+			/* nothing to do here: already processed */
 			success = true;
 			break;
 
@@ -1173,11 +1047,252 @@ PrintQueryResult(PGresult *result)
 			break;
 	}
 
-	fflush(pset.queryFout);
+	fflush(printQueryFout ? printQueryFout : pset.queryFout);
 
 	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 {
+	PQExpBufferData	messages[2];
+	int				current;
+} 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->messages[notes->current], msg);
+}
+
+/*
+ * Show notices stored in buffer, which is then reset.
+ */
+static void
+ShowNoticeMessage(t_notice_messages *notes)
+{
+	PQExpBufferData	*current = &notes->messages[notes->current];
+	if (*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, const printQueryOpt *opt, FILE *printQueryFout, bool *tx_ended)
+{
+	bool				timing = pset.timing;
+	bool				success;
+	instr_time			before;
+	PGresult		   *result;
+	t_notice_messages	notes;
+
+	if (timing)
+		INSTR_TIME_SET_CURRENT(before);
+
+	success = PQsendQuery(pset.db, query);
+
+	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.current = 0;
+	initPQExpBuffer(&notes.messages[0]);
+	initPQExpBuffer(&notes.messages[1]);
+	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);
+
+			/* keep the result status before clearing it */
+			result_status = PQresultStatus(result);
+			ClearOrSaveResult(result);
+			result = NULL;
+			success = false;
+
+			/* switch to next result */
+			if (result_status == PGRES_COPY_BOTH ||
+				result_status == PGRES_COPY_OUT ||
+				result_status == PGRES_COPY_IN)
+				/*
+				 * For some obscure reason PQgetResult does *not* return a NULL in copy
+				 * cases despite the result having been cleared, but keeps returning an
+				 * "empty" result that we have to ignore manually.
+				 */
+				;
+			else
+				result = PQgetResult(pset.db);
+
+			continue;
+		}
+		else if (tx_ended != NULL && ! *tx_ended)
+		{
+			/* on success, tell whether the "current" transaction sequence ended */
+			const char *cmd = PQcmdStatus(result);
+			*tx_ended = strcmp(cmd, "COMMIT") == 0 || strcmp(cmd, "ROLLBACK") == 0 ||
+				strcmp(cmd, "SAVEPOINT") == 0 || strcmp(cmd, "RELEASE") == 0;
+		}
+
+		result_status = PQresultStatus(result);
+
+		/* must handle COPY before changing the current 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. We need to do that to check whether this is the last.
+		 */
+		notes.current ^= 1;
+		next_result = PQgetResult(pset.db);
+		notes.current ^= 1;
+		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 && 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, false, opt, printQueryFout);
+
+		/* set variables on last result if all went well */
+		if (!is_watch && last && success)
+			SetResultVariables(result, true);
+
+		ClearOrSaveResult(result);
+		notes.current ^= 1;
+		result = next_result;
+
+		if (cancel_pressed)
+		{
+			ClearOrSaveAllResults();
+			break;
+		}
+	}
+
+	/* reset notice hook */
+	PQsetNoticeProcessor(pset.db, NoticeProcessor, NULL);
+	termPQExpBuffer(&notes.messages[0]);
+	termPQExpBuffer(&notes.messages[1]);
+
+	/* may need this to recover from conn loss during COPY */
+	if (!CheckConnection())
+		return -1;
+
+	return cancel_pressed ? 0 : success ? 1 : -1;
+}
+
 
 /*
  * SendQuery: send the query string to the backend
@@ -1195,12 +1310,14 @@ bool
 SendQuery(const char *query)
 {
 	bool		timing = pset.timing;
-	PGresult   *result;
+	PGresult    *result = NULL;
 	PGTransactionStatusType transaction_status;
 	double		elapsed_msec = 0;
 	bool		OK = false;
+	bool		res_error;
 	int			i;
 	bool		on_error_rollback_savepoint = false;
+	bool		tx_ended = false;
 
 	if (!pset.db)
 	{
@@ -1239,82 +1356,71 @@ SendQuery(const char *query)
 		fflush(pset.logfile);
 	}
 
+	/* global query cancellation for this query */
 	SetCancelConn(pset.db);
 
 	transaction_status = PQtransactionStatus(pset.db);
 
+	/* issue a BEGIN if needed, corresponding COMMIT/ROLLBACK by user */
 	if (transaction_status == PQTRANS_IDLE &&
 		!pset.autocommit &&
 		!command_no_begin(query))
 	{
+		PGresult    *result;
 		result = PQexec(pset.db, "BEGIN");
-		if (PQresultStatus(result) != PGRES_COMMAND_OK)
+		res_error = PQresultStatus(result) != PGRES_COMMAND_OK;
+		ClearOrSaveResult(result);
+		result = NULL;
+
+		if (res_error)
 		{
 			pg_log_info("%s", PQerrorMessage(pset.db));
-			ClearOrSaveResult(result);
-			ResetCancelConn();
 			goto sendquery_cleanup;
 		}
-		ClearOrSaveResult(result);
+
+		/* must be PQTRANS_INTRANS after a BEGIN */
 		transaction_status = PQtransactionStatus(pset.db);
 	}
 
+	/* create automatic savepoint if needed and possible */
 	if (transaction_status == PQTRANS_INTRANS &&
 		pset.on_error_rollback != PSQL_ERROR_ROLLBACK_OFF &&
 		(pset.cur_cmd_interactive ||
 		 pset.on_error_rollback == PSQL_ERROR_ROLLBACK_ON))
 	{
+		PGresult   *result;
 		result = PQexec(pset.db, "SAVEPOINT pg_psql_temporary_savepoint");
-		if (PQresultStatus(result) != PGRES_COMMAND_OK)
+		res_error = PQresultStatus(result) != PGRES_COMMAND_OK;
+		ClearOrSaveResult(result);
+		result = NULL;
+
+		if (res_error)
 		{
 			pg_log_info("%s", PQerrorMessage(pset.db));
-			ClearOrSaveResult(result);
-			ResetCancelConn();
 			goto sendquery_cleanup;
 		}
-		ClearOrSaveResult(result);
+
 		on_error_rollback_savepoint = true;
 	}
 
+	/* process the query, one way or the other */
 	if (pset.gdesc_flag)
 	{
 		/* Describe query's result columns, without executing it */
 		OK = DescribeQuery(query, &elapsed_msec);
-		ResetCancelConn();
 		result = NULL;			/* PQclear(NULL) does nothing */
 	}
 	else if (pset.fetch_count <= 0 || pset.gexec_flag ||
 			 pset.crosstab_flag || !is_select_command(query))
 	{
 		/* Default fetch-it-all-and-print mode */
-		instr_time	before,
-					after;
-
-		if (timing)
-			INSTR_TIME_SET_CURRENT(before);
-
-		result = PQexec(pset.db, query);
-
-		/* these operations are included in the timing result: */
-		ResetCancelConn();
-		OK = ProcessResult(&result);
-
-		if (timing)
-		{
-			INSTR_TIME_SET_CURRENT(after);
-			INSTR_TIME_SUBTRACT(after, before);
-			elapsed_msec = INSTR_TIME_GET_MILLISEC(after);
-		}
-
-		/* but printing result isn't: */
-		if (OK && result)
-			OK = PrintQueryResult(result);
+		int res = SendQueryAndProcessResults(query, &elapsed_msec, false, NULL, NULL, &tx_ended);
+		OK = (res >= 0);
 	}
 	else
 	{
 		/* Fetch-in-segments mode */
 		OK = ExecQueryUsingCursor(query, &elapsed_msec);
-		ResetCancelConn();
 		result = NULL;			/* PQclear(NULL) does nothing */
 	}
 
@@ -1347,11 +1453,7 @@ SendQuery(const char *query)
 				 * savepoint is gone. If they issued a SAVEPOINT, releasing
 				 * ours would remove theirs.
 				 */
-				if (result &&
-					(strcmp(PQcmdStatus(result), "COMMIT") == 0 ||
-					 strcmp(PQcmdStatus(result), "SAVEPOINT") == 0 ||
-					 strcmp(PQcmdStatus(result), "RELEASE") == 0 ||
-					 strcmp(PQcmdStatus(result), "ROLLBACK") == 0))
+				if (tx_ended)
 					svptcmd = NULL;
 				else
 					svptcmd = "RELEASE pg_psql_temporary_savepoint";
@@ -1373,14 +1475,15 @@ SendQuery(const char *query)
 			PGresult   *svptres;
 
 			svptres = PQexec(pset.db, svptcmd);
-			if (PQresultStatus(svptres) != PGRES_COMMAND_OK)
+			res_error = PQresultStatus(svptres) != PGRES_COMMAND_OK;
+
+			if (res_error)
 			{
 				pg_log_info("%s", PQerrorMessage(pset.db));
 				ClearOrSaveResult(svptres);
 				OK = false;
 
 				PQclear(result);
-				ResetCancelConn();
 				goto sendquery_cleanup;
 			}
 			PQclear(svptres);
@@ -1411,6 +1514,9 @@ SendQuery(const char *query)
 
 sendquery_cleanup:
 
+	/* global cancellation reset */
+	ResetCancelConn();
+
 	/* reset \g's output-to-filename trigger */
 	if (pset.gfname)
 	{
@@ -1491,7 +1597,7 @@ DescribeQuery(const char *query, double *elapsed_msec)
 	PQclear(result);
 
 	result = PQdescribePrepared(pset.db, "");
-	OK = AcceptResult(result) &&
+	OK = AcceptResult(result, true) &&
 		(PQresultStatus(result) == PGRES_COMMAND_OK);
 	if (OK && result)
 	{
@@ -1539,7 +1645,7 @@ DescribeQuery(const char *query, double *elapsed_msec)
 			PQclear(result);
 
 			result = PQexec(pset.db, buf.data);
-			OK = AcceptResult(result);
+			OK = AcceptResult(result, true);
 
 			if (timing)
 			{
@@ -1549,7 +1655,7 @@ DescribeQuery(const char *query, double *elapsed_msec)
 			}
 
 			if (OK && result)
-				OK = PrintQueryResult(result);
+				OK = HandleQueryResult(result, true, false, NULL, NULL);
 
 			termPQExpBuffer(&buf);
 		}
@@ -1609,7 +1715,7 @@ ExecQueryUsingCursor(const char *query, double *elapsed_msec)
 	if (PQtransactionStatus(pset.db) == PQTRANS_IDLE)
 	{
 		result = PQexec(pset.db, "BEGIN");
-		OK = AcceptResult(result) &&
+		OK = AcceptResult(result, true) &&
 			(PQresultStatus(result) == PGRES_COMMAND_OK);
 		ClearOrSaveResult(result);
 		if (!OK)
@@ -1623,7 +1729,7 @@ ExecQueryUsingCursor(const char *query, double *elapsed_msec)
 					  query);
 
 	result = PQexec(pset.db, buf.data);
-	OK = AcceptResult(result) &&
+	OK = AcceptResult(result, true) &&
 		(PQresultStatus(result) == PGRES_COMMAND_OK);
 	if (!OK)
 		SetResultVariables(result, OK);
@@ -1696,7 +1802,7 @@ ExecQueryUsingCursor(const char *query, double *elapsed_msec)
 				is_pager = false;
 			}
 
-			OK = AcceptResult(result);
+			OK = AcceptResult(result, true);
 			Assert(!OK);
 			SetResultVariables(result, OK);
 			ClearOrSaveResult(result);
@@ -1805,7 +1911,7 @@ cleanup:
 	result = PQexec(pset.db, "CLOSE _psql_cursor");
 	if (OK)
 	{
-		OK = AcceptResult(result) &&
+		OK = AcceptResult(result, true) &&
 			(PQresultStatus(result) == PGRES_COMMAND_OK);
 		ClearOrSaveResult(result);
 	}
@@ -1815,7 +1921,7 @@ cleanup:
 	if (started_txn)
 	{
 		result = PQexec(pset.db, OK ? "COMMIT" : "ROLLBACK");
-		OK &= AcceptResult(result) &&
+		OK &= AcceptResult(result, true) &&
 			(PQresultStatus(result) == PGRES_COMMAND_OK);
 		ClearOrSaveResult(result);
 	}
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index 56afa6817e..b3971bce64 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -413,6 +413,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"
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index 80dbea9efd..2399cffa3f 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -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;
 
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index be9dec749d..d08b15886a 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -203,6 +203,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);
 
@@ -1150,6 +1151,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)
 {
@@ -1251,6 +1258,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);
diff --git a/src/bin/psql/t/001_basic.pl b/src/bin/psql/t/001_basic.pl
index 44ecd05add..f58e3365a8 100644
--- a/src/bin/psql/t/001_basic.pl
+++ b/src/bin/psql/t/001_basic.pl
@@ -6,7 +6,7 @@ use warnings;
 
 use PostgreSQL::Test::Cluster;
 use PostgreSQL::Test::Utils;
-use Test::More;
+use Test::More tests => 41;
 
 program_help_ok('psql');
 program_version_ok('psql');
@@ -115,4 +115,20 @@ NOTIFY foo, 'bar';",
 	qr/^Asynchronous notification "foo" with payload "bar" received from server process with PID \d+\.$/,
 	'notification with payload');
 
+# Test voluntary crash
+my ($ret, $out, $err) = $node->psql(
+	'postgres',
+	"SELECT 'before' AS running;\n" .
+	"SELECT pg_terminate_backend(pg_backend_pid());\n" .
+	"SELECT 'AFTER' AS not_running;\n");
+
+is($ret, 2, "server stopped");
+like($out, qr/before/, "output before crash");
+ok($out !~ qr/AFTER/, "no output after crash");
+is($err, 'psql:<stdin>:2: FATAL:  terminating connection due to administrator command
+psql:<stdin>:2: server closed the connection unexpectedly
+	This probably means the server terminated abnormally
+	before or while processing the request.
+psql:<stdin>:2: fatal: connection to server was lost', "expected error message");
+
 done_testing();
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 6957567264..a532b0d694 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -4511,7 +4511,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"))
diff --git a/src/test/regress/expected/copyselect.out b/src/test/regress/expected/copyselect.out
index 72865fe1eb..bb9e026f91 100644
--- a/src/test/regress/expected/copyselect.out
+++ b/src/test/regress/expected/copyselect.out
@@ -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
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 6428ebc507..6f468355ab 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5290,3 +5290,130 @@ ERROR:  relation "notexists" does not exist
 LINE 1: SELECT * FROM notexists;
                       ^
 STATEMENT:  SELECT * FROM notexists;
+\set ECHO none
+ 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)
+
+ three 
+-------
+     3
+(1 row)
+
+NOTICE:  warn 3.5
+CONTEXT:  PL/pgSQL function warn(text) line 2 at RAISE
+ warn 
+------
+ t
+(1 row)
+
+:three 4
+ERROR:  syntax error at or near ";"
+LINE 1: SELECT 5 ; SELECT 6 + ; SELECT warn('6.5') ; SELECT 7 ;
+                              ^
+ eight 
+-------
+     8
+(1 row)
+
+ERROR:  division by zero
+ begin 
+-------
+ ok
+(1 row)
+
+Calvin
+Susie
+Hobbes
+ done 
+------
+ ok
+(1 row)
+
+NOTICE:  warn 1.5
+CONTEXT:  PL/pgSQL function warn(text) line 2 at RAISE
+ two 
+-----
+   2
+(1 row)
+
+# initial AUTOCOMMIT: on
+# AUTOCOMMIT: off
+   s   
+-------
+ hello
+ world
+(2 rows)
+
+# AUTOCOMMIT: on
+   s   
+-------
+ hello
+ world
+(2 rows)
+
+# final AUTOCOMMIT: on
+# initial ON_ERROR_ROLLBACK: off
+# ON_ERROR_ROLLBACK: on
+# AUTOCOMMIT: on
+ERROR:  type "no_such_type" does not exist
+LINE 1: CREATE TABLE bla(s NO_SUCH_TYPE);
+                           ^
+ERROR:  error oops!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+   s    
+--------
+ Calvin
+ Hobbes
+(2 rows)
+
+     show     
+--------------
+ before error
+(1 row)
+
+ERROR:  error boum!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+ERROR:  error bam!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+       s       
+---------------
+ Calvin
+ Hobbes
+ Miss Wormwood
+ Susie
+(4 rows)
+
+# AUTOCOMMIT: off
+ERROR:  error bad!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+ #mum 
+------
+    1
+(1 row)
+
+ERROR:  error bad!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+       s       
+---------------
+ Calvin
+ Dad
+ Hobbes
+ Miss Wormwood
+ Susie
+(5 rows)
+
+# final ON_ERROR_ROLLBACK: off
diff --git a/src/test/regress/expected/transactions.out b/src/test/regress/expected/transactions.out
index 599d511a67..a46fa5d48a 100644
--- a/src/test/regress/expected/transactions.out
+++ b/src/test/regress/expected/transactions.out
@@ -904,8 +904,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
@@ -920,6 +930,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 
@@ -939,8 +955,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;
@@ -967,22 +993,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
diff --git a/src/test/regress/sql/copyselect.sql b/src/test/regress/sql/copyselect.sql
index 1d98dad3c8..e32a4f8e38 100644
--- a/src/test/regress/sql/copyselect.sql
+++ b/src/test/regress/sql/copyselect.sql
@@ -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; -- 0 1
 1
 \.
 2
diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql
index 0f5287f77b..2b06789b5a 100644
--- a/src/test/regress/sql/psql.sql
+++ b/src/test/regress/sql/psql.sql
@@ -1316,3 +1316,125 @@ DROP TABLE oer_test;
 \set ECHO errors
 SELECT * FROM notexists;
 \set ECHO all
+
+\set ECHO none
+
+--
+-- 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);
+
+--
+-- autocommit
+--
+\echo '# initial AUTOCOMMIT:' :AUTOCOMMIT
+\set AUTOCOMMIT off
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+-- implicit BEGIN
+CREATE TABLE foo(s TEXT);
+ROLLBACK;
+CREATE TABLE foo(s TEXT);
+INSERT INTO foo(s) VALUES ('hello'), ('world');
+COMMIT;
+DROP TABLE foo;
+ROLLBACK;
+SELECT * FROM foo ORDER BY 1;
+DROP TABLE foo;
+COMMIT;
+\set AUTOCOMMIT on
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+-- explicit BEGIN
+BEGIN;
+CREATE TABLE foo(s TEXT);
+INSERT INTO foo(s) VALUES ('hello'), ('world');
+COMMIT;
+BEGIN;
+DROP TABLE foo;
+ROLLBACK;
+-- implicit transactions
+SELECT * FROM foo ORDER BY 1;
+DROP TABLE foo;
+\echo '# final AUTOCOMMIT:' :AUTOCOMMIT
+
+--
+-- test ON_ERROR_ROLLBACK
+--
+CREATE FUNCTION psql_error(msg TEXT) RETURNS BOOLEAN AS $$
+  BEGIN
+    RAISE EXCEPTION 'error %', msg;
+  END;
+$$ LANGUAGE plpgsql;
+\echo '# initial ON_ERROR_ROLLBACK:' :ON_ERROR_ROLLBACK
+\set ON_ERROR_ROLLBACK on
+\echo '# ON_ERROR_ROLLBACK:' :ON_ERROR_ROLLBACK
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+BEGIN;
+CREATE TABLE bla(s NO_SUCH_TYPE);         -- fails
+CREATE TABLE bla(s TEXT);                 -- succeeds
+SELECT psql_error('oops!');               -- fails
+INSERT INTO bla VALUES ('Calvin'), ('Hobbes');
+COMMIT;
+SELECT * FROM bla ORDER BY 1;
+BEGIN;
+INSERT INTO bla VALUES ('Susie');         -- succeeds
+-- combined queries
+INSERT INTO bla VALUES ('Rosalyn') \;     -- will rollback
+SELECT 'before error' AS show \;          -- will show!
+  SELECT psql_error('boum!') \;           -- failure
+  SELECT 'after error' AS noshow;         -- hidden by preceeding error
+INSERT INTO bla(s) VALUES ('Moe') \;      -- will rollback
+  SELECT psql_error('bam!');
+INSERT INTO bla VALUES ('Miss Wormwood'); -- succeeds
+COMMIT;
+SELECT * FROM bla ORDER BY 1;
+-- some with autocommit off
+\set AUTOCOMMIT off
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+-- implicit BEGIN
+INSERT INTO bla VALUES ('Dad');           -- succeeds
+SELECT psql_error('bad!');                -- implicit partial rollback
+INSERT INTO bla VALUES ('Mum') \;         -- will rollback
+SELECT COUNT(*) AS "#mum"
+FROM bla WHERE s = 'Mum' \;               -- but be counted here
+SELECT psql_error('bad!');                -- implicit partial rollback
+COMMIT;
+SELECT * FROM bla ORDER BY 1;
+-- reset
+\set AUTOCOMMIT on
+\set ON_ERROR_ROLLBACK off
+\echo '# final ON_ERROR_ROLLBACK:' :ON_ERROR_ROLLBACK
+DROP TABLE bla;
+DROP FUNCTION psql_error;
diff --git a/src/test/regress/sql/transactions.sql b/src/test/regress/sql/transactions.sql
index 0a716b506b..d71c3ce93e 100644
--- a/src/test/regress/sql/transactions.sql
+++ b/src/test/regress/sql/transactions.sql
@@ -506,7 +506,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:


^ permalink  raw  reply  [nested|flat] 14+ messages in thread

* Re: psql - add SHOW_ALL_RESULTS option
  2022-01-04 07:55 Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-01-08 18:32 ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-01-13 05:44   ` Re: psql - add SHOW_ALL_RESULTS option Andres Freund <[email protected]>
  2022-01-15 09:00     ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-03-04 08:50       ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  2022-03-04 13:48         ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-03-12 16:27           ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
@ 2022-03-17 14:51             ` Peter Eisentraut <[email protected]>
  2022-03-17 18:04               ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  0 siblings, 1 reply; 14+ messages in thread

From: Peter Eisentraut @ 2022-03-17 14:51 UTC (permalink / raw)
  To: Fabien COELHO <[email protected]>; +Cc: [email protected] <[email protected]>

On 12.03.22 17:27, Fabien COELHO wrote:
> 
>>> Are you planning to send a rebased patch for this commit fest?
>>
>> Argh, I did it in a reply in another thread:-( Attached v15.
>>
>> So as to help moves things forward, I'd suggest that we should not to 
>> care too much about corner case repetition of some error messages 
>> which are due to libpq internals, so I could remove the ugly buffer 
>> reset from the patch and have the repetition, and if/when the issue is 
>> fixed later in libpq then the repetition will be removed, fine! The 
>> issue is that we just expose the strange behavior of libpq, which is 
>> libpq to solve, not psql.
> 
> See attached v16 which removes the libpq workaround.

I suppose this depends on

https://www.postgresql.org/message-id/flat/ab4288f8-be5c-57fb-2400-e3e857f53e46%40enterprisedb.com

getting committed, because right now this makes the psql TAP tests fail 
because of the duplicate error message.

How should we handle that?


In this part of the patch, there seems to be part of a sentence missing:

+ * Marshal the COPY data.  Either subroutine will get the
+ * connection out of its COPY state, then
+ * once and report any error. Return whether all was ok.






^ permalink  raw  reply  [nested|flat] 14+ messages in thread

* Re: psql - add SHOW_ALL_RESULTS option
  2022-01-04 07:55 Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-01-08 18:32 ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-01-13 05:44   ` Re: psql - add SHOW_ALL_RESULTS option Andres Freund <[email protected]>
  2022-01-15 09:00     ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-03-04 08:50       ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  2022-03-04 13:48         ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-03-12 16:27           ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-03-17 14:51             ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
@ 2022-03-17 18:04               ` Fabien COELHO <[email protected]>
  2022-03-22 15:01                 ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  0 siblings, 1 reply; 14+ messages in thread

From: Fabien COELHO @ 2022-03-17 18:04 UTC (permalink / raw)
  To: Peter Eisentraut <[email protected]>; +Cc: [email protected] <[email protected]>


Hello Peter,

>> See attached v16 which removes the libpq workaround.
>
> I suppose this depends on
>
> https://www.postgresql.org/message-id/flat/ab4288f8-be5c-57fb-2400-e3e857f53e46%40enterprisedb.com
>
> getting committed, because right now this makes the psql TAP tests fail 
> because of the duplicate error message.
>
> How should we handle that?

Ok, it seems I got the patch wrong.

Attached v17 is another try. The point is to record the current status, 
whatever it is, buggy or not, and to update the test when libpq fixes 
things, whenever this is done.

> In this part of the patch, there seems to be part of a sentence missing:

Indeed! The missing part was put back in v17.

-- 
Fabien.

Attachments:

  [text/x-diff] psql-show-all-results-17.patch (48.3K, ../../alpine.DEB.2.22.394.2203171859410.1198186@pseudo/2-psql-show-all-results-17.patch)
  download | inline diff:
diff --git a/contrib/pg_stat_statements/expected/pg_stat_statements.out b/contrib/pg_stat_statements/expected/pg_stat_statements.out
index e0abe34bb6..8f7f93172a 100644
--- a/contrib/pg_stat_statements/expected/pg_stat_statements.out
+++ b/contrib/pg_stat_statements/expected/pg_stat_statements.out
@@ -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 
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index caabb06c53..f01adb1fd2 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -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>
@@ -3570,10 +3563,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>
@@ -4160,6 +4149,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>
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
index d65b9a124f..2f3dd91602 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -34,6 +34,8 @@ 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, const printQueryOpt *opt, FILE *printQueryFout, bool *tx_ended);
 
 
 /*
@@ -354,7 +356,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;
 
@@ -385,7 +387,7 @@ AcceptResult(const PGresult *result)
 				break;
 		}
 
-	if (!OK)
+	if (!OK && show_error)
 	{
 		const char *error = PQerrorMessage(pset.db);
 
@@ -473,6 +475,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
@@ -573,7 +587,7 @@ PSQLexec(const char *query)
 
 	ResetCancelConn();
 
-	if (!AcceptResult(res))
+	if (!AcceptResult(res, true))
 	{
 		ClearOrSaveResult(res);
 		res = NULL;
@@ -596,11 +610,8 @@ int
 PSQLexecWatch(const char *query, const printQueryOpt *opt, FILE *printQueryFout)
 {
 	bool		timing = pset.timing;
-	PGresult   *res;
 	double		elapsed_msec = 0;
-	instr_time	before;
-	instr_time	after;
-	FILE	   *fout;
+	int			res;
 
 	if (!pset.db)
 	{
@@ -609,77 +620,14 @@ PSQLexecWatch(const char *query, const printQueryOpt *opt, FILE *printQueryFout)
 	}
 
 	SetCancelConn(pset.db);
-
-	if (timing)
-		INSTR_TIME_SET_CURRENT(before);
-
-	res = PQexec(pset.db, query);
-
+	res = SendQueryAndProcessResults(query, &elapsed_msec, true, opt, printQueryFout, NULL);
 	ResetCancelConn();
 
-	if (!AcceptResult(res))
-	{
-		ClearOrSaveResult(res);
-		return 0;
-	}
-
-	if (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;
-	}
-
-	fout = printQueryFout ? printQueryFout : pset.queryFout;
-
-	switch (PQresultStatus(res))
-	{
-		case PGRES_TUPLES_OK:
-			printQuery(res, opt, fout, false, pset.logfile);
-			break;
-
-		case PGRES_COMMAND_OK:
-			fprintf(fout, "%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(fout);
-
 	/* Possible microtiming output */
 	if (timing)
 		PrintTiming(elapsed_msec);
 
-	return 1;
+	return res;
 }
 
 
@@ -714,7 +662,7 @@ PrintNotifications(void)
  * Returns true if successful, false otherwise.
  */
 static bool
-PrintQueryTuples(const PGresult *result)
+PrintQueryTuples(const PGresult *result, const printQueryOpt *opt, FILE *printQueryFout)
 {
 	bool		ok = true;
 
@@ -746,8 +694,9 @@ PrintQueryTuples(const PGresult *result)
 	}
 	else
 	{
-		printQuery(result, &pset.popt, pset.queryFout, false, pset.logfile);
-		if (ferror(pset.queryFout))
+		FILE *fout = printQueryFout ? printQueryFout : pset.queryFout;
+		printQuery(result, opt ? opt : &pset.popt, fout, false, pset.logfile);
+		if (ferror(fout))
 		{
 			pg_log_error("could not print result table: %m");
 			ok = false;
@@ -892,213 +841,131 @@ 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.
- *
- * When the command string contained no such COPY command, this function
- * degenerates to an AcceptResult() call.
- *
- * 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.
+ * 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.
+ *
+ * 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.
+ *
+ * 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 **resultp)
+HandleCopyResult(PGresult **resultp)
 {
-	bool		success = true;
-	bool		first_cycle = true;
+	bool		success;
+	FILE	   *copystream;
+	PGresult   *copy_result;
+	ExecStatusType result_status = PQresultStatus(*resultp);
 
-	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(*resultp))
+		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(*resultp);
-		switch (result_status)
+		else if (pset.gfname)
 		{
-			case PGRES_EMPTY_QUERY:
-			case PGRES_COMMAND_OK:
-			case PGRES_TUPLES_OK:
-				is_copy = false;
-				break;
-
-			case PGRES_COPY_OUT:
-			case PGRES_COPY_IN:
-				is_copy = true;
-				break;
-
-			default:
-				/* AcceptResult() should have caught anything else. */
-				is_copy = false;
-				pg_log_error("unexpected PQresultStatus: %d", result_status);
-				break;
-		}
-
-		if (is_copy)
-		{
-			/*
-			 * 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)
+			/* invoked by \g */
+			if (openQueryOutputFile(pset.gfname,
+									&copystream, &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);
-					}
-				}
+				need_close = true;
+				if (is_pipe)
+					disable_sigpipe_trap();
 			}
 			else
-			{
-				/* COPY IN */
-				copystream = pset.copyStream ? pset.copyStream : pset.cur_cmd_source;
-				success = handleCopyIn(pset.db,
-									   copystream,
-									   PQbinaryTuples(*resultp),
-									   &copy_result) && success;
-			}
-			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(*resultp);
-			*resultp = copy_result;
+				copystream = NULL;	/* discard COPY data entirely */
 		}
-		else if (first_cycle)
+		else
 		{
-			/* fast path: no COPY commands; PQexec visited all results */
-			break;
+			/* fall back to the generic query output stream */
+			copystream = pset.queryFout;
 		}
 
+		success = handleCopyOut(pset.db,
+								copystream,
+								&copy_result)
+			&& (copystream != NULL);
+
 		/*
-		 * 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.
+		 * 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.
 		 */
-		next_result = PQgetResult(pset.db);
-		if (!next_result)
-			break;
+		if (copystream == pset.queryFout)
+		{
+			PQclear(copy_result);
+			copy_result = NULL;
+		}
 
-		PQclear(*resultp);
-		*resultp = next_result;
-		first_cycle = false;
+		if (need_close)
+		{
+			/* close \g argument file/pipe */
+			if (is_pipe)
+			{
+				pclose(copystream);
+				restore_sigpipe_trap();
+			}
+			else
+			{
+				fclose(copystream);
+			}
+		}
+	}
+	else
+	{
+		/* COPY IN */
+		copystream = pset.copyStream ? pset.copyStream : pset.cur_cmd_source;
+		success = handleCopyIn(pset.db,
+							   copystream,
+							   PQbinaryTuples(*resultp),
+							   &copy_result);
 	}
 
-	SetResultVariables(*resultp, success);
-
-	/* may need this to recover from conn loss during COPY */
-	if (!first_cycle && !CheckConnection())
-		return false;
+	ResetCancelConn();
+	PQclear(*resultp);
+	*resultp = copy_result;
 
 	return success;
 }
 
-
 /*
  * PrintQueryStatus: report command status as required
  *
- * Note: Utility function for use by PrintQueryResult() only.
+ * Note: Utility function for use by HandleQueryResult() only.
  */
 static void
-PrintQueryStatus(PGresult *result)
+PrintQueryStatus(PGresult *result, FILE *printQueryFout)
 {
 	char		buf[16];
+	FILE	   *fout = printQueryFout ? printQueryFout : pset.queryFout;
 
 	if (!pset.quiet)
 	{
 		if (pset.popt.topt.format == PRINT_HTML)
 		{
-			fputs("<p>", pset.queryFout);
-			html_escaped_print(PQcmdStatus(result), pset.queryFout);
-			fputs("</p>\n", pset.queryFout);
+			fputs("<p>", fout);
+			html_escaped_print(PQcmdStatus(result), fout);
+			fputs("</p>\n", fout);
 		}
 		else
-			fprintf(pset.queryFout, "%s\n", PQcmdStatus(result));
+			fprintf(fout, "%s\n", PQcmdStatus(result));
 	}
 
 	if (pset.logfile)
@@ -1110,43 +977,50 @@ PrintQueryStatus(PGresult *result)
 
 
 /*
- * PrintQueryResult: print out (or store or execute) query result 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
-PrintQueryResult(PGresult *result)
+HandleQueryResult(PGresult *result, bool last, bool is_watch, const printQueryOpt *opt, FILE *printQueryFout)
 {
 	bool		success;
 	const char *cmdstatus;
 
-	if (!result)
+	if (result == NULL)
 		return false;
 
 	switch (PQresultStatus(result))
 	{
 		case PGRES_TUPLES_OK:
 			/* store or execute or print the data ... */
-			if (pset.gset_prefix)
+			if (last && pset.gset_prefix)
 				success = StoreQueryTuple(result);
-			else if (pset.gexec_flag)
+			else if (last && pset.gexec_flag)
 				success = ExecQueryTuples(result);
-			else if (pset.crosstab_flag)
+			else if (last && pset.crosstab_flag)
 				success = PrintResultInCrosstab(result);
+			else if (last || pset.show_all_results)
+				success = PrintQueryTuples(result, opt, printQueryFout);
 			else
-				success = PrintQueryTuples(result);
+				success = true;
+
 			/* if it's INSERT/UPDATE/DELETE RETURNING, also print status */
-			cmdstatus = PQcmdStatus(result);
-			if (strncmp(cmdstatus, "INSERT", 6) == 0 ||
-				strncmp(cmdstatus, "UPDATE", 6) == 0 ||
-				strncmp(cmdstatus, "DELETE", 6) == 0)
-				PrintQueryStatus(result);
+			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, printQueryFout);
+			}
+
 			break;
 
 		case PGRES_COMMAND_OK:
-			PrintQueryStatus(result);
+			if (last || pset.show_all_results)
+				PrintQueryStatus(result, printQueryFout);
 			success = true;
 			break;
 
@@ -1156,7 +1030,7 @@ PrintQueryResult(PGresult *result)
 
 		case PGRES_COPY_OUT:
 		case PGRES_COPY_IN:
-			/* nothing to do here */
+			/* nothing to do here: already processed */
 			success = true;
 			break;
 
@@ -1173,11 +1047,252 @@ PrintQueryResult(PGresult *result)
 			break;
 	}
 
-	fflush(pset.queryFout);
+	fflush(printQueryFout ? printQueryFout : pset.queryFout);
 
 	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 {
+	PQExpBufferData	messages[2];
+	int				current;
+} 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->messages[notes->current], msg);
+}
+
+/*
+ * Show notices stored in buffer, which is then reset.
+ */
+static void
+ShowNoticeMessage(t_notice_messages *notes)
+{
+	PQExpBufferData	*current = &notes->messages[notes->current];
+	if (*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, const printQueryOpt *opt, FILE *printQueryFout, bool *tx_ended)
+{
+	bool				timing = pset.timing;
+	bool				success;
+	instr_time			before;
+	PGresult		   *result;
+	t_notice_messages	notes;
+
+	if (timing)
+		INSTR_TIME_SET_CURRENT(before);
+
+	success = PQsendQuery(pset.db, query);
+
+	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.current = 0;
+	initPQExpBuffer(&notes.messages[0]);
+	initPQExpBuffer(&notes.messages[1]);
+	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);
+
+			/* keep the result status before clearing it */
+			result_status = PQresultStatus(result);
+			ClearOrSaveResult(result);
+			result = NULL;
+			success = false;
+
+			/* switch to next result */
+			if (result_status == PGRES_COPY_BOTH ||
+				result_status == PGRES_COPY_OUT ||
+				result_status == PGRES_COPY_IN)
+				/*
+				 * For some obscure reason PQgetResult does *not* return a NULL in copy
+				 * cases despite the result having been cleared, but keeps returning an
+				 * "empty" result that we have to ignore manually.
+				 */
+				;
+			else
+				result = PQgetResult(pset.db);
+
+			continue;
+		}
+		else if (tx_ended != NULL && ! *tx_ended)
+		{
+			/* on success, tell whether the "current" transaction sequence ended */
+			const char *cmd = PQcmdStatus(result);
+			*tx_ended = strcmp(cmd, "COMMIT") == 0 || strcmp(cmd, "ROLLBACK") == 0 ||
+				strcmp(cmd, "SAVEPOINT") == 0 || strcmp(cmd, "RELEASE") == 0;
+		}
+
+		result_status = PQresultStatus(result);
+
+		/* must handle COPY before changing the current 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. We need to do that to check whether this is the last.
+		 */
+		notes.current ^= 1;
+		next_result = PQgetResult(pset.db);
+		notes.current ^= 1;
+		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 && 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, false, opt, printQueryFout);
+
+		/* set variables on last result if all went well */
+		if (!is_watch && last && success)
+			SetResultVariables(result, true);
+
+		ClearOrSaveResult(result);
+		notes.current ^= 1;
+		result = next_result;
+
+		if (cancel_pressed)
+		{
+			ClearOrSaveAllResults();
+			break;
+		}
+	}
+
+	/* reset notice hook */
+	PQsetNoticeProcessor(pset.db, NoticeProcessor, NULL);
+	termPQExpBuffer(&notes.messages[0]);
+	termPQExpBuffer(&notes.messages[1]);
+
+	/* may need this to recover from conn loss during COPY */
+	if (!CheckConnection())
+		return -1;
+
+	return cancel_pressed ? 0 : success ? 1 : -1;
+}
+
 
 /*
  * SendQuery: send the query string to the backend
@@ -1195,12 +1310,14 @@ bool
 SendQuery(const char *query)
 {
 	bool		timing = pset.timing;
-	PGresult   *result;
+	PGresult    *result = NULL;
 	PGTransactionStatusType transaction_status;
 	double		elapsed_msec = 0;
 	bool		OK = false;
+	bool		res_error;
 	int			i;
 	bool		on_error_rollback_savepoint = false;
+	bool		tx_ended = false;
 
 	if (!pset.db)
 	{
@@ -1239,82 +1356,71 @@ SendQuery(const char *query)
 		fflush(pset.logfile);
 	}
 
+	/* global query cancellation for this query */
 	SetCancelConn(pset.db);
 
 	transaction_status = PQtransactionStatus(pset.db);
 
+	/* issue a BEGIN if needed, corresponding COMMIT/ROLLBACK by user */
 	if (transaction_status == PQTRANS_IDLE &&
 		!pset.autocommit &&
 		!command_no_begin(query))
 	{
+		PGresult    *result;
 		result = PQexec(pset.db, "BEGIN");
-		if (PQresultStatus(result) != PGRES_COMMAND_OK)
+		res_error = PQresultStatus(result) != PGRES_COMMAND_OK;
+		ClearOrSaveResult(result);
+		result = NULL;
+
+		if (res_error)
 		{
 			pg_log_info("%s", PQerrorMessage(pset.db));
-			ClearOrSaveResult(result);
-			ResetCancelConn();
 			goto sendquery_cleanup;
 		}
-		ClearOrSaveResult(result);
+
+		/* must be PQTRANS_INTRANS after a BEGIN */
 		transaction_status = PQtransactionStatus(pset.db);
 	}
 
+	/* create automatic savepoint if needed and possible */
 	if (transaction_status == PQTRANS_INTRANS &&
 		pset.on_error_rollback != PSQL_ERROR_ROLLBACK_OFF &&
 		(pset.cur_cmd_interactive ||
 		 pset.on_error_rollback == PSQL_ERROR_ROLLBACK_ON))
 	{
+		PGresult   *result;
 		result = PQexec(pset.db, "SAVEPOINT pg_psql_temporary_savepoint");
-		if (PQresultStatus(result) != PGRES_COMMAND_OK)
+		res_error = PQresultStatus(result) != PGRES_COMMAND_OK;
+		ClearOrSaveResult(result);
+		result = NULL;
+
+		if (res_error)
 		{
 			pg_log_info("%s", PQerrorMessage(pset.db));
-			ClearOrSaveResult(result);
-			ResetCancelConn();
 			goto sendquery_cleanup;
 		}
-		ClearOrSaveResult(result);
+
 		on_error_rollback_savepoint = true;
 	}
 
+	/* process the query, one way or the other */
 	if (pset.gdesc_flag)
 	{
 		/* Describe query's result columns, without executing it */
 		OK = DescribeQuery(query, &elapsed_msec);
-		ResetCancelConn();
 		result = NULL;			/* PQclear(NULL) does nothing */
 	}
 	else if (pset.fetch_count <= 0 || pset.gexec_flag ||
 			 pset.crosstab_flag || !is_select_command(query))
 	{
 		/* Default fetch-it-all-and-print mode */
-		instr_time	before,
-					after;
-
-		if (timing)
-			INSTR_TIME_SET_CURRENT(before);
-
-		result = PQexec(pset.db, query);
-
-		/* these operations are included in the timing result: */
-		ResetCancelConn();
-		OK = ProcessResult(&result);
-
-		if (timing)
-		{
-			INSTR_TIME_SET_CURRENT(after);
-			INSTR_TIME_SUBTRACT(after, before);
-			elapsed_msec = INSTR_TIME_GET_MILLISEC(after);
-		}
-
-		/* but printing result isn't: */
-		if (OK && result)
-			OK = PrintQueryResult(result);
+		int res = SendQueryAndProcessResults(query, &elapsed_msec, false, NULL, NULL, &tx_ended);
+		OK = (res >= 0);
 	}
 	else
 	{
 		/* Fetch-in-segments mode */
 		OK = ExecQueryUsingCursor(query, &elapsed_msec);
-		ResetCancelConn();
 		result = NULL;			/* PQclear(NULL) does nothing */
 	}
 
@@ -1347,11 +1453,7 @@ SendQuery(const char *query)
 				 * savepoint is gone. If they issued a SAVEPOINT, releasing
 				 * ours would remove theirs.
 				 */
-				if (result &&
-					(strcmp(PQcmdStatus(result), "COMMIT") == 0 ||
-					 strcmp(PQcmdStatus(result), "SAVEPOINT") == 0 ||
-					 strcmp(PQcmdStatus(result), "RELEASE") == 0 ||
-					 strcmp(PQcmdStatus(result), "ROLLBACK") == 0))
+				if (tx_ended)
 					svptcmd = NULL;
 				else
 					svptcmd = "RELEASE pg_psql_temporary_savepoint";
@@ -1373,14 +1475,15 @@ SendQuery(const char *query)
 			PGresult   *svptres;
 
 			svptres = PQexec(pset.db, svptcmd);
-			if (PQresultStatus(svptres) != PGRES_COMMAND_OK)
+			res_error = PQresultStatus(svptres) != PGRES_COMMAND_OK;
+
+			if (res_error)
 			{
 				pg_log_info("%s", PQerrorMessage(pset.db));
 				ClearOrSaveResult(svptres);
 				OK = false;
 
 				PQclear(result);
-				ResetCancelConn();
 				goto sendquery_cleanup;
 			}
 			PQclear(svptres);
@@ -1411,6 +1514,9 @@ SendQuery(const char *query)
 
 sendquery_cleanup:
 
+	/* global cancellation reset */
+	ResetCancelConn();
+
 	/* reset \g's output-to-filename trigger */
 	if (pset.gfname)
 	{
@@ -1491,7 +1597,7 @@ DescribeQuery(const char *query, double *elapsed_msec)
 	PQclear(result);
 
 	result = PQdescribePrepared(pset.db, "");
-	OK = AcceptResult(result) &&
+	OK = AcceptResult(result, true) &&
 		(PQresultStatus(result) == PGRES_COMMAND_OK);
 	if (OK && result)
 	{
@@ -1539,7 +1645,7 @@ DescribeQuery(const char *query, double *elapsed_msec)
 			PQclear(result);
 
 			result = PQexec(pset.db, buf.data);
-			OK = AcceptResult(result);
+			OK = AcceptResult(result, true);
 
 			if (timing)
 			{
@@ -1549,7 +1655,7 @@ DescribeQuery(const char *query, double *elapsed_msec)
 			}
 
 			if (OK && result)
-				OK = PrintQueryResult(result);
+				OK = HandleQueryResult(result, true, false, NULL, NULL);
 
 			termPQExpBuffer(&buf);
 		}
@@ -1609,7 +1715,7 @@ ExecQueryUsingCursor(const char *query, double *elapsed_msec)
 	if (PQtransactionStatus(pset.db) == PQTRANS_IDLE)
 	{
 		result = PQexec(pset.db, "BEGIN");
-		OK = AcceptResult(result) &&
+		OK = AcceptResult(result, true) &&
 			(PQresultStatus(result) == PGRES_COMMAND_OK);
 		ClearOrSaveResult(result);
 		if (!OK)
@@ -1623,7 +1729,7 @@ ExecQueryUsingCursor(const char *query, double *elapsed_msec)
 					  query);
 
 	result = PQexec(pset.db, buf.data);
-	OK = AcceptResult(result) &&
+	OK = AcceptResult(result, true) &&
 		(PQresultStatus(result) == PGRES_COMMAND_OK);
 	if (!OK)
 		SetResultVariables(result, OK);
@@ -1696,7 +1802,7 @@ ExecQueryUsingCursor(const char *query, double *elapsed_msec)
 				is_pager = false;
 			}
 
-			OK = AcceptResult(result);
+			OK = AcceptResult(result, true);
 			Assert(!OK);
 			SetResultVariables(result, OK);
 			ClearOrSaveResult(result);
@@ -1805,7 +1911,7 @@ cleanup:
 	result = PQexec(pset.db, "CLOSE _psql_cursor");
 	if (OK)
 	{
-		OK = AcceptResult(result) &&
+		OK = AcceptResult(result, true) &&
 			(PQresultStatus(result) == PGRES_COMMAND_OK);
 		ClearOrSaveResult(result);
 	}
@@ -1815,7 +1921,7 @@ cleanup:
 	if (started_txn)
 	{
 		result = PQexec(pset.db, OK ? "COMMIT" : "ROLLBACK");
-		OK &= AcceptResult(result) &&
+		OK &= AcceptResult(result, true) &&
 			(PQresultStatus(result) == PGRES_COMMAND_OK);
 		ClearOrSaveResult(result);
 	}
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index 56afa6817e..b3971bce64 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -413,6 +413,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"
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index 80dbea9efd..2399cffa3f 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -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;
 
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index be9dec749d..d08b15886a 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -203,6 +203,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);
 
@@ -1150,6 +1151,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)
 {
@@ -1251,6 +1258,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);
diff --git a/src/bin/psql/t/001_basic.pl b/src/bin/psql/t/001_basic.pl
index 44ecd05add..339357df0b 100644
--- a/src/bin/psql/t/001_basic.pl
+++ b/src/bin/psql/t/001_basic.pl
@@ -6,7 +6,7 @@ use warnings;
 
 use PostgreSQL::Test::Cluster;
 use PostgreSQL::Test::Utils;
-use Test::More;
+use Test::More tests => 41;
 
 program_help_ok('psql');
 program_version_ok('psql');
@@ -115,4 +115,21 @@ NOTIFY foo, 'bar';",
 	qr/^Asynchronous notification "foo" with payload "bar" received from server process with PID \d+\.$/,
 	'notification with payload');
 
+# Test voluntary crash
+my ($ret, $out, $err) = $node->psql(
+	'postgres',
+	"SELECT 'before' AS running;\n" .
+	"SELECT pg_terminate_backend(pg_backend_pid());\n" .
+	"SELECT 'AFTER' AS not_running;\n");
+
+is($ret, 2, "server stopped");
+like($out, qr/before/, "output before crash");
+ok($out !~ qr/AFTER/, "no output after crash");
+is($err, 'psql:<stdin>:2: FATAL:  terminating connection due to administrator command
+psql:<stdin>:2: FATAL:  terminating connection due to administrator command
+server closed the connection unexpectedly
+	This probably means the server terminated abnormally
+	before or while processing the request.
+psql:<stdin>:2: fatal: connection to server was lost', "expected error message");
+
 done_testing();
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 380cbc0b1f..ba7fb80ab5 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -4512,7 +4512,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"))
diff --git a/src/test/regress/expected/copyselect.out b/src/test/regress/expected/copyselect.out
index 72865fe1eb..bb9e026f91 100644
--- a/src/test/regress/expected/copyselect.out
+++ b/src/test/regress/expected/copyselect.out
@@ -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
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 6428ebc507..6f468355ab 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5290,3 +5290,130 @@ ERROR:  relation "notexists" does not exist
 LINE 1: SELECT * FROM notexists;
                       ^
 STATEMENT:  SELECT * FROM notexists;
+\set ECHO none
+ 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)
+
+ three 
+-------
+     3
+(1 row)
+
+NOTICE:  warn 3.5
+CONTEXT:  PL/pgSQL function warn(text) line 2 at RAISE
+ warn 
+------
+ t
+(1 row)
+
+:three 4
+ERROR:  syntax error at or near ";"
+LINE 1: SELECT 5 ; SELECT 6 + ; SELECT warn('6.5') ; SELECT 7 ;
+                              ^
+ eight 
+-------
+     8
+(1 row)
+
+ERROR:  division by zero
+ begin 
+-------
+ ok
+(1 row)
+
+Calvin
+Susie
+Hobbes
+ done 
+------
+ ok
+(1 row)
+
+NOTICE:  warn 1.5
+CONTEXT:  PL/pgSQL function warn(text) line 2 at RAISE
+ two 
+-----
+   2
+(1 row)
+
+# initial AUTOCOMMIT: on
+# AUTOCOMMIT: off
+   s   
+-------
+ hello
+ world
+(2 rows)
+
+# AUTOCOMMIT: on
+   s   
+-------
+ hello
+ world
+(2 rows)
+
+# final AUTOCOMMIT: on
+# initial ON_ERROR_ROLLBACK: off
+# ON_ERROR_ROLLBACK: on
+# AUTOCOMMIT: on
+ERROR:  type "no_such_type" does not exist
+LINE 1: CREATE TABLE bla(s NO_SUCH_TYPE);
+                           ^
+ERROR:  error oops!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+   s    
+--------
+ Calvin
+ Hobbes
+(2 rows)
+
+     show     
+--------------
+ before error
+(1 row)
+
+ERROR:  error boum!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+ERROR:  error bam!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+       s       
+---------------
+ Calvin
+ Hobbes
+ Miss Wormwood
+ Susie
+(4 rows)
+
+# AUTOCOMMIT: off
+ERROR:  error bad!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+ #mum 
+------
+    1
+(1 row)
+
+ERROR:  error bad!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+       s       
+---------------
+ Calvin
+ Dad
+ Hobbes
+ Miss Wormwood
+ Susie
+(5 rows)
+
+# final ON_ERROR_ROLLBACK: off
diff --git a/src/test/regress/expected/transactions.out b/src/test/regress/expected/transactions.out
index 599d511a67..a46fa5d48a 100644
--- a/src/test/regress/expected/transactions.out
+++ b/src/test/regress/expected/transactions.out
@@ -904,8 +904,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
@@ -920,6 +930,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 
@@ -939,8 +955,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;
@@ -967,22 +993,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
diff --git a/src/test/regress/sql/copyselect.sql b/src/test/regress/sql/copyselect.sql
index 1d98dad3c8..e32a4f8e38 100644
--- a/src/test/regress/sql/copyselect.sql
+++ b/src/test/regress/sql/copyselect.sql
@@ -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; -- 0 1
 1
 \.
 2
diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql
index 0f5287f77b..2b06789b5a 100644
--- a/src/test/regress/sql/psql.sql
+++ b/src/test/regress/sql/psql.sql
@@ -1316,3 +1316,125 @@ DROP TABLE oer_test;
 \set ECHO errors
 SELECT * FROM notexists;
 \set ECHO all
+
+\set ECHO none
+
+--
+-- 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);
+
+--
+-- autocommit
+--
+\echo '# initial AUTOCOMMIT:' :AUTOCOMMIT
+\set AUTOCOMMIT off
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+-- implicit BEGIN
+CREATE TABLE foo(s TEXT);
+ROLLBACK;
+CREATE TABLE foo(s TEXT);
+INSERT INTO foo(s) VALUES ('hello'), ('world');
+COMMIT;
+DROP TABLE foo;
+ROLLBACK;
+SELECT * FROM foo ORDER BY 1;
+DROP TABLE foo;
+COMMIT;
+\set AUTOCOMMIT on
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+-- explicit BEGIN
+BEGIN;
+CREATE TABLE foo(s TEXT);
+INSERT INTO foo(s) VALUES ('hello'), ('world');
+COMMIT;
+BEGIN;
+DROP TABLE foo;
+ROLLBACK;
+-- implicit transactions
+SELECT * FROM foo ORDER BY 1;
+DROP TABLE foo;
+\echo '# final AUTOCOMMIT:' :AUTOCOMMIT
+
+--
+-- test ON_ERROR_ROLLBACK
+--
+CREATE FUNCTION psql_error(msg TEXT) RETURNS BOOLEAN AS $$
+  BEGIN
+    RAISE EXCEPTION 'error %', msg;
+  END;
+$$ LANGUAGE plpgsql;
+\echo '# initial ON_ERROR_ROLLBACK:' :ON_ERROR_ROLLBACK
+\set ON_ERROR_ROLLBACK on
+\echo '# ON_ERROR_ROLLBACK:' :ON_ERROR_ROLLBACK
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+BEGIN;
+CREATE TABLE bla(s NO_SUCH_TYPE);         -- fails
+CREATE TABLE bla(s TEXT);                 -- succeeds
+SELECT psql_error('oops!');               -- fails
+INSERT INTO bla VALUES ('Calvin'), ('Hobbes');
+COMMIT;
+SELECT * FROM bla ORDER BY 1;
+BEGIN;
+INSERT INTO bla VALUES ('Susie');         -- succeeds
+-- combined queries
+INSERT INTO bla VALUES ('Rosalyn') \;     -- will rollback
+SELECT 'before error' AS show \;          -- will show!
+  SELECT psql_error('boum!') \;           -- failure
+  SELECT 'after error' AS noshow;         -- hidden by preceeding error
+INSERT INTO bla(s) VALUES ('Moe') \;      -- will rollback
+  SELECT psql_error('bam!');
+INSERT INTO bla VALUES ('Miss Wormwood'); -- succeeds
+COMMIT;
+SELECT * FROM bla ORDER BY 1;
+-- some with autocommit off
+\set AUTOCOMMIT off
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+-- implicit BEGIN
+INSERT INTO bla VALUES ('Dad');           -- succeeds
+SELECT psql_error('bad!');                -- implicit partial rollback
+INSERT INTO bla VALUES ('Mum') \;         -- will rollback
+SELECT COUNT(*) AS "#mum"
+FROM bla WHERE s = 'Mum' \;               -- but be counted here
+SELECT psql_error('bad!');                -- implicit partial rollback
+COMMIT;
+SELECT * FROM bla ORDER BY 1;
+-- reset
+\set AUTOCOMMIT on
+\set ON_ERROR_ROLLBACK off
+\echo '# final ON_ERROR_ROLLBACK:' :ON_ERROR_ROLLBACK
+DROP TABLE bla;
+DROP FUNCTION psql_error;
diff --git a/src/test/regress/sql/transactions.sql b/src/test/regress/sql/transactions.sql
index 0a716b506b..d71c3ce93e 100644
--- a/src/test/regress/sql/transactions.sql
+++ b/src/test/regress/sql/transactions.sql
@@ -506,7 +506,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:


^ permalink  raw  reply  [nested|flat] 14+ messages in thread

* Re: psql - add SHOW_ALL_RESULTS option
  2022-01-04 07:55 Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-01-08 18:32 ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-01-13 05:44   ` Re: psql - add SHOW_ALL_RESULTS option Andres Freund <[email protected]>
  2022-01-15 09:00     ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-03-04 08:50       ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  2022-03-04 13:48         ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-03-12 16:27           ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-03-17 14:51             ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  2022-03-17 18:04               ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
@ 2022-03-22 15:01                 ` Peter Eisentraut <[email protected]>
  0 siblings, 0 replies; 14+ messages in thread

From: Peter Eisentraut @ 2022-03-22 15:01 UTC (permalink / raw)
  To: Fabien COELHO <[email protected]>; +Cc: [email protected] <[email protected]>

On 17.03.22 19:04, Fabien COELHO wrote:
> Indeed! The missing part was put back in v17.

Some unrelated notes on this v17 patch:

-use Test::More;
+use Test::More tests => 41;

The test counts are not needed/wanted anymore.


+
+\set ECHO none
+

This seems inappropriate.


+--
+-- autocommit
+--

+--
+-- test ON_ERROR_ROLLBACK
+--

This test file already contains tests for autocommit and 
ON_ERROR_ROLLBACK.  If you want to change those, please add yours into 
the existing sections, not make new ones.  I'm not sure if your tests 
add any new coverage, or if it is just duplicate.






^ permalink  raw  reply  [nested|flat] 14+ messages in thread


end of thread, other threads:[~2022-03-22 15:01 UTC | newest]

Thread overview: 14+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2020-10-07 03:11 [PATCH v7 3/7] Propagate changes to indisclustered to child/parents Justin Pryzby <[email protected]>
2022-01-04 07:55 Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
2022-01-08 18:32 ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
2022-01-13 05:44   ` Re: psql - add SHOW_ALL_RESULTS option Andres Freund <[email protected]>
2022-01-15 09:00     ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
2022-01-18 16:36       ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
2022-01-23 17:17         ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
2022-01-27 13:30           ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
2022-03-04 08:50       ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
2022-03-04 13:48         ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
2022-03-12 16:27           ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
2022-03-17 14:51             ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
2022-03-17 18:04               ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
2022-03-22 15:01                 ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox