public inbox for [email protected]  
help / color / mirror / Atom feed
[PATCH v34 2/5] Subscripting for array
14+ messages / 4 participants
[nested] [flat]

* [PATCH v34 2/5] Subscripting for array
@ 2019-02-01 10:38 erthalion <[email protected]>
  0 siblings, 0 replies; 14+ messages in thread

From: erthalion @ 2019-02-01 10:38 UTC (permalink / raw)

Subscripting implementation for array data types. It includes all array
specific parts, that were removed from the generalized code. Note, that
for some array-like data types it's not necessary to assign
array_subscript_handler explicitely, since it's done in Catalog.pm

Reviewed-by: Tom Lane, Arthur Zakirov
---
 src/backend/catalog/Catalog.pm        |   1 +
 src/backend/catalog/heap.c            |   2 +-
 src/backend/commands/typecmds.c       |   8 +-
 src/backend/executor/execExprInterp.c |  15 +-
 src/backend/nodes/nodeFuncs.c         |   2 +-
 src/backend/parser/parse_node.c       |  11 -
 src/backend/parser/parse_target.c     |   4 +-
 src/backend/utils/adt/arrayfuncs.c    | 289 ++++++++++++++++++++++++++
 src/include/catalog/pg_proc.dat       |   7 +
 src/include/catalog/pg_type.dat       |  30 ++-
 src/test/regress/expected/arrays.out  |  12 +-
 src/test/regress/sql/arrays.sql       |   4 +-
 12 files changed, 346 insertions(+), 39 deletions(-)

diff --git a/src/backend/catalog/Catalog.pm b/src/backend/catalog/Catalog.pm
index dd39a086ce..b4dfa26518 100644
--- a/src/backend/catalog/Catalog.pm
+++ b/src/backend/catalog/Catalog.pm
@@ -384,6 +384,7 @@ sub GenerateArrayTypes
 		# Arrays require INT alignment, unless the element type requires
 		# DOUBLE alignment.
 		$array_type{typalign} = $elem_type->{typalign} eq 'd' ? 'd' : 'i';
+		$array_type{typsubshandler} = 'array_subscript_handler';
 
 		# Fill in the rest of the array entry's fields.
 		foreach my $column (@$pgtype_schema)
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 4d552589ae..4703d8076b 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -1372,7 +1372,7 @@ heap_create_with_catalog(const char *relname,
 				   0,			/* array dimensions for typBaseType */
 				   false,		/* Type NOT NULL */
 				   InvalidOid,  /* rowtypes never have a collation */
-				   0);	/* array implementation */
+				   F_ARRAY_SUBSCRIPT_HANDLER);	/* array implementation */
 
 		pfree(relarrayname);
 	}
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 33d4fb401d..596c6cf3ca 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -618,7 +618,7 @@ DefineType(ParseState *pstate, List *names, List *parameters)
 			   0,				/* Array dimensions of typbasetype */
 			   false,			/* Type NOT NULL */
 			   collation,		/* type's collation */
-			   0);
+			   F_ARRAY_SUBSCRIPT_HANDLER);
 
 	pfree(array_type);
 
@@ -1065,7 +1065,7 @@ DefineDomain(CreateDomainStmt *stmt)
 			   0,				/* Array dimensions of typbasetype */
 			   false,			/* Type NOT NULL */
 			   domaincoll,		/* type's collation */
-			   0); /* array subscripting implementation */
+			   F_ARRAY_SUBSCRIPT_HANDLER); /* array subscripting implementation */
 
 	pfree(domainArrayName);
 
@@ -1222,7 +1222,7 @@ DefineEnum(CreateEnumStmt *stmt)
 			   0,				/* Array dimensions of typbasetype */
 			   false,			/* Type NOT NULL */
 			   InvalidOid,		/* type's collation */
-			   0);	/* array subscripting implementation */
+			   F_ARRAY_SUBSCRIPT_HANDLER);	/* array subscripting implementation */
 
 	pfree(enumArrayName);
 
@@ -1555,7 +1555,7 @@ DefineRange(CreateRangeStmt *stmt)
 			   0,				/* Array dimensions of typbasetype */
 			   false,			/* Type NOT NULL */
 			   InvalidOid,		/* typcollation */
-			   0);	/* array subscripting implementation */
+			   F_ARRAY_SUBSCRIPT_HANDLER);	/* array subscripting implementation */
 
 	pfree(rangeArrayName);
 
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index 838bb4d005..b87daf65e0 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -3155,7 +3155,7 @@ ExecEvalSubscriptingRef(ExprState *state, ExprEvalStep *op)
 		if (sbsrefstate->isassignment)
 			ereport(ERROR,
 					(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
-					 errmsg("array subscript in assignment must not be null")));
+					 errmsg("subscript in assignment must not be null")));
 		*op->resnull = true;
 		return false;
 	}
@@ -3227,9 +3227,20 @@ ExecEvalSubscriptingRefOld(ExprState *state, ExprEvalStep *op)
 void
 ExecEvalSubscriptingRefAssign(ExprState *state, ExprEvalStep *op)
 {
-	SubscriptingRefState *sbsrefstate = op->d.sbsref_subscript.state;
+	SubscriptingRefState *sbsrefstate = op->d.sbsref.state;
 	SubscriptRoutines	 *sbsroutines = sbsrefstate->sbsroutines;
 
+	/*
+	 * For an assignment to a fixed-length container type, both the original
+	 * container and the value to be assigned into it must be non-NULL, else we
+	 * punt and return the original container.
+	 */
+	if (sbsrefstate->refattrlength > 0)
+	{
+		if (*op->resnull || sbsrefstate->replacenull)
+			return;
+	}
+
 	sbsrefstate->resnull = *op->resnull;
 	*op->resvalue = sbsroutines->assign(*op->resvalue, sbsrefstate);
 	*op->resnull = sbsrefstate->resnull;
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 9ce8f43385..0fd9d8b110 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -70,7 +70,7 @@ exprType(const Node *expr)
 				const SubscriptingRef *sbsref = (const SubscriptingRef *) expr;
 
 				/* slice and/or store operations yield the container type */
-				if (sbsref->reflowerindexpr || sbsref->refassgnexpr)
+				if (IsAssignment(sbsref) || sbsref->reflowerindexpr)
 					type = sbsref->refcontainertype;
 				else
 					type = sbsref->refelemtype;
diff --git a/src/backend/parser/parse_node.c b/src/backend/parser/parse_node.c
index 4f46d6310a..d1c4ea8573 100644
--- a/src/backend/parser/parse_node.c
+++ b/src/backend/parser/parse_node.c
@@ -309,17 +309,6 @@ transformContainerSubscripts(ParseState *pstate,
 			lowerIndexpr = lappend(lowerIndexpr, subexpr);
 			indexprSlice = lappend(indexprSlice, ai);
 		}
-		else
-			Assert(ai->lidx == NULL && !ai->is_slice);
-
-		if (ai->uidx)
-			subexpr = transformExpr(pstate, ai->uidx, pstate->p_expr_kind);
-		else
-		{
-			/* Slice with omitted upper bound, put NULL into the list */
-			Assert(isSlice && ai->is_slice);
-			subexpr = NULL;
-		}
 		subexpr = transformExpr(pstate, ai->uidx, pstate->p_expr_kind);
 		upperIndexpr = lappend(upperIndexpr, subexpr);
 	}
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index d7483c6538..0161d0f540 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -685,7 +685,7 @@ transformAssignmentIndirection(ParseState *pstate,
 							   Node *rhs,
 							   int location)
 {
-	Node	   *result = NULL;
+	Node	   *result;
 	List	   *subscripts = NIL;
 	bool		isSlice = false;
 	ListCell   *i;
@@ -873,6 +873,8 @@ transformAssignmentIndirection(ParseState *pstate,
 					 errhint("You will need to rewrite or cast the expression."),
 					 parser_errposition(pstate, location)));
 	}
+	else
+		result = rhs;
 
 	return result;
 }
diff --git a/src/backend/utils/adt/arrayfuncs.c b/src/backend/utils/adt/arrayfuncs.c
index 392445ea03..21bd5882b0 100644
--- a/src/backend/utils/adt/arrayfuncs.c
+++ b/src/backend/utils/adt/arrayfuncs.c
@@ -25,14 +25,20 @@
 #include "nodes/supportnodes.h"
 #include "optimizer/optimizer.h"
 #include "port/pg_bitutils.h"
+#include "nodes/makefuncs.h"
+#include "executor/execExpr.h"
 #include "utils/array.h"
 #include "utils/arrayaccess.h"
 #include "utils/builtins.h"
 #include "utils/datum.h"
+#include "utils/fmgroids.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/selfuncs.h"
+#include "utils/syscache.h"
 #include "utils/typcache.h"
