feat(migration): add Migrate-To-V2.ps1 idempotent migration runner
This commit is contained in:
59
scripts/migration/Migrate-To-V2.ps1
Normal file
59
scripts/migration/Migrate-To-V2.ps1
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Idempotent migration runner that takes the OtOpcUaConfig database from the v1 schema
|
||||||
|
(with ConfigGeneration / ClusterNodeGenerationState) to the v2 hosting-aligned schema
|
||||||
|
(with Deployment / NodeDeploymentState / ConfigEdit / DataProtectionKeys).
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Backs the database up, applies the idempotent EF migration script, then validates that
|
||||||
|
expected tables exist and legacy tables are gone. Safe to re-run — the EF script itself
|
||||||
|
is idempotent, and the backup picks a unique filename per invocation.
|
||||||
|
|
||||||
|
.PARAMETER ConnectionString
|
||||||
|
Mandatory. Full ADO.NET connection string with permissions to BACKUP DATABASE and
|
||||||
|
apply DDL on the target ConfigDb.
|
||||||
|
|
||||||
|
.PARAMETER BackupPath
|
||||||
|
Optional. Full path for the backup file. Defaults to a timestamped path under $env:TEMP.
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\Migrate-To-V2.ps1 -ConnectionString "Server=sql01;Database=OtOpcUaConfig;Trusted_Connection=True;TrustServerCertificate=True"
|
||||||
|
#>
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)][string] $ConnectionString,
|
||||||
|
[string] $BackupPath = "$env:TEMP\OtOpcUa-V1-Backup-$(Get-Date -Format yyyyMMddHHmmss).bak"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
if (-not (Get-Command Invoke-Sqlcmd -ErrorAction SilentlyContinue)) {
|
||||||
|
throw "Invoke-Sqlcmd not available. Install module: Install-Module SqlServer -Scope CurrentUser"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Step 1/4 — Backup ConfigDb to $BackupPath" -ForegroundColor Cyan
|
||||||
|
Invoke-Sqlcmd -ConnectionString $ConnectionString `
|
||||||
|
-Query "BACKUP DATABASE [OtOpcUaConfig] TO DISK = '$BackupPath' WITH FORMAT, COMPRESSION"
|
||||||
|
|
||||||
|
Write-Host "Step 2/4 — Row counts (before)" -ForegroundColor Cyan
|
||||||
|
$beforeCounts = Invoke-Sqlcmd -ConnectionString $ConnectionString -InputFile "$PSScriptRoot\count-rows.sql"
|
||||||
|
$beforeCounts | Format-Table
|
||||||
|
|
||||||
|
Write-Host "Step 3/4 — Apply Migrate-To-V2.sql" -ForegroundColor Cyan
|
||||||
|
Invoke-Sqlcmd -ConnectionString $ConnectionString -InputFile "$PSScriptRoot\Migrate-To-V2.sql" -QueryTimeout 1800
|
||||||
|
|
||||||
|
Write-Host "Step 4/4 — Row counts (after) + validation" -ForegroundColor Cyan
|
||||||
|
$afterCounts = Invoke-Sqlcmd -ConnectionString $ConnectionString -InputFile "$PSScriptRoot\count-rows.sql"
|
||||||
|
$afterCounts | Format-Table
|
||||||
|
|
||||||
|
$tablesNow = (Invoke-Sqlcmd -ConnectionString $ConnectionString `
|
||||||
|
-Query "SELECT name FROM sys.tables ORDER BY name").name
|
||||||
|
|
||||||
|
foreach ($t in 'Deployment','NodeDeploymentState','ConfigEdit','DataProtectionKeys') {
|
||||||
|
if ($tablesNow -notcontains $t) { throw "Expected v2 table $t missing." }
|
||||||
|
}
|
||||||
|
foreach ($t in 'ConfigGeneration','ClusterNodeGenerationState') {
|
||||||
|
if ($tablesNow -contains $t) { throw "Legacy v1 table $t still present." }
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Migration complete. Backup at $BackupPath" -ForegroundColor Green
|
||||||
3259
scripts/migration/Migrate-To-V2.sql
Normal file
3259
scripts/migration/Migrate-To-V2.sql
Normal file
File diff suppressed because it is too large
Load Diff
26
scripts/migration/count-rows.sql
Normal file
26
scripts/migration/count-rows.sql
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
-- Per-table row counts for pre/post-migration audit.
|
||||||
|
-- Covers every table relevant to the v1 -> v2 transition so the operator can confirm
|
||||||
|
-- live-edit data was preserved and v2 tables came up empty.
|
||||||
|
|
||||||
|
SELECT TableName = t.name, [Rows] = SUM(p.[rows])
|
||||||
|
FROM sys.tables t
|
||||||
|
JOIN sys.partitions p ON p.object_id = t.object_id AND p.index_id IN (0,1)
|
||||||
|
WHERE t.name IN (
|
||||||
|
-- Live-edit configuration (rows must survive)
|
||||||
|
'ServerCluster','ClusterNode','ClusterNodeCredential',
|
||||||
|
'Namespace','UnsArea','UnsLine',
|
||||||
|
'DriverInstance','Device','Equipment','Tag','PollGroup','VirtualTag',
|
||||||
|
'NodeAcl','ExternalIdReservation',
|
||||||
|
'Script','ScriptedAlarm','ScriptedAlarmState',
|
||||||
|
'LdapGroupRoleMapping',
|
||||||
|
'EquipmentImportBatch','EquipmentImportRow',
|
||||||
|
-- Status tables (rebuilt at runtime; counts informational)
|
||||||
|
'DriverHostStatus','DriverInstanceResilienceStatus',
|
||||||
|
-- Audit (preserved)
|
||||||
|
'ConfigAuditLog',
|
||||||
|
-- v2 deploy model (empty pre-migration, populated post)
|
||||||
|
'Deployment','NodeDeploymentState','ConfigEdit','DataProtectionKeys'
|
||||||
|
)
|
||||||
|
GROUP BY t.name
|
||||||
|
ORDER BY t.name;
|
||||||
|
GO
|
||||||
@@ -11,6 +11,10 @@
|
|||||||
<PackageReference Include="Akka.Hosting"/>
|
<PackageReference Include="Akka.Hosting"/>
|
||||||
<PackageReference Include="Serilog.AspNetCore"/>
|
<PackageReference Include="Serilog.AspNetCore"/>
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Condition="$([MSBuild]::IsOSPlatform('Windows'))"/>
|
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Condition="$([MSBuild]::IsOSPlatform('Windows'))"/>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
Reference in New Issue
Block a user