Refuse rollback after migration ledger identity drift

Why: An already-open Store could otherwise execute a numbered down migration after its ledger name changed.

Evidence: The targeted rollback test failed before the guard and passed after it; focused SQLite and isolated PostgreSQL store/migrator checks passed.

Impact: MigrationStatus and MigrateDown fail closed on version-name mismatch before changing schema.

Refs: feature/rdm-two-trees-latest-templates Backend combination probe
This commit is contained in:
weichen
2026-10-14 06:52:29 +00:00
parent f4ed6b063f
commit d9b00c23c8
2 changed files with 23 additions and 2 deletions
+7 -2
View File
@@ -1381,7 +1381,7 @@ func (s *Store) execInTx(stmts []string) error {
// MigrationStatus 返回已应用版本列表(诊断/验收用)。
func (s *Store) MigrationStatus() ([]int, error) {
rows, err := s.db.Query(`SELECT version FROM schema_migrations ORDER BY version`)
rows, err := s.db.Query(`SELECT version,name FROM schema_migrations ORDER BY version`)
if err != nil {
return nil, err
}
@@ -1389,9 +1389,14 @@ func (s *Store) MigrationStatus() ([]int, error) {
out := []int{}
for rows.Next() {
var v int
if err := rows.Scan(&v); err != nil {
var name string
if err := rows.Scan(&v, &name); err != nil {
return nil, err
}
expected, known := MigrationName(v)
if !known || expected != name {
return nil, fmt.Errorf("migrate: migration identity mismatch at v%d; refuse to roll back another schema", v)
}
out = append(out, v)
}
return out, rows.Err()
+16
View File
@@ -105,6 +105,22 @@ func TestMigrateRejectsLedgerNameMismatch(t *testing.T) {
}
}
func TestMigrateDownRejectsLedgerNameDriftBeforeChangingSchema(t *testing.T) {
path := filepath.Join(t.TempDir(), "down-name-drift.db")
st := openSQLiteAt(t, path)
if _, err := st.db.Exec(`UPDATE schema_migrations SET name='wrong-candidate' WHERE version=40`); err != nil {
t.Fatal(err)
}
err := st.MigrateDown(39)
if err == nil || !strings.Contains(err.Error(), "migration identity mismatch") {
t.Fatalf("rollback accepted a changed migration identity: %v", err)
}
var remain int
if err := st.db.QueryRow(`SELECT COUNT(*) FROM schema_migrations WHERE version=40`).Scan(&remain); err != nil || remain != 1 {
t.Fatalf("rollback changed ledger before refusing: remaining=%d err=%v", remain, err)
}
}
// DDL 与账本同事务:在「DDL 已执行、版本未登记」处注入故障 → 全部回滚,
// 不留下 v6 的表,也不留下 v6 的账本行(历史两步实现的中间态缺口)。
func TestMigrateAtomicDDLAndLedger(t *testing.T) {