Phase 1 Streams B–E scaffold + Phase 2 Streams A–C scaffold — 8 new projects with ~70 new tests, all green alongside the 494 v1 IntegrationTests baseline (parity preserved: no v1 tests broken; legacy OtOpcUa.Host untouched). Phase 1 finish: Configuration project (16 entities + 10 enums + DbContext + DesignTimeDbContextFactory + InitialSchema/StoredProcedures/AuthorizationGrants migrations — 8 procs including sp_PublishGeneration with MERGE on ExternalIdReservation per decision #124, sp_RollbackToGeneration cloning rows into a new published generation, sp_ValidateDraft with cross-cluster-namespace + EquipmentUuid-immutability + ZTag/SAPID reservation pre-flight, sp_ComputeGenerationDiff with CHECKSUM-based row signature — plus OtOpcUaNode/OtOpcUaAdmin SQL roles with EXECUTE grants scoped to per-principal-class proc sets and DENY UPDATE/DELETE/INSERT/SELECT on dbo schema); managed DraftValidator covering UNS segment regex, path length, EquipmentUuid immutability across generations, same-cluster namespace binding (decision #122), reservation pre-flight, EquipmentId derivation (decision #125), driver↔namespace compatibility — returning every failing rule in one pass; LiteDB local cache with round-trip + ring pruning + corruption-fast-fail; GenerationApplier with per-entity Added/Removed/Modified diff and dependency-ordered callbacks (namespace → driver → device → equipment → poll-group → tag, Removed before Added); Core project with GenericDriverNodeManager (scaffold for the Phase 2 Galaxy port) and DriverHost lifecycle registry; Server project using Microsoft.Extensions.Hosting BackgroundService replacing TopShelf, with NodeBootstrap that falls back to LiteDB cache when the central DB is unreachable (decision #79); Admin project scaffolded as Blazor Server with Bootstrap 5 sidebar layout, cookie auth, three admin roles (ConfigViewer/ConfigEditor/FleetAdmin), Cluster + Generation services fronting the stored procs. Phase 2 scaffold: Driver.Galaxy.Shared (netstandard2.0) with full MessagePack IPC contract surface — Hello version negotiation, Open/CloseSession, Heartbeat, DiscoverHierarchy + GalaxyObjectInfo/GalaxyAttributeInfo, Read/WriteValues, Subscribe/Unsubscribe/OnDataChange, AlarmSubscribe/Event/Ack, HistoryRead, HostConnectivityStatus, Recycle — plus length-prefixed framing (decision #28) with a 16 MiB cap and thread-safe FrameWriter/FrameReader; Driver.Galaxy.Host (net48) implementing the Tier C cross-cutting protections from driver-stability.md — strict PipeAcl (allow configured server SID only, explicit deny on LocalSystem + Administrators), PipeServer with caller-SID verification via pipe.RunAsClient + WindowsIdentity.GetCurrent and per-process shared-secret Hello, Galaxy-specific MemoryWatchdog (warn at max(1.5×baseline, +200 MB), soft-recycle at max(2×baseline, +200 MB), hard ceiling 1.5 GB, slope ≥5 MB/min over 30-min rolling window), RecyclePolicy (1 soft recycle per hour cap + 03:00 local daily scheduled), PostMortemMmf (1000-entry ring buffer in %ProgramData%\OtOpcUa\driver-postmortem\galaxy.mmf, survives hard crash, readable cross-process), MxAccessHandle : SafeHandle (ReleaseHandle loops Marshal.ReleaseComObject until refcount=0 then calls optional unregister callback), StaPump with responsiveness probe (BlockingCollection dispatcher for Phase 1 — real Win32 GetMessage/DispatchMessage pump slots in with the same semantics when the Galaxy code lift happens), IsExternalInit shim for init setters on .NET 4.8; Driver.Galaxy.Proxy (net10) implementing IDriver + ITagDiscovery forwarding over the IPC channel with MX data-type and security-classification mapping, plus Supervisor pieces — Backoff (5s → 15s → 60s capped, reset-on-stable-run), CircuitBreaker (3 crashes per 5 min opens; 1h → 4h → manual cooldown escalation; sticky alert doesn't auto-clear), HeartbeatMonitor (2s cadence, 3 consecutive misses = host dead per driver-stability.md). Infrastructure: docker SQL Server remapped to host port 14330 to coexist with the native MSSQL14 Galaxy ZB DB instance on 1433; NuGetAuditSuppress applied per-project for two System.Security.Cryptography.Xml advisories that only reach via EF Core Design with PrivateAssets=all (fix ships in 11.0.0-preview); .slnx gains 14 project registrations. Deferred with explicit TODOs in docs/v2/implementation/phase-2-partial-exit-evidence.md: Phase 1 Stream E Admin UI pages (Generations listing + draft-diff-publish, Equipment CRUD with OPC 40010 fields, UNS Areas/Lines tabs, ACLs + permission simulator, Generic JSON config editor, SignalR real-time, Release-Reservation + Merge-Equipment workflows, LDAP login page, AppServer smoke test per decision #142), Phase 2 Stream D (Galaxy MXAccess code lift out of legacy OtOpcUa.Host, dual-service installer, appsettings → DriverConfig migration script, legacy Host deletion — blocked by parity), Phase 2 Stream E (v1 IntegrationTests against v2 topology, Client.CLI walkthrough diff, four 2026-04-13 stability findings regression tests, adversarial review — requires live MXAccess runtime).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
487
src/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs
Normal file
487
src/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs
Normal file
@@ -0,0 +1,487 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Central config DB context. Schema matches <c>docs/v2/config-db-schema.md</c> exactly —
|
||||
/// any divergence is a defect caught by the SchemaComplianceTests introspection check.
|
||||
/// </summary>
|
||||
public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbContext> options)
|
||||
: DbContext(options)
|
||||
{
|
||||
public DbSet<ServerCluster> ServerClusters => Set<ServerCluster>();
|
||||
public DbSet<ClusterNode> ClusterNodes => Set<ClusterNode>();
|
||||
public DbSet<ClusterNodeCredential> ClusterNodeCredentials => Set<ClusterNodeCredential>();
|
||||
public DbSet<ConfigGeneration> ConfigGenerations => Set<ConfigGeneration>();
|
||||
public DbSet<Namespace> Namespaces => Set<Namespace>();
|
||||
public DbSet<UnsArea> UnsAreas => Set<UnsArea>();
|
||||
public DbSet<UnsLine> UnsLines => Set<UnsLine>();
|
||||
public DbSet<DriverInstance> DriverInstances => Set<DriverInstance>();
|
||||
public DbSet<Device> Devices => Set<Device>();
|
||||
public DbSet<Equipment> Equipment => Set<Equipment>();
|
||||
public DbSet<Tag> Tags => Set<Tag>();
|
||||
public DbSet<PollGroup> PollGroups => Set<PollGroup>();
|
||||
public DbSet<NodeAcl> NodeAcls => Set<NodeAcl>();
|
||||
public DbSet<ClusterNodeGenerationState> ClusterNodeGenerationStates => Set<ClusterNodeGenerationState>();
|
||||
public DbSet<ConfigAuditLog> ConfigAuditLogs => Set<ConfigAuditLog>();
|
||||
public DbSet<ExternalIdReservation> ExternalIdReservations => Set<ExternalIdReservation>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
ConfigureServerCluster(modelBuilder);
|
||||
ConfigureClusterNode(modelBuilder);
|
||||
ConfigureClusterNodeCredential(modelBuilder);
|
||||
ConfigureConfigGeneration(modelBuilder);
|
||||
ConfigureNamespace(modelBuilder);
|
||||
ConfigureUnsArea(modelBuilder);
|
||||
ConfigureUnsLine(modelBuilder);
|
||||
ConfigureDriverInstance(modelBuilder);
|
||||
ConfigureDevice(modelBuilder);
|
||||
ConfigureEquipment(modelBuilder);
|
||||
ConfigureTag(modelBuilder);
|
||||
ConfigurePollGroup(modelBuilder);
|
||||
ConfigureNodeAcl(modelBuilder);
|
||||
ConfigureClusterNodeGenerationState(modelBuilder);
|
||||
ConfigureConfigAuditLog(modelBuilder);
|
||||
ConfigureExternalIdReservation(modelBuilder);
|
||||
}
|
||||
|
||||
private static void ConfigureServerCluster(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<ServerCluster>(e =>
|
||||
{
|
||||
e.ToTable("ServerCluster", t =>
|
||||
{
|
||||
t.HasCheckConstraint("CK_ServerCluster_RedundancyMode_NodeCount",
|
||||
"((NodeCount = 1 AND RedundancyMode = 'None') " +
|
||||
"OR (NodeCount = 2 AND RedundancyMode IN ('Warm', 'Hot')))");
|
||||
});
|
||||
e.HasKey(x => x.ClusterId);
|
||||
e.Property(x => x.ClusterId).HasMaxLength(64);
|
||||
e.Property(x => x.Name).HasMaxLength(128);
|
||||
e.Property(x => x.Enterprise).HasMaxLength(32);
|
||||
e.Property(x => x.Site).HasMaxLength(32);
|
||||
e.Property(x => x.RedundancyMode).HasConversion<string>().HasMaxLength(16);
|
||||
e.Property(x => x.Notes).HasMaxLength(1024);
|
||||
e.Property(x => x.CreatedAt).HasColumnType("datetime2(3)").HasDefaultValueSql("SYSUTCDATETIME()");
|
||||
e.Property(x => x.CreatedBy).HasMaxLength(128);
|
||||
e.Property(x => x.ModifiedAt).HasColumnType("datetime2(3)");
|
||||
e.Property(x => x.ModifiedBy).HasMaxLength(128);
|
||||
e.HasIndex(x => x.Name).IsUnique().HasDatabaseName("UX_ServerCluster_Name");
|
||||
e.HasIndex(x => x.Site).HasDatabaseName("IX_ServerCluster_Site");
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigureClusterNode(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<ClusterNode>(e =>
|
||||
{
|
||||
e.ToTable("ClusterNode");
|
||||
e.HasKey(x => x.NodeId);
|
||||
e.Property(x => x.NodeId).HasMaxLength(64);
|
||||
e.Property(x => x.ClusterId).HasMaxLength(64);
|
||||
e.Property(x => x.RedundancyRole).HasConversion<string>().HasMaxLength(16);
|
||||
e.Property(x => x.Host).HasMaxLength(255);
|
||||
e.Property(x => x.ApplicationUri).HasMaxLength(256);
|
||||
e.Property(x => x.DriverConfigOverridesJson).HasColumnType("nvarchar(max)");
|
||||
e.Property(x => x.LastSeenAt).HasColumnType("datetime2(3)");
|
||||
e.Property(x => x.CreatedAt).HasColumnType("datetime2(3)").HasDefaultValueSql("SYSUTCDATETIME()");
|
||||
e.Property(x => x.CreatedBy).HasMaxLength(128);
|
||||
|
||||
e.HasOne(x => x.Cluster).WithMany(c => c.Nodes)
|
||||
.HasForeignKey(x => x.ClusterId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
// Fleet-wide unique per decision #86
|
||||
e.HasIndex(x => x.ApplicationUri).IsUnique().HasDatabaseName("UX_ClusterNode_ApplicationUri");
|
||||
e.HasIndex(x => x.ClusterId).HasDatabaseName("IX_ClusterNode_ClusterId");
|
||||
// At most one Primary per cluster
|
||||
e.HasIndex(x => x.ClusterId).IsUnique()
|
||||
.HasFilter("[RedundancyRole] = 'Primary'")
|
||||
.HasDatabaseName("UX_ClusterNode_Primary_Per_Cluster");
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigureClusterNodeCredential(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<ClusterNodeCredential>(e =>
|
||||
{
|
||||
e.ToTable("ClusterNodeCredential");
|
||||
e.HasKey(x => x.CredentialId);
|
||||
e.Property(x => x.CredentialId).HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||
e.Property(x => x.NodeId).HasMaxLength(64);
|
||||
e.Property(x => x.Kind).HasConversion<string>().HasMaxLength(32);
|
||||
e.Property(x => x.Value).HasMaxLength(512);
|
||||
e.Property(x => x.RotatedAt).HasColumnType("datetime2(3)");
|
||||
e.Property(x => x.CreatedAt).HasColumnType("datetime2(3)").HasDefaultValueSql("SYSUTCDATETIME()");
|
||||
e.Property(x => x.CreatedBy).HasMaxLength(128);
|
||||
|
||||
e.HasOne(x => x.Node).WithMany(n => n.Credentials)
|
||||
.HasForeignKey(x => x.NodeId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
e.HasIndex(x => new { x.NodeId, x.Enabled }).HasDatabaseName("IX_ClusterNodeCredential_NodeId");
|
||||
e.HasIndex(x => new { x.Kind, x.Value }).IsUnique()
|
||||
.HasFilter("[Enabled] = 1")
|
||||
.HasDatabaseName("UX_ClusterNodeCredential_Value");
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigureConfigGeneration(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<ConfigGeneration>(e =>
|
||||
{
|
||||
e.ToTable("ConfigGeneration");
|
||||
e.HasKey(x => x.GenerationId);
|
||||
e.Property(x => x.GenerationId).UseIdentityColumn(seed: 1, increment: 1);
|
||||
e.Property(x => x.ClusterId).HasMaxLength(64);
|
||||
e.Property(x => x.Status).HasConversion<string>().HasMaxLength(16);
|
||||
e.Property(x => x.PublishedAt).HasColumnType("datetime2(3)");
|
||||
e.Property(x => x.PublishedBy).HasMaxLength(128);
|
||||
e.Property(x => x.Notes).HasMaxLength(1024);
|
||||
e.Property(x => x.CreatedAt).HasColumnType("datetime2(3)").HasDefaultValueSql("SYSUTCDATETIME()");
|
||||
e.Property(x => x.CreatedBy).HasMaxLength(128);
|
||||
|
||||
e.HasOne(x => x.Cluster).WithMany(c => c.Generations)
|
||||
.HasForeignKey(x => x.ClusterId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
e.HasOne(x => x.Parent).WithMany()
|
||||
.HasForeignKey(x => x.ParentGenerationId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
e.HasIndex(x => new { x.ClusterId, x.Status, x.GenerationId })
|
||||
.IsDescending(false, false, true)
|
||||
.IncludeProperties(x => x.PublishedAt)
|
||||
.HasDatabaseName("IX_ConfigGeneration_Cluster_Published");
|
||||
// One Draft per cluster at a time
|
||||
e.HasIndex(x => x.ClusterId).IsUnique()
|
||||
.HasFilter("[Status] = 'Draft'")
|
||||
.HasDatabaseName("UX_ConfigGeneration_Draft_Per_Cluster");
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigureNamespace(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<Namespace>(e =>
|
||||
{
|
||||
e.ToTable("Namespace");
|
||||
e.HasKey(x => x.NamespaceRowId);
|
||||
e.Property(x => x.NamespaceRowId).HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||
e.Property(x => x.NamespaceId).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.HasOne(x => x.Generation).WithMany()
|
||||
.HasForeignKey(x => x.GenerationId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
e.HasOne(x => x.Cluster).WithMany(c => c.Namespaces)
|
||||
.HasForeignKey(x => x.ClusterId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
e.HasIndex(x => new { x.GenerationId, x.ClusterId, x.Kind }).IsUnique()
|
||||
.HasDatabaseName("UX_Namespace_Generation_Cluster_Kind");
|
||||
e.HasIndex(x => new { x.GenerationId, x.NamespaceUri }).IsUnique()
|
||||
.HasDatabaseName("UX_Namespace_Generation_NamespaceUri");
|
||||
e.HasIndex(x => new { x.GenerationId, x.NamespaceId }).IsUnique()
|
||||
.HasDatabaseName("UX_Namespace_Generation_LogicalId");
|
||||
e.HasIndex(x => new { x.GenerationId, x.NamespaceId, x.ClusterId }).IsUnique()
|
||||
.HasDatabaseName("UX_Namespace_Generation_LogicalId_Cluster");
|
||||
e.HasIndex(x => new { x.GenerationId, x.ClusterId })
|
||||
.HasDatabaseName("IX_Namespace_Generation_Cluster");
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigureUnsArea(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<UnsArea>(e =>
|
||||
{
|
||||
e.ToTable("UnsArea");
|
||||
e.HasKey(x => x.UnsAreaRowId);
|
||||
e.Property(x => x.UnsAreaRowId).HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||
e.Property(x => x.UnsAreaId).HasMaxLength(64);
|
||||
e.Property(x => x.ClusterId).HasMaxLength(64);
|
||||
e.Property(x => x.Name).HasMaxLength(32);
|
||||
e.Property(x => x.Notes).HasMaxLength(512);
|
||||
|
||||
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.HasIndex(x => new { x.GenerationId, x.ClusterId }).HasDatabaseName("IX_UnsArea_Generation_Cluster");
|
||||
e.HasIndex(x => new { x.GenerationId, x.UnsAreaId }).IsUnique().HasDatabaseName("UX_UnsArea_Generation_LogicalId");
|
||||
e.HasIndex(x => new { x.GenerationId, x.ClusterId, x.Name }).IsUnique().HasDatabaseName("UX_UnsArea_Generation_ClusterName");
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigureUnsLine(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<UnsLine>(e =>
|
||||
{
|
||||
e.ToTable("UnsLine");
|
||||
e.HasKey(x => x.UnsLineRowId);
|
||||
e.Property(x => x.UnsLineRowId).HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||
e.Property(x => x.UnsLineId).HasMaxLength(64);
|
||||
e.Property(x => x.UnsAreaId).HasMaxLength(64);
|
||||
e.Property(x => x.Name).HasMaxLength(32);
|
||||
e.Property(x => x.Notes).HasMaxLength(512);
|
||||
|
||||
e.HasOne(x => x.Generation).WithMany().HasForeignKey(x => x.GenerationId).OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
e.HasIndex(x => new { x.GenerationId, x.UnsAreaId }).HasDatabaseName("IX_UnsLine_Generation_Area");
|
||||
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");
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigureDriverInstance(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<DriverInstance>(e =>
|
||||
{
|
||||
e.ToTable("DriverInstance", t =>
|
||||
{
|
||||
t.HasCheckConstraint("CK_DriverInstance_DriverConfig_IsJson",
|
||||
"ISJSON(DriverConfig) = 1");
|
||||
});
|
||||
e.HasKey(x => x.DriverInstanceRowId);
|
||||
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.Name).HasMaxLength(128);
|
||||
e.Property(x => x.DriverType).HasMaxLength(32);
|
||||
e.Property(x => x.DriverConfig).HasColumnType("nvarchar(max)");
|
||||
|
||||
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.HasIndex(x => new { x.GenerationId, x.ClusterId }).HasDatabaseName("IX_DriverInstance_Generation_Cluster");
|
||||
e.HasIndex(x => new { x.GenerationId, x.NamespaceId }).HasDatabaseName("IX_DriverInstance_Generation_Namespace");
|
||||
e.HasIndex(x => new { x.GenerationId, x.DriverInstanceId }).IsUnique().HasDatabaseName("UX_DriverInstance_Generation_LogicalId");
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigureDevice(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<Device>(e =>
|
||||
{
|
||||
e.ToTable("Device", t =>
|
||||
{
|
||||
t.HasCheckConstraint("CK_Device_DeviceConfig_IsJson", "ISJSON(DeviceConfig) = 1");
|
||||
});
|
||||
e.HasKey(x => x.DeviceRowId);
|
||||
e.Property(x => x.DeviceRowId).HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||
e.Property(x => x.DeviceId).HasMaxLength(64);
|
||||
e.Property(x => x.DriverInstanceId).HasMaxLength(64);
|
||||
e.Property(x => x.Name).HasMaxLength(128);
|
||||
e.Property(x => x.DeviceConfig).HasColumnType("nvarchar(max)");
|
||||
|
||||
e.HasOne(x => x.Generation).WithMany().HasForeignKey(x => x.GenerationId).OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
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");
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigureEquipment(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<Equipment>(e =>
|
||||
{
|
||||
e.ToTable("Equipment");
|
||||
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);
|
||||
e.Property(x => x.ZTag).HasMaxLength(64);
|
||||
e.Property(x => x.SAPID).HasMaxLength(64);
|
||||
e.Property(x => x.Manufacturer).HasMaxLength(64);
|
||||
e.Property(x => x.Model).HasMaxLength(64);
|
||||
e.Property(x => x.SerialNumber).HasMaxLength(64);
|
||||
e.Property(x => x.HardwareRevision).HasMaxLength(32);
|
||||
e.Property(x => x.SoftwareRevision).HasMaxLength(32);
|
||||
e.Property(x => x.AssetLocation).HasMaxLength(256);
|
||||
e.Property(x => x.ManufacturerUri).HasMaxLength(512);
|
||||
e.Property(x => x.DeviceManualUri).HasMaxLength(512);
|
||||
e.Property(x => x.EquipmentClassRef).HasMaxLength(128);
|
||||
|
||||
e.HasOne(x => x.Generation).WithMany().HasForeignKey(x => x.GenerationId).OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
e.HasIndex(x => new { x.GenerationId, x.DriverInstanceId }).HasDatabaseName("IX_Equipment_Generation_Driver");
|
||||
e.HasIndex(x => new { x.GenerationId, x.UnsLineId }).HasDatabaseName("IX_Equipment_Generation_Line");
|
||||
e.HasIndex(x => new { x.GenerationId, x.EquipmentId }).IsUnique().HasDatabaseName("UX_Equipment_Generation_LogicalId");
|
||||
e.HasIndex(x => new { x.GenerationId, x.UnsLineId, x.Name }).IsUnique().HasDatabaseName("UX_Equipment_Generation_LinePath");
|
||||
e.HasIndex(x => new { x.GenerationId, x.EquipmentUuid }).IsUnique().HasDatabaseName("UX_Equipment_Generation_Uuid");
|
||||
e.HasIndex(x => new { x.GenerationId, x.ZTag }).HasFilter("[ZTag] IS NOT NULL").HasDatabaseName("IX_Equipment_Generation_ZTag");
|
||||
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");
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigureTag(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<Tag>(e =>
|
||||
{
|
||||
e.ToTable("Tag", t =>
|
||||
{
|
||||
t.HasCheckConstraint("CK_Tag_TagConfig_IsJson", "ISJSON(TagConfig) = 1");
|
||||
});
|
||||
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);
|
||||
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.HasOne(x => x.Generation).WithMany().HasForeignKey(x => x.GenerationId).OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
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")
|
||||
.HasDatabaseName("IX_Tag_Generation_Equipment");
|
||||
e.HasIndex(x => new { x.GenerationId, x.TagId }).IsUnique().HasDatabaseName("UX_Tag_Generation_LogicalId");
|
||||
e.HasIndex(x => new { x.GenerationId, x.EquipmentId, x.Name }).IsUnique()
|
||||
.HasFilter("[EquipmentId] IS NOT NULL")
|
||||
.HasDatabaseName("UX_Tag_Generation_EquipmentPath");
|
||||
e.HasIndex(x => new { x.GenerationId, x.DriverInstanceId, x.FolderPath, x.Name }).IsUnique()
|
||||
.HasFilter("[EquipmentId] IS NULL")
|
||||
.HasDatabaseName("UX_Tag_Generation_FolderPath");
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigurePollGroup(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<PollGroup>(e =>
|
||||
{
|
||||
e.ToTable("PollGroup", t =>
|
||||
{
|
||||
t.HasCheckConstraint("CK_PollGroup_IntervalMs_Min", "IntervalMs >= 50");
|
||||
});
|
||||
e.HasKey(x => x.PollGroupRowId);
|
||||
e.Property(x => x.PollGroupRowId).HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||
e.Property(x => x.PollGroupId).HasMaxLength(64);
|
||||
e.Property(x => x.DriverInstanceId).HasMaxLength(64);
|
||||
e.Property(x => x.Name).HasMaxLength(128);
|
||||
|
||||
e.HasOne(x => x.Generation).WithMany().HasForeignKey(x => x.GenerationId).OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
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");
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigureNodeAcl(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<NodeAcl>(e =>
|
||||
{
|
||||
e.ToTable("NodeAcl");
|
||||
e.HasKey(x => x.NodeAclRowId);
|
||||
e.Property(x => x.NodeAclRowId).HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||
e.Property(x => x.NodeAclId).HasMaxLength(64);
|
||||
e.Property(x => x.ClusterId).HasMaxLength(64);
|
||||
e.Property(x => x.LdapGroup).HasMaxLength(256);
|
||||
e.Property(x => x.ScopeKind).HasConversion<string>().HasMaxLength(16);
|
||||
e.Property(x => x.ScopeId).HasMaxLength(64);
|
||||
e.Property(x => x.PermissionFlags).HasConversion<int>();
|
||||
e.Property(x => x.Notes).HasMaxLength(512);
|
||||
|
||||
e.HasOne(x => x.Generation).WithMany().HasForeignKey(x => x.GenerationId).OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
e.HasIndex(x => new { x.GenerationId, x.ClusterId }).HasDatabaseName("IX_NodeAcl_Generation_Cluster");
|
||||
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")
|
||||
.HasDatabaseName("IX_NodeAcl_Generation_Scope");
|
||||
e.HasIndex(x => new { x.GenerationId, x.NodeAclId }).IsUnique().HasDatabaseName("UX_NodeAcl_Generation_LogicalId");
|
||||
e.HasIndex(x => new { x.GenerationId, x.ClusterId, x.LdapGroup, x.ScopeKind, x.ScopeId }).IsUnique()
|
||||
.HasDatabaseName("UX_NodeAcl_Generation_GroupScope");
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigureClusterNodeGenerationState(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<ClusterNodeGenerationState>(e =>
|
||||
{
|
||||
e.ToTable("ClusterNodeGenerationState");
|
||||
e.HasKey(x => x.NodeId);
|
||||
e.Property(x => x.NodeId).HasMaxLength(64);
|
||||
e.Property(x => x.LastAppliedAt).HasColumnType("datetime2(3)");
|
||||
e.Property(x => x.LastAppliedStatus).HasConversion<string>().HasMaxLength(16);
|
||||
e.Property(x => x.LastAppliedError).HasMaxLength(2048);
|
||||
e.Property(x => x.LastSeenAt).HasColumnType("datetime2(3)");
|
||||
|
||||
e.HasOne(x => x.Node).WithOne(n => n.GenerationState).HasForeignKey<ClusterNodeGenerationState>(x => x.NodeId).OnDelete(DeleteBehavior.Restrict);
|
||||
e.HasOne(x => x.CurrentGeneration).WithMany().HasForeignKey(x => x.CurrentGenerationId).OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
e.HasIndex(x => x.CurrentGenerationId).HasDatabaseName("IX_ClusterNodeGenerationState_Generation");
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigureConfigAuditLog(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<ConfigAuditLog>(e =>
|
||||
{
|
||||
e.ToTable("ConfigAuditLog", t =>
|
||||
{
|
||||
t.HasCheckConstraint("CK_ConfigAuditLog_DetailsJson_IsJson",
|
||||
"DetailsJson IS NULL OR ISJSON(DetailsJson) = 1");
|
||||
});
|
||||
e.HasKey(x => x.AuditId);
|
||||
e.Property(x => x.AuditId).UseIdentityColumn(seed: 1, increment: 1);
|
||||
e.Property(x => x.Timestamp).HasColumnType("datetime2(3)").HasDefaultValueSql("SYSUTCDATETIME()");
|
||||
e.Property(x => x.Principal).HasMaxLength(128);
|
||||
e.Property(x => x.EventType).HasMaxLength(64);
|
||||
e.Property(x => x.ClusterId).HasMaxLength(64);
|
||||
e.Property(x => x.NodeId).HasMaxLength(64);
|
||||
e.Property(x => x.DetailsJson).HasColumnType("nvarchar(max)");
|
||||
|
||||
e.HasIndex(x => new { x.ClusterId, x.Timestamp })
|
||||
.IsDescending(false, true)
|
||||
.HasDatabaseName("IX_ConfigAuditLog_Cluster_Time");
|
||||
e.HasIndex(x => x.GenerationId)
|
||||
.HasFilter("[GenerationId] IS NOT NULL")
|
||||
.HasDatabaseName("IX_ConfigAuditLog_Generation");
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigureExternalIdReservation(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<ExternalIdReservation>(e =>
|
||||
{
|
||||
e.ToTable("ExternalIdReservation");
|
||||
e.HasKey(x => x.ReservationId);
|
||||
e.Property(x => x.ReservationId).HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||
e.Property(x => x.Kind).HasConversion<string>().HasMaxLength(16);
|
||||
e.Property(x => x.Value).HasMaxLength(64);
|
||||
e.Property(x => x.ClusterId).HasMaxLength(64);
|
||||
e.Property(x => x.FirstPublishedAt).HasColumnType("datetime2(3)").HasDefaultValueSql("SYSUTCDATETIME()");
|
||||
e.Property(x => x.FirstPublishedBy).HasMaxLength(128);
|
||||
e.Property(x => x.LastPublishedAt).HasColumnType("datetime2(3)").HasDefaultValueSql("SYSUTCDATETIME()");
|
||||
e.Property(x => x.ReleasedAt).HasColumnType("datetime2(3)");
|
||||
e.Property(x => x.ReleasedBy).HasMaxLength(128);
|
||||
e.Property(x => x.ReleaseReason).HasMaxLength(512);
|
||||
|
||||
// Active reservations unique per (Kind, Value) — filtered index lets released rows coexist with a new reservation of the same value.
|
||||
// The UX_ filtered index covers active-reservation lookups; history queries over released rows
|
||||
// fall back to the table scan (released rows are rare + small). No separate non-unique (Kind, Value)
|
||||
// index is declared because EF Core merges duplicate column sets into a single index, which would
|
||||
// clobber the filtered-unique name.
|
||||
e.HasIndex(x => new { x.Kind, x.Value }).IsUnique()
|
||||
.HasFilter("[ReleasedAt] IS NULL")
|
||||
.HasDatabaseName("UX_ExternalIdReservation_KindValue_Active");
|
||||
e.HasIndex(x => x.EquipmentUuid).HasDatabaseName("IX_ExternalIdReservation_Equipment");
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user