Skip to content

Commit 15283a0

Browse files
committed
Add RollbackOnly & MigrateOnly with tests
1 parent 72f8274 commit 15283a0

File tree

2 files changed

+218
-0
lines changed

2 files changed

+218
-0
lines changed

pkg/dbmate/db.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,143 @@ func (db *DB) Rollback() error {
741741
return nil
742742
}
743743

744+
// MigrateOnly applies exactly one pending migration that matches the given version.
745+
func (db *DB) MigrateOnly(version string) error {
746+
drv, err := db.Driver()
747+
if err != nil {
748+
return err
749+
}
750+
751+
migrations, err := db.FindMigrations()
752+
if err != nil {
753+
return err
754+
}
755+
756+
var target *Migration
757+
highestApplied := ""
758+
for i, m := range migrations {
759+
if m.Version == version {
760+
target = &migrations[i]
761+
} else if m.Applied && db.Strict && m.Version > highestApplied {
762+
highestApplied = m.Version
763+
}
764+
}
765+
766+
if target == nil {
767+
return ErrMigrationNotFound
768+
}
769+
if target.Applied {
770+
fmt.Fprintf(db.Log, "Skipping (already applied): %s\n", target.FileName)
771+
return nil
772+
}
773+
if db.Strict && target.Version <= highestApplied {
774+
return fmt.Errorf("migration `%s` is out of order with applied `%s` in --strict mode", target.Version, highestApplied)
775+
}
776+
777+
sqlDB, err := db.openDatabaseForMigration(drv)
778+
if err != nil {
779+
return err
780+
}
781+
defer dbutil.MustClose(sqlDB)
782+
783+
fmt.Fprintf(db.Log, "Applying: %s\n", target.FileName)
784+
start := time.Now()
785+
786+
parsed, err := target.Parse()
787+
if err != nil {
788+
return err
789+
}
790+
exec := func(tx dbutil.Transaction) error {
791+
res, err := tx.Exec(parsed.Up)
792+
if err != nil {
793+
return drv.QueryError(parsed.Up, err)
794+
} else if db.Verbose {
795+
db.printVerbose(res)
796+
}
797+
return drv.InsertMigration(tx, target.Version)
798+
}
799+
800+
if parsed.UpOptions.Transaction() {
801+
err = doTransaction(sqlDB, exec)
802+
} else {
803+
err = exec(sqlDB)
804+
}
805+
806+
fmt.Fprintf(db.Log, "Applied: %s in %s\n", target.FileName, time.Since(start))
807+
if err != nil {
808+
return err
809+
}
810+
if db.AutoDumpSchema {
811+
_ = db.DumpSchema()
812+
}
813+
return nil
814+
}
815+
816+
// RollbackOnly rolls back exactly one applied migration that matches the given version.
817+
func (db *DB) RollbackOnly(version string) error {
818+
drv, err := db.Driver()
819+
if err != nil {
820+
return err
821+
}
822+
823+
migrations, err := db.FindMigrations()
824+
if err != nil {
825+
return err
826+
}
827+
828+
var target *Migration
829+
for i, m := range migrations {
830+
if m.Version == version {
831+
target = &migrations[i]
832+
break
833+
}
834+
}
835+
if target == nil {
836+
return ErrMigrationNotFound
837+
}
838+
if !target.Applied {
839+
return fmt.Errorf("migration `%s` is not applied, nothing to rollback", version)
840+
}
841+
842+
sqlDB, err := db.openDatabaseForMigration(drv)
843+
if err != nil {
844+
return err
845+
}
846+
defer dbutil.MustClose(sqlDB)
847+
848+
fmt.Fprintf(db.Log, "Rolling back: %s\n", target.FileName)
849+
start := time.Now()
850+
851+
parsed, err := target.Parse()
852+
if err != nil {
853+
return err
854+
}
855+
exec := func(tx dbutil.Transaction) error {
856+
res, err := tx.Exec(parsed.Down)
857+
if err != nil {
858+
return drv.QueryError(parsed.Down, err)
859+
} else if db.Verbose {
860+
db.printVerbose(res)
861+
}
862+
return drv.DeleteMigration(tx, target.Version)
863+
}
864+
865+
if parsed.DownOptions.Transaction() {
866+
err = doTransaction(sqlDB, exec)
867+
} else {
868+
err = exec(sqlDB)
869+
}
870+
871+
fmt.Fprintf(db.Log, "Rolled back: %s in %s\n", target.FileName, time.Since(start))
872+
if err != nil {
873+
return err
874+
}
875+
if db.AutoDumpSchema {
876+
_ = db.DumpSchema()
877+
}
878+
return nil
879+
}
880+
744881
// Status shows the status of all migrations
745882
func (db *DB) Status(quiet bool) (int, error) {
746883
results, err := db.FindMigrations()

pkg/dbmate/db_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,87 @@ func TestRollbackAll(t *testing.T) {
593593
})
594594
}
595595

