Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit 6eb52da

Browse files
committed
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 commit 4f896da, 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. Commit 4f896da 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
1 parent bfea925 commit 6eb52da

File tree

5 files changed

+320
-37
lines changed

5 files changed

+320
-37
lines changed

src/backend/access/transam/xact.c

+145-15
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ typedef enum TBlockState
145145
/* transaction block states */
146146
TBLOCK_BEGIN, /* starting transaction block */
147147
TBLOCK_INPROGRESS, /* live transaction */
148+
TBLOCK_IMPLICIT_INPROGRESS, /* live transaction after implicit BEGIN */
148149
TBLOCK_PARALLEL_INPROGRESS, /* live transaction inside parallel worker */
149150
TBLOCK_END, /* COMMIT received */
150151
TBLOCK_ABORT, /* failed xact, awaiting ROLLBACK */
@@ -2700,6 +2701,7 @@ StartTransactionCommand(void)
27002701
* previous CommitTransactionCommand.)
27012702
*/
27022703
case TBLOCK_INPROGRESS:
2704+
case TBLOCK_IMPLICIT_INPROGRESS:
27032705
case TBLOCK_SUBINPROGRESS:
27042706
break;
27052707

@@ -2790,6 +2792,7 @@ CommitTransactionCommand(void)
27902792
* counter and return.
27912793
*/
27922794
case TBLOCK_INPROGRESS:
2795+
case TBLOCK_IMPLICIT_INPROGRESS:
27932796
case TBLOCK_SUBINPROGRESS:
27942797
CommandCounterIncrement();
27952798
break;
@@ -3014,10 +3017,12 @@ AbortCurrentTransaction(void)
30143017
break;
30153018

30163019
/*
3017-
* if we aren't in a transaction block, we just do the basic abort
3018-
* & cleanup transaction.
3020+
* If we aren't in a transaction block, we just do the basic abort
3021+
* & cleanup transaction. For this purpose, we treat an implicit
3022+
* transaction block as if it were a simple statement.
30193023
*/
30203024
case TBLOCK_STARTED:
3025+
case TBLOCK_IMPLICIT_INPROGRESS:
30213026
AbortTransaction();
30223027
CleanupTransaction();
30233028
s->blockState = TBLOCK_DEFAULT;
@@ -3148,9 +3153,8 @@ AbortCurrentTransaction(void)
31483153
* completes). Subtransactions are verboten too.
31493154
*
31503155
* isTopLevel: passed down from ProcessUtility to determine whether we are
3151-
* inside a function or multi-query querystring. (We will always fail if
3152-
* this is false, but it's convenient to centralize the check here instead of
3153-
* making callers do it.)
3156+
* inside a function. (We will always fail if this is false, but it's
3157+
* convenient to centralize the check here instead of making callers do it.)
31543158
* stmtType: statement type name, for error messages.
31553159
*/
31563160
void
@@ -3183,8 +3187,7 @@ PreventTransactionChain(bool isTopLevel, const char *stmtType)
31833187
ereport(ERROR,
31843188
(errcode(ERRCODE_ACTIVE_SQL_TRANSACTION),
31853189
/* translator: %s represents an SQL statement name */
3186-
errmsg("%s cannot be executed from a function or multi-command string",
3187-
stmtType)));
3190+
errmsg("%s cannot be executed from a function", stmtType)));
31883191

