From 6f08dd9087937dc1b83d4584af3898609ab491a9 Mon Sep 17 00:00:00 2001 From: Havrileck Alexandre Date: Tue, 24 Aug 2021 21:27:49 +0200 Subject: [PATCH 1/5] feat: Add support to get last run migration --- gormigrate.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/gormigrate.go b/gormigrate.go index cba54eb..bfb62fc 100644 --- a/gormigrate.go +++ b/gormigrate.go @@ -294,6 +294,20 @@ 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) { + return g.getLastRunMigration() +} + func (g *Gormigrate) getLastRunMigration() (*Migration, error) { for i := len(g.migrations) - 1; i >= 0; i-- { migration := g.migrations[i] From 04cf6d4167572c851da9f7fdde2ff0e1605370cb Mon Sep 17 00:00:00 2001 From: Havrileck Alexandre Date: Wed, 25 Aug 2021 21:18:20 +0200 Subject: [PATCH 2/5] docs: Fix usage example with gorm v2 --- README.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2f3f323..77c8757 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 { From 3bc0432693467869dc03796c1c1d9b84defca851 Mon Sep 17 00:00:00 2001 From: Havrileck Alexandre Date: Wed, 25 Aug 2021 21:10:10 +0200 Subject: [PATCH 3/5] feat: Add support for automatic rollback on migration failure --- README.md | 4 ++ gormigrate.go | 13 ++++ gormigrate_test.go | 168 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 185 insertions(+) diff --git a/README.md b/README.md index 77c8757..fb86edf 100644 --- a/README.md +++ b/README.md @@ -169,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 bfb62fc..71710d9 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 @@ -376,6 +381,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..282e7e8 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 From 93325c50f740ed2cce88f01858b8b3fd8aeec134 Mon Sep 17 00:00:00 2001 From: Havrileck Alexandre Date: Wed, 25 Aug 2021 21:38:14 +0200 Subject: [PATCH 4/5] test: Fix test stability --- gormigrate_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gormigrate_test.go b/gormigrate_test.go index 282e7e8..478855c 100644 --- a/gormigrate_test.go +++ b/gormigrate_test.go @@ -586,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) }() From bd770967c860e48f51668c66f7469f0693139d23 Mon Sep 17 00:00:00 2001 From: Havrileck Alexandre Date: Wed, 30 Mar 2022 19:41:58 +0200 Subject: [PATCH 5/5] fix: Fix nil pointer on tx for GetLastRunMigration --- gormigrate.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gormigrate.go b/gormigrate.go index 71710d9..2256123 100644 --- a/gormigrate.go +++ b/gormigrate.go @@ -310,6 +310,9 @@ func (g *Gormigrate) GetLastRunMigrationID() (string, error) { } func (g *Gormigrate) GetLastRunMigration() (*Migration, error) { + // Call Begin here to avoid any nil pointer in tx + g.begin() + return g.getLastRunMigration() }