feat(configdb): composite PK + UX_AuditLog_EventId on AuditEvent mapping (#23)

Bundle C migration aligns AuditLog to ps_AuditLog_Month(OccurredAtUtc).
A partitioned table's clustered key must include the partition column, so
the PK becomes the composite {EventId, OccurredAtUtc} — a divergence from
Bundle B's single-column PK that needs reconciling in the EF mapping
before the migration is generated.

EventId remains the idempotency key for AuditLogRepository.InsertIfNotExistsAsync
(M1-T8), so a dedicated unique index UX_AuditLog_EventId preserves the
single-column uniqueness constraint.

Tests updated:
- Configure_MapsToAuditLogTable_WithCompositePrimaryKey (replaces the
  WithEventIdAsPrimaryKey assertion) verifies {EventId, OccurredAtUtc}.
- Configure_DeclaresUniqueIndex_OnEventIdAlone_ForIdempotencyLookups
  asserts the new UX_AuditLog_EventId is unique and on EventId alone.
- Configure_ExpectedIndexes_WithCorrectNames now expects six index names
  (the original five plus UX_AuditLog_EventId).
This commit is contained in:
Joseph Doherty
2026-05-20 10:19:33 -04:00
parent fb423b11ab
commit 7d9550f779
2 changed files with 39 additions and 5 deletions

View File

@@ -28,8 +28,11 @@ public class AuditLogEntityTypeConfigurationTests : IDisposable
}
[Fact]
public void Configure_MapsToAuditLogTable_WithEventIdAsPrimaryKey()
public void Configure_MapsToAuditLogTable_WithCompositePrimaryKey()
{
// Composite PK {EventId, OccurredAtUtc} is required by the partitioned
// AuditLog table — the clustered key must include the partition column
// (OccurredAtUtc) so each row can be located in its partition (#23 Bundle C).
var entity = _context.Model.FindEntityType(typeof(AuditEvent));
Assert.NotNull(entity);
@@ -37,8 +40,28 @@ public class AuditLogEntityTypeConfigurationTests : IDisposable
var pk = entity.FindPrimaryKey();
Assert.NotNull(pk);
var pkProperty = Assert.Single(pk!.Properties);
Assert.Equal(nameof(AuditEvent.EventId), pkProperty.Name);
var pkPropertyNames = pk!.Properties.Select(p => p.Name).ToArray();
Assert.Equal(new[] { nameof(AuditEvent.EventId), nameof(AuditEvent.OccurredAtUtc) }, pkPropertyNames);
}
[Fact]
public void Configure_DeclaresUniqueIndex_OnEventIdAlone_ForIdempotencyLookups()
{
// EventId remains globally unique (the idempotency key for
// InsertIfNotExistsAsync, per M1-T8) via a dedicated unique index that
// is independent of the composite PK.
var entity = _context.Model.FindEntityType(typeof(AuditEvent));
Assert.NotNull(entity);
var eventIdIndex = entity!.GetIndexes()
.SingleOrDefault(i => i.GetDatabaseName() == "UX_AuditLog_EventId");
Assert.NotNull(eventIdIndex);
Assert.True(eventIdIndex!.IsUnique);
var indexedProperty = Assert.Single(eventIdIndex.Properties);
Assert.Equal(nameof(AuditEvent.EventId), indexedProperty.Name);
}
[Fact]
@@ -56,7 +79,7 @@ public class AuditLogEntityTypeConfigurationTests : IDisposable
}
[Fact]
public void Configure_FiveExpectedIndexes_WithCorrectNames()
public void Configure_ExpectedIndexes_WithCorrectNames()
{
var entity = _context.Model.FindEntityType(typeof(AuditEvent));
Assert.NotNull(entity);
@@ -66,6 +89,8 @@ public class AuditLogEntityTypeConfigurationTests : IDisposable
.OrderBy(n => n, StringComparer.Ordinal)
.ToList();
// Five reconciliation/query indexes from alog.md §4, plus the EventId unique
// index introduced alongside the composite PK (Bundle C).
var expected = new[]
{
"IX_AuditLog_Channel_Status_Occurred",
@@ -73,6 +98,7 @@ public class AuditLogEntityTypeConfigurationTests : IDisposable
"IX_AuditLog_OccurredAtUtc",
"IX_AuditLog_Site_Occurred",
"IX_AuditLog_Target_Occurred",
"UX_AuditLog_EventId",
};
Assert.Equal(expected, indexNames);