Skip to content
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

Handle unknown migrations #44

Open
wants to merge 4 commits 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
97 changes: 76 additions & 21 deletions migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -364,9 +364,8 @@ func PlanMigration(db *sql.DB, dialect string, m MigrationSource, dir MigrationD
if len(existingMigrations) > 0 {
result = append(result, ToCatchup(migrations, existingMigrations, record)...)
}

// Figure out which migrations to apply
toApply := ToApply(migrations, record.Id, dir)
toApply := ToApply(migrations, existingMigrations, dir)
toApplyCount := len(toApply)
if max > 0 && max < toApplyCount {
toApplyCount = max
Expand All @@ -379,6 +378,10 @@ func PlanMigration(db *sql.DB, dialect string, m MigrationSource, dir MigrationD
Queries: v.Up,
})
} else if dir == Down {
if v.Down == nil {
// This happens if we are trying to migrate down a migration unknown to us
return nil, nil, fmt.Errorf("Unable to undo unknown migration %q", v.Id)
}
result = append(result, &PlannedMigration{
Migration: v,
Queries: v.Down,
Expand All @@ -389,34 +392,86 @@ func PlanMigration(db *sql.DB, dialect string, m MigrationSource, dir MigrationD
return result, dbMap, nil
}

// Filter a slice of migrations into ones that should be applied.
func ToApply(migrations []*Migration, current string, direction MigrationDirection) []*Migration {
var index = -1
if current != "" {
for index < len(migrations)-1 {
index++
if migrations[index].Id == current {
// Searches the arrays for the latest elements with common ids.
// Returns the indexes of the common id.
func lastCommonMigration(left []*Migration, right []*Migration) (int, int) {
const none = -1
xIndexMatch := none
yIndexMatch := 0
for i := 0; i < len(left); i++ {
existingId := left[i].Id
for j := yIndexMatch; j < len(right); j++ {
if right[j].Id == existingId {
xIndexMatch = i
yIndexMatch = j
break
}
}
}
if xIndexMatch == none {
// We never found a match; the arrays have nothing in common
return none, none
}
return xIndexMatch, yIndexMatch
}

// Filter a slice of migrations into ones that should be applied.
func ToApply(migrations, existingMigrations []*Migration, direction MigrationDirection) []*Migration {
if direction == Up {
return migrations[index+1:]
} else if direction == Down {
if index == -1 {
return []*Migration{}
}
return toApplyUp(migrations, existingMigrations)
}
if direction == Down {
return toApplyDown(migrations, existingMigrations)
}
panic("Unknown direction")
}

// Add in reverse order
toApply := make([]*Migration, index+1)
for i := 0; i < index+1; i++ {
toApply[index-i] = migrations[i]
}
return toApply
func toApplyUp(migrations, existingMigrations []*Migration) []*Migration {
if len(existingMigrations) == 0 {
return migrations
}
// All migrations after the last common migration need to be applied to the db.
// It's possible that there are some existingMigrations unknown to us--
// we'll just ignore them and hope they don't matter.
_, index := lastCommonMigration(existingMigrations, migrations)
return migrations[index+1:]
}

func toApplyDown(migrations, existingMigrations []*Migration) []*Migration {
if len(existingMigrations) == 0 {
return []*Migration{}
}
allMigrations := mergeMigrations(existingMigrations, migrations)
_, index := lastCommonMigration(existingMigrations, allMigrations)
// Add in reverse order
toApply := make([]*Migration, index+1)
for i := 0; i < index+1; i++ {
toApply[index-i] = allMigrations[i]
}
return toApply
}

panic("Not possible")
// Returns a list of all migrations in either list, sorted by Id.
// Migrations present only in existingMigrations will not have Up or Down set.
func mergeMigrations(existingMigrations, migrations []*Migration) []*Migration {
migrationsMap := map[string]*Migration{}
for _, m := range existingMigrations {
migrationsMap[m.Id] = m
}
// When a Migration appears in both existingMigrations and migrations,
// we want the Migration from migrations, since only that list has
// the Up and Down fields set
for _, m := range migrations {
migrationsMap[m.Id] = m
}

// Flatten our map back into a list that we can sort
allMigrations := make([]*Migration, 0, len(migrationsMap))
for _, m := range migrationsMap {
allMigrations = append(allMigrations, m)
}
sort.Sort(byId(allMigrations))
return allMigrations
}

func ToCatchup(migrations, existingMigrations []*Migration, lastRun *Migration) []*PlannedMigration {
Expand Down
91 changes: 91 additions & 0 deletions migrate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,97 @@ func (s *SqliteMigrateSuite) TestPlanMigrationWithHoles(c *C) {
c.Assert(plannedMigrations[2].Queries[0], Equals, down)
}

func (s *SqliteMigrateSuite) TestMigrationWithMostRecentUnknown(c *C) {
up := "SELECT 0"
down := "SELECT 1"
migrations := &MemoryMigrationSource{
Migrations: []*Migration{
&Migration{
Id: "1",
Up: []string{up},
Down: []string{down},
},
&Migration{
Id: "3",
Up: []string{up},
Down: []string{down},
},
&Migration{
Id: "4",
Up: []string{up},
Down: []string{down},
},
},
}
n, err := Exec(s.Db, "sqlite3", migrations, Up)
c.Assert(err, IsNil)
c.Assert(n, Equals, 3)

// remove "4" from our list of known migrations
migrations.Migrations = migrations.Migrations[0:2]

migrations.Migrations = append(migrations.Migrations, &Migration{
Id: "2",
Up: []string{up},
Down: []string{down},
}, &Migration{
Id: "5",
Up: []string{up},
Down: []string{down},
})

// apply the latest migration
plannedMigrations, _, err := PlanMigration(s.Db, "sqlite3", migrations, Up, 0)
c.Assert(err, IsNil)
c.Assert(plannedMigrations, HasLen, 2)
c.Assert(plannedMigrations[0].Migration.Id, Equals, "2")
c.Assert(plannedMigrations[1].Migration.Id, Equals, "5")

// The most recent migration is unknown; we can't undo it
plannedMigrations, _, err = PlanMigration(s.Db, "sqlite3", migrations, Down, 1)
c.Assert(err, Not(IsNil))
}

func (s *SqliteMigrateSuite) TestMigrationDownWithMiddleUnknown(c *C) {
up := "SELECT 0"
down := "SELECT 1"
migrations := &MemoryMigrationSource{
Migrations: []*Migration{
&Migration{
Id: "1",
Up: []string{up},
Down: []string{down},
},
&Migration{
Id: "2",
Up: []string{up},
Down: []string{down},
},
&Migration{
Id: "3",
Up: []string{up},
Down: []string{down},
},
},
}
n, err := Exec(s.Db, "sqlite3", migrations, Up)
c.Assert(err, IsNil)
c.Assert(n, Equals, 3)

// remove "2" from our list of known migrations
migrations.Migrations = []*Migration{migrations.Migrations[0], migrations.Migrations[2]}

// undo "3"
plannedMigrations, _, err := PlanMigration(s.Db, "sqlite3", migrations, Down, 1)
c.Assert(err, IsNil)
c.Assert(plannedMigrations, HasLen, 1)
c.Assert(plannedMigrations[0].Id, Equals, "3")

// will undo "3", then try "2", but "2" is unknown
plannedMigrations, _, err = PlanMigration(s.Db, "sqlite3", migrations, Down, 2)
c.Assert(err, Not(IsNil))
}

func (s *SqliteMigrateSuite) TestLess(c *C) {
c.Assert((Migration{Id: "1"}).Less(&Migration{Id: "2"}), Equals, true) // 1 less than 2
c.Assert((Migration{Id: "2"}).Less(&Migration{Id: "1"}), Equals, false) // 2 not less than 1
Expand Down
9 changes: 7 additions & 2 deletions sql-migrate/command_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,13 @@ func (c *StatusCommand) Run(args []string) int {
}

for _, r := range records {
rows[r.Id].Migrated = true
rows[r.Id].AppliedAt = r.AppliedAt
status, ok := rows[r.Id]
if !ok {
ui.Warn(fmt.Sprintf("Migration in database %q unknown to sql-migrate; it will not be possible to migrate down this migration", r.Id))
continue
}
status.Migrated = true
status.AppliedAt = r.AppliedAt
}

for _, m := range migrations {
Expand Down
29 changes: 10 additions & 19 deletions toapply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,60 +17,51 @@ type ToApplyMigrateSuite struct {
var _ = Suite(&ToApplyMigrateSuite{})

func (s *ToApplyMigrateSuite) TestGetAll(c *C) {
toApply := ToApply(toapplyMigrations, "", Up)
toApply := ToApply(toapplyMigrations, toapplyMigrations[0:0], Up)
c.Assert(toApply, HasLen, 3)
c.Assert(toApply[0], Equals, toapplyMigrations[0])
c.Assert(toApply[1], Equals, toapplyMigrations[1])
c.Assert(toApply[2], Equals, toapplyMigrations[2])
}

func (s *ToApplyMigrateSuite) TestGetAbc(c *C) {
toApply := ToApply(toapplyMigrations, "abc", Up)
toApply := ToApply(toapplyMigrations, toapplyMigrations[0:1], Up)
c.Assert(toApply, HasLen, 2)
c.Assert(toApply[0], Equals, toapplyMigrations[1])
c.Assert(toApply[1], Equals, toapplyMigrations[2])
}

func (s *ToApplyMigrateSuite) TestGetCde(c *C) {
toApply := ToApply(toapplyMigrations, "cde", Up)
toApply := ToApply(toapplyMigrations, toapplyMigrations[0:2], Up)
c.Assert(toApply, HasLen, 1)
c.Assert(toApply[0], Equals, toapplyMigrations[2])
}

func (s *ToApplyMigrateSuite) TestGetDone(c *C) {
toApply := ToApply(toapplyMigrations, "efg", Up)
c.Assert(toApply, HasLen, 0)

toApply = ToApply(toapplyMigrations, "zzz", Up)
toApply := ToApply(toapplyMigrations, toapplyMigrations[0:3], Up)
c.Assert(toApply, HasLen, 0)
}

func (s *ToApplyMigrateSuite) TestDownDone(c *C) {
toApply := ToApply(toapplyMigrations, "", Down)
toApply := ToApply(toapplyMigrations, toapplyMigrations[0:0], Down)
c.Assert(toApply, HasLen, 0)
}

func (s *ToApplyMigrateSuite) TestDownCde(c *C) {
toApply := ToApply(toapplyMigrations, "cde", Down)
toApply := ToApply(toapplyMigrations, toapplyMigrations[0:2], Down)
c.Assert(toApply, HasLen, 2)
c.Assert(toApply[0], Equals, toapplyMigrations[1])
c.Assert(toApply[1], Equals, toapplyMigrations[0])
}

func (s *ToApplyMigrateSuite) TestDownAbc(c *C) {
toApply := ToApply(toapplyMigrations, "abc", Down)
toApply := ToApply(toapplyMigrations, toapplyMigrations[0:1], Down)
c.Assert(toApply, HasLen, 1)
c.Assert(toApply[0], Equals, toapplyMigrations[0])
}

func (s *ToApplyMigrateSuite) TestDownAll(c *C) {
toApply := ToApply(toapplyMigrations, "efg", Down)
c.Assert(toApply, HasLen, 3)
c.Assert(toApply[0], Equals, toapplyMigrations[2])
c.Assert(toApply[1], Equals, toapplyMigrations[1])
c.Assert(toApply[2], Equals, toapplyMigrations[0])

toApply = ToApply(toapplyMigrations, "zzz", Down)
toApply := ToApply(toapplyMigrations, toapplyMigrations, Down)
c.Assert(toApply, HasLen, 3)
c.Assert(toApply[0], Equals, toapplyMigrations[2])
c.Assert(toApply[1], Equals, toapplyMigrations[1])
Expand All @@ -88,13 +79,13 @@ func (s *ToApplyMigrateSuite) TestAlphaNumericMigrations(c *C) {

sort.Sort(migrations)

toApplyUp := ToApply(migrations, "2_cde", Up)
toApplyUp := ToApply(migrations, migrations[0:2], Up)
c.Assert(toApplyUp, HasLen, 3)
c.Assert(toApplyUp[0].Id, Equals, "10_abc")
c.Assert(toApplyUp[1].Id, Equals, "35_cde")
c.Assert(toApplyUp[2].Id, Equals, "efg")

toApplyDown := ToApply(migrations, "2_cde", Down)
toApplyDown := ToApply(migrations, migrations[0:2], Down)
c.Assert(toApplyDown, HasLen, 2)
c.Assert(toApplyDown[0].Id, Equals, "2_cde")
c.Assert(toApplyDown[1].Id, Equals, "1_abc")
Expand Down