Merge pull request #136 from dimag-jfrog/prepare_pr_2_rebase

Driver for MongoDB (New)
This commit is contained in:
Matthias Kadenbach 2017-01-09 10:38:56 -08:00 committed by GitHub
commit 63c6168c7f
8 changed files with 1198 additions and 0 deletions

View File

@ -11,6 +11,7 @@ go-test:
- mysql
- cassandra
- crate
- mongo
go-build:
<<: *go
command: sh -c 'go get -v && go build -ldflags ''-s'' -o migrater'
@ -27,3 +28,5 @@ cassandra:
image: cassandra:2.2
crate:
image: crate
mongo:
image: mongo:3.2.6

104
driver/mongodb/README.md Normal file
View File

@ -0,0 +1,104 @@
# MongoDB Driver
* Runs pre-registered Golang methods that receive a single `*mgo.Session` parameter and return `error` on failure.
* Stores migration version details in collection ``db_migrations``.
This collection will be auto-generated.
* Migrations do not run in transactions, there are no built-in transactions in MongoDB.
That means that if a migration fails, it will not be rolled back.
* There is no out-of-the-box support for command-line interface via terminal.
## Usage in Go
```go
import "github.com/mattes/migrate/migrate"
// Import your migration methods package so that they are registered and available for the MongoDB driver.
// There is no need to import the MongoDB driver explicitly, as it should already be imported by your migration methods package.
import _ "my_mongo_db_migrator"
// use synchronous versions of migration functions ...
allErrors, ok := migrate.UpSync("mongodb://host:port", "./path")
if !ok {
fmt.Println("Oh no ...")
// do sth with allErrors slice
}
// use the asynchronous version of migration functions ...
pipe := migrate.NewPipe()
go migrate.Up(pipe, "mongodb://host:port", "./path")
// pipe is basically just a channel
// write your own channel listener. see writePipe() in main.go as an example.
```
## Migration files format
The migration files should have an ".mgo" extension and contain a list of registered methods names.
Migration methods should satisfy the following:
* They should be exported (their name should start with a capital letter)
* Their type should be `func (*mgo.Session) error`
Recommended (but not required) naming conventions for migration methods:
* Prefix with V<version> : for example V001 for version 1.
* Suffix with "_up" or "_down" for up and down migrations correspondingly.
001_first_release.up.mgo
```
V001_some_migration_operation_up
V001_some_other_operation_up
...
```
001_first_release.down.mgo
```
V001_some_other_operation_down
V001_some_migration_operation_down
...
```
## Methods registration
For a detailed example see: [sample_mongodb_migrator.go](https://github.com/mattes/migrate/blob/master/driver/mongodb/example/sample_mongdb_migrator.go)
```go
package my_mongo_db_migrator
import (
"github.com/mattes/migrate/driver/mongodb"
"github.com/mattes/migrate/driver/mongodb/gomethods"
"gopkg.in/mgo.v2"
)
// common boilerplate
type MyMongoDbMigrator struct {
}
func (r *MyMongoDbMigrator) DbName() string {
return "<target_db_name_for_migration>"
}
var _ mongodb.MethodsReceiver = (*MyMongoDbMigrator)(nil)
func init() {
gomethods.RegisterMethodsReceiverForDriver("mongodb", &MyMongoDbMigrator{})
}
// Here goes the application-specific migration logic
func (r *MyMongoDbMigrator) V001_some_migration_operation_up(session *mgo.Session) error {
// do something
return nil
}
func (r *MyMongoDbMigrator) V001_some_migration_operation_down(session *mgo.Session) error {
// revert some_migration_operation_up from above
return nil
}
```
## Authors
* Demitry Gershovich, https://github.com/dimag-jfrog

View File

@ -0,0 +1,310 @@
package example
import (
"testing"
"github.com/mattes/migrate/file"
"github.com/mattes/migrate/migrate/direction"
"github.com/mattes/migrate/driver"
"github.com/mattes/migrate/driver/mongodb"
"github.com/mattes/migrate/driver/mongodb/gomethods"
pipep "github.com/mattes/migrate/pipe"
"os"
"reflect"
"time"
)
type ExpectedMigrationResult struct {
Organizations []Organization
Organizations_v2 []Organization_v2
Users []User
Errors []error
}
func RunMigrationAndAssertResult(
t *testing.T,
title string,
d *mongodb.Driver,
file file.File,
expected *ExpectedMigrationResult) {
actualOrganizations := []Organization{}
actualOrganizations_v2 := []Organization_v2{}
actualUsers := []User{}
var err error
var pipe chan interface{}
var errs []error
pipe = pipep.New()
go d.Migrate(file, pipe)
errs = pipep.ReadErrors(pipe)
session := d.Session
if len(expected.Organizations) > 0 {
err = session.DB(DB_NAME).C(ORGANIZATIONS_C).Find(nil).All(&actualOrganizations)
} else {
err = session.DB(DB_NAME).C(ORGANIZATIONS_C).Find(nil).All(&actualOrganizations_v2)
}
if err != nil {
t.Fatal("Failed to query Organizations collection")
}
err = session.DB(DB_NAME).C(USERS_C).Find(nil).All(&actualUsers)
if err != nil {
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) {
t.Fatalf("Migration '%s': FAILED\nexpected organizations %v\nbut got %v", title, expected.Organizations, actualOrganizations)
}
if !reflect.DeepEqual(expected.Organizations_v2, actualOrganizations_v2) {
t.Fatalf("Migration '%s': FAILED\nexpected organizations v2 %v\nbut got %v", title, expected.Organizations_v2, actualOrganizations_v2)
}
if !reflect.DeepEqual(expected.Users, actualUsers) {
t.Fatalf("Migration '%s': FAILED\nexpected users %v\nbut got %v", title, expected.Users, actualUsers)
}
// t.Logf("Migration '%s': PASSED", title)
}
func TestMigrate(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Fatalf("Test failed on panic: %v", r)
}
}()
host := os.Getenv("MONGO_PORT_27017_TCP_ADDR")
port := os.Getenv("MONGO_PORT_27017_TCP_PORT")
driverUrl := "mongodb://" + host + ":" + port
d0 := driver.GetDriver("mongodb")
d, ok := d0.(*mongodb.Driver)
if !ok {
t.Fatal("MongoDbGoMethodsDriver has not registered")
}
if err := d.Initialize(driverUrl); err != nil {
t.Fatal(err)
}
// Reset DB
d.Session.DB(DB_NAME).C(mongodb.MIGRATE_C).DropCollection()
d.Session.DB(DB_NAME).C(ORGANIZATIONS_C).DropCollection()
d.Session.DB(DB_NAME).C(USERS_C).DropCollection()
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")
migrations := []struct {
name string
file file.File
expectedResult ExpectedMigrationResult
}{
{
name: "v0 -> v1",
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{
{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},
},
Organizations_v2: []Organization_v2{},
Users: []User{
{Id: UserIds[0], Name: "Alex"},
{Id: UserIds[1], Name: "Beatrice"},
{Id: UserIds[2], Name: "Cleo"},
},
Errors: []error{},
},
},
{
name: "v1 -> v2",
file: file.File{
Path: "/foobar",
FileName: "002_foobar.up.gm",
Version: 2,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
V002_organizations_rename_location_field_to_headquarters_up
V002_change_user_cleo_to_cleopatra_up
`),
},
expectedResult: ExpectedMigrationResult{
Organizations: []Organization{},
Organizations_v2: []Organization_v2{
{Id: OrganizationIds[0], Name: "Amazon", Headquarters: "Seattle", DateFounded: date1},
{Id: OrganizationIds[1], Name: "Google", Headquarters: "Mountain View", DateFounded: date2},
{Id: OrganizationIds[2], Name: "JFrog", Headquarters: "Santa Clara", DateFounded: date3},
},
Users: []User{
{Id: UserIds[0], Name: "Alex"},
{Id: UserIds[1], Name: "Beatrice"},
{Id: UserIds[2], Name: "Cleopatra"},
},
Errors: []error{},
},
},
{
name: "v2 -> v1",
file: file.File{
Path: "/foobar",
FileName: "002_foobar.down.gm",
Version: 2,
Name: "foobar",
Direction: direction.Down,
Content: []byte(`
V002_change_user_cleo_to_cleopatra_down
V002_organizations_rename_location_field_to_headquarters_down
`),
},
expectedResult: ExpectedMigrationResult{
Organizations: []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},
},
Organizations_v2: []Organization_v2{},
Users: []User{
{Id: UserIds[0], Name: "Alex"},
{Id: UserIds[1], Name: "Beatrice"},
{Id: UserIds[2], Name: "Cleo"},
},
Errors: []error{},
},
},
{
name: "v1 -> v0",
file: file.File{
Path: "/foobar",
FileName: "001_foobar.down.gm",
Version: 1,
Name: "foobar",
Direction: direction.Down,
Content: []byte(`
V001_init_users_down
V001_init_organizations_down
`),
},
expectedResult: ExpectedMigrationResult{
Organizations: []Organization{},
Organizations_v2: []Organization_v2{},
Users: []User{},
Errors: []error{},
},
},
{
name: "v0 -> v1: missing method aborts migration",
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
v001_non_existing_method_up
`),
},
expectedResult: ExpectedMigrationResult{
Organizations: []Organization{},
Organizations_v2: []Organization_v2{},
Users: []User{},
Errors: []error{gomethods.MethodNotFoundError("v001_non_existing_method_up")},
},
},
{
name: "v0 -> v1: not exported method aborts migration",
file: file.File{
Path: "/foobar",
FileName: "001_foobar.up.gm",
Version: 1,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
V001_init_organizations_up
v001_not_exported_method_up
V001_init_users_up
`),
},
expectedResult: ExpectedMigrationResult{
Organizations: []Organization{},
Organizations_v2: []Organization_v2{},
Users: []User{},
Errors: []error{gomethods.MethodNotFoundError("v001_not_exported_method_up")},
},
},
{
name: "v0 -> v1: wrong signature method aborts migration",
file: file.File{
Path: "/foobar",
FileName: "001_foobar.up.gm",
Version: 1,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
V001_init_organizations_up
V001_method_with_wrong_signature_up
V001_init_users_up
`),
},
expectedResult: ExpectedMigrationResult{
Organizations: []Organization{},
Organizations_v2: []Organization_v2{},
Users: []User{},
Errors: []error{gomethods.WrongMethodSignatureError("V001_method_with_wrong_signature_up")},
},
},
{
name: "v1 -> v0: wrong signature method aborts migration",
file: file.File{
Path: "/foobar",
FileName: "001_foobar.down.gm",
Version: 1,
Name: "foobar",
Direction: direction.Down,
Content: []byte(`
V001_init_users_down
V001_method_with_wrong_signature_down
V001_init_organizations_down
`),
},
expectedResult: ExpectedMigrationResult{
Organizations: []Organization{},
Organizations_v2: []Organization_v2{},
Users: []User{},
Errors: []error{gomethods.WrongMethodSignatureError("V001_method_with_wrong_signature_down")},
},
},
}
for _, m := range migrations {
RunMigrationAndAssertResult(t, m.name, d, m.file, &m.expectedResult)
}
if err := d.Close(); err != nil {
t.Fatal(err)
}
}

View File

@ -0,0 +1,153 @@
package example
import (
"github.com/mattes/migrate/driver/mongodb/gomethods"
_ "github.com/mattes/migrate/driver/mongodb/gomethods"
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
"time"
"github.com/mattes/migrate/driver/mongodb"
)
type SampleMongoDbMigrator struct {
}
func (r *SampleMongoDbMigrator) DbName() string {
return DB_NAME
}
var _ mongodb.MethodsReceiver = (*SampleMongoDbMigrator)(nil)
func init() {
gomethods.RegisterMethodsReceiverForDriver("mongodb", &SampleMongoDbMigrator{})
}
// Here goes the specific mongodb golang methods migration logic
const (
DB_NAME = "test"
SHORT_DATE_LAYOUT = "2000-Jan-01"
USERS_C = "users"
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 *SampleMongoDbMigrator) 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 *SampleMongoDbMigrator) V001_init_organizations_down(session *mgo.Session) error {
return session.DB(DB_NAME).C(ORGANIZATIONS_C).DropCollection()
}
func (r *SampleMongoDbMigrator) 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 *SampleMongoDbMigrator) V001_init_users_down(session *mgo.Session) error {
return session.DB(DB_NAME).C(USERS_C).DropCollection()
}
func (r *SampleMongoDbMigrator) 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 *SampleMongoDbMigrator) 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 *SampleMongoDbMigrator) 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 *SampleMongoDbMigrator) 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)
}
// Wrong signature methods for testing
func (r *SampleMongoDbMigrator) v001_not_exported_method_up(session *mgo.Session) error {
return nil
}
func (r *SampleMongoDbMigrator) V001_method_with_wrong_signature_up(s string) error {
return nil
}
func (r *SampleMongoDbMigrator) V001_method_with_wrong_signature_down(session *mgo.Session) (bool, error) {
return true, nil
}

View File

@ -0,0 +1,149 @@
package gomethods
import (
"bufio"
"fmt"
"github.com/mattes/migrate/driver"
"github.com/mattes/migrate/file"
"os"
"path"
"strings"
)
type MethodNotFoundError string
func (e MethodNotFoundError) Error() string {
return fmt.Sprintf("Method '%s' was not found. It is either not existing or has not been exported (starts with lowercase).", string(e))
}
type WrongMethodSignatureError string
func (e WrongMethodSignatureError) Error() string {
return fmt.Sprintf("Method '%s' has wrong signature", string(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.Err)
}
type MigrationMethodInvoker interface {
Validate(methodName string) error
Invoke(methodName string) error
}
type GoMethodsDriver interface {
driver.Driver
MigrationMethodInvoker
MethodsReceiver() interface{}
SetMethodsReceiver(r interface{}) error
}
type Migrator struct {
RollbackOnFailure bool
MethodInvoker MigrationMethodInvoker
}
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.MethodInvoker.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])
if rollbackToMethodName == "" {
continue
}
if err := m.MethodInvoker.Validate(rollbackToMethodName); err != nil {
continue
}
pipe <- rollbackToMethodName
err = m.MethodInvoker.Invoke(rollbackToMethodName)
if err != nil {
pipe <- err
break
}
}
return err
}
}
return nil
}
func getRollbackToMethod(methodName string) string {
if strings.HasSuffix(methodName, "_up") {
return strings.TrimSuffix(methodName, "_up") + "_down"
} else if strings.HasSuffix(methodName, "_down") {
return strings.TrimSuffix(methodName, "_down") + "_up"
} else {
return ""
}
}
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 {
s := string(file.Content)
return strings.Split(s, "\n"), nil
}
}
func (m *Migrator) getMigrationMethods(f file.File) (methods []string, err error) {
var lines []string
lines, err = getFileLines(f)
if err != nil {
return nil, err
}
for _, line := range lines {
line := strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "--") {
// an empty line or a comment, ignore
continue
}
methodName := line
if err := m.MethodInvoker.Validate(methodName); err != nil {
return nil, err
}
methods = append(methods, methodName)
}
return methods, nil
}

View File

@ -0,0 +1,247 @@
package gomethods
import (
"reflect"
"testing"
"github.com/mattes/migrate/file"
"github.com/mattes/migrate/migrate/direction"
pipep "github.com/mattes/migrate/pipe"
)
type FakeGoMethodsInvoker struct {
InvokedMethods []string
}
func (invoker *FakeGoMethodsInvoker) Validate(methodName string) error {
if methodName == "V001_some_non_existing_method_up" {
return MethodNotFoundError(methodName)
}
return nil
}
func (invoker *FakeGoMethodsInvoker) Invoke(methodName string) error {
invoker.InvokedMethods = append(invoker.InvokedMethods, methodName)
if methodName == "V001_some_failing_method_up" || methodName == "V001_some_failing_method_down" {
return &MethodInvocationFailedError{
MethodName: methodName,
Err: SomeError{},
}
}
return nil
}
type SomeError struct{}
func (e SomeError) Error() string { return "Some error happened" }
func TestMigrate(t *testing.T) {
cases := []struct {
name string
file file.File
expectedInvokedMethods []string
expectedErrors []error
expectRollback bool
}{
{
name: "up migration invokes up methods",
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
`),
},
expectedInvokedMethods: []string{"V001_init_organizations_up", "V001_init_users_up"},
expectedErrors: []error{},
},
{
name: "down migration invoked down methods",
file: file.File{
Path: "/foobar",
FileName: "001_foobar.down.gm",
Version: 1,
Name: "foobar",
Direction: direction.Down,
Content: []byte(`
V001_init_users_down
V001_init_organizations_down
`),
},
expectedInvokedMethods: []string{"V001_init_users_down", "V001_init_organizations_down"},
expectedErrors: []error{},
},
{
name: "up migration: non-existing method causes migration not to execute",
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
V001_some_non_existing_method_up
`),
},
expectedInvokedMethods: []string{},
expectedErrors: []error{MethodNotFoundError("V001_some_non_existing_method_up")},
},
{
name: "up migration: failing method stops execution",
file: file.File{
Path: "/foobar",
FileName: "001_foobar.up.gm",
Version: 1,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
V001_init_organizations_up
V001_some_failing_method_up
V001_init_users_up
`),
},
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: failing method stops migration",
file: file.File{
Path: "/foobar",
FileName: "001_foobar.down.gm",
Version: 1,
Name: "foobar",
Direction: direction.Down,
Content: []byte(`
V001_init_users_down
V001_some_failing_method_down
V001_init_organizations_down
`),
},
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.up.gm",
Version: 1,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
V001_init_organizations_up
V001_init_users_up
V001_some_failing_method_up
`),
},
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: failing method causes rollback in rollback mode",
expectRollback: true,
file: file.File{
Path: "/foobar",
FileName: "001_foobar.down.gm",
Version: 1,
Name: "foobar",
Direction: direction.Down,
Content: []byte(`
V001_init_users_down
V001_some_failing_method_down
V001_init_organizations_down
`),
},
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{}
fakeInvoker := &FakeGoMethodsInvoker{InvokedMethods: []string{}}
migrator.MethodInvoker = fakeInvoker
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(fakeInvoker.InvokedMethods, c.expectedInvokedMethods) {
failed = true
t.Errorf("case '%s': FAILED\nexpected invoked methods %v\nbut got %v", c.name, c.expectedInvokedMethods, fakeInvoker.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"},
{"some_method", ""},
}
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)
}
}
}

View File

@ -0,0 +1,39 @@
package gomethods
import (
"fmt"
"github.com/mattes/migrate/driver"
"sync"
)
var methodsReceiversMu sync.Mutex
// Registers a methods receiver for go methods driver
// Users of gomethods migration drivers should call this method
// to register objects with their migration methods before executing the migration
func RegisterMethodsReceiverForDriver(driverName string, receiver interface{}) {
methodsReceiversMu.Lock()
defer methodsReceiversMu.Unlock()
if receiver == nil {
panic("Go methods: Register receiver object is nil")
}
driver := driver.GetDriver(driverName)
if driver == nil {
panic("Go methods: Trying to register receiver for not registered driver " + driverName)
}
methodsDriver, ok := driver.(GoMethodsDriver)
if !ok {
panic("Go methods: Trying to register receiver for non go methods driver " + driverName)
}
if methodsDriver.MethodsReceiver() != nil {
panic("Go methods: Methods receiver already registered for driver " + driverName)
}
if err := methodsDriver.SetMethodsReceiver(receiver); err != nil {
panic(fmt.Sprintf("Go methods: Failed to set methods receiver for driver %s\nError: %v",
driverName, err))
}
}

193
driver/mongodb/mongodb.go Normal file
View File

@ -0,0 +1,193 @@
package mongodb
import (
"errors"
"github.com/mattes/migrate/driver"
"github.com/mattes/migrate/driver/mongodb/gomethods"
"github.com/mattes/migrate/file"
"github.com/mattes/migrate/migrate/direction"
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
"reflect"
"strings"
)
type UnregisteredMethodsReceiverError string
func (e UnregisteredMethodsReceiverError) Error() string {
return "Unregistered methods receiver for driver: " + string(e)
}
type WrongMethodsReceiverTypeError string
func (e WrongMethodsReceiverTypeError) Error() string {
return "Wrong methods receiver type for driver: " + string(e)
}
const MIGRATE_C = "db_migrations"
const DRIVER_NAME = "gomethods.mongodb"
type Driver struct {
Session *mgo.Session
methodsReceiver MethodsReceiver
migrator gomethods.Migrator
}
var _ gomethods.GoMethodsDriver = (*Driver)(nil)
type MethodsReceiver interface {
DbName() string
}
func (d *Driver) MethodsReceiver() interface{} {
return d.methodsReceiver
}
func (d *Driver) SetMethodsReceiver(r interface{}) error {
r1, ok := r.(MethodsReceiver)
if !ok {
return WrongMethodsReceiverTypeError(DRIVER_NAME)
}
d.methodsReceiver = r1
return nil
}
func init() {
driver.RegisterDriver("mongodb", &Driver{})
}
type DbMigration struct {
Id bson.ObjectId `bson:"_id"`
Version uint64 `bson:"version"`
}
func (driver *Driver) Initialize(url string) error {
if driver.methodsReceiver == nil {
return UnregisteredMethodsReceiverError(DRIVER_NAME)
}
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)
c := session.DB(driver.methodsReceiver.DbName()).C(MIGRATE_C)
err = c.EnsureIndex(mgo.Index{
Key: []string{"version"},
Unique: true,
})
if err != nil {
return err
}
driver.Session = session
driver.migrator = gomethods.Migrator{MethodInvoker: driver}
return nil
}
func (driver *Driver) Close() error {
if driver.Session != nil {
driver.Session.Close()
}
return nil
}
func (driver *Driver) FilenameExtension() string {
return "mgo"
}
func (driver *Driver) Version() (uint64, error) {
var latestMigration DbMigration
c := driver.Session.DB(driver.methodsReceiver.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 *Driver) 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.methodsReceiver.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 *Driver) Validate(methodName string) error {
methodWithReceiver, ok := reflect.TypeOf(driver.methodsReceiver).MethodByName(methodName)
if !ok {
return gomethods.MethodNotFoundError(methodName)
}
if methodWithReceiver.PkgPath != "" {
return gomethods.MethodNotFoundError(methodName)
}
methodFunc := reflect.ValueOf(driver.methodsReceiver).MethodByName(methodName)
methodTemplate := func(*mgo.Session) error { return nil }
if methodFunc.Type() != reflect.TypeOf(methodTemplate) {
return gomethods.WrongMethodSignatureError(methodName)
}
return nil
}
func (driver *Driver) Invoke(methodName string) error {
name := methodName
migrateMethod := reflect.ValueOf(driver.methodsReceiver).MethodByName(name)
if !migrateMethod.IsValid() {
return gomethods.MethodNotFoundError(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
}