fix(sql): prevent stale Delete from outranking newer Insert in usp_SyncApply
Route both upsert and delete branches off a single ranked CTE (rn=1 per RecordID over all pending ops ordered by SourceLogID DESC). The previous design used two independent ranked CTEs, which let a stale Delete outrank a newer Insert for the same RecordID and silently drop the row. Also gitignore .claude/ and .workbuddy/ runtime dirs.
This commit is contained in:
@@ -3,6 +3,13 @@
|
||||
-- (excluding the ID key, computed, identity and rowversion columns) and runs a
|
||||
-- dynamic-SQL MERGE (last-write-wins by SourceLogID DESC) for Insert/Update and a
|
||||
-- DELETE for the last op = Delete. SET IDENTITY_INSERT ON preserves Access PKs.
|
||||
--
|
||||
-- CORRECTNESS NOTE: a SINGLE ranked CTE (rn=1 per RecordID, over ALL pending ops
|
||||
-- regardless of OperateType) is read by BOTH the upsert and delete branches.
|
||||
-- Earlier this was two independent ranked CTEs (one over Insert/Update, one over
|
||||
-- Delete), which let a stale Delete outrank a newer Insert for the same RecordID
|
||||
-- and silently drop a row whose true last op was an Insert. Routing off the one
|
||||
-- winning row's OperateType guarantees only the genuine last op wins.
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.usp_SyncApply
|
||||
@MaxRetries INT = 5
|
||||
@@ -54,16 +61,22 @@ BEGIN
|
||||
|
||||
IF @cols IS NOT NULL
|
||||
BEGIN
|
||||
-- Upsert: last write wins (ROW_NUMBER over SourceLogID DESC).
|
||||
-- Upsert: winners (rn=1) whose winning OperateType is
|
||||
-- Insert/Update and carry RowData. The ranked CTE covers ALL
|
||||
-- pending ops so the rn=1 row is the true last op for that
|
||||
-- RecordID; an earlier Delete can no longer outrank a newer
|
||||
-- Insert for the same RecordID.
|
||||
SET @sql = N'SET IDENTITY_INSERT ' + @FullName + N' ON;'
|
||||
+ N';WITH ranked AS ('
|
||||
+ N' SELECT RecordID, OperateType, RowData,'
|
||||
+ N' ROW_NUMBER() OVER (PARTITION BY RecordID ORDER BY SourceLogID DESC) rn'
|
||||
+ N' FROM dbo.SyncQueue'
|
||||
+ N' WHERE TargetSchema=@sch AND TargetTable=@tbl AND Status=''pending'''
|
||||
+ N')'
|
||||
+ N'MERGE ' + @FullName + N' WITH (HOLDLOCK) AS tgt'
|
||||
+ N' USING ('
|
||||
+ N' SELECT RecordID, RowData FROM ('
|
||||
+ N' SELECT *, ROW_NUMBER() OVER (PARTITION BY RecordID ORDER BY SourceLogID DESC) rn'
|
||||
+ N' FROM dbo.SyncQueue'
|
||||
+ N' WHERE TargetSchema=@sch AND TargetTable=@tbl AND Status=''pending'''
|
||||
+ N' AND OperateType IN (''Insert'',''Update'') AND RowData IS NOT NULL'
|
||||
+ N' ) x WHERE rn=1'
|
||||
+ N' SELECT RecordID, RowData FROM ranked'
|
||||
+ N' WHERE rn=1 AND OperateType IN (''Insert'',''Update'') AND RowData IS NOT NULL'
|
||||
+ N') AS src ON tgt.ID = TRY_CAST(src.RecordID AS int)'
|
||||
+ N' WHEN MATCHED THEN UPDATE SET ' + @upd
|
||||
+ N' WHEN NOT MATCHED THEN INSERT (ID,' + @cols + N')'
|
||||
@@ -72,16 +85,17 @@ BEGIN
|
||||
EXEC sp_executesql @sql,
|
||||
N'@sch NVARCHAR(128),@tbl NVARCHAR(255)', @sch, @tbl;
|
||||
|
||||
-- Delete: last op = Delete wins.
|
||||
SET @sql = N'DELETE t FROM ' + @FullName + N' t'
|
||||
+ N' JOIN ('
|
||||
+ N' SELECT RecordID FROM ('
|
||||
+ N' SELECT *, ROW_NUMBER() OVER (PARTITION BY RecordID ORDER BY SourceLogID DESC) rn'
|
||||
+ N' FROM dbo.SyncQueue'
|
||||
+ N' WHERE TargetSchema=@sch AND TargetTable=@tbl AND Status=''pending'''
|
||||
+ N' AND OperateType=''Delete'''
|
||||
+ N' ) x WHERE rn=1'
|
||||
+ N') d ON t.ID = TRY_CAST(d.RecordID AS int);';
|
||||
-- Delete: winners (rn=1) whose winning OperateType is Delete.
|
||||
-- Same ranked CTE — only the genuine last op can be a delete.
|
||||
SET @sql = N';WITH ranked AS ('
|
||||
+ N' SELECT RecordID, OperateType,'
|
||||
+ N' ROW_NUMBER() OVER (PARTITION BY RecordID ORDER BY SourceLogID DESC) rn'
|
||||
+ N' FROM dbo.SyncQueue'
|
||||
+ N' WHERE TargetSchema=@sch AND TargetTable=@tbl AND Status=''pending'''
|
||||
+ N')'
|
||||
+ N'DELETE t FROM ' + @FullName + N' t'
|
||||
+ N' JOIN (SELECT RecordID FROM ranked WHERE rn=1 AND OperateType=''Delete'') d'
|
||||
+ N' ON t.ID = TRY_CAST(d.RecordID AS int);';
|
||||
EXEC sp_executesql @sql,
|
||||
N'@sch NVARCHAR(128),@tbl NVARCHAR(255)', @sch, @tbl;
|
||||
END
|
||||
|
||||
Reference in New Issue
Block a user