mirror of
https://github.com/postgres/postgres.git
synced 2025-07-05 07:21:24 +03:00
Fix handling of savepoint commands within multi-statement Query strings.
Issuing a savepoint-related command in a Query message that contains multiple SQL statements led to a FATAL exit with a complaint about "unexpected state STARTED". This is a shortcoming of commit4f896dac1
, which attempted to prevent such misbehaviors in multi-statement strings; its quick hack of marking the individual statements as "not top-level" does the wrong thing in this case, and isn't a very accurate description of the situation anyway. To fix, let's introduce into xact.c an explicit model of what happens for multi-statement Query strings. This is an "implicit transaction block in progress" state, which for many purposes works like the normal TBLOCK_INPROGRESS state --- in particular, IsTransactionBlock returns true, causing the desired result that PreventTransactionChain will throw error. But in case of error abort it works like TBLOCK_STARTED, allowing the transaction to be cancelled without need for an explicit ROLLBACK command. Commit4f896dac1
is reverted in toto, so that we go back to treating the individual statements as "top level". We could have left it as-is, but this allows sharpening the error message for PreventTransactionChain calls inside functions. Except for getting a normal error instead of a FATAL exit for savepoint commands, this patch should result in no user-visible behavioral change (other than that one error message rewording). There are some things we might want to do in the line of changing the appearance or wording of error and warning messages around this behavior, which would be much simpler to do now that it's an explicitly modeled state. But I haven't done them here. Although this fixes a long-standing bug, no backpatch. The consequences of the bug don't seem severe enough to justify the risk that this commit itself creates some new issue. Patch by me, but it owes something to previous investigation by Takayuki Tsunakawa, who also reported the bug in the first place. Also thanks to Michael Paquier for reviewing. Discussion: https://postgr.es/m/0A3221C70F24FB45833433255569204D1F6BE40D@G01JPEXMBYT05
This commit is contained in:
@ -145,6 +145,7 @@ typedef enum TBlockState
|
||||
/* 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 */
|
||||
@ -2700,6 +2701,7 @@ StartTransactionCommand(void)
|
||||
* previous CommitTransactionCommand.)
|
||||
*/
|
||||
case TBLOCK_INPROGRESS:
|
||||
case TBLOCK_IMPLICIT_INPROGRESS:
|
||||
case TBLOCK_SUBINPROGRESS:
|
||||
break;
|
||||
|
||||
@ -2790,6 +2792,7 @@ CommitTransactionCommand(void)
|
||||
* counter and return.
|
||||
*/
|
||||
case TBLOCK_INPROGRESS:
|
||||
case TBLOCK_IMPLICIT_INPROGRESS:
|
||||
case TBLOCK_SUBINPROGRESS:
|
||||
CommandCounterIncrement();
|
||||
break;
|
||||
@ -3014,10 +3017,12 @@ AbortCurrentTransaction(void)
|
||||
break;
|
||||
|
||||
/*
|
||||
* if we aren't in a transaction block, we just do the basic abort
|
||||
* & cleanup transaction.
|
||||
* 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;
|
||||
@ -3148,9 +3153,8 @@ AbortCurrentTransaction(void)
|
||||
* 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.)
|
||||
* 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
|
||||
@ -3183,8 +3187,7 @@ PreventTransactionChain(bool isTopLevel, const char *stmtType)
|
||||
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)));
|
||||
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 &&
|
||||
@ -3428,6 +3431,15 @@ BeginTransactionBlock(void)
|
||||
s->blockState = TBLOCK_BEGIN;
|
||||
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.
|
||||
*/
|
||||
@ -3503,7 +3515,8 @@ PrepareTransactionBlock(char *gid)
|
||||
* ignore case where we are not in a transaction;
|
||||
* EndTransactionBlock already issued a warning.
|
||||
*/
|
||||
Assert(s->blockState == TBLOCK_STARTED);
|
||||
Assert(s->blockState == TBLOCK_STARTED ||
|
||||
s->blockState == TBLOCK_IMPLICIT_INPROGRESS);
|
||||
/* Don't send back a PREPARE result tag... */
|
||||
result = false;
|
||||
}
|
||||
@ -3541,6 +3554,18 @@ EndTransactionBlock(void)
|
||||
result = true;
|
||||
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.
|
||||
@ -3705,8 +3730,14 @@ UserAbortTransactionBlock(void)
|
||||
* 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")));
|
||||
@ -3743,6 +3774,58 @@ UserAbortTransactionBlock(void)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 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.
|
||||
@ -3780,6 +3863,28 @@ DefineSavepoint(char *name)
|
||||
s->name = MemoryContextStrdup(TopTransactionContext, name);
|
||||
break;
|
||||
|
||||
/*
|
||||
* We disallow savepoint commands in implicit transaction blocks.
|
||||
* There would be no great difficulty in allowing them so far as
|
||||
* this module is concerned, but a savepoint seems inconsistent
|
||||
* with exec_simple_query's behavior of abandoning the whole query
|
||||
* string upon error. Also, the point of an implicit transaction
|
||||
* block (as opposed to a regular one) is to automatically close
|
||||
* after an error, so it's hard to see how a savepoint would fit
|
||||
* into that.
|
||||
*
|
||||
* The error messages for this are phrased as if there were no
|
||||
* active transaction block at all, which is historical but
|
||||
* perhaps could be improved.
|
||||
*/
|
||||
case TBLOCK_IMPLICIT_INPROGRESS:
|
||||
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:
|
||||
@ -3834,8 +3939,7 @@ ReleaseSavepoint(List *options)
|
||||
switch (s->blockState)
|
||||
{
|
||||
/*
|
||||
* We can't rollback to a savepoint if there is no savepoint
|
||||
* defined.
|
||||
* We can't release a savepoint if there is no savepoint defined.
|
||||
*/
|
||||
case TBLOCK_INPROGRESS:
|
||||
ereport(ERROR,
|
||||
@ -3843,6 +3947,15 @@ ReleaseSavepoint(List *options)
|
||||
errmsg("no such savepoint")));
|
||||
break;
|
||||
|
||||
case TBLOCK_IMPLICIT_INPROGRESS:
|
||||
/* See comment about implicit transactions in DefineSavepoint */
|
||||
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.
|
||||
@ -3957,6 +4070,15 @@ RollbackToSavepoint(List *options)
|
||||
errmsg("no such savepoint")));
|
||||
break;
|
||||
|
||||
case TBLOCK_IMPLICIT_INPROGRESS:
|
||||
/* See comment about implicit transactions in DefineSavepoint */
|
||||
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.
|
||||
*/
|
||||
@ -4046,11 +4168,12 @@ RollbackToSavepoint(List *options)
|
||||
/*
|
||||
* 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.
|
||||
* 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)
|
||||
@ -4076,6 +4199,7 @@ BeginInternalSubTransaction(char *name)
|
||||
{
|
||||
case TBLOCK_STARTED:
|
||||
case TBLOCK_INPROGRESS:
|
||||
case TBLOCK_IMPLICIT_INPROGRESS:
|
||||
case TBLOCK_END:
|
||||
case TBLOCK_PREPARE:
|
||||
case TBLOCK_SUBINPROGRESS:
|
||||
@ -4180,6 +4304,7 @@ RollbackAndReleaseCurrentSubTransaction(void)
|
||||
case TBLOCK_DEFAULT:
|
||||
case TBLOCK_STARTED:
|
||||
case TBLOCK_BEGIN:
|
||||
case TBLOCK_IMPLICIT_INPROGRESS:
|
||||
case TBLOCK_PARALLEL_INPROGRESS:
|
||||
case TBLOCK_SUBBEGIN:
|
||||
case TBLOCK_INPROGRESS:
|
||||
@ -4211,6 +4336,7 @@ RollbackAndReleaseCurrentSubTransaction(void)
|
||||
s = CurrentTransactionState; /* changed by pop */
|
||||
AssertState(s->blockState == TBLOCK_SUBINPROGRESS ||
|
||||
s->blockState == TBLOCK_INPROGRESS ||
|
||||
s->blockState == TBLOCK_IMPLICIT_INPROGRESS ||
|
||||
s->blockState == TBLOCK_STARTED);
|
||||
}
|
||||
|
||||
@ -4259,6 +4385,7 @@ AbortOutOfAnyTransaction(void)
|
||||
case TBLOCK_STARTED:
|
||||
case TBLOCK_BEGIN:
|
||||
case TBLOCK_INPROGRESS:
|
||||
case TBLOCK_IMPLICIT_INPROGRESS:
|
||||
case TBLOCK_PARALLEL_INPROGRESS:
|
||||
case TBLOCK_END:
|
||||
case TBLOCK_ABORT_PENDING:
|
||||
@ -4369,6 +4496,7 @@ TransactionBlockStatusCode(void)
|
||||
case TBLOCK_BEGIN:
|
||||
case TBLOCK_SUBBEGIN:
|
||||
case TBLOCK_INPROGRESS:
|
||||
case TBLOCK_IMPLICIT_INPROGRESS:
|
||||
case TBLOCK_PARALLEL_INPROGRESS:
|
||||
case TBLOCK_SUBINPROGRESS:
|
||||
case TBLOCK_END:
|
||||
@ -5036,6 +5164,8 @@ BlockStateAsString(TBlockState blockState)
|
||||
return "BEGIN";
|
||||
case TBLOCK_INPROGRESS:
|
||||
return "INPROGRESS";
|
||||
case TBLOCK_IMPLICIT_INPROGRESS:
|
||||
return "IMPLICIT_INPROGRESS";
|
||||
case TBLOCK_PARALLEL_INPROGRESS:
|
||||
return "PARALLEL_INPROGRESS";
|
||||
case TBLOCK_END:
|
||||
|
Reference in New Issue
Block a user