31893192
/* If we got past IsTransactionBlock test, should be in default state */
31903193
if (CurrentTransactionState->blockState != TBLOCK_DEFAULT &&
@@ -3428,6 +3431,15 @@ BeginTransactionBlock(void)
34283431
s->blockState = TBLOCK_BEGIN;
34293432
break;
34303433

3434+
/*
3435+
* BEGIN converts an implicit transaction block to a regular one.
3436+
* (Note that we allow this even if we've already done some
3437+
* commands, which is a bit odd but matches historical practice.)
3438+
*/
3439+
case TBLOCK_IMPLICIT_INPROGRESS:
3440+
s->blockState = TBLOCK_BEGIN;
3441+
break;
3442+
34313443
/*
34323444
* Already a transaction block in progress.
34333445
*/
@@ -3503,7 +3515,8 @@ PrepareTransactionBlock(char *gid)
35033515
* ignore case where we are not in a transaction;
35043516
* EndTransactionBlock already issued a warning.
35053517
*/
3506-
Assert(s->blockState == TBLOCK_STARTED);
3518+
Assert(s->blockState == TBLOCK_STARTED ||
3519+
s->blockState == TBLOCK_IMPLICIT_INPROGRESS);
35073520
/* Don't send back a PREPARE result tag... */
35083521
result = false;
35093522
}
@@ -3541,6 +3554,18 @@ EndTransactionBlock(void)
35413554
result = true;
35423555
break;
35433556

3557+
/*
3558+
* In an implicit transaction block, commit, but issue a warning
3559+
* because there was no explicit BEGIN before this.
3560+
*/
3561+
case TBLOCK_IMPLICIT_INPROGRESS:
3562+
ereport(WARNING,
3563+
(errcode(ERRCODE_NO_ACTIVE_SQL_TRANSACTION),
3564+
errmsg("there is no transaction in progress")));
3565+
s->blockState = TBLOCK_END;
3566+
result = true;
3567+
break;
3568+
35443569
/*
35453570
* We are in a failed transaction block. Tell
35463571
* CommitTransactionCommand it's time to exit the block.
@@ -3705,8 +3730,14 @@ UserAbortTransactionBlock(void)
37053730
* WARNING and go to abort state. The upcoming call to
37063731
* CommitTransactionCommand() will then put us back into the
37073732
* default state.
3733+
*
3734+
* We do the same thing with ABORT inside an implicit transaction,
3735+
* although in this case we might be rolling back actual database
3736+
* state changes. (It's debatable whether we should issue a
3737+
* WARNING in this case, but we have done so historically.)
37083738
*/
37093739
case TBLOCK_STARTED:
3740+
case TBLOCK_IMPLICIT_INPROGRESS:
37103741
ereport(WARNING,
37113742
(errcode(ERRCODE_NO_ACTIVE_SQL_TRANSACTION),
37123743
errmsg("there is no transaction in progress")));
@@ -3743,6 +3774,58 @@ UserAbortTransactionBlock(void)
37433774
}
37443775
}
37453776

