94 lines
5.2 KiB
C#
94 lines
5.2 KiB
C#
using Microsoft.EntityFrameworkCore.Migrations;
|
|
|
|
#nullable disable
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
|
{
|
|
/// <summary>
|
|
/// Makes <c>dbo.AuditLog</c>'s EventId uniqueness <b>partition-aligned</b> by
|
|
/// dropping the non-aligned <c>UX_AuditLog_EventId</c> and leaving the clustered
|
|
/// <c>PK_AuditLog (EventId, OccurredAtUtc)</c> — already aligned on
|
|
/// <c>ps_AuditLog_Month(OccurredAtUtc)</c> — as the sole enforcement.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <b>Why.</b> <c>ALTER TABLE … SWITCH PARTITION</c> refuses to run while a
|
|
/// non-aligned index exists on the table, so the monthly retention purge
|
|
/// (<c>AuditLogRepository.SwitchOutPartitionAsync</c>) had to DROP
|
|
/// <c>UX_AuditLog_EventId</c>, switch, and then CREATE it again — an OFFLINE
|
|
/// whole-table unique-index build, inside the switch transaction, blocking every
|
|
/// audit writer for its duration. It also opened a window in which the index that
|
|
/// backs ingest idempotency did not exist at all, and a mid-dance failure could
|
|
/// leave the live table without it until a later tick's CATCH branch repaired it.
|
|
/// With alignment there is nothing to drop, so the switch is metadata-only and the
|
|
/// purge stops competing with ingest.
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>Why dropping it is safe — EventId is globally unique by construction.</b>
|
|
/// The composite key enforces uniqueness of the PAIR, not of EventId alone, so in
|
|
/// principle the same EventId could now be stored twice under two different
|
|
/// <c>OccurredAtUtc</c> values (in two different partitions). That cannot happen
|
|
/// here: <c>EventId</c> is a GUID minted ONCE at the emitting site, in the same
|
|
/// operation that stamps <c>OccurredAtUtc</c>, and both travel together verbatim
|
|
/// through telemetry and reconciliation — nothing downstream re-stamps either
|
|
/// field. A given EventId therefore always arrives with the same OccurredAtUtc and
|
|
/// can only ever map to one partition, which makes pair-uniqueness equivalent to
|
|
/// EventId-uniqueness for every row this system produces. GUID collision across
|
|
/// partitions is not a real risk.
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>The idempotency probe still seeks.</b> Both ingest forms test
|
|
/// <c>WHERE EventId = @id</c>, which is the LEADING column of the clustered PK, so
|
|
/// the probe remains an index seek. The cost changes shape rather than order: it
|
|
/// becomes one seek per partition (the partition column is not in the predicate, so
|
|
/// SQL Server cannot eliminate partitions) instead of a single seek on a
|
|
/// non-partitioned index. Against a monthly scheme that is a couple of dozen
|
|
/// shallow B-tree seeks — cheap, and paid on a path that now issues one statement
|
|
/// per telemetry packet rather than one per row.
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>Edition note.</b> The alternative remedy — keeping the non-aligned index and
|
|
/// rebuilding it with <c>ONLINE = ON</c> outside the switch transaction — requires
|
|
/// Enterprise (or Azure SQL / Developer) edition; online index rebuild is not
|
|
/// available on Standard, which this deployment does not guarantee. Alignment
|
|
/// needs no edition-specific feature and removes the rebuild entirely, so it is
|
|
/// preferred regardless of edition.
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>Down is a faithful reverse</b> and recreates the index on <c>[PRIMARY]</c>
|
|
/// exactly as <c>CollapseAuditLogToCanonical</c> created it. Reverting also
|
|
/// reinstates the SWITCH incompatibility, so the purge's guarded defensive
|
|
/// <c>DROP INDEX</c> (retained in <c>SwitchOutPartitionAsync</c> for databases
|
|
/// restored from pre-alignment backups) would remove it again on the next purge.
|
|
/// The partition function/scheme (<c>pf_AuditLog_Month</c> /
|
|
/// <c>ps_AuditLog_Month</c>) and every aligned index are untouched by both
|
|
/// directions.
|
|
/// </para>
|
|
/// </remarks>
|
|
public partial class AlignAuditLogEventIdUniqueness : Migration
|
|
{
|
|
/// <inheritdoc />
|
|
protected override void Up(MigrationBuilder migrationBuilder)
|
|
{
|
|
// Raw, existence-guarded SQL rather than the scaffolded DropIndex: the
|
|
// AuditLog table is raw-SQL managed (partition scheme, persisted computed
|
|
// columns, append-only role grants), so its migrations stay explicit and
|
|
// re-runnable. The guard also lets this apply cleanly to a database whose
|
|
// index was already removed by the purge path's defensive cleanup.
|
|
migrationBuilder.Sql(@"
|
|
IF EXISTS (SELECT 1 FROM sys.indexes
|
|
WHERE name = 'UX_AuditLog_EventId' AND object_id = OBJECT_ID('dbo.AuditLog'))
|
|
DROP INDEX UX_AuditLog_EventId ON dbo.AuditLog;");
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override void Down(MigrationBuilder migrationBuilder)
|
|
{
|
|
migrationBuilder.Sql(@"
|
|
IF NOT EXISTS (SELECT 1 FROM sys.indexes
|
|
WHERE name = 'UX_AuditLog_EventId' AND object_id = OBJECT_ID('dbo.AuditLog'))
|
|
CREATE UNIQUE NONCLUSTERED INDEX UX_AuditLog_EventId ON dbo.AuditLog (EventId) ON [PRIMARY];");
|
|
}
|
|
}
|
|
}
|