Skip to content

Commit 588139e

Browse files
authored
Merge pull request #152375 from michae2/backport25.2-152245
release-25.2: optbuilder: take FOR SHARE locking during RC FK child checks
2 parents 8757f96 + 47144ab commit 588139e

File tree

3 files changed

+213
-8
lines changed

3 files changed

+213
-8
lines changed

pkg/ccl/logictestccl/testdata/logic_test/fk_read_committed

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,9 @@ WITH sleep AS (SELECT pg_sleep(1)) DELETE FROM parent_150282@parent_150282_i_idx
140140
user testuser
141141

142142
statement ok
143+
SET enable_implicit_fk_locking_for_serializable = on;
144+
SET enable_shared_locking_for_serializable = on;
145+
SET enable_durable_locking_for_serializable = on;
143146
INSERT INTO child_150282 VALUES (4, 1);
144147

145148
user root
@@ -176,6 +179,9 @@ WITH sleep AS (SELECT pg_sleep(1)) UPDATE parent_150282 SET p = 4 WHERE i = 2;
176179
user testuser
177180

178181
statement ok
182+
SET enable_implicit_fk_locking_for_serializable = on;
183+
SET enable_shared_locking_for_serializable = on;
184+
SET enable_durable_locking_for_serializable = on;
179185
INSERT INTO child_150282 VALUES (4, 1);
180186

181187
user root
@@ -196,3 +202,191 @@ SELECT * FROM child_150282;
196202
4 4
197203

198204
subtest end
205+
206+
subtest fk_cascade_race_151663
207+
208+
statement ok
209+
CREATE TABLE parent_151663 (
210+
p INT PRIMARY KEY,
211+
q INT
212+
);
213+
214+
statement ok
215+
CREATE TABLE child_151663 (
216+
c INT PRIMARY KEY,
217+
p INT REFERENCES parent_151663 (p),
218+
INDEX (p),
219+
FAMILY (c, p)
220+
);
221+
222+
statement ok
223+
CREATE USER testuser2;
224+
CREATE USER testuser3
225+
226+
statement ok
227+
GRANT ALL ON TABLE parent_151663 TO testuser;
228+
GRANT ALL ON TABLE parent_151663 TO testuser2;
229+
GRANT ALL ON TABLE parent_151663 TO testuser3;
230+
GRANT ALL ON TABLE child_151663 TO testuser3
231+
232+
statement ok
233+
INSERT INTO parent_151663 VALUES (1, 0), (2, 0), (3, 0)
234+
235+
# The timeline of this test is:
236+
#
237+
# testuser testuser2 testuser3 root
238+
# -------- --------- --------- ----
239+
# begin
240+
# update p=2
241+
# begin
242+
# lock p=3
243+
# begin RC
244+
# start DELETE
245+
# wait on p=3
246+
# begin SSI
247+
# start INSERT
248+
# wait on p=2
249+
# rollback
250+
# lock p=1, p=2
251+
# rollback
252+
# wait on p=1
253+
# insert c=10, c=20
254+
# finish INSERT
255+
# commit
256+
# delete p=1
257+
# FK check child
258+
# finish DELETE
259+
# commit
260+
#
261+
# The transactions run by testuser3 and root are the important part. testuser
262+
# and testuser2 only exist to control the timing of the transactions run by
263+
# testuser3 and root.
264+
#
265+
# The transactions run by testuser3 and root conflict with each other. To
266+
# prevent a FK violation they need to either run serially, or one transaction
267+
# needs to fail.
268+
#
269+
# This test demonstrates that we need locking during both the parent FK check
270+
# and the child FK check to prevent a FK violation.
271+
#
272+
# The parent FK check is performed by the Serializable INSERT run by
273+
# root. Without locking during this check, the two transactions are not
274+
# guaranteed to detect the conflict with each other. For example, without this
275+
# locking, the Read Committed DELETE could finish after the INSERT's FK check
276+
# but before its insert. The INSERT could then write a phantom child row and
277+
# have a successful read refresh, and both transactions could commit. Locking
278+
# during the parent FK check avoids this by forcing the transactions to
279+
# coordinate on the parent row.
280+
#
281+
# The child FK check is performed by the Read Committed DELETE run by
282+
# testuser3. Without locking during this check, the Read Committed DELETE could
283+
# perform a stale read for its child FK check, missing a newer child row. For
284+
# example, it could miss the row written by the Serializable INSERT and
285+
# successfully commit. (Serializable isolation does not have this same risk of
286+
# stale reads and thus does not need locking during child FK checks.)
287+
#
288+
# For more discussion of this and other scenarios please see #151663.
289+
290+
user testuser
291+
292+
statement ok
293+
BEGIN
294+
295+
# Use an update on p=2 to block the future insert by root.
296+
statement ok
297+
UPDATE parent_151663 SET q = q + 1 WHERE p = 2
298+
299+
user testuser2
300+
301+
statement ok
302+
BEGIN
303+
304+
# Use a lock on p=3 to block the future delete by testuser3.
305+
statement ok
306+
SELECT * FROM parent_151663 WHERE p = 3 FOR UPDATE
307+
308+
user testuser3
309+
310+
statement ok
311+
BEGIN ISOLATION LEVEL READ COMMITTED
312+
313+
statement ok
314+
SELECT 1
315+
316+
statement async fkdelete error pgcode 23503 delete on table "parent_151663" violates foreign key constraint "child_151663_p_fkey" on table "child_151663"\nDETAIL: Key \(p\)=\(1\) is still referenced from table "child_151663"\.
317+
DELETE FROM parent_151663 WHERE p = (SELECT 1 FROM parent_151663 WHERE p = 3 FOR UPDATE)
318+
319+
user root
320+
321+
# Give the delete a moment to wait on the p=3 lock by testuser2.
322+
statement ok
323+
SELECT pg_sleep(1)
324+
325+
# The serializable insert needs this locking to properly sychronize with the
326+
# read committed delete.
327+
statement ok
328+
SET enable_implicit_fk_locking_for_serializable = on;
329+
SET enable_shared_locking_for_serializable = on;
330+
SET enable_durable_locking_for_serializable = on
331+
332+
statement ok
333+
BEGIN ISOLATION LEVEL SERIALIZABLE
334+
335+
statement async fkinsert
336+
INSERT INTO child_151663 VALUES (10, 1), (20, 2)
337+
338+
user testuser
339+
340+
# Give the insert a moment to wait on the p=2 update by testuser.
341+
statement ok
342+
SELECT pg_sleep(1)
343+
344+
statement ok
345+
ROLLBACK
346+
347+
user testuser2
348+
349+
# Give the insert a moment to lock p=1.
350+
statement ok
351+
SELECT pg_sleep(1)
352+
353+
statement ok
354+
ROLLBACK
355+
356+
user root
357+
358+
awaitstatement fkinsert
359+
360+
statement ok
361+
COMMIT
362+
363+
user testuser3
364+
365+
awaitstatement fkdelete
366+
367+
statement ok
368+
COMMIT
369+
370+
user root
371+
372+
# Check that there are no orphan children.
373+
374+
query II
375+
SELECT * FROM parent_151663 ORDER BY p
376+
----
377+
1 0
378+
2 0
379+
3 0
380+
381+
query II
382+
SELECT * FROM child_151663 ORDER BY c
383+
----
384+
10 1
385+
20 2
386+
387+
statement ok
388+
RESET enable_implicit_fk_locking_for_serializable;
389+
RESET enable_shared_locking_for_serializable;
390+
RESET enable_durable_locking_for_serializable
391+
392+
subtest end