3777+
/*
3778+
* BeginImplicitTransactionBlock
3779+
* Start an implicit transaction block if we're not already in one.
3780+
*
3781+
* Unlike BeginTransactionBlock, this is called directly from the main loop
3782+
* in postgres.c, not within a Portal. So we can just change blockState
3783+
* without a lot of ceremony. We do not expect caller to do
3784+
* CommitTransactionCommand/StartTransactionCommand.
3785+
*/
3786+
void
3787+
BeginImplicitTransactionBlock(void)
3788+
{
3789+
TransactionState s = CurrentTransactionState;
3790+
3791+
/*
3792+
* If we are in STARTED state (that is, no transaction block is open),
3793+
* switch to IMPLICIT_INPROGRESS state, creating an implicit transaction
3794+
* block.
3795+
*
3796+
* For caller convenience, we consider all other transaction states as
3797+
* legal here; otherwise the caller would need its own state check, which
3798+
* seems rather pointless.
3799+
*/
3800+
if (s->blockState == TBLOCK_STARTED)
3801+
s->blockState = TBLOCK_IMPLICIT_INPROGRESS;
3802+
}
3803+
3804+
/*
3805+
* EndImplicitTransactionBlock
3806+
* End an implicit transaction block, if we're in one.
3807+
*
3808+
* Like EndTransactionBlock, we just make any needed blockState change here.
3809+
* The real work will be done in the upcoming CommitTransactionCommand().
3810+
*/
3811+
void
3812+
EndImplicitTransactionBlock(void)
3813+
{
3814+
TransactionState s = CurrentTransactionState;
3815+
3816+
/*
3817+
* If we are in IMPLICIT_INPROGRESS state, switch back to STARTED state,
3818+
* allowing CommitTransactionCommand to commit whatever happened during
3819+
* the implicit transaction block as though it were a single statement.
3820+
*
3821+
* For caller convenience, we consider all other transaction states as
3822+
* legal here; otherwise the caller would need its own state check, which
3823+
* seems rather pointless.
3824+
*/
3825+
if (s->blockState == TBLOCK_IMPLICIT_INPROGRESS)
3826+
s->blockState = TBLOCK_STARTED;
3827+
}
3828+
37463829
/*
37473830
* DefineSavepoint
37483831
* This executes a SAVEPOINT command.
@@ -3780,6 +3863,28 @@ DefineSavepoint(char *name)
37803863
s->name = MemoryContextStrdup(TopTransactionContext, name);
37813864
break;
37823865

3866+
/*
3867+
* We disallow savepoint commands in implicit transaction blocks.
3868+
* There would be no great difficulty in allowing them so far as
3869+
* this module is concerned, but a savepoint seems inconsistent
3870+
* with exec_simple_query's behavior of abandoning the whole query
3871+
* string upon error. Also, the point of an implicit transaction
3872+
* block (as opposed to a regular one) is to automatically close
3873+
* after an error, so it's hard to see how a savepoint would fit
3874+
* into that.
3875+
*
3876+
* The error messages for this are phrased as if there were no
3877+
* active transaction block at all, which is historical but
3878+
* perhaps could be improved.
3879+
*/
3880+
case TBLOCK_IMPLICIT_INPROGRESS:
3881+
ereport(ERROR,
3882+
(errcode(ERRCODE_NO_ACTIVE_SQL_TRANSACTION),
3883+
/* translator: %s represents an SQL statement name */
3884+
errmsg("%s can only be used in transaction blocks",
3885+
"SAVEPOINT")));
3886+
break;
3887+
37833888
/* These cases are invalid. */
37843889
case TBLOCK_DEFAULT:
37853890
case TBLOCK_STARTED:
@@ -3834,15 +3939,23 @@ ReleaseSavepoint(List *options)
38343939
switch (s->blockState)
38353940
{
38363941
/*
3837-
* We can't rollback to a savepoint if there is no savepoint
3838-
* defined.
3942+
* We can't release a savepoint if there is no savepoint defined.
38393943
*/
38403944
case TBLOCK_INPROGRESS:
38413945
ereport(ERROR,
38423946
(errcode(ERRCODE_S_E_INVALID_SPECIFICATION),
38433947
errmsg("no such savepoint")));
38443948
break;
38453949

3950+
case TBLOCK_IMPLICIT_INPROGRESS:
3951+
/* See comment about implicit transactions in DefineSavepoint */
3952+
ereport(ERROR,
3953+
(errcode(ERRCODE_NO_ACTIVE_SQL_TRANSACTION),
3954+
/* translator: %s represents an SQL statement name */
3955+
errmsg("%s can only be used in transaction blocks",
3956+
"RELEASE SAVEPOINT")));
3957+
break;
3958+
38463959
/*
38473960
* We are in a non-aborted subtransaction. This is the only valid
38483961
* case.
@@ -3957,6 +4070,15 @@ RollbackToSavepoint(List *options)
39574070
errmsg("no such savepoint")));
39584071
break;
39594072

4073+
case TBLOCK_IMPLICIT_INPROGRESS:
4074+
/* See comment about implicit transactions in DefineSavepoint */
4075+
ereport(ERROR,
4076+
(errcode(ERRCODE_NO_ACTIVE_SQL_TRANSACTION),
4077+
/* translator: %s represents an SQL statement name */
4078+
errmsg("%s can only be used in transaction blocks",
4079+
"ROLLBACK TO SAVEPOINT")));
4080+
break;
4081+
39604082
/*
39614083
* There is at least one savepoint, so proceed.
39624084
*/
@@ -4046,11 +4168,12 @@ RollbackToSavepoint(List *options)
40464168
/*
40474169
* BeginInternalSubTransaction
40484170
* This is the same as DefineSavepoint except it allows TBLOCK_STARTED,
4049-
* TBLOCK_END, and TBLOCK_PREPARE states, and therefore it can safely be
4050-
* used in functions that might be called when not inside a BEGIN block
4051-
* or when running deferred triggers at COMMIT/PREPARE time. Also, it
4052-
* automatically does CommitTransactionCommand/StartTransactionCommand
4053-
* instead of expecting the caller to do it.
4171+
* TBLOCK_IMPLICIT_INPROGRESS, TBLOCK_END, and TBLOCK_PREPARE states,
4172+
* and therefore it can safely be used in functions that might be called
4173+
* when not inside a BEGIN block or when running deferred triggers at
4174+
* COMMIT/PREPARE time. Also, it automatically does
4175+
* CommitTransactionCommand/StartTransactionCommand instead of expecting
4176+
* the caller to do it.
40544177
*/
40554178
void
40564179
BeginInternalSubTransaction(char *name)
@@ -4076,6 +4199,7 @@ BeginInternalSubTransaction(char *name)
40764199
{
40774200
case TBLOCK_STARTED:
40784201
case TBLOCK_INPROGRESS:
4202+
case TBLOCK_IMPLICIT_INPROGRESS:
40794203
case TBLOCK_END:
40804204
case TBLOCK_PREPARE:
40814205
case TBLOCK_SUBINPROGRESS:
@@ -4180,6 +4304,7 @@ RollbackAndReleaseCurrentSubTransaction(void)
41804304
case TBLOCK_DEFAULT:
41814305
case TBLOCK_STARTED:
41824306
case TBLOCK_BEGIN:
4307+
case TBLOCK_IMPLICIT_INPROGRESS:
41834308
case TBLOCK_PARALLEL_INPROGRESS:
41844309
case TBLOCK_SUBBEGIN:
41854310
case TBLOCK_INPROGRESS:
@@ -4211,6 +4336,7 @@ RollbackAndReleaseCurrentSubTransaction(void)
42114336
s = CurrentTransactionState; /* changed by pop */
42124337
AssertState(s->blockState == TBLOCK_SUBINPROGRESS ||
42134338
s->blockState == TBLOCK_INPROGRESS ||
4339+
s->blockState == TBLOCK_IMPLICIT_INPROGRESS ||
42144340
s->blockState == TBLOCK_STARTED);
42154341
}
42164342

@@ -4259,6 +4385,7 @@ AbortOutOfAnyTransaction(void)
42594385
case TBLOCK_STARTED:
42604386
case TBLOCK_BEGIN:
42614387
case TBLOCK_INPROGRESS:
4388+
case TBLOCK_IMPLICIT_INPROGRESS:
42624389
case TBLOCK_PARALLEL_INPROGRESS:
42634390
case TBLOCK_END:
42644391
case TBLOCK_ABORT_PENDING:
@@ -4369,6 +4496,7 @@ TransactionBlockStatusCode(void)
43694496
case TBLOCK_BEGIN:
43704497
case TBLOCK_SUBBEGIN:
43714498
case TBLOCK_INPROGRESS:
4499+
case TBLOCK_IMPLICIT_INPROGRESS:
43724500
case TBLOCK_PARALLEL_INPROGRESS:
43734501
case TBLOCK_SUBINPROGRESS:
43744502
case TBLOCK_END:
@@ -5036,6 +5164,8 @@ BlockStateAsString(TBlockState blockState)
50365164
return "BEGIN";
50375165
case TBLOCK_INPROGRESS:
50385166
return "INPROGRESS";
5167+
case TBLOCK_IMPLICIT_INPROGRESS:
5168+
return "IMPLICIT_INPROGRESS";
50395169
case TBLOCK_PARALLEL_INPROGRESS:
50405170
return "PARALLEL_INPROGRESS";
50415171
case TBLOCK_END:

0 commit comments

Comments
 (0)