+#include "parser/parse_node.h"
+#include "parser/parse_coerce.h"
 
 
 /*
@@ -159,7 +165,14 @@ static int	width_bucket_array_variable(Datum operand,
 										ArrayType *thresholds,
 										Oid collation,
 										TypeCacheEntry *typentry);
+static SubscriptingRef *array_subscript_prepare(bool isAssignment, SubscriptingRef *sbsref);
+static SubscriptingRef *array_subscript_validate(bool isAssignment, SubscriptingRef *sbsref,
+												 ParseState *pstate);
 
+static Datum array_subscript_fetch(Datum containerSource,
+								   SubscriptingRefState *sbstate);
+static Datum array_subscript_assign(Datum containerSource,
+									SubscriptingRefState *sbstate);
 
 /*
  * array_in :
@@ -6626,3 +6639,279 @@ width_bucket_array_variable(Datum operand,
 
 	return left;
 }
+
+/*
+ * Perform an actual data extraction or modification for the array
+ * subscripting. As a result the extracted Datum or the modified containers
+ * value will be returned.
+ */
+Datum
+array_subscript_assign(Datum containerSource, SubscriptingRefState *sbstate)
+{
+	bool						is_slice = (sbstate->numlower != 0);
+	IntArray					u_index, l_index;
+	bool						eisnull = sbstate->resnull;
+	int							i = 0;
+
+	if (sbstate->refelemlength == 0)
+	{
+		/* do one-time catalog lookups for type info */
+		get_typlenbyvalalign(sbstate->refelemtype,
+							 &sbstate->refelemlength,
+							 &sbstate->refelembyval,
+							 &sbstate->refelemalign);
+	}
+
+	for(i = 0; i < sbstate->numupper; i++)
+		u_index.indx[i] = DatumGetInt32(sbstate->upperindex[i]);
+
+	if (is_slice)
+	{
+		for(i = 0; i < sbstate->numlower; i++)
+			l_index.indx[i] = DatumGetInt32(sbstate->lowerindex[i]);
+	}
+
+	/*
+	 * For assignment to varlena arrays, we handle a NULL original array
+	 * by substituting an empty (zero-dimensional) array; insertion of the
+	 * new element will result in a singleton array value.  It does not
+	 * matter whether the new element is NULL.
+	 */
+	if (eisnull)
+	{
+		containerSource = PointerGetDatum(construct_empty_array(sbstate->refelemtype));
+		sbstate->resnull = false;
+		eisnull = false;
+	}
+
+	if (!is_slice)
+		return array_set_element(containerSource, sbstate->numupper,
+								 u_index.indx,
+								 sbstate->replacevalue,
+								 sbstate->replacenull,
+								 sbstate->refattrlength,
+								 sbstate->refelemlength,
+								 sbstate->refelembyval,
+								 sbstate->refelemalign);
+	else
+		return array_set_slice(containerSource, sbstate->numupper,
+							   u_index.indx, l_index.indx,
+							   sbstate->upperprovided,
+							   sbstate->lowerprovided,
+							   sbstate->replacevalue,
+							   sbstate->replacenull,
+							   sbstate->refattrlength,
+							   sbstate->refelemlength,
+							   sbstate->refelembyval,
+							   sbstate->refelemalign);
+}
+
+Datum
+array_subscript_fetch(Datum containerSource, SubscriptingRefState *sbstate)
+{
+	bool							is_slice = (sbstate->numlower != 0);
+	IntArray						u_index, l_index;
+	int								i = 0;
+
+	if (sbstate->refelemlength == 0)
+	{
+		/* do one-time catalog lookups for type info */
+		get_typlenbyvalalign(sbstate->refelemtype,
+							 &sbstate->refelemlength,
+							 &sbstate->refelembyval,
+							 &sbstate->refelemalign);
+	}
+
+	for(i = 0; i < sbstate->numupper; i++)
+		u_index.indx[i] = DatumGetInt32(sbstate->upperindex[i]);
+
+	if (is_slice)
+	{
+		for(i = 0; i < sbstate->numlower; i++)
+			l_index.indx[i] = DatumGetInt32(sbstate->lowerindex[i]);
+	}
+
+	if (!is_slice)
+		return array_get_element(containerSource, sbstate->numupper,
+								 u_index.indx,
+								 sbstate->refattrlength,
+								 sbstate->refelemlength,
+								 sbstate->refelembyval,
+								 sbstate->refelemalign,
+								 &sbstate->resnull);
+	else
+		return array_get_slice(containerSource, sbstate->numupper,
+							   u_index.indx, l_index.indx,
+							   sbstate->upperprovided,
+							   sbstate->lowerprovided,
+							   sbstate->refattrlength,
+							   sbstate->refelemlength,
+							   sbstate->refelembyval,
+							   sbstate->refelemalign);
+}
+
+/*
+ * Handle array-type subscripting logic.
+ */
+Datum
+array_subscript_handler(PG_FUNCTION_ARGS)
+{
+	SubscriptRoutines *sbsroutines = (SubscriptRoutines *)
+									 palloc(sizeof(SubscriptRoutines));
+
+	sbsroutines->prepare = array_subscript_prepare;
+	sbsroutines->validate = array_subscript_validate;
+	sbsroutines->fetch = array_subscript_fetch;
+	sbsroutines->assign = array_subscript_assign;
+
+	PG_RETURN_POINTER(sbsroutines);
+}
+
+SubscriptingRef *
+array_subscript_prepare(bool isAssignment, SubscriptingRef *sbsref)
+{
+	Oid					array_type = sbsref->refcontainertype;
+	HeapTuple			type_tuple_container;
+	Form_pg_type		type_struct_container;
+	bool				is_slice = sbsref->reflowerindexpr != NIL;
+
+	/* Get the type tuple for the container */
+	type_tuple_container = SearchSysCache1(TYPEOID, ObjectIdGetDatum(array_type));
+	if (!HeapTupleIsValid(type_tuple_container))
+		elog(ERROR, "cache lookup failed for type %u", array_type);
+	type_struct_container = (Form_pg_type) GETSTRUCT(type_tuple_container);
+
+	/* needn't check typisdefined since this will fail anyway */
+	sbsref->refelemtype = type_struct_container->typelem;
+
+	/* Identify type that RHS must provide */
+	if (isAssignment)
+		sbsref->refassgntype = is_slice ? sbsref->refcontainertype : sbsref->refelemtype;
+
+	ReleaseSysCache(type_tuple_container);
+
+	return sbsref;
+}
+
+SubscriptingRef *
+array_subscript_validate(bool isAssignment, SubscriptingRef *sbsref,
+					  ParseState *pstate)
+{
+	bool				is_slice = sbsref->reflowerindexpr != NIL;
+	Oid					typeneeded = InvalidOid,
+						typesource = InvalidOid;
+	Node				*new_from;
+	Node				*subexpr;
+	List				*upperIndexpr = NIL;
+	List				*lowerIndexpr = NIL;
+	ListCell			*u, *l, *s;
+
+	foreach(u, sbsref->refupperindexpr)
+	{
+		subexpr = (Node *) lfirst(u);
+
+		if (subexpr == NULL)
+		{
+			upperIndexpr = lappend(upperIndexpr, subexpr);
+			continue;
+		}
+
+		subexpr = coerce_to_target_type(pstate,
+										subexpr, exprType(subexpr),
+										INT4OID, -1,
+										COERCION_ASSIGNMENT,
+										COERCE_IMPLICIT_CAST,
+										-1);
+		if (subexpr == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_DATATYPE_MISMATCH),
+					 errmsg("array subscript must have type integer"),
+					 parser_errposition(pstate, exprLocation(subexpr))));
+
+		upperIndexpr = lappend(upperIndexpr, subexpr);
+	}
+
+	sbsref->refupperindexpr = upperIndexpr;
+
+	forboth(l, sbsref->reflowerindexpr, s, sbsref->refindexprslice)
+	{
+		A_Indices *ai = (A_Indices *) lfirst(s);
+		subexpr = (Node *) lfirst(l);
+
+		if (subexpr == NULL && !ai->is_slice)
+		{
+			/* Make a constant 1 */
+			subexpr = (Node *) makeConst(INT4OID,
+										 -1,
+										 InvalidOid,
+										 sizeof(int32),
+										 Int32GetDatum(1),
+										 false,
+										 true);		/* pass by value */
+		}
+
+		if (subexpr == NULL)
+		{
+			lowerIndexpr = lappend(lowerIndexpr, subexpr);
+			continue;
+		}
+
+		subexpr = coerce_to_target_type(pstate,
+										subexpr, exprType(subexpr),
+										INT4OID, -1,
+										COERCION_ASSIGNMENT,
+										COERCE_IMPLICIT_CAST,
+										-1);
+		if (subexpr == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_DATATYPE_MISMATCH),
+					 errmsg("array subscript must have type integer"),
+					 parser_errposition(pstate, exprLocation(subexpr))));
+
+		lowerIndexpr = lappend(lowerIndexpr, subexpr);
+	}
+
+	sbsref->reflowerindexpr = lowerIndexpr;
+
+	if (isAssignment)
+	{
+		SubscriptingRef *assignRef = (SubscriptingRef *) sbsref;
+		Node *assignExpr = (Node *) assignRef->refassgnexpr;
+
+		typesource = exprType(assignExpr);
+		typeneeded = is_slice ? sbsref->refcontainertype : sbsref->refelemtype;
+		new_from = coerce_to_target_type(pstate,
+										assignExpr, typesource,
+										typeneeded, sbsref->reftypmod,
+										COERCION_ASSIGNMENT,
+										COERCE_IMPLICIT_CAST,
+										-1);
+		if (new_from == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_DATATYPE_MISMATCH),
+					 errmsg("array assignment requires type %s"
+							" but expression is of type %s",
+							format_type_be(sbsref->refelemtype),
+							format_type_be(typesource)),
+				 errhint("You will need to rewrite or cast the expression."),
+					 parser_errposition(pstate, exprLocation(assignExpr))));
+		assignRef->refassgnexpr = (Expr *) new_from;
+	}
+
+	sbsref->refnestedfunc = F_ARRAY_SUBSCRIPT_HANDLER;
+
+	/* Verify subscript list lengths are within limit */
+	if (list_length(sbsref->refupperindexpr) > MAXDIM)
+		ereport(ERROR,
+				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				 errmsg("number of array dimensions (%d) exceeds the maximum allowed (%d)",
+						list_length(sbsref->refupperindexpr), MAXDIM)));
+
+	if (list_length(sbsref->reflowerindexpr) > MAXDIM)
+		ereport(ERROR,
+				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				 errmsg("number of array dimensions (%d) exceeds the maximum allowed (%d)",
+						list_length(sbsref->reflowerindexpr), MAXDIM)));
+
+	return sbsref;
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f48f5fb4d9..2c6aa65bf8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -10898,6 +10898,13 @@
   proargnames => '{max_data_alignment,database_block_size,blocks_per_segment,wal_block_size,bytes_per_wal_segment,max_identifier_length,max_index_columns,max_toast_chunk_size,large_object_chunk_size,float8_pass_by_value,data_page_checksum_version}',
   prosrc => 'pg_control_init' },
 