596+
func TestMigrateOnly(t *testing.T) {
597+
testEachURL(t, func(t *testing.T, u *url.URL) {
598+
const v1 = "20151129054053" // users
599+
const v2 = "20200227231541" // posts
600+
601+
db := newTestDB(t, u)
602+
drv, err := db.Driver()
603+
require.NoError(t, err)
604+
605+
require.NoError(t, db.Drop())
606+
require.NoError(t, db.Create())
607+
608+
require.NoError(t, db.MigrateOnly(v1))
609+
610+
sqlDB, err := drv.Open()
611+
require.NoError(t, err)
612+
defer dbutil.MustClose(sqlDB)
613+
614+
applied, err := drv.SelectMigrations(sqlDB, -1)
615+
require.NoError(t, err)
616+
require.Equal(t, map[string]bool{v1: true}, applied)
617+
618+
var cnt int
619+
require.NoError(t, sqlDB.QueryRow("select count(*) from users").Scan(&cnt))
620+
require.Error(t, sqlDB.QueryRow("select count(*) from posts").Scan(&cnt))
621+
622+
require.NoError(t, db.MigrateOnly(v1))
623+
624+
err = db.MigrateOnly("99999999999999")
625+
require.ErrorIs(t, err, dbmate.ErrMigrationNotFound)
626+
627+
db.Strict = true
628+
err = db.MigrateOnly(v2)
629+
require.NoError(t, err)
630+
631+
require.NoError(t, db.MigrateOnly(v1))
632+
})
633+
}
634+
635+
func TestRollbackOnly(t *testing.T) {
636+
testEachURL(t, func(t *testing.T, u *url.URL) {
637+
const v1 = "20151129054053" // users
638+
const v2 = "20200227231541" // posts
639+
640+
db := newTestDB(t, u)
641+
drv, err := db.Driver()
642+
require.NoError(t, err)
643+
644+
require.NoError(t, db.Drop())
645+
require.NoError(t, db.Create())
646+
require.NoError(t, db.Migrate())
647+
648+
sqlDB, err := drv.Open()
649+
require.NoError(t, err)
650+
defer dbutil.MustClose(sqlDB)
651+
652+
applied, _ := drv.SelectMigrations(sqlDB, -1)
653+
require.Equal(t, map[string]bool{v1: true, v2: true}, applied)
654+
655+
require.NoError(t, db.RollbackOnly(v2))
656+
657+
applied, _ = drv.SelectMigrations(sqlDB, -1)
658+
require.Equal(t, map[string]bool{v1: true}, applied)
659+
660+
var cnt int
661+
require.NoError(t, sqlDB.QueryRow("select count(*) from users").Scan(&cnt))
662+
require.Error(t, sqlDB.QueryRow("select count(*) from posts").Scan(&cnt))
663+
664+
err = db.RollbackOnly(v2)
665+
require.Error(t, err)
666+
require.Contains(t, err.Error(), "not applied")
667+
668+
err = db.RollbackOnly("99999999999999")
669+
require.ErrorIs(t, err, dbmate.ErrMigrationNotFound)
670+
671+
require.NoError(t, db.RollbackOnly(v1))
672+
applied, _ = drv.SelectMigrations(sqlDB, -1)
673+
require.Empty(t, applied)
674+
})
675+
}
676+
596677
func TestFindMigrations(t *testing.T) {
597678
testEachURL(t, func(t *testing.T, u *url.URL) {
598679
db := newTestDB(t, u)

0 commit comments

Comments
 (0)