pkg/sql/opt/exec/execbuilder/testdata/fk_read_committed

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,9 @@ vectorized: true
191191
estimated row count: 1 (missing stats)
192192
label: buffer 1
193193

194-
# Foreign key checks of the child do not require locking.
194+
# Foreign key checks of the child do not require locking under serializable
195+
# isolation, but do require locking under read committed isolation to avoid
196+
# stale reads.
195197
query T
196198
EXPLAIN (OPT) UPDATE jars SET j = j + 4
197199
----
@@ -210,7 +212,8 @@ update jars
210212
│ └── with-scan &1
211213
├── distinct-on
212214
│ └── scan cookies
213-
│ └── flags: avoid-full-scan
215+
│ ├── flags: avoid-full-scan
216+
│ └── locking: for-share,durability-guaranteed
214217
└── filters
215218
└── j = cookies.j
216219

@@ -356,6 +359,8 @@ vectorized: true
356359
estimated row count: 1,000 (missing stats)
357360
table: cookies@cookies_pkey
358361
spans: FULL SCAN
362+
locking strength: for share
363+
locking durability: guaranteed
359364

360365
query T
361366
EXPLAIN (OPT) DELETE FROM jars WHERE j = 1
@@ -369,7 +374,8 @@ delete jars
369374
└── semi-join (hash)
370375
├── with-scan &1
371376
├── scan cookies
372-
│ └── flags: avoid-full-scan
377+
│ ├── flags: avoid-full-scan
378+
│ └── locking: for-share,durability-guaranteed
373379
└── filters
374380
└── j = cookies.j
375381

@@ -438,6 +444,8 @@ vectorized: true
438444
│ estimated row count: 1,000 (missing stats)
439445
│ table: cookies@cookies_pkey
440446
│ spans: FULL SCAN
447+
│ locking strength: for share
448+
│ locking durability: guaranteed
441449
442450
└── • scan buffer
443451
columns: (j)

pkg/sql/opt/optbuilder/mutation_builder_fk.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -640,11 +640,14 @@ func (h *fkCheckHelper) buildOtherTableScan(parent bool) (outScope *scope, tabMe
640640
// For insertion-side checks, if enable_implicit_fk_locking_for_serializable
641641
// is true or we're using a weaker isolation level, we lock the parent row(s)
642642
// to prevent concurrent mutations of the parent from other transactions from
643-
// violating the FK constraint. Deletion-side checks don't need to lock
644-
// because they can rely on the deletion intent conflicting with locks from
645-
// any concurrent inserts or updates of the child.
646-
if parent && (h.mb.b.evalCtx.TxnIsoLevel != isolation.Serializable ||
647-
h.mb.b.evalCtx.SessionData().ImplicitFKLockingForSerializable) {
643+
// violating the FK constraint. Deletion-side checks don't need to lock the
644+
// child row(s) under Serializable isolation because deletions can rely on the
645+
// deletion intent on the parent row conflicting with locks from any
646+
// concurrent inserts or updates of the child. Deletion-side checks do need to
647+
// lock the child row(s) under Read Committed isolation to ensure there are no
648+
// newer writers.
649+
if h.mb.b.evalCtx.TxnIsoLevel != isolation.Serializable ||
650+
(parent && h.mb.b.evalCtx.SessionData().ImplicitFKLockingForSerializable) {
648651
locking = lockingSpec{
649652
&lockingItem{
650653
item: &tree.LockingItem{

0 commit comments

Comments
 (0)