Describe the problem
In transactional LDR lock derivation, deriveUniqueLocks applies the
"values unchanged" skip (uc.equal(Row, PrevRow)) unconditionally, while
deriveFKLocks guards the same skip with row.IsUpdateRow(). For a DELETE,
Row is decoded from the key, so the PK columns are populated. If a unique
constraint's columns are all PK columns — e.g. PRIMARY KEY (a, b), UNIQUE (a)
— then Row and PrevRow agree on the UC columns, the skip fires, and no
unique lock is emitted for the value the delete releases.
Impact
Source serializes T1 DELETE (a=1,b=2) before T2 INSERT (a=1,b=3) via
UNIQUE(a). On the destination the lock sets don't overlap
(T1={PK(1,2)}, T2={PK(1,3), UC(1)}), so the scheduler may apply T2 before T1
→ duplicate-key error → DLQ/pause. The intra-txn topological sort has the same
hole (edges only follow shared lock hashes).
Code references
Suggested fix
Guard the equality skip with row.IsUpdateRow(), mirroring the FK path.
Epic CRDB-65552
Jira issue: CRDB-65602
Describe the problem
In transactional LDR lock derivation,
deriveUniqueLocksapplies the"values unchanged" skip (
uc.equal(Row, PrevRow)) unconditionally, whilederiveFKLocksguards the same skip withrow.IsUpdateRow(). For a DELETE,Rowis decoded from the key, so the PK columns are populated. If a uniqueconstraint's columns are all PK columns — e.g.
PRIMARY KEY (a, b), UNIQUE (a)— then
RowandPrevRowagree on the UC columns, the skip fires, and nounique lock is emitted for the value the delete releases.
Impact
Source serializes T1
DELETE (a=1,b=2)before T2INSERT (a=1,b=3)viaUNIQUE(a). On the destination the lock sets don't overlap(T1={PK(1,2)}, T2={PK(1,3), UC(1)}), so the scheduler may apply T2 before T1
→ duplicate-key error → DLQ/pause. The intra-txn topological sort has the same
hole (edges only follow shared lock hashes).
Code references
IsUpdateRow()Suggested fix
Guard the equality skip with
row.IsUpdateRow(), mirroring the FK path.Epic CRDB-65552
Jira issue: CRDB-65602