Initial version of collation migration scripts.
This commit is contained in:
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# SQL Server / tooling
|
||||||
|
*.bak
|
||||||
|
*.trn
|
||||||
|
*.mdf
|
||||||
|
*.ndf
|
||||||
|
*.ldf
|
||||||
|
|
||||||
|
# OS / editor
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Archives
|
||||||
|
*.zip
|
||||||
50
README.md
Normal file
50
README.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# SQL Server Collation Migration (UTF-8 / UTF-16) – Data Copy + Verification
|
||||||
|
|
||||||
|
This repository contains **two main scripts** to support a SQL Server 2022 migration scenario:
|
||||||
|
|
||||||
|
1. **Data copy (Variant B / Checkpointing)**: copies data from a source database into a target database **table-by-table** with restart capability.
|
||||||
|
2. **Verification / compare script**: compares source & target databases **without comparing row contents** (rowcounts + schema + programmable objects + principals).
|
||||||
|
|
||||||
|
> Intended usage: build a new target DB (e.g. UTF-8 collation), deploy schema, run copy, then verify.
|
||||||
|
|
||||||
|
## Repository layout
|
||||||
|
|
||||||
|
- `scripts/migrate_copy_checkpoint.sql`
|
||||||
|
Copy data source → target (restartable, per-table commit, FK-aware order, live progress output)
|
||||||
|
|
||||||
|
- `scripts/compare_source_target.sql`
|
||||||
|
Compare source vs target (**textual summaries + result sets**)
|
||||||
|
- Rowcounts per table
|
||||||
|
- Column schema diffs
|
||||||
|
- Views/Procs/Functions hash compare
|
||||||
|
- Trigger hash compare
|
||||||
|
- Users/Roles + Role memberships
|
||||||
|
|
||||||
|
- `docs/USAGE.md`
|
||||||
|
How to run the migration copy safely.
|
||||||
|
|
||||||
|
- `docs/VERIFY.md`
|
||||||
|
How to run and interpret the compare results.
|
||||||
|
|
||||||
|
## Quickstart
|
||||||
|
|
||||||
|
1. Open **SQL Server Management Studio** (SSMS)
|
||||||
|
2. Ensure target DB exists and schema is deployed
|
||||||
|
3. Run:
|
||||||
|
- `scripts/migrate_copy_checkpoint.sql`
|
||||||
|
4. After successful copy, run:
|
||||||
|
- `scripts/compare_source_target.sql`
|
||||||
|
|
||||||
|
Both scripts show **live progress output** in SSMS (Messages tab).
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
At the top of each script set:
|
||||||
|
|
||||||
|
- `@SourceDb` (default `sysdb_UTF8`)
|
||||||
|
- `@TargetDb` (default `sysdb_utf8_jr`)
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The copy script disables triggers, constraints, and nonclustered indexes in target during load and re-enables them afterwards.
|
||||||
|
- The compare script uses hashes (SHA2_256) for object definitions to quickly detect differences.
|
||||||
61
docs/USAGE.md
Normal file
61
docs/USAGE.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# Usage – Data Copy (Variant B / Checkpointing)
|
||||||
|
|
||||||
|
## Preconditions
|
||||||
|
|
||||||
|
1. **Target database exists** (e.g. UTF-8 collation)
|
||||||
|
2. **Target schema is deployed** (tables, constraints, indexes, views, procs, triggers, users/roles etc.)
|
||||||
|
3. You can connect to the SQL Server instance with sufficient permissions.
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
Open `scripts/migrate_copy_checkpoint.sql` in SSMS.
|
||||||
|
|
||||||
|
At the top configure:
|
||||||
|
|
||||||
|
- `@SourceDb` (default: `sysdb_UTF8`)
|
||||||
|
- `@TargetDb` (default: `sysdb_utf8_jr`)
|
||||||
|
|
||||||
|
Optional behavior:
|
||||||
|
|
||||||
|
- `@Mode`
|
||||||
|
- `RESUME` (default): skip tables already marked `DONE`
|
||||||
|
- `RESET`: reset state and rerun all tables
|
||||||
|
|
||||||
|
- `@StopOnError`
|
||||||
|
- `1` (default): stop on first table error
|
||||||
|
- `0`: continue with next tables; check `dbo.MigrationState` afterwards
|
||||||
|
|
||||||
|
- `@BatchSize`
|
||||||
|
- used for tables with `IDENTITY (int/bigint)` to copy in chunks
|
||||||
|
|
||||||
|
Execute the script.
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
The script prints progress like:
|
||||||
|
|
||||||
|
- `... | TABLE_START | dbo.TableX`
|
||||||
|
- `... | TABLE_DONE | dbo.TableX | Old=... New=...`
|
||||||
|
- `... | TABLE_FAILED | dbo.TableX | Error ...`
|
||||||
|
|
||||||
|
In target DB you also have:
|
||||||
|
|
||||||
|
- `dbo.MigrationState` (table-level status)
|
||||||
|
- `dbo.MigrationCopyLog` (append-only log)
|
||||||
|
|
||||||
|
Useful queries:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
USE sysdb_utf8_jr;
|
||||||
|
SELECT * FROM dbo.MigrationState ORDER BY Status, TableSchema, TableName;
|
||||||
|
SELECT TOP 200 * FROM dbo.MigrationCopyLog ORDER BY LogId DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rerun behavior
|
||||||
|
|
||||||
|
Each table is copied as:
|
||||||
|
|
||||||
|
1. `DELETE FROM target.table`
|
||||||
|
2. `INSERT INTO target.table SELECT ... FROM source.table`
|
||||||
|
|
||||||
|
So reruns are safe. The checkpoint state enables resuming.
|
||||||
49
docs/VERIFY.md
Normal file
49
docs/VERIFY.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Verify – Compare Source and Target
|
||||||
|
|
||||||
|
The script `scripts/compare_source_target.sql` compares **structure and high-level consistency** between two databases without comparing row contents.
|
||||||
|
|
||||||
|
## What is compared
|
||||||
|
|
||||||
|
1. **Rowcounts per table**
|
||||||
|
2. **Columns**: datatype, length, nullable, collation
|
||||||
|
3. **Programmable objects**: views, stored procedures, functions (hash of definition)
|
||||||
|
4. **Triggers** (hash of definition)
|
||||||
|
5. **Users/Roles**: principals existence + role memberships
|
||||||
|
|
||||||
|
## How to run
|
||||||
|
|
||||||
|
1. Open `scripts/compare_source_target.sql` in SSMS
|
||||||
|
2. Set:
|
||||||
|
- `@SourceDb` (default: `sysdb_UTF8`)
|
||||||
|
- `@TargetDb` (default: `sysdb_utf8_jr`)
|
||||||
|
3. Execute.
|
||||||
|
|
||||||
|
## Output style (quick to read)
|
||||||
|
|
||||||
|
### Live messages (SSMS Messages tab)
|
||||||
|
|
||||||
|
You get a human-readable summary per step, e.g.:
|
||||||
|
|
||||||
|
- `ROWCOUNTS | SourceTables=... TargetTables=... TablesWithRowDiff=...`
|
||||||
|
- `COLUMNS | ColumnDiffs=...`
|
||||||
|
- `MODULES | Differences=...`
|
||||||
|
- `TRIGGERS | Differences=...`
|
||||||
|
- `PRINCIPALS | Differences=...`
|
||||||
|
- `ROLEMEMBERS | Differences=...`
|
||||||
|
|
||||||
|
At the end:
|
||||||
|
|
||||||
|
`COMPARE_DONE | RowDiffTables=... | ColumnDiffs=... | ...`
|
||||||
|
|
||||||
|
### Result sets (details)
|
||||||
|
|
||||||
|
If differences exist, result sets are shown for:
|
||||||
|
|
||||||
|
- Rowcount diffs
|
||||||
|
- Column diffs
|
||||||
|
- Module diffs
|
||||||
|
- Trigger diffs
|
||||||
|
- Principal diffs
|
||||||
|
- Role membership diffs
|
||||||
|
|
||||||
|
If a section has **no differences**, you’ll see an `..._OK` message and no detail list.
|
||||||
485
scripts/compare_source_target.sql
Normal file
485
scripts/compare_source_target.sql
Normal 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;
|
||||||
679
scripts/migrate_copy_checkpoint.sql
Normal file
679
scripts/migrate_copy_checkpoint.sql
Normal 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
|
||||||
Reference in New Issue
Block a user