/* ============================================================
   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
