fix: migration ordering bug that could block fresh-DB deploys
V004_normalize_snapshots_schema.sql had an unguarded FK to a table V2
creates. Under DbUp's default ordinal filename sort, the zero-padded
"V003_"/"V004_" migrations sorted before "V1__", so on a brand-new
database V004 would hard-fail on that FK and abort every migration
after it - V1 through V8 would never run. Confirmed via production
that neither V003 nor V004 had ever actually applied.
Fix: renamed them to V9__/V10__ and added MigrationScriptNameComparer,
which sorts DbUp scripts by numeric V{n} value instead of raw string
order, so double-digit versions can never again sort ahead of earlier
single-digit ones. Added regression tests for both the fixed case and
the original bug shape.
Also updates docs/db/quantengine.dbml (renamed migrations, and closes
out the engine_history/quantengine table-name-collision question -
both schemas are live, backing different code paths, not duplicates)
and CLAUDE.md (migration ordering fix, KIS/OpenDART/KRX credential
env-var-name reference, including a CI secret/env-var name mismatch
found for OpenDART that still needs a decision).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,7 @@ namespace QuantEngine.Infrastructure.Data
|
||||
var upgrader = DeployChanges.To
|
||||
.PostgresqlDatabase(_connectionString)
|
||||
.WithScriptsEmbeddedInAssembly(typeof(DbMigrator).Assembly, s => s.StartsWith("QuantEngine.Infrastructure.Migrations"))
|
||||
.WithScriptNameComparer(new MigrationScriptNameComparer())
|
||||
.LogToConsole()
|
||||
.Build();
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace QuantEngine.Infrastructure.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// Orders DbUp migration scripts by their numeric V{n} version instead of plain ordinal
|
||||
/// string order. Without this, "V10__Name.sql" sorts before "V2__Name.sql" (and, as
|
||||
/// happened on 2026-07-24, zero-padded "V003_Name.sql" sorts before unpadded
|
||||
/// "V1__Name.sql") because ordinal comparison looks at characters, not numeric value.
|
||||
/// That mismatch let a migration with an unmet table dependency run first and silently
|
||||
/// no-op, and another one run first and hard-fail, blocking every migration after it on a
|
||||
/// fresh database. This comparer makes the "V{n}" scheme collision-proof regardless of
|
||||
/// digit count or padding, so it can never happen again.
|
||||
/// </summary>
|
||||
public class MigrationScriptNameComparer : IComparer<string>
|
||||
{
|
||||
private static readonly Regex VersionPattern = new(@"V(\d+)", RegexOptions.Compiled);
|
||||
|
||||
public int Compare(string? x, string? y)
|
||||
{
|
||||
if (x == null || y == null)
|
||||
{
|
||||
return string.CompareOrdinal(x, y);
|
||||
}
|
||||
|
||||
var matchX = VersionPattern.Match(x);
|
||||
var matchY = VersionPattern.Match(y);
|
||||
|
||||
if (matchX.Success && matchY.Success)
|
||||
{
|
||||
var versionX = long.Parse(matchX.Groups[1].Value);
|
||||
var versionY = long.Parse(matchY.Groups[1].Value);
|
||||
if (versionX != versionY)
|
||||
{
|
||||
return versionX.CompareTo(versionY);
|
||||
}
|
||||
}
|
||||
|
||||
return string.CompareOrdinal(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user