using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
{
///
/// Makes dbo.AuditLog's EventId uniqueness partition-aligned by
/// dropping the non-aligned UX_AuditLog_EventId and leaving the clustered
/// PK_AuditLog (EventId, OccurredAtUtc) — already aligned on
/// ps_AuditLog_Month(OccurredAtUtc) — as the sole enforcement.
///
///
///
/// Why. ALTER TABLE … SWITCH PARTITION refuses to run while a
/// non-aligned index exists on the table, so the monthly retention purge
/// (AuditLogRepository.SwitchOutPartitionAsync) had to DROP
/// UX_AuditLog_EventId, 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.
///
///
/// Why dropping it is safe — EventId is globally unique by construction.
/// 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
/// OccurredAtUtc values (in two different partitions). That cannot happen
/// here: EventId is a GUID minted ONCE at the emitting site, in the same
/// operation that stamps OccurredAtUtc, 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.
///
///
/// The idempotency probe still seeks. Both ingest forms test
/// WHERE EventId = @id, 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.
///
///
/// Edition note. The alternative remedy — keeping the non-aligned index and
/// rebuilding it with ONLINE = ON 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.
///
///
/// Down is a faithful reverse and recreates the index on [PRIMARY]
/// exactly as CollapseAuditLogToCanonical created it. Reverting also
/// reinstates the SWITCH incompatibility, so the purge's guarded defensive
/// DROP INDEX (retained in SwitchOutPartitionAsync for databases
/// restored from pre-alignment backups) would remove it again on the next purge.
/// The partition function/scheme (pf_AuditLog_Month /
/// ps_AuditLog_Month) and every aligned index are untouched by both
/// directions.
///
///
public partial class AlignAuditLogEventIdUniqueness : Migration
{
///
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;");
}
///
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];");
}
}
}