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

Commit 41f5e04

Browse files
committed
Fix a number of issues around modifying a previously updated row.
This commit fixes three, unfortunately related, issues: 1) Since 5db6df0, the introduction of DML via tableam, it was possible to trigger "ERROR: unexpected table_lock_tuple status: 1" when updating a row that was previously updated in the same transaction - but only when the previously updated row was before updated in a concurrent transaction (and READ COMMITTED was used). The reason for that was that that case simply wasn't expected. Fixing that lead to: 2) Even before the above commit, there were error checks (introduced in 6868ed7) preventing a row being updated by different commands within the same statement (say in a function called by an UPDATE) - but that check wasn't performed when the row was first updated in a concurrent transaction - instead the second update was silently skipped in that case. After this change we throw the same error as we'd without the concurrent transaction. 3) The error messages (introduced in 6868ed7) preventing such updates emitted the same error message for both DELETE and UPDATE ("tuple to be updated was already modified by an operation triggered by the current command"). While that could be changed separately, it made it hard to write tests that verify the correct correct behavior of the code. This commit changes heap's implementation of table_lock_tuple() to return TM_SelfModified instead of TM_Invisible (previously loosely modeled after EvalPlanQualFetch), and teaches nodeModifyTable.c to handle that in response to table_lock_tuple() and not just in response to table_(delete|update). Additionally it fixes the wrong error message (see 3 above). The comment for table_lock_tuple() is also adjusted to state that TM_Deleted won't return information in TM_FailureData - it'll not always be available. This also adds tests to ensure that DELETE/UPDATE correctly error out when affecting a row that concurrently was modified by another transaction. Author: Andres Freund Reported-By: Tom Lane, when investigating a bug bug fix to another bug by Amit Langote Discussion: https://postgr.es/m/19321.1554567786@sss.pgh.pa.us
1 parent 964bae4 commit 41f5e04

File tree

6 files changed

+144
-10
lines changed

6 files changed

+144
-10
lines changed

src/backend/access/heap/heapam_handler.c

