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:
Joseph Doherty
2026-07-15 19:19:21 -04:00
parent cb720bb8c3
commit 4d39b98564
54 changed files with 2523 additions and 30115 deletions
@@ -29,30 +29,32 @@ public sealed class DraftSnapshotFactoryTests : IDisposable
/// <summary>Disposes the database context.</summary>
public void Dispose() => _db.Dispose();
/// <summary>Seeds one Equipment plus a Tag and a VirtualTag sharing (EquipmentId, Name); the
/// snapshot must carry both signal collections AND the validator must flag the collision.</summary>
/// <summary>Seeds an Equipment plus a raw Tag and a VirtualTag; the snapshot must carry both
/// signal collections. v3: Tags are raw-only, so the equipment-signal collision rule now operates
/// on VirtualTags — two VirtualTags sharing (EquipmentId, Name) must surface the collision.</summary>
[Fact]
public async Task FromConfigDb_populates_Tags_and_VirtualTags_and_surfaces_collision()
{
SeedEquipment("eq-1");
_db.Tags.Add(BuildTag(equipmentId: "eq-1", name: "speed"));
_db.VirtualTags.Add(BuildVirtualTag(equipmentId: "eq-1", name: "speed"));
_db.Tags.Add(BuildTag(name: "speed"));
_db.VirtualTags.Add(BuildVirtualTag(equipmentId: "eq-1", name: "speed", suffix: "a"));
_db.VirtualTags.Add(BuildVirtualTag(equipmentId: "eq-1", name: "speed", suffix: "b"));
await _db.SaveChangesAsync();
var snapshot = await DraftSnapshotFactory.FromConfigDbAsync(_db);
snapshot.Tags.Count.ShouldBe(1);
snapshot.VirtualTags.Count.ShouldBe(1);
snapshot.VirtualTags.Count.ShouldBe(2);
DraftValidator.Validate(snapshot).ShouldContain(e => e.Code == "EquipmentSignalNameCollision");
}
/// <summary>A Tag and a VirtualTag with distinct names under the same equipment do not collide,
/// so the snapshot validates clean of the collision code.</summary>
/// <summary>Distinct VirtualTag names under the same equipment do not collide, so the snapshot
/// validates clean of the collision code.</summary>
[Fact]
public async Task FromConfigDb_no_collision_when_names_differ()
{
SeedEquipment("eq-1");
_db.Tags.Add(BuildTag(equipmentId: "eq-1", name: "speed"));
_db.Tags.Add(BuildTag(name: "speed"));
_db.VirtualTags.Add(BuildVirtualTag(equipmentId: "eq-1", name: "temperature"));
await _db.SaveChangesAsync();
@@ -109,26 +111,25 @@ public sealed class DraftSnapshotFactoryTests : IDisposable
EquipmentUuid = uuid,
EquipmentId = equipmentId,
Name = "eq",
DriverInstanceId = "d",
UnsLineId = "line-a",
MachineCode = "m",
});
}
private static Tag BuildTag(string equipmentId, string name) => new()
// v3: Tags are raw-only (DeviceId FK, no EquipmentId/DriverInstanceId).
private static Tag BuildTag(string name) => new()
{
TagId = $"tag-{name}",
DriverInstanceId = "d",
EquipmentId = equipmentId,
DeviceId = "dev-1",
Name = name,
DataType = "Float",
AccessLevel = TagAccessLevel.Read,
TagConfig = "{}",
};
private static VirtualTag BuildVirtualTag(string equipmentId, string name) => new()
private static VirtualTag BuildVirtualTag(string equipmentId, string name, string suffix = "") => new()
{
VirtualTagId = $"vtag-{name}",
VirtualTagId = $"vtag-{name}{suffix}",
EquipmentId = equipmentId,
Name = name,
DataType = "Float",