From 8fb9eb0ce7b528e39d688ec3257155086138b572 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 23 May 2026 16:45:19 -0400 Subject: [PATCH] chore(db): align SourceNode unicode metadata + document partition-aligned index recipe Tidies flagged by code review on the T6/T7/T8 migration bundle: - Add `.IsUnicode(false)` to the three SourceNode EF property mappings to match every other ASCII varchar column on the same entities. Physical column was already `varchar(64)` because `HasColumnType` wins, but the EF model metadata flag was inconsistent. - Add `unicode: false` to the three AddColumn calls in the migrations + their Designer snapshots so the historical snapshots match the model. - Update the model snapshot to carry IsUnicode(false) on each SourceNode entry. - Document the SELECT-list invariant on SiteCallAuditRepository.QueryAsync: EF Core's FromSqlInterpolated requires every entity-tracked column in the result set, so future SiteCall columns must extend the list too. - Amend plan Task 6 Step 2 to document the partition-aligned raw-SQL index recipe and the staging-table sync requirement. --- docs/plans/2026-05-23-audit-source-node.md | 19 ++++++++++++++----- .../AuditLogEntityTypeConfiguration.cs | 3 ++- .../NotificationOutboxConfiguration.cs | 3 ++- .../SiteCallEntityTypeConfiguration.cs | 3 ++- ...23201754_AddAuditLogSourceNode.Designer.cs | 1 + .../20260523201754_AddAuditLogSourceNode.cs | 1 + ...1950_AddNotificationSourceNode.Designer.cs | 2 ++ ...0260523201950_AddNotificationSourceNode.cs | 1 + ...23202131_AddSiteCallSourceNode.Designer.cs | 3 +++ .../20260523202131_AddSiteCallSourceNode.cs | 1 + .../ScadaLinkDbContextModelSnapshot.cs | 3 +++ .../Repositories/SiteCallAuditRepository.cs | 5 +++++ 12 files changed, 37 insertions(+), 8 deletions(-) diff --git a/docs/plans/2026-05-23-audit-source-node.md b/docs/plans/2026-05-23-audit-source-node.md index 4c6e278..bd21313 100644 --- a/docs/plans/2026-05-23-audit-source-node.md +++ b/docs/plans/2026-05-23-audit-source-node.md @@ -469,13 +469,22 @@ migrationBuilder.AddColumn( maxLength: 64, nullable: true); -migrationBuilder.CreateIndex( - name: "IX_AuditLog_Node_Occurred", - table: "AuditLog", - columns: new[] { "SourceNode", "OccurredAtUtc" }); +// IMPORTANT: AuditLog is partitioned on ps_AuditLog_Month(OccurredAtUtc). +// `migrationBuilder.CreateIndex(...)` lands the index on [PRIMARY], which breaks +// `ALTER TABLE … SWITCH PARTITION` (the purge mechanism). Match the pattern used +// by the other `IX_AuditLog_*` indexes (see 20260520142214_AddAuditLogTable.cs +// and 20260521184044_AddAuditLogExecutionId.cs) — raw SQL with the partition +// scheme spelled out. Keep the fluent `HasIndex(...).HasDatabaseName(...)` in +// the EF configuration so the model snapshot stays in sync. +migrationBuilder.Sql(@" +CREATE NONCLUSTERED INDEX IX_AuditLog_Node_Occurred +ON dbo.AuditLog (SourceNode, OccurredAtUtc) +ON ps_AuditLog_Month(OccurredAtUtc);"); ``` -`Down()` drops the index then the column. +`Down()` drops the index (`IF EXISTS DROP INDEX … ON dbo.AuditLog`, raw SQL) then the column. + +You will *also* need to extend `AuditLogRepository.SwitchOutPartitionAsync`'s staging-table CREATE to include `SourceNode varchar(64) NULL` in the final ordinal position. `SWITCH PARTITION` rejects schema mismatches between live and staging — without this, the PartitionPurge integration tests fail. **Step 3: Update EF configuration** diff --git a/src/ScadaLink.ConfigurationDatabase/Configurations/AuditLogEntityTypeConfiguration.cs b/src/ScadaLink.ConfigurationDatabase/Configurations/AuditLogEntityTypeConfiguration.cs index 56bff18..e092b65 100644 --- a/src/ScadaLink.ConfigurationDatabase/Configurations/AuditLogEntityTypeConfiguration.cs +++ b/src/ScadaLink.ConfigurationDatabase/Configurations/AuditLogEntityTypeConfiguration.cs @@ -76,7 +76,8 @@ public class AuditLogEntityTypeConfiguration : IEntityTypeConfiguration e.SourceNode) .HasColumnType("varchar(64)") - .HasMaxLength(64); + .HasMaxLength(64) + .IsUnicode(false); // Bounded unicode message column. builder.Property(e => e.ErrorMessage) diff --git a/src/ScadaLink.ConfigurationDatabase/Configurations/NotificationOutboxConfiguration.cs b/src/ScadaLink.ConfigurationDatabase/Configurations/NotificationOutboxConfiguration.cs index c8872a3..6c2c693 100644 --- a/src/ScadaLink.ConfigurationDatabase/Configurations/NotificationOutboxConfiguration.cs +++ b/src/ScadaLink.ConfigurationDatabase/Configurations/NotificationOutboxConfiguration.cs @@ -54,7 +54,8 @@ public class NotificationOutboxConfiguration : IEntityTypeConfiguration n.SourceNode) .HasColumnType("varchar(64)") - .HasMaxLength(64); + .HasMaxLength(64) + .IsUnicode(false); // OriginExecutionId (Audit Log #23): nullable uniqueidentifier carried from the // site so the dispatcher can echo it onto NotifyDeliver audit rows. No index — diff --git a/src/ScadaLink.ConfigurationDatabase/Configurations/SiteCallEntityTypeConfiguration.cs b/src/ScadaLink.ConfigurationDatabase/Configurations/SiteCallEntityTypeConfiguration.cs index 206ad6e..a0203ab 100644 --- a/src/ScadaLink.ConfigurationDatabase/Configurations/SiteCallEntityTypeConfiguration.cs +++ b/src/ScadaLink.ConfigurationDatabase/Configurations/SiteCallEntityTypeConfiguration.cs @@ -66,7 +66,8 @@ public class SiteCallEntityTypeConfiguration : IEntityTypeConfiguration s.SourceNode) .HasColumnType("varchar(64)") - .HasMaxLength(64); + .HasMaxLength(64) + .IsUnicode(false); // Indexes — names locked for reconciliation/migration discoverability. // Source_Created backs "calls from this site" (Central UI Site Calls page, diff --git a/src/ScadaLink.ConfigurationDatabase/Migrations/20260523201754_AddAuditLogSourceNode.Designer.cs b/src/ScadaLink.ConfigurationDatabase/Migrations/20260523201754_AddAuditLogSourceNode.Designer.cs index 860a383..af73714 100644 --- a/src/ScadaLink.ConfigurationDatabase/Migrations/20260523201754_AddAuditLogSourceNode.Designer.cs +++ b/src/ScadaLink.ConfigurationDatabase/Migrations/20260523201754_AddAuditLogSourceNode.Designer.cs @@ -118,6 +118,7 @@ namespace ScadaLink.ConfigurationDatabase.Migrations b.Property("SourceNode") .HasMaxLength(64) + .IsUnicode(false) .HasColumnType("varchar(64)"); b.Property("SourceScript") diff --git a/src/ScadaLink.ConfigurationDatabase/Migrations/20260523201754_AddAuditLogSourceNode.cs b/src/ScadaLink.ConfigurationDatabase/Migrations/20260523201754_AddAuditLogSourceNode.cs index c6a7fd5..0499ece 100644 --- a/src/ScadaLink.ConfigurationDatabase/Migrations/20260523201754_AddAuditLogSourceNode.cs +++ b/src/ScadaLink.ConfigurationDatabase/Migrations/20260523201754_AddAuditLogSourceNode.cs @@ -30,6 +30,7 @@ namespace ScadaLink.ConfigurationDatabase.Migrations name: "SourceNode", table: "AuditLog", type: "varchar(64)", + unicode: false, maxLength: 64, nullable: true); diff --git a/src/ScadaLink.ConfigurationDatabase/Migrations/20260523201950_AddNotificationSourceNode.Designer.cs b/src/ScadaLink.ConfigurationDatabase/Migrations/20260523201950_AddNotificationSourceNode.Designer.cs index 0772cf8..d4ea3b8 100644 --- a/src/ScadaLink.ConfigurationDatabase/Migrations/20260523201950_AddNotificationSourceNode.Designer.cs +++ b/src/ScadaLink.ConfigurationDatabase/Migrations/20260523201950_AddNotificationSourceNode.Designer.cs @@ -118,6 +118,7 @@ namespace ScadaLink.ConfigurationDatabase.Migrations b.Property("SourceNode") .HasMaxLength(64) + .IsUnicode(false) .HasColumnType("varchar(64)"); b.Property("SourceScript") @@ -825,6 +826,7 @@ namespace ScadaLink.ConfigurationDatabase.Migrations b.Property("SourceNode") .HasMaxLength(64) + .IsUnicode(false) .HasColumnType("varchar(64)"); b.Property("SourceScript") diff --git a/src/ScadaLink.ConfigurationDatabase/Migrations/20260523201950_AddNotificationSourceNode.cs b/src/ScadaLink.ConfigurationDatabase/Migrations/20260523201950_AddNotificationSourceNode.cs index 722329b..2510ff6 100644 --- a/src/ScadaLink.ConfigurationDatabase/Migrations/20260523201950_AddNotificationSourceNode.cs +++ b/src/ScadaLink.ConfigurationDatabase/Migrations/20260523201950_AddNotificationSourceNode.cs @@ -26,6 +26,7 @@ namespace ScadaLink.ConfigurationDatabase.Migrations name: "SourceNode", table: "Notifications", type: "varchar(64)", + unicode: false, maxLength: 64, nullable: true); } diff --git a/src/ScadaLink.ConfigurationDatabase/Migrations/20260523202131_AddSiteCallSourceNode.Designer.cs b/src/ScadaLink.ConfigurationDatabase/Migrations/20260523202131_AddSiteCallSourceNode.Designer.cs index 4a5bbae..fe3c141 100644 --- a/src/ScadaLink.ConfigurationDatabase/Migrations/20260523202131_AddSiteCallSourceNode.Designer.cs +++ b/src/ScadaLink.ConfigurationDatabase/Migrations/20260523202131_AddSiteCallSourceNode.Designer.cs @@ -118,6 +118,7 @@ namespace ScadaLink.ConfigurationDatabase.Migrations b.Property("SourceNode") .HasMaxLength(64) + .IsUnicode(false) .HasColumnType("varchar(64)"); b.Property("SourceScript") @@ -267,6 +268,7 @@ namespace ScadaLink.ConfigurationDatabase.Migrations b.Property("SourceNode") .HasMaxLength(64) + .IsUnicode(false) .HasColumnType("varchar(64)"); b.Property("SourceSite") @@ -829,6 +831,7 @@ namespace ScadaLink.ConfigurationDatabase.Migrations b.Property("SourceNode") .HasMaxLength(64) + .IsUnicode(false) .HasColumnType("varchar(64)"); b.Property("SourceScript") diff --git a/src/ScadaLink.ConfigurationDatabase/Migrations/20260523202131_AddSiteCallSourceNode.cs b/src/ScadaLink.ConfigurationDatabase/Migrations/20260523202131_AddSiteCallSourceNode.cs index 8db59c3..9dd92c2 100644 --- a/src/ScadaLink.ConfigurationDatabase/Migrations/20260523202131_AddSiteCallSourceNode.cs +++ b/src/ScadaLink.ConfigurationDatabase/Migrations/20260523202131_AddSiteCallSourceNode.cs @@ -27,6 +27,7 @@ namespace ScadaLink.ConfigurationDatabase.Migrations name: "SourceNode", table: "SiteCalls", type: "varchar(64)", + unicode: false, maxLength: 64, nullable: true); } diff --git a/src/ScadaLink.ConfigurationDatabase/Migrations/ScadaLinkDbContextModelSnapshot.cs b/src/ScadaLink.ConfigurationDatabase/Migrations/ScadaLinkDbContextModelSnapshot.cs index 0ff9638..d78df93 100644 --- a/src/ScadaLink.ConfigurationDatabase/Migrations/ScadaLinkDbContextModelSnapshot.cs +++ b/src/ScadaLink.ConfigurationDatabase/Migrations/ScadaLinkDbContextModelSnapshot.cs @@ -115,6 +115,7 @@ namespace ScadaLink.ConfigurationDatabase.Migrations b.Property("SourceNode") .HasMaxLength(64) + .IsUnicode(false) .HasColumnType("varchar(64)"); b.Property("SourceScript") @@ -264,6 +265,7 @@ namespace ScadaLink.ConfigurationDatabase.Migrations b.Property("SourceNode") .HasMaxLength(64) + .IsUnicode(false) .HasColumnType("varchar(64)"); b.Property("SourceSite") @@ -826,6 +828,7 @@ namespace ScadaLink.ConfigurationDatabase.Migrations b.Property("SourceNode") .HasMaxLength(64) + .IsUnicode(false) .HasColumnType("varchar(64)"); b.Property("SourceScript") diff --git a/src/ScadaLink.ConfigurationDatabase/Repositories/SiteCallAuditRepository.cs b/src/ScadaLink.ConfigurationDatabase/Repositories/SiteCallAuditRepository.cs index cfe0478..e14d46a 100644 --- a/src/ScadaLink.ConfigurationDatabase/Repositories/SiteCallAuditRepository.cs +++ b/src/ScadaLink.ConfigurationDatabase/Repositories/SiteCallAuditRepository.cs @@ -171,6 +171,11 @@ WHERE TrackedOperationId = {idText} // and compose with the keyset cursor, so a StuckOnly page is honest: // never under-filled with a non-null next cursor. Mirrors how // NotificationOutboxRepository.QueryAsync applies NotificationOutboxFilter.StuckCutoff. + // + // SELECT-list maintenance: EF Core's FromSqlInterpolated requires every + // entity-tracked column to appear in the result set. Adding a new column + // to the SiteCall entity means extending the list below too — otherwise + // every read trips "The required column 'X' was not present" at runtime. FormattableString sql = $@" SELECT TOP ({paging.PageSize}) TrackedOperationId, Channel, Target, SourceSite, SourceNode, Status, RetryCount,