refactor(configdb): drop GenerationId FK from live-edit entities

Phase 1b of the v2 entity-model rewrite. The design's live-edit model means
the 12 v2 live-edit entities no longer carry a generation scope — they're
edited directly via AdminOperationsActor, with RowVersion (added in Task 14a)
providing last-write-wins detection.

Entity changes (12 files):

  Equipment, DriverInstance, Device, Tag, PollGroup, Namespace,
  UnsArea, UnsLine, NodeAcl, Script, VirtualTag, ScriptedAlarm

  - Removed: public long GenerationId
  - Removed: public ConfigGeneration? Generation (navigation)

DbContext changes (OtOpcUaConfigDbContext.cs):

  - Removed 12 HasOne(x => x.Generation).WithMany().HasForeignKey... mappings
  - Rewrote ~36 indexes: dropped the GenerationId column from each composite
    key, renamed UX_<Table>_Generation_<X> -> UX_<Table>_<X> and
    IX_<Table>_Generation_<X> -> IX_<Table>_<X>. Logical IDs become globally
    unique (UX_<Table>_LogicalId on the LogicalId column alone).
  - Removed Namespace's redundant UX_Namespace_Generation_LogicalId_Cluster
    index (subsumed by the new UX_Namespace_LogicalId).

Core.Tests fixtures (4 files):

  Removed "GenerationId = 1," lines from:
    - PermissionTrieBuilderTests.cs (NodeAcl Row factory)
    - PermissionTrieTests.cs (NodeAcl Row factory)
    - TriePermissionEvaluatorTests.cs (NodeAcl Row factory + 2 gen{1,5}Row
      mutations that test stale-generation evaluation; the trie itself still
      carries a generation tag via PermissionTrie.GenerationId, fed in via
      PermissionTrieBuilder.Build's generationId parameter, so the tests
      still exercise the production code path)
    - EquipmentNodeWalkerTests.cs (Area/Line/Eq/Tag/VirtualTag/ScriptedAlarm
      builders)

Expected breakage (accepted per Task 56 policy):

  src/Server/ZB.MOM.WW.OtOpcUa.Server   ~25 errors  (DriverInstanceBootstrapper,
                                                     AuthorizationBootstrap,
                                                     EquipmentNamespaceContentLoader,
                                                     Phase7Composer, ...)
  src/Server/ZB.MOM.WW.OtOpcUa.Admin    ~45 errors  (VirtualTags.razor,
                                                     ScriptedAlarms.razor,
                                                     DriverInstanceService,
                                                     EquipmentService,
                                                     EquipmentImportBatchService,
                                                     UnsService,
                                                     FocasDriverDetailService,
                                                     ...)

Server.Tests, Admin.Tests, Admin.E2ETests also break transitively (they
project-reference Server/Admin). All deleted in Task 56.

Verification:
  dotnet build src/Core/ZB.MOM.WW.OtOpcUa.Configuration -> 0 errors
  dotnet build tests/Core/ZB.MOM.WW.OtOpcUa.Core.Tests  -> 0 errors
  dotnet build tests/Core/ZB.MOM.WW.OtOpcUa.Configuration.Tests -> 0 errors
  dotnet build (whole solution) -> 70 errors, all in Server/Admin
This commit is contained in:
Joseph Doherty
2026-05-26 04:06:25 -04:00
parent 4bb4ad8acb
commit 13d3aeab09
17 changed files with 63 additions and 137 deletions

View File

@@ -5,8 +5,6 @@ public sealed class Device
{ {
public Guid DeviceRowId { get; set; } public Guid DeviceRowId { get; set; }
public long GenerationId { get; set; }
public required string DeviceId { get; set; } public required string DeviceId { get; set; }
/// <summary>Logical FK to <see cref="DriverInstance.DriverInstanceId"/>.</summary> /// <summary>Logical FK to <see cref="DriverInstance.DriverInstanceId"/>.</summary>
@@ -21,6 +19,4 @@ public sealed class Device
/// <summary>Optimistic concurrency token for last-write-wins detection in the v2 live-edit model.</summary> /// <summary>Optimistic concurrency token for last-write-wins detection in the v2 live-edit model.</summary>
public byte[] RowVersion { get; set; } = Array.Empty<byte>(); public byte[] RowVersion { get; set; } = Array.Empty<byte>();
public ConfigGeneration? Generation { get; set; }
} }

View File

@@ -5,8 +5,6 @@ public sealed class DriverInstance
{ {
public Guid DriverInstanceRowId { get; set; } public Guid DriverInstanceRowId { get; set; }
public long GenerationId { get; set; }
public required string DriverInstanceId { get; set; } public required string DriverInstanceId { get; set; }
public required string ClusterId { get; set; } public required string ClusterId { get; set; }
@@ -48,6 +46,5 @@ public sealed class DriverInstance
/// <summary>Optimistic concurrency token for last-write-wins detection in the v2 live-edit model.</summary> /// <summary>Optimistic concurrency token for last-write-wins detection in the v2 live-edit model.</summary>
public byte[] RowVersion { get; set; } = Array.Empty<byte>(); public byte[] RowVersion { get; set; } = Array.Empty<byte>();
public ConfigGeneration? Generation { get; set; }
public ServerCluster? Cluster { get; set; } public ServerCluster? Cluster { get; set; }
} }

View File

