diff --git a/migrate.go b/migrate.go index c2b46cb7..aae853fb 100644 --- a/migrate.go +++ b/migrate.go @@ -87,6 +87,7 @@ func (b byId) Less(i, j int) bool { return b[i].Less(b[j]) } type MigrationRecord struct { Id string `db:"id"` AppliedAt time.Time `db:"applied_at"` + DownSql string `db:"down_sql"` } var MigrationDialects = map[string]gorp.Dialect{ @@ -261,10 +262,19 @@ func ExecMax(db *sql.DB, dialect string, m MigrationSource, dir MigrationDirecti } if dir == Up { - err = trans.Insert(&MigrationRecord{ - Id: migration.Id, - AppliedAt: time.Now(), - }) + if len(migration.Migration.Up) != 0 { + //this is a real up - insert the record + err = trans.Insert(&MigrationRecord{ + Id: migration.Id, + AppliedAt: time.Now(), + DownSql: strings.Join(migration.Down, "\n"), + }) + } else { + //no up query means this is supposed ot prune thi smigration from db + _, err = trans.Delete(&MigrationRecord{ + Id: migration.Id, + }) + } if err != nil { return applied, err } @@ -343,6 +353,18 @@ func PlanMigration(db *sql.DB, dialect string, m MigrationSource, dir MigrationD } } + // if we are downgrading our main app, apply the downs of the + // new migrations that have been run. We know a migration is no + // longer relevant if the file that it ran from no longer exists + migrationsToRemove := ToRemove(migrations, migrationRecords) + for _, v := range migrationsToRemove { + result = append(result, &PlannedMigration{ + Migration: v, + Queries: v.Down, + }) + + } + return result, dbMap, nil } @@ -376,6 +398,41 @@ func ToApply(migrations []*Migration, current string, direction MigrationDirecti panic("Not possible") } +// Filter a slice of migrations into ones that should be removed. +// A migrations should be removed if the .sql file no longer exists or +// the MigrationSource no longer has the migration but it still exists in the DB. +// This can happen if an older version of the Application is deployed in a rollback scenario +func ToRemove(migrations []*Migration, migrationRecords []MigrationRecord) []*Migration { + missingMigrations := make([]*Migration, 0) + + for _, mr := range migrationRecords { + var migrationExists = false + for _, m := range migrations { + if m.Id == mr.Id { + migrationExists = true + break + } + } + //fmt.Println(mr, ": ", migrationExists) + if !migrationExists { + var m Migration + m.Id = mr.Id + m.Down = strings.Split(mr.DownSql, "\n") + missingMigrations = append(missingMigrations, &m) + } + } + + // Add in reverse order + index := len(missingMigrations) - 1 + toRemove := make([]*Migration, index+1) + for i := 0; i <= index; i++ { + toRemove[index-i] = missingMigrations[i] + } + return toRemove + + panic("Not possible") +} + func GetMigrationRecords(db *sql.DB, dialect string) ([]*MigrationRecord, error) { dbMap, err := getMigrationDbMap(db, dialect) if err != nil { diff --git a/migrate_test.go b/migrate_test.go index 04281b61..63c38dcc 100644 --- a/migrate_test.go +++ b/migrate_test.go @@ -267,9 +267,14 @@ func (s *SqliteMigrateSuite) TestPlanMigration(c *C) { c.Assert(n, Equals, 3) migrations.Migrations = append(migrations.Migrations, &Migration{ - Id: "11_add_middle_name.sql", - Up: []string{"ALTER TABLE people ADD COLUMN middle_name text"}, - Down: []string{"ALTER TABLE people DROP COLUMN middle_name"}, + Id: "11_add_middle_name.sql", + Up: []string{"ALTER TABLE people ADD COLUMN middle_name text"}, + Down: []string{ + "CREATE TEMPORARY TABLE people_backup(id int, first_name text, last_name text);", + "INSERT INTO people_backup SELECT id, first_name, last_name FROM people;", + "DROP TABLE people;", + "ALTER TABLE people_backup RENAME TO people;", + }, }) plannedMigrations, _, err := PlanMigration(s.Db, "sqlite3", migrations, Up, 0) @@ -277,10 +282,22 @@ func (s *SqliteMigrateSuite) TestPlanMigration(c *C) { c.Assert(plannedMigrations, HasLen, 1) c.Assert(plannedMigrations[0].Migration, Equals, migrations.Migrations[3]) + _, err = Exec(s.Db, "sqlite3", migrations, Up) + + migrations.Migrations = migrations.Migrations[0:3] //simulate redployig app without last sql file + + plannedMigrations, _, err = PlanMigration(s.Db, "sqlite3", migrations, Up, 0) + c.Assert(plannedMigrations, HasLen, 1) + n, err = Exec(s.Db, "sqlite3", migrations, Up) + + plannedMigrations, _, err = PlanMigration(s.Db, "sqlite3", migrations, Up, 0) + c.Assert(plannedMigrations, HasLen, 0) + plannedMigrations, _, err = PlanMigration(s.Db, "sqlite3", migrations, Down, 0) c.Assert(err, IsNil) c.Assert(plannedMigrations, HasLen, 3) c.Assert(plannedMigrations[0].Migration, Equals, migrations.Migrations[2]) c.Assert(plannedMigrations[1].Migration, Equals, migrations.Migrations[1]) c.Assert(plannedMigrations[2].Migration, Equals, migrations.Migrations[0]) + }