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:
2026-07-30 11:36:00 +09:00
parent 70824c2afb
commit 279d1760ef
7 changed files with 159 additions and 27 deletions
@@ -0,0 +1,51 @@
using System.Linq;
using QuantEngine.Infrastructure.Data;
namespace QuantEngine.Core.Tests;
public class MigrationScriptNameComparerTests
{
[Fact]
public void Sorts_DoubleDigit_Version_After_SingleDigit_Versions()
{
var scripts = new[]
{
"QuantEngine.Infrastructure.Migrations.V10__Normalize_Snapshots_Schema.sql",
"QuantEngine.Infrastructure.Migrations.V2__Add_Kis_Collections.sql",
"QuantEngine.Infrastructure.Migrations.V9__Add_Audit_Trail_Tables.sql",
"QuantEngine.Infrastructure.Migrations.V1__Initial_Schema.sql",
"QuantEngine.Infrastructure.Migrations.V8__PostgreSQL_History_First_Schema.sql",
};
var sorted = scripts.OrderBy(s => s, new MigrationScriptNameComparer()).ToArray();
Assert.Equal(new[]
{
"QuantEngine.Infrastructure.Migrations.V1__Initial_Schema.sql",
"QuantEngine.Infrastructure.Migrations.V2__Add_Kis_Collections.sql",
"QuantEngine.Infrastructure.Migrations.V8__PostgreSQL_History_First_Schema.sql",
"QuantEngine.Infrastructure.Migrations.V9__Add_Audit_Trail_Tables.sql",
"QuantEngine.Infrastructure.Migrations.V10__Normalize_Snapshots_Schema.sql",
}, sorted);
}
[Fact]
public void Regression_ZeroPadded_Script_No_Longer_Sorts_Before_V1()
{
// This is the exact bug that shipped 2026-07-24: "V003_..." sorted before "V1__..."
// under plain ordinal comparison, so a migration with unmet table dependencies ran
// first. The comparer must treat "V003" as version 3, landing it between V2 and V4.
var scripts = new[]
{
"QuantEngine.Infrastructure.Migrations.V003_add_audit_trail_tables.sql",
"QuantEngine.Infrastructure.Migrations.V1__Initial_Schema.sql",
"QuantEngine.Infrastructure.Migrations.V4__Add_Initial_Admin.sql",
};
var sorted = scripts.OrderBy(s => s, new MigrationScriptNameComparer()).ToArray();
Assert.Equal("QuantEngine.Infrastructure.Migrations.V1__Initial_Schema.sql", sorted[0]);
Assert.Equal("QuantEngine.Infrastructure.Migrations.V003_add_audit_trail_tables.sql", sorted[1]);
Assert.Equal("QuantEngine.Infrastructure.Migrations.V4__Add_Initial_Admin.sql", sorted[2]);
}
}