@@ -9,8 +9,6 @@ public sealed class Equipment
{ {
public Guid EquipmentRowId { get; set; } public Guid EquipmentRowId { get; set; }
public long GenerationId { get; set; }
/// <summary> /// <summary>
/// System-generated stable internal logical ID. Format: <c>'EQ-' + first 12 hex chars of EquipmentUuid</c>. /// System-generated stable internal logical ID. Format: <c>'EQ-' + first 12 hex chars of EquipmentUuid</c>.
/// NEVER operator-supplied, NEVER in CSV imports, NEVER editable in Admin UI (decision #125). /// NEVER operator-supplied, NEVER in CSV imports, NEVER editable in Admin UI (decision #125).
@@ -62,6 +60,4 @@ public sealed class Equipment
/// <summary>Optimistic concurrency token for last-write-wins detection in the v2 live-edit model.</summary> /// <summary>Optimistic concurrency token for last-write-wins detection in the v2 live-edit model.</summary>
public byte[] RowVersion { get; set; } = Array.Empty<byte>(); public byte[] RowVersion { get; set; } = Array.Empty<byte>();
public ConfigGeneration? Generation { get; set; }
} }

View File

@@ -10,9 +10,7 @@ public sealed class Namespace
{ {
public Guid NamespaceRowId { get; set; } public Guid NamespaceRowId { get; set; }
public long GenerationId { get; set; } /// <summary>Stable logical ID, e.g. "LINE3-OPCUA-equipment". Globally unique in v2.</summary>
/// <summary>Stable logical ID across generations, e.g. "LINE3-OPCUA-equipment".</summary>
public required string NamespaceId { get; set; } public required string NamespaceId { get; set; }
public required string ClusterId { get; set; } public required string ClusterId { get; set; }
@@ -29,6 +27,5 @@ public sealed class Namespace
/// <summary>Optimistic concurrency token for last-write-wins detection in the v2 live-edit model.</summary> /// <summary>Optimistic concurrency token for last-write-wins detection in the v2 live-edit model.</summary>
public byte[] RowVersion { get; set; } = Array.Empty<byte>(); public byte[] RowVersion { get; set; } = Array.Empty<byte>();
public ConfigGeneration? Generation { get; set; }
public ServerCluster? Cluster { get; set; } public ServerCluster? Cluster { get; set; }
} }

View File

@@ -10,8 +10,6 @@ public sealed class NodeAcl
{ {
public Guid NodeAclRowId { get; set; } public Guid NodeAclRowId { get; set; }
public long GenerationId { get; set; }
public required string NodeAclId { get; set; } public required string NodeAclId { get; set; }
public required string ClusterId { get; set; } public required string ClusterId { get; set; }
@@ -30,6 +28,4 @@ public sealed class NodeAcl
/// <summary>Optimistic concurrency token for last-write-wins detection in the v2 live-edit model.</summary> /// <summary>Optimistic concurrency token for last-write-wins detection in the v2 live-edit model.</summary>
public byte[] RowVersion { get; set; } = Array.Empty<byte>(); public byte[] RowVersion { get; set; } = Array.Empty<byte>();
public ConfigGeneration? Generation { get; set; }
} }

View File

@@ -5,8 +5,6 @@ public sealed class PollGroup
{ {
public Guid PollGroupRowId { get; set; } public Guid PollGroupRowId { get; set; }
public long GenerationId { get; set; }
public required string PollGroupId { get; set; } public required string PollGroupId { get; set; }
public required string DriverInstanceId { get; set; } public required string DriverInstanceId { get; set; }
@@ -17,6 +15,4 @@ public sealed class PollGroup
/// <summary>Optimistic concurrency token for last-write-wins detection in the v2 live-edit model.</summary> /// <summary>Optimistic concurrency token for last-write-wins detection in the v2 live-edit model.</summary>
public byte[] RowVersion { get; set; } = Array.Empty<byte>(); public byte[] RowVersion { get; set; } = Array.Empty<byte>();
public ConfigGeneration? Generation { get; set; }
} }

View File

@@ -17,9 +17,8 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
public sealed class Script public sealed class Script
{ {
public Guid ScriptRowId { get; set; } public Guid ScriptRowId { get; set; }
public long GenerationId { get; set; }
/// <summary>Stable logical id. Carries across generations.</summary> /// <summary>Stable logical id. Globally unique in v2.</summary>
public required string ScriptId { get; set; } public required string ScriptId { get; set; }
/// <summary>Operator-friendly name for log filtering + Admin UI list view.</summary> /// <summary>Operator-friendly name for log filtering + Admin UI list view.</summary>
@@ -36,6 +35,4 @@ public sealed class Script
/// <summary>Optimistic concurrency token for last-write-wins detection in the v2 live-edit model.</summary> /// <summary>Optimistic concurrency token for last-write-wins detection in the v2 live-edit model.</summary>
public byte[] RowVersion { get; set; } = Array.Empty<byte>(); public byte[] RowVersion { get; set; } = Array.Empty<byte>();
public ConfigGeneration? Generation { get; set; }
} }

View File

