v3 batch1 WP1: greenfield DbContext + V3Initial squash + seeds + tests
DbContext (OtOpcUaConfigDbContext): - Drop Namespace/EquipmentImportBatch/EquipmentImportRow DbSets + Configure methods. - Add RawFolder/TagGroup/UnsTagReference DbSets + Configure methods with sibling-name filtered unique indexes over nullable parents (two-filtered pattern per level). - Reshape DriverInstance (RawFolderId + UX_DriverInstance_Folder_Name/_ClusterRoot_Name), Tag (required DeviceId, nullable TagGroupId, UX_Tag_Group_Name/_Device_Name, IX_Tag_Device), Equipment (drop DriverInstanceId/DeviceId + IX_Equipment_Driver), Device (UX_Device_Driver_Name). Migration squash -> V3Initial: - Delete the whole Migrations/ folder; scaffold one V3Initial (applies clean to a fresh DB). - Port the hand-written procs + AuthorizationGrants into V3Initial.StoredProcedures.cs. The v1 generation-model procs (ConfigGeneration/GenerationId, dead since V2HostingAlignment) are rewritten as v3-schema-valid retirement stubs that preserve the two-role EXECUTE-grant surface + AuthorizationTests behaviors; sp_ComputeGenerationDiff now references the v3 tables; sp_ReleaseExternalIdReservation ported verbatim. Compile-fixes for the build gate (Wave-C-owned, minimal + TODO(v3 WP4) markers): - DraftSnapshot/DraftSnapshotFactory: drop the retired Namespaces list. - DraftValidator: delete namespace-binding + Galaxy-FullName rules; collision rule now VirtualTag-only. Seeds -> v3 schema: - seed-clusters.sql summary SELECTs; all 5 scripts/smoke/seed-*.sql rewritten (DriverInstance RawFolderId, Device+DeviceConfig endpoint, Tag DeviceId, UnsTagReference; no ConfigGeneration/ Namespace/sp_PublishGeneration). Verified: all seeds apply clean against a fresh V3Initial DB. Tests (Configuration.Tests, 107 pass): - SchemaComplianceTests rewritten to v3 tables + filtered indexes. - New RawSchemaIntegrityTests: DB-enforced sibling-name uniqueness per raw level + UnsTagReference (Equipment,Tag) uniqueness. - Delete DraftValidatorGalaxyFullNameCorpusTests (WP2/WP6 replaces); fix DraftValidatorTests + DraftSnapshotFactoryTests to v3 shapes; repoint scripting-migration existence test to V3Initial.
This commit is contained in:
@@ -123,13 +123,13 @@ COMMIT TRANSACTION;
|
||||
-- Summary (logged by sqlcmd output)
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
-- v3: the Namespace table is retired; DriverInstance is folder-scoped (RawFolderId) and Tags
|
||||
-- are device-scoped (DeviceId + optional TagGroupId). This seed only creates clusters + nodes +
|
||||
-- LDAP role mappings, so the summary reflects just those. Raw-tree config (RawFolder/DriverInstance/
|
||||
-- Device/TagGroup/Tag) is authored via the AdminUI /raw page in later batches.
|
||||
SELECT ClusterId, Name, NodeCount, RedundancyMode FROM dbo.ServerCluster ORDER BY ClusterId;
|
||||
SELECT NodeId, ClusterId, Host, OpcUaPort, ApplicationUri, ServiceLevelBase
|
||||
FROM dbo.ClusterNode ORDER BY ClusterId, NodeId;
|
||||
SELECT NamespaceId, ClusterId, Kind, NamespaceUri FROM dbo.Namespace ORDER BY ClusterId, NamespaceId;
|
||||
SELECT DriverInstanceId, ClusterId, DriverType, NamespaceId, Name
|
||||
FROM dbo.DriverInstance ORDER BY ClusterId, DriverInstanceId;
|
||||
SELECT TagId, DriverInstanceId, FolderPath, Name, DataType FROM dbo.Tag ORDER BY DriverInstanceId, FolderPath, Name;
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- LDAP group -> AdminUI role mappings (shared dev GLAuth, 10.100.0.35)
|
||||
|
||||
@@ -1,128 +1,74 @@
|
||||
-- AB CIP e2e smoke seed — closes #211 (umbrella #209).
|
||||
-- AB CIP (Logix) e2e smoke seed — v3 schema (Raw tree: DriverInstance → Device → Tag; UNS by ref).
|
||||
--
|
||||
-- One-cluster seed pointing at the ab_server ControlLogix fixture
|
||||
-- (`docker compose -f tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Docker/docker-compose.yml --profile controllogix up -d`).
|
||||
-- Publishes a single `TestDINT:DInt` tag under NodeId `ns=<N>;s=TestDINT`
|
||||
-- (ab_server seeds this tag by default).
|
||||
--
|
||||
-- Usage:
|
||||
-- sqlcmd -S "localhost,14330" -d OtOpcUaConfig -U sa -P "OtOpcUaDev_2026!" \
|
||||
-- -i scripts/smoke/seed-abcip-smoke.sql
|
||||
--
|
||||
-- After seeding, point appsettings at this cluster:
|
||||
-- Node:NodeId = "abcip-smoke-node"
|
||||
-- Node:ClusterId = "abcip-smoke"
|
||||
-- Then start server + run `./scripts/e2e/test-abcip.ps1 -BridgeNodeId "ns=2;s=TestDINT"`.
|
||||
-- Idempotent DELETE-and-recreate pointing at the CIP fixture (ab://127.0.0.1:44818/1,0). v3: the
|
||||
-- ConfigGeneration/Namespace model + sp_PublishGeneration are gone; the per-device HostAddress moved
|
||||
-- from DriverConfig.Devices[] into the Device's DeviceConfig (the deviceHostAddress tag key is dropped
|
||||
-- — the tag's DeviceId FK owns placement now).
|
||||
|
||||
SET NOCOUNT ON;
|
||||
SET XACT_ABORT ON;
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
SET ANSI_NULLS ON;
|
||||
SET ANSI_PADDING ON;
|
||||
SET ANSI_WARNINGS ON;
|
||||
SET ARITHABORT ON;
|
||||
SET CONCAT_NULL_YIELDS_NULL ON;
|
||||
|
||||
DECLARE @ClusterId nvarchar(64) = 'abcip-smoke';
|
||||
DECLARE @NodeId nvarchar(64) = 'abcip-smoke-node';
|
||||
DECLARE @DrvId nvarchar(64) = 'abcip-smoke-drv';
|
||||
DECLARE @NsId nvarchar(64) = 'abcip-smoke-ns';
|
||||
DECLARE @DevId nvarchar(64) = 'abcip-smoke-dev';
|
||||
DECLARE @AreaId nvarchar(64) = 'abcip-smoke-area';
|
||||
DECLARE @LineId nvarchar(64) = 'abcip-smoke-line';
|
||||
DECLARE @EqId nvarchar(64) = 'abcip-smoke-eq';
|
||||
DECLARE @EqUuid uniqueidentifier = '41BC12E0-41BC-412E-841B-C12E041BC12E';
|
||||
DECLARE @TagId nvarchar(64) = 'abcip-smoke-tag-testdint';
|
||||
DECLARE @EqUuid uniqueidentifier = 'ABC17000-ABC1-45E0-9ABC-17000ABC1700';
|
||||
DECLARE @Tag nvarchar(64) = 'abcip-smoke-tag-dint';
|
||||
DECLARE @RefId nvarchar(64) = 'abcip-smoke-ref-dint';
|
||||
|
||||
BEGIN TRAN;
|
||||
|
||||
DELETE FROM dbo.Tag WHERE TagId IN (@TagId);
|
||||
DELETE FROM dbo.Equipment WHERE EquipmentId = @EqId;
|
||||
DELETE FROM dbo.UnsLine WHERE UnsLineId = @LineId;
|
||||
DELETE FROM dbo.UnsArea WHERE UnsAreaId = @AreaId;
|
||||
DELETE FROM dbo.DriverInstance WHERE DriverInstanceId = @DrvId;
|
||||
DELETE FROM dbo.Namespace WHERE NamespaceId = @NsId;
|
||||
DELETE FROM dbo.ConfigGeneration WHERE ClusterId = @ClusterId;
|
||||
DELETE FROM dbo.ClusterNodeCredential WHERE NodeId = @NodeId;
|
||||
DELETE FROM dbo.ClusterNodeGenerationState WHERE NodeId = @NodeId;
|
||||
DELETE FROM dbo.ClusterNode WHERE NodeId = @NodeId;
|
||||
DELETE FROM dbo.ServerCluster WHERE ClusterId = @ClusterId;
|
||||
|
||||
DELETE FROM dbo.UnsTagReference WHERE UnsTagReferenceId = @RefId;
|
||||
DELETE FROM dbo.Tag WHERE TagId = @Tag;
|
||||
DELETE FROM dbo.Equipment WHERE EquipmentId = @EqId;
|
||||
DELETE FROM dbo.UnsLine WHERE UnsLineId = @LineId;
|
||||
DELETE FROM dbo.UnsArea WHERE UnsAreaId = @AreaId;
|
||||
DELETE FROM dbo.Device WHERE DeviceId = @DevId;
|
||||
DELETE FROM dbo.DriverInstance WHERE DriverInstanceId = @DrvId;
|
||||
DELETE FROM dbo.ClusterNodeCredential WHERE NodeId = @NodeId;
|
||||
DELETE FROM dbo.ClusterNode WHERE NodeId = @NodeId;
|
||||
DELETE FROM dbo.ServerCluster WHERE ClusterId = @ClusterId;
|
||||
DELETE FROM dbo.ClusterNodeCredential WHERE Kind = 'SqlLogin' AND Value = 'sa';
|
||||
|
||||
INSERT dbo.ServerCluster(ClusterId, Name, Enterprise, Site, NodeCount, RedundancyMode, Enabled, CreatedBy)
|
||||
VALUES (@ClusterId, 'AB CIP Smoke', 'zb', 'lab', 1, 'None', 1, 'abcip-smoke');
|
||||
VALUES (@ClusterId, 'AbCip Smoke', 'zb', 'lab', 1, 'None', 1, 'abcip-smoke');
|
||||
|
||||
INSERT dbo.ClusterNode(NodeId, ClusterId, RedundancyRole, Host, OpcUaPort, DashboardPort,
|
||||
INSERT dbo.ClusterNode(NodeId, ClusterId, Host, OpcUaPort, DashboardPort,
|
||||
ApplicationUri, ServiceLevelBase, Enabled, CreatedBy)
|
||||
VALUES (@NodeId, @ClusterId, 'Primary', 'localhost', 4840, 15050,
|
||||
'urn:OtOpcUa:abcip-smoke-node', 200, 1, 'abcip-smoke');
|
||||
VALUES (@NodeId, @ClusterId, 'localhost', 4840, 15050, 'urn:OtOpcUa:abcip-smoke-node', 200, 1, 'abcip-smoke');
|
||||
|
||||
INSERT dbo.ClusterNodeCredential(NodeId, Kind, Value, Enabled, CreatedBy)
|
||||
VALUES (@NodeId, 'SqlLogin', 'sa', 1, 'abcip-smoke');
|
||||
|
||||
DECLARE @Gen bigint;
|
||||
INSERT dbo.ConfigGeneration(ClusterId, Status, CreatedBy)
|
||||
VALUES (@ClusterId, 'Draft', 'abcip-smoke');
|
||||
SET @Gen = SCOPE_IDENTITY();
|
||||
|
||||
INSERT dbo.Namespace(GenerationId, NamespaceId, ClusterId, Kind, NamespaceUri, Enabled)
|
||||
VALUES (@Gen, @NsId, @ClusterId, 'Equipment', 'urn:abcip-smoke:eq', 1);
|
||||
|
||||
INSERT dbo.UnsArea(GenerationId, UnsAreaId, ClusterId, Name)
|
||||
VALUES (@Gen, @AreaId, @ClusterId, 'lab-floor');
|
||||
|
||||
INSERT dbo.UnsLine(GenerationId, UnsLineId, UnsAreaId, Name)
|
||||
VALUES (@Gen, @LineId, @AreaId, 'abcip-line');
|
||||
|
||||
INSERT dbo.Equipment(GenerationId, EquipmentId, EquipmentUuid, DriverInstanceId, UnsLineId,
|
||||
Name, MachineCode, Enabled)
|
||||
VALUES (@Gen, @EqId, @EqUuid, @DrvId, @LineId, 'ab-sim', 'abcip-001', 1);
|
||||
|
||||
-- AB CIP DriverInstance — single ControlLogix device at the ab_server fixture
|
||||
-- gateway. DriverConfig shape mirrors AbCipDriverConfigDto.
|
||||
INSERT dbo.DriverInstance(GenerationId, DriverInstanceId, ClusterId, NamespaceId,
|
||||
Name, DriverType, DriverConfig, Enabled)
|
||||
VALUES (@Gen, @DrvId, @ClusterId, @NsId, 'ab-server-smoke', 'AbCip', N'{
|
||||
"TimeoutMs": 2000,
|
||||
"Devices": [
|
||||
{
|
||||
"HostAddress": "ab://127.0.0.1:44818/1,0",
|
||||
"PlcFamily": "ControlLogix",
|
||||
"DeviceName": "ab-server"
|
||||
}
|
||||
],
|
||||
"Probe": { "Enabled": true, "IntervalMs": 5000, "TimeoutMs": 2000 },
|
||||
"Tags": [
|
||||
{
|
||||
"Name": "TestDINT",
|
||||
"DeviceHostAddress": "ab://127.0.0.1:44818/1,0",
|
||||
"TagPath": "TestDINT",
|
||||
"DataType": "DInt",
|
||||
"Writable": true,
|
||||
"WriteIdempotent": true
|
||||
}
|
||||
]
|
||||
-- DriverConfig keeps protocol/channel settings; the per-device Devices[] list is retired in favor
|
||||
-- of Device rows carrying HostAddress in DeviceConfig.
|
||||
INSERT dbo.DriverInstance(DriverInstanceId, ClusterId, RawFolderId, Name, DriverType, DriverConfig, Enabled)
|
||||
VALUES (@DrvId, @ClusterId, NULL, 'logix-smoke', 'AbCip', N'{
|
||||
"EnableControllerBrowse": false,
|
||||
"ProbeTimeoutSeconds": 5
|
||||
}', 1);
|
||||
|
||||
INSERT dbo.Tag(GenerationId, TagId, DriverInstanceId, EquipmentId, Name, DataType,
|
||||
AccessLevel, TagConfig, WriteIdempotent)
|
||||
VALUES (@Gen, @TagId, @DrvId, @EqId, 'TestDINT', 'Int32', 'ReadWrite',
|
||||
N'{"FullName":"TestDINT","DataType":"DInt"}', 1);
|
||||
-- DeviceConfig shape finalized against reworked options in Wave B (WP5).
|
||||
INSERT dbo.Device(DeviceId, DriverInstanceId, Name, Enabled, DeviceConfig)
|
||||
VALUES (@DevId, @DrvId, 'plc', 1, N'{ "HostAddress": "ab://127.0.0.1:44818/1,0" }');
|
||||
|
||||
EXEC dbo.sp_PublishGeneration @ClusterId = @ClusterId, @DraftGenerationId = @Gen,
|
||||
@Notes = N'AB CIP smoke — task #211';
|
||||
INSERT dbo.Tag(TagId, DeviceId, TagGroupId, Name, DataType, AccessLevel, WriteIdempotent, TagConfig)
|
||||
VALUES (@Tag, @DevId, NULL, 'TestDINT', 'DInt', 'ReadWrite', 1,
|
||||
N'{ "address": "TestDINT", "dataType": "DInt" }');
|
||||
|
||||
INSERT dbo.UnsArea(UnsAreaId, ClusterId, Name) VALUES (@AreaId, @ClusterId, 'lab-floor');
|
||||
INSERT dbo.UnsLine(UnsLineId, UnsAreaId, Name) VALUES (@LineId, @AreaId, 'abcip-line');
|
||||
INSERT dbo.Equipment(EquipmentId, EquipmentUuid, UnsLineId, Name, MachineCode, Enabled)
|
||||
VALUES (@EqId, @EqUuid, @LineId, 'logix-sim', 'abcip-001', 1);
|
||||
|
||||
INSERT dbo.UnsTagReference(UnsTagReferenceId, EquipmentId, TagId, SortOrder)
|
||||
VALUES (@RefId, @EqId, @Tag, 0);
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRINT '';
|
||||
PRINT 'AB CIP smoke seed complete.';
|
||||
PRINT ' Cluster: ' + @ClusterId;
|
||||
PRINT ' Node: ' + @NodeId;
|
||||
PRINT ' Generation: ' + CONVERT(nvarchar(20), @Gen);
|
||||
PRINT '';
|
||||
PRINT 'Next steps:';
|
||||
PRINT ' 1. Set src/.../Server/appsettings.json Node:NodeId = "abcip-smoke-node"';
|
||||
PRINT ' Node:ClusterId = "abcip-smoke"';
|
||||
PRINT ' 2. docker compose -f tests/.../AbCip.IntegrationTests/Docker/docker-compose.yml --profile controllogix up -d';
|
||||
PRINT ' 3. dotnet run --project src/Server/ZB.MOM.WW.OtOpcUa.Server';
|
||||
PRINT ' 4. ./scripts/e2e/test-abcip.ps1 -BridgeNodeId "ns=2;s=TestDINT"';
|
||||
PRINT 'AbCip smoke seed complete (v3 schema). Cluster: ' + @ClusterId + ' Node: ' + @NodeId;
|
||||
|
||||
@@ -1,125 +1,74 @@
|
||||
-- AB Legacy e2e smoke seed — closes #213 (umbrella #209).
|
||||
-- AB Legacy (PCCC / SLC-500, MicroLogix) e2e smoke seed — v3 schema (DriverInstance → Device → Tag).
|
||||
--
|
||||
-- Works against the ab_server PCCC Docker fixture (one of the slc500 /
|
||||
-- micrologix / plc5 compose profiles) or real SLC 500 / MicroLogix / PLC-5
|
||||
-- hardware. Default HostAddress below points at the Docker fixture with a
|
||||
-- `/1,0` cip-path; libplctag's ab_server rejects empty paths before routing
|
||||
-- to the PCCC dispatcher. Real hardware uses an empty path — change the
|
||||
-- HostAddress to `ab://<plc-ip>:44818/` (note the trailing slash with nothing
|
||||
-- after) before running the seed for that setup.
|
||||
--
|
||||
-- Usage:
|
||||
-- docker compose -f tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/docker-compose.yml --profile slc500 up -d
|
||||
-- sqlcmd -S "localhost,14330" -d OtOpcUaConfig -U sa -P "OtOpcUaDev_2026!" \
|
||||
-- -i scripts/smoke/seed-ablegacy-smoke.sql
|
||||
-- Idempotent DELETE-and-recreate pointing at the fixture (ab://127.0.0.1:44818/1,0). To target real
|
||||
-- hardware, edit the Device's DeviceConfig HostAddress to end with /<empty> (trailing slash, no path).
|
||||
-- v3: the ConfigGeneration/Namespace model + sp_PublishGeneration are gone; the per-device HostAddress
|
||||
-- moved from DriverConfig.Devices[] into the Device's DeviceConfig (the deviceHostAddress tag key is
|
||||
-- dropped — the tag's DeviceId FK owns placement now).
|
||||
|
||||
SET NOCOUNT ON;
|
||||
SET XACT_ABORT ON;
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
SET ANSI_NULLS ON;
|
||||
SET ANSI_PADDING ON;
|
||||
SET ANSI_WARNINGS ON;
|
||||
SET ARITHABORT ON;
|
||||
SET CONCAT_NULL_YIELDS_NULL ON;
|
||||
|
||||
DECLARE @ClusterId nvarchar(64) = 'ablegacy-smoke';
|
||||
DECLARE @NodeId nvarchar(64) = 'ablegacy-smoke-node';
|
||||
DECLARE @DrvId nvarchar(64) = 'ablegacy-smoke-drv';
|
||||
DECLARE @NsId nvarchar(64) = 'ablegacy-smoke-ns';
|
||||
DECLARE @DevId nvarchar(64) = 'ablegacy-smoke-dev';
|
||||
DECLARE @AreaId nvarchar(64) = 'ablegacy-smoke-area';
|
||||
DECLARE @LineId nvarchar(64) = 'ablegacy-smoke-line';
|
||||
DECLARE @EqId nvarchar(64) = 'ablegacy-smoke-eq';
|
||||
DECLARE @EqUuid uniqueidentifier = '5A1D2030-5A1D-4203-A5A1-D20305A1D203';
|
||||
DECLARE @TagId nvarchar(64) = 'ablegacy-smoke-tag-n7_5';
|
||||
DECLARE @EqUuid uniqueidentifier = 'AB1E9AC0-AB1E-45E0-9AB1-E9AC0AB1E9AC';
|
||||
DECLARE @Tag nvarchar(64) = 'ablegacy-smoke-tag-n7-5';
|
||||
DECLARE @RefId nvarchar(64) = 'ablegacy-smoke-ref-n7-5';
|
||||
|
||||
BEGIN TRAN;
|
||||
|
||||
DELETE FROM dbo.Tag WHERE TagId IN (@TagId);
|
||||
DELETE FROM dbo.Equipment WHERE EquipmentId = @EqId;
|
||||
DELETE FROM dbo.UnsLine WHERE UnsLineId = @LineId;
|
||||
DELETE FROM dbo.UnsArea WHERE UnsAreaId = @AreaId;
|
||||
DELETE FROM dbo.DriverInstance WHERE DriverInstanceId = @DrvId;
|
||||
DELETE FROM dbo.Namespace WHERE NamespaceId = @NsId;
|
||||
DELETE FROM dbo.ConfigGeneration WHERE ClusterId = @ClusterId;
|
||||
DELETE FROM dbo.ClusterNodeCredential WHERE NodeId = @NodeId;
|
||||
DELETE FROM dbo.ClusterNodeGenerationState WHERE NodeId = @NodeId;
|
||||
DELETE FROM dbo.ClusterNode WHERE NodeId = @NodeId;
|
||||
DELETE FROM dbo.ServerCluster WHERE ClusterId = @ClusterId;
|
||||
|
||||
DELETE FROM dbo.UnsTagReference WHERE UnsTagReferenceId = @RefId;
|
||||
DELETE FROM dbo.Tag WHERE TagId = @Tag;
|
||||
DELETE FROM dbo.Equipment WHERE EquipmentId = @EqId;
|
||||
DELETE FROM dbo.UnsLine WHERE UnsLineId = @LineId;
|
||||
DELETE FROM dbo.UnsArea WHERE UnsAreaId = @AreaId;
|
||||
DELETE FROM dbo.Device WHERE DeviceId = @DevId;
|
||||
DELETE FROM dbo.DriverInstance WHERE DriverInstanceId = @DrvId;
|
||||
DELETE FROM dbo.ClusterNodeCredential WHERE NodeId = @NodeId;
|
||||
DELETE FROM dbo.ClusterNode WHERE NodeId = @NodeId;
|
||||
DELETE FROM dbo.ServerCluster WHERE ClusterId = @ClusterId;
|
||||
DELETE FROM dbo.ClusterNodeCredential WHERE Kind = 'SqlLogin' AND Value = 'sa';
|
||||
|
||||
INSERT dbo.ServerCluster(ClusterId, Name, Enterprise, Site, NodeCount, RedundancyMode, Enabled, CreatedBy)
|
||||
VALUES (@ClusterId, 'AB Legacy Smoke', 'zb', 'lab', 1, 'None', 1, 'ablegacy-smoke');
|
||||
VALUES (@ClusterId, 'AbLegacy Smoke', 'zb', 'lab', 1, 'None', 1, 'ablegacy-smoke');
|
||||
|
||||
INSERT dbo.ClusterNode(NodeId, ClusterId, RedundancyRole, Host, OpcUaPort, DashboardPort,
|
||||
INSERT dbo.ClusterNode(NodeId, ClusterId, Host, OpcUaPort, DashboardPort,
|
||||
ApplicationUri, ServiceLevelBase, Enabled, CreatedBy)
|
||||
VALUES (@NodeId, @ClusterId, 'Primary', 'localhost', 4840, 15050,
|
||||
'urn:OtOpcUa:ablegacy-smoke-node', 200, 1, 'ablegacy-smoke');
|
||||
VALUES (@NodeId, @ClusterId, 'localhost', 4840, 15050, 'urn:OtOpcUa:ablegacy-smoke-node', 200, 1, 'ablegacy-smoke');
|
||||
|
||||
INSERT dbo.ClusterNodeCredential(NodeId, Kind, Value, Enabled, CreatedBy)
|
||||
VALUES (@NodeId, 'SqlLogin', 'sa', 1, 'ablegacy-smoke');
|
||||
|
||||
DECLARE @Gen bigint;
|
||||
INSERT dbo.ConfigGeneration(ClusterId, Status, CreatedBy)
|
||||
VALUES (@ClusterId, 'Draft', 'ablegacy-smoke');
|
||||
SET @Gen = SCOPE_IDENTITY();
|
||||
|
||||
INSERT dbo.Namespace(GenerationId, NamespaceId, ClusterId, Kind, NamespaceUri, Enabled)
|
||||
VALUES (@Gen, @NsId, @ClusterId, 'Equipment', 'urn:ablegacy-smoke:eq', 1);
|
||||
|
||||
INSERT dbo.UnsArea(GenerationId, UnsAreaId, ClusterId, Name)
|
||||
VALUES (@Gen, @AreaId, @ClusterId, 'lab-floor');
|
||||
|
||||
INSERT dbo.UnsLine(GenerationId, UnsLineId, UnsAreaId, Name)
|
||||
VALUES (@Gen, @LineId, @AreaId, 'ablegacy-line');
|
||||
|
||||
INSERT dbo.Equipment(GenerationId, EquipmentId, EquipmentUuid, DriverInstanceId, UnsLineId,
|
||||
Name, MachineCode, Enabled)
|
||||
VALUES (@Gen, @EqId, @EqUuid, @DrvId, @LineId, 'slc-sim', 'ablegacy-001', 1);
|
||||
|
||||
-- AB Legacy DriverInstance — SLC 500 target. Replace the placeholder gateway
|
||||
-- `192.168.1.10` with the real PLC / RSEmulate host before running.
|
||||
INSERT dbo.DriverInstance(GenerationId, DriverInstanceId, ClusterId, NamespaceId,
|
||||
Name, DriverType, DriverConfig, Enabled)
|
||||
VALUES (@Gen, @DrvId, @ClusterId, @NsId, 'ablegacy-smoke', 'AbLegacy', N'{
|
||||
"TimeoutMs": 2000,
|
||||
"Devices": [
|
||||
{
|
||||
"HostAddress": "ab://127.0.0.1:44818/1,0",
|
||||
"PlcFamily": "Slc500",
|
||||
"DeviceName": "slc-500"
|
||||
}
|
||||
],
|
||||
"Probe": { "Enabled": true, "IntervalMs": 5000, "TimeoutMs": 2000, "ProbeAddress": "S:0" },
|
||||
"Tags": [
|
||||
{
|
||||
"Name": "N7_5",
|
||||
"DeviceHostAddress": "ab://127.0.0.1:44818/1,0",
|
||||
"Address": "N7:5",
|
||||
"DataType": "Int",
|
||||
"Writable": true,
|
||||
"WriteIdempotent": true
|
||||
}
|
||||
]
|
||||
-- DriverConfig keeps protocol/channel settings; per-device HostAddress moves to DeviceConfig.
|
||||
INSERT dbo.DriverInstance(DriverInstanceId, ClusterId, RawFolderId, Name, DriverType, DriverConfig, Enabled)
|
||||
VALUES (@DrvId, @ClusterId, NULL, 'slc-smoke', 'AbLegacy', N'{
|
||||
"ProbeTimeoutSeconds": 5,
|
||||
"Probe": { "Enabled": true, "IntervalMs": 5000, "TimeoutMs": 2000, "ProbeAddress": "S:0" }
|
||||
}', 1);
|
||||
|
||||
INSERT dbo.Tag(GenerationId, TagId, DriverInstanceId, EquipmentId, Name, DataType,
|
||||
AccessLevel, TagConfig, WriteIdempotent)
|
||||
VALUES (@Gen, @TagId, @DrvId, @EqId, 'N7_5', 'Int16', 'ReadWrite',
|
||||
N'{"FullName":"N7_5","Address":"N7:5","DataType":"Int"}', 1);
|
||||
-- DeviceConfig shape finalized against reworked options in Wave B (WP5).
|
||||
INSERT dbo.Device(DeviceId, DriverInstanceId, Name, Enabled, DeviceConfig)
|
||||
VALUES (@DevId, @DrvId, 'plc', 1, N'{ "HostAddress": "ab://127.0.0.1:44818/1,0" }');
|
||||
|
||||
EXEC dbo.sp_PublishGeneration @ClusterId = @ClusterId, @DraftGenerationId = @Gen,
|
||||
@Notes = N'AB Legacy smoke — task #213';
|
||||
INSERT dbo.Tag(TagId, DeviceId, TagGroupId, Name, DataType, AccessLevel, WriteIdempotent, TagConfig)
|
||||
VALUES (@Tag, @DevId, NULL, 'N7_5', 'Int', 'ReadWrite', 1,
|
||||
N'{ "address": "N7:5", "dataType": "Int" }');
|
||||
|
||||
INSERT dbo.UnsArea(UnsAreaId, ClusterId, Name) VALUES (@AreaId, @ClusterId, 'lab-floor');
|
||||
INSERT dbo.UnsLine(UnsLineId, UnsAreaId, Name) VALUES (@LineId, @AreaId, 'ablegacy-line');
|
||||
INSERT dbo.Equipment(EquipmentId, EquipmentUuid, UnsLineId, Name, MachineCode, Enabled)
|
||||
VALUES (@EqId, @EqUuid, @LineId, 'slc-sim', 'ablegacy-001', 1);
|
||||
|
||||
INSERT dbo.UnsTagReference(UnsTagReferenceId, EquipmentId, TagId, SortOrder)
|
||||
VALUES (@RefId, @EqId, @Tag, 0);
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRINT '';
|
||||
PRINT 'AB Legacy smoke seed complete.';
|
||||
PRINT ' Cluster: ' + @ClusterId;
|
||||
PRINT ' Node: ' + @NodeId;
|
||||
PRINT ' Generation: ' + CONVERT(nvarchar(20), @Gen);
|
||||
PRINT '';
|
||||
PRINT 'NOTE: default points at the ab_server slc500 Docker fixture with a /1,0';
|
||||
PRINT ' cip-path (required by ab_server). For real SLC/MicroLogix/PLC-5';
|
||||
PRINT ' hardware, edit the DriverConfig HostAddress to end with /<empty>';
|
||||
PRINT ' e.g. "ab://<plc-ip>:44818/" and re-run this seed.';
|
||||
PRINT 'AbLegacy smoke seed complete (v3 schema). Cluster: ' + @ClusterId + ' Node: ' + @NodeId;
|
||||
|
||||
@@ -1,156 +1,101 @@
|
||||
-- Modbus e2e smoke seed — closes #210 (umbrella #209).
|
||||
-- Modbus e2e smoke seed — v3 schema (Raw tree: DriverInstance → Device → Tag; UNS by reference).
|
||||
--
|
||||
-- Idempotent — DROP-and-recreate of one cluster's worth of Modbus test config:
|
||||
-- * 1 ServerCluster ('modbus-smoke') + ClusterNode ('modbus-smoke-node')
|
||||
-- * 1 ConfigGeneration (Draft → Published at the end)
|
||||
-- * 1 Namespace + UnsArea + UnsLine + Equipment
|
||||
-- * 1 Modbus DriverInstance pointing at the pymodbus standard fixture
|
||||
-- (127.0.0.1:5020 per tests/.../Modbus.IntegrationTests/Docker)
|
||||
-- * 1 Tag at HR[200]:UInt16 (HR[100] is auto-increment in standard.json,
|
||||
-- Idempotent — DELETE-and-recreate of one cluster's worth of Modbus test config:
|
||||
-- * 1 ServerCluster ('modbus-smoke') + ClusterNode ('modbus-smoke-node') + SqlLogin credential
|
||||
-- * 1 Modbus DriverInstance at the cluster root (RawFolderId NULL)
|
||||
-- * 1 Device carrying the pymodbus endpoint in DeviceConfig
|
||||
-- * 1 Tag at HR[200]:UInt16 under the device (HR[100] is auto-increment in standard.json,
|
||||
-- unusable as a write target — the e2e script uses HR[200] for that reason)
|
||||
-- * 1 UnsArea/UnsLine/Equipment + a UnsTagReference projecting the raw tag into UNS
|
||||
--
|
||||
-- v3 changes vs. the pre-v2 original: the ConfigGeneration/GenerationId/Namespace model and the
|
||||
-- sp_PublishGeneration publish step are gone (deploy is a C# ControlPlane operation now); the
|
||||
-- connection endpoint moved from DriverConfig into the Device's DeviceConfig.
|
||||
--
|
||||
-- Usage:
|
||||
-- sqlcmd -S "localhost,14330" -d OtOpcUaConfig -U sa -P "OtOpcUaDev_2026!" \
|
||||
-- -i scripts/smoke/seed-modbus-smoke.sql
|
||||
--
|
||||
-- After seeding, update src/.../Server/appsettings.json:
|
||||
-- Node:NodeId = "modbus-smoke-node"
|
||||
-- Node:ClusterId = "modbus-smoke"
|
||||
--
|
||||
-- Then start the simulator + server + run the e2e script:
|
||||
-- docker compose -f tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Docker/docker-compose.yml --profile standard up -d
|
||||
-- dotnet run --project src/Server/ZB.MOM.WW.OtOpcUa.Server
|
||||
-- ./scripts/e2e/test-modbus.ps1 -BridgeNodeId "ns=2;s=HR200"
|
||||
-- sqlcmd -S "10.100.0.35,14330" -d <cfgdb> -U sa -P "<pw>" -i scripts/smoke/seed-modbus-smoke.sql
|
||||
|
||||
SET NOCOUNT ON;
|
||||
SET XACT_ABORT ON;
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
SET ANSI_NULLS ON;
|
||||
SET ANSI_PADDING ON;
|
||||
SET ANSI_WARNINGS ON;
|
||||
SET ARITHABORT ON;
|
||||
SET CONCAT_NULL_YIELDS_NULL ON;
|
||||
|
||||
DECLARE @ClusterId nvarchar(64) = 'modbus-smoke';
|
||||
DECLARE @NodeId nvarchar(64) = 'modbus-smoke-node';
|
||||
DECLARE @DrvId nvarchar(64) = 'modbus-smoke-drv';
|
||||
DECLARE @NsId nvarchar(64) = 'modbus-smoke-ns';
|
||||
DECLARE @DevId nvarchar(64) = 'modbus-smoke-dev';
|
||||
DECLARE @AreaId nvarchar(64) = 'modbus-smoke-area';
|
||||
DECLARE @LineId nvarchar(64) = 'modbus-smoke-line';
|
||||
DECLARE @EqId nvarchar(64) = 'modbus-smoke-eq';
|
||||
DECLARE @EqUuid uniqueidentifier = '72BD5A10-72BD-45A1-B72B-D5A1072BD5A1';
|
||||
DECLARE @TagHr200 nvarchar(64) = 'modbus-smoke-tag-hr200';
|
||||
DECLARE @RefId nvarchar(64) = 'modbus-smoke-ref-hr200';
|
||||
|
||||
BEGIN TRAN;
|
||||
|
||||
-- Clean prior smoke state (child rows first).
|
||||
DELETE FROM dbo.Tag WHERE TagId IN (@TagHr200);
|
||||
DELETE FROM dbo.Equipment WHERE EquipmentId = @EqId;
|
||||
DELETE FROM dbo.UnsLine WHERE UnsLineId = @LineId;
|
||||
DELETE FROM dbo.UnsArea WHERE UnsAreaId = @AreaId;
|
||||
DELETE FROM dbo.DriverInstance WHERE DriverInstanceId = @DrvId;
|
||||
DELETE FROM dbo.Namespace WHERE NamespaceId = @NsId;
|
||||
DELETE FROM dbo.ConfigGeneration WHERE ClusterId = @ClusterId;
|
||||
DELETE FROM dbo.ClusterNodeCredential WHERE NodeId = @NodeId;
|
||||
DELETE FROM dbo.ClusterNodeGenerationState WHERE NodeId = @NodeId;
|
||||
DELETE FROM dbo.ClusterNode WHERE NodeId = @NodeId;
|
||||
DELETE FROM dbo.ServerCluster WHERE ClusterId = @ClusterId;
|
||||
DELETE FROM dbo.UnsTagReference WHERE UnsTagReferenceId = @RefId;
|
||||
DELETE FROM dbo.Tag WHERE TagId = @TagHr200;
|
||||
DELETE FROM dbo.Equipment WHERE EquipmentId = @EqId;
|
||||
DELETE FROM dbo.UnsLine WHERE UnsLineId = @LineId;
|
||||
DELETE FROM dbo.UnsArea WHERE UnsAreaId = @AreaId;
|
||||
DELETE FROM dbo.Device WHERE DeviceId = @DevId;
|
||||
DELETE FROM dbo.DriverInstance WHERE DriverInstanceId = @DrvId;
|
||||
DELETE FROM dbo.ClusterNodeCredential WHERE NodeId = @NodeId;
|
||||
DELETE FROM dbo.ClusterNode WHERE NodeId = @NodeId;
|
||||
DELETE FROM dbo.ServerCluster WHERE ClusterId = @ClusterId;
|
||||
|
||||
-- `UX_ClusterNodeCredential_Value` is a unique index on (Kind, Value) WHERE
|
||||
-- Enabled=1, so a `sa` login can only bind to one node at a time. Drop any
|
||||
-- prior smoke cluster's binding before we claim the login for this one.
|
||||
-- `UX_ClusterNodeCredential_Value` is a unique index on (Kind, Value) WHERE Enabled=1, so the
|
||||
-- `sa` login can only bind to one node at a time. Drop any prior smoke binding first.
|
||||
DELETE FROM dbo.ClusterNodeCredential WHERE Kind = 'SqlLogin' AND Value = 'sa';
|
||||
|
||||
-- 1. Cluster + Node.
|
||||
-- 1. Cluster + Node + credential binding.
|
||||
INSERT dbo.ServerCluster(ClusterId, Name, Enterprise, Site, NodeCount, RedundancyMode, Enabled, CreatedBy)
|
||||
VALUES (@ClusterId, 'Modbus Smoke', 'zb', 'lab', 1, 'None', 1, 'modbus-smoke');
|
||||
|
||||
-- DashboardPort 15050 rather than 5000 — HttpListener on :5000 requires
|
||||
-- URL-ACL reservation or admin rights on Windows (HttpListenerException 32).
|
||||
-- 15000+ ports are unreserved by default. Safe to change back when deploying
|
||||
-- with a netsh urlacl grant or reverse-proxy fronting :5000.
|
||||
INSERT dbo.ClusterNode(NodeId, ClusterId, RedundancyRole, Host, OpcUaPort, DashboardPort,
|
||||
INSERT dbo.ClusterNode(NodeId, ClusterId, Host, OpcUaPort, DashboardPort,
|
||||
ApplicationUri, ServiceLevelBase, Enabled, CreatedBy)
|
||||
VALUES (@NodeId, @ClusterId, 'Primary', 'localhost', 4840, 15050,
|
||||
VALUES (@NodeId, @ClusterId, 'localhost', 4840, 15050,
|
||||
'urn:OtOpcUa:modbus-smoke-node', 200, 1, 'modbus-smoke');
|
||||
|
||||
-- Bind the SQL login this smoke test connects as to the node identity. The
|
||||
-- sp_GetCurrentGenerationForCluster + sp_UpdateClusterNodeGenerationState
|
||||
-- sprocs raise RAISERROR('Unauthorized: caller %s is not bound to NodeId %s')
|
||||
-- when this row is missing. `Kind='SqlLogin'` / `Value='sa'` matches the
|
||||
-- container's SA user; rotate Value for real deployments using a non-SA login.
|
||||
INSERT dbo.ClusterNodeCredential(NodeId, Kind, Value, Enabled, CreatedBy)
|
||||
VALUES (@NodeId, 'SqlLogin', 'sa', 1, 'modbus-smoke');
|
||||
|
||||
-- 2. Draft generation.
|
||||
DECLARE @Gen bigint;
|
||||
INSERT dbo.ConfigGeneration(ClusterId, Status, CreatedBy)
|
||||
VALUES (@ClusterId, 'Draft', 'modbus-smoke');
|
||||
SET @Gen = SCOPE_IDENTITY();
|
||||
|
||||
-- 3. Namespace.
|
||||
INSERT dbo.Namespace(GenerationId, NamespaceId, ClusterId, Kind, NamespaceUri, Enabled)
|
||||
VALUES (@Gen, @NsId, @ClusterId, 'Equipment', 'urn:modbus-smoke:eq', 1);
|
||||
|
||||
-- 4. UNS hierarchy.
|
||||
INSERT dbo.UnsArea(GenerationId, UnsAreaId, ClusterId, Name)
|
||||
VALUES (@Gen, @AreaId, @ClusterId, 'lab-floor');
|
||||
|
||||
INSERT dbo.UnsLine(GenerationId, UnsLineId, UnsAreaId, Name)
|
||||
VALUES (@Gen, @LineId, @AreaId, 'modbus-line');
|
||||
|
||||
INSERT dbo.Equipment(GenerationId, EquipmentId, EquipmentUuid, DriverInstanceId, UnsLineId,
|
||||
Name, MachineCode, Enabled)
|
||||
VALUES (@Gen, @EqId, @EqUuid, @DrvId, @LineId, 'modbus-sim', 'modbus-001', 1);
|
||||
|
||||
-- 5. Modbus DriverInstance. DriverConfig mirrors ModbusDriverConfigDto
|
||||
-- (mapped to ModbusDriverOptions by ModbusDriverFactoryExtensions).
|
||||
INSERT dbo.DriverInstance(GenerationId, DriverInstanceId, ClusterId, NamespaceId,
|
||||
Name, DriverType, DriverConfig, Enabled)
|
||||
VALUES (@Gen, @DrvId, @ClusterId, @NsId, 'pymodbus-smoke', 'Modbus', N'{
|
||||
"Host": "127.0.0.1",
|
||||
"Port": 5020,
|
||||
"UnitId": 1,
|
||||
-- 2. Modbus DriverInstance at the cluster root (RawFolderId NULL). DriverConfig keeps protocol/
|
||||
-- channel settings only — the endpoint (Host/Port/UnitId) now lives in the Device's DeviceConfig.
|
||||
INSERT dbo.DriverInstance(DriverInstanceId, ClusterId, RawFolderId, Name, DriverType, DriverConfig, Enabled)
|
||||
VALUES (@DrvId, @ClusterId, NULL, 'pymodbus-smoke', 'Modbus', N'{
|
||||
"TimeoutMs": 2000,
|
||||
"AutoReconnect": true,
|
||||
"Probe": { "Enabled": true, "IntervalMs": 5000, "TimeoutMs": 2000, "ProbeAddress": 0 },
|
||||
"Tags": [
|
||||
{
|
||||
"Name": "HR200",
|
||||
"Region": "HoldingRegisters",
|
||||
"Address": 200,
|
||||
"DataType": "UInt16",
|
||||
"Writable": true,
|
||||
"WriteIdempotent": true
|
||||
}
|
||||
]
|
||||
"Probe": { "Enabled": true, "IntervalMs": 5000, "TimeoutMs": 2000, "ProbeAddress": 0 }
|
||||
}', 1);
|
||||
|
||||
-- 6. Tag row bound to the Equipment. Driver reports the same tag via
|
||||
-- DiscoverAsync + the walker maps the UnsArea/Line/Equipment/Tag path to the
|
||||
-- driver's folder/variable (NodeId ends up ns=<driver-ns>;s=HR200 per
|
||||
-- ModbusDriver.DiscoverAsync using FullName = tag.Name).
|
||||
INSERT dbo.Tag(GenerationId, TagId, DriverInstanceId, EquipmentId, Name, DataType,
|
||||
AccessLevel, TagConfig, WriteIdempotent)
|
||||
VALUES (@Gen, @TagHr200, @DrvId, @EqId, 'HR200', 'UInt16', 'ReadWrite',
|
||||
N'{"FullName":"HR200","DataType":"UInt16"}', 1);
|
||||
-- 3. Device — endpoint. DeviceConfig shape finalized against reworked options in Wave B (WP5).
|
||||
INSERT dbo.Device(DeviceId, DriverInstanceId, Name, Enabled, DeviceConfig)
|
||||
VALUES (@DevId, @DrvId, 'plc', 1, N'{ "Host": "127.0.0.1", "Port": 5020, "UnitId": 1 }');
|
||||
|
||||
-- 7. Publish the generation — flips Status Draft → Published, merges
|
||||
-- ExternalIdReservation, claims cluster write lock.
|
||||
EXEC dbo.sp_PublishGeneration @ClusterId = @ClusterId, @DraftGenerationId = @Gen,
|
||||
@Notes = N'Modbus smoke — task #210';
|
||||
-- 4. Raw tag under the device (identity = RawPath; TagConfig carries the driver address only).
|
||||
INSERT dbo.Tag(TagId, DeviceId, TagGroupId, Name, DataType, AccessLevel, WriteIdempotent, TagConfig)
|
||||
VALUES (@TagHr200, @DevId, NULL, 'HR200', 'UInt16', 'ReadWrite', 1,
|
||||
N'{ "region": "HoldingRegisters", "address": 200, "dataType": "UInt16", "writable": true }');
|
||||
|
||||
-- 5. UNS hierarchy + a reference projecting the raw tag into the equipment.
|
||||
INSERT dbo.UnsArea(UnsAreaId, ClusterId, Name) VALUES (@AreaId, @ClusterId, 'lab-floor');
|
||||
INSERT dbo.UnsLine(UnsLineId, UnsAreaId, Name) VALUES (@LineId, @AreaId, 'modbus-line');
|
||||
INSERT dbo.Equipment(EquipmentId, EquipmentUuid, UnsLineId, Name, MachineCode, Enabled)
|
||||
VALUES (@EqId, @EqUuid, @LineId, 'modbus-sim', 'modbus-001', 1);
|
||||
|
||||
INSERT dbo.UnsTagReference(UnsTagReferenceId, EquipmentId, TagId, SortOrder)
|
||||
VALUES (@RefId, @EqId, @TagHr200, 0);
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRINT '';
|
||||
PRINT 'Modbus smoke seed complete.';
|
||||
PRINT ' Cluster: ' + @ClusterId;
|
||||
PRINT ' Node: ' + @NodeId;
|
||||
PRINT ' Generation: ' + CONVERT(nvarchar(20), @Gen);
|
||||
PRINT 'Modbus smoke seed complete (v3 schema).';
|
||||
PRINT ' Cluster: ' + @ClusterId + ' Node: ' + @NodeId;
|
||||
PRINT '';
|
||||
PRINT 'Next steps:';
|
||||
PRINT ' 1. Set src/.../Server/appsettings.json Node:NodeId = "modbus-smoke-node"';
|
||||
PRINT ' Node:ClusterId = "modbus-smoke"';
|
||||
PRINT ' 1. Set Server appsettings Node:NodeId = "modbus-smoke-node" / Node:ClusterId = "modbus-smoke"';
|
||||
PRINT ' 2. docker compose -f tests/.../Modbus.IntegrationTests/Docker/docker-compose.yml --profile standard up -d';
|
||||
PRINT ' 3. dotnet run --project src/Server/ZB.MOM.WW.OtOpcUa.Server';
|
||||
PRINT ' 4. ./scripts/e2e/test-modbus.ps1 -BridgeNodeId "ns=2;s=HR200"';
|
||||
PRINT ' 4. Deploy the config via the AdminUI/ControlPlane, then ./scripts/e2e/test-modbus.ps1';
|
||||
|
||||
@@ -1,186 +1,120 @@
|
||||
-- Phase 7 live OPC UA E2E smoke seed (task #240).
|
||||
-- Phase 7 live OPC UA E2E smoke seed — v3 schema (Raw tree + UNS-by-reference + scripting).
|
||||
--
|
||||
-- Idempotent — DROP-and-recreate of one cluster's worth of test config:
|
||||
-- * 1 ServerCluster ('p7-smoke')
|
||||
-- * 1 ClusterNode ('p7-smoke-node')
|
||||
-- * 1 ConfigGeneration (created Draft, then flipped to Published at the end)
|
||||
-- * 1 Namespace (Equipment kind)
|
||||
-- * 1 UnsArea / UnsLine / Equipment / Tag — Tag bound to a real Galaxy attribute
|
||||
-- * 1 DriverInstance (Galaxy)
|
||||
-- * 1 Script + 1 VirtualTag using it
|
||||
-- * 1 Script + 1 ScriptedAlarm using it
|
||||
-- Idempotent DELETE-and-recreate of one cluster's worth of test config:
|
||||
-- * 1 ServerCluster ('p7-smoke') + ClusterNode + SqlLogin credential
|
||||
-- * 1 Galaxy (GalaxyMxGateway) DriverInstance at the cluster root + 1 Device (gateway endpoint)
|
||||
-- * 1 raw Tag bound to a Galaxy attribute (identity = RawPath; address in TagConfig.attributeRef)
|
||||
-- * 1 UnsArea / UnsLine / Equipment + a UnsTagReference projecting the raw tag into UNS
|
||||
-- * 1 Script + 1 VirtualTag using it; 1 Script + 1 ScriptedAlarm using it
|
||||
--
|
||||
-- Drop & re-create deletes ALL rows scoped to the cluster (in dependency order)
|
||||
-- so re-running this script after a code change starts from a clean state.
|
||||
-- Table-level CHECK constraints are validated on insert; if a constraint is
|
||||
-- violated this script aborts with the offending row's column.
|
||||
--
|
||||
-- Usage:
|
||||
-- sqlcmd -S "localhost,14330" -d OtOpcUaConfig -U sa -P "OtOpcUaDev_2026!" \
|
||||
-- -i scripts/smoke/seed-phase-7-smoke.sql
|
||||
-- v3 changes: ConfigGeneration/GenerationId/Namespace model + sp_PublishGeneration are gone (deploy
|
||||
-- is a C# ControlPlane operation); Galaxy driver type is 'GalaxyMxGateway'; the gateway endpoint
|
||||
-- lives in the Device's DeviceConfig; ctx.GetTag / alarm-message tokens take a RawPath. The exact
|
||||
-- Galaxy TagConfig key ('attributeRef') + scripting RawPath contract are finalized in WP2/WP3/WP4.
|
||||
|
||||
SET NOCOUNT ON;
|
||||
SET XACT_ABORT ON;
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
SET ANSI_NULLS ON;
|
||||
SET ANSI_PADDING ON;
|
||||
SET ANSI_WARNINGS ON;
|
||||
SET ARITHABORT ON;
|
||||
SET CONCAT_NULL_YIELDS_NULL ON;
|
||||
|
||||
DECLARE @ClusterId nvarchar(64) = 'p7-smoke';
|
||||
DECLARE @NodeId nvarchar(64) = 'p7-smoke-node';
|
||||
DECLARE @DrvId nvarchar(64) = 'p7-smoke-galaxy';
|
||||
DECLARE @NsId nvarchar(64) = 'p7-smoke-ns';
|
||||
DECLARE @DevId nvarchar(64) = 'p7-smoke-dev';
|
||||
DECLARE @AreaId nvarchar(64) = 'p7-smoke-area';
|
||||
DECLARE @LineId nvarchar(64) = 'p7-smoke-line';
|
||||
DECLARE @EqId nvarchar(64) = 'p7-smoke-eq';
|
||||
DECLARE @EqUuid uniqueidentifier = '5B2CF10D-5B2C-4F10-B5B2-CF10D5B2CF10';
|
||||
DECLARE @TagId nvarchar(64) = 'p7-smoke-tag-source';
|
||||
DECLARE @RefId nvarchar(64) = 'p7-smoke-ref-source';
|
||||
DECLARE @VtScript nvarchar(64) = 'p7-smoke-script-vt';
|
||||
DECLARE @AlScript nvarchar(64) = 'p7-smoke-script-al';
|
||||
DECLARE @VtId nvarchar(64) = 'p7-smoke-vt-derived';
|
||||
DECLARE @AlId nvarchar(64) = 'p7-smoke-al-overtemp';
|
||||
|
||||
-- RawPath of the source tag: <driver>/<device>/<tag> (cluster root, no folder/group).
|
||||
DECLARE @RawPath nvarchar(256) = N'galaxy-smoke/plc/Source';
|
||||
|
||||
BEGIN TRAN;
|
||||
|
||||
-- Wipe any prior smoke state. Order matters: child rows first.
|
||||
DELETE s FROM dbo.ScriptedAlarmState s
|
||||
WHERE s.ScriptedAlarmId = @AlId;
|
||||
DELETE FROM dbo.ScriptedAlarm WHERE ScriptedAlarmId = @AlId;
|
||||
DELETE FROM dbo.VirtualTag WHERE VirtualTagId = @VtId;
|
||||
DELETE FROM dbo.Script WHERE ScriptId IN (@VtScript, @AlScript);
|
||||
DELETE FROM dbo.Tag WHERE TagId = @TagId;
|
||||
DELETE FROM dbo.Equipment WHERE EquipmentId = @EqId;
|
||||
DELETE FROM dbo.UnsLine WHERE UnsLineId = @LineId;
|
||||
DELETE FROM dbo.UnsArea WHERE UnsAreaId = @AreaId;
|
||||
DELETE FROM dbo.DriverInstance WHERE DriverInstanceId = @DrvId;
|
||||
DELETE FROM dbo.Namespace WHERE NamespaceId = @NsId;
|
||||
DELETE FROM dbo.ConfigGeneration WHERE ClusterId = @ClusterId;
|
||||
-- Wipe prior smoke state. Order matters: child rows first.
|
||||
DELETE FROM dbo.ScriptedAlarmState WHERE ScriptedAlarmId = @AlId;
|
||||
DELETE FROM dbo.ScriptedAlarm WHERE ScriptedAlarmId = @AlId;
|
||||
DELETE FROM dbo.VirtualTag WHERE VirtualTagId = @VtId;
|
||||
DELETE FROM dbo.Script WHERE ScriptId IN (@VtScript, @AlScript);
|
||||
DELETE FROM dbo.UnsTagReference WHERE UnsTagReferenceId = @RefId;
|
||||
DELETE FROM dbo.Tag WHERE TagId = @TagId;
|
||||
DELETE FROM dbo.Equipment WHERE EquipmentId = @EqId;
|
||||
DELETE FROM dbo.UnsLine WHERE UnsLineId = @LineId;
|
||||
DELETE FROM dbo.UnsArea WHERE UnsAreaId = @AreaId;
|
||||
DELETE FROM dbo.Device WHERE DeviceId = @DevId;
|
||||
DELETE FROM dbo.DriverInstance WHERE DriverInstanceId = @DrvId;
|
||||
DELETE FROM dbo.ClusterNodeCredential WHERE NodeId = @NodeId;
|
||||
DELETE FROM dbo.ClusterNodeGenerationState WHERE NodeId = @NodeId;
|
||||
DELETE FROM dbo.ClusterNode WHERE NodeId = @NodeId;
|
||||
DELETE FROM dbo.ServerCluster WHERE ClusterId = @ClusterId;
|
||||
DELETE FROM dbo.ClusterNode WHERE NodeId = @NodeId;
|
||||
DELETE FROM dbo.ServerCluster WHERE ClusterId = @ClusterId;
|
||||
DELETE FROM dbo.ClusterNodeCredential WHERE Kind = 'SqlLogin' AND Value = 'sa';
|
||||
|
||||
-- 1. Cluster + Node
|
||||
-- 1. Cluster + Node + credential binding.
|
||||
INSERT dbo.ServerCluster(ClusterId, Name, Enterprise, Site, NodeCount, RedundancyMode, Enabled, CreatedBy)
|
||||
VALUES (@ClusterId, 'P7 Smoke', 'zb', 'lab', 1, 'None', 1, 'p7-smoke');
|
||||
|
||||
INSERT dbo.ClusterNode(NodeId, ClusterId, RedundancyRole, Host, OpcUaPort, DashboardPort,
|
||||
INSERT dbo.ClusterNode(NodeId, ClusterId, Host, OpcUaPort, DashboardPort,
|
||||
ApplicationUri, ServiceLevelBase, Enabled, CreatedBy)
|
||||
VALUES (@NodeId, @ClusterId, 'Primary', 'localhost', 4840, 5000,
|
||||
'urn:OtOpcUa:p7-smoke-node', 200, 1, 'p7-smoke');
|
||||
VALUES (@NodeId, @ClusterId, 'localhost', 4840, 5000, 'urn:OtOpcUa:p7-smoke-node', 200, 1, 'p7-smoke');
|
||||
|
||||
-- sp_GetCurrentGenerationForCluster gates access by SUSER_SNAME() against
|
||||
-- ClusterNodeCredential; without this binding the Server bootstrap fails with
|
||||
-- `Unauthorized: caller sa is not bound to NodeId p7-smoke-node`. Dev Docker
|
||||
-- SQL runs with `sa`; production deploys would rotate to a per-node login.
|
||||
INSERT dbo.ClusterNodeCredential(NodeId, Kind, Value, Enabled, CreatedBy)
|
||||
VALUES (@NodeId, N'SqlLogin', N'sa', 1, N'p7-smoke');
|
||||
|
||||
-- 2. Generation (created Draft, flipped to Published at the end so insert order
|
||||
-- constraints (one Draft per cluster, etc.) don't fight us).
|
||||
DECLARE @Gen bigint;
|
||||
INSERT dbo.ConfigGeneration(ClusterId, Status, CreatedBy)
|
||||
VALUES (@ClusterId, 'Draft', 'p7-smoke');
|
||||
SET @Gen = SCOPE_IDENTITY();
|
||||
|
||||
-- 3. Namespace
|
||||
INSERT dbo.Namespace(GenerationId, NamespaceId, ClusterId, Kind, NamespaceUri, Enabled)
|
||||
VALUES (@Gen, @NsId, @ClusterId, 'Equipment', 'urn:p7-smoke:eq', 1);
|
||||
|
||||
-- 4. UNS hierarchy
|
||||
INSERT dbo.UnsArea(GenerationId, UnsAreaId, ClusterId, Name)
|
||||
VALUES (@Gen, @AreaId, @ClusterId, 'lab-floor');
|
||||
|
||||
INSERT dbo.UnsLine(GenerationId, UnsLineId, UnsAreaId, Name)
|
||||
VALUES (@Gen, @LineId, @AreaId, 'galaxy-line');
|
||||
|
||||
INSERT dbo.Equipment(GenerationId, EquipmentId, EquipmentUuid, DriverInstanceId, UnsLineId,
|
||||
Name, MachineCode, Enabled)
|
||||
VALUES (@Gen, @EqId, @EqUuid, @DrvId, @LineId, 'reactor-1', 'p7-rx-001', 1);
|
||||
|
||||
-- 5. Driver — Galaxy proxy. DriverConfig JSON tells the proxy how to reach the
|
||||
-- already-running OtOpcUaGalaxyHost. Secret + pipe name match
|
||||
-- .local/galaxy-host-secret.txt + the OtOpcUaGalaxyHost service env.
|
||||
INSERT dbo.DriverInstance(GenerationId, DriverInstanceId, ClusterId, NamespaceId,
|
||||
Name, DriverType, DriverConfig, Enabled)
|
||||
VALUES (@Gen, @DrvId, @ClusterId, @NsId, 'galaxy-smoke', 'Galaxy', N'{
|
||||
"DriverInstanceId": "p7-smoke-galaxy",
|
||||
"PipeName": "OtOpcUaGalaxy",
|
||||
"SharedSecret": "4hgDJ4jLcKXmOmD1Ara8xtE8N3R47Q2y1Xf/Eama/Fk=",
|
||||
-- 2. Galaxy driver at the cluster root. DriverConfig keeps non-endpoint settings; the mxaccessgw
|
||||
-- gateway endpoint lives in the Device's DeviceConfig (shape finalized in Wave B / WP5).
|
||||
INSERT dbo.DriverInstance(DriverInstanceId, ClusterId, RawFolderId, Name, DriverType, DriverConfig, Enabled)
|
||||
VALUES (@DrvId, @ClusterId, NULL, 'galaxy-smoke', 'GalaxyMxGateway', N'{
|
||||
"ConnectTimeoutMs": 10000
|
||||
}', 1);
|
||||
|
||||
-- 6. One driver-sourced Tag bound to the Equipment. TagConfig is the Galaxy
|
||||
-- fullRef; the EquipmentNodeWalker reads the JSON's FullName field and
|
||||
-- hands that string to the Galaxy driver as the MXAccess reference.
|
||||
-- Default points at TestMachine_001.TestHistoryValue — the live dev-box
|
||||
-- Galaxy ships it as Int32 + writable (security_classification=Operate)
|
||||
-- + historized (HistoryExtension primitive), so the E2E script can
|
||||
-- exercise read + write + subscribe + alarm + history against the
|
||||
-- same attribute. Swap to any other writable historized attribute on
|
||||
-- this Galaxy by re-running `gr/queries/attributes_extended.sql` with
|
||||
-- `WHERE is_historized=1 AND security_classification > 0`.
|
||||
INSERT dbo.Tag(GenerationId, TagId, DriverInstanceId, EquipmentId, Name, DataType,
|
||||
AccessLevel, TagConfig, WriteIdempotent)
|
||||
VALUES (@Gen, @TagId, @DrvId, @EqId, 'Source', 'Int32', 'ReadWrite',
|
||||
N'{"FullName":"TestMachine_001.TestHistoryValue","DataType":"Int32"}', 0);
|
||||
INSERT dbo.Device(DeviceId, DriverInstanceId, Name, Enabled, DeviceConfig)
|
||||
VALUES (@DevId, @DrvId, 'plc', 1, N'{ "GatewayAddress": "http://127.0.0.1:5001", "ClientName": "p7-smoke" }');
|
||||
|
||||
-- 7. Scripts (SourceHash is SHA-256 of SourceCode, computed externally — using
|
||||
-- a placeholder here; the engine recomputes on first use anyway).
|
||||
--
|
||||
-- MachineStatus predicate — the dynamic "is the machine running?" status
|
||||
-- derived from the raw Source value. Boolean: true when Source > 0. Matches
|
||||
-- the shape Aveva operators typically want on a machine-status tile (green
|
||||
-- when above zero, otherwise grey) without needing a separate threshold
|
||||
-- attribute. Rename + re-threshold here to mirror site semantics.
|
||||
INSERT dbo.Script(GenerationId, ScriptId, Name, SourceCode, SourceHash, Language)
|
||||
-- 3. Raw tag bound to a Galaxy attribute. Identity is its RawPath; the MXAccess reference lives in
|
||||
-- TagConfig.attributeRef (v3 Galaxy address key; finalized in WP2/WP3).
|
||||
INSERT dbo.Tag(TagId, DeviceId, TagGroupId, Name, DataType, AccessLevel, WriteIdempotent, TagConfig)
|
||||
VALUES (@TagId, @DevId, NULL, 'Source', 'Int32', 'ReadWrite', 0,
|
||||
N'{ "attributeRef": "TestMachine_001.TestHistoryValue", "dataType": "Int32" }');
|
||||
|
||||
-- 4. UNS hierarchy + reference into the equipment.
|
||||
INSERT dbo.UnsArea(UnsAreaId, ClusterId, Name) VALUES (@AreaId, @ClusterId, 'lab-floor');
|
||||
INSERT dbo.UnsLine(UnsLineId, UnsAreaId, Name) VALUES (@LineId, @AreaId, 'galaxy-line');
|
||||
INSERT dbo.Equipment(EquipmentId, EquipmentUuid, UnsLineId, Name, MachineCode, Enabled)
|
||||
VALUES (@EqId, @EqUuid, @LineId, 'reactor-1', 'p7-rx-001', 1);
|
||||
|
||||
INSERT dbo.UnsTagReference(UnsTagReferenceId, EquipmentId, TagId, SortOrder)
|
||||
VALUES (@RefId, @EqId, @TagId, 0);
|
||||
|
||||
-- 5. Scripts. ctx.GetTag takes a RawPath in v3 (SourceHash placeholder; the engine recomputes).
|
||||
INSERT dbo.Script(ScriptId, Name, SourceCode, SourceHash, Language)
|
||||
VALUES
|
||||
(@Gen, @VtScript, 'machine-status-predicate',
|
||||
N'return System.Convert.ToInt32(ctx.GetTag("/lab-floor/galaxy-line/reactor-1/Source").Value) > 0;',
|
||||
(@VtScript, 'machine-status-predicate',
|
||||
N'return System.Convert.ToInt32(ctx.GetTag("' + @RawPath + N'").Value) > 0;',
|
||||
'0000000000000000000000000000000000000000000000000000000000000000', 'CSharp'),
|
||||
(@Gen, @AlScript, 'overtemp-predicate',
|
||||
N'return System.Convert.ToInt32(ctx.GetTag("/lab-floor/galaxy-line/reactor-1/Source").Value) > 50;',
|
||||
(@AlScript, 'overtemp-predicate',
|
||||
N'return System.Convert.ToInt32(ctx.GetTag("' + @RawPath + N'").Value) > 50;',
|
||||
'0000000000000000000000000000000000000000000000000000000000000000', 'CSharp');
|
||||
|
||||
-- 8. VirtualTag — MachineStatus boolean computed by Roslyn each time Source
|
||||
-- changes. Historized so the dashboard can plot a running/idle timeline
|
||||
-- next to the raw TestHistoryValue trend from Aveva Historian.
|
||||
INSERT dbo.VirtualTag(GenerationId, VirtualTagId, EquipmentId, Name, DataType,
|
||||
-- 6. VirtualTag — MachineStatus boolean computed on each Source change.
|
||||
INSERT dbo.VirtualTag(VirtualTagId, EquipmentId, Name, DataType,
|
||||
ScriptId, ChangeTriggered, TimerIntervalMs, Historize, Enabled)
|
||||
VALUES (@Gen, @VtId, @EqId, 'MachineStatus', 'Boolean', @VtScript, 1, NULL, 1, 1);
|
||||
VALUES (@VtId, @EqId, 'MachineStatus', 'Boolean', @VtScript, 1, NULL, 1, 1);
|
||||
|
||||
-- 9. ScriptedAlarm — Active when Source > 50.
|
||||
INSERT dbo.ScriptedAlarm(GenerationId, ScriptedAlarmId, EquipmentId, Name, AlarmType,
|
||||
-- 7. ScriptedAlarm — Active when Source > 50. {token} is a RawPath in v3.
|
||||
INSERT dbo.ScriptedAlarm(ScriptedAlarmId, EquipmentId, Name, AlarmType,
|
||||
Severity, MessageTemplate, PredicateScriptId,
|
||||
HistorizeToAveva, Retain, Enabled)
|
||||
VALUES (@Gen, @AlId, @EqId, 'OverTemp', 'LimitAlarm', 800,
|
||||
N'Reactor source value {/lab-floor/galaxy-line/reactor-1/Source} exceeded 50',
|
||||
VALUES (@AlId, @EqId, 'OverTemp', 'LimitAlarm', 800,
|
||||
N'Reactor source value {' + @RawPath + N'} exceeded 50',
|
||||
@AlScript, 1, 1, 1);
|
||||
|
||||
-- 10. Publish — flip the generation Status. sp_PublishGeneration takes
|
||||
-- concurrency locks + does ExternalIdReservation merging; we drive it via
|
||||
-- EXEC rather than UPDATE so the rest of the publish workflow runs.
|
||||
EXEC dbo.sp_PublishGeneration @ClusterId = @ClusterId, @DraftGenerationId = @Gen,
|
||||
@Notes = N'Phase 7 live smoke — task #240';
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRINT '';
|
||||
PRINT 'Phase 7 smoke seed complete.';
|
||||
PRINT ' Cluster: ' + @ClusterId;
|
||||
PRINT ' Node: ' + @NodeId + ' (set Node:NodeId in appsettings.json)';
|
||||
PRINT ' Generation: ' + CONVERT(nvarchar(20), @Gen);
|
||||
PRINT '';
|
||||
PRINT 'Next steps:';
|
||||
PRINT ' 1. Edit src/Server/ZB.MOM.WW.OtOpcUa.Server/appsettings.json:';
|
||||
PRINT ' Node:NodeId = "p7-smoke-node"';
|
||||
PRINT ' Node:ClusterId = "p7-smoke"';
|
||||
PRINT ' 2. Edit the placeholder Galaxy attribute in dbo.Tag.TagConfig above';
|
||||
PRINT ' so it points at a real attribute on this Galaxy — replace';
|
||||
PRINT ' REPLACE_WITH_REAL_GALAXY_ATTRIBUTE with e.g. "Plant1.Reactor1.Temp".';
|
||||
PRINT ' 3. Start the Server in a non-elevated shell so the Galaxy.Host pipe ACL';
|
||||
PRINT ' accepts the connection:';
|
||||
PRINT ' dotnet run --project src/Server/ZB.MOM.WW.OtOpcUa.Server';
|
||||
PRINT ' 4. Validate via Client.CLI per docs/v2/implementation/phase-7-e2e-smoke.md';
|
||||
PRINT 'Phase 7 smoke seed complete (v3 schema). Cluster: ' + @ClusterId + ' Node: ' + @NodeId;
|
||||
PRINT 'Next: set Server Node:NodeId/ClusterId, deploy via the ControlPlane, then run the Client.CLI checks.';
|
||||
|
||||
@@ -1,127 +1,74 @@
|
||||
-- S7 e2e smoke seed — closes #212 (umbrella #209).
|
||||
-- S7 e2e smoke seed — v3 schema (Raw tree: DriverInstance → Device → Tag; UNS by reference).
|
||||
--
|
||||
-- One-cluster seed pointing at the python-snap7 fixture
|
||||
-- (`docker compose -f tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/Docker/docker-compose.yml --profile s7_1500 up -d`).
|
||||
-- python-snap7 listens on port 1102 (non-priv); real S7 CPUs listen on 102.
|
||||
-- Publishes one Int16 tag at DB1.DBW0 under `ns=<N>;s=DB1_DBW0` (driver
|
||||
-- sanitises the dot for browse names — see S7Driver.DiscoverAsync).
|
||||
-- Idempotent DELETE-and-recreate of one cluster's worth of S7 test config pointing at the snap7
|
||||
-- fixture (127.0.0.1:1102). v3: the ConfigGeneration/Namespace model + sp_PublishGeneration are
|
||||
-- gone; the endpoint (Host/Port/Rack/Slot) moved from DriverConfig into the Device's DeviceConfig.
|
||||
--
|
||||
-- Usage:
|
||||
-- sqlcmd -S "localhost,14330" -d OtOpcUaConfig -U sa -P "OtOpcUaDev_2026!" \
|
||||
-- -i scripts/smoke/seed-s7-smoke.sql
|
||||
--
|
||||
-- After seeding:
|
||||
-- Node:NodeId = "s7-smoke-node"
|
||||
-- Node:ClusterId = "s7-smoke"
|
||||
-- Then start server + run `./scripts/e2e/test-s7.ps1 -BridgeNodeId "ns=2;s=DB1_DBW0" -S7Host "127.0.0.1:1102"`.
|
||||
-- After seeding: set Server appsettings Node:NodeId/ClusterId, deploy via the ControlPlane, then
|
||||
-- `./scripts/e2e/test-s7.ps1 -BridgeNodeId "ns=2;s=DB1_DBW0" -S7Host "127.0.0.1:1102"`.
|
||||
|
||||
SET NOCOUNT ON;
|
||||
SET XACT_ABORT ON;
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
SET ANSI_NULLS ON;
|
||||
SET ANSI_PADDING ON;
|
||||
SET ANSI_WARNINGS ON;
|
||||
SET ARITHABORT ON;
|
||||
SET CONCAT_NULL_YIELDS_NULL ON;
|
||||
|
||||
DECLARE @ClusterId nvarchar(64) = 's7-smoke';
|
||||
DECLARE @NodeId nvarchar(64) = 's7-smoke-node';
|
||||
DECLARE @DrvId nvarchar(64) = 's7-smoke-drv';
|
||||
DECLARE @NsId nvarchar(64) = 's7-smoke-ns';
|
||||
DECLARE @DevId nvarchar(64) = 's7-smoke-dev';
|
||||
DECLARE @AreaId nvarchar(64) = 's7-smoke-area';
|
||||
DECLARE @LineId nvarchar(64) = 's7-smoke-line';
|
||||
DECLARE @EqId nvarchar(64) = 's7-smoke-eq';
|
||||
DECLARE @EqUuid uniqueidentifier = '17BD5A10-17BD-417B-917B-D5A1017BD5A1';
|
||||
DECLARE @TagId nvarchar(64) = 's7-smoke-tag-db1dbw0';
|
||||
DECLARE @EqUuid uniqueidentifier = '5717A5E0-5717-45E0-9571-7A5E05717A5E';
|
||||
DECLARE @Tag nvarchar(64) = 's7-smoke-tag-db1dbw0';
|
||||
DECLARE @RefId nvarchar(64) = 's7-smoke-ref-db1dbw0';
|
||||
|
||||
BEGIN TRAN;
|
||||
|
||||
DELETE FROM dbo.Tag WHERE TagId IN (@TagId);
|
||||
DELETE FROM dbo.Equipment WHERE EquipmentId = @EqId;
|
||||
DELETE FROM dbo.UnsLine WHERE UnsLineId = @LineId;
|
||||
DELETE FROM dbo.UnsArea WHERE UnsAreaId = @AreaId;
|
||||
DELETE FROM dbo.DriverInstance WHERE DriverInstanceId = @DrvId;
|
||||
DELETE FROM dbo.Namespace WHERE NamespaceId = @NsId;
|
||||
DELETE FROM dbo.ConfigGeneration WHERE ClusterId = @ClusterId;
|
||||
DELETE FROM dbo.ClusterNodeCredential WHERE NodeId = @NodeId;
|
||||
DELETE FROM dbo.ClusterNodeGenerationState WHERE NodeId = @NodeId;
|
||||
DELETE FROM dbo.ClusterNode WHERE NodeId = @NodeId;
|
||||
DELETE FROM dbo.ServerCluster WHERE ClusterId = @ClusterId;
|
||||
|
||||
DELETE FROM dbo.UnsTagReference WHERE UnsTagReferenceId = @RefId;
|
||||
DELETE FROM dbo.Tag WHERE TagId = @Tag;
|
||||
DELETE FROM dbo.Equipment WHERE EquipmentId = @EqId;
|
||||
DELETE FROM dbo.UnsLine WHERE UnsLineId = @LineId;
|
||||
DELETE FROM dbo.UnsArea WHERE UnsAreaId = @AreaId;
|
||||
DELETE FROM dbo.Device WHERE DeviceId = @DevId;
|
||||
DELETE FROM dbo.DriverInstance WHERE DriverInstanceId = @DrvId;
|
||||
DELETE FROM dbo.ClusterNodeCredential WHERE NodeId = @NodeId;
|
||||
DELETE FROM dbo.ClusterNode WHERE NodeId = @NodeId;
|
||||
DELETE FROM dbo.ServerCluster WHERE ClusterId = @ClusterId;
|
||||
DELETE FROM dbo.ClusterNodeCredential WHERE Kind = 'SqlLogin' AND Value = 'sa';
|
||||
|
||||
INSERT dbo.ServerCluster(ClusterId, Name, Enterprise, Site, NodeCount, RedundancyMode, Enabled, CreatedBy)
|
||||
VALUES (@ClusterId, 'S7 Smoke', 'zb', 'lab', 1, 'None', 1, 's7-smoke');
|
||||
|
||||
INSERT dbo.ClusterNode(NodeId, ClusterId, RedundancyRole, Host, OpcUaPort, DashboardPort,
|
||||
INSERT dbo.ClusterNode(NodeId, ClusterId, Host, OpcUaPort, DashboardPort,
|
||||
ApplicationUri, ServiceLevelBase, Enabled, CreatedBy)
|
||||
VALUES (@NodeId, @ClusterId, 'Primary', 'localhost', 4840, 15050,
|
||||
'urn:OtOpcUa:s7-smoke-node', 200, 1, 's7-smoke');
|
||||
-- Dashboard moved off :5000 (Windows URL-ACL).
|
||||
VALUES (@NodeId, @ClusterId, 'localhost', 4840, 15050, 'urn:OtOpcUa:s7-smoke-node', 200, 1, 's7-smoke');
|
||||
|
||||
INSERT dbo.ClusterNodeCredential(NodeId, Kind, Value, Enabled, CreatedBy)
|
||||
VALUES (@NodeId, 'SqlLogin', 'sa', 1, 's7-smoke');
|
||||
|
||||
DECLARE @Gen bigint;
|
||||
INSERT dbo.ConfigGeneration(ClusterId, Status, CreatedBy)
|
||||
VALUES (@ClusterId, 'Draft', 's7-smoke');
|
||||
SET @Gen = SCOPE_IDENTITY();
|
||||
|
||||
INSERT dbo.Namespace(GenerationId, NamespaceId, ClusterId, Kind, NamespaceUri, Enabled)
|
||||
VALUES (@Gen, @NsId, @ClusterId, 'Equipment', 'urn:s7-smoke:eq', 1);
|
||||
|
||||
INSERT dbo.UnsArea(GenerationId, UnsAreaId, ClusterId, Name)
|
||||
VALUES (@Gen, @AreaId, @ClusterId, 'lab-floor');
|
||||
|
||||
INSERT dbo.UnsLine(GenerationId, UnsLineId, UnsAreaId, Name)
|
||||
VALUES (@Gen, @LineId, @AreaId, 's7-line');
|
||||
|
||||
INSERT dbo.Equipment(GenerationId, EquipmentId, EquipmentUuid, DriverInstanceId, UnsLineId,
|
||||
Name, MachineCode, Enabled)
|
||||
VALUES (@Gen, @EqId, @EqUuid, @DrvId, @LineId, 's7-sim', 's7-001', 1);
|
||||
|
||||
-- S7 DriverInstance — python-snap7 S7-1500 profile, slot 0, port 1102.
|
||||
-- DriverConfig shape mirrors S7DriverConfigDto.
|
||||
INSERT dbo.DriverInstance(GenerationId, DriverInstanceId, ClusterId, NamespaceId,
|
||||
Name, DriverType, DriverConfig, Enabled)
|
||||
VALUES (@Gen, @DrvId, @ClusterId, @NsId, 'snap7-smoke', 'S7', N'{
|
||||
"Host": "127.0.0.1",
|
||||
"Port": 1102,
|
||||
"CpuType": "S71500",
|
||||
"Rack": 0,
|
||||
"Slot": 0,
|
||||
"TimeoutMs": 5000,
|
||||
"Probe": { "Enabled": true, "IntervalMs": 5000, "TimeoutMs": 2000, "ProbeAddress": "MW0" },
|
||||
"Tags": [
|
||||
{
|
||||
"Name": "DB1_DBW0",
|
||||
"Address": "DB1.DBW0",
|
||||
"DataType": "Int16",
|
||||
"Writable": true,
|
||||
"WriteIdempotent": true
|
||||
}
|
||||
]
|
||||
-- DriverConfig keeps protocol/channel settings; endpoint moves to DeviceConfig.
|
||||
INSERT dbo.DriverInstance(DriverInstanceId, ClusterId, RawFolderId, Name, DriverType, DriverConfig, Enabled)
|
||||
VALUES (@DrvId, @ClusterId, NULL, 'snap7-smoke', 'S7', N'{
|
||||
"Probe": { "Enabled": true, "IntervalMs": 5000, "TimeoutMs": 2000, "ProbeAddress": "MW0" }
|
||||
}', 1);
|
||||
|
||||
INSERT dbo.Tag(GenerationId, TagId, DriverInstanceId, EquipmentId, Name, DataType,
|
||||
AccessLevel, TagConfig, WriteIdempotent)
|
||||
VALUES (@Gen, @TagId, @DrvId, @EqId, 'DB1_DBW0', 'Int16', 'ReadWrite',
|
||||
N'{"FullName":"DB1_DBW0","Address":"DB1.DBW0","DataType":"Int16"}', 1);
|
||||
-- DeviceConfig shape finalized against reworked options in Wave B (WP5).
|
||||
INSERT dbo.Device(DeviceId, DriverInstanceId, Name, Enabled, DeviceConfig)
|
||||
VALUES (@DevId, @DrvId, 'plc', 1, N'{ "Host": "127.0.0.1", "Port": 1102, "Rack": 0, "Slot": 0 }');
|
||||
|
||||
EXEC dbo.sp_PublishGeneration @ClusterId = @ClusterId, @DraftGenerationId = @Gen,
|
||||
@Notes = N'S7 smoke — task #212';
|
||||
INSERT dbo.Tag(TagId, DeviceId, TagGroupId, Name, DataType, AccessLevel, WriteIdempotent, TagConfig)
|
||||
VALUES (@Tag, @DevId, NULL, 'DB1_DBW0', 'Int16', 'ReadWrite', 1,
|
||||
N'{ "address": "DB1.DBW0", "dataType": "Int16" }');
|
||||
|
||||
INSERT dbo.UnsArea(UnsAreaId, ClusterId, Name) VALUES (@AreaId, @ClusterId, 'lab-floor');
|
||||
INSERT dbo.UnsLine(UnsLineId, UnsAreaId, Name) VALUES (@LineId, @AreaId, 's7-line');
|
||||
INSERT dbo.Equipment(EquipmentId, EquipmentUuid, UnsLineId, Name, MachineCode, Enabled)
|
||||
VALUES (@EqId, @EqUuid, @LineId, 's7-sim', 's7-001', 1);
|
||||
|
||||
INSERT dbo.UnsTagReference(UnsTagReferenceId, EquipmentId, TagId, SortOrder)
|
||||
VALUES (@RefId, @EqId, @Tag, 0);
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRINT '';
|
||||
PRINT 'S7 smoke seed complete.';
|
||||
PRINT ' Cluster: ' + @ClusterId;
|
||||
PRINT ' Node: ' + @NodeId;
|
||||
PRINT ' Generation: ' + CONVERT(nvarchar(20), @Gen);
|
||||
PRINT '';
|
||||
PRINT 'Next steps:';
|
||||
PRINT ' 1. Set src/.../Server/appsettings.json Node:NodeId = "s7-smoke-node"';
|
||||
PRINT ' Node:ClusterId = "s7-smoke"';
|
||||
PRINT ' 2. docker compose -f tests/.../S7.IntegrationTests/Docker/docker-compose.yml --profile s7_1500 up -d';
|
||||
PRINT ' 3. dotnet run --project src/Server/ZB.MOM.WW.OtOpcUa.Server';
|
||||
PRINT ' 4. ./scripts/e2e/test-s7.ps1 -BridgeNodeId "ns=2;s=DB1_DBW0" -S7Host "127.0.0.1:1102"';
|
||||
PRINT 'S7 smoke seed complete (v3 schema). Cluster: ' + @ClusterId + ' Node: ' + @NodeId;
|
||||
|
||||
Generated
-1208
File diff suppressed because it is too large
Load Diff
@@ -1,811 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialSchema : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ConfigAuditLog",
|
||||
columns: table => new
|
||||
{
|
||||
AuditId = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
Timestamp = table.Column<DateTime>(type: "datetime2(3)", nullable: false, defaultValueSql: "SYSUTCDATETIME()"),
|
||||
Principal = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
EventType = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
ClusterId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||
NodeId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||
GenerationId = table.Column<long>(type: "bigint", nullable: true),
|
||||
DetailsJson = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ConfigAuditLog", x => x.AuditId);
|
||||
table.CheckConstraint("CK_ConfigAuditLog_DetailsJson_IsJson", "DetailsJson IS NULL OR ISJSON(DetailsJson) = 1");
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ExternalIdReservation",
|
||||
columns: table => new
|
||||
{
|
||||
ReservationId = table.Column<Guid>(type: "uniqueidentifier", nullable: false, defaultValueSql: "NEWSEQUENTIALID()"),
|
||||
Kind = table.Column<string>(type: "nvarchar(16)", maxLength: 16, nullable: false),
|
||||
Value = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
EquipmentUuid = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
ClusterId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
FirstPublishedAt = table.Column<DateTime>(type: "datetime2(3)", nullable: false, defaultValueSql: "SYSUTCDATETIME()"),
|
||||
FirstPublishedBy = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
LastPublishedAt = table.Column<DateTime>(type: "datetime2(3)", nullable: false, defaultValueSql: "SYSUTCDATETIME()"),
|
||||
ReleasedAt = table.Column<DateTime>(type: "datetime2(3)", nullable: true),
|
||||
ReleasedBy = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: true),
|
||||
ReleaseReason = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ExternalIdReservation", x => x.ReservationId);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ServerCluster",
|
||||
columns: table => new
|
||||
{
|
||||
ClusterId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
Enterprise = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: false),
|
||||
Site = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: false),
|
||||
NodeCount = table.Column<byte>(type: "tinyint", nullable: false),
|
||||
RedundancyMode = table.Column<string>(type: "nvarchar(16)", maxLength: 16, nullable: false),
|
||||
Enabled = table.Column<bool>(type: "bit", nullable: false),
|
||||
Notes = table.Column<string>(type: "nvarchar(1024)", maxLength: 1024, nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime2(3)", nullable: false, defaultValueSql: "SYSUTCDATETIME()"),
|
||||
CreatedBy = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
ModifiedAt = table.Column<DateTime>(type: "datetime2(3)", nullable: true),
|
||||
ModifiedBy = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ServerCluster", x => x.ClusterId);
|
||||
table.CheckConstraint("CK_ServerCluster_RedundancyMode_NodeCount", "((NodeCount = 1 AND RedundancyMode = 'None') OR (NodeCount = 2 AND RedundancyMode IN ('Warm', 'Hot')))");
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ClusterNode",
|
||||
columns: table => new
|
||||
{
|
||||
NodeId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
ClusterId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
RedundancyRole = table.Column<string>(type: "nvarchar(16)", maxLength: 16, nullable: false),
|
||||
Host = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false),
|
||||
OpcUaPort = table.Column<int>(type: "int", nullable: false),
|
||||
DashboardPort = table.Column<int>(type: "int", nullable: false),
|
||||
ApplicationUri = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
|
||||
ServiceLevelBase = table.Column<byte>(type: "tinyint", nullable: false),
|
||||
DriverConfigOverridesJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
Enabled = table.Column<bool>(type: "bit", nullable: false),
|
||||
LastSeenAt = table.Column<DateTime>(type: "datetime2(3)", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime2(3)", nullable: false, defaultValueSql: "SYSUTCDATETIME()"),
|
||||
CreatedBy = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ClusterNode", x => x.NodeId);
|
||||
table.ForeignKey(
|
||||
name: "FK_ClusterNode_ServerCluster_ClusterId",
|
||||
column: x => x.ClusterId,
|
||||
principalTable: "ServerCluster",
|
||||
principalColumn: "ClusterId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ConfigGeneration",
|
||||
columns: table => new
|
||||
{
|
||||
GenerationId = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
ClusterId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
Status = table.Column<string>(type: "nvarchar(16)", maxLength: 16, nullable: false),
|
||||
ParentGenerationId = table.Column<long>(type: "bigint", nullable: true),
|
||||
PublishedAt = table.Column<DateTime>(type: "datetime2(3)", nullable: true),
|
||||
PublishedBy = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: true),
|
||||
Notes = table.Column<string>(type: "nvarchar(1024)", maxLength: 1024, nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime2(3)", nullable: false, defaultValueSql: "SYSUTCDATETIME()"),
|
||||
CreatedBy = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ConfigGeneration", x => x.GenerationId);
|
||||
table.ForeignKey(
|
||||
name: "FK_ConfigGeneration_ConfigGeneration_ParentGenerationId",
|
||||
column: x => x.ParentGenerationId,
|
||||
principalTable: "ConfigGeneration",
|
||||
principalColumn: "GenerationId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_ConfigGeneration_ServerCluster_ClusterId",
|
||||
column: x => x.ClusterId,
|
||||
principalTable: "ServerCluster",
|
||||
principalColumn: "ClusterId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ClusterNodeCredential",
|
||||
columns: table => new
|
||||
{
|
||||
CredentialId = table.Column<Guid>(type: "uniqueidentifier", nullable: false, defaultValueSql: "NEWSEQUENTIALID()"),
|
||||
NodeId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
Kind = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: false),
|
||||
Value = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: false),
|
||||
Enabled = table.Column<bool>(type: "bit", nullable: false),
|
||||
RotatedAt = table.Column<DateTime>(type: "datetime2(3)", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime2(3)", nullable: false, defaultValueSql: "SYSUTCDATETIME()"),
|
||||
CreatedBy = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ClusterNodeCredential", x => x.CredentialId);
|
||||
table.ForeignKey(
|
||||
name: "FK_ClusterNodeCredential_ClusterNode_NodeId",
|
||||
column: x => x.NodeId,
|
||||
principalTable: "ClusterNode",
|
||||
principalColumn: "NodeId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ClusterNodeGenerationState",
|
||||
columns: table => new
|
||||
{
|
||||
NodeId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
CurrentGenerationId = table.Column<long>(type: "bigint", nullable: true),
|
||||
LastAppliedAt = table.Column<DateTime>(type: "datetime2(3)", nullable: true),
|
||||
LastAppliedStatus = table.Column<string>(type: "nvarchar(16)", maxLength: 16, nullable: true),
|
||||
LastAppliedError = table.Column<string>(type: "nvarchar(2048)", maxLength: 2048, nullable: true),
|
||||
LastSeenAt = table.Column<DateTime>(type: "datetime2(3)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ClusterNodeGenerationState", x => x.NodeId);
|
||||
table.ForeignKey(
|
||||
name: "FK_ClusterNodeGenerationState_ClusterNode_NodeId",
|
||||
column: x => x.NodeId,
|
||||
principalTable: "ClusterNode",
|
||||
principalColumn: "NodeId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_ClusterNodeGenerationState_ConfigGeneration_CurrentGenerationId",
|
||||
column: x => x.CurrentGenerationId,
|
||||
principalTable: "ConfigGeneration",
|
||||
principalColumn: "GenerationId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Device",
|
||||
columns: table => new
|
||||
{
|
||||
DeviceRowId = table.Column<Guid>(type: "uniqueidentifier", nullable: false, defaultValueSql: "NEWSEQUENTIALID()"),
|
||||
GenerationId = table.Column<long>(type: "bigint", nullable: false),
|
||||
DeviceId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||
DriverInstanceId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
Enabled = table.Column<bool>(type: "bit", nullable: false),
|
||||
DeviceConfig = table.Column<string>(type: "nvarchar(max)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Device", x => x.DeviceRowId);
|
||||
table.CheckConstraint("CK_Device_DeviceConfig_IsJson", "ISJSON(DeviceConfig) = 1");
|
||||
table.ForeignKey(
|
||||
name: "FK_Device_ConfigGeneration_GenerationId",
|
||||
column: x => x.GenerationId,
|
||||
principalTable: "ConfigGeneration",
|
||||
principalColumn: "GenerationId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DriverInstance",
|
||||
columns: table => new
|
||||
{
|
||||
DriverInstanceRowId = table.Column<Guid>(type: "uniqueidentifier", nullable: false, defaultValueSql: "NEWSEQUENTIALID()"),
|
||||
GenerationId = table.Column<long>(type: "bigint", nullable: false),
|
||||
DriverInstanceId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||
ClusterId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
NamespaceId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
DriverType = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: false),
|
||||
Enabled = table.Column<bool>(type: "bit", nullable: false),
|
||||
DriverConfig = table.Column<string>(type: "nvarchar(max)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_DriverInstance", x => x.DriverInstanceRowId);
|
||||
table.CheckConstraint("CK_DriverInstance_DriverConfig_IsJson", "ISJSON(DriverConfig) = 1");
|
||||
table.ForeignKey(
|
||||
name: "FK_DriverInstance_ConfigGeneration_GenerationId",
|
||||
column: x => x.GenerationId,
|
||||
principalTable: "ConfigGeneration",
|
||||
principalColumn: "GenerationId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_DriverInstance_ServerCluster_ClusterId",
|
||||
column: x => x.ClusterId,
|
||||
principalTable: "ServerCluster",
|
||||
principalColumn: "ClusterId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Equipment",
|
||||
columns: table => new
|
||||
{
|
||||
EquipmentRowId = table.Column<Guid>(type: "uniqueidentifier", nullable: false, defaultValueSql: "NEWSEQUENTIALID()"),
|
||||
GenerationId = table.Column<long>(type: "bigint", nullable: false),
|
||||
EquipmentId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||
EquipmentUuid = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
DriverInstanceId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
DeviceId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||
UnsLineId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: false),
|
||||
MachineCode = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
ZTag = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||
SAPID = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||
Manufacturer = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||
Model = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||
SerialNumber = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||
HardwareRevision = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: true),
|
||||
SoftwareRevision = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: true),
|
||||
YearOfConstruction = table.Column<short>(type: "smallint", nullable: true),
|
||||
AssetLocation = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
ManufacturerUri = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true),
|
||||
DeviceManualUri = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true),
|
||||
EquipmentClassRef = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: true),
|
||||
Enabled = table.Column<bool>(type: "bit", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Equipment", x => x.EquipmentRowId);
|
||||
table.ForeignKey(
|
||||
name: "FK_Equipment_ConfigGeneration_GenerationId",
|
||||
column: x => x.GenerationId,
|
||||
principalTable: "ConfigGeneration",
|
||||
principalColumn: "GenerationId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Namespace",
|
||||
columns: table => new
|
||||
{
|
||||
NamespaceRowId = table.Column<Guid>(type: "uniqueidentifier", nullable: false, defaultValueSql: "NEWSEQUENTIALID()"),
|
||||
GenerationId = table.Column<long>(type: "bigint", nullable: false),
|
||||
NamespaceId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||
ClusterId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
Kind = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: false),
|
||||
NamespaceUri = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
|
||||
Enabled = table.Column<bool>(type: "bit", nullable: false),
|
||||
Notes = table.Column<string>(type: "nvarchar(1024)", maxLength: 1024, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Namespace", x => x.NamespaceRowId);
|
||||
table.ForeignKey(
|
||||
name: "FK_Namespace_ConfigGeneration_GenerationId",
|
||||
column: x => x.GenerationId,
|
||||
principalTable: "ConfigGeneration",
|
||||
principalColumn: "GenerationId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_Namespace_ServerCluster_ClusterId",
|
||||
column: x => x.ClusterId,
|
||||
principalTable: "ServerCluster",
|
||||
principalColumn: "ClusterId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "NodeAcl",
|
||||
columns: table => new
|
||||
{
|
||||
NodeAclRowId = table.Column<Guid>(type: "uniqueidentifier", nullable: false, defaultValueSql: "NEWSEQUENTIALID()"),
|
||||
GenerationId = table.Column<long>(type: "bigint", nullable: false),
|
||||
NodeAclId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||
ClusterId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
LdapGroup = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
|
||||
ScopeKind = table.Column<string>(type: "nvarchar(16)", maxLength: 16, nullable: false),
|
||||
ScopeId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||
PermissionFlags = table.Column<int>(type: "int", nullable: false),
|
||||
Notes = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_NodeAcl", x => x.NodeAclRowId);
|
||||
table.ForeignKey(
|
||||
name: "FK_NodeAcl_ConfigGeneration_GenerationId",
|
||||
column: x => x.GenerationId,
|
||||
principalTable: "ConfigGeneration",
|
||||
principalColumn: "GenerationId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PollGroup",
|
||||
columns: table => new
|
||||
{
|
||||
PollGroupRowId = table.Column<Guid>(type: "uniqueidentifier", nullable: false, defaultValueSql: "NEWSEQUENTIALID()"),
|
||||
GenerationId = table.Column<long>(type: "bigint", nullable: false),
|
||||
PollGroupId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||
DriverInstanceId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
IntervalMs = table.Column<int>(type: "int", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PollGroup", x => x.PollGroupRowId);
|
||||
table.CheckConstraint("CK_PollGroup_IntervalMs_Min", "IntervalMs >= 50");
|
||||
table.ForeignKey(
|
||||
name: "FK_PollGroup_ConfigGeneration_GenerationId",
|
||||
column: x => x.GenerationId,
|
||||
principalTable: "ConfigGeneration",
|
||||
principalColumn: "GenerationId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Tag",
|
||||
columns: table => new
|
||||
{
|
||||
TagRowId = table.Column<Guid>(type: "uniqueidentifier", nullable: false, defaultValueSql: "NEWSEQUENTIALID()"),
|
||||
GenerationId = table.Column<long>(type: "bigint", nullable: false),
|
||||
TagId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||
DriverInstanceId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
DeviceId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||
EquipmentId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
FolderPath = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true),
|
||||
DataType = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: false),
|
||||
AccessLevel = table.Column<string>(type: "nvarchar(16)", maxLength: 16, nullable: false),
|
||||
WriteIdempotent = table.Column<bool>(type: "bit", nullable: false),
|
||||
PollGroupId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||
TagConfig = table.Column<string>(type: "nvarchar(max)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Tag", x => x.TagRowId);
|
||||
table.CheckConstraint("CK_Tag_TagConfig_IsJson", "ISJSON(TagConfig) = 1");
|
||||
table.ForeignKey(
|
||||
name: "FK_Tag_ConfigGeneration_GenerationId",
|
||||
column: x => x.GenerationId,
|
||||
principalTable: "ConfigGeneration",
|
||||
principalColumn: "GenerationId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UnsArea",
|
||||
columns: table => new
|
||||
{
|
||||
UnsAreaRowId = table.Column<Guid>(type: "uniqueidentifier", nullable: false, defaultValueSql: "NEWSEQUENTIALID()"),
|
||||
GenerationId = table.Column<long>(type: "bigint", nullable: false),
|
||||
UnsAreaId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||
ClusterId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: false),
|
||||
Notes = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UnsArea", x => x.UnsAreaRowId);
|
||||
table.ForeignKey(
|
||||
name: "FK_UnsArea_ConfigGeneration_GenerationId",
|
||||
column: x => x.GenerationId,
|
||||
principalTable: "ConfigGeneration",
|
||||
principalColumn: "GenerationId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_UnsArea_ServerCluster_ClusterId",
|
||||
column: x => x.ClusterId,
|
||||
principalTable: "ServerCluster",
|
||||
principalColumn: "ClusterId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UnsLine",
|
||||
columns: table => new
|
||||
{
|
||||
UnsLineRowId = table.Column<Guid>(type: "uniqueidentifier", nullable: false, defaultValueSql: "NEWSEQUENTIALID()"),
|
||||
GenerationId = table.Column<long>(type: "bigint", nullable: false),
|
||||
UnsLineId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||
UnsAreaId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: false),
|
||||
Notes = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UnsLine", x => x.UnsLineRowId);
|
||||
table.ForeignKey(
|
||||
name: "FK_UnsLine_ConfigGeneration_GenerationId",
|
||||
column: x => x.GenerationId,
|
||||
principalTable: "ConfigGeneration",
|
||||
principalColumn: "GenerationId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_ClusterNode_ApplicationUri",
|
||||
table: "ClusterNode",
|
||||
column: "ApplicationUri",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_ClusterNode_Primary_Per_Cluster",
|
||||
table: "ClusterNode",
|
||||
column: "ClusterId",
|
||||
unique: true,
|
||||
filter: "[RedundancyRole] = 'Primary'");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ClusterNodeCredential_NodeId",
|
||||
table: "ClusterNodeCredential",
|
||||
columns: new[] { "NodeId", "Enabled" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_ClusterNodeCredential_Value",
|
||||
table: "ClusterNodeCredential",
|
||||
columns: new[] { "Kind", "Value" },
|
||||
unique: true,
|
||||
filter: "[Enabled] = 1");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ClusterNodeGenerationState_Generation",
|
||||
table: "ClusterNodeGenerationState",
|
||||
column: "CurrentGenerationId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ConfigAuditLog_Cluster_Time",
|
||||
table: "ConfigAuditLog",
|
||||
columns: new[] { "ClusterId", "Timestamp" },
|
||||
descending: new[] { false, true });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ConfigAuditLog_Generation",
|
||||
table: "ConfigAuditLog",
|
||||
column: "GenerationId",
|
||||
filter: "[GenerationId] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ConfigGeneration_Cluster_Published",
|
||||
table: "ConfigGeneration",
|
||||
columns: new[] { "ClusterId", "Status", "GenerationId" },
|
||||
descending: new[] { false, false, true })
|
||||
.Annotation("SqlServer:Include", new[] { "PublishedAt" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ConfigGeneration_ParentGenerationId",
|
||||
table: "ConfigGeneration",
|
||||
column: "ParentGenerationId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_ConfigGeneration_Draft_Per_Cluster",
|
||||
table: "ConfigGeneration",
|
||||
column: "ClusterId",
|
||||
unique: true,
|
||||
filter: "[Status] = 'Draft'");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Device_Generation_Driver",
|
||||
table: "Device",
|
||||
columns: new[] { "GenerationId", "DriverInstanceId" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_Device_Generation_LogicalId",
|
||||
table: "Device",
|
||||
columns: new[] { "GenerationId", "DeviceId" },
|
||||
unique: true,
|
||||
filter: "[DeviceId] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DriverInstance_ClusterId",
|
||||
table: "DriverInstance",
|
||||
column: "ClusterId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DriverInstance_Generation_Cluster",
|
||||
table: "DriverInstance",
|
||||
columns: new[] { "GenerationId", "ClusterId" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DriverInstance_Generation_Namespace",
|
||||
table: "DriverInstance",
|
||||
columns: new[] { "GenerationId", "NamespaceId" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_DriverInstance_Generation_LogicalId",
|
||||
table: "DriverInstance",
|
||||
columns: new[] { "GenerationId", "DriverInstanceId" },
|
||||
unique: true,
|
||||
filter: "[DriverInstanceId] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Equipment_Generation_Driver",
|
||||
table: "Equipment",
|
||||
columns: new[] { "GenerationId", "DriverInstanceId" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Equipment_Generation_Line",
|
||||
table: "Equipment",
|
||||
columns: new[] { "GenerationId", "UnsLineId" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Equipment_Generation_MachineCode",
|
||||
table: "Equipment",
|
||||
columns: new[] { "GenerationId", "MachineCode" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Equipment_Generation_SAPID",
|
||||
table: "Equipment",
|
||||
columns: new[] { "GenerationId", "SAPID" },
|
||||
filter: "[SAPID] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Equipment_Generation_ZTag",
|
||||
table: "Equipment",
|
||||
columns: new[] { "GenerationId", "ZTag" },
|
||||
filter: "[ZTag] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_Equipment_Generation_LinePath",
|
||||
table: "Equipment",
|
||||
columns: new[] { "GenerationId", "UnsLineId", "Name" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_Equipment_Generation_LogicalId",
|
||||
table: "Equipment",
|
||||
columns: new[] { "GenerationId", "EquipmentId" },
|
||||
unique: true,
|
||||
filter: "[EquipmentId] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_Equipment_Generation_Uuid",
|
||||
table: "Equipment",
|
||||
columns: new[] { "GenerationId", "EquipmentUuid" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ExternalIdReservation_Equipment",
|
||||
table: "ExternalIdReservation",
|
||||
column: "EquipmentUuid");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_ExternalIdReservation_KindValue_Active",
|
||||
table: "ExternalIdReservation",
|
||||
columns: new[] { "Kind", "Value" },
|
||||
unique: true,
|
||||
filter: "[ReleasedAt] IS NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Namespace_ClusterId",
|
||||
table: "Namespace",
|
||||
column: "ClusterId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Namespace_Generation_Cluster",
|
||||
table: "Namespace",
|
||||
columns: new[] { "GenerationId", "ClusterId" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_Namespace_Generation_Cluster_Kind",
|
||||
table: "Namespace",
|
||||
columns: new[] { "GenerationId", "ClusterId", "Kind" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_Namespace_Generation_LogicalId",
|
||||
table: "Namespace",
|
||||
columns: new[] { "GenerationId", "NamespaceId" },
|
||||
unique: true,
|
||||
filter: "[NamespaceId] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_Namespace_Generation_LogicalId_Cluster",
|
||||
table: "Namespace",
|
||||
columns: new[] { "GenerationId", "NamespaceId", "ClusterId" },
|
||||
unique: true,
|
||||
filter: "[NamespaceId] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_Namespace_Generation_NamespaceUri",
|
||||
table: "Namespace",
|
||||
columns: new[] { "GenerationId", "NamespaceUri" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_NodeAcl_Generation_Cluster",
|
||||
table: "NodeAcl",
|
||||
columns: new[] { "GenerationId", "ClusterId" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_NodeAcl_Generation_Group",
|
||||
table: "NodeAcl",
|
||||
columns: new[] { "GenerationId", "LdapGroup" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_NodeAcl_Generation_Scope",
|
||||
table: "NodeAcl",
|
||||
columns: new[] { "GenerationId", "ScopeKind", "ScopeId" },
|
||||
filter: "[ScopeId] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_NodeAcl_Generation_GroupScope",
|
||||
table: "NodeAcl",
|
||||
columns: new[] { "GenerationId", "ClusterId", "LdapGroup", "ScopeKind", "ScopeId" },
|
||||
unique: true,
|
||||
filter: "[ScopeId] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_NodeAcl_Generation_LogicalId",
|
||||
table: "NodeAcl",
|
||||
columns: new[] { "GenerationId", "NodeAclId" },
|
||||
unique: true,
|
||||
filter: "[NodeAclId] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PollGroup_Generation_Driver",
|
||||
table: "PollGroup",
|
||||
columns: new[] { "GenerationId", "DriverInstanceId" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_PollGroup_Generation_LogicalId",
|
||||
table: "PollGroup",
|
||||
columns: new[] { "GenerationId", "PollGroupId" },
|
||||
unique: true,
|
||||
filter: "[PollGroupId] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ServerCluster_Site",
|
||||
table: "ServerCluster",
|
||||
column: "Site");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_ServerCluster_Name",
|
||||
table: "ServerCluster",
|
||||
column: "Name",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Tag_Generation_Driver_Device",
|
||||
table: "Tag",
|
||||
columns: new[] { "GenerationId", "DriverInstanceId", "DeviceId" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Tag_Generation_Equipment",
|
||||
table: "Tag",
|
||||
columns: new[] { "GenerationId", "EquipmentId" },
|
||||
filter: "[EquipmentId] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_Tag_Generation_EquipmentPath",
|
||||
table: "Tag",
|
||||
columns: new[] { "GenerationId", "EquipmentId", "Name" },
|
||||
unique: true,
|
||||
filter: "[EquipmentId] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_Tag_Generation_FolderPath",
|
||||
table: "Tag",
|
||||
columns: new[] { "GenerationId", "DriverInstanceId", "FolderPath", "Name" },
|
||||
unique: true,
|
||||
filter: "[EquipmentId] IS NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_Tag_Generation_LogicalId",
|
||||
table: "Tag",
|
||||
columns: new[] { "GenerationId", "TagId" },
|
||||
unique: true,
|
||||
filter: "[TagId] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UnsArea_ClusterId",
|
||||
table: "UnsArea",
|
||||
column: "ClusterId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UnsArea_Generation_Cluster",
|
||||
table: "UnsArea",
|
||||
columns: new[] { "GenerationId", "ClusterId" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_UnsArea_Generation_ClusterName",
|
||||
table: "UnsArea",
|
||||
columns: new[] { "GenerationId", "ClusterId", "Name" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_UnsArea_Generation_LogicalId",
|
||||
table: "UnsArea",
|
||||
columns: new[] { "GenerationId", "UnsAreaId" },
|
||||
unique: true,
|
||||
filter: "[UnsAreaId] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UnsLine_Generation_Area",
|
||||
table: "UnsLine",
|
||||
columns: new[] { "GenerationId", "UnsAreaId" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_UnsLine_Generation_AreaName",
|
||||
table: "UnsLine",
|
||||
columns: new[] { "GenerationId", "UnsAreaId", "Name" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_UnsLine_Generation_LogicalId",
|
||||
table: "UnsLine",
|
||||
columns: new[] { "GenerationId", "UnsLineId" },
|
||||
unique: true,
|
||||
filter: "[UnsLineId] IS NOT NULL");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ClusterNodeCredential");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ClusterNodeGenerationState");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ConfigAuditLog");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Device");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "DriverInstance");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Equipment");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ExternalIdReservation");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Namespace");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "NodeAcl");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "PollGroup");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Tag");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UnsArea");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UnsLine");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ClusterNode");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ConfigGeneration");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ServerCluster");
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
-1208
File diff suppressed because it is too large
Load Diff
-510
@@ -1,510 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations;
|
||||
|
||||
/// <summary>
|
||||
/// Stored procedures per <c>config-db-schema.md §"Stored Procedures"</c>. All node + admin DB
|
||||
/// access funnels through these — direct table writes are revoked in the AuthorizationGrants
|
||||
/// migration that follows. CREATE OR ALTER style so procs version with the schema.
|
||||
/// </summary>
|
||||
public partial class StoredProcedures : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql(Procs.GetCurrentGenerationForCluster);
|
||||
migrationBuilder.Sql(Procs.GetGenerationContent);
|
||||
migrationBuilder.Sql(Procs.RegisterNodeGenerationApplied);
|
||||
migrationBuilder.Sql(Procs.ValidateDraft);
|
||||
migrationBuilder.Sql(Procs.PublishGeneration);
|
||||
migrationBuilder.Sql(Procs.RollbackToGeneration);
|
||||
migrationBuilder.Sql(Procs.ComputeGenerationDiff);
|
||||
migrationBuilder.Sql(Procs.ReleaseExternalIdReservation);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
foreach (var name in new[]
|
||||
{
|
||||
"sp_ReleaseExternalIdReservation", "sp_ComputeGenerationDiff", "sp_RollbackToGeneration",
|
||||
"sp_PublishGeneration", "sp_ValidateDraft", "sp_RegisterNodeGenerationApplied",
|
||||
"sp_GetGenerationContent", "sp_GetCurrentGenerationForCluster",
|
||||
})
|
||||
{
|
||||
migrationBuilder.Sql($"IF OBJECT_ID(N'dbo.{name}', N'P') IS NOT NULL DROP PROCEDURE dbo.{name};");
|
||||
}
|
||||
}
|
||||
|
||||
private static class Procs
|
||||
{
|
||||
public const string GetCurrentGenerationForCluster = @"
|
||||
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
|
||||
";
|
||||
|
||||
public const string GetGenerationContent = @"
|
||||
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
|
||||
";
|
||||
|
||||
public const string RegisterNodeGenerationApplied = @"
|
||||
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
|
||||
";
|
||||
|
||||
public const string ValidateDraft = @"
|
||||
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
|
||||
";
|
||||
|
||||
public const string PublishGeneration = @"
|
||||
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
|
||||
";
|
||||
|
||||
public const string RollbackToGeneration = @"
|
||||
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
|
||||
";
|
||||
|
||||
public const string ComputeGenerationDiff = @"
|
||||
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
|
||||
";
|
||||
|
||||
public const string ReleaseExternalIdReservation = @"
|
||||
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
|
||||
";
|
||||
}
|
||||
}
|
||||
Generated
-1208
File diff suppressed because it is too large
Load Diff
-55
@@ -1,55 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations;
|
||||
|
||||
/// <summary>
|
||||
/// Creates the two DB roles per <c>config-db-schema.md §"Authorization Model"</c> and grants
|
||||
/// EXECUTE on the appropriate stored procedures. Deliberately grants no direct table DML — all
|
||||
/// writes funnel through the procs, which authenticate via <c>SUSER_SNAME()</c>.
|
||||
/// Principals (SQL logins, gMSA users, cert-mapped users) are provisioned by the DBA outside
|
||||
/// this migration and then added to one of the two roles.
|
||||
/// </summary>
|
||||
public partial class AuthorizationGrants : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql(@"
|
||||
IF DATABASE_PRINCIPAL_ID('OtOpcUaNode') IS NULL
|
||||
CREATE ROLE OtOpcUaNode;
|
||||
IF DATABASE_PRINCIPAL_ID('OtOpcUaAdmin') IS NULL
|
||||
CREATE ROLE OtOpcUaAdmin;
|
||||
");
|
||||
|
||||
migrationBuilder.Sql(@"
|
||||
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;
|
||||
");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql(@"
|
||||
IF DATABASE_PRINCIPAL_ID('OtOpcUaNode') IS NOT NULL
|
||||
DROP ROLE OtOpcUaNode;
|
||||
IF DATABASE_PRINCIPAL_ID('OtOpcUaAdmin') IS NOT NULL
|
||||
DROP ROLE OtOpcUaAdmin;
|
||||
");
|
||||
}
|
||||
}
|
||||
Generated
-1248
File diff suppressed because it is too large
Load Diff
-49
@@ -1,49 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddDriverHostStatus : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DriverHostStatus",
|
||||
columns: table => new
|
||||
{
|
||||
NodeId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
DriverInstanceId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
HostName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
|
||||
State = table.Column<string>(type: "nvarchar(16)", maxLength: 16, nullable: false),
|
||||
StateChangedUtc = table.Column<DateTime>(type: "datetime2(3)", nullable: false),
|
||||
LastSeenUtc = table.Column<DateTime>(type: "datetime2(3)", nullable: false),
|
||||
Detail = table.Column<string>(type: "nvarchar(1024)", maxLength: 1024, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_DriverHostStatus", x => new { x.NodeId, x.DriverInstanceId, x.HostName });
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DriverHostStatus_LastSeen",
|
||||
table: "DriverHostStatus",
|
||||
column: "LastSeenUtc");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DriverHostStatus_Node",
|
||||
table: "DriverHostStatus",
|
||||
column: "NodeId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "DriverHostStatus");
|
||||
}
|
||||
}
|
||||
}
|
||||
-1287
File diff suppressed because it is too large
Load Diff
-46
@@ -1,46 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddDriverInstanceResilienceStatus : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DriverInstanceResilienceStatus",
|
||||
columns: table => new
|
||||
{
|
||||
DriverInstanceId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
HostName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
|
||||
LastCircuitBreakerOpenUtc = table.Column<DateTime>(type: "datetime2(3)", nullable: true),
|
||||
ConsecutiveFailures = table.Column<int>(type: "int", nullable: false),
|
||||
CurrentBulkheadDepth = table.Column<int>(type: "int", nullable: false),
|
||||
LastRecycleUtc = table.Column<DateTime>(type: "datetime2(3)", nullable: true),
|
||||
BaselineFootprintBytes = table.Column<long>(type: "bigint", nullable: false),
|
||||
CurrentFootprintBytes = table.Column<long>(type: "bigint", nullable: false),
|
||||
LastSampledUtc = table.Column<DateTime>(type: "datetime2(3)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_DriverInstanceResilienceStatus", x => new { x.DriverInstanceId, x.HostName });
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DriverResilience_LastSampled",
|
||||
table: "DriverInstanceResilienceStatus",
|
||||
column: "LastSampledUtc");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "DriverInstanceResilienceStatus");
|
||||
}
|
||||
}
|
||||
}
|
||||
-1342
File diff suppressed because it is too large
Load Diff
-62
@@ -1,62 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddLdapGroupRoleMapping : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "LdapGroupRoleMapping",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
LdapGroup = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: false),
|
||||
Role = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: false),
|
||||
ClusterId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||
IsSystemWide = table.Column<bool>(type: "bit", nullable: false),
|
||||
CreatedAtUtc = table.Column<DateTime>(type: "datetime2(3)", nullable: false),
|
||||
Notes = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_LdapGroupRoleMapping", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_LdapGroupRoleMapping_ServerCluster_ClusterId",
|
||||
column: x => x.ClusterId,
|
||||
principalTable: "ServerCluster",
|
||||
principalColumn: "ClusterId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_LdapGroupRoleMapping_ClusterId",
|
||||
table: "LdapGroupRoleMapping",
|
||||
column: "ClusterId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_LdapGroupRoleMapping_Group",
|
||||
table: "LdapGroupRoleMapping",
|
||||
column: "LdapGroup");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_LdapGroupRoleMapping_Group_Cluster",
|
||||
table: "LdapGroupRoleMapping",
|
||||
columns: new[] { "LdapGroup", "ClusterId" },
|
||||
unique: true,
|
||||
filter: "[ClusterId] IS NOT NULL");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "LdapGroupRoleMapping");
|
||||
}
|
||||
}
|
||||
}
|
||||
-1347
File diff suppressed because it is too large
Load Diff
-37
@@ -1,37 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddDriverInstanceResilienceConfig : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ResilienceConfig",
|
||||
table: "DriverInstance",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddCheckConstraint(
|
||||
name: "CK_DriverInstance_ResilienceConfig_IsJson",
|
||||
table: "DriverInstance",
|
||||
sql: "ResilienceConfig IS NULL OR ISJSON(ResilienceConfig) = 1");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropCheckConstraint(
|
||||
name: "CK_DriverInstance_ResilienceConfig_IsJson",
|
||||
table: "DriverInstance");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ResilienceConfig",
|
||||
table: "DriverInstance");
|
||||
}
|
||||
}
|
||||
}
|
||||
-1505
File diff suppressed because it is too large
Load Diff
-91
@@ -1,91 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddEquipmentImportBatch : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "EquipmentImportBatch",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
ClusterId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
CreatedBy = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
CreatedAtUtc = table.Column<DateTime>(type: "datetime2(3)", nullable: false),
|
||||
RowsStaged = table.Column<int>(type: "int", nullable: false),
|
||||
RowsAccepted = table.Column<int>(type: "int", nullable: false),
|
||||
RowsRejected = table.Column<int>(type: "int", nullable: false),
|
||||
FinalisedAtUtc = table.Column<DateTime>(type: "datetime2(3)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_EquipmentImportBatch", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "EquipmentImportRow",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
BatchId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
LineNumberInFile = table.Column<int>(type: "int", nullable: false),
|
||||
IsAccepted = table.Column<bool>(type: "bit", nullable: false),
|
||||
RejectReason = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true),
|
||||
ZTag = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
MachineCode = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
SAPID = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
EquipmentId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
EquipmentUuid = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
UnsAreaName = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
UnsLineName = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
Manufacturer = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
Model = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
SerialNumber = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
HardwareRevision = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||
SoftwareRevision = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||
YearOfConstruction = table.Column<string>(type: "nvarchar(8)", maxLength: 8, nullable: true),
|
||||
AssetLocation = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true),
|
||||
ManufacturerUri = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true),
|
||||
DeviceManualUri = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_EquipmentImportRow", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_EquipmentImportRow_EquipmentImportBatch_BatchId",
|
||||
column: x => x.BatchId,
|
||||
principalTable: "EquipmentImportBatch",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_EquipmentImportBatch_Creator_Finalised",
|
||||
table: "EquipmentImportBatch",
|
||||
columns: new[] { "CreatedBy", "FinalisedAtUtc" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_EquipmentImportRow_Batch",
|
||||
table: "EquipmentImportRow",
|
||||
column: "BatchId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "EquipmentImportRow");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "EquipmentImportBatch");
|
||||
}
|
||||
}
|
||||
}
|
||||
-1505
File diff suppressed because it is too large
Load Diff
-172
@@ -1,172 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
{
|
||||
/// <summary>
|
||||
/// Extends <c>dbo.sp_ComputeGenerationDiff</c> to emit <c>NodeAcl</c> rows alongside the
|
||||
/// existing Namespace/DriverInstance/Equipment/Tag output — closes the final slice of
|
||||
/// task #196 (DiffViewer ACL section). Logical id for NodeAcl is a composite
|
||||
/// <c>LdapGroup|ScopeKind|ScopeId</c> triple so a Change row surfaces whether the grant
|
||||
/// shifted permissions, moved scope, or was added/removed outright.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public partial class ExtendComputeGenerationDiffWithNodeAcl : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql(Procs.ComputeGenerationDiffV2);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql(Procs.ComputeGenerationDiffV1);
|
||||
}
|
||||
|
||||
private static class Procs
|
||||
{
|
||||
/// <summary>V2 — adds the NodeAcl section to the diff output.</summary>
|
||||
public const string ComputeGenerationDiffV2 = @"
|
||||
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
|
||||
";
|
||||
|
||||
/// <summary>V1 — exact proc shipped in migration 20260417215224_StoredProcedures. Restored on Down().</summary>
|
||||
public const string ComputeGenerationDiffV1 = @"
|
||||
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
|
||||
";
|
||||
}
|
||||
}
|
||||
}
|
||||
-1793
File diff suppressed because it is too large
Load Diff
-186
@@ -1,186 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddPhase7ScriptingTables : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Script",
|
||||
columns: table => new
|
||||
{
|
||||
ScriptRowId = table.Column<Guid>(type: "uniqueidentifier", nullable: false, defaultValueSql: "NEWSEQUENTIALID()"),
|
||||
GenerationId = table.Column<long>(type: "bigint", nullable: false),
|
||||
ScriptId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
SourceCode = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
SourceHash = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
Language = table.Column<string>(type: "nvarchar(16)", maxLength: 16, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Script", x => x.ScriptRowId);
|
||||
table.ForeignKey(
|
||||
name: "FK_Script_ConfigGeneration_GenerationId",
|
||||
column: x => x.GenerationId,
|
||||
principalTable: "ConfigGeneration",
|
||||
principalColumn: "GenerationId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ScriptedAlarm",
|
||||
columns: table => new
|
||||
{
|
||||
ScriptedAlarmRowId = table.Column<Guid>(type: "uniqueidentifier", nullable: false, defaultValueSql: "NEWSEQUENTIALID()"),
|
||||
GenerationId = table.Column<long>(type: "bigint", nullable: false),
|
||||
ScriptedAlarmId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||
EquipmentId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
AlarmType = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: false),
|
||||
Severity = table.Column<int>(type: "int", nullable: false),
|
||||
MessageTemplate = table.Column<string>(type: "nvarchar(1024)", maxLength: 1024, nullable: false),
|
||||
PredicateScriptId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
HistorizeToAveva = table.Column<bool>(type: "bit", nullable: false),
|
||||
Retain = table.Column<bool>(type: "bit", nullable: false),
|
||||
Enabled = table.Column<bool>(type: "bit", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ScriptedAlarm", x => x.ScriptedAlarmRowId);
|
||||
table.CheckConstraint("CK_ScriptedAlarm_AlarmType", "AlarmType IN ('AlarmCondition','LimitAlarm','OffNormalAlarm','DiscreteAlarm')");
|
||||
table.CheckConstraint("CK_ScriptedAlarm_Severity_Range", "Severity BETWEEN 1 AND 1000");
|
||||
table.ForeignKey(
|
||||
name: "FK_ScriptedAlarm_ConfigGeneration_GenerationId",
|
||||
column: x => x.GenerationId,
|
||||
principalTable: "ConfigGeneration",
|
||||
principalColumn: "GenerationId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ScriptedAlarmState",
|
||||
columns: table => new
|
||||
{
|
||||
ScriptedAlarmId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
EnabledState = table.Column<string>(type: "nvarchar(16)", maxLength: 16, nullable: false),
|
||||
AckedState = table.Column<string>(type: "nvarchar(16)", maxLength: 16, nullable: false),
|
||||
ConfirmedState = table.Column<string>(type: "nvarchar(16)", maxLength: 16, nullable: false),
|
||||
ShelvingState = table.Column<string>(type: "nvarchar(16)", maxLength: 16, nullable: false),
|
||||
ShelvingExpiresUtc = table.Column<DateTime>(type: "datetime2(3)", nullable: true),
|
||||
LastAckUser = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: true),
|
||||
LastAckComment = table.Column<string>(type: "nvarchar(1024)", maxLength: 1024, nullable: true),
|
||||
LastAckUtc = table.Column<DateTime>(type: "datetime2(3)", nullable: true),
|
||||
LastConfirmUser = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: true),
|
||||
LastConfirmComment = table.Column<string>(type: "nvarchar(1024)", maxLength: 1024, nullable: true),
|
||||
LastConfirmUtc = table.Column<DateTime>(type: "datetime2(3)", nullable: true),
|
||||
CommentsJson = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
UpdatedAtUtc = table.Column<DateTime>(type: "datetime2(3)", nullable: false, defaultValueSql: "SYSUTCDATETIME()")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ScriptedAlarmState", x => x.ScriptedAlarmId);
|
||||
table.CheckConstraint("CK_ScriptedAlarmState_CommentsJson_IsJson", "ISJSON(CommentsJson) = 1");
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "VirtualTag",
|
||||
columns: table => new
|
||||
{
|
||||
VirtualTagRowId = table.Column<Guid>(type: "uniqueidentifier", nullable: false, defaultValueSql: "NEWSEQUENTIALID()"),
|
||||
GenerationId = table.Column<long>(type: "bigint", nullable: false),
|
||||
VirtualTagId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||
EquipmentId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
DataType = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: false),
|
||||
ScriptId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
ChangeTriggered = table.Column<bool>(type: "bit", nullable: false),
|
||||
TimerIntervalMs = table.Column<int>(type: "int", nullable: true),
|
||||
Historize = table.Column<bool>(type: "bit", nullable: false),
|
||||
Enabled = table.Column<bool>(type: "bit", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_VirtualTag", x => x.VirtualTagRowId);
|
||||
table.CheckConstraint("CK_VirtualTag_TimerInterval_Min", "TimerIntervalMs IS NULL OR TimerIntervalMs >= 50");
|
||||
table.CheckConstraint("CK_VirtualTag_Trigger_AtLeastOne", "ChangeTriggered = 1 OR TimerIntervalMs IS NOT NULL");
|
||||
table.ForeignKey(
|
||||
name: "FK_VirtualTag_ConfigGeneration_GenerationId",
|
||||
column: x => x.GenerationId,
|
||||
principalTable: "ConfigGeneration",
|
||||
principalColumn: "GenerationId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Script_Generation_SourceHash",
|
||||
table: "Script",
|
||||
columns: new[] { "GenerationId", "SourceHash" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_Script_Generation_LogicalId",
|
||||
table: "Script",
|
||||
columns: new[] { "GenerationId", "ScriptId" },
|
||||
unique: true,
|
||||
filter: "[ScriptId] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ScriptedAlarm_Generation_Script",
|
||||
table: "ScriptedAlarm",
|
||||
columns: new[] { "GenerationId", "PredicateScriptId" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_ScriptedAlarm_Generation_EquipmentPath",
|
||||
table: "ScriptedAlarm",
|
||||
columns: new[] { "GenerationId", "EquipmentId", "Name" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_ScriptedAlarm_Generation_LogicalId",
|
||||
table: "ScriptedAlarm",
|
||||
columns: new[] { "GenerationId", "ScriptedAlarmId" },
|
||||
unique: true,
|
||||
filter: "[ScriptedAlarmId] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_VirtualTag_Generation_Script",
|
||||
table: "VirtualTag",
|
||||
columns: new[] { "GenerationId", "ScriptId" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_VirtualTag_Generation_EquipmentPath",
|
||||
table: "VirtualTag",
|
||||
columns: new[] { "GenerationId", "EquipmentId", "Name" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_VirtualTag_Generation_LogicalId",
|
||||
table: "VirtualTag",
|
||||
columns: new[] { "GenerationId", "VirtualTagId" },
|
||||
unique: true,
|
||||
filter: "[VirtualTagId] IS NOT NULL");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Script");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ScriptedAlarm");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ScriptedAlarmState");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "VirtualTag");
|
||||
}
|
||||
}
|
||||
}
|
||||
-1793
File diff suppressed because it is too large
Load Diff
-232
@@ -1,232 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
{
|
||||
/// <summary>
|
||||
/// Phase 7 follow-up (task #241) — extends <c>dbo.sp_ComputeGenerationDiff</c> to emit
|
||||
/// Script / VirtualTag / ScriptedAlarm rows alongside the existing Namespace /
|
||||
/// DriverInstance / Equipment / Tag / NodeAcl output. Admin DiffViewer now shows
|
||||
/// Phase 7 changes between generations.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Logical ids: ScriptId, VirtualTagId, ScriptedAlarmId — stable across generations
|
||||
/// so a Script whose source changes surfaces as Modified (CHECKSUM picks up the
|
||||
/// SourceHash delta) while a renamed script surfaces as Modified on Name alone.
|
||||
/// ScriptedAlarmState is deliberately excluded — it's not generation-scoped, so
|
||||
/// diffing it between generations is meaningless.
|
||||
/// </remarks>
|
||||
/// <inheritdoc />
|
||||
public partial class ExtendComputeGenerationDiffWithPhase7 : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql(Procs.ComputeGenerationDiffV3);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql(Procs.ComputeGenerationDiffV2);
|
||||
}
|
||||
|
||||
private static class Procs
|
||||
{
|
||||
/// <summary>V3 — adds Script / VirtualTag / ScriptedAlarm sections.</summary>
|
||||
public const string ComputeGenerationDiffV3 = @"
|
||||
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
|
||||
";
|
||||
|
||||
/// <summary>V2 — restores the pre-Phase-7 proc on Down().</summary>
|
||||
public const string ComputeGenerationDiffV2 = @"
|
||||
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;
|
||||
|
||||
SELECT TableName, LogicalId, ChangeKind FROM #diff;
|
||||
DROP TABLE #diff;
|
||||
END
|
||||
";
|
||||
}
|
||||
}
|
||||
}
|
||||
-120
@@ -1,120 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
{
|
||||
/// <summary>
|
||||
/// Admin-008: adds <c>@ReleasedBy</c> parameter to
|
||||
/// <c>dbo.sp_ReleaseExternalIdReservation</c> so the operator principal name (the LDAP
|
||||
/// sign-in) is recorded in <c>ExternalIdReservation.ReleasedBy</c> and the
|
||||
/// <c>ConfigAuditLog.Principal</c> column.
|
||||
///
|
||||
/// Prior to this migration the proc used <c>SUSER_SNAME()</c> for both columns, which
|
||||
/// recorded the shared SQL service account rather than the Admin-UI operator who performed
|
||||
/// the release — making the audit trail useless for attribution. The stored proc now
|
||||
/// accepts <c>@ReleasedBy nvarchar(128)</c> and uses it for both columns; validation
|
||||
/// rejects a null/empty value the same way <c>@ReleaseReason</c> is validated.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public partial class AddReleasedByToReleaseExternalIdReservation : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql(Procs.ReleaseExternalIdReservationV2);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql(Procs.ReleaseExternalIdReservationV1);
|
||||
}
|
||||
|
||||
private static class Procs
|
||||
{
|
||||
/// <summary>V2 — accepts <c>@ReleasedBy</c> for proper operator attribution.</summary>
|
||||
public const string ReleaseExternalIdReservationV2 = @"
|
||||
CREATE OR ALTER PROCEDURE dbo.sp_ReleaseExternalIdReservation
|
||||
@Kind nvarchar(16),
|
||||
@Value nvarchar(64),
|
||||
@ReleaseReason nvarchar(512),
|
||||
@ReleasedBy nvarchar(128)
|
||||
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
|
||||
|
||||
IF @ReleasedBy IS NULL OR LEN(@ReleasedBy) = 0
|
||||
BEGIN
|
||||
RAISERROR('ReleasedBy is required', 16, 1);
|
||||
RETURN;
|
||||
END
|
||||
|
||||
UPDATE dbo.ExternalIdReservation
|
||||
SET ReleasedAt = SYSUTCDATETIME(),
|
||||
ReleasedBy = @ReleasedBy,
|
||||
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 all 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 (@ReleasedBy, 'ExternalIdReleased',
|
||||
CONCAT('{""kind"":""', STRING_ESCAPE(@Kind, 'json'),
|
||||
'"",""value"":""', STRING_ESCAPE(@Value, 'json'), '""}'));
|
||||
END
|
||||
";
|
||||
|
||||
/// <summary>V1 — original proc (uses SUSER_SNAME() for attribution). Restored on Down().</summary>
|
||||
public const string ReleaseExternalIdReservationV1 = @"
|
||||
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
|
||||
";
|
||||
}
|
||||
}
|
||||
}
|
||||
-1562
File diff suppressed because it is too large
Load Diff
-1755
File diff suppressed because it is too large
Load Diff
-50
@@ -1,50 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddConfigAuditLogEventIdColumns : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "CorrelationId",
|
||||
table: "ConfigAuditLog",
|
||||
type: "uniqueidentifier",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "EventId",
|
||||
table: "ConfigAuditLog",
|
||||
type: "uniqueidentifier",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_ConfigAuditLog_EventId",
|
||||
table: "ConfigAuditLog",
|
||||
column: "EventId",
|
||||
unique: true,
|
||||
filter: "[EventId] IS NOT NULL");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "UX_ConfigAuditLog_EventId",
|
||||
table: "ConfigAuditLog");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CorrelationId",
|
||||
table: "ConfigAuditLog");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "EventId",
|
||||
table: "ConfigAuditLog");
|
||||
}
|
||||
}
|
||||
}
|
||||
-1755
File diff suppressed because it is too large
Load Diff
-39
@@ -1,39 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
{
|
||||
/// <summary>
|
||||
/// Task 1.7 — canonicalizes the control-plane admin role VALUES persisted in the
|
||||
/// <c>LdapGroupRoleMapping.Role</c> column. The column stores the <c>AdminRole</c> enum
|
||||
/// member name as a string (EF <c>HasConversion<string></c>, <c>nvarchar(32)</c>);
|
||||
/// renaming the enum members (<c>ConfigViewer → Viewer</c>, <c>ConfigEditor → Designer</c>,
|
||||
/// <c>FleetAdmin → Administrator</c>) therefore requires rewriting existing rows so the C#
|
||||
/// enum and the stored strings stay in sync.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is a pure DATA migration: the schema (column type, length, indexes) is unchanged,
|
||||
/// so the model snapshot is byte-identical to the prior migration. The new canonical strings
|
||||
/// ("Viewer" = 6, "Designer" = 8, "Administrator" = 13 chars) all fit the existing
|
||||
/// <c>nvarchar(32)</c> column. Enforcement semantics are preserved — it is a rename only.
|
||||
/// </remarks>
|
||||
public partial class CanonicalizeAdminRoles : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql("UPDATE [LdapGroupRoleMapping] SET [Role] = N'Viewer' WHERE [Role] = N'ConfigViewer';");
|
||||
migrationBuilder.Sql("UPDATE [LdapGroupRoleMapping] SET [Role] = N'Designer' WHERE [Role] = N'ConfigEditor';");
|
||||
migrationBuilder.Sql("UPDATE [LdapGroupRoleMapping] SET [Role] = N'Administrator' WHERE [Role] = N'FleetAdmin';");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql("UPDATE [LdapGroupRoleMapping] SET [Role] = N'FleetAdmin' WHERE [Role] = N'Administrator';");
|
||||
migrationBuilder.Sql("UPDATE [LdapGroupRoleMapping] SET [Role] = N'ConfigEditor' WHERE [Role] = N'Designer';");
|
||||
migrationBuilder.Sql("UPDATE [LdapGroupRoleMapping] SET [Role] = N'ConfigViewer' WHERE [Role] = N'Viewer';");
|
||||
}
|
||||
}
|
||||
}
|
||||
-1759
File diff suppressed because it is too large
Load Diff
-35
@@ -1,35 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
{
|
||||
/// <summary>
|
||||
/// Task 2.2 — adds the nullable <c>Outcome</c> column to <c>ConfigAuditLog</c> for the
|
||||
/// canonical <c>ZB.MOM.WW.Audit.AuditOutcome</c> (stored as its enum member name,
|
||||
/// <c>nvarchar(16)</c>, mirroring how <c>AdminRole</c> is persisted). Purely additive:
|
||||
/// nullable with no backfill, so existing rows and the bespoke stored-procedure audit
|
||||
/// path (which does not derive an outcome) keep writing NULL.
|
||||
/// </summary>
|
||||
public partial class AddConfigAuditLogOutcome : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Outcome",
|
||||
table: "ConfigAuditLog",
|
||||
type: "nvarchar(16)",
|
||||
maxLength: 16,
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Outcome",
|
||||
table: "ConfigAuditLog");
|
||||
}
|
||||
}
|
||||
}
|
||||
-1758
File diff suppressed because it is too large
Load Diff
-42
@@ -1,42 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class NullableEquipmentDriverInstanceId : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "DriverInstanceId",
|
||||
table: "Equipment",
|
||||
type: "nvarchar(64)",
|
||||
maxLength: 64,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "nvarchar(64)",
|
||||
oldMaxLength: 64);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// WARNING: this rollback converts any existing NULL (driver-less) rows to "" for DriverInstanceId.
|
||||
// Only safe when no driver-less equipment exists in the database.
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "DriverInstanceId",
|
||||
table: "Equipment",
|
||||
type: "nvarchar(64)",
|
||||
maxLength: 64,
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "nvarchar(64)",
|
||||
oldMaxLength: 64,
|
||||
oldNullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
-1758
File diff suppressed because it is too large
Load Diff
-181
@@ -1,181 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
{
|
||||
/// <summary>
|
||||
/// Forward-only DATA-cleanup migration for the Galaxy clean break: the
|
||||
/// <c>NamespaceKind.SystemPlatform</c> enum member was removed (Galaxy is now an
|
||||
/// Equipment-kind driver). <c>Namespace.Kind</c> is persisted as a string via
|
||||
/// <c>HasConversion<string></c>, so any leftover row with <c>Kind = 'SystemPlatform'</c>
|
||||
/// would throw at materialisation time (no matching enum member) the moment the new code
|
||||
/// reads the table. This deletes those namespaces plus everything that hung off them.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// There is NO schema/model change in this migration — the model snapshot is unchanged.
|
||||
/// The unique index <c>UX_Namespace_Cluster_Kind</c> (unique on <c>ClusterId, Kind</c>) is
|
||||
/// intentionally KEPT: <c>Simulated</c> remains a reserved kind, so one-of-each-kind-per-cluster
|
||||
/// is still the correct constraint.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The cross-table relationships in this graph are LOGICAL foreign keys (string Id columns,
|
||||
/// e.g. <c>DriverInstance.NamespaceId</c> → <c>Namespace.NamespaceId</c>,
|
||||
/// <c>Tag.DriverInstanceId</c> → <c>DriverInstance.DriverInstanceId</c>) enforced by
|
||||
/// <c>sp_ValidateDraft</c>, NOT physical DB FK constraints — only the <c>*.ClusterId</c>
|
||||
/// relationships are real DB FKs. So no FK constraint can be violated here. The deletes are
|
||||
/// nonetheless ordered child→parent so no orphaned rows are left behind, and every statement
|
||||
/// is keyed off the set of SystemPlatform namespaces, so on a DB with zero such rows every
|
||||
/// DELETE simply affects 0 rows (idempotent / safe to run repeatedly).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Two tables carry LOGICAL refs with no physical FK and so were easy to miss: <c>NodeAcl</c>
|
||||
/// (per-scope grants keyed by <c>ScopeKind</c> + <c>ScopeId</c>) is cleaned for the
|
||||
/// Namespace/Equipment/FolderSegment/Tag scopes that get deleted, and
|
||||
/// <c>ExternalIdReservation</c> (fleet-wide ZTag/SAPID reservations keyed by
|
||||
/// <c>EquipmentUuid</c>) is RELEASED — not deleted — for the affected equipment so the
|
||||
/// reserved external IDs free up while the audit row survives. Both run before the entity
|
||||
/// rows they reference are removed so the id subqueries still resolve.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <inheritdoc />
|
||||
public partial class CleanupSystemPlatformNamespaces : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// Delete in child -> parent order. Everything is keyed off the set of namespaces whose
|
||||
// Kind = 'SystemPlatform' (the string form persisted by HasConversion<string>), funnelled
|
||||
// through the DriverInstances that lived in those namespaces.
|
||||
migrationBuilder.Sql(@"
|
||||
-- Defensive: SystemPlatform namespaces historically held only EquipmentId-NULL mirror tags, but a
|
||||
-- stray equipment-scoped tag / Equipment could be bound to a SystemPlatform-namespace driver. Clean
|
||||
-- both, plus anything that FKs into the affected Equipment (VirtualTag / ScriptedAlarm / state).
|
||||
|
||||
-- Logical-reference cleanup that must run FIRST -------------------------------------------
|
||||
-- NodeAcl rows carry purely LOGICAL scope refs (ScopeKind + ScopeId, no physical FK). When the
|
||||
-- scoped entity is deleted below the ScopeId dangles and the ACL evaluator would still match it,
|
||||
-- silently granting/denying on a node that no longer exists. ScopeKind persists as a string via
|
||||
-- HasConversion<string>, so the values below ('Namespace'/'Equipment'/'FolderSegment'/'Tag') are
|
||||
-- the literal enum member names. These deletes must read the entity-id subqueries while those rows
|
||||
-- still exist, hence they run before the entity DELETEs further down.
|
||||
|
||||
-- Namespace-scoped grants pointing at the SystemPlatform namespaces themselves.
|
||||
DELETE FROM [NodeAcl]
|
||||
WHERE [ScopeKind] = 'Namespace'
|
||||
AND [ScopeId] IN (
|
||||
SELECT [NamespaceId] FROM [Namespace] WHERE [Kind] = 'SystemPlatform');
|
||||
|
||||
-- Equipment- and FolderSegment-scoped grants pointing at Equipment that hangs off the affected
|
||||
-- DriverInstances (both share Equipment.EquipmentId as the ScopeId).
|
||||
DELETE FROM [NodeAcl]
|
||||
WHERE [ScopeKind] IN ('Equipment', 'FolderSegment')
|
||||
AND [ScopeId] IN (
|
||||
SELECT [EquipmentId] FROM [Equipment]
|
||||
WHERE [DriverInstanceId] IN (
|
||||
SELECT [DriverInstanceId] FROM [DriverInstance]
|
||||
WHERE [NamespaceId] IN (
|
||||
SELECT [NamespaceId] FROM [Namespace] WHERE [Kind] = 'SystemPlatform')));
|
||||
|
||||
-- Tag-scoped grants pointing at Tags bound to the affected DriverInstances (ScopeId = Tag.TagId).
|
||||
DELETE FROM [NodeAcl]
|
||||
WHERE [ScopeKind] = 'Tag'
|
||||
AND [ScopeId] IN (
|
||||
SELECT [TagId] FROM [Tag]
|
||||
WHERE [DriverInstanceId] IN (
|
||||
SELECT [DriverInstanceId] FROM [DriverInstance]
|
||||
WHERE [NamespaceId] IN (
|
||||
SELECT [NamespaceId] FROM [Namespace] WHERE [Kind] = 'SystemPlatform')));
|
||||
|
||||
-- ExternalIdReservation is NOT generation-versioned and must outlive equipment deletion as a
|
||||
-- RELEASE, not a delete: active rows (ReleasedAt IS NULL) hold a fleet-wide unique reservation of
|
||||
-- the ZTag/SAPID under a filtered unique index. If we left them active after dropping the
|
||||
-- Equipment, the same external IDs could never be reused. Release them so the IDs free up while the
|
||||
-- audit row survives. Run BEFORE the Equipment DELETE so the EquipmentUuid subquery still resolves.
|
||||
UPDATE [ExternalIdReservation]
|
||||
SET [ReleasedAt] = SYSUTCDATETIME(),
|
||||
[ReleasedBy] = 'CleanupSystemPlatformNamespaces',
|
||||
[ReleaseReason] = 'Equipment removed by SystemPlatform-namespace cleanup migration (retired NamespaceKind).'
|
||||
WHERE [ReleasedAt] IS NULL
|
||||
AND [EquipmentUuid] IN (
|
||||
SELECT [EquipmentUuid] FROM [Equipment]
|
||||
WHERE [DriverInstanceId] IN (
|
||||
SELECT [DriverInstanceId] FROM [DriverInstance]
|
||||
WHERE [NamespaceId] IN (
|
||||
SELECT [NamespaceId] FROM [Namespace] WHERE [Kind] = 'SystemPlatform')));
|
||||
|
||||
-- Grandchildren of the affected Equipment ------------------------------------------------
|
||||
-- ScriptedAlarmState is keyed by ScriptedAlarm.ScriptedAlarmId; remove before its ScriptedAlarm.
|
||||
DELETE FROM [ScriptedAlarmState]
|
||||
WHERE [ScriptedAlarmId] IN (
|
||||
SELECT [ScriptedAlarmId] FROM [ScriptedAlarm]
|
||||
WHERE [EquipmentId] IN (
|
||||
SELECT [EquipmentId] FROM [Equipment]
|
||||
WHERE [DriverInstanceId] IN (
|
||||
SELECT [DriverInstanceId] FROM [DriverInstance]
|
||||
WHERE [NamespaceId] IN (
|
||||
SELECT [NamespaceId] FROM [Namespace] WHERE [Kind] = 'SystemPlatform'))));
|
||||
|
||||
DELETE FROM [ScriptedAlarm]
|
||||
WHERE [EquipmentId] IN (
|
||||
SELECT [EquipmentId] FROM [Equipment]
|
||||
WHERE [DriverInstanceId] IN (
|
||||
SELECT [DriverInstanceId] FROM [DriverInstance]
|
||||
WHERE [NamespaceId] IN (
|
||||
SELECT [NamespaceId] FROM [Namespace] WHERE [Kind] = 'SystemPlatform')));
|
||||
|
||||
DELETE FROM [VirtualTag]
|
||||
WHERE [EquipmentId] IN (
|
||||
SELECT [EquipmentId] FROM [Equipment]
|
||||
WHERE [DriverInstanceId] IN (
|
||||
SELECT [DriverInstanceId] FROM [DriverInstance]
|
||||
WHERE [NamespaceId] IN (
|
||||
SELECT [NamespaceId] FROM [Namespace] WHERE [Kind] = 'SystemPlatform')));
|
||||
|
||||
-- Children of the affected DriverInstances ------------------------------------------------
|
||||
-- Tags bound to those drivers: both EquipmentId-NULL mirror tags AND any equipment-scoped tag.
|
||||
DELETE FROM [Tag]
|
||||
WHERE [DriverInstanceId] IN (
|
||||
SELECT [DriverInstanceId] FROM [DriverInstance]
|
||||
WHERE [NamespaceId] IN (
|
||||
SELECT [NamespaceId] FROM [Namespace] WHERE [Kind] = 'SystemPlatform'));
|
||||
|
||||
DELETE FROM [PollGroup]
|
||||
WHERE [DriverInstanceId] IN (
|
||||
SELECT [DriverInstanceId] FROM [DriverInstance]
|
||||
WHERE [NamespaceId] IN (
|
||||
SELECT [NamespaceId] FROM [Namespace] WHERE [Kind] = 'SystemPlatform'));
|
||||
|
||||
DELETE FROM [Device]
|
||||
WHERE [DriverInstanceId] IN (
|
||||
SELECT [DriverInstanceId] FROM [DriverInstance]
|
||||
WHERE [NamespaceId] IN (
|
||||
SELECT [NamespaceId] FROM [Namespace] WHERE [Kind] = 'SystemPlatform'));
|
||||
|
||||
DELETE FROM [Equipment]
|
||||
WHERE [DriverInstanceId] IN (
|
||||
SELECT [DriverInstanceId] FROM [DriverInstance]
|
||||
WHERE [NamespaceId] IN (
|
||||
SELECT [NamespaceId] FROM [Namespace] WHERE [Kind] = 'SystemPlatform'));
|
||||
|
||||
-- The DriverInstances themselves ----------------------------------------------------------
|
||||
DELETE FROM [DriverInstance]
|
||||
WHERE [NamespaceId] IN (
|
||||
SELECT [NamespaceId] FROM [Namespace] WHERE [Kind] = 'SystemPlatform');
|
||||
|
||||
-- The SystemPlatform namespaces (the rows that would crash EF on read) --------------------
|
||||
DELETE FROM [Namespace] WHERE [Kind] = 'SystemPlatform';
|
||||
");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// Forward-only data cleanup: the deleted SystemPlatform namespaces and their dependent
|
||||
// rows cannot be reconstructed (the data is gone, and SystemPlatform is no longer a valid
|
||||
// NamespaceKind). Intentionally a no-op rather than a throw, so rolling back a LATER
|
||||
// migration does not blow up on this one.
|
||||
}
|
||||
}
|
||||
}
|
||||
+226
-274
@@ -12,8 +12,8 @@ using ZB.MOM.WW.OtOpcUa.Configuration;
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
{
|
||||
[DbContext(typeof(OtOpcUaConfigDbContext))]
|
||||
[Migration("20260526081556_V2HostingAlignment")]
|
||||
partial class V2HostingAlignment
|
||||
[Migration("20260715230637_V3Initial")]
|
||||
partial class V3Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
@@ -168,9 +168,15 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<Guid?>("CorrelationId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("DetailsJson")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<Guid?>("EventId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("EventType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
@@ -183,6 +189,10 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("Outcome")
|
||||
.HasMaxLength(16)
|
||||
.HasColumnType("nvarchar(16)");
|
||||
|
||||
b.Property<string>("Principal")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
@@ -195,6 +205,11 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
|
||||
b.HasKey("AuditId");
|
||||
|
||||
b.HasIndex("EventId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_ConfigAuditLog_EventId")
|
||||
.HasFilter("[EventId] IS NOT NULL");
|
||||
|
||||
b.HasIndex("GenerationId")
|
||||
.HasDatabaseName("IX_ConfigAuditLog_Generation")
|
||||
.HasFilter("[GenerationId] IS NOT NULL");
|
||||
@@ -361,6 +376,10 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
b.HasIndex("DriverInstanceId")
|
||||
.HasDatabaseName("IX_Device_Driver");
|
||||
|
||||
b.HasIndex("DriverInstanceId", "Name")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_Device_Driver_Name");
|
||||
|
||||
b.ToTable("Device", null, t =>
|
||||
{
|
||||
t.HasCheckConstraint("CK_Device_DeviceConfig_IsJson", "ISJSON(DeviceConfig) = 1");
|
||||
@@ -440,8 +459,7 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("NamespaceId")
|
||||
.IsRequired()
|
||||
b.Property<string>("RawFolderId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
@@ -464,8 +482,18 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
.HasDatabaseName("UX_DriverInstance_LogicalId")
|
||||
.HasFilter("[DriverInstanceId] IS NOT NULL");
|
||||
|
||||
b.HasIndex("NamespaceId")
|
||||
.HasDatabaseName("IX_DriverInstance_Namespace");
|
||||
b.HasIndex("RawFolderId")
|
||||
.HasDatabaseName("IX_DriverInstance_RawFolder");
|
||||
|
||||
b.HasIndex("ClusterId", "Name")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_DriverInstance_ClusterRoot_Name")
|
||||
.HasFilter("[RawFolderId] IS NULL");
|
||||
|
||||
b.HasIndex("ClusterId", "RawFolderId", "Name")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_DriverInstance_Folder_Name")
|
||||
.HasFilter("[RawFolderId] IS NOT NULL");
|
||||
|
||||
b.ToTable("DriverInstance", null, t =>
|
||||
{
|
||||
@@ -525,19 +553,10 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("DeviceId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("DeviceManualUri")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<string>("DriverInstanceId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
@@ -610,9 +629,6 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
|
||||
b.HasKey("EquipmentRowId");
|
||||
|
||||
b.HasIndex("DriverInstanceId")
|
||||
.HasDatabaseName("IX_Equipment_Driver");
|
||||
|
||||
b.HasIndex("EquipmentId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_Equipment_LogicalId")
|
||||
@@ -643,148 +659,6 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
b.ToTable("Equipment", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.EquipmentImportBatch", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("ClusterId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<DateTime>("CreatedAtUtc")
|
||||
.HasColumnType("datetime2(3)");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<DateTime?>("FinalisedAtUtc")
|
||||
.HasColumnType("datetime2(3)");
|
||||
|
||||
b.Property<int>("RowsAccepted")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("RowsRejected")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("RowsStaged")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedBy", "FinalisedAtUtc")
|
||||
.HasDatabaseName("IX_EquipmentImportBatch_Creator_Finalised");
|
||||
|
||||
b.ToTable("EquipmentImportBatch", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.EquipmentImportRow", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("AssetLocation")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<Guid>("BatchId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("DeviceManualUri")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<string>("EquipmentId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("EquipmentUuid")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("HardwareRevision")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<bool>("IsAccepted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<int>("LineNumberInFile")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("MachineCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("Manufacturer")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("ManufacturerUri")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("RejectReason")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<string>("SAPID")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("SerialNumber")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("SoftwareRevision")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("UnsAreaName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("UnsLineName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("YearOfConstruction")
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("nvarchar(8)");
|
||||
|
||||
b.Property<string>("ZTag")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BatchId")
|
||||
.HasDatabaseName("IX_EquipmentImportRow_Batch");
|
||||
|
||||
b.ToTable("EquipmentImportRow", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ExternalIdReservation", b =>
|
||||
{
|
||||
b.Property<Guid>("ReservationId")
|
||||
@@ -894,66 +768,6 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
b.ToTable("LdapGroupRoleMapping", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.Namespace", b =>
|
||||
{
|
||||
b.Property<Guid>("NamespaceRowId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||
|
||||
b.Property<string>("ClusterId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Kind")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("NamespaceId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("NamespaceUri")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("nvarchar(1024)");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.HasKey("NamespaceRowId");
|
||||
|
||||
b.HasIndex("ClusterId")
|
||||
.HasDatabaseName("IX_Namespace_Cluster");
|
||||
|
||||
b.HasIndex("NamespaceId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_Namespace_LogicalId")
|
||||
.HasFilter("[NamespaceId] IS NOT NULL");
|
||||
|
||||
b.HasIndex("NamespaceUri")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_Namespace_NamespaceUri");
|
||||
|
||||
b.HasIndex("ClusterId", "Kind")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_Namespace_Cluster_Kind");
|
||||
|
||||
b.ToTable("Namespace", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.NodeAcl", b =>
|
||||
{
|
||||
b.Property<Guid>("NodeAclRowId")
|
||||
@@ -1109,6 +923,63 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.RawFolder", b =>
|
||||
{
|
||||
b.Property<Guid>("RawFolderRowId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||
|
||||
b.Property<string>("ClusterId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("ParentRawFolderId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("RawFolderId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("RawFolderRowId");
|
||||
|
||||
b.HasIndex("ClusterId")
|
||||
.HasDatabaseName("IX_RawFolder_Cluster");
|
||||
|
||||
b.HasIndex("RawFolderId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_RawFolder_LogicalId")
|
||||
.HasFilter("[RawFolderId] IS NOT NULL");
|
||||
|
||||
b.HasIndex("ClusterId", "Name")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_RawFolder_ClusterRoot_Name")
|
||||
.HasFilter("[ParentRawFolderId] IS NULL");
|
||||
|
||||
b.HasIndex("ClusterId", "ParentRawFolderId", "Name")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_RawFolder_Parent_Name")
|
||||
.HasFilter("[ParentRawFolderId] IS NOT NULL");
|
||||
|
||||
b.ToTable("RawFolder", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.Script", b =>
|
||||
{
|
||||
b.Property<Guid>("ScriptRowId")
|
||||
@@ -1388,22 +1259,10 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("DeviceId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("DriverInstanceId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("EquipmentId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("FolderPath")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
@@ -1423,6 +1282,10 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("TagGroupId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("TagId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
@@ -1432,27 +1295,23 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
|
||||
b.HasKey("TagRowId");
|
||||
|
||||
b.HasIndex("EquipmentId")
|
||||
.HasDatabaseName("IX_Tag_Equipment")
|
||||
.HasFilter("[EquipmentId] IS NOT NULL");
|
||||
|
||||
b.HasIndex("TagId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_Tag_LogicalId")
|
||||
.HasFilter("[TagId] IS NOT NULL");
|
||||
|
||||
b.HasIndex("DriverInstanceId", "DeviceId")
|
||||
.HasDatabaseName("IX_Tag_Driver_Device");
|
||||
|
||||
b.HasIndex("EquipmentId", "Name")
|
||||
b.HasIndex("DeviceId", "Name")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_Tag_EquipmentPath")
|
||||
.HasFilter("[EquipmentId] IS NOT NULL");
|
||||
.HasDatabaseName("UX_Tag_Device_Name")
|
||||
.HasFilter("[TagGroupId] IS NULL");
|
||||
|
||||
b.HasIndex("DriverInstanceId", "FolderPath", "Name")
|
||||
b.HasIndex("DeviceId", "TagGroupId")
|
||||
.HasDatabaseName("IX_Tag_Device");
|
||||
|
||||
b.HasIndex("DeviceId", "TagGroupId", "Name")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_Tag_FolderPath")
|
||||
.HasFilter("[EquipmentId] IS NULL");
|
||||
.HasDatabaseName("UX_Tag_Group_Name")
|
||||
.HasFilter("[TagGroupId] IS NOT NULL");
|
||||
|
||||
b.ToTable("Tag", null, t =>
|
||||
{
|
||||
@@ -1460,6 +1319,63 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.TagGroup", b =>
|
||||
{
|
||||
b.Property<Guid>("TagGroupRowId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||
|
||||
b.Property<string>("DeviceId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("ParentTagGroupId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("TagGroupId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.HasKey("TagGroupRowId");
|
||||
|
||||
b.HasIndex("DeviceId")
|
||||
.HasDatabaseName("IX_TagGroup_Device");
|
||||
|
||||
b.HasIndex("TagGroupId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_TagGroup_LogicalId")
|
||||
.HasFilter("[TagGroupId] IS NOT NULL");
|
||||
|
||||
b.HasIndex("DeviceId", "Name")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_TagGroup_DeviceRoot_Name")
|
||||
.HasFilter("[ParentTagGroupId] IS NULL");
|
||||
|
||||
b.HasIndex("DeviceId", "ParentTagGroupId", "Name")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_TagGroup_Parent_Name")
|
||||
.HasFilter("[ParentTagGroupId] IS NOT NULL");
|
||||
|
||||
b.ToTable("TagGroup", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.UnsArea", b =>
|
||||
{
|
||||
b.Property<Guid>("UnsAreaRowId")
|
||||
@@ -1556,6 +1472,60 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
b.ToTable("UnsLine", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.UnsTagReference", b =>
|
||||
{
|
||||
b.Property<Guid>("UnsTagReferenceRowId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||
|
||||
b.Property<string>("DisplayNameOverride")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("EquipmentId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("TagId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("UnsTagReferenceId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.HasKey("UnsTagReferenceRowId");
|
||||
|
||||
b.HasIndex("EquipmentId")
|
||||
.HasDatabaseName("IX_UnsTagReference_Equipment");
|
||||
|
||||
b.HasIndex("TagId")
|
||||
.HasDatabaseName("IX_UnsTagReference_Tag");
|
||||
|
||||
b.HasIndex("UnsTagReferenceId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_UnsTagReference_LogicalId")
|
||||
.HasFilter("[UnsTagReferenceId] IS NOT NULL");
|
||||
|
||||
b.HasIndex("EquipmentId", "TagId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_UnsTagReference_Equip_Tag");
|
||||
|
||||
b.ToTable("UnsTagReference", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.VirtualTag", b =>
|
||||
{
|
||||
b.Property<Guid>("VirtualTagRowId")
|
||||
@@ -1660,17 +1630,6 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
b.Navigation("Cluster");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.EquipmentImportRow", b =>
|
||||
{
|
||||
b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.EquipmentImportBatch", "Batch")
|
||||
.WithMany("Rows")
|
||||
.HasForeignKey("BatchId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Batch");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.LdapGroupRoleMapping", b =>
|
||||
{
|
||||
b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ServerCluster", "Cluster")
|
||||
@@ -1681,17 +1640,6 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
b.Navigation("Cluster");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.Namespace", b =>
|
||||
{
|
||||
b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ServerCluster", "Cluster")
|
||||
.WithMany("Namespaces")
|
||||
.HasForeignKey("ClusterId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Cluster");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.NodeDeploymentState", b =>
|
||||
{
|
||||
b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.Deployment", "Deployment")
|
||||
@@ -1711,6 +1659,15 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
b.Navigation("Node");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.RawFolder", b =>
|
||||
{
|
||||
b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ServerCluster", null)
|
||||
.WithMany("RawFolders")
|
||||
.HasForeignKey("ClusterId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.UnsArea", b =>
|
||||
{
|
||||
b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ServerCluster", "Cluster")
|
||||
@@ -1727,16 +1684,11 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
b.Navigation("Credentials");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.EquipmentImportBatch", b =>
|
||||
{
|
||||
b.Navigation("Rows");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ServerCluster", b =>
|
||||
{
|
||||
b.Navigation("Namespaces");
|
||||
|
||||
b.Navigation("Nodes");
|
||||
|
||||
b.Navigation("RawFolders");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
{
|
||||
/// <summary>
|
||||
/// Hand-authored SQL squashed into <c>V3Initial</c> from the retired per-migration proc files
|
||||
/// (<c>*_StoredProcedures.cs</c>, <c>*_ExtendComputeGenerationDiffWith*.cs</c>) and
|
||||
/// <c>AuthorizationGrants.cs</c>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>Port audit (v3 WP1).</b> The v1 stored procs were built entirely on the
|
||||
/// <c>ConfigGeneration</c> / <c>GenerationId</c> / <c>ClusterNodeGenerationState</c>
|
||||
/// generation model, which <c>V2HostingAlignment</c> (2026-05-26) dropped outright — so
|
||||
/// those procs have been vestigial since v2 (SQL Server's deferred name resolution let them
|
||||
/// keep existing while referencing phantom tables). No production code calls them; the v2/v3
|
||||
/// deploy + diff path is entirely C# (ControlPlane <c>ConfigComposer</c>,
|
||||
/// <c>AddressSpaceChangeClassifier</c>, <c>DraftValidator</c>). Their only consumer is
|
||||
/// <c>AuthorizationTests</c>, which exercises the DB-role EXECUTE grants and expects
|
||||
/// RAISERROR / permission failures — never a successful data operation.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Under greenfield v3 the two-role authorization model (<c>OtOpcUaNode</c> /
|
||||
/// <c>OtOpcUaAdmin</c> + EXECUTE grants) is <b>retained</b> as a real security feature, so the
|
||||
/// eight proc <b>names</b> must exist for the grants + tests to bind. Each proc is rewritten
|
||||
/// here to be schema-valid against the v3 schema (references NO dropped table/column):
|
||||
/// </para>
|
||||
/// <list type="bullet">
|
||||
/// <item><c>sp_GetCurrentGenerationForCluster</c> keeps its <c>ClusterNodeCredential</c> +
|
||||
/// <c>ClusterNode</c> "Unauthorized" auth check (both tables retained), then RAISERRORs a
|
||||
/// v3-retirement message — preserving the exact behavior AuthorizationTests asserts.</item>
|
||||
/// <item><c>sp_PublishGeneration</c> is granted to <c>OtOpcUaAdmin</c> only and RAISERRORs a
|
||||
/// retirement message (never the word "permission"), so the Node-denied vs Admin-allowed
|
||||
/// grant assertions still hold.</item>
|
||||
/// <item><c>sp_ComputeGenerationDiff</c> is rewritten to reference the v3 tables
|
||||
/// (RawFolder / DriverInstance / Device / TagGroup / Tag / Equipment / UnsTagReference /
|
||||
/// NodeAcl) as a schema-valid current-state signature inventory. A genuine
|
||||
/// generation-to-generation diff is architecturally impossible now (no GenerationId column);
|
||||
/// the real diff lives in the C# ControlPlane.</item>
|
||||
/// <item><c>sp_ReleaseExternalIdReservation</c> is ported verbatim — it only touches retained
|
||||
/// tables (<c>ExternalIdReservation</c>, <c>ConfigAuditLog</c>).</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public partial class V3Initial
|
||||
{
|
||||
private static void CreateStoredProcedures(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql(GetCurrentGenerationForCluster);
|
||||
migrationBuilder.Sql(GetGenerationContent);
|
||||
migrationBuilder.Sql(RegisterNodeGenerationApplied);
|
||||
migrationBuilder.Sql(ValidateDraft);
|
||||
migrationBuilder.Sql(PublishGeneration);
|
||||
migrationBuilder.Sql(RollbackToGeneration);
|
||||
migrationBuilder.Sql(ComputeGenerationDiff);
|
||||
migrationBuilder.Sql(ReleaseExternalIdReservation);
|
||||
}
|
||||
|
||||
private static void DropStoredProcedures(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
foreach (var name in new[]
|
||||
{
|
||||
"sp_ReleaseExternalIdReservation", "sp_ComputeGenerationDiff", "sp_RollbackToGeneration",
|
||||
"sp_PublishGeneration", "sp_ValidateDraft", "sp_RegisterNodeGenerationApplied",
|
||||
"sp_GetGenerationContent", "sp_GetCurrentGenerationForCluster",
|
||||
})
|
||||
{
|
||||
migrationBuilder.Sql($"IF OBJECT_ID(N'dbo.{name}', N'P') IS NOT NULL DROP PROCEDURE dbo.{name};");
|
||||
}
|
||||
}
|
||||
|
||||
private static void CreateAuthorizationGrants(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql(@"
|
||||
IF DATABASE_PRINCIPAL_ID('OtOpcUaNode') IS NULL
|
||||
CREATE ROLE OtOpcUaNode;
|
||||
IF DATABASE_PRINCIPAL_ID('OtOpcUaAdmin') IS NULL
|
||||
CREATE ROLE OtOpcUaAdmin;
|
||||
");
|
||||
|
||||
migrationBuilder.Sql(@"
|
||||
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;
|
||||
");
|
||||
}
|
||||
|
||||
private static void DropAuthorizationGrants(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql(@"
|
||||
IF DATABASE_PRINCIPAL_ID('OtOpcUaNode') IS NOT NULL
|
||||
DROP ROLE OtOpcUaNode;
|
||||
IF DATABASE_PRINCIPAL_ID('OtOpcUaAdmin') IS NOT NULL
|
||||
DROP ROLE OtOpcUaAdmin;
|
||||
");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// v3 retirement stubs. The generation model is gone (see class remarks); these preserve the
|
||||
// authorization surface + the AuthorizationTests behaviors while referencing only v3 tables.
|
||||
// ------------------------------------------------------------------------------------------
|
||||
|
||||
private const string GetCurrentGenerationForCluster = @"
|
||||
CREATE OR ALTER PROCEDURE dbo.sp_GetCurrentGenerationForCluster
|
||||
@NodeId nvarchar(64),
|
||||
@ClusterId nvarchar(64)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
-- Auth surface retained verbatim (ClusterNodeCredential + ClusterNode are v3 tables). An
|
||||
-- unbound caller still gets 'Unauthorized', which AuthorizationTests asserts.
|
||||
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
|
||||
|
||||
RAISERROR('sp_GetCurrentGenerationForCluster is retired under v3: config/deploy state moved to the C# ControlPlane (Deployment table).', 16, 1);
|
||||
END
|
||||
";
|
||||
|
||||
private const string GetGenerationContent = @"
|
||||
CREATE OR ALTER PROCEDURE dbo.sp_GetGenerationContent
|
||||
@NodeId nvarchar(64),
|
||||
@GenerationId bigint
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
RAISERROR('sp_GetGenerationContent is retired under v3: generation content is sealed into Deployment.ArtifactBlob by the C# ControlPlane.', 16, 1);
|
||||
END
|
||||
";
|
||||
|
||||
private const string RegisterNodeGenerationApplied = @"
|
||||
CREATE OR ALTER PROCEDURE dbo.sp_RegisterNodeGenerationApplied
|
||||
@NodeId nvarchar(64),
|
||||
@GenerationId bigint,
|
||||
@Status nvarchar(16),
|
||||
@Error nvarchar(max) = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
RAISERROR('sp_RegisterNodeGenerationApplied is retired under v3: node-apply state is tracked in NodeDeploymentState by the C# ControlPlane.', 16, 1);
|
||||
END
|
||||
";
|
||||
|
||||
private const string ValidateDraft = @"
|
||||
CREATE OR ALTER PROCEDURE dbo.sp_ValidateDraft
|
||||
@DraftGenerationId bigint
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
RAISERROR('sp_ValidateDraft is retired under v3: draft validation runs in the managed DraftValidator (Configuration.Validation).', 16, 1);
|
||||
END
|
||||
";
|
||||
|
||||
private const string PublishGeneration = @"
|
||||
CREATE OR ALTER PROCEDURE dbo.sp_PublishGeneration
|
||||
@ClusterId nvarchar(64),
|
||||
@DraftGenerationId bigint,
|
||||
@Notes nvarchar(1024) = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
-- Message intentionally omits the word 'permission' so AuthorizationTests can distinguish a
|
||||
-- grant failure (Node role) from a body-level error (Admin role reaching the proc body).
|
||||
RAISERROR('sp_PublishGeneration is retired under v3: publishing is done by the C# ControlPlane (ConfigComposer + Deployment).', 16, 1);
|
||||
END
|
||||
";
|
||||
|
||||
private const string RollbackToGeneration = @"
|
||||
CREATE OR ALTER PROCEDURE dbo.sp_RollbackToGeneration
|
||||
@ClusterId nvarchar(64),
|
||||
@TargetGenerationId bigint,
|
||||
@Notes nvarchar(1024) = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
RAISERROR('sp_RollbackToGeneration is retired under v3: rollback is a C# ControlPlane operation over the Deployment history.', 16, 1);
|
||||
END
|
||||
";
|
||||
|
||||
// Rewritten to the v3 schema. There is no GenerationId to diff (the per-generation table model
|
||||
// was dropped in v2), so this emits a current-state signature inventory across the v3 tables —
|
||||
// referencing RawFolder, DriverInstance, Device, TagGroup, Tag, Equipment, UnsTagReference, and
|
||||
// NodeAcl. The genuine generation-to-generation diff now lives in the C# ControlPlane
|
||||
// (AddressSpaceChangeClassifier); this proc is retained only to carry the OtOpcUaAdmin EXECUTE
|
||||
// grant and to keep the v3 table surface referenced from SQL.
|
||||
private const string ComputeGenerationDiff = @"
|
||||
CREATE OR ALTER PROCEDURE dbo.sp_ComputeGenerationDiff
|
||||
@FromGenerationId bigint,
|
||||
@ToGenerationId bigint
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
-- @FromGenerationId / @ToGenerationId are ignored: the v3 config tables are single live-state
|
||||
-- (no GenerationId column). Emit a current-state signature per row so the shape (TableName,
|
||||
-- LogicalId, Sig) still round-trips through any legacy caller.
|
||||
SELECT TableName, LogicalId, Sig FROM (
|
||||
SELECT 'RawFolder' AS TableName, RawFolderId AS LogicalId, CHECKSUM(ClusterId, ParentRawFolderId, Name, SortOrder) AS Sig FROM dbo.RawFolder
|
||||
UNION ALL
|
||||
SELECT 'DriverInstance', DriverInstanceId, CHECKSUM(ClusterId, RawFolderId, Name, DriverType, Enabled, CONVERT(varchar(max), DriverConfig)) FROM dbo.DriverInstance
|
||||
UNION ALL
|
||||
SELECT 'Device', DeviceId, CHECKSUM(DriverInstanceId, Name, Enabled, CONVERT(varchar(max), DeviceConfig)) FROM dbo.Device
|
||||
UNION ALL
|
||||
SELECT 'TagGroup', TagGroupId, CHECKSUM(DeviceId, ParentTagGroupId, Name, SortOrder) FROM dbo.TagGroup
|
||||
UNION ALL
|
||||
SELECT 'Tag', TagId, CHECKSUM(DeviceId, TagGroupId, PollGroupId, Name, DataType, AccessLevel, WriteIdempotent, CONVERT(varchar(max), TagConfig)) FROM dbo.Tag
|
||||
UNION ALL
|
||||
SELECT 'Equipment', EquipmentId, CHECKSUM(EquipmentUuid, UnsLineId, Name, MachineCode, ZTag, SAPID, EquipmentClassRef, Manufacturer, Model, SerialNumber) FROM dbo.Equipment
|
||||
UNION ALL
|
||||
SELECT 'UnsTagReference', UnsTagReferenceId, CHECKSUM(EquipmentId, TagId, DisplayNameOverride, SortOrder) FROM dbo.UnsTagReference
|
||||
UNION ALL
|
||||
SELECT 'NodeAcl', NodeAclId, CHECKSUM(ClusterId, LdapGroup, ScopeKind, ScopeId, PermissionFlags, Notes) FROM dbo.NodeAcl
|
||||
) AS inventory;
|
||||
END
|
||||
";
|
||||
|
||||
// Ported verbatim — touches only retained tables (ExternalIdReservation, ConfigAuditLog).
|
||||
private const string ReleaseExternalIdReservation = @"
|
||||
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
|
||||
";
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+209
-271
@@ -373,6 +373,10 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
b.HasIndex("DriverInstanceId")
|
||||
.HasDatabaseName("IX_Device_Driver");
|
||||
|
||||
b.HasIndex("DriverInstanceId", "Name")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_Device_Driver_Name");
|
||||
|
||||
b.ToTable("Device", null, t =>
|
||||
{
|
||||
t.HasCheckConstraint("CK_Device_DeviceConfig_IsJson", "ISJSON(DeviceConfig) = 1");
|
||||
@@ -452,8 +456,7 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("NamespaceId")
|
||||
.IsRequired()
|
||||
b.Property<string>("RawFolderId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
@@ -476,8 +479,18 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
.HasDatabaseName("UX_DriverInstance_LogicalId")
|
||||
.HasFilter("[DriverInstanceId] IS NOT NULL");
|
||||
|
||||
b.HasIndex("NamespaceId")
|
||||
.HasDatabaseName("IX_DriverInstance_Namespace");
|
||||
b.HasIndex("RawFolderId")
|
||||
.HasDatabaseName("IX_DriverInstance_RawFolder");
|
||||
|
||||
b.HasIndex("ClusterId", "Name")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_DriverInstance_ClusterRoot_Name")
|
||||
.HasFilter("[RawFolderId] IS NULL");
|
||||
|
||||
b.HasIndex("ClusterId", "RawFolderId", "Name")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_DriverInstance_Folder_Name")
|
||||
.HasFilter("[RawFolderId] IS NOT NULL");
|
||||
|
||||
b.ToTable("DriverInstance", null, t =>
|
||||
{
|
||||
@@ -537,18 +550,10 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("DeviceId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("DeviceManualUri")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<string>("DriverInstanceId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
@@ -621,9 +626,6 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
|
||||
b.HasKey("EquipmentRowId");
|
||||
|
||||
b.HasIndex("DriverInstanceId")
|
||||
.HasDatabaseName("IX_Equipment_Driver");
|
||||
|
||||
b.HasIndex("EquipmentId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_Equipment_LogicalId")
|
||||
@@ -654,148 +656,6 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
b.ToTable("Equipment", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.EquipmentImportBatch", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("ClusterId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<DateTime>("CreatedAtUtc")
|
||||
.HasColumnType("datetime2(3)");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<DateTime?>("FinalisedAtUtc")
|
||||
.HasColumnType("datetime2(3)");
|
||||
|
||||
b.Property<int>("RowsAccepted")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("RowsRejected")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("RowsStaged")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedBy", "FinalisedAtUtc")
|
||||
.HasDatabaseName("IX_EquipmentImportBatch_Creator_Finalised");
|
||||
|
||||
b.ToTable("EquipmentImportBatch", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.EquipmentImportRow", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("AssetLocation")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<Guid>("BatchId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("DeviceManualUri")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<string>("EquipmentId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("EquipmentUuid")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("HardwareRevision")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<bool>("IsAccepted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<int>("LineNumberInFile")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("MachineCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("Manufacturer")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("ManufacturerUri")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("RejectReason")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<string>("SAPID")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("SerialNumber")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("SoftwareRevision")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("UnsAreaName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("UnsLineName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("YearOfConstruction")
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("nvarchar(8)");
|
||||
|
||||
b.Property<string>("ZTag")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BatchId")
|
||||
.HasDatabaseName("IX_EquipmentImportRow_Batch");
|
||||
|
||||
b.ToTable("EquipmentImportRow", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ExternalIdReservation", b =>
|
||||
{
|
||||
b.Property<Guid>("ReservationId")
|
||||
@@ -905,66 +765,6 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
b.ToTable("LdapGroupRoleMapping", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.Namespace", b =>
|
||||
{
|
||||
b.Property<Guid>("NamespaceRowId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||
|
||||
b.Property<string>("ClusterId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Kind")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("NamespaceId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("NamespaceUri")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("nvarchar(1024)");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.HasKey("NamespaceRowId");
|
||||
|
||||
b.HasIndex("ClusterId")
|
||||
.HasDatabaseName("IX_Namespace_Cluster");
|
||||
|
||||
b.HasIndex("NamespaceId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_Namespace_LogicalId")
|
||||
.HasFilter("[NamespaceId] IS NOT NULL");
|
||||
|
||||
b.HasIndex("NamespaceUri")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_Namespace_NamespaceUri");
|
||||
|
||||
b.HasIndex("ClusterId", "Kind")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_Namespace_Cluster_Kind");
|
||||
|
||||
b.ToTable("Namespace", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.NodeAcl", b =>
|
||||
{
|
||||
b.Property<Guid>("NodeAclRowId")
|
||||
@@ -1120,6 +920,63 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.RawFolder", b =>
|
||||
{
|
||||
b.Property<Guid>("RawFolderRowId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||
|
||||
b.Property<string>("ClusterId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("ParentRawFolderId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("RawFolderId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("RawFolderRowId");
|
||||
|
||||
b.HasIndex("ClusterId")
|
||||
.HasDatabaseName("IX_RawFolder_Cluster");
|
||||
|
||||
b.HasIndex("RawFolderId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_RawFolder_LogicalId")
|
||||
.HasFilter("[RawFolderId] IS NOT NULL");
|
||||
|
||||
b.HasIndex("ClusterId", "Name")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_RawFolder_ClusterRoot_Name")
|
||||
.HasFilter("[ParentRawFolderId] IS NULL");
|
||||
|
||||
b.HasIndex("ClusterId", "ParentRawFolderId", "Name")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_RawFolder_Parent_Name")
|
||||
.HasFilter("[ParentRawFolderId] IS NOT NULL");
|
||||
|
||||
b.ToTable("RawFolder", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.Script", b =>
|
||||
{
|
||||
b.Property<Guid>("ScriptRowId")
|
||||
@@ -1399,22 +1256,10 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("DeviceId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("DriverInstanceId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("EquipmentId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("FolderPath")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
@@ -1434,6 +1279,10 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("TagGroupId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("TagId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
@@ -1443,27 +1292,23 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
|
||||
b.HasKey("TagRowId");
|
||||
|
||||
b.HasIndex("EquipmentId")
|
||||
.HasDatabaseName("IX_Tag_Equipment")
|
||||
.HasFilter("[EquipmentId] IS NOT NULL");
|
||||
|
||||
b.HasIndex("TagId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_Tag_LogicalId")
|
||||
.HasFilter("[TagId] IS NOT NULL");
|
||||
|
||||
b.HasIndex("DriverInstanceId", "DeviceId")
|
||||
.HasDatabaseName("IX_Tag_Driver_Device");
|
||||
|
||||
b.HasIndex("EquipmentId", "Name")
|
||||
b.HasIndex("DeviceId", "Name")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_Tag_EquipmentPath")
|
||||
.HasFilter("[EquipmentId] IS NOT NULL");
|
||||
.HasDatabaseName("UX_Tag_Device_Name")
|
||||
.HasFilter("[TagGroupId] IS NULL");
|
||||
|
||||
b.HasIndex("DriverInstanceId", "FolderPath", "Name")
|
||||
b.HasIndex("DeviceId", "TagGroupId")
|
||||
.HasDatabaseName("IX_Tag_Device");
|
||||
|
||||
b.HasIndex("DeviceId", "TagGroupId", "Name")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_Tag_FolderPath")
|
||||
.HasFilter("[EquipmentId] IS NULL");
|
||||
.HasDatabaseName("UX_Tag_Group_Name")
|
||||
.HasFilter("[TagGroupId] IS NOT NULL");
|
||||
|
||||
b.ToTable("Tag", null, t =>
|
||||
{
|
||||
@@ -1471,6 +1316,63 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.TagGroup", b =>
|
||||
{
|
||||
b.Property<Guid>("TagGroupRowId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||
|
||||
b.Property<string>("DeviceId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("ParentTagGroupId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("TagGroupId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.HasKey("TagGroupRowId");
|
||||
|
||||
b.HasIndex("DeviceId")
|
||||
.HasDatabaseName("IX_TagGroup_Device");
|
||||
|
||||
b.HasIndex("TagGroupId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_TagGroup_LogicalId")
|
||||
.HasFilter("[TagGroupId] IS NOT NULL");
|
||||
|
||||
b.HasIndex("DeviceId", "Name")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_TagGroup_DeviceRoot_Name")
|
||||
.HasFilter("[ParentTagGroupId] IS NULL");
|
||||
|
||||
b.HasIndex("DeviceId", "ParentTagGroupId", "Name")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_TagGroup_Parent_Name")
|
||||
.HasFilter("[ParentTagGroupId] IS NOT NULL");
|
||||
|
||||
b.ToTable("TagGroup", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.UnsArea", b =>
|
||||
{
|
||||
b.Property<Guid>("UnsAreaRowId")
|
||||
@@ -1567,6 +1469,60 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
b.ToTable("UnsLine", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.UnsTagReference", b =>
|
||||
{
|
||||
b.Property<Guid>("UnsTagReferenceRowId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||
|
||||
b.Property<string>("DisplayNameOverride")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("EquipmentId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("rowversion");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("TagId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("UnsTagReferenceId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.HasKey("UnsTagReferenceRowId");
|
||||
|
||||
b.HasIndex("EquipmentId")
|
||||
.HasDatabaseName("IX_UnsTagReference_Equipment");
|
||||
|
||||
b.HasIndex("TagId")
|
||||
.HasDatabaseName("IX_UnsTagReference_Tag");
|
||||
|
||||
b.HasIndex("UnsTagReferenceId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_UnsTagReference_LogicalId")
|
||||
.HasFilter("[UnsTagReferenceId] IS NOT NULL");
|
||||
|
||||
b.HasIndex("EquipmentId", "TagId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_UnsTagReference_Equip_Tag");
|
||||
|
||||
b.ToTable("UnsTagReference", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.VirtualTag", b =>
|
||||
{
|
||||
b.Property<Guid>("VirtualTagRowId")
|
||||
@@ -1671,17 +1627,6 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
b.Navigation("Cluster");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.EquipmentImportRow", b =>
|
||||
{
|
||||
b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.EquipmentImportBatch", "Batch")
|
||||
.WithMany("Rows")
|
||||
.HasForeignKey("BatchId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Batch");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.LdapGroupRoleMapping", b =>
|
||||
{
|
||||
b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ServerCluster", "Cluster")
|
||||
@@ -1692,17 +1637,6 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
b.Navigation("Cluster");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.Namespace", b =>
|
||||
{
|
||||
b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ServerCluster", "Cluster")
|
||||
.WithMany("Namespaces")
|
||||
.HasForeignKey("ClusterId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Cluster");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.NodeDeploymentState", b =>
|
||||
{
|
||||
b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.Deployment", "Deployment")
|
||||
@@ -1722,6 +1656,15 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
b.Navigation("Node");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.RawFolder", b =>
|
||||
{
|
||||
b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ServerCluster", null)
|
||||
.WithMany("RawFolders")
|
||||
.HasForeignKey("ClusterId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.UnsArea", b =>
|
||||
{
|
||||
b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ServerCluster", "Cluster")
|
||||
@@ -1738,16 +1681,11 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
b.Navigation("Credentials");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.EquipmentImportBatch", b =>
|
||||
{
|
||||
b.Navigation("Rows");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ServerCluster", b =>
|
||||
{
|
||||
b.Navigation("Namespaces");
|
||||
|
||||
b.Navigation("Nodes");
|
||||
|
||||
b.Navigation("RawFolders");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
|
||||
@@ -18,8 +18,12 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
||||
public DbSet<ClusterNode> ClusterNodes => Set<ClusterNode>();
|
||||
/// <summary>Gets the DbSet of cluster node credentials.</summary>
|
||||
public DbSet<ClusterNodeCredential> ClusterNodeCredentials => Set<ClusterNodeCredential>();
|
||||
/// <summary>Gets the DbSet of namespaces.</summary>
|
||||
public DbSet<Namespace> Namespaces => Set<Namespace>();
|
||||
/// <summary>Gets the DbSet of v3 Raw-tree driver-organizing folders.</summary>
|
||||
public DbSet<RawFolder> RawFolders => Set<RawFolder>();
|
||||
/// <summary>Gets the DbSet of v3 Raw-tree tag groups (nestable under a device).</summary>
|
||||
public DbSet<TagGroup> TagGroups => Set<TagGroup>();
|
||||
/// <summary>Gets the DbSet of v3 UNS tag references (equipment → raw tag projection).</summary>
|
||||
public DbSet<UnsTagReference> UnsTagReferences => Set<UnsTagReference>();
|
||||
/// <summary>Gets the DbSet of UNS areas.</summary>
|
||||
public DbSet<UnsArea> UnsAreas => Set<UnsArea>();
|
||||
/// <summary>Gets the DbSet of UNS lines.</summary>
|
||||
@@ -46,10 +50,6 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
||||
public DbSet<DriverInstanceResilienceStatus> DriverInstanceResilienceStatuses => Set<DriverInstanceResilienceStatus>();
|
||||
/// <summary>Gets the DbSet of LDAP group role mappings.</summary>
|
||||
public DbSet<LdapGroupRoleMapping> LdapGroupRoleMappings => Set<LdapGroupRoleMapping>();
|
||||
/// <summary>Gets the DbSet of equipment import batches.</summary>
|
||||
public DbSet<EquipmentImportBatch> EquipmentImportBatches => Set<EquipmentImportBatch>();
|
||||
/// <summary>Gets the DbSet of equipment import rows.</summary>
|
||||
public DbSet<EquipmentImportRow> EquipmentImportRows => Set<EquipmentImportRow>();
|
||||
/// <summary>Gets the DbSet of scripts.</summary>
|
||||
public DbSet<Script> Scripts => Set<Script>();
|
||||
/// <summary>Gets the DbSet of virtual tags.</summary>
|
||||
@@ -79,13 +79,15 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
||||
ConfigureServerCluster(modelBuilder);
|
||||
ConfigureClusterNode(modelBuilder);
|
||||
ConfigureClusterNodeCredential(modelBuilder);
|
||||
ConfigureNamespace(modelBuilder);
|
||||
ConfigureRawFolder(modelBuilder);
|
||||
ConfigureUnsArea(modelBuilder);
|
||||
ConfigureUnsLine(modelBuilder);
|
||||
ConfigureDriverInstance(modelBuilder);
|
||||
ConfigureDevice(modelBuilder);
|
||||
ConfigureTagGroup(modelBuilder);
|
||||
ConfigureEquipment(modelBuilder);
|
||||
ConfigureTag(modelBuilder);
|
||||
ConfigureUnsTagReference(modelBuilder);
|
||||
ConfigurePollGroup(modelBuilder);
|
||||
ConfigureNodeAcl(modelBuilder);
|
||||
ConfigureConfigAuditLog(modelBuilder);
|
||||
@@ -93,7 +95,6 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
||||
ConfigureDriverHostStatus(modelBuilder);
|
||||
ConfigureDriverInstanceResilienceStatus(modelBuilder);
|
||||
ConfigureLdapGroupRoleMapping(modelBuilder);
|
||||
ConfigureEquipmentImportBatch(modelBuilder);
|
||||
ConfigureScript(modelBuilder);
|
||||
ConfigureVirtualTag(modelBuilder);
|
||||
ConfigureScriptedAlarm(modelBuilder);
|
||||
@@ -183,32 +184,39 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigureNamespace(ModelBuilder modelBuilder)
|
||||
private static void ConfigureRawFolder(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<Namespace>(e =>
|
||||
modelBuilder.Entity<RawFolder>(e =>
|
||||
{
|
||||
e.ToTable("Namespace");
|
||||
e.HasKey(x => x.NamespaceRowId);
|
||||
e.Property(x => x.NamespaceRowId).HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||
e.Property(x => x.NamespaceId).HasMaxLength(64);
|
||||
e.ToTable("RawFolder");
|
||||
e.HasKey(x => x.RawFolderRowId);
|
||||
e.Property(x => x.RawFolderRowId).HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||
e.Property(x => x.RawFolderId).HasMaxLength(64);
|
||||
e.Property(x => x.ClusterId).HasMaxLength(64);
|
||||
e.Property(x => x.Kind).HasConversion<string>().HasMaxLength(32);
|
||||
e.Property(x => x.NamespaceUri).HasMaxLength(256);
|
||||
e.Property(x => x.Notes).HasMaxLength(1024);
|
||||
e.Property(x => x.ParentRawFolderId).HasMaxLength(64);
|
||||
e.Property(x => x.Name).HasMaxLength(128);
|
||||
e.Property(x => x.RowVersion).IsRowVersion();
|
||||
|
||||
e.HasOne(x => x.Cluster).WithMany(c => c.Namespaces)
|
||||
// Cluster-rooted. Restrict so a cluster with folders cannot be deleted out from under
|
||||
// them (matches the OnDelete(Restrict) convention across the config schema). Child FKs
|
||||
// (DriverInstance.RawFolderId, RawFolder.ParentRawFolderId) are logical-only, so
|
||||
// non-empty-folder-delete is enforced at the service layer in later batches.
|
||||
e.HasOne<ServerCluster>().WithMany(c => c.RawFolders)
|
||||
.HasForeignKey(x => x.ClusterId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
e.HasIndex(x => new { x.ClusterId, x.Kind }).IsUnique()
|
||||
.HasDatabaseName("UX_Namespace_Cluster_Kind");
|
||||
e.HasIndex(x => x.NamespaceUri).IsUnique()
|
||||
.HasDatabaseName("UX_Namespace_NamespaceUri");
|
||||
e.HasIndex(x => x.NamespaceId).IsUnique()
|
||||
.HasDatabaseName("UX_Namespace_LogicalId");
|
||||
e.HasIndex(x => x.ClusterId)
|
||||
.HasDatabaseName("IX_Namespace_Cluster");
|
||||
e.HasIndex(x => x.RawFolderId).IsUnique().HasDatabaseName("UX_RawFolder_LogicalId");
|
||||
e.HasIndex(x => x.ClusterId).HasDatabaseName("IX_RawFolder_Cluster");
|
||||
|
||||
// Sibling-name uniqueness. Two filtered indexes cover the nullable ParentRawFolderId:
|
||||
// one for nested folders (Parent NOT NULL), one for cluster-root folders (Parent NULL),
|
||||
// mirroring the Tag filtered-index convention.
|
||||
e.HasIndex(x => new { x.ClusterId, x.ParentRawFolderId, x.Name }).IsUnique()
|
||||
.HasFilter("[ParentRawFolderId] IS NOT NULL")
|
||||
.HasDatabaseName("UX_RawFolder_Parent_Name");
|
||||
e.HasIndex(x => new { x.ClusterId, x.Name }).IsUnique()
|
||||
.HasFilter("[ParentRawFolderId] IS NULL")
|
||||
.HasDatabaseName("UX_RawFolder_ClusterRoot_Name");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -267,7 +275,7 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
||||
e.Property(x => x.DriverInstanceRowId).HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||
e.Property(x => x.DriverInstanceId).HasMaxLength(64);
|
||||
e.Property(x => x.ClusterId).HasMaxLength(64);
|
||||
e.Property(x => x.NamespaceId).HasMaxLength(64);
|
||||
e.Property(x => x.RawFolderId).HasMaxLength(64);
|
||||
e.Property(x => x.Name).HasMaxLength(128);
|
||||
e.Property(x => x.DriverType).HasMaxLength(32);
|
||||
e.Property(x => x.DriverConfig).HasColumnType("nvarchar(max)");
|
||||
@@ -277,8 +285,18 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
||||
e.HasOne(x => x.Cluster).WithMany().HasForeignKey(x => x.ClusterId).OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
e.HasIndex(x => x.ClusterId).HasDatabaseName("IX_DriverInstance_Cluster");
|
||||
e.HasIndex(x => x.NamespaceId).HasDatabaseName("IX_DriverInstance_Namespace");
|
||||
e.HasIndex(x => x.RawFolderId).HasDatabaseName("IX_DriverInstance_RawFolder");
|
||||
e.HasIndex(x => x.DriverInstanceId).IsUnique().HasDatabaseName("UX_DriverInstance_LogicalId");
|
||||
|
||||
// v3 sibling-name uniqueness (a RawPath segment). Two filtered indexes cover the nullable
|
||||
// RawFolderId: nested drivers keyed by (ClusterId, RawFolderId, Name), cluster-root drivers
|
||||
// by (ClusterId, Name).
|
||||
e.HasIndex(x => new { x.ClusterId, x.RawFolderId, x.Name }).IsUnique()
|
||||
.HasFilter("[RawFolderId] IS NOT NULL")
|
||||
.HasDatabaseName("UX_DriverInstance_Folder_Name");
|
||||
e.HasIndex(x => new { x.ClusterId, x.Name }).IsUnique()
|
||||
.HasFilter("[RawFolderId] IS NULL")
|
||||
.HasDatabaseName("UX_DriverInstance_ClusterRoot_Name");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -300,6 +318,38 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
||||
|
||||
e.HasIndex(x => x.DriverInstanceId).HasDatabaseName("IX_Device_Driver");
|
||||
e.HasIndex(x => x.DeviceId).IsUnique().HasDatabaseName("UX_Device_LogicalId");
|
||||
// v3 sibling-name uniqueness within a driver (a RawPath segment).
|
||||
e.HasIndex(x => new { x.DriverInstanceId, x.Name }).IsUnique()
|
||||
.HasDatabaseName("UX_Device_Driver_Name");
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigureTagGroup(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<TagGroup>(e =>
|
||||
{
|
||||
e.ToTable("TagGroup");
|
||||
e.HasKey(x => x.TagGroupRowId);
|
||||
e.Property(x => x.TagGroupRowId).HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||
e.Property(x => x.TagGroupId).HasMaxLength(64);
|
||||
e.Property(x => x.DeviceId).HasMaxLength(64);
|
||||
e.Property(x => x.ParentTagGroupId).HasMaxLength(64);
|
||||
e.Property(x => x.Name).HasMaxLength(128);
|
||||
e.Property(x => x.RowVersion).IsRowVersion();
|
||||
|
||||
e.HasIndex(x => x.TagGroupId).IsUnique().HasDatabaseName("UX_TagGroup_LogicalId");
|
||||
e.HasIndex(x => x.DeviceId).HasDatabaseName("IX_TagGroup_Device");
|
||||
|
||||
// Sibling-name uniqueness. Two filtered indexes cover the nullable ParentTagGroupId:
|
||||
// nested groups keyed by (DeviceId, ParentTagGroupId, Name), device-root groups by
|
||||
// (DeviceId, Name). DeviceId/ParentTagGroupId are logical FKs (no navigation); non-empty
|
||||
// group deletes are blocked at the service layer in later batches.
|
||||
e.HasIndex(x => new { x.DeviceId, x.ParentTagGroupId, x.Name }).IsUnique()
|
||||
.HasFilter("[ParentTagGroupId] IS NOT NULL")
|
||||
.HasDatabaseName("UX_TagGroup_Parent_Name");
|
||||
e.HasIndex(x => new { x.DeviceId, x.Name }).IsUnique()
|
||||
.HasFilter("[ParentTagGroupId] IS NULL")
|
||||
.HasDatabaseName("UX_TagGroup_DeviceRoot_Name");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -311,8 +361,6 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
||||
e.HasKey(x => x.EquipmentRowId);
|
||||
e.Property(x => x.EquipmentRowId).HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||
e.Property(x => x.EquipmentId).HasMaxLength(64);
|
||||
e.Property(x => x.DriverInstanceId).HasMaxLength(64);
|
||||
e.Property(x => x.DeviceId).HasMaxLength(64);
|
||||
e.Property(x => x.UnsLineId).HasMaxLength(64);
|
||||
e.Property(x => x.Name).HasMaxLength(32);
|
||||
e.Property(x => x.MachineCode).HasMaxLength(64);
|
||||
@@ -329,7 +377,6 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
||||
e.Property(x => x.EquipmentClassRef).HasMaxLength(128);
|
||||
e.Property(x => x.RowVersion).IsRowVersion();
|
||||
|
||||
e.HasIndex(x => x.DriverInstanceId).HasDatabaseName("IX_Equipment_Driver");
|
||||
e.HasIndex(x => x.UnsLineId).HasDatabaseName("IX_Equipment_Line");
|
||||
e.HasIndex(x => x.EquipmentId).IsUnique().HasDatabaseName("UX_Equipment_LogicalId");
|
||||
e.HasIndex(x => new { x.UnsLineId, x.Name }).IsUnique().HasDatabaseName("UX_Equipment_LinePath");
|
||||
@@ -351,28 +398,29 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
||||
e.HasKey(x => x.TagRowId);
|
||||
e.Property(x => x.TagRowId).HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||
e.Property(x => x.TagId).HasMaxLength(64);
|
||||
e.Property(x => x.DriverInstanceId).HasMaxLength(64);
|
||||
e.Property(x => x.DeviceId).HasMaxLength(64);
|
||||
e.Property(x => x.EquipmentId).HasMaxLength(64);
|
||||
// v3: raw-only tag. Required DeviceId FK; nullable TagGroupId. EquipmentId/FolderPath/
|
||||
// DriverInstanceId are retired — identity is the RawPath (device + optional group + name).
|
||||
e.Property(x => x.DeviceId).HasMaxLength(64).IsRequired();
|
||||
e.Property(x => x.TagGroupId).HasMaxLength(64);
|
||||
e.Property(x => x.Name).HasMaxLength(128);
|
||||
e.Property(x => x.FolderPath).HasMaxLength(512);
|
||||
e.Property(x => x.DataType).HasMaxLength(32);
|
||||
e.Property(x => x.AccessLevel).HasConversion<string>().HasMaxLength(16);
|
||||
e.Property(x => x.PollGroupId).HasMaxLength(64);
|
||||
e.Property(x => x.TagConfig).HasColumnType("nvarchar(max)");
|
||||
e.Property(x => x.RowVersion).IsRowVersion();
|
||||
|
||||
e.HasIndex(x => new { x.DriverInstanceId, x.DeviceId }).HasDatabaseName("IX_Tag_Driver_Device");
|
||||
e.HasIndex(x => x.EquipmentId)
|
||||
.HasFilter("[EquipmentId] IS NOT NULL")
|
||||
.HasDatabaseName("IX_Tag_Equipment");
|
||||
e.HasIndex(x => new { x.DeviceId, x.TagGroupId }).HasDatabaseName("IX_Tag_Device");
|
||||
e.HasIndex(x => x.TagId).IsUnique().HasDatabaseName("UX_Tag_LogicalId");
|
||||
e.HasIndex(x => new { x.EquipmentId, x.Name }).IsUnique()
|
||||
.HasFilter("[EquipmentId] IS NOT NULL")
|
||||
.HasDatabaseName("UX_Tag_EquipmentPath");
|
||||
e.HasIndex(x => new { x.DriverInstanceId, x.FolderPath, x.Name }).IsUnique()
|
||||
.HasFilter("[EquipmentId] IS NULL")
|
||||
.HasDatabaseName("UX_Tag_FolderPath");
|
||||
|
||||
// Sibling-name uniqueness (a RawPath segment). Two filtered indexes cover the nullable
|
||||
// TagGroupId: grouped tags keyed by (DeviceId, TagGroupId, Name), device-root tags by
|
||||
// (DeviceId, Name).
|
||||
e.HasIndex(x => new { x.DeviceId, x.TagGroupId, x.Name }).IsUnique()
|
||||
.HasFilter("[TagGroupId] IS NOT NULL")
|
||||
.HasDatabaseName("UX_Tag_Group_Name");
|
||||
e.HasIndex(x => new { x.DeviceId, x.Name }).IsUnique()
|
||||
.HasFilter("[TagGroupId] IS NULL")
|
||||
.HasDatabaseName("UX_Tag_Device_Name");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -567,51 +615,28 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigureEquipmentImportBatch(ModelBuilder modelBuilder)
|
||||
private static void ConfigureUnsTagReference(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<EquipmentImportBatch>(e =>
|
||||
modelBuilder.Entity<UnsTagReference>(e =>
|
||||
{
|
||||
e.ToTable("EquipmentImportBatch");
|
||||
e.HasKey(x => x.Id);
|
||||
e.Property(x => x.ClusterId).HasMaxLength(64);
|
||||
e.Property(x => x.CreatedBy).HasMaxLength(128);
|
||||
e.Property(x => x.CreatedAtUtc).HasColumnType("datetime2(3)");
|
||||
e.Property(x => x.FinalisedAtUtc).HasColumnType("datetime2(3)");
|
||||
|
||||
// Admin preview modal filters by user; finalise / drop both hit this index.
|
||||
e.HasIndex(x => new { x.CreatedBy, x.FinalisedAtUtc })
|
||||
.HasDatabaseName("IX_EquipmentImportBatch_Creator_Finalised");
|
||||
});
|
||||
|
||||
modelBuilder.Entity<EquipmentImportRow>(e =>
|
||||
{
|
||||
e.ToTable("EquipmentImportRow");
|
||||
e.HasKey(x => x.Id);
|
||||
e.Property(x => x.ZTag).HasMaxLength(128);
|
||||
e.Property(x => x.MachineCode).HasMaxLength(128);
|
||||
e.Property(x => x.SAPID).HasMaxLength(128);
|
||||
e.ToTable("UnsTagReference");
|
||||
e.HasKey(x => x.UnsTagReferenceRowId);
|
||||
e.Property(x => x.UnsTagReferenceRowId).HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||
e.Property(x => x.UnsTagReferenceId).HasMaxLength(64);
|
||||
e.Property(x => x.EquipmentId).HasMaxLength(64);
|
||||
e.Property(x => x.EquipmentUuid).HasMaxLength(64);
|
||||
e.Property(x => x.Name).HasMaxLength(128);
|
||||
e.Property(x => x.UnsAreaName).HasMaxLength(64);
|
||||
e.Property(x => x.UnsLineName).HasMaxLength(64);
|
||||
e.Property(x => x.Manufacturer).HasMaxLength(256);
|
||||
e.Property(x => x.Model).HasMaxLength(256);
|
||||
e.Property(x => x.SerialNumber).HasMaxLength(256);
|
||||
e.Property(x => x.HardwareRevision).HasMaxLength(64);
|
||||
e.Property(x => x.SoftwareRevision).HasMaxLength(64);
|
||||
e.Property(x => x.YearOfConstruction).HasMaxLength(8);
|
||||
e.Property(x => x.AssetLocation).HasMaxLength(512);
|
||||
e.Property(x => x.ManufacturerUri).HasMaxLength(512);
|
||||
e.Property(x => x.DeviceManualUri).HasMaxLength(512);
|
||||
e.Property(x => x.RejectReason).HasMaxLength(512);
|
||||
e.Property(x => x.TagId).HasMaxLength(64);
|
||||
e.Property(x => x.DisplayNameOverride).HasMaxLength(128);
|
||||
e.Property(x => x.RowVersion).IsRowVersion();
|
||||
|
||||
e.HasOne(x => x.Batch)
|
||||
.WithMany(b => b.Rows)
|
||||
.HasForeignKey(x => x.BatchId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
e.HasIndex(x => x.UnsTagReferenceId).IsUnique().HasDatabaseName("UX_UnsTagReference_LogicalId");
|
||||
e.HasIndex(x => x.EquipmentId).HasDatabaseName("IX_UnsTagReference_Equipment");
|
||||
e.HasIndex(x => x.TagId).HasDatabaseName("IX_UnsTagReference_Tag");
|
||||
|
||||
e.HasIndex(x => x.BatchId).HasDatabaseName("IX_EquipmentImportRow_Batch");
|
||||
// One reference per (equipment, raw tag). EquipmentId/TagId are logical FKs (no
|
||||
// navigation); the "raw-tag delete blocked while referenced" rule is enforced at the
|
||||
// service layer in later batches.
|
||||
e.HasIndex(x => new { x.EquipmentId, x.TagId }).IsUnique()
|
||||
.HasDatabaseName("UX_UnsTagReference_Equip_Tag");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -25,8 +25,9 @@ public sealed class DraftSnapshot
|
||||
/// </summary>
|
||||
public string? Site { get; init; }
|
||||
|
||||
/// <summary>Gets the list of OPC UA namespaces.</summary>
|
||||
public IReadOnlyList<Namespace> Namespaces { get; init; } = [];
|
||||
// v3: the Namespace entity is retired (the two OPC UA namespaces are implicit). The old
|
||||
// Namespaces list + namespace-binding validation are gone. WP4 (Wave C) owns the new v3
|
||||
// rules (raw-name charset, historized-tagname length, UNS effective-leaf uniqueness).
|
||||
/// <summary>Gets the list of driver instances.</summary>
|
||||
public IReadOnlyList<DriverInstance> DriverInstances { get; init; } = [];
|
||||
/// <summary>Gets the list of devices.</summary>
|
||||
|
||||
@@ -36,7 +36,7 @@ public static class DraftSnapshotFactory
|
||||
// EquipmentUuid checks run in separate validator passes — no rule here reads these fields.
|
||||
GenerationId = 0, // generation model dropped; placeholder (no rule reads it)
|
||||
ClusterId = string.Empty, // global snapshot; rules compare entity ClusterId fields, not this
|
||||
Namespaces = await db.Namespaces.AsNoTracking().ToListAsync(ct),
|
||||
// v3: Namespace entity retired — no namespace list to materialize.
|
||||
DriverInstances = await db.DriverInstances.AsNoTracking().ToListAsync(ct),
|
||||
Devices = await db.Devices.AsNoTracking().ToListAsync(ct),
|
||||
UnsAreas = await db.UnsAreas.AsNoTracking().ToListAsync(ct),
|
||||
|
||||
@@ -30,56 +30,34 @@ public static class DraftValidator
|
||||
ValidateUnsSegments(draft, errors);
|
||||
ValidatePathLength(draft, errors);
|
||||
ValidateEquipmentUuidImmutability(draft, errors);
|
||||
ValidateSameClusterNamespaceBinding(draft, errors);
|
||||
ValidateReservationPreflight(draft, errors);
|
||||
ValidateEquipmentIdDerivation(draft, errors);
|
||||
ValidateNoEquipmentSignalNameCollision(draft, errors);
|
||||
ValidateGalaxyTagFullName(draft, errors);
|
||||
|
||||
// v3 WP1 note: the retired rules ValidateSameClusterNamespaceBinding (Namespace entity gone)
|
||||
// and ValidateGalaxyTagFullName (blob-as-identity gone; Tags are raw-only, not equipment-bound)
|
||||
// are deleted here. TODO(v3 WP4): add the new v3 rules — raw-name charset (no '/', no
|
||||
// lead/trail whitespace) at every level, historized effective-tagname ≤ 255 chars, and UNS
|
||||
// effective-leaf uniqueness across UnsTagReference/VirtualTag/ScriptedAlarm.
|
||||
return errors;
|
||||
}
|
||||
|
||||
private static void ValidateGalaxyTagFullName(DraftSnapshot draft, List<ValidationError> errors)
|
||||
{
|
||||
var typeByDriver = draft.DriverInstances
|
||||
.ToDictionary(d => d.DriverInstanceId, d => d.DriverType, StringComparer.Ordinal);
|
||||
foreach (var t in draft.Tags)
|
||||
{
|
||||
if (t.EquipmentId is null) continue;
|
||||
if (!typeByDriver.TryGetValue(t.DriverInstanceId, out var dtype) || dtype != "GalaxyMxGateway")
|
||||
continue;
|
||||
// The Galaxy rule wants the EXPLICIT reference, not the raw-blob fallback — so it reads
|
||||
// TagConfigIntent.ExplicitFullName (the "FullName" property only when present-and-string,
|
||||
// else null), preserving the historical null-on-absent semantics by construction (R2-11).
|
||||
if (string.IsNullOrWhiteSpace(TagConfigIntent.Parse(t.TagConfig).ExplicitFullName))
|
||||
errors.Add(new("GalaxyTagMissingReference",
|
||||
$"Galaxy tag '{t.TagId}' on equipment '{t.EquipmentId}' is missing a Galaxy reference (TagConfig.FullName)",
|
||||
t.TagId));
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateNoEquipmentSignalNameCollision(DraftSnapshot draft, List<ValidationError> errors)
|
||||
{
|
||||
// Materialiser NodeId key: "{EquipmentId}[/{FolderPath}]/{Name}". Tag (EquipmentId != null) and
|
||||
// VirtualTag share this space with no DB cross-table uniqueness, so the same key from both collides.
|
||||
static string Key(string eq, string? folder, string name) =>
|
||||
string.IsNullOrWhiteSpace(folder) ? $"{eq}/{name}" : $"{eq}/{folder}/{name}";
|
||||
// v3: Tags are raw-only (no EquipmentId), so the old Tag-vs-VirtualTag NodeId collision does
|
||||
// not apply here. Until WP4 wires the full UNS effective-leaf rule (across UnsTagReference,
|
||||
// VirtualTag, and ScriptedAlarm), enforce the still-valid invariant that a VirtualTag Name is
|
||||
// unique within its owning equipment — the OPC UA NodeId key is "{EquipmentId}/{Name}".
|
||||
var signals = draft.VirtualTags.Select(v => (Eq: v.EquipmentId, v.Name));
|
||||
|
||||
var signals = draft.Tags
|
||||
.Where(t => t.EquipmentId is not null)
|
||||
.Select(t => (Key: Key(t.EquipmentId!, t.FolderPath, t.Name), Eq: t.EquipmentId!, t.Name))
|
||||
// VirtualTag has no FolderPath column today — null is correct here; update if it ever gains one.
|
||||
.Concat(draft.VirtualTags
|
||||
.Select(v => (Key: Key(v.EquipmentId, null, v.Name), Eq: v.EquipmentId, v.Name)));
|
||||
|
||||
foreach (var g in signals.GroupBy(s => s.Key, StringComparer.Ordinal))
|
||||
foreach (var g in signals.GroupBy(s => $"{s.Eq}/{s.Name}", StringComparer.Ordinal))
|
||||
{
|
||||
var items = g.ToList();
|
||||
if (items.Count <= 1) continue;
|
||||
var f = items[0];
|
||||
errors.Add(new("EquipmentSignalNameCollision",
|
||||
$"{items.Count} signals collide on OPC UA NodeId '{g.Key}' (equipment '{f.Eq}', name '{f.Name}'); " +
|
||||
"a Name must be unique across Tag and VirtualTag within an equipment+folder",
|
||||
"a VirtualTag Name must be unique within an equipment",
|
||||
f.Eq));
|
||||
}
|
||||
}
|
||||
@@ -149,27 +127,6 @@ public static class DraftValidator
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateSameClusterNamespaceBinding(DraftSnapshot draft, List<ValidationError> errors)
|
||||
{
|
||||
var nsById = draft.Namespaces.ToDictionary(n => n.NamespaceId);
|
||||
|
||||
foreach (var di in draft.DriverInstances)
|
||||
{
|
||||
if (!nsById.TryGetValue(di.NamespaceId, out var ns))
|
||||
{
|
||||
errors.Add(new("NamespaceUnresolved",
|
||||
$"DriverInstance '{di.DriverInstanceId}' references unknown NamespaceId '{di.NamespaceId}'",
|
||||
di.DriverInstanceId));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ns.ClusterId != di.ClusterId)
|
||||
errors.Add(new("BadCrossClusterNamespaceBinding",
|
||||
$"DriverInstance '{di.DriverInstanceId}' is in cluster '{di.ClusterId}' but references namespace in cluster '{ns.ClusterId}'",
|
||||
di.DriverInstanceId));
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateReservationPreflight(DraftSnapshot draft, List<ValidationError> errors)
|
||||
{
|
||||
var activeByKindValue = draft.ActiveReservations
|
||||
|
||||
@@ -29,30 +29,32 @@ public sealed class DraftSnapshotFactoryTests : IDisposable
|
||||
/// <summary>Disposes the database context.</summary>
|
||||
public void Dispose() => _db.Dispose();
|
||||
|
||||
/// <summary>Seeds one Equipment plus a Tag and a VirtualTag sharing (EquipmentId, Name); the
|
||||
/// snapshot must carry both signal collections AND the validator must flag the collision.</summary>
|
||||
/// <summary>Seeds an Equipment plus a raw Tag and a VirtualTag; the snapshot must carry both
|
||||
/// signal collections. v3: Tags are raw-only, so the equipment-signal collision rule now operates
|
||||
/// on VirtualTags — two VirtualTags sharing (EquipmentId, Name) must surface the collision.</summary>
|
||||
[Fact]
|
||||
public async Task FromConfigDb_populates_Tags_and_VirtualTags_and_surfaces_collision()
|
||||
{
|
||||
SeedEquipment("eq-1");
|
||||
_db.Tags.Add(BuildTag(equipmentId: "eq-1", name: "speed"));
|
||||
_db.VirtualTags.Add(BuildVirtualTag(equipmentId: "eq-1", name: "speed"));
|
||||
_db.Tags.Add(BuildTag(name: "speed"));
|
||||
_db.VirtualTags.Add(BuildVirtualTag(equipmentId: "eq-1", name: "speed", suffix: "a"));
|
||||
_db.VirtualTags.Add(BuildVirtualTag(equipmentId: "eq-1", name: "speed", suffix: "b"));
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
var snapshot = await DraftSnapshotFactory.FromConfigDbAsync(_db);
|
||||
|
||||
snapshot.Tags.Count.ShouldBe(1);
|
||||
snapshot.VirtualTags.Count.ShouldBe(1);
|
||||
snapshot.VirtualTags.Count.ShouldBe(2);
|
||||
DraftValidator.Validate(snapshot).ShouldContain(e => e.Code == "EquipmentSignalNameCollision");
|
||||
}
|
||||
|
||||
/// <summary>A Tag and a VirtualTag with distinct names under the same equipment do not collide,
|
||||
/// so the snapshot validates clean of the collision code.</summary>
|
||||
/// <summary>Distinct VirtualTag names under the same equipment do not collide, so the snapshot
|
||||
/// validates clean of the collision code.</summary>
|
||||
[Fact]
|
||||
public async Task FromConfigDb_no_collision_when_names_differ()
|
||||
{
|
||||
SeedEquipment("eq-1");
|
||||
_db.Tags.Add(BuildTag(equipmentId: "eq-1", name: "speed"));
|
||||
_db.Tags.Add(BuildTag(name: "speed"));
|
||||
_db.VirtualTags.Add(BuildVirtualTag(equipmentId: "eq-1", name: "temperature"));
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
@@ -109,26 +111,25 @@ public sealed class DraftSnapshotFactoryTests : IDisposable
|
||||
EquipmentUuid = uuid,
|
||||
EquipmentId = equipmentId,
|
||||
Name = "eq",
|
||||
DriverInstanceId = "d",
|
||||
UnsLineId = "line-a",
|
||||
MachineCode = "m",
|
||||
});
|
||||
}
|
||||
|
||||
private static Tag BuildTag(string equipmentId, string name) => new()
|
||||
// v3: Tags are raw-only (DeviceId FK, no EquipmentId/DriverInstanceId).
|
||||
private static Tag BuildTag(string name) => new()
|
||||
{
|
||||
TagId = $"tag-{name}",
|
||||
DriverInstanceId = "d",
|
||||
EquipmentId = equipmentId,
|
||||
DeviceId = "dev-1",
|
||||
Name = name,
|
||||
DataType = "Float",
|
||||
AccessLevel = TagAccessLevel.Read,
|
||||
TagConfig = "{}",
|
||||
};
|
||||
|
||||
private static VirtualTag BuildVirtualTag(string equipmentId, string name) => new()
|
||||
private static VirtualTag BuildVirtualTag(string equipmentId, string name, string suffix = "") => new()
|
||||
{
|
||||
VirtualTagId = $"vtag-{name}",
|
||||
VirtualTagId = $"vtag-{name}{suffix}",
|
||||
EquipmentId = equipmentId,
|
||||
Name = name,
|
||||
DataType = "Float",
|
||||
|
||||
-60
@@ -1,60 +0,0 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Validation;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Characterization net (R2-11) pinning the DraftValidator Galaxy rule's <em>explicit</em>-FullName
|
||||
/// semantics across the golden TagConfig corpus BEFORE the private FullName reader is delegated to
|
||||
/// <c>TagConfigIntent.Parse(...).ExplicitFullName</c>. Distinct from the walker/composer raw-blob
|
||||
/// fallback: <c>GalaxyTagMissingReference</c> fires whenever the TagConfig has no usable
|
||||
/// <em>explicit</em> string <c>FullName</c> — i.e. absent / non-string / whitespace / non-object /
|
||||
/// malformed — and does NOT fire only when a non-blank string FullName is present. Must be green on
|
||||
/// the unmodified tree and stay green through the swap.
|
||||
/// </summary>
|
||||
public sealed class DraftValidatorGalaxyFullNameCorpusTests
|
||||
{
|
||||
[Theory]
|
||||
// Present, non-blank string FullName → rule does NOT fire
|
||||
[InlineData("{\"FullName\":\"X.Y\"}", false)]
|
||||
[InlineData("{\"FullName\":\"A.B\",\"region\":\"Coils\"}", false)]
|
||||
// Absent / non-string / whitespace / non-object / malformed → rule FIRES
|
||||
[InlineData("{}", true)]
|
||||
[InlineData("{\"region\":\"HoldingRegisters\"}", true)]
|
||||
[InlineData("{\"FullName\":123}", true)]
|
||||
[InlineData("{\"FullName\":\" \"}", true)]
|
||||
[InlineData("[1,2]", true)]
|
||||
[InlineData("not json {", true)]
|
||||
public void Galaxy_missing_reference_fires_iff_no_explicit_string_FullName(string tagConfig, bool shouldFire)
|
||||
{
|
||||
var draft = new DraftSnapshot
|
||||
{
|
||||
GenerationId = 1,
|
||||
ClusterId = "c",
|
||||
DriverInstances =
|
||||
[
|
||||
new DriverInstance
|
||||
{
|
||||
DriverInstanceId = "d-galaxy", ClusterId = "c", NamespaceId = "ns-1",
|
||||
Name = "Galaxy", DriverType = "GalaxyMxGateway", DriverConfig = "{}",
|
||||
},
|
||||
],
|
||||
Tags =
|
||||
[
|
||||
new Tag
|
||||
{
|
||||
TagId = "tag-galaxytag", DriverInstanceId = "d-galaxy", EquipmentId = "eq-1",
|
||||
Name = "galaxytag", FolderPath = null, DataType = "Float",
|
||||
AccessLevel = TagAccessLevel.Read, TagConfig = tagConfig,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var fired = DraftValidator.Validate(draft)
|
||||
.Any(e => e.Code == "GalaxyTagMissingReference" && e.Context == "tag-galaxytag");
|
||||
fired.ShouldBe(shouldFire);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,14 @@ using ZB.MOM.WW.OtOpcUa.Configuration.Validation;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// v3 WP1: the namespace-binding + Galaxy-FullName rules are retired (Namespace entity gone;
|
||||
/// Tags are raw-only, not equipment-bound). The retained rules (UNS segments, path length,
|
||||
/// EquipmentUuid immutability, external-id reservation preflight, EquipmentId derivation,
|
||||
/// cluster topology, VirtualTag equipment-signal collision) are exercised below against the v3
|
||||
/// entity shapes. WP4 (Wave C) adds the new v3 rules (raw-name charset, historized-tagname length,
|
||||
/// UNS effective-leaf uniqueness).
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class DraftValidatorTests
|
||||
{
|
||||
@@ -32,7 +40,6 @@ public sealed class DraftValidatorTests
|
||||
EquipmentUuid = uuid,
|
||||
EquipmentId = DraftValidator.DeriveEquipmentId(uuid),
|
||||
Name = name,
|
||||
DriverInstanceId = "d",
|
||||
UnsLineId = "line-a",
|
||||
MachineCode = "m",
|
||||
},
|
||||
@@ -44,35 +51,6 @@ public sealed class DraftValidatorTests
|
||||
hasUnsError.ShouldBe(!shouldPass);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that namespace cannot be bound to a different cluster.</summary>
|
||||
[Fact]
|
||||
public void Cross_cluster_namespace_binding_is_rejected()
|
||||
{
|
||||
var draft = new DraftSnapshot
|
||||
{
|
||||
GenerationId = 1, ClusterId = "c-A",
|
||||
Namespaces = [new Namespace { NamespaceId = "ns-1", ClusterId = "c-B", NamespaceUri = "urn:x", Kind = NamespaceKind.Equipment }],
|
||||
DriverInstances = [new DriverInstance { DriverInstanceId = "d-1", ClusterId = "c-A", NamespaceId = "ns-1", Name = "drv", DriverType = "Modbus", DriverConfig = "{}" }],
|
||||
};
|
||||
|
||||
var errors = DraftValidator.Validate(draft);
|
||||
errors.ShouldContain(e => e.Code == "BadCrossClusterNamespaceBinding");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that namespace in the same cluster is accepted.</summary>
|
||||
[Fact]
|
||||
public void Same_cluster_namespace_binding_is_accepted()
|
||||
{
|
||||
var draft = new DraftSnapshot
|
||||
{
|
||||
GenerationId = 1, ClusterId = "c-A",
|
||||
Namespaces = [new Namespace { NamespaceId = "ns-1", ClusterId = "c-A", NamespaceUri = "urn:x", Kind = NamespaceKind.Equipment }],
|
||||
DriverInstances = [new DriverInstance { DriverInstanceId = "d-1", ClusterId = "c-A", NamespaceId = "ns-1", Name = "drv", DriverType = "Modbus", DriverConfig = "{}" }],
|
||||
};
|
||||
|
||||
DraftValidator.Validate(draft).ShouldNotContain(e => e.Code == "BadCrossClusterNamespaceBinding");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that equipment UUID must remain immutable across generations.</summary>
|
||||
[Fact]
|
||||
public void EquipmentUuid_change_across_generations_is_rejected()
|
||||
@@ -84,8 +62,8 @@ public sealed class DraftValidatorTests
|
||||
var draft = new DraftSnapshot
|
||||
{
|
||||
GenerationId = 2, ClusterId = "c",
|
||||
Equipment = [new Equipment { EquipmentUuid = newUuid, EquipmentId = eid, Name = "eq", DriverInstanceId = "d", UnsLineId = "line-a", MachineCode = "m" }],
|
||||
PriorEquipment = [new Equipment { EquipmentUuid = oldUuid, EquipmentId = eid, Name = "eq", DriverInstanceId = "d", UnsLineId = "line-a", MachineCode = "m" }],
|
||||
Equipment = [new Equipment { EquipmentUuid = newUuid, EquipmentId = eid, Name = "eq", UnsLineId = "line-a", MachineCode = "m" }],
|
||||
PriorEquipment = [new Equipment { EquipmentUuid = oldUuid, EquipmentId = eid, Name = "eq", UnsLineId = "line-a", MachineCode = "m" }],
|
||||
};
|
||||
|
||||
DraftValidator.Validate(draft).ShouldContain(e => e.Code == "EquipmentUuidImmutable");
|
||||
@@ -101,7 +79,7 @@ public sealed class DraftValidatorTests
|
||||
var draft = new DraftSnapshot
|
||||
{
|
||||
GenerationId = 1, ClusterId = "c",
|
||||
Equipment = [new Equipment { EquipmentUuid = uuid, EquipmentId = DraftValidator.DeriveEquipmentId(uuid), Name = "eq", DriverInstanceId = "d", UnsLineId = "line-a", MachineCode = "m", ZTag = "ZT-001" }],
|
||||
Equipment = [new Equipment { EquipmentUuid = uuid, EquipmentId = DraftValidator.DeriveEquipmentId(uuid), Name = "eq", UnsLineId = "line-a", MachineCode = "m", ZTag = "ZT-001" }],
|
||||
ActiveReservations = [new ExternalIdReservation { Kind = ReservationKind.ZTag, Value = "ZT-001", EquipmentUuid = otherUuid, ClusterId = "c", FirstPublishedBy = "t" }],
|
||||
};
|
||||
|
||||
@@ -116,96 +94,38 @@ public sealed class DraftValidatorTests
|
||||
var draft = new DraftSnapshot
|
||||
{
|
||||
GenerationId = 1, ClusterId = "c",
|
||||
Equipment = [new Equipment { EquipmentUuid = uuid, EquipmentId = "EQ-operator-typed", Name = "eq", DriverInstanceId = "d", UnsLineId = "line-a", MachineCode = "m" }],
|
||||
Equipment = [new Equipment { EquipmentUuid = uuid, EquipmentId = "EQ-operator-typed", Name = "eq", UnsLineId = "line-a", MachineCode = "m" }],
|
||||
};
|
||||
|
||||
DraftValidator.Validate(draft).ShouldContain(e => e.Code == "EquipmentIdNotDerived");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the canonical Galaxy driver type (GalaxyMxGateway, per PR 7.2 —
|
||||
/// it was "Galaxy" pre-PR-7.2) is a standard Equipment-kind driver: binding it to an
|
||||
/// Equipment namespace produces no kind-mismatch error.</summary>
|
||||
/// <summary>Verifies that all violations are reported simultaneously (single-pass validation).</summary>
|
||||
[Fact]
|
||||
public void GalaxyMxGateway_driver_in_Equipment_namespace_is_allowed()
|
||||
{
|
||||
var draft = new DraftSnapshot
|
||||
{
|
||||
GenerationId = 1, ClusterId = "c",
|
||||
Namespaces = [new Namespace { NamespaceId = "ns-1", ClusterId = "c", NamespaceUri = "urn:x", Kind = NamespaceKind.Equipment }],
|
||||
DriverInstances = [new DriverInstance { DriverInstanceId = "d-1", ClusterId = "c", NamespaceId = "ns-1", Name = "drv", DriverType = "GalaxyMxGateway", DriverConfig = "{}" }],
|
||||
};
|
||||
|
||||
DraftValidator.Validate(draft).ShouldBeEmpty();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that all validation errors are reported simultaneously.</summary>
|
||||
[Fact]
|
||||
public void Draft_with_three_violations_surfaces_all_three()
|
||||
public void Draft_with_multiple_violations_surfaces_all_of_them()
|
||||
{
|
||||
var uuid = Guid.NewGuid();
|
||||
var draft = new DraftSnapshot
|
||||
{
|
||||
GenerationId = 1, ClusterId = "c-A",
|
||||
Namespaces = [new Namespace { NamespaceId = "ns-1", ClusterId = "c-B", NamespaceUri = "urn:x", Kind = NamespaceKind.Equipment }],
|
||||
DriverInstances = [new DriverInstance { DriverInstanceId = "d-1", ClusterId = "c-A", NamespaceId = "ns-1", Name = "drv", DriverType = "GalaxyMxGateway", DriverConfig = "{}" }],
|
||||
Equipment = [new Equipment { EquipmentUuid = uuid, EquipmentId = "EQ-wrong", Name = "BAD NAME", DriverInstanceId = "d-1", UnsLineId = "line-a", MachineCode = "m" }],
|
||||
GenerationId = 1, ClusterId = "c",
|
||||
// EQ-wrong is not the canonical derivation, and "BAD NAME" fails the UNS segment regex.
|
||||
Equipment = [new Equipment { EquipmentUuid = uuid, EquipmentId = "EQ-wrong", Name = "BAD NAME", UnsLineId = "line-a", MachineCode = "m" }],
|
||||
};
|
||||
|
||||
var errors = DraftValidator.Validate(draft);
|
||||
errors.ShouldContain(e => e.Code == "BadCrossClusterNamespaceBinding");
|
||||
errors.ShouldContain(e => e.Code == "EquipmentIdNotDerived");
|
||||
errors.ShouldContain(e => e.Code == "UnsSegmentInvalid");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// Full-config probe — proves a realistic canonical deployed config passes ALL rules.
|
||||
// This guards the deploy-path gate flip (reject-on-ANY-error): if a clean canonical
|
||||
// config fired any rule, flipping the gate would block every deploy.
|
||||
// ------------------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Probe for the deploy-path gate activation: a full, realistic config modelling
|
||||
/// the REAL deployed shape. Galaxy is now a standard Equipment-kind driver (the SystemPlatform
|
||||
/// namespace split is retired), so the GalaxyMxGateway driver + its Galaxy-hierarchy Tags
|
||||
/// (EquipmentId=null from the seed) live in the Equipment namespace alongside a Modbus driver
|
||||
/// + UNS area/line + canonical Equipment rows + VirtualTags from the company overlay. This must
|
||||
/// produce ZERO validation errors so the reject-on-any-error gate is safe to activate.</summary>
|
||||
/// <summary>Probe for the deploy-path gate: a full, realistic v3 config (UNS area/line +
|
||||
/// canonical Equipment + distinct-named VirtualTags) must produce ZERO validation errors so the
|
||||
/// reject-on-any-error deploy gate is safe to activate.</summary>
|
||||
[Fact]
|
||||
public void Full_realistic_config_passes_all_rules()
|
||||
{
|
||||
// Equipment side: Galaxy + Modbus drivers in a single Equipment namespace + UNS + canonical equipment.
|
||||
var eqNamespace = new Namespace
|
||||
{
|
||||
NamespaceId = "MAIN-OPCUA-equipment",
|
||||
ClusterId = "MAIN",
|
||||
Kind = NamespaceKind.Equipment,
|
||||
NamespaceUri = "urn:zb:main:equipment",
|
||||
};
|
||||
|
||||
// Galaxy hierarchy (OtOpcUa seed shape): GalaxyMxGateway driver, Tags carry EquipmentId=null.
|
||||
var spDriver = new DriverInstance
|
||||
{
|
||||
DriverInstanceId = "main-galaxy",
|
||||
ClusterId = "MAIN",
|
||||
NamespaceId = eqNamespace.NamespaceId,
|
||||
Name = "Galaxy",
|
||||
DriverType = "GalaxyMxGateway",
|
||||
DriverConfig = "{}",
|
||||
};
|
||||
|
||||
var eqDriver = new DriverInstance
|
||||
{
|
||||
DriverInstanceId = "main-modbus",
|
||||
ClusterId = "MAIN",
|
||||
NamespaceId = eqNamespace.NamespaceId,
|
||||
Name = "Modbus",
|
||||
DriverType = "Modbus",
|
||||
DriverConfig = "{}",
|
||||
};
|
||||
|
||||
var area = new UnsArea { UnsAreaId = "area-filling", ClusterId = "MAIN", Name = "filling" };
|
||||
var line = new UnsLine { UnsLineId = "line-1", UnsAreaId = area.UnsAreaId, Name = "line-1" };
|
||||
|
||||
// Canonical EquipmentIds — derived from the EquipmentUuid via the same rule the overlay loader uses.
|
||||
var rinserUuid = Guid.NewGuid();
|
||||
var fillerUuid = Guid.NewGuid();
|
||||
var rinser = new Equipment
|
||||
@@ -213,43 +133,27 @@ public sealed class DraftValidatorTests
|
||||
EquipmentUuid = rinserUuid,
|
||||
EquipmentId = DraftValidator.DeriveEquipmentId(rinserUuid),
|
||||
Name = "rinser-01",
|
||||
DriverInstanceId = eqDriver.DriverInstanceId,
|
||||
UnsLineId = line.UnsLineId,
|
||||
MachineCode = "machine_001",
|
||||
ZTag = null,
|
||||
SAPID = null,
|
||||
};
|
||||
var filler = new Equipment
|
||||
{
|
||||
EquipmentUuid = fillerUuid,
|
||||
EquipmentId = DraftValidator.DeriveEquipmentId(fillerUuid),
|
||||
Name = "filler-02",
|
||||
DriverInstanceId = eqDriver.DriverInstanceId,
|
||||
UnsLineId = line.UnsLineId,
|
||||
MachineCode = "machine_002",
|
||||
ZTag = null,
|
||||
SAPID = null,
|
||||
};
|
||||
|
||||
var draft = new DraftSnapshot
|
||||
{
|
||||
GenerationId = 0,
|
||||
ClusterId = string.Empty, // global snapshot — matches DraftSnapshotFactory.FromConfigDbAsync
|
||||
// Enterprise/Site left null — matches the deploy path's conservative fallback
|
||||
Namespaces = [eqNamespace],
|
||||
DriverInstances = [spDriver, eqDriver],
|
||||
UnsAreas = [area],
|
||||
UnsLines = [line],
|
||||
Equipment = [rinser, filler],
|
||||
Tags =
|
||||
[
|
||||
// Galaxy-hierarchy tags from the seed: EquipmentId null, Galaxy folder hierarchy.
|
||||
BuildTag(equipmentId: null, name: "PV", folderPath: "Area.Tank01"),
|
||||
BuildTag(equipmentId: null, name: "SP", folderPath: "Area.Tank01"),
|
||||
],
|
||||
VirtualTags =
|
||||
[
|
||||
// One per equipment, names distinct within each owning equipment.
|
||||
BuildVirtualTag(equipmentId: rinser.EquipmentId, name: "oee"),
|
||||
BuildVirtualTag(equipmentId: filler.EquipmentId, name: "throughput"),
|
||||
],
|
||||
@@ -264,133 +168,46 @@ public sealed class DraftValidatorTests
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// Driver-less Equipment namespace — core safety claim (feat/driverless-equipment-namespace)
|
||||
// ValidateNoEquipmentSignalNameCollision — v3: VirtualTag name uniqueness within equipment
|
||||
// ------------------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Characterisation test: a draft that contains an Equipment-kind namespace with
|
||||
/// ZERO <see cref="DriverInstance"/> rows, and a single <see cref="Equipment"/> whose
|
||||
/// <see cref="Equipment.DriverInstanceId"/> is <see langword="null"/>, must pass
|
||||
/// <see cref="DraftValidator.Validate"/> with no errors. This locks in the design's core
|
||||
/// safety claim — the validator must never implicitly require a driver for Equipment-namespace
|
||||
/// equipment.</summary>
|
||||
/// <summary>Two VirtualTags sharing (EquipmentId, Name) collide on a single OPC UA NodeId.</summary>
|
||||
[Fact]
|
||||
public void Validate_accepts_driverless_equipment_in_driverless_equipment_namespace()
|
||||
{
|
||||
// An Equipment-kind namespace — no DriverInstance rows at all for this namespace.
|
||||
var eqNamespace = new Namespace
|
||||
{
|
||||
NamespaceId = "MAIN-OPCUA-equipment",
|
||||
ClusterId = "MAIN",
|
||||
Kind = NamespaceKind.Equipment,
|
||||
NamespaceUri = "urn:zb:main:equipment",
|
||||
};
|
||||
|
||||
// UNS topology required by ValidateUnsSegments / ValidatePathLength.
|
||||
var area = new UnsArea { UnsAreaId = "area-filling", ClusterId = "MAIN", Name = "filling" };
|
||||
var line = new UnsLine { UnsLineId = "line-1", UnsAreaId = area.UnsAreaId, Name = "line-1" };
|
||||
|
||||
// Canonical EquipmentId derived from UUID — satisfies ValidateEquipmentIdDerivation.
|
||||
var uuid = Guid.NewGuid();
|
||||
var equipment = new Equipment
|
||||
{
|
||||
EquipmentUuid = uuid,
|
||||
EquipmentId = DraftValidator.DeriveEquipmentId(uuid),
|
||||
Name = "rinser-01",
|
||||
DriverInstanceId = null, // ← driver-less: the property under test
|
||||
UnsLineId = line.UnsLineId,
|
||||
MachineCode = "machine_001",
|
||||
ZTag = null,
|
||||
SAPID = null,
|
||||
};
|
||||
|
||||
var draft = new DraftSnapshot
|
||||
{
|
||||
GenerationId = 0,
|
||||
ClusterId = string.Empty, // global snapshot — matches DraftSnapshotFactory.FromConfigDbAsync
|
||||
Namespaces = [eqNamespace], // Equipment namespace present
|
||||
DriverInstances = [], // ← zero drivers: the other half of the safety claim
|
||||
UnsAreas = [area],
|
||||
UnsLines = [line],
|
||||
Equipment = [equipment],
|
||||
};
|
||||
|
||||
var errors = DraftValidator.Validate(draft);
|
||||
|
||||
errors.ShouldBeEmpty(
|
||||
"a driver-less Equipment in an Equipment-kind namespace with no DriverInstances must pass " +
|
||||
"all validator rules; firing rules: " +
|
||||
string.Join("; ", errors.Select(e => $"[{e.Code}] {e.Message}")));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// ValidateNoEquipmentSignalNameCollision — Tag/VirtualTag NodeId collision
|
||||
// ------------------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that an equipment-bound Tag and a VirtualTag sharing the same
|
||||
/// equipment+name collide on a single OPC UA NodeId.</summary>
|
||||
[Fact]
|
||||
public void Tag_and_VirtualTag_same_equipment_and_name_collide()
|
||||
public void Two_VirtualTags_same_equipment_and_name_collide()
|
||||
{
|
||||
var draft = new DraftSnapshot
|
||||
{
|
||||
GenerationId = 1, ClusterId = "c",
|
||||
Tags = [BuildTag(equipmentId: "eq-1", name: "speed", folderPath: null)],
|
||||
VirtualTags = [BuildVirtualTag(equipmentId: "eq-1", name: "speed")],
|
||||
VirtualTags =
|
||||
[
|
||||
BuildVirtualTag(equipmentId: "eq-1", name: "speed", suffix: "a"),
|
||||
BuildVirtualTag(equipmentId: "eq-1", name: "speed", suffix: "b"),
|
||||
],
|
||||
};
|
||||
|
||||
DraftValidator.Validate(draft).ShouldContain(e => e.Code == "EquipmentSignalNameCollision");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a Tag in a sub-folder does not collide with a VirtualTag at the
|
||||
/// equipment root even when the names match — the NodeId keys differ.</summary>
|
||||
/// <summary>VirtualTags with the same name under DIFFERENT equipment do not collide.</summary>
|
||||
[Fact]
|
||||
public void Same_name_different_folder_does_not_collide()
|
||||
public void Same_name_different_equipment_does_not_collide()
|
||||
{
|
||||
var draft = new DraftSnapshot
|
||||
{
|
||||
GenerationId = 1, ClusterId = "c",
|
||||
Tags = [BuildTag(equipmentId: "eq-1", name: "speed", folderPath: "metrics")],
|
||||
VirtualTags = [BuildVirtualTag(equipmentId: "eq-1", name: "speed")],
|
||||
VirtualTags =
|
||||
[
|
||||
BuildVirtualTag(equipmentId: "eq-1", name: "speed"),
|
||||
BuildVirtualTag(equipmentId: "eq-2", name: "speed"),
|
||||
],
|
||||
};
|
||||
|
||||
DraftValidator.Validate(draft).ShouldNotContain(e => e.Code == "EquipmentSignalNameCollision");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a FolderPath-scoped tag (EquipmentId == null) is excluded from the
|
||||
/// equipment-signal node space and so never collides with an equipment VirtualTag.</summary>
|
||||
[Fact]
|
||||
public void Unbound_tag_EquipmentId_null_does_not_collide_with_equipment_vtag()
|
||||
private static VirtualTag BuildVirtualTag(string equipmentId, string name, string suffix = "") => new()
|
||||
{
|
||||
var draft = new DraftSnapshot
|
||||
{
|
||||
GenerationId = 1, ClusterId = "c",
|
||||
Tags = [BuildTag(equipmentId: null, name: "speed", folderPath: null)],
|
||||
VirtualTags = [BuildVirtualTag(equipmentId: "eq-1", name: "speed")],
|
||||
};
|
||||
|
||||
DraftValidator.Validate(draft).ShouldNotContain(e => e.Code == "EquipmentSignalNameCollision");
|
||||
}
|
||||
|
||||
private static Tag BuildTag(
|
||||
string? equipmentId,
|
||||
string name,
|
||||
string? folderPath,
|
||||
string driverInstanceId = "d",
|
||||
string tagConfig = "{}") => new()
|
||||
{
|
||||
TagId = $"tag-{name}",
|
||||
DriverInstanceId = driverInstanceId,
|
||||
EquipmentId = equipmentId,
|
||||
Name = name,
|
||||
FolderPath = folderPath,
|
||||
DataType = "Float",
|
||||
AccessLevel = TagAccessLevel.Read,
|
||||
TagConfig = tagConfig,
|
||||
};
|
||||
|
||||
private static VirtualTag BuildVirtualTag(string equipmentId, string name) => new()
|
||||
{
|
||||
VirtualTagId = $"vtag-{name}",
|
||||
VirtualTagId = $"vtag-{equipmentId}-{name}{suffix}",
|
||||
EquipmentId = equipmentId,
|
||||
Name = name,
|
||||
DataType = "Float",
|
||||
@@ -398,58 +215,7 @@ public sealed class DraftValidatorTests
|
||||
};
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// ValidateGalaxyTagFullName — Galaxy equipment tags must carry TagConfig.FullName
|
||||
// ------------------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that an equipment-scoped Tag bound to a GalaxyMxGateway driver whose
|
||||
/// TagConfig has no FullName (a Galaxy tag with no Galaxy reference) is rejected — it would
|
||||
/// subscribe to nothing.</summary>
|
||||
[Fact]
|
||||
public void GalaxyTag_missing_FullName_is_rejected()
|
||||
{
|
||||
var draft = new DraftSnapshot
|
||||
{
|
||||
GenerationId = 1, ClusterId = "c",
|
||||
DriverInstances = [new DriverInstance { DriverInstanceId = "d-galaxy", ClusterId = "c", NamespaceId = "ns-1", Name = "Galaxy", DriverType = "GalaxyMxGateway", DriverConfig = "{}" }],
|
||||
Tags = [BuildTag(equipmentId: "eq-1", name: "galaxytag", folderPath: null, driverInstanceId: "d-galaxy", tagConfig: "{}")],
|
||||
};
|
||||
|
||||
DraftValidator.Validate(draft).ShouldContain(e => e.Code == "GalaxyTagMissingReference" && e.Context == "tag-galaxytag");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an equipment-scoped Galaxy tag carrying a TagConfig.FullName
|
||||
/// reference is accepted.</summary>
|
||||
[Fact]
|
||||
public void GalaxyTag_with_FullName_is_accepted()
|
||||
{
|
||||
var draft = new DraftSnapshot
|
||||
{
|
||||
GenerationId = 1, ClusterId = "c",
|
||||
DriverInstances = [new DriverInstance { DriverInstanceId = "d-galaxy", ClusterId = "c", NamespaceId = "ns-1", Name = "Galaxy", DriverType = "GalaxyMxGateway", DriverConfig = "{}" }],
|
||||
Tags = [BuildTag(equipmentId: "eq-1", name: "galaxytag", folderPath: null, driverInstanceId: "d-galaxy", tagConfig: "{\"FullName\":\"X.Y\"}")],
|
||||
};
|
||||
|
||||
DraftValidator.Validate(draft).ShouldNotContain(e => e.Code == "GalaxyTagMissingReference");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an equipment-scoped Tag bound to a NON-Galaxy driver with an empty
|
||||
/// TagConfig is NOT flagged as a Galaxy tag missing its reference — only GalaxyMxGateway-bound
|
||||
/// equipment tags carry the Galaxy FullName requirement.</summary>
|
||||
[Fact]
|
||||
public void GalaxyTag_check_skips_nonGalaxy_equipment_tag()
|
||||
{
|
||||
var draft = new DraftSnapshot
|
||||
{
|
||||
GenerationId = 1, ClusterId = "c",
|
||||
DriverInstances = [new DriverInstance { DriverInstanceId = "d-modbus", ClusterId = "c", NamespaceId = "ns-1", Name = "Modbus", DriverType = "Modbus", DriverConfig = "{}" }],
|
||||
Tags = [BuildTag(equipmentId: "eq-1", name: "galaxytag", folderPath: null, driverInstanceId: "d-modbus", tagConfig: "{}")],
|
||||
};
|
||||
|
||||
DraftValidator.Validate(draft).ShouldNotContain(e => e.Code == "GalaxyTagMissingReference");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// Phase 6.3 task #148 part 2 — ValidateClusterTopology
|
||||
// Phase 6.3 task #148 part 2 — ValidateClusterTopology (unchanged in v3)
|
||||
// ------------------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that cluster topology validation checks node count against declared redundancy mode.</summary>
|
||||
@@ -479,7 +245,6 @@ public sealed class DraftValidatorTests
|
||||
[Fact]
|
||||
public void ValidateClusterTopology_flags_disabled_node_mismatch()
|
||||
{
|
||||
// Declared 2 + Hot, but one node disabled — runtime would boot InvalidTopology.
|
||||
var cluster = BuildCluster(nodeCount: 2, mode: RedundancyMode.Hot);
|
||||
var nodes = new[]
|
||||
{
|
||||
@@ -491,11 +256,6 @@ public sealed class DraftValidatorTests
|
||||
errors.ShouldContain(e => e.Code == "ClusterEnabledNodeCountMismatch");
|
||||
}
|
||||
|
||||
// v2: the "exactly one Primary per cluster" check is gone — Akka cluster's
|
||||
// role-leader-of-"driver" elects the primary at runtime. The corresponding
|
||||
// ValidateClusterTopology_flags_multiple_Primary test (and the
|
||||
// ClusterMultiplePrimary error code it asserted) were removed alongside Task 14d.
|
||||
|
||||
/// <summary>Verifies that a valid standalone cluster passes validation.</summary>
|
||||
[Fact]
|
||||
public void ValidateClusterTopology_returns_no_errors_on_valid_standalone()
|
||||
@@ -540,13 +300,6 @@ public sealed class DraftValidatorTests
|
||||
[Fact]
|
||||
public void PathLength_uses_actual_Enterprise_Site_when_provided()
|
||||
{
|
||||
// Craft a snapshot where the 32+32 approximation would flag PathTooLong but the
|
||||
// actual Enterprise/Site lengths would not. Equipment name intentionally exceeds the
|
||||
// UNS-segment 32-char limit (it would also trigger UnsSegmentInvalid — that is fine;
|
||||
// both checks are independent and the test is filtering only on PathTooLong).
|
||||
//
|
||||
// approx: 32+32 + 32+32+90 +4 = 222 > 200 → would flag PathTooLong
|
||||
// actual: 2 +1 + 32+32+90 +4 = 161 ≤ 200 → no PathTooLong
|
||||
var areaId = "area-a";
|
||||
var lineId = "line-b";
|
||||
var uuid = Guid.NewGuid();
|
||||
@@ -567,7 +320,6 @@ public sealed class DraftValidatorTests
|
||||
EquipmentUuid = uuid,
|
||||
EquipmentId = DraftValidator.DeriveEquipmentId(uuid),
|
||||
Name = eqName,
|
||||
DriverInstanceId = "d",
|
||||
UnsLineId = lineId,
|
||||
MachineCode = "m",
|
||||
},
|
||||
@@ -584,11 +336,6 @@ public sealed class DraftValidatorTests
|
||||
[Fact]
|
||||
public void PathLength_conservative_fallback_when_Enterprise_Site_absent()
|
||||
{
|
||||
// Without Enterprise/Site on the snapshot the validator assumes 32+32.
|
||||
// A path whose segments sum to 93 chars (area=32 + line=32 + eq=29) fits
|
||||
// under 200 even with the 32+32 approximation (32+32+93+4 = 161 ≤ 200) and
|
||||
// must NOT be flagged — the fallback must not over-penalise valid paths that
|
||||
// would also be valid under real Enterprise/Site values.
|
||||
var areaId = "area-x";
|
||||
var lineId = "line-y";
|
||||
var uuid = Guid.NewGuid();
|
||||
@@ -597,7 +344,6 @@ public sealed class DraftValidatorTests
|
||||
{
|
||||
GenerationId = 1,
|
||||
ClusterId = "c",
|
||||
// Enterprise and Site deliberately omitted — conservative fallback path
|
||||
UnsAreas = [new UnsArea { UnsAreaId = areaId, ClusterId = "c", Name = new string('a', 32) }],
|
||||
UnsLines = [new UnsLine { UnsLineId = lineId, UnsAreaId = areaId, Name = new string('b', 32) }],
|
||||
Equipment =
|
||||
@@ -607,7 +353,6 @@ public sealed class DraftValidatorTests
|
||||
EquipmentUuid = uuid,
|
||||
EquipmentId = DraftValidator.DeriveEquipmentId(uuid),
|
||||
Name = new string('c', 29),
|
||||
DriverInstanceId = "d",
|
||||
UnsLineId = lineId,
|
||||
MachineCode = "m",
|
||||
},
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// v3 WP1 — exercises the DB-enforceable integrity + sibling-name uniqueness rules of the Raw tree
|
||||
/// against the real SQL schema (the filtered unique indexes over nullable parent columns and the
|
||||
/// UnsTagReference (Equipment, Tag) uniqueness). Runs against the shared SchemaComplianceFixture DB;
|
||||
/// every row uses a GUID-suffixed logical id so tests do not clobber one another.
|
||||
/// </summary>
|
||||
[Trait("Category", "SchemaCompliance")]
|
||||
[Collection(nameof(SchemaComplianceCollection))]
|
||||
public sealed class RawSchemaIntegrityTests
|
||||
{
|
||||
private readonly SchemaComplianceFixture _fixture;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="RawSchemaIntegrityTests"/> class.</summary>
|
||||
/// <param name="fixture">The schema compliance fixture (real SQL DB).</param>
|
||||
public RawSchemaIntegrityTests(SchemaComplianceFixture fixture) => _fixture = fixture;
|
||||
|
||||
private OtOpcUaConfigDbContext NewContext()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<OtOpcUaConfigDbContext>()
|
||||
.UseSqlServer(_fixture.ConnectionString)
|
||||
.Options;
|
||||
return new OtOpcUaConfigDbContext(options);
|
||||
}
|
||||
|
||||
// Logical ids are nvarchar(64); prefix + '-' + 32 hex stays well under that.
|
||||
private static string Id(string prefix) => $"{prefix}-{Guid.NewGuid():N}";
|
||||
|
||||
private string SeedCluster()
|
||||
{
|
||||
var clusterId = Id("cl");
|
||||
using var db = NewContext();
|
||||
db.ServerClusters.Add(new ServerCluster
|
||||
{
|
||||
ClusterId = clusterId,
|
||||
Name = Id("Cluster"),
|
||||
Enterprise = "zb",
|
||||
Site = "dev",
|
||||
NodeCount = 1,
|
||||
RedundancyMode = RedundancyMode.None,
|
||||
CreatedBy = "wp1-test",
|
||||
});
|
||||
db.SaveChanges();
|
||||
return clusterId;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RawFolder_root_sibling_name_must_be_unique_within_cluster()
|
||||
{
|
||||
var clusterId = SeedCluster();
|
||||
using var db = NewContext();
|
||||
db.RawFolders.Add(new RawFolder { RawFolderId = Id("rf"), ClusterId = clusterId, Name = "plc-area" });
|
||||
db.RawFolders.Add(new RawFolder { RawFolderId = Id("rf"), ClusterId = clusterId, Name = "plc-area" });
|
||||
|
||||
Should.Throw<DbUpdateException>(() => db.SaveChanges());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RawFolder_same_root_name_in_different_cluster_is_allowed()
|
||||
{
|
||||
var clusterA = SeedCluster();
|
||||
var clusterB = SeedCluster();
|
||||
using var db = NewContext();
|
||||
db.RawFolders.Add(new RawFolder { RawFolderId = Id("rf"), ClusterId = clusterA, Name = "shared-name" });
|
||||
db.RawFolders.Add(new RawFolder { RawFolderId = Id("rf"), ClusterId = clusterB, Name = "shared-name" });
|
||||
|
||||
Should.NotThrow(() => db.SaveChanges());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RawFolder_nested_sibling_name_must_be_unique_within_parent()
|
||||
{
|
||||
var clusterId = SeedCluster();
|
||||
var parentId = Id("rf-parent");
|
||||
using var db = NewContext();
|
||||
db.RawFolders.Add(new RawFolder { RawFolderId = parentId, ClusterId = clusterId, Name = "parent" });
|
||||
db.RawFolders.Add(new RawFolder { RawFolderId = Id("rf"), ClusterId = clusterId, ParentRawFolderId = parentId, Name = "child" });
|
||||
db.RawFolders.Add(new RawFolder { RawFolderId = Id("rf"), ClusterId = clusterId, ParentRawFolderId = parentId, Name = "child" });
|
||||
|
||||
Should.Throw<DbUpdateException>(() => db.SaveChanges());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DriverInstance_cluster_root_sibling_name_must_be_unique()
|
||||
{
|
||||
var clusterId = SeedCluster();
|
||||
using var db = NewContext();
|
||||
db.DriverInstances.Add(new DriverInstance { DriverInstanceId = Id("di"), ClusterId = clusterId, Name = "modbus", DriverType = "Modbus", DriverConfig = "{}" });
|
||||
db.DriverInstances.Add(new DriverInstance { DriverInstanceId = Id("di"), ClusterId = clusterId, Name = "modbus", DriverType = "Modbus", DriverConfig = "{}" });
|
||||
|
||||
Should.Throw<DbUpdateException>(() => db.SaveChanges());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Device_sibling_name_must_be_unique_within_driver()
|
||||
{
|
||||
var driverId = Id("di");
|
||||
using var db = NewContext();
|
||||
db.Devices.Add(new Device { DeviceId = Id("dev"), DriverInstanceId = driverId, Name = "plc-1", DeviceConfig = "{}" });
|
||||
db.Devices.Add(new Device { DeviceId = Id("dev"), DriverInstanceId = driverId, Name = "plc-1", DeviceConfig = "{}" });
|
||||
|
||||
Should.Throw<DbUpdateException>(() => db.SaveChanges());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TagGroup_device_root_sibling_name_must_be_unique()
|
||||
{
|
||||
var deviceId = Id("dev");
|
||||
using var db = NewContext();
|
||||
db.TagGroups.Add(new TagGroup { TagGroupId = Id("tg"), DeviceId = deviceId, Name = "motors" });
|
||||
db.TagGroups.Add(new TagGroup { TagGroupId = Id("tg"), DeviceId = deviceId, Name = "motors" });
|
||||
|
||||
Should.Throw<DbUpdateException>(() => db.SaveChanges());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tag_device_root_sibling_name_must_be_unique()
|
||||
{
|
||||
var deviceId = Id("dev");
|
||||
using var db = NewContext();
|
||||
db.Tags.Add(NewTag(deviceId, tagGroupId: null, name: "speed"));
|
||||
db.Tags.Add(NewTag(deviceId, tagGroupId: null, name: "speed"));
|
||||
|
||||
Should.Throw<DbUpdateException>(() => db.SaveChanges());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tag_grouped_sibling_name_must_be_unique_within_group()
|
||||
{
|
||||
var deviceId = Id("dev");
|
||||
var groupId = Id("tg");
|
||||
using var db = NewContext();
|
||||
db.Tags.Add(NewTag(deviceId, tagGroupId: groupId, name: "speed"));
|
||||
db.Tags.Add(NewTag(deviceId, tagGroupId: groupId, name: "speed"));
|
||||
|
||||
Should.Throw<DbUpdateException>(() => db.SaveChanges());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tag_same_name_in_different_groups_is_allowed()
|
||||
{
|
||||
var deviceId = Id("dev");
|
||||
using var db = NewContext();
|
||||
db.Tags.Add(NewTag(deviceId, tagGroupId: Id("tg"), name: "speed"));
|
||||
db.Tags.Add(NewTag(deviceId, tagGroupId: Id("tg"), name: "speed"));
|
||||
|
||||
Should.NotThrow(() => db.SaveChanges());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnsTagReference_equipment_tag_pair_must_be_unique()
|
||||
{
|
||||
var equipmentId = Id("eq");
|
||||
var tagId = Id("tag");
|
||||
using var db = NewContext();
|
||||
db.UnsTagReferences.Add(new UnsTagReference { UnsTagReferenceId = Id("utr"), EquipmentId = equipmentId, TagId = tagId });
|
||||
db.UnsTagReferences.Add(new UnsTagReference { UnsTagReferenceId = Id("utr"), EquipmentId = equipmentId, TagId = tagId });
|
||||
|
||||
Should.Throw<DbUpdateException>(() => db.SaveChanges());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnsTagReference_same_tag_different_equipment_is_allowed()
|
||||
{
|
||||
var tagId = Id("tag");
|
||||
using var db = NewContext();
|
||||
db.UnsTagReferences.Add(new UnsTagReference { UnsTagReferenceId = Id("utr"), EquipmentId = Id("eq"), TagId = tagId });
|
||||
db.UnsTagReferences.Add(new UnsTagReference { UnsTagReferenceId = Id("utr"), EquipmentId = Id("eq"), TagId = tagId });
|
||||
|
||||
Should.NotThrow(() => db.SaveChanges());
|
||||
}
|
||||
|
||||
private static Tag NewTag(string deviceId, string? tagGroupId, string name) => new()
|
||||
{
|
||||
TagId = Id("tag"),
|
||||
DeviceId = deviceId,
|
||||
TagGroupId = tagGroupId,
|
||||
Name = name,
|
||||
DataType = "Float",
|
||||
AccessLevel = TagAccessLevel.Read,
|
||||
TagConfig = "{}",
|
||||
};
|
||||
}
|
||||
@@ -28,14 +28,15 @@ public sealed class SchemaComplianceTests
|
||||
{
|
||||
"ServerCluster", "ClusterNode", "ClusterNodeCredential",
|
||||
"ConfigAuditLog",
|
||||
"Namespace", "UnsArea", "UnsLine",
|
||||
"UnsArea", "UnsLine",
|
||||
// v3 Raw-tree tables (Namespace retired; RawFolder/TagGroup/UnsTagReference added)
|
||||
"RawFolder", "TagGroup", "UnsTagReference",
|
||||
"DriverInstance", "Device", "Equipment", "Tag", "PollGroup", "VirtualTag",
|
||||
"NodeAcl", "ExternalIdReservation",
|
||||
"DriverHostStatus",
|
||||
"DriverInstanceResilienceStatus",
|
||||
"LdapGroupRoleMapping",
|
||||
"EquipmentImportBatch",
|
||||
"EquipmentImportRow",
|
||||
// v3 dropped the orphaned EquipmentImportBatch / EquipmentImportRow tables
|
||||
"Script", "ScriptedAlarm", "ScriptedAlarmState",
|
||||
// v2 deploy-model tables (Phase 1 of Akka + fused-hosting alignment)
|
||||
"Deployment", "NodeDeploymentState", "ConfigEdit", "DataProtectionKeys",
|
||||
@@ -60,6 +61,15 @@ SELECT name FROM sys.tables WHERE name <> '__EFMigrationsHistory' ORDER BY name;
|
||||
{
|
||||
("UX_ClusterNodeCredential_Value", "([Enabled]=(1))"),
|
||||
("UX_ExternalIdReservation_KindValue_Active", "([ReleasedAt] IS NULL)"),
|
||||
// v3 sibling-name filtered unique indexes over nullable parent columns.
|
||||
("UX_RawFolder_Parent_Name", "([ParentRawFolderId] IS NOT NULL)"),
|
||||
("UX_RawFolder_ClusterRoot_Name", "([ParentRawFolderId] IS NULL)"),
|
||||
("UX_DriverInstance_Folder_Name", "([RawFolderId] IS NOT NULL)"),
|
||||
("UX_DriverInstance_ClusterRoot_Name", "([RawFolderId] IS NULL)"),
|
||||
("UX_TagGroup_Parent_Name", "([ParentTagGroupId] IS NOT NULL)"),
|
||||
("UX_TagGroup_DeviceRoot_Name", "([ParentTagGroupId] IS NULL)"),
|
||||
("UX_Tag_Group_Name", "([TagGroupId] IS NOT NULL)"),
|
||||
("UX_Tag_Device_Name", "([TagGroupId] IS NULL)"),
|
||||
};
|
||||
|
||||
var rows = QueryRows(@"
|
||||
@@ -148,20 +158,36 @@ SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Equipment
|
||||
columns.ShouldContain(col, $"Equipment missing expected column: {col}");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Namespace has at least one unique index.</summary>
|
||||
/// <summary>Verifies the retired v2 tables (Namespace, EquipmentImportBatch/Row) are absent.</summary>
|
||||
[Fact]
|
||||
public void Namespace_has_some_unique_index()
|
||||
public void Retired_v2_tables_are_absent()
|
||||
{
|
||||
// v2 dropped the "per-generation" qualifier from namespace uniqueness when live-edit
|
||||
// replaced draft/publish. The v2 index name is implementation-defined; assert that
|
||||
// *some* unique index exists on Namespace to catch unintentional index drops.
|
||||
var indexes = QueryStrings(@"
|
||||
var tables = QueryStrings("SELECT name FROM sys.tables;").ToHashSet();
|
||||
tables.ShouldNotContain("Namespace");
|
||||
tables.ShouldNotContain("EquipmentImportBatch");
|
||||
tables.ShouldNotContain("EquipmentImportRow");
|
||||
}
|
||||
|
||||
/// <summary>Verifies the new v3 Raw-tree tables each carry their logical-id unique index.</summary>
|
||||
[Fact]
|
||||
public void V3_raw_tree_tables_have_logical_id_unique_indexes()
|
||||
{
|
||||
var expected = new[]
|
||||
{
|
||||
("RawFolder", "UX_RawFolder_LogicalId"),
|
||||
("TagGroup", "UX_TagGroup_LogicalId"),
|
||||
("UnsTagReference", "UX_UnsTagReference_LogicalId"),
|
||||
};
|
||||
|
||||
foreach (var (table, index) in expected)
|
||||
{
|
||||
var indexes = QueryStrings($@"
|
||||
SELECT i.name
|
||||
FROM sys.indexes i
|
||||
JOIN sys.tables t ON i.object_id = t.object_id
|
||||
WHERE t.name = 'Namespace' AND i.is_unique = 1;").ToList();
|
||||
|
||||
indexes.ShouldNotBeEmpty();
|
||||
WHERE t.name = '{table}' AND i.is_unique = 1;").ToHashSet();
|
||||
indexes.ShouldContain(index, $"{table} missing unique index {index}");
|
||||
}
|
||||
}
|
||||
|
||||
private List<string> QueryStrings(string sql)
|
||||
|
||||
@@ -182,13 +182,13 @@ public sealed class ScriptingEntitiesTests
|
||||
ctx.ScriptedAlarmStates.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the AddPhase7ScriptingTables migration exists in the assembly.</summary>
|
||||
/// <summary>Verifies that the squashed V3Initial migration exists in the assembly.</summary>
|
||||
[Fact]
|
||||
public void AddPhase7ScriptingTables_migration_exists_in_assembly()
|
||||
public void V3Initial_migration_exists_in_assembly()
|
||||
{
|
||||
// The migration type carries the design-time snapshot + Up/Down methods EF uses to
|
||||
// apply the schema. Missing = schema won't roll forward in deployments.
|
||||
var t = typeof(Migrations.AddPhase7ScriptingTables);
|
||||
// v3 WP1 squashed every prior migration into V3Initial. The migration type carries the
|
||||
// design-time snapshot + Up/Down (incl. the scripting tables) EF uses to apply the schema.
|
||||
var t = typeof(Migrations.V3Initial);
|
||||
t.ShouldNotBeNull();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user