+{ oid => '6099',
+  descr => 'Array subscripting logic',
+  proname => 'array_subscript_handler',
+  prorettype => 'internal',
+  proargtypes => 'internal',
+  prosrc => 'array_subscript_handler' },
+
 # collation management functions
 { oid => '3445', descr => 'import collations from operating system',
   proname => 'pg_import_system_collations', procost => '100',
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index b2cec07416..21489a02ae 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -54,7 +54,8 @@
   typname => 'name', typlen => 'NAMEDATALEN', typbyval => 'f',
   typcategory => 'S', typelem => 'char', typinput => 'namein',
   typoutput => 'nameout', typreceive => 'namerecv', typsend => 'namesend',
-  typalign => 'c', typcollation => 'C' },
+  typalign => 'c', typcollation => 'C',
+  typsubshandler => 'array_subscript_handler' },
 { oid => '20', array_type_oid => '1016',
   descr => '~18 digit integer, 8-byte storage',
   typname => 'int8', typlen => '8', typbyval => 'FLOAT8PASSBYVAL',
@@ -70,7 +71,7 @@
   typname => 'int2vector', typlen => '-1', typbyval => 'f', typcategory => 'A',
   typelem => 'int2', typinput => 'int2vectorin', typoutput => 'int2vectorout',
   typreceive => 'int2vectorrecv', typsend => 'int2vectorsend',
-  typalign => 'i' },
+  typalign => 'i', typsubshandler => 'array_subscript_handler' },
 { oid => '23', array_type_oid => '1007',
   descr => '-2 billion to 2 billion integer, 4-byte storage',
   typname => 'int4', typlen => '4', typbyval => 't', typcategory => 'N',
@@ -109,7 +110,8 @@
   descr => 'array of oids, used in system tables',
   typname => 'oidvector', typlen => '-1', typbyval => 'f', typcategory => 'A',
   typelem => 'oid', typinput => 'oidvectorin', typoutput => 'oidvectorout',
-  typreceive => 'oidvectorrecv', typsend => 'oidvectorsend', typalign => 'i' },
+  typreceive => 'oidvectorrecv', typsend => 'oidvectorsend', typalign => 'i',
+  typsubshandler => 'array_subscript_handler' },
 
 # hand-built rowtype entries for bootstrapped catalogs
 # NB: OIDs assigned here must match the BKI_ROWTYPE_OID declarations
@@ -188,32 +190,37 @@
   descr => 'geometric point \'(x, y)\'',
   typname => 'point', typlen => '16', typbyval => 'f', typcategory => 'G',
   typelem => 'float8', typinput => 'point_in', typoutput => 'point_out',
-  typreceive => 'point_recv', typsend => 'point_send', typalign => 'd' },
+  typreceive => 'point_recv', typsend => 'point_send', typalign => 'd',
+  typsubshandler => 'array_subscript_handler' },
 { oid => '601', array_type_oid => '1018',
   descr => 'geometric line segment \'(pt1,pt2)\'',
   typname => 'lseg', typlen => '32', typbyval => 'f', typcategory => 'G',
   typelem => 'point', typinput => 'lseg_in', typoutput => 'lseg_out',
-  typreceive => 'lseg_recv', typsend => 'lseg_send', typalign => 'd' },
+  typreceive => 'lseg_recv', typsend => 'lseg_send', typalign => 'd',
+  typsubshandler => 'array_subscript_handler' },
 { oid => '602', array_type_oid => '1019',
   descr => 'geometric path \'(pt1,...)\'',
   typname => 'path', typlen => '-1', typbyval => 'f', typcategory => 'G',
   typinput => 'path_in', typoutput => 'path_out', typreceive => 'path_recv',
-  typsend => 'path_send', typalign => 'd', typstorage => 'x' },
+  typsend => 'path_send', typalign => 'd', typstorage => 'x',
+  typsubshandler => 'array_subscript_handler' },
 { oid => '603', array_type_oid => '1020',
   descr => 'geometric box \'(lower left,upper right)\'',
   typname => 'box', typlen => '32', typbyval => 'f', typcategory => 'G',
   typdelim => ';', typelem => 'point', typinput => 'box_in',
   typoutput => 'box_out', typreceive => 'box_recv', typsend => 'box_send',
-  typalign => 'd' },
+  typalign => 'd', typsubshandler => 'array_subscript_handler' },
 { oid => '604', array_type_oid => '1027',
   descr => 'geometric polygon \'(pt1,...)\'',
   typname => 'polygon', typlen => '-1', typbyval => 'f', typcategory => 'G',
   typinput => 'poly_in', typoutput => 'poly_out', typreceive => 'poly_recv',
-  typsend => 'poly_send', typalign => 'd', typstorage => 'x' },
+  typsend => 'poly_send', typalign => 'd', typstorage => 'x',
+  typsubshandler => 'array_subscript_handler' },
 { oid => '628', array_type_oid => '629', descr => 'geometric line',
   typname => 'line', typlen => '24', typbyval => 'f', typcategory => 'G',
   typelem => 'float8', typinput => 'line_in', typoutput => 'line_out',
-  typreceive => 'line_recv', typsend => 'line_send', typalign => 'd' },
+  typreceive => 'line_recv', typsend => 'line_send', typalign => 'd',
+  typsubshandler => 'array_subscript_handler' },
 
 # OIDS 700 - 799
 