@@ -17,9 +17,8 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
public sealed class ScriptedAlarm public sealed class ScriptedAlarm
{ {
public Guid ScriptedAlarmRowId { get; set; } public Guid ScriptedAlarmRowId { get; set; }
public long GenerationId { get; set; }
/// <summary>Stable logical id — drives <c>AlarmConditionType.ConditionName</c>.</summary> /// <summary>Stable logical id — drives <c>AlarmConditionType.ConditionName</c>. Globally unique in v2.</summary>
public required string ScriptedAlarmId { get; set; } public required string ScriptedAlarmId { get; set; }
/// <summary>Logical FK to <see cref="Equipment.EquipmentId"/> — owner of this alarm.</summary> /// <summary>Logical FK to <see cref="Equipment.EquipmentId"/> — owner of this alarm.</summary>
@@ -57,6 +56,4 @@ public sealed class ScriptedAlarm
/// <summary>Optimistic concurrency token for last-write-wins detection in the v2 live-edit model.</summary> /// <summary>Optimistic concurrency token for last-write-wins detection in the v2 live-edit model.</summary>
public byte[] RowVersion { get; set; } = Array.Empty<byte>(); public byte[] RowVersion { get; set; } = Array.Empty<byte>();
public ConfigGeneration? Generation { get; set; }
} }

View File

@@ -11,8 +11,6 @@ public sealed class Tag
{ {
public Guid TagRowId { get; set; } public Guid TagRowId { get; set; }
public long GenerationId { get; set; }
public required string TagId { get; set; } public required string TagId { get; set; }
public required string DriverInstanceId { get; set; } public required string DriverInstanceId { get; set; }
@@ -45,6 +43,4 @@ public sealed class Tag
/// <summary>Optimistic concurrency token for last-write-wins detection in the v2 live-edit model.</summary> /// <summary>Optimistic concurrency token for last-write-wins detection in the v2 live-edit model.</summary>
public byte[] RowVersion { get; set; } = Array.Empty<byte>(); public byte[] RowVersion { get; set; } = Array.Empty<byte>();
public ConfigGeneration? Generation { get; set; }
} }

View File

@@ -5,8 +5,6 @@ public sealed class UnsArea
{ {
public Guid UnsAreaRowId { get; set; } public Guid UnsAreaRowId { get; set; }
public long GenerationId { get; set; }
public required string UnsAreaId { get; set; } public required string UnsAreaId { get; set; }
public required string ClusterId { get; set; } public required string ClusterId { get; set; }
@@ -19,6 +17,5 @@ public sealed class UnsArea
/// <summary>Optimistic concurrency token for last-write-wins detection in the v2 live-edit model.</summary> /// <summary>Optimistic concurrency token for last-write-wins detection in the v2 live-edit model.</summary>
public byte[] RowVersion { get; set; } = Array.Empty<byte>(); public byte[] RowVersion { get; set; } = Array.Empty<byte>();
public ConfigGeneration? Generation { get; set; }
public ServerCluster? Cluster { get; set; } public ServerCluster? Cluster { get; set; }
} }

View File

@@ -5,11 +5,9 @@ public sealed class UnsLine
{ {
public Guid UnsLineRowId { get; set; } public Guid UnsLineRowId { get; set; }
public long GenerationId { get; set; }
public required string UnsLineId { get; set; } public required string UnsLineId { get; set; }
/// <summary>Logical FK to <see cref="UnsArea.UnsAreaId"/>; resolved within the same generation.</summary> /// <summary>Logical FK to <see cref="UnsArea.UnsAreaId"/>.</summary>
public required string UnsAreaId { get; set; } public required string UnsAreaId { get; set; }
/// <summary>UNS level 4 segment: matches <c>^[a-z0-9-]{1,32}$</c> OR equals literal <c>_default</c>.</summary> /// <summary>UNS level 4 segment: matches <c>^[a-z0-9-]{1,32}$</c> OR equals literal <c>_default</c>.</summary>
@@ -19,6 +17,4 @@ public sealed class UnsLine
/// <summary>Optimistic concurrency token for last-write-wins detection in the v2 live-edit model.</summary> /// <summary>Optimistic concurrency token for last-write-wins detection in the v2 live-edit model.</summary>
public byte[] RowVersion { get; set; } = Array.Empty<byte>(); public byte[] RowVersion { get; set; } = Array.Empty<byte>();
public ConfigGeneration? Generation { get; set; }
} }

View File

@@ -21,9 +21,8 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
public sealed class VirtualTag public sealed class VirtualTag
{ {
public Guid VirtualTagRowId { get; set; } public Guid VirtualTagRowId { get; set; }
public long GenerationId { get; set; }
/// <summary>Stable logical id.</summary> /// <summary>Stable logical id. Globally unique in v2.</summary>
public required string VirtualTagId { get; set; } public required string VirtualTagId { get; set; }
/// <summary>Logical FK to <see cref="Equipment.EquipmentId"/> — owner of this virtual tag.</summary> /// <summary>Logical FK to <see cref="Equipment.EquipmentId"/> — owner of this virtual tag.</summary>
@@ -51,6 +50,4 @@ public sealed class VirtualTag
/// <summary>Optimistic concurrency token for last-write-wins detection in the v2 live-edit model.</summary> /// <summary>Optimistic concurrency token for last-write-wins detection in the v2 live-edit model.</summary>
public byte[] RowVersion { get; set; } = Array.Empty<byte>(); public byte[] RowVersion { get; set; } = Array.Empty<byte>();
public ConfigGeneration? Generation { get; set; }
} }

View File

