Hello Andres,
The reason this test constantly fails on cfbot windows is a use-after-free bug.
Indeed! Thanks a lot for the catch and the debug!The ClearOrSaveResult function is quite annoying because it may or may not clear the result as a side effect.
Attached v14 moves the status extraction before the possible clear. I've added a couple of results = NULL after such calls in the code.
-- Fabien.
diff --git a/contrib/pg_stat_statements/expected/pg_stat_statements.out b/contrib/pg_stat_statements/expected/pg_stat_statements.out index e0abe34bb6..8f7f93172a 100644 --- a/contrib/pg_stat_statements/expected/pg_stat_statements.out +++ b/contrib/pg_stat_statements/expected/pg_stat_statements.out @@ -50,8 +50,28 @@ BEGIN \; SELECT 2.0 AS "float" \; SELECT 'world' AS "text" \; COMMIT; + float +------- + 2.0 +(1 row) + + text +------- + world +(1 row) + -- compound with empty statements and spurious leading spacing \;\; SELECT 3 + 3 \;\;\; SELECT ' ' || ' !' \;\; SELECT 1 + 4 \;; + ?column? +---------- + 6 +(1 row) + + ?column? +---------- + ! +(1 row) + ?column? ---------- 5 @@ -61,6 +81,11 @@ COMMIT; SELECT 1 + 1 + 1 AS "add" \gset SELECT :add + 1 + 1 AS "add" \; SELECT :add + 1 + 1 AS "add" \gset + add +----- + 5 +(1 row) + -- set operator SELECT 1 AS i UNION SELECT 2 ORDER BY i; i diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 1ab200a4ad..0a22850912 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -127,18 +127,11 @@ echo '\x \\ SELECT * FROM foo;' | psql commands included in the string to divide it into multiple transactions. (See <xref linkend="protocol-flow-multi-statement"/> for more details about how the server handles multi-query strings.) - Also, <application>psql</application> only prints the - result of the last <acronym>SQL</acronym> command in the string. - This is different from the behavior when the same string is read from - a file or fed to <application>psql</application>'s standard input, - because then <application>psql</application> sends - each <acronym>SQL</acronym> command separately. </para> <para> - Because of this behavior, putting more than one SQL command in a - single <option>-c</option> string often has unexpected results. - It's better to use repeated <option>-c</option> commands or feed - multiple commands to <application>psql</application>'s standard input, + If having several commands executed in one transaction is not desired, + use repeated <option>-c</option> commands or feed multiple commands to + <application>psql</application>'s standard input, either using <application>echo</application> as illustrated above, or via a shell here-document, for example: <programlisting> @@ -3570,10 +3563,6 @@ select 1\; select 2\; select 3; commands included in the string to divide it into multiple transactions. (See <xref linkend="protocol-flow-multi-statement"/> for more details about how the server handles multi-query strings.) - <application>psql</application> prints only the last query result - it receives for each request; in this example, although all - three <command>SELECT</command>s are indeed executed, <application>psql</application> - only prints the <literal>3</literal>. </para> </listitem> </varlistentry> @@ -4160,6 +4149,18 @@ bar </varlistentry> <varlistentry> + <term><varname>SHOW_ALL_RESULTS</varname></term> + <listitem> + <para> + When this variable is set to <literal>off</literal>, only the last + result of a combined query (<literal>\;</literal>) is shown instead of + all of them. The default is <literal>on</literal>. The off behavior + is for compatibility with older versions of psql. + </para> + </listitem> + </varlistentry> + + <varlistentry> <term><varname>SHOW_CONTEXT</varname></term> <listitem> <para> diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c index 3503605a7d..47eabcbb8e 100644 --- a/src/bin/psql/common.c +++ b/src/bin/psql/common.c @@ -34,6 +34,8 @@ static bool DescribeQuery(const char *query, double *elapsed_msec); static bool ExecQueryUsingCursor(const char *query, double *elapsed_msec); static bool command_no_begin(const char *query); static bool is_select_command(const char *query); +static int SendQueryAndProcessResults(const char *query, double *pelapsed_msec, + bool is_watch, const printQueryOpt *opt, FILE *printQueryFout, bool *tx_ended); /* @@ -354,7 +356,7 @@ CheckConnection(void) * Returns true for valid result, false for error state. */ static bool -AcceptResult(const PGresult *result) +AcceptResult(const PGresult *result, bool show_error) { bool OK; @@ -385,7 +387,7 @@ AcceptResult(const PGresult *result) break; } - if (!OK) + if (!OK && show_error) { const char *error = PQerrorMessage(pset.db); @@ -473,6 +475,18 @@ ClearOrSaveResult(PGresult *result) } } +/* + * Consume all results + */ +static void +ClearOrSaveAllResults() +{ + PGresult *result; + + while ((result = PQgetResult(pset.db)) != NULL) + ClearOrSaveResult(result); +} + /* * Print microtiming output. Always print raw milliseconds; if the interval @@ -573,7 +587,7 @@ PSQLexec(const char *query) ResetCancelConn(); - if (!AcceptResult(res)) + if (!AcceptResult(res, true)) { ClearOrSaveResult(res); res = NULL; @@ -596,11 +610,8 @@ int PSQLexecWatch(const char *query, const printQueryOpt *opt, FILE *printQueryFout) { bool timing = pset.timing; - PGresult *res; double elapsed_msec = 0; - instr_time before; - instr_time after; - FILE *fout; + int res; if (!pset.db) { @@ -609,77 +620,14 @@ PSQLexecWatch(const char *query, const printQueryOpt *opt, FILE *printQueryFout) } SetCancelConn(pset.db); - - if (timing) - INSTR_TIME_SET_CURRENT(before); - - res = PQexec(pset.db, query); - + res = SendQueryAndProcessResults(query, &elapsed_msec, true, opt, printQueryFout, NULL); ResetCancelConn(); - if (!AcceptResult(res)) - { - ClearOrSaveResult(res); - return 0; - } - - if (timing) - { - INSTR_TIME_SET_CURRENT(after); - INSTR_TIME_SUBTRACT(after, before); - elapsed_msec = INSTR_TIME_GET_MILLISEC(after); - } - - /* - * If SIGINT is sent while the query is processing, the interrupt will be - * consumed. The user's intention, though, is to cancel the entire watch - * process, so detect a sent cancellation request and exit in this case. - */ - if (cancel_pressed) - { - PQclear(res); - return 0; - } - - fout = printQueryFout ? printQueryFout : pset.queryFout; - - switch (PQresultStatus(res)) - { - case PGRES_TUPLES_OK: - printQuery(res, opt, fout, false, pset.logfile); - break; - - case PGRES_COMMAND_OK: - fprintf(fout, "%s\n%s\n\n", opt->title, PQcmdStatus(res)); - break; - - case PGRES_EMPTY_QUERY: - pg_log_error("\\watch cannot be used with an empty query"); - PQclear(res); - return -1; - - case PGRES_COPY_OUT: - case PGRES_COPY_IN: - case PGRES_COPY_BOTH: - pg_log_error("\\watch cannot be used with COPY"); - PQclear(res); - return -1; - - default: - pg_log_error("unexpected result status for \\watch"); - PQclear(res); - return -1; - } - - PQclear(res); - - fflush(fout); - /* Possible microtiming output */ if (timing) PrintTiming(elapsed_msec); - return 1; + return res; } @@ -714,7 +662,7 @@ PrintNotifications(void) * Returns true if successful, false otherwise. */ static bool -PrintQueryTuples(const PGresult *results) +PrintQueryTuples(const PGresult *results, const printQueryOpt *opt, FILE *printQueryFout) { bool result = true; @@ -746,8 +694,9 @@ PrintQueryTuples(const PGresult *results) } else { - printQuery(results, &pset.popt, pset.queryFout, false, pset.logfile); - if (ferror(pset.queryFout)) + FILE *fout = printQueryFout ? printQueryFout : pset.queryFout; + printQuery(results, opt ? opt : &pset.popt, fout, false, pset.logfile); + if (ferror(fout)) { pg_log_error("could not print result table: %m"); result = false; @@ -892,213 +841,131 @@ loop_exit: /* - * ProcessResult: utility function for use by SendQuery() only - * - * When our command string contained a COPY FROM STDIN or COPY TO STDOUT, - * PQexec() has stopped at the PGresult associated with the first such - * command. In that event, we'll marshal data for the COPY and then cycle - * through any subsequent PGresult objects. - * - * When the command string contained no such COPY command, this function - * degenerates to an AcceptResult() call. - * - * Changes its argument to point to the last PGresult of the command string, - * or NULL if that result was for a COPY TO STDOUT. (Returning NULL prevents - * the command status from being printed, which we want in that case so that - * the status line doesn't get taken as part of the COPY data.) - * - * Returns true on complete success, false otherwise. Possible failure modes - * include purely client-side problems; check the transaction status for the - * server-side opinion. + * Marshal the COPY data. Either subroutine will get the + * connection out of its COPY state, then call PQresultStatus() + * once and report any error. Return whether all was ok. + * + * For COPY OUT, direct the output to pset.copyStream if it's set, + * otherwise to pset.gfname if it's set, otherwise to queryFout. + * For COPY IN, use pset.copyStream as data source if it's set, + * otherwise cur_cmd_source. + * + * Update result if further processing is necessary, or NULL otherwise. + * Return a result when queryFout can safely output a result status: + * on COPY IN, or on COPY OUT if written to something other than pset.queryFout. + * Returning NULL prevents the command status from being printed, which + * we want if the status line doesn't get taken as part of the COPY data. */ static bool -ProcessResult(PGresult **results) +HandleCopyResult(PGresult **result) { - bool success = true; - bool first_cycle = true; + bool success; + FILE *copystream; + PGresult *copy_result; + ExecStatusType result_status = PQresultStatus(*result); - for (;;) + Assert(result_status == PGRES_COPY_OUT || + result_status == PGRES_COPY_IN); + + SetCancelConn(pset.db); + + if (result_status == PGRES_COPY_OUT) { - ExecStatusType result_status; - bool is_copy; - PGresult *next_result; + bool need_close = false; + bool is_pipe = false; - if (!AcceptResult(*results)) + if (pset.copyStream) { - /* - * Failure at this point is always a server-side failure or a - * failure to submit the command string. Either way, we're - * finished with this command string. - */ - success = false; - break; + /* invoked by \copy */ + copystream = pset.copyStream; } - - result_status = PQresultStatus(*results); - switch (result_status) + else if (pset.gfname) { - case PGRES_EMPTY_QUERY: - case PGRES_COMMAND_OK: - case PGRES_TUPLES_OK: - is_copy = false; - break; - - case PGRES_COPY_OUT: - case PGRES_COPY_IN: - is_copy = true; - break; - - default: - /* AcceptResult() should have caught anything else. */ - is_copy = false; - pg_log_error("unexpected PQresultStatus: %d", result_status); - break; - } - - if (is_copy) - { - /* - * Marshal the COPY data. Either subroutine will get the - * connection out of its COPY state, then call PQresultStatus() - * once and report any error. - * - * For COPY OUT, direct the output to pset.copyStream if it's set, - * otherwise to pset.gfname if it's set, otherwise to queryFout. - * For COPY IN, use pset.copyStream as data source if it's set, - * otherwise cur_cmd_source. - */ - FILE *copystream; - PGresult *copy_result; - - SetCancelConn(pset.db); - if (result_status == PGRES_COPY_OUT) + /* invoked by \g */ + if (openQueryOutputFile(pset.gfname, + ©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) @@ -1110,43 +977,50 @@ PrintQueryStatus(PGresult *results) /* - * PrintQueryResults: print out (or store or execute) query results as required - * - * Note: Utility function for use by SendQuery() only. + * HandleQueryResult: print out, store or execute one query result + * as required. * * Returns true if the query executed successfully, false otherwise. */ static bool -PrintQueryResults(PGresult *results) +HandleQueryResult(PGresult *result, bool last, bool is_watch, const printQueryOpt *opt, FILE *printQueryFout) { bool success; const char *cmdstatus; - if (!results) + if (result == NULL) return false; - switch (PQresultStatus(results)) + switch (PQresultStatus(result)) { case PGRES_TUPLES_OK: /* store or execute or print the data ... */ - if (pset.gset_prefix) - success = StoreQueryTuple(results); - else if (pset.gexec_flag) - success = ExecQueryTuples(results); - else if (pset.crosstab_flag) - success = PrintResultsInCrosstab(results); + if (last && pset.gset_prefix) + success = StoreQueryTuple(result); + else if (last && pset.gexec_flag) + success = ExecQueryTuples(result); + else if (last && pset.crosstab_flag) + success = PrintResultsInCrosstab(result); + else if (last || pset.show_all_results) + success = PrintQueryTuples(result, opt, printQueryFout); else - success = PrintQueryTuples(results); + success = true; + /* if it's INSERT/UPDATE/DELETE RETURNING, also print status */ - cmdstatus = PQcmdStatus(results); - if (strncmp(cmdstatus, "INSERT", 6) == 0 || - strncmp(cmdstatus, "UPDATE", 6) == 0 || - strncmp(cmdstatus, "DELETE", 6) == 0) - PrintQueryStatus(results); + if (last || pset.show_all_results) + { + cmdstatus = PQcmdStatus(result); + if (strncmp(cmdstatus, "INSERT", 6) == 0 || + strncmp(cmdstatus, "UPDATE", 6) == 0 || + strncmp(cmdstatus, "DELETE", 6) == 0) + PrintQueryStatus(result, printQueryFout); + } + break; case PGRES_COMMAND_OK: - PrintQueryStatus(results); + if (last || pset.show_all_results) + PrintQueryStatus(result, printQueryFout); success = true; break; @@ -1156,7 +1030,7 @@ PrintQueryResults(PGresult *results) case PGRES_COPY_OUT: case PGRES_COPY_IN: - /* nothing to do here */ + /* nothing to do here: already processed */ success = true; break; @@ -1169,15 +1043,263 @@ PrintQueryResults(PGresult *results) default: success = false; pg_log_error("unexpected PQresultStatus: %d", - PQresultStatus(results)); + PQresultStatus(result)); break; } - fflush(pset.queryFout); + fflush(printQueryFout ? printQueryFout : pset.queryFout); return success; } +/* + * Data structure and functions to record notices while they are + * emitted, so that they can be shown later. + * + * We need to know which result is last, which requires to extract + * one result in advance, hence two buffers are needed. + */ +typedef struct { + PQExpBufferData messages[2]; + int current; +} t_notice_messages; + +/* + * Store notices in appropriate buffer, for later display. + */ +static void +AppendNoticeMessage(void *arg, const char *msg) +{ + t_notice_messages *notes = (t_notice_messages*) arg; + appendPQExpBufferStr(¬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 *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(¬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); + + /* + * On connection loss another result with a message will be + * generated, we do not want to see this error again. + */ + PQclearErrorMessage(pset.db); + } + CheckConnection(); + if (!is_watch) + SetResultVariables(result, false); + + /* keep the result status before clearing it */ + result_status = PQresultStatus(result); + ClearOrSaveResult(result); + result = NULL; + success = false; + + /* switch to next result */ + if (result_status == PGRES_COPY_BOTH || + result_status == PGRES_COPY_OUT || + result_status == PGRES_COPY_IN) + /* + * For some obscure reason PQgetResult does *not* return a NULL in copy + * cases despite the result having been cleared, but keeps returning an + * "empty" result that we have to ignore manually. + */ + ; + else + result = PQgetResult(pset.db); + + continue; + } + else if (tx_ended != NULL && ! *tx_ended) + { + /* on success, tell whether the "current" transaction sequence ended */ + const char *cmd = PQcmdStatus(result); + *tx_ended = strcmp(cmd, "COMMIT") == 0 || strcmp(cmd, "ROLLBACK") == 0 || + strcmp(cmd, "SAVEPOINT") == 0 || strcmp(cmd, "RELEASE") == 0; + } + + result_status = PQresultStatus(result); + + /* must handle COPY before changing the current result */ + Assert(result_status != PGRES_COPY_BOTH); + if (result_status == PGRES_COPY_IN || + result_status == PGRES_COPY_OUT) + { + ShowNoticeMessage(¬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 && 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 @@ -1195,12 +1317,14 @@ bool SendQuery(const char *query) { bool timing = pset.timing; - PGresult *results; + PGresult *results = NULL; PGTransactionStatusType transaction_status; double elapsed_msec = 0; bool OK = false; + bool res_error; int i; bool on_error_rollback_savepoint = false; + bool tx_ended = false; if (!pset.db) { @@ -1239,82 +1363,71 @@ SendQuery(const char *query) fflush(pset.logfile); } + /* global query cancellation for this query */ SetCancelConn(pset.db); transaction_status = PQtransactionStatus(pset.db); + /* issue a BEGIN if needed, corresponding COMMIT/ROLLBACK by user */ if (transaction_status == PQTRANS_IDLE && !pset.autocommit && !command_no_begin(query)) { - results = PQexec(pset.db, "BEGIN"); - if (PQresultStatus(results) != PGRES_COMMAND_OK) + PGresult *result; + result = PQexec(pset.db, "BEGIN"); + res_error = PQresultStatus(result) != PGRES_COMMAND_OK; + ClearOrSaveResult(result); + result = NULL; + + if (res_error) { pg_log_info("%s", PQerrorMessage(pset.db)); - ClearOrSaveResult(results); - ResetCancelConn(); goto sendquery_cleanup; } - ClearOrSaveResult(results); + + /* must be PQTRANS_INTRANS after a BEGIN */ transaction_status = PQtransactionStatus(pset.db); } + /* create automatic savepoint if needed and possible */ if (transaction_status == PQTRANS_INTRANS && pset.on_error_rollback != PSQL_ERROR_ROLLBACK_OFF && (pset.cur_cmd_interactive || pset.on_error_rollback == PSQL_ERROR_ROLLBACK_ON)) { - results = PQexec(pset.db, "SAVEPOINT pg_psql_temporary_savepoint"); - if (PQresultStatus(results) != PGRES_COMMAND_OK) + PGresult *result; + result = PQexec(pset.db, "SAVEPOINT pg_psql_temporary_savepoint"); + res_error = PQresultStatus(result) != PGRES_COMMAND_OK; + ClearOrSaveResult(result); + result = NULL; + + if (res_error) { pg_log_info("%s", PQerrorMessage(pset.db)); - ClearOrSaveResult(results); - ResetCancelConn(); goto sendquery_cleanup; } - ClearOrSaveResult(results); + on_error_rollback_savepoint = true; } + /* process the query, one way or the other */ if (pset.gdesc_flag) { /* Describe query's result columns, without executing it */ OK = DescribeQuery(query, &elapsed_msec); - ResetCancelConn(); results = NULL; /* PQclear(NULL) does nothing */ } else if (pset.fetch_count <= 0 || pset.gexec_flag || pset.crosstab_flag || !is_select_command(query)) { /* Default fetch-it-all-and-print mode */ - instr_time before, - after; - - if (timing) - INSTR_TIME_SET_CURRENT(before); - - results = PQexec(pset.db, query); - - /* these operations are included in the timing result: */ - ResetCancelConn(); - OK = ProcessResult(&results); - - if (timing) - { - INSTR_TIME_SET_CURRENT(after); - INSTR_TIME_SUBTRACT(after, before); - elapsed_msec = INSTR_TIME_GET_MILLISEC(after); - } - - /* but printing results isn't: */ - if (OK && results) - OK = PrintQueryResults(results); + int res = SendQueryAndProcessResults(query, &elapsed_msec, false, NULL, NULL, &tx_ended); + OK = (res >= 0); } else { /* Fetch-in-segments mode */ OK = ExecQueryUsingCursor(query, &elapsed_msec); - ResetCancelConn(); results = NULL; /* PQclear(NULL) does nothing */ } @@ -1347,11 +1460,7 @@ SendQuery(const char *query) * savepoint is gone. If they issued a SAVEPOINT, releasing * ours would remove theirs. */ - if (results && - (strcmp(PQcmdStatus(results), "COMMIT") == 0 || - strcmp(PQcmdStatus(results), "SAVEPOINT") == 0 || - strcmp(PQcmdStatus(results), "RELEASE") == 0 || - strcmp(PQcmdStatus(results), "ROLLBACK") == 0)) + if (tx_ended) svptcmd = NULL; else svptcmd = "RELEASE pg_psql_temporary_savepoint"; @@ -1373,14 +1482,15 @@ SendQuery(const char *query) PGresult *svptres; svptres = PQexec(pset.db, svptcmd); - if (PQresultStatus(svptres) != PGRES_COMMAND_OK) + res_error = PQresultStatus(svptres) != PGRES_COMMAND_OK; + + if (res_error) { pg_log_info("%s", PQerrorMessage(pset.db)); ClearOrSaveResult(svptres); OK = false; PQclear(results); - ResetCancelConn(); goto sendquery_cleanup; } PQclear(svptres); @@ -1411,6 +1521,9 @@ SendQuery(const char *query) sendquery_cleanup: + /* global cancellation reset */ + ResetCancelConn(); + /* reset \g's output-to-filename trigger */ if (pset.gfname) { @@ -1491,7 +1604,7 @@ DescribeQuery(const char *query, double *elapsed_msec) PQclear(results); results = PQdescribePrepared(pset.db, ""); - OK = AcceptResult(results) && + OK = AcceptResult(results, true) && (PQresultStatus(results) == PGRES_COMMAND_OK); if (OK && results) { @@ -1539,7 +1652,7 @@ DescribeQuery(const char *query, double *elapsed_msec) PQclear(results); results = PQexec(pset.db, buf.data); - OK = AcceptResult(results); + OK = AcceptResult(results, true); if (timing) { @@ -1549,7 +1662,7 @@ DescribeQuery(const char *query, double *elapsed_msec) } if (OK && results) - OK = PrintQueryResults(results); + OK = HandleQueryResult(results, true, false, NULL, NULL); termPQExpBuffer(&buf); } @@ -1609,7 +1722,7 @@ ExecQueryUsingCursor(const char *query, double *elapsed_msec) if (PQtransactionStatus(pset.db) == PQTRANS_IDLE) { results = PQexec(pset.db, "BEGIN"); - OK = AcceptResult(results) && + OK = AcceptResult(results, true) && (PQresultStatus(results) == PGRES_COMMAND_OK); ClearOrSaveResult(results); if (!OK) @@ -1623,7 +1736,7 @@ ExecQueryUsingCursor(const char *query, double *elapsed_msec) query); results = PQexec(pset.db, buf.data); - OK = AcceptResult(results) && + OK = AcceptResult(results, true) && (PQresultStatus(results) == PGRES_COMMAND_OK); if (!OK) SetResultVariables(results, OK); @@ -1696,7 +1809,7 @@ ExecQueryUsingCursor(const char *query, double *elapsed_msec) is_pager = false; } - OK = AcceptResult(results); + OK = AcceptResult(results, true); Assert(!OK); SetResultVariables(results, OK); ClearOrSaveResult(results); @@ -1805,7 +1918,7 @@ cleanup: results = PQexec(pset.db, "CLOSE _psql_cursor"); if (OK) { - OK = AcceptResult(results) && + OK = AcceptResult(results, true) && (PQresultStatus(results) == PGRES_COMMAND_OK); ClearOrSaveResult(results); } @@ -1815,7 +1928,7 @@ cleanup: if (started_txn) { results = PQexec(pset.db, OK ? "COMMIT" : "ROLLBACK"); - OK &= AcceptResult(results) && + OK &= AcceptResult(results, true) && (PQresultStatus(results) == PGRES_COMMAND_OK); ClearOrSaveResult(results); } diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c index 937d6e9d49..82504258d0 100644 --- a/src/bin/psql/help.c +++ b/src/bin/psql/help.c @@ -413,6 +413,8 @@ helpVariables(unsigned short int pager) fprintf(output, _(" SERVER_VERSION_NAME\n" " SERVER_VERSION_NUM\n" " server's version (in short string or numeric format)\n")); + fprintf(output, _(" SHOW_ALL_RESULTS\n" + " show all results of a combined query (\\;) instead of only the last\n")); fprintf(output, _(" SHOW_CONTEXT\n" " controls display of message context fields [never, errors, always]\n")); fprintf(output, _(" SINGLELINE\n" diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h index f614b26e2c..57bddec5a5 100644 --- a/src/bin/psql/settings.h +++ b/src/bin/psql/settings.h @@ -148,6 +148,7 @@ typedef struct _psqlSettings const char *prompt2; const char *prompt3; PGVerbosity verbosity; /* current error verbosity level */ + bool show_all_results; PGContextVisibility show_context; /* current context display level */ } PsqlSettings; diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c index be9dec749d..d08b15886a 100644 --- a/src/bin/psql/startup.c +++ b/src/bin/psql/startup.c @@ -203,6 +203,7 @@ main(int argc, char *argv[]) SetVariable(pset.vars, "PROMPT1", DEFAULT_PROMPT1); SetVariable(pset.vars, "PROMPT2", DEFAULT_PROMPT2); SetVariable(pset.vars, "PROMPT3", DEFAULT_PROMPT3); + SetVariableBool(pset.vars, "SHOW_ALL_RESULTS"); parse_psql_options(argc, argv, &options); @@ -1150,6 +1151,12 @@ verbosity_hook(const char *newval) return true; } +static bool +show_all_results_hook(const char *newval) +{ + return ParseVariableBool(newval, "SHOW_ALL_RESULTS", &pset.show_all_results); +} + static char * show_context_substitute_hook(char *newval) { @@ -1251,6 +1258,9 @@ EstablishVariableSpace(void) SetVariableHooks(pset.vars, "VERBOSITY", verbosity_substitute_hook, verbosity_hook); + SetVariableHooks(pset.vars, "SHOW_ALL_RESULTS", + bool_substitute_hook, + show_all_results_hook); SetVariableHooks(pset.vars, "SHOW_CONTEXT", show_context_substitute_hook, show_context_hook); diff --git a/src/bin/psql/t/001_basic.pl b/src/bin/psql/t/001_basic.pl index 9e14dc71ff..77c60a0e7b 100644 --- a/src/bin/psql/t/001_basic.pl +++ b/src/bin/psql/t/001_basic.pl @@ -6,7 +6,8 @@ use warnings; use PostgreSQL::Test::Cluster; use PostgreSQL::Test::Utils; -use Test::More tests => 25; + +use Test::More tests => 29; program_help_ok('psql'); program_version_ok('psql'); @@ -80,3 +81,19 @@ psql_like( 'handling of unexpected PQresultStatus', 'START_REPLICATION 0/0', undef, qr/unexpected PQresultStatus: 8$/); + +# Test voluntary crash +my ($ret, $out, $err) = $node->psql( + 'postgres', + "SELECT 'before' AS running;\n" . + "SELECT pg_terminate_backend(pg_backend_pid());\n" . + "SELECT 'AFTER' AS not_running;\n"); + +is($ret, 2, "server stopped"); +like($out, qr/before/, "output before crash"); +ok($out !~ qr/AFTER/, "no output after crash"); +is($err, 'psql:<stdin>:2: FATAL: terminating connection due to administrator command +psql:<stdin>:2: server closed the connection unexpectedly + This probably means the server terminated abnormally + before or while processing the request. +psql:<stdin>:2: fatal: connection to server was lost', "expected error message"); diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c index 71f1a5c00d..9252bd57a2 100644 --- a/src/bin/psql/tab-complete.c +++ b/src/bin/psql/tab-complete.c @@ -4351,7 +4351,7 @@ psql_completion(const char *text, int start, int end) matches = complete_from_variables(text, "", "", false); else if (TailMatchesCS("\\set", MatchAny)) { - if (TailMatchesCS("AUTOCOMMIT|ON_ERROR_STOP|QUIET|" + if (TailMatchesCS("AUTOCOMMIT|ON_ERROR_STOP|QUIET|SHOW_ALL_RESULTS|" "SINGLELINE|SINGLESTEP")) COMPLETE_WITH_CS("on", "off"); else if (TailMatchesCS("COMP_KEYWORD_CASE")) diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index e8bcc88370..2a3d29aee5 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -186,3 +186,4 @@ PQpipelineStatus 183 PQsetTraceFlags 184 PQmblenBounded 185 PQsendFlushRequest 186 +PQclearErrorMessage 187 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 5fc16be849..5bf51154cc 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -6771,6 +6771,12 @@ PQerrorMessage(const PGconn *conn) return conn->errorMessage.data; } +void +PQclearErrorMessage(PGconn *conn) +{ + resetPQExpBuffer(&conn->errorMessage); +} + /* * In Windows, socket values are unsigned, and an invalid socket value * (INVALID_SOCKET) is ~0, which equals -1 in comparisons (with no compiler diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 20eb855abc..b73b44e817 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -347,6 +347,7 @@ extern const char *PQparameterStatus(const PGconn *conn, extern int PQprotocolVersion(const PGconn *conn); extern int PQserverVersion(const PGconn *conn); extern char *PQerrorMessage(const PGconn *conn); +extern void PQclearErrorMessage(PGconn *conn); extern int PQsocket(const PGconn *conn); extern int PQbackendPID(const PGconn *conn); extern PGpipelineStatus PQpipelineStatus(const PGconn *conn); diff --git a/src/test/regress/expected/copyselect.out b/src/test/regress/expected/copyselect.out index 72865fe1eb..bb9e026f91 100644 --- a/src/test/regress/expected/copyselect.out +++ b/src/test/regress/expected/copyselect.out @@ -126,7 +126,7 @@ copy (select 1) to stdout\; select 1/0; -- row, then error ERROR: division by zero select 1/0\; copy (select 1) to stdout; -- error only ERROR: division by zero -copy (select 1) to stdout\; copy (select 2) to stdout\; select 0\; select 3; -- 1 2 3 +copy (select 1) to stdout\; copy (select 2) to stdout\; select 3\; select 4; -- 1 2 3 4 1 2 ?column? @@ -134,8 +134,18 @@ copy (select 1) to stdout\; copy (select 2) to stdout\; select 0\; select 3; -- 3 (1 row) + ?column? +---------- + 4 +(1 row) + create table test3 (c int); -select 0\; copy test3 from stdin\; copy test3 from stdin\; select 1; -- 1 +select 0\; copy test3 from stdin\; copy test3 from stdin\; select 1; -- 0 1 + ?column? +---------- + 0 +(1 row) + ?column? ---------- 1 diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index 6428ebc507..428ee941b9 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -5290,3 +5290,129 @@ ERROR: relation "notexists" does not exist LINE 1: SELECT * FROM notexists; ^ STATEMENT: SELECT * FROM notexists; + one +----- + 1 +(1 row) + +NOTICE: warn 1.5 +CONTEXT: PL/pgSQL function warn(text) line 2 at RAISE + warn +------ + t +(1 row) + + two +----- + 2 +(1 row) + + three +------- + 3 +(1 row) + +NOTICE: warn 3.5 +CONTEXT: PL/pgSQL function warn(text) line 2 at RAISE + warn +------ + t +(1 row) + +:three 4 +ERROR: syntax error at or near ";" +LINE 1: SELECT 5 ; SELECT 6 + ; SELECT warn('6.5') ; SELECT 7 ; + ^ + eight +------- + 8 +(1 row) + +ERROR: division by zero + begin +------- + ok +(1 row) + +Calvin +Susie +Hobbes + done +------ + ok +(1 row) + +NOTICE: warn 1.5 +CONTEXT: PL/pgSQL function warn(text) line 2 at RAISE + two +----- + 2 +(1 row) + +# initial AUTOCOMMIT: on +# AUTOCOMMIT: off + s +------- + hello + world +(2 rows) + +# AUTOCOMMIT: on + s +------- + hello + world +(2 rows) + +# final AUTOCOMMIT: on +# initial ON_ERROR_ROLLBACK: off +# ON_ERROR_ROLLBACK: on +# AUTOCOMMIT: on +ERROR: type "no_such_type" does not exist +LINE 1: CREATE TABLE bla(s NO_SUCH_TYPE); + ^ +ERROR: error oops! +CONTEXT: PL/pgSQL function psql_error(text) line 3 at RAISE + s +-------- + Calvin + Hobbes +(2 rows) + + show +-------------- + before error +(1 row) + +ERROR: error boum! +CONTEXT: PL/pgSQL function psql_error(text) line 3 at RAISE +ERROR: error bam! +CONTEXT: PL/pgSQL function psql_error(text) line 3 at RAISE + s +--------------- + Calvin + Hobbes + Miss Wormwood + Susie +(4 rows) + +# AUTOCOMMIT: off +ERROR: error bad! +CONTEXT: PL/pgSQL function psql_error(text) line 3 at RAISE + #mum +------ + 1 +(1 row) + +ERROR: error bad! +CONTEXT: PL/pgSQL function psql_error(text) line 3 at RAISE + s +--------------- + Calvin + Dad + Hobbes + Miss Wormwood + Susie +(5 rows) + +# final ON_ERROR_ROLLBACK: off diff --git a/src/test/regress/expected/transactions.out b/src/test/regress/expected/transactions.out index 61862d595d..be1db0d5c0 100644 --- a/src/test/regress/expected/transactions.out +++ b/src/test/regress/expected/transactions.out @@ -900,8 +900,18 @@ DROP TABLE abc; -- tests rely on the fact that psql will not break SQL commands apart at a -- backslash-quoted semicolon, but will send them as one Query. create temp table i_table (f1 int); --- psql will show only the last result in a multi-statement Query +-- psql will show all results of a multi-statement Query SELECT 1\; SELECT 2\; SELECT 3; + ?column? +---------- + 1 +(1 row) + + ?column? +---------- + 2 +(1 row) + ?column? ---------- 3 @@ -916,6 +926,12 @@ insert into i_table values(1)\; select * from i_table; -- 1/0 error will cause rolling back the whole implicit transaction insert into i_table values(2)\; select * from i_table\; select 1/0; + f1 +---- + 1 + 2 +(2 rows) + ERROR: division by zero select * from i_table; f1 @@ -935,8 +951,18 @@ WARNING: there is no transaction in progress -- begin converts implicit transaction into a regular one that -- can extend past the end of the Query select 1\; begin\; insert into i_table values(5); + ?column? +---------- + 1 +(1 row) + commit; select 1\; begin\; insert into i_table values(6); + ?column? +---------- + 1 +(1 row) + rollback; -- commit in implicit-transaction state commits but issues a warning. insert into i_table values(7)\; commit\; insert into i_table values(8)\; select 1/0; @@ -963,22 +989,52 @@ rollback; -- we are not in a transaction at this point WARNING: there is no transaction in progress -- implicit transaction block is still a transaction block, for e.g. VACUUM SELECT 1\; VACUUM; + ?column? +---------- + 1 +(1 row) + ERROR: VACUUM cannot run inside a transaction block SELECT 1\; COMMIT\; VACUUM; WARNING: there is no transaction in progress + ?column? +---------- + 1 +(1 row) + ERROR: VACUUM cannot run inside a transaction block -- we disallow savepoint-related commands in implicit-transaction state SELECT 1\; SAVEPOINT sp; + ?column? +---------- + 1 +(1 row) + ERROR: SAVEPOINT can only be used in transaction blocks SELECT 1\; COMMIT\; SAVEPOINT sp; WARNING: there is no transaction in progress + ?column? +---------- + 1 +(1 row) + ERROR: SAVEPOINT can only be used in transaction blocks ROLLBACK TO SAVEPOINT sp\; SELECT 2; ERROR: ROLLBACK TO SAVEPOINT can only be used in transaction blocks SELECT 2\; RELEASE SAVEPOINT sp\; SELECT 3; + ?column? +---------- + 2 +(1 row) + ERROR: RELEASE SAVEPOINT can only be used in transaction blocks -- but this is OK, because the BEGIN converts it to a regular xact SELECT 1\; BEGIN\; SAVEPOINT sp\; ROLLBACK TO SAVEPOINT sp\; COMMIT; + ?column? +---------- + 1 +(1 row) + -- Tests for AND CHAIN in implicit transaction blocks SET TRANSACTION READ ONLY\; COMMIT AND CHAIN; -- error ERROR: COMMIT AND CHAIN can only be used in transaction blocks diff --git a/src/test/regress/sql/copyselect.sql b/src/test/regress/sql/copyselect.sql index 1d98dad3c8..e32a4f8e38 100644 --- a/src/test/regress/sql/copyselect.sql +++ b/src/test/regress/sql/copyselect.sql @@ -84,10 +84,10 @@ drop table test1; -- psql handling of COPY in multi-command strings copy (select 1) to stdout\; select 1/0; -- row, then error select 1/0\; copy (select 1) to stdout; -- error only -copy (select 1) to stdout\; copy (select 2) to stdout\; select 0\; select 3; -- 1 2 3 +copy (select 1) to stdout\; copy (select 2) to stdout\; select 3\; select 4; -- 1 2 3 4 create table test3 (c int); -select 0\; copy test3 from stdin\; copy test3 from stdin\; select 1; -- 1 +select 0\; copy test3 from stdin\; copy test3 from stdin\; select 1; -- 0 1 1 \. 2 diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql index d4e4fdbbb7..69521dc915 100644 --- a/src/test/regress/sql/psql.sql +++ b/src/test/regress/sql/psql.sql @@ -1316,3 +1316,123 @@ DROP TABLE oer_test; \set ECHO errors SELECT * FROM notexists; \set ECHO none + +-- +-- combined queries +-- +CREATE FUNCTION warn(msg TEXT) RETURNS BOOLEAN LANGUAGE plpgsql +AS $$ + BEGIN RAISE NOTICE 'warn %', msg ; RETURN TRUE ; END +$$; +-- show both +SELECT 1 AS one \; SELECT warn('1.5') \; SELECT 2 AS two ; +-- \gset applies to last query only +SELECT 3 AS three \; SELECT warn('3.5') \; SELECT 4 AS four \gset +\echo :three :four +-- syntax error stops all processing +SELECT 5 \; SELECT 6 + \; SELECT warn('6.5') \; SELECT 7 ; +-- with aborted transaction, stop on first error +BEGIN \; SELECT 8 AS eight \; SELECT 9/0 AS nine \; ROLLBACK \; SELECT 10 AS ten ; +-- close previously aborted transaction +ROLLBACK; +-- misc SQL commands +-- (non SELECT output is sent to stderr, thus is not shown in expected results) +SELECT 'ok' AS "begin" \; +CREATE TABLE psql_comics(s TEXT) \; +INSERT INTO psql_comics VALUES ('Calvin'), ('hobbes') \; +COPY psql_comics FROM STDIN \; +UPDATE psql_comics SET s = 'Hobbes' WHERE s = 'hobbes' \; +DELETE FROM psql_comics WHERE s = 'Moe' \; +COPY psql_comics TO STDOUT \; +TRUNCATE psql_comics \; +DROP TABLE psql_comics \; +SELECT 'ok' AS "done" ; +Moe +Susie +\. +\set SHOW_ALL_RESULTS off +SELECT 1 AS one \; SELECT warn('1.5') \; SELECT 2 AS two ; +\set SHOW_ALL_RESULTS on +DROP FUNCTION warn(TEXT); + +-- +-- autocommit +-- +\echo '# initial AUTOCOMMIT:' :AUTOCOMMIT +\set AUTOCOMMIT off +\echo '# AUTOCOMMIT:' :AUTOCOMMIT +-- implicit BEGIN +CREATE TABLE foo(s TEXT); +ROLLBACK; +CREATE TABLE foo(s TEXT); +INSERT INTO foo(s) VALUES ('hello'), ('world'); +COMMIT; +DROP TABLE foo; +ROLLBACK; +SELECT * FROM foo ORDER BY 1; +DROP TABLE foo; +COMMIT; +\set AUTOCOMMIT on +\echo '# AUTOCOMMIT:' :AUTOCOMMIT +-- explicit BEGIN +BEGIN; +CREATE TABLE foo(s TEXT); +INSERT INTO foo(s) VALUES ('hello'), ('world'); +COMMIT; +BEGIN; +DROP TABLE foo; +ROLLBACK; +-- implicit transactions +SELECT * FROM foo ORDER BY 1; +DROP TABLE foo; +\echo '# final AUTOCOMMIT:' :AUTOCOMMIT + +-- +-- test ON_ERROR_ROLLBACK +-- +CREATE FUNCTION psql_error(msg TEXT) RETURNS BOOLEAN AS $$ + BEGIN + RAISE EXCEPTION 'error %', msg; + END; +$$ LANGUAGE plpgsql; +\echo '# initial ON_ERROR_ROLLBACK:' :ON_ERROR_ROLLBACK +\set ON_ERROR_ROLLBACK on +\echo '# ON_ERROR_ROLLBACK:' :ON_ERROR_ROLLBACK +\echo '# AUTOCOMMIT:' :AUTOCOMMIT +BEGIN; +CREATE TABLE bla(s NO_SUCH_TYPE); -- fails +CREATE TABLE bla(s TEXT); -- succeeds +SELECT psql_error('oops!'); -- fails +INSERT INTO bla VALUES ('Calvin'), ('Hobbes'); +COMMIT; +SELECT * FROM bla ORDER BY 1; +BEGIN; +INSERT INTO bla VALUES ('Susie'); -- succeeds +-- combined queries +INSERT INTO bla VALUES ('Rosalyn') \; -- will rollback +SELECT 'before error' AS show \; -- will show! + SELECT psql_error('boum!') \; -- failure + SELECT 'after error' AS noshow; -- hidden by preceeding error +INSERT INTO bla(s) VALUES ('Moe') \; -- will rollback + SELECT psql_error('bam!'); +INSERT INTO bla VALUES ('Miss Wormwood'); -- succeeds +COMMIT; +SELECT * FROM bla ORDER BY 1; +-- some with autocommit off +\set AUTOCOMMIT off +\echo '# AUTOCOMMIT:' :AUTOCOMMIT +-- implicit BEGIN +INSERT INTO bla VALUES ('Dad'); -- succeeds +SELECT psql_error('bad!'); -- implicit partial rollback +INSERT INTO bla VALUES ('Mum') \; -- will rollback +SELECT COUNT(*) AS "#mum" +FROM bla WHERE s = 'Mum' \; -- but be counted here +SELECT psql_error('bad!'); -- implicit partial rollback +COMMIT; +SELECT * FROM bla ORDER BY 1; +-- reset +\set AUTOCOMMIT on +\set ON_ERROR_ROLLBACK off +\echo '# final ON_ERROR_ROLLBACK:' :ON_ERROR_ROLLBACK +DROP TABLE bla; +DROP FUNCTION psql_error; diff --git a/src/test/regress/sql/transactions.sql b/src/test/regress/sql/transactions.sql index 8886280c0a..7fc9f09468 100644 --- a/src/test/regress/sql/transactions.sql +++ b/src/test/regress/sql/transactions.sql @@ -504,7 +504,7 @@ DROP TABLE abc; create temp table i_table (f1 int); --- psql will show only the last result in a multi-statement Query +-- psql will show all results of a multi-statement Query SELECT 1\; SELECT 2\; SELECT 3; -- this implicitly commits: