Skip to content

Rollback database when backing sql source is removed #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 61 additions & 4 deletions migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be a problem, anyone who is currently using sql-migrate won't have this column in their migrations table.

We've come at the sad point that we'll need to migrate the migrations table, but we have no way of doing so.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh, that's a good point. I'll try to add something that detects an older database and migrate it but that feels "off", especially since package is meant to migrate databases!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm. What if we store the DownSql in a separate new table? Does that let us avoid compatibility issues?

}

var MigrationDialects = map[string]gorp.Dialect{
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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 {
Expand Down
23 changes: 20 additions & 3 deletions migrate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,20 +267,37 @@ 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)
c.Assert(err, IsNil)
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])

}