@@ -272,7 +279,7 @@
 { oid => '1033', array_type_oid => '1034', descr => 'access control list',
   typname => 'aclitem', typlen => '12', typbyval => 'f', typcategory => 'U',
   typinput => 'aclitemin', typoutput => 'aclitemout', typreceive => '-',
-  typsend => '-', typalign => 'i' },
+  typsend => '-', typalign => 'i', typsubshandler => 'array_subscript_handler' },
 { oid => '1042', array_type_oid => '1014',
   descr => 'char(length), blank-padded string, fixed storage length',
   typname => 'bpchar', typlen => '-1', typbyval => 'f', typcategory => 'S',
@@ -311,7 +318,8 @@
   typcategory => 'D', typispreferred => 't', typinput => 'timestamptz_in',
   typoutput => 'timestamptz_out', typreceive => 'timestamptz_recv',
   typsend => 'timestamptz_send', typmodin => 'timestamptztypmodin',
-  typmodout => 'timestamptztypmodout', typalign => 'd' },
+  typmodout => 'timestamptztypmodout', typalign => 'd',
+  typsubshandler => 'array_subscript_handler' },
 { oid => '1186', array_type_oid => '1187',
   descr => '@ <number> <units>, time interval',
   typname => 'interval', typlen => '16', typbyval => 'f', typcategory => 'T',
diff --git a/src/test/regress/expected/arrays.out b/src/test/regress/expected/arrays.out
index f9d9ad6aef..46b8cd4b98 100644
--- a/src/test/regress/expected/arrays.out
+++ b/src/test/regress/expected/arrays.out
@@ -190,9 +190,9 @@ select ('[0:2][0:2]={{1,2,3},{4,5,6},{7,8,9}}'::int[])[1:2][2];
 --
 -- check subscription corner cases
 --
--- More subscripts than MAXDIMS(6)
-SELECT ('{}'::int[])[1][2][3][4][5][6][7];
-ERROR:  number of array dimensions (7) exceeds the maximum allowed (6)
+-- More subscripts than MAXDIMS(12)
+SELECT ('{}'::int[])[1][2][3][4][5][6][7][8][9][10][11][12][13];
+ERROR:  number of array dimensions (13) exceeds the maximum allowed (6)
 -- NULL index yields NULL when selecting
 SELECT ('{{{1},{2},{3}},{{4},{5},{6}}}'::int[])[1][NULL][1];
  int4 
@@ -216,15 +216,15 @@ SELECT ('{{{1},{2},{3}},{{4},{5},{6}}}'::int[])[1][1:NULL][1];
 UPDATE arrtest
   SET c[NULL] = '{"can''t assign"}'
   WHERE array_dims(c) is not null;
-ERROR:  array subscript in assignment must not be null
+ERROR:  subscript in assignment must not be null
 UPDATE arrtest
   SET c[NULL:1] = '{"can''t assign"}'
   WHERE array_dims(c) is not null;
-ERROR:  array subscript in assignment must not be null
+ERROR:  subscript in assignment must not be null
 UPDATE arrtest
   SET c[1:NULL] = '{"can''t assign"}'
   WHERE array_dims(c) is not null;
-ERROR:  array subscript in assignment must not be null
+ERROR:  subscript in assignment must not be null
 -- test slices with empty lower and/or upper index
 CREATE TEMP TABLE arrtest_s (
   a       int2[],
diff --git a/src/test/regress/sql/arrays.sql b/src/test/regress/sql/arrays.sql
index 2b689ae88f..afccaf25e1 100644
--- a/src/test/regress/sql/arrays.sql
+++ b/src/test/regress/sql/arrays.sql
@@ -106,8 +106,8 @@ select ('[0:2][0:2]={{1,2,3},{4,5,6},{7,8,9}}'::int[])[1:2][2];
 --
 -- check subscription corner cases
 --
--- More subscripts than MAXDIMS(6)
-SELECT ('{}'::int[])[1][2][3][4][5][6][7];
+-- More subscripts than MAXDIMS(12)
+SELECT ('{}'::int[])[1][2][3][4][5][6][7][8][9][10][11][12][13];
 -- NULL index yields NULL when selecting
 SELECT ('{{{1},{2},{3}},{{4},{5},{6}}}'::int[])[1][NULL][1];
 SELECT ('{{{1},{2},{3}},{{4},{5},{6}}}'::int[])[1][NULL:1][1];
-- 
2.21.0


--j77abfbqdsj2lq6d
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
 filename="v34-0003-Subscripting-for-jsonb.patch"



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

* Re: psql - add SHOW_ALL_RESULTS option
@ 2022-03-22 14:52 Peter Eisentraut <[email protected]>
  2022-03-23 12:58 ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  0 siblings, 1 reply; 14+ messages in thread

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

On 17.03.22 19:04, Fabien COELHO wrote:
> 
> 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.

Your patch contains this test case:

+# 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");

The expected output (which passes) contains this line twice:

psql:<stdin>:2: FATAL:  terminating connection due to administrator command
psql:<stdin>:2: FATAL:  terminating connection due to administrator command

If I paste this test case into current master without your patch, I only 
get this line once.  So your patch is changing this output.  The whole 
point of the libpq fixes was to not have this duplicate output.  So I 
think something is still wrong somewhere.






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

* Re: psql - add SHOW_ALL_RESULTS option
  2022-03-22 14:52 Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
@ 2022-03-23 12:58 ` Fabien COELHO <[email protected]>
  2022-03-25 15:04   ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  0 siblings, 1 reply; 14+ messages in thread

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


Hello Peter,

>> 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.
>
> [...]
>
> The expected output (which passes) contains this line twice:
>
> psql:<stdin>:2: FATAL:  terminating connection due to administrator command
> psql:<stdin>:2: FATAL:  terminating connection due to administrator command


> If I paste this test case into current master without your patch, I only get 
> this line once.  So your patch is changing this output.  The whole point of 
> the libpq fixes was to not have this duplicate output.  So I think something 
> is still wrong somewhere.

Hmmm. Yes and no:-)

The previous path inside libpq silently ignores intermediate results, it 
skips all results to keep only the last one. The new approach does not 
discard resultss silently, hence the duplicated output, because they are 
actually there and have always been there in the first place, they were 
just ignored: The previous "good" result is really a side effect of a bad 
implementation in a corner case, which just becomes apparent when opening 
the list of results.

So my opinion is still to dissociate the libpq "bug/behavior" fix from 
this feature, as they are only loosely connected, because it is a very 
corner case anyway.

An alternative would be to remove the test case, but I'd prefer that it is 
kept.

If you want to wait for libpq to provide a solution for this corner case, 
I'm afraid that "never" is the likely result, especially as no test case 
exercices this path to show that there is a problem somewhere, so nobody 
should care to fix it. I'm not sure it is even worth it given the highly 
special situation which triggers the issue, which is not such an actual 
problem (ok, the user is told twice that there was a connection loss, no 
big deal).

-- 
Fabien.





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

* Re: psql - add SHOW_ALL_RESULTS option
  2022-03-22 14:52 Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  2022-03-23 12:58 ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
@ 2022-03-25 15:04   ` Peter Eisentraut <[email protected]>
  2022-03-25 15:46     ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  0 siblings, 1 reply; 14+ messages in thread

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

On 23.03.22 13:58, Fabien COELHO wrote:
> If you want to wait for libpq to provide a solution for this corner 
> case, I'm afraid that "never" is the likely result, especially as no 
> test case exercices this path to show that there is a problem somewhere, 
> so nobody should care to fix it. I'm not sure it is even worth it given 
> the highly special situation which triggers the issue, which is not such 
> an actual problem (ok, the user is told twice that there was a 
> connection loss, no big deal).

As Tom said earlier, wasn't this fixed by 618c16707?  If not, is there 
any other discussion on the specifics of this issue?  I'm not aware of one.





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

* Re: psql - add SHOW_ALL_RESULTS option
  2022-03-22 14:52 Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  2022-03-23 12:58 ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-03-25 15:04   ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
@ 2022-03-25 15:46     ` Fabien COELHO <[email protected]>
  2022-03-26 15:55       ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  0 siblings, 1 reply; 14+ messages in thread

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


> As Tom said earlier, wasn't this fixed by 618c16707?  If not, is there any 
> other discussion on the specifics of this issue?  I'm not aware of one.

Hmmm… I'll try to understand why the doubled message seems to be still 
there.

-- 
Fabien.

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

* Re: psql - add SHOW_ALL_RESULTS option
  2022-03-22 14:52 Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  2022-03-23 12:58 ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-03-25 15:04   ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  2022-03-25 15:46     ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
@ 2022-03-26 15:55       ` Fabien COELHO <[email protected]>
  2022-03-31 16:54         ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  0 siblings, 1 reply; 14+ messages in thread

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


Hello Peter,

>> As Tom said earlier, wasn't this fixed by 618c16707?  If not, is there any 
>> other discussion on the specifics of this issue?  I'm not aware of one.

This answer is that I had kept psql's calls to PQerrorMessage which 
reports errors from the connection, whereas it needed to change to 
PQresultErrorMessage to benefit from the libpq improvement.

I made the added autocommit/on_error_rollback tests at the end really 
focus on multi-statement queries (\;), as was more or less intended.

I updated the tap test.

-- 
Fabien.

Attachments:

  [text/x-diff] psql-show-all-results-18.patch (52.0K, ../../alpine.DEB.2.22.394.2203261650010.3931183@pseudo/2-psql-show-all-results-18.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..c84688e89c 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 = PQresultErrorMessage(result);
+
+			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..dd35cafa9d 100644
--- a/src/bin/psql/t/001_basic.pl
+++ b/src/bin/psql/t/001_basic.pl
@@ -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 63bfdf11c6..9c93471aec 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -4564,7 +4564,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..185c505312 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5290,3 +5290,245 @@ ERROR:  relation "notexists" does not exist
 LINE 1: SELECT * FROM notexists;
                       ^
 STATEMENT:  SELECT * FROM notexists;
+--
+-- combined queries
+--
+CREATE FUNCTION warn(msg TEXT) RETURNS BOOLEAN LANGUAGE plpgsql
+AS $$
+  BEGIN RAISE NOTICE 'warn %', msg ; RETURN TRUE ; END
+$$;
+-- show both
+SELECT 1 AS one \; SELECT warn('1.5') \; SELECT 2 AS two ;
+ one 
+-----
+   1
+(1 row)
+
+NOTICE:  warn 1.5
+CONTEXT:  PL/pgSQL function warn(text) line 2 at RAISE
+ warn 
+------
+ t
+(1 row)
+
+ two 
+-----
+   2
+(1 row)
+
+-- \gset applies to last query only
+SELECT 3 AS three \; SELECT warn('3.5') \; SELECT 4 AS four \gset
+ three 
+-------
+     3
+(1 row)
+
+NOTICE:  warn 3.5
+CONTEXT:  PL/pgSQL function warn(text) line 2 at RAISE
+ warn 
+------
+ t
+(1 row)
+
+\echo :three :four
+:three 4
+-- syntax error stops all processing
+SELECT 5 \; SELECT 6 + \; SELECT warn('6.5') \; SELECT 7 ;
+ERROR:  syntax error at or near ";"
+LINE 1: SELECT 5 ; SELECT 6 + ; SELECT warn('6.5') ; SELECT 7 ;
+                              ^
+-- with aborted transaction, stop on first error
+BEGIN \; SELECT 8 AS eight \; SELECT 9/0 AS nine \; ROLLBACK \; SELECT 10 AS ten ;
+ eight 
+-------
+     8
+(1 row)
+
+ERROR:  division by zero
+-- close previously aborted transaction
+ROLLBACK;
+-- miscellaneous SQL commands
+-- (non SELECT output is sent to stderr, thus is not shown in expected results)
+SELECT 'ok' AS "begin" \;
+CREATE TABLE psql_comics(s TEXT) \;
+INSERT INTO psql_comics VALUES ('Calvin'), ('hobbes') \;
+COPY psql_comics FROM STDIN \;
+UPDATE psql_comics SET s = 'Hobbes' WHERE s = 'hobbes' \;
+DELETE FROM psql_comics WHERE s = 'Moe' \;
+COPY psql_comics TO STDOUT \;
+TRUNCATE psql_comics \;
+DROP TABLE psql_comics \;
+SELECT 'ok' AS "done" ;
+ begin 
+-------
+ ok
+(1 row)
+
+Calvin
+Susie
+Hobbes
+ done 
+------
+ ok
+(1 row)
+
+\set SHOW_ALL_RESULTS off
+SELECT 1 AS one \; SELECT warn('1.5') \; SELECT 2 AS two ;
+NOTICE:  warn 1.5
+CONTEXT:  PL/pgSQL function warn(text) line 2 at RAISE
+ two 
+-----
+   2
+(1 row)
+
+\set SHOW_ALL_RESULTS on
+DROP FUNCTION warn(TEXT);
+--
+-- AUTOCOMMIT and combined queries
+--
+\set AUTOCOMMIT off
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+# AUTOCOMMIT: off
+-- BEGIN is now implicit
+CREATE TABLE foo(s TEXT) \;
+ROLLBACK;
+CREATE TABLE foo(s TEXT) \;
+INSERT INTO foo(s) VALUES ('hello'), ('world') \;
+COMMIT;
+DROP TABLE foo \;
+ROLLBACK;
+-- table foo is still there
+SELECT * FROM foo ORDER BY 1 \;
+DROP TABLE foo \;
+COMMIT;
+   s   
+-------
+ hello
+ world
+(2 rows)
+
+\set AUTOCOMMIT on
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+# AUTOCOMMIT: on
+-- BEGIN now explicit for multi-statement transactions
+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;
+   s   
+-------
+ hello
+ world
+(2 rows)
+
+--
+-- test ON_ERROR_ROLLBACK and combined queries
+--
+CREATE FUNCTION psql_error(msg TEXT) RETURNS BOOLEAN AS $$
+  BEGIN
+    RAISE EXCEPTION 'error %', msg;
+  END;
+$$ LANGUAGE plpgsql;
+\set ON_ERROR_ROLLBACK on
+\echo '# ON_ERROR_ROLLBACK:' :ON_ERROR_ROLLBACK
+# ON_ERROR_ROLLBACK: on
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+# AUTOCOMMIT: on
+BEGIN;
+CREATE TABLE bla(s NO_SUCH_TYPE);               -- fails
+ERROR:  type "no_such_type" does not exist
+LINE 1: CREATE TABLE bla(s NO_SUCH_TYPE);
+                           ^
+CREATE TABLE bla(s TEXT);                       -- succeeds
+SELECT psql_error('oops!');                     -- fails
+ERROR:  error oops!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+INSERT INTO bla VALUES ('Calvin'), ('Hobbes');
+COMMIT;
+SELECT * FROM bla ORDER BY 1;
+   s    
+--------
+ Calvin
+ Hobbes
+(2 rows)
+
+BEGIN;
+INSERT INTO bla VALUES ('Susie');         -- succeeds
+-- now with combined queries
+INSERT INTO bla VALUES ('Rosalyn') \;     -- will rollback
+SELECT 'before error' AS show \;          -- will show nevertheless!
+  SELECT psql_error('boum!') \;           -- failure
+  SELECT 'after error' AS noshow;         -- hidden by preceeding error
+     show     
+--------------
+ before error
+(1 row)
+
+ERROR:  error boum!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+INSERT INTO bla(s) VALUES ('Moe') \;      -- will rollback
+  SELECT psql_error('bam!');
+ERROR:  error bam!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+INSERT INTO bla VALUES ('Miss Wormwood'); -- succeeds
+COMMIT;
+SELECT * FROM bla ORDER BY 1;
+       s       
+---------------
+ Calvin
+ Hobbes
+ Miss Wormwood
+ Susie
+(4 rows)
+
+-- some with autocommit off
+\set AUTOCOMMIT off
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+# AUTOCOMMIT: off
+-- implicit BEGIN
+INSERT INTO bla VALUES ('Dad');           -- succeeds
+SELECT psql_error('bad!');                -- implicit partial rollback
+ERROR:  error bad!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+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
+ #mum 
+------
+    1
+(1 row)
+
+ERROR:  error bad!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+COMMIT;
+SELECT COUNT(*) AS "#mum"
+FROM bla WHERE s = 'Mum' \;               -- no mum here
+SELECT * FROM bla ORDER BY 1;
+ #mum 
+------
+    0
+(1 row)
+
+       s       
+---------------
+ Calvin
+ Dad
+ Hobbes
+ Miss Wormwood
+ Susie
+(5 rows)
+
+-- reset all
+\set AUTOCOMMIT on
+\set ON_ERROR_ROLLBACK off
+\echo '# final ON_ERROR_ROLLBACK:' :ON_ERROR_ROLLBACK
+# final ON_ERROR_ROLLBACK: off
+DROP TABLE bla;
+DROP FUNCTION psql_error;
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..8f49a5f347 100644
--- a/src/test/regress/sql/psql.sql
+++ b/src/test/regress/sql/psql.sql
@@ -1316,3 +1316,144 @@ DROP TABLE oer_test;
 \set ECHO errors
 SELECT * FROM notexists;
 \set ECHO all
+
+--
+-- 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;
+
+-- miscellaneous 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 and combined queries
+--
+\set AUTOCOMMIT off
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+-- BEGIN is now implicit
+
+CREATE TABLE foo(s TEXT) \;
+ROLLBACK;
+
+CREATE TABLE foo(s TEXT) \;
+INSERT INTO foo(s) VALUES ('hello'), ('world') \;
+COMMIT;
+
+DROP TABLE foo \;
+ROLLBACK;
+
+-- table foo is still there
+SELECT * FROM foo ORDER BY 1 \;
+DROP TABLE foo \;
+COMMIT;
+
+\set AUTOCOMMIT on
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+-- BEGIN now explicit for multi-statement transactions
+
+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;
+
+--
+-- test ON_ERROR_ROLLBACK and combined queries
+--
+CREATE FUNCTION psql_error(msg TEXT) RETURNS BOOLEAN AS $$
+  BEGIN
+    RAISE EXCEPTION 'error %', msg;
+  END;
+$$ LANGUAGE plpgsql;
+
+\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
+-- now with combined queries
+INSERT INTO bla VALUES ('Rosalyn') \;     -- will rollback
+SELECT 'before error' AS show \;          -- will show nevertheless!
+  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 COUNT(*) AS "#mum"
+FROM bla WHERE s = 'Mum' \;               -- no mum here
+SELECT * FROM bla ORDER BY 1;
+
+-- reset all
+\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-03-22 14:52 Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  2022-03-23 12:58 ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-03-25 15:04   ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  2022-03-25 15:46     ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-03-26 15:55       ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
@ 2022-03-31 16:54         ` Fabien COELHO <[email protected]>
  2022-04-01 05:46           ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  0 siblings, 1 reply; 14+ messages in thread

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


Attached a rebase.

-- 
Fabien.

Attachments:

  [text/x-diff] psql-show-all-results-19.patch (51.6K, ../../alpine.DEB.2.22.394.2203311853340.1072349@pseudo/2-psql-show-all-results-19.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..c84688e89c 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 = PQresultErrorMessage(result);
+
+			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 66796f4978..8ecb9b2583 100644
--- a/src/bin/psql/t/001_basic.pl
+++ b/src/bin/psql/t/001_basic.pl
@@ -126,7 +126,7 @@ is($ret, 2, 'server crash: psql exit code');
 like($out, qr/before/, 'server crash: output before crash');
 ok($out !~ qr/AFTER/, 'server crash: no output after crash');
 is($err, 'psql:<stdin>:2: FATAL:  terminating connection due to administrator command
-server closed the connection unexpectedly
+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',
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 3f9dfffd57..47cdd2eb7e 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -4627,7 +4627,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..185c505312 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5290,3 +5290,245 @@ ERROR:  relation "notexists" does not exist
 LINE 1: SELECT * FROM notexists;
                       ^
 STATEMENT:  SELECT * FROM notexists;
+--
+-- combined queries
+--
+CREATE FUNCTION warn(msg TEXT) RETURNS BOOLEAN LANGUAGE plpgsql
+AS $$
+  BEGIN RAISE NOTICE 'warn %', msg ; RETURN TRUE ; END
+$$;
+-- show both
+SELECT 1 AS one \; SELECT warn('1.5') \; SELECT 2 AS two ;
+ one 
+-----
+   1
+(1 row)
+
+NOTICE:  warn 1.5
+CONTEXT:  PL/pgSQL function warn(text) line 2 at RAISE
+ warn 
+------
+ t
+(1 row)
+
+ two 
+-----
+   2
+(1 row)
+
+-- \gset applies to last query only
+SELECT 3 AS three \; SELECT warn('3.5') \; SELECT 4 AS four \gset
+ three 
+-------
+     3
+(1 row)
+
+NOTICE:  warn 3.5
+CONTEXT:  PL/pgSQL function warn(text) line 2 at RAISE
+ warn 
+------
+ t
+(1 row)
+
+\echo :three :four
+:three 4
+-- syntax error stops all processing
+SELECT 5 \; SELECT 6 + \; SELECT warn('6.5') \; SELECT 7 ;
+ERROR:  syntax error at or near ";"
+LINE 1: SELECT 5 ; SELECT 6 + ; SELECT warn('6.5') ; SELECT 7 ;
+                              ^
+-- with aborted transaction, stop on first error
+BEGIN \; SELECT 8 AS eight \; SELECT 9/0 AS nine \; ROLLBACK \; SELECT 10 AS ten ;
+ eight 
+-------
+     8
+(1 row)
+
+ERROR:  division by zero
+-- close previously aborted transaction
+ROLLBACK;
+-- miscellaneous SQL commands
+-- (non SELECT output is sent to stderr, thus is not shown in expected results)
+SELECT 'ok' AS "begin" \;
+CREATE TABLE psql_comics(s TEXT) \;
+INSERT INTO psql_comics VALUES ('Calvin'), ('hobbes') \;
+COPY psql_comics FROM STDIN \;
+UPDATE psql_comics SET s = 'Hobbes' WHERE s = 'hobbes' \;
+DELETE FROM psql_comics WHERE s = 'Moe' \;
+COPY psql_comics TO STDOUT \;
+TRUNCATE psql_comics \;
+DROP TABLE psql_comics \;
+SELECT 'ok' AS "done" ;
+ begin 
+-------
+ ok
+(1 row)
+
+Calvin
+Susie
+Hobbes
+ done 
+------
+ ok
+(1 row)
+
+\set SHOW_ALL_RESULTS off
+SELECT 1 AS one \; SELECT warn('1.5') \; SELECT 2 AS two ;
+NOTICE:  warn 1.5
+CONTEXT:  PL/pgSQL function warn(text) line 2 at RAISE
+ two 
+-----
+   2
+(1 row)
+
+\set SHOW_ALL_RESULTS on
+DROP FUNCTION warn(TEXT);
+--
+-- AUTOCOMMIT and combined queries
+--
+\set AUTOCOMMIT off
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+# AUTOCOMMIT: off
+-- BEGIN is now implicit
+CREATE TABLE foo(s TEXT) \;
+ROLLBACK;
+CREATE TABLE foo(s TEXT) \;
+INSERT INTO foo(s) VALUES ('hello'), ('world') \;
+COMMIT;
+DROP TABLE foo \;
+ROLLBACK;
+-- table foo is still there
+SELECT * FROM foo ORDER BY 1 \;
+DROP TABLE foo \;
+COMMIT;
+   s   
+-------
+ hello
+ world
+(2 rows)
+
+\set AUTOCOMMIT on
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+# AUTOCOMMIT: on
+-- BEGIN now explicit for multi-statement transactions
+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;
+   s   
+-------
+ hello
+ world
+(2 rows)
+
+--
+-- test ON_ERROR_ROLLBACK and combined queries
+--
+CREATE FUNCTION psql_error(msg TEXT) RETURNS BOOLEAN AS $$
+  BEGIN
+    RAISE EXCEPTION 'error %', msg;
+  END;
+$$ LANGUAGE plpgsql;
+\set ON_ERROR_ROLLBACK on
+\echo '# ON_ERROR_ROLLBACK:' :ON_ERROR_ROLLBACK
+# ON_ERROR_ROLLBACK: on
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+# AUTOCOMMIT: on
+BEGIN;
+CREATE TABLE bla(s NO_SUCH_TYPE);               -- fails
+ERROR:  type "no_such_type" does not exist
+LINE 1: CREATE TABLE bla(s NO_SUCH_TYPE);
+                           ^
+CREATE TABLE bla(s TEXT);                       -- succeeds
+SELECT psql_error('oops!');                     -- fails
+ERROR:  error oops!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+INSERT INTO bla VALUES ('Calvin'), ('Hobbes');
+COMMIT;
+SELECT * FROM bla ORDER BY 1;
+   s    
+--------
+ Calvin
+ Hobbes
+(2 rows)
+
+BEGIN;
+INSERT INTO bla VALUES ('Susie');         -- succeeds
+-- now with combined queries
+INSERT INTO bla VALUES ('Rosalyn') \;     -- will rollback
+SELECT 'before error' AS show \;          -- will show nevertheless!
+  SELECT psql_error('boum!') \;           -- failure
+  SELECT 'after error' AS noshow;         -- hidden by preceeding error
+     show     
+--------------
+ before error
+(1 row)
+
+ERROR:  error boum!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+INSERT INTO bla(s) VALUES ('Moe') \;      -- will rollback
+  SELECT psql_error('bam!');
+ERROR:  error bam!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+INSERT INTO bla VALUES ('Miss Wormwood'); -- succeeds
+COMMIT;
+SELECT * FROM bla ORDER BY 1;
+       s       
+---------------
+ Calvin
+ Hobbes
+ Miss Wormwood
+ Susie
+(4 rows)
+
+-- some with autocommit off
+\set AUTOCOMMIT off
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+# AUTOCOMMIT: off
+-- implicit BEGIN
+INSERT INTO bla VALUES ('Dad');           -- succeeds
+SELECT psql_error('bad!');                -- implicit partial rollback
+ERROR:  error bad!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+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
+ #mum 
+------
+    1
+(1 row)
+
+ERROR:  error bad!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+COMMIT;
+SELECT COUNT(*) AS "#mum"
+FROM bla WHERE s = 'Mum' \;               -- no mum here
+SELECT * FROM bla ORDER BY 1;
+ #mum 
+------
+    0
+(1 row)
+
+       s       
+---------------
+ Calvin
+ Dad
+ Hobbes
+ Miss Wormwood
+ Susie
+(5 rows)
+
+-- reset all
+\set AUTOCOMMIT on
+\set ON_ERROR_ROLLBACK off
+\echo '# final ON_ERROR_ROLLBACK:' :ON_ERROR_ROLLBACK
+# final ON_ERROR_ROLLBACK: off
+DROP TABLE bla;
+DROP FUNCTION psql_error;
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..8f49a5f347 100644
--- a/src/test/regress/sql/psql.sql
+++ b/src/test/regress/sql/psql.sql
@@ -1316,3 +1316,144 @@ DROP TABLE oer_test;
 \set ECHO errors
 SELECT * FROM notexists;
 \set ECHO all
+
+--
+-- 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;
+
+-- miscellaneous 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 and combined queries
+--
+\set AUTOCOMMIT off
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+-- BEGIN is now implicit
+
+CREATE TABLE foo(s TEXT) \;
+ROLLBACK;
+
+CREATE TABLE foo(s TEXT) \;
+INSERT INTO foo(s) VALUES ('hello'), ('world') \;
+COMMIT;
+
+DROP TABLE foo \;
+ROLLBACK;
+
+-- table foo is still there
+SELECT * FROM foo ORDER BY 1 \;
+DROP TABLE foo \;
+COMMIT;
+
+\set AUTOCOMMIT on
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+-- BEGIN now explicit for multi-statement transactions
+
+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;
+
+--
+-- test ON_ERROR_ROLLBACK and combined queries
+--
+CREATE FUNCTION psql_error(msg TEXT) RETURNS BOOLEAN AS $$
+  BEGIN
+    RAISE EXCEPTION 'error %', msg;
+  END;
+$$ LANGUAGE plpgsql;
+
+\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
+-- now with combined queries
+INSERT INTO bla VALUES ('Rosalyn') \;     -- will rollback
+SELECT 'before error' AS show \;          -- will show nevertheless!
+  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 COUNT(*) AS "#mum"
+FROM bla WHERE s = 'Mum' \;               -- no mum here
+SELECT * FROM bla ORDER BY 1;
+
+-- reset all
+\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-03-22 14:52 Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  2022-03-23 12:58 ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-03-25 15:04   ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  2022-03-25 15:46     ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-03-26 15:55       ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-03-31 16:54         ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
@ 2022-04-01 05:46           ` Fabien COELHO <[email protected]>
  2022-04-01 14:09             ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  0 siblings, 1 reply; 14+ messages in thread

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


> Attached a rebase.

Again, after the SendQuery refactoring extraction.

-- 
Fabien.

Attachments:

  [text/x-diff] psql-show-all-results-20.patch (53.1K, ../../alpine.DEB.2.22.394.2204010745570.1603090@pseudo/2-psql-show-all-results-20.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 c9847c8f9a..c84688e89c 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -32,9 +32,10 @@
 
 static bool DescribeQuery(const char *query, double *elapsed_msec);
 static bool ExecQueryUsingCursor(const char *query, double *elapsed_msec);
-static bool ExecQueryAndProcessResult(const char *query, double *elapsed_msec, bool *svpt_gone_p);
 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);
 
 
 /*
@@ -355,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;
 
@@ -386,7 +387,7 @@ AcceptResult(const PGresult *result)
 				break;
 		}
 
-	if (!OK)
+	if (!OK && show_error)
 	{
 		const char *error = PQerrorMessage(pset.db);
 
@@ -474,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
@@ -574,7 +587,7 @@ PSQLexec(const char *query)
 
 	ResetCancelConn();
 
-	if (!AcceptResult(res))
+	if (!AcceptResult(res, true))
 	{
 		ClearOrSaveResult(res);
 		res = NULL;
@@ -597,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)
 	{
@@ -610,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;
 }
 
 
@@ -715,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;
 
@@ -747,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;
@@ -893,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)
@@ -1111,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;
 
@@ -1157,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;
 
@@ -1174,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 = PQresultErrorMessage(result);
+
+			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
@@ -1196,12 +1310,14 @@ bool
 SendQuery(const char *query)
 {
 	bool		timing = pset.timing;
+	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		svpt_gone = false;
+	bool		tx_ended = false;
 
 	if (!pset.db)
 	{
@@ -1240,64 +1356,72 @@ 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;
-
+		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 */
-		OK = ExecQueryAndProcessResult(query, &elapsed_msec, &svpt_gone);
+		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 */
 	}
 
 	if (!OK && pset.echo == PSQL_ECHO_ERRORS)
@@ -1322,11 +1446,16 @@ SendQuery(const char *query)
 				break;
 
 			case PQTRANS_INTRANS:
+
 				/*
-				 * Release our savepoint, but do nothing if they are messing
-				 * with savepoints themselves
+				 * Do nothing if they are messing with savepoints themselves:
+				 * If the user did COMMIT AND CHAIN, RELEASE or ROLLBACK, our
+				 * savepoint is gone. If they issued a SAVEPOINT, releasing
+				 * ours would remove theirs.
 				 */
-				if (!svpt_gone)
+				if (tx_ended)
+					svptcmd = NULL;
+				else
 					svptcmd = "RELEASE pg_psql_temporary_savepoint";
 				break;
 
@@ -1346,19 +1475,23 @@ 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;
 
-				ResetCancelConn();
+				PQclear(result);
 				goto sendquery_cleanup;
 			}
 			PQclear(svptres);
 		}
 	}
 
