diff --git a/internal/store/migrate.go b/internal/store/migrate.go index 6103aac..ea39d6b 100644 --- a/internal/store/migrate.go +++ b/internal/store/migrate.go @@ -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() diff --git a/internal/store/migrate_atomic_test.go b/internal/store/migrate_atomic_test.go index 7256f15..ca8f1d0 100644 --- a/internal/store/migrate_atomic_test.go +++ b/internal/store/migrate_atomic_test.go @@ -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) {