I wrote: > ... PFA a patch > that invents a notion of an "implicit" transaction block.
On further consideration, I think the control logic I added in exec_simple_query() is a shade bogus. I set it up to only force an implicit transaction block when there are at least two statements remaining to execute. However, that has the result of allowing, eg, begin\; select 1\; commit\; vacuum; Now in principle it's perfectly OK to allow that, since the vacuum is alone in its transaction. But it feels more like an implementation artifact than a good design. The existing code doesn't allow it, and we might have a hard time duplicating this behavior if we ever significantly rewrote the transaction infrastructure. Plus I'd hate to have to explain it to users. I think we'd be better off enforcing transaction block restrictions on every statement in a multi-command string, regardless of the location of any COMMIT/ROLLBACK within the string. Hence, attached a v2 that does it like that. I also fully reverted 4f896dac1 by undoing its changes to PreventTransactionChain; other than that, the changes in xact.c are the same as before. regards, tom lane
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5e7e812..8b33676 100644 *** a/src/backend/access/transam/xact.c --- b/src/backend/access/transam/xact.c *************** typedef enum TBlockState *** 145,150 **** --- 145,151 ---- /* transaction block states */ TBLOCK_BEGIN, /* starting transaction block */ TBLOCK_INPROGRESS, /* live transaction */ + TBLOCK_IMPLICIT_INPROGRESS, /* live transaction after implicit BEGIN */ TBLOCK_PARALLEL_INPROGRESS, /* live transaction inside parallel worker */ TBLOCK_END, /* COMMIT received */ TBLOCK_ABORT, /* failed xact, awaiting ROLLBACK */ *************** StartTransactionCommand(void) *** 2700,2705 **** --- 2701,2707 ---- * previous CommitTransactionCommand.) */ case TBLOCK_INPROGRESS: + case TBLOCK_IMPLICIT_INPROGRESS: case TBLOCK_SUBINPROGRESS: break; *************** CommitTransactionCommand(void) *** 2790,2795 **** --- 2792,2798 ---- * counter and return. */ case TBLOCK_INPROGRESS: + case TBLOCK_IMPLICIT_INPROGRESS: case TBLOCK_SUBINPROGRESS: CommandCounterIncrement(); break; *************** AbortCurrentTransaction(void) *** 3014,3023 **** break; /* ! * if we aren't in a transaction block, we just do the basic abort ! * & cleanup transaction. */ case TBLOCK_STARTED: AbortTransaction(); CleanupTransaction(); s->blockState = TBLOCK_DEFAULT; --- 3017,3028 ---- break; /* ! * If we aren't in a transaction block, we just do the basic abort ! * & cleanup transaction. For this purpose, we treat an implicit ! * transaction block as if it were a simple statement. */ case TBLOCK_STARTED: + case TBLOCK_IMPLICIT_INPROGRESS: AbortTransaction(); CleanupTransaction(); s->blockState = TBLOCK_DEFAULT; *************** AbortCurrentTransaction(void) *** 3148,3156 **** * completes). Subtransactions are verboten too. * * isTopLevel: passed down from ProcessUtility to determine whether we are ! * inside a function or multi-query querystring. (We will always fail if ! * this is false, but it's convenient to centralize the check here instead of ! * making callers do it.) * stmtType: statement type name, for error messages. */ void --- 3153,3160 ---- * completes). Subtransactions are verboten too. * * isTopLevel: passed down from ProcessUtility to determine whether we are ! * inside a function. (We will always fail if this is false, but it's ! * convenient to centralize the check here instead of making callers do it.) * stmtType: statement type name, for error messages. */ void *************** PreventTransactionChain(bool isTopLevel, *** 3183,3190 **** ereport(ERROR, (errcode(ERRCODE_ACTIVE_SQL_TRANSACTION), /* translator: %s represents an SQL statement name */ ! errmsg("%s cannot be executed from a function or multi-command string", ! stmtType))); /* If we got past IsTransactionBlock test, should be in default state */ if (CurrentTransactionState->blockState != TBLOCK_DEFAULT && --- 3187,3193 ---- ereport(ERROR, (errcode(ERRCODE_ACTIVE_SQL_TRANSACTION), /* translator: %s represents an SQL statement name */ ! errmsg("%s cannot be executed from a function", stmtType))); /* If we got past IsTransactionBlock test, should be in default state */ if (CurrentTransactionState->blockState != TBLOCK_DEFAULT && *************** BeginTransactionBlock(void) *** 3429,3434 **** --- 3432,3446 ---- break; /* + * BEGIN converts an implicit transaction block to a regular one. + * (Note that we allow this even if we've already done some + * commands, which is a bit odd but matches historical practice.) + */ + case TBLOCK_IMPLICIT_INPROGRESS: + s->blockState = TBLOCK_BEGIN; + break; + + /* * Already a transaction block in progress. */ case TBLOCK_INPROGRESS: *************** PrepareTransactionBlock(char *gid) *** 3503,3509 **** * ignore case where we are not in a transaction; * EndTransactionBlock already issued a warning. */ ! Assert(s->blockState == TBLOCK_STARTED); /* Don't send back a PREPARE result tag... */ result = false; } --- 3515,3522 ---- * ignore case where we are not in a transaction; * EndTransactionBlock already issued a warning. */ ! Assert(s->blockState == TBLOCK_STARTED || ! s->blockState == TBLOCK_IMPLICIT_INPROGRESS); /* Don't send back a PREPARE result tag... */ result = false; } *************** EndTransactionBlock(void) *** 3542,3547 **** --- 3555,3572 ---- break; /* + * In an implicit transaction block, commit, but issue a warning + * because there was no explicit BEGIN before this. + */ + case TBLOCK_IMPLICIT_INPROGRESS: + ereport(WARNING, + (errcode(ERRCODE_NO_ACTIVE_SQL_TRANSACTION), + errmsg("there is no transaction in progress"))); + s->blockState = TBLOCK_END; + result = true; + break; + + /* * We are in a failed transaction block. Tell * CommitTransactionCommand it's time to exit the block. */ *************** UserAbortTransactionBlock(void) *** 3705,3712 **** --- 3730,3743 ---- * WARNING and go to abort state. The upcoming call to * CommitTransactionCommand() will then put us back into the * default state. + * + * We do the same thing with ABORT inside an implicit transaction, + * although in this case we might be rolling back actual database + * state changes. (It's debatable whether we should issue a + * WARNING in this case, but we have done so historically.) */ case TBLOCK_STARTED: + case TBLOCK_IMPLICIT_INPROGRESS: ereport(WARNING, (errcode(ERRCODE_NO_ACTIVE_SQL_TRANSACTION), errmsg("there is no transaction in progress"))); *************** UserAbortTransactionBlock(void) *** 3744,3749 **** --- 3775,3832 ---- } /* + * BeginImplicitTransactionBlock + * Start an implicit transaction block if we're not already in one. + * + * Unlike BeginTransactionBlock, this is called directly from the main loop + * in postgres.c, not within a Portal. So we can just change blockState + * without a lot of ceremony. We do not expect caller to do + * CommitTransactionCommand/StartTransactionCommand. + */ + void + BeginImplicitTransactionBlock(void) + { + TransactionState s = CurrentTransactionState; + + /* + * If we are in STARTED state (that is, no transaction block is open), + * switch to IMPLICIT_INPROGRESS state, creating an implicit transaction + * block. + * + * For caller convenience, we consider all other transaction states as + * legal here; otherwise the caller would need its own state check, which + * seems rather pointless. + */ + if (s->blockState == TBLOCK_STARTED) + s->blockState = TBLOCK_IMPLICIT_INPROGRESS; + } + + /* + * EndImplicitTransactionBlock + * End an implicit transaction block, if we're in one. + * + * Like EndTransactionBlock, we just make any needed blockState change here. + * The real work will be done in the upcoming CommitTransactionCommand(). + */ + void + EndImplicitTransactionBlock(void) + { + TransactionState s = CurrentTransactionState; + + /* + * If we are in IMPLICIT_INPROGRESS state, switch back to STARTED state, + * allowing CommitTransactionCommand to commit whatever happened during + * the implicit transaction block as though it were a single statement. + * + * For caller convenience, we consider all other transaction states as + * legal here; otherwise the caller would need its own state check, which + * seems rather pointless. + */ + if (s->blockState == TBLOCK_IMPLICIT_INPROGRESS) + s->blockState = TBLOCK_STARTED; + } + + /* * DefineSavepoint * This executes a SAVEPOINT command. */ *************** DefineSavepoint(char *name) *** 3780,3785 **** --- 3863,3877 ---- s->name = MemoryContextStrdup(TopTransactionContext, name); break; + case TBLOCK_IMPLICIT_INPROGRESS: + /* Disallow SAVEPOINT in an implicit transaction */ + ereport(ERROR, + (errcode(ERRCODE_NO_ACTIVE_SQL_TRANSACTION), + /* translator: %s represents an SQL statement name */ + errmsg("%s can only be used in transaction blocks", + "SAVEPOINT"))); + break; + /* These cases are invalid. */ case TBLOCK_DEFAULT: case TBLOCK_STARTED: *************** ReleaseSavepoint(List *options) *** 3834,3841 **** switch (s->blockState) { /* ! * We can't rollback to a savepoint if there is no savepoint ! * defined. */ case TBLOCK_INPROGRESS: ereport(ERROR, --- 3926,3932 ---- switch (s->blockState) { /* ! * We can't release a savepoint if there is no savepoint defined. */ case TBLOCK_INPROGRESS: ereport(ERROR, *************** ReleaseSavepoint(List *options) *** 3843,3848 **** --- 3934,3948 ---- errmsg("no such savepoint"))); break; + case TBLOCK_IMPLICIT_INPROGRESS: + /* Disallow RELEASE SAVEPOINT in an implicit transaction */ + ereport(ERROR, + (errcode(ERRCODE_NO_ACTIVE_SQL_TRANSACTION), + /* translator: %s represents an SQL statement name */ + errmsg("%s can only be used in transaction blocks", + "RELEASE SAVEPOINT"))); + break; + /* * We are in a non-aborted subtransaction. This is the only valid * case. *************** RollbackToSavepoint(List *options) *** 3957,3962 **** --- 4057,4071 ---- errmsg("no such savepoint"))); break; + case TBLOCK_IMPLICIT_INPROGRESS: + /* Disallow ROLLBACK TO SAVEPOINT in an implicit transaction */ + ereport(ERROR, + (errcode(ERRCODE_NO_ACTIVE_SQL_TRANSACTION), + /* translator: %s represents an SQL statement name */ + errmsg("%s can only be used in transaction blocks", + "ROLLBACK TO SAVEPOINT"))); + break; + /* * There is at least one savepoint, so proceed. */ *************** RollbackToSavepoint(List *options) *** 4046,4056 **** /* * BeginInternalSubTransaction * This is the same as DefineSavepoint except it allows TBLOCK_STARTED, ! * TBLOCK_END, and TBLOCK_PREPARE states, and therefore it can safely be ! * used in functions that might be called when not inside a BEGIN block ! * or when running deferred triggers at COMMIT/PREPARE time. Also, it ! * automatically does CommitTransactionCommand/StartTransactionCommand ! * instead of expecting the caller to do it. */ void BeginInternalSubTransaction(char *name) --- 4155,4166 ---- /* * BeginInternalSubTransaction * This is the same as DefineSavepoint except it allows TBLOCK_STARTED, ! * TBLOCK_IMPLICIT_INPROGRESS, TBLOCK_END, and TBLOCK_PREPARE states, ! * and therefore it can safely be used in functions that might be called ! * when not inside a BEGIN block or when running deferred triggers at ! * COMMIT/PREPARE time. Also, it automatically does ! * CommitTransactionCommand/StartTransactionCommand instead of expecting ! * the caller to do it. */ void BeginInternalSubTransaction(char *name) *************** BeginInternalSubTransaction(char *name) *** 4076,4081 **** --- 4186,4192 ---- { case TBLOCK_STARTED: case TBLOCK_INPROGRESS: + case TBLOCK_IMPLICIT_INPROGRESS: case TBLOCK_END: case TBLOCK_PREPARE: case TBLOCK_SUBINPROGRESS: *************** RollbackAndReleaseCurrentSubTransaction( *** 4180,4185 **** --- 4291,4297 ---- case TBLOCK_DEFAULT: case TBLOCK_STARTED: case TBLOCK_BEGIN: + case TBLOCK_IMPLICIT_INPROGRESS: case TBLOCK_PARALLEL_INPROGRESS: case TBLOCK_SUBBEGIN: case TBLOCK_INPROGRESS: *************** RollbackAndReleaseCurrentSubTransaction( *** 4211,4216 **** --- 4323,4329 ---- s = CurrentTransactionState; /* changed by pop */ AssertState(s->blockState == TBLOCK_SUBINPROGRESS || s->blockState == TBLOCK_INPROGRESS || + s->blockState == TBLOCK_IMPLICIT_INPROGRESS || s->blockState == TBLOCK_STARTED); } *************** AbortOutOfAnyTransaction(void) *** 4259,4264 **** --- 4372,4378 ---- case TBLOCK_STARTED: case TBLOCK_BEGIN: case TBLOCK_INPROGRESS: + case TBLOCK_IMPLICIT_INPROGRESS: case TBLOCK_PARALLEL_INPROGRESS: case TBLOCK_END: case TBLOCK_ABORT_PENDING: *************** TransactionBlockStatusCode(void) *** 4369,4374 **** --- 4483,4489 ---- case TBLOCK_BEGIN: case TBLOCK_SUBBEGIN: case TBLOCK_INPROGRESS: + case TBLOCK_IMPLICIT_INPROGRESS: case TBLOCK_PARALLEL_INPROGRESS: case TBLOCK_SUBINPROGRESS: case TBLOCK_END: *************** BlockStateAsString(TBlockState blockStat *** 5036,5041 **** --- 5151,5158 ---- return "BEGIN"; case TBLOCK_INPROGRESS: return "INPROGRESS"; + case TBLOCK_IMPLICIT_INPROGRESS: + return "IMPLICIT_INPROGRESS"; case TBLOCK_PARALLEL_INPROGRESS: return "PARALLEL_INPROGRESS"; case TBLOCK_END: diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 8d3fecf..c10d891 100644 *** a/src/backend/tcop/postgres.c --- b/src/backend/tcop/postgres.c *************** exec_simple_query(const char *query_stri *** 883,892 **** ListCell *parsetree_item; bool save_log_statement_stats = log_statement_stats; bool was_logged = false; ! bool isTopLevel; char msec_str[32]; - /* * Report query to various monitoring facilities. */ --- 883,891 ---- ListCell *parsetree_item; bool save_log_statement_stats = log_statement_stats; bool was_logged = false; ! bool use_implicit_block; char msec_str[32]; /* * Report query to various monitoring facilities. */ *************** exec_simple_query(const char *query_stri *** 947,959 **** MemoryContextSwitchTo(oldcontext); /* ! * We'll tell PortalRun it's a top-level command iff there's exactly one ! * raw parsetree. If more than one, it's effectively a transaction block ! * and we want PreventTransactionChain to reject unsafe commands. (Note: ! * we're assuming that query rewrite cannot add commands that are ! * significant to PreventTransactionChain.) */ ! isTopLevel = (list_length(parsetree_list) == 1); /* * Run through the raw parsetree(s) and process each one. --- 946,959 ---- MemoryContextSwitchTo(oldcontext); /* ! * For historical reasons, if multiple SQL statements are given in a ! * single "simple Query" message, we execute them as a single transaction, ! * unless explicit transaction control commands are included to make ! * portions of the list be separate transactions. To represent this ! * behavior properly in the transaction machinery, we use an "implicit" ! * transaction block. */ ! use_implicit_block = (list_length(parsetree_list) > 1); /* * Run through the raw parsetree(s) and process each one. *************** exec_simple_query(const char *query_stri *** 1001,1006 **** --- 1001,1016 ---- /* Make sure we are in a transaction command */ start_xact_command(); + /* + * If using an implicit transaction block, and we're not already in a + * transaction block, start an implicit block to force this statement + * to be grouped together with any following ones. (We must do this + * each time through the loop; otherwise, a COMMIT/ROLLBACK in the + * list would cause later statements to not be grouped.) + */ + if (use_implicit_block) + BeginImplicitTransactionBlock(); + /* If we got a cancel signal in parsing or prior command, quit */ CHECK_FOR_INTERRUPTS(); *************** exec_simple_query(const char *query_stri *** 1098,1104 **** */ (void) PortalRun(portal, FETCH_ALL, ! isTopLevel, true, receiver, receiver, --- 1108,1114 ---- */ (void) PortalRun(portal, FETCH_ALL, ! true, /* always top level */ true, receiver, receiver, *************** exec_simple_query(const char *query_stri *** 1108,1122 **** PortalDrop(portal, false); ! if (IsA(parsetree->stmt, TransactionStmt)) ! { ! /* ! * If this was a transaction control statement, commit it. We will ! * start a new xact command for the next command (if any). ! */ ! finish_xact_command(); ! } ! else if (lnext(parsetree_item) == NULL) { /* * If this is the last parsetree of the query string, close down --- 1118,1124 ---- PortalDrop(portal, false); ! if (lnext(parsetree_item) == NULL) { /* * If this is the last parsetree of the query string, close down *************** exec_simple_query(const char *query_stri *** 1124,1132 **** * is so that any end-of-transaction errors are reported before * the command-complete message is issued, to avoid confusing * clients who will expect either a command-complete message or an ! * error, not one and then the other. But for compatibility with ! * historical Postgres behavior, we do not force a transaction ! * boundary between queries appearing in a single query string. */ finish_xact_command(); } --- 1126,1143 ---- * is so that any end-of-transaction errors are reported before * the command-complete message is issued, to avoid confusing * clients who will expect either a command-complete message or an ! * error, not one and then the other. Also, if we're using an ! * implicit transaction block, we must close that out first. ! */ ! if (use_implicit_block) ! EndImplicitTransactionBlock(); ! finish_xact_command(); ! } ! else if (IsA(parsetree->stmt, TransactionStmt)) ! { ! /* ! * If this was a transaction control statement, commit it. We will ! * start a new xact command for the next command. */ finish_xact_command(); } *************** exec_simple_query(const char *query_stri *** 1149,1155 **** } /* end loop over parsetrees */ /* ! * Close down transaction statement, if one is open. */ finish_xact_command(); --- 1160,1168 ---- } /* end loop over parsetrees */ /* ! * Close down transaction statement, if one is open. (This will only do ! * something if the parsetree list was empty; otherwise the last loop ! * iteration already did it.) */ finish_xact_command(); diff --git a/src/include/access/xact.h b/src/include/access/xact.h index ad5aad9..f2c10f9 100644 *** a/src/include/access/xact.h --- b/src/include/access/xact.h *************** extern void BeginTransactionBlock(void); *** 352,357 **** --- 352,359 ---- extern bool EndTransactionBlock(void); extern bool PrepareTransactionBlock(char *gid); extern void UserAbortTransactionBlock(void); + extern void BeginImplicitTransactionBlock(void); + extern void EndImplicitTransactionBlock(void); extern void ReleaseSavepoint(List *options); extern void DefineSavepoint(char *name); extern void RollbackToSavepoint(List *options); diff --git a/src/test/regress/expected/transactions.out b/src/test/regress/expected/transactions.out index d9b702d..a7fdcf4 100644 *** a/src/test/regress/expected/transactions.out --- b/src/test/regress/expected/transactions.out *************** ERROR: portal "ctt" cannot be run *** 659,664 **** --- 659,748 ---- COMMIT; DROP FUNCTION create_temp_tab(); DROP FUNCTION invert(x float8); + -- Test assorted behaviors around the implicit transaction block created + -- when multiple SQL commands are sent in a single Query message. These + -- 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 + SELECT 1\; SELECT 2\; SELECT 3; + ?column? + ---------- + 3 + (1 row) + + -- this implicitly commits: + insert into i_table values(1)\; select * from i_table; + f1 + ---- + 1 + (1 row) + + -- 1/0 error will cause rolling back the whole implicit transaction + insert into i_table values(2)\; select * from i_table\; select 1/0; + ERROR: division by zero + select * from i_table; + f1 + ---- + 1 + (1 row) + + rollback; -- we are not in a transaction at this point + WARNING: there is no transaction in progress + -- can use regular begin/commit/rollback within a single Query + begin\; insert into i_table values(3)\; commit; + rollback; -- we are not in a transaction at this point + WARNING: there is no transaction in progress + begin\; insert into i_table values(4)\; rollback; + rollback; -- we are not in a transaction at this point + 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); + commit; + select 1\; begin\; insert into i_table values(6); + 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; + WARNING: there is no transaction in progress + ERROR: division by zero + -- similarly, rollback aborts but issues a warning. + insert into i_table values(9)\; rollback\; select 2; + WARNING: there is no transaction in progress + ?column? + ---------- + 2 + (1 row) + + select * from i_table; + f1 + ---- + 1 + 3 + 5 + 7 + (4 rows) + + 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; + ERROR: VACUUM cannot run inside a transaction block + SELECT 1\; COMMIT\; VACUUM; + WARNING: there is no transaction in progress + ERROR: VACUUM cannot run inside a transaction block + -- we disallow savepoint-related commands in implicit-transaction state + SELECT 1\; SAVEPOINT sp; + ERROR: SAVEPOINT can only be used in transaction blocks + SELECT 1\; COMMIT\; SAVEPOINT sp; + WARNING: there is no transaction in progress + 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; + 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; -- Test for successful cleanup of an aborted transaction at session exit. -- THIS MUST BE THE LAST TEST IN THIS FILE. begin; diff --git a/src/test/regress/sql/transactions.sql b/src/test/regress/sql/transactions.sql index bf9cb05..82661ab 100644 *** a/src/test/regress/sql/transactions.sql --- b/src/test/regress/sql/transactions.sql *************** DROP FUNCTION create_temp_tab(); *** 419,424 **** --- 419,478 ---- DROP FUNCTION invert(x float8); + -- Test assorted behaviors around the implicit transaction block created + -- when multiple SQL commands are sent in a single Query message. These + -- 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 + SELECT 1\; SELECT 2\; SELECT 3; + + -- this implicitly commits: + 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; + select * from i_table; + + rollback; -- we are not in a transaction at this point + + -- can use regular begin/commit/rollback within a single Query + begin\; insert into i_table values(3)\; commit; + rollback; -- we are not in a transaction at this point + begin\; insert into i_table values(4)\; rollback; + rollback; -- we are not in a transaction at this point + + -- 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); + commit; + select 1\; begin\; insert into i_table values(6); + 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; + -- similarly, rollback aborts but issues a warning. + insert into i_table values(9)\; rollback\; select 2; + + select * from i_table; + + rollback; -- we are not in a transaction at this point + + -- implicit transaction block is still a transaction block, for e.g. VACUUM + SELECT 1\; VACUUM; + SELECT 1\; COMMIT\; VACUUM; + + -- we disallow savepoint-related commands in implicit-transaction state + SELECT 1\; SAVEPOINT sp; + SELECT 1\; COMMIT\; SAVEPOINT sp; + ROLLBACK TO SAVEPOINT sp\; SELECT 2; + SELECT 2\; RELEASE SAVEPOINT sp\; SELECT 3; + + -- but this is OK, because the BEGIN converts it to a regular xact + SELECT 1\; BEGIN\; SAVEPOINT sp\; ROLLBACK TO SAVEPOINT sp\; COMMIT; + + -- Test for successful cleanup of an aborted transaction at session exit. -- THIS MUST BE THE LAST TEST IN THIS FILE.
-- Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org) To make changes to your subscription: http://www.postgresql.org/mailpref/pgsql-hackers