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:
@@ -15,7 +15,15 @@ public class AuditLogEntityTypeConfiguration : IEntityTypeConfiguration<AuditEve
|
||||
{
|
||||
builder.ToTable("AuditLog");
|
||||
|
||||
builder.HasKey(e => e.EventId);
|
||||
// Composite PK includes OccurredAtUtc — required by the monthly partition scheme
|
||||
// (ps_AuditLog_Month) so the clustered key is partition-aligned. EventId still
|
||||
// needs to be globally unique for InsertIfNotExistsAsync idempotency, so a
|
||||
// separate unique index is declared on EventId alone.
|
||||
builder.HasKey(e => new { e.EventId, e.OccurredAtUtc });
|
||||
|
||||
builder.HasIndex(e => e.EventId)
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_AuditLog_EventId");
|
||||
|
||||
// Enum-as-string columns: bounded varchar(32) ASCII.
|
||||
builder.Property(e => e.Channel)
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user