feat(migration): add Migrate-To-V2.ps1 idempotent migration runner

This commit is contained in:
Joseph Doherty
2026-05-26 04:26:01 -04:00
parent 605dbf3dcc
commit c168c1c9c6
4 changed files with 3348 additions and 0 deletions

View 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

File diff suppressed because it is too large Load Diff

View 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

View File

@@ -11,6 +11,10 @@
<PackageReference Include="Akka.Hosting"/>
<PackageReference Include="Serilog.AspNetCore"/>
<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>