diff --git a/.travis.yml b/.travis.yml index bc1545b2..15bfbe2c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,7 @@ install: - go install ./... script: - - go test -v ./... + - go test -v -enable-mysql ./... - bash test-integration/postgres.sh - bash test-integration/mysql.sh - bash test-integration/mysql-flag.sh diff --git a/README.md b/README.md index 0aff0c61..037708f6 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Using [modl](https://github.com/jmoiron/modl)? Check out [modl-migrate](https:// * Atomic migrations * Up/down migrations to allow rollback * Supports multiple database types in one project +* Support for concurrent migrations (by utilizing a DB-based mutex) ## Installation diff --git a/migrate.go b/migrate.go index 4adf9c50..09e01dd9 100644 --- a/migrate.go +++ b/migrate.go @@ -29,6 +29,16 @@ var tableName = "gorp_migrations" var schemaName = "" var numberPrefixRegex = regexp.MustCompile(`^(\d+).*$`) +// Lock related bits +var ( + DefaultLockWaitTime = time.Duration(1 * time.Minute) + + lockTableName = "gorp_lock" + lockName = "sql-migrate" + lockWatchInterval = time.Duration(1 * time.Second) + lockMaxStaleAge = time.Duration(1 * time.Minute) +) + // TxError is returned when any error is encountered during a database // transaction. It contains the relevant *Migration and notes it's Id in the // Error function output. @@ -121,6 +131,11 @@ type MigrationRecord struct { AppliedAt time.Time `db:"applied_at"` } +type LockRecord struct { + Lock string `db:"lock"` + AcquiredAt time.Time `db:"acquired_at"` +} + var MigrationDialects = map[string]gorp.Dialect{ "sqlite3": gorp.SqliteDialect{}, "postgres": gorp.PostgresDialect{}, @@ -262,6 +277,58 @@ type SqlExecutor interface { Delete(list ...interface{}) (int64, error) } +// Wrapper for ExecMaxWithLock(); same behavior except max migrations is set to 0 (no limit) +func ExecWithLock(db *sql.DB, dialect string, m MigrationSource, dir MigrationDirection, waitTime time.Duration) (int, error) { + return ExecMaxWithLock(db, dialect, m, dir, 0, waitTime) +} + +// Perform a migration while utilizing a simple db-based mutex. +// +// This functionality is useful if you are running more than 1 instance of your +// app that performs in-app migrations. This will make sure the migrations do +// not collide with eachother. +// +// When using this functionality, a single `sql-migrate` instance will be designated +// as the 'master migrator'; other instances will stay in 'waitState' and will +// wait until the lock is either: +// +// * Released (lock record is removed from the `gorp_lock` table) +// * At which point, the 'waitState' migrators will exit cleanly (and not +// perform any migrations) +// +// OR +// +// * The 'waitTime' is exceeded, in which case, `sql-migrate` instances in `waitState` +// will return an error saying that they've exceeded the wait time. +// +// Finally, if for some reason your app crashes/gets killed before the lock was +// able to get cleaned up - the stale lock will be cleaned up on next start up. +// +// Note: If you are running into the latter case, considering bumping up the `waitTime`. +func ExecMaxWithLock(db *sql.DB, dialect string, m MigrationSource, dir MigrationDirection, max int, waitTime time.Duration) (int, error) { + if dialect == "sqlite3" { + return 0, errors.New("ExecWithLock does not support sqlite3 dialect") + } + + dbMap, err := getMigrationDbMap(db, dialect) + if err != nil { + return 0, fmt.Errorf("Unable to instantiate dbmap: %v", err) + } + + mlock, err := newMigrationLock(dbMap, waitTime) + // Skip ExecMax if we encountered an error during newMigrationLock() + if err != nil { + return 0, err + } + + // We are the master migrator so we must clean up our lock + if !mlock.waitState { + defer mlock.end() + } + + return ExecMax(db, dialect, m, dir, max) +} + // Execute a set of migrations // // Returns the number of applied migrations. @@ -269,6 +336,118 @@ func Exec(db *sql.DB, dialect string, m MigrationSource, dir MigrationDirection) return ExecMax(db, dialect, m, dir, 0) } +type migrationLock struct { + id int + dbMap *gorp.DbMap + waitState bool + waitTime time.Duration +} + +// * check for (and delete) outdated lock +// * insert lock in db +// * if insert fails, means an existing lock is in place; +// * set 'wait' to 'true' +// * periodically check for lock existance ("master migrator" should remove lock when done) +// * stop waiting if lock doesn't disappear +// * if insert succeeds, means we are the "master migrator" +// * return from beginLock() -> let ExecMax do its thing; +// * once ExecMax finishes, clean up our lock +func newMigrationLock(dbMap *gorp.DbMap, waitTime time.Duration) (*migrationLock, error) { + mlock := &migrationLock{ + id: time.Now().Nanosecond(), + dbMap: dbMap, + waitState: false, + waitTime: waitTime, + } + + // Remove potentially stale lock + if err := mlock.removeStaleLock(); err != nil { + return nil, err + } + + insertErr := mlock.dbMap.Insert(&LockRecord{ + Lock: lockName, + AcquiredAt: time.Now(), + }) + + if insertErr != nil { + // Insert failed, we are in 'wait' state; begin watching existing lock + mlock.waitState = true + + if err := mlock.beginWatch(); err != nil { + // We have exceeded 'waitTime', bail out + return nil, err + } + + // Lock was released; nothing to do + return mlock, nil + } + + // lock insertion succeeded, good to go + return mlock, nil +} + +// Remove a (potentially) stale lock +// +// Delete lock record if the lock is older than "now() - lockMaxStaleAge". +func (m *migrationLock) removeStaleLock() error { + maxDate := time.Now().Add(-lockMaxStaleAge) + + _, err := m.dbMap.Exec("DELETE FROM gorp_lock WHERE acquired_at <= ?", maxDate) + if err != nil { + return fmt.Errorf("Unable to remove stale lock: %v", err) + } + + return nil +} + +// Periodically check for the existence of a 'lock' record +// +// If the lock record disappears before the 'waitTime' is up, return no error. +// If 'waitTime' is exceeded, return a 'wait time exceeded' error. +func (m *migrationLock) beginWatch() error { + ticker := time.NewTicker(lockWatchInterval) + defer ticker.Stop() + + beginTime := time.Now() + + for { + <-ticker.C + + // Time waiting for lock clearance has elapsed + if time.Since(beginTime) > m.waitTime { + return fmt.Errorf("Exceeded lock clearance wait time (%v)", time.Since(beginTime)) + } + + var lockRecord LockRecord + + err := m.dbMap.SelectOne(&lockRecord, fmt.Sprintf("SELECT * FROM %v", lockTableName)) + if err != nil { + if err == sql.ErrNoRows { + break + } + + return err + } + } + + return nil +} + +// Remove lock record (if we are the 'master migrator') +func (m *migrationLock) end() { + // Nothing to do if we were in 'waitState' + if m.waitState { + return + } + + // perform lock clean up + _, err := m.dbMap.Delete(&LockRecord{Lock: lockName}) + if err != nil { + fmt.Printf("Ran into an error during lock cleanup: %v\n", err) + } +} + // Execute a set of migrations // // Will apply at most `max` migrations. Pass 0 for no limit (or use Exec). @@ -498,6 +677,9 @@ Check https://github.com/go-sql-driver/mysql#parsetime for more info.`) dbMap.AddTableWithNameAndSchema(MigrationRecord{}, schemaName, tableName).SetKeys(false, "Id") //dbMap.TraceOn("", log.New(os.Stdout, "migrate: ", log.Lmicroseconds)) + // Create lock table + dbMap.AddTableWithNameAndSchema(LockRecord{}, schemaName, lockTableName).SetKeys(false, "Lock").ColMap("Lock").SetUnique(true) + err := dbMap.CreateTablesIfNotExists() if err != nil { return nil, err diff --git a/migrate_mysql_test.go b/migrate_mysql_test.go new file mode 100644 index 00000000..1f2701cc --- /dev/null +++ b/migrate_mysql_test.go @@ -0,0 +1,525 @@ +package migrate + +import ( + "database/sql" + "flag" + "fmt" + "time" + + _ "github.com/go-sql-driver/mysql" + . "gopkg.in/check.v1" + "gopkg.in/gorp.v1" +) + +var enableMySQLFlag = flag.Bool("enable-mysql", false, "Perform mysql tests (default=false)") + +var ( + testDBName = "test_db" + testDBHost = "127.0.0.1" + testDBPort = "3306" + testDBUser = "root" + testDBPass = "" + + testDBDSN = fmt.Sprintf("%v:%v@tcp(%v:%v)/?parseTime=true&timeout=10s", + testDBUser, testDBPass, testDBHost, testDBPort) + + testDBFullDSN = fmt.Sprintf("%v:%v@tcp(%v:%v)/%v?parseTime=true&timeout=10s", + testDBUser, testDBPass, testDBHost, testDBPort, testDBName) +) + +var mysqlMigrations = []*Migration{ + &Migration{ + Id: "123", + Up: []string{"CREATE TABLE people (id int)"}, + Down: []string{"DROP TABLE people"}, + }, + &Migration{ + Id: "124", + Up: []string{"ALTER TABLE people ADD COLUMN first_name text"}, + Down: []string{"SELECT 0"}, // Not really supported + }, +} + +type MySQLMigrateSuite struct { + Db *sql.DB + DbMap *gorp.DbMap +} + +var _ = Suite(&MySQLMigrateSuite{}) + +// Drop initial DB (if found) +func (s *MySQLMigrateSuite) SetUpSuite(c *C) { + if !*enableMySQLFlag { + c.Skip("Skipping mysql tests due to -enable-mysql flag not being set") + } + + db, err := sql.Open("mysql", testDBDSN) + c.Assert(err, IsNil) + + db.Exec(fmt.Sprintf("DROP DATABASE `%v`", testDBName)) + c.Assert(db.Close(), IsNil) +} + +func (s *MySQLMigrateSuite) SetUpTest(c *C) { + // Initial connection without DB + initialDB, err := sql.Open("mysql", testDBDSN) + c.Assert(err, IsNil) + + _, dbCreateErr := initialDB.Exec(fmt.Sprintf("CREATE DATABASE `%v`", testDBName)) + c.Assert(dbCreateErr, IsNil) + + initialDB.Close() + + // final connect + db, err := sql.Open("mysql", testDBFullDSN) + c.Assert(err, IsNil) + + s.Db = db + s.DbMap = &gorp.DbMap{Db: db, Dialect: &gorp.MySQLDialect{}} +} + +func (s *MySQLMigrateSuite) TearDownTest(c *C) { + _, err := s.Db.Exec(fmt.Sprintf("DROP DATABASE `%v`", testDBName)) + c.Assert(err, IsNil) +} + +func (s *MySQLMigrateSuite) TestRunMigration(c *C) { + migrations := &MemoryMigrationSource{ + Migrations: mysqlMigrations[:1], + } + + // Executes one migration + n, err := Exec(s.Db, "mysql", migrations, Up) + c.Assert(err, IsNil) + c.Assert(n, Equals, 1) + + // Can use table now + _, err = s.DbMap.Exec("SELECT * FROM people") + c.Assert(err, IsNil) + + // Shouldn't apply migration again + n, err = Exec(s.Db, "mysql", migrations, Up) + c.Assert(err, IsNil) + c.Assert(n, Equals, 0) +} + +func (s *MySQLMigrateSuite) TestRunMigrationEscapeTable(c *C) { + migrations := &MemoryMigrationSource{ + Migrations: mysqlMigrations[:1], + } + + SetTable(`my migrations`) + + // Executes one migration + n, err := Exec(s.Db, "mysql", migrations, Up) + c.Assert(err, IsNil) + c.Assert(n, Equals, 1) +} + +func (s *MySQLMigrateSuite) TestMigrateMultiple(c *C) { + migrations := &MemoryMigrationSource{ + Migrations: mysqlMigrations[:2], + } + + // Executes two migrations + n, err := Exec(s.Db, "mysql", migrations, Up) + c.Assert(err, IsNil) + c.Assert(n, Equals, 2) + + // Can use column now + _, err = s.DbMap.Exec("SELECT first_name FROM people") + c.Assert(err, IsNil) +} + +type execResult struct { + n int + err error +} + +func (s *MySQLMigrateSuite) concurrentMigrate(useLock bool, waitTime time.Duration) []*execResult { + migrations := &MemoryMigrationSource{ + Migrations: mysqlMigrations[:2], + } + + numMigrate := 10 + errChannel := make(chan *execResult, numMigrate) + + for i := 1; i <= numMigrate; i++ { + go func() { + var n int + var err error + + if useLock { + n, err = ExecWithLock(s.Db, "mysql", migrations, Up, time.Duration(waitTime)) + } else { + n, err = Exec(s.Db, "mysql", migrations, Up) + } + + errChannel <- &execResult{ + n: n, + err: err, + } + }() + } + + var execResults []*execResult + + for i := 1; i <= numMigrate; i++ { + result := <-errChannel + execResults = append(execResults, result) + } + + return execResults +} + +func (s *MySQLMigrateSuite) TestConcurrentMigrateWithoutLock(c *C) { + results := s.concurrentMigrate(false, time.Duration(1*time.Second)) + + var errorFound bool + var badIndex int + + for i, v := range results { + if v.err != nil { + errorFound = true + badIndex = i + break + } + } + + // Concurrent migrates with Exec() should run into at least 1 failure + c.Assert(errorFound, Equals, true) + c.Assert(results[badIndex].err, NotNil) +} + +func (s *MySQLMigrateSuite) TestConcurrentMigrateWithLock(c *C) { + results := s.concurrentMigrate(true, time.Duration(5*time.Second)) + + var errorFound bool + + for _, v := range results { + if v.err != nil { + errorFound = true + } + } + + // Concurrent migrates with ExecWithLock() should NOT run into any errors + c.Assert(errorFound, Equals, false) +} + +func (s *MySQLMigrateSuite) TestConcurrentMigrateWithLockShortWaitTime(c *C) { + results := s.concurrentMigrate(true, time.Duration(500*time.Nanosecond)) + + var errorFound bool + var badIndex int + + for i, v := range results { + if v.err != nil { + errorFound = true + badIndex = i + } + } + + // Concurrent migrates with ExecWithLock but too low of a waittime should + // result in at least 1 failure + c.Assert(errorFound, Equals, true) + c.Assert(results[badIndex].err, ErrorMatches, "Exceeded lock clearance wait time.+") +} + +func (s *MySQLMigrateSuite) TestMigrateIncremental(c *C) { + migrations := &MemoryMigrationSource{ + Migrations: mysqlMigrations[:1], + } + + // Executes one migration + n, err := Exec(s.Db, "mysql", migrations, Up) + c.Assert(err, IsNil) + c.Assert(n, Equals, 1) + + // Execute a new migration + migrations = &MemoryMigrationSource{ + Migrations: mysqlMigrations[:2], + } + n, err = Exec(s.Db, "mysql", migrations, Up) + c.Assert(err, IsNil) + c.Assert(n, Equals, 1) + + // Can use column now + _, err = s.DbMap.Exec("SELECT first_name FROM people") + c.Assert(err, IsNil) +} + +func (s *MySQLMigrateSuite) TestFileMigrate(c *C) { + migrations := &FileMigrationSource{ + Dir: "test-migrations", + } + + // Executes two migrations + n, err := Exec(s.Db, "mysql", migrations, Up) + c.Assert(err, IsNil) + c.Assert(n, Equals, 2) + + // Has data + id, err := s.DbMap.SelectInt("SELECT id FROM people") + c.Assert(err, IsNil) + c.Assert(id, Equals, int64(1)) +} + +func (s *MySQLMigrateSuite) TestAssetMigrate(c *C) { + migrations := &AssetMigrationSource{ + Asset: Asset, + AssetDir: AssetDir, + Dir: "test-migrations", + } + + // Executes two migrations + n, err := Exec(s.Db, "mysql", migrations, Up) + c.Assert(err, IsNil) + c.Assert(n, Equals, 2) + + // Has data + id, err := s.DbMap.SelectInt("SELECT id FROM people") + c.Assert(err, IsNil) + c.Assert(id, Equals, int64(1)) +} + +func (s *MySQLMigrateSuite) TestMigrateMax(c *C) { + migrations := &FileMigrationSource{ + Dir: "test-migrations", + } + + // Executes one migration + n, err := ExecMax(s.Db, "mysql", migrations, Up, 1) + c.Assert(err, IsNil) + c.Assert(n, Equals, 1) + + id, err := s.DbMap.SelectInt("SELECT COUNT(*) FROM people") + c.Assert(err, IsNil) + c.Assert(id, Equals, int64(0)) +} + +func (s *MySQLMigrateSuite) TestMigrateDown(c *C) { + migrations := &FileMigrationSource{ + Dir: "test-migrations", + } + + n, err := Exec(s.Db, "mysql", migrations, Up) + c.Assert(err, IsNil) + c.Assert(n, Equals, 2) + + // Has data + id, err := s.DbMap.SelectInt("SELECT id FROM people") + c.Assert(err, IsNil) + c.Assert(id, Equals, int64(1)) + + // Undo the last one + n, err = ExecMax(s.Db, "mysql", migrations, Down, 1) + c.Assert(err, IsNil) + c.Assert(n, Equals, 1) + + // No more data + id, err = s.DbMap.SelectInt("SELECT COUNT(*) FROM people") + c.Assert(err, IsNil) + c.Assert(id, Equals, int64(0)) + + // Remove the table. + n, err = ExecMax(s.Db, "mysql", migrations, Down, 1) + c.Assert(err, IsNil) + c.Assert(n, Equals, 1) + + // Cannot query it anymore + _, err = s.DbMap.SelectInt("SELECT COUNT(*) FROM people") + c.Assert(err, Not(IsNil)) + + // Nothing left to do. + n, err = ExecMax(s.Db, "mysql", migrations, Down, 1) + c.Assert(err, IsNil) + c.Assert(n, Equals, 0) +} + +func (s *MySQLMigrateSuite) TestMigrateDownFull(c *C) { + migrations := &FileMigrationSource{ + Dir: "test-migrations", + } + + n, err := Exec(s.Db, "mysql", migrations, Up) + c.Assert(err, IsNil) + c.Assert(n, Equals, 2) + + // Has data + id, err := s.DbMap.SelectInt("SELECT id FROM people") + c.Assert(err, IsNil) + c.Assert(id, Equals, int64(1)) + + // Undo the last one + n, err = Exec(s.Db, "mysql", migrations, Down) + c.Assert(err, IsNil) + c.Assert(n, Equals, 2) + + // Cannot query it anymore + _, err = s.DbMap.SelectInt("SELECT COUNT(*) FROM people") + c.Assert(err, Not(IsNil)) + + // Nothing left to do. + n, err = Exec(s.Db, "mysql", migrations, Down) + c.Assert(err, IsNil) + c.Assert(n, Equals, 0) +} + +func (s *MySQLMigrateSuite) TestMigrateTransaction(c *C) { + migrations := &MemoryMigrationSource{ + Migrations: []*Migration{ + mysqlMigrations[0], + mysqlMigrations[1], + &Migration{ + Id: "125", + Up: []string{"INSERT INTO people (id, first_name) VALUES (1, 'Test')", "SELECT fail"}, + Down: []string{}, // Not important here + }, + }, + } + + // Should fail, transaction should roll back the INSERT. + n, err := Exec(s.Db, "mysql", migrations, Up) + c.Assert(err, Not(IsNil)) + c.Assert(n, Equals, 2) + + // INSERT should be rolled back + count, err := s.DbMap.SelectInt("SELECT COUNT(*) FROM people") + c.Assert(err, IsNil) + c.Assert(count, Equals, int64(0)) +} + +func (s *MySQLMigrateSuite) TestPlanMigration(c *C) { + migrations := &MemoryMigrationSource{ + Migrations: []*Migration{ + &Migration{ + Id: "1_create_table.sql", + Up: []string{"CREATE TABLE people (id int)"}, + Down: []string{"DROP TABLE people"}, + }, + &Migration{ + Id: "2_alter_table.sql", + Up: []string{"ALTER TABLE people ADD COLUMN first_name text"}, + Down: []string{"SELECT 0"}, // Not really supported + }, + &Migration{ + Id: "10_add_last_name.sql", + Up: []string{"ALTER TABLE people ADD COLUMN last_name text"}, + Down: []string{"ALTER TABLE people DROP COLUMN last_name"}, + }, + }, + } + n, err := Exec(s.Db, "mysql", migrations, Up) + c.Assert(err, IsNil) + c.Assert(n, Equals, 3) + + migrations.Migrations = append(migrations.Migrations, &Migration{ + Id: "11_add_middle_name.sql", + Up: []string{"ALTER TABLE people ADD COLUMN middle_name text"}, + Down: []string{"ALTER TABLE people DROP COLUMN middle_name"}, + }) + + plannedMigrations, _, err := PlanMigration(s.Db, "mysql", migrations, Up, 0) + c.Assert(err, IsNil) + c.Assert(plannedMigrations, HasLen, 1) + c.Assert(plannedMigrations[0].Migration, Equals, migrations.Migrations[3]) + + plannedMigrations, _, err = PlanMigration(s.Db, "mysql", migrations, Down, 0) + c.Assert(err, IsNil) + c.Assert(plannedMigrations, HasLen, 3) + c.Assert(plannedMigrations[0].Migration, Equals, migrations.Migrations[2]) + c.Assert(plannedMigrations[1].Migration, Equals, migrations.Migrations[1]) + c.Assert(plannedMigrations[2].Migration, Equals, migrations.Migrations[0]) +} + +func (s *MySQLMigrateSuite) TestPlanMigrationWithHoles(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}, + }, + }, + } + n, err := Exec(s.Db, "mysql", migrations, Up) + c.Assert(err, IsNil) + c.Assert(n, Equals, 2) + + migrations.Migrations = append(migrations.Migrations, &Migration{ + Id: "2", + Up: []string{up}, + Down: []string{down}, + }) + + migrations.Migrations = append(migrations.Migrations, &Migration{ + Id: "4", + Up: []string{up}, + Down: []string{down}, + }) + + migrations.Migrations = append(migrations.Migrations, &Migration{ + Id: "5", + Up: []string{up}, + Down: []string{down}, + }) + + // apply all the missing migrations + plannedMigrations, _, err := PlanMigration(s.Db, "mysql", migrations, Up, 0) + c.Assert(err, IsNil) + c.Assert(plannedMigrations, HasLen, 3) + c.Assert(plannedMigrations[0].Migration.Id, Equals, "2") + c.Assert(plannedMigrations[0].Queries[0], Equals, up) + c.Assert(plannedMigrations[1].Migration.Id, Equals, "4") + c.Assert(plannedMigrations[1].Queries[0], Equals, up) + c.Assert(plannedMigrations[2].Migration.Id, Equals, "5") + c.Assert(plannedMigrations[2].Queries[0], Equals, up) + + // first catch up to current target state 123, then migrate down 1 step to 12 + plannedMigrations, _, err = PlanMigration(s.Db, "mysql", migrations, Down, 1) + c.Assert(err, IsNil) + c.Assert(plannedMigrations, HasLen, 2) + c.Assert(plannedMigrations[0].Migration.Id, Equals, "2") + c.Assert(plannedMigrations[0].Queries[0], Equals, up) + c.Assert(plannedMigrations[1].Migration.Id, Equals, "3") + c.Assert(plannedMigrations[1].Queries[0], Equals, down) + + // first catch up to current target state 123, then migrate down 2 steps to 1 + plannedMigrations, _, err = PlanMigration(s.Db, "mysql", migrations, Down, 2) + c.Assert(err, IsNil) + c.Assert(plannedMigrations, HasLen, 3) + c.Assert(plannedMigrations[0].Migration.Id, Equals, "2") + c.Assert(plannedMigrations[0].Queries[0], Equals, up) + c.Assert(plannedMigrations[1].Migration.Id, Equals, "3") + c.Assert(plannedMigrations[1].Queries[0], Equals, down) + c.Assert(plannedMigrations[2].Migration.Id, Equals, "2") + c.Assert(plannedMigrations[2].Queries[0], Equals, down) +} + +func (s *MySQLMigrateSuite) 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 + c.Assert((Migration{Id: "1"}).Less(&Migration{Id: "a"}), Equals, true) // 1 less than a + c.Assert((Migration{Id: "a"}).Less(&Migration{Id: "1"}), Equals, false) // a not less than 1 + c.Assert((Migration{Id: "a"}).Less(&Migration{Id: "a"}), Equals, false) // a not less than a + c.Assert((Migration{Id: "1-a"}).Less(&Migration{Id: "1-b"}), Equals, true) // 1-a less than 1-b + c.Assert((Migration{Id: "1-b"}).Less(&Migration{Id: "1-a"}), Equals, false) // 1-b not less than 1-a + c.Assert((Migration{Id: "1"}).Less(&Migration{Id: "10"}), Equals, true) // 1 less than 10 + c.Assert((Migration{Id: "10"}).Less(&Migration{Id: "1"}), Equals, false) // 10 not less than 1 + c.Assert((Migration{Id: "1_foo"}).Less(&Migration{Id: "10_bar"}), Equals, true) // 1_foo not less than 1 + c.Assert((Migration{Id: "10_bar"}).Less(&Migration{Id: "1_foo"}), Equals, false) // 10 not less than 1 + // 20160126_1100 less than 20160126_1200 + c.Assert((Migration{Id: "20160126_1100"}). + Less(&Migration{Id: "20160126_1200"}), Equals, true) + // 20160126_1200 not less than 20160126_1100 + c.Assert((Migration{Id: "20160126_1200"}). + Less(&Migration{Id: "20160126_1100"}), Equals, false) + +} diff --git a/migrate_test.go b/migrate_test.go index 47f812e5..29dfacb5 100644 --- a/migrate_test.go +++ b/migrate_test.go @@ -2,14 +2,19 @@ package migrate import ( "database/sql" + "flag" + "fmt" "io/ioutil" "os" + "time" _ "github.com/mattn/go-sqlite3" . "gopkg.in/check.v1" "gopkg.in/gorp.v1" ) +var disableSQLiteFlag = flag.Bool("disable-sqlite", false, "Disable sqlite tests (default=true)") + var testDatabaseFile *os.File var sqliteMigrations = []*Migration{ &Migration{ @@ -31,6 +36,12 @@ type SqliteMigrateSuite struct { var _ = Suite(&SqliteMigrateSuite{}) +func (s *SqliteMigrateSuite) SetUpSuite(c *C) { + if *disableSQLiteFlag { + c.Skip("Skipping sqlite tests due to '-disable-sqlite' flag being set") + } +} + func (s *SqliteMigrateSuite) SetUpTest(c *C) { var err error testDatabaseFile, err = ioutil.TempFile("", "sql-migrate-sqlite") @@ -259,6 +270,69 @@ func (s *SqliteMigrateSuite) TestMigrateTransaction(c *C) { c.Assert(count, Equals, int64(0)) } +func (s *SqliteMigrateSuite) TestRemoveStaleLock(c *C) { + // create temp lock table + s.DbMap.AddTableWithNameAndSchema(LockRecord{}, "", lockTableName).SetKeys(false, "Lock").ColMap("Lock").SetUnique(true) + err := s.DbMap.CreateTablesIfNotExists() + c.Assert(err, IsNil) + + mlock := &migrationLock{ + dbMap: s.DbMap, + } + + // insert stale lock + s.DbMap.Insert(&LockRecord{ + Lock: lockName, + AcquiredAt: time.Now().Add(-(1 * time.Hour)), + }) + + err1 := mlock.removeStaleLock() + c.Assert(err1, IsNil) + + var lockRecord LockRecord + + selectErr1 := s.DbMap.SelectOne(&lockRecord, fmt.Sprintf("SELECT * FROM %v", lockTableName)) + + // Old lock should be removed + c.Assert(selectErr1, Equals, sql.ErrNoRows) + c.Assert(lockRecord.Lock, Equals, "") + + // insert non-expired lock + s.DbMap.Insert(&LockRecord{ + Lock: lockName, + AcquiredAt: time.Now(), + }) + + err = mlock.removeStaleLock() + c.Assert(err, IsNil) + + selectErr2 := s.DbMap.SelectOne(&lockRecord, fmt.Sprintf("SELECT * FROM %v", lockTableName)) + + c.Assert(selectErr2, IsNil) + c.Assert(lockRecord, NotNil) + c.Assert(lockRecord.Lock, Equals, lockName) +} + +func (s *SqliteMigrateSuite) TestExecMaxWithLock(c *C) { + migrations := &MemoryMigrationSource{ + Migrations: sqliteMigrations[:2], + } + + n, err := ExecMaxWithLock(s.Db, "sqlite3", migrations, Up, 0, time.Duration(1*time.Second)) + c.Assert(err, ErrorMatches, "ExecWithLock does not support sqlite3 dialect") + c.Assert(n, Equals, 0) +} + +func (s *SqliteMigrateSuite) TestExecWithLock(c *C) { + migrations := &MemoryMigrationSource{ + Migrations: sqliteMigrations[:2], + } + + n, err := ExecWithLock(s.Db, "sqlite3", migrations, Up, time.Duration(1*time.Second)) + c.Assert(err, ErrorMatches, "ExecWithLock does not support sqlite3 dialect") + c.Assert(n, Equals, 0) +} + func (s *SqliteMigrateSuite) TestPlanMigration(c *C) { migrations := &MemoryMigrationSource{ Migrations: []*Migration{