/* ============================================================ Migration Copy Script (Variant B - Checkpointing / Restartable) SQL Server 2022 Source DB: sysdb_UTF8 Target DB: sysdb_utf8_jr Features: - FK-aware table order (parent -> child) with cycle fallback - Per-table transaction + checkpointing in dbo.MigrationState - Idempotent per table: DELETE target table then INSERT - Disables triggers + constraints + nonclustered indexes before load - Rebuilds indexes, re-enables constraints (WITH CHECK), enables triggers after load - Collation conflict resistant (explicit COLLATE DATABASE_DEFAULT in comparisons) - Visible progress in SSMS using RAISERROR ... WITH NOWAIT Controls: @Mode: 'RESUME' or 'RESET' @StopOnError: 1 stop at first failure, 0 continue @BatchSize: range size for batching identity int/bigint Notes: - This script copies DATA only. Schema must already exist in target. ============================================================ */ SET NOCOUNT ON; SET XACT_ABORT ON; DECLARE @SourceDb sysname = N'sysdb_UTF8'; DECLARE @TargetDb sysname = N'sysdb_utf8_jr'; DECLARE @Mode varchar(10) = 'RESUME'; -- 'RESUME' or 'RESET' DECLARE @StopOnError bit = 1; -- 1 stop, 0 continue DECLARE @BatchSize int = 50000; -- identity range size for batching DECLARE @Verbose bit = 1; -- 1 prints progress DECLARE @QSourceDb sysname = QUOTENAME(@SourceDb); DECLARE @QTargetDb sysname = QUOTENAME(@TargetDb); DECLARE @RunId uniqueidentifier = NEWID(); -------------------------------------------------------------------------------- -- Helper: print progress immediately in SSMS -------------------------------------------------------------------------------- DECLARE @msg nvarchar(4000); DECLARE @sev int = 10; DECLARE @Now nvarchar(30); SET @Now = CONVERT(nvarchar(30), SYSDATETIME(), 121); SET @msg = CONCAT(@Now, N' | START | Source=', @SourceDb, N' -> Target=', @TargetDb, N' | Mode=', @Mode); RAISERROR(@msg, @sev, 1) WITH NOWAIT; -------------------------------------------------------------------------------- -- 0) Ensure we are in target DB context for DATABASE_DEFAULT collation usage -------------------------------------------------------------------------------- DECLARE @UseTargetSql nvarchar(max) = N'USE ' + @QTargetDb + N';'; EXEC sp_executesql @UseTargetSql; -------------------------------------------------------------------------------- -- 1) Setup state/log tables in TARGET -------------------------------------------------------------------------------- DECLARE @SetupSql nvarchar(max) = N' USE ' + @QTargetDb + N'; IF OBJECT_ID(''dbo.MigrationState'',''U'') IS NULL BEGIN CREATE TABLE dbo.MigrationState( TableSchema sysname NOT NULL, TableName sysname NOT NULL, Status varchar(20) NOT NULL, -- PENDING/RUNNING/DONE/FAILED StartedAt datetime2 NULL, FinishedAt datetime2 NULL, RowsCopied bigint NULL, LastError nvarchar(max) NULL, LastRunId uniqueidentifier NULL, CONSTRAINT PK_MigrationState PRIMARY KEY (TableSchema, TableName) ); END; IF OBJECT_ID(''dbo.MigrationCopyLog'',''U'') IS NULL BEGIN CREATE TABLE dbo.MigrationCopyLog( LogId int IDENTITY(1,1) PRIMARY KEY, LogTime datetime2 NOT NULL DEFAULT SYSUTCDATETIME(), RunId uniqueidentifier NULL, Step nvarchar(200) NOT NULL, Details nvarchar(max) NULL ); END; INSERT dbo.MigrationCopyLog(RunId, Step, Details) VALUES ( @RunId, N''RUN_START'', CONCAT( N''SourceDb='', @SourceDb, N''; TargetDb='', DB_NAME(), N''; Mode='', @Mode, N''; StopOnError='', @StopOnError, N''; BatchSize='', @BatchSize ) ); '; EXEC sp_executesql @SetupSql, N'@RunId uniqueidentifier, @SourceDb sysname, @Mode varchar(10), @StopOnError bit, @BatchSize int', @RunId=@RunId, @SourceDb=@SourceDb, @Mode=@Mode, @StopOnError=@StopOnError, @BatchSize=@BatchSize; SET @Now = CONVERT(nvarchar(30), SYSDATETIME(), 121); RAISERROR(CONCAT(@Now, N' | SETUP | State+Log ready in ', @TargetDb), @sev, 1) WITH NOWAIT; -------------------------------------------------------------------------------- -- 2) Collect SOURCE tables into #Tables (names stored in TARGET collation) -------------------------------------------------------------------------------- IF OBJECT_ID('tempdb..#Tables') IS NOT NULL DROP TABLE #Tables; CREATE TABLE #Tables ( SchemaName nvarchar(128) COLLATE DATABASE_DEFAULT NOT NULL, TableName nvarchar(128) COLLATE DATABASE_DEFAULT NOT NULL, FullName nvarchar(300) COLLATE DATABASE_DEFAULT NOT NULL, NodeId int IDENTITY(1,1) PRIMARY KEY, InDegree int NOT NULL DEFAULT(0), ProcessOrder int NULL ); DECLARE @SqlTables nvarchar(max) = N' SELECT s.name AS SchemaName, t.name AS TableName, QUOTENAME(s.name) + ''.'' + QUOTENAME(t.name) AS FullName FROM ' + @QSourceDb + N'.sys.tables t JOIN ' + @QSourceDb + N'.sys.schemas s ON s.schema_id = t.schema_id WHERE t.is_ms_shipped = 0 AND t.name NOT LIKE ''dt%'';'; INSERT INTO #Tables(SchemaName, TableName, FullName) EXEC sp_executesql @SqlTables; IF NOT EXISTS (SELECT 1 FROM #Tables) THROW 50001, 'No user tables found in source.', 1; SET @Now = CONVERT(nvarchar(30), SYSDATETIME(), 121); RAISERROR(CONCAT(@Now, N' | DISCOVER | Found tables: ', (SELECT COUNT(*) FROM #Tables)), @sev, 1) WITH NOWAIT; -------------------------------------------------------------------------------- -- 3) Collect FK edges (Parent -> Child) from SOURCE -------------------------------------------------------------------------------- IF OBJECT_ID('tempdb..#Edges') IS NOT NULL DROP TABLE #Edges; CREATE TABLE #Edges ( ParentSchema nvarchar(128) COLLATE DATABASE_DEFAULT NOT NULL, ParentTable nvarchar(128) COLLATE DATABASE_DEFAULT NOT NULL, ChildSchema nvarchar(128) COLLATE DATABASE_DEFAULT NOT NULL, ChildTable nvarchar(128) COLLATE DATABASE_DEFAULT NOT NULL ); DECLARE @SqlEdges nvarchar(max) = N' SELECT sp.name AS ParentSchema, tp.name AS ParentTable, sc.name AS ChildSchema, tc.name AS ChildTable FROM ' + @QSourceDb + N'.sys.foreign_keys fk JOIN ' + @QSourceDb + N'.sys.tables tc ON tc.object_id = fk.parent_object_id JOIN ' + @QSourceDb + N'.sys.schemas sc ON sc.schema_id = tc.schema_id JOIN ' + @QSourceDb + N'.sys.tables tp ON tp.object_id = fk.referenced_object_id JOIN ' + @QSourceDb + N'.sys.schemas sp ON sp.schema_id = tp.schema_id WHERE tc.is_ms_shipped = 0 AND tp.is_ms_shipped = 0;'; INSERT INTO #Edges(ParentSchema, ParentTable, ChildSchema, ChildTable) EXEC sp_executesql @SqlEdges; SET @Now = CONVERT(nvarchar(30), SYSDATETIME(), 121); RAISERROR(CONCAT(@Now, N' | DISCOVER | Found FKs: ', (SELECT COUNT(*) FROM #Edges)), @sev, 1) WITH NOWAIT; -------------------------------------------------------------------------------- -- 4) Compute indegrees and order tables (Kahn). Cycle fallback appended by name. -------------------------------------------------------------------------------- UPDATE t SET InDegree = x.Cnt FROM #Tables t JOIN ( SELECT ChildSchema, ChildTable, COUNT(*) AS Cnt FROM #Edges GROUP BY ChildSchema, ChildTable ) x ON x.ChildSchema = t.SchemaName AND x.ChildTable = t.TableName; IF OBJECT_ID('tempdb..#Queue') IS NOT NULL DROP TABLE #Queue; CREATE TABLE #Queue(NodeId int PRIMARY KEY); INSERT INTO #Queue(NodeId) SELECT NodeId FROM #Tables WHERE InDegree = 0; DECLARE @order int = 1; WHILE EXISTS (SELECT 1 FROM #Queue) BEGIN DECLARE @nid int; SELECT TOP(1) @nid = NodeId FROM #Queue ORDER BY NodeId; DELETE FROM #Queue WHERE NodeId=@nid; UPDATE #Tables SET ProcessOrder=@order WHERE NodeId=@nid; SET @order += 1; DECLARE @ps nvarchar(128), @pt nvarchar(128); SELECT @ps=SchemaName, @pt=TableName FROM #Tables WHERE NodeId=@nid; ;WITH children AS ( SELECT c.NodeId FROM #Edges e JOIN #Tables c ON c.SchemaName = e.ChildSchema AND c.TableName = e.ChildTable WHERE e.ParentSchema = @ps AND e.ParentTable = @pt ) UPDATE t SET InDegree = InDegree - 1 FROM #Tables t JOIN children ch ON ch.NodeId=t.NodeId; INSERT INTO #Queue(NodeId) SELECT t.NodeId FROM #Tables t WHERE t.ProcessOrder IS NULL AND t.InDegree = 0 AND NOT EXISTS (SELECT 1 FROM #Queue q WHERE q.NodeId=t.NodeId); END -- Cycle fallback UPDATE t SET ProcessOrder = @order + rn.rn - 1 FROM #Tables t JOIN ( SELECT NodeId, ROW_NUMBER() OVER (ORDER BY SchemaName, TableName) AS rn FROM #Tables WHERE ProcessOrder IS NULL ) rn ON rn.NodeId=t.NodeId; SET @Now = CONVERT(nvarchar(30), SYSDATETIME(), 121); RAISERROR(CONCAT(@Now, N' | ORDER | Table order ready.'), @sev, 1) WITH NOWAIT; -------------------------------------------------------------------------------- -- 5) Initialize/Reset MigrationState based on #Tables (Collation-safe comparisons) -------------------------------------------------------------------------------- DECLARE @InitStateSql nvarchar(max) = N' USE ' + @QTargetDb + N'; -- Insert missing state rows INSERT INTO dbo.MigrationState(TableSchema, TableName, Status) SELECT t.SchemaName, t.TableName, ''PENDING'' FROM #Tables t WHERE NOT EXISTS ( SELECT 1 FROM dbo.MigrationState s WHERE s.TableSchema COLLATE DATABASE_DEFAULT = t.SchemaName COLLATE DATABASE_DEFAULT AND s.TableName COLLATE DATABASE_DEFAULT = t.TableName COLLATE DATABASE_DEFAULT ); -- Reset mode IF (@Mode = ''RESET'') BEGIN UPDATE s SET Status=''PENDING'', StartedAt=NULL, FinishedAt=NULL, RowsCopied=NULL, LastError=NULL, LastRunId=NULL FROM dbo.MigrationState s WHERE EXISTS ( SELECT 1 FROM #Tables t WHERE t.SchemaName COLLATE DATABASE_DEFAULT = s.TableSchema COLLATE DATABASE_DEFAULT AND t.TableName COLLATE DATABASE_DEFAULT = s.TableName COLLATE DATABASE_DEFAULT ); END; INSERT dbo.MigrationCopyLog(RunId, Step, Details) VALUES (@RunId, N''STATE_READY'', NULL); '; EXEC sp_executesql @InitStateSql, N'@Mode varchar(10), @RunId uniqueidentifier', @Mode=@Mode, @RunId=@RunId; SET @Now = CONVERT(nvarchar(30), SYSDATETIME(), 121); RAISERROR(CONCAT(@Now, N' | STATE | MigrationState initialized (Mode=', @Mode, N')'), @sev, 1) WITH NOWAIT; -------------------------------------------------------------------------------- -- 6) Prepare target for load: disable triggers, nocheck constraints, disable NC indexes -------------------------------------------------------------------------------- DECLARE @PrepSql nvarchar(max) = N' USE ' + @QTargetDb + N'; DECLARE @cmd nvarchar(max); -- disable triggers SET @cmd = N''''; SELECT @cmd = @cmd + N''DISABLE TRIGGER ALL ON '' + QUOTENAME(s.name) + N''.'' + QUOTENAME(t.name) + N'';'' + CHAR(13)+CHAR(10) FROM sys.tables t JOIN sys.schemas s ON s.schema_id=t.schema_id WHERE t.is_ms_shipped=0; EXEC sp_executesql @cmd; -- nocheck constraints SET @cmd = N''''; SELECT @cmd = @cmd + N''ALTER TABLE '' + QUOTENAME(s.name) + N''.'' + QUOTENAME(t.name) + N'' NOCHECK CONSTRAINT ALL;'' + CHAR(13)+CHAR(10) FROM sys.tables t JOIN sys.schemas s ON s.schema_id=t.schema_id WHERE t.is_ms_shipped=0; EXEC sp_executesql @cmd; -- disable nonclustered indexes (skip PK/unique constraint) SET @cmd = N''''; SELECT @cmd = @cmd + N''ALTER INDEX '' + QUOTENAME(i.name) + N'' ON '' + QUOTENAME(s.name) + N''.'' + QUOTENAME(t.name) + N'' DISABLE;'' + CHAR(13)+CHAR(10) FROM sys.indexes i JOIN sys.tables t ON t.object_id=i.object_id JOIN sys.schemas s ON s.schema_id=t.schema_id WHERE t.is_ms_shipped=0 AND i.type_desc=''NONCLUSTERED'' AND i.is_primary_key=0 AND i.is_unique_constraint=0 AND i.name IS NOT NULL; EXEC sp_executesql @cmd; INSERT dbo.MigrationCopyLog(RunId, Step, Details) VALUES (@RunId, N''TARGET_PREPARED'', NULL); '; EXEC sp_executesql @PrepSql, N'@RunId uniqueidentifier', @RunId=@RunId; SET @Now = CONVERT(nvarchar(30), SYSDATETIME(), 121); RAISERROR(CONCAT(@Now, N' | PREP | Disabled triggers, constraints (nocheck), NC indexes in target.'), @sev, 1) WITH NOWAIT; -------------------------------------------------------------------------------- -- 7) Process tables (per table transaction + NOWAIT progress) -------------------------------------------------------------------------------- DECLARE @Schema nvarchar(128), @Table nvarchar(128); DECLARE @Continue bit = 1; DECLARE cur CURSOR LOCAL FAST_FORWARD FOR SELECT SchemaName, TableName FROM #Tables ORDER BY ProcessOrder; OPEN cur; FETCH NEXT FROM cur INTO @Schema, @Table; WHILE @@FETCH_STATUS = 0 AND @Continue = 1 BEGIN -- Get current status (collation-safe) DECLARE @Status varchar(20); DECLARE @GetStatusSql nvarchar(max) = N' USE ' + @QTargetDb + N'; SELECT @Status = Status FROM dbo.MigrationState WHERE TableSchema COLLATE DATABASE_DEFAULT = @Schema COLLATE DATABASE_DEFAULT AND TableName COLLATE DATABASE_DEFAULT = @Table COLLATE DATABASE_DEFAULT; '; EXEC sp_executesql @GetStatusSql, N'@Schema sysname, @Table sysname, @Status varchar(20) OUTPUT', @Schema=@Schema, @Table=@Table, @Status=@Status OUTPUT; IF @Mode = 'RESUME' AND @Status = 'DONE' BEGIN IF @Verbose = 1 BEGIN SET @Now = CONVERT(nvarchar(30), SYSDATETIME(), 121); RAISERROR(CONCAT(@Now, N' | SKIP | ', @Schema, N'.', @Table, N' already DONE'), @sev, 1) WITH NOWAIT; END FETCH NEXT FROM cur INTO @Schema, @Table; CONTINUE; END SET @Now = CONVERT(nvarchar(30), SYSDATETIME(), 121); RAISERROR(CONCAT(@Now, N' | TABLE_START | ', @Schema, N'.', @Table, N' (prev=', COALESCE(@Status,'?'), N')'), @sev, 1) WITH NOWAIT; BEGIN TRY -- Start per-table transaction EXEC (N'USE ' + @QTargetDb + N'; BEGIN TRAN;'); -- Mark RUNNING DECLARE @MarkRunning nvarchar(max) = N' USE ' + @QTargetDb + N'; UPDATE dbo.MigrationState SET Status=''RUNNING'', StartedAt=SYSUTCDATETIME(), FinishedAt=NULL, RowsCopied=NULL, LastError=NULL, LastRunId=@RunId WHERE TableSchema COLLATE DATABASE_DEFAULT = @Schema COLLATE DATABASE_DEFAULT AND TableName COLLATE DATABASE_DEFAULT = @Table COLLATE DATABASE_DEFAULT; INSERT dbo.MigrationCopyLog(RunId, Step, Details) VALUES (@RunId, N''TABLE_START'', CONCAT(@Schema, N''.'', @Table)); '; EXEC sp_executesql @MarkRunning, N'@RunId uniqueidentifier, @Schema sysname, @Table sysname', @RunId=@RunId, @Schema=@Schema, @Table=@Table; -------------------------------------------------------------------- -- Copy logic inside target context; returns OldRows/NewRows -------------------------------------------------------------------- DECLARE @CopySql nvarchar(max) = N' USE ' + @QTargetDb + N'; DECLARE @SchemaName sysname = @Schema; DECLARE @TableName sysname = @Table; DECLARE @Tgt nvarchar(400) = QUOTENAME(@SchemaName) + N''.'' + QUOTENAME(@TableName); DECLARE @HasIdentity bit = 0; DECLARE @IdentCol sysname = NULL; DECLARE @IdentType sysname = NULL; DECLARE @ColList nvarchar(max); DECLARE @SelList nvarchar(max); DECLARE @DelSql nvarchar(max); DECLARE @ReseedSql nvarchar(max); DECLARE @InsSql nvarchar(max); DECLARE @cntOld bigint, @cntNew bigint; -- Source rowcount (source DB) SELECT @cntOld = COUNT_BIG(*) FROM ' + @QSourceDb + N'.' + QUOTENAME(@Schema) + N'.' + QUOTENAME(@Table) + N'; -- Identity column in target (if any) SELECT TOP(1) @HasIdentity = 1, @IdentCol = c.name, @IdentType = ty.name FROM sys.columns c JOIN sys.tables t ON t.object_id=c.object_id JOIN sys.schemas s ON s.schema_id=t.schema_id JOIN sys.types ty ON ty.user_type_id=c.user_type_id WHERE s.name=@SchemaName AND t.name=@TableName AND c.is_identity=1 ORDER BY c.column_id; -- Build column lists based on TARGET types (so we know varchar/char) ;WITH tgt AS ( SELECT c.name, c.column_id, c.is_computed, ty.name AS type_name FROM sys.columns c JOIN sys.tables t ON t.object_id=c.object_id JOIN sys.schemas s ON s.schema_id=t.schema_id JOIN sys.types ty ON ty.user_type_id=c.user_type_id WHERE s.name=@SchemaName AND t.name=@TableName AND c.is_computed=0 AND ty.name NOT IN (''timestamp'',''rowversion'') ), src AS ( SELECT c.name FROM ' + @QSourceDb + N'.sys.columns c JOIN ' + @QSourceDb + N'.sys.tables t ON t.object_id=c.object_id JOIN ' + @QSourceDb + N'.sys.schemas s ON s.schema_id=t.schema_id WHERE s.name=@SchemaName AND t.name=@TableName ), cols AS ( SELECT t.name, t.column_id, t.type_name FROM tgt t JOIN src s ON s.name COLLATE DATABASE_DEFAULT = t.name COLLATE DATABASE_DEFAULT ) SELECT @ColList = STRING_AGG(QUOTENAME(name), N'','') WITHIN GROUP (ORDER BY column_id), @SelList = STRING_AGG( CASE WHEN type_name IN (''varchar'',''char'') THEN QUOTENAME(name) + N'' COLLATE DATABASE_DEFAULT'' ELSE QUOTENAME(name) END, N'','' ) WITHIN GROUP (ORDER BY column_id) FROM cols; IF @ColList IS NULL OR @SelList IS NULL THROW 50010, ''No common insertable columns between source and target for this table.'', 1; -- DELETE target (rerunnable) SET @DelSql = N''DELETE FROM '' + @Tgt + N'';''; EXEC sp_executesql @DelSql; -- Reseed identity (if any) IF @HasIdentity = 1 BEGIN SET @ReseedSql = N''DBCC CHECKIDENT ('' + @Tgt + N'', RESEED, 0) WITH NO_INFOMSGS;''; EXEC sp_executesql @ReseedSql; END -- Batched insert if identity int/bigint IF @HasIdentity = 1 AND @IdentType IN (''int'',''bigint'') BEGIN DECLARE @min bigint, @max bigint, @start bigint, @end bigint; DECLARE @MM nvarchar(max); SET @MM = N''SELECT @min = MIN(CONVERT(bigint,'' + QUOTENAME(@IdentCol) + N'')), @max = MAX(CONVERT(bigint,'' + QUOTENAME(@IdentCol) + N'')) FROM ' + @QSourceDb + N'.'' + QUOTENAME(@SchemaName) + N''.'' + QUOTENAME(@TableName) + N'';''; EXEC sp_executesql @MM, N''@min bigint OUTPUT, @max bigint OUTPUT'', @min=@min OUTPUT, @max=@max OUTPUT; IF @min IS NOT NULL AND @max IS NOT NULL BEGIN SET @start = @min; WHILE @start <= @max BEGIN SET @end = @start + @BatchSize - 1; SET @InsSql = N''''; IF @HasIdentity = 1 SET @InsSql = @InsSql + N''SET IDENTITY_INSERT '' + @Tgt + N'' ON;'' + CHAR(13)+CHAR(10); SET @InsSql = @InsSql + N''INSERT INTO '' + @Tgt + N'' ('' + @ColList + N'')'' + CHAR(13)+CHAR(10) + N''SELECT '' + @SelList + CHAR(13)+CHAR(10) + N''FROM ' + @QSourceDb + N'.'' + QUOTENAME(@SchemaName) + N''.'' + QUOTENAME(@TableName) + CHAR(13)+CHAR(10) + N''WHERE '' + QUOTENAME(@IdentCol) + N'' >= '' + CAST(@start AS nvarchar(30)) + N'' AND '' + QUOTENAME(@IdentCol) + N'' <= '' + CAST(@end AS nvarchar(30)) + N'';'' + CHAR(13)+CHAR(10); IF @HasIdentity = 1 SET @InsSql = @InsSql + N''SET IDENTITY_INSERT '' + @Tgt + N'' OFF;'' + CHAR(13)+CHAR(10); EXEC sp_executesql @InsSql; SET @start = @end + 1; END END END ELSE BEGIN -- Non-batched insert SET @InsSql = N''''; IF @HasIdentity = 1 SET @InsSql = @InsSql + N''SET IDENTITY_INSERT '' + @Tgt + N'' ON;'' + CHAR(13)+CHAR(10); SET @InsSql = @InsSql + N''INSERT INTO '' + @Tgt + N'' ('' + @ColList + N'')'' + CHAR(13)+CHAR(10) + N''SELECT '' + @SelList + CHAR(13)+CHAR(10) + N''FROM ' + @QSourceDb + N'.'' + QUOTENAME(@SchemaName) + N''.'' + QUOTENAME(@TableName) + N'';'' + CHAR(13)+CHAR(10); IF @HasIdentity = 1 SET @InsSql = @InsSql + N''SET IDENTITY_INSERT '' + @Tgt + N'' OFF;'' + CHAR(13)+CHAR(10); EXEC sp_executesql @InsSql; END -- Target rowcount SELECT @cntNew = COUNT_BIG(*) FROM ' + QUOTENAME(@Schema) + N'.' + QUOTENAME(@Table) + N'; SELECT @cntOld AS OldRows, @cntNew AS NewRows; '; IF OBJECT_ID('tempdb..#Cnt') IS NOT NULL DROP TABLE #Cnt; CREATE TABLE #Cnt(OldRows bigint, NewRows bigint); INSERT INTO #Cnt(OldRows, NewRows) EXEC sp_executesql @CopySql, N'@Schema sysname, @Table sysname, @BatchSize int', @Schema=@Schema, @Table=@Table, @BatchSize=@BatchSize; DECLARE @OldRows bigint, @NewRows bigint; SELECT TOP(1) @OldRows=OldRows, @NewRows=NewRows FROM #Cnt; -- Mark DONE DECLARE @MarkDone nvarchar(max) = N' USE ' + @QTargetDb + N'; UPDATE dbo.MigrationState SET Status=''DONE'', FinishedAt=SYSUTCDATETIME(), RowsCopied=@Rows, LastError=NULL, LastRunId=@RunId WHERE TableSchema COLLATE DATABASE_DEFAULT = @Schema COLLATE DATABASE_DEFAULT AND TableName COLLATE DATABASE_DEFAULT = @Table COLLATE DATABASE_DEFAULT; INSERT dbo.MigrationCopyLog(RunId, Step, Details) VALUES (@RunId, N''TABLE_DONE'', CONCAT(@Schema, N''.'', @Table, N''; OldRows='', @Old, N''; NewRows='', @New)); '; EXEC sp_executesql @MarkDone, N'@RunId uniqueidentifier, @Schema sysname, @Table sysname, @Rows bigint, @Old bigint, @New bigint', @RunId=@RunId, @Schema=@Schema, @Table=@Table, @Rows=@NewRows, @Old=@OldRows, @New=@NewRows; -- Commit this table EXEC (N'USE ' + @QTargetDb + N'; COMMIT;'); SET @Now = CONVERT(nvarchar(30), SYSDATETIME(), 121); RAISERROR(CONCAT(@Now, N' | TABLE_DONE | ', @Schema, N'.', @Table, N' | Old=', @OldRows, N' New=', @NewRows), @sev, 1) WITH NOWAIT; END TRY BEGIN CATCH DECLARE @Err nvarchar(max) = CONCAT(N'Error ', ERROR_NUMBER(), N': ', ERROR_MESSAGE(), N' (Line ', ERROR_LINE(), N')'); -- Rollback if open EXEC (N'USE ' + @QTargetDb + N'; IF XACT_STATE() <> 0 ROLLBACK;'); -- Mark FAILED DECLARE @MarkFail nvarchar(max) = N' USE ' + @QTargetDb + N'; UPDATE dbo.MigrationState SET Status=''FAILED'', FinishedAt=SYSUTCDATETIME(), LastError=@Err, LastRunId=@RunId WHERE TableSchema COLLATE DATABASE_DEFAULT = @Schema COLLATE DATABASE_DEFAULT AND TableName COLLATE DATABASE_DEFAULT = @Table COLLATE DATABASE_DEFAULT; INSERT dbo.MigrationCopyLog(RunId, Step, Details) VALUES (@RunId, N''TABLE_FAILED'', CONCAT(@Schema, N''.'', @Table, N''; '', @Err)); '; EXEC sp_executesql @MarkFail, N'@RunId uniqueidentifier, @Schema sysname, @Table sysname, @Err nvarchar(max)', @RunId=@RunId, @Schema=@Schema, @Table=@Table, @Err=@Err; SET @Now = CONVERT(nvarchar(30), SYSDATETIME(), 121); RAISERROR(CONCAT(@Now, N' | TABLE_FAILED | ', @Schema, N'.', @Table, N' | ', @Err), @sev, 1) WITH NOWAIT; IF @StopOnError = 1 SET @Continue = 0; END CATCH; FETCH NEXT FROM cur INTO @Schema, @Table; END CLOSE cur; DEALLOCATE cur; IF @Continue = 0 BEGIN SET @Now = CONVERT(nvarchar(30), SYSDATETIME(), 121); RAISERROR(CONCAT(@Now, N' | ABORT | Stopped due to StopOnError=1. See dbo.MigrationState/MigrationCopyLog.'), @sev, 1) WITH NOWAIT; THROW 50050, 'Run aborted due to table error (see dbo.MigrationState / dbo.MigrationCopyLog).', 1; END -------------------------------------------------------------------------------- -- 8) Finalize target: rebuild NC indexes, enable constraints WITH CHECK, enable triggers -------------------------------------------------------------------------------- DECLARE @FinalizeSql nvarchar(max) = N' USE ' + @QTargetDb + N'; DECLARE @cmd nvarchar(max); -- rebuild nonclustered indexes SET @cmd = N''''; SELECT @cmd = @cmd + N''ALTER INDEX '' + QUOTENAME(i.name) + N'' ON '' + QUOTENAME(s.name) + N''.'' + QUOTENAME(t.name) + N'' REBUILD;'' + CHAR(13)+CHAR(10) FROM sys.indexes i JOIN sys.tables t ON t.object_id=i.object_id JOIN sys.schemas s ON s.schema_id=t.schema_id WHERE t.is_ms_shipped=0 AND i.type_desc=''NONCLUSTERED'' AND i.is_primary_key=0 AND i.is_unique_constraint=0 AND i.name IS NOT NULL; EXEC sp_executesql @cmd; -- enable constraints with validation SET @cmd = N''''; SELECT @cmd = @cmd + N''ALTER TABLE '' + QUOTENAME(s.name) + N''.'' + QUOTENAME(t.name) + N'' WITH CHECK CHECK CONSTRAINT ALL;'' + CHAR(13)+CHAR(10) FROM sys.tables t JOIN sys.schemas s ON s.schema_id=t.schema_id WHERE t.is_ms_shipped=0; EXEC sp_executesql @cmd; -- enable triggers SET @cmd = N''''; SELECT @cmd = @cmd + N''ENABLE TRIGGER ALL ON '' + QUOTENAME(s.name) + N''.'' + QUOTENAME(t.name) + N'';'' + CHAR(13)+CHAR(10) FROM sys.tables t JOIN sys.schemas s ON s.schema_id=t.schema_id WHERE t.is_ms_shipped=0; EXEC sp_executesql @cmd; INSERT dbo.MigrationCopyLog(RunId, Step, Details) VALUES (@RunId, N''RUN_DONE'', NULL); '; EXEC sp_executesql @FinalizeSql, N'@RunId uniqueidentifier', @RunId=@RunId; SET @Now = CONVERT(nvarchar(30), SYSDATETIME(), 121); RAISERROR(CONCAT(@Now, N' | DONE | Migration finished successfully. Check ', @TargetDb, N'.dbo.MigrationState / MigrationCopyLog'), @sev, 1) WITH NOWAIT; GO