mirror of https://github.com/status-im/migrate.git
Merge pull request #136 from dimag-jfrog/prepare_pr_2_rebase
Driver for MongoDB (New)
This commit is contained in:
commit
63c6168c7f
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue