diff --git a/README.md b/README.md index 2f3f323..fb86edf 100644 --- a/README.md +++ b/README.md @@ -34,17 +34,25 @@ import ( "github.com/go-gormigrate/gormigrate/v2" "gorm.io/gorm" - _ "github.com/jinzhu/gorm/dialects/sqlite" + "gorm.io/driver/sqlite" ) func main() { - db, err := gorm.Open("sqlite3", "mydb.sqlite3") + newLogger := logger.New( + log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer + logger.Config{ + SlowThreshold: time.Second, // Slow SQL threshold + LogLevel: logger.Silent, // Log level + IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger + Colorful: false, // Disable color + }, + ) + + db, err := db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{ Logger: newLogger }) if err != nil { log.Fatal(err) } - db.LogMode(true) - m := gormigrate.New(db, gormigrate.DefaultOptions, []*gormigrate.Migration{ // create persons table { @@ -161,6 +169,10 @@ type Options struct { // ValidateUnknownMigrations will cause migrate to fail if there's unknown migration // IDs in the database ValidateUnknownMigrations bool + // AutomaticRollback will automatically run rollback methods if provided + // and if migrate function failed. This is only done when UseTransaction is disabled. + // Otherwise it will be rollback by the transaction itself. + AutomaticRollback bool } ``` diff --git a/gormigrate.go b/gormigrate.go index cba54eb..2256123 100644 --- a/gormigrate.go +++ b/gormigrate.go @@ -34,6 +34,10 @@ type Options struct { // ValidateUnknownMigrations will cause migrate to fail if there's unknown migration // IDs in the database ValidateUnknownMigrations bool + // AutomaticRollback will automatically run rollback methods if provided + // and if migrate function failed. This is only done when UseTransaction is disabled. + // Otherwise it will be rollback by the transaction itself. + AutomaticRollback bool } // Migration represents a database migration (a modification to be made on the database). @@ -81,6 +85,7 @@ var ( IDColumnSize: 255, UseTransaction: false, ValidateUnknownMigrations: false, + AutomaticRollback: false, } // ErrRollbackImpossible is returned when trying to rollback a migration @@ -294,6 +299,23 @@ func (g *Gormigrate) RollbackTo(migrationID string) error { return g.commit() } +func (g *Gormigrate) GetLastRunMigrationID() (string, error) { + mig, err := g.GetLastRunMigration() + // Check error + if err != nil { + return "", err + } + + return mig.ID, nil +} + +func (g *Gormigrate) GetLastRunMigration() (*Migration, error) { + // Call Begin here to avoid any nil pointer in tx + g.begin() + + return g.getLastRunMigration() +} + func (g *Gormigrate) getLastRunMigration() (*Migration, error) { for i := len(g.migrations) - 1; i >= 0; i-- { migration := g.migrations[i] @@ -362,6 +384,14 @@ func (g *Gormigrate) runMigration(migration *Migration) error { } if !migrationRan { if err := migration.Migrate(g.tx); err != nil { + if !g.options.UseTransaction && + g.options.AutomaticRollback && + migration.Rollback != nil { + if err2 := migration.Rollback(g.tx); err2 != nil { + return err + } + } + return err } diff --git a/gormigrate_test.go b/gormigrate_test.go index 8189e2d..478855c 100644 --- a/gormigrate_test.go +++ b/gormigrate_test.go @@ -365,6 +365,174 @@ func TestMigration_WithUseTransactionsShouldRollback(t *testing.T) { }, "postgres", "sqlite3", "mssql") } +// This test will only focus on automatic rollback or not, not on the database itself or migrations. +func TestMigration_WithAutomaticRollbackShouldRollback(t *testing.T) { + t.Run("rollback without any error", func(t *testing.T) { + wantedError := errors.New("wanted") + rollbackCalled := false + + forEachDatabase(t, func(db *gorm.DB) { + m := New(db, &Options{AutomaticRollback: true}, []*Migration{ + { + ID: "201904231300", + Migrate: func(tx *gorm.DB) error { + return wantedError + }, + Rollback: func(tx *gorm.DB) error { + rollbackCalled = true + return nil + }, + }, + }) + + // Migration should return an error and not leave around a Pet table + err := m.Migrate() + assert.Error(t, err) + assert.Equal(t, wantedError, err) + assert.True(t, rollbackCalled) + }) + }) + + t.Run("rollback with an error", func(t *testing.T) { + wantedError := errors.New("wanted") + rollbackError := errors.New("rollback error") + rollbackCalled := false + + forEachDatabase(t, func(db *gorm.DB) { + m := New(db, &Options{AutomaticRollback: true}, []*Migration{ + { + ID: "201904231300", + Migrate: func(tx *gorm.DB) error { + return wantedError + }, + Rollback: func(tx *gorm.DB) error { + rollbackCalled = true + return rollbackError + }, + }, + }) + + // Migration should return an error and not leave around a Pet table + err := m.Migrate() + assert.Error(t, err) + assert.Equal(t, wantedError, err) + assert.True(t, rollbackCalled) + }) + }) +} + +// This test will only focus on automatic rollback or not, not on the database itself or migrations. +func TestMigration_WithoutAutomaticRollbackShouldNotRollback(t *testing.T) { + t.Run("rollback without any error", func(t *testing.T) { + wantedError := errors.New("wanted") + rollbackCalled := false + + forEachDatabase(t, func(db *gorm.DB) { + m := New(db, DefaultOptions, []*Migration{ + { + ID: "201904231300", + Migrate: func(tx *gorm.DB) error { + return wantedError + }, + Rollback: func(tx *gorm.DB) error { + rollbackCalled = true + return nil + }, + }, + }) + + // Migration should return an error and not leave around a Pet table + err := m.Migrate() + assert.Error(t, err) + assert.Equal(t, wantedError, err) + assert.False(t, rollbackCalled) + }) + }) + + t.Run("rollback with an error", func(t *testing.T) { + wantedError := errors.New("wanted") + rollbackError := errors.New("rollback error") + rollbackCalled := false + + forEachDatabase(t, func(db *gorm.DB) { + m := New(db, DefaultOptions, []*Migration{ + { + ID: "201904231300", + Migrate: func(tx *gorm.DB) error { + return wantedError + }, + Rollback: func(tx *gorm.DB) error { + rollbackCalled = true + return rollbackError + }, + }, + }) + + // Migration should return an error and not leave around a Pet table + err := m.Migrate() + assert.Error(t, err) + assert.Equal(t, wantedError, err) + assert.False(t, rollbackCalled) + }) + }) +} + +// This test will only focus on automatic rollback or not, not on the database itself or migrations. +func TestMigration_WithoutAutomaticRollbackAndUseTransactionShouldNotRollback(t *testing.T) { + t.Run("rollback without any error", func(t *testing.T) { + wantedError := errors.New("wanted") + rollbackCalled := false + + forEachDatabase(t, func(db *gorm.DB) { + m := New(db, &Options{UseTransaction: true, AutomaticRollback: true}, []*Migration{ + { + ID: "201904231300", + Migrate: func(tx *gorm.DB) error { + return wantedError + }, + Rollback: func(tx *gorm.DB) error { + rollbackCalled = true + return nil + }, + }, + }) + + // Migration should return an error and not leave around a Pet table + err := m.Migrate() + assert.Error(t, err) + assert.Equal(t, wantedError, err) + assert.False(t, rollbackCalled) + }) + }) + + t.Run("rollback with an error", func(t *testing.T) { + wantedError := errors.New("wanted") + rollbackError := errors.New("rollback error") + rollbackCalled := false + + forEachDatabase(t, func(db *gorm.DB) { + m := New(db, &Options{UseTransaction: true, AutomaticRollback: true}, []*Migration{ + { + ID: "201904231300", + Migrate: func(tx *gorm.DB) error { + return wantedError + }, + Rollback: func(tx *gorm.DB) error { + rollbackCalled = true + return rollbackError + }, + }, + }) + + // Migration should return an error and not leave around a Pet table + err := m.Migrate() + assert.Error(t, err) + assert.Equal(t, wantedError, err) + assert.False(t, rollbackCalled) + }) + }) +} + func TestUnexpectedMigrationEnabled(t *testing.T) { forEachDatabase(t, func(db *gorm.DB) { options := DefaultOptions @@ -418,7 +586,7 @@ func forEachDatabase(t *testing.T, fn func(database *gorm.DB), dialects ...strin require.NoError(t, err, "Could not connect to database %s, %v", database.dialect, err) // ensure tables do not exists - assert.NoError(t, db.Migrator().DropTable("migrations", "people", "pets")) + assert.NoError(t, db.Migrator().DropTable("migrations", "people", "pets", "cars")) fn(db) }()