Skip to content

Commit

Permalink
Merge pull request #37 from mdales/mwd-error-on-unknown-migration
Browse files Browse the repository at this point in the history
Fail migration with error if DB contains unknown migrations.
  • Loading branch information
andreynering authored Jul 7, 2019
2 parents 3185a53 + fca964f commit 13b3726
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 4 deletions.
52 changes: 48 additions & 4 deletions gormigrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ type Options struct {
// UseTransaction makes Gormigrate execute migrations inside a single transaction.
// Keep in mind that not all databases support DDL commands inside transactions.
UseTransaction bool
// ValidateUnknownMigrations will cause migrate to fail if there's unknown migration
// IDs in the database
ValidateUnknownMigrations bool
}

// Migration represents a database migration (a modification to be made on the database).
Expand Down Expand Up @@ -73,10 +76,11 @@ func (e *DuplicatedIDError) Error() string {
var (
// DefaultOptions can be used if you don't want to think about options.
DefaultOptions = &Options{
TableName: "migrations",
IDColumnName: "id",
IDColumnSize: 255,
UseTransaction: false,
TableName: "migrations",
IDColumnName: "id",
IDColumnSize: 255,
UseTransaction: false,
ValidateUnknownMigrations: false,
}

// ErrRollbackImpossible is returned when trying to rollback a migration
Expand All @@ -96,6 +100,9 @@ var (
// ErrMigrationIDDoesNotExist is returned when migrating or rolling back to a migration ID that
// does not exist in the list of migrations
ErrMigrationIDDoesNotExist = errors.New("gormigrate: Tried to migrate to an ID that doesn't exist")

// ErrUnknownPastMigration is returned if a migration exists in the DB that doesn't exist in the code
ErrUnknownPastMigration = errors.New("gormigrate: Found migration in DB that does not exist in code")
)

// New returns a new Gormigrate.
Expand Down Expand Up @@ -164,6 +171,16 @@ func (g *Gormigrate) migrate(migrationID string) error {
return err
}

if g.options.ValidateUnknownMigrations {
unknownMigrations, err := g.unknownMigrationsHaveHappened()
if err != nil {
return err
}
if unknownMigrations {
return ErrUnknownPastMigration
}
}

if g.initSchema != nil {
canInitializeSchema, err := g.canInitializeSchema()
if err != nil {
Expand Down Expand Up @@ -394,6 +411,33 @@ func (g *Gormigrate) canInitializeSchema() (bool, error) {
return count == 0, err
}

func (g *Gormigrate) unknownMigrationsHaveHappened() (bool, error) {
sql := fmt.Sprintf("SELECT %s FROM %s", g.options.IDColumnName, g.options.TableName)
rows, err := g.tx.Raw(sql).Rows()
if err != nil {
return false, err
}
defer rows.Close()

validIDSet := make(map[string]struct{}, len(g.migrations)+1)
validIDSet[initSchemaMigrationID] = struct{}{}
for _, migration := range g.migrations {
validIDSet[migration.ID] = struct{}{}
}

for rows.Next() {
var pastMigrationID string
if err := rows.Scan(&pastMigrationID); err != nil {
return false, err
}
if _, ok := validIDSet[pastMigrationID]; !ok {
return true, nil
}
}

return false, nil
}

func (g *Gormigrate) insertMigration(id string) error {
sql := fmt.Sprintf("INSERT INTO %s (%s) VALUES (?)", g.options.TableName, g.options.IDColumnName)
return g.tx.Exec(sql, id).Error
Expand Down
32 changes: 32 additions & 0 deletions gormigrate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,38 @@ func TestMigration_WithUseTransactionsShouldRollback(t *testing.T) {
}, "postgres", "sqlite3", "mssql")
}

func TestUnexpectedMigrationEnabled(t *testing.T) {
forEachDatabase(t, func(db *gorm.DB) {
options := DefaultOptions
options.ValidateUnknownMigrations = true
m := New(db, options, migrations)

// Migrate without initialisation
assert.NoError(t, m.Migrate())

// Try with fewer migrations. Should fail as we see a migration in the db that
// we don't recognise any more
n := New(db, DefaultOptions, migrations[:1])
assert.Equal(t, ErrUnknownPastMigration, n.Migrate())
})
}

func TestUnexpectedMigrationDisabled(t *testing.T) {
forEachDatabase(t, func(db *gorm.DB) {
options := DefaultOptions
options.ValidateUnknownMigrations = false
m := New(db, options, migrations)

// Migrate without initialisation
assert.NoError(t, m.Migrate())

// Try with fewer migrations. Should pass as we see a migration in the db that
// we don't recognise any more, but the validation defaults off
n := New(db, DefaultOptions, migrations[:1])
assert.NoError(t, n.Migrate())
})
}

func tableCount(t *testing.T, db *gorm.DB, tableName string) (count int) {
assert.NoError(t, db.Table(tableName).Count(&count).Error)
return
Expand Down

0 comments on commit 13b3726

Please sign in to comment.