fix(config): also clean NodeAcl + release ExternalIdReservation in SystemPlatform cleanup migration

This commit is contained in:
Joseph Doherty
2026-06-12 22:34:13 -04:00
parent 7d25480fee
commit b8277922b6
@@ -29,6 +29,15 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
/// 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>
/// <para>
/// Two tables carry LOGICAL refs with no physical FK and so were easy to miss: <c>NodeAcl</c>
/// (per-scope grants keyed by <c>ScopeKind</c> + <c>ScopeId</c>) is cleaned for the
/// Namespace/Equipment/FolderSegment/Tag scopes that get deleted, and
/// <c>ExternalIdReservation</c> (fleet-wide ZTag/SAPID reservations keyed by
/// <c>EquipmentUuid</c>) is RELEASED — not deleted — for the affected equipment so the
/// reserved external IDs free up while the audit row survives. Both run before the entity
/// rows they reference are removed so the id subqueries still resolve.
/// </para>
/// </remarks>
/// <inheritdoc />
public partial class CleanupSystemPlatformNamespaces : Migration
@@ -44,6 +53,58 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
-- 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).
-- Logical-reference cleanup that must run FIRST -------------------------------------------
-- NodeAcl rows carry purely LOGICAL scope refs (ScopeKind + ScopeId, no physical FK). When the
-- scoped entity is deleted below the ScopeId dangles and the ACL evaluator would still match it,
-- silently granting/denying on a node that no longer exists. ScopeKind persists as a string via
-- HasConversion<string>, so the values below ('Namespace'/'Equipment'/'FolderSegment'/'Tag') are
-- the literal enum member names. These deletes must read the entity-id subqueries while those rows
-- still exist, hence they run before the entity DELETEs further down.
-- Namespace-scoped grants pointing at the SystemPlatform namespaces themselves.
DELETE FROM [NodeAcl]
WHERE [ScopeKind] = 'Namespace'
AND [ScopeId] IN (
SELECT [NamespaceId] FROM [Namespace] WHERE [Kind] = 'SystemPlatform');
-- Equipment- and FolderSegment-scoped grants pointing at Equipment that hangs off the affected
-- DriverInstances (both share Equipment.EquipmentId as the ScopeId).
DELETE FROM [NodeAcl]
WHERE [ScopeKind] IN ('Equipment', 'FolderSegment')
AND [ScopeId] IN (
SELECT [EquipmentId] FROM [Equipment]
WHERE [DriverInstanceId] IN (
SELECT [DriverInstanceId] FROM [DriverInstance]
WHERE [NamespaceId] IN (
SELECT [NamespaceId] FROM [Namespace] WHERE [Kind] = 'SystemPlatform')));
-- Tag-scoped grants pointing at Tags bound to the affected DriverInstances (ScopeId = Tag.TagId).
DELETE FROM [NodeAcl]
WHERE [ScopeKind] = 'Tag'
AND [ScopeId] IN (
SELECT [TagId] FROM [Tag]
WHERE [DriverInstanceId] IN (
SELECT [DriverInstanceId] FROM [DriverInstance]
WHERE [NamespaceId] IN (
SELECT [NamespaceId] FROM [Namespace] WHERE [Kind] = 'SystemPlatform')));
-- ExternalIdReservation is NOT generation-versioned and must outlive equipment deletion as a
-- RELEASE, not a delete: active rows (ReleasedAt IS NULL) hold a fleet-wide unique reservation of
-- the ZTag/SAPID under a filtered unique index. If we left them active after dropping the
-- Equipment, the same external IDs could never be reused. Release them so the IDs free up while the
-- audit row survives. Run BEFORE the Equipment DELETE so the EquipmentUuid subquery still resolves.
UPDATE [ExternalIdReservation]
SET [ReleasedAt] = SYSUTCDATETIME(),
[ReleasedBy] = 'CleanupSystemPlatformNamespaces',
[ReleaseReason] = 'Equipment removed by SystemPlatform-namespace cleanup migration (retired NamespaceKind).'
WHERE [ReleasedAt] IS NULL
AND [EquipmentUuid] IN (
SELECT [EquipmentUuid] FROM [Equipment]
WHERE [DriverInstanceId] IN (
SELECT [DriverInstanceId] FROM [DriverInstance]
WHERE [NamespaceId] IN (
SELECT [NamespaceId] FROM [Namespace] WHERE [Kind] = 'SystemPlatform')));
-- Grandchildren of the affected Equipment ------------------------------------------------
-- ScriptedAlarmState is keyed by ScriptedAlarm.ScriptedAlarmId; remove before its ScriptedAlarm.
DELETE FROM [ScriptedAlarmState]