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<string> 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.
This commit is contained in:
Joseph Doherty
2026-05-23 16:45:19 -04:00
parent 1a77bc5f38
commit 8fb9eb0ce7
12 changed files with 37 additions and 8 deletions

View File

@@ -469,13 +469,22 @@ migrationBuilder.AddColumn<string>(
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**

View File

@@ -76,7 +76,8 @@ public class AuditLogEntityTypeConfiguration : IEntityTypeConfiguration<AuditEve
// produced before this feature shipped. ASCII — varchar(64), no unicode.
builder.Property(e => e.SourceNode)
.HasColumnType("varchar(64)")
.HasMaxLength(64);
.HasMaxLength(64)
.IsUnicode(false);
// Bounded unicode message column.
builder.Property(e => e.ErrorMessage)

View File

@@ -54,7 +54,8 @@ public class NotificationOutboxConfiguration : IEntityTypeConfiguration<Notifica
// echoed onto NotifyDeliver audit rows (#23) for cross-row correlation.
builder.Property(n => 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 —

View File

@@ -66,7 +66,8 @@ public class SiteCallEntityTypeConfiguration : IEntityTypeConfiguration<SiteCall
// per-site, not per-node, on this table.
builder.Property(s => 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,

View File

@@ -118,6 +118,7 @@ namespace ScadaLink.ConfigurationDatabase.Migrations
b.Property<string>("SourceNode")
.HasMaxLength(64)
.IsUnicode(false)
.HasColumnType("varchar(64)");
b.Property<string>("SourceScript")

View File

@@ -30,6 +30,7 @@ namespace ScadaLink.ConfigurationDatabase.Migrations
name: "SourceNode",
table: "AuditLog",
type: "varchar(64)",
unicode: false,
maxLength: 64,
nullable: true);

View File

@@ -118,6 +118,7 @@ namespace ScadaLink.ConfigurationDatabase.Migrations
b.Property<string>("SourceNode")
.HasMaxLength(64)
.IsUnicode(false)
.HasColumnType("varchar(64)");
b.Property<string>("SourceScript")
@@ -825,6 +826,7 @@ namespace ScadaLink.ConfigurationDatabase.Migrations
b.Property<string>("SourceNode")
.HasMaxLength(64)
.IsUnicode(false)
.HasColumnType("varchar(64)");
b.Property<string>("SourceScript")

View File

@@ -26,6 +26,7 @@ namespace ScadaLink.ConfigurationDatabase.Migrations
name: "SourceNode",
table: "Notifications",
type: "varchar(64)",
unicode: false,
maxLength: 64,
nullable: true);
}

View File

@@ -118,6 +118,7 @@ namespace ScadaLink.ConfigurationDatabase.Migrations
b.Property<string>("SourceNode")
.HasMaxLength(64)
.IsUnicode(false)
.HasColumnType("varchar(64)");
b.Property<string>("SourceScript")
@@ -267,6 +268,7 @@ namespace ScadaLink.ConfigurationDatabase.Migrations
b.Property<string>("SourceNode")
.HasMaxLength(64)
.IsUnicode(false)
.HasColumnType("varchar(64)");
b.Property<string>("SourceSite")
@@ -829,6 +831,7 @@ namespace ScadaLink.ConfigurationDatabase.Migrations
b.Property<string>("SourceNode")
.HasMaxLength(64)
.IsUnicode(false)
.HasColumnType("varchar(64)");
b.Property<string>("SourceScript")

View File

@@ -27,6 +27,7 @@ namespace ScadaLink.ConfigurationDatabase.Migrations
name: "SourceNode",
table: "SiteCalls",
type: "varchar(64)",
unicode: false,
maxLength: 64,
nullable: true);
}

View File

@@ -115,6 +115,7 @@ namespace ScadaLink.ConfigurationDatabase.Migrations
b.Property<string>("SourceNode")
.HasMaxLength(64)
.IsUnicode(false)
.HasColumnType("varchar(64)");
b.Property<string>("SourceScript")
@@ -264,6 +265,7 @@ namespace ScadaLink.ConfigurationDatabase.Migrations
b.Property<string>("SourceNode")
.HasMaxLength(64)
.IsUnicode(false)
.HasColumnType("varchar(64)");
b.Property<string>("SourceSite")
@@ -826,6 +828,7 @@ namespace ScadaLink.ConfigurationDatabase.Migrations
b.Property<string>("SourceNode")
.HasMaxLength(64)
.IsUnicode(false)
.HasColumnType("varchar(64)");
b.Property<string>("SourceScript")

View File

@@ -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,