feat(audit): EventId + CorrelationId columns + filtered unique index (F3 + F4)

ConfigAuditLog gains two nullable columns (EventId, CorrelationId) + a filtered
unique index UX_ConfigAuditLog_EventId. EF migration
20260526105027_AddConfigAuditLogEventIdColumns is additive (nullable + filtered
index = legacy rows backfill cleanly).

AuditWriterActor now writes EventId + CorrelationId into the dedicated columns
instead of synthesising a JSON wrapper into DetailsJson. Cross-restart dedup
is now real: a retry of an already-flushed batch hits the unique index and
SaveChanges throws; the existing catch drops the duplicate without losing the
rest of the batch.

WrapDetails helper deleted — F4 (its JSON hardening) becomes moot.

AuditWriterActorTests.Details_wrapper_embeds_eventId_and_correlationId renamed
+ rewritten to assert against the columns. All 29 ControlPlane tests pass,
all 95 v2 tests green.
This commit is contained in:
Joseph Doherty
2026-05-26 06:52:53 -04:00
parent 8e5c8e29f7
commit f57f61deac
7 changed files with 1850 additions and 19 deletions
@@ -413,6 +413,8 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
e.Property(x => x.ClusterId).HasMaxLength(64);
e.Property(x => x.NodeId).HasMaxLength(64);
e.Property(x => x.DetailsJson).HasColumnType("nvarchar(max)");
e.Property(x => x.EventId);
e.Property(x => x.CorrelationId);
e.HasIndex(x => new { x.ClusterId, x.Timestamp })
.IsDescending(false, true)
@@ -420,6 +422,14 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
e.HasIndex(x => x.GenerationId)
.HasFilter("[GenerationId] IS NOT NULL")
.HasDatabaseName("IX_ConfigAuditLog_Generation");
// Filtered unique index gives cross-restart idempotency for the AuditWriterActor:
// a retry of an already-flushed batch will hit this constraint and the catch in
// FlushBuffer drops the duplicate insert. Nullable + filter so legacy backfill rows
// (EventId=NULL) don't collide.
e.HasIndex(x => x.EventId)
.IsUnique()
.HasFilter("[EventId] IS NOT NULL")
.HasDatabaseName("UX_ConfigAuditLog_EventId");
});
}