@@ -209,23 +209,18 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
e.Property(x => x.Notes).HasMaxLength(1024); e.Property(x => x.Notes).HasMaxLength(1024);
e.Property(x => x.RowVersion).IsRowVersion(); e.Property(x => x.RowVersion).IsRowVersion();
e.HasOne(x => x.Generation).WithMany()
.HasForeignKey(x => x.GenerationId)
.OnDelete(DeleteBehavior.Restrict);
e.HasOne(x => x.Cluster).WithMany(c => c.Namespaces) e.HasOne(x => x.Cluster).WithMany(c => c.Namespaces)
.HasForeignKey(x => x.ClusterId) .HasForeignKey(x => x.ClusterId)
.OnDelete(DeleteBehavior.Restrict); .OnDelete(DeleteBehavior.Restrict);
e.HasIndex(x => new { x.GenerationId, x.ClusterId, x.Kind }).IsUnique() e.HasIndex(x => new { x.ClusterId, x.Kind }).IsUnique()
.HasDatabaseName("UX_Namespace_Generation_Cluster_Kind"); .HasDatabaseName("UX_Namespace_Cluster_Kind");
e.HasIndex(x => new { x.GenerationId, x.NamespaceUri }).IsUnique() e.HasIndex(x => x.NamespaceUri).IsUnique()
.HasDatabaseName("UX_Namespace_Generation_NamespaceUri"); .HasDatabaseName("UX_Namespace_NamespaceUri");
e.HasIndex(x => new { x.GenerationId, x.NamespaceId }).IsUnique() e.HasIndex(x => x.NamespaceId).IsUnique()
.HasDatabaseName("UX_Namespace_Generation_LogicalId"); .HasDatabaseName("UX_Namespace_LogicalId");
e.HasIndex(x => new { x.GenerationId, x.NamespaceId, x.ClusterId }).IsUnique() e.HasIndex(x => x.ClusterId)
.HasDatabaseName("UX_Namespace_Generation_LogicalId_Cluster"); .HasDatabaseName("IX_Namespace_Cluster");
e.HasIndex(x => new { x.GenerationId, x.ClusterId })
.HasDatabaseName("IX_Namespace_Generation_Cluster");
}); });
} }
@@ -242,12 +237,11 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
e.Property(x => x.Notes).HasMaxLength(512); e.Property(x => x.Notes).HasMaxLength(512);
e.Property(x => x.RowVersion).IsRowVersion(); e.Property(x => x.RowVersion).IsRowVersion();
e.HasOne(x => x.Generation).WithMany().HasForeignKey(x => x.GenerationId).OnDelete(DeleteBehavior.Restrict);
e.HasOne(x => x.Cluster).WithMany().HasForeignKey(x => x.ClusterId).OnDelete(DeleteBehavior.Restrict); e.HasOne(x => x.Cluster).WithMany().HasForeignKey(x => x.ClusterId).OnDelete(DeleteBehavior.Restrict);
e.HasIndex(x => new { x.GenerationId, x.ClusterId }).HasDatabaseName("IX_UnsArea_Generation_Cluster"); e.HasIndex(x => x.ClusterId).HasDatabaseName("IX_UnsArea_Cluster");
e.HasIndex(x => new { x.GenerationId, x.UnsAreaId }).IsUnique().HasDatabaseName("UX_UnsArea_Generation_LogicalId"); e.HasIndex(x => x.UnsAreaId).IsUnique().HasDatabaseName("UX_UnsArea_LogicalId");
e.HasIndex(x => new { x.GenerationId, x.ClusterId, x.Name }).IsUnique().HasDatabaseName("UX_UnsArea_Generation_ClusterName"); e.HasIndex(x => new { x.ClusterId, x.Name }).IsUnique().HasDatabaseName("UX_UnsArea_ClusterName");
}); });
} }
@@ -264,11 +258,9 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
e.Property(x => x.Notes).HasMaxLength(512); e.Property(x => x.Notes).HasMaxLength(512);
e.Property(x => x.RowVersion).IsRowVersion(); e.Property(x => x.RowVersion).IsRowVersion();
e.HasOne(x => x.Generation).WithMany().HasForeignKey(x => x.GenerationId).OnDelete(DeleteBehavior.Restrict); e.HasIndex(x => x.UnsAreaId).HasDatabaseName("IX_UnsLine_Area");
e.HasIndex(x => x.UnsLineId).IsUnique().HasDatabaseName("UX_UnsLine_LogicalId");
e.HasIndex(x => new { x.GenerationId, x.UnsAreaId }).HasDatabaseName("IX_UnsLine_Generation_Area"); e.HasIndex(x => new { x.UnsAreaId, x.Name }).IsUnique().HasDatabaseName("UX_UnsLine_AreaName");
e.HasIndex(x => new { x.GenerationId, x.UnsLineId }).IsUnique().HasDatabaseName("UX_UnsLine_Generation_LogicalId");
e.HasIndex(x => new { x.GenerationId, x.UnsAreaId, x.Name }).IsUnique().HasDatabaseName("UX_UnsLine_Generation_AreaName");
}); });
} }
@@ -294,12 +286,11 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
e.Property(x => x.ResilienceConfig).HasColumnType("nvarchar(max)"); e.Property(x => x.ResilienceConfig).HasColumnType("nvarchar(max)");
e.Property(x => x.RowVersion).IsRowVersion(); e.Property(x => x.RowVersion).IsRowVersion();
e.HasOne(x => x.Generation).WithMany().HasForeignKey(x => x.GenerationId).OnDelete(DeleteBehavior.Restrict);
e.HasOne(x => x.Cluster).WithMany().HasForeignKey(x => x.ClusterId).OnDelete(DeleteBehavior.Restrict); e.HasOne(x => x.Cluster).WithMany().HasForeignKey(x => x.ClusterId).OnDelete(DeleteBehavior.Restrict);
e.HasIndex(x => new { x.GenerationId, x.ClusterId }).HasDatabaseName("IX_DriverInstance_Generation_Cluster"); e.HasIndex(x => x.ClusterId).HasDatabaseName("IX_DriverInstance_Cluster");
e.HasIndex(x => new { x.GenerationId, x.NamespaceId }).HasDatabaseName("IX_DriverInstance_Generation_Namespace"); e.HasIndex(x => x.NamespaceId).HasDatabaseName("IX_DriverInstance_Namespace");
e.HasIndex(x => new { x.GenerationId, x.DriverInstanceId }).IsUnique().HasDatabaseName("UX_DriverInstance_Generation_LogicalId"); e.HasIndex(x => x.DriverInstanceId).IsUnique().HasDatabaseName("UX_DriverInstance_LogicalId");
}); });
} }
@@ -319,10 +310,8 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
e.Property(x => x.DeviceConfig).HasColumnType("nvarchar(max)"); e.Property(x => x.DeviceConfig).HasColumnType("nvarchar(max)");
e.Property(x => x.RowVersion).IsRowVersion(); e.Property(x => x.RowVersion).IsRowVersion();
e.HasOne(x => x.Generation).WithMany().HasForeignKey(x => x.GenerationId).OnDelete(DeleteBehavior.Restrict); e.HasIndex(x => x.DriverInstanceId).HasDatabaseName("IX_Device_Driver");
e.HasIndex(x => x.DeviceId).IsUnique().HasDatabaseName("UX_Device_LogicalId");
e.HasIndex(x => new { x.GenerationId, x.DriverInstanceId }).HasDatabaseName("IX_Device_Generation_Driver");
e.HasIndex(x => new { x.GenerationId, x.DeviceId }).IsUnique().HasDatabaseName("UX_Device_Generation_LogicalId");
}); });
} }
@@ -352,16 +341,14 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
e.Property(x => x.EquipmentClassRef).HasMaxLength(128); e.Property(x => x.EquipmentClassRef).HasMaxLength(128);
e.Property(x => x.RowVersion).IsRowVersion(); e.Property(x => x.RowVersion).IsRowVersion();
e.HasOne(x => x.Generation).WithMany().HasForeignKey(x => x.GenerationId).OnDelete(DeleteBehavior.Restrict); e.HasIndex(x => x.DriverInstanceId).HasDatabaseName("IX_Equipment_Driver");
e.HasIndex(x => x.UnsLineId).HasDatabaseName("IX_Equipment_Line");
e.HasIndex(x => new { x.GenerationId, x.DriverInstanceId }).HasDatabaseName("IX_Equipment_Generation_Driver"); e.HasIndex(x => x.EquipmentId).IsUnique().HasDatabaseName("UX_Equipment_LogicalId");
e.HasIndex(x => new { x.GenerationId, x.UnsLineId }).HasDatabaseName("IX_Equipment_Generation_Line"); e.HasIndex(x => new { x.UnsLineId, x.Name }).IsUnique().HasDatabaseName("UX_Equipment_LinePath");
e.HasIndex(x => new { x.GenerationId, x.EquipmentId }).IsUnique().HasDatabaseName("UX_Equipment_Generation_LogicalId"); e.HasIndex(x => x.EquipmentUuid).IsUnique().HasDatabaseName("UX_Equipment_Uuid");
e.HasIndex(x => new { x.GenerationId, x.UnsLineId, x.Name }).IsUnique().HasDatabaseName("UX_Equipment_Generation_LinePath"); e.HasIndex(x => x.ZTag).HasFilter("[ZTag] IS NOT NULL").HasDatabaseName("IX_Equipment_ZTag");
e.HasIndex(x => new { x.GenerationId, x.EquipmentUuid }).IsUnique().HasDatabaseName("UX_Equipment_Generation_Uuid"); e.HasIndex(x => x.SAPID).HasFilter("[SAPID] IS NOT NULL").HasDatabaseName("IX_Equipment_SAPID");
e.HasIndex(x => new { x.GenerationId, x.ZTag }).HasFilter("[ZTag] IS NOT NULL").HasDatabaseName("IX_Equipment_Generation_ZTag"); e.HasIndex(x => x.MachineCode).HasDatabaseName("IX_Equipment_MachineCode");
e.HasIndex(x => new { x.GenerationId, x.SAPID }).HasFilter("[SAPID] IS NOT NULL").HasDatabaseName("IX_Equipment_Generation_SAPID");
e.HasIndex(x => new { x.GenerationId, x.MachineCode }).HasDatabaseName("IX_Equipment_Generation_MachineCode");
}); });
} }
@@ -387,19 +374,17 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
e.Property(x => x.TagConfig).HasColumnType("nvarchar(max)"); e.Property(x => x.TagConfig).HasColumnType("nvarchar(max)");
e.Property(x => x.RowVersion).IsRowVersion(); e.Property(x => x.RowVersion).IsRowVersion();
e.HasOne(x => x.Generation).WithMany().HasForeignKey(x => x.GenerationId).OnDelete(DeleteBehavior.Restrict); e.HasIndex(x => new { x.DriverInstanceId, x.DeviceId }).HasDatabaseName("IX_Tag_Driver_Device");
e.HasIndex(x => x.EquipmentId)
e.HasIndex(x => new { x.GenerationId, x.DriverInstanceId, x.DeviceId }).HasDatabaseName("IX_Tag_Generation_Driver_Device");
e.HasIndex(x => new { x.GenerationId, x.EquipmentId })
.HasFilter("[EquipmentId] IS NOT NULL") .HasFilter("[EquipmentId] IS NOT NULL")
.HasDatabaseName("IX_Tag_Generation_Equipment"); .HasDatabaseName("IX_Tag_Equipment");
e.HasIndex(x => new { x.GenerationId, x.TagId }).IsUnique().HasDatabaseName("UX_Tag_Generation_LogicalId"); e.HasIndex(x => x.TagId).IsUnique().HasDatabaseName("UX_Tag_LogicalId");
e.HasIndex(x => new { x.GenerationId, x.EquipmentId, x.Name }).IsUnique() e.HasIndex(x => new { x.EquipmentId, x.Name }).IsUnique()
.HasFilter("[EquipmentId] IS NOT NULL") .HasFilter("[EquipmentId] IS NOT NULL")
.HasDatabaseName("UX_Tag_Generation_EquipmentPath"); .HasDatabaseName("UX_Tag_EquipmentPath");
e.HasIndex(x => new { x.GenerationId, x.DriverInstanceId, x.FolderPath, x.Name }).IsUnique() e.HasIndex(x => new { x.DriverInstanceId, x.FolderPath, x.Name }).IsUnique()
.HasFilter("[EquipmentId] IS NULL") .HasFilter("[EquipmentId] IS NULL")
.HasDatabaseName("UX_Tag_Generation_FolderPath"); .HasDatabaseName("UX_Tag_FolderPath");
}); });
} }
@@ -418,10 +403,8 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
e.Property(x => x.Name).HasMaxLength(128); e.Property(x => x.Name).HasMaxLength(128);
e.Property(x => x.RowVersion).IsRowVersion(); e.Property(x => x.RowVersion).IsRowVersion();
e.HasOne(x => x.Generation).WithMany().HasForeignKey(x => x.GenerationId).OnDelete(DeleteBehavior.Restrict); e.HasIndex(x => x.DriverInstanceId).HasDatabaseName("IX_PollGroup_Driver");
e.HasIndex(x => x.PollGroupId).IsUnique().HasDatabaseName("UX_PollGroup_LogicalId");
e.HasIndex(x => new { x.GenerationId, x.DriverInstanceId }).HasDatabaseName("IX_PollGroup_Generation_Driver");
e.HasIndex(x => new { x.GenerationId, x.PollGroupId }).IsUnique().HasDatabaseName("UX_PollGroup_Generation_LogicalId");
}); });
} }
@@ -441,16 +424,14 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
e.Property(x => x.Notes).HasMaxLength(512); e.Property(x => x.Notes).HasMaxLength(512);
e.Property(x => x.RowVersion).IsRowVersion(); e.Property(x => x.RowVersion).IsRowVersion();
e.HasOne(x => x.Generation).WithMany().HasForeignKey(x => x.GenerationId).OnDelete(DeleteBehavior.Restrict); e.HasIndex(x => x.ClusterId).HasDatabaseName("IX_NodeAcl_Cluster");
e.HasIndex(x => x.LdapGroup).HasDatabaseName("IX_NodeAcl_Group");
e.HasIndex(x => new { x.GenerationId, x.ClusterId }).HasDatabaseName("IX_NodeAcl_Generation_Cluster"); e.HasIndex(x => new { x.ScopeKind, x.ScopeId })
e.HasIndex(x => new { x.GenerationId, x.LdapGroup }).HasDatabaseName("IX_NodeAcl_Generation_Group");
e.HasIndex(x => new { x.GenerationId, x.ScopeKind, x.ScopeId })
.HasFilter("[ScopeId] IS NOT NULL") .HasFilter("[ScopeId] IS NOT NULL")
.HasDatabaseName("IX_NodeAcl_Generation_Scope"); .HasDatabaseName("IX_NodeAcl_Scope");
e.HasIndex(x => new { x.GenerationId, x.NodeAclId }).IsUnique().HasDatabaseName("UX_NodeAcl_Generation_LogicalId"); e.HasIndex(x => x.NodeAclId).IsUnique().HasDatabaseName("UX_NodeAcl_LogicalId");
e.HasIndex(x => new { x.GenerationId, x.ClusterId, x.LdapGroup, x.ScopeKind, x.ScopeId }).IsUnique() e.HasIndex(x => new { x.ClusterId, x.LdapGroup, x.ScopeKind, x.ScopeId }).IsUnique()
.HasDatabaseName("UX_NodeAcl_Generation_GroupScope"); .HasDatabaseName("UX_NodeAcl_GroupScope");
}); });
} }
@@ -666,10 +647,8 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
e.Property(x => x.Language).HasMaxLength(16); e.Property(x => x.Language).HasMaxLength(16);
e.Property(x => x.RowVersion).IsRowVersion(); e.Property(x => x.RowVersion).IsRowVersion();
e.HasOne(x => x.Generation).WithMany().HasForeignKey(x => x.GenerationId).OnDelete(DeleteBehavior.Restrict); e.HasIndex(x => x.ScriptId).IsUnique().HasDatabaseName("UX_Script_LogicalId");
e.HasIndex(x => x.SourceHash).HasDatabaseName("IX_Script_SourceHash");
e.HasIndex(x => new { x.GenerationId, x.ScriptId }).IsUnique().HasDatabaseName("UX_Script_Generation_LogicalId");
e.HasIndex(x => new { x.GenerationId, x.SourceHash }).HasDatabaseName("IX_Script_Generation_SourceHash");
}); });
} }
@@ -693,11 +672,9 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
e.Property(x => x.ScriptId).HasMaxLength(64); e.Property(x => x.ScriptId).HasMaxLength(64);
e.Property(x => x.RowVersion).IsRowVersion(); e.Property(x => x.RowVersion).IsRowVersion();
e.HasOne(x => x.Generation).WithMany().HasForeignKey(x => x.GenerationId).OnDelete(DeleteBehavior.Restrict); e.HasIndex(x => x.VirtualTagId).IsUnique().HasDatabaseName("UX_VirtualTag_LogicalId");
e.HasIndex(x => new { x.EquipmentId, x.Name }).IsUnique().HasDatabaseName("UX_VirtualTag_EquipmentPath");
e.HasIndex(x => new { x.GenerationId, x.VirtualTagId }).IsUnique().HasDatabaseName("UX_VirtualTag_Generation_LogicalId"); e.HasIndex(x => x.ScriptId).HasDatabaseName("IX_VirtualTag_Script");
e.HasIndex(x => new { x.GenerationId, x.EquipmentId, x.Name }).IsUnique().HasDatabaseName("UX_VirtualTag_Generation_EquipmentPath");
e.HasIndex(x => new { x.GenerationId, x.ScriptId }).HasDatabaseName("IX_VirtualTag_Generation_Script");
}); });
} }
@@ -721,11 +698,9 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
e.Property(x => x.PredicateScriptId).HasMaxLength(64); e.Property(x => x.PredicateScriptId).HasMaxLength(64);
e.Property(x => x.RowVersion).IsRowVersion(); e.Property(x => x.RowVersion).IsRowVersion();
e.HasOne(x => x.Generation).WithMany().HasForeignKey(x => x.GenerationId).OnDelete(DeleteBehavior.Restrict); e.HasIndex(x => x.ScriptedAlarmId).IsUnique().HasDatabaseName("UX_ScriptedAlarm_LogicalId");
e.HasIndex(x => new { x.EquipmentId, x.Name }).IsUnique().HasDatabaseName("UX_ScriptedAlarm_EquipmentPath");
e.HasIndex(x => new { x.GenerationId, x.ScriptedAlarmId }).IsUnique().HasDatabaseName("UX_ScriptedAlarm_Generation_LogicalId"); e.HasIndex(x => x.PredicateScriptId).HasDatabaseName("IX_ScriptedAlarm_Script");
e.HasIndex(x => new { x.GenerationId, x.EquipmentId, x.Name }).IsUnique().HasDatabaseName("UX_ScriptedAlarm_Generation_EquipmentPath");
e.HasIndex(x => new { x.GenerationId, x.PredicateScriptId }).HasDatabaseName("IX_ScriptedAlarm_Generation_Script");
}); });
} }