+	ClearOrSaveResult(result);
+
 	/* Possible microtiming output */
 	if (timing)
 		PrintTiming(elapsed_msec);
@@ -1381,6 +1514,9 @@ SendQuery(const char *query)
 
 sendquery_cleanup:
 
+	/* global cancellation reset */
+	ResetCancelConn();
+
 	/* reset \g's output-to-filename trigger */
 	if (pset.gfname)
 	{
@@ -1461,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)
 	{
@@ -1509,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)
 			{
@@ -1519,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);
 		}
@@ -1535,60 +1671,6 @@ DescribeQuery(const char *query, double *elapsed_msec)
 }
 
 
-/*
- * ExecQueryAndProcessResults: SendQuery() subroutine for the normal way to
- * send a query
- */
-static bool
-ExecQueryAndProcessResult(const char *query, double *elapsed_msec, bool *svpt_gone_p)
-{
-	bool		timing = pset.timing;
-	bool		OK;
-	instr_time	before,
-				after;
-	PGresult   *result;
-
-	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);
-
-	/*
-	 * Check if the user ran any command that would destroy our internal
-	 * savepoint: If the user did COMMIT AND CHAIN, RELEASE or ROLLBACK, our
-	 * savepoint is gone. If they issued a SAVEPOINT, releasing ours would
-	 * remove theirs.
-	 */
-	if (result && svpt_gone_p)
-	{
-		const char *cmd = PQcmdStatus(result);
-		*svpt_gone_p = (strcmp(cmd, "COMMIT") == 0 ||
-						strcmp(cmd, "SAVEPOINT") == 0 ||
-						strcmp(cmd, "RELEASE") == 0 ||
-						strcmp(cmd, "ROLLBACK") == 0);
-	}
-
-	ClearOrSaveResult(result);
-
-	return OK;
-}
-
-
 /*
  * ExecQueryUsingCursor: run a SELECT-like query using a cursor
  *
@@ -1633,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)
@@ -1647,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);
@@ -1720,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);
@@ -1829,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);
 	}
@@ -1839,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 66796f4978..8ecb9b2583 100644
--- a/src/bin/psql/t/001_basic.pl
+++ b/src/bin/psql/t/001_basic.pl
@@ -126,7 +126,7 @@ is($ret, 2, 'server crash: psql exit code');
 like($out, qr/before/, 'server crash: output before crash');
 ok($out !~ qr/AFTER/, 'server crash: no output after crash');
 is($err, 'psql:<stdin>:2: FATAL:  terminating connection due to administrator command
-server closed the connection unexpectedly
+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',
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 3f9dfffd57..47cdd2eb7e 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -4627,7 +4627,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..185c505312 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5290,3 +5290,245 @@ ERROR:  relation "notexists" does not exist
 LINE 1: SELECT * FROM notexists;
                       ^
 STATEMENT:  SELECT * FROM notexists;
+--
+-- combined queries
+--
+CREATE FUNCTION warn(msg TEXT) RETURNS BOOLEAN LANGUAGE plpgsql
+AS $$
+  BEGIN RAISE NOTICE 'warn %', msg ; RETURN TRUE ; END
+$$;
+-- show both
+SELECT 1 AS one \; SELECT warn('1.5') \; SELECT 2 AS two ;
+ one 
+-----
+   1
+(1 row)
+
+NOTICE:  warn 1.5
+CONTEXT:  PL/pgSQL function warn(text) line 2 at RAISE
+ warn 
+------
+ t
+(1 row)
+
+ two 
+-----
+   2
+(1 row)
+
+-- \gset applies to last query only
+SELECT 3 AS three \; SELECT warn('3.5') \; SELECT 4 AS four \gset
+ three 
+-------
+     3
+(1 row)
+
+NOTICE:  warn 3.5
+CONTEXT:  PL/pgSQL function warn(text) line 2 at RAISE
+ warn 
+------
+ t
+(1 row)
+
+\echo :three :four
+:three 4
+-- syntax error stops all processing
+SELECT 5 \; SELECT 6 + \; SELECT warn('6.5') \; SELECT 7 ;
+ERROR:  syntax error at or near ";"
+LINE 1: SELECT 5 ; SELECT 6 + ; SELECT warn('6.5') ; SELECT 7 ;
+                              ^
+-- with aborted transaction, stop on first error
+BEGIN \; SELECT 8 AS eight \; SELECT 9/0 AS nine \; ROLLBACK \; SELECT 10 AS ten ;
+ eight 
+-------
+     8
+(1 row)
+
+ERROR:  division by zero
+-- close previously aborted transaction
+ROLLBACK;
+-- miscellaneous SQL commands
+-- (non SELECT output is sent to stderr, thus is not shown in expected results)
+SELECT 'ok' AS "begin" \;
+CREATE TABLE psql_comics(s TEXT) \;
+INSERT INTO psql_comics VALUES ('Calvin'), ('hobbes') \;
+COPY psql_comics FROM STDIN \;
+UPDATE psql_comics SET s = 'Hobbes' WHERE s = 'hobbes' \;
+DELETE FROM psql_comics WHERE s = 'Moe' \;
+COPY psql_comics TO STDOUT \;
+TRUNCATE psql_comics \;
+DROP TABLE psql_comics \;
+SELECT 'ok' AS "done" ;
+ begin 
+-------
+ ok
+(1 row)
+
+Calvin
+Susie
+Hobbes
+ done 
+------
+ ok
+(1 row)
+
+\set SHOW_ALL_RESULTS off
+SELECT 1 AS one \; SELECT warn('1.5') \; SELECT 2 AS two ;
+NOTICE:  warn 1.5
+CONTEXT:  PL/pgSQL function warn(text) line 2 at RAISE
+ two 
+-----
+   2
+(1 row)
+
+\set SHOW_ALL_RESULTS on
+DROP FUNCTION warn(TEXT);
+--
+-- AUTOCOMMIT and combined queries
+--
+\set AUTOCOMMIT off
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+# AUTOCOMMIT: off
+-- BEGIN is now implicit
+CREATE TABLE foo(s TEXT) \;
+ROLLBACK;
+CREATE TABLE foo(s TEXT) \;
+INSERT INTO foo(s) VALUES ('hello'), ('world') \;
+COMMIT;
+DROP TABLE foo \;
+ROLLBACK;
+-- table foo is still there
+SELECT * FROM foo ORDER BY 1 \;
+DROP TABLE foo \;
+COMMIT;
+   s   
+-------
+ hello
+ world
+(2 rows)
+
+\set AUTOCOMMIT on
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+# AUTOCOMMIT: on
+-- BEGIN now explicit for multi-statement transactions
+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;
+   s   
+-------
+ hello
+ world
+(2 rows)
+
+--
+-- test ON_ERROR_ROLLBACK and combined queries
+--
+CREATE FUNCTION psql_error(msg TEXT) RETURNS BOOLEAN AS $$
+  BEGIN
+    RAISE EXCEPTION 'error %', msg;
+  END;
+$$ LANGUAGE plpgsql;
+\set ON_ERROR_ROLLBACK on
+\echo '# ON_ERROR_ROLLBACK:' :ON_ERROR_ROLLBACK
+# ON_ERROR_ROLLBACK: on
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+# AUTOCOMMIT: on
+BEGIN;
+CREATE TABLE bla(s NO_SUCH_TYPE);               -- fails
+ERROR:  type "no_such_type" does not exist
+LINE 1: CREATE TABLE bla(s NO_SUCH_TYPE);
+                           ^
+CREATE TABLE bla(s TEXT);                       -- succeeds
+SELECT psql_error('oops!');                     -- fails
+ERROR:  error oops!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+INSERT INTO bla VALUES ('Calvin'), ('Hobbes');
+COMMIT;
+SELECT * FROM bla ORDER BY 1;
+   s    
+--------
+ Calvin
+ Hobbes
+(2 rows)
+
+BEGIN;
+INSERT INTO bla VALUES ('Susie');         -- succeeds
+-- now with combined queries
+INSERT INTO bla VALUES ('Rosalyn') \;     -- will rollback
+SELECT 'before error' AS show \;          -- will show nevertheless!
+  SELECT psql_error('boum!') \;           -- failure
+  SELECT 'after error' AS noshow;         -- hidden by preceeding error
+     show     
+--------------
+ before error
+(1 row)
+
+ERROR:  error boum!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+INSERT INTO bla(s) VALUES ('Moe') \;      -- will rollback
+  SELECT psql_error('bam!');
+ERROR:  error bam!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+INSERT INTO bla VALUES ('Miss Wormwood'); -- succeeds
+COMMIT;
+SELECT * FROM bla ORDER BY 1;
+       s       
+---------------
+ Calvin
+ Hobbes
+ Miss Wormwood
+ Susie
+(4 rows)
+
+-- some with autocommit off
+\set AUTOCOMMIT off
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+# AUTOCOMMIT: off
+-- implicit BEGIN
+INSERT INTO bla VALUES ('Dad');           -- succeeds
+SELECT psql_error('bad!');                -- implicit partial rollback
+ERROR:  error bad!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+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
+ #mum 
+------
+    1
+(1 row)
+
+ERROR:  error bad!
+CONTEXT:  PL/pgSQL function psql_error(text) line 3 at RAISE
+COMMIT;
+SELECT COUNT(*) AS "#mum"
+FROM bla WHERE s = 'Mum' \;               -- no mum here
+SELECT * FROM bla ORDER BY 1;
+ #mum 
+------
+    0
+(1 row)
+
+       s       
+---------------
+ Calvin
+ Dad
+ Hobbes
+ Miss Wormwood
+ Susie
+(5 rows)
+
+-- reset all
+\set AUTOCOMMIT on
+\set ON_ERROR_ROLLBACK off
+\echo '# final ON_ERROR_ROLLBACK:' :ON_ERROR_ROLLBACK
+# final ON_ERROR_ROLLBACK: off
+DROP TABLE bla;
+DROP FUNCTION psql_error;
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..8f49a5f347 100644
--- a/src/test/regress/sql/psql.sql
+++ b/src/test/regress/sql/psql.sql
@@ -1316,3 +1316,144 @@ DROP TABLE oer_test;
 \set ECHO errors
 SELECT * FROM notexists;
 \set ECHO all
+
+--
+-- 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;
+
+-- miscellaneous 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 and combined queries
+--
+\set AUTOCOMMIT off
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+-- BEGIN is now implicit
+
+CREATE TABLE foo(s TEXT) \;
+ROLLBACK;
+
+CREATE TABLE foo(s TEXT) \;
+INSERT INTO foo(s) VALUES ('hello'), ('world') \;
+COMMIT;
+
+DROP TABLE foo \;
+ROLLBACK;
+
+-- table foo is still there
+SELECT * FROM foo ORDER BY 1 \;
+DROP TABLE foo \;
+COMMIT;
+
+\set AUTOCOMMIT on
+\echo '# AUTOCOMMIT:' :AUTOCOMMIT
+-- BEGIN now explicit for multi-statement transactions
+
+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;
+
+--
+-- test ON_ERROR_ROLLBACK and combined queries
+--
+CREATE FUNCTION psql_error(msg TEXT) RETURNS BOOLEAN AS $$
+  BEGIN
+    RAISE EXCEPTION 'error %', msg;
+  END;
+$$ LANGUAGE plpgsql;
+
+\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
+-- now with combined queries
+INSERT INTO bla VALUES ('Rosalyn') \;     -- will rollback
+SELECT 'before error' AS show \;          -- will show nevertheless!
+  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 COUNT(*) AS "#mum"
+FROM bla WHERE s = 'Mum' \;               -- no mum here
+SELECT * FROM bla ORDER BY 1;
+
+-- reset all
+\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-03-22 14:52 Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  2022-03-23 12:58 ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-03-25 15:04   ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  2022-03-25 15:46     ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-03-26 15:55       ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-03-31 16:54         ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-04-01 05:46           ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
@ 2022-04-01 14:09             ` Peter Eisentraut <[email protected]>
  2022-04-02 13:26               ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  0 siblings, 1 reply; 14+ messages in thread

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

On 01.04.22 07:46, Fabien COELHO wrote:
>> Attached a rebase.
> 
> Again, after the SendQuery refactoring extraction.

I'm doing this locally, so don't feel obliged to send more of these. ;-)

I've started committing this now, in pieces.






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

* Re: psql - add SHOW_ALL_RESULTS option
  2022-03-22 14:52 Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  2022-03-23 12:58 ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-03-25 15:04   ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  2022-03-25 15:46     ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-03-26 15:55       ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-03-31 16:54         ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-04-01 05:46           ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-04-01 14:09             ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
@ 2022-04-02 13:26               ` Fabien COELHO <[email protected]>
  2022-04-04 21:32                 ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  0 siblings, 1 reply; 14+ messages in thread

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


>> Again, after the SendQuery refactoring extraction.
>
> I'm doing this locally, so don't feel obliged to send more of these. ;-)

Good for me :-)

-- 
Fabien.






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

* Re: psql - add SHOW_ALL_RESULTS option
  2022-03-22 14:52 Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  2022-03-23 12:58 ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-03-25 15:04   ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  2022-03-25 15:46     ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-03-26 15:55       ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-03-31 16:54         ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-04-01 05:46           ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-04-01 14:09             ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  2022-04-02 13:26               ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
@ 2022-04-04 21:32                 ` Peter Eisentraut <[email protected]>
  2022-04-06 02:06                   ` Re: psql - add SHOW_ALL_RESULTS option - pg_regress output Andres Freund <[email protected]>
  0 siblings, 1 reply; 14+ messages in thread

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

On 02.04.22 15:26, Fabien COELHO wrote:
> 
>>> Again, after the SendQuery refactoring extraction.
>>
>> I'm doing this locally, so don't feel obliged to send more of these. ;-)
> 
> Good for me :-)

This has been committed.

I reduced some of your stylistic changes in order to keep the surface 
area of this complicated patch small.  We can apply some of those later 
if you are interested.  Right now, let's let it settle a bit.







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

* Re: psql - add SHOW_ALL_RESULTS option - pg_regress output
  2022-03-22 14:52 Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  2022-03-23 12:58 ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-03-25 15:04   ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  2022-03-25 15:46     ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-03-26 15:55       ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-03-31 16:54         ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-04-01 05:46           ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-04-01 14:09             ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  2022-04-02 13:26               ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-04-04 21:32                 ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
@ 2022-04-06 02:06                   ` Andres Freund <[email protected]>
  2022-04-06 08:37                     ` Re: psql - add SHOW_ALL_RESULTS option - pg_regress output Peter Eisentraut <[email protected]>
  0 siblings, 1 reply; 14+ messages in thread

From: Andres Freund @ 2022-04-06 02:06 UTC (permalink / raw)
  To: Peter Eisentraut <[email protected]>; +Cc: Fabien COELHO <[email protected]>; [email protected] <[email protected]>

Hi,

On 2022-04-04 23:32:50 +0200, Peter Eisentraut wrote:
> This has been committed.

It's somewhat annoying that made pg_regress even more verbose than before:

============== removing existing temp instance        ==============
============== creating temporary instance            ==============
============== initializing database system           ==============
============== starting postmaster                    ==============
running on port 51696 with PID 2203449
============== creating database "regression"         ==============
CREATE DATABASE
ALTER DATABASE
ALTER DATABASE
ALTER DATABASE
ALTER DATABASE
ALTER DATABASE
ALTER DATABASE
============== running regression test queries        ==============

Unfortunately it appears that neither can CREATE DATABASE set GUCs, nor can
ALTER DATABASE set multiple GUCs in one statement.

Perhaps we can just set SHOW_ALL_RESULTS off for that psql command?

- Andres






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

* Re: psql - add SHOW_ALL_RESULTS option - pg_regress output
  2022-03-22 14:52 Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  2022-03-23 12:58 ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-03-25 15:04   ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  2022-03-25 15:46     ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-03-26 15:55       ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-03-31 16:54         ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-04-01 05:46           ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-04-01 14:09             ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  2022-04-02 13:26               ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-04-04 21:32                 ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  2022-04-06 02:06                   ` Re: psql - add SHOW_ALL_RESULTS option - pg_regress output Andres Freund <[email protected]>
@ 2022-04-06 08:37                     ` Peter Eisentraut <[email protected]>
  2022-04-06 17:05                       ` Re: psql - add SHOW_ALL_RESULTS option - pg_regress output Andres Freund <[email protected]>
  0 siblings, 1 reply; 14+ messages in thread

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

On 06.04.22 04:06, Andres Freund wrote:
> On 2022-04-04 23:32:50 +0200, Peter Eisentraut wrote:
>> This has been committed.
> 
> It's somewhat annoying that made pg_regress even more verbose than before:
> 
> ============== removing existing temp instance        ==============
> ============== creating temporary instance            ==============
> ============== initializing database system           ==============
> ============== starting postmaster                    ==============
> running on port 51696 with PID 2203449
> ============== creating database "regression"         ==============
> CREATE DATABASE
> ALTER DATABASE
> ALTER DATABASE
> ALTER DATABASE
> ALTER DATABASE
> ALTER DATABASE
> ALTER DATABASE
> ============== running regression test queries        ==============
> 
> Unfortunately it appears that neither can CREATE DATABASE set GUCs, nor can
> ALTER DATABASE set multiple GUCs in one statement.
> 
> Perhaps we can just set SHOW_ALL_RESULTS off for that psql command?

Do you mean the extra "ALTER DATABASE" lines?  Couldn't we just turn all 
of those off?  AFAICT, no one likes them.






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

* Re: psql - add SHOW_ALL_RESULTS option - pg_regress output
  2022-03-22 14:52 Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  2022-03-23 12:58 ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-03-25 15:04   ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  2022-03-25 15:46     ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-03-26 15:55       ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-03-31 16:54         ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-04-01 05:46           ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-04-01 14:09             ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  2022-04-02 13:26               ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
  2022-04-04 21:32                 ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
  2022-04-06 02:06                   ` Re: psql - add SHOW_ALL_RESULTS option - pg_regress output Andres Freund <[email protected]>
  2022-04-06 08:37                     ` Re: psql - add SHOW_ALL_RESULTS option - pg_regress output Peter Eisentraut <[email protected]>
@ 2022-04-06 17:05                       ` Andres Freund <[email protected]>
  0 siblings, 0 replies; 14+ messages in thread

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

Hi,

On 2022-04-06 10:37:29 +0200, Peter Eisentraut wrote:
> On 06.04.22 04:06, Andres Freund wrote:
> > On 2022-04-04 23:32:50 +0200, Peter Eisentraut wrote:
> > > This has been committed.
> > 
> > It's somewhat annoying that made pg_regress even more verbose than before:
> > 
> > ============== removing existing temp instance        ==============
> > ============== creating temporary instance            ==============
> > ============== initializing database system           ==============
> > ============== starting postmaster                    ==============
> > running on port 51696 with PID 2203449
> > ============== creating database "regression"         ==============
> > CREATE DATABASE
> > ALTER DATABASE
> > ALTER DATABASE
> > ALTER DATABASE
> > ALTER DATABASE
> > ALTER DATABASE
> > ALTER DATABASE
> > ============== running regression test queries        ==============
> > 
> > Unfortunately it appears that neither can CREATE DATABASE set GUCs, nor can
> > ALTER DATABASE set multiple GUCs in one statement.
> > 
> > Perhaps we can just set SHOW_ALL_RESULTS off for that psql command?
> 
> Do you mean the extra "ALTER DATABASE" lines?  Couldn't we just turn all of
> those off?  AFAICT, no one likes them.

Yea. Previously there was just CREATE DATABASE. And yes, it seems like we
should use -q in psql_start_command().

Daniel has a patch to shrink pg_regress output overall, but it came too late
for 15. It'd still be good to avoid further increasing the size till then IMO.

Greetings,

Andres Freund






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


end of thread, other threads:[~2022-04-06 17:05 UTC | newest]

Thread overview: 14+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2019-02-01 10:38 [PATCH v34 2/5] Subscripting for array erthalion <[email protected]>
2022-03-22 14:52 Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
2022-03-23 12:58 ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
2022-03-25 15:04   ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
2022-03-25 15:46     ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
2022-03-26 15:55       ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
2022-03-31 16:54         ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
2022-04-01 05:46           ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
2022-04-01 14:09             ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
2022-04-02 13:26               ` Re: psql - add SHOW_ALL_RESULTS option Fabien COELHO <[email protected]>
2022-04-04 21:32                 ` Re: psql - add SHOW_ALL_RESULTS option Peter Eisentraut <[email protected]>
2022-04-06 02:06                   ` Re: psql - add SHOW_ALL_RESULTS option - pg_regress output Andres Freund <[email protected]>
2022-04-06 08:37                     ` Re: psql - add SHOW_ALL_RESULTS option - pg_regress output Peter Eisentraut <[email protected]>
2022-04-06 17:05                       ` Re: psql - add SHOW_ALL_RESULTS option - pg_regress output Andres Freund <[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