Ok. I noticed. The patch got significantly broken by the watch pager commit. I also have to enhance the added tests (per Peter request).I wrote a test to check psql query cancel support. I checked that it fails against the patch that was reverted. Maybe this is useful.
Here is the updated version (v8? I'm not sure what the right count is), which works for me and for "make check", including some tests added for uncovered paths.
I included your tap test (thanks again!) with some more comments and cleanup.
I tested manually for the pager feature, which mostly work, althoug "pspg --stream" does not seem to expect two tables, or maybe there is a way to switch between these that I have not found.
-- Fabien.
diff --git a/contrib/pg_stat_statements/expected/pg_stat_statements.out b/contrib/pg_stat_statements/expected/pg_stat_statements.out index 40b5109b55..ccce72fb85 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 fcab5c0d51..7c5504bc74 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> @@ -3542,10 +3535,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> @@ -4132,6 +4121,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 5640786678..564a4816de 100644 --- a/src/bin/psql/common.c +++ b/src/bin/psql/common.c @@ -33,6 +33,8 @@ static bool DescribeQuery(const char *query, double *elapsed_msec); static bool ExecQueryUsingCursor(const char *query, double *elapsed_msec); static bool command_no_begin(const char *query); static bool is_select_command(const char *query); +static int SendQueryAndProcessResults(const char *query, double *pelapsed_msec, + bool is_watch, const printQueryOpt *opt, FILE *printQueryFout); /* @@ -353,7 +355,7 @@ CheckConnection(void) * Returns true for valid result, false for error state. */ static bool -AcceptResult(const PGresult *result) +AcceptResult(const PGresult *result, bool show_error) { bool OK; @@ -384,7 +386,7 @@ AcceptResult(const PGresult *result) break; } - if (!OK) + if (!OK && show_error) { const char *error = PQerrorMessage(pset.db); @@ -472,6 +474,18 @@ ClearOrSaveResult(PGresult *result) } } +/* + * Consume all results + */ +static void +ClearOrSaveAllResults() +{ + PGresult *result; + + while ((result = PQgetResult(pset.db)) != NULL) + ClearOrSaveResult(result); +} + /* * Print microtiming output. Always print raw milliseconds; if the interval @@ -572,7 +586,7 @@ PSQLexec(const char *query) ResetCancelConn(); - if (!AcceptResult(res)) + if (!AcceptResult(res, true)) { ClearOrSaveResult(res); res = NULL; @@ -594,11 +608,8 @@ PSQLexec(const char *query) int PSQLexecWatch(const char *query, const printQueryOpt *opt, FILE *printQueryFout) { - PGresult *res; double elapsed_msec = 0; - instr_time before; - instr_time after; - FILE *fout; + int res; if (!pset.db) { @@ -607,77 +618,14 @@ PSQLexecWatch(const char *query, const printQueryOpt *opt, FILE *printQueryFout) } SetCancelConn(pset.db); - - if (pset.timing) - INSTR_TIME_SET_CURRENT(before); - - res = PQexec(pset.db, query); - + res = SendQueryAndProcessResults(query, &elapsed_msec, true, opt, printQueryFout); ResetCancelConn(); - if (!AcceptResult(res)) - { - ClearOrSaveResult(res); - return 0; - } - - if (pset.timing) - { - INSTR_TIME_SET_CURRENT(after); - INSTR_TIME_SUBTRACT(after, before); - elapsed_msec = INSTR_TIME_GET_MILLISEC(after); - } - - /* - * If SIGINT is sent while the query is processing, the interrupt will be - * consumed. The user's intention, though, is to cancel the entire watch - * process, so detect a sent cancellation request and exit in this case. - */ - if (cancel_pressed) - { - PQclear(res); - return 0; - } - - 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 (pset.timing) PrintTiming(elapsed_msec); - return 1; + return res; } @@ -712,7 +660,7 @@ PrintNotifications(void) * Returns true if successful, false otherwise. */ static bool -PrintQueryTuples(const PGresult *results) +PrintQueryTuples(const PGresult *results, const printQueryOpt *opt, FILE *printQueryFout) { bool result = true; @@ -744,8 +692,9 @@ PrintQueryTuples(const PGresult *results) } else { - printQuery(results, &pset.popt, pset.queryFout, false, pset.logfile); - if (ferror(pset.queryFout)) + FILE *fout = printQueryFout ? printQueryFout : pset.queryFout; + printQuery(results, opt ? opt : &pset.popt, fout, false, pset.logfile); + if (ferror(fout)) { pg_log_error("could not print result table: %m"); result = false; @@ -890,213 +839,131 @@ loop_exit: /* - * ProcessResult: utility function for use by SendQuery() only - * - * When our command string contained a COPY FROM STDIN or COPY TO STDOUT, - * PQexec() has stopped at the PGresult associated with the first such - * command. In that event, we'll marshal data for the COPY and then cycle - * through any subsequent PGresult objects. - * - * When the command string contained no such COPY command, this function - * degenerates to an AcceptResult() call. - * - * Changes its argument to point to the last PGresult of the command string, - * or NULL if that result was for a COPY TO STDOUT. (Returning NULL prevents - * the command status from being printed, which we want in that case so that - * the status line doesn't get taken as part of the COPY data.) - * - * Returns true on complete success, false otherwise. Possible failure modes - * include purely client-side problems; check the transaction status for the - * server-side opinion. + * Marshal the COPY data. Either subroutine will get the + * connection out of its COPY state, then call PQresultStatus() + * once and report any error. Return whether all was ok. + * + * For COPY OUT, direct the output to pset.copyStream if it's set, + * otherwise to pset.gfname if it's set, otherwise to queryFout. + * For COPY IN, use pset.copyStream as data source if it's set, + * otherwise cur_cmd_source. + * + * Update result if further processing is necessary, or NULL otherwise. + * Return a result when queryFout can safely output a result status: + * on COPY IN, or on COPY OUT if written to something other than pset.queryFout. + * Returning NULL prevents the command status from being printed, which + * we want if the status line doesn't get taken as part of the COPY data. */ static bool -ProcessResult(PGresult **results) +HandleCopyResult(PGresult **result) { - bool success = true; - bool first_cycle = true; + bool success; + FILE *copystream; + PGresult *copy_result; + ExecStatusType result_status = PQresultStatus(*result); - for (;;) + Assert(result_status == PGRES_COPY_OUT || + result_status == PGRES_COPY_IN); + + SetCancelConn(pset.db); + + if (result_status == PGRES_COPY_OUT) { - ExecStatusType result_status; - bool is_copy; - PGresult *next_result; + bool need_close = false; + bool is_pipe = false; - if (!AcceptResult(*results)) + if (pset.copyStream) { - /* - * Failure at this point is always a server-side failure or a - * failure to submit the command string. Either way, we're - * finished with this command string. - */ - success = false; - break; + /* invoked by \copy */ + copystream = pset.copyStream; } - - result_status = PQresultStatus(*results); - switch (result_status) + else if (pset.gfname) { - case PGRES_EMPTY_QUERY: - case PGRES_COMMAND_OK: - case PGRES_TUPLES_OK: - is_copy = false; - break; - - case PGRES_COPY_OUT: - case PGRES_COPY_IN: - is_copy = true; - break; - - default: - /* AcceptResult() should have caught anything else. */ - is_copy = false; - pg_log_error("unexpected PQresultStatus: %d", result_status); - break; - } - - if (is_copy) - { - /* - * Marshal the COPY data. Either subroutine will get the - * connection out of its COPY state, then call PQresultStatus() - * once and report any error. - * - * For COPY OUT, direct the output to pset.copyStream if it's set, - * otherwise to pset.gfname if it's set, otherwise to queryFout. - * For COPY IN, use pset.copyStream as data source if it's set, - * otherwise cur_cmd_source. - */ - FILE *copystream; - PGresult *copy_result; - - SetCancelConn(pset.db); - if (result_status == PGRES_COPY_OUT) + /* invoked by \g */ + if (openQueryOutputFile(pset.gfname, + ©stream, &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, - ©stream, &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, - ©_result) - && success - && (copystream != NULL); - - /* - * Suppress status printing if the report would go to the same - * place as the COPY data just went. Note this doesn't - * prevent error reporting, since handleCopyOut did that. - */ - if (copystream == pset.queryFout) - { - PQclear(copy_result); - copy_result = NULL; - } - - if (need_close) - { - /* close \g argument file/pipe */ - if (is_pipe) - { - pclose(copystream); - restore_sigpipe_trap(); - } - else - { - fclose(copystream); - } - } + need_close = true; + if (is_pipe) + disable_sigpipe_trap(); } else - { - /* COPY IN */ - copystream = pset.copyStream ? pset.copyStream : pset.cur_cmd_source; - success = handleCopyIn(pset.db, - copystream, - PQbinaryTuples(*results), - ©_result) && success; - } - ResetCancelConn(); - - /* - * Replace the PGRES_COPY_OUT/IN result with COPY command's exit - * status, or with NULL if we want to suppress printing anything. - */ - PQclear(*results); - *results = copy_result; + copystream = NULL; /* discard COPY data entirely */ } - else if (first_cycle) + else { - /* fast path: no COPY commands; PQexec visited all results */ - break; + /* fall back to the generic query output stream */ + copystream = pset.queryFout; } + success = handleCopyOut(pset.db, + copystream, + ©_result) + && (copystream != NULL); + /* - * Check PQgetResult() again. In the typical case of a single-command - * string, it will return NULL. Otherwise, we'll have other results - * to process that may include other COPYs. We keep the last result. + * Suppress status printing if the report would go to the same + * place as the COPY data just went. Note this doesn't + * prevent error reporting, since handleCopyOut did that. */ - next_result = PQgetResult(pset.db); - if (!next_result) - break; + if (copystream == pset.queryFout) + { + PQclear(copy_result); + copy_result = NULL; + } - PQclear(*results); - *results = next_result; - first_cycle = false; + if (need_close) + { + /* close \g argument file/pipe */ + if (is_pipe) + { + pclose(copystream); + restore_sigpipe_trap(); + } + else + { + fclose(copystream); + } + } + } + else + { + /* COPY IN */ + copystream = pset.copyStream ? pset.copyStream : pset.cur_cmd_source; + success = handleCopyIn(pset.db, + copystream, + PQbinaryTuples(*result), + ©_result); } - SetResultVariables(*results, success); - - /* may need this to recover from conn loss during COPY */ - if (!first_cycle && !CheckConnection()) - return false; + ResetCancelConn(); + PQclear(*result); + *result = copy_result; return success; } - /* * PrintQueryStatus: report command status as required * - * Note: Utility function for use by PrintQueryResults() only. + * Note: Utility function for use by HandleQueryResult() only. */ static void -PrintQueryStatus(PGresult *results) +PrintQueryStatus(PGresult *results, FILE *printQueryFout) { char buf[16]; + FILE *fout = printQueryFout ? printQueryFout : pset.queryFout; if (!pset.quiet) { if (pset.popt.topt.format == PRINT_HTML) { - fputs("<p>", pset.queryFout); - html_escaped_print(PQcmdStatus(results), pset.queryFout); - fputs("</p>\n", pset.queryFout); + fputs("<p>", fout); + html_escaped_print(PQcmdStatus(results), fout); + fputs("</p>\n", fout); } else - fprintf(pset.queryFout, "%s\n", PQcmdStatus(results)); + fprintf(fout, "%s\n", PQcmdStatus(results)); } if (pset.logfile) @@ -1108,43 +975,50 @@ PrintQueryStatus(PGresult *results) /* - * PrintQueryResults: print out (or store or execute) query results as required - * - * Note: Utility function for use by SendQuery() only. + * HandleQueryResult: print out, store or execute one query result + * as required. * * Returns true if the query executed successfully, false otherwise. */ static bool -PrintQueryResults(PGresult *results) +HandleQueryResult(PGresult *result, bool last, bool is_watch, const printQueryOpt *opt, FILE *printQueryFout) { bool success; const char *cmdstatus; - if (!results) + if (result == NULL) return false; - switch (PQresultStatus(results)) + switch (PQresultStatus(result)) { case PGRES_TUPLES_OK: /* store or execute or print the data ... */ - if (pset.gset_prefix) - success = StoreQueryTuple(results); - else if (pset.gexec_flag) - success = ExecQueryTuples(results); - else if (pset.crosstab_flag) - success = PrintResultsInCrosstab(results); + if (last && pset.gset_prefix) + success = StoreQueryTuple(result); + else if (last && pset.gexec_flag) + success = ExecQueryTuples(result); + else if (last && pset.crosstab_flag) + success = PrintResultsInCrosstab(result); + else if (last || pset.show_all_results) + success = PrintQueryTuples(result, opt, printQueryFout); else - success = PrintQueryTuples(results); + success = true; + /* if it's INSERT/UPDATE/DELETE RETURNING, also print status */ - cmdstatus = PQcmdStatus(results); - if (strncmp(cmdstatus, "INSERT", 6) == 0 || - strncmp(cmdstatus, "UPDATE", 6) == 0 || - strncmp(cmdstatus, "DELETE", 6) == 0) - PrintQueryStatus(results); + if (last || pset.show_all_results) + { + cmdstatus = PQcmdStatus(result); + if (strncmp(cmdstatus, "INSERT", 6) == 0 || + strncmp(cmdstatus, "UPDATE", 6) == 0 || + strncmp(cmdstatus, "DELETE", 6) == 0) + PrintQueryStatus(result, printQueryFout); + } + break; case PGRES_COMMAND_OK: - PrintQueryStatus(results); + if (last || pset.show_all_results) + PrintQueryStatus(result, printQueryFout); success = true; break; @@ -1154,7 +1028,7 @@ PrintQueryResults(PGresult *results) case PGRES_COPY_OUT: case PGRES_COPY_IN: - /* nothing to do here */ + /* nothing to do here: already processed */ success = true; break; @@ -1167,15 +1041,232 @@ PrintQueryResults(PGresult *results) default: success = false; pg_log_error("unexpected PQresultStatus: %d", - PQresultStatus(results)); + PQresultStatus(result)); break; } - fflush(pset.queryFout); + fflush(printQueryFout ? printQueryFout : pset.queryFout); return success; } +/* + * Data structure and functions to record notices while they are + * emitted, so that they can be shown later. + * + * We need to know which result is last, which requires to extract + * one result in advance, hence two buffers are needed. + */ +typedef struct { + PQExpBufferData messages[2]; + int current; +} t_notice_messages; + +/* + * Store notices in appropriate buffer, for later display. + */ +static void +AppendNoticeMessage(void *arg, const char *msg) +{ + t_notice_messages *notes = (t_notice_messages*) arg; + appendPQExpBufferStr(¬es->messages[notes->current], msg); +} + +/* + * Show notices stored in buffer, which is then reset. + */ +static void +ShowNoticeMessage(t_notice_messages *notes) +{ + PQExpBufferData *current = ¬es->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 success; + instr_time before; + PGresult *result; + t_notice_messages notes; + + if (pset.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(¬es.messages[0]); + initPQExpBuffer(¬es.messages[1]); + PQsetNoticeProcessor(pset.db, AppendNoticeMessage, ¬es); + + /* first result */ + result = PQgetResult(pset.db); + + while (result != NULL) + { + ExecStatusType result_status; + PGresult *next_result; + bool last; + + if (!AcceptResult(result, false)) + { + /* + * Some error occured, either a server-side failure or + * a failure to submit the command string. Record that. + */ + const char *error = PQerrorMessage(pset.db); + + ShowNoticeMessage(¬es); + if (strlen(error)) + pg_log_info("%s", error); + CheckConnection(); + if (!is_watch) + SetResultVariables(result, false); + ClearOrSaveResult(result); + success = false; + + /* and switch to next result */ + result = PQgetResult(pset.db); + continue; + } + + 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(¬es); + + 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, ¬es); + } + + /* + * 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 && pset.timing) + { + instr_time now; + INSTR_TIME_SET_CURRENT(now); + INSTR_TIME_SUBTRACT(now, before); + *pelapsed_msec = INSTR_TIME_GET_MILLISEC(now); + } + + /* notices already shown above for copy */ + ShowNoticeMessage(¬es); + + /* 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(¬es.messages[0]); + termPQExpBuffer(¬es.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 @@ -1192,10 +1283,11 @@ PrintQueryResults(PGresult *results) bool SendQuery(const char *query) { - PGresult *results; + PGresult *results = NULL; PGTransactionStatusType transaction_status; double elapsed_msec = 0; bool OK = false; + bool res_error; int i; bool on_error_rollback_savepoint = false; static bool on_error_rollback_warning = false; @@ -1237,26 +1329,32 @@ SendQuery(const char *query) fflush(pset.logfile); } + /* global query cancellation for this query */ SetCancelConn(pset.db); transaction_status = PQtransactionStatus(pset.db); + /* issue a BEGIN if needed, corresponding COMMIT/ROLLBACK by user */ if (transaction_status == PQTRANS_IDLE && !pset.autocommit && !command_no_begin(query)) { - results = PQexec(pset.db, "BEGIN"); - if (PQresultStatus(results) != PGRES_COMMAND_OK) + PGresult *result; + result = PQexec(pset.db, "BEGIN"); + res_error = PQresultStatus(result) != PGRES_COMMAND_OK; + ClearOrSaveResult(result); + + if (res_error) { pg_log_info("%s", PQerrorMessage(pset.db)); - ClearOrSaveResult(results); - ResetCancelConn(); goto sendquery_cleanup; } - ClearOrSaveResult(results); + + /* must be PQTRANS_INTRANS after a BEGIN */ transaction_status = PQtransactionStatus(pset.db); } + /* create automatic savepoint if needed and possible */ if (transaction_status == PQTRANS_INTRANS && pset.on_error_rollback != PSQL_ERROR_ROLLBACK_OFF && (pset.cur_cmd_interactive || @@ -1273,58 +1371,40 @@ SendQuery(const char *query) } else { - results = PQexec(pset.db, "SAVEPOINT pg_psql_temporary_savepoint"); - if (PQresultStatus(results) != PGRES_COMMAND_OK) + PGresult *result; + result = PQexec(pset.db, "SAVEPOINT pg_psql_temporary_savepoint"); + res_error = PQresultStatus(result) != PGRES_COMMAND_OK; + ClearOrSaveResult(result); + + if (res_error) { pg_log_info("%s", PQerrorMessage(pset.db)); - ClearOrSaveResult(results); - ResetCancelConn(); goto sendquery_cleanup; } - ClearOrSaveResult(results); + on_error_rollback_savepoint = true; } } + /* process the query, one way or the other */ if (pset.gdesc_flag) { /* Describe query's result columns, without executing it */ OK = DescribeQuery(query, &elapsed_msec); - ResetCancelConn(); results = NULL; /* PQclear(NULL) does nothing */ } else if (pset.fetch_count <= 0 || pset.gexec_flag || pset.crosstab_flag || !is_select_command(query)) { /* Default fetch-it-all-and-print mode */ - instr_time before, - after; - - if (pset.timing) - INSTR_TIME_SET_CURRENT(before); - - results = PQexec(pset.db, query); - - /* these operations are included in the timing result: */ - ResetCancelConn(); - OK = ProcessResult(&results); - - if (pset.timing) - { - INSTR_TIME_SET_CURRENT(after); - INSTR_TIME_SUBTRACT(after, before); - elapsed_msec = INSTR_TIME_GET_MILLISEC(after); - } - - /* but printing results isn't: */ - if (OK && results) - OK = PrintQueryResults(results); + int res = SendQueryAndProcessResults(query, &elapsed_msec, false, NULL, NULL); + OK = (res >= 0); + /* do NOT overwrite results here, for on_error_rollback_savepoint */ } else { /* Fetch-in-segments mode */ OK = ExecQueryUsingCursor(query, &elapsed_msec); - ResetCancelConn(); results = NULL; /* PQclear(NULL) does nothing */ } @@ -1383,14 +1463,15 @@ SendQuery(const char *query) PGresult *svptres; svptres = PQexec(pset.db, svptcmd); - if (PQresultStatus(svptres) != PGRES_COMMAND_OK) + res_error = PQresultStatus(svptres) != PGRES_COMMAND_OK; + + if (res_error) { pg_log_info("%s", PQerrorMessage(pset.db)); ClearOrSaveResult(svptres); OK = false; PQclear(results); - ResetCancelConn(); goto sendquery_cleanup; } PQclear(svptres); @@ -1421,6 +1502,9 @@ SendQuery(const char *query) sendquery_cleanup: + /* global cancellation reset */ + ResetCancelConn(); + /* reset \g's output-to-filename trigger */ if (pset.gfname) { @@ -1500,7 +1584,7 @@ DescribeQuery(const char *query, double *elapsed_msec) PQclear(results); results = PQdescribePrepared(pset.db, ""); - OK = AcceptResult(results) && + OK = AcceptResult(results, true) && (PQresultStatus(results) == PGRES_COMMAND_OK); if (OK && results) { @@ -1548,7 +1632,7 @@ DescribeQuery(const char *query, double *elapsed_msec) PQclear(results); results = PQexec(pset.db, buf.data); - OK = AcceptResult(results); + OK = AcceptResult(results, true); if (pset.timing) { @@ -1558,7 +1642,7 @@ DescribeQuery(const char *query, double *elapsed_msec) } if (OK && results) - OK = PrintQueryResults(results); + OK = HandleQueryResult(results, true, false, NULL, NULL); termPQExpBuffer(&buf); } @@ -1617,7 +1701,7 @@ ExecQueryUsingCursor(const char *query, double *elapsed_msec) if (PQtransactionStatus(pset.db) == PQTRANS_IDLE) { results = PQexec(pset.db, "BEGIN"); - OK = AcceptResult(results) && + OK = AcceptResult(results, true) && (PQresultStatus(results) == PGRES_COMMAND_OK); ClearOrSaveResult(results); if (!OK) @@ -1631,7 +1715,7 @@ ExecQueryUsingCursor(const char *query, double *elapsed_msec) query); results = PQexec(pset.db, buf.data); - OK = AcceptResult(results) && + OK = AcceptResult(results, true) && (PQresultStatus(results) == PGRES_COMMAND_OK); if (!OK) SetResultVariables(results, OK); @@ -1704,7 +1788,7 @@ ExecQueryUsingCursor(const char *query, double *elapsed_msec) is_pager = false; } - OK = AcceptResult(results); + OK = AcceptResult(results, true); Assert(!OK); SetResultVariables(results, OK); ClearOrSaveResult(results); @@ -1813,7 +1897,7 @@ cleanup: results = PQexec(pset.db, "CLOSE _psql_cursor"); if (OK) { - OK = AcceptResult(results) && + OK = AcceptResult(results, true) && (PQresultStatus(results) == PGRES_COMMAND_OK); ClearOrSaveResult(results); } @@ -1823,7 +1907,7 @@ cleanup: if (started_txn) { results = PQexec(pset.db, OK ? "COMMIT" : "ROLLBACK"); - OK &= AcceptResult(results) && + OK &= AcceptResult(results, true) && (PQresultStatus(results) == PGRES_COMMAND_OK); ClearOrSaveResult(results); } diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c index d3fda67edd..46b4a33a5b 100644 --- a/src/bin/psql/help.c +++ b/src/bin/psql/help.c @@ -412,6 +412,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 83f2e6f254..62583ad6ca 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 2931530f33..0afd97cf8f 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); @@ -1149,6 +1150,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) { @@ -1250,6 +1257,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/020_cancel.pl b/src/bin/psql/t/020_cancel.pl new file mode 100644 index 0000000000..06bad74161 --- /dev/null +++ b/src/bin/psql/t/020_cancel.pl @@ -0,0 +1,39 @@ +use strict; +use warnings; + +use PostgresNode; +use TestLib; +use Test::More tests => 2; + +my $tempdir = TestLib::tempdir; + +my $node = get_new_node('main'); +$node->init; +$node->start; + +# test query canceling by sending SIGINT to a running psql +SKIP: { + skip "cancel test requires a Unix shell", 2 if $windows_os; + + # use a clean environment + local %ENV = $node->_get_env(); + + # send a kill after 1 second + local $SIG{ALRM} = sub { + my $psql_pid = TestLib::slurp_file("$tempdir/psql.pid"); + kill 'INT', $psql_pid; + }; + alarm 1; + + # run psql to export its pid and wait for the kill + my $stdin = "\\! echo \$PPID >$tempdir/psql.pid\nselect pg_sleep(5);"; + my ($stdout, $stderr); + my $result = IPC::Run::run(['psql', '-v', 'ON_ERROR_STOP=1'], '<', \$stdin, '>', \$stdout, '2>', \$stderr); + + # query must have been interrupted + ok(!$result, 'query failed'); + like($stderr, qr/canceling statement due to user request/, 'query was canceled'); +} + +$node->stop(); +done_testing(); diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c index d6bf725971..ffe54754a6 100644 --- a/src/bin/psql/tab-complete.c +++ b/src/bin/psql/tab-complete.c @@ -4130,7 +4130,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 1b2f6bc418..71e3b1e9fe 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -5179,3 +5179,243 @@ List of access methods pg_catalog | && | anyarray | anyarray | boolean | overlaps (1 row) +-- +-- combined queries +-- +CREATE FUNCTION warn(msg TEXT) RETURNS BOOLEAN LANGUAGE plpgsql +AS $$ + BEGIN RAISE NOTICE 'warn %', msg ; RETURN TRUE ; END +$$; +-- show both +SELECT 1 AS one \; SELECT warn('1.5') \; SELECT 2 AS two ; + one +----- + 1 +(1 row) + +NOTICE: warn 1.5 +CONTEXT: PL/pgSQL function warn(text) line 2 at RAISE + warn +------ + t +(1 row) + + two +----- + 2 +(1 row) + +-- \gset applies to last query only +SELECT 3 AS three \; SELECT warn('3.5') \; SELECT 4 AS four \gset + three +------- + 3 +(1 row) + +NOTICE: warn 3.5 +CONTEXT: PL/pgSQL function warn(text) line 2 at RAISE + warn +------ + t +(1 row) + +\echo :three :four +:three 4 +-- syntax error stops all processing +SELECT 5 \; SELECT 6 + \; SELECT warn('6.5') \; SELECT 7 ; +ERROR: syntax error at or near ";" +LINE 1: SELECT 5 ; SELECT 6 + ; SELECT warn('6.5') ; SELECT 7 ; + ^ +-- with aborted transaction, stop on first error +BEGIN \; SELECT 8 AS eight \; SELECT 9/0 AS nine \; ROLLBACK \; SELECT 10 AS ten ; + eight +------- + 8 +(1 row) + +ERROR: division by zero +-- close previously aborted transaction +ROLLBACK; +-- misc SQL commands +-- (non SELECT output is sent to stderr, thus is not shown in expected results) +SELECT 'ok' AS "begin" \; +CREATE TABLE psql_comics(s TEXT) \; +INSERT INTO psql_comics VALUES ('Calvin'), ('hobbes') \; +COPY psql_comics FROM STDIN \; +UPDATE psql_comics SET s = 'Hobbes' WHERE s = 'hobbes' \; +DELETE FROM psql_comics WHERE s = 'Moe' \; +COPY psql_comics TO STDOUT \; +TRUNCATE psql_comics \; +DROP TABLE psql_comics \; +SELECT 'ok' AS "done" ; + begin +------- + ok +(1 row) + +Calvin +Susie +Hobbes + done +------ + ok +(1 row) + +\set SHOW_ALL_RESULTS off +SELECT 1 AS one \; SELECT warn('1.5') \; SELECT 2 AS two ; +NOTICE: warn 1.5 +CONTEXT: PL/pgSQL function warn(text) line 2 at RAISE + two +----- + 2 +(1 row) + +\set SHOW_ALL_RESULTS on +DROP FUNCTION warn(TEXT); +-- +-- autocommit +-- +\echo '# initial AUTOCOMMIT:' :AUTOCOMMIT +# initial AUTOCOMMIT: on +\set AUTOCOMMIT off +\echo '# AUTOCOMMIT:' :AUTOCOMMIT +# AUTOCOMMIT: off +-- implicit BEGIN +CREATE TABLE foo(s TEXT); +ROLLBACK; +CREATE TABLE foo(s TEXT); +INSERT INTO foo(s) VALUES ('hello'), ('world'); +COMMIT; +DROP TABLE foo; +ROLLBACK; +SELECT * FROM foo ORDER BY 1; + s +------- + hello + world +(2 rows) + +DROP TABLE foo; +COMMIT; +\set AUTOCOMMIT on +\echo '# AUTOCOMMIT:' :AUTOCOMMIT +# AUTOCOMMIT: on +-- explicit BEGIN +BEGIN; +CREATE TABLE foo(s TEXT); +INSERT INTO foo(s) VALUES ('hello'), ('world'); +COMMIT; +BEGIN; +DROP TABLE foo; +ROLLBACK; +-- implicit transactions +SELECT * FROM foo ORDER BY 1; + s +------- + hello + world +(2 rows) + +DROP TABLE foo; +\echo '# final AUTOCOMMIT:' :AUTOCOMMIT +# final AUTOCOMMIT: on +-- +-- test ON_ERROR_ROLLBACK +-- +CREATE FUNCTION psql_error(msg TEXT) RETURNS BOOLEAN AS $$ + BEGIN + RAISE EXCEPTION 'error %', msg; + END; +$$ LANGUAGE plpgsql; +\echo '# initial ON_ERROR_ROLLBACK:' :ON_ERROR_ROLLBACK +# initial ON_ERROR_ROLLBACK: off +\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 +-- combined queries +INSERT INTO bla VALUES ('Rosalyn') \; -- will rollback +SELECT 'before error' AS show \; -- will show! + SELECT psql_error('boum!') \; -- failure + SELECT 'after error' AS noshow; -- hidden by preceeding error + 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 * FROM bla ORDER BY 1; + s +--------------- + Calvin + Dad + Hobbes + Miss Wormwood + Susie +(5 rows) + +-- reset +\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 61862d595d..be1db0d5c0 100644 --- a/src/test/regress/expected/transactions.out +++ b/src/test/regress/expected/transactions.out @@ -900,8 +900,18 @@ DROP TABLE abc; -- tests rely on the fact that psql will not break SQL commands apart at a -- backslash-quoted semicolon, but will send them as one Query. create temp table i_table (f1 int); --- psql will show only the last result in a multi-statement Query +-- psql will show all results of a multi-statement Query SELECT 1\; SELECT 2\; SELECT 3; + ?column? +---------- + 1 +(1 row) + + ?column? +---------- + 2 +(1 row) + ?column? ---------- 3 @@ -916,6 +926,12 @@ insert into i_table values(1)\; select * from i_table; -- 1/0 error will cause rolling back the whole implicit transaction insert into i_table values(2)\; select * from i_table\; select 1/0; + f1 +---- + 1 + 2 +(2 rows) + ERROR: division by zero select * from i_table; f1 @@ -935,8 +951,18 @@ WARNING: there is no transaction in progress -- begin converts implicit transaction into a regular one that -- can extend past the end of the Query select 1\; begin\; insert into i_table values(5); + ?column? +---------- + 1 +(1 row) + commit; select 1\; begin\; insert into i_table values(6); + ?column? +---------- + 1 +(1 row) + rollback; -- commit in implicit-transaction state commits but issues a warning. insert into i_table values(7)\; commit\; insert into i_table values(8)\; select 1/0; @@ -963,22 +989,52 @@ rollback; -- we are not in a transaction at this point WARNING: there is no transaction in progress -- implicit transaction block is still a transaction block, for e.g. VACUUM SELECT 1\; VACUUM; + ?column? +---------- + 1 +(1 row) + ERROR: VACUUM cannot run inside a transaction block SELECT 1\; COMMIT\; VACUUM; WARNING: there is no transaction in progress + ?column? +---------- + 1 +(1 row) + ERROR: VACUUM cannot run inside a transaction block -- we disallow savepoint-related commands in implicit-transaction state SELECT 1\; SAVEPOINT sp; + ?column? +---------- + 1 +(1 row) + ERROR: SAVEPOINT can only be used in transaction blocks SELECT 1\; COMMIT\; SAVEPOINT sp; WARNING: there is no transaction in progress + ?column? +---------- + 1 +(1 row) + ERROR: SAVEPOINT can only be used in transaction blocks ROLLBACK TO SAVEPOINT sp\; SELECT 2; ERROR: ROLLBACK TO SAVEPOINT can only be used in transaction blocks SELECT 2\; RELEASE SAVEPOINT sp\; SELECT 3; + ?column? +---------- + 2 +(1 row) + ERROR: RELEASE SAVEPOINT can only be used in transaction blocks -- but this is OK, because the BEGIN converts it to a regular xact SELECT 1\; BEGIN\; SAVEPOINT sp\; ROLLBACK TO SAVEPOINT sp\; COMMIT; + ?column? +---------- + 1 +(1 row) + -- Tests for AND CHAIN in implicit transaction blocks SET TRANSACTION READ ONLY\; COMMIT AND CHAIN; -- error ERROR: COMMIT AND CHAIN can only be used in transaction blocks diff --git a/src/test/regress/sql/copyselect.sql b/src/test/regress/sql/copyselect.sql index 1d98dad3c8..e32a4f8e38 100644 --- a/src/test/regress/sql/copyselect.sql +++ b/src/test/regress/sql/copyselect.sql @@ -84,10 +84,10 @@ drop table test1; -- psql handling of COPY in multi-command strings copy (select 1) to stdout\; select 1/0; -- row, then error select 1/0\; copy (select 1) to stdout; -- error only -copy (select 1) to stdout\; copy (select 2) to stdout\; select 0\; select 3; -- 1 2 3 +copy (select 1) to stdout\; copy (select 2) to stdout\; select 3\; select 4; -- 1 2 3 4 create table test3 (c int); -select 0\; copy test3 from stdin\; copy test3 from stdin\; select 1; -- 1 +select 0\; copy test3 from stdin\; copy test3 from stdin\; select 1; -- 0 1 1 \. 2 diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql index 68121d171c..b2f3f32404 100644 --- a/src/test/regress/sql/psql.sql +++ b/src/test/regress/sql/psql.sql @@ -1241,3 +1241,123 @@ drop role regress_partitioning_role; \dfa bit* small* \do - pg_catalog.int4 \do && anyarray * + +-- +-- combined queries +-- +CREATE FUNCTION warn(msg TEXT) RETURNS BOOLEAN LANGUAGE plpgsql +AS $$ + BEGIN RAISE NOTICE 'warn %', msg ; RETURN TRUE ; END +$$; +-- show both +SELECT 1 AS one \; SELECT warn('1.5') \; SELECT 2 AS two ; +-- \gset applies to last query only +SELECT 3 AS three \; SELECT warn('3.5') \; SELECT 4 AS four \gset +\echo :three :four +-- syntax error stops all processing +SELECT 5 \; SELECT 6 + \; SELECT warn('6.5') \; SELECT 7 ; +-- with aborted transaction, stop on first error +BEGIN \; SELECT 8 AS eight \; SELECT 9/0 AS nine \; ROLLBACK \; SELECT 10 AS ten ; +-- close previously aborted transaction +ROLLBACK; +-- misc SQL commands +-- (non SELECT output is sent to stderr, thus is not shown in expected results) +SELECT 'ok' AS "begin" \; +CREATE TABLE psql_comics(s TEXT) \; +INSERT INTO psql_comics VALUES ('Calvin'), ('hobbes') \; +COPY psql_comics FROM STDIN \; +UPDATE psql_comics SET s = 'Hobbes' WHERE s = 'hobbes' \; +DELETE FROM psql_comics WHERE s = 'Moe' \; +COPY psql_comics TO STDOUT \; +TRUNCATE psql_comics \; +DROP TABLE psql_comics \; +SELECT 'ok' AS "done" ; +Moe +Susie +\. +\set SHOW_ALL_RESULTS off +SELECT 1 AS one \; SELECT warn('1.5') \; SELECT 2 AS two ; +\set SHOW_ALL_RESULTS on +DROP FUNCTION warn(TEXT); + +-- +-- autocommit +-- +\echo '# initial AUTOCOMMIT:' :AUTOCOMMIT +\set AUTOCOMMIT off +\echo '# AUTOCOMMIT:' :AUTOCOMMIT +-- implicit BEGIN +CREATE TABLE foo(s TEXT); +ROLLBACK; +CREATE TABLE foo(s TEXT); +INSERT INTO foo(s) VALUES ('hello'), ('world'); +COMMIT; +DROP TABLE foo; +ROLLBACK; +SELECT * FROM foo ORDER BY 1; +DROP TABLE foo; +COMMIT; +\set AUTOCOMMIT on +\echo '# AUTOCOMMIT:' :AUTOCOMMIT +-- explicit BEGIN +BEGIN; +CREATE TABLE foo(s TEXT); +INSERT INTO foo(s) VALUES ('hello'), ('world'); +COMMIT; +BEGIN; +DROP TABLE foo; +ROLLBACK; +-- implicit transactions +SELECT * FROM foo ORDER BY 1; +DROP TABLE foo; +\echo '# final AUTOCOMMIT:' :AUTOCOMMIT + +-- +-- test ON_ERROR_ROLLBACK +-- +CREATE FUNCTION psql_error(msg TEXT) RETURNS BOOLEAN AS $$ + BEGIN + RAISE EXCEPTION 'error %', msg; + END; +$$ LANGUAGE plpgsql; +\echo '# initial ON_ERROR_ROLLBACK:' :ON_ERROR_ROLLBACK +\set ON_ERROR_ROLLBACK on +\echo '# ON_ERROR_ROLLBACK:' :ON_ERROR_ROLLBACK +\echo '# AUTOCOMMIT:' :AUTOCOMMIT +BEGIN; +CREATE TABLE bla(s NO_SUCH_TYPE); -- fails +CREATE TABLE bla(s TEXT); -- succeeds +SELECT psql_error('oops!'); -- fails +INSERT INTO bla VALUES ('Calvin'), ('Hobbes'); +COMMIT; +SELECT * FROM bla ORDER BY 1; +BEGIN; +INSERT INTO bla VALUES ('Susie'); -- succeeds +-- combined queries +INSERT INTO bla VALUES ('Rosalyn') \; -- will rollback +SELECT 'before error' AS show \; -- will show! + SELECT psql_error('boum!') \; -- failure + SELECT 'after error' AS noshow; -- hidden by preceeding error +INSERT INTO bla(s) VALUES ('Moe') \; -- will rollback + SELECT psql_error('bam!'); +INSERT INTO bla VALUES ('Miss Wormwood'); -- succeeds +COMMIT; +SELECT * FROM bla ORDER BY 1; +-- some with autocommit off +\set AUTOCOMMIT off +\echo '# AUTOCOMMIT:' :AUTOCOMMIT +-- implicit BEGIN +INSERT INTO bla VALUES ('Dad'); -- succeeds +SELECT psql_error('bad!'); -- implicit partial rollback +INSERT INTO bla VALUES ('Mum') \; -- will rollback +SELECT COUNT(*) AS "#mum" +FROM bla WHERE s = 'Mum' \; -- but be counted here +SELECT psql_error('bad!'); -- implicit partial rollback +COMMIT; +SELECT * FROM bla ORDER BY 1; +-- reset +\set AUTOCOMMIT on +\set ON_ERROR_ROLLBACK off +\echo '# final ON_ERROR_ROLLBACK:' :ON_ERROR_ROLLBACK +DROP TABLE bla; +DROP FUNCTION psql_error; diff --git a/src/test/regress/sql/transactions.sql b/src/test/regress/sql/transactions.sql index 8886280c0a..7fc9f09468 100644 --- a/src/test/regress/sql/transactions.sql +++ b/src/test/regress/sql/transactions.sql @@ -504,7 +504,7 @@ DROP TABLE abc; create temp table i_table (f1 int); --- psql will show only the last result in a multi-statement Query +-- psql will show all results of a multi-statement Query SELECT 1\; SELECT 2\; SELECT 3; -- this implicitly commits: