- redefined mongo db driver to be autonomous and not depending on specific methods

- defined methods receivers registration by name and change the migration files format to include them
- added extensive testing
This commit is contained in:
dimag 2016-08-09 16:47:34 +03:00
parent c71fea90d6
commit 570e4b42be
8 changed files with 462 additions and 412 deletions

View File

@ -2,40 +2,57 @@ package gomethods
import ( import (
//"bytes" //"bytes"
"reflect" "bufio"
"database/sql/driver"
"fmt" "fmt"
"strings" "github.com/dimag-jfrog/migrate/file"
"os" "os"
"path" "path"
"bufio" "strings"
"github.com/dimag-jfrog/migrate/file"
) )
type UnregisteredMethodsReceiverError string
func (e UnregisteredMethodsReceiverError) Error() string {
return "Unregistered methods receiver: " + string(e)
}
type MissingMethodError string type MissingMethodError string
func (e MissingMethodError) Error() string { return "Non existing migrate method: " + string(e) }
func (e MissingMethodError) Error() string { return "Non existing migrate method: " + string(e) }
type WrongMethodSignatureError string type WrongMethodSignatureError string
func (e WrongMethodSignatureError) Error() string { return fmt.Sprintf("Method %s has wrong signature", e) }
func (e WrongMethodSignatureError) Error() string {
return fmt.Sprintf("Method %s has wrong signature", e)
}
type MethodInvocationFailedError struct { type MethodInvocationFailedError struct {
MethodName string MethodName string
Err error Err error
} }
func (e *MethodInvocationFailedError) Error() string { func (e *MethodInvocationFailedError) Error() string {
return fmt.Sprintf("Method %s returned an error: %v", e.MethodName, e.Error) return fmt.Sprintf("Method %s returned an error: %v", e.MethodName, e.Error)
} }
type MigrationMethodInvoker interface {
IsValid(methodName string, methodReceiver interface{}) bool
Invoke(methodName string, methodReceiver interface{}) error
}
type GoMethodsDriver interface {
driver.Driver
MigrationMethodInvoker
}
type Migrator struct { type Migrator struct {
MigrationMethodsReceiver interface{}
RollbackOnFailure bool RollbackOnFailure bool
MethodInvoker MigrationMethodInvoker
} }
func (m *Migrator) Migrate(f file.File, pipe chan interface{}) error { func (m *Migrator) Migrate(f file.File, pipe chan interface{}) error {
methods, err := m.getMigrationMethods(f) methods, methodsReceiver, err := m.getMigrationMethods(f)
if err != nil { if err != nil {
pipe <- err pipe <- err
return err return err
@ -43,7 +60,7 @@ func (m *Migrator) Migrate(f file.File, pipe chan interface{}) error {
for i, methodName := range methods { for i, methodName := range methods {
pipe <- methodName pipe <- methodName
err := m.Invoke(methodName) err := m.MethodInvoker.Invoke(methodName, methodsReceiver)
if err != nil { if err != nil {
pipe <- err pipe <- err
if !m.RollbackOnFailure { if !m.RollbackOnFailure {
@ -51,14 +68,15 @@ func (m *Migrator) Migrate(f file.File, pipe chan interface{}) error {
} }
// on failure, try to rollback methods in this migration // on failure, try to rollback methods in this migration
for j := i-1; j >= 0; j-- { for j := i - 1; j >= 0; j-- {
rollbackToMethodName := getRollbackToMethod(methods[j]) rollbackToMethodName := getRollbackToMethod(methods[j])
if rollbackToMethodName == "" || !m.IsValid(rollbackToMethodName) { if rollbackToMethodName == "" ||
!m.MethodInvoker.IsValid(rollbackToMethodName, methodsReceiver) {
continue continue
} }
pipe <- rollbackToMethodName pipe <- rollbackToMethodName
err = m.Invoke(rollbackToMethodName) err = m.MethodInvoker.Invoke(rollbackToMethodName, methodsReceiver)
if err != nil { if err != nil {
pipe <- err pipe <- err
break break
@ -71,33 +89,6 @@ func (m *Migrator) Migrate(f file.File, pipe chan interface{}) error {
return nil return nil
} }
func (m *Migrator) IsValid(methodName string) bool {
return reflect.ValueOf(m.MigrationMethodsReceiver).MethodByName(methodName).IsValid()
}
func (m *Migrator) Invoke(methodName string) error {
name := methodName
migrateMethod := reflect.ValueOf(m.MigrationMethodsReceiver).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) { func reverseInPlace(a []string) {
for i := 0; i < len(a)/2; i++ { for i := 0; i < len(a)/2; i++ {
j := len(a) - i - 1 j := len(a) - i - 1
@ -139,28 +130,40 @@ func getFileLines(file file.File) ([]string, error) {
} }
} }
func (m *Migrator) getMigrationMethods(f file.File) ([]string, error) { func (m *Migrator) getMigrationMethods(f file.File) (methods []string, methodsReceiver interface{}, err error) {
var lines, methods []string var lines []string
lines, err := getFileLines(f)
lines, err = getFileLines(f)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
for _, line := range lines { for _, line := range lines {
methodName := strings.TrimSpace(line) line := strings.TrimSpace(line)
if methodName == "" || strings.HasPrefix(methodName, "--") { if line == "" || strings.HasPrefix(line, "--") {
// an empty line or a comment, ignore // an empty line or a comment, ignore
continue continue
} }
if !m.IsValid(methodName) { if methodsReceiver == nil {
return nil, MissingMethodError(methodName) receiverName := line
} methodsReceiver = GetMethodsReceiver(receiverName)
if methodsReceiver == nil {
return nil, nil, UnregisteredMethodsReceiverError(receiverName)
}
continue
methods = append(methods, methodName) } else {
methodName := line
if !m.MethodInvoker.IsValid(methodName, methodsReceiver) {
return nil, nil, MissingMethodError(methodName)
}
methods = append(methods, methodName)
}
} }
return methods, nil return methods, methodsReceiver, nil
} }

View File

@ -10,133 +10,105 @@ import (
pipep "github.com/dimag-jfrog/migrate/pipe" pipep "github.com/dimag-jfrog/migrate/pipe"
) )
type FakeGoMethodsDriver struct { type FakeGoMethodsInvoker struct {
InvokedMethods []string InvokedMethods []string
Migrator Migrator
} }
func (driver *FakeGoMethodsDriver) Initialize(url string) error { func (invoker *FakeGoMethodsInvoker) IsValid(methodName string, methodReceiver interface{}) bool {
return nil if methodName == "V001_some_non_existing_method_up" {
return false
}
return true
} }
func (driver *FakeGoMethodsDriver) Close() error { func (invoker *FakeGoMethodsInvoker) Invoke(methodName string, methodReceiver interface{}) error {
return nil invoker.InvokedMethods = append(invoker.InvokedMethods, methodName)
}
func (driver *FakeGoMethodsDriver) FilenameExtension() string { if methodName == "V001_some_failing_method_up" || methodName == "V001_some_failing_method_down" {
return "gm" return &MethodInvocationFailedError{
} MethodName: methodName,
Err: SomeError{},
func (driver *FakeGoMethodsDriver) Version() (uint64, error) { }
return uint64(0), nil } else {
} return 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{} type SomeError struct{}
func (e SomeError) Error() string { return "Some error happened" }
func (driver *FakeGoMethodsDriver) V001_some_failing_method_up() error { func (e SomeError) Error() string { return "Some error happened" }
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) { func TestMigrate(t *testing.T) {
cases := []struct { cases := []struct {
name string name string
file file.File file file.File
expectedInvokedMethods []string expectedInvokedMethods []string
expectedErrors []error expectedErrors []error
expectRollback bool expectRollback bool
}{ }{
{ {
name: "up migration invokes up methods", name: "up migration invokes up methods",
file: file.File { file: file.File{
Path: "/foobar", Path: "/foobar",
FileName: "001_foobar.up.gm", FileName: "001_foobar.up.gm",
Version: 1, Version: 1,
Name: "foobar", Name: "foobar",
Direction: direction.Up, Direction: direction.Up,
Content: []byte(` Content: []byte(`
FakeMethodsReceiver
V001_init_organizations_up V001_init_organizations_up
V001_init_users_up V001_init_users_up
`), `),
}, },
expectedInvokedMethods: []string{"V001_init_organizations_up", "V001_init_users_up"}, expectedInvokedMethods: []string{"V001_init_organizations_up", "V001_init_users_up"},
expectedErrors: []error{}, expectedErrors: []error{},
}, },
{ {
name: "down migration invoked down methods", name: "down migration invoked down methods",
file: file.File { file: file.File{
Path: "/foobar", Path: "/foobar",
FileName: "001_foobar.down.gm", FileName: "001_foobar.down.gm",
Version: 1, Version: 1,
Name: "foobar", Name: "foobar",
Direction: direction.Down, Direction: direction.Down,
Content: []byte(` Content: []byte(`
FakeMethodsReceiver
V001_init_users_down V001_init_users_down
V001_init_organizations_down V001_init_organizations_down
`), `),
}, },
expectedInvokedMethods: []string{"V001_init_users_down", "V001_init_organizations_down"}, expectedInvokedMethods: []string{"V001_init_users_down", "V001_init_organizations_down"},
expectedErrors: []error{}, expectedErrors: []error{},
}, },
{ {
name: "up migration: non-existing method causes migration not to execute", name: "up migration: non-existing method causes migration not to execute",
file: file.File { file: file.File{
Path: "/foobar", Path: "/foobar",
FileName: "001_foobar.up.gm", FileName: "001_foobar.up.gm",
Version: 1, Version: 1,
Name: "foobar", Name: "foobar",
Direction: direction.Up, Direction: direction.Up,
Content: []byte(` Content: []byte(`
FakeMethodsReceiver
V001_init_organizations_up V001_init_organizations_up
V001_init_users_up V001_init_users_up
V001_some_non_existing_method_up V001_some_non_existing_method_up
`), `),
}, },
expectedInvokedMethods: []string{}, expectedInvokedMethods: []string{},
expectedErrors: []error{ MissingMethodError("V001_some_non_existing_method_up") }, expectedErrors: []error{MissingMethodError("V001_some_non_existing_method_up")},
}, },
{ {
name: "up migration: failing method stops execution", name: "up migration: failing method stops execution",
file: file.File { file: file.File{
Path: "/foobar", Path: "/foobar",
FileName: "001_foobar.up.gm", FileName: "001_foobar.up.gm",
Version: 1, Version: 1,
Name: "foobar", Name: "foobar",
Direction: direction.Up, Direction: direction.Up,
Content: []byte(` Content: []byte(`
FakeMethodsReceiver
V001_init_organizations_up V001_init_organizations_up
V001_some_failing_method_up V001_some_failing_method_up
V001_init_users_up V001_init_users_up
@ -146,20 +118,21 @@ func TestMigrate(t *testing.T) {
"V001_init_organizations_up", "V001_init_organizations_up",
"V001_some_failing_method_up", "V001_some_failing_method_up",
}, },
expectedErrors: []error{ &MethodInvocationFailedError{ expectedErrors: []error{&MethodInvocationFailedError{
MethodName: "V001_some_failing_method_up", MethodName: "V001_some_failing_method_up",
Err: SomeError{}, Err: SomeError{},
}}, }},
}, },
{ {
name: "down migration: failing method stops migration", name: "down migration: failing method stops migration",
file: file.File { file: file.File{
Path: "/foobar", Path: "/foobar",
FileName: "001_foobar.down.gm", FileName: "001_foobar.down.gm",
Version: 1, Version: 1,
Name: "foobar", Name: "foobar",
Direction: direction.Down, Direction: direction.Down,
Content: []byte(` Content: []byte(`
FakeMethodsReceiver
V001_init_users_down V001_init_users_down
V001_some_failing_method_down V001_some_failing_method_down
V001_init_organizations_down V001_init_organizations_down
@ -169,21 +142,22 @@ func TestMigrate(t *testing.T) {
"V001_init_users_down", "V001_init_users_down",
"V001_some_failing_method_down", "V001_some_failing_method_down",
}, },
expectedErrors: []error{ &MethodInvocationFailedError{ expectedErrors: []error{&MethodInvocationFailedError{
MethodName: "V001_some_failing_method_down", MethodName: "V001_some_failing_method_down",
Err: SomeError{}, Err: SomeError{},
}}, }},
}, },
{ {
name: "up migration: failing method causes rollback in rollback mode", name: "up migration: failing method causes rollback in rollback mode",
expectRollback: true, expectRollback: true,
file: file.File { file: file.File{
Path: "/foobar", Path: "/foobar",
FileName: "001_foobar.up.gm", FileName: "001_foobar.up.gm",
Version: 1, Version: 1,
Name: "foobar", Name: "foobar",
Direction: direction.Up, Direction: direction.Up,
Content: []byte(` Content: []byte(`
FakeMethodsReceiver
V001_init_organizations_up V001_init_organizations_up
V001_init_users_up V001_init_users_up
V001_some_failing_method_up V001_some_failing_method_up
@ -196,21 +170,22 @@ func TestMigrate(t *testing.T) {
"V001_init_users_down", "V001_init_users_down",
"V001_init_organizations_down", "V001_init_organizations_down",
}, },
expectedErrors: []error{ &MethodInvocationFailedError{ expectedErrors: []error{&MethodInvocationFailedError{
MethodName: "V001_some_failing_method_up", MethodName: "V001_some_failing_method_up",
Err: SomeError{}, Err: SomeError{},
}}, }},
}, },
{ {
name: "down migration: failing method causes rollback in rollback mode", name: "down migration: failing method causes rollback in rollback mode",
expectRollback: true, expectRollback: true,
file: file.File { file: file.File{
Path: "/foobar", Path: "/foobar",
FileName: "001_foobar.down.gm", FileName: "001_foobar.down.gm",
Version: 1, Version: 1,
Name: "foobar", Name: "foobar",
Direction: direction.Down, Direction: direction.Down,
Content: []byte(` Content: []byte(`
FakeMethodsReceiver
V001_init_users_down V001_init_users_down
V001_some_failing_method_down V001_some_failing_method_down
V001_init_organizations_down V001_init_organizations_down
@ -221,18 +196,20 @@ func TestMigrate(t *testing.T) {
"V001_some_failing_method_down", "V001_some_failing_method_down",
"V001_init_users_up", "V001_init_users_up",
}, },
expectedErrors: []error{ &MethodInvocationFailedError{ expectedErrors: []error{&MethodInvocationFailedError{
MethodName: "V001_some_failing_method_down", MethodName: "V001_some_failing_method_down",
Err: SomeError{}, Err: SomeError{},
}}, }},
}, },
} }
RegisterMethodsReceiver("FakeMethodsReceiver", "")
for _, c := range cases { for _, c := range cases {
migrator := Migrator{} migrator := Migrator{}
d := &FakeGoMethodsDriver{Migrator: migrator, InvokedMethods:[]string{}} fakeInvoker := &FakeGoMethodsInvoker{InvokedMethods: []string{}}
migrator.MigrationMethodsReceiver = d
migrator.MethodInvoker = fakeInvoker
migrator.RollbackOnFailure = c.expectRollback migrator.RollbackOnFailure = c.expectRollback
pipe := pipep.New() pipe := pipep.New()
@ -243,9 +220,9 @@ func TestMigrate(t *testing.T) {
errs := pipep.ReadErrors(pipe) errs := pipep.ReadErrors(pipe)
var failed bool var failed bool
if !reflect.DeepEqual(d.InvokedMethods, c.expectedInvokedMethods) { if !reflect.DeepEqual(fakeInvoker.InvokedMethods, c.expectedInvokedMethods) {
failed = true failed = true
t.Errorf("case '%s': FAILED\nexpected invoked methods %v\nbut got %v", c.name, c.expectedInvokedMethods, d.InvokedMethods) t.Errorf("case '%s': FAILED\nexpected invoked methods %v\nbut got %v", c.name, c.expectedInvokedMethods, fakeInvoker.InvokedMethods)
} }
if !reflect.DeepEqual(errs, c.expectedErrors) { if !reflect.DeepEqual(errs, c.expectedErrors) {
failed = true failed = true
@ -258,11 +235,9 @@ func TestMigrate(t *testing.T) {
} }
} }
func TestGetRollbackToMethod(t *testing.T) { func TestGetRollbackToMethod(t *testing.T) {
cases := []struct { cases := []struct {
method string method string
expectedRollbackMethod string expectedRollbackMethod string
}{ }{
{"some_method_up", "some_method_down"}, {"some_method_up", "some_method_down"},
@ -282,7 +257,7 @@ func TestGetRollbackToMethod(t *testing.T) {
} }
func TestReverseInPlace(t *testing.T) { func TestReverseInPlace(t *testing.T) {
methods := []string { methods := []string{
"method1_down", "method1_down",
"method2_down", "method2_down",
"method3_down", "method3_down",
@ -290,7 +265,7 @@ func TestReverseInPlace(t *testing.T) {
"method5_down", "method5_down",
} }
expectedReversedMethods := []string { expectedReversedMethods := []string{
"method5_down", "method5_down",
"method4_down", "method4_down",
"method3_down", "method3_down",
@ -304,4 +279,3 @@ func TestReverseInPlace(t *testing.T) {
t.Errorf("Expected reverse methods %v but got %v", expectedReversedMethods, methods) t.Errorf("Expected reverse methods %v but got %v", expectedReversedMethods, methods)
} }
} }

View File

@ -0,0 +1,30 @@
package gomethods
import (
"sync"
)
var methodsReceiversMu sync.Mutex
var methodsReceivers = make(map[string]interface{})
// Registers a methods receiver object so it can be created from its name. Users of gomethods migration drivers should
// call this method to register objects with their migration methods before executing the migration
func RegisterMethodsReceiver(name string, receiver interface{}) {
methodsReceiversMu.Lock()
defer methodsReceiversMu.Unlock()
if receiver == nil {
panic("Go methods: Register receiver object is nil")
}
if _, dup := methodsReceivers[name]; dup {
panic("Go methods: Register called twice for receiver " + name)
}
methodsReceivers[name] = receiver
}
// Retrieves a registered driver by name
func GetMethodsReceiver(name string) interface{} {
methodsReceiversMu.Lock()
defer methodsReceiversMu.Unlock()
receiver := methodsReceivers[name]
return receiver
}

View File

@ -0,0 +1,134 @@
package mongodb
import (
"errors"
"github.com/dimag-jfrog/migrate/driver"
"github.com/dimag-jfrog/migrate/driver/gomethods"
"github.com/dimag-jfrog/migrate/file"
"github.com/dimag-jfrog/migrate/migrate/direction"
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
"reflect"
"strings"
)
const MIGRATE_DB = "db_migrations"
const MIGRATE_C = "db_migrations"
type MongoDbGoMethodsDriver struct {
Session *mgo.Session
DbName string
migrator gomethods.Migrator
}
func init() {
driver.RegisterDriver("mongodb", &MongoDbGoMethodsDriver{})
}
type DbMigration struct {
Id bson.ObjectId `bson:"_id,omitempty"`
Version uint64 `bson:"version"`
}
func (driver *MongoDbGoMethodsDriver) Initialize(url string) 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 = MIGRATE_DB
driver.migrator = gomethods.Migrator{MethodInvoker: driver}
return nil
}
func (driver *MongoDbGoMethodsDriver) Close() error {
if driver.Session != nil {
driver.Session.Close()
}
return nil
}
func (driver *MongoDbGoMethodsDriver) FilenameExtension() string {
return "mgo"
}
func (driver *MongoDbGoMethodsDriver) 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 *MongoDbGoMethodsDriver) 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
}
}
}
func (driver *MongoDbGoMethodsDriver) IsValid(methodName string, methodsReceiver interface{}) bool {
return reflect.ValueOf(methodsReceiver).MethodByName(methodName).IsValid()
}
func (driver *MongoDbGoMethodsDriver) Invoke(methodName string, methodsReceiver interface{}) error {
name := methodName
migrateMethod := reflect.ValueOf(methodsReceiver).MethodByName(name)
if !migrateMethod.IsValid() {
return gomethods.MissingMethodError(methodName)
}
retValues := migrateMethod.Call([]reflect.Value{reflect.ValueOf(driver.Session)})
if len(retValues) != 1 {
return gomethods.WrongMethodSignatureError(name)
}
if !retValues[0].IsNil() {
err, ok := retValues[0].Interface().(error)
if !ok {
return gomethods.WrongMethodSignatureError(name)
}
return &gomethods.MethodInvocationFailedError{MethodName: name, Err: err}
}
return nil
}

View File

@ -1,107 +0,0 @@
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) 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
}
}
}

View File

@ -0,0 +1,129 @@
package mongodb_example
import (
"github.com/dimag-jfrog/migrate/driver/gomethods"
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
"time"
)
type MyMgoMethodsReceiver struct {
}
func init() {
gomethods.RegisterMethodsReceiver("MyMgoMethodsReceiver", &MyMgoMethodsReceiver{})
}
// 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 Organization_v2 struct {
Id bson.ObjectId `bson:"_id,omitempty"`
Name string `bson:"name"`
Headquarters string `bson:"headquarters"`
DateFounded time.Time `bson:"date_founded"`
}
type User struct {
Id bson.ObjectId `bson:"_id"`
Name string `bson:"name"`
}
var OrganizationIds []bson.ObjectId = []bson.ObjectId{
bson.NewObjectId(),
bson.NewObjectId(),
bson.NewObjectId(),
}
var UserIds []bson.ObjectId = []bson.ObjectId{
bson.NewObjectId(),
bson.NewObjectId(),
bson.NewObjectId(),
}
func (r *MyMgoMethodsReceiver) V001_init_organizations_up(session *mgo.Session) 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: OrganizationIds[0], Name: "Amazon", Location: "Seattle", DateFounded: date1},
{Id: OrganizationIds[1], Name: "Google", Location: "Mountain View", DateFounded: date2},
{Id: OrganizationIds[2], Name: "JFrog", Location: "Santa Clara", DateFounded: date3},
}
for _, org := range orgs {
err := session.DB(DB_NAME).C(ORGANIZATIONS_C).Insert(org)
if err != nil {
return err
}
}
return nil
}
func (r *MyMgoMethodsReceiver) V001_init_organizations_down(session *mgo.Session) error {
return session.DB(DB_NAME).C(ORGANIZATIONS_C).DropCollection()
}
func (r *MyMgoMethodsReceiver) V001_init_users_up(session *mgo.Session) error {
users := []User{
{Id: UserIds[0], Name: "Alex"},
{Id: UserIds[1], Name: "Beatrice"},
{Id: UserIds[2], Name: "Cleo"},
}
for _, user := range users {
err := session.DB(DB_NAME).C(USERS_C).Insert(user)
if err != nil {
return err
}
}
return nil
}
func (r *MyMgoMethodsReceiver) V001_init_users_down(session *mgo.Session) error {
return session.DB(DB_NAME).C(USERS_C).DropCollection()
}
func (r *MyMgoMethodsReceiver) V002_organizations_rename_location_field_to_headquarters_up(session *mgo.Session) error {
c := session.DB(DB_NAME).C(ORGANIZATIONS_C)
_, err := c.UpdateAll(nil, bson.M{"$rename": bson.M{"location": "headquarters"}})
return err
}
func (r *MyMgoMethodsReceiver) V002_organizations_rename_location_field_to_headquarters_down(session *mgo.Session) error {
c := session.DB(DB_NAME).C(ORGANIZATIONS_C)
_, err := c.UpdateAll(nil, bson.M{"$rename": bson.M{"headquarters": "location"}})
return err
}
func (r *MyMgoMethodsReceiver) V002_change_user_cleo_to_cleopatra_up(session *mgo.Session) error {
c := 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 (r *MyMgoMethodsReceiver) V002_change_user_cleo_to_cleopatra_down(session *mgo.Session) error {
c := 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)
}

View File

@ -1,4 +1,4 @@
package usage_examples package mongodb_example
import ( import (
"testing" "testing"
@ -6,24 +6,24 @@ import (
"github.com/dimag-jfrog/migrate/file" "github.com/dimag-jfrog/migrate/file"
"github.com/dimag-jfrog/migrate/migrate/direction" "github.com/dimag-jfrog/migrate/migrate/direction"
"github.com/dimag-jfrog/migrate/driver/gomethods"
"github.com/dimag-jfrog/migrate/driver/gomethods/mongodb"
pipep "github.com/dimag-jfrog/migrate/pipe" pipep "github.com/dimag-jfrog/migrate/pipe"
"reflect" "reflect"
"time" "time"
"github.com/dimag-jfrog/migrate/driver/gomethods"
) )
type ExpectedMigrationResult struct { type ExpectedMigrationResult struct {
Organizations []Organization Organizations []Organization
Organizations_v2 []Organization_v2 Organizations_v2 []Organization_v2
Users []User Users []User
Errors []error Errors []error
} }
func RunMigrationAndAssertResult( func RunMigrationAndAssertResult(
t *testing.T, t *testing.T,
title string, title string,
d *GoMethodsMongoDbDriver, d *mongodb.MongoDbGoMethodsDriver,
file file.File, file file.File,
expected *ExpectedMigrationResult) { expected *ExpectedMigrationResult) {
@ -53,6 +53,10 @@ func RunMigrationAndAssertResult(
t.Fatal("Failed to query Users collection") t.Fatal("Failed to query Users collection")
} }
if !reflect.DeepEqual(expected.Errors, errs) {
t.Fatalf("Migration '%s': FAILED\nexpected errors %v\nbut got %v", title, expected.Errors, errs)
}
if !reflect.DeepEqual(expected.Organizations, actualOrganizations) { if !reflect.DeepEqual(expected.Organizations, actualOrganizations) {
t.Fatalf("Migration '%s': FAILED\nexpected organizations %v\nbut got %v", title, expected.Organizations, actualOrganizations) t.Fatalf("Migration '%s': FAILED\nexpected organizations %v\nbut got %v", title, expected.Organizations, actualOrganizations)
} }
@ -65,13 +69,9 @@ func RunMigrationAndAssertResult(
t.Fatalf("Migration '%s': FAILED\nexpected users %v\nbut got %v", title, expected.Users, actualUsers) t.Fatalf("Migration '%s': FAILED\nexpected users %v\nbut got %v", title, expected.Users, actualUsers)
} }
if !reflect.DeepEqual(expected.Errors, errs) {
t.Fatalf("Migration '%s': FAILED\nexpected errors %v\nbut got %v", title, expected.Errors, errs)
}
t.Logf("Migration '%s': PASSED", title) t.Logf("Migration '%s': PASSED", title)
} }
func TestMigrate(t *testing.T) { func TestMigrate(t *testing.T) {
//host := os.Getenv("MONGODB_PORT_27017_TCP_ADDR") //host := os.Getenv("MONGODB_PORT_27017_TCP_ADDR")
//port := os.Getenv("MONGODB_PORT_27017_TCP_PORT") //port := os.Getenv("MONGODB_PORT_27017_TCP_PORT")
@ -79,7 +79,9 @@ func TestMigrate(t *testing.T) {
port := "27017" port := "27017"
driverUrl := "mongodb://" + host + ":" + port driverUrl := "mongodb://" + host + ":" + port
d := &GoMethodsMongoDbDriver{} //gomethods.RegisterMethodsReceiver("MyMgoMethodsReceiver", &MyMgoMethodsReceiver{})
d := &mongodb.MongoDbGoMethodsDriver{}
if err := d.Initialize(driverUrl); err != nil { if err := d.Initialize(driverUrl); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -93,8 +95,8 @@ func TestMigrate(t *testing.T) {
date3, _ := time.Parse(SHORT_DATE_LAYOUT, "2008-Apr-28") date3, _ := time.Parse(SHORT_DATE_LAYOUT, "2008-Apr-28")
migrations := []struct { migrations := []struct {
name string name string
file file.File file file.File
expectedResult ExpectedMigrationResult expectedResult ExpectedMigrationResult
}{ }{
{ {
@ -106,15 +108,16 @@ func TestMigrate(t *testing.T) {
Name: "foobar", Name: "foobar",
Direction: direction.Up, Direction: direction.Up,
Content: []byte(` Content: []byte(`
MyMgoMethodsReceiver
V001_init_organizations_up V001_init_organizations_up
V001_init_users_up V001_init_users_up
`), `),
}, },
expectedResult: ExpectedMigrationResult{ expectedResult: ExpectedMigrationResult{
Organizations: []Organization{ Organizations: []Organization{
{Id: OrganizationIds[0], Name: "Amazon", Location:"Seattle", DateFounded: date1}, {Id: OrganizationIds[0], Name: "Amazon", Location: "Seattle", DateFounded: date1},
{Id: OrganizationIds[1], Name: "Google", Location:"Mountain View", DateFounded: date2}, {Id: OrganizationIds[1], Name: "Google", Location: "Mountain View", DateFounded: date2},
{Id: OrganizationIds[2], Name: "JFrog", Location:"Santa Clara", DateFounded: date3}, {Id: OrganizationIds[2], Name: "JFrog", Location: "Santa Clara", DateFounded: date3},
}, },
Organizations_v2: []Organization_v2{}, Organizations_v2: []Organization_v2{},
Users: []User{ Users: []User{
@ -134,6 +137,7 @@ func TestMigrate(t *testing.T) {
Name: "foobar", Name: "foobar",
Direction: direction.Up, Direction: direction.Up,
Content: []byte(` Content: []byte(`
MyMgoMethodsReceiver
V002_organizations_rename_location_field_to_headquarters_up V002_organizations_rename_location_field_to_headquarters_up
V002_change_user_cleo_to_cleopatra_up V002_change_user_cleo_to_cleopatra_up
`), `),
@ -141,9 +145,9 @@ func TestMigrate(t *testing.T) {
expectedResult: ExpectedMigrationResult{ expectedResult: ExpectedMigrationResult{
Organizations: []Organization{}, Organizations: []Organization{},
Organizations_v2: []Organization_v2{ Organizations_v2: []Organization_v2{
{Id: OrganizationIds[0], Name: "Amazon", Headquarters:"Seattle", DateFounded: date1}, {Id: OrganizationIds[0], Name: "Amazon", Headquarters: "Seattle", DateFounded: date1},
{Id: OrganizationIds[1], Name: "Google", Headquarters:"Mountain View", DateFounded: date2}, {Id: OrganizationIds[1], Name: "Google", Headquarters: "Mountain View", DateFounded: date2},
{Id: OrganizationIds[2], Name: "JFrog", Headquarters:"Santa Clara", DateFounded: date3}, {Id: OrganizationIds[2], Name: "JFrog", Headquarters: "Santa Clara", DateFounded: date3},
}, },
Users: []User{ Users: []User{
{Id: UserIds[0], Name: "Alex"}, {Id: UserIds[0], Name: "Alex"},
@ -162,15 +166,16 @@ func TestMigrate(t *testing.T) {
Name: "foobar", Name: "foobar",
Direction: direction.Down, Direction: direction.Down,
Content: []byte(` Content: []byte(`
MyMgoMethodsReceiver
V002_change_user_cleo_to_cleopatra_down V002_change_user_cleo_to_cleopatra_down
V002_organizations_rename_location_field_to_headquarters_down V002_organizations_rename_location_field_to_headquarters_down
`), `),
}, },
expectedResult: ExpectedMigrationResult{ expectedResult: ExpectedMigrationResult{
Organizations: []Organization{ Organizations: []Organization{
{Id: OrganizationIds[0], Name: "Amazon", Location:"Seattle", DateFounded: date1}, {Id: OrganizationIds[0], Name: "Amazon", Location: "Seattle", DateFounded: date1},
{Id: OrganizationIds[1], Name: "Google", Location:"Mountain View", DateFounded: date2}, {Id: OrganizationIds[1], Name: "Google", Location: "Mountain View", DateFounded: date2},
{Id: OrganizationIds[2], Name: "JFrog", Location:"Santa Clara", DateFounded: date3}, {Id: OrganizationIds[2], Name: "JFrog", Location: "Santa Clara", DateFounded: date3},
}, },
Organizations_v2: []Organization_v2{}, Organizations_v2: []Organization_v2{},
Users: []User{ Users: []User{
@ -190,15 +195,16 @@ func TestMigrate(t *testing.T) {
Name: "foobar", Name: "foobar",
Direction: direction.Down, Direction: direction.Down,
Content: []byte(` Content: []byte(`
MyMgoMethodsReceiver
V001_init_users_down V001_init_users_down
V001_init_organizations_down V001_init_organizations_down
`), `),
}, },
expectedResult: ExpectedMigrationResult{ expectedResult: ExpectedMigrationResult{
Organizations: []Organization{}, Organizations: []Organization{},
Organizations_v2: []Organization_v2{}, Organizations_v2: []Organization_v2{},
Users: []User{}, Users: []User{},
Errors: []error{}, Errors: []error{},
}, },
}, },
{ {
@ -210,16 +216,37 @@ func TestMigrate(t *testing.T) {
Name: "foobar", Name: "foobar",
Direction: direction.Up, Direction: direction.Up,
Content: []byte(` Content: []byte(`
MyMgoMethodsReceiver
V001_init_organizations_up V001_init_organizations_up
V001_init_users_up V001_init_users_up
v001_non_existing_method_up v001_non_existing_method_up
`), `),
}, },
expectedResult: ExpectedMigrationResult{ expectedResult: ExpectedMigrationResult{
Organizations: []Organization{}, Organizations: []Organization{},
Organizations_v2: []Organization_v2{}, Organizations_v2: []Organization_v2{},
Users: []User{}, Users: []User{},
Errors: []error{ gomethods.MissingMethodError("v001_non_existing_method_up") }, Errors: []error{gomethods.MissingMethodError("v001_non_existing_method_up")},
},
},
{
name: "v0 -> v1: not defined message receiver",
file: file.File{
Path: "/foobar",
FileName: "001_foobar.up.gm",
Version: 1,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
V001_init_organizations_up
V001_init_users_up
`),
},
expectedResult: ExpectedMigrationResult{
Organizations: []Organization{},
Organizations_v2: []Organization_v2{},
Users: []User{},
Errors: []error{gomethods.UnregisteredMethodsReceiverError("V001_init_organizations_up")},
}, },
}, },
} }

View File

@ -1,140 +0,0 @@
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{MigrationMethodsReceiver: 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 Organization_v2 struct {
Id bson.ObjectId `bson:"_id,omitempty"`
Name string `bson:"name"`
Headquarters string `bson:"headquarters"`
DateFounded time.Time `bson:"date_founded"`
}
type User struct {
Id bson.ObjectId `bson:"_id"`
Name string `bson:"name"`
}
var OrganizationIds []bson.ObjectId = []bson.ObjectId{
bson.NewObjectId(),
bson.NewObjectId(),
bson.NewObjectId(),
}
var UserIds []bson.ObjectId = []bson.ObjectId{
bson.NewObjectId(),
bson.NewObjectId(),
bson.NewObjectId(),
}
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: OrganizationIds[0], Name: "Amazon", Location:"Seattle", DateFounded: date1},
{Id: OrganizationIds[1], Name: "Google", Location:"Mountain View", DateFounded: date2},
{Id: OrganizationIds[2], 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: UserIds[0], Name: "Alex"},
{Id: UserIds[1], Name: "Beatrice"},
{Id: UserIds[2], 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)
}