initial version 3.0.0 preview
hard work
This commit is contained in:
+3
-1
@@ -1,2 +1,4 @@
|
||||
.DS_Store
|
||||
test.db
|
||||
cli/build
|
||||
cli/cli
|
||||
cli/migrate
|
||||
|
||||
+1
-7
@@ -2,7 +2,6 @@ language: go
|
||||
sudo: required
|
||||
|
||||
go:
|
||||
- 1.4
|
||||
- 1.5
|
||||
- 1.6
|
||||
- 1.7
|
||||
@@ -10,10 +9,5 @@ go:
|
||||
services:
|
||||
- docker
|
||||
|
||||
before_install:
|
||||
- curl -L https://github.com/docker/compose/releases/download/1.4.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: make test
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
FROM scratch
|
||||
ADD migrate /migrate
|
||||
ENTRYPOINT ["/migrate"]
|
||||
@@ -1,6 +1,8 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Matthias Kadenbach
|
||||
Copyright (c) 2016 Matthias Kadenbach
|
||||
|
||||
https://github.com/mattes/migrate
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -18,4 +20,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
THE SOFTWARE.
|
||||
|
||||
@@ -1,30 +1,52 @@
|
||||
TESTFLAGS?=
|
||||
IMAGE=mattes/migrate
|
||||
DCR=docker-compose run --rm
|
||||
GOTEST=go test $(TESTFLAGS) `go list ./... | grep -v "/vendor/"`
|
||||
SOURCE?=file go-bindata github
|
||||
DATABASE?=postgres
|
||||
VERSION?=$(shell git describe --tags 2>/dev/null)
|
||||
TEST_FLAGS?=
|
||||
|
||||
.PHONY: clean test build release docker-build docker-push run
|
||||
all: release
|
||||
# define comma and space
|
||||
, := ,
|
||||
space :=
|
||||
space +=
|
||||
|
||||
build-cli: clean
|
||||
-mkdir ./cli/build
|
||||
cd ./cli && GOOS=linux GOARCH=amd64 go build -a -o build/migrate.$(VERSION).linux-amd64 -ldflags="-X main.Version=$(VERSION)" -tags '$(DATABASE) $(SOURCE)' .
|
||||
cd ./cli && GOOS=darwin GOARCH=amd64 go build -a -o build/migrate.$(VERSION).darwin-amd64 -ldflags="-X main.Version=$(VERSION)" -tags '$(DATABASE) $(SOURCE)' .
|
||||
cd ./cli && GOOS=windows GOARCH=amd64 go build -a -o build/migrate.$(VERSION).windows-amd64.exe -ldflags="-X main.Version=$(VERSION)" -tags '$(DATABASE) $(SOURCE)' .
|
||||
cd ./cli/build && find . -name 'migrate*' | xargs -I{} tar czf {}.tar.gz {}
|
||||
cd ./cli/build && shasum -a 256 * > sha256sum.txt
|
||||
cat ./cli/build/sha256sum.txt
|
||||
|
||||
clean:
|
||||
rm -f migrate
|
||||
-rm -r ./cli/build
|
||||
|
||||
fmt:
|
||||
@gofmt -s -w `go list -f {{.Dir}} ./... | grep -v "/vendor/"`
|
||||
test-short:
|
||||
make test-with-flags --ignore-errors TEST_FLAGS='-short'
|
||||
|
||||
test: fmt
|
||||
$(DCR) go-test
|
||||
test:
|
||||
make test-with-flags TEST_FLAGS='-race -v -cover -bench=. -benchmem'
|
||||
|
||||
go-test: fmt
|
||||
@$(GOTEST)
|
||||
coverage:
|
||||
make test-with-flags TEST_FLAGS='-cover -short'
|
||||
|
||||
build:
|
||||
$(DCR) go-build
|
||||
test-with-flags:
|
||||
@echo SOURCE: $(SOURCE)
|
||||
@echo DATABASE: $(DATABASE)
|
||||
|
||||
release: test build docker-build docker-push
|
||||
@go test $(TEST_FLAGS) .
|
||||
@go test $(TEST_FLAGS) ./cli/...
|
||||
|
||||
docker-build:
|
||||
docker build --rm -t $(IMAGE) .
|
||||
@go test $(TEST_FLAGS) ./source/{$(subst $(space),$(,),$(SOURCE)),}
|
||||
@go test $(TEST_FLAGS) ./source/testing
|
||||
@go test $(TEST_FLAGS) ./source/stub
|
||||
|
||||
@go test $(TEST_FLAGS) ./database/{$(subst $(space),$(,),$(DATABASE)),}
|
||||
@go test $(TEST_FLAGS) ./database/testing
|
||||
@go test $(TEST_FLAGS) ./database/stub
|
||||
|
||||
# deprecated v1compat:
|
||||
@go test $(TEST_FLAGS) ./migrate/...
|
||||
|
||||
|
||||
.PHONY: build-cli clean test-short test coverage test-with-flags
|
||||
|
||||
docker-push:
|
||||
docker push $(IMAGE)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// +build github
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "github.com/mattes/migrate/source/github"
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
// +build go-bindata
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "github.com/mattes/migrate/source/go-bindata"
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
// +build postgres
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "github.com/mattes/migrate/database/postgres"
|
||||
)
|
||||
@@ -0,0 +1,51 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/mattes/migrate"
|
||||
_ "github.com/mattes/migrate/database/stub" // TODO remove again
|
||||
_ "github.com/mattes/migrate/source/file"
|
||||
)
|
||||
|
||||
func gotoCmd(m *migrate.Migrate, v uint) {
|
||||
if err := m.Migrate(v); err != nil {
|
||||
log.fatalErr(err)
|
||||
}
|
||||
}
|
||||
|
||||
func upCmd(m *migrate.Migrate, limit int) {
|
||||
if limit >= 0 {
|
||||
if err := m.Steps(limit); err != nil {
|
||||
log.fatalErr(err)
|
||||
}
|
||||
} else {
|
||||
if err := m.Up(); err != nil {
|
||||
log.fatalErr(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func downCmd(m *migrate.Migrate, limit int) {
|
||||
if limit >= 0 {
|
||||
if err := m.Steps(-limit); err != nil {
|
||||
log.fatalErr(err)
|
||||
}
|
||||
} else {
|
||||
if err := m.Down(); err != nil {
|
||||
log.fatalErr(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func dropCmd(m *migrate.Migrate) {
|
||||
if err := m.Drop(); err != nil {
|
||||
log.fatalErr(err)
|
||||
}
|
||||
}
|
||||
|
||||
func versionCmd(m *migrate.Migrate) {
|
||||
v, err := m.Version()
|
||||
if err != nil {
|
||||
log.fatalErr(err)
|
||||
}
|
||||
log.Println(v)
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
logpkg "log"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Log struct {
|
||||
verbose bool
|
||||
}
|
||||
|
||||
func (l *Log) Printf(format string, v ...interface{}) {
|
||||
if l.verbose {
|
||||
logpkg.Printf(format, v...)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Log) Println(args ...interface{}) {
|
||||
if l.verbose {
|
||||
logpkg.Println(args...)
|
||||
} else {
|
||||
fmt.Fprintln(os.Stderr, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Log) Verbose() bool {
|
||||
return l.verbose
|
||||
}
|
||||
|
||||
func (l *Log) fatalf(format string, v ...interface{}) {
|
||||
l.Printf(format, v...)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func (l *Log) fatal(args ...interface{}) {
|
||||
l.Println(args...)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func (l *Log) fatalErr(err error) {
|
||||
l.fatal("error:", err)
|
||||
}
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/mattes/migrate"
|
||||
)
|
||||
|
||||
// set main log
|
||||
var log = &Log{}
|
||||
|
||||
func main() {
|
||||
helpPtr := flag.Bool("help", false, "")
|
||||
versionPtr := flag.Bool("version", false, "")
|
||||
verbosePtr := flag.Bool("verbose", false, "")
|
||||
prefetchPtr := flag.Uint("prefetch", 10, "")
|
||||
pathPtr := flag.String("path", "", "")
|
||||
databasePtr := flag.String("database", "", "")
|
||||
sourcePtr := flag.String("source", "", "")
|
||||
|
||||
flag.Usage = func() {
|
||||
fmt.Fprint(os.Stderr,
|
||||
`Usage: migrate OPTIONS COMMAND [arg...]
|
||||
migrate [ -version | -help ]
|
||||
|
||||
Options:
|
||||
-source Location of the migrations (driver://url)
|
||||
-path Shorthand for -source=file://path
|
||||
-database Run migrations against this database (driver://url)
|
||||
-prefetch N Number of migrations to load in advance before executing (default 10)
|
||||
-verbose Print verbose logging
|
||||
-version Print version
|
||||
-help Print usage
|
||||
|
||||
Commands:
|
||||
goto V Migrate to version V
|
||||
up [N] Apply all or N up migrations
|
||||
down [N] Apply all or N down migrations
|
||||
drop Drop everyting inside database
|
||||
version Print current migration version
|
||||
`)
|
||||
}
|
||||
|
||||
flag.Parse()
|
||||
|
||||
// initialize logger
|
||||
log.verbose = *verbosePtr
|
||||
|
||||
// show cli version
|
||||
if *versionPtr {
|
||||
fmt.Fprintln(os.Stderr, Version)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// show help
|
||||
if *helpPtr {
|
||||
flag.Usage()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// translate -path into -source if given
|
||||
if *sourcePtr == "" && *pathPtr != "" {
|
||||
*sourcePtr = fmt.Sprintf("file://%v", *pathPtr)
|
||||
}
|
||||
|
||||
// initialize migrate
|
||||
// don't catch migraterErr here and let each command decide
|
||||
// how it wants to handle the error
|
||||
migrater, migraterErr := migrate.New(*sourcePtr, *databasePtr)
|
||||
defer func() {
|
||||
if migraterErr == nil {
|
||||
migrater.Close()
|
||||
}
|
||||
}()
|
||||
if migraterErr == nil {
|
||||
migrater.Log = log
|
||||
migrater.PrefetchMigrations = *prefetchPtr
|
||||
|
||||
// handle Ctrl+c
|
||||
signals := make(chan os.Signal, 1)
|
||||
signal.Notify(signals, syscall.SIGINT)
|
||||
go func() {
|
||||
for range signals {
|
||||
log.Println("Stopping after this running migration ...")
|
||||
migrater.GracefulStop <- true
|
||||
return
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
switch flag.Arg(0) {
|
||||
case "goto":
|
||||
if migraterErr != nil {
|
||||
log.fatalErr(migraterErr)
|
||||
}
|
||||
|
||||
if flag.Arg(1) == "" {
|
||||
log.fatal("error: please specify version argument V")
|
||||
}
|
||||
|
||||
v, err := strconv.ParseUint(flag.Arg(1), 10, 64)
|
||||
if err != nil {
|
||||
log.fatal("error: can't read version argument V")
|
||||
}
|
||||
|
||||
gotoCmd(migrater, uint(v))
|
||||
|
||||
if log.verbose {
|
||||
log.Println("Finished after", time.Now().Sub(startTime))
|
||||
}
|
||||
|
||||
case "up":
|
||||
if migraterErr != nil {
|
||||
log.fatalErr(migraterErr)
|
||||
}
|
||||
|
||||
limit := -1
|
||||
if flag.Arg(1) != "" {
|
||||
n, err := strconv.ParseUint(flag.Arg(1), 10, 64)
|
||||
if err != nil {
|
||||
log.fatal("error: can't read limit argument N")
|
||||
}
|
||||
limit = int(n)
|
||||
}
|
||||
|
||||
upCmd(migrater, limit)
|
||||
|
||||
if log.verbose {
|
||||
log.Println("Finished after", time.Now().Sub(startTime))
|
||||
}
|
||||
|
||||
case "down":
|
||||
if migraterErr != nil {
|
||||
log.fatalErr(migraterErr)
|
||||
}
|
||||
|
||||
limit := -1
|
||||
if flag.Arg(1) != "" {
|
||||
n, err := strconv.ParseUint(flag.Arg(1), 10, 64)
|
||||
if err != nil {
|
||||
log.fatal("error: can't read limit argument N")
|
||||
}
|
||||
limit = int(n)
|
||||
}
|
||||
|
||||
downCmd(migrater, limit)
|
||||
|
||||
if log.verbose {
|
||||
log.Println("Finished after", time.Now().Sub(startTime))
|
||||
}
|
||||
|
||||
case "drop":
|
||||
if migraterErr != nil {
|
||||
log.fatalErr(migraterErr)
|
||||
}
|
||||
|
||||
dropCmd(migrater)
|
||||
|
||||
if log.verbose {
|
||||
log.Println("Finished after", time.Now().Sub(startTime))
|
||||
}
|
||||
|
||||
case "version":
|
||||
if migraterErr != nil {
|
||||
log.fatalErr(migraterErr)
|
||||
}
|
||||
|
||||
versionCmd(migrater)
|
||||
|
||||
default:
|
||||
flag.Usage()
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package main
|
||||
|
||||
// Version is set in Makefile with build flags
|
||||
var Version = "dev"
|
||||
@@ -0,0 +1,70 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
nurl "net/url"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrLocked = fmt.Errorf("unable to acquire lock")
|
||||
)
|
||||
|
||||
const NilVersion int = -1
|
||||
|
||||
var driversMu sync.RWMutex
|
||||
var drivers = make(map[string]Driver)
|
||||
|
||||
type Driver interface {
|
||||
Open(url string) (Driver, error)
|
||||
|
||||
Close() error
|
||||
|
||||
Lock() error
|
||||
|
||||
Unlock() error
|
||||
|
||||
// when version = NilVersion, "deinitialize"
|
||||
// migration can be nil, in that case, just store version
|
||||
Run(version int, migration io.Reader) error
|
||||
|
||||
// version > 0: regular version
|
||||
// version -1: nil version (const NilVersion)
|
||||
// version < -1: will panic
|
||||
Version() (int, error)
|
||||
|
||||
Drop() error
|
||||
}
|
||||
|
||||
func Open(url string) (Driver, error) {
|
||||
u, err := nurl.Parse(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if u.Scheme == "" {
|
||||
return nil, fmt.Errorf("database driver: invalid URL scheme")
|
||||
}
|
||||
|
||||
driversMu.RLock()
|
||||
d, ok := drivers[u.Scheme]
|
||||
driversMu.RUnlock()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("database driver: unknown driver %v (forgotton import?)", u.Scheme)
|
||||
}
|
||||
|
||||
return d.Open(url)
|
||||
}
|
||||
|
||||
func Register(name string, driver Driver) {
|
||||
driversMu.Lock()
|
||||
defer driversMu.Unlock()
|
||||
if driver == nil {
|
||||
panic("Register driver is nil")
|
||||
}
|
||||
if _, dup := drivers[name]; dup {
|
||||
panic("Register called twice for driver " + name)
|
||||
}
|
||||
drivers[name] = driver
|
||||
}
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
mysql
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS users;
|
||||
@@ -0,0 +1,5 @@
|
||||
CREATE TABLE users (
|
||||
user_id integer unique,
|
||||
name varchar(40),
|
||||
email varchar(40)
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE users DROP COLUMN IF EXISTS city;
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE users ADD COLUMN city varchar(100);
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DROP INDEX IF EXISTS users_email_index;
|
||||
@@ -0,0 +1,3 @@
|
||||
CREATE UNIQUE INDEX CONCURRENTLY users_email_index ON users (email);
|
||||
|
||||
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS books;
|
||||
@@ -0,0 +1,5 @@
|
||||
CREATE TABLE books (
|
||||
user_id integer,
|
||||
name varchar(40),
|
||||
author varchar(40)
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS movies;
|
||||
@@ -0,0 +1,5 @@
|
||||
CREATE TABLE movies (
|
||||
user_id integer,
|
||||
name varchar(40),
|
||||
director varchar(40)
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
|
||||
@@ -0,0 +1 @@
|
||||
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
|
||||
@@ -0,0 +1 @@
|
||||
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
|
||||
@@ -0,0 +1 @@
|
||||
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
|
||||
@@ -0,0 +1,224 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
nurl "net/url"
|
||||
|
||||
"github.com/lib/pq"
|
||||
"github.com/mattes/migrate/database"
|
||||
)
|
||||
|
||||
func init() {
|
||||
database.Register("postgres", &Postgres{})
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
}
|
||||
|
||||
func WithInstance(instance *sql.DB, config *Config) (database.Driver, error) {
|
||||
return &Postgres{
|
||||
db: instance,
|
||||
config: config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Postgres struct {
|
||||
db *sql.DB
|
||||
url *nurl.URL
|
||||
isLocked bool
|
||||
config *Config
|
||||
}
|
||||
|
||||
var (
|
||||
ErrNoSqlInstance = fmt.Errorf("expected *sql.DB")
|
||||
ErrNoDatabaseName = fmt.Errorf("no database name")
|
||||
)
|
||||
|
||||
const tableName = "schema_migrations"
|
||||
|
||||
func (p *Postgres) Open(url string) (database.Driver, error) {
|
||||
purl, err := nurl.Parse(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db, err := sql.Open("postgres", url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := db.Ping(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
px := &Postgres{
|
||||
db: db,
|
||||
url: purl,
|
||||
}
|
||||
if err := px.ensureVersionTable(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return px, nil
|
||||
}
|
||||
|
||||
func (p *Postgres) Close() error {
|
||||
return p.db.Close()
|
||||
}
|
||||
|
||||
// https://www.postgresql.org/docs/9.6/static/explicit-locking.html#ADVISORY-LOCKS
|
||||
func (p *Postgres) Lock() error {
|
||||
if p.isLocked {
|
||||
return database.ErrLocked
|
||||
}
|
||||
|
||||
aid, err := p.generateAdvisoryLockId()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// It will either obtain the lock immediately and return true, or return false if the lock cannot be acquired immediately.
|
||||
var success bool
|
||||
if err := p.db.QueryRow("SELECT pg_try_advisory_lock($1)", aid).Scan(&success); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if success {
|
||||
p.isLocked = true
|
||||
return nil
|
||||
}
|
||||
|
||||
return database.ErrLocked
|
||||
}
|
||||
|
||||
func (p *Postgres) Unlock() error {
|
||||
if !p.isLocked {
|
||||
return nil
|
||||
}
|
||||
|
||||
aid, err := p.generateAdvisoryLockId()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := p.db.Exec("SELECT pg_advisory_unlock($1)", aid); err != nil {
|
||||
return err
|
||||
}
|
||||
p.isLocked = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Postgres) Run(version int, migration io.Reader) error {
|
||||
if migration == nil {
|
||||
// just apply version
|
||||
return p.saveVersion(version)
|
||||
}
|
||||
|
||||
mgr, err := ioutil.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// it would be nice to be able to wrap the migration into the transaction, too
|
||||
// unfortunately things like `CREATE INDEX CONCURRENTLY` aren't possible in a
|
||||
// transaction. so if something fails between running the migration, and
|
||||
// storing the latest migration version in the version table, we alert the user
|
||||
// who then needs to manually fix.
|
||||
// TODO: two phase commit?
|
||||
if _, err := p.db.Exec(string(mgr[:])); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return p.saveVersion(version)
|
||||
}
|
||||
|
||||
func (p *Postgres) saveVersion(version int) error {
|
||||
tx, err := p.db.Begin()
|
||||
if err != nil {
|
||||
return err // TODO: warn user
|
||||
}
|
||||
|
||||
if _, err := p.db.Exec("TRUNCATE " + tableName + ""); err != nil {
|
||||
tx.Rollback()
|
||||
return err // TODO: warn user
|
||||
}
|
||||
|
||||
if version >= 0 {
|
||||
if _, err := p.db.Exec("INSERT INTO "+tableName+" (version) VALUES ($1)", version); err != nil {
|
||||
tx.Rollback()
|
||||
return err // TODO: warn user
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err // TODO: warn user
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Postgres) Version() (int, error) {
|
||||
var version uint64
|
||||
err := p.db.QueryRow("SELECT version FROM " + tableName + " ORDER BY version DESC LIMIT 1").Scan(&version)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return database.NilVersion, nil
|
||||
case err != nil:
|
||||
if e, ok := err.(*pq.Error); ok {
|
||||
if e.Code.Name() == "undefined_table" {
|
||||
return database.NilVersion, nil
|
||||
}
|
||||
}
|
||||
return 0, err
|
||||
default:
|
||||
return int(version), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Postgres) Drop() error {
|
||||
if _, err := p.db.Exec("DROP SCHEMA public cascade "); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := p.db.Exec("CREATE SCHEMA public"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := p.ensureVersionTable(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Postgres) ensureVersionTable() error {
|
||||
r := p.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 := p.db.Exec("CREATE TABLE IF NOT EXISTS " + tableName + " (version bigint not null primary key);"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const AdvisoryLockIdSalt uint = 1486364155
|
||||
|
||||
// inspired by rails migrations, see https://goo.gl/8o9bCT
|
||||
func (p *Postgres) generateAdvisoryLockId() (string, error) {
|
||||
if p.url == nil {
|
||||
return "", ErrNoDatabaseName
|
||||
}
|
||||
dbname := p.url.Path
|
||||
if len(dbname) == 0 {
|
||||
return "", ErrNoDatabaseName
|
||||
}
|
||||
sum := crc32.ChecksumIEEE([]byte(dbname))
|
||||
sum = sum * uint32(AdvisoryLockIdSalt)
|
||||
return fmt.Sprintf("%v", sum), nil
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package postgres
|
||||
|
||||
// error codes https://github.com/lib/pq/blob/master/error.go
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io"
|
||||
nurl "net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/lib/pq"
|
||||
dt "github.com/mattes/migrate/database/testing"
|
||||
mt "github.com/mattes/migrate/testing"
|
||||
)
|
||||
|
||||
var versions = []string{
|
||||
"postgres:9.6",
|
||||
"postgres:9.5",
|
||||
"postgres:9.4",
|
||||
"postgres:9.3",
|
||||
"postgres:9.2",
|
||||
}
|
||||
|
||||
func isReady(i mt.Instance) bool {
|
||||
db, err := sql.Open("postgres", fmt.Sprintf("postgres://postgres@%v:%v/postgres?sslmode=disable", i.Host(), i.Port()))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer db.Close()
|
||||
err = db.Ping()
|
||||
if err == io.EOF {
|
||||
return false
|
||||
|
||||
} else if e, ok := err.(*pq.Error); ok {
|
||||
if e.Code.Name() == "cannot_connect_now" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
mt.ParallelTest(t, versions, isReady,
|
||||
func(t *testing.T, i mt.Instance) {
|
||||
p := &Postgres{}
|
||||
d, err := p.Open(fmt.Sprintf("postgres://postgres@%v:%v/postgres?sslmode=disable", i.Host(), i.Port()))
|
||||
if err != nil {
|
||||
t.Fatalf("%#v", err)
|
||||
}
|
||||
dt.Test(t, d, []byte("SELECT 1"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithSchema(t *testing.T) {
|
||||
mt.ParallelTest(t, versions, isReady,
|
||||
func(t *testing.T, i mt.Instance) {
|
||||
p := &Postgres{}
|
||||
d, err := p.Open(fmt.Sprintf("postgres://postgres@%v:%v/postgres?sslmode=disable", i.Host(), i.Port()))
|
||||
if err != nil {
|
||||
t.Fatalf("%#v", err)
|
||||
}
|
||||
|
||||
// create foobar schema
|
||||
if err := d.Run(100, bytes.NewReader([]byte("CREATE SCHEMA foobar AUTHORIZATION postgres"))); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// re-connect using that schema
|
||||
d2, err := p.Open(fmt.Sprintf("postgres://postgres@%v:%v/postgres?sslmode=disable&search_path=foobar", i.Host(), i.Port()))
|
||||
if err != nil {
|
||||
t.Fatalf("%#v", err)
|
||||
}
|
||||
|
||||
version, err := d2.Version()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if version != -1 {
|
||||
t.Fatal("expected NilVersion")
|
||||
}
|
||||
|
||||
// now update version and compare
|
||||
if err := d2.Run(2, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
version, err = d2.Version()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if version != 2 {
|
||||
t.Fatal("expected version 2")
|
||||
}
|
||||
|
||||
// meanwhile, the public schema still has the other version
|
||||
version, err = d.Version()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if version != 100 {
|
||||
t.Fatal("expected version 2")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithInstance(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func TestGenerateAdvisoryLockId(t *testing.T) {
|
||||
p := &Postgres{}
|
||||
|
||||
if _, err := p.generateAdvisoryLockId(); err == nil {
|
||||
t.Errorf("expected err not to be nil")
|
||||
}
|
||||
|
||||
p.url = &nurl.URL{Path: "database_name"}
|
||||
id, err := p.generateAdvisoryLockId()
|
||||
if err != nil {
|
||||
t.Errorf("expected err to be nil, got %v", err)
|
||||
}
|
||||
if len(id) == 0 {
|
||||
t.Errorf("expected generated id not to be empty")
|
||||
}
|
||||
t.Logf("generated id: %v", id)
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package stub
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"reflect"
|
||||
|
||||
"github.com/mattes/migrate/database"
|
||||
)
|
||||
|
||||
func init() {
|
||||
database.Register("stub", &Stub{})
|
||||
}
|
||||
|
||||
type Stub struct {
|
||||
Url string
|
||||
Instance interface{}
|
||||
CurrentVersion int
|
||||
MigrationSequence []string
|
||||
LastRunMigration []byte // todo: make []string
|
||||
IsLocked bool
|
||||
|
||||
Config *Config
|
||||
}
|
||||
|
||||
func (s *Stub) Open(url string) (database.Driver, error) {
|
||||
return &Stub{
|
||||
Url: url,
|
||||
CurrentVersion: -1,
|
||||
MigrationSequence: make([]string, 0),
|
||||
Config: &Config{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Config struct{}
|
||||
|
||||
func WithInstance(instance interface{}, config *Config) (database.Driver, error) {
|
||||
return &Stub{
|
||||
Instance: instance,
|
||||
CurrentVersion: -1,
|
||||
MigrationSequence: make([]string, 0),
|
||||
Config: config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Stub) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Stub) Lock() error {
|
||||
if s.IsLocked {
|
||||
return database.ErrLocked
|
||||
}
|
||||
s.IsLocked = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Stub) Unlock() error {
|
||||
s.IsLocked = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Stub) Run(version int, migration io.Reader) error {
|
||||
s.CurrentVersion = version
|
||||
|
||||
if migration != nil {
|
||||
m, err := ioutil.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.LastRunMigration = m
|
||||
s.MigrationSequence = append(s.MigrationSequence, string(m[:]))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Stub) Version() (int, error) {
|
||||
if s.CurrentVersion < 0 {
|
||||
return database.NilVersion, nil
|
||||
}
|
||||
return s.CurrentVersion, nil
|
||||
}
|
||||
|
||||
const DROP = "DROP"
|
||||
|
||||
func (s *Stub) Drop() error {
|
||||
s.CurrentVersion = -1
|
||||
s.LastRunMigration = nil
|
||||
s.MigrationSequence = append(s.MigrationSequence, DROP)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Stub) EqualSequence(seq []string) bool {
|
||||
return reflect.DeepEqual(seq, s.MigrationSequence)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package stub
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
dt "github.com/mattes/migrate/database/testing"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
s := &Stub{}
|
||||
d, err := s.Open("")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dt.Test(t, d, []byte("/* foobar migration */"))
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package testing
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/mattes/migrate/database"
|
||||
)
|
||||
|
||||
func Test(t *testing.T, d database.Driver, migration []byte) {
|
||||
if migration == nil {
|
||||
panic("test must provide migration reader")
|
||||
}
|
||||
|
||||
TestNilVersion(t, d) // test first
|
||||
TestLockAndUnlock(t, d)
|
||||
TestRun(t, d, bytes.NewReader(migration)) // also tests Drop()
|
||||
TestRunWithNilVersion(t, d, bytes.NewReader(migration))
|
||||
TestRunWithNilMigration(t, d)
|
||||
}
|
||||
|
||||
func TestNilVersion(t *testing.T, d database.Driver) {
|
||||
v, err := d.Version()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v != database.NilVersion {
|
||||
t.Fatalf("Version: expected version to be NilVersion (-1), got %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLockAndUnlock(t *testing.T, d database.Driver) {
|
||||
// TODO: add timeouts, in case something goes wrong
|
||||
if err := d.Lock(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// try to acquire lock again
|
||||
if err := d.Lock(); err == nil {
|
||||
t.Fatal("Lock: expected err not to be nil")
|
||||
}
|
||||
|
||||
// unlock
|
||||
if err := d.Unlock(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// try to lock
|
||||
if err := d.Lock(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := d.Unlock(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun(t *testing.T, d database.Driver, migration io.Reader) {
|
||||
// Run migration
|
||||
err := d.Run(1485475009, migration)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check version
|
||||
version, err := d.Version()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if version != 1485475009 {
|
||||
t.Fatalf("Version: expected 1485475009, got %v", version)
|
||||
}
|
||||
|
||||
// Drop everything
|
||||
if err := d.Drop(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check version again
|
||||
if v, err := d.Version(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if v != database.NilVersion {
|
||||
t.Fatalf("Version: expected version to be NilVersion (-1), got %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunWithNilVersion(t *testing.T, d database.Driver, migration io.Reader) {
|
||||
// Run migration
|
||||
err := d.Run(database.NilVersion, migration)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check version
|
||||
version, err := d.Version()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if version != database.NilVersion {
|
||||
t.Fatalf("Version: expected database.NilVersion (-1), got %v", version)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunWithNilMigration(t *testing.T, d database.Driver) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatal("got panic, make sure to handle nil migration io.Reader")
|
||||
}
|
||||
}()
|
||||
|
||||
// Run with nil migration
|
||||
err := d.Run(1486242612, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check version
|
||||
version, err := d.Version()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if version != 1486242612 {
|
||||
t.Fatalf("TestRunWithNilMigration: expected version 1486242612, got %v", version)
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
go: &go
|
||||
image: golang
|
||||
working_dir: /go/src/github.com/mattes/migrate
|
||||
volumes:
|
||||
- $GOPATH:/go
|
||||
go-test:
|
||||
<<: *go
|
||||
command: sh -c 'go get -t -v ./... && go test -v ./...'
|
||||
links:
|
||||
- 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
|
||||
@@ -1,11 +0,0 @@
|
||||
# Bash Driver
|
||||
|
||||
* Runs bash scripts. What you do in the scripts is up to you.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
migrate -url bash://xxx -path ./migrations create increment_xyz
|
||||
migrate -url bash://xxx -path ./migrations up
|
||||
migrate help # for more info
|
||||
```
|
||||
@@ -1,36 +0,0 @@
|
||||
// Package bash implements the Driver interface.
|
||||
package bash
|
||||
|
||||
import (
|
||||
"github.com/mattes/migrate/driver"
|
||||
"github.com/mattes/migrate/file"
|
||||
)
|
||||
|
||||
type Driver struct {
|
||||
}
|
||||
|
||||
func (driver *Driver) Initialize(url string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver *Driver) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver *Driver) FilenameExtension() string {
|
||||
return "sh"
|
||||
}
|
||||
|
||||
func (driver *Driver) Migrate(f file.File, pipe chan interface{}) {
|
||||
defer close(pipe)
|
||||
pipe <- f
|
||||
return
|
||||
}
|
||||
|
||||
func (driver *Driver) Version() (uint64, error) {
|
||||
return uint64(0), nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
driver.RegisterDriver("bash", &Driver{})
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package bash
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMigrate(t *testing.T) {
|
||||
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
# Cassandra Driver
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
migrate -url cassandra://host:port/keyspace -path ./db/migrations create add_field_to_table
|
||||
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
|
||||
* Johnny Bergström, https://github.com/balboah
|
||||
* pateld982, http://github.com/pateld982
|
||||
@@ -1,214 +0,0 @@
|
||||
// Package cassandra implements the Driver interface.
|
||||
package cassandra
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"github.com/mattes/migrate/driver"
|
||||
"github.com/mattes/migrate/file"
|
||||
"github.com/mattes/migrate/migrate/direction"
|
||||
)
|
||||
|
||||
type Driver struct {
|
||||
session *gocql.Session
|
||||
}
|
||||
|
||||
const (
|
||||
tableName = "schema_migrations"
|
||||
versionRow = 1
|
||||
)
|
||||
|
||||
type counterStmt bool
|
||||
|
||||
func (c counterStmt) Exec(session *gocql.Session) error {
|
||||
var version int64
|
||||
if err := session.Query("SELECT version FROM "+tableName+" WHERE versionRow = ?", versionRow).Scan(&version); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if bool(c) {
|
||||
version++
|
||||
} else {
|
||||
version--
|
||||
}
|
||||
|
||||
return session.Query("UPDATE "+tableName+" SET version = ? WHERE versionRow = ?", version, versionRow).Exec()
|
||||
}
|
||||
|
||||
const (
|
||||
up counterStmt = true
|
||||
down counterStmt = false
|
||||
)
|
||||
|
||||
// Cassandra Driver URL format:
|
||||
// cassandra://host:port/keyspace?protocol=version&consistency=level
|
||||
//
|
||||
// Examples:
|
||||
// cassandra://localhost/SpaceOfKeys?protocol=4
|
||||
// cassandra://localhost/SpaceOfKeys?protocol=4&consistency=all
|
||||
// cassandra://localhost/SpaceOfKeys?consistency=quorum
|
||||
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 {
|
||||
consistency, err := parseConsistency(u.Query().Get("consistency"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cluster.Consistency = consistency
|
||||
}
|
||||
|
||||
if len(u.Query().Get("protocol")) > 0 {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := driver.ensureVersionTableExists(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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 int, versionRow bigint primary key);").Exec()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = driver.Version()
|
||||
if err != nil {
|
||||
if err.Error() == "not found" {
|
||||
return driver.session.Query("UPDATE "+tableName+" SET version = ? WHERE versionRow = ?", 1, versionRow).Exec()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver *Driver) FilenameExtension() string {
|
||||
return "cql"
|
||||
}
|
||||
|
||||
func (driver *Driver) version(d direction.Direction, invert bool) error {
|
||||
var stmt counterStmt
|
||||
switch d {
|
||||
case direction.Up:
|
||||
stmt = up
|
||||
case direction.Down:
|
||||
stmt = down
|
||||
}
|
||||
if invert {
|
||||
stmt = !stmt
|
||||
}
|
||||
return stmt.Exec(driver.session)
|
||||
}
|
||||
|
||||
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.version(f.Direction, true); err != nil {
|
||||
pipe <- err
|
||||
}
|
||||
pipe <- err
|
||||
}
|
||||
close(pipe)
|
||||
}()
|
||||
|
||||
pipe <- f
|
||||
if err = driver.version(f.Direction, false); 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
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"
|
||||
)
|
||||
|
||||
func TestMigrate(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode.")
|
||||
}
|
||||
|
||||
var session *gocql.Session
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cluster := gocql.NewCluster(u.Host)
|
||||
cluster.Keyspace = u.Path[1:len(u.Path)]
|
||||
cluster.Consistency = gocql.All
|
||||
cluster.Timeout = 1 * time.Minute
|
||||
|
||||
session, err = cluster.CreateSession()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := session.Query(`DROP KEYSPACE IF EXISTS migrate;`).Exec(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
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()
|
||||
driverUrl = "cassandra://" + host + ":" + port + "/migrate"
|
||||
|
||||
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 varint primary key,
|
||||
msg text
|
||||
);
|
||||
|
||||
`),
|
||||
},
|
||||
{
|
||||
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
|
||||
)
|
||||
`),
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
# 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
|
||||
```
|
||||
@@ -1,115 +0,0 @@
|
||||
// Package crate implements a driver for the Crate.io database
|
||||
package crate
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
_ "github.com/herenow/go-crate"
|
||||
"github.com/mattes/migrate/driver"
|
||||
"github.com/mattes/migrate/file"
|
||||
"github.com/mattes/migrate/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, line := 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
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
package crate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mattes/migrate/file"
|
||||
"github.com/mattes/migrate/migrate/direction"
|
||||
pipep "github.com/mattes/migrate/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)
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
// Package driver holds the driver interface.
|
||||
package driver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
neturl "net/url" // alias to allow `url string` func signature in New
|
||||
|
||||
"github.com/mattes/migrate/file"
|
||||
)
|
||||
|
||||
// Driver is the interface type that needs to implemented by all drivers.
|
||||
type Driver interface {
|
||||
|
||||
// Initialize is the first function to be called.
|
||||
// Check the url string and open and verify any connection
|
||||
// that has to be made.
|
||||
Initialize(url string) error
|
||||
|
||||
// Close is the last function to be called.
|
||||
// Close any open connection here.
|
||||
Close() error
|
||||
|
||||
// FilenameExtension returns the extension of the migration files.
|
||||
// The returned string must not begin with a dot.
|
||||
FilenameExtension() string
|
||||
|
||||
// Migrate is the heart of the driver.
|
||||
// It will receive a file which the driver should apply
|
||||
// to its backend or whatever. The migration function should use
|
||||
// the pipe channel to return any errors or other useful information.
|
||||
Migrate(file file.File, pipe chan interface{})
|
||||
|
||||
// Version returns the current migration version.
|
||||
Version() (uint64, error)
|
||||
}
|
||||
|
||||
// New returns Driver and calls Initialize on it
|
||||
func New(url string) (Driver, error) {
|
||||
u, err := neturl.Parse(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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 driver's filename extension
|
||||
// is not correct or empty.
|
||||
func verifyFilenameExtension(driverName string, d Driver) {
|
||||
f := d.FilenameExtension()
|
||||
if f == "" {
|
||||
panic(fmt.Sprintf("%s.FilenameExtension() returns empty string.", driverName))
|
||||
}
|
||||
if f[0:1] == "." {
|
||||
panic(fmt.Sprintf("%s.FilenameExtension() returned string must not start with a dot.", driverName))
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
# MongoDB Driver
|
||||
|
||||
* Runs pre-registered Golang methods that receive a single `*mgo.Session` parameter and return `error` on failure.
|
||||
* Stores migration version details in collection ``db_migrations``.
|
||||
This collection will be auto-generated.
|
||||
* Migrations do not run in transactions, there are no built-in transactions in MongoDB.
|
||||
That means that if a migration fails, it will not be rolled back.
|
||||
* There is no out-of-the-box support for command-line interface via terminal.
|
||||
|
||||
## Usage in Go
|
||||
|
||||
```go
|
||||
import "github.com/mattes/migrate/migrate"
|
||||
|
||||
// Import your migration methods package so that they are registered and available for the MongoDB driver.
|
||||
// There is no need to import the MongoDB driver explicitly, as it should already be imported by your migration methods package.
|
||||
import _ "my_mongo_db_migrator"
|
||||
|
||||
// use synchronous versions of migration functions ...
|
||||
allErrors, ok := migrate.UpSync("mongodb://host:port", "./path")
|
||||
if !ok {
|
||||
fmt.Println("Oh no ...")
|
||||
// do sth with allErrors slice
|
||||
}
|
||||
|
||||
// use the asynchronous version of migration functions ...
|
||||
pipe := migrate.NewPipe()
|
||||
go migrate.Up(pipe, "mongodb://host:port", "./path")
|
||||
// pipe is basically just a channel
|
||||
// write your own channel listener. see writePipe() in main.go as an example.
|
||||
```
|
||||
|
||||
## Migration files format
|
||||
|
||||
The migration files should have an ".mgo" extension and contain a list of registered methods names.
|
||||
|
||||
Migration methods should satisfy the following:
|
||||
* They should be exported (their name should start with a capital letter)
|
||||
* Their type should be `func (*mgo.Session) error`
|
||||
|
||||
Recommended (but not required) naming conventions for migration methods:
|
||||
* Prefix with V<version> : for example V001 for version 1.
|
||||
* Suffix with "_up" or "_down" for up and down migrations correspondingly.
|
||||
|
||||
001_first_release.up.mgo
|
||||
```
|
||||
V001_some_migration_operation_up
|
||||
V001_some_other_operation_up
|
||||
...
|
||||
```
|
||||
|
||||
001_first_release.down.mgo
|
||||
```
|
||||
V001_some_other_operation_down
|
||||
V001_some_migration_operation_down
|
||||
...
|
||||
```
|
||||
|
||||
|
||||
## Methods registration
|
||||
|
||||
For a detailed example see: [sample_mongodb_migrator.go](https://github.com/mattes/migrate/blob/master/driver/mongodb/example/sample_mongdb_migrator.go)
|
||||
|
||||
```go
|
||||
package my_mongo_db_migrator
|
||||
|
||||
import (
|
||||
"github.com/mattes/migrate/driver/mongodb"
|
||||
"github.com/mattes/migrate/driver/mongodb/gomethods"
|
||||
"gopkg.in/mgo.v2"
|
||||
)
|
||||
|
||||
// common boilerplate
|
||||
type MyMongoDbMigrator struct {
|
||||
}
|
||||
|
||||
func (r *MyMongoDbMigrator) DbName() string {
|
||||
return "<target_db_name_for_migration>"
|
||||
}
|
||||
|
||||
var _ mongodb.MethodsReceiver = (*MyMongoDbMigrator)(nil)
|
||||
|
||||
func init() {
|
||||
gomethods.RegisterMethodsReceiverForDriver("mongodb", &MyMongoDbMigrator{})
|
||||
}
|
||||
|
||||
|
||||
// Here goes the application-specific migration logic
|
||||
func (r *MyMongoDbMigrator) V001_some_migration_operation_up(session *mgo.Session) error {
|
||||
// do something
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *MyMongoDbMigrator) V001_some_migration_operation_down(session *mgo.Session) error {
|
||||
// revert some_migration_operation_up from above
|
||||
return nil
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Authors
|
||||
|
||||
* Demitry Gershovich, https://github.com/dimag-jfrog
|
||||
|
||||
@@ -1,310 +0,0 @@
|
||||
package example
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattes/migrate/file"
|
||||
"github.com/mattes/migrate/migrate/direction"
|
||||
|
||||
"github.com/mattes/migrate/driver"
|
||||
"github.com/mattes/migrate/driver/mongodb"
|
||||
"github.com/mattes/migrate/driver/mongodb/gomethods"
|
||||
pipep "github.com/mattes/migrate/pipe"
|
||||
"os"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ExpectedMigrationResult struct {
|
||||
Organizations []Organization
|
||||
Organizations_v2 []Organization_v2
|
||||
Users []User
|
||||
Errors []error
|
||||
}
|
||||
|
||||
func RunMigrationAndAssertResult(
|
||||
t *testing.T,
|
||||
title string,
|
||||
d *mongodb.Driver,
|
||||
file file.File,
|
||||
expected *ExpectedMigrationResult) {
|
||||
|
||||
actualOrganizations := []Organization{}
|
||||
actualOrganizations_v2 := []Organization_v2{}
|
||||
actualUsers := []User{}
|
||||
var err error
|
||||
var pipe chan interface{}
|
||||
var errs []error
|
||||
|
||||
pipe = pipep.New()
|
||||
go d.Migrate(file, pipe)
|
||||
errs = pipep.ReadErrors(pipe)
|
||||
|
||||
session := d.Session
|
||||
if len(expected.Organizations) > 0 {
|
||||
err = session.DB(DB_NAME).C(ORGANIZATIONS_C).Find(nil).All(&actualOrganizations)
|
||||
} else {
|
||||
err = session.DB(DB_NAME).C(ORGANIZATIONS_C).Find(nil).All(&actualOrganizations_v2)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal("Failed to query Organizations collection")
|
||||
}
|
||||
|
||||
err = session.DB(DB_NAME).C(USERS_C).Find(nil).All(&actualUsers)
|
||||
if err != nil {
|
||||
t.Fatal("Failed to query Users collection")
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expected.Errors, errs) {
|
||||
t.Fatalf("Migration '%s': FAILED\nexpected errors %v\nbut got %v", title, expected.Errors, errs)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expected.Organizations, actualOrganizations) {
|
||||
t.Fatalf("Migration '%s': FAILED\nexpected organizations %v\nbut got %v", title, expected.Organizations, actualOrganizations)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expected.Organizations_v2, actualOrganizations_v2) {
|
||||
t.Fatalf("Migration '%s': FAILED\nexpected organizations v2 %v\nbut got %v", title, expected.Organizations_v2, actualOrganizations_v2)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expected.Users, actualUsers) {
|
||||
t.Fatalf("Migration '%s': FAILED\nexpected users %v\nbut got %v", title, expected.Users, actualUsers)
|
||||
|
||||
}
|
||||
// t.Logf("Migration '%s': PASSED", title)
|
||||
}
|
||||
|
||||
func TestMigrate(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatalf("Test failed on panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
host := os.Getenv("MONGO_PORT_27017_TCP_ADDR")
|
||||
port := os.Getenv("MONGO_PORT_27017_TCP_PORT")
|
||||
|
||||
driverUrl := "mongodb://" + host + ":" + port
|
||||
|
||||
d0 := driver.GetDriver("mongodb")
|
||||
d, ok := d0.(*mongodb.Driver)
|
||||
if !ok {
|
||||
t.Fatal("MongoDbGoMethodsDriver has not registered")
|
||||
}
|
||||
|
||||
if err := d.Initialize(driverUrl); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Reset DB
|
||||
d.Session.DB(DB_NAME).C(mongodb.MIGRATE_C).DropCollection()
|
||||
d.Session.DB(DB_NAME).C(ORGANIZATIONS_C).DropCollection()
|
||||
d.Session.DB(DB_NAME).C(USERS_C).DropCollection()
|
||||
|
||||
date1, _ := time.Parse(SHORT_DATE_LAYOUT, "1994-Jul-05")
|
||||
date2, _ := time.Parse(SHORT_DATE_LAYOUT, "1998-Sep-04")
|
||||
date3, _ := time.Parse(SHORT_DATE_LAYOUT, "2008-Apr-28")
|
||||
|
||||
migrations := []struct {
|
||||
name string
|
||||
file file.File
|
||||
expectedResult ExpectedMigrationResult
|
||||
}{
|
||||
{
|
||||
name: "v0 -> v1",
|
||||
file: file.File{
|
||||
Path: "/foobar",
|
||||
FileName: "001_foobar.up.gm",
|
||||
Version: 1,
|
||||
Name: "foobar",
|
||||
Direction: direction.Up,
|
||||
Content: []byte(`
|
||||
V001_init_organizations_up
|
||||
V001_init_users_up
|
||||
`),
|
||||
},
|
||||
expectedResult: ExpectedMigrationResult{
|
||||
Organizations: []Organization{
|
||||
{Id: OrganizationIds[0], Name: "Amazon", Location: "Seattle", DateFounded: date1},
|
||||
{Id: OrganizationIds[1], Name: "Google", Location: "Mountain View", DateFounded: date2},
|
||||
{Id: OrganizationIds[2], Name: "JFrog", Location: "Santa Clara", DateFounded: date3},
|
||||
},
|
||||
Organizations_v2: []Organization_v2{},
|
||||
Users: []User{
|
||||
{Id: UserIds[0], Name: "Alex"},
|
||||
{Id: UserIds[1], Name: "Beatrice"},
|
||||
{Id: UserIds[2], Name: "Cleo"},
|
||||
},
|
||||
Errors: []error{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "v1 -> v2",
|
||||
file: file.File{
|
||||
Path: "/foobar",
|
||||
FileName: "002_foobar.up.gm",
|
||||
Version: 2,
|
||||
Name: "foobar",
|
||||
Direction: direction.Up,
|
||||
Content: []byte(`
|
||||
V002_organizations_rename_location_field_to_headquarters_up
|
||||
V002_change_user_cleo_to_cleopatra_up
|
||||
`),
|
||||
},
|
||||
expectedResult: ExpectedMigrationResult{
|
||||
Organizations: []Organization{},
|
||||
Organizations_v2: []Organization_v2{
|
||||
{Id: OrganizationIds[0], Name: "Amazon", Headquarters: "Seattle", DateFounded: date1},
|
||||
{Id: OrganizationIds[1], Name: "Google", Headquarters: "Mountain View", DateFounded: date2},
|
||||
{Id: OrganizationIds[2], Name: "JFrog", Headquarters: "Santa Clara", DateFounded: date3},
|
||||
},
|
||||
Users: []User{
|
||||
{Id: UserIds[0], Name: "Alex"},
|
||||
{Id: UserIds[1], Name: "Beatrice"},
|
||||
{Id: UserIds[2], Name: "Cleopatra"},
|
||||
},
|
||||
Errors: []error{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "v2 -> v1",
|
||||
file: file.File{
|
||||
Path: "/foobar",
|
||||
FileName: "002_foobar.down.gm",
|
||||
Version: 2,
|
||||
Name: "foobar",
|
||||
Direction: direction.Down,
|
||||
Content: []byte(`
|
||||
V002_change_user_cleo_to_cleopatra_down
|
||||
V002_organizations_rename_location_field_to_headquarters_down
|
||||
`),
|
||||
},
|
||||
expectedResult: ExpectedMigrationResult{
|
||||
Organizations: []Organization{
|
||||
{Id: OrganizationIds[0], Name: "Amazon", Location: "Seattle", DateFounded: date1},
|
||||
{Id: OrganizationIds[1], Name: "Google", Location: "Mountain View", DateFounded: date2},
|
||||
{Id: OrganizationIds[2], Name: "JFrog", Location: "Santa Clara", DateFounded: date3},
|
||||
},
|
||||
Organizations_v2: []Organization_v2{},
|
||||
Users: []User{
|
||||
{Id: UserIds[0], Name: "Alex"},
|
||||
{Id: UserIds[1], Name: "Beatrice"},
|
||||
{Id: UserIds[2], Name: "Cleo"},
|
||||
},
|
||||
Errors: []error{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "v1 -> v0",
|
||||
file: file.File{
|
||||
Path: "/foobar",
|
||||
FileName: "001_foobar.down.gm",
|
||||
Version: 1,
|
||||
Name: "foobar",
|
||||
Direction: direction.Down,
|
||||
Content: []byte(`
|
||||
V001_init_users_down
|
||||
V001_init_organizations_down
|
||||
`),
|
||||
},
|
||||
expectedResult: ExpectedMigrationResult{
|
||||
Organizations: []Organization{},
|
||||
Organizations_v2: []Organization_v2{},
|
||||
Users: []User{},
|
||||
Errors: []error{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "v0 -> v1: missing method aborts migration",
|
||||
file: file.File{
|
||||
Path: "/foobar",
|
||||
FileName: "001_foobar.up.gm",
|
||||
Version: 1,
|
||||
Name: "foobar",
|
||||
Direction: direction.Up,
|
||||
Content: []byte(`
|
||||
V001_init_organizations_up
|
||||
V001_init_users_up
|
||||
v001_non_existing_method_up
|
||||
`),
|
||||
},
|
||||
expectedResult: ExpectedMigrationResult{
|
||||
Organizations: []Organization{},
|
||||
Organizations_v2: []Organization_v2{},
|
||||
Users: []User{},
|
||||
Errors: []error{gomethods.MethodNotFoundError("v001_non_existing_method_up")},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "v0 -> v1: not exported method aborts migration",
|
||||
file: file.File{
|
||||
Path: "/foobar",
|
||||
FileName: "001_foobar.up.gm",
|
||||
Version: 1,
|
||||
Name: "foobar",
|
||||
Direction: direction.Up,
|
||||
Content: []byte(`
|
||||
V001_init_organizations_up
|
||||
v001_not_exported_method_up
|
||||
V001_init_users_up
|
||||
`),
|
||||
},
|
||||
expectedResult: ExpectedMigrationResult{
|
||||
Organizations: []Organization{},
|
||||
Organizations_v2: []Organization_v2{},
|
||||
Users: []User{},
|
||||
Errors: []error{gomethods.MethodNotFoundError("v001_not_exported_method_up")},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "v0 -> v1: wrong signature method aborts migration",
|
||||
file: file.File{
|
||||
Path: "/foobar",
|
||||
FileName: "001_foobar.up.gm",
|
||||
Version: 1,
|
||||
Name: "foobar",
|
||||
Direction: direction.Up,
|
||||
Content: []byte(`
|
||||
V001_init_organizations_up
|
||||
V001_method_with_wrong_signature_up
|
||||
V001_init_users_up
|
||||
`),
|
||||
},
|
||||
expectedResult: ExpectedMigrationResult{
|
||||
Organizations: []Organization{},
|
||||
Organizations_v2: []Organization_v2{},
|
||||
Users: []User{},
|
||||
Errors: []error{gomethods.WrongMethodSignatureError("V001_method_with_wrong_signature_up")},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "v1 -> v0: wrong signature method aborts migration",
|
||||
file: file.File{
|
||||
Path: "/foobar",
|
||||
FileName: "001_foobar.down.gm",
|
||||
Version: 1,
|
||||
Name: "foobar",
|
||||
Direction: direction.Down,
|
||||
Content: []byte(`
|
||||
V001_init_users_down
|
||||
V001_method_with_wrong_signature_down
|
||||
V001_init_organizations_down
|
||||
`),
|
||||
},
|
||||
expectedResult: ExpectedMigrationResult{
|
||||
Organizations: []Organization{},
|
||||
Organizations_v2: []Organization_v2{},
|
||||
Users: []User{},
|
||||
Errors: []error{gomethods.WrongMethodSignatureError("V001_method_with_wrong_signature_down")},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, m := range migrations {
|
||||
RunMigrationAndAssertResult(t, m.name, d, m.file, &m.expectedResult)
|
||||
}
|
||||
|
||||
if err := d.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
package example
|
||||
|
||||
import (
|
||||
"github.com/mattes/migrate/driver/mongodb/gomethods"
|
||||
_ "github.com/mattes/migrate/driver/mongodb/gomethods"
|
||||
"gopkg.in/mgo.v2"
|
||||
"gopkg.in/mgo.v2/bson"
|
||||
"time"
|
||||
|
||||
"github.com/mattes/migrate/driver/mongodb"
|
||||
)
|
||||
|
||||
type SampleMongoDbMigrator struct {
|
||||
}
|
||||
|
||||
func (r *SampleMongoDbMigrator) DbName() string {
|
||||
return DB_NAME
|
||||
}
|
||||
|
||||
var _ mongodb.MethodsReceiver = (*SampleMongoDbMigrator)(nil)
|
||||
|
||||
func init() {
|
||||
gomethods.RegisterMethodsReceiverForDriver("mongodb", &SampleMongoDbMigrator{})
|
||||
}
|
||||
|
||||
// Here goes the specific mongodb golang methods migration logic
|
||||
|
||||
const (
|
||||
DB_NAME = "test"
|
||||
SHORT_DATE_LAYOUT = "2000-Jan-01"
|
||||
USERS_C = "users"
|
||||
ORGANIZATIONS_C = "organizations"
|
||||
)
|
||||
|
||||
type Organization struct {
|
||||
Id bson.ObjectId `bson:"_id,omitempty"`
|
||||
Name string `bson:"name"`
|
||||
Location string `bson:"location"`
|
||||
DateFounded time.Time `bson:"date_founded"`
|
||||
}
|
||||
|
||||
type Organization_v2 struct {
|
||||
Id bson.ObjectId `bson:"_id,omitempty"`
|
||||
Name string `bson:"name"`
|
||||
Headquarters string `bson:"headquarters"`
|
||||
DateFounded time.Time `bson:"date_founded"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Id bson.ObjectId `bson:"_id"`
|
||||
Name string `bson:"name"`
|
||||
}
|
||||
|
||||
var OrganizationIds []bson.ObjectId = []bson.ObjectId{
|
||||
bson.NewObjectId(),
|
||||
bson.NewObjectId(),
|
||||
bson.NewObjectId(),
|
||||
}
|
||||
|
||||
var UserIds []bson.ObjectId = []bson.ObjectId{
|
||||
bson.NewObjectId(),
|
||||
bson.NewObjectId(),
|
||||
bson.NewObjectId(),
|
||||
}
|
||||
|
||||
func (r *SampleMongoDbMigrator) V001_init_organizations_up(session *mgo.Session) error {
|
||||
date1, _ := time.Parse(SHORT_DATE_LAYOUT, "1994-Jul-05")
|
||||
date2, _ := time.Parse(SHORT_DATE_LAYOUT, "1998-Sep-04")
|
||||
date3, _ := time.Parse(SHORT_DATE_LAYOUT, "2008-Apr-28")
|
||||
|
||||
orgs := []Organization{
|
||||
{Id: OrganizationIds[0], Name: "Amazon", Location: "Seattle", DateFounded: date1},
|
||||
{Id: OrganizationIds[1], Name: "Google", Location: "Mountain View", DateFounded: date2},
|
||||
{Id: OrganizationIds[2], Name: "JFrog", Location: "Santa Clara", DateFounded: date3},
|
||||
}
|
||||
|
||||
for _, org := range orgs {
|
||||
err := session.DB(DB_NAME).C(ORGANIZATIONS_C).Insert(org)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SampleMongoDbMigrator) V001_init_organizations_down(session *mgo.Session) error {
|
||||
return session.DB(DB_NAME).C(ORGANIZATIONS_C).DropCollection()
|
||||
}
|
||||
|
||||
func (r *SampleMongoDbMigrator) V001_init_users_up(session *mgo.Session) error {
|
||||
users := []User{
|
||||
{Id: UserIds[0], Name: "Alex"},
|
||||
{Id: UserIds[1], Name: "Beatrice"},
|
||||
{Id: UserIds[2], Name: "Cleo"},
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
err := session.DB(DB_NAME).C(USERS_C).Insert(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SampleMongoDbMigrator) V001_init_users_down(session *mgo.Session) error {
|
||||
return session.DB(DB_NAME).C(USERS_C).DropCollection()
|
||||
}
|
||||
|
||||
func (r *SampleMongoDbMigrator) V002_organizations_rename_location_field_to_headquarters_up(session *mgo.Session) error {
|
||||
c := session.DB(DB_NAME).C(ORGANIZATIONS_C)
|
||||
|
||||
_, err := c.UpdateAll(nil, bson.M{"$rename": bson.M{"location": "headquarters"}})
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *SampleMongoDbMigrator) V002_organizations_rename_location_field_to_headquarters_down(session *mgo.Session) error {
|
||||
c := session.DB(DB_NAME).C(ORGANIZATIONS_C)
|
||||
|
||||
_, err := c.UpdateAll(nil, bson.M{"$rename": bson.M{"headquarters": "location"}})
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *SampleMongoDbMigrator) V002_change_user_cleo_to_cleopatra_up(session *mgo.Session) error {
|
||||
c := session.DB(DB_NAME).C(USERS_C)
|
||||
|
||||
colQuerier := bson.M{"name": "Cleo"}
|
||||
change := bson.M{"$set": bson.M{"name": "Cleopatra"}}
|
||||
|
||||
return c.Update(colQuerier, change)
|
||||
}
|
||||
|
||||
func (r *SampleMongoDbMigrator) V002_change_user_cleo_to_cleopatra_down(session *mgo.Session) error {
|
||||
c := session.DB(DB_NAME).C(USERS_C)
|
||||
|
||||
colQuerier := bson.M{"name": "Cleopatra"}
|
||||
change := bson.M{"$set": bson.M{"name": "Cleo"}}
|
||||
|
||||
return c.Update(colQuerier, change)
|
||||
}
|
||||
|
||||
// Wrong signature methods for testing
|
||||
func (r *SampleMongoDbMigrator) v001_not_exported_method_up(session *mgo.Session) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SampleMongoDbMigrator) V001_method_with_wrong_signature_up(s string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SampleMongoDbMigrator) V001_method_with_wrong_signature_down(session *mgo.Session) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
package gomethods
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"github.com/mattes/migrate/driver"
|
||||
"github.com/mattes/migrate/file"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type MethodNotFoundError string
|
||||
|
||||
func (e MethodNotFoundError) Error() string {
|
||||
return fmt.Sprintf("Method '%s' was not found. It is either not existing or has not been exported (starts with lowercase).", string(e))
|
||||
}
|
||||
|
||||
type WrongMethodSignatureError string
|
||||
|
||||
func (e WrongMethodSignatureError) Error() string {
|
||||
return fmt.Sprintf("Method '%s' has wrong signature", string(e))
|
||||
}
|
||||
|
||||
type MethodInvocationFailedError struct {
|
||||
MethodName string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *MethodInvocationFailedError) Error() string {
|
||||
return fmt.Sprintf("Method '%s' returned an error: %v", e.MethodName, e.Err)
|
||||
}
|
||||
|
||||
type MigrationMethodInvoker interface {
|
||||
Validate(methodName string) error
|
||||
Invoke(methodName string) error
|
||||
}
|
||||
|
||||
type GoMethodsDriver interface {
|
||||
driver.Driver
|
||||
|
||||
MigrationMethodInvoker
|
||||
MethodsReceiver() interface{}
|
||||
SetMethodsReceiver(r interface{}) error
|
||||
}
|
||||
|
||||
type Migrator struct {
|
||||
RollbackOnFailure bool
|
||||
MethodInvoker MigrationMethodInvoker
|
||||
}
|
||||
|
||||
func (m *Migrator) Migrate(f file.File, pipe chan interface{}) error {
|
||||
methods, err := m.getMigrationMethods(f)
|
||||
if err != nil {
|
||||
pipe <- err
|
||||
return err
|
||||
}
|
||||
|
||||
for i, methodName := range methods {
|
||||
pipe <- methodName
|
||||
err := m.MethodInvoker.Invoke(methodName)
|
||||
if err != nil {
|
||||
pipe <- err
|
||||
if !m.RollbackOnFailure {
|
||||
return err
|
||||
}
|
||||
|
||||
// on failure, try to rollback methods in this migration
|
||||
for j := i - 1; j >= 0; j-- {
|
||||
rollbackToMethodName := getRollbackToMethod(methods[j])
|
||||
if rollbackToMethodName == "" {
|
||||
continue
|
||||
}
|
||||
if err := m.MethodInvoker.Validate(rollbackToMethodName); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
pipe <- rollbackToMethodName
|
||||
err = m.MethodInvoker.Invoke(rollbackToMethodName)
|
||||
if err != nil {
|
||||
pipe <- err
|
||||
break
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getRollbackToMethod(methodName string) string {
|
||||
if strings.HasSuffix(methodName, "_up") {
|
||||
return strings.TrimSuffix(methodName, "_up") + "_down"
|
||||
} else if strings.HasSuffix(methodName, "_down") {
|
||||
return strings.TrimSuffix(methodName, "_down") + "_up"
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func getFileLines(file file.File) ([]string, error) {
|
||||
if len(file.Content) == 0 {
|
||||
lines := make([]string, 0)
|
||||
file, err := os.Open(path.Join(file.Path, file.FileName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
|
||||
for scanner.Scan() {
|
||||
lines = append(lines, scanner.Text())
|
||||
}
|
||||
return lines, nil
|
||||
} else {
|
||||
s := string(file.Content)
|
||||
return strings.Split(s, "\n"), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Migrator) getMigrationMethods(f file.File) (methods []string, err error) {
|
||||
var lines []string
|
||||
|
||||
lines, err = getFileLines(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, line := range lines {
|
||||
line := strings.TrimSpace(line)
|
||||
|
||||
if line == "" || strings.HasPrefix(line, "--") {
|
||||
// an empty line or a comment, ignore
|
||||
continue
|
||||
}
|
||||
|
||||
methodName := line
|
||||
if err := m.MethodInvoker.Validate(methodName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
methods = append(methods, methodName)
|
||||
}
|
||||
|
||||
return methods, nil
|
||||
|
||||
}
|
||||
@@ -1,247 +0,0 @@
|
||||
package gomethods
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/mattes/migrate/file"
|
||||
"github.com/mattes/migrate/migrate/direction"
|
||||
|
||||
pipep "github.com/mattes/migrate/pipe"
|
||||
)
|
||||
|
||||
type FakeGoMethodsInvoker struct {
|
||||
InvokedMethods []string
|
||||
}
|
||||
|
||||
func (invoker *FakeGoMethodsInvoker) Validate(methodName string) error {
|
||||
if methodName == "V001_some_non_existing_method_up" {
|
||||
return MethodNotFoundError(methodName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (invoker *FakeGoMethodsInvoker) Invoke(methodName string) error {
|
||||
invoker.InvokedMethods = append(invoker.InvokedMethods, methodName)
|
||||
|
||||
if methodName == "V001_some_failing_method_up" || methodName == "V001_some_failing_method_down" {
|
||||
return &MethodInvocationFailedError{
|
||||
MethodName: methodName,
|
||||
Err: SomeError{},
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type SomeError struct{}
|
||||
|
||||
func (e SomeError) Error() string { return "Some error happened" }
|
||||
|
||||
func TestMigrate(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
file file.File
|
||||
expectedInvokedMethods []string
|
||||
expectedErrors []error
|
||||
expectRollback bool
|
||||
}{
|
||||
{
|
||||
name: "up migration invokes up methods",
|
||||
file: file.File{
|
||||
Path: "/foobar",
|
||||
FileName: "001_foobar.up.gm",
|
||||
Version: 1,
|
||||
Name: "foobar",
|
||||
Direction: direction.Up,
|
||||
Content: []byte(`
|
||||
V001_init_organizations_up
|
||||
V001_init_users_up
|
||||
`),
|
||||
},
|
||||
expectedInvokedMethods: []string{"V001_init_organizations_up", "V001_init_users_up"},
|
||||
expectedErrors: []error{},
|
||||
},
|
||||
{
|
||||
name: "down migration invoked down methods",
|
||||
file: file.File{
|
||||
Path: "/foobar",
|
||||
FileName: "001_foobar.down.gm",
|
||||
Version: 1,
|
||||
Name: "foobar",
|
||||
Direction: direction.Down,
|
||||
Content: []byte(`
|
||||
V001_init_users_down
|
||||
V001_init_organizations_down
|
||||
`),
|
||||
},
|
||||
expectedInvokedMethods: []string{"V001_init_users_down", "V001_init_organizations_down"},
|
||||
expectedErrors: []error{},
|
||||
},
|
||||
{
|
||||
name: "up migration: non-existing method causes migration not to execute",
|
||||
file: file.File{
|
||||
Path: "/foobar",
|
||||
FileName: "001_foobar.up.gm",
|
||||
Version: 1,
|
||||
Name: "foobar",
|
||||
Direction: direction.Up,
|
||||
Content: []byte(`
|
||||
V001_init_organizations_up
|
||||
V001_init_users_up
|
||||
V001_some_non_existing_method_up
|
||||
`),
|
||||
},
|
||||
expectedInvokedMethods: []string{},
|
||||
expectedErrors: []error{MethodNotFoundError("V001_some_non_existing_method_up")},
|
||||
},
|
||||
{
|
||||
name: "up migration: failing method stops execution",
|
||||
file: file.File{
|
||||
Path: "/foobar",
|
||||
FileName: "001_foobar.up.gm",
|
||||
Version: 1,
|
||||
Name: "foobar",
|
||||
Direction: direction.Up,
|
||||
Content: []byte(`
|
||||
V001_init_organizations_up
|
||||
V001_some_failing_method_up
|
||||
V001_init_users_up
|
||||
`),
|
||||
},
|
||||
expectedInvokedMethods: []string{
|
||||
"V001_init_organizations_up",
|
||||
"V001_some_failing_method_up",
|
||||
},
|
||||
expectedErrors: []error{&MethodInvocationFailedError{
|
||||
MethodName: "V001_some_failing_method_up",
|
||||
Err: SomeError{},
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "down migration: failing method stops migration",
|
||||
file: file.File{
|
||||
Path: "/foobar",
|
||||
FileName: "001_foobar.down.gm",
|
||||
Version: 1,
|
||||
Name: "foobar",
|
||||
Direction: direction.Down,
|
||||
Content: []byte(`
|
||||
V001_init_users_down
|
||||
V001_some_failing_method_down
|
||||
V001_init_organizations_down
|
||||
`),
|
||||
},
|
||||
expectedInvokedMethods: []string{
|
||||
"V001_init_users_down",
|
||||
"V001_some_failing_method_down",
|
||||
},
|
||||
expectedErrors: []error{&MethodInvocationFailedError{
|
||||
MethodName: "V001_some_failing_method_down",
|
||||
Err: SomeError{},
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "up migration: failing method causes rollback in rollback mode",
|
||||
expectRollback: true,
|
||||
file: file.File{
|
||||
Path: "/foobar",
|
||||
FileName: "001_foobar.up.gm",
|
||||
Version: 1,
|
||||
Name: "foobar",
|
||||
Direction: direction.Up,
|
||||
Content: []byte(`
|
||||
V001_init_organizations_up
|
||||
V001_init_users_up
|
||||
V001_some_failing_method_up
|
||||
`),
|
||||
},
|
||||
expectedInvokedMethods: []string{
|
||||
"V001_init_organizations_up",
|
||||
"V001_init_users_up",
|
||||
"V001_some_failing_method_up",
|
||||
"V001_init_users_down",
|
||||
"V001_init_organizations_down",
|
||||
},
|
||||
expectedErrors: []error{&MethodInvocationFailedError{
|
||||
MethodName: "V001_some_failing_method_up",
|
||||
Err: SomeError{},
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "down migration: failing method causes rollback in rollback mode",
|
||||
expectRollback: true,
|
||||
file: file.File{
|
||||
Path: "/foobar",
|
||||
FileName: "001_foobar.down.gm",
|
||||
Version: 1,
|
||||
Name: "foobar",
|
||||
Direction: direction.Down,
|
||||
Content: []byte(`
|
||||
V001_init_users_down
|
||||
V001_some_failing_method_down
|
||||
V001_init_organizations_down
|
||||
`),
|
||||
},
|
||||
expectedInvokedMethods: []string{
|
||||
"V001_init_users_down",
|
||||
"V001_some_failing_method_down",
|
||||
"V001_init_users_up",
|
||||
},
|
||||
expectedErrors: []error{&MethodInvocationFailedError{
|
||||
MethodName: "V001_some_failing_method_down",
|
||||
Err: SomeError{},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
migrator := Migrator{}
|
||||
fakeInvoker := &FakeGoMethodsInvoker{InvokedMethods: []string{}}
|
||||
|
||||
migrator.MethodInvoker = fakeInvoker
|
||||
migrator.RollbackOnFailure = c.expectRollback
|
||||
|
||||
pipe := pipep.New()
|
||||
go func() {
|
||||
migrator.Migrate(c.file, pipe)
|
||||
close(pipe)
|
||||
}()
|
||||
errs := pipep.ReadErrors(pipe)
|
||||
|
||||
var failed bool
|
||||
if !reflect.DeepEqual(fakeInvoker.InvokedMethods, c.expectedInvokedMethods) {
|
||||
failed = true
|
||||
t.Errorf("case '%s': FAILED\nexpected invoked methods %v\nbut got %v", c.name, c.expectedInvokedMethods, fakeInvoker.InvokedMethods)
|
||||
}
|
||||
if !reflect.DeepEqual(errs, c.expectedErrors) {
|
||||
failed = true
|
||||
t.Errorf("case '%s': FAILED\nexpected errors %v\nbut got %v", c.name, c.expectedErrors, errs)
|
||||
|
||||
}
|
||||
if !failed {
|
||||
//t.Logf("case '%s': PASSED", c.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRollbackToMethod(t *testing.T) {
|
||||
cases := []struct {
|
||||
method string
|
||||
expectedRollbackMethod string
|
||||
}{
|
||||
{"some_method_up", "some_method_down"},
|
||||
{"some_method_down", "some_method_up"},
|
||||
{"up_down_up", "up_down_down"},
|
||||
{"down_up", "down_down"},
|
||||
{"down_down", "down_up"},
|
||||
{"some_method", ""},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
actualRollbackMethod := getRollbackToMethod(c.method)
|
||||
if actualRollbackMethod != c.expectedRollbackMethod {
|
||||
t.Errorf("Expected rollback method to be %s but got %s", c.expectedRollbackMethod, actualRollbackMethod)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package gomethods
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mattes/migrate/driver"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var methodsReceiversMu sync.Mutex
|
||||
|
||||
// Registers a methods receiver for go methods driver
|
||||
// Users of gomethods migration drivers should call this method
|
||||
// to register objects with their migration methods before executing the migration
|
||||
func RegisterMethodsReceiverForDriver(driverName string, receiver interface{}) {
|
||||
methodsReceiversMu.Lock()
|
||||
defer methodsReceiversMu.Unlock()
|
||||
if receiver == nil {
|
||||
panic("Go methods: Register receiver object is nil")
|
||||
}
|
||||
|
||||
driver := driver.GetDriver(driverName)
|
||||
if driver == nil {
|
||||
panic("Go methods: Trying to register receiver for not registered driver " + driverName)
|
||||
}
|
||||
|
||||
methodsDriver, ok := driver.(GoMethodsDriver)
|
||||
if !ok {
|
||||
panic("Go methods: Trying to register receiver for non go methods driver " + driverName)
|
||||
}
|
||||
|
||||
if methodsDriver.MethodsReceiver() != nil {
|
||||
panic("Go methods: Methods receiver already registered for driver " + driverName)
|
||||
}
|
||||
|
||||
if err := methodsDriver.SetMethodsReceiver(receiver); err != nil {
|
||||
panic(fmt.Sprintf("Go methods: Failed to set methods receiver for driver %s\nError: %v",
|
||||
driverName, err))
|
||||
}
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/mattes/migrate/driver"
|
||||
"github.com/mattes/migrate/driver/mongodb/gomethods"
|
||||
"github.com/mattes/migrate/file"
|
||||
"github.com/mattes/migrate/migrate/direction"
|
||||
"gopkg.in/mgo.v2"
|
||||
"gopkg.in/mgo.v2/bson"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type UnregisteredMethodsReceiverError string
|
||||
|
||||
func (e UnregisteredMethodsReceiverError) Error() string {
|
||||
return "Unregistered methods receiver for driver: " + string(e)
|
||||
}
|
||||
|
||||
type WrongMethodsReceiverTypeError string
|
||||
|
||||
func (e WrongMethodsReceiverTypeError) Error() string {
|
||||
return "Wrong methods receiver type for driver: " + string(e)
|
||||
}
|
||||
|
||||
const MIGRATE_C = "db_migrations"
|
||||
const DRIVER_NAME = "gomethods.mongodb"
|
||||
|
||||
type Driver struct {
|
||||
Session *mgo.Session
|
||||
|
||||
methodsReceiver MethodsReceiver
|
||||
migrator gomethods.Migrator
|
||||
}
|
||||
|
||||
var _ gomethods.GoMethodsDriver = (*Driver)(nil)
|
||||
|
||||
type MethodsReceiver interface {
|
||||
DbName() string
|
||||
}
|
||||
|
||||
func (d *Driver) MethodsReceiver() interface{} {
|
||||
return d.methodsReceiver
|
||||
}
|
||||
|
||||
func (d *Driver) SetMethodsReceiver(r interface{}) error {
|
||||
r1, ok := r.(MethodsReceiver)
|
||||
if !ok {
|
||||
return WrongMethodsReceiverTypeError(DRIVER_NAME)
|
||||
}
|
||||
|
||||
d.methodsReceiver = r1
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
driver.RegisterDriver("mongodb", &Driver{})
|
||||
}
|
||||
|
||||
type DbMigration struct {
|
||||
Id bson.ObjectId `bson:"_id"`
|
||||
Version uint64 `bson:"version"`
|
||||
}
|
||||
|
||||
func (driver *Driver) Initialize(url string) error {
|
||||
if driver.methodsReceiver == nil {
|
||||
return UnregisteredMethodsReceiverError(DRIVER_NAME)
|
||||
}
|
||||
|
||||
urlWithoutScheme := strings.SplitN(url, "mongodb://", 2)
|
||||
if len(urlWithoutScheme) != 2 {
|
||||
return errors.New("invalid mongodb:// scheme")
|
||||
}
|
||||
|
||||
session, err := mgo.Dial(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
session.SetMode(mgo.Monotonic, true)
|
||||
|
||||
c := session.DB(driver.methodsReceiver.DbName()).C(MIGRATE_C)
|
||||
err = c.EnsureIndex(mgo.Index{
|
||||
Key: []string{"version"},
|
||||
Unique: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
driver.Session = session
|
||||
driver.migrator = gomethods.Migrator{MethodInvoker: driver}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver *Driver) Close() error {
|
||||
if driver.Session != nil {
|
||||
driver.Session.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver *Driver) FilenameExtension() string {
|
||||
return "mgo"
|
||||
}
|
||||
|
||||
func (driver *Driver) Version() (uint64, error) {
|
||||
var latestMigration DbMigration
|
||||
c := driver.Session.DB(driver.methodsReceiver.DbName()).C(MIGRATE_C)
|
||||
|
||||
err := c.Find(bson.M{}).Sort("-version").One(&latestMigration)
|
||||
|
||||
switch {
|
||||
case err == mgo.ErrNotFound:
|
||||
return 0, nil
|
||||
case err != nil:
|
||||
return 0, err
|
||||
default:
|
||||
return latestMigration.Version, nil
|
||||
}
|
||||
}
|
||||
func (driver *Driver) Migrate(f file.File, pipe chan interface{}) {
|
||||
defer close(pipe)
|
||||
pipe <- f
|
||||
|
||||
err := driver.migrator.Migrate(f, pipe)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
migrate_c := driver.Session.DB(driver.methodsReceiver.DbName()).C(MIGRATE_C)
|
||||
|
||||
if f.Direction == direction.Up {
|
||||
id := bson.NewObjectId()
|
||||
dbMigration := DbMigration{Id: id, Version: f.Version}
|
||||
|
||||
err := migrate_c.Insert(dbMigration)
|
||||
if err != nil {
|
||||
pipe <- err
|
||||
return
|
||||
}
|
||||
|
||||
} else if f.Direction == direction.Down {
|
||||
err := migrate_c.Remove(bson.M{"version": f.Version})
|
||||
if err != nil {
|
||||
pipe <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (driver *Driver) Validate(methodName string) error {
|
||||
methodWithReceiver, ok := reflect.TypeOf(driver.methodsReceiver).MethodByName(methodName)
|
||||
if !ok {
|
||||
return gomethods.MethodNotFoundError(methodName)
|
||||
}
|
||||
if methodWithReceiver.PkgPath != "" {
|
||||
return gomethods.MethodNotFoundError(methodName)
|
||||
}
|
||||
|
||||
methodFunc := reflect.ValueOf(driver.methodsReceiver).MethodByName(methodName)
|
||||
methodTemplate := func(*mgo.Session) error { return nil }
|
||||
|
||||
if methodFunc.Type() != reflect.TypeOf(methodTemplate) {
|
||||
return gomethods.WrongMethodSignatureError(methodName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver *Driver) Invoke(methodName string) error {
|
||||
name := methodName
|
||||
migrateMethod := reflect.ValueOf(driver.methodsReceiver).MethodByName(name)
|
||||
if !migrateMethod.IsValid() {
|
||||
return gomethods.MethodNotFoundError(methodName)
|
||||
}
|
||||
|
||||
retValues := migrateMethod.Call([]reflect.Value{reflect.ValueOf(driver.Session)})
|
||||
if len(retValues) != 1 {
|
||||
return gomethods.WrongMethodSignatureError(name)
|
||||
}
|
||||
|
||||
if !retValues[0].IsNil() {
|
||||
err, ok := retValues[0].Interface().(error)
|
||||
if !ok {
|
||||
return gomethods.WrongMethodSignatureError(name)
|
||||
}
|
||||
return &gomethods.MethodInvocationFailedError{MethodName: name, Err: err}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
# MySQL Driver
|
||||
|
||||
### See [issue #1](https://github.com/mattes/migrate/issues/1#issuecomment-58728186) before using this 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 mysql://user@tcp(host:port)/database -path ./db/migrations create add_field_to_table
|
||||
migrate -url mysql://user@tcp(host:port)/database -path ./db/migrations up
|
||||
migrate help # for more info
|
||||
```
|
||||
|
||||
See full [DSN (Data Source Name) documentation](https://github.com/go-sql-driver/mysql/#dsn-data-source-name).
|
||||
|
||||
## Authors
|
||||
|
||||
* Matthias Kadenbach, https://github.com/mattes
|
||||
@@ -1,185 +0,0 @@
|
||||
// Package mysql implements the Driver interface.
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-sql-driver/mysql"
|
||||
"github.com/mattes/migrate/driver"
|
||||
"github.com/mattes/migrate/file"
|
||||
"github.com/mattes/migrate/migrate/direction"
|
||||
)
|
||||
|
||||
type Driver struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
const tableName = "schema_migrations"
|
||||
|
||||
func (driver *Driver) Initialize(url string) error {
|
||||
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
|
||||
}
|
||||
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 {
|
||||
_, 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
|
||||
}
|
||||
|
||||
func (driver *Driver) FilenameExtension() string {
|
||||
return "sql"
|
||||
}
|
||||
|
||||
func (driver *Driver) Migrate(f file.File, pipe chan interface{}) {
|
||||
defer close(pipe)
|
||||
pipe <- f
|
||||
|
||||
// http://go-database-sql.org/modifying.html, Working with Transactions
|
||||
// You should not mingle the use of transaction-related functions such as Begin() and Commit() with SQL statements such as BEGIN and COMMIT in your SQL code.
|
||||
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
|
||||
}
|
||||
|
||||
// TODO this is not good! unfortunately there is no mysql driver that
|
||||
// supports multiple statements per query.
|
||||
sqlStmts := bytes.Split(f.Content, []byte(";"))
|
||||
|
||||
for _, sqlStmt := range sqlStmts {
|
||||
sqlStmt = bytes.TrimSpace(sqlStmt)
|
||||
if len(sqlStmt) > 0 {
|
||||
if _, err := tx.Exec(string(sqlStmt)); err != nil {
|
||||
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())
|
||||
}
|
||||
|
||||
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").Scan(&version)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return 0, nil
|
||||
case err != nil:
|
||||
return 0, err
|
||||
default:
|
||||
return version, nil
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
driver.RegisterDriver("mysql", &Driver{})
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mattes/migrate/file"
|
||||
"github.com/mattes/migrate/migrate/direction"
|
||||
pipep "github.com/mattes/migrate/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("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", strings.SplitN(driverUrl, "mysql://", 2)[1])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := connection.Exec(`DROP TABLE IF EXISTS yolo, yolo1, ` + 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 int(11) not null primary key auto_increment
|
||||
);
|
||||
|
||||
CREATE TABLE yolo1 (
|
||||
id int(11) not null primary key auto_increment
|
||||
);
|
||||
`),
|
||||
},
|
||||
{
|
||||
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(`
|
||||
|
||||
// a comment
|
||||
CREATE TABLE error (
|
||||
id 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,20 +0,0 @@
|
||||
# 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
|
||||
@@ -1,157 +0,0 @@
|
||||
// Package neo4j implements the Driver interface.
|
||||
package neo4j
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"bytes"
|
||||
"strings"
|
||||
"errors"
|
||||
"github.com/jmcvetta/neoism"
|
||||
"github.com/mattes/migrate/driver"
|
||||
"github.com/mattes/migrate/file"
|
||||
"github.com/mattes/migrate/migrate/direction"
|
||||
)
|
||||
|
||||
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{})
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
package neo4j
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/jmcvetta/neoism"
|
||||
"github.com/mattes/migrate/file"
|
||||
"github.com/mattes/migrate/migrate/direction"
|
||||
pipep "github.com/mattes/migrate/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,25 +0,0 @@
|
||||
# PostgreSQL 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 postgres://user@host:port/database -path ./db/migrations create add_field_to_table
|
||||
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"
|
||||
|
||||
# see more docs: https://godoc.org/github.com/lib/pq#hdr-Connection_String_Parameters
|
||||
```
|
||||
|
||||
## Authors
|
||||
|
||||
* Matthias Kadenbach, https://github.com/mattes
|
||||
@@ -1,143 +0,0 @@
|
||||
// Package postgres implements the Driver interface.
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/lib/pq"
|
||||
"github.com/mattes/migrate/driver"
|
||||
"github.com/mattes/migrate/file"
|
||||
"github.com/mattes/migrate/migrate/direction"
|
||||
)
|
||||
|
||||
type Driver struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
const tableName = "schema_migrations"
|
||||
|
||||
func (driver *Driver) Initialize(url string) error {
|
||||
db, err := sql.Open("postgres", 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 {
|
||||
r := driver.db.QueryRow("SELECT count(*) FROM information_schema.tables WHERE table_name = $1;", 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
|
||||
}
|
||||
|
||||
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 ($1)", 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=$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 {
|
||||
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 {
|
||||
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 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("postgres", &Driver{})
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mattes/migrate/file"
|
||||
"github.com/mattes/migrate/migrate/direction"
|
||||
pipep "github.com/mattes/migrate/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("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)
|
||||
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)
|
||||
}
|
||||
|
||||
// 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 serial 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 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()
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
# 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
|
||||
-130
@@ -1,130 +0,0 @@
|
||||
package ql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/mattes/migrate/file"
|
||||
"github.com/mattes/migrate/driver"
|
||||
"github.com/mattes/migrate/migrate/direction"
|
||||
|
||||
_ "github.com/cznic/ql/driver"
|
||||
"strings"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
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{})
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package driver
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var driversMu sync.Mutex
|
||||
var drivers = make(map[string]Driver)
|
||||
|
||||
// Registers 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
# 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
|
||||
@@ -1,131 +0,0 @@
|
||||
// Package sqlite3 implements the Driver interface.
|
||||
package sqlite3
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mattes/migrate/driver"
|
||||
"github.com/mattes/migrate/file"
|
||||
"github.com/mattes/migrate/migrate/direction"
|
||||
"github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
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 <- errors.New(fmt.Sprintf("SQLite Error (%s); Extended (%s)\nError: %s", sqliteErr.Code.Error(), sqliteErr.ExtendedCode.Error(), sqliteErr.Error()))
|
||||
} else {
|
||||
pipe <- errors.New(fmt.Sprintf("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{})
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
package sqlite3
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
"github.com/mattes/migrate/file"
|
||||
"github.com/mattes/migrate/migrate/direction"
|
||||
pipep "github.com/mattes/migrate/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)
|
||||
}
|
||||
}
|
||||
-340
@@ -1,340 +0,0 @@
|
||||
// Package file contains functions for low-level migration files handling.
|
||||
package file
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mattes/migrate/migrate/direction"
|
||||
"go/token"
|
||||
"io/ioutil"
|
||||
"path"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var filenameRegex = `^([0-9]+)_(.*)\.(up|down)\.%s$`
|
||||
|
||||
// FilenameRegex builds regular expression stmt with given
|
||||
// filename extension from driver.
|
||||
func FilenameRegex(filenameExtension string) *regexp.Regexp {
|
||||
return regexp.MustCompile(fmt.Sprintf(filenameRegex, filenameExtension))
|
||||
}
|
||||
|
||||
// File represents one file on disk.
|
||||
// Example: 001_initial_plan_to_do_sth.up.sql
|
||||
type File struct {
|
||||
// absolute path to file
|
||||
Path string
|
||||
|
||||
// the name of the file
|
||||
FileName string
|
||||
|
||||
// version parsed from filename
|
||||
Version uint64
|
||||
|
||||
// the actual migration name parsed from filename
|
||||
Name string
|
||||
|
||||
// content of the file
|
||||
Content []byte
|
||||
|
||||
// UP or DOWN migration
|
||||
Direction direction.Direction
|
||||
}
|
||||
|
||||
// Files is a slice of Files
|
||||
type Files []File
|
||||
|
||||
// MigrationFile represents both the UP and the DOWN migration file.
|
||||
type MigrationFile struct {
|
||||
// version of the migration file, parsed from the filenames
|
||||
Version uint64
|
||||
|
||||
// reference to the *up* migration file
|
||||
UpFile *File
|
||||
|
||||
// reference to the *down* migration file
|
||||
DownFile *File
|
||||
}
|
||||
|
||||
// MigrationFiles is a slice of MigrationFiles
|
||||
type MigrationFiles []MigrationFile
|
||||
|
||||
// ReadContent reads the file's content if the content is empty
|
||||
func (f *File) ReadContent() error {
|
||||
if len(f.Content) == 0 {
|
||||
content, err := ioutil.ReadFile(path.Join(f.Path, f.FileName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.Content = content
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToFirstFrom fetches all (down) migration files including the migration file
|
||||
// of the current version to the very first migration file.
|
||||
func (mf *MigrationFiles) ToFirstFrom(version uint64) (Files, error) {
|
||||
sort.Sort(sort.Reverse(mf))
|
||||
files := make(Files, 0)
|
||||
for _, migrationFile := range *mf {
|
||||
if migrationFile.Version <= version && migrationFile.DownFile != nil {
|
||||
files = append(files, *migrationFile.DownFile)
|
||||
}
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// ToLastFrom fetches all (up) migration files to the most recent migration file.
|
||||
// The migration file of the current version is not included.
|
||||
func (mf *MigrationFiles) ToLastFrom(version uint64) (Files, error) {
|
||||
sort.Sort(mf)
|
||||
files := make(Files, 0)
|
||||
for _, migrationFile := range *mf {
|
||||
if migrationFile.Version > version && migrationFile.UpFile != nil {
|
||||
files = append(files, *migrationFile.UpFile)
|
||||
}
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// From travels relatively through migration files.
|
||||
//
|
||||
// +1 will fetch the next up migration file
|
||||
// +2 will fetch the next two up migration files
|
||||
// +n will fetch ...
|
||||
// -1 will fetch the the previous down migration file
|
||||
// -2 will fetch the next two previous down migration files
|
||||
// -n will fetch ...
|
||||
func (mf *MigrationFiles) From(version uint64, relativeN int) (Files, error) {
|
||||
var d direction.Direction
|
||||
if relativeN > 0 {
|
||||
d = direction.Up
|
||||
} else if relativeN < 0 {
|
||||
d = direction.Down
|
||||
} else { // relativeN == 0
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if d == direction.Down {
|
||||
sort.Sort(sort.Reverse(mf))
|
||||
} else {
|
||||
sort.Sort(mf)
|
||||
}
|
||||
|
||||
files := make(Files, 0)
|
||||
|
||||
counter := relativeN
|
||||
if relativeN < 0 {
|
||||
counter = relativeN * -1
|
||||
}
|
||||
|
||||
for _, migrationFile := range *mf {
|
||||
if counter > 0 {
|
||||
|
||||
if d == direction.Up && migrationFile.Version > version && migrationFile.UpFile != nil {
|
||||
files = append(files, *migrationFile.UpFile)
|
||||
counter -= 1
|
||||
} else if d == direction.Down && migrationFile.Version <= version && migrationFile.DownFile != nil {
|
||||
files = append(files, *migrationFile.DownFile)
|
||||
counter -= 1
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// ReadMigrationFiles reads all migration files from a given path
|
||||
func ReadMigrationFiles(path string, filenameRegex *regexp.Regexp) (files MigrationFiles, err error) {
|
||||
// find all migration files in path
|
||||
ioFiles, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
type tmpFile struct {
|
||||
version uint64
|
||||
name string
|
||||
filename string
|
||||
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})
|
||||
}
|
||||
}
|
||||
|
||||
// put tmpFiles into MigrationFile struct
|
||||
parsedVersions := make(map[uint64]bool)
|
||||
newFiles := make(MigrationFiles, 0)
|
||||
for _, file := range tmpFiles {
|
||||
if _, ok := parsedVersions[file.version]; !ok {
|
||||
migrationFile := MigrationFile{
|
||||
Version: file.version,
|
||||
}
|
||||
|
||||
var lookFordirection direction.Direction
|
||||
switch file.d {
|
||||
case direction.Up:
|
||||
migrationFile.UpFile = &File{
|
||||
Path: path,
|
||||
FileName: file.filename,
|
||||
Version: file.version,
|
||||
Name: file.name,
|
||||
Content: nil,
|
||||
Direction: direction.Up,
|
||||
}
|
||||
lookFordirection = direction.Down
|
||||
case direction.Down:
|
||||
migrationFile.DownFile = &File{
|
||||
Path: path,
|
||||
FileName: file.filename,
|
||||
Version: file.version,
|
||||
Name: file.name,
|
||||
Content: nil,
|
||||
Direction: direction.Down,
|
||||
}
|
||||
lookFordirection = direction.Up
|
||||
default:
|
||||
return nil, errors.New("Unsupported direction.Direction Type")
|
||||
}
|
||||
|
||||
for _, file2 := range tmpFiles {
|
||||
if file2.version == file.version && file2.d == lookFordirection {
|
||||
switch lookFordirection {
|
||||
case direction.Up:
|
||||
migrationFile.UpFile = &File{
|
||||
Path: path,
|
||||
FileName: file2.filename,
|
||||
Version: file.version,
|
||||
Name: file2.name,
|
||||
Content: nil,
|
||||
Direction: direction.Up,
|
||||
}
|
||||
case direction.Down:
|
||||
migrationFile.DownFile = &File{
|
||||
Path: path,
|
||||
FileName: file2.filename,
|
||||
Version: file.version,
|
||||
Name: file2.name,
|
||||
Content: nil,
|
||||
Direction: direction.Down,
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
newFiles = append(newFiles, migrationFile)
|
||||
parsedVersions[file.version] = true
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(newFiles)
|
||||
return newFiles, nil
|
||||
}
|
||||
|
||||
// parseFilenameSchema parses the filename
|
||||
func parseFilenameSchema(filename string, filenameRegex *regexp.Regexp) (version uint64, name string, d direction.Direction, err error) {
|
||||
matches := filenameRegex.FindStringSubmatch(filename)
|
||||
if len(matches) != 4 {
|
||||
return 0, "", 0, errors.New("Unable to parse filename schema")
|
||||
}
|
||||
|
||||
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]))
|
||||
}
|
||||
|
||||
if matches[3] == "up" {
|
||||
d = direction.Up
|
||||
} 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 version, matches[2], d, nil
|
||||
}
|
||||
|
||||
// Len is the number of elements in the collection.
|
||||
// Required by Sort Interface{}
|
||||
func (mf MigrationFiles) Len() int {
|
||||
return len(mf)
|
||||
}
|
||||
|
||||
// Less reports whether the element with
|
||||
// index i should sort before the element with index j.
|
||||
// Required by Sort Interface{}
|
||||
func (mf MigrationFiles) Less(i, j int) bool {
|
||||
return mf[i].Version < mf[j].Version
|
||||
}
|
||||
|
||||
// Swap swaps the elements with indexes i and j.
|
||||
// Required by Sort Interface{}
|
||||
func (mf MigrationFiles) Swap(i, j int) {
|
||||
mf[i], mf[j] = mf[j], mf[i]
|
||||
}
|
||||
|
||||
// LineColumnFromOffset reads data and returns line and column integer
|
||||
// for a given offset.
|
||||
func LineColumnFromOffset(data []byte, offset int) (line, column int) {
|
||||
// TODO is there a better way?
|
||||
fs := token.NewFileSet()
|
||||
tf := fs.AddFile("", fs.Base(), len(data))
|
||||
tf.SetLinesForContent(data)
|
||||
pos := tf.Position(tf.Pos(offset))
|
||||
return pos.Line, pos.Column
|
||||
}
|
||||
|
||||
// LinesBeforeAndAfter reads n lines before and after a given line.
|
||||
// Set lineNumbers to true, to prepend line numbers.
|
||||
func LinesBeforeAndAfter(data []byte, line, before, after int, lineNumbers bool) []byte {
|
||||
// TODO(mattes): Trim empty lines at the beginning and at the end
|
||||
// TODO(mattes): Trim offset whitespace at the beginning of each line, so that indentation is preserved
|
||||
startLine := line - before
|
||||
endLine := line + after
|
||||
lines := bytes.SplitN(data, []byte("\n"), endLine+1)
|
||||
|
||||
if startLine < 0 {
|
||||
startLine = 0
|
||||
}
|
||||
if endLine > len(lines) {
|
||||
endLine = len(lines)
|
||||
}
|
||||
|
||||
selectLines := lines[startLine:endLine]
|
||||
newLines := make([][]byte, 0)
|
||||
lineCounter := startLine + 1
|
||||
lineNumberDigits := len(strconv.Itoa(len(selectLines)))
|
||||
for _, l := range selectLines {
|
||||
lineCounterStr := strconv.Itoa(lineCounter)
|
||||
if len(lineCounterStr)%lineNumberDigits != 0 {
|
||||
lineCounterStr = strings.Repeat(" ", lineNumberDigits-len(lineCounterStr)%lineNumberDigits) + lineCounterStr
|
||||
}
|
||||
|
||||
lNew := l
|
||||
if lineNumbers {
|
||||
lNew = append([]byte(lineCounterStr+": "), lNew...)
|
||||
}
|
||||
newLines = append(newLines, lNew)
|
||||
lineCounter += 1
|
||||
}
|
||||
|
||||
return bytes.Join(newLines, []byte("\n"))
|
||||
}
|
||||
@@ -1,253 +0,0 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"github.com/mattes/migrate/migrate/direction"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseFilenameSchema(t *testing.T) {
|
||||
var tests = []struct {
|
||||
filename string
|
||||
filenameExtension string
|
||||
expectVersion uint64
|
||||
expectName string
|
||||
expectDirection direction.Direction
|
||||
expectErr bool
|
||||
}{
|
||||
{"001_test_file.up.sql", "sql", 1, "test_file", direction.Up, false},
|
||||
{"001_test_file.down.sql", "sql", 1, "test_file", direction.Down, false},
|
||||
{"10034_test_file.down.sql", "sql", 10034, "test_file", direction.Down, false},
|
||||
{"-1_test_file.down.sql", "sql", 0, "", direction.Up, true},
|
||||
{"test_file.down.sql", "sql", 0, "", direction.Up, true},
|
||||
{"100_test_file.down", "sql", 0, "", direction.Up, true},
|
||||
{"100_test_file.sql", "sql", 0, "", direction.Up, true},
|
||||
{"100_test_file", "sql", 0, "", direction.Up, true},
|
||||
{"test_file", "sql", 0, "", direction.Up, true},
|
||||
{"100", "sql", 0, "", direction.Up, true},
|
||||
{".sql", "sql", 0, "", direction.Up, true},
|
||||
{"up.sql", "sql", 0, "", direction.Up, true},
|
||||
{"down.sql", "sql", 0, "", direction.Up, true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
version, name, migrate, err := parseFilenameSchema(test.filename, FilenameRegex(test.filenameExtension))
|
||||
if test.expectErr && err == nil {
|
||||
t.Fatal("Expected error, but got none.", test)
|
||||
}
|
||||
if !test.expectErr && err != nil {
|
||||
t.Fatal("Did not expect error, but got one:", err, test)
|
||||
}
|
||||
if err == nil {
|
||||
if version != test.expectVersion {
|
||||
t.Error("Wrong version number", test)
|
||||
}
|
||||
if name != test.expectName {
|
||||
t.Error("wrong name", test)
|
||||
}
|
||||
if migrate != test.expectDirection {
|
||||
t.Error("wrong migrate", test)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFiles(t *testing.T) {
|
||||
tmpdir, err := ioutil.TempDir("/tmp", "TestLookForMigrationFilesInSearchPath")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
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)
|
||||
ioutil.WriteFile(path.Join(tmpdir, "002_migrationfile.down.sql"), nil, 0755)
|
||||
|
||||
ioutil.WriteFile(path.Join(tmpdir, "001_migrationfile.up.sql"), nil, 0755)
|
||||
ioutil.WriteFile(path.Join(tmpdir, "001_migrationfile.down.sql"), nil, 0755)
|
||||
|
||||
ioutil.WriteFile(path.Join(tmpdir, "101_create_table.up.sql"), nil, 0755)
|
||||
ioutil.WriteFile(path.Join(tmpdir, "101_drop_tables.down.sql"), nil, 0755)
|
||||
|
||||
ioutil.WriteFile(path.Join(tmpdir, "301_migrationfile.up.sql"), nil, 0755)
|
||||
|
||||
ioutil.WriteFile(path.Join(tmpdir, "401_migrationfile.down.sql"), []byte("test"), 0755)
|
||||
|
||||
files, err := ReadMigrationFiles(tmpdir, FilenameRegex("sql"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
t.Fatal("No files returned.")
|
||||
}
|
||||
|
||||
if len(files) != 5 {
|
||||
t.Fatal("Wrong number of files returned.")
|
||||
}
|
||||
|
||||
// test sort order
|
||||
if files[0].Version != 1 || files[1].Version != 2 || files[2].Version != 101 || files[3].Version != 301 || files[4].Version != 401 {
|
||||
t.Error("Sort order is incorrect")
|
||||
t.Error(files)
|
||||
}
|
||||
|
||||
// test UpFile and DownFile
|
||||
if files[0].UpFile == nil {
|
||||
t.Fatalf("Missing up file for version %v", files[0].Version)
|
||||
}
|
||||
if files[0].DownFile == nil {
|
||||
t.Fatalf("Missing down file for version %v", files[0].Version)
|
||||
}
|
||||
|
||||
if files[1].UpFile == nil {
|
||||
t.Fatalf("Missing up file for version %v", files[1].Version)
|
||||
}
|
||||
if files[1].DownFile == nil {
|
||||
t.Fatalf("Missing down file for version %v", files[1].Version)
|
||||
}
|
||||
|
||||
if files[2].UpFile == nil {
|
||||
t.Fatalf("Missing up file for version %v", files[2].Version)
|
||||
}
|
||||
if files[2].DownFile == nil {
|
||||
t.Fatalf("Missing down file for version %v", files[2].Version)
|
||||
}
|
||||
|
||||
if files[3].UpFile == nil {
|
||||
t.Fatalf("Missing up file for version %v", files[3].Version)
|
||||
}
|
||||
if files[3].DownFile != nil {
|
||||
t.Fatalf("There should not be a down file for version %v", files[3].Version)
|
||||
}
|
||||
|
||||
if files[4].UpFile != nil {
|
||||
t.Fatalf("There should not be a up file for version %v", files[4].Version)
|
||||
}
|
||||
if files[4].DownFile == nil {
|
||||
t.Fatalf("Missing down file for version %v", files[4].Version)
|
||||
}
|
||||
|
||||
// test read
|
||||
if err := files[4].DownFile.ReadContent(); err != nil {
|
||||
t.Error("Unable to read file", err)
|
||||
}
|
||||
if files[4].DownFile.Content == nil {
|
||||
t.Fatal("Read content is nil")
|
||||
}
|
||||
if string(files[4].DownFile.Content) != "test" {
|
||||
t.Fatal("Read content is wrong")
|
||||
}
|
||||
|
||||
// test names
|
||||
if files[0].UpFile.Name != "migrationfile" {
|
||||
t.Error("file name is not correct", files[0].UpFile.Name)
|
||||
}
|
||||
if files[0].UpFile.FileName != "001_migrationfile.up.sql" {
|
||||
t.Error("file name is not correct", files[0].UpFile.FileName)
|
||||
}
|
||||
|
||||
// test file.From()
|
||||
// there should be the following versions:
|
||||
// 1(up&down), 2(up&down), 101(up&down), 301(up), 401(down)
|
||||
var tests = []struct {
|
||||
from uint64
|
||||
relative int
|
||||
expectRange []uint64
|
||||
}{
|
||||
{0, 2, []uint64{1, 2}},
|
||||
{1, 4, []uint64{2, 101, 301}},
|
||||
{1, 0, nil},
|
||||
{0, 1, []uint64{1}},
|
||||
{0, 0, nil},
|
||||
{101, -2, []uint64{101, 2}},
|
||||
{401, -1, []uint64{401}},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
rangeFiles, err := files.From(test.from, test.relative)
|
||||
if err != nil {
|
||||
t.Error("Unable to fetch range:", err)
|
||||
}
|
||||
if len(rangeFiles) != len(test.expectRange) {
|
||||
t.Fatalf("file.From(): expected %v files, got %v. For test %v.", len(test.expectRange), len(rangeFiles), test.expectRange)
|
||||
}
|
||||
|
||||
for i, version := range test.expectRange {
|
||||
if rangeFiles[i].Version != version {
|
||||
t.Fatal("file.From(): returned files dont match expectations", test.expectRange)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// test ToFirstFrom
|
||||
tffFiles, err := files.ToFirstFrom(401)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(tffFiles) != 4 {
|
||||
t.Fatalf("Wrong number of files returned by ToFirstFrom(), expected %v, got %v.", 5, len(tffFiles))
|
||||
}
|
||||
if tffFiles[0].Direction != direction.Down {
|
||||
t.Error("ToFirstFrom() did not return DownFiles")
|
||||
}
|
||||
|
||||
// test ToLastFrom
|
||||
tofFiles, err := files.ToLastFrom(0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(tofFiles) != 4 {
|
||||
t.Fatalf("Wrong number of files returned by ToLastFrom(), expected %v, got %v.", 5, len(tofFiles))
|
||||
}
|
||||
if tofFiles[0].Direction != direction.Up {
|
||||
t.Error("ToFirstFrom() did not return UpFiles")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package migrate
|
||||
|
||||
type Logger interface {
|
||||
Printf(format string, v ...interface{})
|
||||
Verbose() bool
|
||||
}
|
||||
@@ -1,247 +0,0 @@
|
||||
// Package main is the CLI.
|
||||
// You can use the CLI via Terminal.
|
||||
// import "github.com/mattes/migrate/migrate" for usage within Go.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
_ "github.com/mattes/migrate/driver/bash"
|
||||
_ "github.com/mattes/migrate/driver/cassandra"
|
||||
_ "github.com/mattes/migrate/driver/crate"
|
||||
_ "github.com/mattes/migrate/driver/mysql"
|
||||
_ "github.com/mattes/migrate/driver/neo4j"
|
||||
_ "github.com/mattes/migrate/driver/postgres"
|
||||
_ "github.com/mattes/migrate/driver/ql"
|
||||
_ "github.com/mattes/migrate/driver/sqlite3"
|
||||
"github.com/mattes/migrate/file"
|
||||
"github.com/mattes/migrate/migrate"
|
||||
"github.com/mattes/migrate/migrate/direction"
|
||||
pipep "github.com/mattes/migrate/pipe"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
if *migrationsPath == "" {
|
||||
*migrationsPath, _ = os.Getwd()
|
||||
}
|
||||
|
||||
switch command {
|
||||
case "create":
|
||||
verifyMigrationsPath(*migrationsPath)
|
||||
name := flag.Arg(1)
|
||||
if name == "" {
|
||||
fmt.Println("Please specify name.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
migrationFile, err := migrate.Create(*url, *migrationsPath, name)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Version %v migration files created in %v:\n", migrationFile.Version, *migrationsPath)
|
||||
fmt.Println(migrationFile.UpFile.FileName)
|
||||
fmt.Println(migrationFile.DownFile.FileName)
|
||||
|
||||
case "migrate":
|
||||
verifyMigrationsPath(*migrationsPath)
|
||||
relativeN := flag.Arg(1)
|
||||
relativeNInt, err := strconv.Atoi(relativeN)
|
||||
if err != nil {
|
||||
fmt.Println("Unable to parse param <n>.")
|
||||
os.Exit(1)
|
||||
}
|
||||
timerStart = time.Now()
|
||||
pipe := pipep.New()
|
||||
go migrate.Migrate(pipe, *url, *migrationsPath, relativeNInt)
|
||||
ok := writePipe(pipe)
|
||||
printTimer()
|
||||
if !ok {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
case "goto":
|
||||
verifyMigrationsPath(*migrationsPath)
|
||||
toVersion := flag.Arg(1)
|
||||
toVersionInt, err := strconv.Atoi(toVersion)
|
||||
if err != nil || toVersionInt < 0 {
|
||||
fmt.Println("Unable to parse param <v>.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
currentVersion, err := migrate.Version(*url, *migrationsPath)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
relativeNInt := toVersionInt - int(currentVersion)
|
||||
|
||||
timerStart = time.Now()
|
||||
pipe := pipep.New()
|
||||
go migrate.Migrate(pipe, *url, *migrationsPath, relativeNInt)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
ok := writePipe(pipe)
|
||||
printTimer()
|
||||
if !ok {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
case "version":
|
||||
verifyMigrationsPath(*migrationsPath)
|
||||
version, err := migrate.Version(*url, *migrationsPath)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println(version)
|
||||
|
||||
default:
|
||||
helpCmd()
|
||||
os.Exit(1)
|
||||
case "help":
|
||||
helpCmd()
|
||||
}
|
||||
}
|
||||
|
||||
func writePipe(pipe chan interface{}) (ok bool) {
|
||||
okFlag := true
|
||||
if pipe != nil {
|
||||
for {
|
||||
select {
|
||||
case item, more := <-pipe:
|
||||
if !more {
|
||||
return okFlag
|
||||
} else {
|
||||
switch item.(type) {
|
||||
|
||||
case string:
|
||||
fmt.Println(item.(string))
|
||||
|
||||
case error:
|
||||
c := color.New(color.FgRed)
|
||||
c.Println(item.(error).Error(), "\n")
|
||||
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.Print("<")
|
||||
}
|
||||
fmt.Printf(" %s\n", f.FileName)
|
||||
|
||||
default:
|
||||
text := fmt.Sprint(item)
|
||||
fmt.Println(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return okFlag
|
||||
}
|
||||
|
||||
func verifyMigrationsPath(path string) {
|
||||
if path == "" {
|
||||
fmt.Println("Please specify path")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
var timerStart time.Time
|
||||
|
||||
func printTimer() {
|
||||
diff := time.Now().Sub(timerStart).Seconds()
|
||||
if diff > 60 {
|
||||
fmt.Printf("\n%.4f minutes\n", diff/60)
|
||||
} else {
|
||||
fmt.Printf("\n%.4f seconds\n", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func helpCmd() {
|
||||
os.Stderr.WriteString(
|
||||
`usage: migrate [-path=<path>] -url=<url> <command> [<args>]
|
||||
|
||||
Commands:
|
||||
create <name> Create a new migration
|
||||
up Apply all -up- migrations
|
||||
down Apply all -down- migrations
|
||||
reset Down followed by Up
|
||||
redo Roll back most recent migration, then apply it again
|
||||
version Show current migration version
|
||||
migrate <n> Apply migrations -n|+n
|
||||
goto <v> Migrate to version v
|
||||
help Show this help
|
||||
|
||||
'-path' defaults to current working directory.
|
||||
`)
|
||||
}
|
||||
+708
@@ -0,0 +1,708 @@
|
||||
package migrate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mattes/migrate/database"
|
||||
"github.com/mattes/migrate/source"
|
||||
)
|
||||
|
||||
var DefaultPrefetchMigrations = uint(10)
|
||||
|
||||
var (
|
||||
ErrNoChange = fmt.Errorf("no change")
|
||||
ErrNilVersion = fmt.Errorf("no migration")
|
||||
ErrLocked = fmt.Errorf("database locked")
|
||||
)
|
||||
|
||||
type ErrShortLimit struct {
|
||||
Short uint
|
||||
}
|
||||
|
||||
func (e ErrShortLimit) Error() string {
|
||||
return fmt.Sprintf("limit %v short", e.Short)
|
||||
}
|
||||
|
||||
type Migrate struct {
|
||||
sourceName string
|
||||
sourceDrv source.Driver
|
||||
databaseName string
|
||||
databaseDrv database.Driver
|
||||
|
||||
Log Logger
|
||||
|
||||
GracefulStop chan bool
|
||||
isGracefulStop bool
|
||||
|
||||
isLockedMu *sync.Mutex
|
||||
isLocked bool
|
||||
|
||||
PrefetchMigrations uint
|
||||
}
|
||||
|
||||
func New(sourceUrl, databaseUrl string) (*Migrate, error) {
|
||||
m := newCommon()
|
||||
|
||||
sourceName, err := nameFromUrl(sourceUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.sourceName = sourceName
|
||||
|
||||
databaseName, err := nameFromUrl(databaseUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.databaseName = databaseName
|
||||
|
||||
sourceDrv, err := source.Open(sourceUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.sourceDrv = sourceDrv
|
||||
|
||||
databaseDrv, err := database.Open(databaseUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.databaseDrv = databaseDrv
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func NewWithDatabaseInstance(sourceUrl string, databaseName string, databaseInstance database.Driver) (*Migrate, error) {
|
||||
m := newCommon()
|
||||
|
||||
sourceName, err := nameFromUrl(sourceUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.sourceName = sourceName
|
||||
|
||||
m.databaseName = databaseName
|
||||
|
||||
sourceDrv, err := source.Open(sourceUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.sourceDrv = sourceDrv
|
||||
|
||||
m.databaseDrv = databaseInstance
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func NewWithSourceInstance(sourceName string, sourceInstance source.Driver, databaseUrl string) (*Migrate, error) {
|
||||
m := newCommon()
|
||||
|
||||
databaseName, err := nameFromUrl(databaseUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.databaseName = databaseName
|
||||
|
||||
m.sourceName = sourceName
|
||||
|
||||
databaseDrv, err := database.Open(databaseUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.databaseDrv = databaseDrv
|
||||
|
||||
m.sourceDrv = sourceInstance
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func NewWithInstance(sourceName string, sourceInstance source.Driver, databaseName string, databaseInstance database.Driver) (*Migrate, error) {
|
||||
m := newCommon()
|
||||
|
||||
m.sourceName = sourceName
|
||||
m.databaseName = databaseName
|
||||
|
||||
m.sourceDrv = sourceInstance
|
||||
m.databaseDrv = databaseInstance
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func newCommon() *Migrate {
|
||||
return &Migrate{
|
||||
GracefulStop: make(chan bool, 1),
|
||||
PrefetchMigrations: DefaultPrefetchMigrations,
|
||||
isLockedMu: &sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Migrate) Close() (sourceErr error, databaseErr error) {
|
||||
databaseSrvClose := make(chan error)
|
||||
sourceSrvClose := make(chan error)
|
||||
|
||||
go func() {
|
||||
databaseSrvClose <- m.databaseDrv.Close()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
sourceSrvClose <- m.sourceDrv.Close()
|
||||
}()
|
||||
|
||||
return <-sourceSrvClose, <-databaseSrvClose
|
||||
}
|
||||
|
||||
func (m *Migrate) Migrate(version uint) error {
|
||||
if err := m.lock(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
curVersion, err := m.databaseDrv.Version()
|
||||
if err != nil {
|
||||
return m.unlockErr(err)
|
||||
}
|
||||
|
||||
ret := make(chan interface{}, m.PrefetchMigrations)
|
||||
go m.read(curVersion, int(version), ret)
|
||||
|
||||
return m.unlockErr(m.runMigrations(ret))
|
||||
}
|
||||
|
||||
func (m *Migrate) Steps(n int) error {
|
||||
if n == 0 {
|
||||
return ErrNoChange
|
||||
}
|
||||
|
||||
if err := m.lock(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
curVersion, err := m.databaseDrv.Version()
|
||||
if err != nil {
|
||||
return m.unlockErr(err)
|
||||
}
|
||||
|
||||
ret := make(chan interface{}, m.PrefetchMigrations)
|
||||
|
||||
if n > 0 {
|
||||
go m.readUp(curVersion, n, ret)
|
||||
} else {
|
||||
go m.readDown(curVersion, -n, ret)
|
||||
}
|
||||
|
||||
return m.unlockErr(m.runMigrations(ret))
|
||||
}
|
||||
|
||||
func (m *Migrate) Up() error {
|
||||
if err := m.lock(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
curVersion, err := m.databaseDrv.Version()
|
||||
if err != nil {
|
||||
return m.unlockErr(err)
|
||||
}
|
||||
|
||||
ret := make(chan interface{}, m.PrefetchMigrations)
|
||||
|
||||
go m.readUp(curVersion, -1, ret)
|
||||
return m.unlockErr(m.runMigrations(ret))
|
||||
}
|
||||
|
||||
func (m *Migrate) Down() error {
|
||||
if err := m.lock(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
curVersion, err := m.databaseDrv.Version()
|
||||
if err != nil {
|
||||
return m.unlockErr(err)
|
||||
}
|
||||
|
||||
ret := make(chan interface{}, m.PrefetchMigrations)
|
||||
go m.readDown(curVersion, -1, ret)
|
||||
return m.unlockErr(m.runMigrations(ret))
|
||||
}
|
||||
|
||||
func (m *Migrate) Drop() error {
|
||||
if err := m.lock(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.databaseDrv.Drop(); err != nil {
|
||||
return m.unlockErr(err)
|
||||
}
|
||||
return m.unlock()
|
||||
}
|
||||
|
||||
func (m *Migrate) Version() (uint, error) {
|
||||
v, err := m.databaseDrv.Version()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if v == database.NilVersion {
|
||||
return 0, ErrNilVersion
|
||||
}
|
||||
|
||||
return suint(v), nil
|
||||
}
|
||||
|
||||
func (m *Migrate) read(from int, to int, ret chan<- interface{}) {
|
||||
defer close(ret)
|
||||
|
||||
// check if from version exists
|
||||
if from >= 0 {
|
||||
if m.versionExists(suint(from)) != nil {
|
||||
ret <- os.ErrNotExist
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// check if to version exists
|
||||
if to >= 0 {
|
||||
if m.versionExists(suint(to)) != nil {
|
||||
ret <- os.ErrNotExist
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// no change?
|
||||
if from == to {
|
||||
ret <- ErrNoChange
|
||||
return
|
||||
}
|
||||
|
||||
if from < to {
|
||||
// it's going up
|
||||
// apply first migration if from is nil version
|
||||
if from == -1 {
|
||||
firstVersion, err := m.sourceDrv.First()
|
||||
if err != nil {
|
||||
ret <- err
|
||||
return
|
||||
}
|
||||
|
||||
migr, err := m.newMigration(firstVersion, int(firstVersion))
|
||||
if err != nil {
|
||||
ret <- err
|
||||
return
|
||||
}
|
||||
|
||||
ret <- migr
|
||||
go migr.Buffer()
|
||||
from = int(firstVersion)
|
||||
}
|
||||
|
||||
// run until we reach target ...
|
||||
for from < to {
|
||||
if m.stop() {
|
||||
return
|
||||
}
|
||||
|
||||
next, err := m.sourceDrv.Next(suint(from))
|
||||
if err != nil {
|
||||
ret <- err
|
||||
return
|
||||
}
|
||||
|
||||
migr, err := m.newMigration(next, int(next))
|
||||
if err != nil {
|
||||
ret <- err
|
||||
return
|
||||
}
|
||||
|
||||
ret <- migr
|
||||
go migr.Buffer()
|
||||
from = int(next)
|
||||
}
|
||||
|
||||
} else {
|
||||
// it's going down
|
||||
// run until we reach target ...
|
||||
for from > to && from >= 0 {
|
||||
if m.stop() {
|
||||
return
|
||||
}
|
||||
|
||||
prev, err := m.sourceDrv.Prev(suint(from))
|
||||
if os.IsNotExist(err) && to == -1 {
|
||||
// apply nil migration
|
||||
migr, err := m.newMigration(suint(from), -1)
|
||||
if err != nil {
|
||||
ret <- err
|
||||
return
|
||||
}
|
||||
ret <- migr
|
||||
go migr.Buffer()
|
||||
return
|
||||
|
||||
} else if err != nil {
|
||||
ret <- err
|
||||
return
|
||||
}
|
||||
|
||||
migr, err := m.newMigration(suint(from), int(prev))
|
||||
if err != nil {
|
||||
ret <- err
|
||||
return
|
||||
}
|
||||
|
||||
ret <- migr
|
||||
go migr.Buffer()
|
||||
from = int(prev)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Migrate) readUp(from int, limit int, ret chan<- interface{}) {
|
||||
defer close(ret)
|
||||
|
||||
// check if from version exists
|
||||
if from >= 0 {
|
||||
if m.versionExists(suint(from)) != nil {
|
||||
ret <- os.ErrNotExist
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if limit == 0 {
|
||||
ret <- ErrNoChange
|
||||
return
|
||||
}
|
||||
|
||||
count := 0
|
||||
for count < limit || limit == -1 {
|
||||
if m.stop() {
|
||||
return
|
||||
}
|
||||
|
||||
// apply first migration if from is nil version
|
||||
if from == -1 {
|
||||
firstVersion, err := m.sourceDrv.First()
|
||||
if err != nil {
|
||||
ret <- err
|
||||
return
|
||||
}
|
||||
|
||||
migr, err := m.newMigration(firstVersion, int(firstVersion))
|
||||
if err != nil {
|
||||
ret <- err
|
||||
return
|
||||
}
|
||||
|
||||
ret <- migr
|
||||
go migr.Buffer()
|
||||
from = int(firstVersion)
|
||||
count++
|
||||
continue
|
||||
}
|
||||
|
||||
// apply next migration
|
||||
next, err := m.sourceDrv.Next(suint(from))
|
||||
if os.IsNotExist(err) {
|
||||
// no limit, but no migrations applied?
|
||||
if limit == -1 && count == 0 {
|
||||
ret <- ErrNoChange
|
||||
return
|
||||
}
|
||||
|
||||
// no limit, reached end
|
||||
if limit == -1 {
|
||||
return
|
||||
}
|
||||
|
||||
// reached end, and didn't apply any migrations
|
||||
if limit > 0 && count == 0 {
|
||||
ret <- os.ErrNotExist
|
||||
return
|
||||
}
|
||||
|
||||
// applied less migrations than limit?
|
||||
if count < limit {
|
||||
ret <- ErrShortLimit{suint(limit - count)}
|
||||
return
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
ret <- err
|
||||
return
|
||||
}
|
||||
|
||||
migr, err := m.newMigration(next, int(next))
|
||||
if err != nil {
|
||||
ret <- err
|
||||
return
|
||||
}
|
||||
|
||||
ret <- migr
|
||||
go migr.Buffer()
|
||||
from = int(next)
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Migrate) readDown(from int, limit int, ret chan<- interface{}) {
|
||||
defer close(ret)
|
||||
|
||||
// check if from version exists
|
||||
if from >= 0 {
|
||||
if m.versionExists(suint(from)) != nil {
|
||||
ret <- os.ErrNotExist
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if limit == 0 {
|
||||
ret <- ErrNoChange
|
||||
return
|
||||
}
|
||||
|
||||
// no change if already at nil version
|
||||
if from == -1 && limit == -1 {
|
||||
ret <- ErrNoChange
|
||||
return
|
||||
}
|
||||
|
||||
// can't go over limit if already at nil version
|
||||
if from == -1 && limit > 0 {
|
||||
ret <- os.ErrNotExist
|
||||
return
|
||||
}
|
||||
|
||||
count := 0
|
||||
for count < limit || limit == -1 {
|
||||
if m.stop() {
|
||||
return
|
||||
}
|
||||
|
||||
prev, err := m.sourceDrv.Prev(suint(from))
|
||||
if os.IsNotExist(err) {
|
||||
// no limit or haven't reached limit, apply "first" migration
|
||||
if limit == -1 || limit-count > 0 {
|
||||
firstVersion, err := m.sourceDrv.First()
|
||||
if err != nil {
|
||||
ret <- err
|
||||
return
|
||||
}
|
||||
|
||||
migr, err := m.newMigration(firstVersion, -1)
|
||||
if err != nil {
|
||||
ret <- err
|
||||
return
|
||||
}
|
||||
ret <- migr
|
||||
go migr.Buffer()
|
||||
count++
|
||||
}
|
||||
|
||||
if count < limit {
|
||||
ret <- ErrShortLimit{suint(limit - count)}
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
ret <- err
|
||||
return
|
||||
}
|
||||
|
||||
migr, err := m.newMigration(suint(from), int(prev))
|
||||
if err != nil {
|
||||
ret <- err
|
||||
return
|
||||
}
|
||||
|
||||
ret <- migr
|
||||
go migr.Buffer()
|
||||
from = int(prev)
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
// ret chan expects *Migration or error
|
||||
func (m *Migrate) runMigrations(ret <-chan interface{}) error {
|
||||
for r := range ret {
|
||||
|
||||
if m.stop() {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch r.(type) {
|
||||
case error:
|
||||
return r.(error)
|
||||
|
||||
case *Migration:
|
||||
migr := r.(*Migration)
|
||||
|
||||
if migr.Body == nil {
|
||||
m.logVerbosePrintf("Execute %v\n", migr.StringLong())
|
||||
if err := m.databaseDrv.Run(migr.TargetVersion, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
} else {
|
||||
m.logVerbosePrintf("Read and execute %v\n", migr.StringLong())
|
||||
if err := m.databaseDrv.Run(migr.TargetVersion, migr.BufferedBody); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
endTime := time.Now()
|
||||
readTime := migr.FinishedReading.Sub(migr.StartedBuffering)
|
||||
runTime := endTime.Sub(migr.FinishedReading)
|
||||
|
||||
// log either verbose or normal
|
||||
if m.Log != nil {
|
||||
if m.Log.Verbose() {
|
||||
m.logPrintf("Finished %v (read %v, ran %v)\n", migr.StringLong(), readTime, runTime)
|
||||
} else {
|
||||
m.logPrintf("%v (%v)\n", migr.StringLong(), readTime+runTime)
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
panic("unknown type")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrate) versionExists(version uint) error {
|
||||
// try up migration first
|
||||
up, _, err := m.sourceDrv.ReadUp(version)
|
||||
if err == nil {
|
||||
defer up.Close()
|
||||
}
|
||||
if os.IsExist(err) {
|
||||
return nil
|
||||
} else if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
// then try down migration
|
||||
down, _, err := m.sourceDrv.ReadDown(version)
|
||||
if err == nil {
|
||||
defer down.Close()
|
||||
}
|
||||
if os.IsExist(err) {
|
||||
return nil
|
||||
} else if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
func (m *Migrate) stop() bool {
|
||||
if m.isGracefulStop {
|
||||
return true
|
||||
}
|
||||
|
||||
select {
|
||||
case <-m.GracefulStop:
|
||||
m.isGracefulStop = true
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Migrate) newMigration(version uint, targetVersion int) (*Migration, error) {
|
||||
var migr *Migration
|
||||
|
||||
if targetVersion >= int(version) {
|
||||
r, identifier, err := m.sourceDrv.ReadUp(version)
|
||||
if os.IsNotExist(err) {
|
||||
// create "empty" migration
|
||||
migr, err = NewMigration(nil, "", version, targetVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
|
||||
} else {
|
||||
// create migration from up source
|
||||
migr, err = NewMigration(r, identifier, version, targetVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
r, identifier, err := m.sourceDrv.ReadDown(version)
|
||||
if os.IsNotExist(err) {
|
||||
// create "empty" migration
|
||||
migr, err = NewMigration(nil, "", version, targetVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
|
||||
} else {
|
||||
// create migration from down source
|
||||
migr, err = NewMigration(r, identifier, version, targetVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if m.PrefetchMigrations > 0 && migr.Body != nil {
|
||||
m.logVerbosePrintf("Start buffering %v\n", migr.StringLong())
|
||||
} else {
|
||||
m.logVerbosePrintf("Scheduled %v\n", migr.StringLong())
|
||||
}
|
||||
|
||||
return migr, nil
|
||||
}
|
||||
|
||||
func (m *Migrate) lock() error {
|
||||
m.isLockedMu.Lock()
|
||||
defer m.isLockedMu.Unlock()
|
||||
|
||||
if !m.isLocked {
|
||||
if err := m.databaseDrv.Lock(); err != nil {
|
||||
return err
|
||||
}
|
||||
m.isLocked = true
|
||||
return nil
|
||||
}
|
||||
|
||||
return ErrLocked
|
||||
}
|
||||
|
||||
func (m *Migrate) unlock() error {
|
||||
m.isLockedMu.Lock()
|
||||
defer m.isLockedMu.Unlock()
|
||||
|
||||
if err := m.databaseDrv.Unlock(); err != nil {
|
||||
// can potentially create deadlock when never succeeds
|
||||
// TODO: add timeout
|
||||
return err
|
||||
}
|
||||
|
||||
m.isLocked = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrate) unlockErr(prevErr error) error {
|
||||
if err := m.unlock(); err != nil {
|
||||
return NewMultiError(prevErr, err)
|
||||
}
|
||||
return prevErr
|
||||
}
|
||||
func (m *Migrate) logPrintf(format string, v ...interface{}) {
|
||||
if m.Log != nil {
|
||||
m.Log.Printf(format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Migrate) logVerbosePrintf(format string, v ...interface{}) {
|
||||
if m.Log != nil && m.Log.Verbose() {
|
||||
m.Log.Printf(format, v...)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
// Package direction just holds convenience constants for Up and Down migrations.
|
||||
package direction
|
||||
|
||||
type Direction int
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"github.com/mattes/migrate/migrate/direction"
|
||||
)
|
||||
|
||||
type MigrationFile struct {
|
||||
Version uint64
|
||||
UpFile *File
|
||||
DownFile *File
|
||||
}
|
||||
|
||||
type File struct {
|
||||
Path string
|
||||
FileName string
|
||||
Version uint64
|
||||
Name string
|
||||
Content []byte
|
||||
Direction direction.Direction
|
||||
}
|
||||
@@ -1,338 +0,0 @@
|
||||
// Package migrate is imported by other Go code.
|
||||
// It is the entry point to all migration functions.
|
||||
package migrate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/signal"
|
||||
"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"
|
||||
)
|
||||
|
||||
// Up applies all available migrations
|
||||
func Up(pipe chan interface{}, url, migrationsPath string) {
|
||||
d, files, version, err := initDriverAndReadMigrationFilesAndGetVersion(url, migrationsPath)
|
||||
if err != nil {
|
||||
go pipep.Close(pipe, err)
|
||||
return
|
||||
}
|
||||
|
||||
applyMigrationFiles, err := files.ToLastFrom(version)
|
||||
if 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, signals); !ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err := d.Close(); err != nil {
|
||||
pipe <- err
|
||||
}
|
||||
go pipep.Close(pipe, nil)
|
||||
return
|
||||
} else {
|
||||
if err := d.Close(); err != nil {
|
||||
pipe <- err
|
||||
}
|
||||
go pipep.Close(pipe, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// UpSync is synchronous version of Up
|
||||
func UpSync(url, migrationsPath string) (err []error, ok bool) {
|
||||
pipe := pipep.New()
|
||||
go Up(pipe, url, migrationsPath)
|
||||
err = pipep.ReadErrors(pipe)
|
||||
return err, len(err) == 0
|
||||
}
|
||||
|
||||
// Down rolls back all migrations
|
||||
func Down(pipe chan interface{}, url, migrationsPath string) {
|
||||
d, files, version, err := initDriverAndReadMigrationFilesAndGetVersion(url, migrationsPath)
|
||||
if err != nil {
|
||||
go pipep.Close(pipe, err)
|
||||
return
|
||||
}
|
||||
|
||||
applyMigrationFiles, err := files.ToFirstFrom(version)
|
||||
if 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, signals); !ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err2 := d.Close(); err2 != nil {
|
||||
pipe <- err2
|
||||
}
|
||||
go pipep.Close(pipe, nil)
|
||||
return
|
||||
} else {
|
||||
if err2 := d.Close(); err2 != nil {
|
||||
pipe <- err2
|
||||
}
|
||||
go pipep.Close(pipe, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// DownSync is synchronous version of Down
|
||||
func DownSync(url, migrationsPath string) (err []error, ok bool) {
|
||||
pipe := pipep.New()
|
||||
go Down(pipe, url, migrationsPath)
|
||||
err = pipep.ReadErrors(pipe)
|
||||
return err, len(err) == 0
|
||||
}
|
||||
|
||||
// Redo rolls back the most recently applied migration, then runs it again.
|
||||
func Redo(pipe chan interface{}, url, migrationsPath string) {
|
||||
pipe1 := pipep.New()
|
||||
go Migrate(pipe1, url, migrationsPath, -1)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// RedoSync is synchronous version of Redo
|
||||
func RedoSync(url, migrationsPath string) (err []error, ok bool) {
|
||||
pipe := pipep.New()
|
||||
go Redo(pipe, url, migrationsPath)
|
||||
err = pipep.ReadErrors(pipe)
|
||||
return err, len(err) == 0
|
||||
}
|
||||
|
||||
// Reset runs the down and up migration function
|
||||
func Reset(pipe chan interface{}, url, migrationsPath string) {
|
||||
pipe1 := pipep.New()
|
||||
go Down(pipe1, url, migrationsPath)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// ResetSync is synchronous version of Reset
|
||||
func ResetSync(url, migrationsPath string) (err []error, ok bool) {
|
||||
pipe := pipep.New()
|
||||
go Reset(pipe, url, migrationsPath)
|
||||
err = pipep.ReadErrors(pipe)
|
||||
return err, len(err) == 0
|
||||
}
|
||||
|
||||
// Migrate applies relative +n/-n migrations
|
||||
func Migrate(pipe chan interface{}, url, migrationsPath string, relativeN int) {
|
||||
d, files, version, err := initDriverAndReadMigrationFilesAndGetVersion(url, migrationsPath)
|
||||
if err != nil {
|
||||
go pipep.Close(pipe, err)
|
||||
return
|
||||
}
|
||||
|
||||
applyMigrationFiles, err := files.From(version, relativeN)
|
||||
if 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, signals); !ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// MigrateSync is synchronous version of Migrate
|
||||
func MigrateSync(url, migrationsPath string, relativeN int) (err []error, ok bool) {
|
||||
pipe := pipep.New()
|
||||
go Migrate(pipe, url, migrationsPath, relativeN)
|
||||
err = pipep.ReadErrors(pipe)
|
||||
return err, len(err) == 0
|
||||
}
|
||||
|
||||
// Version returns the current migration version
|
||||
func Version(url, migrationsPath string) (version uint64, err error) {
|
||||
d, err := driver.New(url)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
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()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
version := uint64(time.Now().Unix())
|
||||
|
||||
for _, f := range files {
|
||||
if f.Version == version {
|
||||
version++
|
||||
}
|
||||
}
|
||||
|
||||
versionStr := strconv.FormatUint(version, 10)
|
||||
|
||||
length := 10
|
||||
if len(versionStr)%length != 0 {
|
||||
versionStr = strings.Repeat("0", length-len(versionStr)%length) + versionStr
|
||||
}
|
||||
|
||||
filenamef := "%s_%s.%s.%s"
|
||||
name = strings.Replace(name, " ", "_", -1)
|
||||
|
||||
mfile := &file.MigrationFile{
|
||||
Version: version,
|
||||
UpFile: &file.File{
|
||||
Path: migrationsPath,
|
||||
FileName: fmt.Sprintf(filenamef, versionStr, name, "up", d.FilenameExtension()),
|
||||
Name: name,
|
||||
Content: []byte(""),
|
||||
Direction: direction.Up,
|
||||
},
|
||||
DownFile: &file.File{
|
||||
Path: migrationsPath,
|
||||
FileName: fmt.Sprintf(filenamef, versionStr, name, "down", d.FilenameExtension()),
|
||||
Name: name,
|
||||
Content: []byte(""),
|
||||
Direction: direction.Down,
|
||||
},
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(path.Join(mfile.UpFile.Path, mfile.UpFile.FileName), mfile.UpFile.Content, 0644); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ioutil.WriteFile(path.Join(mfile.DownFile.Path, mfile.DownFile.FileName), mfile.DownFile.Content, 0644); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mfile, nil
|
||||
}
|
||||
|
||||
// initDriverAndReadMigrationFilesAndGetVersion is a small helper
|
||||
// function that is common to most of the migration funcs
|
||||
func initDriverAndReadMigrationFilesAndGetVersion(url, migrationsPath string) (driver.Driver, *file.MigrationFiles, uint64, error) {
|
||||
d, err := driver.New(url)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
files, err := file.ReadMigrationFiles(migrationsPath, file.FilenameRegex(d.FilenameExtension()))
|
||||
if err != nil {
|
||||
d.Close() // TODO what happens with errors from this func?
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
version, err := d.Version()
|
||||
if err != nil {
|
||||
d.Close() // TODO what happens with errors from this func?
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
return d, &files, version, nil
|
||||
}
|
||||
|
||||
// NewPipe is a convenience function for pipe.New().
|
||||
// This is helpful if the user just wants to import this package and nothing else.
|
||||
func NewPipe() chan interface{} {
|
||||
return pipep.New()
|
||||
}
|
||||
|
||||
// interrupts is an internal variable that holds the state of
|
||||
// interrupt handling
|
||||
var interrupts = true
|
||||
|
||||
// Graceful enables interrupts checking. Once the first ^C is received
|
||||
// it will finish the currently running migration and abort execution
|
||||
// of the next migration. If ^C is received twice, it will stop
|
||||
// execution immediately.
|
||||
func Graceful() {
|
||||
interrupts = true
|
||||
}
|
||||
|
||||
// NonGraceful disables interrupts checking. The first received ^C will
|
||||
// stop execution immediately.
|
||||
func NonGraceful() {
|
||||
interrupts = false
|
||||
}
|
||||
|
||||
// interrupts returns a signal channel if interrupts checking is
|
||||
// enabled. nil otherwise.
|
||||
func handleInterrupts() chan os.Signal {
|
||||
if interrupts {
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,284 +0,0 @@
|
||||
package migrate
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
// Ensure imports for each driver we wish to test
|
||||
|
||||
_ "github.com/mattes/migrate/driver/postgres"
|
||||
_ "github.com/mattes/migrate/driver/sqlite3"
|
||||
_ "github.com/mattes/migrate/driver/ql"
|
||||
)
|
||||
|
||||
// Add Driver URLs here to test basic Up, Down, .. functions.
|
||||
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) {
|
||||
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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := Create(driverUrl, tmpdir, "another migration"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
files, err := ioutil.ReadDir(tmpdir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(files) != 4 {
|
||||
t.Fatal("Expected 4 new files, got", len(files))
|
||||
}
|
||||
fileNameRegexp := regexp.MustCompile(`^\d{10}_(.*.[up|down].sql)`)
|
||||
|
||||
expectFiles := []string{
|
||||
"test_migration.up.sql", "test_migration.down.sql",
|
||||
"another_migration.up.sql", "another_migration.down.sql",
|
||||
}
|
||||
|
||||
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.Errorf("expected %v files, got %v", len(expectFiles), foundCounter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReset(t *testing.T) {
|
||||
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")
|
||||
f, err := Create(driverUrl, tmpdir, "migration2")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
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) {
|
||||
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)
|
||||
|
||||
initVersion, _ := Version(driverUrl, tmpdir)
|
||||
|
||||
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, err := Version(driverUrl, tmpdir); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if version != secondMigration.Version {
|
||||
t.Fatalf("Expected version %v, got %v", version, secondMigration.Version)
|
||||
}
|
||||
|
||||
if err, ok := DownSync(driverUrl, tmpdir); !ok {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
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) {
|
||||
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)
|
||||
|
||||
initVersion, _ := Version(driverUrl, tmpdir)
|
||||
|
||||
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, err := Version(driverUrl, tmpdir); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if version != initVersion {
|
||||
t.Fatalf("Expected initial version %v, got %v", initVersion, version)
|
||||
}
|
||||
|
||||
if err, ok := UpSync(driverUrl, tmpdir); !ok {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
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) {
|
||||
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)
|
||||
|
||||
initVersion, _ := Version(driverUrl, tmpdir)
|
||||
|
||||
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, 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)
|
||||
}
|
||||
|
||||
if err, ok := RedoSync(driverUrl, tmpdir); !ok {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
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) {
|
||||
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)
|
||||
|
||||
initVersion, _ := Version(driverUrl, tmpdir)
|
||||
|
||||
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, 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)
|
||||
}
|
||||
|
||||
if err, ok := MigrateSync(driverUrl, tmpdir, -2); !ok {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if version, err := Version(driverUrl, tmpdir); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if version != 0 {
|
||||
t.Fatalf("Expected 0, got %v", version)
|
||||
}
|
||||
|
||||
if err, ok := MigrateSync(driverUrl, tmpdir, +1); !ok {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Deprecated: package migrate is here to make sure v2 is downwards compatible with v1
|
||||
package migrate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mattes/migrate"
|
||||
"github.com/mattes/migrate/migrate/file"
|
||||
)
|
||||
|
||||
var deprecatedMessage = "You are using a deprecated version of migrate, update at https://github.com/mattes/migrate"
|
||||
|
||||
func Up(pipe chan interface{}, url, migrationsPath string) {
|
||||
m, err := migrate.New("file://"+migrationsPath, url)
|
||||
if err != nil {
|
||||
pipe <- err
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.Up(); err != nil {
|
||||
pipe <- err
|
||||
}
|
||||
}
|
||||
|
||||
func UpSync(url, migrationsPath string) (err []error, ok bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func Down(pipe chan interface{}, url, migrationsPath string) {
|
||||
}
|
||||
|
||||
func DownSync(url, migrationsPath string) (err []error, ok bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func Redo(pipe chan interface{}, url, migrationsPath string) {
|
||||
}
|
||||
|
||||
func RedoSync(url, migrationsPath string) (err []error, ok bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func Reset(pipe chan interface{}, url, migrationsPath string) {
|
||||
}
|
||||
|
||||
func ResetSync(url, migrationsPath string) (err []error, ok bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func Migrate(pipe chan interface{}, url, migrationsPath string, relativeN int) {
|
||||
|
||||
}
|
||||
|
||||
func MigrateSync(url, migrationsPath string, relativeN int) (err []error, ok bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func Version(url, migrationsPath string) (version uint64, err error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func Create(url, migrationsPath, name string) (*file.MigrationFile, error) {
|
||||
return nil, fmt.Errorf(deprecatedMessage)
|
||||
}
|
||||
|
||||
func NewPipe() chan interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func Graceful() {}
|
||||
|
||||
func NonGraceful() {}
|
||||
+746
@@ -0,0 +1,746 @@
|
||||
package migrate
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
dStub "github.com/mattes/migrate/database/stub"
|
||||
"github.com/mattes/migrate/source"
|
||||
sStub "github.com/mattes/migrate/source/stub"
|
||||
)
|
||||
|
||||
// u = up migration, d = down migration, n = version
|
||||
// | 1 | - | 3 | 4 | 5 | - | 7 |
|
||||
// | u d | - | u | u d | d | - | u d |
|
||||
// var sourceStubMigrations = source.NewMigrations()
|
||||
|
||||
var sourceStubMigrations *source.Migrations
|
||||
|
||||
func init() {
|
||||
sourceStubMigrations = source.NewMigrations()
|
||||
sourceStubMigrations.Append(&source.Migration{Version: 1, Direction: source.Up})
|
||||
sourceStubMigrations.Append(&source.Migration{Version: 1, Direction: source.Down})
|
||||
sourceStubMigrations.Append(&source.Migration{Version: 3, Direction: source.Up})
|
||||
sourceStubMigrations.Append(&source.Migration{Version: 4, Direction: source.Up})
|
||||
sourceStubMigrations.Append(&source.Migration{Version: 4, Direction: source.Down})
|
||||
sourceStubMigrations.Append(&source.Migration{Version: 5, Direction: source.Down})
|
||||
sourceStubMigrations.Append(&source.Migration{Version: 7, Direction: source.Up})
|
||||
sourceStubMigrations.Append(&source.Migration{Version: 7, Direction: source.Down})
|
||||
}
|
||||
|
||||
type DummyInstance struct{ Name string }
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
m, err := New("stub://", "stub://")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if m.sourceName != "stub" {
|
||||
t.Errorf("expected stub, got %v", m.sourceName)
|
||||
}
|
||||
if m.sourceDrv == nil {
|
||||
t.Error("expected sourceDrv not to be nil")
|
||||
}
|
||||
|
||||
if m.databaseName != "stub" {
|
||||
t.Errorf("expected stub, got %v", m.databaseName)
|
||||
}
|
||||
if m.databaseDrv == nil {
|
||||
t.Error("expected databaseDrv not to be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewWithDatabaseInstance(t *testing.T) {
|
||||
dummyDb := &DummyInstance{"database"}
|
||||
dbInst, err := dStub.WithInstance(dummyDb, &dStub.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
m, err := NewWithDatabaseInstance("stub://", "stub", dbInst)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if m.sourceName != "stub" {
|
||||
t.Errorf("expected stub, got %v", m.sourceName)
|
||||
}
|
||||
if m.sourceDrv == nil {
|
||||
t.Error("expected sourceDrv not to be nil")
|
||||
}
|
||||
|
||||
if m.databaseName != "stub" {
|
||||
t.Errorf("expected stub, got %v", m.databaseName)
|
||||
}
|
||||
if m.databaseDrv == nil {
|
||||
t.Error("expected databaseDrv not to be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewWithSourceInstance(t *testing.T) {
|
||||
dummySource := &DummyInstance{"source"}
|
||||
sInst, err := sStub.WithInstance(dummySource, &sStub.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
m, err := NewWithSourceInstance("stub", sInst, "stub://")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if m.sourceName != "stub" {
|
||||
t.Errorf("expected stub, got %v", m.sourceName)
|
||||
}
|
||||
if m.sourceDrv == nil {
|
||||
t.Error("expected sourceDrv not to be nil")
|
||||
}
|
||||
|
||||
if m.databaseName != "stub" {
|
||||
t.Errorf("expected stub, got %v", m.databaseName)
|
||||
}
|
||||
if m.databaseDrv == nil {
|
||||
t.Error("expected databaseDrv not to be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewWithInstance(t *testing.T) {
|
||||
dummyDb := &DummyInstance{"database"}
|
||||
dbInst, err := dStub.WithInstance(dummyDb, &dStub.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
dummySource := &DummyInstance{"source"}
|
||||
sInst, err := sStub.WithInstance(dummySource, &sStub.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
m, err := NewWithInstance("stub", sInst, "stub", dbInst)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if m.sourceName != "stub" {
|
||||
t.Errorf("expected stub, got %v", m.sourceName)
|
||||
}
|
||||
if m.sourceDrv == nil {
|
||||
t.Error("expected sourceDrv not to be nil")
|
||||
}
|
||||
|
||||
if m.databaseName != "stub" {
|
||||
t.Errorf("expected stub, got %v", m.databaseName)
|
||||
}
|
||||
if m.databaseDrv == nil {
|
||||
t.Error("expected databaseDrv not to be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClose(t *testing.T) {
|
||||
m, _ := New("stub://", "stub://")
|
||||
sourceErr, databaseErr := m.Close()
|
||||
if sourceErr != nil {
|
||||
t.Error(sourceErr)
|
||||
}
|
||||
if databaseErr != nil {
|
||||
t.Error(databaseErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrate(t *testing.T) {
|
||||
m, _ := New("stub://", "stub://")
|
||||
m.sourceDrv.(*sStub.Stub).Migrations = sourceStubMigrations
|
||||
dbDrv := m.databaseDrv.(*dStub.Stub)
|
||||
seq := newMigSeq()
|
||||
|
||||
tt := []struct {
|
||||
version uint
|
||||
expectErr error
|
||||
expectVersion uint
|
||||
expectSeq migrationSequence
|
||||
}{
|
||||
// migrate all the way Up in single steps
|
||||
{version: 0, expectErr: os.ErrNotExist},
|
||||
{version: 1, expectErr: nil, expectVersion: 1, expectSeq: seq.add(M(1))},
|
||||
{version: 2, expectErr: os.ErrNotExist},
|
||||
{version: 3, expectErr: nil, expectVersion: 3, expectSeq: seq.add(M(3))},
|
||||
{version: 4, expectErr: nil, expectVersion: 4, expectSeq: seq.add(M(4))},
|
||||
{version: 5, expectErr: nil, expectVersion: 5, expectSeq: seq.add()}, // 5 has no up migration
|
||||
{version: 6, expectErr: os.ErrNotExist},
|
||||
{version: 7, expectErr: nil, expectVersion: 7, expectSeq: seq.add(M(7))},
|
||||
{version: 8, expectErr: os.ErrNotExist},
|
||||
|
||||
// migrate all the way Down in single steps
|
||||
{version: 6, expectErr: os.ErrNotExist},
|
||||
{version: 5, expectErr: nil, expectVersion: 5, expectSeq: seq.add(M(7, 5))},
|
||||
{version: 4, expectErr: nil, expectVersion: 4, expectSeq: seq.add(M(5, 4))},
|
||||
{version: 3, expectErr: nil, expectVersion: 3, expectSeq: seq.add(M(4, 3))},
|
||||
{version: 2, expectErr: os.ErrNotExist},
|
||||
{version: 1, expectErr: nil, expectVersion: 1, expectSeq: seq.add()}, // 3 has no down migration
|
||||
{version: 0, expectErr: os.ErrNotExist},
|
||||
|
||||
// migrate all the way Up in one step
|
||||
{version: 7, expectErr: nil, expectVersion: 7, expectSeq: seq.add(M(3), M(4), M(7))},
|
||||
|
||||
// migrate all the way Down in one step
|
||||
{version: 1, expectErr: nil, expectVersion: 1, expectSeq: seq.add(M(7, 5), M(5, 4), M(4, 3), M(3, 1))},
|
||||
|
||||
// can't migrate the same version twice
|
||||
{version: 1, expectErr: ErrNoChange},
|
||||
}
|
||||
|
||||
for i, v := range tt {
|
||||
err := m.Migrate(v.version)
|
||||
if (v.expectErr == os.ErrNotExist && !os.IsNotExist(err)) ||
|
||||
(v.expectErr != os.ErrNotExist && err != v.expectErr) {
|
||||
t.Errorf("expected err %v, got %v, in %v", v.expectErr, err, i)
|
||||
|
||||
} else if err == nil {
|
||||
version, err := m.Version()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if version != v.expectVersion {
|
||||
t.Errorf("expected version %v, got %v, in %v", v.expectVersion, version, i)
|
||||
}
|
||||
equalDbSeq(t, i, v.expectSeq, dbDrv)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSteps(t *testing.T) {
|
||||
m, _ := New("stub://", "stub://")
|
||||
m.sourceDrv.(*sStub.Stub).Migrations = sourceStubMigrations
|
||||
dbDrv := m.databaseDrv.(*dStub.Stub)
|
||||
seq := newMigSeq()
|
||||
|
||||
tt := []struct {
|
||||
n int
|
||||
expectErr error
|
||||
expectVersion int
|
||||
expectSeq migrationSequence
|
||||
}{
|
||||
// step must be != 0
|
||||
{n: 0, expectErr: ErrNoChange},
|
||||
|
||||
// can't go Down if ErrNilVersion
|
||||
{n: -1, expectErr: os.ErrNotExist},
|
||||
|
||||
// migrate all the way Up
|
||||
{n: 1, expectErr: nil, expectVersion: 1, expectSeq: seq.add(M(1))},
|
||||
{n: 1, expectErr: nil, expectVersion: 3, expectSeq: seq.add(M(3))},
|
||||
{n: 1, expectErr: nil, expectVersion: 4, expectSeq: seq.add(M(4))},
|
||||
{n: 1, expectErr: nil, expectVersion: 5, expectSeq: seq.add()},
|
||||
{n: 1, expectErr: nil, expectVersion: 7, expectSeq: seq.add(M(7))},
|
||||
{n: 1, expectErr: os.ErrNotExist},
|
||||
|
||||
// migrate all the way Down
|
||||
{n: -1, expectErr: nil, expectVersion: 5, expectSeq: seq.add(M(7, 5))},
|
||||
{n: -1, expectErr: nil, expectVersion: 4, expectSeq: seq.add(M(5, 4))},
|
||||
{n: -1, expectErr: nil, expectVersion: 3, expectSeq: seq.add(M(4, 3))},
|
||||
{n: -1, expectErr: nil, expectVersion: 1, expectSeq: seq.add(M(3, 1))},
|
||||
{n: -1, expectErr: nil, expectVersion: -1, expectSeq: seq.add(M(1, -1))},
|
||||
|
||||
// migrate Up in bigger step
|
||||
{n: 4, expectErr: nil, expectVersion: 5, expectSeq: seq.add(M(1), M(3), M(4), M(5))},
|
||||
|
||||
// apply one migration, then reaches out of boundary
|
||||
{n: 2, expectErr: ErrShortLimit{1}, expectVersion: 7, expectSeq: seq.add(M(7))},
|
||||
|
||||
// migrate Down in bigger step
|
||||
{n: -4, expectErr: nil, expectVersion: 1, expectSeq: seq.add(M(7, 5), M(5, 4), M(4, 3), M(3, 1))},
|
||||
|
||||
// apply one migration, then reaches out of boundary
|
||||
{n: -2, expectErr: ErrShortLimit{1}, expectVersion: -1, expectSeq: seq.add(M(1, -1))},
|
||||
}
|
||||
|
||||
for i, v := range tt {
|
||||
err := m.Steps(v.n)
|
||||
if (v.expectErr == os.ErrNotExist && !os.IsNotExist(err)) ||
|
||||
(v.expectErr != os.ErrNotExist && err != v.expectErr) {
|
||||
t.Errorf("expected err %v, got %v, in %v", v.expectErr, err, i)
|
||||
|
||||
} else if err == nil {
|
||||
version, err := m.Version()
|
||||
if err != ErrNilVersion && err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if v.expectVersion == -1 && err != ErrNilVersion {
|
||||
t.Errorf("expected ErrNilVersion, got %v, in %v", version, i)
|
||||
|
||||
} else if v.expectVersion >= 0 && version != uint(v.expectVersion) {
|
||||
t.Errorf("expected version %v, got %v, in %v", v.expectVersion, version, i)
|
||||
}
|
||||
equalDbSeq(t, i, v.expectSeq, dbDrv)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpAndDown(t *testing.T) {
|
||||
m, _ := New("stub://", "stub://")
|
||||
m.sourceDrv.(*sStub.Stub).Migrations = sourceStubMigrations
|
||||
dbDrv := m.databaseDrv.(*dStub.Stub)
|
||||
seq := newMigSeq()
|
||||
|
||||
// go Up first
|
||||
if err := m.Up(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
equalDbSeq(t, 0, seq.add(M(1), M(3), M(4), M(5), M(7)), dbDrv)
|
||||
|
||||
// go Down
|
||||
if err := m.Down(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
equalDbSeq(t, 1, seq.add(M(7, 5), M(5, 4), M(4, 3), M(3, 1), M(1, -1)), dbDrv)
|
||||
|
||||
// go 1 Up and then all the way Up
|
||||
if err := m.Steps(1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m.Up(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
equalDbSeq(t, 2, seq.add(M(1), M(3), M(4), M(5), M(7)), dbDrv)
|
||||
|
||||
// go 1 Down and then all the way Down
|
||||
if err := m.Steps(-1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m.Down(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
equalDbSeq(t, 0, seq.add(M(7, 5), M(5, 4), M(4, 3), M(3, 1), M(1, -1)), dbDrv)
|
||||
}
|
||||
|
||||
func TestDrop(t *testing.T) {
|
||||
m, _ := New("stub://", "stub://")
|
||||
m.sourceDrv.(*sStub.Stub).Migrations = sourceStubMigrations
|
||||
dbDrv := m.databaseDrv.(*dStub.Stub)
|
||||
|
||||
if err := m.Drop(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if dbDrv.MigrationSequence[len(dbDrv.MigrationSequence)-1] != dStub.DROP {
|
||||
t.Fatalf("expected database to DROP, got sequence %v", dbDrv.MigrationSequence)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersion(t *testing.T) {
|
||||
m, _ := New("stub://", "stub://")
|
||||
dbDrv := m.databaseDrv.(*dStub.Stub)
|
||||
|
||||
_, err := m.Version()
|
||||
if err != ErrNilVersion {
|
||||
t.Fatalf("expected ErrNilVersion, got %v", err)
|
||||
}
|
||||
|
||||
if err := dbDrv.Run(1, bytes.NewBufferString("1_up")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
v, err := m.Version()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if v != 1 {
|
||||
t.Fatalf("expected version 1, got %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRead(t *testing.T) {
|
||||
m, _ := New("stub://", "stub://")
|
||||
m.sourceDrv.(*sStub.Stub).Migrations = sourceStubMigrations
|
||||
|
||||
tt := []struct {
|
||||
from int
|
||||
to int
|
||||
expectErr error
|
||||
expectMigrations migrationSequence
|
||||
}{
|
||||
{from: -1, to: -1, expectErr: ErrNoChange},
|
||||
{from: -1, to: 0, expectErr: os.ErrNotExist},
|
||||
{from: -1, to: 1, expectErr: nil, expectMigrations: newMigSeq(M(1))},
|
||||
{from: -1, to: 2, expectErr: os.ErrNotExist},
|
||||
{from: -1, to: 3, expectErr: nil, expectMigrations: newMigSeq(M(1), M(3))},
|
||||
{from: -1, to: 4, expectErr: nil, expectMigrations: newMigSeq(M(1), M(3), M(4))},
|
||||
{from: -1, to: 5, expectErr: nil, expectMigrations: newMigSeq(M(1), M(3), M(4), M(5))},
|
||||
{from: -1, to: 6, expectErr: os.ErrNotExist},
|
||||
{from: -1, to: 7, expectErr: nil, expectMigrations: newMigSeq(M(1), M(3), M(4), M(5), M(7))},
|
||||
{from: -1, to: 8, expectErr: os.ErrNotExist},
|
||||
|
||||
{from: 0, to: -1, expectErr: os.ErrNotExist},
|
||||
{from: 0, to: 0, expectErr: os.ErrNotExist},
|
||||
{from: 0, to: 1, expectErr: os.ErrNotExist},
|
||||
{from: 0, to: 2, expectErr: os.ErrNotExist},
|
||||
{from: 0, to: 3, expectErr: os.ErrNotExist},
|
||||
{from: 0, to: 4, expectErr: os.ErrNotExist},
|
||||
{from: 0, to: 5, expectErr: os.ErrNotExist},
|
||||
{from: 0, to: 6, expectErr: os.ErrNotExist},
|
||||
{from: 0, to: 7, expectErr: os.ErrNotExist},
|
||||
{from: 0, to: 8, expectErr: os.ErrNotExist},
|
||||
|
||||
{from: 1, to: -1, expectErr: nil, expectMigrations: newMigSeq(M(1, -1))},
|
||||
{from: 1, to: 0, expectErr: os.ErrNotExist},
|
||||
{from: 1, to: 1, expectErr: ErrNoChange},
|
||||
{from: 1, to: 2, expectErr: os.ErrNotExist},
|
||||
{from: 1, to: 3, expectErr: nil, expectMigrations: newMigSeq(M(3))},
|
||||
{from: 1, to: 4, expectErr: nil, expectMigrations: newMigSeq(M(3), M(4))},
|
||||
{from: 1, to: 5, expectErr: nil, expectMigrations: newMigSeq(M(3), M(4), M(5))},
|
||||
{from: 1, to: 6, expectErr: os.ErrNotExist},
|
||||
{from: 1, to: 7, expectErr: nil, expectMigrations: newMigSeq(M(3), M(4), M(5), M(7))},
|
||||
{from: 1, to: 8, expectErr: os.ErrNotExist},
|
||||
|
||||
{from: 2, to: -1, expectErr: os.ErrNotExist},
|
||||
{from: 2, to: 0, expectErr: os.ErrNotExist},
|
||||
{from: 2, to: 1, expectErr: os.ErrNotExist},
|
||||
{from: 2, to: 2, expectErr: os.ErrNotExist},
|
||||
{from: 2, to: 3, expectErr: os.ErrNotExist},
|
||||
{from: 2, to: 4, expectErr: os.ErrNotExist},
|
||||
{from: 2, to: 5, expectErr: os.ErrNotExist},
|
||||
{from: 2, to: 6, expectErr: os.ErrNotExist},
|
||||
{from: 2, to: 7, expectErr: os.ErrNotExist},
|
||||
{from: 2, to: 8, expectErr: os.ErrNotExist},
|
||||
|
||||
{from: 3, to: -1, expectErr: nil, expectMigrations: newMigSeq(M(3, 1), M(1, -1))},
|
||||
{from: 3, to: 0, expectErr: os.ErrNotExist},
|
||||
{from: 3, to: 1, expectErr: nil, expectMigrations: newMigSeq(M(3, 1))},
|
||||
{from: 3, to: 2, expectErr: os.ErrNotExist},
|
||||
{from: 3, to: 3, expectErr: ErrNoChange},
|
||||
{from: 3, to: 4, expectErr: nil, expectMigrations: newMigSeq(M(4))},
|
||||
{from: 3, to: 5, expectErr: nil, expectMigrations: newMigSeq(M(4), M(5))},
|
||||
{from: 3, to: 6, expectErr: os.ErrNotExist},
|
||||
{from: 3, to: 7, expectErr: nil, expectMigrations: newMigSeq(M(4), M(5), M(7))},
|
||||
{from: 3, to: 8, expectErr: os.ErrNotExist},
|
||||
|
||||
{from: 4, to: -1, expectErr: nil, expectMigrations: newMigSeq(M(4, 3), M(3, 1), M(1, -1))},
|
||||
{from: 4, to: 0, expectErr: os.ErrNotExist},
|
||||
{from: 4, to: 1, expectErr: nil, expectMigrations: newMigSeq(M(4, 3), M(3, 1))},
|
||||
{from: 4, to: 2, expectErr: os.ErrNotExist},
|
||||
{from: 4, to: 3, expectErr: nil, expectMigrations: newMigSeq(M(4, 3))},
|
||||
{from: 4, to: 4, expectErr: ErrNoChange},
|
||||
{from: 4, to: 5, expectErr: nil, expectMigrations: newMigSeq(M(5))},
|
||||
{from: 4, to: 6, expectErr: os.ErrNotExist},
|
||||
{from: 4, to: 7, expectErr: nil, expectMigrations: newMigSeq(M(5), M(7))},
|
||||
{from: 4, to: 8, expectErr: os.ErrNotExist},
|
||||
|
||||
{from: 5, to: -1, expectErr: nil, expectMigrations: newMigSeq(M(5, 4), M(4, 3), M(3, 1), M(1, -1))},
|
||||
{from: 5, to: 0, expectErr: os.ErrNotExist},
|
||||
{from: 5, to: 1, expectErr: nil, expectMigrations: newMigSeq(M(5, 4), M(4, 3), M(3, 1))},
|
||||
{from: 5, to: 2, expectErr: os.ErrNotExist},
|
||||
{from: 5, to: 3, expectErr: nil, expectMigrations: newMigSeq(M(5, 4), M(4, 3))},
|
||||
{from: 5, to: 4, expectErr: nil, expectMigrations: newMigSeq(M(5, 4))},
|
||||
{from: 5, to: 5, expectErr: ErrNoChange},
|
||||
{from: 5, to: 6, expectErr: os.ErrNotExist},
|
||||
{from: 5, to: 7, expectErr: nil, expectMigrations: newMigSeq(M(7))},
|
||||
{from: 5, to: 8, expectErr: os.ErrNotExist},
|
||||
|
||||
{from: 6, to: -1, expectErr: os.ErrNotExist},
|
||||
{from: 6, to: 0, expectErr: os.ErrNotExist},
|
||||
{from: 6, to: 1, expectErr: os.ErrNotExist},
|
||||
{from: 6, to: 2, expectErr: os.ErrNotExist},
|
||||
{from: 6, to: 3, expectErr: os.ErrNotExist},
|
||||
{from: 6, to: 4, expectErr: os.ErrNotExist},
|
||||
{from: 6, to: 5, expectErr: os.ErrNotExist},
|
||||
{from: 6, to: 6, expectErr: os.ErrNotExist},
|
||||
{from: 6, to: 7, expectErr: os.ErrNotExist},
|
||||
{from: 6, to: 8, expectErr: os.ErrNotExist},
|
||||
|
||||
{from: 7, to: -1, expectErr: nil, expectMigrations: newMigSeq(M(7, 5), M(5, 4), M(4, 3), M(3, 1), M(1, -1))},
|
||||
{from: 7, to: 0, expectErr: os.ErrNotExist},
|
||||
{from: 7, to: 1, expectErr: nil, expectMigrations: newMigSeq(M(7, 5), M(5, 4), M(4, 3), M(3, 1))},
|
||||
{from: 7, to: 2, expectErr: os.ErrNotExist},
|
||||
{from: 7, to: 3, expectErr: nil, expectMigrations: newMigSeq(M(7, 5), M(5, 4), M(4, 3))},
|
||||
{from: 7, to: 4, expectErr: nil, expectMigrations: newMigSeq(M(7, 5), M(5, 4))},
|
||||
{from: 7, to: 5, expectErr: nil, expectMigrations: newMigSeq(M(7, 5))},
|
||||
{from: 7, to: 6, expectErr: os.ErrNotExist},
|
||||
{from: 7, to: 7, expectErr: ErrNoChange},
|
||||
{from: 7, to: 8, expectErr: os.ErrNotExist},
|
||||
|
||||
{from: 8, to: -1, expectErr: os.ErrNotExist},
|
||||
{from: 8, to: 0, expectErr: os.ErrNotExist},
|
||||
{from: 8, to: 1, expectErr: os.ErrNotExist},
|
||||
{from: 8, to: 2, expectErr: os.ErrNotExist},
|
||||
{from: 8, to: 3, expectErr: os.ErrNotExist},
|
||||
{from: 8, to: 4, expectErr: os.ErrNotExist},
|
||||
{from: 8, to: 5, expectErr: os.ErrNotExist},
|
||||
{from: 8, to: 6, expectErr: os.ErrNotExist},
|
||||
{from: 8, to: 7, expectErr: os.ErrNotExist},
|
||||
{from: 8, to: 8, expectErr: os.ErrNotExist},
|
||||
}
|
||||
|
||||
for i, v := range tt {
|
||||
ret := make(chan interface{})
|
||||
go m.read(v.from, v.to, ret)
|
||||
migrations, err := migrationsFromChannel(ret)
|
||||
|
||||
if (v.expectErr == os.ErrNotExist && !os.IsNotExist(err)) ||
|
||||
(v.expectErr != os.ErrNotExist && v.expectErr != err) {
|
||||
t.Errorf("expected %v, got %v, in %v", v.expectErr, err, i)
|
||||
t.Logf("%v, in %v", migrations, i)
|
||||
}
|
||||
if len(v.expectMigrations) > 0 {
|
||||
equalMigSeq(t, i, v.expectMigrations, migrations)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadUp(t *testing.T) {
|
||||
m, _ := New("stub://", "stub://")
|
||||
m.sourceDrv.(*sStub.Stub).Migrations = sourceStubMigrations
|
||||
|
||||
tt := []struct {
|
||||
from int
|
||||
limit int // -1 means no limit
|
||||
expectErr error
|
||||
expectMigrations migrationSequence
|
||||
}{
|
||||
{from: -1, limit: -1, expectErr: nil, expectMigrations: newMigSeq(M(1), M(3), M(4), M(5), M(7))},
|
||||
{from: -1, limit: 0, expectErr: ErrNoChange},
|
||||
{from: -1, limit: 1, expectErr: nil, expectMigrations: newMigSeq(M(1))},
|
||||
{from: -1, limit: 2, expectErr: nil, expectMigrations: newMigSeq(M(1), M(3))},
|
||||
|
||||
{from: 0, limit: -1, expectErr: os.ErrNotExist},
|
||||
{from: 0, limit: 0, expectErr: os.ErrNotExist},
|
||||
{from: 0, limit: 1, expectErr: os.ErrNotExist},
|
||||
{from: 0, limit: 2, expectErr: os.ErrNotExist},
|
||||
|
||||
{from: 1, limit: -1, expectErr: nil, expectMigrations: newMigSeq(M(3), M(4), M(5), M(7))},
|
||||
{from: 1, limit: 0, expectErr: ErrNoChange},
|
||||
{from: 1, limit: 1, expectErr: nil, expectMigrations: newMigSeq(M(3))},
|
||||
{from: 1, limit: 2, expectErr: nil, expectMigrations: newMigSeq(M(3), M(4))},
|
||||
|
||||
{from: 2, limit: -1, expectErr: os.ErrNotExist},
|
||||
{from: 2, limit: 0, expectErr: os.ErrNotExist},
|
||||
{from: 2, limit: 1, expectErr: os.ErrNotExist},
|
||||
{from: 2, limit: 2, expectErr: os.ErrNotExist},
|
||||
|
||||
{from: 3, limit: -1, expectErr: nil, expectMigrations: newMigSeq(M(4), M(5), M(7))},
|
||||
{from: 3, limit: 0, expectErr: ErrNoChange},
|
||||
{from: 3, limit: 1, expectErr: nil, expectMigrations: newMigSeq(M(4))},
|
||||
{from: 3, limit: 2, expectErr: nil, expectMigrations: newMigSeq(M(4), M(5))},
|
||||
|
||||
{from: 4, limit: -1, expectErr: nil, expectMigrations: newMigSeq(M(5), M(7))},
|
||||
{from: 4, limit: 0, expectErr: ErrNoChange},
|
||||
{from: 4, limit: 1, expectErr: nil, expectMigrations: newMigSeq(M(5))},
|
||||
{from: 4, limit: 2, expectErr: nil, expectMigrations: newMigSeq(M(5), M(7))},
|
||||
|
||||
{from: 5, limit: -1, expectErr: nil, expectMigrations: newMigSeq(M(7))},
|
||||
{from: 5, limit: 0, expectErr: ErrNoChange},
|
||||
{from: 5, limit: 1, expectErr: nil, expectMigrations: newMigSeq(M(7))},
|
||||
{from: 5, limit: 2, expectErr: ErrShortLimit{1}, expectMigrations: newMigSeq(M(7))},
|
||||
|
||||
{from: 6, limit: -1, expectErr: os.ErrNotExist},
|
||||
{from: 6, limit: 0, expectErr: os.ErrNotExist},
|
||||
{from: 6, limit: 1, expectErr: os.ErrNotExist},
|
||||
{from: 6, limit: 2, expectErr: os.ErrNotExist},
|
||||
|
||||
{from: 7, limit: -1, expectErr: ErrNoChange},
|
||||
{from: 7, limit: 0, expectErr: ErrNoChange},
|
||||
{from: 7, limit: 1, expectErr: os.ErrNotExist},
|
||||
{from: 7, limit: 2, expectErr: os.ErrNotExist},
|
||||
|
||||
{from: 8, limit: -1, expectErr: os.ErrNotExist},
|
||||
{from: 8, limit: 0, expectErr: os.ErrNotExist},
|
||||
{from: 8, limit: 1, expectErr: os.ErrNotExist},
|
||||
{from: 8, limit: 2, expectErr: os.ErrNotExist},
|
||||
}
|
||||
|
||||
for i, v := range tt {
|
||||
ret := make(chan interface{})
|
||||
go m.readUp(v.from, v.limit, ret)
|
||||
migrations, err := migrationsFromChannel(ret)
|
||||
|
||||
if (v.expectErr == os.ErrNotExist && !os.IsNotExist(err)) ||
|
||||
(v.expectErr != os.ErrNotExist && v.expectErr != err) {
|
||||
t.Errorf("expected %v, got %v, in %v", v.expectErr, err, i)
|
||||
t.Logf("%v, in %v", migrations, i)
|
||||
}
|
||||
if len(v.expectMigrations) > 0 {
|
||||
equalMigSeq(t, i, v.expectMigrations, migrations)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadDown(t *testing.T) {
|
||||
m, _ := New("stub://", "stub://")
|
||||
m.sourceDrv.(*sStub.Stub).Migrations = sourceStubMigrations
|
||||
|
||||
tt := []struct {
|
||||
from int
|
||||
limit int // -1 means no limit
|
||||
expectErr error
|
||||
expectMigrations migrationSequence
|
||||
}{
|
||||
{from: -1, limit: -1, expectErr: ErrNoChange},
|
||||
{from: -1, limit: 0, expectErr: ErrNoChange},
|
||||
{from: -1, limit: 1, expectErr: os.ErrNotExist},
|
||||
{from: -1, limit: 2, expectErr: os.ErrNotExist},
|
||||
|
||||
{from: 0, limit: -1, expectErr: os.ErrNotExist},
|
||||
{from: 0, limit: 0, expectErr: os.ErrNotExist},
|
||||
{from: 0, limit: 1, expectErr: os.ErrNotExist},
|
||||
{from: 0, limit: 2, expectErr: os.ErrNotExist},
|
||||
|
||||
{from: 1, limit: -1, expectErr: nil, expectMigrations: newMigSeq(M(1, -1))},
|
||||
{from: 1, limit: 0, expectErr: ErrNoChange},
|
||||
{from: 1, limit: 1, expectErr: nil, expectMigrations: newMigSeq(M(1, -1))},
|
||||
{from: 1, limit: 2, expectErr: ErrShortLimit{1}, expectMigrations: newMigSeq(M(1, -1))},
|
||||
|
||||
{from: 2, limit: -1, expectErr: os.ErrNotExist},
|
||||
{from: 2, limit: 0, expectErr: os.ErrNotExist},
|
||||
{from: 2, limit: 1, expectErr: os.ErrNotExist},
|
||||
{from: 2, limit: 2, expectErr: os.ErrNotExist},
|
||||
|
||||
{from: 3, limit: -1, expectErr: nil, expectMigrations: newMigSeq(M(3, 1), M(1, -1))},
|
||||
{from: 3, limit: 0, expectErr: ErrNoChange},
|
||||
{from: 3, limit: 1, expectErr: nil, expectMigrations: newMigSeq(M(3, 1))},
|
||||
{from: 3, limit: 2, expectErr: nil, expectMigrations: newMigSeq(M(3, 1), M(1, -1))},
|
||||
|
||||
{from: 4, limit: -1, expectErr: nil, expectMigrations: newMigSeq(M(4, 3), M(3, 1), M(1, -1))},
|
||||
{from: 4, limit: 0, expectErr: ErrNoChange},
|
||||
{from: 4, limit: 1, expectErr: nil, expectMigrations: newMigSeq(M(4, 3))},
|
||||
{from: 4, limit: 2, expectErr: nil, expectMigrations: newMigSeq(M(4, 3), M(3, 1))},
|
||||
|
||||
{from: 5, limit: -1, expectErr: nil, expectMigrations: newMigSeq(M(5, 4), M(4, 3), M(3, 1), M(1, -1))},
|
||||
{from: 5, limit: 0, expectErr: ErrNoChange},
|
||||
{from: 5, limit: 1, expectErr: nil, expectMigrations: newMigSeq(M(5, 4))},
|
||||
{from: 5, limit: 2, expectErr: nil, expectMigrations: newMigSeq(M(5, 4), M(4, 3))},
|
||||
|
||||
{from: 6, limit: -1, expectErr: os.ErrNotExist},
|
||||
{from: 6, limit: 0, expectErr: os.ErrNotExist},
|
||||
{from: 6, limit: 1, expectErr: os.ErrNotExist},
|
||||
{from: 6, limit: 2, expectErr: os.ErrNotExist},
|
||||
|
||||
{from: 7, limit: -1, expectErr: nil, expectMigrations: newMigSeq(M(7, 5), M(5, 4), M(4, 3), M(3, 1), M(1, -1))},
|
||||
{from: 7, limit: 0, expectErr: ErrNoChange},
|
||||
{from: 7, limit: 1, expectErr: nil, expectMigrations: newMigSeq(M(7, 5))},
|
||||
{from: 7, limit: 2, expectErr: nil, expectMigrations: newMigSeq(M(7, 5), M(5, 4))},
|
||||
|
||||
{from: 8, limit: -1, expectErr: os.ErrNotExist},
|
||||
{from: 8, limit: 0, expectErr: os.ErrNotExist},
|
||||
{from: 8, limit: 1, expectErr: os.ErrNotExist},
|
||||
{from: 8, limit: 2, expectErr: os.ErrNotExist},
|
||||
}
|
||||
|
||||
for i, v := range tt {
|
||||
ret := make(chan interface{})
|
||||
go m.readDown(v.from, v.limit, ret)
|
||||
migrations, err := migrationsFromChannel(ret)
|
||||
|
||||
if (v.expectErr == os.ErrNotExist && !os.IsNotExist(err)) ||
|
||||
(v.expectErr != os.ErrNotExist && v.expectErr != err) {
|
||||
t.Errorf("expected %v, got %v, in %v", v.expectErr, err, i)
|
||||
t.Logf("%v, in %v", migrations, i)
|
||||
}
|
||||
if len(v.expectMigrations) > 0 {
|
||||
equalMigSeq(t, i, v.expectMigrations, migrations)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLock(t *testing.T) {
|
||||
m, _ := New("stub://", "stub://")
|
||||
if err := m.lock(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := m.lock(); err == nil {
|
||||
t.Fatal("should be locked already")
|
||||
}
|
||||
}
|
||||
|
||||
func migrationsFromChannel(ret chan interface{}) ([]*Migration, error) {
|
||||
slice := make([]*Migration, 0)
|
||||
for r := range ret {
|
||||
switch r.(type) {
|
||||
case error:
|
||||
return slice, r.(error)
|
||||
|
||||
case *Migration:
|
||||
slice = append(slice, r.(*Migration))
|
||||
}
|
||||
}
|
||||
return slice, nil
|
||||
}
|
||||
|
||||
type migrationSequence []*Migration
|
||||
|
||||
func newMigSeq(migr ...*Migration) migrationSequence {
|
||||
return migr
|
||||
}
|
||||
|
||||
func (m *migrationSequence) add(migr ...*Migration) migrationSequence {
|
||||
*m = append(*m, migr...)
|
||||
return *m
|
||||
}
|
||||
|
||||
func (m *migrationSequence) bodySequence() []string {
|
||||
r := make([]string, 0)
|
||||
for _, v := range *m {
|
||||
if v.Body != nil {
|
||||
body, err := ioutil.ReadAll(v.Body)
|
||||
if err != nil {
|
||||
panic(err) // that should never happen
|
||||
}
|
||||
|
||||
// reset body reader
|
||||
// TODO: is there a better/nicer way?
|
||||
v.Body = ioutil.NopCloser(bytes.NewReader(body))
|
||||
|
||||
r = append(r, string(body[:]))
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// M is a convenience func to create a new *Migration
|
||||
func M(version uint, targetVersion ...int) *Migration {
|
||||
if len(targetVersion) > 1 {
|
||||
panic("only one targetVersion allowed")
|
||||
}
|
||||
_ = fmt.Sprintf("")
|
||||
ts := int(version)
|
||||
if len(targetVersion) == 1 {
|
||||
ts = targetVersion[0]
|
||||
}
|
||||
|
||||
m, _ := New("stub://", "stub://")
|
||||
m.sourceDrv.(*sStub.Stub).Migrations = sourceStubMigrations
|
||||
migr, err := m.newMigration(version, ts)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return migr
|
||||
}
|
||||
|
||||
func equalMigSeq(t *testing.T, i int, expected, got migrationSequence) {
|
||||
if len(expected) != len(got) {
|
||||
t.Errorf("expected migrations %v, got %v, in %v", expected, got, i)
|
||||
|
||||
} else {
|
||||
for ii := 0; ii < len(expected); ii++ {
|
||||
if expected[ii].Version != got[ii].Version {
|
||||
t.Errorf("expected version %v, got %v, in %v", expected[ii].Version, got[ii].Version, i)
|
||||
}
|
||||
|
||||
if expected[ii].TargetVersion != got[ii].TargetVersion {
|
||||
t.Errorf("expected targetVersion %v, got %v, in %v", expected[ii].TargetVersion, got[ii].TargetVersion, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func equalDbSeq(t *testing.T, i int, expected migrationSequence, got *dStub.Stub) {
|
||||
bs := expected.bodySequence()
|
||||
if !got.EqualSequence(bs) {
|
||||
t.Fatalf("\nexpected sequence %v,\ngot %v, in %v", bs, got.MigrationSequence, i)
|
||||
}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
package migrate
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
var DefaultBufferSize = uint(100000)
|
||||
|
||||
type Migration struct {
|
||||
Identifier string
|
||||
Version uint
|
||||
TargetVersion int
|
||||
|
||||
Body io.ReadCloser
|
||||
BufferedBody io.Reader
|
||||
BufferSize uint
|
||||
bufferWriter io.WriteCloser
|
||||
|
||||
Scheduled time.Time
|
||||
StartedBuffering time.Time
|
||||
FinishedBuffering time.Time
|
||||
FinishedReading time.Time
|
||||
BytesRead int64
|
||||
}
|
||||
|
||||
func NewMigration(body io.ReadCloser, identifier string, version uint, targetVersion int) (*Migration, error) {
|
||||
tnow := time.Now()
|
||||
m := &Migration{
|
||||
Identifier: identifier,
|
||||
Version: version,
|
||||
TargetVersion: targetVersion,
|
||||
Scheduled: tnow,
|
||||
}
|
||||
|
||||
if body == nil {
|
||||
if len(identifier) == 0 {
|
||||
m.Identifier = "<empty>"
|
||||
}
|
||||
|
||||
m.StartedBuffering = tnow
|
||||
m.FinishedBuffering = tnow
|
||||
m.FinishedReading = tnow
|
||||
return m, nil
|
||||
}
|
||||
|
||||
br, bw := io.Pipe()
|
||||
m.Body = body // want to simulate low latency? newSlowReader(body)
|
||||
m.BufferSize = DefaultBufferSize
|
||||
m.BufferedBody = br
|
||||
m.bufferWriter = bw
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Migration) String() string {
|
||||
return fmt.Sprintf("%v [%v=>%v]", m.Identifier, m.Version, m.TargetVersion)
|
||||
}
|
||||
|
||||
func (m *Migration) StringLong() string {
|
||||
directionStr := "u"
|
||||
if m.TargetVersion < int(m.Version) {
|
||||
directionStr = "d"
|
||||
}
|
||||
return fmt.Sprintf("%v/%v %v", m.Version, directionStr, m.Identifier)
|
||||
}
|
||||
|
||||
// Buffer buffers up to BufferSize (blocking, call with goroutine)
|
||||
func (m *Migration) Buffer() error {
|
||||
if m.Body == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
m.StartedBuffering = time.Now()
|
||||
|
||||
b := bufio.NewReaderSize(m.Body, int(m.BufferSize))
|
||||
|
||||
// start reading from body, peek won't move the read pointer though
|
||||
// poor man's solution?
|
||||
b.Peek(int(m.BufferSize))
|
||||
|
||||
m.FinishedBuffering = time.Now()
|
||||
|
||||
// write to bufferWriter, this will block until
|
||||
// something starts reading from m.Buffer
|
||||
n, err := b.WriteTo(m.bufferWriter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.FinishedReading = time.Now()
|
||||
m.BytesRead = n
|
||||
|
||||
// close bufferWriter so Buffer knows that there is no
|
||||
// more data coming
|
||||
m.bufferWriter.Close()
|
||||
|
||||
// it's safe to close the Body too
|
||||
m.Body.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
// Package pipe has functions for pipe channel handling.
|
||||
package pipe
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
)
|
||||
|
||||
// New creates a new pipe. A pipe is basically a channel.
|
||||
func New() chan interface{} {
|
||||
return make(chan interface{}, 0)
|
||||
}
|
||||
|
||||
// Close closes a pipe and optionally sends an error
|
||||
func Close(pipe chan interface{}, err error) {
|
||||
if err != nil {
|
||||
pipe <- err
|
||||
}
|
||||
close(pipe)
|
||||
}
|
||||
|
||||
// WaitAndRedirect waits for pipe to be closed and
|
||||
// redirects all messages from pipe to redirectPipe
|
||||
// while it waits. It also checks if there was an
|
||||
// interrupt send and will quit gracefully if yes.
|
||||
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
|
||||
if interruptsReceived > 1 {
|
||||
os.Exit(5)
|
||||
} else {
|
||||
// add white space at beginning for ^C splitting
|
||||
redirectPipe <- " Aborting after this migration ... Hit again to force quit."
|
||||
}
|
||||
|
||||
case item, ok := <-pipe:
|
||||
if !ok {
|
||||
return !errorReceived && interruptsReceived == 0
|
||||
} else {
|
||||
redirectPipe <- item
|
||||
switch item.(type) {
|
||||
case error:
|
||||
errorReceived = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return !errorReceived && interruptsReceived == 0
|
||||
}
|
||||
|
||||
// ReadErrors selects all received errors and returns them.
|
||||
// This is helpful for synchronous migration functions.
|
||||
func ReadErrors(pipe chan interface{}) []error {
|
||||
err := make([]error, 0)
|
||||
if pipe != nil {
|
||||
for {
|
||||
select {
|
||||
case item, ok := <-pipe:
|
||||
if !ok {
|
||||
return err
|
||||
} else {
|
||||
switch item.(type) {
|
||||
case error:
|
||||
err = append(err, item.(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func stopNotifyInterruptChannel(interruptChannel chan os.Signal) {
|
||||
if interruptChannel != nil {
|
||||
signal.Stop(interruptChannel)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
nurl "net/url"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var driversMu sync.RWMutex
|
||||
var drivers = make(map[string]Driver)
|
||||
|
||||
type Driver interface {
|
||||
Open(url string) (Driver, error)
|
||||
|
||||
Close() error
|
||||
|
||||
First() (version uint, err error)
|
||||
|
||||
Prev(version uint) (prevVersion uint, err error)
|
||||
|
||||
Next(version uint) (nextVersion uint, err error)
|
||||
|
||||
ReadUp(version uint) (r io.ReadCloser, identifier string, err error)
|
||||
|
||||
ReadDown(version uint) (r io.ReadCloser, identifier string, err error)
|
||||
}
|
||||
|
||||
func Open(url string) (Driver, error) {
|
||||
u, err := nurl.Parse(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if u.Scheme == "" {
|
||||
return nil, fmt.Errorf("source driver: invalid URL scheme")
|
||||
}
|
||||
|
||||
driversMu.RLock()
|
||||
d, ok := drivers[u.Scheme]
|
||||
driversMu.RUnlock()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("source driver: unknown driver %v (forgotton import?)", u.Scheme)
|
||||
}
|
||||
|
||||
return d.Open(url)
|
||||
}
|
||||
|
||||
func Register(name string, driver Driver) {
|
||||
driversMu.Lock()
|
||||
defer driversMu.Unlock()
|
||||
if driver == nil {
|
||||
panic("Register driver is nil")
|
||||
}
|
||||
if _, dup := drivers[name]; dup {
|
||||
panic("Register called twice for driver " + name)
|
||||
}
|
||||
drivers[name] = driver
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
nurl "net/url"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/mattes/migrate/source"
|
||||
)
|
||||
|
||||
func init() {
|
||||
source.Register("file", &File{})
|
||||
}
|
||||
|
||||
type File struct {
|
||||
url string
|
||||
path string
|
||||
migrations *source.Migrations
|
||||
}
|
||||
|
||||
func (f *File) Open(url string) (source.Driver, error) {
|
||||
u, err := nurl.Parse(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// default to current directory if empty
|
||||
if u.Path == "" {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u.Path = wd
|
||||
}
|
||||
|
||||
// scan directory
|
||||
files, err := ioutil.ReadDir(u.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nf := &File{
|
||||
url: url,
|
||||
path: u.Path,
|
||||
migrations: source.NewMigrations(),
|
||||
}
|
||||
|
||||
for _, fi := range files {
|
||||
if !fi.IsDir() {
|
||||
m, err := source.DefaultParse(fi.Name())
|
||||
if err != nil {
|
||||
continue // ignore files that we can't parse
|
||||
}
|
||||
if !nf.migrations.Append(m) {
|
||||
return nil, fmt.Errorf("unable to parse file %v", fi.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
return nf, nil
|
||||
}
|
||||
|
||||
func (f *File) Close() error {
|
||||
// nothing do to here
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *File) First() (version uint, err error) {
|
||||
if v, ok := f.migrations.First(); !ok {
|
||||
return 0, &os.PathError{"first", f.path, os.ErrNotExist}
|
||||
} else {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (f *File) Prev(version uint) (prevVersion uint, err error) {
|
||||
if v, ok := f.migrations.Prev(version); !ok {
|
||||
return 0, &os.PathError{fmt.Sprintf("prev for version %v", version), f.path, os.ErrNotExist}
|
||||
} else {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (f *File) Next(version uint) (nextVersion uint, err error) {
|
||||
if v, ok := f.migrations.Next(version); !ok {
|
||||
return 0, &os.PathError{fmt.Sprintf("next for version %v", version), f.path, os.ErrNotExist}
|
||||
} else {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (f *File) ReadUp(version uint) (r io.ReadCloser, identifier string, err error) {
|
||||
if m, ok := f.migrations.Up(version); ok {
|
||||
r, err := os.Open(path.Join(f.path, m.Raw))
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return r, m.Identifier, nil
|
||||
}
|
||||
return nil, "", &os.PathError{fmt.Sprintf("read version %v", version), f.path, os.ErrNotExist}
|
||||
}
|
||||
|
||||
func (f *File) ReadDown(version uint) (r io.ReadCloser, identifier string, err error) {
|
||||
if m, ok := f.migrations.Down(version); ok {
|
||||
r, err := os.Open(path.Join(f.path, m.Raw))
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return r, m.Identifier, nil
|
||||
}
|
||||
return nil, "", &os.PathError{fmt.Sprintf("read version %v", version), f.path, os.ErrNotExist}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
st "github.com/mattes/migrate/source/testing"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
tmpDir, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// write files that meet driver test requirements
|
||||
mustWriteFile(t, tmpDir, "1_foobar.up.sql", "1 up")
|
||||
mustWriteFile(t, tmpDir, "1_foobar.down.sql", "1 down")
|
||||
|
||||
mustWriteFile(t, tmpDir, "3_foobar.up.sql", "3 up")
|
||||
|
||||
mustWriteFile(t, tmpDir, "4_foobar.up.sql", "4 up")
|
||||
mustWriteFile(t, tmpDir, "4_foobar.down.sql", "4 down")
|
||||
|
||||
mustWriteFile(t, tmpDir, "5_foobar.down.sql", "5 down")
|
||||
|
||||
mustWriteFile(t, tmpDir, "7_foobar.up.sql", "7 up")
|
||||
mustWriteFile(t, tmpDir, "7_foobar.down.sql", "7 down")
|
||||
|
||||
f := &File{}
|
||||
d, err := f.Open("file://" + tmpDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
st.Test(t, d)
|
||||
}
|
||||
|
||||
func TestOpen(t *testing.T) {
|
||||
tmpDir, err := ioutil.TempDir("", "TestOpen")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
mustWriteFile(t, tmpDir, "1_foobar.up.sql", "")
|
||||
mustWriteFile(t, tmpDir, "1_foobar.down.sql", "")
|
||||
|
||||
f := &File{}
|
||||
_, err = f.Open("file://" + tmpDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenDefaultsToCurrentDirectory(t *testing.T) {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
f := &File{}
|
||||
d, err := f.Open("file://")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if d.(*File).path != wd {
|
||||
t.Fatal("expected driver to default to current directory")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenWithDuplicateVersion(t *testing.T) {
|
||||
tmpDir, err := ioutil.TempDir("", "TestOpenWithDuplicateVersion")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
mustWriteFile(t, tmpDir, "1_foo.up.sql", "") // 1 up
|
||||
mustWriteFile(t, tmpDir, "1_bar.up.sql", "") // 1 up
|
||||
|
||||
f := &File{}
|
||||
_, err = f.Open("file://" + tmpDir)
|
||||
if err == nil {
|
||||
t.Fatal("expected err")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClose(t *testing.T) {
|
||||
tmpDir, err := ioutil.TempDir("", "TestOpen")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
f := &File{}
|
||||
d, err := f.Open("file://" + tmpDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if d.Close() != nil {
|
||||
t.Fatal("expected nil")
|
||||
}
|
||||
}
|
||||
|
||||
func mustWriteFile(t testing.TB, dir, file string, body string) {
|
||||
if err := ioutil.WriteFile(path.Join(dir, file), []byte(body), 06444); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustCreateBenchmarkDir(t *testing.B) (dir string) {
|
||||
tmpDir, err := ioutil.TempDir("", "Benchmark")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
mustWriteFile(t, tmpDir, fmt.Sprintf("%v_foobar.up.sql", i), "")
|
||||
mustWriteFile(t, tmpDir, fmt.Sprintf("%v_foobar.down.sql", i), "")
|
||||
}
|
||||
|
||||
return tmpDir
|
||||
}
|
||||
|
||||
func BenchmarkOpen(b *testing.B) {
|
||||
dir := mustCreateBenchmarkDir(b)
|
||||
defer os.RemoveAll(dir)
|
||||
b.ResetTimer()
|
||||
for n := 0; n < b.N; n++ {
|
||||
f := &File{}
|
||||
f.Open("file://" + dir)
|
||||
}
|
||||
b.StopTimer()
|
||||
}
|
||||
|
||||
func BenchmarkNext(b *testing.B) {
|
||||
dir := mustCreateBenchmarkDir(b)
|
||||
defer os.RemoveAll(dir)
|
||||
f := &File{}
|
||||
d, _ := f.Open("file://" + dir)
|
||||
b.ResetTimer()
|
||||
v, err := d.First()
|
||||
for n := 0; n < b.N; n++ {
|
||||
for !os.IsNotExist(err) {
|
||||
v, err = d.Next(v)
|
||||
}
|
||||
}
|
||||
b.StopTimer()
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
.github_test_secrets
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS users;
|
||||
@@ -0,0 +1,5 @@
|
||||
CREATE TABLE users (
|
||||
user_id integer unique,
|
||||
name varchar(40),
|
||||
email varchar(40)
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE users DROP COLUMN IF EXISTS city;
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE users ADD COLUMN city varchar(100);
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DROP INDEX IF EXISTS users_email_index;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user