Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfe3ec0fce | ||
|
|
01b25ee18b | ||
|
|
afc973c4e5 | ||
|
|
250543a561 | ||
|
|
1dffba9fa5 | ||
|
|
964e6bc070 | ||
|
|
1887c0f3e3 | ||
|
|
62dba4f8ea | ||
|
|
78ca4eb007 | ||
|
|
d21443f74b | ||
|
|
7f4000c0d6 | ||
|
|
ecd74e1ce6 | ||
|
|
9f14676773 | ||
|
|
7dde434714 | ||
|
|
fff063351d | ||
|
|
5498e70562 | ||
|
|
0da98940d6 | ||
|
|
4cfc044489 | ||
|
|
83c703f09f | ||
|
|
b7845a779e | ||
|
|
09640b136e | ||
|
|
7ca71e567d | ||
|
|
c7653285b2 | ||
|
|
f4979054e2 | ||
|
|
305a63a447 | ||
|
|
528bc5bbbb | ||
|
|
83e27c8b88 | ||
|
|
e8bfb1b2b3 | ||
|
|
2cd9bfcd04 | ||
|
|
21ccec2aa1 | ||
|
|
fe6711645a | ||
|
|
adf1cff659 | ||
|
|
da12e27062 | ||
|
|
d2ad6f8ecf | ||
|
|
bbb58aca8d | ||
|
|
5aed294d25 | ||
|
|
e92f038f40 | ||
|
|
0b3426bf5d | ||
|
|
0861ff957d | ||
|
|
32607ef715 | ||
|
|
c643ffd6d6 | ||
|
|
b8b3883950 | ||
|
|
3547fc54cb | ||
|
|
64c41a8af0 | ||
|
|
4191ac9cfe | ||
|
|
f1738e78db | ||
|
|
de494e4b98 | ||
|
|
585509f3bc | ||
|
|
f630dc5971 | ||
|
|
9a8f78ba3b | ||
|
|
63c6168c7f | ||
|
|
0c80688879 | ||
|
|
1993f0783b | ||
|
|
70238f448b | ||
|
|
914c38e379 | ||
|
|
0f28b01383 | ||
|
|
6909594c0b | ||
|
|
583921e8bf | ||
|
|
61ecf2c096 | ||
|
|
0eecb74230 | ||
|
|
b7b75f3c59 | ||
|
|
fe48ddfdcc | ||
|
|
70b23c3f84 | ||
|
|
223908f9d2 | ||
|
|
1838852d4d | ||
|
|
175550643c | ||
|
|
34a3e58ab9 | ||
|
|
dec52a778f | ||
|
|
8865262a03 | ||
|
|
eb0eec4471 | ||
|
|
da6c505f41 | ||
|
|
966ba843e0 | ||
|
|
5773f75698 | ||
|
|
3e3cef3763 | ||
|
|
d4796c8104 | ||
|
|
cd15e67614 | ||
|
|
11b4db8a90 | ||
|
|
474d0f5a60 | ||
|
|
b4d9b6ae24 | ||
|
|
e97cecb0cf | ||
|
|
492671437f | ||
|
|
cb2fa36c8b | ||
|
|
b94dd6fc29 | ||
|
|
7910a55d0e | ||
|
|
ae018f8ade | ||
|
|
33aebee975 | ||
|
|
478fb42df5 | ||
|
|
9c7172291e | ||
|
|
9e11d704a4 | ||
|
|
d290bf9460 | ||
|
|
04e1cb0cfc | ||
|
|
9a558eed89 | ||
|
|
74b161fdb2 | ||
|
|
32cb825110 | ||
|
|
9bd4c8c83c | ||
|
|
e9ede5506a | ||
|
|
e27b9c2a7b | ||
|
|
834a096bf8 | ||
|
|
866ff82ffe | ||
|
|
570e4b42be | ||
|
|
c71fea90d6 | ||
|
|
5f7b2caee8 | ||
|
|
64101a13e9 | ||
|
|
8630ea78b7 | ||
|
|
aa4dc1252b | ||
|
|
0cd470ee98 | ||
|
|
cba0cc9847 | ||
|
|
42b45a6de8 | ||
|
|
cd5f75d47c | ||
|
|
6612806e06 | ||
|
|
a7fa9eb3fb | ||
|
|
907d8cd935 | ||
|
|
b5ef068704 | ||
|
|
b68c58c136 | ||
|
|
f230ced201 | ||
|
|
91a3364232 | ||
|
|
ce1b59bb81 | ||
|
|
12c19e08b7 | ||
|
|
7f9f7e5978 | ||
|
|
90bab2409b | ||
|
|
10d89631a3 | ||
|
|
5593c9892b | ||
|
|
081a4e2cae | ||
|
|
5a8219c970 | ||
|
|
5a245a959a | ||
|
|
9694775972 | ||
|
|
e303d64469 | ||
|
|
6028882529 | ||
|
|
2f6745a42f | ||
|
|
8fe1a56731 | ||
|
|
f5053fbe29 | ||
|
|
2a2bc4fec8 | ||
|
|
ade683e8b5 | ||
|
|
65674ac4cc | ||
|
|
9754378aa5 | ||
|
|
080f49baf4 | ||
|
|
3bc76ee0bb | ||
|
|
46c831b8e9 | ||
|
|
a35e2a3c7a | ||
|
|
8ae2c91ef4 | ||
|
|
b4e84ac8f9 | ||
|
|
ef698d8132 | ||
|
|
5cf4286653 | ||
|
|
2957444991 | ||
|
|
0c7f7b79e0 | ||
|
|
e351ab27b9 | ||
|
|
432a0b56d5 | ||
|
|
531fdf64e5 | ||
|
|
7a0e24ebf3 | ||
|
|
0f9ca3b392 | ||
|
|
02c5fc24d3 | ||
|
|
e857fcc785 | ||
|
|
07fedf8445 | ||
|
|
c848ca946e | ||
|
|
879cc40d38 | ||
|
|
ca48601396 | ||
|
|
c7bacb831b | ||
|
|
68a658aff4 | ||
|
|
d8df2cd1cc | ||
|
|
0741616d2e | ||
|
|
9bb037339f | ||
|
|
7dcd8fe1d7 | ||
|
|
8d39928220 | ||
|
|
cd2c561564 | ||
|
|
0c9a8408c7 | ||
|
|
e72d592ef5 | ||
|
|
bf81444464 | ||
|
|
cac6e08b44 | ||
|
|
f12373eb83 | ||
|
|
3502e0252c | ||
|
|
ee6a71da4c | ||
|
|
1333184c7b | ||
|
|
ef6572b9aa | ||
|
|
820c58b7f2 | ||
|
|
ab520d7b6b | ||
|
|
2cf4b2c33e | ||
|
|
bcba31af11 | ||
|
|
f61157f6a6 | ||
|
|
48fe666dbe | ||
|
|
05e520bd44 | ||
|
|
3ce2dc92c2 | ||
|
|
6b535262fd | ||
|
|
94efc48c49 | ||
|
|
6bce23e36a | ||
|
|
c94461932f | ||
|
|
150ce9b524 | ||
|
|
84ea5f6842 | ||
|
|
5f1ca5dadc | ||
|
|
d5f5eb65a6 | ||
|
|
7ae5048cb3 | ||
|
|
357fbc15d9 | ||
|
|
c1f0a1a044 | ||
|
|
5b6715333f | ||
|
|
7b1a07fa97 |
+2
-1
@@ -1 +1,2 @@
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
test.db
|
||||
+14
-13
@@ -1,20 +1,21 @@
|
||||
language: go
|
||||
sudo: required
|
||||
|
||||
go:
|
||||
- 1.2
|
||||
- 1.3
|
||||
- tip
|
||||
|
||||
addons:
|
||||
postgresql: "9.3"
|
||||
- 1.4.x
|
||||
- 1.5.x
|
||||
- 1.6.x
|
||||
- 1.7.x
|
||||
- 1.8.x
|
||||
|
||||
services:
|
||||
- cassandra
|
||||
- docker
|
||||
|
||||
before_script:
|
||||
- >
|
||||
/usr/local/cassandra/bin/cqlsh -e "CREATE KEYSPACE migratetest WITH REPLICATION = {'class': 'SimpleStrategy', 'replication_factor' : 1};"
|
||||
- psql -c 'create database migratetest;' -U postgres
|
||||
- mysql -e 'create database migratetest;'
|
||||
before_install:
|
||||
- curl -L https://github.com/docker/compose/releases/download/1.11.2/docker-compose-`uname -s`-`uname -m` > docker-compose
|
||||
- chmod +x docker-compose
|
||||
- sudo mv docker-compose /usr/local/bin
|
||||
- sed -i -e 's/golang/golang:'"$TRAVIS_GO_VERSION"'/' docker-compose.yml
|
||||
|
||||
script: go test -p 1 ./...
|
||||
|
||||
script: make test
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
FROM scratch
|
||||
ADD migrate /migrate
|
||||
ENTRYPOINT ["/migrate"]
|
||||
@@ -0,0 +1,30 @@
|
||||
TESTFLAGS?=
|
||||
IMAGE=mattes/migrate
|
||||
DCR=docker-compose run --rm
|
||||
GOTEST=go test $(TESTFLAGS) `go list ./... | grep -v "/vendor/"`
|
||||
|
||||
.PHONY: clean test build release docker-build docker-push run
|
||||
all: release
|
||||
|
||||
clean:
|
||||
rm -f migrate
|
||||
|
||||
fmt:
|
||||
@gofmt -s -w `go list -f {{.Dir}} ./... | grep -v "/vendor/"`
|
||||
|
||||
test: fmt
|
||||
$(DCR) go-test
|
||||
|
||||
go-test: fmt
|
||||
@$(GOTEST)
|
||||
|
||||
build:
|
||||
$(DCR) go-build
|
||||
|
||||
release: test build docker-build docker-push
|
||||
|
||||
docker-build:
|
||||
docker build --rm -t $(IMAGE) .
|
||||
|
||||
docker-push:
|
||||
docker push $(IMAGE)
|
||||
@@ -1,19 +1,23 @@
|
||||
# You are looking at a deprecated version.
|
||||
# Use the [latest version](https://github.com/mattes/migrate) instead.
|
||||
|
||||
---
|
||||
|
||||
# migrate
|
||||
|
||||
[](https://travis-ci.org/mattes/migrate)
|
||||
[](https://godoc.org/github.com/mattes/migrate)
|
||||
[](https://travis-ci.org/mattes/migrate)
|
||||
|
||||
A migration helper written in Go. Use it in your existing Golang code
|
||||
or run commands via the CLI.
|
||||
|
||||
```
|
||||
GoCode import github.com/mattes/migrate/migrate
|
||||
CLI go get -u github.com/mattes/migrate
|
||||
GoCode import gopkg.in/mattes/migrate.v1/migrate
|
||||
CLI go get -u gopkg.in/mattes/migrate.v1
|
||||
```
|
||||
|
||||
__Features__
|
||||
|
||||
* Super easy to implement [Driver interface](http://godoc.org/github.com/mattes/migrate/driver#Driver).
|
||||
* Super easy to implement [Driver interface](http://godoc.org/gopkg.in/mattes/migrate.v1/driver#Driver).
|
||||
* Gracefully quit running migrations on ``^C``.
|
||||
* No magic search paths routines, no hard-coded config files.
|
||||
* CLI is build on top of the ``migrate package``.
|
||||
@@ -21,20 +25,24 @@ __Features__
|
||||
|
||||
## Available Drivers
|
||||
|
||||
* [PostgreSQL](https://github.com/mattes/migrate/tree/master/driver/postgres)
|
||||
* [Cassandra](https://github.com/mattes/migrate/tree/master/driver/cassandra)
|
||||
* SQLite ([planned](https://github.com/mattes/migrate/issues/2))
|
||||
* [MySQL](https://github.com/mattes/migrate/tree/master/driver/mysql) ([experimental](https://github.com/mattes/migrate/issues/1#issuecomment-58728186))
|
||||
* Bash (planned)
|
||||
* [PostgreSQL](driver/postgres)
|
||||
* [Cassandra](driver/cassandra)
|
||||
* [SQLite](driver/sqlite3)
|
||||
* [MySQL](driver/mysql) ([experimental](https://github.com/mattes/migrate/issues/1#issuecomment-58728186))
|
||||
* [Neo4j](driver/neo4j)
|
||||
* [Ql](driver/ql)
|
||||
* [MongoDB](driver/mongodb)
|
||||
* [CrateDB](driver/crate)
|
||||
* [MSSQL](driver/mssql)
|
||||
|
||||
Need another driver? Just implement the [Driver interface](http://godoc.org/github.com/mattes/migrate/driver#Driver) and open a PR.
|
||||
Need another driver? Just implement the [Driver interface](http://godoc.org/gopkg.in/mattes/migrate.v1/driver#Driver) and open a PR.
|
||||
|
||||
|
||||
## Usage from Terminal
|
||||
|
||||
```bash
|
||||
# install
|
||||
go get github.com/mattes/migrate
|
||||
go get gopkg.in/mattes/migrate.v1
|
||||
|
||||
# create new migration file in path
|
||||
migrate -url driver://url -path ./migrations create migration_file_xyz
|
||||
@@ -65,16 +73,21 @@ migrate -url driver://url -path ./migrations migrate -2
|
||||
migrate -url driver://url -path ./migrations migrate -n
|
||||
|
||||
# go to specific migration
|
||||
migrate -url driver://url -path ./migrations goto 1
|
||||
migrate -url driver://url -path ./migrations goto 10
|
||||
migrate -url driver://url -path ./migrations goto v
|
||||
```
|
||||
|
||||
|
||||
## Usage in Go
|
||||
|
||||
See GoDoc here: http://godoc.org/github.com/mattes/migrate/migrate
|
||||
See GoDoc here: http://godoc.org/gopkg.in/mattes/migrate.v1/migrate
|
||||
|
||||
```go
|
||||
import "github.com/mattes/migrate/migrate"
|
||||
import "gopkg.in/mattes/migrate.v1/migrate"
|
||||
|
||||
// Import any required drivers so that they are registered and available
|
||||
import _ "gopkg.in/mattes/migrate.v1/driver/mysql"
|
||||
|
||||
// use synchronous versions of migration functions ...
|
||||
allErrors, ok := migrate.UpSync("driver://url", "./path")
|
||||
@@ -95,15 +108,15 @@ go migrate.Up(pipe, "driver://url", "./path")
|
||||
The format of migration files looks like this:
|
||||
|
||||
```
|
||||
001_initial_plan_to_do_sth.up.sql # up migration instructions
|
||||
001_initial_plan_to_do_sth.down.sql # down migration instructions
|
||||
002_xxx.up.sql
|
||||
002_xxx.down.sql
|
||||
1481574547_initial_plan_to_do_sth.up.sql # up migration instructions
|
||||
1481574547_initial_plan_to_do_sth.down.sql # down migration instructions
|
||||
1482438365_xxx.up.sql
|
||||
1482438365_xxx.down.sql
|
||||
...
|
||||
```
|
||||
|
||||
Why two files? This way you could still do sth like
|
||||
``psql -f ./db/migrations/001_initial_plan_to_do_sth.up.sql`` and there is no
|
||||
``psql -f ./db/migrations/1481574547_initial_plan_to_do_sth.up.sql`` and there is no
|
||||
need for any custom markup language to divide up and down migrations. Please note
|
||||
that the filename extension depends on the driver.
|
||||
|
||||
@@ -114,5 +127,6 @@ that the filename extension depends on the driver.
|
||||
* https://github.com/tanel/dbmigrate
|
||||
* https://github.com/BurntSushi/migration
|
||||
* https://github.com/DavidHuie/gomigrate
|
||||
* https://github.com/rubenv/sql-migrate
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
go: &go
|
||||
image: golang
|
||||
working_dir: /go/src/gopkg.in/mattes/migrate.v1
|
||||
volumes:
|
||||
- $GOPATH/src/github.com/mattes/migrate:/go/src/gopkg.in/mattes/migrate.v1
|
||||
go-test:
|
||||
<<: *go
|
||||
command: sh -c 'go get -t -v ./... && go test -v ./...'
|
||||
links:
|
||||
- mssql
|
||||
- postgres
|
||||
- mysql
|
||||
- cassandra
|
||||
- crate
|
||||
- mongo
|
||||
go-build:
|
||||
<<: *go
|
||||
command: sh -c 'go get -v && go build -ldflags ''-s'' -o migrater'
|
||||
environment:
|
||||
CGO_ENABLED: 1
|
||||
postgres:
|
||||
image: postgres
|
||||
mysql:
|
||||
image: mysql
|
||||
environment:
|
||||
MYSQL_DATABASE: migratetest
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
|
||||
cassandra:
|
||||
image: cassandra:2.2
|
||||
crate:
|
||||
image: crate
|
||||
mongo:
|
||||
image: mongo:3.2.6
|
||||
mssql:
|
||||
image: microsoft/mssql-server-linux
|
||||
environment:
|
||||
SA_PASSWORD: Passw0rd
|
||||
ACCEPT_EULA: "Y"
|
||||
@@ -8,4 +8,4 @@
|
||||
migrate -url bash://xxx -path ./migrations create increment_xyz
|
||||
migrate -url bash://xxx -path ./migrations up
|
||||
migrate help # for more info
|
||||
```
|
||||
```
|
||||
|
||||
+6
-2
@@ -2,8 +2,8 @@
|
||||
package bash
|
||||
|
||||
import (
|
||||
"github.com/mattes/migrate/file"
|
||||
_ "github.com/mattes/migrate/migrate/direction"
|
||||
"gopkg.in/mattes/migrate.v1/driver"
|
||||
"gopkg.in/mattes/migrate.v1/file"
|
||||
)
|
||||
|
||||
type Driver struct {
|
||||
@@ -30,3 +30,7 @@ func (driver *Driver) Migrate(f file.File, pipe chan interface{}) {
|
||||
func (driver *Driver) Version() (uint64, error) {
|
||||
return uint64(0), nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
driver.RegisterDriver("bash", &Driver{})
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package bash
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
import "testing"
|
||||
|
||||
func TestMigrate(t *testing.T) {
|
||||
|
||||
|
||||
@@ -8,6 +8,13 @@ migrate -url cassandra://host:port/keyspace -path ./db/migrations up
|
||||
migrate help # for more info
|
||||
```
|
||||
|
||||
Url format
|
||||
- Authentication: `cassandra://username:password@host:port/keyspace`
|
||||
- Cassandra v3.x: `cassandra://host:port/keyspace?protocol=4`
|
||||
|
||||
|
||||
## Authors
|
||||
|
||||
* Paul Bergeron, https://github.com/dinedal
|
||||
* Paul Bergeron, https://github.com/dinedal
|
||||
* Johnny Bergström, https://github.com/balboah
|
||||
* pateld982, http://github.com/pateld982
|
||||
|
||||
+136
-40
@@ -2,34 +2,78 @@
|
||||
package cassandra
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"github.com/mattes/migrate/file"
|
||||
"github.com/mattes/migrate/migrate/direction"
|
||||
"gopkg.in/mattes/migrate.v1/driver"
|
||||
"gopkg.in/mattes/migrate.v1/file"
|
||||
"gopkg.in/mattes/migrate.v1/migrate/direction"
|
||||
)
|
||||
|
||||
// Driver implements migrate Driver interface
|
||||
type Driver struct {
|
||||
session *gocql.Session
|
||||
}
|
||||
|
||||
const tableName = "schema_migrations"
|
||||
const versionRow = 1
|
||||
const (
|
||||
tableName = "schema_migrations"
|
||||
)
|
||||
|
||||
// Cassandra Driver URL format:
|
||||
// cassandra://host:port/keyspace
|
||||
//
|
||||
// Example:
|
||||
// cassandra://localhost/SpaceOfKeys
|
||||
// Initialize will be called first
|
||||
func (driver *Driver) Initialize(rawurl string) error {
|
||||
u, err := url.Parse(rawurl)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse connectil url: %v", err)
|
||||
}
|
||||
|
||||
if u.Path == "" {
|
||||
return fmt.Errorf("no keyspace provided in connection url")
|
||||
}
|
||||
|
||||
cluster := gocql.NewCluster(u.Host)
|
||||
cluster.Keyspace = u.Path[1:len(u.Path)]
|
||||
cluster.Consistency = gocql.All
|
||||
cluster.Timeout = 1 * time.Minute
|
||||
|
||||
if len(u.Query().Get("consistency")) > 0 {
|
||||
var consistency gocql.Consistency
|
||||
consistency, err = parseConsistency(u.Query().Get("consistency"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cluster.Consistency = consistency
|
||||
}
|
||||
|
||||
if len(u.Query().Get("protocol")) > 0 {
|
||||
var protoversion int
|
||||
protoversion, err = strconv.Atoi(u.Query().Get("protocol"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cluster.ProtoVersion = protoversion
|
||||
}
|
||||
|
||||
// Check if url user struct is null
|
||||
if u.User != nil {
|
||||
password, passwordSet := u.User.Password()
|
||||
|
||||
if passwordSet == false {
|
||||
return fmt.Errorf("Missing password. Please provide password")
|
||||
}
|
||||
|
||||
cluster.Authenticator = gocql.PasswordAuthenticator{
|
||||
Username: u.User.Username(),
|
||||
Password: password,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
driver.session, err = cluster.CreateSession()
|
||||
|
||||
if err != nil {
|
||||
@@ -42,62 +86,114 @@ func (driver *Driver) Initialize(rawurl string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close last function to be called. Closes cassandra session
|
||||
func (driver *Driver) Close() error {
|
||||
driver.session.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver *Driver) ensureVersionTableExists() error {
|
||||
err := driver.session.Query("CREATE TABLE IF NOT EXISTS " + tableName + " (version counter, versionRow bigint primary key);").Exec()
|
||||
err := driver.session.Query(fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (id uuid primary key, version bigint)", tableName)).Exec()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = driver.Version()
|
||||
if err != nil {
|
||||
driver.session.Query("UPDATE "+tableName+" SET version = version + 1 where versionRow = ?", versionRow).Exec()
|
||||
if _, err = driver.Version(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FilenameExtension return extension of migrations files
|
||||
func (driver *Driver) FilenameExtension() string {
|
||||
return "cql"
|
||||
}
|
||||
|
||||
func (driver *Driver) updateVersion(version uint64, dir direction.Direction) error {
|
||||
var ids []string
|
||||
var id string
|
||||
var err error
|
||||
iter := driver.session.Query(fmt.Sprintf("SELECT id FROM %s WHERE version >= ? ALLOW FILTERING", tableName), version).Iter()
|
||||
for iter.Scan(&id) {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
if len(ids) > 0 {
|
||||
err = driver.session.Query(fmt.Sprintf("DELETE FROM %s WHERE id IN ?", tableName), ids).Exec()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if dir == direction.Up {
|
||||
return driver.session.Query(fmt.Sprintf("INSERT INTO %s (id, version) VALUES (uuid(), ?)", tableName), version).Exec()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Migrate run migration file. Restore previous version in case of fail
|
||||
func (driver *Driver) Migrate(f file.File, pipe chan interface{}) {
|
||||
defer close(pipe)
|
||||
pipe <- f
|
||||
|
||||
if f.Direction == direction.Up {
|
||||
err := driver.session.Query("UPDATE "+tableName+" SET version = version + 1 where versionRow = ?", versionRow).Exec()
|
||||
if err != nil {
|
||||
pipe <- err
|
||||
return
|
||||
}
|
||||
} else if f.Direction == direction.Down {
|
||||
err := driver.session.Query("UPDATE "+tableName+" SET version = version - 1 where versionRow = ?", versionRow).Exec()
|
||||
if err != nil {
|
||||
pipe <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := f.ReadContent(); err != nil {
|
||||
pipe <- err
|
||||
return
|
||||
}
|
||||
|
||||
err := driver.session.Query(string(f.Content)).Exec()
|
||||
|
||||
var err error
|
||||
previousVersion, err := driver.Version()
|
||||
if err != nil {
|
||||
pipe <- err
|
||||
close(pipe)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
// Invert version direction if we couldn't apply the changes for some reason.
|
||||
if updErr := driver.updateVersion(previousVersion, direction.Up); updErr != nil {
|
||||
pipe <- updErr
|
||||
}
|
||||
pipe <- err
|
||||
}
|
||||
close(pipe)
|
||||
}()
|
||||
|
||||
pipe <- f
|
||||
if err = driver.updateVersion(f.Version, f.Direction); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = f.ReadContent(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, query := range strings.Split(string(f.Content), ";") {
|
||||
query = strings.TrimSpace(query)
|
||||
if len(query) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if err = driver.session.Query(query).Exec(); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Version return current version
|
||||
func (driver *Driver) Version() (uint64, error) {
|
||||
var version int64
|
||||
err := driver.session.Query("SELECT version FROM "+tableName+" WHERE versionRow = ?", versionRow).Scan(&version)
|
||||
return uint64(version) - 1, err
|
||||
err := driver.session.Query(fmt.Sprintf("SELECT max(version) FROM %s", tableName)).Scan(&version)
|
||||
return uint64(version), err
|
||||
}
|
||||
|
||||
func init() {
|
||||
driver.RegisterDriver("cassandra", &Driver{})
|
||||
}
|
||||
|
||||
// ParseConsistency wraps gocql.ParseConsistency to return an error
|
||||
// instead of a panicing.
|
||||
func parseConsistency(consistencyStr string) (consistency gocql.Consistency, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
var ok bool
|
||||
err, ok = r.(error)
|
||||
if !ok {
|
||||
err = fmt.Errorf("Failed to parse consistency \"%s\": %v", consistencyStr, r)
|
||||
}
|
||||
}
|
||||
}()
|
||||
consistency = gocql.ParseConsistency(consistencyStr)
|
||||
|
||||
return consistency, nil
|
||||
}
|
||||
|
||||
@@ -2,21 +2,29 @@ package cassandra
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"github.com/mattes/migrate/file"
|
||||
"github.com/mattes/migrate/migrate/direction"
|
||||
pipep "github.com/mattes/migrate/pipe"
|
||||
"gopkg.in/mattes/migrate.v1/file"
|
||||
"gopkg.in/mattes/migrate.v1/migrate/direction"
|
||||
pipep "gopkg.in/mattes/migrate.v1/pipe"
|
||||
)
|
||||
|
||||
func TestMigrate(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode.")
|
||||
}
|
||||
|
||||
var session *gocql.Session
|
||||
driverUrl := "cassandra://localhost/migratetest"
|
||||
|
||||
host := os.Getenv("CASSANDRA_PORT_9042_TCP_ADDR")
|
||||
port := os.Getenv("CASSANDRA_PORT_9042_TCP_PORT")
|
||||
driverURL := "cassandra://" + host + ":" + port + "/system"
|
||||
|
||||
// prepare a clean test database
|
||||
u, err := url.Parse(driverUrl)
|
||||
u, err := url.Parse(driverURL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -27,20 +35,25 @@ func TestMigrate(t *testing.T) {
|
||||
cluster.Timeout = 1 * time.Minute
|
||||
|
||||
session, err = cluster.CreateSession()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := session.Query(`DROP TABLE IF EXISTS yolo`).Exec(); err != nil {
|
||||
if err = session.Query(`DROP KEYSPACE IF EXISTS migrate;`).Exec(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := session.Query(`DROP TABLE IF EXISTS ` + tableName).Exec(); err != nil {
|
||||
if err = session.Query(`CREATE KEYSPACE IF NOT EXISTS migrate WITH REPLICATION = {'class': 'SimpleStrategy', 'replication_factor': 1};`).Exec(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cluster.Keyspace = "migrate"
|
||||
session, err = cluster.CreateSession()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
driverURL = "cassandra://" + host + ":" + port + "/migrate"
|
||||
|
||||
d := &Driver{}
|
||||
if err := d.Initialize(driverUrl); err != nil {
|
||||
if err := d.Initialize(driverURL); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -53,8 +66,10 @@ func TestMigrate(t *testing.T) {
|
||||
Direction: direction.Up,
|
||||
Content: []byte(`
|
||||
CREATE TABLE yolo (
|
||||
id varint primary key
|
||||
id varint primary key,
|
||||
msg text
|
||||
);
|
||||
|
||||
`),
|
||||
},
|
||||
{
|
||||
@@ -105,5 +120,35 @@ func TestMigrate(t *testing.T) {
|
||||
if err := d.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitializeReturnsErrorsForBadUrls(t *testing.T) {
|
||||
var session *gocql.Session
|
||||
|
||||
host := os.Getenv("CASSANDRA_PORT_9042_TCP_ADDR")
|
||||
port := os.Getenv("CASSANDRA_PORT_9042_TCP_PORT")
|
||||
|
||||
cluster := gocql.NewCluster(host)
|
||||
cluster.Consistency = gocql.All
|
||||
cluster.Timeout = 1 * time.Minute
|
||||
|
||||
session, err := cluster.CreateSession()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer session.Close()
|
||||
if err := session.Query(`CREATE KEYSPACE IF NOT EXISTS migrate WITH REPLICATION = {'class': 'SimpleStrategy', 'replication_factor': 1};`).Exec(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
d := &Driver{}
|
||||
invalidURL := "sdf://asdf://as?df?a"
|
||||
if err := d.Initialize(invalidURL); err == nil {
|
||||
t.Errorf("expected an error to be returned if url could not be parsed")
|
||||
}
|
||||
|
||||
noKeyspace := "cassandra://" + host + ":" + port
|
||||
if err := d.Initialize(noKeyspace); err == nil {
|
||||
t.Errorf("expected an error to be returned if no keyspace provided")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# Crate driver
|
||||
|
||||
This is a driver for the [Crate](https://crate.io) database. It is based on the Crate
|
||||
sql driver by [herenow](https://github.com/herenow/go-crate).
|
||||
|
||||
This driver does not use transactions! This is not a limitation of the driver, but a
|
||||
limitation of Crate. So handle situations with failed migrations with care!
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
migrate -url http://host:port -path ./db/migrations create add_field_to_table
|
||||
migrate -url http://host:port -path ./db/migrations up
|
||||
migrate help # for more info
|
||||
```
|
||||
@@ -0,0 +1,115 @@
|
||||
// Package crate implements a driver for the Crate.io database
|
||||
package crate
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
_ "github.com/herenow/go-crate"
|
||||
"gopkg.in/mattes/migrate.v1/driver"
|
||||
"gopkg.in/mattes/migrate.v1/file"
|
||||
"gopkg.in/mattes/migrate.v1/migrate/direction"
|
||||
)
|
||||
|
||||
func init() {
|
||||
driver.RegisterDriver("crate", &Driver{})
|
||||
}
|
||||
|
||||
type Driver struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
const tableName = "schema_migrations"
|
||||
|
||||
func (driver *Driver) Initialize(url string) error {
|
||||
url = strings.Replace(url, "crate", "http", 1)
|
||||
db, err := sql.Open("crate", url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := db.Ping(); err != nil {
|
||||
return err
|
||||
}
|
||||
driver.db = db
|
||||
|
||||
if err := driver.ensureVersionTableExists(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver *Driver) Close() error {
|
||||
if err := driver.db.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver *Driver) FilenameExtension() string {
|
||||
return "sql"
|
||||
}
|
||||
|
||||
func (driver *Driver) Version() (uint64, error) {
|
||||
var version uint64
|
||||
err := driver.db.QueryRow("SELECT version FROM " + tableName + " ORDER BY version DESC LIMIT 1").Scan(&version)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return 0, nil
|
||||
case err != nil:
|
||||
return 0, err
|
||||
default:
|
||||
return version, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (driver *Driver) Migrate(f file.File, pipe chan interface{}) {
|
||||
defer close(pipe)
|
||||
pipe <- f
|
||||
|
||||
if err := f.ReadContent(); err != nil {
|
||||
pipe <- err
|
||||
return
|
||||
}
|
||||
|
||||
lines := splitContent(string(f.Content))
|
||||
for _, line := range lines {
|
||||
_, err := driver.db.Exec(line)
|
||||
if err != nil {
|
||||
pipe <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if f.Direction == direction.Up {
|
||||
if _, err := driver.db.Exec("INSERT INTO "+tableName+" (version) VALUES (?)", f.Version); err != nil {
|
||||
pipe <- err
|
||||
return
|
||||
}
|
||||
} else if f.Direction == direction.Down {
|
||||
if _, err := driver.db.Exec("DELETE FROM "+tableName+" WHERE version=?", f.Version); err != nil {
|
||||
pipe <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func splitContent(content string) []string {
|
||||
lines := strings.Split(content, ";")
|
||||
resultLines := make([]string, 0, len(lines))
|
||||
for i := range lines {
|
||||
line := strings.Replace(lines[i], ";", "", -1)
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" {
|
||||
resultLines = append(resultLines, line)
|
||||
}
|
||||
}
|
||||
return resultLines
|
||||
}
|
||||
|
||||
func (driver *Driver) ensureVersionTableExists() error {
|
||||
if _, err := driver.db.Exec(fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (version INTEGER PRIMARY KEY)", tableName)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package crate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"gopkg.in/mattes/migrate.v1/file"
|
||||
"gopkg.in/mattes/migrate.v1/migrate/direction"
|
||||
pipep "gopkg.in/mattes/migrate.v1/pipe"
|
||||
)
|
||||
|
||||
func TestContentSplit(t *testing.T) {
|
||||
content := `CREATE TABLE users (user_id STRING primary key, first_name STRING, last_name STRING, email STRING, password_hash STRING) CLUSTERED INTO 3 shards WITH (number_of_replicas = 0);
|
||||
CREATE TABLE units (unit_id STRING primary key, name STRING, members array(string)) CLUSTERED INTO 3 shards WITH (number_of_replicas = 0);
|
||||
CREATE TABLE available_connectors (technology_id STRING primary key, description STRING, icon STRING, link STRING, configuration_parameters array(object as (name STRING, type STRING))) CLUSTERED INTO 3 shards WITH (number_of_replicas = 0);
|
||||
`
|
||||
|
||||
lines := splitContent(content)
|
||||
if len(lines) != 3 {
|
||||
t.Errorf("Expected 3 lines, but got %d", len(lines))
|
||||
}
|
||||
|
||||
if lines[0] != "CREATE TABLE users (user_id STRING primary key, first_name STRING, last_name STRING, email STRING, password_hash STRING) CLUSTERED INTO 3 shards WITH (number_of_replicas = 0)" {
|
||||
t.Error("Line does not match expected output")
|
||||
}
|
||||
|
||||
if lines[1] != "CREATE TABLE units (unit_id STRING primary key, name STRING, members array(string)) CLUSTERED INTO 3 shards WITH (number_of_replicas = 0)" {
|
||||
t.Error("Line does not match expected output")
|
||||
}
|
||||
|
||||
if lines[2] != "CREATE TABLE available_connectors (technology_id STRING primary key, description STRING, icon STRING, link STRING, configuration_parameters array(object as (name STRING, type STRING))) CLUSTERED INTO 3 shards WITH (number_of_replicas = 0)" {
|
||||
t.Error("Line does not match expected output")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrate(t *testing.T) {
|
||||
host := os.Getenv("CRATE_PORT_4200_TCP_ADDR")
|
||||
port := os.Getenv("CRATE_PORT_4200_TCP_PORT")
|
||||
|
||||
url := fmt.Sprintf("crate://%s:%s", host, port)
|
||||
|
||||
driver := &Driver{}
|
||||
|
||||
if err := driver.Initialize(url); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
successFiles := []file.File{
|
||||
{
|
||||
Path: "/foobar",
|
||||
FileName: "001_foobar.up.sql",
|
||||
Version: 1,
|
||||
Name: "foobar",
|
||||
Direction: direction.Up,
|
||||
Content: []byte(`
|
||||
CREATE TABLE yolo (
|
||||
id integer primary key,
|
||||
msg string
|
||||
);
|
||||
`),
|
||||
},
|
||||
{
|
||||
Path: "/foobar",
|
||||
FileName: "002_foobar.down.sql",
|
||||
Version: 1,
|
||||
Name: "foobar",
|
||||
Direction: direction.Down,
|
||||
Content: []byte(`
|
||||
DROP TABLE yolo;
|
||||
`),
|
||||
},
|
||||
}
|
||||
|
||||
failFiles := []file.File{
|
||||
{
|
||||
Path: "/foobar",
|
||||
FileName: "002_foobar.up.sql",
|
||||
Version: 2,
|
||||
Name: "foobar",
|
||||
Direction: direction.Up,
|
||||
Content: []byte(`
|
||||
CREATE TABLE error (
|
||||
id THIS WILL CAUSE AN ERROR
|
||||
)
|
||||
`),
|
||||
},
|
||||
}
|
||||
|
||||
for _, file := range successFiles {
|
||||
pipe := pipep.New()
|
||||
go driver.Migrate(file, pipe)
|
||||
errs := pipep.ReadErrors(pipe)
|
||||
if len(errs) > 0 {
|
||||
t.Fatal(errs)
|
||||
}
|
||||
}
|
||||
|
||||
for _, file := range failFiles {
|
||||
pipe := pipep.New()
|
||||
go driver.Migrate(file, pipe)
|
||||
errs := pipep.ReadErrors(pipe)
|
||||
if len(errs) == 0 {
|
||||
t.Fatal("Migration should have failed but succeeded")
|
||||
}
|
||||
}
|
||||
|
||||
if err := driver.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
+27
-41
@@ -2,15 +2,10 @@
|
||||
package driver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
neturl "net/url" // alias to allow `url string` func signature in New
|
||||
|
||||
"github.com/mattes/migrate/driver/bash"
|
||||
"github.com/mattes/migrate/driver/cassandra"
|
||||
"github.com/mattes/migrate/driver/mysql"
|
||||
"github.com/mattes/migrate/driver/postgres"
|
||||
"github.com/mattes/migrate/file"
|
||||
"gopkg.in/mattes/migrate.v1/file"
|
||||
)
|
||||
|
||||
// Driver is the interface type that needs to implemented by all drivers.
|
||||
@@ -46,44 +41,35 @@ func New(url string) (Driver, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch u.Scheme {
|
||||
case "postgres":
|
||||
d := &postgres.Driver{}
|
||||
verifyFilenameExtension("postgres", d)
|
||||
if err := d.Initialize(url); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d, nil
|
||||
|
||||
case "mysql":
|
||||
d := &mysql.Driver{}
|
||||
verifyFilenameExtension("mysql", d)
|
||||
if err := d.Initialize(url); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d, nil
|
||||
|
||||
case "bash":
|
||||
d := &bash.Driver{}
|
||||
verifyFilenameExtension("bash", d)
|
||||
if err := d.Initialize(url); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d, nil
|
||||
|
||||
case "cassandra":
|
||||
d := &cassandra.Driver{}
|
||||
verifyFilenameExtension("cassanda", d)
|
||||
if err := d.Initialize(url); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d, nil
|
||||
default:
|
||||
return nil, errors.New(fmt.Sprintf("Driver '%s' not found.", u.Scheme))
|
||||
d := GetDriver(u.Scheme)
|
||||
if d == nil {
|
||||
return nil, fmt.Errorf("Driver '%s' not found", u.Scheme)
|
||||
}
|
||||
verifyFilenameExtension(u.Scheme, d)
|
||||
if err := d.Initialize(url); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// verifyFilenameExtension panics if the drivers filename extension
|
||||
// FilenameExtensionFromURL return extension for migration files. Used for create migrations
|
||||
func FilenameExtensionFromURL(url string) (string, error) {
|
||||
u, err := neturl.Parse(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
d := GetDriver(u.Scheme)
|
||||
if d == nil {
|
||||
return "", fmt.Errorf("Driver '%s' not found", u.Scheme)
|
||||
}
|
||||
verifyFilenameExtension(u.Scheme, d)
|
||||
|
||||
return d.FilenameExtension(), nil
|
||||
}
|
||||
|
||||
// verifyFilenameExtension panics if the driver's filename extension
|
||||
// is not correct or empty.
|
||||
func verifyFilenameExtension(driverName string, d Driver) {
|
||||
f := d.FilenameExtension()
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
package driver
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
if _, err := New("unknown://url"); err == nil {
|
||||
t.Error("no error although driver unknown")
|
||||
}
|
||||
}
|
||||
@@ -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 "gopkg.in/mattes/migrate.v1/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/v1/driver/mongodb/example/sample_mongdb_migrator.go)
|
||||
|
||||
```go
|
||||
package my_mongo_db_migrator
|
||||
|
||||
import (
|
||||
"gopkg.in/mattes/migrate.v1/driver/mongodb"
|
||||
"gopkg.in/mattes/migrate.v1/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,311 @@
|
||||
package example
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gopkg.in/mattes/migrate.v1/file"
|
||||
"gopkg.in/mattes/migrate.v1/migrate/direction"
|
||||
|
||||
"os"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"gopkg.in/mattes/migrate.v1/driver"
|
||||
"gopkg.in/mattes/migrate.v1/driver/mongodb"
|
||||
"gopkg.in/mattes/migrate.v1/driver/mongodb/gomethods"
|
||||
pipep "gopkg.in/mattes/migrate.v1/pipe"
|
||||
)
|
||||
|
||||
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,154 @@
|
||||
package example
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gopkg.in/mattes/migrate.v1/driver/mongodb/gomethods"
|
||||
_ "gopkg.in/mattes/migrate.v1/driver/mongodb/gomethods"
|
||||
"gopkg.in/mgo.v2"
|
||||
"gopkg.in/mgo.v2/bson"
|
||||
|
||||
"gopkg.in/mattes/migrate.v1/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,150 @@
|
||||
package gomethods
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/mattes/migrate.v1/driver"
|
||||
"gopkg.in/mattes/migrate.v1/file"
|
||||
)
|
||||
|
||||
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"
|
||||
|
||||
"gopkg.in/mattes/migrate.v1/file"
|
||||
"gopkg.in/mattes/migrate.v1/migrate/direction"
|
||||
|
||||
pipep "gopkg.in/mattes/migrate.v1/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,40 @@
|
||||
package gomethods
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"gopkg.in/mattes/migrate.v1/driver"
|
||||
)
|
||||
|
||||
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,194 @@
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/mattes/migrate.v1/driver"
|
||||
"gopkg.in/mattes/migrate.v1/driver/mongodb/gomethods"
|
||||
"gopkg.in/mattes/migrate.v1/file"
|
||||
"gopkg.in/mattes/migrate.v1/migrate/direction"
|
||||
"gopkg.in/mgo.v2"
|
||||
"gopkg.in/mgo.v2/bson"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
# MSSQL Driver
|
||||
|
||||
* Runs migrations in transactions.
|
||||
That means that if a migration fails, it will be safely rolled back.
|
||||
* Tries to return helpful error messages.
|
||||
* Stores migration version details in table ``schema_migrations``.
|
||||
This table will be auto-generated.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
migrate -url="sqlserver://sa:Passw0rd@localhost:1433?database=master" -path ./db/migrations create add_field_to_table
|
||||
migrate -url="sqlserver://sa:Passw0rd@localhost:1433?database=master" -path ./db/migrations up
|
||||
migrate help # for more info
|
||||
|
||||
# see more docs: https://github.com/denisenkom/go-mssqldb
|
||||
```
|
||||
|
||||
## Authors
|
||||
|
||||
* Andras Laczi, https://github.com/alaczi
|
||||
@@ -0,0 +1,128 @@
|
||||
// Package mssql implements the Driver interface.
|
||||
package mssql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/denisenkom/go-mssqldb"
|
||||
"gopkg.in/mattes/migrate.v1/driver"
|
||||
"gopkg.in/mattes/migrate.v1/file"
|
||||
"gopkg.in/mattes/migrate.v1/migrate/direction"
|
||||
)
|
||||
|
||||
type Driver struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
const tableName = "schema_migrations"
|
||||
|
||||
func (driver *Driver) Initialize(url string) error {
|
||||
db, err := sql.Open("mssql", url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := db.Ping(); err != nil {
|
||||
return err
|
||||
}
|
||||
driver.db = db
|
||||
|
||||
if err := driver.ensureVersionTableExists(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver *Driver) Close() error {
|
||||
if err := driver.db.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver *Driver) ensureVersionTableExists() error {
|
||||
if _, err := driver.db.Exec("if object_id('" + tableName + "') is null create table [" + tableName + "] ([version] BIGINT NOT NULL PRIMARY KEY);"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver *Driver) FilenameExtension() string {
|
||||
return "sql"
|
||||
}
|
||||
|
||||
func (driver *Driver) Migrate(f file.File, pipe chan interface{}) {
|
||||
defer close(pipe)
|
||||
pipe <- f
|
||||
|
||||
tx, err := driver.db.Begin()
|
||||
if err != nil {
|
||||
pipe <- err
|
||||
return
|
||||
}
|
||||
|
||||
if f.Direction == direction.Up {
|
||||
if _, err := tx.Exec("INSERT INTO "+tableName+" (version) VALUES (?)", f.Version); err != nil {
|
||||
pipe <- err
|
||||
if err := tx.Rollback(); err != nil {
|
||||
pipe <- err
|
||||
}
|
||||
return
|
||||
}
|
||||
} else if f.Direction == direction.Down {
|
||||
if _, err := tx.Exec("DELETE FROM "+tableName+" WHERE version=?", f.Version); err != nil {
|
||||
pipe <- err
|
||||
if err := tx.Rollback(); err != nil {
|
||||
pipe <- err
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := f.ReadContent(); err != nil {
|
||||
pipe <- err
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(string(f.Content)); err != nil {
|
||||
switch msErr := err.(type) {
|
||||
case *mssql.Error:
|
||||
errorPart := file.LinesBeforeAndAfter(f.Content, int(msErr.LineNo), 5, 5, true)
|
||||
pipe <- errors.New(fmt.Sprintf("%v: %s in line %v:\n\n%s", msErr.Number, msErr.Message, msErr.LineNo, string(errorPart)))
|
||||
if err := tx.Rollback(); err != nil {
|
||||
pipe <- err
|
||||
}
|
||||
return
|
||||
default:
|
||||
pipe <- err
|
||||
if err := tx.Rollback(); err != nil {
|
||||
pipe <- err
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
pipe <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (driver *Driver) Version() (uint64, error) {
|
||||
var version uint64
|
||||
err := driver.db.QueryRow("SELECT version FROM " + tableName + " ORDER BY version DESC OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY").Scan(&version)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return 0, nil
|
||||
case err != nil:
|
||||
return 0, err
|
||||
default:
|
||||
return version, nil
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
driver.RegisterDriver("mssql", &Driver{})
|
||||
driver.RegisterDriver("sqlserver", &Driver{})
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package mssql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gopkg.in/mattes/migrate.v1/file"
|
||||
"gopkg.in/mattes/migrate.v1/migrate/direction"
|
||||
pipep "gopkg.in/mattes/migrate.v1/pipe"
|
||||
)
|
||||
|
||||
// TestMigrate runs some additional tests on Migrate().
|
||||
// Basic testing is already done in migrate/migrate_test.go
|
||||
func TestMigrate(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode.")
|
||||
}
|
||||
|
||||
host := os.Getenv("MSSQL_PORT_1433_TCP_ADDR")
|
||||
port := os.Getenv("MSSQL_PORT_1433_TCP_PORT")
|
||||
driverURL := fmt.Sprintf("server=%s;user id=%s;password=%s;port=%s;database=%s;encrypt=disable;log=2;TrustServerCertificate=true",
|
||||
host,
|
||||
"sa",
|
||||
"Passw0rd",
|
||||
port,
|
||||
"master",
|
||||
)
|
||||
// retry connection for 2 minutes
|
||||
until := time.Now().Add(time.Second * 120)
|
||||
|
||||
ticker := time.NewTicker(time.Second)
|
||||
var connection *sql.DB
|
||||
var err error
|
||||
// prepare clean database
|
||||
for tick := range ticker.C {
|
||||
if tick.After(until) {
|
||||
ticker.Stop()
|
||||
break
|
||||
}
|
||||
connection, err = sql.Open("mssql", driverURL)
|
||||
err = connection.Ping()
|
||||
if err == nil {
|
||||
ticker.Stop()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatal("Failed to connect to mssql docker container after 2 minutes", err)
|
||||
}
|
||||
|
||||
if _, err := connection.Exec(`
|
||||
DROP TABLE IF EXISTS yolo;
|
||||
DROP TABLE IF EXISTS ` + tableName + `;`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
d := &Driver{}
|
||||
if err := d.Initialize(driverURL); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// testing idempotency: second call should be a no-op, since table already exists
|
||||
if err := d.Initialize(driverURL); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
files := []file.File{
|
||||
{
|
||||
Path: "/foobar",
|
||||
FileName: "001_foobar.up.sql",
|
||||
Version: 1,
|
||||
Name: "foobar",
|
||||
Direction: direction.Up,
|
||||
Content: []byte(`
|
||||
CREATE TABLE yolo (
|
||||
id BIGINT IDENTITY NOT NULL PRIMARY KEY
|
||||
);
|
||||
`),
|
||||
},
|
||||
{
|
||||
Path: "/foobar",
|
||||
FileName: "002_foobar.down.sql",
|
||||
Version: 1,
|
||||
Name: "foobar",
|
||||
Direction: direction.Down,
|
||||
Content: []byte(`
|
||||
DROP TABLE yolo;
|
||||
`),
|
||||
},
|
||||
{
|
||||
Path: "/foobar",
|
||||
FileName: "002_foobar.up.sql",
|
||||
Version: 2,
|
||||
Name: "foobar",
|
||||
Direction: direction.Up,
|
||||
Content: []byte(`
|
||||
CREATE TABLE error (
|
||||
id THIS WILL CAUSE AN ERROR
|
||||
)
|
||||
`),
|
||||
},
|
||||
{
|
||||
Path: "/foobar",
|
||||
FileName: "20170118205923_demo.up.sql",
|
||||
Version: 20170118205923,
|
||||
Name: "demo",
|
||||
Direction: direction.Up,
|
||||
Content: []byte(`
|
||||
CREATE TABLE demo (
|
||||
id BIGINT IDENTITY NOT NULL PRIMARY KEY
|
||||
)
|
||||
`),
|
||||
},
|
||||
{
|
||||
Path: "/foobar",
|
||||
FileName: "20170118205923_demo.down.sql",
|
||||
Version: 20170118205923,
|
||||
Name: "demo",
|
||||
Direction: direction.Down,
|
||||
Content: []byte(`
|
||||
DROP TABLE demo
|
||||
`),
|
||||
},
|
||||
}
|
||||
|
||||
pipe := pipep.New()
|
||||
go d.Migrate(files[0], pipe)
|
||||
errs := pipep.ReadErrors(pipe)
|
||||
if len(errs) > 0 {
|
||||
t.Fatal(errs)
|
||||
}
|
||||
|
||||
pipe = pipep.New()
|
||||
go d.Migrate(files[1], pipe)
|
||||
errs = pipep.ReadErrors(pipe)
|
||||
if len(errs) > 0 {
|
||||
t.Fatal(errs)
|
||||
}
|
||||
|
||||
pipe = pipep.New()
|
||||
go d.Migrate(files[2], pipe)
|
||||
errs = pipep.ReadErrors(pipe)
|
||||
if len(errs) == 0 {
|
||||
t.Error("Expected test case to fail")
|
||||
}
|
||||
|
||||
pipe = pipep.New()
|
||||
go d.Migrate(files[3], pipe)
|
||||
errs = pipep.ReadErrors(pipe)
|
||||
if len(errs) > 0 {
|
||||
t.Fatal(errs)
|
||||
}
|
||||
|
||||
pipe = pipep.New()
|
||||
go d.Migrate(files[4], pipe)
|
||||
errs = pipep.ReadErrors(pipe)
|
||||
if len(errs) > 0 {
|
||||
t.Fatal(errs)
|
||||
}
|
||||
|
||||
if err := d.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -21,4 +21,4 @@ See full [DSN (Data Source Name) documentation](https://github.com/go-sql-driver
|
||||
|
||||
## Authors
|
||||
|
||||
* Matthias Kadenbach, https://github.com/mattes
|
||||
* Matthias Kadenbach, https://github.com/mattes
|
||||
|
||||
+65
-47
@@ -7,12 +7,14 @@ import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/go-sql-driver/mysql"
|
||||
"github.com/mattes/migrate/file"
|
||||
"github.com/mattes/migrate/migrate/direction"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-sql-driver/mysql"
|
||||
"gopkg.in/mattes/migrate.v1/driver"
|
||||
"gopkg.in/mattes/migrate.v1/file"
|
||||
"gopkg.in/mattes/migrate.v1/migrate/direction"
|
||||
)
|
||||
|
||||
type Driver struct {
|
||||
@@ -22,7 +24,12 @@ type Driver struct {
|
||||
const tableName = "schema_migrations"
|
||||
|
||||
func (driver *Driver) Initialize(url string) error {
|
||||
db, err := sql.Open("mysql", strings.TrimLeft(url, "mysql://"))
|
||||
urlWithoutScheme := strings.SplitN(url, "mysql://", 2)
|
||||
if len(urlWithoutScheme) != 2 {
|
||||
return errors.New("invalid mysql:// scheme")
|
||||
}
|
||||
|
||||
db, err := sql.Open("mysql", urlWithoutScheme[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -45,9 +52,12 @@ func (driver *Driver) Close() error {
|
||||
}
|
||||
|
||||
func (driver *Driver) ensureVersionTableExists() error {
|
||||
if _, err := driver.db.Exec("CREATE TABLE IF NOT EXISTS " + tableName + " (version int not null primary key);"); err != nil {
|
||||
_, err := driver.db.Exec("CREATE TABLE IF NOT EXISTS " + tableName + " (version int not null primary key);")
|
||||
|
||||
if _, isWarn := err.(mysql.MySQLWarnings); err != nil && !isWarn {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -95,54 +105,58 @@ func (driver *Driver) Migrate(f file.File, pipe chan interface{}) {
|
||||
sqlStmts := bytes.Split(f.Content, []byte(";"))
|
||||
|
||||
for _, sqlStmt := range sqlStmts {
|
||||
if len(bytes.TrimSpace(sqlStmt)) > 0 {
|
||||
sqlStmt = bytes.TrimSpace(sqlStmt)
|
||||
if len(sqlStmt) > 0 {
|
||||
if _, err := tx.Exec(string(sqlStmt)); err != nil {
|
||||
mysqlErr := err.(*mysql.MySQLError)
|
||||
mysqlErr, isErr := err.(*mysql.MySQLError)
|
||||
|
||||
if isErr {
|
||||
re, err := regexp.Compile(`at line ([0-9]+)$`)
|
||||
if err != nil {
|
||||
pipe <- err
|
||||
if err := tx.Rollback(); err != nil {
|
||||
pipe <- err
|
||||
}
|
||||
}
|
||||
|
||||
var lineNo int
|
||||
lineNoRe := re.FindStringSubmatch(mysqlErr.Message)
|
||||
if len(lineNoRe) == 2 {
|
||||
lineNo, err = strconv.Atoi(lineNoRe[1])
|
||||
}
|
||||
if err == nil {
|
||||
|
||||
// get white-space offset
|
||||
// TODO this is broken, because we use sqlStmt instead of f.Content
|
||||
wsLineOffset := 0
|
||||
b := bufio.NewReader(bytes.NewBuffer(sqlStmt))
|
||||
for {
|
||||
line, _, err := b.ReadLine()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if bytes.TrimSpace(line) == nil {
|
||||
wsLineOffset += 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
message := mysqlErr.Error()
|
||||
message = re.ReplaceAllString(message, fmt.Sprintf("at line %v", lineNo+wsLineOffset))
|
||||
|
||||
errorPart := file.LinesBeforeAndAfter(sqlStmt, lineNo, 5, 5, true)
|
||||
pipe <- errors.New(fmt.Sprintf("%s\n\n%s", message, string(errorPart)))
|
||||
} else {
|
||||
pipe <- errors.New(mysqlErr.Error())
|
||||
}
|
||||
|
||||
re, err := regexp.Compile(`at line ([0-9]+)$`)
|
||||
if err != nil {
|
||||
pipe <- err
|
||||
if err := tx.Rollback(); err != nil {
|
||||
pipe <- err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var lineNo int
|
||||
lineNoRe := re.FindStringSubmatch(mysqlErr.Message)
|
||||
if len(lineNoRe) == 2 {
|
||||
lineNo, err = strconv.Atoi(lineNoRe[1])
|
||||
}
|
||||
if err == nil {
|
||||
|
||||
// get white-space offset
|
||||
// TODO this is broken, because we use sqlStmt instead of f.Content
|
||||
wsLineOffset := 0
|
||||
b := bufio.NewReader(bytes.NewBuffer(sqlStmt))
|
||||
for {
|
||||
line, _, err := b.ReadLine()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if bytes.TrimSpace(line) == nil {
|
||||
wsLineOffset += 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
message := mysqlErr.Error()
|
||||
message = re.ReplaceAllString(message, fmt.Sprintf("at line %v", lineNo+wsLineOffset))
|
||||
|
||||
errorPart := file.LinesBeforeAndAfter(sqlStmt, lineNo, 5, 5, true)
|
||||
pipe <- errors.New(fmt.Sprintf("%s\n\n%s", message, string(errorPart)))
|
||||
} else {
|
||||
pipe <- errors.New(mysqlErr.Error())
|
||||
}
|
||||
|
||||
if err := tx.Rollback(); err != nil {
|
||||
pipe <- err
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,3 +179,7 @@ func (driver *Driver) Version() (uint64, error) {
|
||||
return version, nil
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
driver.RegisterDriver("mysql", &Driver{})
|
||||
}
|
||||
|
||||
@@ -2,19 +2,27 @@ package mysql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/mattes/migrate/file"
|
||||
"github.com/mattes/migrate/migrate/direction"
|
||||
pipep "github.com/mattes/migrate/pipe"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gopkg.in/mattes/migrate.v1/file"
|
||||
"gopkg.in/mattes/migrate.v1/migrate/direction"
|
||||
pipep "gopkg.in/mattes/migrate.v1/pipe"
|
||||
)
|
||||
|
||||
// TestMigrate runs some additional tests on Migrate().
|
||||
// Basic testing is already done in migrate/migrate_test.go
|
||||
func TestMigrate(t *testing.T) {
|
||||
driverUrl := "root@tcp(127.0.0.1:3306)/migratetest"
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode.")
|
||||
}
|
||||
host := os.Getenv("MYSQL_PORT_3306_TCP_ADDR")
|
||||
port := os.Getenv("MYSQL_PORT_3306_TCP_PORT")
|
||||
driverUrl := "mysql://root@tcp(" + host + ":" + port + ")/migratetest"
|
||||
|
||||
// prepare clean database
|
||||
connection, err := sql.Open("mysql", driverUrl)
|
||||
connection, err := sql.Open("mysql", strings.SplitN(driverUrl, "mysql://", 2)[1])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Neo4j Driver
|
||||
|
||||
* Runs migrations in transcations.
|
||||
That means that if a migration failes, it will be safely rolled back.
|
||||
|
||||
* Stores migration version details with the label ``SchemaMigrations``.
|
||||
An unique constraint for the field :SchemaMigrations(version) will be auto-generated.
|
||||
|
||||
* Neo4j cannot perform schema and data updates in a transaction, therefore it's necessary to use different migration files
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
migrate -url neo4j://user:password@host:port/db/data -path ./db/migrations create add_field_to_table
|
||||
migrate -url neo4j://user:password@host:port/db/data -path ./db/migrations up
|
||||
migrate help # for more info
|
||||
```
|
||||
## Author
|
||||
|
||||
* Carlos Forero, https://github.com/carlosforero
|
||||
@@ -0,0 +1,167 @@
|
||||
// Package neo4j implements the Driver interface.
|
||||
package neo4j
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/jmcvetta/neoism"
|
||||
"gopkg.in/mattes/migrate.v1/driver"
|
||||
"gopkg.in/mattes/migrate.v1/file"
|
||||
"gopkg.in/mattes/migrate.v1/migrate/direction"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Driver struct {
|
||||
db *neoism.Database
|
||||
}
|
||||
|
||||
const labelName = "SchemaMigration"
|
||||
|
||||
func (driver *Driver) Initialize(url string) error {
|
||||
url = strings.Replace(url, "neo4j", "http", 1)
|
||||
|
||||
db, err := neoism.Connect(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
driver.db = db
|
||||
|
||||
if err := driver.ensureVersionConstraintExists(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver *Driver) Close() error {
|
||||
driver.db = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver *Driver) FilenameExtension() string {
|
||||
return "cql"
|
||||
}
|
||||
|
||||
func (driver *Driver) ensureVersionConstraintExists() error {
|
||||
uc, _ := driver.db.UniqueConstraints("SchemaMigration", "version")
|
||||
if len(uc) == 0 {
|
||||
_, err := driver.db.CreateUniqueConstraint("SchemaMigration", "version")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver *Driver) setVersion(d direction.Direction, v uint64, invert bool) error {
|
||||
|
||||
cqUp := neoism.CypherQuery{
|
||||
Statement: `CREATE (n:SchemaMigration {version: {Version}}) RETURN n`,
|
||||
Parameters: neoism.Props{"Version": v},
|
||||
}
|
||||
|
||||
cqDown := neoism.CypherQuery{
|
||||
Statement: `MATCH (n:SchemaMigration {version: {Version}}) DELETE n`,
|
||||
Parameters: neoism.Props{"Version": v},
|
||||
}
|
||||
|
||||
var cq neoism.CypherQuery
|
||||
switch d {
|
||||
case direction.Up:
|
||||
if invert {
|
||||
cq = cqDown
|
||||
} else {
|
||||
cq = cqUp
|
||||
}
|
||||
case direction.Down:
|
||||
if invert {
|
||||
cq = cqUp
|
||||
} else {
|
||||
cq = cqDown
|
||||
}
|
||||
}
|
||||
return driver.db.Cypher(&cq)
|
||||
}
|
||||
|
||||
func (driver *Driver) Migrate(f file.File, pipe chan interface{}) {
|
||||
var err error
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
// Invert version direction if we couldn't apply the changes for some reason.
|
||||
if err := driver.setVersion(f.Direction, f.Version, true); err != nil {
|
||||
pipe <- err
|
||||
}
|
||||
pipe <- err
|
||||
}
|
||||
close(pipe)
|
||||
}()
|
||||
|
||||
pipe <- f
|
||||
|
||||
if err = driver.setVersion(f.Direction, f.Version, false); err != nil {
|
||||
pipe <- err
|
||||
return
|
||||
}
|
||||
|
||||
if err = f.ReadContent(); err != nil {
|
||||
pipe <- err
|
||||
return
|
||||
}
|
||||
|
||||
cQueries := []*neoism.CypherQuery{}
|
||||
|
||||
// Neoism doesn't support multiple statements per query.
|
||||
cqlStmts := bytes.Split(f.Content, []byte(";"))
|
||||
|
||||
for _, cqlStmt := range cqlStmts {
|
||||
cqlStmt = bytes.TrimSpace(cqlStmt)
|
||||
if len(cqlStmt) > 0 {
|
||||
cq := neoism.CypherQuery{Statement: string(cqlStmt)}
|
||||
cQueries = append(cQueries, &cq)
|
||||
}
|
||||
}
|
||||
|
||||
var tx *neoism.Tx
|
||||
|
||||
tx, err = driver.db.Begin(cQueries)
|
||||
if err != nil {
|
||||
pipe <- err
|
||||
for _, err := range tx.Errors {
|
||||
pipe <- errors.New(fmt.Sprintf("%v", err.Message))
|
||||
}
|
||||
if err = tx.Rollback(); err != nil {
|
||||
pipe <- err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
pipe <- err
|
||||
for _, err := range tx.Errors {
|
||||
pipe <- errors.New(fmt.Sprintf("%v", err.Message))
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (driver *Driver) Version() (uint64, error) {
|
||||
res := []struct {
|
||||
Version uint64 `json:"n.version"`
|
||||
}{}
|
||||
|
||||
cq := neoism.CypherQuery{
|
||||
Statement: `MATCH (n:SchemaMigration)
|
||||
RETURN n.version ORDER BY n.version DESC LIMIT 1`,
|
||||
Result: &res,
|
||||
}
|
||||
|
||||
if err := driver.db.Cypher(&cq); err != nil || len(res) == 0 {
|
||||
return 0, err
|
||||
}
|
||||
return res[0].Version, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
driver.RegisterDriver("neo4j", &Driver{})
|
||||
driver.RegisterDriver("neo4js", &Driver{})
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package neo4j
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/jmcvetta/neoism"
|
||||
"gopkg.in/mattes/migrate.v1/file"
|
||||
"gopkg.in/mattes/migrate.v1/migrate/direction"
|
||||
pipep "gopkg.in/mattes/migrate.v1/pipe"
|
||||
)
|
||||
|
||||
// TestMigrate runs some additional tests on Migrate().
|
||||
// Basic testing is already done in migrate/migrate_test.go
|
||||
func TestMigrate(t *testing.T) {
|
||||
t.Skip("TODO: fix test: neo4j_test.go:26: Get http://neo4j:test@/db/data/: http: no Host in request URL")
|
||||
|
||||
host := os.Getenv("NEO4J_PORT_7474_TCP_ADDR")
|
||||
port := os.Getenv("NEO4J_PORT_7474_TCP_PORT")
|
||||
|
||||
driverUrl := "http://neo4j:test@" + host + ":" + port + "/db/data"
|
||||
|
||||
// prepare clean database
|
||||
db, err := neoism.Connect(driverUrl)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cq := neoism.CypherQuery{
|
||||
Statement: `DROP INDEX ON :Yolo(name)`,
|
||||
}
|
||||
|
||||
// If an error dropping the index then ignore it
|
||||
db.Cypher(&cq)
|
||||
|
||||
driverUrl = "neo4j://neo4j:test@" + host + ":" + port + "/db/data"
|
||||
|
||||
d := &Driver{}
|
||||
if err := d.Initialize(driverUrl); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
files := []file.File{
|
||||
{
|
||||
Path: "/foobar",
|
||||
FileName: "001_foobar.up.cql",
|
||||
Version: 1,
|
||||
Name: "foobar",
|
||||
Direction: direction.Up,
|
||||
Content: []byte(`
|
||||
CREATE INDEX ON :Yolo(name)
|
||||
`),
|
||||
},
|
||||
{
|
||||
Path: "/foobar",
|
||||
FileName: "001_foobar.down.cql",
|
||||
Version: 1,
|
||||
Name: "foobar",
|
||||
Direction: direction.Down,
|
||||
Content: []byte(`
|
||||
DROP INDEX ON :Yolo(name)
|
||||
`),
|
||||
},
|
||||
{
|
||||
Path: "/foobar",
|
||||
FileName: "002_foobar.up.cql",
|
||||
Version: 2,
|
||||
Name: "foobar",
|
||||
Direction: direction.Up,
|
||||
Content: []byte(`
|
||||
CREATE INDEX :Yolo(name) THIS WILL CAUSE AN ERROR
|
||||
`),
|
||||
},
|
||||
}
|
||||
|
||||
pipe := pipep.New()
|
||||
go d.Migrate(files[0], pipe)
|
||||
errs := pipep.ReadErrors(pipe)
|
||||
if len(errs) > 0 {
|
||||
t.Fatal(errs)
|
||||
}
|
||||
|
||||
pipe = pipep.New()
|
||||
go d.Migrate(files[1], pipe)
|
||||
errs = pipep.ReadErrors(pipe)
|
||||
if len(errs) > 0 {
|
||||
t.Fatal(errs)
|
||||
}
|
||||
|
||||
pipe = pipep.New()
|
||||
go d.Migrate(files[2], pipe)
|
||||
errs = pipep.ReadErrors(pipe)
|
||||
if len(errs) == 0 {
|
||||
t.Error("Expected test case to fail")
|
||||
}
|
||||
|
||||
if err := d.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
# PostgreSQL Driver
|
||||
|
||||
* Runs migrations in transcations.
|
||||
That means that if a migration failes, it will be safely rolled back.
|
||||
* Runs migrations in transactions.
|
||||
That means that if a migration fails, it will be safely rolled back.
|
||||
* Tries to return helpful error messages.
|
||||
* Stores migration version details in table ``schema_migrations``.
|
||||
This table will be auto-generated.
|
||||
@@ -15,9 +15,11 @@ migrate -url postgres://user@host:port/database -path ./db/migrations up
|
||||
migrate help # for more info
|
||||
|
||||
# TODO(mattes): thinking about adding some custom flag to allow migration within schemas:
|
||||
-url="postgres://user@host:port/database?schema=name"
|
||||
-url="postgres://user@host:port/database?schema=name"
|
||||
|
||||
# see more docs: https://godoc.org/github.com/lib/pq#hdr-Connection_String_Parameters
|
||||
```
|
||||
|
||||
## Authors
|
||||
|
||||
* Matthias Kadenbach, https://github.com/mattes
|
||||
* Matthias Kadenbach, https://github.com/mattes
|
||||
|
||||
+39
-15
@@ -5,10 +5,12 @@ import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/lib/pq"
|
||||
"github.com/mattes/migrate/file"
|
||||
"github.com/mattes/migrate/migrate/direction"
|
||||
"strconv"
|
||||
|
||||
"github.com/lib/pq"
|
||||
"gopkg.in/mattes/migrate.v1/driver"
|
||||
"gopkg.in/mattes/migrate.v1/file"
|
||||
"gopkg.in/mattes/migrate.v1/migrate/direction"
|
||||
)
|
||||
|
||||
type Driver struct {
|
||||
@@ -41,7 +43,15 @@ func (driver *Driver) Close() error {
|
||||
}
|
||||
|
||||
func (driver *Driver) ensureVersionTableExists() error {
|
||||
if _, err := driver.db.Exec("CREATE TABLE IF NOT EXISTS " + tableName + " (version int not null primary key);"); err != nil {
|
||||
r := driver.db.QueryRow("SELECT count(*) FROM information_schema.tables WHERE table_name = $1 AND table_schema = (SELECT current_schema());", tableName)
|
||||
c := 0
|
||||
if err := r.Scan(&c); err != nil {
|
||||
return err
|
||||
}
|
||||
if c > 0 {
|
||||
return nil
|
||||
}
|
||||
if _, err := driver.db.Exec("CREATE TABLE IF NOT EXISTS " + tableName + " (version bigint not null primary key);"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -85,20 +95,28 @@ func (driver *Driver) Migrate(f file.File, pipe chan interface{}) {
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(string(f.Content)); err != nil {
|
||||
pqErr := err.(*pq.Error)
|
||||
offset, err := strconv.Atoi(pqErr.Position)
|
||||
if err == nil && offset >= 0 {
|
||||
lineNo, columnNo := file.LineColumnFromOffset(f.Content, offset-1)
|
||||
errorPart := file.LinesBeforeAndAfter(f.Content, lineNo, 5, 5, true)
|
||||
pipe <- errors.New(fmt.Sprintf("%s %v: %s in line %v, column %v:\n\n%s", pqErr.Severity, pqErr.Code, pqErr.Message, lineNo, columnNo, string(errorPart)))
|
||||
} else {
|
||||
pipe <- errors.New(fmt.Sprintf("%s %v: %s", pqErr.Severity, pqErr.Code, pqErr.Message))
|
||||
}
|
||||
switch pqErr := err.(type) {
|
||||
case *pq.Error:
|
||||
offset, err := strconv.Atoi(pqErr.Position)
|
||||
if err == nil && offset >= 0 {
|
||||
lineNo, columnNo := file.LineColumnFromOffset(f.Content, offset-1)
|
||||
errorPart := file.LinesBeforeAndAfter(f.Content, lineNo, 5, 5, true)
|
||||
pipe <- errors.New(fmt.Sprintf("%s %v: %s in line %v, column %v:\n\n%s", pqErr.Severity, pqErr.Code, pqErr.Message, lineNo, columnNo, string(errorPart)))
|
||||
} else {
|
||||
pipe <- errors.New(fmt.Sprintf("%s %v: %s", pqErr.Severity, pqErr.Code, pqErr.Message))
|
||||
}
|
||||
|
||||
if err := tx.Rollback(); err != nil {
|
||||
if err := tx.Rollback(); err != nil {
|
||||
pipe <- err
|
||||
}
|
||||
return
|
||||
default:
|
||||
pipe <- err
|
||||
if err := tx.Rollback(); err != nil {
|
||||
pipe <- err
|
||||
}
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
@@ -119,3 +137,9 @@ func (driver *Driver) Version() (uint64, error) {
|
||||
return version, nil
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
drv := Driver{}
|
||||
driver.RegisterDriver("postgres", &drv)
|
||||
driver.RegisterDriver("postgresql", &drv)
|
||||
}
|
||||
|
||||
@@ -2,19 +2,27 @@ package postgres
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/mattes/migrate/file"
|
||||
"github.com/mattes/migrate/migrate/direction"
|
||||
pipep "github.com/mattes/migrate/pipe"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"gopkg.in/mattes/migrate.v1/file"
|
||||
"gopkg.in/mattes/migrate.v1/migrate/direction"
|
||||
pipep "gopkg.in/mattes/migrate.v1/pipe"
|
||||
)
|
||||
|
||||
// TestMigrate runs some additional tests on Migrate().
|
||||
// Basic testing is already done in migrate/migrate_test.go
|
||||
func TestMigrate(t *testing.T) {
|
||||
driverUrl := "postgres://localhost/migratetest?sslmode=disable"
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode.")
|
||||
}
|
||||
|
||||
host := os.Getenv("POSTGRES_PORT_5432_TCP_ADDR")
|
||||
port := os.Getenv("POSTGRES_PORT_5432_TCP_PORT")
|
||||
driverURL := "postgres://postgres@" + host + ":" + port + "/template1?sslmode=disable"
|
||||
|
||||
// prepare clean database
|
||||
connection, err := sql.Open("postgres", driverUrl)
|
||||
connection, err := sql.Open("postgres", driverURL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -25,7 +33,12 @@ func TestMigrate(t *testing.T) {
|
||||
}
|
||||
|
||||
d := &Driver{}
|
||||
if err := d.Initialize(driverUrl); err != nil {
|
||||
if err := d.Initialize(driverURL); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// testing idempotency: second call should be a no-op, since table already exists
|
||||
if err := d.Initialize(driverURL); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -64,6 +77,28 @@ func TestMigrate(t *testing.T) {
|
||||
)
|
||||
`),
|
||||
},
|
||||
{
|
||||
Path: "/foobar",
|
||||
FileName: "20170118205923_demo.up.sql",
|
||||
Version: 20170118205923,
|
||||
Name: "demo",
|
||||
Direction: direction.Up,
|
||||
Content: []byte(`
|
||||
CREATE TABLE demo (
|
||||
id serial not null primary key
|
||||
)
|
||||
`),
|
||||
},
|
||||
{
|
||||
Path: "/foobar",
|
||||
FileName: "20170118205923_demo.down.sql",
|
||||
Version: 20170118205923,
|
||||
Name: "demo",
|
||||
Direction: direction.Down,
|
||||
Content: []byte(`
|
||||
DROP TABLE demo
|
||||
`),
|
||||
},
|
||||
}
|
||||
|
||||
pipe := pipep.New()
|
||||
@@ -87,6 +122,20 @@ func TestMigrate(t *testing.T) {
|
||||
t.Error("Expected test case to fail")
|
||||
}
|
||||
|
||||
pipe = pipep.New()
|
||||
go d.Migrate(files[3], pipe)
|
||||
errs = pipep.ReadErrors(pipe)
|
||||
if len(errs) > 0 {
|
||||
t.Fatal(errs)
|
||||
}
|
||||
|
||||
pipe = pipep.New()
|
||||
go d.Migrate(files[4], pipe)
|
||||
errs = pipep.ReadErrors(pipe)
|
||||
if len(errs) > 0 {
|
||||
t.Fatal(errs)
|
||||
}
|
||||
|
||||
if err := d.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Ql Driver
|
||||
|
||||
* Supports both in-memory and file ql databases, with the URL schemes `ql+file://` and `ql+memory://`
|
||||
* In-memory driver is not of much use, since the database is destroyed once the migration operation finishes, but is included for completeness.
|
||||
* Stores migration version details in automatically generated table ``schema_migrations``.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
migrate -url ql+file://./data.db -path ./migrations create add_field_to_table
|
||||
migrate -url ql+file://./data.db -path ./db/migrations up
|
||||
migrate help # for more info
|
||||
```
|
||||
|
||||
## Authors
|
||||
|
||||
* Sam Willcocks, https://github.com/wlcx
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
package ql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"gopkg.in/mattes/migrate.v1/driver"
|
||||
"gopkg.in/mattes/migrate.v1/file"
|
||||
"gopkg.in/mattes/migrate.v1/migrate/direction"
|
||||
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
_ "github.com/cznic/ql/driver"
|
||||
)
|
||||
|
||||
const tableName = "schema_migration"
|
||||
|
||||
type Driver struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func (d *Driver) Initialize(url string) (err error) {
|
||||
d.db, err = sql.Open("ql", strings.TrimPrefix(url, "ql+"))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err = d.db.Ping(); err != nil {
|
||||
return
|
||||
}
|
||||
if err = d.ensureVersionTableExists(); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Driver) Close() error {
|
||||
if err := d.db.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Driver) FilenameExtension() string {
|
||||
return "sql"
|
||||
}
|
||||
|
||||
func (d *Driver) Migrate(f file.File, pipe chan interface{}) {
|
||||
defer close(pipe)
|
||||
pipe <- f
|
||||
|
||||
tx, err := d.db.Begin()
|
||||
if err != nil {
|
||||
pipe <- err
|
||||
return
|
||||
}
|
||||
|
||||
switch f.Direction {
|
||||
case direction.Up:
|
||||
if _, err := tx.Exec("INSERT INTO "+tableName+" (version) VALUES (uint($1))", f.Version); err != nil {
|
||||
pipe <- err
|
||||
if err := tx.Rollback(); err != nil {
|
||||
pipe <- err
|
||||
}
|
||||
return
|
||||
}
|
||||
case direction.Down:
|
||||
if _, err := tx.Exec("DELETE FROM "+tableName+" WHERE version=uint($1)", f.Version); err != nil {
|
||||
pipe <- err
|
||||
if err := tx.Rollback(); err != nil {
|
||||
pipe <- err
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := f.ReadContent(); err != nil {
|
||||
pipe <- err
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(string(f.Content)); err != nil {
|
||||
pipe <- err
|
||||
if err := tx.Rollback(); err != nil {
|
||||
pipe <- err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
pipe <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Driver) Version() (uint64, error) {
|
||||
var version uint64
|
||||
err := d.db.QueryRow("SELECT version FROM " + tableName + " ORDER BY version DESC LIMIT 1").Scan(&version)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return 0, nil
|
||||
case err != nil:
|
||||
return 0, err
|
||||
default:
|
||||
return version, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Driver) ensureVersionTableExists() error {
|
||||
tx, err := d.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Exec(fmt.Sprintf(`
|
||||
CREATE TABLE IF NOT EXISTS %s (version uint64);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS version_unique ON %s (version);
|
||||
`, tableName, tableName)); err != nil {
|
||||
if err := tx.Rollback(); err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
driver.RegisterDriver("ql+file", &Driver{})
|
||||
driver.RegisterDriver("ql+memory", &Driver{})
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package driver
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var driversMu sync.Mutex
|
||||
var drivers = make(map[string]Driver)
|
||||
|
||||
// RegisterDriver register a driver so it can be created from its name. Drivers should
|
||||
// call this from an init() function so that they registers themselves on
|
||||
// import
|
||||
func RegisterDriver(name string, driver Driver) {
|
||||
driversMu.Lock()
|
||||
defer driversMu.Unlock()
|
||||
if driver == nil {
|
||||
panic("driver: Register driver is nil")
|
||||
}
|
||||
if _, dup := drivers[name]; dup {
|
||||
panic("sql: Register called twice for driver " + name)
|
||||
}
|
||||
drivers[name] = driver
|
||||
}
|
||||
|
||||
// GetDriver retrieves a registered driver by name
|
||||
func GetDriver(name string) Driver {
|
||||
driversMu.Lock()
|
||||
defer driversMu.Unlock()
|
||||
driver := drivers[name]
|
||||
return driver
|
||||
}
|
||||
|
||||
// Drivers returns a sorted list of the names of the registered drivers.
|
||||
func Drivers() []string {
|
||||
driversMu.Lock()
|
||||
defer driversMu.Unlock()
|
||||
var list []string
|
||||
for name := range drivers {
|
||||
list = append(list, name)
|
||||
}
|
||||
sort.Strings(list)
|
||||
return list
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
# Sqlite3 Driver
|
||||
|
||||
* Runs migrations in transcations.
|
||||
That means that if a migration failes, it will be safely rolled back.
|
||||
* Tries to return helpful error messages.
|
||||
* Stores migration version details in table ``schema_migrations``.
|
||||
This table will be auto-generated.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
migrate -url sqlite3://database.sqlite -path ./db/migrations create add_field_to_table
|
||||
migrate -url sqlite3://database.sqlite -path ./db/migrations up
|
||||
migrate help # for more info
|
||||
```
|
||||
|
||||
## Authors
|
||||
|
||||
* Matthias Kadenbach, https://github.com/mattes
|
||||
* Caesar Wirth, https://github.com/cjwirth
|
||||
@@ -0,0 +1,131 @@
|
||||
// Package sqlite3 implements the Driver interface.
|
||||
package sqlite3
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mattn/go-sqlite3"
|
||||
"gopkg.in/mattes/migrate.v1/driver"
|
||||
"gopkg.in/mattes/migrate.v1/file"
|
||||
"gopkg.in/mattes/migrate.v1/migrate/direction"
|
||||
)
|
||||
|
||||
type Driver struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
const tableName = "schema_migration"
|
||||
|
||||
func (driver *Driver) Initialize(url string) error {
|
||||
filename := strings.SplitN(url, "sqlite3://", 2)
|
||||
if len(filename) != 2 {
|
||||
return errors.New("invalid sqlite3:// scheme")
|
||||
}
|
||||
|
||||
db, err := sql.Open("sqlite3", filename[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := db.Ping(); err != nil {
|
||||
return err
|
||||
}
|
||||
driver.db = db
|
||||
|
||||
if err := driver.ensureVersionTableExists(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver *Driver) Close() error {
|
||||
if err := driver.db.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver *Driver) ensureVersionTableExists() error {
|
||||
if _, err := driver.db.Exec("CREATE TABLE IF NOT EXISTS " + tableName + " (version INTEGER PRIMARY KEY AUTOINCREMENT);"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver *Driver) FilenameExtension() string {
|
||||
return "sql"
|
||||
}
|
||||
|
||||
func (driver *Driver) Migrate(f file.File, pipe chan interface{}) {
|
||||
defer close(pipe)
|
||||
pipe <- f
|
||||
|
||||
tx, err := driver.db.Begin()
|
||||
if err != nil {
|
||||
pipe <- err
|
||||
return
|
||||
}
|
||||
|
||||
if f.Direction == direction.Up {
|
||||
if _, err := tx.Exec("INSERT INTO "+tableName+" (version) VALUES (?)", f.Version); err != nil {
|
||||
pipe <- err
|
||||
if err := tx.Rollback(); err != nil {
|
||||
pipe <- err
|
||||
}
|
||||
return
|
||||
}
|
||||
} else if f.Direction == direction.Down {
|
||||
if _, err := tx.Exec("DELETE FROM "+tableName+" WHERE version=?", f.Version); err != nil {
|
||||
pipe <- err
|
||||
if err := tx.Rollback(); err != nil {
|
||||
pipe <- err
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := f.ReadContent(); err != nil {
|
||||
pipe <- err
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(string(f.Content)); err != nil {
|
||||
sqliteErr, isErr := err.(sqlite3.Error)
|
||||
|
||||
if isErr {
|
||||
// The sqlite3 library only provides error codes, not position information. Output what we do know
|
||||
pipe <- fmt.Errorf("SQLite Error (%s); Extended (%s)\nError: %s", sqliteErr.Code.Error(), sqliteErr.ExtendedCode.Error(), sqliteErr.Error())
|
||||
} else {
|
||||
pipe <- fmt.Errorf("An error occurred: %s", err.Error())
|
||||
}
|
||||
|
||||
if err := tx.Rollback(); err != nil {
|
||||
pipe <- err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
pipe <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (driver *Driver) Version() (uint64, error) {
|
||||
var version uint64
|
||||
err := driver.db.QueryRow("SELECT version FROM " + tableName + " ORDER BY version DESC LIMIT 1").Scan(&version)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return 0, nil
|
||||
case err != nil:
|
||||
return 0, err
|
||||
default:
|
||||
return version, nil
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
driver.RegisterDriver("sqlite3", &Driver{})
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package sqlite3
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
"gopkg.in/mattes/migrate.v1/file"
|
||||
"gopkg.in/mattes/migrate.v1/migrate/direction"
|
||||
pipep "gopkg.in/mattes/migrate.v1/pipe"
|
||||
)
|
||||
|
||||
// TestMigrate runs some additional tests on Migrate()
|
||||
// Basic testing is already done in migrate/migrate_test.go
|
||||
func TestMigrate(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode.")
|
||||
}
|
||||
|
||||
driverFile := ":memory:"
|
||||
driverURL := "sqlite3://" + driverFile
|
||||
|
||||
// prepare clean database
|
||||
connection, err := sql.Open("sqlite3", driverFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := connection.Exec(`
|
||||
DROP TABLE IF EXISTS yolo;
|
||||
DROP TABLE IF EXISTS ` + tableName + `;`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
d := &Driver{}
|
||||
if err := d.Initialize(driverURL); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
files := []file.File{
|
||||
{
|
||||
Path: "/foobar",
|
||||
FileName: "001_foobar.up.sql",
|
||||
Version: 1,
|
||||
Name: "foobar",
|
||||
Direction: direction.Up,
|
||||
Content: []byte(`
|
||||
CREATE TABLE yolo (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT
|
||||
);
|
||||
`),
|
||||
},
|
||||
{
|
||||
Path: "/foobar",
|
||||
FileName: "002_foobar.down.sql",
|
||||
Version: 1,
|
||||
Name: "foobar",
|
||||
Direction: direction.Down,
|
||||
Content: []byte(`
|
||||
DROP TABLE yolo;
|
||||
`),
|
||||
},
|
||||
{
|
||||
Path: "/foobar",
|
||||
FileName: "002_foobar.up.sql",
|
||||
Version: 1,
|
||||
Name: "foobar",
|
||||
Direction: direction.Down,
|
||||
Content: []byte(`
|
||||
CREATE TABLE error (
|
||||
THIS; WILL CAUSE; AN ERROR;
|
||||
)
|
||||
`),
|
||||
},
|
||||
}
|
||||
|
||||
pipe := pipep.New()
|
||||
go d.Migrate(files[0], pipe)
|
||||
errs := pipep.ReadErrors(pipe)
|
||||
if len(errs) > 0 {
|
||||
t.Fatal(errs)
|
||||
}
|
||||
|
||||
pipe = pipep.New()
|
||||
go d.Migrate(files[1], pipe)
|
||||
errs = pipep.ReadErrors(pipe)
|
||||
if len(errs) > 0 {
|
||||
t.Fatal(errs)
|
||||
}
|
||||
|
||||
pipe = pipep.New()
|
||||
go d.Migrate(files[2], pipe)
|
||||
errs = pipep.ReadErrors(pipe)
|
||||
if len(errs) == 0 {
|
||||
t.Error("Expected test case to fail")
|
||||
}
|
||||
|
||||
if err := d.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
+16
-6
@@ -5,7 +5,6 @@ import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mattes/migrate/migrate/direction"
|
||||
"go/token"
|
||||
"io/ioutil"
|
||||
"path"
|
||||
@@ -13,6 +12,8 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/mattes/migrate.v1/migrate/direction"
|
||||
)
|
||||
|
||||
var filenameRegex = `^([0-9]+)_(.*)\.(up|down)\.%s$`
|
||||
@@ -137,10 +138,10 @@ func (mf *MigrationFiles) From(version uint64, relativeN int) (Files, error) {
|
||||
|
||||
if d == direction.Up && migrationFile.Version > version && migrationFile.UpFile != nil {
|
||||
files = append(files, *migrationFile.UpFile)
|
||||
counter -= 1
|
||||
counter--
|
||||
} else if d == direction.Down && migrationFile.Version <= version && migrationFile.DownFile != nil {
|
||||
files = append(files, *migrationFile.DownFile)
|
||||
counter -= 1
|
||||
counter--
|
||||
}
|
||||
} else {
|
||||
break
|
||||
@@ -163,9 +164,18 @@ func ReadMigrationFiles(path string, filenameRegex *regexp.Regexp) (files Migrat
|
||||
d direction.Direction
|
||||
}
|
||||
tmpFiles := make([]*tmpFile, 0)
|
||||
tmpFileMap := map[uint64]map[direction.Direction]tmpFile{}
|
||||
for _, file := range ioFiles {
|
||||
version, name, d, err := parseFilenameSchema(file.Name(), filenameRegex)
|
||||
if err == nil {
|
||||
if _, ok := tmpFileMap[version]; !ok {
|
||||
tmpFileMap[version] = map[direction.Direction]tmpFile{}
|
||||
}
|
||||
if existing, ok := tmpFileMap[version][d]; !ok {
|
||||
tmpFileMap[version][d] = tmpFile{version: version, name: name, filename: file.Name(), d: d}
|
||||
} else {
|
||||
return nil, fmt.Errorf("duplicate migration file version %d : %q and %q", version, existing.filename, file.Name())
|
||||
}
|
||||
tmpFiles = append(tmpFiles, &tmpFile{version, name, file.Name(), d})
|
||||
}
|
||||
}
|
||||
@@ -249,7 +259,7 @@ func parseFilenameSchema(filename string, filenameRegex *regexp.Regexp) (version
|
||||
|
||||
version, err = strconv.ParseUint(matches[1], 10, 0)
|
||||
if err != nil {
|
||||
return 0, "", 0, errors.New(fmt.Sprintf("Unable to parse version '%v' in filename schema", matches[0]))
|
||||
return 0, "", 0, fmt.Errorf("Unable to parse version '%v' in filename schema", matches[0])
|
||||
}
|
||||
|
||||
if matches[3] == "up" {
|
||||
@@ -257,7 +267,7 @@ func parseFilenameSchema(filename string, filenameRegex *regexp.Regexp) (version
|
||||
} else if matches[3] == "down" {
|
||||
d = direction.Down
|
||||
} else {
|
||||
return 0, "", 0, errors.New(fmt.Sprintf("Unable to parse up|down '%v' in filename schema", matches[3]))
|
||||
return 0, "", 0, fmt.Errorf("Unable to parse up|down '%v' in filename schema", matches[3])
|
||||
}
|
||||
|
||||
return version, matches[2], d, nil
|
||||
@@ -324,7 +334,7 @@ func LinesBeforeAndAfter(data []byte, line, before, after int, lineNumbers bool)
|
||||
lNew = append([]byte(lineCounterStr+": "), lNew...)
|
||||
}
|
||||
newLines = append(newLines, lNew)
|
||||
lineCounter += 1
|
||||
lineCounter++
|
||||
}
|
||||
|
||||
return bytes.Join(newLines, []byte("\n"))
|
||||
|
||||
+48
-4
@@ -1,11 +1,12 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"github.com/mattes/migrate/migrate/direction"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"gopkg.in/mattes/migrate.v1/migrate/direction"
|
||||
)
|
||||
|
||||
func TestParseFilenameSchema(t *testing.T) {
|
||||
@@ -61,7 +62,7 @@ func TestFiles(t *testing.T) {
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
if err := ioutil.WriteFile(path.Join(tmpdir, "nonsense.txt"), nil, 0755); err != nil {
|
||||
if err = ioutil.WriteFile(path.Join(tmpdir, "nonsense.txt"), nil, 0755); err != nil {
|
||||
t.Fatal("Unable to write files in tmpdir", err)
|
||||
}
|
||||
ioutil.WriteFile(path.Join(tmpdir, "002_migrationfile.up.sql"), nil, 0755)
|
||||
@@ -133,7 +134,7 @@ func TestFiles(t *testing.T) {
|
||||
}
|
||||
|
||||
// test read
|
||||
if err := files[4].DownFile.ReadContent(); err != nil {
|
||||
if err = files[4].DownFile.ReadContent(); err != nil {
|
||||
t.Error("Unable to read file", err)
|
||||
}
|
||||
if files[4].DownFile.Content == nil {
|
||||
@@ -169,7 +170,8 @@ func TestFiles(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
rangeFiles, err := files.From(test.from, test.relative)
|
||||
var rangeFiles Files
|
||||
rangeFiles, err = files.From(test.from, test.relative)
|
||||
if err != nil {
|
||||
t.Error("Unable to fetch range:", err)
|
||||
}
|
||||
@@ -209,3 +211,45 @@ func TestFiles(t *testing.T) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestDuplicateFiles(t *testing.T) {
|
||||
dups := []string{
|
||||
"001_migration.up.sql",
|
||||
"001_duplicate.up.sql",
|
||||
}
|
||||
|
||||
root, cleanFn, err := makeFiles("TestDuplicateFiles", dups...)
|
||||
defer cleanFn()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = ReadMigrationFiles(root, FilenameRegex("sql"))
|
||||
if err == nil {
|
||||
t.Fatal("Expected duplicate migration file error")
|
||||
}
|
||||
}
|
||||
|
||||
// makeFiles takes an identifier, and a list of file names and uses them to create a temporary
|
||||
// directory populated with files named with the names passed in. makeFiles returns the root
|
||||
// directory name, and a func suitable for a defer cleanup to remove the temporary files after
|
||||
// the calling function exits.
|
||||
func makeFiles(testname string, names ...string) (root string, cleanup func(), err error) {
|
||||
cleanup = func() {}
|
||||
root, err = ioutil.TempDir("/tmp", testname)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cleanup = func() { os.RemoveAll(root) }
|
||||
if err = ioutil.WriteFile(path.Join(root, "nonsense.txt"), nil, 0755); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, name := range names {
|
||||
if err = ioutil.WriteFile(path.Join(root, name), nil, 0755); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,29 +1,42 @@
|
||||
// Package main is the CLI.
|
||||
// You can use the CLI via Terminal.
|
||||
// import "github.com/mattes/migrate/migrate" for usage within Go.
|
||||
// import "gopkg.in/mattes/migrate.v1/migrate" for usage within Go.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/fatih/color"
|
||||
"github.com/mattes/migrate/file"
|
||||
"github.com/mattes/migrate/migrate"
|
||||
"github.com/mattes/migrate/migrate/direction"
|
||||
pipep "github.com/mattes/migrate/pipe"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
_ "gopkg.in/mattes/migrate.v1/driver/bash"
|
||||
_ "gopkg.in/mattes/migrate.v1/driver/cassandra"
|
||||
_ "gopkg.in/mattes/migrate.v1/driver/crate"
|
||||
_ "gopkg.in/mattes/migrate.v1/driver/mssql"
|
||||
_ "gopkg.in/mattes/migrate.v1/driver/mysql"
|
||||
_ "gopkg.in/mattes/migrate.v1/driver/neo4j"
|
||||
_ "gopkg.in/mattes/migrate.v1/driver/postgres"
|
||||
_ "gopkg.in/mattes/migrate.v1/driver/ql"
|
||||
_ "gopkg.in/mattes/migrate.v1/driver/sqlite3"
|
||||
"gopkg.in/mattes/migrate.v1/file"
|
||||
"gopkg.in/mattes/migrate.v1/migrate"
|
||||
"gopkg.in/mattes/migrate.v1/migrate/direction"
|
||||
pipep "gopkg.in/mattes/migrate.v1/pipe"
|
||||
)
|
||||
|
||||
var url = flag.String("url", "", "")
|
||||
var url = flag.String("url", os.Getenv("MIGRATE_URL"), "")
|
||||
var migrationsPath = flag.String("path", "", "")
|
||||
var version = flag.Bool("version", false, "Show migrate version")
|
||||
|
||||
func main() {
|
||||
flag.Usage = func() {
|
||||
helpCmd()
|
||||
}
|
||||
|
||||
flag.Parse()
|
||||
command := flag.Arg(0)
|
||||
|
||||
if *version {
|
||||
fmt.Println(Version)
|
||||
os.Exit(0)
|
||||
@@ -48,7 +61,7 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Version %v migration files created in %v:\n", migrationFile.Version, migrationsPath)
|
||||
fmt.Printf("Version %v migration files created in %v:\n", migrationFile.Version, *migrationsPath)
|
||||
fmt.Println(migrationFile.UpFile.FileName)
|
||||
fmt.Println(migrationFile.DownFile.FileName)
|
||||
|
||||
@@ -63,8 +76,11 @@ func main() {
|
||||
timerStart = time.Now()
|
||||
pipe := pipep.New()
|
||||
go migrate.Migrate(pipe, *url, *migrationsPath, relativeNInt)
|
||||
writePipe(pipe)
|
||||
ok := writePipe(pipe)
|
||||
printTimer()
|
||||
if !ok {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
case "goto":
|
||||
verifyMigrationsPath(*migrationsPath)
|
||||
@@ -86,40 +102,55 @@ func main() {
|
||||
timerStart = time.Now()
|
||||
pipe := pipep.New()
|
||||
go migrate.Migrate(pipe, *url, *migrationsPath, relativeNInt)
|
||||
writePipe(pipe)
|
||||
ok := writePipe(pipe)
|
||||
printTimer()
|
||||
if !ok {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
case "up":
|
||||
verifyMigrationsPath(*migrationsPath)
|
||||
timerStart = time.Now()
|
||||
pipe := pipep.New()
|
||||
go migrate.Up(pipe, *url, *migrationsPath)
|
||||
writePipe(pipe)
|
||||
ok := writePipe(pipe)
|
||||
printTimer()
|
||||
if !ok {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
case "down":
|
||||
verifyMigrationsPath(*migrationsPath)
|
||||
timerStart = time.Now()
|
||||
pipe := pipep.New()
|
||||
go migrate.Down(pipe, *url, *migrationsPath)
|
||||
writePipe(pipe)
|
||||
ok := writePipe(pipe)
|
||||
printTimer()
|
||||
if !ok {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
case "redo":
|
||||
verifyMigrationsPath(*migrationsPath)
|
||||
timerStart = time.Now()
|
||||
pipe := pipep.New()
|
||||
go migrate.Redo(pipe, *url, *migrationsPath)
|
||||
writePipe(pipe)
|
||||
ok := writePipe(pipe)
|
||||
printTimer()
|
||||
if !ok {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
case "reset":
|
||||
verifyMigrationsPath(*migrationsPath)
|
||||
timerStart = time.Now()
|
||||
pipe := pipep.New()
|
||||
go migrate.Reset(pipe, *url, *migrationsPath)
|
||||
writePipe(pipe)
|
||||
ok := writePipe(pipe)
|
||||
printTimer()
|
||||
if !ok {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
case "version":
|
||||
verifyMigrationsPath(*migrationsPath)
|
||||
@@ -131,47 +162,51 @@ func main() {
|
||||
fmt.Println(version)
|
||||
|
||||
default:
|
||||
fallthrough
|
||||
helpCmd()
|
||||
os.Exit(1)
|
||||
case "help":
|
||||
helpCmd()
|
||||
}
|
||||
}
|
||||
|
||||
func writePipe(pipe chan interface{}) {
|
||||
func writePipe(pipe chan interface{}) (ok bool) {
|
||||
okFlag := true
|
||||
if pipe != nil {
|
||||
for {
|
||||
select {
|
||||
case item, ok := <-pipe:
|
||||
if !ok {
|
||||
return
|
||||
} else {
|
||||
switch item.(type) {
|
||||
case item, more := <-pipe:
|
||||
if !more {
|
||||
return okFlag
|
||||
}
|
||||
switch item.(type) {
|
||||
|
||||
case string:
|
||||
fmt.Println(item.(string))
|
||||
case string:
|
||||
fmt.Println(item.(string))
|
||||
|
||||
case error:
|
||||
case error:
|
||||
c := color.New(color.FgRed)
|
||||
c.Printf("%s\n\n", item.(error).Error())
|
||||
okFlag = false
|
||||
|
||||
case file.File:
|
||||
f := item.(file.File)
|
||||
if f.Direction == direction.Up {
|
||||
c := color.New(color.FgGreen)
|
||||
c.Print(">")
|
||||
} else if f.Direction == direction.Down {
|
||||
c := color.New(color.FgRed)
|
||||
c.Println(item.(error).Error(), "\n")
|
||||
|
||||
case file.File:
|
||||
f := item.(file.File)
|
||||
c := color.New(color.FgBlue)
|
||||
if f.Direction == direction.Up {
|
||||
c.Print(">")
|
||||
} else if f.Direction == direction.Down {
|
||||
c.Print("<")
|
||||
}
|
||||
fmt.Printf(" %s\n", f.FileName)
|
||||
|
||||
default:
|
||||
text := fmt.Sprint(item)
|
||||
fmt.Println(text)
|
||||
c.Print("<")
|
||||
}
|
||||
fmt.Printf(" %s\n", f.FileName)
|
||||
|
||||
default:
|
||||
text := fmt.Sprint(item)
|
||||
fmt.Println(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return okFlag
|
||||
}
|
||||
|
||||
func verifyMigrationsPath(path string) {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
// Package direction just holds convenience constants for Up and Down migrations.
|
||||
package direction
|
||||
|
||||
// Direction - type that indicates direction of migration(up or down)
|
||||
type Direction int
|
||||
|
||||
const (
|
||||
Up Direction = +1
|
||||
Down = -1
|
||||
// Up - up migration
|
||||
Up Direction = +1
|
||||
// Down - down migration
|
||||
Down Direction = -1
|
||||
)
|
||||
|
||||
+66
-46
@@ -10,11 +10,12 @@ import (
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mattes/migrate/driver"
|
||||
"github.com/mattes/migrate/file"
|
||||
"github.com/mattes/migrate/migrate/direction"
|
||||
pipep "github.com/mattes/migrate/pipe"
|
||||
"gopkg.in/mattes/migrate.v1/driver"
|
||||
"gopkg.in/mattes/migrate.v1/file"
|
||||
"gopkg.in/mattes/migrate.v1/migrate/direction"
|
||||
pipep "gopkg.in/mattes/migrate.v1/pipe"
|
||||
)
|
||||
|
||||
// Up applies all available migrations
|
||||
@@ -27,33 +28,35 @@ func Up(pipe chan interface{}, url, migrationsPath string) {
|
||||
|
||||
applyMigrationFiles, err := files.ToLastFrom(version)
|
||||
if err != nil {
|
||||
if err2 := d.Close(); err != nil {
|
||||
if err2 := d.Close(); err2 != nil {
|
||||
pipe <- err2
|
||||
}
|
||||
go pipep.Close(pipe, err)
|
||||
return
|
||||
}
|
||||
|
||||
signals := handleInterrupts()
|
||||
defer signal.Stop(signals)
|
||||
|
||||
if len(applyMigrationFiles) > 0 {
|
||||
for _, f := range applyMigrationFiles {
|
||||
pipe1 := pipep.New()
|
||||
go d.Migrate(f, pipe1)
|
||||
if ok := pipep.WaitAndRedirect(pipe1, pipe, handleInterrupts()); !ok {
|
||||
if ok := pipep.WaitAndRedirect(pipe1, pipe, signals); !ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err2 := d.Close(); err != nil {
|
||||
pipe <- err2
|
||||
}
|
||||
go pipep.Close(pipe, nil)
|
||||
return
|
||||
} else {
|
||||
if err2 := d.Close(); err != nil {
|
||||
pipe <- err2
|
||||
if err := d.Close(); err != nil {
|
||||
pipe <- err
|
||||
}
|
||||
go pipep.Close(pipe, nil)
|
||||
return
|
||||
}
|
||||
if err := d.Close(); err != nil {
|
||||
pipe <- err
|
||||
}
|
||||
go pipep.Close(pipe, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// UpSync is synchronous version of Up
|
||||
@@ -74,33 +77,35 @@ func Down(pipe chan interface{}, url, migrationsPath string) {
|
||||
|
||||
applyMigrationFiles, err := files.ToFirstFrom(version)
|
||||
if err != nil {
|
||||
if err2 := d.Close(); err != nil {
|
||||
if err2 := d.Close(); err2 != nil {
|
||||
pipe <- err2
|
||||
}
|
||||
go pipep.Close(pipe, err)
|
||||
return
|
||||
}
|
||||
|
||||
signals := handleInterrupts()
|
||||
defer signal.Stop(signals)
|
||||
|
||||
if len(applyMigrationFiles) > 0 {
|
||||
for _, f := range applyMigrationFiles {
|
||||
pipe1 := pipep.New()
|
||||
go d.Migrate(f, pipe1)
|
||||
if ok := pipep.WaitAndRedirect(pipe1, pipe, handleInterrupts()); !ok {
|
||||
if ok := pipep.WaitAndRedirect(pipe1, pipe, signals); !ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err2 := d.Close(); err != nil {
|
||||
pipe <- err2
|
||||
}
|
||||
go pipep.Close(pipe, nil)
|
||||
return
|
||||
} else {
|
||||
if err2 := d.Close(); err != nil {
|
||||
if err2 := d.Close(); err2 != nil {
|
||||
pipe <- err2
|
||||
}
|
||||
go pipep.Close(pipe, nil)
|
||||
return
|
||||
}
|
||||
if err2 := d.Close(); err2 != nil {
|
||||
pipe <- err2
|
||||
}
|
||||
go pipep.Close(pipe, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// DownSync is synchronous version of Down
|
||||
@@ -115,12 +120,15 @@ func DownSync(url, migrationsPath string) (err []error, ok bool) {
|
||||
func Redo(pipe chan interface{}, url, migrationsPath string) {
|
||||
pipe1 := pipep.New()
|
||||
go Migrate(pipe1, url, migrationsPath, -1)
|
||||
if ok := pipep.WaitAndRedirect(pipe1, pipe, handleInterrupts()); !ok {
|
||||
|
||||
signals := handleInterrupts()
|
||||
defer signal.Stop(signals)
|
||||
|
||||
if ok := pipep.WaitAndRedirect(pipe1, pipe, signals); !ok {
|
||||
go pipep.Close(pipe, nil)
|
||||
return
|
||||
} else {
|
||||
go Migrate(pipe, url, migrationsPath, +1)
|
||||
}
|
||||
go Migrate(pipe, url, migrationsPath, +1)
|
||||
}
|
||||
|
||||
// RedoSync is synchronous version of Redo
|
||||
@@ -135,12 +143,15 @@ func RedoSync(url, migrationsPath string) (err []error, ok bool) {
|
||||
func Reset(pipe chan interface{}, url, migrationsPath string) {
|
||||
pipe1 := pipep.New()
|
||||
go Down(pipe1, url, migrationsPath)
|
||||
if ok := pipep.WaitAndRedirect(pipe1, pipe, handleInterrupts()); !ok {
|
||||
|
||||
signals := handleInterrupts()
|
||||
defer signal.Stop(signals)
|
||||
|
||||
if ok := pipep.WaitAndRedirect(pipe1, pipe, signals); !ok {
|
||||
go pipep.Close(pipe, nil)
|
||||
return
|
||||
} else {
|
||||
go Up(pipe, url, migrationsPath)
|
||||
}
|
||||
go Up(pipe, url, migrationsPath)
|
||||
}
|
||||
|
||||
// ResetSync is synchronous version of Reset
|
||||
@@ -161,28 +172,31 @@ func Migrate(pipe chan interface{}, url, migrationsPath string, relativeN int) {
|
||||
|
||||
applyMigrationFiles, err := files.From(version, relativeN)
|
||||
if err != nil {
|
||||
if err2 := d.Close(); err != nil {
|
||||
if err2 := d.Close(); err2 != nil {
|
||||
pipe <- err2
|
||||
}
|
||||
go pipep.Close(pipe, err)
|
||||
return
|
||||
}
|
||||
|
||||
signals := handleInterrupts()
|
||||
defer signal.Stop(signals)
|
||||
|
||||
if len(applyMigrationFiles) > 0 && relativeN != 0 {
|
||||
for _, f := range applyMigrationFiles {
|
||||
pipe1 := pipep.New()
|
||||
go d.Migrate(f, pipe1)
|
||||
if ok := pipep.WaitAndRedirect(pipe1, pipe, handleInterrupts()); !ok {
|
||||
if ok := pipep.WaitAndRedirect(pipe1, pipe, signals); !ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err2 := d.Close(); err != nil {
|
||||
if err2 := d.Close(); err2 != nil {
|
||||
pipe <- err2
|
||||
}
|
||||
go pipep.Close(pipe, nil)
|
||||
return
|
||||
}
|
||||
if err2 := d.Close(); err != nil {
|
||||
if err2 := d.Close(); err2 != nil {
|
||||
pipe <- err2
|
||||
}
|
||||
go pipep.Close(pipe, nil)
|
||||
@@ -203,29 +217,35 @@ func Version(url, migrationsPath string) (version uint64, err error) {
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer func() {
|
||||
err = d.Close()
|
||||
}()
|
||||
return d.Version()
|
||||
}
|
||||
|
||||
// Create creates new migration files on disk
|
||||
func Create(url, migrationsPath, name string) (*file.MigrationFile, error) {
|
||||
d, err := driver.New(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files, err := file.ReadMigrationFiles(migrationsPath, file.FilenameRegex(d.FilenameExtension()))
|
||||
ext, err := driver.FilenameExtensionFromURL(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
version := uint64(0)
|
||||
if len(files) > 0 {
|
||||
lastFile := files[len(files)-1]
|
||||
version = lastFile.Version
|
||||
files, err := file.ReadMigrationFiles(migrationsPath, file.FilenameRegex(ext))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
version += 1
|
||||
|
||||
version := uint64(time.Now().Unix())
|
||||
|
||||
for _, f := range files {
|
||||
if f.Version == version {
|
||||
version++
|
||||
}
|
||||
}
|
||||
|
||||
versionStr := strconv.FormatUint(version, 10)
|
||||
|
||||
length := 4 // TODO(mattes) check existing files and try to guess length
|
||||
length := 10
|
||||
if len(versionStr)%length != 0 {
|
||||
versionStr = strings.Repeat("0", length-len(versionStr)%length) + versionStr
|
||||
}
|
||||
@@ -237,14 +257,14 @@ func Create(url, migrationsPath, name string) (*file.MigrationFile, error) {
|
||||
Version: version,
|
||||
UpFile: &file.File{
|
||||
Path: migrationsPath,
|
||||
FileName: fmt.Sprintf(filenamef, versionStr, name, "up", d.FilenameExtension()),
|
||||
FileName: fmt.Sprintf(filenamef, versionStr, name, "up", ext),
|
||||
Name: name,
|
||||
Content: []byte(""),
|
||||
Direction: direction.Up,
|
||||
},
|
||||
DownFile: &file.File{
|
||||
Path: migrationsPath,
|
||||
FileName: fmt.Sprintf(filenamef, versionStr, name, "down", d.FilenameExtension()),
|
||||
FileName: fmt.Sprintf(filenamef, versionStr, name, "down", ext),
|
||||
Name: name,
|
||||
Content: []byte(""),
|
||||
Direction: direction.Down,
|
||||
|
||||
+163
-116
@@ -2,26 +2,44 @@ package migrate
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
// Ensure imports for each driver we wish to test
|
||||
|
||||
_ "gopkg.in/mattes/migrate.v1/driver/postgres"
|
||||
_ "gopkg.in/mattes/migrate.v1/driver/ql"
|
||||
_ "gopkg.in/mattes/migrate.v1/driver/sqlite3"
|
||||
)
|
||||
|
||||
// Add Driver URLs here to test basic Up, Down, .. functions.
|
||||
var driverUrls = []string{
|
||||
"postgres://localhost/migratetest?sslmode=disable",
|
||||
var driverURLs = []string{
|
||||
"postgres://postgres@" + os.Getenv("POSTGRES_PORT_5432_TCP_ADDR") + ":" + os.Getenv("POSTGRES_PORT_5432_TCP_PORT") + "/template1?sslmode=disable",
|
||||
"ql+file://./test.db",
|
||||
}
|
||||
|
||||
func tearDown(driverURL, tmpdir string) {
|
||||
DownSync(driverURL, tmpdir)
|
||||
os.RemoveAll(tmpdir)
|
||||
}
|
||||
|
||||
func TestCreate(t *testing.T) {
|
||||
for _, driverUrl := range driverUrls {
|
||||
t.Logf("Test driver: %s", driverUrl)
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode.")
|
||||
}
|
||||
for _, driverURL := range driverURLs {
|
||||
t.Logf("Test driver: %s", driverURL)
|
||||
tmpdir, err := ioutil.TempDir("/tmp", "migrate-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer tearDown(driverURL, tmpdir)
|
||||
|
||||
if _, err := Create(driverUrl, tmpdir, "test_migration"); err != nil {
|
||||
if _, err = Create(driverURL, tmpdir, "test_migration"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := Create(driverUrl, tmpdir, "another migration"); err != nil {
|
||||
if _, err = Create(driverURL, tmpdir, "another migration"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -30,208 +48,237 @@ func TestCreate(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(files) != 4 {
|
||||
t.Fatal("Expected 2 new files, got", len(files))
|
||||
t.Fatal("Expected 4 new files, got", len(files))
|
||||
}
|
||||
fileNameRegexp := regexp.MustCompile(`^\d{10}_(.*.[up|down].sql)`)
|
||||
|
||||
expectFiles := []string{
|
||||
"0001_test_migration.up.sql", "0001_test_migration.down.sql",
|
||||
"0002_another_migration.up.sql", "0002_another_migration.down.sql",
|
||||
"test_migration.up.sql", "test_migration.down.sql",
|
||||
"another_migration.up.sql", "another_migration.down.sql",
|
||||
}
|
||||
foundCounter := 0
|
||||
for _, expectFile := range expectFiles {
|
||||
for _, file := range files {
|
||||
if expectFile == file.Name() {
|
||||
foundCounter += 1
|
||||
break
|
||||
|
||||
var foundCounter int
|
||||
|
||||
for _, file := range files {
|
||||
if x := fileNameRegexp.FindStringSubmatch(file.Name()); len(x) != 2 {
|
||||
t.Errorf("expected %v to match %v", file.Name(), fileNameRegexp)
|
||||
} else {
|
||||
for _, expect := range expectFiles {
|
||||
if expect == x[1] {
|
||||
foundCounter++
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if foundCounter != len(expectFiles) {
|
||||
t.Error("not all expected files have been found")
|
||||
t.Errorf("expected %v files, got %v", len(expectFiles), foundCounter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReset(t *testing.T) {
|
||||
for _, driverUrl := range driverUrls {
|
||||
t.Logf("Test driver: %s", driverUrl)
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode.")
|
||||
}
|
||||
for _, driverURL := range driverURLs {
|
||||
t.Logf("Test driver: %s", driverURL)
|
||||
tmpdir, err := ioutil.TempDir("/tmp", "migrate-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer tearDown(driverURL, tmpdir)
|
||||
|
||||
Create(driverUrl, tmpdir, "migration1")
|
||||
Create(driverUrl, tmpdir, "migration2")
|
||||
|
||||
errs, ok := ResetSync(driverUrl, tmpdir)
|
||||
if !ok {
|
||||
t.Fatal(errs)
|
||||
}
|
||||
version, err := Version(driverUrl, tmpdir)
|
||||
Create(driverURL, tmpdir, "migration1")
|
||||
f, err := Create(driverURL, tmpdir, "migration2")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if version != 2 {
|
||||
t.Fatalf("Expected version 2, got %v", version)
|
||||
|
||||
if err, ok := ResetSync(driverURL, tmpdir); !ok {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if version, err := Version(driverURL, tmpdir); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if version != f.Version {
|
||||
t.Fatalf("Expected version %v, got %v", version, f.Version)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDown(t *testing.T) {
|
||||
for _, driverUrl := range driverUrls {
|
||||
t.Logf("Test driver: %s", driverUrl)
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode.")
|
||||
}
|
||||
for _, driverURL := range driverURLs {
|
||||
t.Logf("Test driver: %s", driverURL)
|
||||
tmpdir, err := ioutil.TempDir("/tmp", "migrate-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer tearDown(driverURL, tmpdir)
|
||||
|
||||
Create(driverUrl, tmpdir, "migration1")
|
||||
Create(driverUrl, tmpdir, "migration2")
|
||||
initVersion, _ := Version(driverURL, tmpdir)
|
||||
|
||||
errs, ok := ResetSync(driverUrl, tmpdir)
|
||||
if !ok {
|
||||
t.Fatal(errs)
|
||||
}
|
||||
version, err := Version(driverUrl, tmpdir)
|
||||
if err != nil {
|
||||
firstMigration, _ := Create(driverURL, tmpdir, "migration1")
|
||||
secondMigration, _ := Create(driverURL, tmpdir, "migration2")
|
||||
|
||||
t.Logf("init %v first %v second %v", initVersion, firstMigration.Version, secondMigration.Version)
|
||||
|
||||
if err, ok := ResetSync(driverURL, tmpdir); !ok {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if version != 2 {
|
||||
t.Fatalf("Expected version 2, got %v", version)
|
||||
|
||||
if version, err := Version(driverURL, tmpdir); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if version != secondMigration.Version {
|
||||
t.Fatalf("Expected version %v, got %v", version, secondMigration.Version)
|
||||
}
|
||||
|
||||
errs, ok = DownSync(driverUrl, tmpdir)
|
||||
if !ok {
|
||||
t.Fatal(errs)
|
||||
}
|
||||
version, err = Version(driverUrl, tmpdir)
|
||||
if err != nil {
|
||||
if err, ok := DownSync(driverURL, tmpdir); !ok {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if version != 0 {
|
||||
t.Fatalf("Expected version 0, got %v", version)
|
||||
|
||||
if version, err := Version(driverURL, tmpdir); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if version != 0 {
|
||||
t.Fatalf("Expected 0, got %v", version)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUp(t *testing.T) {
|
||||
for _, driverUrl := range driverUrls {
|
||||
t.Logf("Test driver: %s", driverUrl)
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode.")
|
||||
}
|
||||
for _, driverURL := range driverURLs {
|
||||
t.Logf("Test driver: %s", driverURL)
|
||||
tmpdir, err := ioutil.TempDir("/tmp", "migrate-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer tearDown(driverURL, tmpdir)
|
||||
|
||||
Create(driverUrl, tmpdir, "migration1")
|
||||
Create(driverUrl, tmpdir, "migration2")
|
||||
initVersion, _ := Version(driverURL, tmpdir)
|
||||
|
||||
errs, ok := DownSync(driverUrl, tmpdir)
|
||||
if !ok {
|
||||
t.Fatal(errs)
|
||||
}
|
||||
version, err := Version(driverUrl, tmpdir)
|
||||
if err != nil {
|
||||
firstMigration, _ := Create(driverURL, tmpdir, "migration1")
|
||||
secondMigration, _ := Create(driverURL, tmpdir, "migration2")
|
||||
|
||||
t.Logf("init %v first %v second %v", initVersion, firstMigration.Version, secondMigration.Version)
|
||||
|
||||
if err, ok := DownSync(driverURL, tmpdir); !ok {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if version != 0 {
|
||||
t.Fatalf("Expected version 0, got %v", version)
|
||||
|
||||
if version, err := Version(driverURL, tmpdir); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if version != initVersion {
|
||||
t.Fatalf("Expected initial version %v, got %v", initVersion, version)
|
||||
}
|
||||
|
||||
errs, ok = UpSync(driverUrl, tmpdir)
|
||||
if !ok {
|
||||
t.Fatal(errs)
|
||||
}
|
||||
version, err = Version(driverUrl, tmpdir)
|
||||
if err != nil {
|
||||
if err, ok := UpSync(driverURL, tmpdir); !ok {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if version != 2 {
|
||||
t.Fatalf("Expected version 2, got %v", version)
|
||||
|
||||
if version, err := Version(driverURL, tmpdir); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if version != secondMigration.Version {
|
||||
t.Fatalf("Expected migrated version %v, got %v", secondMigration.Version, version)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedo(t *testing.T) {
|
||||
for _, driverUrl := range driverUrls {
|
||||
t.Logf("Test driver: %s", driverUrl)
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode.")
|
||||
}
|
||||
for _, driverURL := range driverURLs {
|
||||
t.Logf("Test driver: %s", driverURL)
|
||||
tmpdir, err := ioutil.TempDir("/tmp", "migrate-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer tearDown(driverURL, tmpdir)
|
||||
|
||||
Create(driverUrl, tmpdir, "migration1")
|
||||
Create(driverUrl, tmpdir, "migration2")
|
||||
initVersion, _ := Version(driverURL, tmpdir)
|
||||
|
||||
errs, ok := ResetSync(driverUrl, tmpdir)
|
||||
if !ok {
|
||||
t.Fatal(errs)
|
||||
}
|
||||
version, err := Version(driverUrl, tmpdir)
|
||||
if err != nil {
|
||||
firstMigration, _ := Create(driverURL, tmpdir, "migration1")
|
||||
secondMigration, _ := Create(driverURL, tmpdir, "migration2")
|
||||
|
||||
t.Logf("init %v first %v second %v", initVersion, firstMigration.Version, secondMigration.Version)
|
||||
|
||||
if err, ok := ResetSync(driverURL, tmpdir); !ok {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if version != 2 {
|
||||
t.Fatalf("Expected version 2, got %v", version)
|
||||
|
||||
if version, err := Version(driverURL, tmpdir); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if version != secondMigration.Version {
|
||||
t.Fatalf("Expected migrated version %v, got %v", secondMigration.Version, version)
|
||||
}
|
||||
|
||||
errs, ok = RedoSync(driverUrl, tmpdir)
|
||||
if !ok {
|
||||
t.Fatal(errs)
|
||||
}
|
||||
version, err = Version(driverUrl, tmpdir)
|
||||
if err != nil {
|
||||
if err, ok := RedoSync(driverURL, tmpdir); !ok {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if version != 2 {
|
||||
t.Fatalf("Expected version 2, got %v", version)
|
||||
|
||||
if version, err := Version(driverURL, tmpdir); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if version != secondMigration.Version {
|
||||
t.Fatalf("Expected migrated version %v, got %v", secondMigration.Version, version)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrate(t *testing.T) {
|
||||
for _, driverUrl := range driverUrls {
|
||||
t.Logf("Test driver: %s", driverUrl)
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode.")
|
||||
}
|
||||
for _, driverURL := range driverURLs {
|
||||
t.Logf("Test driver: %s", driverURL)
|
||||
tmpdir, err := ioutil.TempDir("/tmp", "migrate-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer tearDown(driverURL, tmpdir)
|
||||
|
||||
Create(driverUrl, tmpdir, "migration1")
|
||||
Create(driverUrl, tmpdir, "migration2")
|
||||
initVersion, _ := Version(driverURL, tmpdir)
|
||||
|
||||
errs, ok := ResetSync(driverUrl, tmpdir)
|
||||
if !ok {
|
||||
t.Fatal(errs)
|
||||
}
|
||||
version, err := Version(driverUrl, tmpdir)
|
||||
if err != nil {
|
||||
firstMigration, _ := Create(driverURL, tmpdir, "migration1")
|
||||
secondMigration, _ := Create(driverURL, tmpdir, "migration2")
|
||||
|
||||
t.Logf("init %v first %v second %v", initVersion, firstMigration.Version, secondMigration.Version)
|
||||
|
||||
if err, ok := ResetSync(driverURL, tmpdir); !ok {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if version != 2 {
|
||||
t.Fatalf("Expected version 2, got %v", version)
|
||||
|
||||
if version, err := Version(driverURL, tmpdir); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if version != secondMigration.Version {
|
||||
t.Fatalf("Expected migrated version %v, got %v", secondMigration.Version, version)
|
||||
}
|
||||
|
||||
errs, ok = MigrateSync(driverUrl, tmpdir, -2)
|
||||
if !ok {
|
||||
t.Fatal(errs)
|
||||
}
|
||||
version, err = Version(driverUrl, tmpdir)
|
||||
if err != nil {
|
||||
if err, ok := MigrateSync(driverURL, tmpdir, -2); !ok {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if version != 0 {
|
||||
t.Fatalf("Expected version 0, got %v", version)
|
||||
}
|
||||
|
||||
errs, ok = MigrateSync(driverUrl, tmpdir, +1)
|
||||
if !ok {
|
||||
t.Fatal(errs)
|
||||
if version, err := Version(driverURL, tmpdir); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if version != 0 {
|
||||
t.Fatalf("Expected 0, got %v", version)
|
||||
}
|
||||
version, err = Version(driverUrl, tmpdir)
|
||||
if err != nil {
|
||||
|
||||
if err, ok := MigrateSync(driverURL, tmpdir, +1); !ok {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if version != 1 {
|
||||
t.Fatalf("Expected version 1, got %v", version)
|
||||
|
||||
if version, err := Version(driverURL, tmpdir); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if version != firstMigration.Version {
|
||||
t.Fatalf("Expected first version %v, got %v", firstMigration.Version, version)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+18
-12
@@ -3,6 +3,7 @@ package pipe
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
)
|
||||
|
||||
// New creates a new pipe. A pipe is basically a channel.
|
||||
@@ -25,12 +26,13 @@ func Close(pipe chan interface{}, err error) {
|
||||
func WaitAndRedirect(pipe, redirectPipe chan interface{}, interrupt chan os.Signal) (ok bool) {
|
||||
errorReceived := false
|
||||
interruptsReceived := 0
|
||||
defer stopNotifyInterruptChannel(interrupt)
|
||||
if pipe != nil && redirectPipe != nil {
|
||||
for {
|
||||
select {
|
||||
|
||||
case <-interrupt:
|
||||
interruptsReceived += 1
|
||||
interruptsReceived++
|
||||
if interruptsReceived > 1 {
|
||||
os.Exit(5)
|
||||
} else {
|
||||
@@ -41,12 +43,11 @@ func WaitAndRedirect(pipe, redirectPipe chan interface{}, interrupt chan os.Sign
|
||||
case item, ok := <-pipe:
|
||||
if !ok {
|
||||
return !errorReceived && interruptsReceived == 0
|
||||
} else {
|
||||
redirectPipe <- item
|
||||
switch item.(type) {
|
||||
case error:
|
||||
errorReceived = true
|
||||
}
|
||||
}
|
||||
redirectPipe <- item
|
||||
switch item.(type) {
|
||||
case error:
|
||||
errorReceived = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,14 +65,19 @@ func ReadErrors(pipe chan interface{}) []error {
|
||||
case item, ok := <-pipe:
|
||||
if !ok {
|
||||
return err
|
||||
} else {
|
||||
switch item.(type) {
|
||||
case error:
|
||||
err = append(err, item.(error))
|
||||
}
|
||||
}
|
||||
switch item.(type) {
|
||||
case error:
|
||||
err = append(err, item.(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func stopNotifyInterruptChannel(interruptChannel chan os.Signal) {
|
||||
if interruptChannel != nil {
|
||||
signal.Stop(interruptChannel)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
package main
|
||||
|
||||
const Version string = "1.2.0"
|
||||
// Version - version of tool
|
||||
const Version string = "1.3.1"
|
||||
|
||||
Reference in New Issue
Block a user