diff --git a/.gitignore b/.gitignore index ee2f41f..9bd9f47 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,11 @@ .DS_Store -test.db \ No newline at end of file +test.db +*.iml +.DS_Store +.idea +*.zip +*.tar.* +*.nuget +*.jar +*.war + diff --git a/driver/driver.go b/driver/driver.go index 5c2f6fc..a668d52 100644 --- a/driver/driver.go +++ b/driver/driver.go @@ -34,6 +34,15 @@ type Driver interface { Version() (uint64, error) } +// Driver that has some custom migration file format +// and wants to use a different parsing strategy. +type DriverWithFilenameParser interface { + + Driver + + FilenameParser() file.FilenameParser +} + // New returns Driver and calls Initialize on it func New(url string) (Driver, error) { u, err := neturl.Parse(url) diff --git a/driver/gomethods/gomethods_migrator.go b/driver/gomethods/gomethods_migrator.go new file mode 100644 index 0000000..68b9ff5 --- /dev/null +++ b/driver/gomethods/gomethods_migrator.go @@ -0,0 +1,176 @@ +package gomethods + +import ( + //"bytes" + "reflect" + "fmt" + "strings" + "os" + "path" + "bufio" + "github.com/dimag-jfrog/migrate/driver" + "github.com/dimag-jfrog/migrate/file" + "github.com/dimag-jfrog/migrate/migrate/direction" +) + + +type MissingMethodError string +func (e MissingMethodError) Error() string { return "Non existing migrate method: " + string(e) } + + +type WrongMethodSignatureError string +func (e WrongMethodSignatureError) Error() string { return fmt.Sprintf("Method %s has wrong signature", e) } + +type MethodInvocationFailedError struct { + MethodName string + Err error +} + +func (e *MethodInvocationFailedError) Error() string { + return fmt.Sprintf("Method %s returned an error: %v", e.MethodName, e.Error) +} + + +type Migrator struct { + Driver driver.DriverWithFilenameParser + RollbackOnFailure bool +} + +func (m *Migrator) Migrate(f file.File, pipe chan interface{}) error { + methods, err := m.getMigrationMethods(f) + if err != nil { + pipe <- err + return err + } + + for i, methodName := range methods { + pipe <- methodName + err := m.Invoke(methodName) + if err != nil { + pipe <- err + if !m.RollbackOnFailure { + return err + } + + // on failure, try to rollback methods in this migration + for j := i-1; j >= 0; j-- { + rollbackToMethodName := getRollbackToMethod(methods[j]) + pipe <- rollbackToMethodName + err = m.Invoke(rollbackToMethodName) + if err != nil { + pipe <- err + break + } + } + return err + } + } + + return nil +} + +func (m *Migrator) IsValid(methodName string) bool { + return reflect.ValueOf(m.Driver).MethodByName(methodName).IsValid() +} + +func (m *Migrator) Invoke(methodName string) error { + name := methodName + migrateMethod := reflect.ValueOf(m.Driver).MethodByName(name) + if !migrateMethod.IsValid() { + return MissingMethodError(methodName) + } + + retValues := migrateMethod.Call([]reflect.Value{}) + if len(retValues) != 1 { + return WrongMethodSignatureError(name) + } + + if !retValues[0].IsNil() { + err, ok := retValues[0].Interface().(error) + if !ok { + return WrongMethodSignatureError(name) + } + return &MethodInvocationFailedError{ MethodName:name, Err:err} + } + + return nil +} + +func reverseInPlace(a []string) { + for i := 0; i < len(a)/2; i++ { + j := len(a) - i - 1 + a[i], a[j] = a[j], a[i] + } +} + +func getRollbackToMethod(methodName string) string { + if strings.HasSuffix(methodName, "_up") { + return strings.TrimSuffix(methodName, "_up") + "_down" + } else { + return strings.TrimSuffix(methodName, "_down") + "_up" + } +} + +func getFileLines(file file.File) ([]string, error) { + if len(file.Content) == 0 { + lines := make([]string, 0) + file, err := os.Open(path.Join(file.Path, file.FileName)) + if err != nil { + return nil, err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + return lines, nil + } else { + //n := bytes.IndexByte(file.Content, 0) + //n := bytes.Index(file.Content, []byte{0}) + //s := string(file.Content[:n]) + s := string(file.Content) + return strings.Split(s, "\n"), nil + } +} + +func (m *Migrator) getMigrationMethods(f file.File) ([]string, error) { + var lines, methods []string + lines, err := getFileLines(f) + if err != nil { + return nil, err + } + + for _, line := range lines { + operationName := strings.TrimSpace(line) + + if operationName == "" || strings.HasPrefix(operationName, "--") { + // an empty line or a comment, ignore + continue + } + + upMethodName := operationName + "_up" + downMethodName := operationName + "_down" + + if !m.IsValid(upMethodName) { + return nil, MissingMethodError(upMethodName) + } + if !m.IsValid(downMethodName) { + return nil, MissingMethodError(downMethodName) + } + + if f.Direction == direction.Up { + methods = append(methods, upMethodName) + } else { + methods = append(methods, downMethodName) + } + } + + _,_,fileType,_ := m.Driver.FilenameParser().Parse(f.FileName) + if fileType == direction.Both && f.Direction == direction.Down { + reverseInPlace(methods) + } + return methods, nil + +} diff --git a/driver/gomethods/gomethods_migrator_test.go b/driver/gomethods/gomethods_migrator_test.go new file mode 100644 index 0000000..58fd4f2 --- /dev/null +++ b/driver/gomethods/gomethods_migrator_test.go @@ -0,0 +1,342 @@ +package gomethods + +import ( + "reflect" + "testing" + + "github.com/dimag-jfrog/migrate/file" + "github.com/dimag-jfrog/migrate/migrate/direction" + + pipep "github.com/dimag-jfrog/migrate/pipe" +) + +type FakeGoMethodsDriver struct { + InvokedMethods []string + Migrator Migrator +} + +func (driver *FakeGoMethodsDriver) Initialize(url string) error { + return nil +} + +func (driver *FakeGoMethodsDriver) Close() error { + return nil +} + +func (driver *FakeGoMethodsDriver) FilenameParser() file.FilenameParser { + return file.UpDownAndBothFilenameParser{ FilenameExtension: driver.FilenameExtension() } +} + +func (driver *FakeGoMethodsDriver) FilenameExtension() string { + return "gm" +} + +func (driver *FakeGoMethodsDriver) Version() (uint64, error) { + return uint64(0), nil +} + +func (driver *FakeGoMethodsDriver) Migrate(f file.File, pipe chan interface{}) { + defer close(pipe) + pipe <- f + return +} + +func (driver *FakeGoMethodsDriver) V001_init_organizations_up() error { + driver.InvokedMethods = append(driver.InvokedMethods, "V001_init_organizations_up") + return nil +} + +func (driver *FakeGoMethodsDriver) V001_init_organizations_down() error { + driver.InvokedMethods = append(driver.InvokedMethods, "V001_init_organizations_down") + return nil + +} + +func (driver *FakeGoMethodsDriver) V001_init_users_up() error { + driver.InvokedMethods = append(driver.InvokedMethods, "V001_init_users_up") + return nil +} + +func (driver *FakeGoMethodsDriver) V001_init_users_down() error { + driver.InvokedMethods = append(driver.InvokedMethods, "V001_init_users_down") + return nil +} + +type SomeError struct{} +func (e SomeError) Error() string { return "Some error happened" } + +func (driver *FakeGoMethodsDriver) V001_some_failing_method_up() error { + driver.InvokedMethods = append(driver.InvokedMethods, "V001_some_failing_method_up") + return SomeError{} +} + +func (driver *FakeGoMethodsDriver) V001_some_failing_method_down() error { + driver.InvokedMethods = append(driver.InvokedMethods, "V001_some_failing_method_down") + return SomeError{} +} + +func TestMigrate(t *testing.T) { + cases := []struct { + name string + file file.File + expectedInvokedMethods []string + expectedErrors []error + expectRollback bool + }{ + { + name: "up migration, both directions-file: invokes up methods in order", + file: file.File { + Path: "/foobar", + FileName: "001_foobar.gm", + Version: 1, + Name: "foobar", + Direction: direction.Up, + Content: []byte(` + V001_init_organizations + V001_init_users + `), + }, + expectedInvokedMethods: []string{"V001_init_organizations_up", "V001_init_users_up"}, + expectedErrors: []error{}, + }, + { + name: "down migration, both-directions-file: reverts direction of invoked down methods", + file: file.File { + Path: "/foobar", + FileName: "001_foobar.gm", + Version: 1, + Name: "foobar", + Direction: direction.Down, + Content: []byte(` + V001_init_organizations + V001_init_users + `), + }, + expectedInvokedMethods: []string{"V001_init_users_down", "V001_init_organizations_down"}, + expectedErrors: []error{}, + }, + { + name: "up migration, up direction-file: invokes up methods in order", + file: file.File { + Path: "/foobar", + FileName: "001_foobar.up.gm", + Version: 1, + Name: "foobar", + Direction: direction.Up, + Content: []byte(` + V001_init_organizations + V001_init_users + `), + }, + expectedInvokedMethods: []string{"V001_init_organizations_up", "V001_init_users_up"}, + expectedErrors: []error{}, + }, + { + name: "down migration, down directions-file: keeps order of invoked down methods", + file: file.File { + Path: "/foobar", + FileName: "001_foobar.down.gm", + Version: 1, + Name: "foobar", + Direction: direction.Down, + Content: []byte(` + V001_init_organizations + V001_init_users + `), + }, + expectedInvokedMethods: []string{"V001_init_organizations_down", "V001_init_users_down"}, + expectedErrors: []error{}, + }, + { + name: "up migration: non-existing method causes migration not to execute", + file: file.File { + Path: "/foobar", + FileName: "001_foobar.gm", + Version: 1, + Name: "foobar", + Direction: direction.Up, + Content: []byte(` + V001_init_organizations + V001_init_users + V001_some_non_existing_method + `), + }, + expectedInvokedMethods: []string{}, + expectedErrors: []error{ MissingMethodError("V001_some_non_existing_method_up") }, + }, + { + name: "up migration: failing method stops execution", + file: file.File { + Path: "/foobar", + FileName: "001_foobar.gm", + Version: 1, + Name: "foobar", + Direction: direction.Up, + Content: []byte(` + V001_init_organizations + V001_some_failing_method + V001_init_users + `), + }, + expectedInvokedMethods: []string{ + "V001_init_organizations_up", + "V001_some_failing_method_up", + }, + expectedErrors: []error{ &MethodInvocationFailedError{ + MethodName: "V001_some_failing_method_up", + Err: SomeError{}, + }}, + }, + { + name: "down migration, both-directions-file: failing method stops migration", + file: file.File { + Path: "/foobar", + FileName: "001_foobar.gm", + Version: 1, + Name: "foobar", + Direction: direction.Down, + Content: []byte(` + V001_init_organizations + V001_some_failing_method + V001_init_users + `), + }, + expectedInvokedMethods: []string{ + "V001_init_users_down", + "V001_some_failing_method_down", + }, + expectedErrors: []error{ &MethodInvocationFailedError{ + MethodName: "V001_some_failing_method_down", + Err: SomeError{}, + }}, + }, + { + name: "up migration: failing method causes rollback in rollback mode", + expectRollback: true, + file: file.File { + Path: "/foobar", + FileName: "001_foobar.gm", + Version: 1, + Name: "foobar", + Direction: direction.Up, + Content: []byte(` + V001_init_organizations + V001_init_users + V001_some_failing_method + `), + }, + expectedInvokedMethods: []string{ + "V001_init_organizations_up", + "V001_init_users_up", + "V001_some_failing_method_up", + "V001_init_users_down", + "V001_init_organizations_down", + }, + expectedErrors: []error{ &MethodInvocationFailedError{ + MethodName: "V001_some_failing_method_up", + Err: SomeError{}, + }}, + }, + { + name: "down migration, both-directions-file: failing method causes rollback in rollback mode", + expectRollback: true, + file: file.File { + Path: "/foobar", + FileName: "001_foobar.gm", + Version: 1, + Name: "foobar", + Direction: direction.Down, + Content: []byte(` + V001_init_organizations + V001_some_failing_method + V001_init_users + `), + }, + expectedInvokedMethods: []string{ + "V001_init_users_down", + "V001_some_failing_method_down", + "V001_init_users_up", + }, + expectedErrors: []error{ &MethodInvocationFailedError{ + MethodName: "V001_some_failing_method_down", + Err: SomeError{}, + }}, + }, + + } + + for _, c := range cases { + migrator := Migrator{} + d := &FakeGoMethodsDriver{Migrator: migrator, InvokedMethods:[]string{}} + migrator.Driver = d + migrator.RollbackOnFailure = c.expectRollback + + pipe := pipep.New() + go func() { + migrator.Migrate(c.file, pipe) + close(pipe) + }() + errs := pipep.ReadErrors(pipe) + + var failed bool + if !reflect.DeepEqual(d.InvokedMethods, c.expectedInvokedMethods) { + failed = true + t.Errorf("case '%s': FAILED\nexpected invoked methods %v\nbut got %v", c.name, c.expectedInvokedMethods, d.InvokedMethods) + } + if !reflect.DeepEqual(errs, c.expectedErrors) { + failed = true + t.Errorf("case '%s': FAILED\nexpected errors %v\nbut got %v", c.name, c.expectedErrors, errs) + + } + if !failed { + t.Logf("case '%s': PASSED", c.name) + } + } +} + + + +func TestGetRollbackToMethod(t *testing.T) { + cases := []struct { + method string + expectedRollbackMethod string + }{ + {"some_method_up", "some_method_down"}, + {"some_method_down", "some_method_up"}, + {"up_down_up", "up_down_down"}, + {"down_up", "down_down"}, + {"down_down", "down_up"}, + } + + for _, c := range cases { + actualRollbackMethod := getRollbackToMethod(c.method) + if actualRollbackMethod != c.expectedRollbackMethod { + t.Errorf("Expected rollback method to be %s but got %s", c.expectedRollbackMethod, actualRollbackMethod) + } + } +} + +func TestReverseInPlace(t *testing.T) { + methods := []string { + "method1_down", + "method2_down", + "method3_down", + "method4_down", + "method5_down", + } + + expectedReversedMethods := []string { + "method5_down", + "method4_down", + "method3_down", + "method2_down", + "method1_down", + } + + reverseInPlace(methods) + + if !reflect.DeepEqual(methods, expectedReversedMethods) { + t.Errorf("Expected reverse methods %v but got %v", expectedReversedMethods, methods) + } +} + diff --git a/driver/gomethods/mongodb/mongodb_template.go b/driver/gomethods/mongodb/mongodb_template.go new file mode 100644 index 0000000..48817cf --- /dev/null +++ b/driver/gomethods/mongodb/mongodb_template.go @@ -0,0 +1,111 @@ +package mongodb + +import ( + "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" + "errors" + "strings" + "github.com/dimag-jfrog/migrate/migrate/direction" + "github.com/dimag-jfrog/migrate/file" + "github.com/dimag-jfrog/migrate/driver/gomethods" +) + +const MIGRATE_C = "db_migrations" + + +// This is not a real driver since the Initialize method requires a gomethods.Migrator +// The real driver will contain the DriverTemplate and implement all the custom migration Golang methods +// See example in usage_examples for details +type DriverTemplate struct { + Session *mgo.Session + DbName string + + migrator gomethods.Migrator +} + + +type DbMigration struct { + Id bson.ObjectId `bson:"_id,omitempty"` + Version uint64 `bson:"version"` +} + +func (driver *DriverTemplate) Initialize(url, dbName string, migrator gomethods.Migrator) error { + urlWithoutScheme := strings.SplitN(url, "mongodb://", 2) + if len(urlWithoutScheme) != 2 { + return errors.New("invalid mongodb:// scheme") + } + + session, err := mgo.Dial(url) + if err != nil { + return err + } + session.SetMode(mgo.Monotonic, true) + + driver.Session = session + driver.DbName = dbName + driver.migrator = migrator + + return nil +} + +func (driver *DriverTemplate) Close() error { + if driver.Session != nil { + driver.Session.Close() + } + return nil +} + +func (driver *DriverTemplate) FilenameParser() file.FilenameParser { + return file.UpDownAndBothFilenameParser{FilenameExtension: driver.FilenameExtension()} +} + +func (driver *DriverTemplate) FilenameExtension() string { + return "mgo" +} + + +func (driver *DriverTemplate) Version() (uint64, error) { + var latestMigration DbMigration + c := driver.Session.DB(driver.DbName).C(MIGRATE_C) + + + err := c.Find(bson.M{}).Sort("-version").One(&latestMigration) + + switch { + case err == mgo.ErrNotFound: + return 0, nil + case err != nil: + return 0, err + default: + return latestMigration.Version, nil + } +} +func (driver *DriverTemplate) Migrate(f file.File, pipe chan interface{}) { + defer close(pipe) + pipe <- f + + err := driver.migrator.Migrate(f, pipe) + if err != nil { + return + } + + migrate_c := driver.Session.DB(driver.DbName).C(MIGRATE_C) + + if f.Direction == direction.Up { + id := bson.NewObjectId() + dbMigration := DbMigration{Id: id, Version: f.Version} + + err := migrate_c.Insert(dbMigration) + if err != nil { + pipe <- err + return + } + + } else if f.Direction == direction.Down { + err := migrate_c.Remove(bson.M{"version": f.Version}) + if err != nil { + pipe <- err + return + } + } +} diff --git a/driver/gomethods/usage_examples/mongodb.go b/driver/gomethods/usage_examples/mongodb.go new file mode 100644 index 0000000..be0ccb2 --- /dev/null +++ b/driver/gomethods/usage_examples/mongodb.go @@ -0,0 +1,121 @@ +package usage_examples + +import ( + "github.com/dimag-jfrog/migrate/driver" + "github.com/dimag-jfrog/migrate/driver/gomethods" + "github.com/dimag-jfrog/migrate/driver/gomethods/mongodb" + "gopkg.in/mgo.v2/bson" + "time" +) + +// This boilerplate part is necessary and the same +// regardless of the specific mongodb golang methods driver + +type GoMethodsMongoDbDriver struct { + mongodb.DriverTemplate +} + +func (d *GoMethodsMongoDbDriver) Initialize(url string) error { + return d.DriverTemplate.Initialize(url, DB_NAME, gomethods.Migrator{Driver: d}) +} + +func init() { + driver.RegisterDriver("mongodb", &GoMethodsMongoDbDriver{mongodb.DriverTemplate{}}) +} + + + +// Here goes the specific mongodb golang methods driver logic + +const DB_NAME = "test" +const SHORT_DATE_LAYOUT = "2000-Jan-01" +const USERS_C = "users" +const ORGANIZATIONS_C = "organizations" + +type Organization struct { + Id bson.ObjectId `bson:"_id,omitempty"` + Name string `bson:"name"` + Location string `bson:"location"` + DateFounded time.Time `bson:"date_founded"` +} + +type User struct { + Id bson.ObjectId `bson:"_id"` + Name string `bson:"name"` +} + +func (m *GoMethodsMongoDbDriver) V001_init_organizations_up() error { + date1, _ := time.Parse(SHORT_DATE_LAYOUT, "1994-Jul-05") + date2, _ := time.Parse(SHORT_DATE_LAYOUT, "1998-Sep-04") + date3, _ := time.Parse(SHORT_DATE_LAYOUT, "2008-Apr-28") + + orgs := []Organization{ + {Id: bson.NewObjectId(), Name: "Amazon", Location:"Seattle", DateFounded: date1}, + {Id: bson.NewObjectId(), Name: "Google", Location:"Mountain View", DateFounded: date2}, + {Id: bson.NewObjectId(), Name: "JFrog", Location:"Santa Clara", DateFounded: date3}, + } + + for _, org := range orgs { + err := m.Session.DB(DB_NAME).C(ORGANIZATIONS_C).Insert(org) + if err != nil { + return err + } + } + return nil +} + +func (m *GoMethodsMongoDbDriver) V001_init_organizations_down() error { + return m.Session.DB(DB_NAME).C(ORGANIZATIONS_C).DropCollection() +} + +func (m *GoMethodsMongoDbDriver) V001_init_users_up() error { + users := []User{ + {Id: bson.NewObjectId(), Name: "Alex"}, + {Id: bson.NewObjectId(), Name: "Beatrice"}, + {Id: bson.NewObjectId(), Name: "Cleo"}, + } + + for _, user := range users { + err := m.Session.DB(DB_NAME).C(USERS_C).Insert(user) + if err != nil { + return err + } + } + return nil +} + +func (m *GoMethodsMongoDbDriver) V001_init_users_down() error { + return m.Session.DB(DB_NAME).C(USERS_C).DropCollection() +} + +func (m *GoMethodsMongoDbDriver) V002_organizations_rename_location_field_to_headquarters_up() error { + c := m.Session.DB(DB_NAME).C(ORGANIZATIONS_C) + + _, err := c.UpdateAll(nil, bson.M{"$rename": bson.M{"location": "headquarters"}}) + return err +} + +func (m *GoMethodsMongoDbDriver) V002_organizations_rename_location_field_to_headquarters_down() error { + c := m.Session.DB(DB_NAME).C(ORGANIZATIONS_C) + + _, err := c.UpdateAll(nil, bson.M{"$rename": bson.M{"headquarters": "location"}}) + return err +} + +func (m *GoMethodsMongoDbDriver) V002_change_user_cleo_to_cleopatra_up() error { + c := m.Session.DB(DB_NAME).C(USERS_C) + + colQuerier := bson.M{"name": "Cleo"} + change := bson.M{"$set": bson.M{"name": "Cleopatra"}} + + return c.Update(colQuerier, change) +} + +func (m *GoMethodsMongoDbDriver) V002_change_user_cleo_to_cleopatra_down() error { + c := m.Session.DB(DB_NAME).C(USERS_C) + + colQuerier := bson.M{"name": "Cleopatra"} + change := bson.M{"$set": bson.M{"name": "Cleo",}} + + return c.Update(colQuerier, change) +} \ No newline at end of file diff --git a/driver/gomethods/usage_examples/mongodb_test.go b/driver/gomethods/usage_examples/mongodb_test.go new file mode 100644 index 0000000..ec1a85d --- /dev/null +++ b/driver/gomethods/usage_examples/mongodb_test.go @@ -0,0 +1,123 @@ +package usage_examples + +import ( + "testing" + + "github.com/dimag-jfrog/migrate/file" + "github.com/dimag-jfrog/migrate/migrate/direction" + + pipep "github.com/dimag-jfrog/migrate/pipe" +) + + + +func TestMigrate(t *testing.T) { + //host := os.Getenv("MONGODB_PORT_27017_TCP_ADDR") + //port := os.Getenv("MONGODB_PORT_27017_TCP_PORT") + host := "127.0.0.1" + port := "27017" + driverUrl := "mongodb://" + host + ":" + port + + d := &GoMethodsMongoDbDriver{} + if err := d.Initialize(driverUrl); err != nil { + t.Fatal(err) + } + + content1 := []byte(` + V001_init_organizations + V001_init_users + `) + content2 := []byte(` + V002_organizations_rename_location_field_to_headquarters + V002_change_user_cleo_to_cleopatra + `) + + files := []file.File{ + { + Path: "/foobar", + FileName: "001_foobar.mgo", + Version: 1, + Name: "foobar", + Direction: direction.Up, + Content: content1, + }, + { + Path: "/foobar", + FileName: "001_foobar.mgo", + Version: 1, + Name: "foobar", + Direction: direction.Down, + Content: content1, + }, + { + Path: "/foobar", + FileName: "002_foobar.mgo", + Version: 2, + Name: "foobar", + Direction: direction.Up, + Content: content2, + }, + { + Path: "/foobar", + FileName: "002_foobar.mgo", + Version: 2, + Name: "foobar", + Direction: direction.Down, + Content: content2, + }, + { + Path: "/foobar", + FileName: "001_foobar.mgo", + Version: 1, + Name: "foobar", + Direction: direction.Up, + Content: []byte(` + V001_init_organizations + V001_init_users + V001_non_existing_operation + `), + }, + } + + var pipe chan interface{} + var errs []error + + pipe = pipep.New() + go d.Migrate(files[0], pipe) + errs = pipep.ReadErrors(pipe) + if len(errs) > 0 { + t.Fatal(errs) + } + + pipe = pipep.New() + go d.Migrate(files[2], pipe) + errs = pipep.ReadErrors(pipe) + if len(errs) > 0 { + t.Fatal(errs) + } + + pipe = pipep.New() + go d.Migrate(files[3], pipe) + errs = pipep.ReadErrors(pipe) + if len(errs) > 0 { + t.Fatal(errs) + } + + //pipe = pipep.New() + //go d.Migrate(files[1], pipe) + //errs = pipep.ReadErrors(pipe) + //if len(errs) > 0 { + // t.Fatal(errs) + //} + + pipe = pipep.New() + go d.Migrate(files[4], pipe) + errs = pipep.ReadErrors(pipe) + if len(errs) == 0 { + t.Error("Expected test case to fail") + } + + if err := d.Close(); err != nil { + t.Fatal(err) + } +} diff --git a/file/file.go b/file/file.go index 14617ba..794e951 100644 --- a/file/file.go +++ b/file/file.go @@ -9,20 +9,11 @@ import ( "go/token" "io/ioutil" "path" - "regexp" "sort" "strconv" "strings" ) -var filenameRegex = `^([0-9]+)_(.*)\.(up|down)\.%s$` - -// FilenameRegex builds regular expression stmt with given -// filename extension from driver. -func FilenameRegex(filenameExtension string) *regexp.Regexp { - return regexp.MustCompile(fmt.Sprintf(filenameRegex, filenameExtension)) -} - // File represents one file on disk. // Example: 001_initial_plan_to_do_sth.up.sql type File struct { @@ -149,8 +140,17 @@ func (mf *MigrationFiles) From(version uint64, relativeN int) (Files, error) { return files, nil } + +func ReadMigrationFiles(path, filenameExtension string) (MigrationFiles, error){ + return doReadMigrationFiles(path, DefaultFilenameParser{FilenameExtension: filenameExtension}) +} + +func ReadMigrationFilesWithFilenameParser(path string, filenameParser FilenameParser) (MigrationFiles, error){ + return doReadMigrationFiles(path, filenameParser) +} + // ReadMigrationFiles reads all migration files from a given path -func ReadMigrationFiles(path string, filenameRegex *regexp.Regexp) (files MigrationFiles, err error) { +func doReadMigrationFiles(path string, filenameParser FilenameParser) (files MigrationFiles, err error) { // find all migration files in path ioFiles, err := ioutil.ReadDir(path) if err != nil { @@ -165,7 +165,7 @@ func ReadMigrationFiles(path string, filenameRegex *regexp.Regexp) (files Migrat tmpFiles := make([]*tmpFile, 0) tmpFileMap := map[uint64]map[direction.Direction]tmpFile{} for _, file := range ioFiles { - version, name, d, err := parseFilenameSchema(file.Name(), filenameRegex) + version, name, d, err := filenameParser.Parse(file.Name()) if err == nil { if _, ok := tmpFileMap[version]; !ok { tmpFileMap[version] = map[direction.Direction]tmpFile{} @@ -210,33 +210,56 @@ func ReadMigrationFiles(path string, filenameRegex *regexp.Regexp) (files Migrat Direction: direction.Down, } lookFordirection = direction.Up + case direction.Both: + migrationFile.UpFile = &File{ + Path: path, + FileName: file.filename, + Version: file.version, + Name: file.name, + Content: nil, + Direction: direction.Up, + } + migrationFile.DownFile = &File{ + Path: path, + FileName: file.filename, + Version: file.version, + Name: file.name, + Content: nil, + Direction: direction.Down, + } default: return nil, errors.New("Unsupported direction.Direction Type") } for _, file2 := range tmpFiles { - if file2.version == file.version && file2.d == lookFordirection { - switch lookFordirection { - case direction.Up: - migrationFile.UpFile = &File{ - Path: path, - FileName: file2.filename, - Version: file.version, - Name: file2.name, - Content: nil, - Direction: direction.Up, + if file2.version == file.version { + if file.d == direction.Both { + if file.d != file2.d { + return nil, errors.New("Incompatible direction.Direction types") } - case direction.Down: - migrationFile.DownFile = &File{ - Path: path, - FileName: file2.filename, - Version: file.version, - Name: file2.name, - Content: nil, - Direction: direction.Down, + } else if file2.d == lookFordirection { + switch lookFordirection { + case direction.Up: + migrationFile.UpFile = &File{ + Path: path, + FileName: file2.filename, + Version: file.version, + Name: file2.name, + Content: nil, + Direction: direction.Up, + } + case direction.Down: + migrationFile.DownFile = &File{ + Path: path, + FileName: file2.filename, + Version: file.version, + Name: file2.name, + Content: nil, + Direction: direction.Down, + } } + break } - break } } @@ -249,29 +272,6 @@ func ReadMigrationFiles(path string, filenameRegex *regexp.Regexp) (files Migrat return newFiles, nil } -// parseFilenameSchema parses the filename -func parseFilenameSchema(filename string, filenameRegex *regexp.Regexp) (version uint64, name string, d direction.Direction, err error) { - matches := filenameRegex.FindStringSubmatch(filename) - if len(matches) != 4 { - return 0, "", 0, errors.New("Unable to parse filename schema") - } - - version, err = strconv.ParseUint(matches[1], 10, 0) - if err != nil { - return 0, "", 0, errors.New(fmt.Sprintf("Unable to parse version '%v' in filename schema", matches[0])) - } - - if matches[3] == "up" { - d = direction.Up - } else if matches[3] == "down" { - d = direction.Down - } else { - return 0, "", 0, errors.New(fmt.Sprintf("Unable to parse up|down '%v' in filename schema", matches[3])) - } - - return version, matches[2], d, nil -} - // Len is the number of elements in the collection. // Required by Sort Interface{} func (mf MigrationFiles) Len() int { diff --git a/file/file_test.go b/file/file_test.go index c6bd0b3..648f554 100644 --- a/file/file_test.go +++ b/file/file_test.go @@ -8,52 +8,6 @@ import ( "testing" ) -func TestParseFilenameSchema(t *testing.T) { - var tests = []struct { - filename string - filenameExtension string - expectVersion uint64 - expectName string - expectDirection direction.Direction - expectErr bool - }{ - {"001_test_file.up.sql", "sql", 1, "test_file", direction.Up, false}, - {"001_test_file.down.sql", "sql", 1, "test_file", direction.Down, false}, - {"10034_test_file.down.sql", "sql", 10034, "test_file", direction.Down, false}, - {"-1_test_file.down.sql", "sql", 0, "", direction.Up, true}, - {"test_file.down.sql", "sql", 0, "", direction.Up, true}, - {"100_test_file.down", "sql", 0, "", direction.Up, true}, - {"100_test_file.sql", "sql", 0, "", direction.Up, true}, - {"100_test_file", "sql", 0, "", direction.Up, true}, - {"test_file", "sql", 0, "", direction.Up, true}, - {"100", "sql", 0, "", direction.Up, true}, - {".sql", "sql", 0, "", direction.Up, true}, - {"up.sql", "sql", 0, "", direction.Up, true}, - {"down.sql", "sql", 0, "", direction.Up, true}, - } - - for _, test := range tests { - version, name, migrate, err := parseFilenameSchema(test.filename, FilenameRegex(test.filenameExtension)) - if test.expectErr && err == nil { - t.Fatal("Expected error, but got none.", test) - } - if !test.expectErr && err != nil { - t.Fatal("Did not expect error, but got one:", err, test) - } - if err == nil { - if version != test.expectVersion { - t.Error("Wrong version number", test) - } - if name != test.expectName { - t.Error("wrong name", test) - } - if migrate != test.expectDirection { - t.Error("wrong migrate", test) - } - } - } -} - func TestFiles(t *testing.T) { tmpdir, err := ioutil.TempDir("/tmp", "TestLookForMigrationFilesInSearchPath") if err != nil { @@ -77,7 +31,7 @@ func TestFiles(t *testing.T) { ioutil.WriteFile(path.Join(tmpdir, "401_migrationfile.down.sql"), []byte("test"), 0755) - files, err := ReadMigrationFiles(tmpdir, FilenameRegex("sql")) + files, err := ReadMigrationFiles(tmpdir, "sql") if err != nil { t.Fatal(err) } @@ -223,7 +177,7 @@ func TestDuplicateFiles(t *testing.T) { t.Fatal(err) } - _, err = ReadMigrationFiles(root, FilenameRegex("sql")) + _, err = ReadMigrationFiles(root, "sql") if err == nil { t.Fatal("Expected duplicate migration file error") } diff --git a/file/filename_parser.go b/file/filename_parser.go new file mode 100644 index 0000000..6fb8521 --- /dev/null +++ b/file/filename_parser.go @@ -0,0 +1,85 @@ +package file + +import ( + "errors" + "github.com/dimag-jfrog/migrate/migrate/direction" + "regexp" + "fmt" + "strconv" + "strings" +) + + +type FilenameParser interface { + Parse (filename string) (version uint64, name string, d direction.Direction, err error) +} + + +var defaultFilenameRegexTemplate = `^([0-9]+)_(.*)\.(up|down)\.%s$` + +func parseDefaultFilenameSchema(filename, filenameRegex string) (version uint64, name string, d direction.Direction, err error) { + regexp := regexp.MustCompile(filenameRegex) + matches := regexp.FindStringSubmatch(filename) + if len(matches) != 4 { + return 0, "", 0, errors.New("Unable to parse filename schema") + } + + version, err = strconv.ParseUint(matches[1], 10, 0) + if err != nil { + return 0, "", 0, errors.New(fmt.Sprintf("Unable to parse version '%v' in filename schema", matches[0])) + } + + name = matches[2] + + if matches[3] == "up" { + d = direction.Up + } else if matches[3] == "down" { + d = direction.Down + } else { + return 0, "", 0, errors.New(fmt.Sprintf("Unable to parse up|down '%v' in filename schema", matches[3])) + } + + return version, name, d, nil +} + +type DefaultFilenameParser struct { + FilenameExtension string +} + +func (parser DefaultFilenameParser) Parse (filename string) (version uint64, name string, d direction.Direction, err error) { + filenameRegex := fmt.Sprintf(defaultFilenameRegexTemplate, parser.FilenameExtension) + return parseDefaultFilenameSchema(filename, filenameRegex) +} + + +type UpDownAndBothFilenameParser struct { + FilenameExtension string +} + +func (parser UpDownAndBothFilenameParser) Parse(filename string) (version uint64, name string, d direction.Direction, err error) { + ext := parser.FilenameExtension + if !strings.HasSuffix(filename, ext) { + return 0, "", 0, errors.New("Filename ") + } + + var matches []string + if strings.HasSuffix(filename, ".up." + ext) || strings.HasSuffix(filename, ".down." + ext) { + filenameRegex := fmt.Sprintf(defaultFilenameRegexTemplate, parser.FilenameExtension) + return parseDefaultFilenameSchema(filename, filenameRegex) + } + + regex := regexp.MustCompile(fmt.Sprintf(`^([0-9]+)_(.*)\.%s$`, ext)) + matches = regex.FindStringSubmatch(filename) + if len(matches) != 3 { + return 0, "", 0, errors.New("Unable to parse filename schema") + } + + version, err = strconv.ParseUint(matches[1], 10, 0) + if err != nil { + return 0, "", 0, errors.New(fmt.Sprintf("Unable to parse version '%v' in filename schema", matches[0])) + } + name = matches[2] + d = direction.Both + + return version, name, d, nil +} diff --git a/file/filename_parser_test.go b/file/filename_parser_test.go new file mode 100644 index 0000000..ea9135d --- /dev/null +++ b/file/filename_parser_test.go @@ -0,0 +1,85 @@ +package file + +import ( + "github.com/dimag-jfrog/migrate/migrate/direction" + "testing" +) + + +type ParsingTest struct { + filename string + filenameExtension string + expectVersion uint64 + expectName string + expectDirection direction.Direction + expectErr bool +} + +func testParser(t *testing.T, parser FilenameParser, test *ParsingTest) { + version, name, migrate, err := parser.Parse(test.filename) + if test.expectErr && err == nil { + t.Fatal("Expected error, but got none.", test) + } + if !test.expectErr && err != nil { + t.Fatal("Did not expect error, but got one:", err, test) + } + if err == nil { + if version != test.expectVersion { + t.Error("Wrong version number", test) + } + if name != test.expectName { + t.Error("wrong name", test) + } + if migrate != test.expectDirection { + t.Error("wrong migrate", test) + } + } +} + +func TestParseDefaultFilenameSchema(t *testing.T) { + var tests = []ParsingTest { + {"001_test_file.up.sql", "sql", 1, "test_file", direction.Up, false}, + {"001_test_file.down.sql", "sql", 1, "test_file", direction.Down, false}, + {"10034_test_file.down.sql", "sql", 10034, "test_file", direction.Down, false}, + {"-1_test_file.down.sql", "sql", 0, "", direction.Up, true}, + {"test_file.down.sql", "sql", 0, "", direction.Up, true}, + {"100_test_file.down", "sql", 0, "", direction.Up, true}, + {"100_test_file.sql", "sql", 0, "", direction.Up, true}, + {"100_test_file", "sql", 0, "", direction.Up, true}, + {"test_file", "sql", 0, "", direction.Up, true}, + {"100", "sql", 0, "", direction.Up, true}, + {".sql", "sql", 0, "", direction.Up, true}, + {"up.sql", "sql", 0, "", direction.Up, true}, + {"down.sql", "sql", 0, "", direction.Up, true}, + } + + for _, test := range tests { + parser := DefaultFilenameParser{FilenameExtension:test.filenameExtension} + testParser(t, &parser, &test) + } +} + +func TestParseUpDownAndBothFilenameSchema(t *testing.T) { + var tests = []ParsingTest { + {"001_test_file.up.sql", "sql", 1, "test_file", direction.Up, false}, + {"001_test_file.down.sql", "sql", 1, "test_file", direction.Down, false}, + {"10034_test_file.down.sql", "sql", 10034, "test_file", direction.Down, false}, + {"-1_test_file.down.sql", "sql", 0, "", direction.Up, true}, + {"test_file.down.sql", "sql", 0, "", direction.Up, true}, + {"100_test_file.down", "sql", 0, "", direction.Up, true}, + {"100_test_file.sql", "sql", 100, "test_file", direction.Both, false}, + {"001_test_file.mgo", "mgo", 1, "test_file", direction.Both, false}, + {"-1_test_file.mgo", "sql", 0, "", direction.Up, true}, + {"100_test_file", "sql", 0, "", direction.Up, true}, + {"test_file", "sql", 0, "", direction.Up, true}, + {"100", "sql", 0, "", direction.Up, true}, + {".sql", "sql", 0, "", direction.Up, true}, + {"up.sql", "sql", 0, "", direction.Up, true}, + {"down.sql", "sql", 0, "", direction.Up, true}, + } + + for _, test := range tests { + parser := UpDownAndBothFilenameParser{FilenameExtension:test.filenameExtension} + testParser(t, &parser, &test) + } +} diff --git a/migrate/direction/direction.go b/migrate/direction/direction.go index ed5e5ec..3ebc7b8 100644 --- a/migrate/direction/direction.go +++ b/migrate/direction/direction.go @@ -6,4 +6,5 @@ type Direction int const ( Up Direction = +1 Down = -1 + Both = 0 )