feat(audit): flip AuditLog clustered key to (OccurredAtUtc, EventId) (ScadaBridge#111)
Variant B2 of the #78 benchmark: migration FlipAuditLogClusteredKey drops the twelve non-clustered indexes, re-adds PK_AuditLog as (OccurredAtUtc, EventId) on ps_AuditLog_Month, adds the aligned unique UX_AuditLog_EventId_Occurred (EventId, OccurredAtUtc) for the idempotency probe, and rebuilds the twelve indexes unchanged. Guarded (re-runnable) Up, inverse-rebuild Down; production scripts docs/plans/sql/FlipAuditLogClusteredKey{,.rollback}.sql. The partition-switch purge staging table now reads the live clustered key order from sys.index_columns, so it switches either shape (old binaries hard-code the old order: ship with the binaries, both centrals stopped). Evidence: 300k rows, page 1 90,532 -> 34 logical reads (ordered backward clustered scan under Top), keyset page 90,532 -> 46, probe 14 -> 14; FlipAuditLogClusteredKeyMigrationTests pins indexes, grants, Down/Up, the staging key (mutation: hard-coded old key fails SWITCH) and the read ratio. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
+20
-11
@@ -39,9 +39,10 @@ public class AuditLogEntityTypeConfigurationTests : IDisposable
|
||||
[Fact]
|
||||
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.
|
||||
// Composite PK {OccurredAtUtc, EventId} (FlipAuditLogClusteredKey, ScadaBridge#111):
|
||||
// the clustered key must include the partition column (OccurredAtUtc), and leading
|
||||
// with it lets the Audit Log page's newest-first TOP-N / keyset read an ordered range
|
||||
// instead of the whole partitioned table.
|
||||
var entity = _context.Model.FindEntityType(typeof(AuditLogRow));
|
||||
|
||||
Assert.NotNull(entity);
|
||||
@@ -51,12 +52,15 @@ public class AuditLogEntityTypeConfigurationTests : IDisposable
|
||||
Assert.NotNull(pk);
|
||||
|
||||
var pkPropertyNames = pk!.Properties.Select(p => p.Name).ToArray();
|
||||
Assert.Equal(new[] { nameof(AuditLogRow.EventId), nameof(AuditLogRow.OccurredAtUtc) }, pkPropertyNames);
|
||||
Assert.Equal(new[] { nameof(AuditLogRow.OccurredAtUtc), nameof(AuditLogRow.EventId) }, pkPropertyNames);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that no non-aligned EventId index is declared and uniqueness rides the aligned clustered key.</summary>
|
||||
/// <summary>
|
||||
/// Verifies that no single-column (non-aligned) EventId index is declared, and that EventId
|
||||
/// uniqueness rides the aligned unique (EventId, OccurredAtUtc) index.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Configure_DeclaresNoNonAlignedEventIdIndex_UniquenessRidesTheAlignedClusteredKey()
|
||||
public void Configure_DeclaresNoNonAlignedEventIdIndex_UniquenessRidesTheAlignedEventIdIndex()
|
||||
{
|
||||
// AlignAuditLogEventIdUniqueness: the standalone non-aligned
|
||||
// UX_AuditLog_EventId is gone. EventId uniqueness now rides the clustered
|
||||
@@ -69,6 +73,10 @@ public class AuditLogEntityTypeConfigurationTests : IDisposable
|
||||
//
|
||||
// Re-declaring a single-column unique index here would silently reinstate
|
||||
// the SWITCH incompatibility, so this test pins its absence.
|
||||
//
|
||||
// FlipAuditLogClusteredKey (ScadaBridge#111) moved OccurredAtUtc to the front of the
|
||||
// clustered key, so the EventId-leading seek now comes from UX_AuditLog_EventId_Occurred:
|
||||
// unique, and keyed (EventId, OccurredAtUtc) so it stays partition-aligned.
|
||||
var entity = _context.Model.FindEntityType(typeof(AuditLogRow));
|
||||
Assert.NotNull(entity);
|
||||
|
||||
@@ -76,11 +84,11 @@ public class AuditLogEntityTypeConfigurationTests : IDisposable
|
||||
entity!.GetIndexes(),
|
||||
i => i.GetDatabaseName() == "UX_AuditLog_EventId");
|
||||
|
||||
var pk = entity.FindPrimaryKey();
|
||||
Assert.NotNull(pk);
|
||||
var ux = Assert.Single(entity.GetIndexes(), i => i.GetDatabaseName() == "UX_AuditLog_EventId_Occurred");
|
||||
Assert.True(ux.IsUnique);
|
||||
Assert.Equal(
|
||||
new[] { nameof(AuditLogRow.EventId), nameof(AuditLogRow.OccurredAtUtc) },
|
||||
pk!.Properties.Select(p => p.Name).ToArray());
|
||||
ux.Properties.Select(p => p.Name).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the configuration maps the expected number of properties.</summary>
|
||||
@@ -189,8 +197,9 @@ public class AuditLogEntityTypeConfigurationTests : IDisposable
|
||||
"IX_AuditLog_Site_Occurred",
|
||||
"IX_AuditLog_Target_Occurred",
|
||||
"IX_AuditLog_Unchained", // hash chain (ScadaBridge#41)
|
||||
// UX_AuditLog_EventId is intentionally absent — dropped by
|
||||
// AlignAuditLogEventIdUniqueness; the aligned clustered PK carries it.
|
||||
"UX_AuditLog_EventId_Occurred", // FlipAuditLogClusteredKey (ScadaBridge#111)
|
||||
// UX_AuditLog_EventId (single-column, non-aligned) is intentionally absent —
|
||||
// dropped by AlignAuditLogEventIdUniqueness.
|
||||
};
|
||||
|
||||
Assert.Equal(expected, indexNames);
|
||||
|
||||
Reference in New Issue
Block a user