-- usp_SyncApply: set-based apply of pending SyncQueue rows. -- Per distinct (TargetSchema, TargetTable) it builds column lists from sys.columns -- (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. CREATE OR ALTER PROCEDURE dbo.usp_SyncApply @MaxRetries INT = 5 AS BEGIN SET NOCOUNT ON; DECLARE @sch NVARCHAR(128), @tbl NVARCHAR(255), @FullName NVARCHAR(514); DECLARE @sql NVARCHAR(MAX), @cols NVARCHAR(MAX), @upd NVARCHAR(MAX), @ins NVARCHAR(MAX); -- Re-queue rows still under the retry budget. UPDATE dbo.SyncQueue SET Status = 'pending' WHERE Status = 'error' AND RetryCount < @MaxRetries; DECLARE cur CURSOR LOCAL FAST_FORWARD FOR SELECT DISTINCT TargetSchema, TargetTable FROM dbo.SyncQueue WHERE Status = 'pending'; OPEN cur; FETCH NEXT FROM cur INTO @sch, @tbl; WHILE @@FETCH_STATUS = 0 BEGIN SET @FullName = QUOTENAME(@sch) + N'.' + QUOTENAME(@tbl); BEGIN TRY BEGIN TRAN; -- @cols: comma-quoted column names (for INSERT target list) -- @upd: tgt.col = JSON_VALUE(src.RowData,'$.col') -- @ins: JSON_VALUE(src.RowData,'$.col') (for INSERT VALUES) -- NOTE: every JSON path key is wrapped in double quotes ('$."col"'). -- This is mandatory for non-ASCII column names (e.g. Chinese names -- like 名字) and harmless for ASCII names, so we quote unconditionally. SELECT @cols = STRING_AGG(QUOTENAME(c.name), N',') WITHIN GROUP (ORDER BY c.column_id), @upd = STRING_AGG(QUOTENAME(c.name) + N'=JSON_VALUE(src.RowData,''$."' + c.name + N'"'')', N',') WITHIN GROUP (ORDER BY c.column_id), @ins = STRING_AGG(N'JSON_VALUE(src.RowData,''$."' + c.name + N'"'')', N',') WITHIN GROUP (ORDER BY c.column_id) FROM sys.columns c WHERE c.object_id = OBJECT_ID(@FullName) AND c.is_computed = 0 AND c.is_identity = 0 AND TYPE_NAME(c.system_type_id) <> 'timestamp' AND c.name <> 'ID'; IF @cols IS NOT NULL BEGIN -- Upsert: last write wins (ROW_NUMBER over SourceLogID DESC). SET @sql = N'SET IDENTITY_INSERT ' + @FullName + N' ON;' + 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') 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')' + N' VALUES (TRY_CAST(src.RecordID AS int),' + @ins + N');' + N'SET IDENTITY_INSERT ' + @FullName + N' OFF;'; 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);'; EXEC sp_executesql @sql, N'@sch NVARCHAR(128),@tbl NVARCHAR(255)', @sch, @tbl; END UPDATE dbo.SyncQueue SET Status = 'applied', AppliedAt = SYSDATETIME() WHERE TargetSchema = @sch AND TargetTable = @tbl AND Status = 'pending'; COMMIT; END TRY BEGIN CATCH -- ROLLBACK unwinds the per-table transaction (and, if the caller -- started one, the caller's too). A proc that errors under an outer -- transaction is expected to leave the txn uncommittable; the caller -- issues its own rollback. We roll back here so the partial per-table -- work is discarded before marking rows as 'error'/'dead'. IF @@TRANCOUNT > 0 ROLLBACK; UPDATE dbo.SyncQueue SET Status = CASE WHEN RetryCount + 1 >= @MaxRetries THEN 'dead' ELSE 'error' END, RetryCount = RetryCount + 1, ErrorMsg = ERROR_MESSAGE() WHERE TargetSchema = @sch AND TargetTable = @tbl AND Status = 'pending'; END CATCH FETCH NEXT FROM cur INTO @sch, @tbl; END CLOSE cur; DEALLOCATE cur; END GO