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
@@ -28,14 +28,15 @@ public sealed class SchemaComplianceTests
{
"ServerCluster", "ClusterNode", "ClusterNodeCredential",
"ConfigAuditLog",
"Namespace", "UnsArea", "UnsLine",
"UnsArea", "UnsLine",
// v3 Raw-tree tables (Namespace retired; RawFolder/TagGroup/UnsTagReference added)
"RawFolder", "TagGroup", "UnsTagReference",
"DriverInstance", "Device", "Equipment", "Tag", "PollGroup", "VirtualTag",
"NodeAcl", "ExternalIdReservation",
"DriverHostStatus",
"DriverInstanceResilienceStatus",
"LdapGroupRoleMapping",
"EquipmentImportBatch",
"EquipmentImportRow",
// v3 dropped the orphaned EquipmentImportBatch / EquipmentImportRow tables
"Script", "ScriptedAlarm", "ScriptedAlarmState",
// v2 deploy-model tables (Phase 1 of Akka + fused-hosting alignment)
"Deployment", "NodeDeploymentState", "ConfigEdit", "DataProtectionKeys",
@@ -60,6 +61,15 @@ SELECT name FROM sys.tables WHERE name <> '__EFMigrationsHistory' ORDER BY name;
{
("UX_ClusterNodeCredential_Value", "([Enabled]=(1))"),
("UX_ExternalIdReservation_KindValue_Active", "([ReleasedAt] IS NULL)"),
// v3 sibling-name filtered unique indexes over nullable parent columns.
("UX_RawFolder_Parent_Name", "([ParentRawFolderId] IS NOT NULL)"),
("UX_RawFolder_ClusterRoot_Name", "([ParentRawFolderId] IS NULL)"),
("UX_DriverInstance_Folder_Name", "([RawFolderId] IS NOT NULL)"),
("UX_DriverInstance_ClusterRoot_Name", "([RawFolderId] IS NULL)"),
("UX_TagGroup_Parent_Name", "([ParentTagGroupId] IS NOT NULL)"),
("UX_TagGroup_DeviceRoot_Name", "([ParentTagGroupId] IS NULL)"),
("UX_Tag_Group_Name", "([TagGroupId] IS NOT NULL)"),
("UX_Tag_Device_Name", "([TagGroupId] IS NULL)"),
};
var rows = QueryRows(@"
@@ -148,20 +158,36 @@ SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Equipment
columns.ShouldContain(col, $"Equipment missing expected column: {col}");
}
/// <summary>Verifies that Namespace has at least one unique index.</summary>
/// <summary>Verifies the retired v2 tables (Namespace, EquipmentImportBatch/Row) are absent.</summary>
[Fact]
public void Namespace_has_some_unique_index()
public void Retired_v2_tables_are_absent()
{
// v2 dropped the "per-generation" qualifier from namespace uniqueness when live-edit
// replaced draft/publish. The v2 index name is implementation-defined; assert that
// *some* unique index exists on Namespace to catch unintentional index drops.
var indexes = QueryStrings(@"
var tables = QueryStrings("SELECT name FROM sys.tables;").ToHashSet();
tables.ShouldNotContain("Namespace");
tables.ShouldNotContain("EquipmentImportBatch");
tables.ShouldNotContain("EquipmentImportRow");
}
/// <summary>Verifies the new v3 Raw-tree tables each carry their logical-id unique index.</summary>
[Fact]
public void V3_raw_tree_tables_have_logical_id_unique_indexes()
{
var expected = new[]
{
("RawFolder", "UX_RawFolder_LogicalId"),
("TagGroup", "UX_TagGroup_LogicalId"),
("UnsTagReference", "UX_UnsTagReference_LogicalId"),
};
foreach (var (table, index) in expected)
{
var indexes = QueryStrings($@"
SELECT i.name
FROM sys.indexes i
JOIN sys.tables t ON i.object_id = t.object_id
WHERE t.name = 'Namespace' AND i.is_unique = 1;").ToList();
indexes.ShouldNotBeEmpty();
WHERE t.name = '{table}' AND i.is_unique = 1;").ToHashSet();
indexes.ShouldContain(index, $"{table} missing unique index {index}");
}
}
private List<string> QueryStrings(string sql)