+7-1
Original file line numberDiff line numberDiff line change
@@ -463,8 +463,14 @@ heapam_tuple_lock(Relation relation, ItemPointer tid, Snapshot snapshot,
463463
if (TransactionIdIsCurrentTransactionId(priorXmax) &&
464464
HeapTupleHeaderGetCmin(tuple->t_data) >= cid)
465465
{
466+
tmfd->xmax = priorXmax;
467+
/*
468+
* Cmin is the problematic value, so store that. See
469+
* above.
470+
*/
471+
tmfd->cmax = HeapTupleHeaderGetCmin(tuple->t_data);
466472
ReleaseBuffer(buffer);
467-
return TM_Invisible;
473+
return TM_SelfModified;
468474
}
469475

470476
/*

src/backend/executor/nodeModifyTable.c

+39-5
Original file line numberDiff line numberDiff line change
@@ -799,7 +799,7 @@ ldelete:;
799799
if (tmfd.cmax != estate->es_output_cid)
800800
ereport(ERROR,
801801
(errcode(ERRCODE_TRIGGERED_DATA_CHANGE_VIOLATION),
802-
errmsg("tuple to be updated was already modified by an operation triggered by the current command"),
802+
errmsg("tuple to be deleted was already modified by an operation triggered by the current command"),
803803
errhint("Consider using an AFTER trigger instead of a BEFORE trigger to propagate changes to other rows.")));
804804

805805
/* Else, already deleted by self; nothing to do */
@@ -858,6 +858,25 @@ ldelete:;
858858
else
859859
goto ldelete;
860860

861+
case TM_SelfModified:
862+
/*
863+
* This can be reached when following an update
864+
* chain from a tuple updated by another session,
865+
* reaching a tuple that was already updated in
866+
* this transaction. If previously updated by this
867+
* command, ignore the delete, otherwise error
868+
* out.
869+
*
870+
* See also TM_SelfModified response to
871+
* table_delete() above.
872+
*/
873+
if (tmfd.cmax != estate->es_output_cid)
874+
ereport(ERROR,
875+
(errcode(ERRCODE_TRIGGERED_DATA_CHANGE_VIOLATION),
876+
errmsg("tuple to be deleted was already modified by an operation triggered by the current command"),
877+
errhint("Consider using an AFTER trigger instead of a BEFORE trigger to propagate changes to other rows.")));
878+
return NULL;
879+
861880
case TM_Deleted:
862881
/* tuple already deleted; nothing to do */
863882
return NULL;
@@ -870,10 +889,6 @@ ldelete:;
870889
* already have errored out if the first version
871890
* is invisible.
872891
*
873-
* TM_SelfModified should be impossible, as we'd
874-
* otherwise should have hit the TM_SelfModified
875-
* case in response to table_delete above.
876-
*
877892
* TM_Updated should be impossible, because we're
878893
* locking the latest version via
879894
* TUPLE_LOCK_FLAG_FIND_LAST_VERSION.
@@ -1379,6 +1394,25 @@ lreplace:;
13791394
/* tuple already deleted; nothing to do */
13801395
return NULL;
13811396

1397+
case TM_SelfModified:
1398+
/*
1399+
* This can be reached when following an update
1400+
* chain from a tuple updated by another session,
1401+
* reaching a tuple that was already updated in
1402+
* this transaction. If previously modified by
1403+
* this command, ignore the redundant update,
1404+
* otherwise error out.
1405+
*
1406+
* See also TM_SelfModified response to
1407+
* table_update() above.
1408+
*/
1409+
if (tmfd.cmax != estate->es_output_cid)
1410+
ereport(ERROR,
1411+
(errcode(ERRCODE_TRIGGERED_DATA_CHANGE_VIOLATION),
1412+
errmsg("tuple to be updated was already modified by an operation triggered by the current command"),
1413+
errhint("Consider using an AFTER trigger instead of a BEFORE trigger to propagate changes to other rows.")));
1414+
return NULL;
1415+
13821416
default:
13831417
/* see table_lock_tuple call in ExecDelete() */
13841418
elog(ERROR, "unexpected table_lock_tuple status: %u",

src/include/access/tableam.h

+3-3
Original file line numberDiff line numberDiff line change
@@ -1216,9 +1216,9 @@ table_update(Relation rel, ItemPointer otid, TupleTableSlot *slot,
12161216
* TM_Deleted: lock failed because tuple deleted by other xact
12171217
* TM_WouldBlock: lock couldn't be acquired and wait_policy is skip
12181218
*
1219-
* In the failure cases other than TM_Invisible, the routine fills *tmfd with
1220-
* the tuple's t_ctid, t_xmax, and, if possible, t_cmax. See comments for
1221-
* struct TM_FailureData for additional info.
1219+
* In the failure cases other than TM_Invisible and TM_Deleted, the routine
1220+
* fills *tmfd with the tuple's t_ctid, t_xmax, and, if possible, t_cmax. See
1221+
* comments for struct TM_FailureData for additional info.
12221222
*/
12231223
static inline TM_Result
12241224
table_lock_tuple(Relation rel, ItemPointer tid, Snapshot snapshot,

src/test/isolation/expected/eval-plan-qual.out

+67
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,73 @@ accountid balance
258258
checking 1050
259259
savings 600
260260

261+
starting permutation: wx1 updwcte c1 c2 read
262+
step wx1: UPDATE accounts SET balance = balance - 200 WHERE accountid = 'checking' RETURNING balance;
263+
balance
264+
265+
400
266+
step updwcte: WITH doup AS (UPDATE accounts SET balance = balance + 1100 WHERE accountid = 'checking' RETURNING *) UPDATE accounts a SET balance = doup.balance + 100 FROM doup RETURNING *; <waiting ...>
267+
step c1: COMMIT;
268+
step updwcte: <... completed>
269+
accountid balance accountid balance
270+
271+
savings 1600 checking 1500
272+
step c2: COMMIT;
273+
step read: SELECT * FROM accounts ORDER BY accountid;
274+
accountid balance
275+
276+
checking 1500
277+
savings 1600
278+
279+
starting permutation: wx1 updwctefail c1 c2 read
280+
step wx1: UPDATE accounts SET balance = balance - 200 WHERE accountid = 'checking' RETURNING balance;
281+
balance
282+
283+
400
284+
step updwctefail: WITH doup AS (UPDATE accounts SET balance = balance + 1100 WHERE accountid = 'checking' RETURNING *, update_checking(999)) UPDATE accounts a SET balance = doup.balance + 100 FROM doup RETURNING *; <waiting ...>
285+
step c1: COMMIT;
286+
step updwctefail: <... completed>
287+
error in steps c1 updwctefail: ERROR: tuple to be updated was already modified by an operation triggered by the current command
288+
step c2: COMMIT;
289+
step read: SELECT * FROM accounts ORDER BY accountid;
290+
accountid balance
291+
292+
checking 400
293+
savings 600
294+
295+
starting permutation: wx1 delwcte c1 c2 read
296+
step wx1: UPDATE accounts SET balance = balance - 200 WHERE accountid = 'checking' RETURNING balance;
297+
balance
298+
299+
400
300+
step delwcte: WITH doup AS (UPDATE accounts SET balance = balance + 1100 WHERE accountid = 'checking' RETURNING *) DELETE FROM accounts a USING doup RETURNING *; <waiting ...>
301+
step c1: COMMIT;
302+
step delwcte: <... completed>
303+
accountid balance accountid balance
304+
305+
savings 600 checking 1500
306+
step c2: COMMIT;
307+
step read: SELECT * FROM accounts ORDER BY accountid;
308+
accountid balance
309+
310+
checking 1500
311+
312+
starting permutation: wx1 delwctefail c1 c2 read
313+
step wx1: UPDATE accounts SET balance = balance - 200 WHERE accountid = 'checking' RETURNING balance;
314+
balance
315+
316+
400
317+
step delwctefail: WITH doup AS (UPDATE accounts SET balance = balance + 1100 WHERE accountid = 'checking' RETURNING *, update_checking(999)) DELETE FROM accounts a USING doup RETURNING *; <waiting ...>
318+
step c1: COMMIT;
319+
step delwctefail: <... completed>
320+
error in steps c1 delwctefail: ERROR: tuple to be deleted was already modified by an operation triggered by the current command
321+
step c2: COMMIT;
322+
step read: SELECT * FROM accounts ORDER BY accountid;
323+
accountid balance
324+
325+
checking 400
326+
savings 600
327+
261328
starting permutation: upsert1 upsert2 c1 c2 read
262329
step upsert1:
263330
WITH upsert AS

src/test/isolation/specs/eval-plan-qual.spec

+27
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ setup
99
CREATE TABLE accounts (accountid text PRIMARY KEY, balance numeric not null);
1010
INSERT INTO accounts VALUES ('checking', 600), ('savings', 600);
1111

12+
CREATE FUNCTION update_checking(int) RETURNS bool LANGUAGE sql AS $$
13+
UPDATE accounts SET balance = balance + 1 WHERE accountid = 'checking'; SELECT true;$$;
14+
1215
CREATE TABLE accounts_ext (accountid text PRIMARY KEY, balance numeric not null, other text);
1316
INSERT INTO accounts_ext VALUES ('checking', 600, 'other'), ('savings', 700, null);
1417
ALTER TABLE accounts_ext ADD COLUMN newcol int DEFAULT 42;
@@ -34,6 +37,7 @@ setup
3437
teardown
3538
{
3639
DROP TABLE accounts;
40+
DROP FUNCTION update_checking(int);
3741
DROP TABLE accounts_ext;
3842
DROP TABLE p CASCADE;
3943
DROP TABLE table_a, table_b, jointest;
@@ -170,6 +174,16 @@ step "updateforcip3" {
170174
}
171175
step "wrtwcte" { UPDATE table_a SET value = 'tableAValue2' WHERE id = 1; }
172176
step "wrjt" { UPDATE jointest SET data = 42 WHERE id = 7; }
177+
178+
# Use writable CTEs to create self-updated rows, that then are
179+
# (updated|deleted). The *fail versions of the tests additionally
180+
# perform an update, via a function, in a different command, to test
181+
# behaviour relating to that.
182+
step "updwcte" { WITH doup AS (UPDATE accounts SET balance = balance + 1100 WHERE accountid = 'checking' RETURNING *) UPDATE accounts a SET balance = doup.balance + 100 FROM doup RETURNING *; }
183+
step "updwctefail" { WITH doup AS (UPDATE accounts SET balance = balance + 1100 WHERE accountid = 'checking' RETURNING *, update_checking(999)) UPDATE accounts a SET balance = doup.balance + 100 FROM doup RETURNING *; }
184+
step "delwcte" { WITH doup AS (UPDATE accounts SET balance = balance + 1100 WHERE accountid = 'checking' RETURNING *) DELETE FROM accounts a USING doup RETURNING *; }
185+
step "delwctefail" { WITH doup AS (UPDATE accounts SET balance = balance + 1100 WHERE accountid = 'checking' RETURNING *, update_checking(999)) DELETE FROM accounts a USING doup RETURNING *; }
186+
173187
step "c2" { COMMIT; }
174188
step "r2" { ROLLBACK; }
175189

@@ -221,6 +235,19 @@ permutation "wx2" "d2" "d1" "r2" "c1" "read"
221235
permutation "d1" "wx2" "c1" "c2" "read"
222236
permutation "d1" "wx2" "r1" "c2" "read"
223237

238+
# test that an update to a self-modified row is ignored when
239+
# previously updated by the same cid
240+
permutation "wx1" "updwcte" "c1" "c2" "read"
241+
# test that an update to a self-modified row throws error when
242+
# previously updated by a different cid
243+
permutation "wx1" "updwctefail" "c1" "c2" "read"
244+
# test that a delete to a self-modified row is ignored when
245+
# previously updated by the same cid
246+
permutation "wx1" "delwcte" "c1" "c2" "read"
247+
# test that a delete to a self-modified row throws error when
248+
# previously updated by a different cid
249+
permutation "wx1" "delwctefail" "c1" "c2" "read"
250+
224251
permutation "upsert1" "upsert2" "c1" "c2" "read"
225252
permutation "readp1" "writep1" "readp2" "c1" "c2"
226253
permutation "writep2" "returningp1" "c1" "c2"

src/test/regress/expected/triggers.out

+1-1
Original file line numberDiff line numberDiff line change
@@ -1607,7 +1607,7 @@ select * from parent; select * from child;
16071607
(1 row)
16081608

16091609
delete from parent where aid = 1; -- should fail
1610-
ERROR: tuple to be updated was already modified by an operation triggered by the current command
1610+
ERROR: tuple to be deleted was already modified by an operation triggered by the current command
16111611
HINT: Consider using an AFTER trigger instead of a BEFORE trigger to propagate changes to other rows.
16121612
select * from parent; select * from child;
16131613
aid | val1 | val2 | val3 | val4 | bcnt

0 commit comments

Comments
 (0)