Bug? Expected functionality? No error thrown on rollback if migration doesn't exist
Description
When attempting to rollback a migration whose file is not present in the current codebase, dbmate silently skips the missing migration and rolls back the previous available migration instead of throwing an error.
We encountered this when a migration was accidentally run against our staging database from a local machine. The staging environment didn't have the migration file, but when we attempted to rollback (from the staging app), dbmate reported success while actually rolling back the wrong migration.
While this scenario represents user error and isn't a standard workflow, the silent failure behavior is problematic because it provides misleading feedback and can cause unintended schema changes.
Version: 2.26.0 Database: Postgres Operating System: Heroku
Steps To Reproduce
- Apply migration A to database
- Apply migration B to database
- Remove migration B file from codebase (simulating missing migration file)
- Run dbmate rollback
- dbmate reports "Done." and rolls back migration A instead of migration B
Expected Behavior dbmate should throw an error when the most recent migration file cannot be found, such as: Error: Cannot rollback migration [filename] - migration file not found
Actual Behavior dbmate silently skips the missing migration file and rolls back the next available migration, reporting success while performing an unintended operation.
Problem
When using --strict mode, dbmate should fail if a migration file is missing during rollback operations. Currently, it silently ignores missing files.
Proposed Solution
Add a file existence check when db.Strict is enabled before attempting to rollback.
Patch
diff --git a/pkg/dbmate/db.go b/pkg/dbmate/db.go
index fb55683..1234567 100644
--- a/pkg/dbmate/db.go
+++ b/pkg/dbmate/db.go
@@ -360,6 +360,15 @@ func (db *DB) Rollback() error {
if latest == nil {
return ErrNoRollback
}
+
+ // In strict mode, fail if the migration file is not found
+ if db.Strict {
+ _, err := latest.FS.Open(latest.FilePath)
+ if err != nil {
+ return fmt.Errorf("can't rollback migration %s in --strict mode: %w", latest.Version, ErrMigrationNotFound)
+ }
+ }
+
fmt.Fprintf(db.Log, "Rolling back: %s\n", latest.FileName)
start := time.Now()
Is this a duplicate of #628? Not a duplicate, but a similar motivation or circumstance?