View File

@@ -28,7 +28,6 @@ public sealed class PermissionTrieBuilderTests
{ {
NodeAclRowId = Guid.NewGuid(), NodeAclRowId = Guid.NewGuid(),
NodeAclId = $"acl-{Guid.NewGuid():N}", NodeAclId = $"acl-{Guid.NewGuid():N}",
GenerationId = 1,
ClusterId = clusterId, ClusterId = clusterId,
LdapGroup = group, LdapGroup = group,
ScopeKind = scope, ScopeKind = scope,

View File

@@ -14,7 +14,6 @@ public sealed class PermissionTrieTests
{ {
NodeAclRowId = Guid.NewGuid(), NodeAclRowId = Guid.NewGuid(),
NodeAclId = $"acl-{Guid.NewGuid():N}", NodeAclId = $"acl-{Guid.NewGuid():N}",
GenerationId = 1,
ClusterId = clusterId, ClusterId = clusterId,
LdapGroup = group, LdapGroup = group,
ScopeKind = scope, ScopeKind = scope,

View File

@@ -24,7 +24,6 @@ public sealed class TriePermissionEvaluatorTests
{ {
NodeAclRowId = Guid.NewGuid(), NodeAclRowId = Guid.NewGuid(),
NodeAclId = $"acl-{Guid.NewGuid():N}", NodeAclId = $"acl-{Guid.NewGuid():N}",
GenerationId = 1,
ClusterId = "c1", ClusterId = "c1",
LdapGroup = group, LdapGroup = group,
ScopeKind = scope, ScopeKind = scope,
@@ -149,7 +148,6 @@ public sealed class TriePermissionEvaluatorTests
// publishes generation 2 with the grant removed and it becomes the cache "current". // publishes generation 2 with the grant removed and it becomes the cache "current".
// The evaluator must still honour the session's bound generation 1, not generation 2. // The evaluator must still honour the session's bound generation 1, not generation 2.
var gen1Row = Row("cn=ops", NodeAclScopeKind.Cluster, null, NodePermissions.Read); var gen1Row = Row("cn=ops", NodeAclScopeKind.Cluster, null, NodePermissions.Read);
gen1Row.GenerationId = 1;
var cache = new PermissionTrieCache(); var cache = new PermissionTrieCache();
cache.Install(PermissionTrieBuilder.Build("c1", 1, [gen1Row])); cache.Install(PermissionTrieBuilder.Build("c1", 1, [gen1Row]));
cache.Install(PermissionTrieBuilder.Build("c1", 2, [])); // gen 2 — grant revoked, now current cache.Install(PermissionTrieBuilder.Build("c1", 2, [])); // gen 2 — grant revoked, now current
@@ -169,7 +167,6 @@ public sealed class TriePermissionEvaluatorTests
// Core-002 regression: session is bound to a generation no longer in the cache. // Core-002 regression: session is bound to a generation no longer in the cache.
// The evaluator must fail closed rather than silently using the current generation. // The evaluator must fail closed rather than silently using the current generation.
var gen5Row = Row("cn=ops", NodeAclScopeKind.Cluster, null, NodePermissions.Read); var gen5Row = Row("cn=ops", NodeAclScopeKind.Cluster, null, NodePermissions.Read);
gen5Row.GenerationId = 5;
var cache = new PermissionTrieCache(); var cache = new PermissionTrieCache();
cache.Install(PermissionTrieBuilder.Build("c1", 5, [gen5Row])); cache.Install(PermissionTrieBuilder.Build("c1", 5, [gen5Row]));
var evaluator = new TriePermissionEvaluator(cache, _time); var evaluator = new TriePermissionEvaluator(cache, _time);

View File

@@ -153,7 +153,7 @@ public sealed class EquipmentNodeWalkerTests
var eq = Eq("eq-1", "line-1", "oven-3"); var eq = Eq("eq-1", "line-1", "oven-3");
var vtag = new VirtualTag var vtag = new VirtualTag
{ {
VirtualTagRowId = Guid.NewGuid(), GenerationId = 1, VirtualTagRowId = Guid.NewGuid(),
VirtualTagId = "vt-1", EquipmentId = "eq-1", Name = "LineRate", VirtualTagId = "vt-1", EquipmentId = "eq-1", Name = "LineRate",
DataType = "Float32", ScriptId = "scr-1", Historize = true, DataType = "Float32", ScriptId = "scr-1", Historize = true,
}; };
@@ -179,7 +179,7 @@ public sealed class EquipmentNodeWalkerTests
var eq = Eq("eq-1", "line-1", "oven-3"); var eq = Eq("eq-1", "line-1", "oven-3");
var alarm = new ScriptedAlarm var alarm = new ScriptedAlarm
{ {
ScriptedAlarmRowId = Guid.NewGuid(), GenerationId = 1, ScriptedAlarmRowId = Guid.NewGuid(),
ScriptedAlarmId = "al-1", EquipmentId = "eq-1", Name = "HighTemp", ScriptedAlarmId = "al-1", EquipmentId = "eq-1", Name = "HighTemp",
AlarmType = "LimitAlarm", MessageTemplate = "{Temp} exceeded", AlarmType = "LimitAlarm", MessageTemplate = "{Temp} exceeded",
PredicateScriptId = "scr-9", Severity = 800, PredicateScriptId = "scr-9", Severity = 800,
@@ -205,13 +205,13 @@ public sealed class EquipmentNodeWalkerTests
var eq = Eq("eq-1", "line-1", "oven-3"); var eq = Eq("eq-1", "line-1", "oven-3");
var vtag = new VirtualTag var vtag = new VirtualTag
{ {
VirtualTagRowId = Guid.NewGuid(), GenerationId = 1, VirtualTagRowId = Guid.NewGuid(),
VirtualTagId = "vt-1", EquipmentId = "eq-1", Name = "Disabled", VirtualTagId = "vt-1", EquipmentId = "eq-1", Name = "Disabled",
DataType = "Float32", ScriptId = "scr-1", Enabled = false, DataType = "Float32", ScriptId = "scr-1", Enabled = false,
}; };
var alarm = new ScriptedAlarm var alarm = new ScriptedAlarm
{ {
ScriptedAlarmRowId = Guid.NewGuid(), GenerationId = 1, ScriptedAlarmRowId = Guid.NewGuid(),
ScriptedAlarmId = "al-1", EquipmentId = "eq-1", Name = "DisabledAlarm", ScriptedAlarmId = "al-1", EquipmentId = "eq-1", Name = "DisabledAlarm",
AlarmType = "LimitAlarm", MessageTemplate = "x", AlarmType = "LimitAlarm", MessageTemplate = "x",
PredicateScriptId = "scr-9", Enabled = false, PredicateScriptId = "scr-9", Enabled = false,
@@ -313,18 +313,17 @@ public sealed class EquipmentNodeWalkerTests
private static UnsArea Area(string id, string name) => new() private static UnsArea Area(string id, string name) => new()
{ {
UnsAreaId = id, ClusterId = "c1", Name = name, GenerationId = 1, UnsAreaId = id, ClusterId = "c1", Name = name,
}; };
private static UnsLine Line(string id, string areaId, string name) => new() private static UnsLine Line(string id, string areaId, string name) => new()
{ {
UnsLineId = id, UnsAreaId = areaId, Name = name, GenerationId = 1, UnsLineId = id, UnsAreaId = areaId, Name = name,
}; };
private static Equipment Eq(string equipmentId, string lineId, string name) => new() private static Equipment Eq(string equipmentId, string lineId, string name) => new()
{ {
EquipmentRowId = Guid.NewGuid(), EquipmentRowId = Guid.NewGuid(),
GenerationId = 1,
EquipmentId = equipmentId, EquipmentId = equipmentId,
EquipmentUuid = Guid.NewGuid(), EquipmentUuid = Guid.NewGuid(),
DriverInstanceId = "drv", DriverInstanceId = "drv",
@@ -337,7 +336,6 @@ public sealed class EquipmentNodeWalkerTests
string? equipmentId, string? tagConfig = null) => new() string? equipmentId, string? tagConfig = null) => new()
{ {
TagRowId = Guid.NewGuid(), TagRowId = Guid.NewGuid(),
GenerationId = 1,
TagId = tagId, TagId = tagId,
DriverInstanceId = "drv", DriverInstanceId = "drv",
EquipmentId = equipmentId, EquipmentId = equipmentId,