v3 batch1 WP1: greenfield DbContext + V3Initial squash + seeds + tests
DbContext (OtOpcUaConfigDbContext): - Drop Namespace/EquipmentImportBatch/EquipmentImportRow DbSets + Configure methods. - Add RawFolder/TagGroup/UnsTagReference DbSets + Configure methods with sibling-name filtered unique indexes over nullable parents (two-filtered pattern per level). - Reshape DriverInstance (RawFolderId + UX_DriverInstance_Folder_Name/_ClusterRoot_Name), Tag (required DeviceId, nullable TagGroupId, UX_Tag_Group_Name/_Device_Name, IX_Tag_Device), Equipment (drop DriverInstanceId/DeviceId + IX_Equipment_Driver), Device (UX_Device_Driver_Name). Migration squash -> V3Initial: - Delete the whole Migrations/ folder; scaffold one V3Initial (applies clean to a fresh DB). - Port the hand-written procs + AuthorizationGrants into V3Initial.StoredProcedures.cs. The v1 generation-model procs (ConfigGeneration/GenerationId, dead since V2HostingAlignment) are rewritten as v3-schema-valid retirement stubs that preserve the two-role EXECUTE-grant surface + AuthorizationTests behaviors; sp_ComputeGenerationDiff now references the v3 tables; sp_ReleaseExternalIdReservation ported verbatim. Compile-fixes for the build gate (Wave-C-owned, minimal + TODO(v3 WP4) markers): - DraftSnapshot/DraftSnapshotFactory: drop the retired Namespaces list. - DraftValidator: delete namespace-binding + Galaxy-FullName rules; collision rule now VirtualTag-only. Seeds -> v3 schema: - seed-clusters.sql summary SELECTs; all 5 scripts/smoke/seed-*.sql rewritten (DriverInstance RawFolderId, Device+DeviceConfig endpoint, Tag DeviceId, UnsTagReference; no ConfigGeneration/ Namespace/sp_PublishGeneration). Verified: all seeds apply clean against a fresh V3Initial DB. Tests (Configuration.Tests, 107 pass): - SchemaComplianceTests rewritten to v3 tables + filtered indexes. - New RawSchemaIntegrityTests: DB-enforced sibling-name uniqueness per raw level + UnsTagReference (Equipment,Tag) uniqueness. - Delete DraftValidatorGalaxyFullNameCorpusTests (WP2/WP6 replaces); fix DraftValidatorTests + DraftSnapshotFactoryTests to v3 shapes; repoint scripting-migration existence test to V3Initial.
This commit is contained in:
@@ -18,8 +18,12 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
||||
public DbSet<ClusterNode> ClusterNodes => Set<ClusterNode>();
|
||||
/// <summary>Gets the DbSet of cluster node credentials.</summary>
|
||||
public DbSet<ClusterNodeCredential> ClusterNodeCredentials => Set<ClusterNodeCredential>();
|
||||
/// <summary>Gets the DbSet of namespaces.</summary>
|
||||
public DbSet<Namespace> Namespaces => Set<Namespace>();
|
||||
/// <summary>Gets the DbSet of v3 Raw-tree driver-organizing folders.</summary>
|
||||
public DbSet<RawFolder> RawFolders => Set<RawFolder>();
|
||||
/// <summary>Gets the DbSet of v3 Raw-tree tag groups (nestable under a device).</summary>
|
||||
public DbSet<TagGroup> TagGroups => Set<TagGroup>();
|
||||
/// <summary>Gets the DbSet of v3 UNS tag references (equipment → raw tag projection).</summary>
|
||||
public DbSet<UnsTagReference> UnsTagReferences => Set<UnsTagReference>();
|
||||
/// <summary>Gets the DbSet of UNS areas.</summary>
|
||||
public DbSet<UnsArea> UnsAreas => Set<UnsArea>();
|
||||
/// <summary>Gets the DbSet of UNS lines.</summary>
|
||||
@@ -46,10 +50,6 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
||||
public DbSet<DriverInstanceResilienceStatus> DriverInstanceResilienceStatuses => Set<DriverInstanceResilienceStatus>();
|
||||
/// <summary>Gets the DbSet of LDAP group role mappings.</summary>
|
||||
public DbSet<LdapGroupRoleMapping> LdapGroupRoleMappings => Set<LdapGroupRoleMapping>();
|
||||
/// <summary>Gets the DbSet of equipment import batches.</summary>
|
||||
public DbSet<EquipmentImportBatch> EquipmentImportBatches => Set<EquipmentImportBatch>();
|
||||
/// <summary>Gets the DbSet of equipment import rows.</summary>
|
||||
public DbSet<EquipmentImportRow> EquipmentImportRows => Set<EquipmentImportRow>();
|
||||
/// <summary>Gets the DbSet of scripts.</summary>
|
||||
public DbSet<Script> Scripts => Set<Script>();
|
||||
/// <summary>Gets the DbSet of virtual tags.</summary>
|
||||
@@ -79,13 +79,15 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
||||
ConfigureServerCluster(modelBuilder);
|
||||
ConfigureClusterNode(modelBuilder);
|
||||
ConfigureClusterNodeCredential(modelBuilder);
|
||||
ConfigureNamespace(modelBuilder);
|
||||
ConfigureRawFolder(modelBuilder);
|
||||
ConfigureUnsArea(modelBuilder);
|
||||
ConfigureUnsLine(modelBuilder);
|
||||
ConfigureDriverInstance(modelBuilder);
|
||||
ConfigureDevice(modelBuilder);
|
||||
ConfigureTagGroup(modelBuilder);
|
||||
ConfigureEquipment(modelBuilder);
|
||||
ConfigureTag(modelBuilder);
|
||||
ConfigureUnsTagReference(modelBuilder);
|
||||
ConfigurePollGroup(modelBuilder);
|
||||
ConfigureNodeAcl(modelBuilder);
|
||||
ConfigureConfigAuditLog(modelBuilder);
|
||||
@@ -93,7 +95,6 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
||||
ConfigureDriverHostStatus(modelBuilder);
|
||||
ConfigureDriverInstanceResilienceStatus(modelBuilder);
|
||||
ConfigureLdapGroupRoleMapping(modelBuilder);
|
||||
ConfigureEquipmentImportBatch(modelBuilder);
|
||||
ConfigureScript(modelBuilder);
|
||||
ConfigureVirtualTag(modelBuilder);
|
||||
ConfigureScriptedAlarm(modelBuilder);
|
||||
@@ -183,32 +184,39 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigureNamespace(ModelBuilder modelBuilder)
|
||||
private static void ConfigureRawFolder(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<Namespace>(e =>
|
||||
modelBuilder.Entity<RawFolder>(e =>
|
||||
{
|
||||
e.ToTable("Namespace");
|
||||
e.HasKey(x => x.NamespaceRowId);
|
||||
e.Property(x => x.NamespaceRowId).HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||
e.Property(x => x.NamespaceId).HasMaxLength(64);
|
||||
e.ToTable("RawFolder");
|
||||
e.HasKey(x => x.RawFolderRowId);
|
||||
e.Property(x => x.RawFolderRowId).HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||
e.Property(x => x.RawFolderId).HasMaxLength(64);
|
||||
e.Property(x => x.ClusterId).HasMaxLength(64);
|
||||
e.Property(x => x.Kind).HasConversion<string>().HasMaxLength(32);
|
||||
e.Property(x => x.NamespaceUri).HasMaxLength(256);
|
||||
e.Property(x => x.Notes).HasMaxLength(1024);
|
||||
e.Property(x => x.ParentRawFolderId).HasMaxLength(64);
|
||||
e.Property(x => x.Name).HasMaxLength(128);
|
||||
e.Property(x => x.RowVersion).IsRowVersion();
|
||||
|
||||
e.HasOne(x => x.Cluster).WithMany(c => c.Namespaces)
|
||||
// Cluster-rooted. Restrict so a cluster with folders cannot be deleted out from under
|
||||
// them (matches the OnDelete(Restrict) convention across the config schema). Child FKs
|
||||
// (DriverInstance.RawFolderId, RawFolder.ParentRawFolderId) are logical-only, so
|
||||
// non-empty-folder-delete is enforced at the service layer in later batches.
|
||||
e.HasOne<ServerCluster>().WithMany(c => c.RawFolders)
|
||||
.HasForeignKey(x => x.ClusterId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
e.HasIndex(x => new { x.ClusterId, x.Kind }).IsUnique()
|
||||
.HasDatabaseName("UX_Namespace_Cluster_Kind");
|
||||
e.HasIndex(x => x.NamespaceUri).IsUnique()
|
||||
.HasDatabaseName("UX_Namespace_NamespaceUri");
|
||||
e.HasIndex(x => x.NamespaceId).IsUnique()
|
||||
.HasDatabaseName("UX_Namespace_LogicalId");
|
||||
e.HasIndex(x => x.ClusterId)
|
||||
.HasDatabaseName("IX_Namespace_Cluster");
|
||||
e.HasIndex(x => x.RawFolderId).IsUnique().HasDatabaseName("UX_RawFolder_LogicalId");
|
||||
e.HasIndex(x => x.ClusterId).HasDatabaseName("IX_RawFolder_Cluster");
|
||||
|
||||
// Sibling-name uniqueness. Two filtered indexes cover the nullable ParentRawFolderId:
|
||||
// one for nested folders (Parent NOT NULL), one for cluster-root folders (Parent NULL),
|
||||
// mirroring the Tag filtered-index convention.
|
||||
e.HasIndex(x => new { x.ClusterId, x.ParentRawFolderId, x.Name }).IsUnique()
|
||||
.HasFilter("[ParentRawFolderId] IS NOT NULL")
|
||||
.HasDatabaseName("UX_RawFolder_Parent_Name");
|
||||
e.HasIndex(x => new { x.ClusterId, x.Name }).IsUnique()
|
||||
.HasFilter("[ParentRawFolderId] IS NULL")
|
||||
.HasDatabaseName("UX_RawFolder_ClusterRoot_Name");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -267,7 +275,7 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
||||
e.Property(x => x.DriverInstanceRowId).HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||
e.Property(x => x.DriverInstanceId).HasMaxLength(64);
|
||||
e.Property(x => x.ClusterId).HasMaxLength(64);
|
||||
e.Property(x => x.NamespaceId).HasMaxLength(64);
|
||||
e.Property(x => x.RawFolderId).HasMaxLength(64);
|
||||
e.Property(x => x.Name).HasMaxLength(128);
|
||||
e.Property(x => x.DriverType).HasMaxLength(32);
|
||||
e.Property(x => x.DriverConfig).HasColumnType("nvarchar(max)");
|
||||
@@ -277,8 +285,18 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
||||
e.HasOne(x => x.Cluster).WithMany().HasForeignKey(x => x.ClusterId).OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
e.HasIndex(x => x.ClusterId).HasDatabaseName("IX_DriverInstance_Cluster");
|
||||
e.HasIndex(x => x.NamespaceId).HasDatabaseName("IX_DriverInstance_Namespace");
|
||||
e.HasIndex(x => x.RawFolderId).HasDatabaseName("IX_DriverInstance_RawFolder");
|
||||
e.HasIndex(x => x.DriverInstanceId).IsUnique().HasDatabaseName("UX_DriverInstance_LogicalId");
|
||||
|
||||
// v3 sibling-name uniqueness (a RawPath segment). Two filtered indexes cover the nullable
|
||||
// RawFolderId: nested drivers keyed by (ClusterId, RawFolderId, Name), cluster-root drivers
|
||||
// by (ClusterId, Name).
|
||||
e.HasIndex(x => new { x.ClusterId, x.RawFolderId, x.Name }).IsUnique()
|
||||
.HasFilter("[RawFolderId] IS NOT NULL")
|
||||
.HasDatabaseName("UX_DriverInstance_Folder_Name");
|
||||
e.HasIndex(x => new { x.ClusterId, x.Name }).IsUnique()
|
||||
.HasFilter("[RawFolderId] IS NULL")
|
||||
.HasDatabaseName("UX_DriverInstance_ClusterRoot_Name");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -300,6 +318,38 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
||||
|
||||
e.HasIndex(x => x.DriverInstanceId).HasDatabaseName("IX_Device_Driver");
|
||||
e.HasIndex(x => x.DeviceId).IsUnique().HasDatabaseName("UX_Device_LogicalId");
|
||||
// v3 sibling-name uniqueness within a driver (a RawPath segment).
|
||||
e.HasIndex(x => new { x.DriverInstanceId, x.Name }).IsUnique()
|
||||
.HasDatabaseName("UX_Device_Driver_Name");
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigureTagGroup(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<TagGroup>(e =>
|
||||
{
|
||||
e.ToTable("TagGroup");
|
||||
e.HasKey(x => x.TagGroupRowId);
|
||||
e.Property(x => x.TagGroupRowId).HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||
e.Property(x => x.TagGroupId).HasMaxLength(64);
|
||||
e.Property(x => x.DeviceId).HasMaxLength(64);
|
||||
e.Property(x => x.ParentTagGroupId).HasMaxLength(64);
|
||||
e.Property(x => x.Name).HasMaxLength(128);
|
||||
e.Property(x => x.RowVersion).IsRowVersion();
|
||||
|
||||
e.HasIndex(x => x.TagGroupId).IsUnique().HasDatabaseName("UX_TagGroup_LogicalId");
|
||||
e.HasIndex(x => x.DeviceId).HasDatabaseName("IX_TagGroup_Device");
|
||||
|
||||
// Sibling-name uniqueness. Two filtered indexes cover the nullable ParentTagGroupId:
|
||||
// nested groups keyed by (DeviceId, ParentTagGroupId, Name), device-root groups by
|
||||
// (DeviceId, Name). DeviceId/ParentTagGroupId are logical FKs (no navigation); non-empty
|
||||
// group deletes are blocked at the service layer in later batches.
|
||||
e.HasIndex(x => new { x.DeviceId, x.ParentTagGroupId, x.Name }).IsUnique()
|
||||
.HasFilter("[ParentTagGroupId] IS NOT NULL")
|
||||
.HasDatabaseName("UX_TagGroup_Parent_Name");
|
||||
e.HasIndex(x => new { x.DeviceId, x.Name }).IsUnique()
|
||||
.HasFilter("[ParentTagGroupId] IS NULL")
|
||||
.HasDatabaseName("UX_TagGroup_DeviceRoot_Name");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -311,8 +361,6 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
||||
e.HasKey(x => x.EquipmentRowId);
|
||||
e.Property(x => x.EquipmentRowId).HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||
e.Property(x => x.EquipmentId).HasMaxLength(64);
|
||||
e.Property(x => x.DriverInstanceId).HasMaxLength(64);
|
||||
e.Property(x => x.DeviceId).HasMaxLength(64);
|
||||
e.Property(x => x.UnsLineId).HasMaxLength(64);
|
||||
e.Property(x => x.Name).HasMaxLength(32);
|
||||
e.Property(x => x.MachineCode).HasMaxLength(64);
|
||||
@@ -329,7 +377,6 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
||||
e.Property(x => x.EquipmentClassRef).HasMaxLength(128);
|
||||
e.Property(x => x.RowVersion).IsRowVersion();
|
||||
|
||||
e.HasIndex(x => x.DriverInstanceId).HasDatabaseName("IX_Equipment_Driver");
|
||||
e.HasIndex(x => x.UnsLineId).HasDatabaseName("IX_Equipment_Line");
|
||||
e.HasIndex(x => x.EquipmentId).IsUnique().HasDatabaseName("UX_Equipment_LogicalId");
|
||||
e.HasIndex(x => new { x.UnsLineId, x.Name }).IsUnique().HasDatabaseName("UX_Equipment_LinePath");
|
||||
@@ -351,28 +398,29 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
||||
e.HasKey(x => x.TagRowId);
|
||||
e.Property(x => x.TagRowId).HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||
e.Property(x => x.TagId).HasMaxLength(64);
|
||||
e.Property(x => x.DriverInstanceId).HasMaxLength(64);
|
||||
e.Property(x => x.DeviceId).HasMaxLength(64);
|
||||
e.Property(x => x.EquipmentId).HasMaxLength(64);
|
||||
// v3: raw-only tag. Required DeviceId FK; nullable TagGroupId. EquipmentId/FolderPath/
|
||||
// DriverInstanceId are retired — identity is the RawPath (device + optional group + name).
|
||||
e.Property(x => x.DeviceId).HasMaxLength(64).IsRequired();
|
||||
e.Property(x => x.TagGroupId).HasMaxLength(64);
|
||||
e.Property(x => x.Name).HasMaxLength(128);
|
||||
e.Property(x => x.FolderPath).HasMaxLength(512);
|
||||
e.Property(x => x.DataType).HasMaxLength(32);
|
||||
e.Property(x => x.AccessLevel).HasConversion<string>().HasMaxLength(16);
|
||||
e.Property(x => x.PollGroupId).HasMaxLength(64);
|
||||
e.Property(x => x.TagConfig).HasColumnType("nvarchar(max)");
|
||||
e.Property(x => x.RowVersion).IsRowVersion();
|
||||
|
||||
e.HasIndex(x => new { x.DriverInstanceId, x.DeviceId }).HasDatabaseName("IX_Tag_Driver_Device");
|
||||
e.HasIndex(x => x.EquipmentId)
|
||||
.HasFilter("[EquipmentId] IS NOT NULL")
|
||||
.HasDatabaseName("IX_Tag_Equipment");
|
||||
e.HasIndex(x => new { x.DeviceId, x.TagGroupId }).HasDatabaseName("IX_Tag_Device");
|
||||
e.HasIndex(x => x.TagId).IsUnique().HasDatabaseName("UX_Tag_LogicalId");
|
||||
e.HasIndex(x => new { x.EquipmentId, x.Name }).IsUnique()
|
||||
.HasFilter("[EquipmentId] IS NOT NULL")
|
||||
.HasDatabaseName("UX_Tag_EquipmentPath");
|
||||
e.HasIndex(x => new { x.DriverInstanceId, x.FolderPath, x.Name }).IsUnique()
|
||||
.HasFilter("[EquipmentId] IS NULL")
|
||||
.HasDatabaseName("UX_Tag_FolderPath");
|
||||
|
||||
// Sibling-name uniqueness (a RawPath segment). Two filtered indexes cover the nullable
|
||||
// TagGroupId: grouped tags keyed by (DeviceId, TagGroupId, Name), device-root tags by
|
||||
// (DeviceId, Name).
|
||||
e.HasIndex(x => new { x.DeviceId, x.TagGroupId, x.Name }).IsUnique()
|
||||
.HasFilter("[TagGroupId] IS NOT NULL")
|
||||
.HasDatabaseName("UX_Tag_Group_Name");
|
||||
e.HasIndex(x => new { x.DeviceId, x.Name }).IsUnique()
|
||||
.HasFilter("[TagGroupId] IS NULL")
|
||||
.HasDatabaseName("UX_Tag_Device_Name");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -567,51 +615,28 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigureEquipmentImportBatch(ModelBuilder modelBuilder)
|
||||
private static void ConfigureUnsTagReference(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<EquipmentImportBatch>(e =>
|
||||
modelBuilder.Entity<UnsTagReference>(e =>
|
||||
{
|
||||
e.ToTable("EquipmentImportBatch");
|
||||
e.HasKey(x => x.Id);
|
||||
e.Property(x => x.ClusterId).HasMaxLength(64);
|
||||
e.Property(x => x.CreatedBy).HasMaxLength(128);
|
||||
e.Property(x => x.CreatedAtUtc).HasColumnType("datetime2(3)");
|
||||
e.Property(x => x.FinalisedAtUtc).HasColumnType("datetime2(3)");
|
||||
|
||||
// Admin preview modal filters by user; finalise / drop both hit this index.
|
||||
e.HasIndex(x => new { x.CreatedBy, x.FinalisedAtUtc })
|
||||
.HasDatabaseName("IX_EquipmentImportBatch_Creator_Finalised");
|
||||
});
|
||||
|
||||
modelBuilder.Entity<EquipmentImportRow>(e =>
|
||||
{
|
||||
e.ToTable("EquipmentImportRow");
|
||||
e.HasKey(x => x.Id);
|
||||
e.Property(x => x.ZTag).HasMaxLength(128);
|
||||
e.Property(x => x.MachineCode).HasMaxLength(128);
|
||||
e.Property(x => x.SAPID).HasMaxLength(128);
|
||||
e.ToTable("UnsTagReference");
|
||||
e.HasKey(x => x.UnsTagReferenceRowId);
|
||||
e.Property(x => x.UnsTagReferenceRowId).HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||
e.Property(x => x.UnsTagReferenceId).HasMaxLength(64);
|
||||
e.Property(x => x.EquipmentId).HasMaxLength(64);
|
||||
e.Property(x => x.EquipmentUuid).HasMaxLength(64);
|
||||
e.Property(x => x.Name).HasMaxLength(128);
|
||||
e.Property(x => x.UnsAreaName).HasMaxLength(64);
|
||||
e.Property(x => x.UnsLineName).HasMaxLength(64);
|
||||
e.Property(x => x.Manufacturer).HasMaxLength(256);
|
||||
e.Property(x => x.Model).HasMaxLength(256);
|
||||
e.Property(x => x.SerialNumber).HasMaxLength(256);
|
||||
e.Property(x => x.HardwareRevision).HasMaxLength(64);
|
||||
e.Property(x => x.SoftwareRevision).HasMaxLength(64);
|
||||
e.Property(x => x.YearOfConstruction).HasMaxLength(8);
|
||||
e.Property(x => x.AssetLocation).HasMaxLength(512);
|
||||
e.Property(x => x.ManufacturerUri).HasMaxLength(512);
|
||||
e.Property(x => x.DeviceManualUri).HasMaxLength(512);
|
||||
e.Property(x => x.RejectReason).HasMaxLength(512);
|
||||
e.Property(x => x.TagId).HasMaxLength(64);
|
||||
e.Property(x => x.DisplayNameOverride).HasMaxLength(128);
|
||||
e.Property(x => x.RowVersion).IsRowVersion();
|
||||
|
||||
e.HasOne(x => x.Batch)
|
||||
.WithMany(b => b.Rows)
|
||||
.HasForeignKey(x => x.BatchId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
e.HasIndex(x => x.UnsTagReferenceId).IsUnique().HasDatabaseName("UX_UnsTagReference_LogicalId");
|
||||
e.HasIndex(x => x.EquipmentId).HasDatabaseName("IX_UnsTagReference_Equipment");
|
||||
e.HasIndex(x => x.TagId).HasDatabaseName("IX_UnsTagReference_Tag");
|
||||
|
||||
e.HasIndex(x => x.BatchId).HasDatabaseName("IX_EquipmentImportRow_Batch");
|
||||
// One reference per (equipment, raw tag). EquipmentId/TagId are logical FKs (no
|
||||
// navigation); the "raw-tag delete blocked while referenced" rule is enforced at the
|
||||
// service layer in later batches.
|
||||
e.HasIndex(x => new { x.EquipmentId, x.TagId }).IsUnique()
|
||||
.HasDatabaseName("UX_UnsTagReference_Equip_Tag");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user