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.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.
|
// Enum-as-string columns: bounded varchar(32) ASCII.
|
||||||
builder.Property(e => e.Channel)
|
builder.Property(e => e.Channel)
|
||||||
|
|||||||
@@ -28,8 +28,11 @@ public class AuditLogEntityTypeConfigurationTests : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[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));
|
var entity = _context.Model.FindEntityType(typeof(AuditEvent));
|
||||||
|
|
||||||
Assert.NotNull(entity);
|
Assert.NotNull(entity);
|
||||||
@@ -37,8 +40,28 @@ public class AuditLogEntityTypeConfigurationTests : IDisposable
|
|||||||
|
|
||||||
var pk = entity.FindPrimaryKey();
|
var pk = entity.FindPrimaryKey();
|
||||||
Assert.NotNull(pk);
|
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]
|
[Fact]
|
||||||
@@ -56,7 +79,7 @@ public class AuditLogEntityTypeConfigurationTests : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Configure_FiveExpectedIndexes_WithCorrectNames()
|
public void Configure_ExpectedIndexes_WithCorrectNames()
|
||||||
{
|
{
|
||||||
var entity = _context.Model.FindEntityType(typeof(AuditEvent));
|
var entity = _context.Model.FindEntityType(typeof(AuditEvent));
|
||||||
Assert.NotNull(entity);
|
Assert.NotNull(entity);
|
||||||
@@ -66,6 +89,8 @@ public class AuditLogEntityTypeConfigurationTests : IDisposable
|
|||||||
.OrderBy(n => n, StringComparer.Ordinal)
|
.OrderBy(n => n, StringComparer.Ordinal)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
// Five reconciliation/query indexes from alog.md §4, plus the EventId unique
|
||||||
|
// index introduced alongside the composite PK (Bundle C).
|
||||||
var expected = new[]
|
var expected = new[]
|
||||||
{
|
{
|
||||||
"IX_AuditLog_Channel_Status_Occurred",
|
"IX_AuditLog_Channel_Status_Occurred",
|
||||||
@@ -73,6 +98,7 @@ public class AuditLogEntityTypeConfigurationTests : IDisposable
|
|||||||
"IX_AuditLog_OccurredAtUtc",
|
"IX_AuditLog_OccurredAtUtc",
|
||||||
"IX_AuditLog_Site_Occurred",
|
"IX_AuditLog_Site_Occurred",
|
||||||
"IX_AuditLog_Target_Occurred",
|
"IX_AuditLog_Target_Occurred",
|
||||||
|
"UX_AuditLog_EventId",
|
||||||
};
|
};
|
||||||
|
|
||||||
Assert.Equal(expected, indexNames);
|
Assert.Equal(expected, indexNames);
|
||||||
|
|||||||
Reference in New Issue
Block a user