diff --git a/1_global_migrate_test.go b/1_global_migrate_test.go index 49a5518..2df991a 100644 --- a/1_global_migrate_test.go +++ b/1_global_migrate_test.go @@ -3,7 +3,7 @@ package migrate import ( "testing" - "github.com/globalsign/mgo" + "go.mongodb.org/mongo-driver/mongo" ) func TestGlobalMigrateSetGet(t *testing.T) { @@ -11,13 +11,13 @@ func TestGlobalMigrateSetGet(t *testing.T) { defer func() { globalMigrate = oldMigrate }() - db := &mgo.Database{} + db := &mongo.Database{} globalMigrate = NewMigrate(db) if globalMigrate.db != db { t.Errorf("Unexpected non-equal dbs") } - db2 := &mgo.Database{} + db2 := &mongo.Database{} SetDatabase(db2) if globalMigrate.db != db2 { t.Errorf("Unexpected non-equal dbs") @@ -35,9 +35,9 @@ func TestMigrationsRegistration(t *testing.T) { }() globalMigrate = NewMigrate(nil) - err := Register(func(db *mgo.Database) error { + err := Register(func(db *mongo.Database) error { return nil - }, func(db *mgo.Database) error { + }, func(db *mongo.Database) error { return nil }) if err != nil { @@ -53,9 +53,9 @@ func TestMigrationsRegistration(t *testing.T) { t.Errorf("Unexpected version/description: %d %s", registered[0].Version, registered[0].Description) } - err = Register(func(db *mgo.Database) error { + err = Register(func(db *mongo.Database) error { return nil - }, func(db *mgo.Database) error { + }, func(db *mongo.Database) error { return nil }) if err == nil { @@ -72,9 +72,9 @@ func TestMigrationMustRegistration(t *testing.T) { } }() globalMigrate = NewMigrate(nil) - MustRegister(func(db *mgo.Database) error { + MustRegister(func(db *mongo.Database) error { return nil - }, func(db *mgo.Database) error { + }, func(db *mongo.Database) error { return nil }) registered := RegisteredMigrations() diff --git a/1_sample_data_test.go b/1_sample_data_test.go index 50575d8..7067e5a 100644 --- a/1_sample_data_test.go +++ b/1_sample_data_test.go @@ -3,16 +3,26 @@ package migrate import ( - "github.com/globalsign/mgo" - "github.com/globalsign/mgo/bson" + "context" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" ) const globalTestCollection = "test-global" func init() { - Register(func(db *mgo.Database) error { - return db.C(globalTestCollection).Insert(bson.M{"a": "b"}) - }, func(db *mgo.Database) error { - return db.C(globalTestCollection).Remove(bson.M{"a": "b"}) + Register(func(db *mongo.Database) error { + _, err := db.Collection(globalTestCollection).InsertOne(context.TODO(), bson.D{{"a", "b"}}) + if err != nil { + return err + } + return nil + }, func(db *mongo.Database) error { + _, err := db.Collection(globalTestCollection).DeleteOne(context.TODO(), bson.D{{"a", "b"}}) + if err != nil { + return err + } + return nil }) } diff --git a/2_sample_index_test.go b/2_sample_index_test.go index f298657..dd2eaf7 100644 --- a/2_sample_index_test.go +++ b/2_sample_index_test.go @@ -3,15 +3,30 @@ package migrate import ( - "github.com/globalsign/mgo" + "context" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" ) const globalTestIndexName = "test_idx_2" func init() { - Register(func(db *mgo.Database) error { - return db.C(globalTestCollection).EnsureIndex(mgo.Index{Name: globalTestIndexName, Key: []string{"a"}}) - }, func(db *mgo.Database) error { - return db.C(globalTestCollection).DropIndexName(globalTestIndexName) + Register(func(db *mongo.Database) error { + keys := bson.D{{"a", 1}} + opt := options.Index().SetName(globalTestIndexName) + model := mongo.IndexModel{Keys: keys, Options: opt} + _, err := db.Collection(globalTestCollection).Indexes().CreateOne(context.TODO(), model) + if err != nil { + return err + } + return nil + }, func(db *mongo.Database) error { + _, err := db.Collection(globalTestCollection).Indexes().DropOne(context.TODO(), globalTestIndexName) + if err != nil { + return err + } + return nil }) } diff --git a/README.md b/README.md index f5063f7..21e381d 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,7 @@ [![GoDoc](https://godoc.org/github.com/xakep666/mongo-migrate?status.svg)](https://godoc.org/github.com/xakep666/mongo-migrate) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -This package allows to perform versioned migrations on your MongoDB using [mgo driver](https://github.com/globalsign/mgo). -It depends only on standard library and mgo driver. +This package allows to perform versioned migrations on your MongoDB using [mongo-go-driver](https://github.com/mongodb/mongo-go-driver). Inspired by [go-pg migrations](https://github.com/go-pg/migrations). Table of Contents @@ -40,16 +39,29 @@ File name should be like `_.go`. package migrations import ( - "github.com/globalsign/mgo" - "github.com/globalsign/mgo/bson" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" migrate "github.com/xakep666/mongo-migrate" ) func init() { - migrate.Register(func(db *mgo.Database) error { - return db.C("my-coll").EnsureIndex(mgo.Index{Name: "my-index", Key: []string{"my-key"}})) - }, func(db *mgo.Database) error { - return db.C("my-coll").DropIndexName("my-index") + migrate.Register(func(db *mongo.Database) error { + opt := options.Index().SetName("my-index") + keys := bson.D{{"my-key", 1}} + model := mongo.IndexModel{Keys: keys, Options: opt} + _, err := db.Collection("my-coll").Indexes().CreateOne(context.TODO(), model) + if err != nil { + return err + } + + return nil + }, func(db *mongo.Database) error { + _, err := db.Collection("my-coll").Indexes().DropOne(context.TODO(), "my-index") + if err != nil { + return err + } + return nil }) } ``` @@ -66,53 +78,71 @@ import ( * Run migrations. ```go -func MongoConnect(host, user, password, database string) (*mgo.Database, error) { - session, err := mgo.DialWithInfo(&mgo.DialInfo{ - Addrs: []string{host}, - Database: database, - Username: user, - Password: password, - }) - if err != nil { - return nil, err - } - db := session.DB("") - migrate.SetDatabase(db) - if err := migrate.Up(migrate.AllAvailable); err != nil { - return nil, err - } - return db, nil +func MongoConnect(host, user, password, database string) (*mongo.Database, error) { + uri := fmt.Sprintf("mongodb://%s:%s@%s:27017", user, password, host) + opt := options.Client().ApplyURI(uri) + client, err := mongo.NewClient(opt) + if err != nil { + return nil, err + } + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + err = client.Connect(ctx) + if err != nil { + return nil, err + } + db = client.Database(database) + migrate.SetDatabase(db) + if err := migrate.Up(migrate.AllAvailable); err != nil { + return nil, err + } + return db, nil } ``` ### Use case #2. Migrations in application code. * Just define it anywhere you want and run it. ```go -func MongoConnect(host, user, password, database string) (*mgo.Database, error) { - session, err := mgo.DialWithInfo(&mgo.DialInfo{ - Addrs: []string{host}, - Database: database, - Username: user, - Password: password, - }) - if err != nil { - return nil, err - } - db := session.DB("") - m := migrate.NewMigrate(db, migrate.Migration{ - Version: 1, - Description: "add my-index", - Up: func(db *mgo.Database) error { - return db.C("my-coll").EnsureIndex(mgo.Index{Name: "my-index", Key: []string{"my-key"}}) - }, - Down: func(db *mgo.Database) error { - return db.C("my-coll").DropIndexName("my-index") - }, - }) - if err := m.Up(migrate.AllAvailable); err != nil { - return nil, err - } - return db, nil +func MongoConnect(host, user, password, database string) (*mongo.Database, error) { + uri := fmt.Sprintf("mongodb://%s:%s@%s:27017", user, password, host) + opt := options.Client().ApplyURI(uri) + client, err := mongo.NewClient(opt) + if err != nil { + return nil, err + } + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + err = client.Connect(ctx) + if err != nil { + return nil, err + } + db = client.Database(database) + m := migrate.NewMigrate(db, migrate.Migration{ + Version: 1, + Description: "add my-index", + Up: func(db *mongo.Database) error { + opt := options.Index().SetName("my-index") + keys := bson.D{{"my-key", 1}} + model := mongo.IndexModel{Keys: keys, Options: opt} + _, err := db.Collection("my-coll").Indexes().CreateOne(context.TODO(), model) + if err != nil { + return err + } + + return nil + }, + Down: func(db *mongo.Database) error { + _, err := db.Collection("my-coll").Indexes().DropOne(context.TODO(), "my-index") + if err != nil { + return err + } + return nil + }, + }) + if err := m.Up(migrate.AllAvailable); err != nil { + return nil, err + } + return db, nil } ``` diff --git a/bad_migration_file_test.go b/bad_migration_file_test.go index 75018ce..06b4f9d 100644 --- a/bad_migration_file_test.go +++ b/bad_migration_file_test.go @@ -3,7 +3,7 @@ package migrate import ( "testing" - "github.com/globalsign/mgo" + "go.mongodb.org/mongo-driver/mongo" ) func TestBadMigrationFile(t *testing.T) { @@ -13,9 +13,9 @@ func TestBadMigrationFile(t *testing.T) { }() globalMigrate = NewMigrate(nil) - err := Register(func(db *mgo.Database) error { + err := Register(func(db *mongo.Database) error { return nil - }, func(db *mgo.Database) error { + }, func(db *mongo.Database) error { return nil }) if err == nil { @@ -32,9 +32,9 @@ func TestBadMigrationFilePanic(t *testing.T) { } }() globalMigrate = NewMigrate(nil) - MustRegister(func(db *mgo.Database) error { + MustRegister(func(db *mongo.Database) error { return nil - }, func(db *mgo.Database) error { + }, func(db *mongo.Database) error { return nil }) } diff --git a/global_migrate.go b/global_migrate.go index a4df54a..9ebec4f 100644 --- a/global_migrate.go +++ b/global_migrate.go @@ -4,7 +4,7 @@ import ( "fmt" "runtime" - "github.com/globalsign/mgo" + "go.mongodb.org/mongo-driver/mongo" ) var globalMigrate = NewMigrate(nil) @@ -34,19 +34,33 @@ func internalRegister(up, down MigrationFunc, skip int) error { // // - Use the following template inside: // -// package migrations -// import ( -// "github.com/globalsign/mgo" -// "github.com/xakep666/mongo-migrate" -// ) +// package migrations // -// func init() { -// migrate.Register(func (db *mgo.Database) error { -// return db.C(collection).EnsureIndex(index) -// }, func (db *mgo.Database) error { -// return db.C(collection).DropIndexName(index.Name) -// }) -// } +// import ( +// "go.mongodb.org/mongo-driver/bson" +// "go.mongodb.org/mongo-driver/mongo" +// "go.mongodb.org/mongo-driver/mongo/options" +// "github.com/xakep666/mongo-migrate" +// ) +// +// func init() { +// Register(func(db *mongo.Database) error { +// opt := options.Index().SetName("my-index") +// keys := bson.D{{"my-key", 1}} +// model := mongo.IndexModel{Keys: keys, Options: opt} +// _, err := db.Collection("my-coll").Indexes().CreateOne(context.TODO(), model) +// if err != nil { +// return err +// } +// return nil +// }, func(db *mongo.Database) error { +// _, err := db.Collection("my-coll").Indexes().DropOne(context.TODO(), "my-index") +// if err != nil { +// return err +// } +// return nil +// }) +// } func Register(up, down MigrationFunc) error { return internalRegister(up, down, 2) } @@ -66,7 +80,7 @@ func RegisteredMigrations() []Migration { } // SetDatabase sets database for global migrate. -func SetDatabase(db *mgo.Database) { +func SetDatabase(db *mongo.Database) { globalMigrate.db = db } diff --git a/global_migrate_integration_test.go b/global_migrate_integration_test.go index 412fa42..903fc36 100644 --- a/global_migrate_integration_test.go +++ b/global_migrate_integration_test.go @@ -7,8 +7,8 @@ import ( ) func TestGlobalMigrateUp(t *testing.T) { - defer cleanup(mongo) - SetDatabase(mongo) + defer cleanup(db) + SetDatabase(db) if err := Up(-1); err != nil { t.Errorf("Unexpected error: %v", err) @@ -26,8 +26,8 @@ func TestGlobalMigrateUp(t *testing.T) { } func TestGlobalMigrateDown(t *testing.T) { - defer cleanup(mongo) - SetDatabase(mongo) + defer cleanup(db) + SetDatabase(db) if err := Up(-1); err != nil { t.Errorf("Unexpected error: %v", err) diff --git a/go.mod b/go.mod index af587b2..33bfc89 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,16 @@ module github.com/xakep666/mongo-migrate -require github.com/globalsign/mgo v0.0.0-20180615134936-113d3961e731 +require ( + github.com/go-stack/stack v1.8.0 // indirect + github.com/golang/snappy v0.0.1 // indirect + github.com/google/go-cmp v0.3.0 // indirect + github.com/pkg/errors v0.8.1 + github.com/stretchr/testify v1.3.0 // indirect + github.com/tidwall/pretty v0.0.0-20190325153808-1166b9ac2b65 // indirect + github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c // indirect + github.com/xdg/stringprep v1.0.0 // indirect + go.mongodb.org/mongo-driver v1.0.2 + golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f // indirect + golang.org/x/sync v0.0.0-20190423024810-112230192c58 // indirect + golang.org/x/text v0.3.2 // indirect +) diff --git a/go.sum b/go.sum index e071f87..6b218f0 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,35 @@ -github.com/globalsign/mgo v0.0.0-20180615134936-113d3961e731 h1:y7wyeiA6T+TT+HGC9DYypvLkUeg99N4rqHMzn2MmjYk= -github.com/globalsign/mgo v0.0.0-20180615134936-113d3961e731/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/tidwall/pretty v0.0.0-20190325153808-1166b9ac2b65 h1:rQ229MBgvW68s1/g6f1/63TgYwYxfF4E+bi/KC19P8g= +github.com/tidwall/pretty v0.0.0-20190325153808-1166b9ac2b65/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c h1:u40Z8hqBAAQyv+vATcGgV0YCnDjqSL7/q/JyPhhJSPk= +github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= +github.com/xdg/stringprep v1.0.0 h1:d9X0esnoa3dFsV0FG35rAT0RIhYFlPq7MiP+DW89La0= +github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= +go.mongodb.org/mongo-driver v1.0.2 h1:RwjK1tKt7VPqQh3tsjiEqKJg75GNhP/loch+PwRc4ig= +go.mongodb.org/mongo-driver v1.0.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f h1:R423Cnkcp5JABoeemiGEPlt9tHXFfw5kvc0yqlxRPWo= +golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/migrate.go b/migrate.go index eabc1cc..27293e5 100644 --- a/migrate.go +++ b/migrate.go @@ -2,15 +2,24 @@ package migrate import ( + "context" "time" - "github.com/globalsign/mgo" + "github.com/pkg/errors" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" ) +type collectionSpecification struct { + Name string `bson:"name"` + Type string `bson:"type"` +} + type versionRecord struct { - Version uint64 - Description string `bson:",omitempty"` - Timestamp time.Time + Version uint64 `bson:"version"` + Description string `bson:"description,omitempty"` + Timestamp time.Time `bson:"timestamp"` } const defaultMigrationsCollection = "migrations" @@ -24,12 +33,12 @@ const AllAvailable = -1 // This document consists migration version, migration description and timestamp. // Current database version determined as version in latest added document (biggest "_id") from collection mentioned above. type Migrate struct { - db *mgo.Database + db *mongo.Database migrations []Migration migrationsCollection string } -func NewMigrate(db *mgo.Database, migrations ...Migration) *Migrate { +func NewMigrate(db *mongo.Database, migrations ...Migration) *Migrate { internalMigrations := make([]Migration, len(migrations)) copy(internalMigrations, migrations) return &Migrate{ @@ -45,13 +54,14 @@ func (m *Migrate) SetMigrationsCollection(name string) { m.migrationsCollection = name } -func (m *Migrate) isCollectionExist(name string) (bool, error) { - colls, err := m.db.CollectionNames() +func (m *Migrate) isCollectionExist(name string) (isExist bool, err error) { + collections, err := m.getCollections() if err != nil { return false, err } - for _, v := range colls { - if name == v { + + for _, c := range collections { + if name == c.Name { return true, nil } } @@ -66,12 +76,54 @@ func (m *Migrate) createCollectionIfNotExist(name string) error { if exist { return nil } - // I had a problem here with bson.D: mongo returned error like "command not found: '0'" - return m.db.Run(struct { - Create string `bson:"create"` - }{ - Create: name, - }, nil) + + command := bson.D{bson.E{Key: "create", Value: name}} + err = m.db.RunCommand(nil, command).Err() + if err != nil { + return err + } + + return nil +} + +func (m *Migrate) getCollections() (collections []collectionSpecification, err error) { + filter := bson.D{bson.E{Key: "type", Value: "collection"}} + options := options.ListCollections().SetNameOnly(true) + + cursor, err := m.db.ListCollections(context.Background(), filter, options) + if err != nil { + return nil, err + } + + if cursor != nil { + defer func(cursor *mongo.Cursor) { + curErr := cursor.Close(context.TODO()) + if curErr != nil { + if err != nil { + err = errors.Wrapf(curErr, "migrate: get collection failed: %s", err.Error()) + } else { + err = curErr + } + } + }(cursor) + } + + for cursor.Next(context.TODO()) { + var collection collectionSpecification + + err := cursor.Decode(&collection) + if err != nil { + return nil, err + } + + collections = append(collections, collection) + } + + if err := cursor.Err(); err != nil { + return nil, err + } + + return } // Version returns current database version and comment. @@ -80,25 +132,43 @@ func (m *Migrate) Version() (uint64, string, error) { return 0, "", err } - var rec versionRecord + filter := bson.D{{}} + sort := bson.D{bson.E{Key: "_id", Value: -1}} + options := options.FindOne().SetSort(sort) + // find record with greatest id (assuming it`s latest also) - err := m.db.C(m.migrationsCollection).Find(nil).Sort("-_id").One(&rec) - if err == mgo.ErrNotFound { - return 0, "", nil + result := m.db.Collection(m.migrationsCollection).FindOne(context.TODO(), filter, options) + if err := result.Err(); err != nil { + return 0, "", err } + + var rec versionRecord + err := result.Decode(&rec) if err != nil { + if err == mongo.ErrNoDocuments { + return 0, "", nil + } + return 0, "", err } + return rec.Version, rec.Description, nil } // SetVersion forcibly changes database version to provided. func (m *Migrate) SetVersion(version uint64, description string) error { - return m.db.C(m.migrationsCollection).Insert(versionRecord{ + rec := versionRecord{ Version: version, Timestamp: time.Now().UTC(), Description: description, - }) + } + + _, err := m.db.Collection(m.migrationsCollection).InsertOne(context.TODO(), rec) + if err != nil { + return err + } + + return nil } // Up performs "up" migrations to latest available version. diff --git a/migration.go b/migration.go index 43d761b..9f617fa 100644 --- a/migration.go +++ b/migration.go @@ -3,12 +3,13 @@ package migrate import ( "sort" - "github.com/globalsign/mgo" + "go.mongodb.org/mongo-driver/mongo" ) -type MigrationFunc func(db *mgo.Database) error +// MigrationFunc used to define actions to be performed for a migration. +type MigrationFunc func(db *mongo.Database) error -// Migrate represents single database migration. +// Migration represents single database migration. // Migration contains: // // - version: migration version, must be unique in migration list diff --git a/migration_integration_test.go b/migration_integration_test.go index 9758b50..051070d 100644 --- a/migration_integration_test.go +++ b/migration_integration_test.go @@ -3,46 +3,87 @@ package migrate import ( + "context" "errors" "net/url" "os" "strings" "testing" + "time" - "github.com/globalsign/mgo" - "github.com/globalsign/mgo/bson" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" ) const testCollection = "test" -func cleanup(db *mgo.Database) { - collections, err := db.CollectionNames() +type index struct { + Key map[string]int + NS string + Name string +} + +func cleanup(db *mongo.Database) { + filter := bson.D{bson.E{Key: "type", Value: "collection"}} + options := options.ListCollections().SetNameOnly(true) + + cursor, err := db.ListCollections(context.Background(), filter, options) if err != nil { panic(err) } + + defer cursor.Close(context.TODO()) + + var collections []collectionSpecification + + for cursor.Next(context.TODO()) { + var collection collectionSpecification + + err := cursor.Decode(&collection) + if err != nil { + panic(err) + } + + collections = append(collections, collection) + } + + if err := cursor.Err(); err != nil { + panic(err) + } + for _, collection := range collections { - db.C(collection).DropAllIndexes() - db.C(collection).DropCollection() + _, err := db.Collection(collection.Name).Indexes().DropAll(context.TODO()) + if err != nil { + panic(err) + } + err = db.Collection(collection.Name).Drop(context.TODO()) + if err != nil { + panic(err) + } } } -var mongo *mgo.Database +var db *mongo.Database func TestMain(m *testing.M) { addr, err := url.Parse(os.Getenv("MONGO_URL")) - session, err := mgo.Dial(addr.String()) + opt := options.Client().ApplyURI(addr.String()) + client, err := mongo.NewClient(opt) + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + err = client.Connect(ctx) if err != nil { panic(err) } - defer session.Close() - mongo = session.DB(strings.TrimLeft(addr.Path, "/")) - defer cleanup(mongo) + db = client.Database(strings.TrimLeft(addr.Path, "/")) + defer cleanup(db) os.Exit(m.Run()) } func TestSetGetVersion(t *testing.T) { - defer cleanup(mongo) - migrate := NewMigrate(mongo) + defer cleanup(db) + migrate := NewMigrate(db) if err := migrate.SetVersion(1, "hello"); err != nil { t.Errorf("Unexpected error: %v", err) return @@ -87,8 +128,8 @@ func TestSetGetVersion(t *testing.T) { } func TestVersionBeforeSet(t *testing.T) { - defer cleanup(mongo) - migrate := NewMigrate(mongo) + defer cleanup(db) + migrate := NewMigrate(db) version, _, err := migrate.Version() if err != nil { t.Errorf("Unexpected error: %v", err) @@ -101,13 +142,26 @@ func TestVersionBeforeSet(t *testing.T) { } func TestUpMigrations(t *testing.T) { - defer cleanup(mongo) - migrate := NewMigrate(mongo, - Migration{Version: 1, Description: "hello", Up: func(db *mgo.Database) error { - return db.C(testCollection).Insert(bson.M{"hello": "world"}) + defer cleanup(db) + migrate := NewMigrate(db, + Migration{Version: 1, Description: "hello", Up: func(db *mongo.Database) error { + _, err := db.Collection(testCollection).InsertOne(context.TODO(), bson.D{{"hello", "world"}}) + if err != nil { + return err + } + + return nil }}, - Migration{Version: 2, Description: "world", Up: func(db *mgo.Database) error { - return db.C(testCollection).EnsureIndex(mgo.Index{Name: "test_idx", Key: []string{"hello"}}) + Migration{Version: 2, Description: "world", Up: func(db *mongo.Database) error { + opt := options.Index().SetName("test_idx") + keys := bson.D{{"hello", 1}} + model := mongo.IndexModel{Keys: keys, Options: opt} + _, err := db.Collection(testCollection).Indexes().CreateOne(context.TODO(), model) + if err != nil { + return err + } + + return nil }}, ) if err := migrate.Up(AllAvailable); err != nil { @@ -123,40 +177,86 @@ func TestUpMigrations(t *testing.T) { t.Errorf("Unexpected version/description %v %v", version, description) return } - doc := bson.M{} - if err := mongo.C(testCollection).Find(bson.M{"hello": "world"}).One(&doc); err != nil { + result := db.Collection(testCollection).FindOne(context.TODO(), bson.D{{"hello", "world"}}) + if result.Err() != nil { t.Errorf("Unexpected error: %v", err) return } - if doc["hello"].(string) != "world" { + doc := bson.D{} + if err := result.Decode(&doc); err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + if doc.Map()["hello"].(string) != "world" { t.Errorf("Unexpected data") return } - indexes, err := mongo.C(testCollection).Indexes() + cursor, err := db.Collection(testCollection).Indexes().List(context.TODO()) if err != nil { t.Errorf("Unexpected error: %v", err) return } - for _, index := range indexes { - if index.Name == "test_idx" { + defer cursor.Close(context.TODO()) + + var indexes []index + for cursor.Next(context.TODO()) { + var index index + + err := cursor.Decode(&index) + if err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + + indexes = append(indexes, index) + } + + if err := cursor.Err(); err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + + for _, v := range indexes { + if v.Name == "test_idx" { return } } + t.Errorf("Expected index not found") } func TestDownMigrations(t *testing.T) { - defer cleanup(mongo) - migrate := NewMigrate(mongo, - Migration{Version: 1, Description: "hello", Up: func(db *mgo.Database) error { - return db.C(testCollection).Insert(bson.M{"hello": "world"}) - }, Down: func(db *mgo.Database) error { - return db.C(testCollection).Remove(bson.M{"hello": "world"}) + defer cleanup(db) + migrate := NewMigrate(db, + Migration{Version: 1, Description: "hello", Up: func(db *mongo.Database) error { + _, err := db.Collection(testCollection).InsertOne(context.TODO(), bson.D{{"hello", "world"}}) + if err != nil { + return err + } + return nil + }, Down: func(db *mongo.Database) error { + _, err := db.Collection(testCollection).DeleteOne(context.TODO(), bson.D{{"hello", "world"}}) + if err != nil { + return err + } + return nil }}, - Migration{Version: 2, Description: "world", Up: func(db *mgo.Database) error { - return db.C(testCollection).EnsureIndex(mgo.Index{Name: "test_idx", Key: []string{"hello"}}) - }, Down: func(db *mgo.Database) error { - return db.C(testCollection).DropIndexName("test_idx") + Migration{Version: 2, Description: "world", Up: func(db *mongo.Database) error { + opt := options.Index().SetName("test_idx") + keys := bson.D{{"hello", 1}} + model := mongo.IndexModel{Keys: keys, Options: opt} + _, err := db.Collection(testCollection).Indexes().CreateOne(context.TODO(), model) + if err != nil { + return err + } + + return nil + }, Down: func(db *mongo.Database) error { + _, err := db.Collection(testCollection).Indexes().DropOne(context.TODO(), "test_idx") + if err != nil { + return err + } + return nil }}, ) if err := migrate.Up(AllAvailable); err != nil { @@ -176,18 +276,38 @@ func TestDownMigrations(t *testing.T) { t.Errorf("Unexpected version: %v", version) return } - err = mongo.C(testCollection).Find(bson.M{"hello": "world"}).One(&bson.M{}) - if err != mgo.ErrNotFound { + result := db.Collection(testCollection).FindOne(context.TODO(), bson.D{{"hello", "world"}}) + if err := result.Decode(&bson.D{}); err != mongo.ErrNoDocuments { t.Errorf("Unexpected error: %v", err) return } - indexes, err := mongo.C(testCollection).Indexes() + cursor, err := db.Collection(testCollection).Indexes().List(context.TODO()) if err != nil { t.Errorf("Unexpected error: %v", err) return } - for _, index := range indexes { - if index.Name == "test_idx" { + defer cursor.Close(context.TODO()) + + var indexes []index + for cursor.Next(context.TODO()) { + var index index + + err := cursor.Decode(&index) + if err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + + indexes = append(indexes, index) + } + + if err := cursor.Err(); err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + + for _, v := range indexes { + if v.Name == "test_idx" { t.Errorf("Index unexpectedly found") return } @@ -195,16 +315,31 @@ func TestDownMigrations(t *testing.T) { } func TestPartialUpMigrations(t *testing.T) { - defer cleanup(mongo) - migrate := NewMigrate(mongo, - Migration{Version: 1, Description: "hello", Up: func(db *mgo.Database) error { - return db.C(testCollection).Insert(bson.M{"hello": "world"}) + defer cleanup(db) + migrate := NewMigrate(db, + Migration{Version: 1, Description: "hello", Up: func(db *mongo.Database) error { + _, err := db.Collection(testCollection).InsertOne(context.TODO(), bson.D{{"hello", "world"}}) + if err != nil { + return err + } + return nil }}, - Migration{Version: 2, Description: "world", Up: func(db *mgo.Database) error { - return db.C(testCollection).EnsureIndex(mgo.Index{Name: "test_idx", Key: []string{"hello"}}) + Migration{Version: 2, Description: "world", Up: func(db *mongo.Database) error { + opt := options.Index().SetName("test_idx") + keys := bson.D{{"hello", 1}} + model := mongo.IndexModel{Keys: keys, Options: opt} + _, err := db.Collection(testCollection).Indexes().CreateOne(context.TODO(), model) + if err != nil { + return err + } + return nil }}, - Migration{Version: 3, Description: "shouldn`t be applied", Up: func(db *mgo.Database) error { - return db.C(testCollection).Insert(bson.M{"a": "b"}) + Migration{Version: 3, Description: "shouldn`t be applied", Up: func(db *mongo.Database) error { + _, err := db.Collection(testCollection).InsertOne(context.TODO(), bson.D{{"a", "b"}}) + if err != nil { + return err + } + return nil }}, ) if err := migrate.Up(2); err != nil { @@ -220,20 +355,46 @@ func TestPartialUpMigrations(t *testing.T) { t.Errorf("Unexpected version/description %v %v", version, description) return } - doc := bson.M{} - if err := mongo.C(testCollection).Find(bson.M{"hello": "world"}).One(&doc); err != nil { + result := db.Collection(testCollection).FindOne(context.TODO(), bson.D{{"hello", "world"}}) + if err := result.Err(); err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + var doc bson.D + err = result.Decode(&doc) + if err != nil { t.Errorf("Unexpected error: %v", err) return } - if doc["hello"].(string) != "world" { + if doc.Map()["hello"].(string) != "world" { t.Errorf("Unexpected data") return } - indexes, err := mongo.C(testCollection).Indexes() + cursor, err := db.Collection(testCollection).Indexes().List(context.TODO()) if err != nil { t.Errorf("Unexpected error: %v", err) return } + defer cursor.Close(context.TODO()) + + var indexes []index + for cursor.Next(context.TODO()) { + var index index + + err := cursor.Decode(&index) + if err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + + indexes = append(indexes, index) + } + + if err := cursor.Err(); err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + for _, index := range indexes { if index.Name == "test_idx" { goto okIndex @@ -241,38 +402,65 @@ func TestPartialUpMigrations(t *testing.T) { } t.Errorf("Expected index not found") okIndex: - err = mongo.C(testCollection).Find(bson.M{"a": "b"}).One(&bson.M{}) - if err != mgo.ErrNotFound { + res := db.Collection(testCollection).FindOne(context.TODO(), bson.D{{"a", "b"}}) + if err := res.Decode(&bson.D{}); err != mongo.ErrNoDocuments { t.Errorf("Unexpectedly found data from non-applied migration") return } } func TestPartialDownMigrations(t *testing.T) { - defer cleanup(mongo) - migrate := NewMigrate(mongo, - Migration{Version: 1, Description: "hello", Up: func(db *mgo.Database) error { - return db.C(testCollection).Insert(bson.M{"hello": "world"}) - }, Down: func(db *mgo.Database) error { - return db.C(testCollection).Remove(bson.M{"hello": "world"}) + defer cleanup(db) + migrate := NewMigrate(db, + Migration{Version: 1, Description: "hello", Up: func(db *mongo.Database) error { + _, err := db.Collection(testCollection).InsertOne(context.TODO(), bson.D{{"hello", "world"}}) + if err != nil { + return err + } + return nil + }, Down: func(db *mongo.Database) error { + _, err := db.Collection(testCollection).DeleteOne(context.TODO(), bson.D{{"hello", "world"}}) + if err != nil { + return err + } + return nil }}, - Migration{Version: 2, Description: "world", Up: func(db *mgo.Database) error { - return db.C(testCollection).EnsureIndex(mgo.Index{Name: "test_idx", Key: []string{"hello"}}) - }, Down: func(db *mgo.Database) error { - return db.C(testCollection).DropIndexName("test_idx") + Migration{Version: 2, Description: "world", Up: func(db *mongo.Database) error { + keys := bson.D{{"hello", 1}} + opt := options.Index().SetName("test_idx") + model := mongo.IndexModel{Keys: keys, Options: opt} + _, err := db.Collection(testCollection).Indexes().CreateOne(context.TODO(), model) + if err != nil { + return err + } + return err + }, Down: func(db *mongo.Database) error { + _, err := db.Collection(testCollection).Indexes().DropOne(context.TODO(), "test_idx") + if err != nil { + return err + } + return nil }}, - Migration{Version: 3, Description: "next", Up: func(db *mgo.Database) error { - return db.C(testCollection).Insert(bson.M{"a": "b"}) - }, Down: func(db *mgo.Database) error { - return db.C(testCollection).Remove(bson.M{"a": "b"}) + Migration{Version: 3, Description: "next", Up: func(db *mongo.Database) error { + _, err := db.Collection(testCollection).InsertOne(context.TODO(), bson.D{{"a", "b"}}) + if err != nil { + return err + } + return nil + }, Down: func(db *mongo.Database) error { + _, err := db.Collection(testCollection).DeleteOne(context.TODO(), bson.D{{"a", "b"}}) + if err != nil { + return err + } + return nil }}, ) if err := migrate.Up(AllAvailable); err != nil { t.Errorf("Unexpected error: %v", err) return } - err := mongo.C(testCollection).Find(bson.M{"a": "b"}).One(&bson.M{}) - if err != nil { + result := db.Collection(testCollection).FindOne(context.TODO(), bson.D{{"a", "b"}}) + if err := result.Err(); err != nil { t.Errorf("Unexpected error: %v", err) return } @@ -289,18 +477,18 @@ func TestPartialDownMigrations(t *testing.T) { t.Errorf("Unexpected version/description: %v %v", version, description) return } - err = mongo.C(testCollection).Find(bson.M{"a": "b"}).One(&bson.M{}) - if err != mgo.ErrNotFound { + res := db.Collection(testCollection).FindOne(context.TODO(), bson.D{{"a", "b"}}) + if err := res.Decode(&bson.D{}); err != mongo.ErrNoDocuments { t.Errorf("Unexpected error: %v", err) return } } func TestUpMigrationWithErrors(t *testing.T) { - defer cleanup(mongo) + defer cleanup(db) expectedErr := errors.New("normal error") - migrate := NewMigrate(mongo, - Migration{Version: 1, Description: "hello", Up: func(db *mgo.Database) error { + migrate := NewMigrate(db, + Migration{Version: 1, Description: "hello", Up: func(db *mongo.Database) error { return expectedErr }}, ) @@ -320,12 +508,16 @@ func TestUpMigrationWithErrors(t *testing.T) { } func TestDownMigrationWithErrors(t *testing.T) { - defer cleanup(mongo) + defer cleanup(db) expectedErr := errors.New("normal error") - migrate := NewMigrate(mongo, - Migration{Version: 1, Description: "hello", Up: func(db *mgo.Database) error { - return db.C(testCollection).Insert(bson.M{"hello": "world"}) - }, Down: func(db *mgo.Database) error { + migrate := NewMigrate(db, + Migration{Version: 1, Description: "hello", Up: func(db *mongo.Database) error { + _, err := db.Collection(testCollection).InsertOne(context.TODO(), bson.D{{"hello", "world"}}) + if err != nil { + return err + } + return nil + }, Down: func(db *mongo.Database) error { return expectedErr }}, ) @@ -349,14 +541,14 @@ func TestDownMigrationWithErrors(t *testing.T) { } func TestMultipleUpMigration(t *testing.T) { - defer cleanup(mongo) + defer cleanup(db) var cnt int - migrate := NewMigrate(mongo, - Migration{Version: 1, Description: "hello", Up: func(db *mgo.Database) error { + migrate := NewMigrate(db, + Migration{Version: 1, Description: "hello", Up: func(db *mongo.Database) error { cnt++ return nil }}, - Migration{Version: 2, Description: "world", Up: func(db *mgo.Database) error { + Migration{Version: 2, Description: "world", Up: func(db *mongo.Database) error { cnt++ return nil }}, @@ -385,18 +577,18 @@ func TestMultipleUpMigration(t *testing.T) { } func TestMultipleDownMigration(t *testing.T) { - defer cleanup(mongo) + defer cleanup(db) var cnt int - migrate := NewMigrate(mongo, - Migration{Version: 1, Description: "hello", Up: func(db *mgo.Database) error { + migrate := NewMigrate(db, + Migration{Version: 1, Description: "hello", Up: func(db *mongo.Database) error { return nil - }, Down: func(db *mgo.Database) error { + }, Down: func(db *mongo.Database) error { cnt++ return nil }}, - Migration{Version: 2, Description: "world", Up: func(db *mgo.Database) error { + Migration{Version: 2, Description: "world", Up: func(db *mongo.Database) error { return nil - }, Down: func(db *mgo.Database) error { + }, Down: func(db *mongo.Database) error { cnt++ return nil }},