From 4bb4ad8acbb5ece097c714c85118b9b683047249 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 26 May 2026 03:58:58 -0400 Subject: [PATCH] feat(configdb): add RowVersion to live-edit entities Phase 1a of the v2 entity-model rewrite. Adds: public byte[] RowVersion { get; set; } = Array.Empty(); and the EF Core mapping e.Property(x => x.RowVersion).IsRowVersion(); to 12 live-edit entities: Equipment, DriverInstance, Device, Tag, PollGroup, Namespace, UnsArea, UnsLine, NodeAcl, Script, VirtualTag, ScriptedAlarm These are the entities that v2 admins will edit directly via AdminOperationsActor (no draft staging). RowVersion enables last-write-wins detection when two operators race on the same row. GenerationId FKs are still in place on these entities (removed in Task 14b); this commit only adds the rowversion column so the migration in Task 14f can emit ADD COLUMN before DROP FK as a single atomic step. --- .../Entities/Device.cs | 3 +++ .../Entities/DriverInstance.cs | 3 +++ .../Entities/Equipment.cs | 3 +++ .../Entities/Namespace.cs | 3 +++ .../Entities/NodeAcl.cs | 3 +++ .../Entities/PollGroup.cs | 3 +++ .../Entities/Script.cs | 3 +++ .../Entities/ScriptedAlarm.cs | 3 +++ .../ZB.MOM.WW.OtOpcUa.Configuration/Entities/Tag.cs | 3 +++ .../Entities/UnsArea.cs | 3 +++ .../Entities/UnsLine.cs | 3 +++ .../Entities/VirtualTag.cs | 3 +++ .../OtOpcUaConfigDbContext.cs | 12 ++++++++++++ 13 files changed, 48 insertions(+) diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Device.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Device.cs index 603005b..b2608b2 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Device.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Device.cs @@ -19,5 +19,8 @@ public sealed class Device /// Schemaless per-driver-type device config (host, port, unit ID, slot, etc.). public required string DeviceConfig { get; set; } + /// Optimistic concurrency token for last-write-wins detection in the v2 live-edit model. + public byte[] RowVersion { get; set; } = Array.Empty(); + public ConfigGeneration? Generation { get; set; } } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/DriverInstance.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/DriverInstance.cs index f2168a3..3453562 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/DriverInstance.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/DriverInstance.cs @@ -45,6 +45,9 @@ public sealed class DriverInstance /// public string? ResilienceConfig { get; set; } + /// Optimistic concurrency token for last-write-wins detection in the v2 live-edit model. + public byte[] RowVersion { get; set; } = Array.Empty(); + public ConfigGeneration? Generation { get; set; } public ServerCluster? Cluster { get; set; } } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Equipment.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Equipment.cs index adc68ae..53c4bbc 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Equipment.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Equipment.cs @@ -60,5 +60,8 @@ public sealed class Equipment public bool Enabled { get; set; } = true; + /// Optimistic concurrency token for last-write-wins detection in the v2 live-edit model. + public byte[] RowVersion { get; set; } = Array.Empty(); + public ConfigGeneration? Generation { get; set; } } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Namespace.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Namespace.cs index fea7459..6e194cd 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Namespace.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Namespace.cs @@ -26,6 +26,9 @@ public sealed class Namespace public string? Notes { get; set; } + /// Optimistic concurrency token for last-write-wins detection in the v2 live-edit model. + public byte[] RowVersion { get; set; } = Array.Empty(); + public ConfigGeneration? Generation { get; set; } public ServerCluster? Cluster { get; set; } } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/NodeAcl.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/NodeAcl.cs index 57cb906..cc9f6b6 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/NodeAcl.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/NodeAcl.cs @@ -28,5 +28,8 @@ public sealed class NodeAcl public string? Notes { get; set; } + /// Optimistic concurrency token for last-write-wins detection in the v2 live-edit model. + public byte[] RowVersion { get; set; } = Array.Empty(); + public ConfigGeneration? Generation { get; set; } } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/PollGroup.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/PollGroup.cs index 856fad2..6413706 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/PollGroup.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/PollGroup.cs @@ -15,5 +15,8 @@ public sealed class PollGroup public int IntervalMs { get; set; } + /// Optimistic concurrency token for last-write-wins detection in the v2 live-edit model. + public byte[] RowVersion { get; set; } = Array.Empty(); + public ConfigGeneration? Generation { get; set; } } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Script.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Script.cs index 67174bf..edde823 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Script.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Script.cs @@ -34,5 +34,8 @@ public sealed class Script /// Language — always "CSharp" today; placeholder for future engines (Python/Lua). public string Language { get; set; } = "CSharp"; + /// Optimistic concurrency token for last-write-wins detection in the v2 live-edit model. + public byte[] RowVersion { get; set; } = Array.Empty(); + public ConfigGeneration? Generation { get; set; } } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ScriptedAlarm.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ScriptedAlarm.cs index f99f4be..e540a7c 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ScriptedAlarm.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ScriptedAlarm.cs @@ -55,5 +55,8 @@ public sealed class ScriptedAlarm public bool Enabled { get; set; } = true; + /// Optimistic concurrency token for last-write-wins detection in the v2 live-edit model. + public byte[] RowVersion { get; set; } = Array.Empty(); + public ConfigGeneration? Generation { get; set; } } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Tag.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Tag.cs index 35f2c17..31becc8 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Tag.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Tag.cs @@ -43,5 +43,8 @@ public sealed class Tag /// Register address / scaling / poll group / byte-order / etc. — schemaless per driver type. public required string TagConfig { get; set; } + /// Optimistic concurrency token for last-write-wins detection in the v2 live-edit model. + public byte[] RowVersion { get; set; } = Array.Empty(); + public ConfigGeneration? Generation { get; set; } } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/UnsArea.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/UnsArea.cs index d1b0bd0..dc53073 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/UnsArea.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/UnsArea.cs @@ -16,6 +16,9 @@ public sealed class UnsArea public string? Notes { get; set; } + /// Optimistic concurrency token for last-write-wins detection in the v2 live-edit model. + public byte[] RowVersion { get; set; } = Array.Empty(); + public ConfigGeneration? Generation { get; set; } public ServerCluster? Cluster { get; set; } } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/UnsLine.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/UnsLine.cs index 1a41b74..e7c612c 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/UnsLine.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/UnsLine.cs @@ -17,5 +17,8 @@ public sealed class UnsLine public string? Notes { get; set; } + /// Optimistic concurrency token for last-write-wins detection in the v2 live-edit model. + public byte[] RowVersion { get; set; } = Array.Empty(); + public ConfigGeneration? Generation { get; set; } } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/VirtualTag.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/VirtualTag.cs index eff66a6..dbb04be 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/VirtualTag.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/VirtualTag.cs @@ -49,5 +49,8 @@ public sealed class VirtualTag public bool Enabled { get; set; } = true; + /// Optimistic concurrency token for last-write-wins detection in the v2 live-edit model. + public byte[] RowVersion { get; set; } = Array.Empty(); + public ConfigGeneration? Generation { get; set; } } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs index cba2bec..cd05219 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs @@ -207,6 +207,7 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions x.Kind).HasConversion().HasMaxLength(32); e.Property(x => x.NamespaceUri).HasMaxLength(256); e.Property(x => x.Notes).HasMaxLength(1024); + e.Property(x => x.RowVersion).IsRowVersion(); e.HasOne(x => x.Generation).WithMany() .HasForeignKey(x => x.GenerationId) @@ -239,6 +240,7 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions x.ClusterId).HasMaxLength(64); e.Property(x => x.Name).HasMaxLength(32); e.Property(x => x.Notes).HasMaxLength(512); + e.Property(x => x.RowVersion).IsRowVersion(); e.HasOne(x => x.Generation).WithMany().HasForeignKey(x => x.GenerationId).OnDelete(DeleteBehavior.Restrict); e.HasOne(x => x.Cluster).WithMany().HasForeignKey(x => x.ClusterId).OnDelete(DeleteBehavior.Restrict); @@ -260,6 +262,7 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions x.UnsAreaId).HasMaxLength(64); e.Property(x => x.Name).HasMaxLength(32); e.Property(x => x.Notes).HasMaxLength(512); + e.Property(x => x.RowVersion).IsRowVersion(); e.HasOne(x => x.Generation).WithMany().HasForeignKey(x => x.GenerationId).OnDelete(DeleteBehavior.Restrict); @@ -289,6 +292,7 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions x.DriverType).HasMaxLength(32); e.Property(x => x.DriverConfig).HasColumnType("nvarchar(max)"); e.Property(x => x.ResilienceConfig).HasColumnType("nvarchar(max)"); + e.Property(x => x.RowVersion).IsRowVersion(); e.HasOne(x => x.Generation).WithMany().HasForeignKey(x => x.GenerationId).OnDelete(DeleteBehavior.Restrict); e.HasOne(x => x.Cluster).WithMany().HasForeignKey(x => x.ClusterId).OnDelete(DeleteBehavior.Restrict); @@ -313,6 +317,7 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions x.DriverInstanceId).HasMaxLength(64); e.Property(x => x.Name).HasMaxLength(128); e.Property(x => x.DeviceConfig).HasColumnType("nvarchar(max)"); + e.Property(x => x.RowVersion).IsRowVersion(); e.HasOne(x => x.Generation).WithMany().HasForeignKey(x => x.GenerationId).OnDelete(DeleteBehavior.Restrict); @@ -345,6 +350,7 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions x.ManufacturerUri).HasMaxLength(512); e.Property(x => x.DeviceManualUri).HasMaxLength(512); e.Property(x => x.EquipmentClassRef).HasMaxLength(128); + e.Property(x => x.RowVersion).IsRowVersion(); e.HasOne(x => x.Generation).WithMany().HasForeignKey(x => x.GenerationId).OnDelete(DeleteBehavior.Restrict); @@ -379,6 +385,7 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions x.AccessLevel).HasConversion().HasMaxLength(16); e.Property(x => x.PollGroupId).HasMaxLength(64); e.Property(x => x.TagConfig).HasColumnType("nvarchar(max)"); + e.Property(x => x.RowVersion).IsRowVersion(); e.HasOne(x => x.Generation).WithMany().HasForeignKey(x => x.GenerationId).OnDelete(DeleteBehavior.Restrict); @@ -409,6 +416,7 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions x.PollGroupId).HasMaxLength(64); e.Property(x => x.DriverInstanceId).HasMaxLength(64); e.Property(x => x.Name).HasMaxLength(128); + e.Property(x => x.RowVersion).IsRowVersion(); e.HasOne(x => x.Generation).WithMany().HasForeignKey(x => x.GenerationId).OnDelete(DeleteBehavior.Restrict); @@ -431,6 +439,7 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions x.ScopeId).HasMaxLength(64); e.Property(x => x.PermissionFlags).HasConversion(); e.Property(x => x.Notes).HasMaxLength(512); + e.Property(x => x.RowVersion).IsRowVersion(); e.HasOne(x => x.Generation).WithMany().HasForeignKey(x => x.GenerationId).OnDelete(DeleteBehavior.Restrict); @@ -655,6 +664,7 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions x.SourceCode).HasColumnType("nvarchar(max)"); e.Property(x => x.SourceHash).HasMaxLength(64); e.Property(x => x.Language).HasMaxLength(16); + e.Property(x => x.RowVersion).IsRowVersion(); e.HasOne(x => x.Generation).WithMany().HasForeignKey(x => x.GenerationId).OnDelete(DeleteBehavior.Restrict); @@ -681,6 +691,7 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions x.Name).HasMaxLength(128); e.Property(x => x.DataType).HasMaxLength(32); e.Property(x => x.ScriptId).HasMaxLength(64); + e.Property(x => x.RowVersion).IsRowVersion(); e.HasOne(x => x.Generation).WithMany().HasForeignKey(x => x.GenerationId).OnDelete(DeleteBehavior.Restrict); @@ -708,6 +719,7 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions x.AlarmType).HasMaxLength(32); e.Property(x => x.MessageTemplate).HasMaxLength(1024); e.Property(x => x.PredicateScriptId).HasMaxLength(64); + e.Property(x => x.RowVersion).IsRowVersion(); e.HasOne(x => x.Generation).WithMany().HasForeignKey(x => x.GenerationId).OnDelete(DeleteBehavior.Restrict);