From c168c1c9c6b90b97c19c0d5a7eadcd1568ab9c9e Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 26 May 2026 04:26:01 -0400 Subject: [PATCH] feat(migration): add Migrate-To-V2.ps1 idempotent migration runner --- scripts/migration/Migrate-To-V2.ps1 | 59 + scripts/migration/Migrate-To-V2.sql | 3259 +++++++++++++++++ scripts/migration/count-rows.sql | 26 + .../ZB.MOM.WW.OtOpcUa.Host.csproj | 4 + 4 files changed, 3348 insertions(+) create mode 100644 scripts/migration/Migrate-To-V2.ps1 create mode 100644 scripts/migration/Migrate-To-V2.sql create mode 100644 scripts/migration/count-rows.sql diff --git a/scripts/migration/Migrate-To-V2.ps1 b/scripts/migration/Migrate-To-V2.ps1 new file mode 100644 index 0000000..cbd512e --- /dev/null +++ b/scripts/migration/Migrate-To-V2.ps1 @@ -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 diff --git a/scripts/migration/Migrate-To-V2.sql b/scripts/migration/Migrate-To-V2.sql new file mode 100644 index 0000000..f3cc25d --- /dev/null +++ b/scripts/migration/Migrate-To-V2.sql @@ -0,0 +1,3259 @@ +IF OBJECT_ID(N'[__EFMigrationsHistory]') IS NULL +BEGIN + CREATE TABLE [__EFMigrationsHistory] ( + [MigrationId] nvarchar(150) NOT NULL, + [ProductVersion] nvarchar(32) NOT NULL, + CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId]) + ); +END; +GO + +BEGIN TRANSACTION; +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE TABLE [ConfigAuditLog] ( + [AuditId] bigint NOT NULL IDENTITY, + [Timestamp] datetime2(3) NOT NULL DEFAULT (SYSUTCDATETIME()), + [Principal] nvarchar(128) NOT NULL, + [EventType] nvarchar(64) NOT NULL, + [ClusterId] nvarchar(64) NULL, + [NodeId] nvarchar(64) NULL, + [GenerationId] bigint NULL, + [DetailsJson] nvarchar(max) NULL, + CONSTRAINT [PK_ConfigAuditLog] PRIMARY KEY ([AuditId]), + CONSTRAINT [CK_ConfigAuditLog_DetailsJson_IsJson] CHECK (DetailsJson IS NULL OR ISJSON(DetailsJson) = 1) + ); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE TABLE [ExternalIdReservation] ( + [ReservationId] uniqueidentifier NOT NULL DEFAULT (NEWSEQUENTIALID()), + [Kind] nvarchar(16) NOT NULL, + [Value] nvarchar(64) NOT NULL, + [EquipmentUuid] uniqueidentifier NOT NULL, + [ClusterId] nvarchar(64) NOT NULL, + [FirstPublishedAt] datetime2(3) NOT NULL DEFAULT (SYSUTCDATETIME()), + [FirstPublishedBy] nvarchar(128) NOT NULL, + [LastPublishedAt] datetime2(3) NOT NULL DEFAULT (SYSUTCDATETIME()), + [ReleasedAt] datetime2(3) NULL, + [ReleasedBy] nvarchar(128) NULL, + [ReleaseReason] nvarchar(512) NULL, + CONSTRAINT [PK_ExternalIdReservation] PRIMARY KEY ([ReservationId]) + ); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE TABLE [ServerCluster] ( + [ClusterId] nvarchar(64) NOT NULL, + [Name] nvarchar(128) NOT NULL, + [Enterprise] nvarchar(32) NOT NULL, + [Site] nvarchar(32) NOT NULL, + [NodeCount] tinyint NOT NULL, + [RedundancyMode] nvarchar(16) NOT NULL, + [Enabled] bit NOT NULL, + [Notes] nvarchar(1024) NULL, + [CreatedAt] datetime2(3) NOT NULL DEFAULT (SYSUTCDATETIME()), + [CreatedBy] nvarchar(128) NOT NULL, + [ModifiedAt] datetime2(3) NULL, + [ModifiedBy] nvarchar(128) NULL, + CONSTRAINT [PK_ServerCluster] PRIMARY KEY ([ClusterId]), + CONSTRAINT [CK_ServerCluster_RedundancyMode_NodeCount] CHECK (((NodeCount = 1 AND RedundancyMode = 'None') OR (NodeCount = 2 AND RedundancyMode IN ('Warm', 'Hot')))) + ); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE TABLE [ClusterNode] ( + [NodeId] nvarchar(64) NOT NULL, + [ClusterId] nvarchar(64) NOT NULL, + [RedundancyRole] nvarchar(16) NOT NULL, + [Host] nvarchar(255) NOT NULL, + [OpcUaPort] int NOT NULL, + [DashboardPort] int NOT NULL, + [ApplicationUri] nvarchar(256) NOT NULL, + [ServiceLevelBase] tinyint NOT NULL, + [DriverConfigOverridesJson] nvarchar(max) NULL, + [Enabled] bit NOT NULL, + [LastSeenAt] datetime2(3) NULL, + [CreatedAt] datetime2(3) NOT NULL DEFAULT (SYSUTCDATETIME()), + [CreatedBy] nvarchar(128) NOT NULL, + CONSTRAINT [PK_ClusterNode] PRIMARY KEY ([NodeId]), + CONSTRAINT [FK_ClusterNode_ServerCluster_ClusterId] FOREIGN KEY ([ClusterId]) REFERENCES [ServerCluster] ([ClusterId]) ON DELETE NO ACTION + ); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE TABLE [ConfigGeneration] ( + [GenerationId] bigint NOT NULL IDENTITY, + [ClusterId] nvarchar(64) NOT NULL, + [Status] nvarchar(16) NOT NULL, + [ParentGenerationId] bigint NULL, + [PublishedAt] datetime2(3) NULL, + [PublishedBy] nvarchar(128) NULL, + [Notes] nvarchar(1024) NULL, + [CreatedAt] datetime2(3) NOT NULL DEFAULT (SYSUTCDATETIME()), + [CreatedBy] nvarchar(128) NOT NULL, + CONSTRAINT [PK_ConfigGeneration] PRIMARY KEY ([GenerationId]), + CONSTRAINT [FK_ConfigGeneration_ConfigGeneration_ParentGenerationId] FOREIGN KEY ([ParentGenerationId]) REFERENCES [ConfigGeneration] ([GenerationId]) ON DELETE NO ACTION, + CONSTRAINT [FK_ConfigGeneration_ServerCluster_ClusterId] FOREIGN KEY ([ClusterId]) REFERENCES [ServerCluster] ([ClusterId]) ON DELETE NO ACTION + ); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE TABLE [ClusterNodeCredential] ( + [CredentialId] uniqueidentifier NOT NULL DEFAULT (NEWSEQUENTIALID()), + [NodeId] nvarchar(64) NOT NULL, + [Kind] nvarchar(32) NOT NULL, + [Value] nvarchar(512) NOT NULL, + [Enabled] bit NOT NULL, + [RotatedAt] datetime2(3) NULL, + [CreatedAt] datetime2(3) NOT NULL DEFAULT (SYSUTCDATETIME()), + [CreatedBy] nvarchar(128) NOT NULL, + CONSTRAINT [PK_ClusterNodeCredential] PRIMARY KEY ([CredentialId]), + CONSTRAINT [FK_ClusterNodeCredential_ClusterNode_NodeId] FOREIGN KEY ([NodeId]) REFERENCES [ClusterNode] ([NodeId]) ON DELETE NO ACTION + ); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE TABLE [ClusterNodeGenerationState] ( + [NodeId] nvarchar(64) NOT NULL, + [CurrentGenerationId] bigint NULL, + [LastAppliedAt] datetime2(3) NULL, + [LastAppliedStatus] nvarchar(16) NULL, + [LastAppliedError] nvarchar(2048) NULL, + [LastSeenAt] datetime2(3) NULL, + CONSTRAINT [PK_ClusterNodeGenerationState] PRIMARY KEY ([NodeId]), + CONSTRAINT [FK_ClusterNodeGenerationState_ClusterNode_NodeId] FOREIGN KEY ([NodeId]) REFERENCES [ClusterNode] ([NodeId]) ON DELETE NO ACTION, + CONSTRAINT [FK_ClusterNodeGenerationState_ConfigGeneration_CurrentGenerationId] FOREIGN KEY ([CurrentGenerationId]) REFERENCES [ConfigGeneration] ([GenerationId]) ON DELETE NO ACTION + ); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE TABLE [Device] ( + [DeviceRowId] uniqueidentifier NOT NULL DEFAULT (NEWSEQUENTIALID()), + [GenerationId] bigint NOT NULL, + [DeviceId] nvarchar(64) NULL, + [DriverInstanceId] nvarchar(64) NOT NULL, + [Name] nvarchar(128) NOT NULL, + [Enabled] bit NOT NULL, + [DeviceConfig] nvarchar(max) NOT NULL, + CONSTRAINT [PK_Device] PRIMARY KEY ([DeviceRowId]), + CONSTRAINT [CK_Device_DeviceConfig_IsJson] CHECK (ISJSON(DeviceConfig) = 1), + CONSTRAINT [FK_Device_ConfigGeneration_GenerationId] FOREIGN KEY ([GenerationId]) REFERENCES [ConfigGeneration] ([GenerationId]) ON DELETE NO ACTION + ); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE TABLE [DriverInstance] ( + [DriverInstanceRowId] uniqueidentifier NOT NULL DEFAULT (NEWSEQUENTIALID()), + [GenerationId] bigint NOT NULL, + [DriverInstanceId] nvarchar(64) NULL, + [ClusterId] nvarchar(64) NOT NULL, + [NamespaceId] nvarchar(64) NOT NULL, + [Name] nvarchar(128) NOT NULL, + [DriverType] nvarchar(32) NOT NULL, + [Enabled] bit NOT NULL, + [DriverConfig] nvarchar(max) NOT NULL, + CONSTRAINT [PK_DriverInstance] PRIMARY KEY ([DriverInstanceRowId]), + CONSTRAINT [CK_DriverInstance_DriverConfig_IsJson] CHECK (ISJSON(DriverConfig) = 1), + CONSTRAINT [FK_DriverInstance_ConfigGeneration_GenerationId] FOREIGN KEY ([GenerationId]) REFERENCES [ConfigGeneration] ([GenerationId]) ON DELETE NO ACTION, + CONSTRAINT [FK_DriverInstance_ServerCluster_ClusterId] FOREIGN KEY ([ClusterId]) REFERENCES [ServerCluster] ([ClusterId]) ON DELETE NO ACTION + ); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE TABLE [Equipment] ( + [EquipmentRowId] uniqueidentifier NOT NULL DEFAULT (NEWSEQUENTIALID()), + [GenerationId] bigint NOT NULL, + [EquipmentId] nvarchar(64) NULL, + [EquipmentUuid] uniqueidentifier NOT NULL, + [DriverInstanceId] nvarchar(64) NOT NULL, + [DeviceId] nvarchar(64) NULL, + [UnsLineId] nvarchar(64) NOT NULL, + [Name] nvarchar(32) NOT NULL, + [MachineCode] nvarchar(64) NOT NULL, + [ZTag] nvarchar(64) NULL, + [SAPID] nvarchar(64) NULL, + [Manufacturer] nvarchar(64) NULL, + [Model] nvarchar(64) NULL, + [SerialNumber] nvarchar(64) NULL, + [HardwareRevision] nvarchar(32) NULL, + [SoftwareRevision] nvarchar(32) NULL, + [YearOfConstruction] smallint NULL, + [AssetLocation] nvarchar(256) NULL, + [ManufacturerUri] nvarchar(512) NULL, + [DeviceManualUri] nvarchar(512) NULL, + [EquipmentClassRef] nvarchar(128) NULL, + [Enabled] bit NOT NULL, + CONSTRAINT [PK_Equipment] PRIMARY KEY ([EquipmentRowId]), + CONSTRAINT [FK_Equipment_ConfigGeneration_GenerationId] FOREIGN KEY ([GenerationId]) REFERENCES [ConfigGeneration] ([GenerationId]) ON DELETE NO ACTION + ); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE TABLE [Namespace] ( + [NamespaceRowId] uniqueidentifier NOT NULL DEFAULT (NEWSEQUENTIALID()), + [GenerationId] bigint NOT NULL, + [NamespaceId] nvarchar(64) NULL, + [ClusterId] nvarchar(64) NOT NULL, + [Kind] nvarchar(32) NOT NULL, + [NamespaceUri] nvarchar(256) NOT NULL, + [Enabled] bit NOT NULL, + [Notes] nvarchar(1024) NULL, + CONSTRAINT [PK_Namespace] PRIMARY KEY ([NamespaceRowId]), + CONSTRAINT [FK_Namespace_ConfigGeneration_GenerationId] FOREIGN KEY ([GenerationId]) REFERENCES [ConfigGeneration] ([GenerationId]) ON DELETE NO ACTION, + CONSTRAINT [FK_Namespace_ServerCluster_ClusterId] FOREIGN KEY ([ClusterId]) REFERENCES [ServerCluster] ([ClusterId]) ON DELETE NO ACTION + ); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE TABLE [NodeAcl] ( + [NodeAclRowId] uniqueidentifier NOT NULL DEFAULT (NEWSEQUENTIALID()), + [GenerationId] bigint NOT NULL, + [NodeAclId] nvarchar(64) NULL, + [ClusterId] nvarchar(64) NOT NULL, + [LdapGroup] nvarchar(256) NOT NULL, + [ScopeKind] nvarchar(16) NOT NULL, + [ScopeId] nvarchar(64) NULL, + [PermissionFlags] int NOT NULL, + [Notes] nvarchar(512) NULL, + CONSTRAINT [PK_NodeAcl] PRIMARY KEY ([NodeAclRowId]), + CONSTRAINT [FK_NodeAcl_ConfigGeneration_GenerationId] FOREIGN KEY ([GenerationId]) REFERENCES [ConfigGeneration] ([GenerationId]) ON DELETE NO ACTION + ); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE TABLE [PollGroup] ( + [PollGroupRowId] uniqueidentifier NOT NULL DEFAULT (NEWSEQUENTIALID()), + [GenerationId] bigint NOT NULL, + [PollGroupId] nvarchar(64) NULL, + [DriverInstanceId] nvarchar(64) NOT NULL, + [Name] nvarchar(128) NOT NULL, + [IntervalMs] int NOT NULL, + CONSTRAINT [PK_PollGroup] PRIMARY KEY ([PollGroupRowId]), + CONSTRAINT [CK_PollGroup_IntervalMs_Min] CHECK (IntervalMs >= 50), + CONSTRAINT [FK_PollGroup_ConfigGeneration_GenerationId] FOREIGN KEY ([GenerationId]) REFERENCES [ConfigGeneration] ([GenerationId]) ON DELETE NO ACTION + ); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE TABLE [Tag] ( + [TagRowId] uniqueidentifier NOT NULL DEFAULT (NEWSEQUENTIALID()), + [GenerationId] bigint NOT NULL, + [TagId] nvarchar(64) NULL, + [DriverInstanceId] nvarchar(64) NOT NULL, + [DeviceId] nvarchar(64) NULL, + [EquipmentId] nvarchar(64) NULL, + [Name] nvarchar(128) NOT NULL, + [FolderPath] nvarchar(512) NULL, + [DataType] nvarchar(32) NOT NULL, + [AccessLevel] nvarchar(16) NOT NULL, + [WriteIdempotent] bit NOT NULL, + [PollGroupId] nvarchar(64) NULL, + [TagConfig] nvarchar(max) NOT NULL, + CONSTRAINT [PK_Tag] PRIMARY KEY ([TagRowId]), + CONSTRAINT [CK_Tag_TagConfig_IsJson] CHECK (ISJSON(TagConfig) = 1), + CONSTRAINT [FK_Tag_ConfigGeneration_GenerationId] FOREIGN KEY ([GenerationId]) REFERENCES [ConfigGeneration] ([GenerationId]) ON DELETE NO ACTION + ); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE TABLE [UnsArea] ( + [UnsAreaRowId] uniqueidentifier NOT NULL DEFAULT (NEWSEQUENTIALID()), + [GenerationId] bigint NOT NULL, + [UnsAreaId] nvarchar(64) NULL, + [ClusterId] nvarchar(64) NOT NULL, + [Name] nvarchar(32) NOT NULL, + [Notes] nvarchar(512) NULL, + CONSTRAINT [PK_UnsArea] PRIMARY KEY ([UnsAreaRowId]), + CONSTRAINT [FK_UnsArea_ConfigGeneration_GenerationId] FOREIGN KEY ([GenerationId]) REFERENCES [ConfigGeneration] ([GenerationId]) ON DELETE NO ACTION, + CONSTRAINT [FK_UnsArea_ServerCluster_ClusterId] FOREIGN KEY ([ClusterId]) REFERENCES [ServerCluster] ([ClusterId]) ON DELETE NO ACTION + ); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE TABLE [UnsLine] ( + [UnsLineRowId] uniqueidentifier NOT NULL DEFAULT (NEWSEQUENTIALID()), + [GenerationId] bigint NOT NULL, + [UnsLineId] nvarchar(64) NULL, + [UnsAreaId] nvarchar(64) NOT NULL, + [Name] nvarchar(32) NOT NULL, + [Notes] nvarchar(512) NULL, + CONSTRAINT [PK_UnsLine] PRIMARY KEY ([UnsLineRowId]), + CONSTRAINT [FK_UnsLine_ConfigGeneration_GenerationId] FOREIGN KEY ([GenerationId]) REFERENCES [ConfigGeneration] ([GenerationId]) ON DELETE NO ACTION + ); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE UNIQUE INDEX [UX_ClusterNode_ApplicationUri] ON [ClusterNode] ([ApplicationUri]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [UX_ClusterNode_Primary_Per_Cluster] ON [ClusterNode] ([ClusterId]) WHERE [RedundancyRole] = ''Primary'''); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE INDEX [IX_ClusterNodeCredential_NodeId] ON [ClusterNodeCredential] ([NodeId], [Enabled]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [UX_ClusterNodeCredential_Value] ON [ClusterNodeCredential] ([Kind], [Value]) WHERE [Enabled] = 1'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE INDEX [IX_ClusterNodeGenerationState_Generation] ON [ClusterNodeGenerationState] ([CurrentGenerationId]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE INDEX [IX_ConfigAuditLog_Cluster_Time] ON [ConfigAuditLog] ([ClusterId], [Timestamp] DESC); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + EXEC(N'CREATE INDEX [IX_ConfigAuditLog_Generation] ON [ConfigAuditLog] ([GenerationId]) WHERE [GenerationId] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE INDEX [IX_ConfigGeneration_Cluster_Published] ON [ConfigGeneration] ([ClusterId], [Status], [GenerationId] DESC) INCLUDE ([PublishedAt]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE INDEX [IX_ConfigGeneration_ParentGenerationId] ON [ConfigGeneration] ([ParentGenerationId]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [UX_ConfigGeneration_Draft_Per_Cluster] ON [ConfigGeneration] ([ClusterId]) WHERE [Status] = ''Draft'''); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE INDEX [IX_Device_Generation_Driver] ON [Device] ([GenerationId], [DriverInstanceId]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [UX_Device_Generation_LogicalId] ON [Device] ([GenerationId], [DeviceId]) WHERE [DeviceId] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE INDEX [IX_DriverInstance_ClusterId] ON [DriverInstance] ([ClusterId]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE INDEX [IX_DriverInstance_Generation_Cluster] ON [DriverInstance] ([GenerationId], [ClusterId]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE INDEX [IX_DriverInstance_Generation_Namespace] ON [DriverInstance] ([GenerationId], [NamespaceId]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [UX_DriverInstance_Generation_LogicalId] ON [DriverInstance] ([GenerationId], [DriverInstanceId]) WHERE [DriverInstanceId] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE INDEX [IX_Equipment_Generation_Driver] ON [Equipment] ([GenerationId], [DriverInstanceId]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE INDEX [IX_Equipment_Generation_Line] ON [Equipment] ([GenerationId], [UnsLineId]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE INDEX [IX_Equipment_Generation_MachineCode] ON [Equipment] ([GenerationId], [MachineCode]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + EXEC(N'CREATE INDEX [IX_Equipment_Generation_SAPID] ON [Equipment] ([GenerationId], [SAPID]) WHERE [SAPID] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + EXEC(N'CREATE INDEX [IX_Equipment_Generation_ZTag] ON [Equipment] ([GenerationId], [ZTag]) WHERE [ZTag] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE UNIQUE INDEX [UX_Equipment_Generation_LinePath] ON [Equipment] ([GenerationId], [UnsLineId], [Name]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [UX_Equipment_Generation_LogicalId] ON [Equipment] ([GenerationId], [EquipmentId]) WHERE [EquipmentId] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE UNIQUE INDEX [UX_Equipment_Generation_Uuid] ON [Equipment] ([GenerationId], [EquipmentUuid]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE INDEX [IX_ExternalIdReservation_Equipment] ON [ExternalIdReservation] ([EquipmentUuid]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [UX_ExternalIdReservation_KindValue_Active] ON [ExternalIdReservation] ([Kind], [Value]) WHERE [ReleasedAt] IS NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE INDEX [IX_Namespace_ClusterId] ON [Namespace] ([ClusterId]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE INDEX [IX_Namespace_Generation_Cluster] ON [Namespace] ([GenerationId], [ClusterId]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE UNIQUE INDEX [UX_Namespace_Generation_Cluster_Kind] ON [Namespace] ([GenerationId], [ClusterId], [Kind]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [UX_Namespace_Generation_LogicalId] ON [Namespace] ([GenerationId], [NamespaceId]) WHERE [NamespaceId] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [UX_Namespace_Generation_LogicalId_Cluster] ON [Namespace] ([GenerationId], [NamespaceId], [ClusterId]) WHERE [NamespaceId] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE UNIQUE INDEX [UX_Namespace_Generation_NamespaceUri] ON [Namespace] ([GenerationId], [NamespaceUri]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE INDEX [IX_NodeAcl_Generation_Cluster] ON [NodeAcl] ([GenerationId], [ClusterId]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE INDEX [IX_NodeAcl_Generation_Group] ON [NodeAcl] ([GenerationId], [LdapGroup]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + EXEC(N'CREATE INDEX [IX_NodeAcl_Generation_Scope] ON [NodeAcl] ([GenerationId], [ScopeKind], [ScopeId]) WHERE [ScopeId] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [UX_NodeAcl_Generation_GroupScope] ON [NodeAcl] ([GenerationId], [ClusterId], [LdapGroup], [ScopeKind], [ScopeId]) WHERE [ScopeId] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [UX_NodeAcl_Generation_LogicalId] ON [NodeAcl] ([GenerationId], [NodeAclId]) WHERE [NodeAclId] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE INDEX [IX_PollGroup_Generation_Driver] ON [PollGroup] ([GenerationId], [DriverInstanceId]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [UX_PollGroup_Generation_LogicalId] ON [PollGroup] ([GenerationId], [PollGroupId]) WHERE [PollGroupId] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE INDEX [IX_ServerCluster_Site] ON [ServerCluster] ([Site]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE UNIQUE INDEX [UX_ServerCluster_Name] ON [ServerCluster] ([Name]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE INDEX [IX_Tag_Generation_Driver_Device] ON [Tag] ([GenerationId], [DriverInstanceId], [DeviceId]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + EXEC(N'CREATE INDEX [IX_Tag_Generation_Equipment] ON [Tag] ([GenerationId], [EquipmentId]) WHERE [EquipmentId] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [UX_Tag_Generation_EquipmentPath] ON [Tag] ([GenerationId], [EquipmentId], [Name]) WHERE [EquipmentId] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [UX_Tag_Generation_FolderPath] ON [Tag] ([GenerationId], [DriverInstanceId], [FolderPath], [Name]) WHERE [EquipmentId] IS NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [UX_Tag_Generation_LogicalId] ON [Tag] ([GenerationId], [TagId]) WHERE [TagId] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE INDEX [IX_UnsArea_ClusterId] ON [UnsArea] ([ClusterId]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE INDEX [IX_UnsArea_Generation_Cluster] ON [UnsArea] ([GenerationId], [ClusterId]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE UNIQUE INDEX [UX_UnsArea_Generation_ClusterName] ON [UnsArea] ([GenerationId], [ClusterId], [Name]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [UX_UnsArea_Generation_LogicalId] ON [UnsArea] ([GenerationId], [UnsAreaId]) WHERE [UnsAreaId] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE INDEX [IX_UnsLine_Generation_Area] ON [UnsLine] ([GenerationId], [UnsAreaId]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + CREATE UNIQUE INDEX [UX_UnsLine_Generation_AreaName] ON [UnsLine] ([GenerationId], [UnsAreaId], [Name]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [UX_UnsLine_Generation_LogicalId] ON [UnsLine] ([GenerationId], [UnsLineId]) WHERE [UnsLineId] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417212220_InitialSchema' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260417212220_InitialSchema', N'10.0.7'); +END; + +COMMIT; +GO + +BEGIN TRANSACTION; +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417215224_StoredProcedures' +) +BEGIN + + CREATE OR ALTER PROCEDURE dbo.sp_GetCurrentGenerationForCluster + @NodeId nvarchar(64), + @ClusterId nvarchar(64) + AS + BEGIN + SET NOCOUNT ON; + + DECLARE @Caller nvarchar(128) = SUSER_SNAME(); + + IF NOT EXISTS ( + SELECT 1 FROM dbo.ClusterNodeCredential + WHERE NodeId = @NodeId AND Value = @Caller AND Enabled = 1) + BEGIN + RAISERROR('Unauthorized: caller %s is not bound to NodeId %s', 16, 1, @Caller, @NodeId); + RETURN; + END + + IF NOT EXISTS ( + SELECT 1 FROM dbo.ClusterNode + WHERE NodeId = @NodeId AND ClusterId = @ClusterId AND Enabled = 1) + BEGIN + RAISERROR('Forbidden: NodeId %s does not belong to ClusterId %s', 16, 1, @NodeId, @ClusterId); + RETURN; + END + + SELECT TOP 1 GenerationId, ClusterId, Status, PublishedAt, PublishedBy, Notes + FROM dbo.ConfigGeneration + WHERE ClusterId = @ClusterId AND Status = 'Published' + ORDER BY GenerationId DESC; + END + +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417215224_StoredProcedures' +) +BEGIN + + CREATE OR ALTER PROCEDURE dbo.sp_GetGenerationContent + @NodeId nvarchar(64), + @GenerationId bigint + AS + BEGIN + SET NOCOUNT ON; + + DECLARE @Caller nvarchar(128) = SUSER_SNAME(); + DECLARE @ClusterId nvarchar(64); + + SELECT @ClusterId = ClusterId FROM dbo.ConfigGeneration WHERE GenerationId = @GenerationId; + + IF @ClusterId IS NULL + BEGIN + RAISERROR('GenerationId %I64d not found', 16, 1, @GenerationId); + RETURN; + END + + IF NOT EXISTS ( + SELECT 1 + FROM dbo.ClusterNodeCredential c + JOIN dbo.ClusterNode n ON n.NodeId = c.NodeId + WHERE c.NodeId = @NodeId AND c.Value = @Caller AND c.Enabled = 1 + AND n.ClusterId = @ClusterId AND n.Enabled = 1) + BEGIN + RAISERROR('Forbidden: caller %s not bound to a node in ClusterId %s', 16, 1, @Caller, @ClusterId); + RETURN; + END + + SELECT * FROM dbo.Namespace WHERE GenerationId = @GenerationId; + SELECT * FROM dbo.UnsArea WHERE GenerationId = @GenerationId; + SELECT * FROM dbo.UnsLine WHERE GenerationId = @GenerationId; + SELECT * FROM dbo.DriverInstance WHERE GenerationId = @GenerationId; + SELECT * FROM dbo.Device WHERE GenerationId = @GenerationId; + SELECT * FROM dbo.Equipment WHERE GenerationId = @GenerationId; + SELECT * FROM dbo.PollGroup WHERE GenerationId = @GenerationId; + SELECT * FROM dbo.Tag WHERE GenerationId = @GenerationId; + SELECT * FROM dbo.NodeAcl WHERE GenerationId = @GenerationId; + END + +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417215224_StoredProcedures' +) +BEGIN + + CREATE OR ALTER PROCEDURE dbo.sp_RegisterNodeGenerationApplied + @NodeId nvarchar(64), + @GenerationId bigint, + @Status nvarchar(16), + @Error nvarchar(max) = NULL + AS + BEGIN + SET NOCOUNT ON; + + DECLARE @Caller nvarchar(128) = SUSER_SNAME(); + + IF NOT EXISTS ( + SELECT 1 FROM dbo.ClusterNodeCredential + WHERE NodeId = @NodeId AND Value = @Caller AND Enabled = 1) + BEGIN + RAISERROR('Unauthorized: caller %s is not bound to NodeId %s', 16, 1, @Caller, @NodeId); + RETURN; + END + + MERGE dbo.ClusterNodeGenerationState AS tgt + USING (SELECT @NodeId AS NodeId) AS src ON tgt.NodeId = src.NodeId + WHEN MATCHED THEN UPDATE SET + CurrentGenerationId = @GenerationId, + LastAppliedAt = SYSUTCDATETIME(), + LastAppliedStatus = @Status, + LastAppliedError = @Error, + LastSeenAt = SYSUTCDATETIME() + WHEN NOT MATCHED THEN INSERT + (NodeId, CurrentGenerationId, LastAppliedAt, LastAppliedStatus, LastAppliedError, LastSeenAt) + VALUES (@NodeId, @GenerationId, SYSUTCDATETIME(), @Status, @Error, SYSUTCDATETIME()); + + -- Build DetailsJson via STRING_ESCAPE so a @Status containing a double-quote/backslash cannot + -- produce malformed JSON (which would fail CK_ConfigAuditLog_DetailsJson_IsJson and abort the + -- transaction) or inject extra JSON structure into the audit record. + INSERT dbo.ConfigAuditLog (Principal, EventType, NodeId, GenerationId, DetailsJson) + VALUES (@Caller, 'NodeApplied', @NodeId, @GenerationId, + CONCAT('{"status":"', STRING_ESCAPE(@Status, 'json'), '"}')); + END + +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417215224_StoredProcedures' +) +BEGIN + + CREATE OR ALTER PROCEDURE dbo.sp_ValidateDraft + @DraftGenerationId bigint + AS + BEGIN + SET NOCOUNT ON; + + DECLARE @ClusterId nvarchar(64); + DECLARE @Status nvarchar(16); + + SELECT @ClusterId = ClusterId, @Status = Status + FROM dbo.ConfigGeneration WHERE GenerationId = @DraftGenerationId; + + IF @ClusterId IS NULL + BEGIN + RAISERROR('GenerationId %I64d not found', 16, 1, @DraftGenerationId); + RETURN; + END + + IF @Status <> 'Draft' + BEGIN + RAISERROR('GenerationId %I64d is not in Draft status (current=%s)', 16, 1, @DraftGenerationId, @Status); + RETURN; + END + + IF EXISTS ( + SELECT 1 FROM dbo.Tag t + LEFT JOIN dbo.DriverInstance d ON d.GenerationId = t.GenerationId AND d.DriverInstanceId = t.DriverInstanceId + WHERE t.GenerationId = @DraftGenerationId AND d.DriverInstanceId IS NULL) + BEGIN + RAISERROR('Draft has tags with unresolved DriverInstanceId', 16, 1); + RETURN; + END + + IF EXISTS ( + SELECT 1 FROM dbo.Tag t + LEFT JOIN dbo.Device dv ON dv.GenerationId = t.GenerationId AND dv.DeviceId = t.DeviceId + WHERE t.GenerationId = @DraftGenerationId AND t.DeviceId IS NOT NULL AND dv.DeviceId IS NULL) + BEGIN + RAISERROR('Draft has tags with unresolved DeviceId', 16, 1); + RETURN; + END + + IF EXISTS ( + SELECT 1 FROM dbo.Tag t + LEFT JOIN dbo.PollGroup pg ON pg.GenerationId = t.GenerationId AND pg.PollGroupId = t.PollGroupId + WHERE t.GenerationId = @DraftGenerationId AND t.PollGroupId IS NOT NULL AND pg.PollGroupId IS NULL) + BEGIN + RAISERROR('Draft has tags with unresolved PollGroupId', 16, 1); + RETURN; + END + + IF EXISTS ( + SELECT 1 + FROM dbo.DriverInstance di + JOIN dbo.Namespace ns ON ns.GenerationId = di.GenerationId AND ns.NamespaceId = di.NamespaceId + WHERE di.GenerationId = @DraftGenerationId + AND ns.ClusterId <> di.ClusterId) + BEGIN + INSERT dbo.ConfigAuditLog (Principal, EventType, ClusterId, GenerationId) + VALUES (SUSER_SNAME(), 'CrossClusterNamespaceAttempt', @ClusterId, @DraftGenerationId); + RAISERROR('BadCrossClusterNamespaceBinding: namespace and driver must belong to the same cluster', 16, 1); + RETURN; + END + + IF EXISTS ( + SELECT 1 + FROM dbo.Equipment draft + JOIN dbo.Equipment prior + ON prior.EquipmentId = draft.EquipmentId + AND prior.EquipmentUuid <> draft.EquipmentUuid + AND prior.GenerationId <> draft.GenerationId + JOIN dbo.ConfigGeneration pg ON pg.GenerationId = prior.GenerationId + WHERE draft.GenerationId = @DraftGenerationId + AND pg.ClusterId = @ClusterId) + BEGIN + RAISERROR('EquipmentUuid immutability violated for an EquipmentId that existed in a prior generation', 16, 1); + RETURN; + END + + IF EXISTS ( + SELECT 1 + FROM dbo.Equipment draft + JOIN dbo.ExternalIdReservation r + ON r.Kind = 'ZTag' AND r.Value = draft.ZTag AND r.ReleasedAt IS NULL + AND r.EquipmentUuid <> draft.EquipmentUuid + WHERE draft.GenerationId = @DraftGenerationId AND draft.ZTag IS NOT NULL) + BEGIN + RAISERROR('BadDuplicateExternalIdentifier: a ZTag in the draft is reserved by a different EquipmentUuid', 16, 1); + RETURN; + END + + IF EXISTS ( + SELECT 1 + FROM dbo.Equipment draft + JOIN dbo.ExternalIdReservation r + ON r.Kind = 'SAPID' AND r.Value = draft.SAPID AND r.ReleasedAt IS NULL + AND r.EquipmentUuid <> draft.EquipmentUuid + WHERE draft.GenerationId = @DraftGenerationId AND draft.SAPID IS NOT NULL) + BEGIN + RAISERROR('BadDuplicateExternalIdentifier: a SAPID in the draft is reserved by a different EquipmentUuid', 16, 1); + RETURN; + END + END + +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417215224_StoredProcedures' +) +BEGIN + + CREATE OR ALTER PROCEDURE dbo.sp_PublishGeneration + @ClusterId nvarchar(64), + @DraftGenerationId bigint, + @Notes nvarchar(1024) = NULL + AS + BEGIN + SET NOCOUNT ON; + SET XACT_ABORT ON; + + -- Transaction-nesting awareness: if a caller (e.g. sp_RollbackToGeneration) already + -- holds a transaction, we use SAVE TRANSACTION so our failure path rolls back only to + -- the savepoint instead of issuing a bare ROLLBACK that wipes the caller's transaction + -- (which sets @@TRANCOUNT = 0 and causes error 3902 on the caller's subsequent COMMIT). + DECLARE @OwnsTxn bit = 0; + DECLARE @SaveName nvarchar(32) = N'sp_PublishGeneration'; + + IF @@TRANCOUNT = 0 + BEGIN + BEGIN TRANSACTION; + SET @OwnsTxn = 1; + END + ELSE + BEGIN + SAVE TRANSACTION sp_PublishGeneration; + END + + DECLARE @Lock nvarchar(255) = N'OtOpcUa_Publish_' + @ClusterId; + DECLARE @LockResult int; + EXEC @LockResult = sp_getapplock @Resource = @Lock, @LockMode = 'Exclusive', @LockTimeout = 0; + IF @LockResult < 0 + BEGIN + RAISERROR('PublishConflict: another publish is in progress for cluster %s', 16, 1, @ClusterId); + IF @OwnsTxn = 1 ROLLBACK; + ELSE ROLLBACK TRANSACTION sp_PublishGeneration; + RETURN; + END + + -- sp_ValidateDraft signals every rejection with RAISERROR(..., 16, 1) — a severity-16 error is + -- NOT batch-aborting and SET XACT_ABORT ON does not abort the transaction for it, so without a + -- TRY/CATCH control would return here and the draft would publish despite failed validation. + -- Catch the validation error, roll back the publish transaction (only to our savepoint when a + -- caller owns the outer transaction), and re-raise so the caller sees the real validation failure. + BEGIN TRY + EXEC dbo.sp_ValidateDraft @DraftGenerationId = @DraftGenerationId; + END TRY + BEGIN CATCH + IF @OwnsTxn = 1 ROLLBACK; + ELSE ROLLBACK TRANSACTION sp_PublishGeneration; + THROW; + END CATCH + + MERGE dbo.ExternalIdReservation AS tgt + USING ( + SELECT 'ZTag' AS Kind, ZTag AS Value, EquipmentUuid + FROM dbo.Equipment + WHERE GenerationId = @DraftGenerationId AND ZTag IS NOT NULL + UNION ALL + SELECT 'SAPID', SAPID, EquipmentUuid + FROM dbo.Equipment + WHERE GenerationId = @DraftGenerationId AND SAPID IS NOT NULL + ) AS src + ON tgt.Kind = src.Kind AND tgt.Value = src.Value AND tgt.EquipmentUuid = src.EquipmentUuid + WHEN MATCHED THEN UPDATE SET LastPublishedAt = SYSUTCDATETIME() + WHEN NOT MATCHED BY TARGET THEN + INSERT (Kind, Value, EquipmentUuid, ClusterId, FirstPublishedBy, LastPublishedAt) + VALUES (src.Kind, src.Value, src.EquipmentUuid, @ClusterId, SUSER_SNAME(), SYSUTCDATETIME()); + + UPDATE dbo.ConfigGeneration + SET Status = 'Superseded' + WHERE ClusterId = @ClusterId AND Status = 'Published'; + + UPDATE dbo.ConfigGeneration + SET Status = 'Published', + PublishedAt = SYSUTCDATETIME(), + PublishedBy = SUSER_SNAME(), + Notes = ISNULL(@Notes, Notes) + WHERE GenerationId = @DraftGenerationId AND ClusterId = @ClusterId AND Status = 'Draft'; + + IF @@ROWCOUNT = 0 + BEGIN + RAISERROR('Draft %I64d for cluster %s not in Draft status (was it already published?)', 16, 1, @DraftGenerationId, @ClusterId); + IF @OwnsTxn = 1 ROLLBACK; + ELSE ROLLBACK TRANSACTION sp_PublishGeneration; + RETURN; + END + + INSERT dbo.ConfigAuditLog (Principal, EventType, ClusterId, GenerationId) + VALUES (SUSER_SNAME(), 'Published', @ClusterId, @DraftGenerationId); + + IF @OwnsTxn = 1 COMMIT; + END + +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417215224_StoredProcedures' +) +BEGIN + + CREATE OR ALTER PROCEDURE dbo.sp_RollbackToGeneration + @ClusterId nvarchar(64), + @TargetGenerationId bigint, + @Notes nvarchar(1024) = NULL + AS + BEGIN + SET NOCOUNT ON; + SET XACT_ABORT ON; + BEGIN TRANSACTION; + + IF NOT EXISTS ( + SELECT 1 FROM dbo.ConfigGeneration + WHERE GenerationId = @TargetGenerationId AND ClusterId = @ClusterId + AND Status IN ('Published', 'Superseded')) + BEGIN + RAISERROR('Target generation %I64d not found or not rollback-eligible', 16, 1, @TargetGenerationId); + ROLLBACK; RETURN; + END + + DECLARE @NewGenId bigint; + INSERT dbo.ConfigGeneration (ClusterId, Status, CreatedAt, CreatedBy, PublishedAt, PublishedBy, Notes) + VALUES (@ClusterId, 'Draft', SYSUTCDATETIME(), SUSER_SNAME(), NULL, NULL, + ISNULL(@Notes, CONCAT('Rollback clone of generation ', @TargetGenerationId))); + SET @NewGenId = SCOPE_IDENTITY(); + + INSERT dbo.Namespace (GenerationId, NamespaceId, ClusterId, Kind, NamespaceUri, Enabled, Notes) + SELECT @NewGenId, NamespaceId, ClusterId, Kind, NamespaceUri, Enabled, Notes FROM dbo.Namespace WHERE GenerationId = @TargetGenerationId; + INSERT dbo.UnsArea (GenerationId, UnsAreaId, ClusterId, Name, Notes) + SELECT @NewGenId, UnsAreaId, ClusterId, Name, Notes FROM dbo.UnsArea WHERE GenerationId = @TargetGenerationId; + INSERT dbo.UnsLine (GenerationId, UnsLineId, UnsAreaId, Name, Notes) + SELECT @NewGenId, UnsLineId, UnsAreaId, Name, Notes FROM dbo.UnsLine WHERE GenerationId = @TargetGenerationId; + INSERT dbo.DriverInstance (GenerationId, DriverInstanceId, ClusterId, NamespaceId, Name, DriverType, Enabled, DriverConfig) + SELECT @NewGenId, DriverInstanceId, ClusterId, NamespaceId, Name, DriverType, Enabled, DriverConfig FROM dbo.DriverInstance WHERE GenerationId = @TargetGenerationId; + INSERT dbo.Device (GenerationId, DeviceId, DriverInstanceId, Name, Enabled, DeviceConfig) + SELECT @NewGenId, DeviceId, DriverInstanceId, Name, Enabled, DeviceConfig FROM dbo.Device WHERE GenerationId = @TargetGenerationId; + INSERT dbo.Equipment (GenerationId, EquipmentId, EquipmentUuid, DriverInstanceId, DeviceId, UnsLineId, Name, MachineCode, ZTag, SAPID, Manufacturer, Model, SerialNumber, HardwareRevision, SoftwareRevision, YearOfConstruction, AssetLocation, ManufacturerUri, DeviceManualUri, EquipmentClassRef, Enabled) + SELECT @NewGenId, EquipmentId, EquipmentUuid, DriverInstanceId, DeviceId, UnsLineId, Name, MachineCode, ZTag, SAPID, Manufacturer, Model, SerialNumber, HardwareRevision, SoftwareRevision, YearOfConstruction, AssetLocation, ManufacturerUri, DeviceManualUri, EquipmentClassRef, Enabled FROM dbo.Equipment WHERE GenerationId = @TargetGenerationId; + INSERT dbo.PollGroup (GenerationId, PollGroupId, DriverInstanceId, Name, IntervalMs) + SELECT @NewGenId, PollGroupId, DriverInstanceId, Name, IntervalMs FROM dbo.PollGroup WHERE GenerationId = @TargetGenerationId; + INSERT dbo.Tag (GenerationId, TagId, DriverInstanceId, DeviceId, EquipmentId, Name, FolderPath, DataType, AccessLevel, WriteIdempotent, PollGroupId, TagConfig) + SELECT @NewGenId, TagId, DriverInstanceId, DeviceId, EquipmentId, Name, FolderPath, DataType, AccessLevel, WriteIdempotent, PollGroupId, TagConfig FROM dbo.Tag WHERE GenerationId = @TargetGenerationId; + INSERT dbo.NodeAcl (GenerationId, NodeAclId, ClusterId, LdapGroup, ScopeKind, ScopeId, PermissionFlags, Notes) + SELECT @NewGenId, NodeAclId, ClusterId, LdapGroup, ScopeKind, ScopeId, PermissionFlags, Notes FROM dbo.NodeAcl WHERE GenerationId = @TargetGenerationId; + + EXEC dbo.sp_PublishGeneration @ClusterId = @ClusterId, @DraftGenerationId = @NewGenId, @Notes = @Notes; + + -- @TargetGenerationId is a bigint, but build the JSON value via an explicit numeric CONVERT so + -- the emitted token is always a bare JSON number — never reliant on implicit string coercion. + INSERT dbo.ConfigAuditLog (Principal, EventType, ClusterId, GenerationId, DetailsJson) + VALUES (SUSER_SNAME(), 'RolledBack', @ClusterId, @NewGenId, + CONCAT('{"rolledBackTo":', CONVERT(nvarchar(20), CONVERT(bigint, @TargetGenerationId)), '}')); + + COMMIT; + END + +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417215224_StoredProcedures' +) +BEGIN + + CREATE OR ALTER PROCEDURE dbo.sp_ComputeGenerationDiff + @FromGenerationId bigint, + @ToGenerationId bigint + AS + BEGIN + SET NOCOUNT ON; + + CREATE TABLE #diff (TableName nvarchar(32), LogicalId nvarchar(64), ChangeKind nvarchar(16)); + + WITH f AS (SELECT NamespaceId AS LogicalId, CHECKSUM(NamespaceUri, Kind, Enabled, Notes) AS Sig FROM dbo.Namespace WHERE GenerationId = @FromGenerationId), + t AS (SELECT NamespaceId AS LogicalId, CHECKSUM(NamespaceUri, Kind, Enabled, Notes) AS Sig FROM dbo.Namespace WHERE GenerationId = @ToGenerationId) + INSERT #diff + SELECT 'Namespace', CONVERT(nvarchar(64), COALESCE(f.LogicalId, t.LogicalId)), + CASE WHEN f.LogicalId IS NULL THEN 'Added' + WHEN t.LogicalId IS NULL THEN 'Removed' + WHEN f.Sig <> t.Sig THEN 'Modified' + ELSE 'Unchanged' END + FROM f FULL OUTER JOIN t ON f.LogicalId = t.LogicalId + WHERE f.LogicalId IS NULL OR t.LogicalId IS NULL OR f.Sig <> t.Sig; + + WITH f AS (SELECT DriverInstanceId AS LogicalId, CHECKSUM(ClusterId, NamespaceId, Name, DriverType, Enabled, CONVERT(varchar(max), DriverConfig)) AS Sig FROM dbo.DriverInstance WHERE GenerationId = @FromGenerationId), + t AS (SELECT DriverInstanceId AS LogicalId, CHECKSUM(ClusterId, NamespaceId, Name, DriverType, Enabled, CONVERT(varchar(max), DriverConfig)) AS Sig FROM dbo.DriverInstance WHERE GenerationId = @ToGenerationId) + INSERT #diff + SELECT 'DriverInstance', CONVERT(nvarchar(64), COALESCE(f.LogicalId, t.LogicalId)), + CASE WHEN f.LogicalId IS NULL THEN 'Added' + WHEN t.LogicalId IS NULL THEN 'Removed' + WHEN f.Sig <> t.Sig THEN 'Modified' + ELSE 'Unchanged' END + FROM f FULL OUTER JOIN t ON f.LogicalId = t.LogicalId + WHERE f.LogicalId IS NULL OR t.LogicalId IS NULL OR f.Sig <> t.Sig; + + WITH f AS (SELECT EquipmentId AS LogicalId, CHECKSUM(EquipmentUuid, DriverInstanceId, UnsLineId, Name, MachineCode, ZTag, SAPID, EquipmentClassRef, Manufacturer, Model, SerialNumber) AS Sig FROM dbo.Equipment WHERE GenerationId = @FromGenerationId), + t AS (SELECT EquipmentId AS LogicalId, CHECKSUM(EquipmentUuid, DriverInstanceId, UnsLineId, Name, MachineCode, ZTag, SAPID, EquipmentClassRef, Manufacturer, Model, SerialNumber) AS Sig FROM dbo.Equipment WHERE GenerationId = @ToGenerationId) + INSERT #diff + SELECT 'Equipment', CONVERT(nvarchar(64), COALESCE(f.LogicalId, t.LogicalId)), + CASE WHEN f.LogicalId IS NULL THEN 'Added' + WHEN t.LogicalId IS NULL THEN 'Removed' + WHEN f.Sig <> t.Sig THEN 'Modified' + ELSE 'Unchanged' END + FROM f FULL OUTER JOIN t ON f.LogicalId = t.LogicalId + WHERE f.LogicalId IS NULL OR t.LogicalId IS NULL OR f.Sig <> t.Sig; + + WITH f AS (SELECT TagId AS LogicalId, CHECKSUM(DriverInstanceId, DeviceId, EquipmentId, PollGroupId, FolderPath, Name, DataType, AccessLevel, WriteIdempotent, CONVERT(varchar(max), TagConfig)) AS Sig FROM dbo.Tag WHERE GenerationId = @FromGenerationId), + t AS (SELECT TagId AS LogicalId, CHECKSUM(DriverInstanceId, DeviceId, EquipmentId, PollGroupId, FolderPath, Name, DataType, AccessLevel, WriteIdempotent, CONVERT(varchar(max), TagConfig)) AS Sig FROM dbo.Tag WHERE GenerationId = @ToGenerationId) + INSERT #diff + SELECT 'Tag', CONVERT(nvarchar(64), COALESCE(f.LogicalId, t.LogicalId)), + CASE WHEN f.LogicalId IS NULL THEN 'Added' + WHEN t.LogicalId IS NULL THEN 'Removed' + WHEN f.Sig <> t.Sig THEN 'Modified' + ELSE 'Unchanged' END + FROM f FULL OUTER JOIN t ON f.LogicalId = t.LogicalId + WHERE f.LogicalId IS NULL OR t.LogicalId IS NULL OR f.Sig <> t.Sig; + + SELECT TableName, LogicalId, ChangeKind FROM #diff; + DROP TABLE #diff; + END + +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417215224_StoredProcedures' +) +BEGIN + + CREATE OR ALTER PROCEDURE dbo.sp_ReleaseExternalIdReservation + @Kind nvarchar(16), + @Value nvarchar(64), + @ReleaseReason nvarchar(512) + AS + BEGIN + SET NOCOUNT ON; + SET XACT_ABORT ON; + + IF @ReleaseReason IS NULL OR LEN(@ReleaseReason) = 0 + BEGIN + RAISERROR('ReleaseReason is required', 16, 1); + RETURN; + END + + UPDATE dbo.ExternalIdReservation + SET ReleasedAt = SYSUTCDATETIME(), + ReleasedBy = SUSER_SNAME(), + ReleaseReason = @ReleaseReason + WHERE Kind = @Kind AND Value = @Value AND ReleasedAt IS NULL; + + IF @@ROWCOUNT = 0 + BEGIN + RAISERROR('No active reservation found for (%s, %s)', 16, 1, @Kind, @Value); + RETURN; + END + + -- Escape both caller-supplied values via STRING_ESCAPE so quotes/backslashes cannot break the + -- JSON document or inject additional structure into the audit record. + INSERT dbo.ConfigAuditLog (Principal, EventType, DetailsJson) + VALUES (SUSER_SNAME(), 'ExternalIdReleased', + CONCAT('{"kind":"', STRING_ESCAPE(@Kind, 'json'), + '","value":"', STRING_ESCAPE(@Value, 'json'), '"}')); + END + +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417215224_StoredProcedures' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260417215224_StoredProcedures', N'10.0.7'); +END; + +COMMIT; +GO + +BEGIN TRANSACTION; +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417220857_AuthorizationGrants' +) +BEGIN + + IF DATABASE_PRINCIPAL_ID('OtOpcUaNode') IS NULL + CREATE ROLE OtOpcUaNode; + IF DATABASE_PRINCIPAL_ID('OtOpcUaAdmin') IS NULL + CREATE ROLE OtOpcUaAdmin; + +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417220857_AuthorizationGrants' +) +BEGIN + + GRANT EXECUTE ON OBJECT::dbo.sp_GetCurrentGenerationForCluster TO OtOpcUaNode; + GRANT EXECUTE ON OBJECT::dbo.sp_GetGenerationContent TO OtOpcUaNode; + GRANT EXECUTE ON OBJECT::dbo.sp_RegisterNodeGenerationApplied TO OtOpcUaNode; + + GRANT EXECUTE ON OBJECT::dbo.sp_GetCurrentGenerationForCluster TO OtOpcUaAdmin; + GRANT EXECUTE ON OBJECT::dbo.sp_GetGenerationContent TO OtOpcUaAdmin; + GRANT EXECUTE ON OBJECT::dbo.sp_ValidateDraft TO OtOpcUaAdmin; + GRANT EXECUTE ON OBJECT::dbo.sp_PublishGeneration TO OtOpcUaAdmin; + GRANT EXECUTE ON OBJECT::dbo.sp_RollbackToGeneration TO OtOpcUaAdmin; + GRANT EXECUTE ON OBJECT::dbo.sp_ComputeGenerationDiff TO OtOpcUaAdmin; + GRANT EXECUTE ON OBJECT::dbo.sp_ReleaseExternalIdReservation TO OtOpcUaAdmin; + + DENY UPDATE, DELETE, INSERT ON SCHEMA::dbo TO OtOpcUaNode; + DENY UPDATE, DELETE, INSERT ON SCHEMA::dbo TO OtOpcUaAdmin; + DENY SELECT ON SCHEMA::dbo TO OtOpcUaNode; + -- Admins may SELECT for reporting views in the future — grant views explicitly, not the schema. + DENY SELECT ON SCHEMA::dbo TO OtOpcUaAdmin; + +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260417220857_AuthorizationGrants' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260417220857_AuthorizationGrants', N'10.0.7'); +END; + +COMMIT; +GO + +BEGIN TRANSACTION; +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260418193608_AddDriverHostStatus' +) +BEGIN + CREATE TABLE [DriverHostStatus] ( + [NodeId] nvarchar(64) NOT NULL, + [DriverInstanceId] nvarchar(64) NOT NULL, + [HostName] nvarchar(256) NOT NULL, + [State] nvarchar(16) NOT NULL, + [StateChangedUtc] datetime2(3) NOT NULL, + [LastSeenUtc] datetime2(3) NOT NULL, + [Detail] nvarchar(1024) NULL, + CONSTRAINT [PK_DriverHostStatus] PRIMARY KEY ([NodeId], [DriverInstanceId], [HostName]) + ); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260418193608_AddDriverHostStatus' +) +BEGIN + CREATE INDEX [IX_DriverHostStatus_LastSeen] ON [DriverHostStatus] ([LastSeenUtc]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260418193608_AddDriverHostStatus' +) +BEGIN + CREATE INDEX [IX_DriverHostStatus_Node] ON [DriverHostStatus] ([NodeId]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260418193608_AddDriverHostStatus' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260418193608_AddDriverHostStatus', N'10.0.7'); +END; + +COMMIT; +GO + +BEGIN TRANSACTION; +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260419124034_AddDriverInstanceResilienceStatus' +) +BEGIN + CREATE TABLE [DriverInstanceResilienceStatus] ( + [DriverInstanceId] nvarchar(64) NOT NULL, + [HostName] nvarchar(256) NOT NULL, + [LastCircuitBreakerOpenUtc] datetime2(3) NULL, + [ConsecutiveFailures] int NOT NULL, + [CurrentBulkheadDepth] int NOT NULL, + [LastRecycleUtc] datetime2(3) NULL, + [BaselineFootprintBytes] bigint NOT NULL, + [CurrentFootprintBytes] bigint NOT NULL, + [LastSampledUtc] datetime2(3) NOT NULL, + CONSTRAINT [PK_DriverInstanceResilienceStatus] PRIMARY KEY ([DriverInstanceId], [HostName]) + ); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260419124034_AddDriverInstanceResilienceStatus' +) +BEGIN + CREATE INDEX [IX_DriverResilience_LastSampled] ON [DriverInstanceResilienceStatus] ([LastSampledUtc]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260419124034_AddDriverInstanceResilienceStatus' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260419124034_AddDriverInstanceResilienceStatus', N'10.0.7'); +END; + +COMMIT; +GO + +BEGIN TRANSACTION; +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260419131444_AddLdapGroupRoleMapping' +) +BEGIN + CREATE TABLE [LdapGroupRoleMapping] ( + [Id] uniqueidentifier NOT NULL, + [LdapGroup] nvarchar(512) NOT NULL, + [Role] nvarchar(32) NOT NULL, + [ClusterId] nvarchar(64) NULL, + [IsSystemWide] bit NOT NULL, + [CreatedAtUtc] datetime2(3) NOT NULL, + [Notes] nvarchar(512) NULL, + CONSTRAINT [PK_LdapGroupRoleMapping] PRIMARY KEY ([Id]), + CONSTRAINT [FK_LdapGroupRoleMapping_ServerCluster_ClusterId] FOREIGN KEY ([ClusterId]) REFERENCES [ServerCluster] ([ClusterId]) ON DELETE CASCADE + ); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260419131444_AddLdapGroupRoleMapping' +) +BEGIN + CREATE INDEX [IX_LdapGroupRoleMapping_ClusterId] ON [LdapGroupRoleMapping] ([ClusterId]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260419131444_AddLdapGroupRoleMapping' +) +BEGIN + CREATE INDEX [IX_LdapGroupRoleMapping_Group] ON [LdapGroupRoleMapping] ([LdapGroup]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260419131444_AddLdapGroupRoleMapping' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [UX_LdapGroupRoleMapping_Group_Cluster] ON [LdapGroupRoleMapping] ([LdapGroup], [ClusterId]) WHERE [ClusterId] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260419131444_AddLdapGroupRoleMapping' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260419131444_AddLdapGroupRoleMapping', N'10.0.7'); +END; + +COMMIT; +GO + +BEGIN TRANSACTION; +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260419161932_AddDriverInstanceResilienceConfig' +) +BEGIN + ALTER TABLE [DriverInstance] ADD [ResilienceConfig] nvarchar(max) NULL; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260419161932_AddDriverInstanceResilienceConfig' +) +BEGIN + EXEC(N'ALTER TABLE [DriverInstance] ADD CONSTRAINT [CK_DriverInstance_ResilienceConfig_IsJson] CHECK (ResilienceConfig IS NULL OR ISJSON(ResilienceConfig) = 1)'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260419161932_AddDriverInstanceResilienceConfig' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260419161932_AddDriverInstanceResilienceConfig', N'10.0.7'); +END; + +COMMIT; +GO + +BEGIN TRANSACTION; +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260419185124_AddEquipmentImportBatch' +) +BEGIN + CREATE TABLE [EquipmentImportBatch] ( + [Id] uniqueidentifier NOT NULL, + [ClusterId] nvarchar(64) NOT NULL, + [CreatedBy] nvarchar(128) NOT NULL, + [CreatedAtUtc] datetime2(3) NOT NULL, + [RowsStaged] int NOT NULL, + [RowsAccepted] int NOT NULL, + [RowsRejected] int NOT NULL, + [FinalisedAtUtc] datetime2(3) NULL, + CONSTRAINT [PK_EquipmentImportBatch] PRIMARY KEY ([Id]) + ); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260419185124_AddEquipmentImportBatch' +) +BEGIN + CREATE TABLE [EquipmentImportRow] ( + [Id] uniqueidentifier NOT NULL, + [BatchId] uniqueidentifier NOT NULL, + [LineNumberInFile] int NOT NULL, + [IsAccepted] bit NOT NULL, + [RejectReason] nvarchar(512) NULL, + [ZTag] nvarchar(128) NOT NULL, + [MachineCode] nvarchar(128) NOT NULL, + [SAPID] nvarchar(128) NOT NULL, + [EquipmentId] nvarchar(64) NOT NULL, + [EquipmentUuid] nvarchar(64) NOT NULL, + [Name] nvarchar(128) NOT NULL, + [UnsAreaName] nvarchar(64) NOT NULL, + [UnsLineName] nvarchar(64) NOT NULL, + [Manufacturer] nvarchar(256) NULL, + [Model] nvarchar(256) NULL, + [SerialNumber] nvarchar(256) NULL, + [HardwareRevision] nvarchar(64) NULL, + [SoftwareRevision] nvarchar(64) NULL, + [YearOfConstruction] nvarchar(8) NULL, + [AssetLocation] nvarchar(512) NULL, + [ManufacturerUri] nvarchar(512) NULL, + [DeviceManualUri] nvarchar(512) NULL, + CONSTRAINT [PK_EquipmentImportRow] PRIMARY KEY ([Id]), + CONSTRAINT [FK_EquipmentImportRow_EquipmentImportBatch_BatchId] FOREIGN KEY ([BatchId]) REFERENCES [EquipmentImportBatch] ([Id]) ON DELETE CASCADE + ); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260419185124_AddEquipmentImportBatch' +) +BEGIN + CREATE INDEX [IX_EquipmentImportBatch_Creator_Finalised] ON [EquipmentImportBatch] ([CreatedBy], [FinalisedAtUtc]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260419185124_AddEquipmentImportBatch' +) +BEGIN + CREATE INDEX [IX_EquipmentImportRow_Batch] ON [EquipmentImportRow] ([BatchId]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260419185124_AddEquipmentImportBatch' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260419185124_AddEquipmentImportBatch', N'10.0.7'); +END; + +COMMIT; +GO + +BEGIN TRANSACTION; +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260420000001_ExtendComputeGenerationDiffWithNodeAcl' +) +BEGIN + + CREATE OR ALTER PROCEDURE dbo.sp_ComputeGenerationDiff + @FromGenerationId bigint, + @ToGenerationId bigint + AS + BEGIN + SET NOCOUNT ON; + + CREATE TABLE #diff (TableName nvarchar(32), LogicalId nvarchar(128), ChangeKind nvarchar(16)); + + WITH f AS (SELECT NamespaceId AS LogicalId, CHECKSUM(NamespaceUri, Kind, Enabled, Notes) AS Sig FROM dbo.Namespace WHERE GenerationId = @FromGenerationId), + t AS (SELECT NamespaceId AS LogicalId, CHECKSUM(NamespaceUri, Kind, Enabled, Notes) AS Sig FROM dbo.Namespace WHERE GenerationId = @ToGenerationId) + INSERT #diff + SELECT 'Namespace', CONVERT(nvarchar(128), COALESCE(f.LogicalId, t.LogicalId)), + CASE WHEN f.LogicalId IS NULL THEN 'Added' + WHEN t.LogicalId IS NULL THEN 'Removed' + WHEN f.Sig <> t.Sig THEN 'Modified' + ELSE 'Unchanged' END + FROM f FULL OUTER JOIN t ON f.LogicalId = t.LogicalId + WHERE f.LogicalId IS NULL OR t.LogicalId IS NULL OR f.Sig <> t.Sig; + + WITH f AS (SELECT DriverInstanceId AS LogicalId, CHECKSUM(ClusterId, NamespaceId, Name, DriverType, Enabled, CONVERT(varchar(max), DriverConfig)) AS Sig FROM dbo.DriverInstance WHERE GenerationId = @FromGenerationId), + t AS (SELECT DriverInstanceId AS LogicalId, CHECKSUM(ClusterId, NamespaceId, Name, DriverType, Enabled, CONVERT(varchar(max), DriverConfig)) AS Sig FROM dbo.DriverInstance WHERE GenerationId = @ToGenerationId) + INSERT #diff + SELECT 'DriverInstance', CONVERT(nvarchar(128), COALESCE(f.LogicalId, t.LogicalId)), + CASE WHEN f.LogicalId IS NULL THEN 'Added' + WHEN t.LogicalId IS NULL THEN 'Removed' + WHEN f.Sig <> t.Sig THEN 'Modified' + ELSE 'Unchanged' END + FROM f FULL OUTER JOIN t ON f.LogicalId = t.LogicalId + WHERE f.LogicalId IS NULL OR t.LogicalId IS NULL OR f.Sig <> t.Sig; + + WITH f AS (SELECT EquipmentId AS LogicalId, CHECKSUM(EquipmentUuid, DriverInstanceId, UnsLineId, Name, MachineCode, ZTag, SAPID, EquipmentClassRef, Manufacturer, Model, SerialNumber) AS Sig FROM dbo.Equipment WHERE GenerationId = @FromGenerationId), + t AS (SELECT EquipmentId AS LogicalId, CHECKSUM(EquipmentUuid, DriverInstanceId, UnsLineId, Name, MachineCode, ZTag, SAPID, EquipmentClassRef, Manufacturer, Model, SerialNumber) AS Sig FROM dbo.Equipment WHERE GenerationId = @ToGenerationId) + INSERT #diff + SELECT 'Equipment', CONVERT(nvarchar(128), COALESCE(f.LogicalId, t.LogicalId)), + CASE WHEN f.LogicalId IS NULL THEN 'Added' + WHEN t.LogicalId IS NULL THEN 'Removed' + WHEN f.Sig <> t.Sig THEN 'Modified' + ELSE 'Unchanged' END + FROM f FULL OUTER JOIN t ON f.LogicalId = t.LogicalId + WHERE f.LogicalId IS NULL OR t.LogicalId IS NULL OR f.Sig <> t.Sig; + + WITH f AS (SELECT TagId AS LogicalId, CHECKSUM(DriverInstanceId, DeviceId, EquipmentId, PollGroupId, FolderPath, Name, DataType, AccessLevel, WriteIdempotent, CONVERT(varchar(max), TagConfig)) AS Sig FROM dbo.Tag WHERE GenerationId = @FromGenerationId), + t AS (SELECT TagId AS LogicalId, CHECKSUM(DriverInstanceId, DeviceId, EquipmentId, PollGroupId, FolderPath, Name, DataType, AccessLevel, WriteIdempotent, CONVERT(varchar(max), TagConfig)) AS Sig FROM dbo.Tag WHERE GenerationId = @ToGenerationId) + INSERT #diff + SELECT 'Tag', CONVERT(nvarchar(128), COALESCE(f.LogicalId, t.LogicalId)), + CASE WHEN f.LogicalId IS NULL THEN 'Added' + WHEN t.LogicalId IS NULL THEN 'Removed' + WHEN f.Sig <> t.Sig THEN 'Modified' + ELSE 'Unchanged' END + FROM f FULL OUTER JOIN t ON f.LogicalId = t.LogicalId + WHERE f.LogicalId IS NULL OR t.LogicalId IS NULL OR f.Sig <> t.Sig; + + -- NodeAcl section. Logical id is the (LdapGroup, ScopeKind, ScopeId) triple so the diff + -- distinguishes same row with new permissions (Modified via CHECKSUM on PermissionFlags + Notes) + -- from a scope move (which surfaces as Added + Removed of different logical ids). + WITH f AS ( + SELECT CONVERT(nvarchar(128), LdapGroup + '|' + CONVERT(nvarchar(16), ScopeKind) + '|' + ISNULL(ScopeId, '(cluster)')) AS LogicalId, + CHECKSUM(ClusterId, PermissionFlags, Notes) AS Sig + FROM dbo.NodeAcl WHERE GenerationId = @FromGenerationId), + t AS ( + SELECT CONVERT(nvarchar(128), LdapGroup + '|' + CONVERT(nvarchar(16), ScopeKind) + '|' + ISNULL(ScopeId, '(cluster)')) AS LogicalId, + CHECKSUM(ClusterId, PermissionFlags, Notes) AS Sig + FROM dbo.NodeAcl WHERE GenerationId = @ToGenerationId) + INSERT #diff + SELECT 'NodeAcl', COALESCE(f.LogicalId, t.LogicalId), + CASE WHEN f.LogicalId IS NULL THEN 'Added' + WHEN t.LogicalId IS NULL THEN 'Removed' + WHEN f.Sig <> t.Sig THEN 'Modified' + ELSE 'Unchanged' END + FROM f FULL OUTER JOIN t ON f.LogicalId = t.LogicalId + WHERE f.LogicalId IS NULL OR t.LogicalId IS NULL OR f.Sig <> t.Sig; + + SELECT TableName, LogicalId, ChangeKind FROM #diff; + DROP TABLE #diff; + END + +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260420000001_ExtendComputeGenerationDiffWithNodeAcl' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260420000001_ExtendComputeGenerationDiffWithNodeAcl', N'10.0.7'); +END; + +COMMIT; +GO + +BEGIN TRANSACTION; +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260420231641_AddPhase7ScriptingTables' +) +BEGIN + CREATE TABLE [Script] ( + [ScriptRowId] uniqueidentifier NOT NULL DEFAULT (NEWSEQUENTIALID()), + [GenerationId] bigint NOT NULL, + [ScriptId] nvarchar(64) NULL, + [Name] nvarchar(128) NOT NULL, + [SourceCode] nvarchar(max) NOT NULL, + [SourceHash] nvarchar(64) NOT NULL, + [Language] nvarchar(16) NOT NULL, + CONSTRAINT [PK_Script] PRIMARY KEY ([ScriptRowId]), + CONSTRAINT [FK_Script_ConfigGeneration_GenerationId] FOREIGN KEY ([GenerationId]) REFERENCES [ConfigGeneration] ([GenerationId]) ON DELETE NO ACTION + ); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260420231641_AddPhase7ScriptingTables' +) +BEGIN + CREATE TABLE [ScriptedAlarm] ( + [ScriptedAlarmRowId] uniqueidentifier NOT NULL DEFAULT (NEWSEQUENTIALID()), + [GenerationId] bigint NOT NULL, + [ScriptedAlarmId] nvarchar(64) NULL, + [EquipmentId] nvarchar(64) NOT NULL, + [Name] nvarchar(128) NOT NULL, + [AlarmType] nvarchar(32) NOT NULL, + [Severity] int NOT NULL, + [MessageTemplate] nvarchar(1024) NOT NULL, + [PredicateScriptId] nvarchar(64) NOT NULL, + [HistorizeToAveva] bit NOT NULL, + [Retain] bit NOT NULL, + [Enabled] bit NOT NULL, + CONSTRAINT [PK_ScriptedAlarm] PRIMARY KEY ([ScriptedAlarmRowId]), + CONSTRAINT [CK_ScriptedAlarm_AlarmType] CHECK (AlarmType IN ('AlarmCondition','LimitAlarm','OffNormalAlarm','DiscreteAlarm')), + CONSTRAINT [CK_ScriptedAlarm_Severity_Range] CHECK (Severity BETWEEN 1 AND 1000), + CONSTRAINT [FK_ScriptedAlarm_ConfigGeneration_GenerationId] FOREIGN KEY ([GenerationId]) REFERENCES [ConfigGeneration] ([GenerationId]) ON DELETE NO ACTION + ); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260420231641_AddPhase7ScriptingTables' +) +BEGIN + CREATE TABLE [ScriptedAlarmState] ( + [ScriptedAlarmId] nvarchar(64) NOT NULL, + [EnabledState] nvarchar(16) NOT NULL, + [AckedState] nvarchar(16) NOT NULL, + [ConfirmedState] nvarchar(16) NOT NULL, + [ShelvingState] nvarchar(16) NOT NULL, + [ShelvingExpiresUtc] datetime2(3) NULL, + [LastAckUser] nvarchar(128) NULL, + [LastAckComment] nvarchar(1024) NULL, + [LastAckUtc] datetime2(3) NULL, + [LastConfirmUser] nvarchar(128) NULL, + [LastConfirmComment] nvarchar(1024) NULL, + [LastConfirmUtc] datetime2(3) NULL, + [CommentsJson] nvarchar(max) NOT NULL, + [UpdatedAtUtc] datetime2(3) NOT NULL DEFAULT (SYSUTCDATETIME()), + CONSTRAINT [PK_ScriptedAlarmState] PRIMARY KEY ([ScriptedAlarmId]), + CONSTRAINT [CK_ScriptedAlarmState_CommentsJson_IsJson] CHECK (ISJSON(CommentsJson) = 1) + ); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260420231641_AddPhase7ScriptingTables' +) +BEGIN + CREATE TABLE [VirtualTag] ( + [VirtualTagRowId] uniqueidentifier NOT NULL DEFAULT (NEWSEQUENTIALID()), + [GenerationId] bigint NOT NULL, + [VirtualTagId] nvarchar(64) NULL, + [EquipmentId] nvarchar(64) NOT NULL, + [Name] nvarchar(128) NOT NULL, + [DataType] nvarchar(32) NOT NULL, + [ScriptId] nvarchar(64) NOT NULL, + [ChangeTriggered] bit NOT NULL, + [TimerIntervalMs] int NULL, + [Historize] bit NOT NULL, + [Enabled] bit NOT NULL, + CONSTRAINT [PK_VirtualTag] PRIMARY KEY ([VirtualTagRowId]), + CONSTRAINT [CK_VirtualTag_TimerInterval_Min] CHECK (TimerIntervalMs IS NULL OR TimerIntervalMs >= 50), + CONSTRAINT [CK_VirtualTag_Trigger_AtLeastOne] CHECK (ChangeTriggered = 1 OR TimerIntervalMs IS NOT NULL), + CONSTRAINT [FK_VirtualTag_ConfigGeneration_GenerationId] FOREIGN KEY ([GenerationId]) REFERENCES [ConfigGeneration] ([GenerationId]) ON DELETE NO ACTION + ); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260420231641_AddPhase7ScriptingTables' +) +BEGIN + CREATE INDEX [IX_Script_Generation_SourceHash] ON [Script] ([GenerationId], [SourceHash]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260420231641_AddPhase7ScriptingTables' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [UX_Script_Generation_LogicalId] ON [Script] ([GenerationId], [ScriptId]) WHERE [ScriptId] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260420231641_AddPhase7ScriptingTables' +) +BEGIN + CREATE INDEX [IX_ScriptedAlarm_Generation_Script] ON [ScriptedAlarm] ([GenerationId], [PredicateScriptId]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260420231641_AddPhase7ScriptingTables' +) +BEGIN + CREATE UNIQUE INDEX [UX_ScriptedAlarm_Generation_EquipmentPath] ON [ScriptedAlarm] ([GenerationId], [EquipmentId], [Name]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260420231641_AddPhase7ScriptingTables' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [UX_ScriptedAlarm_Generation_LogicalId] ON [ScriptedAlarm] ([GenerationId], [ScriptedAlarmId]) WHERE [ScriptedAlarmId] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260420231641_AddPhase7ScriptingTables' +) +BEGIN + CREATE INDEX [IX_VirtualTag_Generation_Script] ON [VirtualTag] ([GenerationId], [ScriptId]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260420231641_AddPhase7ScriptingTables' +) +BEGIN + CREATE UNIQUE INDEX [UX_VirtualTag_Generation_EquipmentPath] ON [VirtualTag] ([GenerationId], [EquipmentId], [Name]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260420231641_AddPhase7ScriptingTables' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [UX_VirtualTag_Generation_LogicalId] ON [VirtualTag] ([GenerationId], [VirtualTagId]) WHERE [VirtualTagId] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260420231641_AddPhase7ScriptingTables' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260420231641_AddPhase7ScriptingTables', N'10.0.7'); +END; + +COMMIT; +GO + +BEGIN TRANSACTION; +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260420232000_ExtendComputeGenerationDiffWithPhase7' +) +BEGIN + + CREATE OR ALTER PROCEDURE dbo.sp_ComputeGenerationDiff + @FromGenerationId bigint, + @ToGenerationId bigint + AS + BEGIN + SET NOCOUNT ON; + + CREATE TABLE #diff (TableName nvarchar(32), LogicalId nvarchar(128), ChangeKind nvarchar(16)); + + WITH f AS (SELECT NamespaceId AS LogicalId, CHECKSUM(NamespaceUri, Kind, Enabled, Notes) AS Sig FROM dbo.Namespace WHERE GenerationId = @FromGenerationId), + t AS (SELECT NamespaceId AS LogicalId, CHECKSUM(NamespaceUri, Kind, Enabled, Notes) AS Sig FROM dbo.Namespace WHERE GenerationId = @ToGenerationId) + INSERT #diff + SELECT 'Namespace', CONVERT(nvarchar(128), COALESCE(f.LogicalId, t.LogicalId)), + CASE WHEN f.LogicalId IS NULL THEN 'Added' + WHEN t.LogicalId IS NULL THEN 'Removed' + WHEN f.Sig <> t.Sig THEN 'Modified' + ELSE 'Unchanged' END + FROM f FULL OUTER JOIN t ON f.LogicalId = t.LogicalId + WHERE f.LogicalId IS NULL OR t.LogicalId IS NULL OR f.Sig <> t.Sig; + + WITH f AS (SELECT DriverInstanceId AS LogicalId, CHECKSUM(ClusterId, NamespaceId, Name, DriverType, Enabled, CONVERT(varchar(max), DriverConfig)) AS Sig FROM dbo.DriverInstance WHERE GenerationId = @FromGenerationId), + t AS (SELECT DriverInstanceId AS LogicalId, CHECKSUM(ClusterId, NamespaceId, Name, DriverType, Enabled, CONVERT(varchar(max), DriverConfig)) AS Sig FROM dbo.DriverInstance WHERE GenerationId = @ToGenerationId) + INSERT #diff + SELECT 'DriverInstance', CONVERT(nvarchar(128), COALESCE(f.LogicalId, t.LogicalId)), + CASE WHEN f.LogicalId IS NULL THEN 'Added' + WHEN t.LogicalId IS NULL THEN 'Removed' + WHEN f.Sig <> t.Sig THEN 'Modified' + ELSE 'Unchanged' END + FROM f FULL OUTER JOIN t ON f.LogicalId = t.LogicalId + WHERE f.LogicalId IS NULL OR t.LogicalId IS NULL OR f.Sig <> t.Sig; + + WITH f AS (SELECT EquipmentId AS LogicalId, CHECKSUM(EquipmentUuid, DriverInstanceId, UnsLineId, Name, MachineCode, ZTag, SAPID, EquipmentClassRef, Manufacturer, Model, SerialNumber) AS Sig FROM dbo.Equipment WHERE GenerationId = @FromGenerationId), + t AS (SELECT EquipmentId AS LogicalId, CHECKSUM(EquipmentUuid, DriverInstanceId, UnsLineId, Name, MachineCode, ZTag, SAPID, EquipmentClassRef, Manufacturer, Model, SerialNumber) AS Sig FROM dbo.Equipment WHERE GenerationId = @ToGenerationId) + INSERT #diff + SELECT 'Equipment', CONVERT(nvarchar(128), COALESCE(f.LogicalId, t.LogicalId)), + CASE WHEN f.LogicalId IS NULL THEN 'Added' + WHEN t.LogicalId IS NULL THEN 'Removed' + WHEN f.Sig <> t.Sig THEN 'Modified' + ELSE 'Unchanged' END + FROM f FULL OUTER JOIN t ON f.LogicalId = t.LogicalId + WHERE f.LogicalId IS NULL OR t.LogicalId IS NULL OR f.Sig <> t.Sig; + + WITH f AS (SELECT TagId AS LogicalId, CHECKSUM(DriverInstanceId, DeviceId, EquipmentId, PollGroupId, FolderPath, Name, DataType, AccessLevel, WriteIdempotent, CONVERT(varchar(max), TagConfig)) AS Sig FROM dbo.Tag WHERE GenerationId = @FromGenerationId), + t AS (SELECT TagId AS LogicalId, CHECKSUM(DriverInstanceId, DeviceId, EquipmentId, PollGroupId, FolderPath, Name, DataType, AccessLevel, WriteIdempotent, CONVERT(varchar(max), TagConfig)) AS Sig FROM dbo.Tag WHERE GenerationId = @ToGenerationId) + INSERT #diff + SELECT 'Tag', CONVERT(nvarchar(128), COALESCE(f.LogicalId, t.LogicalId)), + CASE WHEN f.LogicalId IS NULL THEN 'Added' + WHEN t.LogicalId IS NULL THEN 'Removed' + WHEN f.Sig <> t.Sig THEN 'Modified' + ELSE 'Unchanged' END + FROM f FULL OUTER JOIN t ON f.LogicalId = t.LogicalId + WHERE f.LogicalId IS NULL OR t.LogicalId IS NULL OR f.Sig <> t.Sig; + + WITH f AS ( + SELECT CONVERT(nvarchar(128), LdapGroup + '|' + CONVERT(nvarchar(16), ScopeKind) + '|' + ISNULL(ScopeId, '(cluster)')) AS LogicalId, + CHECKSUM(ClusterId, PermissionFlags, Notes) AS Sig + FROM dbo.NodeAcl WHERE GenerationId = @FromGenerationId), + t AS ( + SELECT CONVERT(nvarchar(128), LdapGroup + '|' + CONVERT(nvarchar(16), ScopeKind) + '|' + ISNULL(ScopeId, '(cluster)')) AS LogicalId, + CHECKSUM(ClusterId, PermissionFlags, Notes) AS Sig + FROM dbo.NodeAcl WHERE GenerationId = @ToGenerationId) + INSERT #diff + SELECT 'NodeAcl', COALESCE(f.LogicalId, t.LogicalId), + CASE WHEN f.LogicalId IS NULL THEN 'Added' + WHEN t.LogicalId IS NULL THEN 'Removed' + WHEN f.Sig <> t.Sig THEN 'Modified' + ELSE 'Unchanged' END + FROM f FULL OUTER JOIN t ON f.LogicalId = t.LogicalId + WHERE f.LogicalId IS NULL OR t.LogicalId IS NULL OR f.Sig <> t.Sig; + + -- Phase 7 — Script section. CHECKSUM picks up source changes via SourceHash + rename + -- via Name; Language future-proofs for non-C# engines. Same Name + same Source = + -- Unchanged (identical hash). + WITH f AS (SELECT ScriptId AS LogicalId, CHECKSUM(Name, SourceHash, Language) AS Sig FROM dbo.Script WHERE GenerationId = @FromGenerationId), + t AS (SELECT ScriptId AS LogicalId, CHECKSUM(Name, SourceHash, Language) AS Sig FROM dbo.Script WHERE GenerationId = @ToGenerationId) + INSERT #diff + SELECT 'Script', CONVERT(nvarchar(128), COALESCE(f.LogicalId, t.LogicalId)), + CASE WHEN f.LogicalId IS NULL THEN 'Added' + WHEN t.LogicalId IS NULL THEN 'Removed' + WHEN f.Sig <> t.Sig THEN 'Modified' + ELSE 'Unchanged' END + FROM f FULL OUTER JOIN t ON f.LogicalId = t.LogicalId + WHERE f.LogicalId IS NULL OR t.LogicalId IS NULL OR f.Sig <> t.Sig; + + -- Phase 7 — VirtualTag section. + WITH f AS (SELECT VirtualTagId AS LogicalId, CHECKSUM(EquipmentId, Name, DataType, ScriptId, ChangeTriggered, TimerIntervalMs, Historize, Enabled) AS Sig FROM dbo.VirtualTag WHERE GenerationId = @FromGenerationId), + t AS (SELECT VirtualTagId AS LogicalId, CHECKSUM(EquipmentId, Name, DataType, ScriptId, ChangeTriggered, TimerIntervalMs, Historize, Enabled) AS Sig FROM dbo.VirtualTag WHERE GenerationId = @ToGenerationId) + INSERT #diff + SELECT 'VirtualTag', CONVERT(nvarchar(128), COALESCE(f.LogicalId, t.LogicalId)), + CASE WHEN f.LogicalId IS NULL THEN 'Added' + WHEN t.LogicalId IS NULL THEN 'Removed' + WHEN f.Sig <> t.Sig THEN 'Modified' + ELSE 'Unchanged' END + FROM f FULL OUTER JOIN t ON f.LogicalId = t.LogicalId + WHERE f.LogicalId IS NULL OR t.LogicalId IS NULL OR f.Sig <> t.Sig; + + -- Phase 7 — ScriptedAlarm section. ScriptedAlarmState (operator ack trail) is + -- logical-id keyed outside the generation scope + intentionally excluded here — + -- diffing ack state between generations is semantically meaningless. + WITH f AS (SELECT ScriptedAlarmId AS LogicalId, CHECKSUM(EquipmentId, Name, AlarmType, Severity, MessageTemplate, PredicateScriptId, HistorizeToAveva, Retain, Enabled) AS Sig FROM dbo.ScriptedAlarm WHERE GenerationId = @FromGenerationId), + t AS (SELECT ScriptedAlarmId AS LogicalId, CHECKSUM(EquipmentId, Name, AlarmType, Severity, MessageTemplate, PredicateScriptId, HistorizeToAveva, Retain, Enabled) AS Sig FROM dbo.ScriptedAlarm WHERE GenerationId = @ToGenerationId) + INSERT #diff + SELECT 'ScriptedAlarm', CONVERT(nvarchar(128), COALESCE(f.LogicalId, t.LogicalId)), + CASE WHEN f.LogicalId IS NULL THEN 'Added' + WHEN t.LogicalId IS NULL THEN 'Removed' + WHEN f.Sig <> t.Sig THEN 'Modified' + ELSE 'Unchanged' END + FROM f FULL OUTER JOIN t ON f.LogicalId = t.LogicalId + WHERE f.LogicalId IS NULL OR t.LogicalId IS NULL OR f.Sig <> t.Sig; + + SELECT TableName, LogicalId, ChangeKind FROM #diff; + DROP TABLE #diff; + END + +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260420232000_ExtendComputeGenerationDiffWithPhase7' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260420232000_ExtendComputeGenerationDiffWithPhase7', N'10.0.7'); +END; + +COMMIT; +GO + +BEGIN TRANSACTION; +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + ALTER TABLE [Device] DROP CONSTRAINT [FK_Device_ConfigGeneration_GenerationId]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + ALTER TABLE [DriverInstance] DROP CONSTRAINT [FK_DriverInstance_ConfigGeneration_GenerationId]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + ALTER TABLE [Equipment] DROP CONSTRAINT [FK_Equipment_ConfigGeneration_GenerationId]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + ALTER TABLE [Namespace] DROP CONSTRAINT [FK_Namespace_ConfigGeneration_GenerationId]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + ALTER TABLE [NodeAcl] DROP CONSTRAINT [FK_NodeAcl_ConfigGeneration_GenerationId]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + ALTER TABLE [PollGroup] DROP CONSTRAINT [FK_PollGroup_ConfigGeneration_GenerationId]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + ALTER TABLE [Script] DROP CONSTRAINT [FK_Script_ConfigGeneration_GenerationId]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + ALTER TABLE [ScriptedAlarm] DROP CONSTRAINT [FK_ScriptedAlarm_ConfigGeneration_GenerationId]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + ALTER TABLE [Tag] DROP CONSTRAINT [FK_Tag_ConfigGeneration_GenerationId]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + ALTER TABLE [UnsArea] DROP CONSTRAINT [FK_UnsArea_ConfigGeneration_GenerationId]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + ALTER TABLE [UnsLine] DROP CONSTRAINT [FK_UnsLine_ConfigGeneration_GenerationId]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + ALTER TABLE [VirtualTag] DROP CONSTRAINT [FK_VirtualTag_ConfigGeneration_GenerationId]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP TABLE [ClusterNodeGenerationState]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP TABLE [ConfigGeneration]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [IX_VirtualTag_Generation_Script] ON [VirtualTag]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [UX_VirtualTag_Generation_EquipmentPath] ON [VirtualTag]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [UX_VirtualTag_Generation_LogicalId] ON [VirtualTag]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [IX_UnsLine_Generation_Area] ON [UnsLine]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [UX_UnsLine_Generation_AreaName] ON [UnsLine]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [UX_UnsLine_Generation_LogicalId] ON [UnsLine]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [IX_UnsArea_Generation_Cluster] ON [UnsArea]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [UX_UnsArea_Generation_ClusterName] ON [UnsArea]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [UX_UnsArea_Generation_LogicalId] ON [UnsArea]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [IX_Tag_Generation_Driver_Device] ON [Tag]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [IX_Tag_Generation_Equipment] ON [Tag]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [UX_Tag_Generation_EquipmentPath] ON [Tag]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [UX_Tag_Generation_FolderPath] ON [Tag]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [UX_Tag_Generation_LogicalId] ON [Tag]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [IX_ScriptedAlarm_Generation_Script] ON [ScriptedAlarm]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [UX_ScriptedAlarm_Generation_EquipmentPath] ON [ScriptedAlarm]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [UX_ScriptedAlarm_Generation_LogicalId] ON [ScriptedAlarm]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [IX_Script_Generation_SourceHash] ON [Script]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [UX_Script_Generation_LogicalId] ON [Script]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [IX_PollGroup_Generation_Driver] ON [PollGroup]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [UX_PollGroup_Generation_LogicalId] ON [PollGroup]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [IX_NodeAcl_Generation_Cluster] ON [NodeAcl]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [IX_NodeAcl_Generation_Group] ON [NodeAcl]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [IX_NodeAcl_Generation_Scope] ON [NodeAcl]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [UX_NodeAcl_Generation_GroupScope] ON [NodeAcl]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [UX_NodeAcl_Generation_LogicalId] ON [NodeAcl]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [IX_Namespace_Generation_Cluster] ON [Namespace]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [UX_Namespace_Generation_Cluster_Kind] ON [Namespace]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [UX_Namespace_Generation_LogicalId] ON [Namespace]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [UX_Namespace_Generation_LogicalId_Cluster] ON [Namespace]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [UX_Namespace_Generation_NamespaceUri] ON [Namespace]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [IX_Equipment_Generation_Driver] ON [Equipment]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [IX_Equipment_Generation_Line] ON [Equipment]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [IX_Equipment_Generation_MachineCode] ON [Equipment]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [IX_Equipment_Generation_SAPID] ON [Equipment]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [IX_Equipment_Generation_ZTag] ON [Equipment]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [UX_Equipment_Generation_LinePath] ON [Equipment]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [UX_Equipment_Generation_LogicalId] ON [Equipment]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [UX_Equipment_Generation_Uuid] ON [Equipment]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [IX_DriverInstance_Generation_Cluster] ON [DriverInstance]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [IX_DriverInstance_Generation_Namespace] ON [DriverInstance]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [UX_DriverInstance_Generation_LogicalId] ON [DriverInstance]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [IX_Device_Generation_Driver] ON [Device]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [UX_Device_Generation_LogicalId] ON [Device]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DROP INDEX [UX_ClusterNode_Primary_Per_Cluster] ON [ClusterNode]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DECLARE @var nvarchar(max); + SELECT @var = QUOTENAME([d].[name]) + FROM [sys].[default_constraints] [d] + INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] + WHERE ([d].[parent_object_id] = OBJECT_ID(N'[VirtualTag]') AND [c].[name] = N'GenerationId'); + IF @var IS NOT NULL EXEC(N'ALTER TABLE [VirtualTag] DROP CONSTRAINT ' + @var + ';'); + ALTER TABLE [VirtualTag] DROP COLUMN [GenerationId]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DECLARE @var1 nvarchar(max); + SELECT @var1 = QUOTENAME([d].[name]) + FROM [sys].[default_constraints] [d] + INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] + WHERE ([d].[parent_object_id] = OBJECT_ID(N'[UnsLine]') AND [c].[name] = N'GenerationId'); + IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [UnsLine] DROP CONSTRAINT ' + @var1 + ';'); + ALTER TABLE [UnsLine] DROP COLUMN [GenerationId]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DECLARE @var2 nvarchar(max); + SELECT @var2 = QUOTENAME([d].[name]) + FROM [sys].[default_constraints] [d] + INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] + WHERE ([d].[parent_object_id] = OBJECT_ID(N'[UnsArea]') AND [c].[name] = N'GenerationId'); + IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [UnsArea] DROP CONSTRAINT ' + @var2 + ';'); + ALTER TABLE [UnsArea] DROP COLUMN [GenerationId]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DECLARE @var3 nvarchar(max); + SELECT @var3 = QUOTENAME([d].[name]) + FROM [sys].[default_constraints] [d] + INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] + WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Tag]') AND [c].[name] = N'GenerationId'); + IF @var3 IS NOT NULL EXEC(N'ALTER TABLE [Tag] DROP CONSTRAINT ' + @var3 + ';'); + ALTER TABLE [Tag] DROP COLUMN [GenerationId]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DECLARE @var4 nvarchar(max); + SELECT @var4 = QUOTENAME([d].[name]) + FROM [sys].[default_constraints] [d] + INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] + WHERE ([d].[parent_object_id] = OBJECT_ID(N'[ScriptedAlarm]') AND [c].[name] = N'GenerationId'); + IF @var4 IS NOT NULL EXEC(N'ALTER TABLE [ScriptedAlarm] DROP CONSTRAINT ' + @var4 + ';'); + ALTER TABLE [ScriptedAlarm] DROP COLUMN [GenerationId]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DECLARE @var5 nvarchar(max); + SELECT @var5 = QUOTENAME([d].[name]) + FROM [sys].[default_constraints] [d] + INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] + WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Script]') AND [c].[name] = N'GenerationId'); + IF @var5 IS NOT NULL EXEC(N'ALTER TABLE [Script] DROP CONSTRAINT ' + @var5 + ';'); + ALTER TABLE [Script] DROP COLUMN [GenerationId]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DECLARE @var6 nvarchar(max); + SELECT @var6 = QUOTENAME([d].[name]) + FROM [sys].[default_constraints] [d] + INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] + WHERE ([d].[parent_object_id] = OBJECT_ID(N'[PollGroup]') AND [c].[name] = N'GenerationId'); + IF @var6 IS NOT NULL EXEC(N'ALTER TABLE [PollGroup] DROP CONSTRAINT ' + @var6 + ';'); + ALTER TABLE [PollGroup] DROP COLUMN [GenerationId]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DECLARE @var7 nvarchar(max); + SELECT @var7 = QUOTENAME([d].[name]) + FROM [sys].[default_constraints] [d] + INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] + WHERE ([d].[parent_object_id] = OBJECT_ID(N'[NodeAcl]') AND [c].[name] = N'GenerationId'); + IF @var7 IS NOT NULL EXEC(N'ALTER TABLE [NodeAcl] DROP CONSTRAINT ' + @var7 + ';'); + ALTER TABLE [NodeAcl] DROP COLUMN [GenerationId]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DECLARE @var8 nvarchar(max); + SELECT @var8 = QUOTENAME([d].[name]) + FROM [sys].[default_constraints] [d] + INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] + WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Namespace]') AND [c].[name] = N'GenerationId'); + IF @var8 IS NOT NULL EXEC(N'ALTER TABLE [Namespace] DROP CONSTRAINT ' + @var8 + ';'); + ALTER TABLE [Namespace] DROP COLUMN [GenerationId]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DECLARE @var9 nvarchar(max); + SELECT @var9 = QUOTENAME([d].[name]) + FROM [sys].[default_constraints] [d] + INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] + WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Equipment]') AND [c].[name] = N'GenerationId'); + IF @var9 IS NOT NULL EXEC(N'ALTER TABLE [Equipment] DROP CONSTRAINT ' + @var9 + ';'); + ALTER TABLE [Equipment] DROP COLUMN [GenerationId]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DECLARE @var10 nvarchar(max); + SELECT @var10 = QUOTENAME([d].[name]) + FROM [sys].[default_constraints] [d] + INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] + WHERE ([d].[parent_object_id] = OBJECT_ID(N'[DriverInstance]') AND [c].[name] = N'GenerationId'); + IF @var10 IS NOT NULL EXEC(N'ALTER TABLE [DriverInstance] DROP CONSTRAINT ' + @var10 + ';'); + ALTER TABLE [DriverInstance] DROP COLUMN [GenerationId]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DECLARE @var11 nvarchar(max); + SELECT @var11 = QUOTENAME([d].[name]) + FROM [sys].[default_constraints] [d] + INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] + WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Device]') AND [c].[name] = N'GenerationId'); + IF @var11 IS NOT NULL EXEC(N'ALTER TABLE [Device] DROP CONSTRAINT ' + @var11 + ';'); + ALTER TABLE [Device] DROP COLUMN [GenerationId]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + DECLARE @var12 nvarchar(max); + SELECT @var12 = QUOTENAME([d].[name]) + FROM [sys].[default_constraints] [d] + INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] + WHERE ([d].[parent_object_id] = OBJECT_ID(N'[ClusterNode]') AND [c].[name] = N'RedundancyRole'); + IF @var12 IS NOT NULL EXEC(N'ALTER TABLE [ClusterNode] DROP CONSTRAINT ' + @var12 + ';'); + ALTER TABLE [ClusterNode] DROP COLUMN [RedundancyRole]; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + EXEC sp_rename N'[UnsArea].[IX_UnsArea_ClusterId]', N'IX_UnsArea_Cluster', 'INDEX'; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + EXEC sp_rename N'[Namespace].[IX_Namespace_ClusterId]', N'IX_Namespace_Cluster', 'INDEX'; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + EXEC sp_rename N'[DriverInstance].[IX_DriverInstance_ClusterId]', N'IX_DriverInstance_Cluster', 'INDEX'; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + ALTER TABLE [VirtualTag] ADD [RowVersion] rowversion NOT NULL; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + ALTER TABLE [UnsLine] ADD [RowVersion] rowversion NOT NULL; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + ALTER TABLE [UnsArea] ADD [RowVersion] rowversion NOT NULL; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + ALTER TABLE [Tag] ADD [RowVersion] rowversion NOT NULL; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + ALTER TABLE [ScriptedAlarm] ADD [RowVersion] rowversion NOT NULL; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + ALTER TABLE [Script] ADD [RowVersion] rowversion NOT NULL; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + ALTER TABLE [PollGroup] ADD [RowVersion] rowversion NOT NULL; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + ALTER TABLE [NodeAcl] ADD [RowVersion] rowversion NOT NULL; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + ALTER TABLE [Namespace] ADD [RowVersion] rowversion NOT NULL; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + ALTER TABLE [Equipment] ADD [RowVersion] rowversion NOT NULL; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + ALTER TABLE [DriverInstance] ADD [RowVersion] rowversion NOT NULL; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + ALTER TABLE [Device] ADD [RowVersion] rowversion NOT NULL; +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + CREATE TABLE [ConfigEdit] ( + [EditId] uniqueidentifier NOT NULL DEFAULT (NEWSEQUENTIALID()), + [EntityType] nvarchar(64) NOT NULL, + [EntityId] uniqueidentifier NOT NULL, + [FieldsJson] nvarchar(max) NOT NULL, + [ExecutionId] uniqueidentifier NULL, + [EditedBy] nvarchar(128) NOT NULL, + [EditedAtUtc] datetime2(3) NOT NULL DEFAULT (SYSUTCDATETIME()), + [SourceNode] nvarchar(64) NOT NULL, + CONSTRAINT [PK_ConfigEdit] PRIMARY KEY ([EditId]), + CONSTRAINT [CK_ConfigEdit_FieldsJson_IsJson] CHECK (ISJSON(FieldsJson) = 1) + ); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + CREATE TABLE [DataProtectionKeys] ( + [Id] int NOT NULL IDENTITY, + [FriendlyName] nvarchar(max) NULL, + [Xml] nvarchar(max) NULL, + CONSTRAINT [PK_DataProtectionKeys] PRIMARY KEY ([Id]) + ); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + CREATE TABLE [Deployment] ( + [DeploymentId] uniqueidentifier NOT NULL DEFAULT (NEWSEQUENTIALID()), + [RevisionHash] nvarchar(64) NOT NULL, + [Status] int NOT NULL, + [CreatedBy] nvarchar(128) NOT NULL, + [CreatedAtUtc] datetime2(3) NOT NULL DEFAULT (SYSUTCDATETIME()), + [ArtifactBlob] varbinary(max) NOT NULL, + [RowVersion] rowversion NOT NULL, + [FailureReason] nvarchar(2048) NULL, + [SealedAtUtc] datetime2(3) NULL, + CONSTRAINT [PK_Deployment] PRIMARY KEY ([DeploymentId]) + ); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + CREATE TABLE [NodeDeploymentState] ( + [NodeId] nvarchar(64) NOT NULL, + [DeploymentId] uniqueidentifier NOT NULL, + [Status] int NOT NULL, + [StartedAtUtc] datetime2(3) NOT NULL DEFAULT (SYSUTCDATETIME()), + [AppliedAtUtc] datetime2(3) NULL, + [FailureReason] nvarchar(2048) NULL, + [RowVersion] rowversion NOT NULL, + CONSTRAINT [PK_NodeDeploymentState] PRIMARY KEY ([NodeId], [DeploymentId]), + CONSTRAINT [FK_NodeDeploymentState_ClusterNode_NodeId] FOREIGN KEY ([NodeId]) REFERENCES [ClusterNode] ([NodeId]) ON DELETE NO ACTION, + CONSTRAINT [FK_NodeDeploymentState_Deployment_DeploymentId] FOREIGN KEY ([DeploymentId]) REFERENCES [Deployment] ([DeploymentId]) ON DELETE CASCADE + ); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + CREATE INDEX [IX_VirtualTag_Script] ON [VirtualTag] ([ScriptId]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + CREATE UNIQUE INDEX [UX_VirtualTag_EquipmentPath] ON [VirtualTag] ([EquipmentId], [Name]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [UX_VirtualTag_LogicalId] ON [VirtualTag] ([VirtualTagId]) WHERE [VirtualTagId] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + CREATE INDEX [IX_UnsLine_Area] ON [UnsLine] ([UnsAreaId]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + CREATE UNIQUE INDEX [UX_UnsLine_AreaName] ON [UnsLine] ([UnsAreaId], [Name]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [UX_UnsLine_LogicalId] ON [UnsLine] ([UnsLineId]) WHERE [UnsLineId] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + CREATE UNIQUE INDEX [UX_UnsArea_ClusterName] ON [UnsArea] ([ClusterId], [Name]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [UX_UnsArea_LogicalId] ON [UnsArea] ([UnsAreaId]) WHERE [UnsAreaId] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + CREATE INDEX [IX_Tag_Driver_Device] ON [Tag] ([DriverInstanceId], [DeviceId]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + EXEC(N'CREATE INDEX [IX_Tag_Equipment] ON [Tag] ([EquipmentId]) WHERE [EquipmentId] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [UX_Tag_EquipmentPath] ON [Tag] ([EquipmentId], [Name]) WHERE [EquipmentId] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [UX_Tag_FolderPath] ON [Tag] ([DriverInstanceId], [FolderPath], [Name]) WHERE [EquipmentId] IS NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [UX_Tag_LogicalId] ON [Tag] ([TagId]) WHERE [TagId] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + CREATE INDEX [IX_ScriptedAlarm_Script] ON [ScriptedAlarm] ([PredicateScriptId]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + CREATE UNIQUE INDEX [UX_ScriptedAlarm_EquipmentPath] ON [ScriptedAlarm] ([EquipmentId], [Name]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [UX_ScriptedAlarm_LogicalId] ON [ScriptedAlarm] ([ScriptedAlarmId]) WHERE [ScriptedAlarmId] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + CREATE INDEX [IX_Script_SourceHash] ON [Script] ([SourceHash]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [UX_Script_LogicalId] ON [Script] ([ScriptId]) WHERE [ScriptId] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + CREATE INDEX [IX_PollGroup_Driver] ON [PollGroup] ([DriverInstanceId]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [UX_PollGroup_LogicalId] ON [PollGroup] ([PollGroupId]) WHERE [PollGroupId] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + CREATE INDEX [IX_NodeAcl_Cluster] ON [NodeAcl] ([ClusterId]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + CREATE INDEX [IX_NodeAcl_Group] ON [NodeAcl] ([LdapGroup]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + EXEC(N'CREATE INDEX [IX_NodeAcl_Scope] ON [NodeAcl] ([ScopeKind], [ScopeId]) WHERE [ScopeId] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [UX_NodeAcl_GroupScope] ON [NodeAcl] ([ClusterId], [LdapGroup], [ScopeKind], [ScopeId]) WHERE [ScopeId] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [UX_NodeAcl_LogicalId] ON [NodeAcl] ([NodeAclId]) WHERE [NodeAclId] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + CREATE UNIQUE INDEX [UX_Namespace_Cluster_Kind] ON [Namespace] ([ClusterId], [Kind]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [UX_Namespace_LogicalId] ON [Namespace] ([NamespaceId]) WHERE [NamespaceId] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + CREATE UNIQUE INDEX [UX_Namespace_NamespaceUri] ON [Namespace] ([NamespaceUri]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + CREATE INDEX [IX_Equipment_Driver] ON [Equipment] ([DriverInstanceId]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + CREATE INDEX [IX_Equipment_Line] ON [Equipment] ([UnsLineId]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + CREATE INDEX [IX_Equipment_MachineCode] ON [Equipment] ([MachineCode]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + EXEC(N'CREATE INDEX [IX_Equipment_SAPID] ON [Equipment] ([SAPID]) WHERE [SAPID] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + EXEC(N'CREATE INDEX [IX_Equipment_ZTag] ON [Equipment] ([ZTag]) WHERE [ZTag] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + CREATE UNIQUE INDEX [UX_Equipment_LinePath] ON [Equipment] ([UnsLineId], [Name]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [UX_Equipment_LogicalId] ON [Equipment] ([EquipmentId]) WHERE [EquipmentId] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + CREATE UNIQUE INDEX [UX_Equipment_Uuid] ON [Equipment] ([EquipmentUuid]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + CREATE INDEX [IX_DriverInstance_Namespace] ON [DriverInstance] ([NamespaceId]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [UX_DriverInstance_LogicalId] ON [DriverInstance] ([DriverInstanceId]) WHERE [DriverInstanceId] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + CREATE INDEX [IX_Device_Driver] ON [Device] ([DriverInstanceId]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + EXEC(N'CREATE UNIQUE INDEX [UX_Device_LogicalId] ON [Device] ([DeviceId]) WHERE [DeviceId] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + CREATE INDEX [IX_ClusterNode_ClusterId] ON [ClusterNode] ([ClusterId]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + CREATE INDEX [IX_ConfigEdit_EditedAt] ON [ConfigEdit] ([EditedAtUtc]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + CREATE INDEX [IX_ConfigEdit_Entity] ON [ConfigEdit] ([EntityType], [EntityId]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + EXEC(N'CREATE INDEX [IX_ConfigEdit_Execution] ON [ConfigEdit] ([ExecutionId]) WHERE [ExecutionId] IS NOT NULL'); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + CREATE INDEX [IX_Deployment_CreatedAt] ON [Deployment] ([CreatedAtUtc]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + CREATE INDEX [IX_Deployment_Status] ON [Deployment] ([Status]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + CREATE INDEX [IX_NodeDeploymentState_Deployment] ON [NodeDeploymentState] ([DeploymentId]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + CREATE INDEX [IX_NodeDeploymentState_Status] ON [NodeDeploymentState] ([Status]); +END; + +IF NOT EXISTS ( + SELECT * FROM [__EFMigrationsHistory] + WHERE [MigrationId] = N'20260526081556_V2HostingAlignment' +) +BEGIN + INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) + VALUES (N'20260526081556_V2HostingAlignment', N'10.0.7'); +END; + +COMMIT; +GO + diff --git a/scripts/migration/count-rows.sql b/scripts/migration/count-rows.sql new file mode 100644 index 0000000..7b52c86 --- /dev/null +++ b/scripts/migration/count-rows.sql @@ -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 diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Host/ZB.MOM.WW.OtOpcUa.Host.csproj b/src/Server/ZB.MOM.WW.OtOpcUa.Host/ZB.MOM.WW.OtOpcUa.Host.csproj index f251e81..48a4716 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Host/ZB.MOM.WW.OtOpcUa.Host.csproj +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Host/ZB.MOM.WW.OtOpcUa.Host.csproj @@ -11,6 +11,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive +