feat(configdb): ReviewRemediationIndexes migration — AuditLog (Kind, Actor) + AuditLogEntries composite indexes
AuditLog indexes are raw SQL ON ps_AuditLog_Month so they stay partition-aligned for the switch purge.
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
-- ReviewRemediationIndexes (2026-10-22). Run with QUOTED_IDENTIFIER ON (sqlcmd -I, or keep
|
||||
-- this header): AuditLog has persisted computed columns / filtered indexes and SQL Server
|
||||
-- raises error 1934 otherwise.
|
||||
-- AuditLog is monthly-partitioned; the two AuditLog indexes are created ON
|
||||
-- ps_AuditLog_Month(OccurredAtUtc) so they stay partition-aligned and the partition-switch
|
||||
-- purge keeps working. Building them scans the whole AuditLog: on a large production table
|
||||
-- run in a quiet window, and on Enterprise edition add WITH (ONLINE = ON) to the two
|
||||
-- AuditLog CREATE INDEX statements (and to the AuditLogEntries ones if that table is large).
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
SET ANSI_NULLS ON;
|
||||
GO
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
IF NOT EXISTS (
|
||||
SELECT * FROM [__EFMigrationsHistory]
|
||||
WHERE [MigrationId] = N'20261022133907_ReviewRemediationIndexes'
|
||||
)
|
||||
BEGIN
|
||||
CREATE INDEX [IX_AuditLogEntries_EntityType_Timestamp] ON [AuditLogEntries] ([EntityType], [Timestamp] DESC);
|
||||
END;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT * FROM [__EFMigrationsHistory]
|
||||
WHERE [MigrationId] = N'20261022133907_ReviewRemediationIndexes'
|
||||
)
|
||||
BEGIN
|
||||
CREATE INDEX [IX_AuditLogEntries_Timestamp_Id] ON [AuditLogEntries] ([Timestamp] DESC, [Id] DESC);
|
||||
END;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT * FROM [__EFMigrationsHistory]
|
||||
WHERE [MigrationId] = N'20261022133907_ReviewRemediationIndexes'
|
||||
)
|
||||
BEGIN
|
||||
CREATE INDEX [IX_AuditLogEntries_User_Timestamp] ON [AuditLogEntries] ([User], [Timestamp] DESC);
|
||||
END;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT * FROM [__EFMigrationsHistory]
|
||||
WHERE [MigrationId] = N'20261022133907_ReviewRemediationIndexes'
|
||||
)
|
||||
BEGIN
|
||||
|
||||
CREATE NONCLUSTERED INDEX IX_AuditLog_Kind_Occurred
|
||||
ON dbo.AuditLog (Kind, OccurredAtUtc DESC)
|
||||
ON ps_AuditLog_Month(OccurredAtUtc);
|
||||
END;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT * FROM [__EFMigrationsHistory]
|
||||
WHERE [MigrationId] = N'20261022133907_ReviewRemediationIndexes'
|
||||
)
|
||||
BEGIN
|
||||
|
||||
CREATE NONCLUSTERED INDEX IX_AuditLog_Actor_Occurred
|
||||
ON dbo.AuditLog (Actor, OccurredAtUtc DESC)
|
||||
ON ps_AuditLog_Month(OccurredAtUtc);
|
||||
END;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT * FROM [__EFMigrationsHistory]
|
||||
WHERE [MigrationId] = N'20261022133907_ReviewRemediationIndexes'
|
||||
)
|
||||
BEGIN
|
||||
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
|
||||
VALUES (N'20261022133907_ReviewRemediationIndexes', N'10.0.7');
|
||||
END;
|
||||
|
||||
COMMIT;
|
||||
GO
|
||||
|
||||
@@ -40,6 +40,19 @@ public class AuditLogEntryConfiguration : IEntityTypeConfiguration<AuditLogEntry
|
||||
builder.HasIndex(a => a.EntityType);
|
||||
builder.HasIndex(a => a.EntityId);
|
||||
builder.HasIndex(a => a.Action);
|
||||
// Review-remediation composite indexes (ReviewRemediationIndexes). The single-column
|
||||
// Timestamp/User/EntityType indexes above are kept: (Timestamp DESC, Id DESC) only
|
||||
// subsumes Timestamp conceptually, and dropping existing indexes is deliberately
|
||||
// left to a later, measured change.
|
||||
builder.HasIndex(a => new { a.EntityType, a.Timestamp })
|
||||
.IsDescending(false, true)
|
||||
.HasDatabaseName("IX_AuditLogEntries_EntityType_Timestamp");
|
||||
builder.HasIndex(a => new { a.User, a.Timestamp })
|
||||
.IsDescending(false, true)
|
||||
.HasDatabaseName("IX_AuditLogEntries_User_Timestamp");
|
||||
builder.HasIndex(a => new { a.Timestamp, a.Id })
|
||||
.IsDescending(true, true)
|
||||
.HasDatabaseName("IX_AuditLogEntries_Timestamp_Id");
|
||||
builder.HasIndex(a => a.BundleImportId).HasDatabaseName("IX_AuditLogEntries_BundleImportId");
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -229,5 +229,17 @@ public class AuditLogEntityTypeConfiguration : IEntityTypeConfiguration<AuditLog
|
||||
.IsDescending(false, true)
|
||||
.HasFilter("[Target] IS NOT NULL")
|
||||
.HasDatabaseName("IX_AuditLog_Target_Occurred");
|
||||
|
||||
// Review-remediation indexes (ReviewRemediationIndexes migration): the Audit
|
||||
// Log page's Kind and Actor filters ordered newest-first. Kind is a persisted
|
||||
// computed column (indexable). Created via raw SQL ON ps_AuditLog_Month so they
|
||||
// stay partition-aligned for the partition-switch purge.
|
||||
builder.HasIndex(e => new { e.Kind, e.OccurredAtUtc })
|
||||
.IsDescending(false, true)
|
||||
.HasDatabaseName("IX_AuditLog_Kind_Occurred");
|
||||
|
||||
builder.HasIndex(e => new { e.Actor, e.OccurredAtUtc })
|
||||
.IsDescending(false, true)
|
||||
.HasDatabaseName("IX_AuditLog_Actor_Occurred");
|
||||
}
|
||||
}
|
||||
|
||||
+2169
File diff suppressed because it is too large
Load Diff
+72
@@ -0,0 +1,72 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||
{
|
||||
/// <summary>Review-remediation composite indexes on AuditLog (partition-aligned, raw SQL) and AuditLogEntries.</summary>
|
||||
public partial class ReviewRemediationIndexes : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AuditLogEntries_EntityType_Timestamp",
|
||||
table: "AuditLogEntries",
|
||||
columns: new[] { "EntityType", "Timestamp" },
|
||||
descending: new[] { false, true });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AuditLogEntries_Timestamp_Id",
|
||||
table: "AuditLogEntries",
|
||||
columns: new[] { "Timestamp", "Id" },
|
||||
descending: new bool[0]);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AuditLogEntries_User_Timestamp",
|
||||
table: "AuditLogEntries",
|
||||
columns: new[] { "User", "Timestamp" },
|
||||
descending: new[] { false, true });
|
||||
|
||||
// AuditLog is monthly-partitioned (ps_AuditLog_Month) with a partition-switch
|
||||
// purge, so these two indexes MUST be partition-aligned. EF's CreateIndex cannot
|
||||
// express the ON ps_AuditLog_Month(OccurredAtUtc) clause -> raw SQL, exactly as
|
||||
// IX_AuditLog_Node_Occurred (AddAuditLogSourceNode). Kind is a PERSISTED computed
|
||||
// column, so it is indexable. On a large production AuditLog consider adding
|
||||
// WITH (ONLINE = ON) (Enterprise edition) to the generated script.
|
||||
migrationBuilder.Sql(@"
|
||||
CREATE NONCLUSTERED INDEX IX_AuditLog_Kind_Occurred
|
||||
ON dbo.AuditLog (Kind, OccurredAtUtc DESC)
|
||||
ON ps_AuditLog_Month(OccurredAtUtc);");
|
||||
|
||||
migrationBuilder.Sql(@"
|
||||
CREATE NONCLUSTERED INDEX IX_AuditLog_Actor_Occurred
|
||||
ON dbo.AuditLog (Actor, OccurredAtUtc DESC)
|
||||
ON ps_AuditLog_Month(OccurredAtUtc);");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_AuditLogEntries_EntityType_Timestamp",
|
||||
table: "AuditLogEntries");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_AuditLogEntries_Timestamp_Id",
|
||||
table: "AuditLogEntries");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_AuditLogEntries_User_Timestamp",
|
||||
table: "AuditLogEntries");
|
||||
|
||||
migrationBuilder.Sql(@"
|
||||
IF EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_AuditLog_Actor_Occurred' AND object_id = OBJECT_ID('dbo.AuditLog'))
|
||||
DROP INDEX IX_AuditLog_Actor_Occurred ON dbo.AuditLog;");
|
||||
|
||||
migrationBuilder.Sql(@"
|
||||
IF EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_AuditLog_Kind_Occurred' AND object_id = OBJECT_ID('dbo.AuditLog'))
|
||||
DROP INDEX IX_AuditLog_Kind_Occurred ON dbo.AuditLog;");
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
@@ -98,6 +98,18 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||
|
||||
b.HasIndex("User");
|
||||
|
||||
b.HasIndex("EntityType", "Timestamp")
|
||||
.IsDescending(false, true)
|
||||
.HasDatabaseName("IX_AuditLogEntries_EntityType_Timestamp");
|
||||
|
||||
b.HasIndex("Timestamp", "Id")
|
||||
.IsDescending()
|
||||
.HasDatabaseName("IX_AuditLogEntries_Timestamp_Id");
|
||||
|
||||
b.HasIndex("User", "Timestamp")
|
||||
.IsDescending(false, true)
|
||||
.HasDatabaseName("IX_AuditLogEntries_User_Timestamp");
|
||||
|
||||
b.ToTable("AuditLogEntries");
|
||||
});
|
||||
|
||||
@@ -1871,6 +1883,14 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||
b.HasIndex("ParentExecutionId")
|
||||
.HasDatabaseName("IX_AuditLog_ParentExecution");
|
||||
|
||||
b.HasIndex("Actor", "OccurredAtUtc")
|
||||
.IsDescending(false, true)
|
||||
.HasDatabaseName("IX_AuditLog_Actor_Occurred");
|
||||
|
||||
b.HasIndex("Kind", "OccurredAtUtc")
|
||||
.IsDescending(false, true)
|
||||
.HasDatabaseName("IX_AuditLog_Kind_Occurred");
|
||||
|
||||
b.HasIndex("SourceNode", "OccurredAtUtc")
|
||||
.HasDatabaseName("IX_AuditLog_Node_Occurred");
|
||||
|
||||
|
||||
+2
@@ -176,9 +176,11 @@ public class AuditLogEntityTypeConfigurationTests : IDisposable
|
||||
// the canonical/computed shape but the discoverable names do not change).
|
||||
var expected = new[]
|
||||
{
|
||||
"IX_AuditLog_Actor_Occurred",
|
||||
"IX_AuditLog_Channel_Status_Occurred",
|
||||
"IX_AuditLog_CorrelationId",
|
||||
"IX_AuditLog_Execution",
|
||||
"IX_AuditLog_Kind_Occurred",
|
||||
"IX_AuditLog_Node_Occurred",
|
||||
"IX_AuditLog_OccurredAtUtc",
|
||||
"IX_AuditLog_ParentExecution",
|
||||
|
||||
Reference in New Issue
Block a user