Initial version of collation migration scripts.

This commit is contained in:
2026-02-05 07:48:34 +01:00
commit 505a9d52a6
6 changed files with 1339 additions and 0 deletions

View File

@@ -0,0 +1,485 @@
/* ============================================================
Compare Script (Schema + Rowcounts + Programmable Objects)
SQL Server 2022
Compares SOURCE and TARGET databases without comparing row contents:
- Rowcount per table (diff list + summary)
- Tables/columns (datatype/length/nullable/collation) diff list + summary
- Programmable objects (views/procs/functions) via SHA2_256 hash of definition
- Triggers via SHA2_256 hash of definition
- Users/Roles existence + role memberships (name-based)
Output:
- Textual progress and summary via RAISERROR ... WITH NOWAIT
- Result sets with detailed diffs
Configure DB names below.
============================================================ */
SET NOCOUNT ON;
SET XACT_ABORT ON;
DECLARE @SourceDb sysname = N'sysdb_UTF8';
DECLARE @TargetDb sysname = N'sysdb_utf8_jr';
DECLARE @QSource sysname = QUOTENAME(@SourceDb);
DECLARE @QTarget sysname = QUOTENAME(@TargetDb);
DECLARE @sev int = 10;
DECLARE @Now nvarchar(30);
DECLARE @msg nvarchar(4000);
SET @Now = CONVERT(nvarchar(30), SYSDATETIME(), 121);
SET @msg = CONCAT(@Now, N' | COMPARE_START | Source=', @SourceDb, N' | Target=', @TargetDb);
RAISERROR(@msg, @sev, 1) WITH NOWAIT;
--------------------------------------------------------------------------------
-- 1) Rowcounts per table
--------------------------------------------------------------------------------
SET @Now = CONVERT(nvarchar(30), SYSDATETIME(), 121);
RAISERROR(CONCAT(@Now, N' | STEP 1/5 | Rowcounts per table...'), @sev, 1) WITH NOWAIT;
IF OBJECT_ID('tempdb..#RowCounts') IS NOT NULL DROP TABLE #RowCounts;
CREATE TABLE #RowCounts(
DbName sysname NOT NULL,
SchemaName sysname NOT NULL,
TableName sysname NOT NULL,
RowCount bigint NOT NULL
);
DECLARE @sql nvarchar(max);
SET @sql = N'
INSERT INTO #RowCounts(DbName, SchemaName, TableName, RowCount)
SELECT N''' + REPLACE(@SourceDb,'''','''''') + N''', s.name, t.name, SUM(p.rows)
FROM ' + @QSource + N'.sys.tables t
JOIN ' + @QSource + N'.sys.schemas s ON s.schema_id = t.schema_id
JOIN ' + @QSource + N'.sys.partitions p ON p.object_id = t.object_id AND p.index_id IN (0,1)
WHERE t.is_ms_shipped=0
GROUP BY s.name, t.name;';
EXEC sp_executesql @sql;
SET @sql = N'
INSERT INTO #RowCounts(DbName, SchemaName, TableName, RowCount)
SELECT N''' + REPLACE(@TargetDb,'''','''''') + N''', s.name, t.name, SUM(p.rows)
FROM ' + @QTarget + N'.sys.tables t
JOIN ' + @QTarget + N'.sys.schemas s ON s.schema_id = t.schema_id
JOIN ' + @QTarget + N'.sys.partitions p ON p.object_id = t.object_id AND p.index_id IN (0,1)
WHERE t.is_ms_shipped=0
GROUP BY s.name, t.name;';
EXEC sp_executesql @sql;
;WITH s AS (
SELECT SchemaName, TableName, RowCount FROM #RowCounts WHERE DbName=@SourceDb
),
t AS (
SELECT SchemaName, TableName, RowCount FROM #RowCounts WHERE DbName=@TargetDb
),
d AS (
SELECT
COALESCE(s.SchemaName, t.SchemaName) AS SchemaName,
COALESCE(s.TableName, t.TableName) AS TableName,
s.RowCount AS SourceRows,
t.RowCount AS TargetRows,
(ISNULL(t.RowCount,0) - ISNULL(s.RowCount,0)) AS Diff
FROM s
FULL OUTER JOIN t
ON t.SchemaName = s.SchemaName
AND t.TableName = s.TableName
)
SELECT *
INTO #RowDiff
FROM d
WHERE ISNULL(SourceRows,-1) <> ISNULL(TargetRows,-1);
DECLARE @RowDiffCount int = (SELECT COUNT(*) FROM #RowDiff);
DECLARE @RowTotalSource int = (SELECT COUNT(*) FROM (SELECT DISTINCT SchemaName, TableName FROM #RowCounts WHERE DbName=@SourceDb) x);
DECLARE @RowTotalTarget int = (SELECT COUNT(*) FROM (SELECT DISTINCT SchemaName, TableName FROM #RowCounts WHERE DbName=@TargetDb) x);
SET @Now = CONVERT(nvarchar(30), SYSDATETIME(), 121);
RAISERROR(CONCAT(@Now, N' | ROWCOUNTS | SourceTables=', @RowTotalSource, N' TargetTables=', @RowTotalTarget, N' TablesWithRowDiff=', @RowDiffCount), @sev, 1) WITH NOWAIT;
IF @RowDiffCount = 0
RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | ROWCOUNTS_OK | All table rowcounts match.'), @sev, 1) WITH NOWAIT;
ELSE
BEGIN
RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | ROWCOUNTS_DIFF | Showing tables with rowcount differences...'), @sev, 1) WITH NOWAIT;
SELECT * FROM #RowDiff ORDER BY ABS(Diff) DESC, SchemaName, TableName;
END
--------------------------------------------------------------------------------
-- 2) Column/schema diffs
--------------------------------------------------------------------------------
SET @Now = CONVERT(nvarchar(30), SYSDATETIME(), 121);
RAISERROR(CONCAT(@Now, N' | STEP 2/5 | Table/column schema diffs...'), @sev, 1) WITH NOWAIT;
IF OBJECT_ID('tempdb..#Cols') IS NOT NULL DROP TABLE #Cols;
CREATE TABLE #Cols(
DbName sysname,
SchemaName sysname,
TableName sysname,
ColumnName sysname,
TypeName sysname,
MaxLen int,
PrecisionVal int,
ScaleVal int,
IsNullable bit,
CollationName sysname NULL
);
SET @sql = N'
INSERT #Cols
SELECT N''' + REPLACE(@SourceDb,'''','''''') + N''',
s.name, t.name, c.name,
ty.name,
c.max_length,
c.precision,
c.scale,
c.is_nullable,
c.collation_name
FROM ' + @QSource + N'.sys.columns c
JOIN ' + @QSource + N'.sys.tables t ON t.object_id=c.object_id
JOIN ' + @QSource + N'.sys.schemas s ON s.schema_id=t.schema_id
JOIN ' + @QSource + N'.sys.types ty ON ty.user_type_id=c.user_type_id
WHERE t.is_ms_shipped=0;';
EXEC sp_executesql @sql;
SET @sql = N'
INSERT #Cols
SELECT N''' + REPLACE(@TargetDb,'''','''''') + N''',
s.name, t.name, c.name,
ty.name,
c.max_length,
c.precision,
c.scale,
c.is_nullable,
c.collation_name
FROM ' + @QTarget + N'.sys.columns c
JOIN ' + @QTarget + N'.sys.tables t ON t.object_id=c.object_id
JOIN ' + @QTarget + N'.sys.schemas s ON s.schema_id=t.schema_id
JOIN ' + @QTarget + N'.sys.types ty ON ty.user_type_id=c.user_type_id
WHERE t.is_ms_shipped=0;';
EXEC sp_executesql @sql;
;WITH s AS (SELECT * FROM #Cols WHERE DbName=@SourceDb),
t AS (SELECT * FROM #Cols WHERE DbName=@TargetDb),
d AS (
SELECT
COALESCE(s.SchemaName, t.SchemaName) AS SchemaName,
COALESCE(s.TableName, t.TableName) AS TableName,
COALESCE(s.ColumnName, t.ColumnName) AS ColumnName,
CONCAT(ISNULL(s.TypeName,N''), N'(', ISNULL(CONVERT(nvarchar(20),s.MaxLen),N''), N')') AS SourceType,
CONCAT(ISNULL(t.TypeName,N''), N'(', ISNULL(CONVERT(nvarchar(20),t.MaxLen),N''), N')') AS TargetType,
s.IsNullable AS SourceNullable,
t.IsNullable AS TargetNullable,
s.CollationName AS SourceCollation,
t.CollationName AS TargetCollation
FROM s
FULL OUTER JOIN t
ON t.SchemaName = s.SchemaName
AND t.TableName = s.TableName
AND t.ColumnName = s.ColumnName
)
SELECT *
INTO #ColDiff
FROM d
WHERE
ISNULL(SourceType,N'') <> ISNULL(TargetType,N'')
OR ISNULL(SourceNullable,2) <> ISNULL(TargetNullable,2)
OR ISNULL(SourceCollation,N'') <> ISNULL(TargetCollation,N'');
DECLARE @ColDiffCount int = (SELECT COUNT(*) FROM #ColDiff);
RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | COLUMNS | ColumnDiffs=', @ColDiffCount), @sev, 1) WITH NOWAIT;
IF @ColDiffCount = 0
RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | COLUMNS_OK | No column definition diffs detected.'), @sev, 1) WITH NOWAIT;
ELSE
BEGIN
RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | COLUMNS_DIFF | Showing column diffs...'), @sev, 1) WITH NOWAIT;
SELECT * FROM #ColDiff ORDER BY SchemaName, TableName, ColumnName;
END
--------------------------------------------------------------------------------
-- 3) Programmable objects
--------------------------------------------------------------------------------
RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | STEP 3/5 | Programmable objects (hash compare)...'), @sev, 1) WITH NOWAIT;
IF OBJECT_ID('tempdb..#Modules') IS NOT NULL DROP TABLE #Modules;
CREATE TABLE #Modules(
DbName sysname,
ObjectType nvarchar(60),
SchemaName sysname,
ObjectName sysname,
DefinitionHash varbinary(32) NULL
);
SET @sql = N'
INSERT #Modules
SELECT
N''' + REPLACE(@SourceDb,'''','''''') + N''',
o.type_desc,
s.name,
o.name,
HASHBYTES(''SHA2_256'', CONVERT(varbinary(max), m.definition))
FROM ' + @QSource + N'.sys.objects o
JOIN ' + @QSource + N'.sys.schemas s ON s.schema_id=o.schema_id
JOIN ' + @QSource + N'.sys.sql_modules m ON m.object_id=o.object_id
WHERE o.is_ms_shipped=0
AND o.type IN (''V'',''P'',''FN'',''IF'',''TF'');';
EXEC sp_executesql @sql;
SET @sql = N'
INSERT #Modules
SELECT
N''' + REPLACE(@TargetDb,'''','''''') + N''',
o.type_desc,
s.name,
o.name,
HASHBYTES(''SHA2_256'', CONVERT(varbinary(max), m.definition))
FROM ' + @QTarget + N'.sys.objects o
JOIN ' + @QTarget + N'.sys.schemas s ON s.schema_id=o.schema_id
JOIN ' + @QTarget + N'.sys.sql_modules m ON m.object_id=o.object_id
WHERE o.is_ms_shipped=0
AND o.type IN (''V'',''P'',''FN'',''IF'',''TF'');';
EXEC sp_executesql @sql;
;WITH s AS (SELECT * FROM #Modules WHERE DbName=@SourceDb),
t AS (SELECT * FROM #Modules WHERE DbName=@TargetDb),
d AS (
SELECT
COALESCE(s.ObjectType, t.ObjectType) AS ObjectType,
COALESCE(s.SchemaName, t.SchemaName) AS SchemaName,
COALESCE(s.ObjectName, t.ObjectName) AS ObjectName,
CASE WHEN s.ObjectName IS NULL THEN 'MISSING_IN_SOURCE'
WHEN t.ObjectName IS NULL THEN 'MISSING_IN_TARGET'
WHEN s.DefinitionHash <> t.DefinitionHash THEN 'DIFFERENT_DEFINITION'
ELSE 'OK' END AS Status
FROM s
FULL OUTER JOIN t
ON t.ObjectType = s.ObjectType
AND t.SchemaName = s.SchemaName
AND t.ObjectName = s.ObjectName
)
SELECT *
INTO #ModuleDiff
FROM d
WHERE Status <> 'OK';
DECLARE @ModuleDiffCount int = (SELECT COUNT(*) FROM #ModuleDiff);
RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | MODULES | Differences=', @ModuleDiffCount), @sev, 1) WITH NOWAIT;
IF @ModuleDiffCount = 0
RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | MODULES_OK | No module definition diffs detected.'), @sev, 1) WITH NOWAIT;
ELSE
BEGIN
RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | MODULES_DIFF | Showing module diffs...'), @sev, 1) WITH NOWAIT;
SELECT * FROM #ModuleDiff ORDER BY ObjectType, SchemaName, ObjectName;
END
--------------------------------------------------------------------------------
-- 4) Triggers
--------------------------------------------------------------------------------
RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | STEP 4/5 | Triggers (hash compare)...'), @sev, 1) WITH NOWAIT;
IF OBJECT_ID('tempdb..#Triggers') IS NOT NULL DROP TABLE #Triggers;
CREATE TABLE #Triggers(
DbName sysname,
TriggerSchema sysname,
TriggerName sysname,
ParentSchema sysname,
ParentName sysname,
DefinitionHash varbinary(32) NULL
);
SET @sql = N'
INSERT #Triggers
SELECT
N''' + REPLACE(@SourceDb,'''','''''') + N''',
ss.name,
tr.name,
ps.name,
pt.name,
HASHBYTES(''SHA2_256'', CONVERT(varbinary(max), m.definition))
FROM ' + @QSource + N'.sys.triggers tr
JOIN ' + @QSource + N'.sys.tables pt ON pt.object_id = tr.parent_id
JOIN ' + @QSource + N'.sys.schemas ps ON ps.schema_id = pt.schema_id
JOIN ' + @QSource + N'.sys.schemas ss ON ss.schema_id = tr.schema_id
JOIN ' + @QSource + N'.sys.sql_modules m ON m.object_id = tr.object_id
WHERE tr.is_ms_shipped=0;';
EXEC sp_executesql @sql;
SET @sql = N'
INSERT #Triggers
SELECT
N''' + REPLACE(@TargetDb,'''','''''') + N''',
ss.name,
tr.name,
ps.name,
pt.name,
HASHBYTES(''SHA2_256'', CONVERT(varbinary(max), m.definition))
FROM ' + @QTarget + N'.sys.triggers tr
JOIN ' + @QTarget + N'.sys.tables pt ON pt.object_id = tr.parent_id
JOIN ' + @QTarget + N'.sys.schemas ps ON ps.schema_id = pt.schema_id
JOIN ' + @QTarget + N'.sys.schemas ss ON ss.schema_id = tr.schema_id
JOIN ' + @QTarget + N'.sys.sql_modules m ON m.object_id = tr.object_id
WHERE tr.is_ms_shipped=0;';
EXEC sp_executesql @sql;
;WITH s AS (SELECT * FROM #Triggers WHERE DbName=@SourceDb),
t AS (SELECT * FROM #Triggers WHERE DbName=@TargetDb),
d AS (
SELECT
COALESCE(s.TriggerSchema, t.TriggerSchema) AS TriggerSchema,
COALESCE(s.TriggerName, t.TriggerName) AS TriggerName,
COALESCE(s.ParentSchema, t.ParentSchema) AS ParentSchema,
COALESCE(s.ParentName, t.ParentName) AS ParentTable,
CASE WHEN s.TriggerName IS NULL THEN 'MISSING_IN_SOURCE'
WHEN t.TriggerName IS NULL THEN 'MISSING_IN_TARGET'
WHEN s.DefinitionHash <> t.DefinitionHash THEN 'DIFFERENT_DEFINITION'
ELSE 'OK' END AS Status
FROM s
FULL OUTER JOIN t
ON t.TriggerSchema = s.TriggerSchema
AND t.TriggerName = s.TriggerName
AND t.ParentSchema = s.ParentSchema
AND t.ParentName = s.ParentName
)
SELECT *
INTO #TriggerDiff
FROM d
WHERE Status <> 'OK';
DECLARE @TriggerDiffCount int = (SELECT COUNT(*) FROM #TriggerDiff);
RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | TRIGGERS | Differences=', @TriggerDiffCount), @sev, 1) WITH NOWAIT;
IF @TriggerDiffCount = 0
RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | TRIGGERS_OK | No trigger definition diffs detected.'), @sev, 1) WITH NOWAIT;
ELSE
BEGIN
RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | TRIGGERS_DIFF | Showing trigger diffs...'), @sev, 1) WITH NOWAIT;
SELECT * FROM #TriggerDiff ORDER BY TriggerSchema, TriggerName, ParentSchema, ParentTable;
END
--------------------------------------------------------------------------------
-- 5) Users/Roles
--------------------------------------------------------------------------------
RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | STEP 5/5 | Users/Roles (existence + memberships)...'), @sev, 1) WITH NOWAIT;
IF OBJECT_ID('tempdb..#Principals') IS NOT NULL DROP TABLE #Principals;
CREATE TABLE #Principals(
DbName sysname,
Name sysname,
TypeDesc nvarchar(60)
);
SET @sql = N'
INSERT #Principals
SELECT N''' + REPLACE(@SourceDb,'''','''''') + N''', name, type_desc
FROM ' + @QSource + N'.sys.database_principals
WHERE type IN (''S'',''U'',''G'',''R'')
AND name NOT IN (''dbo'',''guest'',''INFORMATION_SCHEMA'',''sys'');';
EXEC sp_executesql @sql;
SET @sql = N'
INSERT #Principals
SELECT N''' + REPLACE(@TargetDb,'''','''''') + N''', name, type_desc
FROM ' + @QTarget + N'.sys.database_principals
WHERE type IN (''S'',''U'',''G'',''R'')
AND name NOT IN (''dbo'',''guest'',''INFORMATION_SCHEMA'',''sys'');';
EXEC sp_executesql @sql;
;WITH s AS (SELECT * FROM #Principals WHERE DbName=@SourceDb),
t AS (SELECT * FROM #Principals WHERE DbName=@TargetDb),
d AS (
SELECT
COALESCE(s.Name, t.Name) AS PrincipalName,
s.TypeDesc AS SourceType,
t.TypeDesc AS TargetType,
CASE WHEN s.Name IS NULL THEN 'MISSING_IN_SOURCE'
WHEN t.Name IS NULL THEN 'MISSING_IN_TARGET'
WHEN s.TypeDesc <> t.TypeDesc THEN 'TYPE_DIFF'
ELSE 'OK' END AS Status
FROM s
FULL OUTER JOIN t ON t.Name = s.Name
)
SELECT *
INTO #PrincipalDiff
FROM d
WHERE Status <> 'OK';
DECLARE @PrincipalDiffCount int = (SELECT COUNT(*) FROM #PrincipalDiff);
RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | PRINCIPALS | Differences=', @PrincipalDiffCount), @sev, 1) WITH NOWAIT;
IF @PrincipalDiffCount > 0
BEGIN
RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | PRINCIPALS_DIFF | Showing principal diffs...'), @sev, 1) WITH NOWAIT;
SELECT * FROM #PrincipalDiff ORDER BY PrincipalName;
END
ELSE
RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | PRINCIPALS_OK | No principal diffs detected.'), @sev, 1) WITH NOWAIT;
IF OBJECT_ID('tempdb..#RoleMembers') IS NOT NULL DROP TABLE #RoleMembers;
CREATE TABLE #RoleMembers(
DbName sysname,
RoleName sysname,
MemberName sysname
);
SET @sql = N'
INSERT #RoleMembers
SELECT N''' + REPLACE(@SourceDb,'''','''''') + N''',
r.name AS RoleName, m.name AS MemberName
FROM ' + @QSource + N'.sys.database_role_members rm
JOIN ' + @QSource + N'.sys.database_principals r ON r.principal_id = rm.role_principal_id
JOIN ' + @QSource + N'.sys.database_principals m ON m.principal_id = rm.member_principal_id
WHERE r.name NOT IN (''public'');';
EXEC sp_executesql @sql;
SET @sql = N'
INSERT #RoleMembers
SELECT N''' + REPLACE(@TargetDb,'''','''''') + N''',
r.name AS RoleName, m.name AS MemberName
FROM ' + @QTarget + N'.sys.database_role_members rm
JOIN ' + @QTarget + N'.sys.database_principals r ON r.principal_id = rm.role_principal_id
JOIN ' + @QTarget + N'.sys.database_principals m ON m.principal_id = rm.member_principal_id
WHERE r.name NOT IN (''public'');';
EXEC sp_executesql @sql;
;WITH s AS (SELECT RoleName, MemberName FROM #RoleMembers WHERE DbName=@SourceDb),
t AS (SELECT RoleName, MemberName FROM #RoleMembers WHERE DbName=@TargetDb),
d AS (
SELECT
COALESCE(s.RoleName, t.RoleName) AS RoleName,
COALESCE(s.MemberName, t.MemberName) AS MemberName,
CASE WHEN s.RoleName IS NULL THEN 'MISSING_IN_SOURCE'
WHEN t.RoleName IS NULL THEN 'MISSING_IN_TARGET'
ELSE 'OK' END AS Status
FROM s
FULL OUTER JOIN t
ON t.RoleName = s.RoleName AND t.MemberName = s.MemberName
)
SELECT *
INTO #RoleMemberDiff
FROM d
WHERE Status <> 'OK';
DECLARE @RoleMemberDiffCount int = (SELECT COUNT(*) FROM #RoleMemberDiff);
RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | ROLEMEMBERS | Differences=', @RoleMemberDiffCount), @sev, 1) WITH NOWAIT;
IF @RoleMemberDiffCount > 0
BEGIN
RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | ROLEMEMBERS_DIFF | Showing role membership diffs...'), @sev, 1) WITH NOWAIT;
SELECT * FROM #RoleMemberDiff ORDER BY RoleName, MemberName;
END
ELSE
RAISERROR(CONCAT(CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | ROLEMEMBERS_OK | No role membership diffs detected.'), @sev, 1) WITH NOWAIT;
RAISERROR(CONCAT(
CONVERT(nvarchar(30), SYSDATETIME(), 121), N' | COMPARE_DONE | ',
N'RowDiffTables=', @RowDiffCount,
N' | ColumnDiffs=', @ColDiffCount,
N' | ModuleDiffs=', @ModuleDiffCount,
N' | TriggerDiffs=', @TriggerDiffCount,
N' | PrincipalDiffs=', @PrincipalDiffCount,
N' | RoleMemberDiffs=', @RoleMemberDiffCount
), @sev, 1) WITH NOWAIT;

View File

@@ -0,0 +1,679 @@
/* ============================================================
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