initial version 3.0.0 preview

hard work
This commit is contained in:
Matthias Kadenbach
2017-02-07 22:01:29 -08:00
parent e92f038f40
commit ee384ed8cc
129 changed files with 5158 additions and 4902 deletions
+3 -1
View File
@@ -1,2 +1,4 @@
.DS_Store
test.db
cli/build
cli/cli
cli/migrate
+1 -7
View File
@@ -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
-3
View File
@@ -1,3 +0,0 @@
FROM scratch
ADD migrate /migrate
ENTRYPOINT ["/migrate"]
+4 -2
View File
@@ -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.
+42 -20
View File
@@ -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)
+7
View File
@@ -0,0 +1,7 @@
// +build github
package main
import (
_ "github.com/mattes/migrate/source/github"
)
+7
View File
@@ -0,0 +1,7 @@
// +build go-bindata
package main
import (
_ "github.com/mattes/migrate/source/go-bindata"
)
+7
View File
@@ -0,0 +1,7 @@
// +build postgres
package main
import (
_ "github.com/mattes/migrate/database/postgres"
)
+51
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
}
+4
View File
@@ -0,0 +1,4 @@
package main
// Version is set in Makefile with build flags
var Version = "dev"
View File
View File
+70
View File
@@ -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
}
+1
View File
@@ -0,0 +1 @@
mysql
View File
View File
View File
@@ -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.
+224
View File
@@ -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
}
+128
View File
@@ -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)
}
View File
View File
View File
+96
View File
@@ -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)
}
+16
View File
@@ -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 */"))
}
+125
View File
@@ -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)
}
}
-32
View File
@@ -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
-11
View File
@@ -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
```
-36
View File
@@ -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{})
}
-7
View File
@@ -1,7 +0,0 @@
package bash
import "testing"
func TestMigrate(t *testing.T) {
}
-20
View File
@@ -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
-214
View File
@@ -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
}
-152
View File
@@ -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")
}
}
-15
View File
@@ -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
```
-115
View File
@@ -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
}
-111
View File
@@ -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)
}
}
-66
View File
@@ -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))
}
}
-104
View File
@@ -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
-310
View File
@@ -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))
}
}
-193
View File
@@ -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
}
-24
View File
@@ -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
-185
View File
@@ -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{})
}
-106
View File
@@ -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)
}
}
-20
View File
@@ -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
-157
View File
@@ -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{})
}
-100
View File
@@ -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)
}
}
-25
View File
@@ -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
-143
View File
@@ -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{})
}
-142
View File
@@ -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)
}
}
-17
View File
@@ -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
View File
@@ -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{})
}
-44
View File
@@ -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
}
-21
View File
@@ -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
-131
View File
@@ -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{})
}
-99
View File
@@ -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
View File
@@ -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"))
}
-253
View File
@@ -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
}
+6
View File
@@ -0,0 +1,6 @@
package migrate
type Logger interface {
Printf(format string, v ...interface{})
Verbose() bool
}
-247
View File
@@ -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
View File
@@ -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
View File
@@ -1,4 +1,3 @@
// Package direction just holds convenience constants for Up and Down migrations.
package direction
type Direction int
+20
View File
@@ -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
}
-338
View File
@@ -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
}
-284
View File
@@ -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)
}
}
}
+72
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
-85
View File
@@ -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)
}
}
View File
View File
View File
+59
View File
@@ -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
}
+114
View File
@@ -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}
}
+156
View File
@@ -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()
}
+1
View File
@@ -0,0 +1 @@
.github_test_secrets
View File
@@ -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