Phase 1d of the v2 entity-model rewrite. The static RedundancyRole column
is replaced by Akka cluster's role-leader-of-"driver" election at runtime
(see RedundancyStateActor + ServiceLevelCalculator in Task 35).
Changes:
- Removed `public required RedundancyRole RedundancyRole` from
ClusterNode entity.
- Removed `e.Property(x => x.RedundancyRole).HasConversion<string>()...`
mapping from OtOpcUaConfigDbContext.ConfigureClusterNode.
- Removed the `UX_ClusterNode_Primary_Per_Cluster` filtered unique index
(filter referenced [RedundancyRole]='Primary').
- Dropped `using ZB.MOM.WW.OtOpcUa.Configuration.Enums` from ClusterNode.cs
(no longer needed).
- Deleted `Enums/RedundancyRole.cs` — the enum is unused in v2-kept code.
- DraftValidator: dropped the "exactly one Primary per cluster"
validation block. Comment in place explaining v2 picks primary at
runtime via Akka.
- DraftValidatorTests: dropped ValidateClusterTopology_flags_multiple_Primary
test; reworked BuildNode helper to no longer take a `role` argument.
Untouched (Server + Admin still reference RedundancyRole; accepted broken
per Task 56 policy):
src/Server/ZB.MOM.WW.OtOpcUa.Server/Redundancy/{ClusterTopologyLoader,
RedundancyStatePublisher, RedundancyTopology, ServiceLevelCalculator}.cs
src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/RedundancyMetrics.cs
DB-runtime tests will fail against the new schema (Task 14f's migration
drops the column) — to be updated in Task 14f's SchemaComplianceTests
update:
- SchemaComplianceTests.cs:55 (expected filtered index list)
- StoredProceduresTests.cs:263 (raw INSERT names the column)
Verification:
src/Core/ZB.MOM.WW.OtOpcUa.Configuration -> 0 errors
tests/Core/ZB.MOM.WW.OtOpcUa.Configuration.Tests -> 0 errors
whole solution -> 71 errors
(70 from Task 14b in Server/Admin, +1 new Server/Redundancy reference)
809 lines
41 KiB
C#
809 lines
41 KiB
C#
using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
|
|
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), IDataProtectionKeyContext
|
|
{
|
|
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>();
|
|
public DbSet<DriverHostStatus> DriverHostStatuses => Set<DriverHostStatus>();
|
|
public DbSet<DriverInstanceResilienceStatus> DriverInstanceResilienceStatuses => Set<DriverInstanceResilienceStatus>();
|
|
public DbSet<LdapGroupRoleMapping> LdapGroupRoleMappings => Set<LdapGroupRoleMapping>();
|
|
public DbSet<EquipmentImportBatch> EquipmentImportBatches => Set<EquipmentImportBatch>();
|
|
public DbSet<EquipmentImportRow> EquipmentImportRows => Set<EquipmentImportRow>();
|
|
public DbSet<Script> Scripts => Set<Script>();
|
|
public DbSet<VirtualTag> VirtualTags => Set<VirtualTag>();
|
|
public DbSet<ScriptedAlarm> ScriptedAlarms => Set<ScriptedAlarm>();
|
|
public DbSet<ScriptedAlarmState> ScriptedAlarmStates => Set<ScriptedAlarmState>();
|
|
|
|
// v2 deploy-model tables (Phase 1 of the Akka + fused-hosting alignment). Replace the
|
|
// ConfigGeneration/ClusterNodeGenerationState pair when Task 14's migration runs.
|
|
public DbSet<Deployment> Deployments => Set<Deployment>();
|
|
public DbSet<NodeDeploymentState> NodeDeploymentStates => Set<NodeDeploymentState>();
|
|
public DbSet<ConfigEdit> ConfigEdits => Set<ConfigEdit>();
|
|
|
|
// ASP.NET DataProtection key ring storage (decision: keys persisted in ConfigDb so every
|
|
// admin-role node decrypts the same cookies without sharing a filesystem).
|
|
public DbSet<DataProtectionKey> DataProtectionKeys => Set<DataProtectionKey>();
|
|
|
|
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);
|
|
ConfigureDriverHostStatus(modelBuilder);
|
|
ConfigureDriverInstanceResilienceStatus(modelBuilder);
|
|
ConfigureLdapGroupRoleMapping(modelBuilder);
|
|
ConfigureEquipmentImportBatch(modelBuilder);
|
|
ConfigureScript(modelBuilder);
|
|
ConfigureVirtualTag(modelBuilder);
|
|
ConfigureScriptedAlarm(modelBuilder);
|
|
ConfigureScriptedAlarmState(modelBuilder);
|
|
ConfigureDeployment(modelBuilder);
|
|
ConfigureNodeDeploymentState(modelBuilder);
|
|
ConfigureConfigEdit(modelBuilder);
|
|
ConfigureDataProtectionKey(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.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");
|
|
// v2: the "one Primary per cluster" filtered unique index (and the RedundancyRole
|
|
// column it filtered on) are gone. Akka cluster leader-of-driver-role is the
|
|
// authoritative primary signal (see RedundancyStateActor + ServiceLevelCalculator,
|
|
// Task 35).
|
|
});
|
|
}
|
|
|
|
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.Property(x => x.RowVersion).IsRowVersion();
|
|
|
|
e.HasOne(x => x.Cluster).WithMany(c => c.Namespaces)
|
|
.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");
|
|
});
|
|
}
|
|
|
|
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.Property(x => x.RowVersion).IsRowVersion();
|
|
|
|
e.HasOne(x => x.Cluster).WithMany().HasForeignKey(x => x.ClusterId).OnDelete(DeleteBehavior.Restrict);
|
|
|
|
e.HasIndex(x => x.ClusterId).HasDatabaseName("IX_UnsArea_Cluster");
|
|
e.HasIndex(x => x.UnsAreaId).IsUnique().HasDatabaseName("UX_UnsArea_LogicalId");
|
|
e.HasIndex(x => new { x.ClusterId, x.Name }).IsUnique().HasDatabaseName("UX_UnsArea_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.Property(x => x.RowVersion).IsRowVersion();
|
|
|
|
e.HasIndex(x => x.UnsAreaId).HasDatabaseName("IX_UnsLine_Area");
|
|
e.HasIndex(x => x.UnsLineId).IsUnique().HasDatabaseName("UX_UnsLine_LogicalId");
|
|
e.HasIndex(x => new { x.UnsAreaId, x.Name }).IsUnique().HasDatabaseName("UX_UnsLine_AreaName");
|
|
});
|
|
}
|
|
|
|
private static void ConfigureDriverInstance(ModelBuilder modelBuilder)
|
|
{
|
|
modelBuilder.Entity<DriverInstance>(e =>
|
|
{
|
|
e.ToTable("DriverInstance", t =>
|
|
{
|
|
t.HasCheckConstraint("CK_DriverInstance_DriverConfig_IsJson",
|
|
"ISJSON(DriverConfig) = 1");
|
|
t.HasCheckConstraint("CK_DriverInstance_ResilienceConfig_IsJson",
|
|
"ResilienceConfig IS NULL OR ISJSON(ResilienceConfig) = 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.Property(x => x.ResilienceConfig).HasColumnType("nvarchar(max)");
|
|
e.Property(x => x.RowVersion).IsRowVersion();
|
|
|
|
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.DriverInstanceId).IsUnique().HasDatabaseName("UX_DriverInstance_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.Property(x => x.RowVersion).IsRowVersion();
|
|
|
|
e.HasIndex(x => x.DriverInstanceId).HasDatabaseName("IX_Device_Driver");
|
|
e.HasIndex(x => x.DeviceId).IsUnique().HasDatabaseName("UX_Device_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.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");
|
|
e.HasIndex(x => x.EquipmentUuid).IsUnique().HasDatabaseName("UX_Equipment_Uuid");
|
|
e.HasIndex(x => x.ZTag).HasFilter("[ZTag] IS NOT NULL").HasDatabaseName("IX_Equipment_ZTag");
|
|
e.HasIndex(x => x.SAPID).HasFilter("[SAPID] IS NOT NULL").HasDatabaseName("IX_Equipment_SAPID");
|
|
e.HasIndex(x => x.MachineCode).HasDatabaseName("IX_Equipment_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.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 => 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");
|
|
});
|
|
}
|
|
|
|
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.Property(x => x.RowVersion).IsRowVersion();
|
|
|
|
e.HasIndex(x => x.DriverInstanceId).HasDatabaseName("IX_PollGroup_Driver");
|
|
e.HasIndex(x => x.PollGroupId).IsUnique().HasDatabaseName("UX_PollGroup_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.Property(x => x.RowVersion).IsRowVersion();
|
|
|
|
e.HasIndex(x => x.ClusterId).HasDatabaseName("IX_NodeAcl_Cluster");
|
|
e.HasIndex(x => x.LdapGroup).HasDatabaseName("IX_NodeAcl_Group");
|
|
e.HasIndex(x => new { x.ScopeKind, x.ScopeId })
|
|
.HasFilter("[ScopeId] IS NOT NULL")
|
|
.HasDatabaseName("IX_NodeAcl_Scope");
|
|
e.HasIndex(x => x.NodeAclId).IsUnique().HasDatabaseName("UX_NodeAcl_LogicalId");
|
|
e.HasIndex(x => new { x.ClusterId, x.LdapGroup, x.ScopeKind, x.ScopeId }).IsUnique()
|
|
.HasDatabaseName("UX_NodeAcl_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");
|
|
});
|
|
}
|
|
|
|
private static void ConfigureDriverHostStatus(ModelBuilder modelBuilder)
|
|
{
|
|
modelBuilder.Entity<DriverHostStatus>(e =>
|
|
{
|
|
e.ToTable("DriverHostStatus");
|
|
// Composite key — one row per (server node, driver instance, probe-reported host).
|
|
// A redundant 2-node cluster with one Galaxy driver reporting 3 platforms produces
|
|
// 6 rows because each server node owns its own runtime view; the composite key is
|
|
// what lets both views coexist without shadowing each other.
|
|
e.HasKey(x => new { x.NodeId, x.DriverInstanceId, x.HostName });
|
|
e.Property(x => x.NodeId).HasMaxLength(64);
|
|
e.Property(x => x.DriverInstanceId).HasMaxLength(64);
|
|
e.Property(x => x.HostName).HasMaxLength(256);
|
|
e.Property(x => x.State).HasConversion<string>().HasMaxLength(16);
|
|
e.Property(x => x.StateChangedUtc).HasColumnType("datetime2(3)");
|
|
e.Property(x => x.LastSeenUtc).HasColumnType("datetime2(3)");
|
|
e.Property(x => x.Detail).HasMaxLength(1024);
|
|
|
|
// NodeId-only index drives the Admin UI's per-cluster drill-down (select all host
|
|
// statuses for the nodes of a specific cluster via join on ClusterNode.ClusterId).
|
|
e.HasIndex(x => x.NodeId).HasDatabaseName("IX_DriverHostStatus_Node");
|
|
// LastSeenUtc index powers the Admin UI's stale-row query (now - LastSeen > N).
|
|
e.HasIndex(x => x.LastSeenUtc).HasDatabaseName("IX_DriverHostStatus_LastSeen");
|
|
});
|
|
}
|
|
|
|
private static void ConfigureDriverInstanceResilienceStatus(ModelBuilder modelBuilder)
|
|
{
|
|
modelBuilder.Entity<DriverInstanceResilienceStatus>(e =>
|
|
{
|
|
e.ToTable("DriverInstanceResilienceStatus");
|
|
e.HasKey(x => new { x.DriverInstanceId, x.HostName });
|
|
e.Property(x => x.DriverInstanceId).HasMaxLength(64);
|
|
e.Property(x => x.HostName).HasMaxLength(256);
|
|
e.Property(x => x.LastCircuitBreakerOpenUtc).HasColumnType("datetime2(3)");
|
|
e.Property(x => x.LastRecycleUtc).HasColumnType("datetime2(3)");
|
|
e.Property(x => x.LastSampledUtc).HasColumnType("datetime2(3)");
|
|
// LastSampledUtc drives the Admin UI's stale-sample filter same way DriverHostStatus's
|
|
// LastSeenUtc index does for connectivity rows.
|
|
e.HasIndex(x => x.LastSampledUtc).HasDatabaseName("IX_DriverResilience_LastSampled");
|
|
});
|
|
}
|
|
|
|
private static void ConfigureLdapGroupRoleMapping(ModelBuilder modelBuilder)
|
|
{
|
|
modelBuilder.Entity<LdapGroupRoleMapping>(e =>
|
|
{
|
|
e.ToTable("LdapGroupRoleMapping");
|
|
e.HasKey(x => x.Id);
|
|
e.Property(x => x.LdapGroup).HasMaxLength(512).IsRequired();
|
|
e.Property(x => x.Role).HasConversion<string>().HasMaxLength(32);
|
|
e.Property(x => x.ClusterId).HasMaxLength(64);
|
|
e.Property(x => x.CreatedAtUtc).HasColumnType("datetime2(3)");
|
|
e.Property(x => x.Notes).HasMaxLength(512);
|
|
|
|
// FK to ServerCluster when cluster-scoped; null for system-wide grants.
|
|
e.HasOne(x => x.Cluster)
|
|
.WithMany()
|
|
.HasForeignKey(x => x.ClusterId)
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
|
|
// Uniqueness: one row per (LdapGroup, ClusterId). Null ClusterId is treated as its own
|
|
// "bucket" so a system-wide row coexists with cluster-scoped rows for the same group.
|
|
// SQL Server treats NULL as a distinct value in unique-index comparisons by default
|
|
// since 2008 SP1 onwards under the session setting we use — tested in SchemaCompliance.
|
|
e.HasIndex(x => new { x.LdapGroup, x.ClusterId })
|
|
.IsUnique()
|
|
.HasDatabaseName("UX_LdapGroupRoleMapping_Group_Cluster");
|
|
|
|
// Hot-path lookup during cookie auth: "what grants does this user's set of LDAP
|
|
// groups carry?". Fires on every sign-in so the index earns its keep.
|
|
e.HasIndex(x => x.LdapGroup).HasDatabaseName("IX_LdapGroupRoleMapping_Group");
|
|
});
|
|
}
|
|
|
|
private static void ConfigureEquipmentImportBatch(ModelBuilder modelBuilder)
|
|
{
|
|
modelBuilder.Entity<EquipmentImportBatch>(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.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.HasOne(x => x.Batch)
|
|
.WithMany(b => b.Rows)
|
|
.HasForeignKey(x => x.BatchId)
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
|
|
e.HasIndex(x => x.BatchId).HasDatabaseName("IX_EquipmentImportRow_Batch");
|
|
});
|
|
}
|
|
|
|
private static void ConfigureScript(ModelBuilder modelBuilder)
|
|
{
|
|
modelBuilder.Entity<Script>(e =>
|
|
{
|
|
e.ToTable("Script");
|
|
e.HasKey(x => x.ScriptRowId);
|
|
e.Property(x => x.ScriptRowId).HasDefaultValueSql("NEWSEQUENTIALID()");
|
|
e.Property(x => x.ScriptId).HasMaxLength(64);
|
|
e.Property(x => x.Name).HasMaxLength(128);
|
|
e.Property(x => x.SourceCode).HasColumnType("nvarchar(max)");
|
|
e.Property(x => x.SourceHash).HasMaxLength(64);
|
|
e.Property(x => x.Language).HasMaxLength(16);
|
|
e.Property(x => x.RowVersion).IsRowVersion();
|
|
|
|
e.HasIndex(x => x.ScriptId).IsUnique().HasDatabaseName("UX_Script_LogicalId");
|
|
e.HasIndex(x => x.SourceHash).HasDatabaseName("IX_Script_SourceHash");
|
|
});
|
|
}
|
|
|
|
private static void ConfigureVirtualTag(ModelBuilder modelBuilder)
|
|
{
|
|
modelBuilder.Entity<VirtualTag>(e =>
|
|
{
|
|
e.ToTable("VirtualTag", t =>
|
|
{
|
|
t.HasCheckConstraint("CK_VirtualTag_Trigger_AtLeastOne",
|
|
"ChangeTriggered = 1 OR TimerIntervalMs IS NOT NULL");
|
|
t.HasCheckConstraint("CK_VirtualTag_TimerInterval_Min",
|
|
"TimerIntervalMs IS NULL OR TimerIntervalMs >= 50");
|
|
});
|
|
e.HasKey(x => x.VirtualTagRowId);
|
|
e.Property(x => x.VirtualTagRowId).HasDefaultValueSql("NEWSEQUENTIALID()");
|
|
e.Property(x => x.VirtualTagId).HasMaxLength(64);
|
|
e.Property(x => x.EquipmentId).HasMaxLength(64);
|
|
e.Property(x => x.Name).HasMaxLength(128);
|
|
e.Property(x => x.DataType).HasMaxLength(32);
|
|
e.Property(x => x.ScriptId).HasMaxLength(64);
|
|
e.Property(x => x.RowVersion).IsRowVersion();
|
|
|
|
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 => x.ScriptId).HasDatabaseName("IX_VirtualTag_Script");
|
|
});
|
|
}
|
|
|
|
private static void ConfigureScriptedAlarm(ModelBuilder modelBuilder)
|
|
{
|
|
modelBuilder.Entity<ScriptedAlarm>(e =>
|
|
{
|
|
e.ToTable("ScriptedAlarm", t =>
|
|
{
|
|
t.HasCheckConstraint("CK_ScriptedAlarm_Severity_Range", "Severity BETWEEN 1 AND 1000");
|
|
t.HasCheckConstraint("CK_ScriptedAlarm_AlarmType",
|
|
"AlarmType IN ('AlarmCondition','LimitAlarm','OffNormalAlarm','DiscreteAlarm')");
|
|
});
|
|
e.HasKey(x => x.ScriptedAlarmRowId);
|
|
e.Property(x => x.ScriptedAlarmRowId).HasDefaultValueSql("NEWSEQUENTIALID()");
|
|
e.Property(x => x.ScriptedAlarmId).HasMaxLength(64);
|
|
e.Property(x => x.EquipmentId).HasMaxLength(64);
|
|
e.Property(x => x.Name).HasMaxLength(128);
|
|
e.Property(x => x.AlarmType).HasMaxLength(32);
|
|
e.Property(x => x.MessageTemplate).HasMaxLength(1024);
|
|
e.Property(x => x.PredicateScriptId).HasMaxLength(64);
|
|
e.Property(x => x.RowVersion).IsRowVersion();
|
|
|
|
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 => x.PredicateScriptId).HasDatabaseName("IX_ScriptedAlarm_Script");
|
|
});
|
|
}
|
|
|
|
private static void ConfigureScriptedAlarmState(ModelBuilder modelBuilder)
|
|
{
|
|
modelBuilder.Entity<ScriptedAlarmState>(e =>
|
|
{
|
|
// Logical-id keyed (not generation-scoped) because ack state follows the alarm's
|
|
// stable identity across generations — Modified alarms keep their ack audit trail.
|
|
e.ToTable("ScriptedAlarmState", t =>
|
|
{
|
|
t.HasCheckConstraint("CK_ScriptedAlarmState_CommentsJson_IsJson", "ISJSON(CommentsJson) = 1");
|
|
});
|
|
e.HasKey(x => x.ScriptedAlarmId);
|
|
e.Property(x => x.ScriptedAlarmId).HasMaxLength(64);
|
|
e.Property(x => x.EnabledState).HasMaxLength(16);
|
|
e.Property(x => x.AckedState).HasMaxLength(16);
|
|
e.Property(x => x.ConfirmedState).HasMaxLength(16);
|
|
e.Property(x => x.ShelvingState).HasMaxLength(16);
|
|
e.Property(x => x.ShelvingExpiresUtc).HasColumnType("datetime2(3)");
|
|
e.Property(x => x.LastAckUser).HasMaxLength(128);
|
|
e.Property(x => x.LastAckComment).HasMaxLength(1024);
|
|
e.Property(x => x.LastAckUtc).HasColumnType("datetime2(3)");
|
|
e.Property(x => x.LastConfirmUser).HasMaxLength(128);
|
|
e.Property(x => x.LastConfirmComment).HasMaxLength(1024);
|
|
e.Property(x => x.LastConfirmUtc).HasColumnType("datetime2(3)");
|
|
e.Property(x => x.CommentsJson).HasColumnType("nvarchar(max)");
|
|
e.Property(x => x.UpdatedAtUtc).HasColumnType("datetime2(3)").HasDefaultValueSql("SYSUTCDATETIME()");
|
|
});
|
|
}
|
|
|
|
private static void ConfigureDeployment(ModelBuilder modelBuilder)
|
|
{
|
|
modelBuilder.Entity<Deployment>(e =>
|
|
{
|
|
e.ToTable("Deployment");
|
|
e.HasKey(x => x.DeploymentId);
|
|
e.Property(x => x.DeploymentId).HasDefaultValueSql("NEWSEQUENTIALID()");
|
|
e.Property(x => x.RevisionHash).HasMaxLength(64).IsRequired();
|
|
e.Property(x => x.Status).HasConversion<int>();
|
|
e.Property(x => x.CreatedBy).HasMaxLength(128).IsRequired();
|
|
e.Property(x => x.CreatedAtUtc).HasColumnType("datetime2(3)").HasDefaultValueSql("SYSUTCDATETIME()");
|
|
e.Property(x => x.ArtifactBlob).HasColumnType("varbinary(max)");
|
|
e.Property(x => x.RowVersion).IsRowVersion();
|
|
e.Property(x => x.FailureReason).HasMaxLength(2048);
|
|
e.Property(x => x.SealedAtUtc).HasColumnType("datetime2(3)");
|
|
|
|
e.HasIndex(x => x.Status).HasDatabaseName("IX_Deployment_Status");
|
|
e.HasIndex(x => x.CreatedAtUtc).HasDatabaseName("IX_Deployment_CreatedAt");
|
|
});
|
|
}
|
|
|
|
private static void ConfigureNodeDeploymentState(ModelBuilder modelBuilder)
|
|
{
|
|
modelBuilder.Entity<NodeDeploymentState>(e =>
|
|
{
|
|
e.ToTable("NodeDeploymentState");
|
|
e.HasKey(x => new { x.NodeId, x.DeploymentId });
|
|
e.Property(x => x.NodeId).HasMaxLength(64);
|
|
e.Property(x => x.Status).HasConversion<int>();
|
|
e.Property(x => x.StartedAtUtc).HasColumnType("datetime2(3)").HasDefaultValueSql("SYSUTCDATETIME()");
|
|
e.Property(x => x.AppliedAtUtc).HasColumnType("datetime2(3)");
|
|
e.Property(x => x.FailureReason).HasMaxLength(2048);
|
|
e.Property(x => x.RowVersion).IsRowVersion();
|
|
|
|
e.HasOne(x => x.Node).WithMany().HasForeignKey(x => x.NodeId).OnDelete(DeleteBehavior.Restrict);
|
|
e.HasOne(x => x.Deployment).WithMany().HasForeignKey(x => x.DeploymentId).OnDelete(DeleteBehavior.Cascade);
|
|
|
|
e.HasIndex(x => x.DeploymentId).HasDatabaseName("IX_NodeDeploymentState_Deployment");
|
|
e.HasIndex(x => x.Status).HasDatabaseName("IX_NodeDeploymentState_Status");
|
|
});
|
|
}
|
|
|
|
private static void ConfigureConfigEdit(ModelBuilder modelBuilder)
|
|
{
|
|
modelBuilder.Entity<ConfigEdit>(e =>
|
|
{
|
|
e.ToTable("ConfigEdit", t =>
|
|
{
|
|
t.HasCheckConstraint("CK_ConfigEdit_FieldsJson_IsJson", "ISJSON(FieldsJson) = 1");
|
|
});
|
|
e.HasKey(x => x.EditId);
|
|
e.Property(x => x.EditId).HasDefaultValueSql("NEWSEQUENTIALID()");
|
|
e.Property(x => x.EntityType).HasMaxLength(64).IsRequired();
|
|
e.Property(x => x.FieldsJson).HasColumnType("nvarchar(max)").IsRequired();
|
|
e.Property(x => x.EditedBy).HasMaxLength(128).IsRequired();
|
|
e.Property(x => x.EditedAtUtc).HasColumnType("datetime2(3)").HasDefaultValueSql("SYSUTCDATETIME()");
|
|
e.Property(x => x.SourceNode).HasMaxLength(64).IsRequired();
|
|
|
|
// Replays of admin operations group rows by ExecutionId, then by time.
|
|
e.HasIndex(x => new { x.EntityType, x.EntityId }).HasDatabaseName("IX_ConfigEdit_Entity");
|
|
e.HasIndex(x => x.ExecutionId).HasFilter("[ExecutionId] IS NOT NULL").HasDatabaseName("IX_ConfigEdit_Execution");
|
|
e.HasIndex(x => x.EditedAtUtc).HasDatabaseName("IX_ConfigEdit_EditedAt");
|
|
});
|
|
}
|
|
|
|
private static void ConfigureDataProtectionKey(ModelBuilder modelBuilder)
|
|
{
|
|
// ASP.NET DataProtection ships its own EF mapping; override only the table name so it
|
|
// matches the rest of the schema's PascalCase convention.
|
|
modelBuilder.Entity<DataProtectionKey>(e =>
|
|
{
|
|
e.ToTable("DataProtectionKeys");
|
|
});
|
|
}
|
|
}
|