feat(config): forward-only migration deleting orphaned SystemPlatform namespace data (clean break)

This commit is contained in:
Joseph Doherty
2026-06-12 22:25:00 -04:00
parent dcbaf63ab1
commit ba42bed538
2 changed files with 1878 additions and 0 deletions
@@ -0,0 +1,120 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
{
/// <summary>
/// Forward-only DATA-cleanup migration for the Galaxy clean break: the
/// <c>NamespaceKind.SystemPlatform</c> enum member was removed (Galaxy is now an
/// Equipment-kind driver). <c>Namespace.Kind</c> is persisted as a string via
/// <c>HasConversion&lt;string&gt;</c>, so any leftover row with <c>Kind = 'SystemPlatform'</c>
/// would throw at materialisation time (no matching enum member) the moment the new code
/// reads the table. This deletes those namespaces plus everything that hung off them.
/// </summary>
/// <remarks>
/// <para>
/// There is NO schema/model change in this migration — the model snapshot is unchanged.
/// The unique index <c>UX_Namespace_Cluster_Kind</c> (unique on <c>ClusterId, Kind</c>) is
/// intentionally KEPT: <c>Simulated</c> remains a reserved kind, so one-of-each-kind-per-cluster
/// is still the correct constraint.
/// </para>
/// <para>
/// The cross-table relationships in this graph are LOGICAL foreign keys (string Id columns,
/// e.g. <c>DriverInstance.NamespaceId</c> → <c>Namespace.NamespaceId</c>,
/// <c>Tag.DriverInstanceId</c> → <c>DriverInstance.DriverInstanceId</c>) enforced by
/// <c>sp_ValidateDraft</c>, NOT physical DB FK constraints — only the <c>*.ClusterId</c>
/// relationships are real DB FKs. So no FK constraint can be violated here. The deletes are
/// nonetheless ordered child→parent so no orphaned rows are left behind, and every statement
/// is keyed off the set of SystemPlatform namespaces, so on a DB with zero such rows every
/// DELETE simply affects 0 rows (idempotent / safe to run repeatedly).
/// </para>
/// </remarks>
/// <inheritdoc />
public partial class CleanupSystemPlatformNamespaces : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// Delete in child -> parent order. Everything is keyed off the set of namespaces whose
// Kind = 'SystemPlatform' (the string form persisted by HasConversion<string>), funnelled
// through the DriverInstances that lived in those namespaces.
migrationBuilder.Sql(@"
-- Defensive: SystemPlatform namespaces historically held only EquipmentId-NULL mirror tags, but a
-- stray equipment-scoped tag / Equipment could be bound to a SystemPlatform-namespace driver. Clean
-- both, plus anything that FKs into the affected Equipment (VirtualTag / ScriptedAlarm / state).
-- Grandchildren of the affected Equipment ------------------------------------------------
-- ScriptedAlarmState is keyed by ScriptedAlarm.ScriptedAlarmId; remove before its ScriptedAlarm.
DELETE FROM [ScriptedAlarmState]
WHERE [ScriptedAlarmId] IN (
SELECT [ScriptedAlarmId] FROM [ScriptedAlarm]
WHERE [EquipmentId] IN (
SELECT [EquipmentId] FROM [Equipment]
WHERE [DriverInstanceId] IN (
SELECT [DriverInstanceId] FROM [DriverInstance]
WHERE [NamespaceId] IN (
SELECT [NamespaceId] FROM [Namespace] WHERE [Kind] = 'SystemPlatform'))));
DELETE FROM [ScriptedAlarm]
WHERE [EquipmentId] IN (
SELECT [EquipmentId] FROM [Equipment]
WHERE [DriverInstanceId] IN (
SELECT [DriverInstanceId] FROM [DriverInstance]
WHERE [NamespaceId] IN (
SELECT [NamespaceId] FROM [Namespace] WHERE [Kind] = 'SystemPlatform')));
DELETE FROM [VirtualTag]
WHERE [EquipmentId] IN (
SELECT [EquipmentId] FROM [Equipment]
WHERE [DriverInstanceId] IN (
SELECT [DriverInstanceId] FROM [DriverInstance]
WHERE [NamespaceId] IN (
SELECT [NamespaceId] FROM [Namespace] WHERE [Kind] = 'SystemPlatform')));
-- Children of the affected DriverInstances ------------------------------------------------
-- Tags bound to those drivers: both EquipmentId-NULL mirror tags AND any equipment-scoped tag.
DELETE FROM [Tag]
WHERE [DriverInstanceId] IN (
SELECT [DriverInstanceId] FROM [DriverInstance]
WHERE [NamespaceId] IN (
SELECT [NamespaceId] FROM [Namespace] WHERE [Kind] = 'SystemPlatform'));
DELETE FROM [PollGroup]
WHERE [DriverInstanceId] IN (
SELECT [DriverInstanceId] FROM [DriverInstance]
WHERE [NamespaceId] IN (
SELECT [NamespaceId] FROM [Namespace] WHERE [Kind] = 'SystemPlatform'));
DELETE FROM [Device]
WHERE [DriverInstanceId] IN (
SELECT [DriverInstanceId] FROM [DriverInstance]
WHERE [NamespaceId] IN (
SELECT [NamespaceId] FROM [Namespace] WHERE [Kind] = 'SystemPlatform'));
DELETE FROM [Equipment]
WHERE [DriverInstanceId] IN (
SELECT [DriverInstanceId] FROM [DriverInstance]
WHERE [NamespaceId] IN (
SELECT [NamespaceId] FROM [Namespace] WHERE [Kind] = 'SystemPlatform'));
-- The DriverInstances themselves ----------------------------------------------------------
DELETE FROM [DriverInstance]
WHERE [NamespaceId] IN (
SELECT [NamespaceId] FROM [Namespace] WHERE [Kind] = 'SystemPlatform');
-- The SystemPlatform namespaces (the rows that would crash EF on read) --------------------
DELETE FROM [Namespace] WHERE [Kind] = 'SystemPlatform';
");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
// Forward-only data cleanup: the deleted SystemPlatform namespaces and their dependent
// rows cannot be reconstructed (the data is gone, and SystemPlatform is no longer a valid
// NamespaceKind). Intentionally a no-op rather than a throw, so rolling back a LATER
// migration does not blow up on this one.
}
}
}