├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── docker-compose.yml ├── driver ├── bash │ ├── README.md │ ├── bash.go │ └── bash_test.go ├── cassandra │ ├── README.md │ ├── cassandra.go │ └── cassandra_test.go ├── crate │ ├── README.md │ ├── crate.go │ └── crate_test.go ├── driver.go ├── mysql │ ├── README.md │ ├── mysql.go │ └── mysql_test.go ├── postgres │ ├── README.md │ ├── postgres.go │ └── postgres_test.go ├── registry.go └── sqlite3 │ ├── README.md │ ├── sqlite3.go │ └── sqlite3_test.go ├── file ├── file.go └── file_test.go ├── main.go ├── migrate ├── direction │ └── direction.go ├── migrate.go └── migrate_test.go ├── pipe └── pipe.go └── version.go /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | test.db -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: required 3 | 4 | go: 5 | - 1.6 6 | - 1.7 7 | 8 | go_import_path: github.com/gemnasium/migrate 9 | 10 | services: 11 | - docker 12 | 13 | before_install: 14 | - sed -i -e 's/golang/golang:'"$TRAVIS_GO_VERSION"'/' docker-compose.yml 15 | 16 | script: make test 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Migrate Changelog 2 | 3 | ## master 4 | 5 | - [postgresql] Avoid DDL when checking for versions table (#23) 6 | - [postgresql] Start switching to sqlx to write cleaner code 7 | - [postgresql] Transactions can be disabled per migration file 8 | 9 | ## v1.4.1 - 2016-12-16 10 | 11 | * [cassandra] Add [disable_init_host_lookup](https://github.com/gocql/gocql/blob/master/cluster.go#L92) url param (@GeorgeMac / #17) 12 | 13 | ## v1.4.0 - 2016-11-22 14 | 15 | * [crate] Add [Crate](https://crate.io) database support, based on the Crate sql driver by [herenow](https://github.com/herenow/go-crate) (@dereulenspiegel / #16) 16 | 17 | ## v1.3.2 - 2016-11-11 18 | 19 | * [sqlite] Allow multiple statements per migration (dklimkin / #11) 20 | 21 | ## v1.3.1 - 2016-08-16 22 | 23 | * Make MySQL driver aware of SSL certificates for TLS connection by scanning ENV variables (https://github.com/mattes/migrate/pull/117/files) 24 | 25 | ## v1.3.0 - 2016-08-15 26 | 27 | * Initial changelog release 28 | * Timestamp migration, instead of increments (https://github.com/mattes/migrate/issues/102) 29 | * Versions will now be tagged 30 | * Added consistency parameter to cassandra connection string (https://github.com/mattes/migrate/pull/114) 31 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | ADD migrater /migrate 3 | ENTRYPOINT ["/migrate"] 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Matthias Kadenbach 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | IMAGE=gemnasium/migrate 2 | DCR=docker-compose run --rm 3 | .PHONY: clean test build release docker-build docker-push run 4 | 5 | all: release 6 | 7 | clean: 8 | rm -f migrate 9 | 10 | test: 11 | $(DCR) go-test 12 | 13 | build: 14 | $(DCR) go-build 15 | 16 | release: test build docker-build docker-push 17 | 18 | docker-build: 19 | docker build --rm -t $(IMAGE) . 20 | 21 | docker-push: 22 | docker push $(IMAGE) 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # migrate 2 | 3 | [![Build Status](https://travis-ci.org/gemnasium/migrate.svg?branch=master)](https://travis-ci.org/gemnasium/migrate) 4 | [![GoDoc](https://godoc.org/github.com/gemnasium/migrate?status.svg)](https://godoc.org/github.com/gemnasium/migrate) 5 | 6 | A migration helper written in Go. Use it in your existing Golang code 7 | or run commands via the CLI. 8 | 9 | ``` 10 | GoCode import github.com/gemnasium/migrate/migrate 11 | CLI go get -u github.com/gemnasium/migrate 12 | ``` 13 | 14 | __Features__ 15 | 16 | * Super easy to implement [Driver interface](http://godoc.org/github.com/gemnasium/migrate/driver#Driver). 17 | * Gracefully quit running migrations on ``^C``. 18 | * No magic search paths routines, no hard-coded config files. 19 | * CLI is build on top of the ``migrate package``. 20 | 21 | 22 | ## Available Drivers 23 | 24 | * [PostgreSQL](https://github.com/gemnasium/migrate/tree/master/driver/postgres) 25 | * [Cassandra](https://github.com/gemnasium/migrate/tree/master/driver/cassandra) 26 | * [SQLite](https://github.com/gemnasium/migrate/tree/master/driver/sqlite3) 27 | * [MySQL](https://github.com/gemnasium/migrate/tree/master/driver/mysql) ([experimental](https://github.com/mattes/migrate/issues/1#issuecomment-58728186)) 28 | * Bash (planned) 29 | 30 | Need another driver? Just implement the [Driver interface](http://godoc.org/github.com/gemnasium/migrate/driver#Driver) and open a PR. 31 | 32 | 33 | ## Usage from Terminal 34 | 35 | ```bash 36 | # install 37 | go get github.com/gemnasium/migrate 38 | 39 | # create new migration file in path 40 | migrate -url driver://url -path ./migrations create migration_file_xyz 41 | 42 | # apply all available migrations 43 | migrate -url driver://url -path ./migrations up 44 | 45 | # roll back all migrations 46 | migrate -url driver://url -path ./migrations down 47 | 48 | # roll back the most recently applied migration, then run it again. 49 | migrate -url driver://url -path ./migrations redo 50 | 51 | # run down and then up command 52 | migrate -url driver://url -path ./migrations reset 53 | 54 | # show the current migration version 55 | migrate -url driver://url -path ./migrations version 56 | 57 | # apply the next n migrations 58 | migrate -url driver://url -path ./migrations migrate +1 59 | migrate -url driver://url -path ./migrations migrate +2 60 | migrate -url driver://url -path ./migrations migrate +n 61 | 62 | # roll back the previous n migrations 63 | migrate -url driver://url -path ./migrations migrate -1 64 | migrate -url driver://url -path ./migrations migrate -2 65 | migrate -url driver://url -path ./migrations migrate -n 66 | 67 | # go to specific migration 68 | migrate -url driver://url -path ./migrations goto 1 69 | migrate -url driver://url -path ./migrations goto 10 70 | migrate -url driver://url -path ./migrations goto v 71 | ``` 72 | 73 | 74 | ## Usage in Go 75 | 76 | See GoDoc here: http://godoc.org/github.com/gemnasium/migrate/migrate 77 | 78 | ```go 79 | import "github.com/gemnasium/migrate/migrate" 80 | 81 | // Import any required drivers so that they are registered and available 82 | import _ "github.com/gemnasium/migrate/driver/mysql" 83 | 84 | // use synchronous versions of migration functions ... 85 | allErrors, ok := migrate.UpSync("driver://url", "./path") 86 | if !ok { 87 | fmt.Println("Oh no ...") 88 | // do sth with allErrors slice 89 | } 90 | 91 | // use the asynchronous version of migration functions ... 92 | pipe := migrate.NewPipe() 93 | go migrate.Up(pipe, "driver://url", "./path") 94 | // pipe is basically just a channel 95 | // write your own channel listener. see writePipe() in main.go as an example. 96 | ``` 97 | 98 | ## Migration files 99 | 100 | The format of migration files looks like this: 101 | 102 | ``` 103 | 20060102150405_initial_plan_to_do_sth.up.sql # up migration instructions 104 | 20060102150405_initial_plan_to_do_sth.down.sql # down migration instructions 105 | 20060102150506_xxx.up.sql 106 | 20060102150506_xxx.down.sql 107 | ... 108 | ``` 109 | 110 | Why two files? This way you could still do sth like 111 | ``psql -f ./db/migrations/20060102150405_initial_plan_to_do_sth.up.sql`` and there is no 112 | need for any custom markup language to divide up and down migrations. Please note 113 | that the filename extension depends on the driver. 114 | 115 | 116 | ## Alternatives 117 | 118 | * https://bitbucket.org/liamstask/goose 119 | * https://github.com/tanel/dbmigrate 120 | * https://github.com/BurntSushi/migration 121 | * https://github.com/DavidHuie/gomigrate 122 | * https://github.com/rubenv/sql-migrate 123 | 124 | 125 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | go: &go 2 | image: golang 3 | working_dir: /go/src/github.com/gemnasium/migrate 4 | volumes: 5 | - $GOPATH:/go 6 | go-test: 7 | <<: *go 8 | command: sh -c 'go get -t -v ./... && go test -p=1 -v ./...' 9 | links: 10 | - postgres 11 | - mysql 12 | - cassandra 13 | - crate 14 | go-build: 15 | <<: *go 16 | command: sh -c 'go get -v && go build -ldflags ''-s'' -o migrater' 17 | environment: 18 | CGO_ENABLED: 1 19 | postgres: 20 | image: postgres 21 | mysql: 22 | image: mysql 23 | environment: 24 | MYSQL_DATABASE: migratetest 25 | MYSQL_ALLOW_EMPTY_PASSWORD: "yes" 26 | cassandra: 27 | image: cassandra:2.2 28 | crate: 29 | image: crate 30 | -------------------------------------------------------------------------------- /driver/bash/README.md: -------------------------------------------------------------------------------- 1 | # Bash Driver 2 | 3 | * Runs bash scripts. What you do in the scripts is up to you. 4 | 5 | ## Usage 6 | 7 | ```bash 8 | migrate -url bash://xxx -path ./migrations create increment_xyz 9 | migrate -url bash://xxx -path ./migrations up 10 | migrate help # for more info 11 | ``` 12 | -------------------------------------------------------------------------------- /driver/bash/bash.go: -------------------------------------------------------------------------------- 1 | // Package bash implements the Driver interface. 2 | package bash 3 | 4 | import ( 5 | "github.com/gemnasium/migrate/driver" 6 | "github.com/gemnasium/migrate/file" 7 | ) 8 | 9 | type Driver struct { 10 | } 11 | 12 | func (driver *Driver) Initialize(url string) error { 13 | return nil 14 | } 15 | 16 | func (driver *Driver) Close() error { 17 | return nil 18 | } 19 | 20 | func (driver *Driver) FilenameExtension() string { 21 | return "sh" 22 | } 23 | 24 | func (driver *Driver) Migrate(f file.File, pipe chan interface{}) { 25 | defer close(pipe) 26 | pipe <- f 27 | return 28 | } 29 | 30 | // Version returns the current migration version. 31 | func (driver *Driver) Version() (file.Version, error) { 32 | return file.Version(0), nil 33 | } 34 | 35 | // Versions returns the list of applied migrations. 36 | func (driver *Driver) Versions() (file.Versions, error) { 37 | return file.Versions{0}, nil 38 | } 39 | 40 | func init() { 41 | driver.RegisterDriver("bash", &Driver{}) 42 | } 43 | -------------------------------------------------------------------------------- /driver/bash/bash_test.go: -------------------------------------------------------------------------------- 1 | package bash 2 | 3 | import "testing" 4 | 5 | func TestMigrate(t *testing.T) { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /driver/cassandra/README.md: -------------------------------------------------------------------------------- 1 | # Cassandra Driver 2 | 3 | ## Usage 4 | 5 | ```bash 6 | migrate -url cassandra://host:port/keyspace -path ./db/migrations create add_field_to_table 7 | migrate -url cassandra://host:port/keyspace -path ./db/migrations up 8 | migrate help # for more info 9 | ``` 10 | 11 | Url format 12 | - Authentication: `cassandra://username:password@host:port/keyspace` 13 | - Cassandra v3.x: `cassandra://host:port/keyspace?protocol=4&consistency=all&disable_init_host_lookup` 14 | 15 | > Cassandra in Docker users on a Mac: when using gcql + migrate, use the `disable_init_host_lookup` option in the connection URL. This will alleviate the issue of gocql trying to connect to internal docker IP addresses. 16 | 17 | ## Authors 18 | 19 | * Paul Bergeron, https://github.com/dinedal 20 | * Johnny Bergström, https://github.com/balboah 21 | * pateld982, http://github.com/pateld982 22 | -------------------------------------------------------------------------------- /driver/cassandra/cassandra.go: -------------------------------------------------------------------------------- 1 | // Package cassandra implements the Driver interface. 2 | package cassandra 3 | 4 | import ( 5 | "fmt" 6 | "net/url" 7 | "sort" 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | "github.com/gemnasium/migrate/driver" 13 | "github.com/gemnasium/migrate/file" 14 | "github.com/gemnasium/migrate/migrate/direction" 15 | "github.com/gocql/gocql" 16 | ) 17 | 18 | type Driver struct { 19 | session *gocql.Session 20 | } 21 | 22 | const ( 23 | tableName = "schema_migrations" 24 | ) 25 | 26 | // Cassandra Driver URL format: 27 | // cassandra://host:port/keyspace?protocol=version&consistency=level 28 | // 29 | // Examples: 30 | // cassandra://localhost/SpaceOfKeys?protocol=4 31 | // cassandra://localhost/SpaceOfKeys?protocol=4&consistency=all 32 | // cassandra://localhost/SpaceOfKeys?consistency=quorum 33 | func (driver *Driver) Initialize(rawurl string) error { 34 | u, err := url.Parse(rawurl) 35 | 36 | cluster := gocql.NewCluster(u.Host) 37 | cluster.Keyspace = u.Path[1:len(u.Path)] 38 | cluster.Consistency = gocql.All 39 | cluster.Timeout = 1 * time.Minute 40 | 41 | if len(u.Query().Get("consistency")) > 0 { 42 | consistency, err := parseConsistency(u.Query().Get("consistency")) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | cluster.Consistency = consistency 48 | } 49 | 50 | if len(u.Query().Get("protocol")) > 0 { 51 | protoversion, err := strconv.Atoi(u.Query().Get("protocol")) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | cluster.ProtoVersion = protoversion 57 | } 58 | 59 | if _, ok := u.Query()["disable_init_host_lookup"]; ok { 60 | cluster.DisableInitialHostLookup = true 61 | } 62 | 63 | // Check if url user struct is null 64 | if u.User != nil { 65 | password, passwordSet := u.User.Password() 66 | 67 | if passwordSet == false { 68 | return fmt.Errorf("Missing password. Please provide password.") 69 | } 70 | 71 | cluster.Authenticator = gocql.PasswordAuthenticator{ 72 | Username: u.User.Username(), 73 | Password: password, 74 | } 75 | 76 | } 77 | 78 | driver.session, err = cluster.CreateSession() 79 | if err != nil { 80 | return err 81 | } 82 | 83 | if err := driver.ensureVersionTableExists(); err != nil { 84 | return err 85 | } 86 | 87 | return nil 88 | } 89 | 90 | func (driver *Driver) Close() error { 91 | driver.session.Close() 92 | return nil 93 | } 94 | 95 | func (driver *Driver) ensureVersionTableExists() error { 96 | err := driver.session.Query("CREATE TABLE IF NOT EXISTS " + tableName + " (version bigint primary key);").Exec() 97 | return err 98 | } 99 | 100 | func (driver *Driver) FilenameExtension() string { 101 | return "cql" 102 | } 103 | 104 | func (driver *Driver) Migrate(f file.File, pipe chan interface{}) { 105 | var err error 106 | defer func() { 107 | if err != nil { 108 | // Invert version direction if we couldn't apply the changes for some reason. 109 | if errRollback := driver.session.Query("DELETE FROM "+tableName+" WHERE version = ?", f.Version).Exec(); errRollback != nil { 110 | pipe <- errRollback 111 | } 112 | pipe <- err 113 | } 114 | close(pipe) 115 | }() 116 | 117 | pipe <- f 118 | 119 | if err = f.ReadContent(); err != nil { 120 | return 121 | } 122 | 123 | if f.Direction == direction.Up { 124 | if err = driver.session.Query("INSERT INTO "+tableName+" (version) VALUES (?)", f.Version).Exec(); err != nil { 125 | return 126 | } 127 | } else if f.Direction == direction.Down { 128 | if err = driver.session.Query("DELETE FROM "+tableName+" WHERE version = ?", f.Version).Exec(); err != nil { 129 | return 130 | } 131 | } 132 | 133 | for _, query := range strings.Split(string(f.Content), ";") { 134 | query = strings.TrimSpace(query) 135 | if len(query) == 0 { 136 | continue 137 | } 138 | 139 | if err = driver.session.Query(query).Exec(); err != nil { 140 | return 141 | } 142 | } 143 | } 144 | 145 | // Version returns the current migration version. 146 | func (driver *Driver) Version() (file.Version, error) { 147 | versions, err := driver.Versions() 148 | if len(versions) == 0 { 149 | return 0, err 150 | } 151 | return versions[0], err 152 | } 153 | 154 | // Versions returns the list of applied migrations. 155 | func (driver *Driver) Versions() (file.Versions, error) { 156 | versions := file.Versions{} 157 | iter := driver.session.Query("SELECT version FROM " + tableName).Iter() 158 | var version int64 159 | for iter.Scan(&version) { 160 | versions = append(versions, file.Version(version)) 161 | } 162 | err := iter.Close() 163 | sort.Sort(sort.Reverse(versions)) 164 | return versions, err 165 | } 166 | 167 | func init() { 168 | driver.RegisterDriver("cassandra", &Driver{}) 169 | } 170 | 171 | // ParseConsistency wraps gocql.ParseConsistency to return an error 172 | // instead of a panicing. 173 | func parseConsistency(consistencyStr string) (consistency gocql.Consistency, err error) { 174 | defer func() { 175 | if r := recover(); r != nil { 176 | var ok bool 177 | err, ok = r.(error) 178 | if !ok { 179 | err = fmt.Errorf("Failed to parse consistency \"%s\": %v", consistencyStr, r) 180 | } 181 | } 182 | }() 183 | consistency = gocql.ParseConsistency(consistencyStr) 184 | 185 | return consistency, nil 186 | } 187 | -------------------------------------------------------------------------------- /driver/cassandra/cassandra_test.go: -------------------------------------------------------------------------------- 1 | package cassandra 2 | 3 | import ( 4 | "net/url" 5 | "os" 6 | "reflect" 7 | "testing" 8 | "time" 9 | 10 | "github.com/gemnasium/migrate/file" 11 | "github.com/gemnasium/migrate/migrate/direction" 12 | pipep "github.com/gemnasium/migrate/pipe" 13 | "github.com/gocql/gocql" 14 | ) 15 | 16 | func TestMigrate(t *testing.T) { 17 | var session *gocql.Session 18 | 19 | host := os.Getenv("CASSANDRA_PORT_9042_TCP_ADDR") 20 | port := os.Getenv("CASSANDRA_PORT_9042_TCP_PORT") 21 | driverURL := "cassandra://" + host + ":" + port + "/system?protocol=4" 22 | 23 | // prepare a clean test database. 24 | u, err := url.Parse(driverURL) 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | 29 | cluster := gocql.NewCluster(u.Host) 30 | cluster.Keyspace = u.Path[1:len(u.Path)] 31 | cluster.Consistency = gocql.All 32 | cluster.Timeout = 1 * time.Minute 33 | cluster.ProtoVersion = 4 34 | 35 | session, err = cluster.CreateSession() 36 | if err != nil { 37 | //t.Fatal(err) 38 | } 39 | 40 | if err := resetKeySpace(session); err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | cluster.Keyspace = "migrate" 45 | session, err = cluster.CreateSession() 46 | driverURL = "cassandra://" + host + ":" + port + "/migrate?protocol=4" 47 | 48 | d := &Driver{} 49 | if err := d.Initialize(driverURL); err != nil { 50 | t.Fatal(err) 51 | } 52 | 53 | files := []file.File{ 54 | { 55 | Path: "/foobar", 56 | FileName: "20060102150405_foobar.up.sql", 57 | Version: 20060102150405, 58 | Name: "foobar", 59 | Direction: direction.Up, 60 | Content: []byte(` 61 | CREATE TABLE yolo ( 62 | id varint primary key, 63 | msg text 64 | ); 65 | 66 | CREATE INDEX ON yolo (msg); 67 | `), 68 | }, 69 | { 70 | Path: "/foobar", 71 | FileName: "20060102150405_foobar.down.sql", 72 | Version: 20060102150405, 73 | Name: "foobar", 74 | Direction: direction.Down, 75 | Content: []byte(` 76 | DROP TABLE yolo; 77 | `), 78 | }, 79 | { 80 | Path: "/foobar", 81 | FileName: "20060102150406_foobar.up.sql", 82 | Version: 20060102150406, 83 | Name: "foobar", 84 | Direction: direction.Up, 85 | Content: []byte(` 86 | CREATE TABLE error ( 87 | id THIS WILL CAUSE AN ERROR 88 | ) 89 | `), 90 | }, 91 | } 92 | 93 | pipe := pipep.New() 94 | go d.Migrate(files[0], pipe) 95 | errs := pipep.ReadErrors(pipe) 96 | if len(errs) > 0 { 97 | t.Fatal(errs) 98 | } 99 | 100 | version, err := d.Version() 101 | if err != nil { 102 | t.Fatal(err) 103 | } 104 | 105 | if version != 20060102150405 { 106 | t.Errorf("Expected version to be: %d, got: %d", 20060102150405, version) 107 | } 108 | 109 | // Check versions applied in DB. 110 | expectedVersions := file.Versions{20060102150405} 111 | versions, err := d.Versions() 112 | if err != nil { 113 | t.Errorf("Could not fetch versions: %s", err) 114 | } 115 | 116 | if !reflect.DeepEqual(versions, expectedVersions) { 117 | t.Errorf("Expected versions to be: %v, got: %v", expectedVersions, versions) 118 | } 119 | 120 | pipe = pipep.New() 121 | go d.Migrate(files[1], pipe) 122 | errs = pipep.ReadErrors(pipe) 123 | if len(errs) > 0 { 124 | t.Fatal(errs) 125 | } 126 | 127 | pipe = pipep.New() 128 | go d.Migrate(files[2], pipe) 129 | errs = pipep.ReadErrors(pipe) 130 | if len(errs) == 0 { 131 | t.Error("Expected test case to fail") 132 | } 133 | 134 | // Check versions applied in DB. 135 | expectedVersions = file.Versions{} 136 | versions, err = d.Versions() 137 | if err != nil { 138 | t.Errorf("Could not fetch versions: %s", err) 139 | } 140 | 141 | if !reflect.DeepEqual(versions, expectedVersions) { 142 | t.Errorf("Expected versions to be: %v, got: %v", expectedVersions, versions) 143 | } 144 | 145 | if err := resetKeySpace(session); err != nil { 146 | t.Fatal(err) 147 | } 148 | 149 | if err := d.Close(); err != nil { 150 | t.Fatal(err) 151 | } 152 | 153 | } 154 | 155 | func resetKeySpace(session *gocql.Session) error { 156 | session.Query(`DROP KEYSPACE migrate;`).Exec() 157 | return session.Query(`CREATE KEYSPACE IF NOT EXISTS migrate WITH REPLICATION = {'class': 'SimpleStrategy', 'replication_factor': 1};`).Exec() 158 | } 159 | -------------------------------------------------------------------------------- /driver/crate/README.md: -------------------------------------------------------------------------------- 1 | # Crate driver 2 | 3 | This is a driver for the [Crate](https://crate.io) database. It is based on the Crate 4 | sql driver by [herenow](https://github.com/herenow/go-crate). 5 | 6 | This driver does not use transactions! This is not a limitation of the driver, but a 7 | limitation of Crate. So handle situations with failed migrations with care! 8 | 9 | ## Usage 10 | 11 | ```bash 12 | migrate -url http://host:port -path ./db/migrations create add_field_to_table 13 | migrate -url http://host:port -path ./db/migrations up 14 | migrate help # for more info 15 | ``` -------------------------------------------------------------------------------- /driver/crate/crate.go: -------------------------------------------------------------------------------- 1 | // Package crate implements a driver for the Crate.io database 2 | package crate 3 | 4 | import ( 5 | "database/sql" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/gemnasium/migrate/driver" 10 | "github.com/gemnasium/migrate/file" 11 | "github.com/gemnasium/migrate/migrate/direction" 12 | _ "github.com/herenow/go-crate" 13 | ) 14 | 15 | func init() { 16 | driver.RegisterDriver("crate", &Driver{}) 17 | } 18 | 19 | type Driver struct { 20 | db *sql.DB 21 | } 22 | 23 | const tableName = "schema_migrations" 24 | 25 | func (driver *Driver) Initialize(url string) error { 26 | url = strings.Replace(url, "crate", "http", 1) 27 | db, err := sql.Open("crate", url) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | if err := db.Ping(); err != nil { 33 | return err 34 | } 35 | driver.db = db 36 | 37 | if err := driver.ensureVersionTableExists(); err != nil { 38 | return err 39 | } 40 | return nil 41 | } 42 | 43 | func (driver *Driver) Close() error { 44 | if err := driver.db.Close(); err != nil { 45 | return err 46 | } 47 | return nil 48 | } 49 | 50 | func (driver *Driver) FilenameExtension() string { 51 | return "sql" 52 | } 53 | 54 | // Version returns the current migration version. 55 | func (driver *Driver) Version() (file.Version, error) { 56 | var version file.Version 57 | err := driver.db.QueryRow("SELECT version FROM " + tableName + " ORDER BY version DESC LIMIT 1").Scan(&version) 58 | switch { 59 | case err == sql.ErrNoRows: 60 | return 0, nil 61 | case err != nil: 62 | return 0, err 63 | default: 64 | return version, nil 65 | } 66 | } 67 | 68 | // Versions returns the list of applied migrations. 69 | func (driver *Driver) Versions() (file.Versions, error) { 70 | versions := file.Versions{} 71 | 72 | rows, err := driver.db.Query("SELECT version FROM " + tableName + " ORDER BY version DESC") 73 | if err != nil { 74 | return versions, err 75 | } 76 | defer rows.Close() 77 | for rows.Next() { 78 | var version file.Version 79 | err := rows.Scan(&version) 80 | if err != nil { 81 | return versions, err 82 | } 83 | versions = append(versions, version) 84 | } 85 | err = rows.Err() 86 | return versions, err 87 | } 88 | 89 | func (driver *Driver) Migrate(f file.File, pipe chan interface{}) { 90 | defer close(pipe) 91 | pipe <- f 92 | 93 | if err := f.ReadContent(); err != nil { 94 | pipe <- err 95 | return 96 | } 97 | 98 | lines := splitContent(string(f.Content)) 99 | for _, line := range lines { 100 | _, err := driver.db.Exec(line) 101 | if err != nil { 102 | pipe <- err 103 | return 104 | } 105 | } 106 | 107 | if f.Direction == direction.Up { 108 | if _, err := driver.db.Exec("INSERT INTO "+tableName+" (version) VALUES (?)", f.Version); err != nil { 109 | pipe <- err 110 | return 111 | } 112 | } else if f.Direction == direction.Down { 113 | if _, err := driver.db.Exec("DELETE FROM "+tableName+" WHERE version=?", f.Version); err != nil { 114 | pipe <- err 115 | return 116 | } 117 | } 118 | } 119 | 120 | func splitContent(content string) []string { 121 | lines := strings.Split(content, ";") 122 | resultLines := make([]string, 0, len(lines)) 123 | for i, line := range lines { 124 | line = strings.Replace(lines[i], ";", "", -1) 125 | line = strings.TrimSpace(line) 126 | if line != "" { 127 | resultLines = append(resultLines, line) 128 | } 129 | } 130 | return resultLines 131 | } 132 | 133 | func (driver *Driver) ensureVersionTableExists() error { 134 | if _, err := driver.db.Exec(fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (version LONG PRIMARY KEY)", tableName)); err != nil { 135 | return err 136 | } 137 | return nil 138 | } 139 | -------------------------------------------------------------------------------- /driver/crate/crate_test.go: -------------------------------------------------------------------------------- 1 | package crate 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | 8 | "github.com/gemnasium/migrate/file" 9 | "github.com/gemnasium/migrate/migrate/direction" 10 | pipep "github.com/gemnasium/migrate/pipe" 11 | ) 12 | 13 | func TestContentSplit(t *testing.T) { 14 | 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); 15 | CREATE TABLE units (unit_id STRING primary key, name STRING, members array(string)) CLUSTERED INTO 3 shards WITH (number_of_replicas = 0); 16 | 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); 17 | ` 18 | 19 | lines := splitContent(content) 20 | if len(lines) != 3 { 21 | t.Errorf("Expected 3 lines, but got %d", len(lines)) 22 | } 23 | 24 | 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)" { 25 | t.Error("Line does not match expected output") 26 | } 27 | 28 | 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)" { 29 | t.Error("Line does not match expected output") 30 | } 31 | 32 | 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)" { 33 | t.Error("Line does not match expected output") 34 | } 35 | } 36 | 37 | func TestMigrate(t *testing.T) { 38 | host := os.Getenv("CRATE_PORT_4200_TCP_ADDR") 39 | port := os.Getenv("CRATE_PORT_4200_TCP_PORT") 40 | 41 | url := fmt.Sprintf("crate://%s:%s", host, port) 42 | 43 | driver := &Driver{} 44 | 45 | if err := driver.Initialize(url); err != nil { 46 | t.Fatal(err) 47 | } 48 | 49 | successFiles := []file.File{ 50 | { 51 | Path: "/foobar", 52 | FileName: "20161122192905_foobar.up.sql", 53 | Version: 20161122192905, 54 | Name: "foobar", 55 | Direction: direction.Up, 56 | Content: []byte(` 57 | CREATE TABLE yolo ( 58 | id integer primary key, 59 | msg string 60 | ); 61 | `), 62 | }, 63 | { 64 | Path: "/foobar", 65 | FileName: "20161122192905_foobar.down.sql", 66 | Version: 20161122192905, 67 | Name: "foobar", 68 | Direction: direction.Down, 69 | Content: []byte(` 70 | DROP TABLE yolo; 71 | `), 72 | }, 73 | } 74 | 75 | failFiles := []file.File{ 76 | { 77 | Path: "/foobar", 78 | FileName: "20161122193005_foobar.up.sql", 79 | Version: 20161122193005, 80 | Name: "foobar", 81 | Direction: direction.Up, 82 | Content: []byte(` 83 | CREATE TABLE error ( 84 | id THIS WILL CAUSE AN ERROR 85 | ) 86 | `), 87 | }, 88 | } 89 | 90 | for _, file := range successFiles { 91 | pipe := pipep.New() 92 | go driver.Migrate(file, pipe) 93 | errs := pipep.ReadErrors(pipe) 94 | if len(errs) > 0 { 95 | t.Fatal(errs) 96 | } 97 | } 98 | 99 | for _, file := range failFiles { 100 | pipe := pipep.New() 101 | go driver.Migrate(file, pipe) 102 | errs := pipep.ReadErrors(pipe) 103 | if len(errs) == 0 { 104 | t.Fatal("Migration should have failed but succeeded") 105 | } 106 | } 107 | 108 | if err := driver.Close(); err != nil { 109 | t.Fatal(err) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /driver/driver.go: -------------------------------------------------------------------------------- 1 | // Package driver holds the driver interface. 2 | package driver 3 | 4 | import ( 5 | "fmt" 6 | neturl "net/url" // alias to allow `url string` func signature in New 7 | 8 | "github.com/gemnasium/migrate/file" 9 | ) 10 | 11 | // Driver is the interface type that needs to implemented by all drivers. 12 | type Driver interface { 13 | 14 | // Initialize is the first function to be called. 15 | // Check the url string and open and verify any connection 16 | // that has to be made. 17 | Initialize(url string) error 18 | 19 | // Close is the last function to be called. 20 | // Close any open connection here. 21 | Close() error 22 | 23 | // FilenameExtension returns the extension of the migration files. 24 | // The returned string must not begin with a dot. 25 | FilenameExtension() string 26 | 27 | // Migrate is the heart of the driver. 28 | // It will receive a file which the driver should apply 29 | // to its backend or whatever. The migration function should use 30 | // the pipe channel to return any errors or other useful information. 31 | Migrate(file file.File, pipe chan interface{}) 32 | 33 | // Version returns the current migration version. 34 | Version() (file.Version, error) 35 | 36 | // Versions returns the list of applied migrations. 37 | Versions() (file.Versions, error) 38 | } 39 | 40 | // New returns Driver and calls Initialize on it. 41 | func New(url string) (Driver, error) { 42 | u, err := neturl.Parse(url) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | d := GetDriver(u.Scheme) 48 | if d == nil { 49 | return nil, fmt.Errorf("Driver '%s' not found.", u.Scheme) 50 | } 51 | verifyFilenameExtension(u.Scheme, d) 52 | if err := d.Initialize(url); err != nil { 53 | return nil, err 54 | } 55 | 56 | return d, nil 57 | } 58 | 59 | // verifyFilenameExtension panics if the driver's filename extension 60 | // is not correct or empty. 61 | func verifyFilenameExtension(driverName string, d Driver) { 62 | f := d.FilenameExtension() 63 | if f == "" { 64 | panic(fmt.Sprintf("%s.FilenameExtension() returns empty string.", driverName)) 65 | } 66 | if f[0:1] == "." { 67 | panic(fmt.Sprintf("%s.FilenameExtension() returned string must not start with a dot.", driverName)) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /driver/mysql/README.md: -------------------------------------------------------------------------------- 1 | # MySQL Driver 2 | 3 | ### See [issue #1](https://github.com/gemnasium/migrate/issues/1#issuecomment-58728186) before using this driver! 4 | 5 | * Runs migrations in transactions. 6 | That means that if a migration fails, it will be safely rolled back. 7 | * Tries to return helpful error messages. 8 | * Stores migration version details in table ``schema_migrations``. 9 | This table will be auto-generated. 10 | 11 | 12 | ## Usage 13 | 14 | ```bash 15 | migrate -url mysql://user@tcp(host:port)/database -path ./db/migrations create add_field_to_table 16 | migrate -url mysql://user@tcp(host:port)/database -path ./db/migrations up 17 | migrate help # for more info 18 | ``` 19 | 20 | See full [DSN (Data Source Name) documentation](https://github.com/go-sql-driver/mysql/#dsn-data-source-name). 21 | 22 | ### SSL 23 | 24 | The MySQL driver will set a TLS config if the following env variables are set: 25 | 26 | - `MYSQL_SERVER_CA` 27 | - `MYSQL_CLIENT_KEY` 28 | - `MYSQL_CLIENT_CERT` 29 | 30 | ## Authors 31 | 32 | * Matthias Kadenbach, https://github.com/gemnasium 33 | -------------------------------------------------------------------------------- /driver/mysql/mysql.go: -------------------------------------------------------------------------------- 1 | // Package mysql implements the Driver interface. 2 | package mysql 3 | 4 | import ( 5 | "bufio" 6 | "bytes" 7 | "crypto/tls" 8 | "crypto/x509" 9 | "database/sql" 10 | "errors" 11 | "fmt" 12 | "io/ioutil" 13 | "os" 14 | "regexp" 15 | "strconv" 16 | "strings" 17 | 18 | "github.com/gemnasium/migrate/driver" 19 | "github.com/gemnasium/migrate/file" 20 | "github.com/gemnasium/migrate/migrate/direction" 21 | "github.com/go-sql-driver/mysql" 22 | ) 23 | 24 | type Driver struct { 25 | db *sql.DB 26 | } 27 | 28 | const tableName = "schema_migrations" 29 | 30 | func (driver *Driver) Initialize(url string) error { 31 | urlWithoutScheme := strings.SplitN(url, "mysql://", 2) 32 | if len(urlWithoutScheme) != 2 { 33 | return errors.New("invalid mysql:// scheme") 34 | } 35 | 36 | // check if env vars vor mysql ssl connection are set and if yes use them 37 | if os.Getenv("MYSQL_SERVER_CA") != "" && os.Getenv("MYSQL_CLIENT_KEY") != "" && os.Getenv("MYSQL_CLIENT_CERT") != "" { 38 | rootCertPool := x509.NewCertPool() 39 | pem, err := ioutil.ReadFile(os.Getenv("MYSQL_SERVER_CA")) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | if ok := rootCertPool.AppendCertsFromPEM(pem); !ok { 45 | return errors.New("Failed to append PEM") 46 | } 47 | 48 | clientCert := make([]tls.Certificate, 0, 1) 49 | certs, err := tls.LoadX509KeyPair(os.Getenv("MYSQL_CLIENT_CERT"), os.Getenv("MYSQL_CLIENT_KEY")) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | clientCert = append(clientCert, certs) 55 | mysql.RegisterTLSConfig("custom", &tls.Config{ 56 | RootCAs: rootCertPool, 57 | Certificates: clientCert, 58 | InsecureSkipVerify: true, 59 | }) 60 | 61 | urlWithoutScheme[1] += "&tls=custom" 62 | } 63 | 64 | db, err := sql.Open("mysql", urlWithoutScheme[1]) 65 | if err != nil { 66 | return err 67 | } 68 | if err := db.Ping(); err != nil { 69 | return err 70 | } 71 | driver.db = db 72 | 73 | if err := driver.ensureVersionTableExists(); err != nil { 74 | return err 75 | } 76 | return nil 77 | } 78 | 79 | func (driver *Driver) Close() error { 80 | if err := driver.db.Close(); err != nil { 81 | return err 82 | } 83 | return nil 84 | } 85 | 86 | func (driver *Driver) ensureVersionTableExists() error { 87 | _, err := driver.db.Exec("CREATE TABLE IF NOT EXISTS " + tableName + " (version bigint not null primary key);") 88 | 89 | if err != nil { 90 | return err 91 | } 92 | r := driver.db.QueryRow("SELECT data_type FROM information_schema.columns where table_name = ? and column_name = 'version'", tableName) 93 | dataType := "" 94 | if err := r.Scan(&dataType); err != nil { 95 | return err 96 | } 97 | if dataType != "int" { 98 | return nil 99 | } 100 | _, err = driver.db.Exec("ALTER TABLE " + tableName + " MODIFY version bigint") 101 | return err 102 | } 103 | 104 | func (driver *Driver) FilenameExtension() string { 105 | return "sql" 106 | } 107 | 108 | func (driver *Driver) Migrate(f file.File, pipe chan interface{}) { 109 | defer close(pipe) 110 | pipe <- f 111 | 112 | // http://go-database-sql.org/modifying.html, Working with Transactions 113 | // 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. 114 | tx, err := driver.db.Begin() 115 | if err != nil { 116 | pipe <- err 117 | return 118 | } 119 | 120 | if f.Direction == direction.Up { 121 | if _, err := tx.Exec("INSERT INTO "+tableName+" (version) VALUES (?)", f.Version); err != nil { 122 | pipe <- err 123 | if err := tx.Rollback(); err != nil { 124 | pipe <- err 125 | } 126 | return 127 | } 128 | } else if f.Direction == direction.Down { 129 | if _, err := tx.Exec("DELETE FROM "+tableName+" WHERE version = ?", f.Version); err != nil { 130 | pipe <- err 131 | if err := tx.Rollback(); err != nil { 132 | pipe <- err 133 | } 134 | return 135 | } 136 | } 137 | 138 | if err := f.ReadContent(); err != nil { 139 | pipe <- err 140 | return 141 | } 142 | 143 | // TODO this is not good! unfortunately there is no mysql driver that 144 | // supports multiple statements per query. 145 | sqlStmts := bytes.Split(f.Content, []byte(";")) 146 | 147 | for _, sqlStmt := range sqlStmts { 148 | sqlStmt = bytes.TrimSpace(sqlStmt) 149 | if len(sqlStmt) > 0 { 150 | if _, err := tx.Exec(string(sqlStmt)); err != nil { 151 | mysqlErr, isErr := err.(*mysql.MySQLError) 152 | 153 | if isErr { 154 | re, err := regexp.Compile(`at line ([0-9]+)$`) 155 | if err != nil { 156 | pipe <- err 157 | if err := tx.Rollback(); err != nil { 158 | pipe <- err 159 | } 160 | } 161 | 162 | var lineNo int 163 | lineNoRe := re.FindStringSubmatch(mysqlErr.Message) 164 | if len(lineNoRe) == 2 { 165 | lineNo, err = strconv.Atoi(lineNoRe[1]) 166 | } 167 | if err == nil { 168 | 169 | // get white-space offset 170 | // TODO this is broken, because we use sqlStmt instead of f.Content 171 | wsLineOffset := 0 172 | b := bufio.NewReader(bytes.NewBuffer(sqlStmt)) 173 | for { 174 | line, _, err := b.ReadLine() 175 | if err != nil { 176 | break 177 | } 178 | if bytes.TrimSpace(line) == nil { 179 | wsLineOffset += 1 180 | } else { 181 | break 182 | } 183 | } 184 | 185 | message := mysqlErr.Error() 186 | message = re.ReplaceAllString(message, fmt.Sprintf("at line %v", lineNo+wsLineOffset)) 187 | 188 | errorPart := file.LinesBeforeAndAfter(sqlStmt, lineNo, 5, 5, true) 189 | pipe <- errors.New(fmt.Sprintf("%s\n\n%s", message, string(errorPart))) 190 | } else { 191 | pipe <- errors.New(mysqlErr.Error()) 192 | } 193 | 194 | if err := tx.Rollback(); err != nil { 195 | pipe <- err 196 | } 197 | 198 | return 199 | } 200 | } 201 | } 202 | } 203 | 204 | if err := tx.Commit(); err != nil { 205 | pipe <- err 206 | return 207 | } 208 | } 209 | 210 | // Version returns the current migration version. 211 | func (driver *Driver) Version() (file.Version, error) { 212 | var version file.Version 213 | err := driver.db.QueryRow("SELECT version FROM " + tableName + " ORDER BY version DESC").Scan(&version) 214 | switch { 215 | case err == sql.ErrNoRows: 216 | return 0, nil 217 | case err != nil: 218 | return 0, err 219 | default: 220 | return version, nil 221 | } 222 | } 223 | 224 | // Versions returns the list of applied migrations. 225 | func (driver *Driver) Versions() (file.Versions, error) { 226 | versions := file.Versions{} 227 | 228 | rows, err := driver.db.Query("SELECT version FROM " + tableName + " ORDER BY version DESC") 229 | if err != nil { 230 | return versions, err 231 | } 232 | defer rows.Close() 233 | for rows.Next() { 234 | var version file.Version 235 | err := rows.Scan(&version) 236 | if err != nil { 237 | return versions, err 238 | } 239 | versions = append(versions, version) 240 | } 241 | err = rows.Err() 242 | return versions, err 243 | } 244 | 245 | func init() { 246 | driver.RegisterDriver("mysql", &Driver{}) 247 | } 248 | -------------------------------------------------------------------------------- /driver/mysql/mysql_test.go: -------------------------------------------------------------------------------- 1 | package mysql 2 | 3 | import ( 4 | "database/sql" 5 | "os" 6 | "reflect" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/gemnasium/migrate/file" 11 | "github.com/gemnasium/migrate/migrate/direction" 12 | pipep "github.com/gemnasium/migrate/pipe" 13 | ) 14 | 15 | // TestMigrate runs some additional tests on Migrate(). 16 | // Basic testing is already done in migrate/migrate_test.go 17 | func TestMigrate(t *testing.T) { 18 | host := os.Getenv("MYSQL_PORT_3306_TCP_ADDR") 19 | port := os.Getenv("MYSQL_PORT_3306_TCP_PORT") 20 | driverURL := "mysql://root@tcp(" + host + ":" + port + ")/migratetest" 21 | 22 | // prepare clean database 23 | connection, err := sql.Open("mysql", strings.SplitN(driverURL, "mysql://", 2)[1]) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | dropTestTables(t, connection) 29 | 30 | migrate(t, driverURL) 31 | 32 | dropTestTables(t, connection) 33 | 34 | // Make an old-style 32-bit int version column that we'll have to upgrade. 35 | _, err = connection.Exec("CREATE TABLE IF NOT EXISTS " + tableName + " (version int not null primary key);") 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | 40 | migrate(t, driverURL) 41 | } 42 | 43 | func migrate(t *testing.T, driverURL string) { 44 | d := &Driver{} 45 | if err := d.Initialize(driverURL); err != nil { 46 | t.Fatal(err) 47 | } 48 | 49 | files := []file.File{ 50 | { 51 | Path: "/foobar", 52 | FileName: "20060102150405_foobar.up.sql", 53 | Version: 20060102150405, 54 | Name: "foobar", 55 | Direction: direction.Up, 56 | Content: []byte(` 57 | CREATE TABLE yolo ( 58 | id int(11) not null primary key auto_increment 59 | ); 60 | 61 | CREATE TABLE yolo1 ( 62 | id int(11) not null primary key auto_increment 63 | ); 64 | `), 65 | }, 66 | { 67 | Path: "/foobar", 68 | FileName: "20060102150405_foobar.down.sql", 69 | Version: 20060102150405, 70 | Name: "foobar", 71 | Direction: direction.Down, 72 | Content: []byte(` 73 | DROP TABLE yolo; 74 | `), 75 | }, 76 | { 77 | Path: "/foobar", 78 | FileName: "20070000000000_foobar.up.sql", 79 | Version: 20070000000000, 80 | Name: "foobar", 81 | Direction: direction.Up, 82 | Content: []byte(` 83 | 84 | // a comment 85 | CREATE TABLE error ( 86 | id THIS WILL CAUSE AN ERROR 87 | ); 88 | `), 89 | }, 90 | } 91 | 92 | pipe := pipep.New() 93 | go d.Migrate(files[0], pipe) 94 | errs := pipep.ReadErrors(pipe) 95 | if len(errs) > 0 { 96 | t.Fatal(errs) 97 | } 98 | 99 | version, err := d.Version() 100 | if err != nil { 101 | t.Fatal(err) 102 | } 103 | 104 | if version != 20060102150405 { 105 | t.Errorf("Expected version to be: %d, got: %d", 20060102150405, version) 106 | } 107 | 108 | // Check versions applied in DB 109 | expectedVersions := file.Versions{20060102150405} 110 | versions, err := d.Versions() 111 | if err != nil { 112 | t.Errorf("Could not fetch versions: %s", err) 113 | } 114 | 115 | pipe = pipep.New() 116 | go d.Migrate(files[1], pipe) 117 | errs = pipep.ReadErrors(pipe) 118 | if len(errs) > 0 { 119 | t.Fatal(errs) 120 | } 121 | 122 | pipe = pipep.New() 123 | go d.Migrate(files[2], pipe) 124 | errs = pipep.ReadErrors(pipe) 125 | if len(errs) == 0 { 126 | t.Error("Expected test case to fail") 127 | } 128 | 129 | // Check versions applied in DB 130 | expectedVersions = file.Versions{} 131 | versions, err = d.Versions() 132 | if err != nil { 133 | t.Errorf("Could not fetch versions: %s", err) 134 | } 135 | 136 | if !reflect.DeepEqual(versions, expectedVersions) { 137 | t.Errorf("Expected versions to be: %v, got: %v", expectedVersions, versions) 138 | } 139 | 140 | if err := d.Close(); err != nil { 141 | t.Fatal(err) 142 | } 143 | } 144 | 145 | func dropTestTables(t *testing.T, db *sql.DB) { 146 | if _, err := db.Exec(`DROP TABLE IF EXISTS yolo, yolo1, ` + tableName); err != nil { 147 | t.Fatal(err) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /driver/postgres/README.md: -------------------------------------------------------------------------------- 1 | # PostgreSQL Driver 2 | 3 | * Runs migrations in transactions. 4 | That means that if a migration fails, it will be safely rolled back. 5 | * Tries to return helpful error messages. 6 | * Stores migration version details in table ``schema_migrations``. 7 | This table will be auto-generated. 8 | 9 | 10 | ## Usage 11 | 12 | ```bash 13 | migrate -url postgres://user@host:port/database -path ./db/migrations create add_field_to_table 14 | migrate -url postgres://user@host:port/database -path ./db/migrations up 15 | migrate help # for more info 16 | 17 | # TODO(gemnasium): thinking about adding some custom flag to allow migration within schemas: 18 | -url="postgres://user@host:port/database?schema=name" 19 | ``` 20 | 21 | ## Disable DDL transactions 22 | 23 | Some queries, like `alter type ... add value` cannot be executed inside a transaction block. 24 | Since all migrations are executed in a transaction block by default (per migration file), a special option must be specified inside the migration file: 25 | 26 | ```sql 27 | -- disable_ddl_transaction 28 | alter type ...; 29 | ``` 30 | 31 | The option `disable_ddl_transaction` must be in a sql comment of the first line of the migration file. 32 | 33 | Please note that you can't put several `alter type ... add value ...` in a single file. Doing so will result in a `ERROR 25001: ALTER TYPE ... ADD cannot be executed from a function or multi-command string` sql exception during migration. 34 | 35 | Since the file will be executed without transaction, it's probably not a good idea to exec more than one statement anyway. If the last statement of the file fails, chances to run again the migration without error will be very limited. 36 | 37 | -------------------------------------------------------------------------------- /driver/postgres/postgres.go: -------------------------------------------------------------------------------- 1 | // Package postgres implements the Driver interface. 2 | package postgres 3 | 4 | import ( 5 | "database/sql" 6 | "fmt" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/gemnasium/migrate/driver" 11 | "github.com/gemnasium/migrate/file" 12 | "github.com/gemnasium/migrate/migrate/direction" 13 | "github.com/jmoiron/sqlx" 14 | "github.com/lib/pq" 15 | ) 16 | 17 | type Driver struct { 18 | db *sqlx.DB 19 | } 20 | 21 | const tableName = "schema_migrations" 22 | const txDisabledOption = "disable_ddl_transaction" 23 | 24 | func (driver *Driver) Initialize(url string) error { 25 | db, err := sqlx.Open("postgres", url) 26 | if err != nil { 27 | return err 28 | } 29 | if err := db.Ping(); err != nil { 30 | return err 31 | } 32 | driver.db = db 33 | 34 | return driver.ensureVersionTableExists() 35 | } 36 | 37 | func (driver *Driver) SetDB(db *sql.DB) { 38 | driver.db = sqlx.NewDb(db, "postgres") 39 | } 40 | 41 | func (driver *Driver) Close() error { 42 | return driver.db.Close() 43 | } 44 | 45 | func (driver *Driver) ensureVersionTableExists() error { 46 | // avoid DDL statements if possible for BDR (see #23) 47 | var c int 48 | driver.db.Get(&c, "SELECT count(*) FROM information_schema.tables WHERE table_name = $1;", tableName) 49 | if c > 0 { 50 | // table schema_migrations already exists, check if the schema is correct, ie: version is a bigint 51 | 52 | var dataType string 53 | err := driver.db.Get(&dataType, "SELECT data_type FROM information_schema.columns where table_name = $1 and column_name = 'version'", tableName) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | if dataType == "bigint" { 59 | return nil 60 | } 61 | 62 | _, err = driver.db.Exec("ALTER TABLE " + tableName + " ALTER COLUMN version TYPE bigint USING version::bigint") 63 | return err 64 | } 65 | 66 | _, err := driver.db.Exec("CREATE TABLE IF NOT EXISTS " + tableName + " (version bigint not null primary key);") 67 | return err 68 | } 69 | 70 | func (driver *Driver) FilenameExtension() string { 71 | return "sql" 72 | } 73 | 74 | func (driver *Driver) Migrate(f file.File, pipe chan interface{}) { 75 | defer close(pipe) 76 | pipe <- f 77 | 78 | tx, err := driver.db.Begin() 79 | if err != nil { 80 | pipe <- err 81 | return 82 | } 83 | 84 | if f.Direction == direction.Up { 85 | if _, err := tx.Exec("INSERT INTO "+tableName+" (version) VALUES ($1)", f.Version); err != nil { 86 | pipe <- err 87 | if err := tx.Rollback(); err != nil { 88 | pipe <- err 89 | } 90 | return 91 | } 92 | } else if f.Direction == direction.Down { 93 | if _, err := tx.Exec("DELETE FROM "+tableName+" WHERE version=$1", f.Version); err != nil { 94 | pipe <- err 95 | if err := tx.Rollback(); err != nil { 96 | pipe <- err 97 | } 98 | return 99 | } 100 | } 101 | 102 | if err := f.ReadContent(); err != nil { 103 | pipe <- err 104 | return 105 | } 106 | 107 | if txDisabled(fileOptions(f.Content)) { 108 | _, err = driver.db.Exec(string(f.Content)) 109 | } else { 110 | _, err = tx.Exec(string(f.Content)) 111 | } 112 | 113 | if err != nil { 114 | pqErr := err.(*pq.Error) 115 | offset, err := strconv.Atoi(pqErr.Position) 116 | if err == nil && offset >= 0 { 117 | lineNo, columnNo := file.LineColumnFromOffset(f.Content, offset-1) 118 | errorPart := file.LinesBeforeAndAfter(f.Content, lineNo, 5, 5, true) 119 | pipe <- fmt.Errorf("%s %v: %s in line %v, column %v:\n\n%s", pqErr.Severity, pqErr.Code, pqErr.Message, lineNo, columnNo, string(errorPart)) 120 | } else { 121 | pipe <- fmt.Errorf("%s %v: %s", pqErr.Severity, pqErr.Code, pqErr.Message) 122 | } 123 | 124 | if err := tx.Rollback(); err != nil { 125 | pipe <- err 126 | } 127 | return 128 | } 129 | 130 | if err := tx.Commit(); err != nil { 131 | pipe <- err 132 | return 133 | } 134 | } 135 | 136 | // Version returns the current migration version. 137 | func (driver *Driver) Version() (file.Version, error) { 138 | var version file.Version 139 | err := driver.db.Get(&version, "SELECT version FROM "+tableName+" ORDER BY version DESC LIMIT 1") 140 | if err == sql.ErrNoRows { 141 | return version, nil 142 | } 143 | 144 | return version, err 145 | } 146 | 147 | // Versions returns the list of applied migrations. 148 | func (driver *Driver) Versions() (file.Versions, error) { 149 | versions := file.Versions{} 150 | err := driver.db.Select(&versions, "SELECT version FROM "+tableName+" ORDER BY version DESC") 151 | return versions, err 152 | } 153 | 154 | // fileOptions returns the list of options extracted from the first line of the file content. 155 | // Format: "-- <...>" 156 | func fileOptions(content []byte) []string { 157 | firstLine := strings.Split(string(content), "\n")[0] 158 | if !strings.HasPrefix(firstLine, "-- ") { 159 | return []string{} 160 | } 161 | opts := strings.TrimPrefix(firstLine, "-- ") 162 | return strings.Split(opts, " ") 163 | } 164 | 165 | func txDisabled(opts []string) bool { 166 | for _, v := range opts { 167 | if v == txDisabledOption { 168 | return true 169 | } 170 | } 171 | return false 172 | } 173 | 174 | func init() { 175 | driver.RegisterDriver("postgres", &Driver{}) 176 | } 177 | -------------------------------------------------------------------------------- /driver/postgres/postgres_test.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "database/sql" 5 | "os" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/gemnasium/migrate/file" 10 | "github.com/gemnasium/migrate/migrate/direction" 11 | pipep "github.com/gemnasium/migrate/pipe" 12 | ) 13 | 14 | // TestMigrate runs some additional tests on Migrate(). 15 | // Basic testing is already done in migrate/migrate_test.go 16 | func TestMigrate(t *testing.T) { 17 | host := os.Getenv("POSTGRES_PORT_5432_TCP_ADDR") 18 | port := os.Getenv("POSTGRES_PORT_5432_TCP_PORT") 19 | driverUrl := "postgres://postgres@" + host + ":" + port + "/template1?sslmode=disable" 20 | 21 | // prepare clean database 22 | connection, err := sql.Open("postgres", driverUrl) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | dropTestTables(t, connection) 28 | 29 | migrate(t, driverUrl) 30 | 31 | dropTestTables(t, connection) 32 | 33 | // Make an old-style `int` version column that we'll have to upgrade. 34 | _, err = connection.Exec("CREATE TABLE IF NOT EXISTS " + tableName + " (version int not null primary key)") 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | 39 | migrate(t, driverUrl) 40 | } 41 | 42 | func migrate(t *testing.T, driverUrl string) { 43 | d := &Driver{} 44 | if err := d.Initialize(driverUrl); err != nil { 45 | t.Fatal(err) 46 | } 47 | 48 | // testing idempotency: second call should be a no-op, since table already exists 49 | if err := d.Initialize(driverUrl); err != nil { 50 | t.Fatal(err) 51 | } 52 | 53 | files := []file.File{ 54 | { 55 | Path: "/foobar", 56 | FileName: "20060102150405_foobar.up.sql", 57 | Version: 20060102150405, 58 | Name: "foobar", 59 | Direction: direction.Up, 60 | Content: []byte(` 61 | CREATE TABLE yolo ( 62 | id serial not null primary key 63 | ); 64 | CREATE TYPE colors AS ENUM ( 65 | 'red', 66 | 'green' 67 | ); 68 | `), 69 | }, 70 | { 71 | Path: "/foobar", 72 | FileName: "20060102150405_foobar.down.sql", 73 | Version: 20060102150405, 74 | Name: "foobar", 75 | Direction: direction.Down, 76 | Content: []byte(` 77 | DROP TABLE yolo; 78 | `), 79 | }, 80 | { 81 | Path: "/foobar", 82 | FileName: "20060102150406_foobar.up.sql", 83 | Version: 20060102150406, 84 | Name: "foobar", 85 | Direction: direction.Up, 86 | Content: []byte(`-- disable_ddl_transaction 87 | ALTER TYPE colors ADD VALUE 'blue' AFTER 'red'; 88 | `), 89 | }, 90 | { 91 | Path: "/foobar", 92 | FileName: "20060102150406_foobar.down.sql", 93 | Version: 20060102150406, 94 | Name: "foobar", 95 | Direction: direction.Down, 96 | Content: []byte(` 97 | DROP TYPE colors; 98 | `), 99 | }, 100 | { 101 | Path: "/foobar", 102 | FileName: "20060102150407_foobar.up.sql", 103 | Version: 20060102150407, 104 | Name: "foobar", 105 | Direction: direction.Up, 106 | Content: []byte(` 107 | CREATE TABLE error ( 108 | id THIS WILL CAUSE AN ERROR 109 | ) 110 | `), 111 | }, 112 | } 113 | 114 | // should create table yolo 115 | pipe := pipep.New() 116 | go d.Migrate(files[0], pipe) 117 | errs := pipep.ReadErrors(pipe) 118 | if len(errs) > 0 { 119 | t.Fatal(errs) 120 | } 121 | 122 | version, err := d.Version() 123 | if err != nil { 124 | t.Fatal(err) 125 | } 126 | 127 | if version != 20060102150405 { 128 | t.Errorf("Expected version to be: %d, got: %d", 20060102150405, version) 129 | } 130 | 131 | // Check versions applied in DB 132 | expectedVersions := file.Versions{20060102150405} 133 | versions, err := d.Versions() 134 | if err != nil { 135 | t.Errorf("Could not fetch versions: %s", err) 136 | } 137 | 138 | if !reflect.DeepEqual(versions, expectedVersions) { 139 | t.Errorf("Expected versions to be: %v, got: %v", expectedVersions, versions) 140 | } 141 | 142 | // should alter type colors 143 | pipe = pipep.New() 144 | go d.Migrate(files[2], pipe) 145 | errs = pipep.ReadErrors(pipe) 146 | if len(errs) > 0 { 147 | t.Fatal(errs) 148 | } 149 | 150 | colors := []string{} 151 | expectedColors := []string{"red", "blue", "green"} 152 | d.db.Select(&colors, "SELECT unnest(enum_range(NULL::colors));") 153 | if !reflect.DeepEqual(colors, expectedColors) { 154 | t.Errorf("Expected colors enum to be %q, got %q\n", expectedColors, colors) 155 | } 156 | 157 | pipe = pipep.New() 158 | go d.Migrate(files[3], pipe) 159 | errs = pipep.ReadErrors(pipe) 160 | if len(errs) > 0 { 161 | t.Fatal(errs) 162 | } 163 | 164 | pipe = pipep.New() 165 | go d.Migrate(files[1], pipe) 166 | errs = pipep.ReadErrors(pipe) 167 | if len(errs) > 0 { 168 | t.Fatal(errs) 169 | } 170 | 171 | pipe = pipep.New() 172 | go d.Migrate(files[4], pipe) 173 | errs = pipep.ReadErrors(pipe) 174 | if len(errs) == 0 { 175 | t.Error("Expected test case to fail") 176 | } 177 | 178 | // Check versions applied in DB 179 | expectedVersions = file.Versions{} 180 | versions, err = d.Versions() 181 | if err != nil { 182 | t.Errorf("Could not fetch versions: %s", err) 183 | } 184 | 185 | if !reflect.DeepEqual(versions, expectedVersions) { 186 | t.Errorf("Expected versions to be: %v, got: %v", expectedVersions, versions) 187 | } 188 | 189 | if err := d.Close(); err != nil { 190 | t.Fatal(err) 191 | } 192 | 193 | } 194 | 195 | func dropTestTables(t *testing.T, db *sql.DB) { 196 | if _, err := db.Exec(` 197 | DROP TYPE IF EXISTS colors; 198 | DROP TABLE IF EXISTS yolo; 199 | DROP TABLE IF EXISTS ` + tableName + `;`); err != nil { 200 | t.Fatal(err) 201 | } 202 | 203 | } 204 | -------------------------------------------------------------------------------- /driver/registry.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "sort" 5 | "sync" 6 | ) 7 | 8 | var driversMu sync.Mutex 9 | var drivers = make(map[string]Driver) 10 | 11 | // Registers a driver so it can be created from its name. Drivers should call 12 | // this from an init() function so that they registers themselves on import. 13 | func RegisterDriver(name string, driver Driver) { 14 | driversMu.Lock() 15 | defer driversMu.Unlock() 16 | if driver == nil { 17 | panic("driver: Register driver is nil") 18 | } 19 | if _, dup := drivers[name]; dup { 20 | panic("sql: Register called twice for driver " + name) 21 | } 22 | drivers[name] = driver 23 | } 24 | 25 | // Retrieves a registered driver by name. 26 | func GetDriver(name string) Driver { 27 | driversMu.Lock() 28 | defer driversMu.Unlock() 29 | driver := drivers[name] 30 | return driver 31 | } 32 | 33 | // Drivers returns a sorted list of the names of the registered drivers. 34 | func Drivers() []string { 35 | driversMu.Lock() 36 | defer driversMu.Unlock() 37 | var list []string 38 | for name := range drivers { 39 | list = append(list, name) 40 | } 41 | sort.Strings(list) 42 | return list 43 | } 44 | -------------------------------------------------------------------------------- /driver/sqlite3/README.md: -------------------------------------------------------------------------------- 1 | # Sqlite3 Driver 2 | 3 | * Runs migrations in transactions. 4 | That means that if a migration fails, it will be safely rolled back. 5 | * Tries to return helpful error messages. 6 | * Stores migration version details in table ``schema_migrations``. 7 | This table will be auto-generated. 8 | 9 | 10 | ## Usage 11 | 12 | ```bash 13 | migrate -url sqlite3://database.sqlite -path ./db/migrations create add_field_to_table 14 | migrate -url sqlite3://database.sqlite -path ./db/migrations up 15 | migrate help # for more info 16 | ``` 17 | 18 | ## Authors 19 | 20 | * Matthias Kadenbach, https://github.com/gemnasium 21 | * Caesar Wirth, https://github.com/cjwirth 22 | -------------------------------------------------------------------------------- /driver/sqlite3/sqlite3.go: -------------------------------------------------------------------------------- 1 | // Package sqlite3 implements the Driver interface. 2 | package sqlite3 3 | 4 | import ( 5 | "database/sql" 6 | "errors" 7 | "fmt" 8 | "strings" 9 | 10 | "github.com/gemnasium/migrate/driver" 11 | "github.com/gemnasium/migrate/file" 12 | "github.com/gemnasium/migrate/migrate/direction" 13 | "github.com/mattn/go-sqlite3" 14 | ) 15 | 16 | type Driver struct { 17 | db *sql.DB 18 | } 19 | 20 | const tableName = "schema_migration" 21 | 22 | func (driver *Driver) Initialize(url string) error { 23 | filename := strings.SplitN(url, "sqlite3://", 2) 24 | if len(filename) != 2 { 25 | return errors.New("invalid sqlite3:// scheme") 26 | } 27 | 28 | db, err := sql.Open("sqlite3", filename[1]) 29 | if err != nil { 30 | return err 31 | } 32 | if err := db.Ping(); err != nil { 33 | return err 34 | } 35 | driver.db = db 36 | 37 | if err := driver.ensureVersionTableExists(); err != nil { 38 | return err 39 | } 40 | return nil 41 | } 42 | 43 | func (driver *Driver) Close() error { 44 | if err := driver.db.Close(); err != nil { 45 | return err 46 | } 47 | return nil 48 | } 49 | 50 | func (driver *Driver) ensureVersionTableExists() error { 51 | if _, err := driver.db.Exec("CREATE TABLE IF NOT EXISTS " + tableName + " (version INTEGER PRIMARY KEY AUTOINCREMENT);"); err != nil { 52 | return err 53 | } 54 | return nil 55 | } 56 | 57 | func (driver *Driver) FilenameExtension() string { 58 | return "sql" 59 | } 60 | 61 | func (driver *Driver) Migrate(f file.File, pipe chan interface{}) { 62 | defer close(pipe) 63 | pipe <- f 64 | 65 | tx, err := driver.db.Begin() 66 | if err != nil { 67 | pipe <- err 68 | return 69 | } 70 | 71 | if f.Direction == direction.Up { 72 | if _, err := tx.Exec("INSERT INTO "+tableName+" (version) VALUES (?)", f.Version); err != nil { 73 | pipe <- err 74 | if err := tx.Rollback(); err != nil { 75 | pipe <- err 76 | } 77 | return 78 | } 79 | } else if f.Direction == direction.Down { 80 | if _, err := tx.Exec("DELETE FROM "+tableName+" WHERE version=?", f.Version); err != nil { 81 | pipe <- err 82 | if err := tx.Rollback(); err != nil { 83 | pipe <- err 84 | } 85 | return 86 | } 87 | } 88 | 89 | if err := f.ReadContent(); err != nil { 90 | pipe <- err 91 | return 92 | } 93 | 94 | queries := splitStatements(string(f.Content)) 95 | for _, query := range queries { 96 | if _, err := tx.Exec(query); err != nil { 97 | sqliteErr, isErr := err.(sqlite3.Error) 98 | if isErr { 99 | // The sqlite3 library only provides error codes, not position information. Output what we do know. 100 | pipe <- fmt.Errorf("SQLite Error (%s); Extended (%s)\nError: %s", 101 | sqliteErr.Code.Error(), sqliteErr.ExtendedCode.Error(), sqliteErr.Error()) 102 | } else { 103 | pipe <- fmt.Errorf("An error occurred when running query [%q]: %v", query, err) 104 | } 105 | if err := tx.Rollback(); err != nil { 106 | pipe <- err 107 | } 108 | return 109 | } 110 | } 111 | 112 | if err := tx.Commit(); err != nil { 113 | pipe <- err 114 | return 115 | } 116 | } 117 | 118 | // Version returns the current migration version. 119 | func (driver *Driver) Version() (file.Version, error) { 120 | var version file.Version 121 | err := driver.db.QueryRow("SELECT version FROM " + tableName + " ORDER BY version DESC LIMIT 1").Scan(&version) 122 | switch { 123 | case err == sql.ErrNoRows: 124 | return 0, nil 125 | case err != nil: 126 | return 0, err 127 | default: 128 | return version, nil 129 | } 130 | } 131 | 132 | // Versions returns the list of applied migrations. 133 | func (driver *Driver) Versions() (file.Versions, error) { 134 | versions := file.Versions{} 135 | 136 | rows, err := driver.db.Query("SELECT version FROM " + tableName + " ORDER BY version DESC") 137 | if err != nil { 138 | return versions, err 139 | } 140 | defer rows.Close() 141 | for rows.Next() { 142 | var version file.Version 143 | err := rows.Scan(&version) 144 | if err != nil { 145 | return versions, err 146 | } 147 | versions = append(versions, version) 148 | } 149 | err = rows.Err() 150 | return versions, err 151 | } 152 | 153 | func init() { 154 | driver.RegisterDriver("sqlite3", &Driver{}) 155 | } 156 | 157 | // This naive implementation doesn't account for quoted ";" inside statements. 158 | // It should work for most migrations but can be improved in the future. 159 | func splitStatements(in string) []string { 160 | result := make([]string, 0) 161 | 162 | qs := strings.Split(in, ";") 163 | for _, q := range qs { 164 | if q = strings.TrimSpace(q); q != "" { 165 | result = append(result, q+";") 166 | } 167 | } 168 | return result 169 | } 170 | -------------------------------------------------------------------------------- /driver/sqlite3/sqlite3_test.go: -------------------------------------------------------------------------------- 1 | package sqlite3 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/gemnasium/migrate/file" 10 | "github.com/gemnasium/migrate/migrate/direction" 11 | pipep "github.com/gemnasium/migrate/pipe" 12 | ) 13 | 14 | // TestMigrate runs some additional tests on Migrate() 15 | // Basic testing is already done in migrate/migrate_test.go 16 | func TestMigrate(t *testing.T) { 17 | f, err := ioutil.TempFile(os.TempDir(), "migrate_test") 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | defer os.Remove(f.Name()) 22 | 23 | d := &Driver{} 24 | if err := d.Initialize("sqlite3://" + f.Name()); err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | files := []file.File{ 29 | { 30 | Path: "/foobar", 31 | FileName: "20060102150405_foobar.up.sql", 32 | Version: 20060102150405, 33 | Name: "foobar", 34 | Direction: direction.Up, 35 | Content: []byte(` 36 | CREATE TABLE yolo ( 37 | id INTEGER PRIMARY KEY AUTOINCREMENT 38 | ); 39 | `), 40 | }, 41 | { 42 | Path: "/foobar", 43 | FileName: "20060102200405_alter_table.up.sql", 44 | Version: 20060102200405, 45 | Name: "alter_table", 46 | Direction: direction.Up, 47 | Content: []byte(` 48 | ALTER TABLE yolo ADD COLUMN data1 VCHAR(255); 49 | ALTER TABLE yolo ADD COLUMN data2 VCHAR(255); 50 | `), 51 | }, 52 | { 53 | Path: "/foobar", 54 | FileName: "20060102150405_foobar.down.sql", 55 | Version: 20060102150405, 56 | Name: "foobar", 57 | Direction: direction.Down, 58 | Content: []byte(` 59 | DROP TABLE yolo; 60 | `), 61 | }, 62 | { 63 | Path: "/foobar", 64 | FileName: "20060102150406_failing.up.sql", 65 | Version: 20060103200406, 66 | Name: "failing", 67 | Direction: direction.Down, 68 | Content: []byte(` 69 | CREATE TABLE error ( 70 | THIS; WILL CAUSE; AN ERROR; 71 | ) 72 | `), 73 | }, 74 | } 75 | 76 | pipe := pipep.New() 77 | go d.Migrate(files[0], pipe) 78 | errs := pipep.ReadErrors(pipe) 79 | if len(errs) > 0 { 80 | t.Fatal(errs) 81 | } 82 | 83 | version, err := d.Version() 84 | if err != nil { 85 | t.Fatal(err) 86 | } 87 | if version != files[0].Version { 88 | t.Errorf("Expected version to be: %d, got: %d", files[0].Version, version) 89 | } 90 | 91 | // Check versions applied in DB. 92 | expectedVersions := file.Versions{files[0].Version} 93 | versions, err := d.Versions() 94 | if err != nil { 95 | t.Errorf("Could not fetch versions: %s", err) 96 | } 97 | if !reflect.DeepEqual(versions, expectedVersions) { 98 | t.Errorf("Expected versions to be: %v, got: %v", expectedVersions, versions) 99 | } 100 | 101 | pipe = pipep.New() 102 | go d.Migrate(files[1], pipe) 103 | errs = pipep.ReadErrors(pipe) 104 | if len(errs) > 0 { 105 | t.Fatal(errs) 106 | } 107 | if _, err := d.db.Query("SELECT id, data1, data2 FROM yolo"); err != nil { 108 | t.Errorf("Sequential migration failed: %v", err) 109 | } 110 | 111 | // Check versions applied in DB. 112 | expectedVersions = file.Versions{files[1].Version, files[0].Version} 113 | versions, err = d.Versions() 114 | if err != nil { 115 | t.Errorf("Could not fetch versions: %s", err) 116 | } 117 | if !reflect.DeepEqual(versions, expectedVersions) { 118 | t.Errorf("Expected versions to be: %v, got: %v", expectedVersions, versions) 119 | } 120 | 121 | pipe = pipep.New() 122 | go d.Migrate(files[2], pipe) 123 | errs = pipep.ReadErrors(pipe) 124 | if len(errs) > 0 { 125 | t.Fatal(errs) 126 | } 127 | 128 | pipe = pipep.New() 129 | go d.Migrate(files[3], pipe) 130 | errs = pipep.ReadErrors(pipe) 131 | if len(errs) == 0 { 132 | t.Error("Expected test case to fail") 133 | } 134 | 135 | if err := d.Close(); err != nil { 136 | t.Fatal(err) 137 | } 138 | } 139 | 140 | func TestSplitStatements(t *testing.T) { 141 | testCases := []struct { 142 | name string 143 | q string 144 | want []string 145 | }{ 146 | {"empty noop", "", []string{}}, 147 | {"single query", "CREATE TABLE a id INT;", []string{"CREATE TABLE a id INT;"}}, 148 | {"multiple queries", "CREATE TABLE a id INT; CREATE TABLE b id INT; ", 149 | []string{"CREATE TABLE a id INT;", "CREATE TABLE b id INT;"}, 150 | }, 151 | {"with line breaks", "CREATE TABLE a id INT;\n\n\t CREATE TABLE b id INT; ", 152 | []string{"CREATE TABLE a id INT;", "CREATE TABLE b id INT;"}, 153 | }, 154 | } 155 | for _, tc := range testCases { 156 | got := splitStatements(tc.q) 157 | if !reflect.DeepEqual(got, tc.want) { 158 | t.Errorf("(%s) splitStatements(%q) = %q, want: %q", tc.name, tc.q, got, tc.want) 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /file/file.go: -------------------------------------------------------------------------------- 1 | // Package file contains functions for low-level migration files handling. 2 | package file 3 | 4 | import ( 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "go/token" 9 | "io/ioutil" 10 | "path" 11 | "regexp" 12 | "sort" 13 | "strconv" 14 | "strings" 15 | 16 | "github.com/gemnasium/migrate/migrate/direction" 17 | ) 18 | 19 | var filenameRegex = `^([0-9]+)_(.*)\.(up|down)\.%s$` 20 | 21 | // FilenameRegex builds regular expression stmt with given 22 | // filename extension from driver. 23 | func FilenameRegex(filenameExtension string) *regexp.Regexp { 24 | return regexp.MustCompile(fmt.Sprintf(filenameRegex, filenameExtension)) 25 | } 26 | 27 | type Version uint64 // Version is the migration version. 28 | type Versions []Version // Versions is the list of migrations. 29 | 30 | // Contains checks if a _version_ is contained in the list of migrations. 31 | func (versions Versions) Contains(version Version) bool { 32 | for _, v := range versions { 33 | if v == version { 34 | return true 35 | } 36 | } 37 | return false 38 | } 39 | 40 | // Len is the number of elements in the collection. 41 | // Required by Sort Interface{} 42 | func (versions Versions) Len() int { 43 | return len(versions) 44 | } 45 | 46 | // Less reports whether the element with index i should sort before the element 47 | // with index j. Required by Sort Interface{}. 48 | func (versions Versions) Less(i, j int) bool { 49 | return versions[i] < versions[j] 50 | } 51 | 52 | // Swap swaps the elements with indexes i and j. Required by Sort Interface{}. 53 | func (versions Versions) Swap(i, j int) { 54 | versions[i], versions[j] = versions[j], versions[i] 55 | } 56 | 57 | // File represents one file on disk. 58 | // Example: 20060102150405_initial_plan_to_do_sth.up.sql 59 | type File struct { 60 | // absolute path to file 61 | Path string 62 | 63 | // the name of the file 64 | FileName string 65 | 66 | // version parsed from filename 67 | Version Version 68 | 69 | // the actual migration name parsed from filename 70 | Name string 71 | 72 | // content of the file 73 | Content []byte 74 | 75 | // UP or DOWN migration 76 | Direction direction.Direction 77 | } 78 | 79 | // Files is a slice of Files. 80 | type Files []File 81 | 82 | // MigrationFile represents both the UP and the DOWN migration file. 83 | type MigrationFile struct { 84 | // version of the migration file, parsed from the filenames 85 | Version Version 86 | 87 | // reference to the *up* migration file 88 | UpFile *File 89 | 90 | // reference to the *down* migration file 91 | DownFile *File 92 | } 93 | 94 | // MigrationFiles is a slice of MigrationFiles. 95 | type MigrationFiles []MigrationFile 96 | 97 | // ReadContent reads the file into the content if it's empty. 98 | func (f *File) ReadContent() error { 99 | if len(f.Content) == 0 { 100 | content, err := ioutil.ReadFile(path.Join(f.Path, f.FileName)) 101 | if err != nil { 102 | return err 103 | } 104 | f.Content = content 105 | } 106 | return nil 107 | } 108 | 109 | // Pending returns the list of pending migration files. 110 | func (mf *MigrationFiles) Pending(versions Versions) (Files, error) { 111 | sort.Sort(mf) 112 | files := make(Files, 0) 113 | for _, migrationFile := range *mf { 114 | if !versions.Contains(migrationFile.Version) && migrationFile.UpFile != nil { 115 | files = append(files, *migrationFile.UpFile) 116 | } 117 | } 118 | return files, nil 119 | } 120 | 121 | // Applied returns the list of applied migration files. 122 | func (mf *MigrationFiles) Applied(versions Versions) (Files, error) { 123 | sort.Sort(sort.Reverse(mf)) 124 | files := make(Files, 0) 125 | for _, migrationFile := range *mf { 126 | if versions.Contains(migrationFile.Version) && migrationFile.DownFile != nil { 127 | files = append(files, *migrationFile.DownFile) 128 | } 129 | } 130 | return files, nil 131 | } 132 | 133 | // Relative travels relatively through migration files. 134 | // 135 | // +1 will fetch the next up migration file 136 | // +2 will fetch the next two up migration files 137 | // +n will fetch ... 138 | // -1 will fetch the the previous down migration file 139 | // -2 will fetch the next two previous down migration files 140 | // -n will fetch ... 141 | func (mf *MigrationFiles) Relative(relativeN int, versions Versions) (Files, error) { 142 | var d direction.Direction 143 | if relativeN > 0 { 144 | d = direction.Up 145 | } else if relativeN < 0 { 146 | d = direction.Down 147 | } else { // relativeN == 0 148 | return nil, nil 149 | } 150 | 151 | var files Files 152 | var err error 153 | if d == direction.Up { 154 | files, err = mf.Pending(versions) 155 | } else { 156 | relativeN = -relativeN 157 | files, err = mf.Applied(versions) 158 | } 159 | if relativeN > len(files) { 160 | relativeN = len(files) 161 | } 162 | return files[:relativeN], err 163 | } 164 | 165 | // ReadMigrationFiles reads all migration files from a given path. 166 | func ReadMigrationFiles(path string, filenameRegex *regexp.Regexp) (files MigrationFiles, err error) { 167 | // find all migration files in path. 168 | ioFiles, err := ioutil.ReadDir(path) 169 | if err != nil { 170 | return nil, err 171 | } 172 | type tmpFile struct { 173 | version Version 174 | name string 175 | filename string 176 | d direction.Direction 177 | } 178 | tmpFiles := make([]*tmpFile, 0) 179 | tmpFileMap := map[Version]map[direction.Direction]tmpFile{} 180 | for _, file := range ioFiles { 181 | 182 | version, name, d, err := parseFilenameSchema(file.Name(), filenameRegex) 183 | if err == nil { 184 | if _, ok := tmpFileMap[version]; !ok { 185 | tmpFileMap[version] = map[direction.Direction]tmpFile{} 186 | } 187 | if existing, ok := tmpFileMap[version][d]; !ok { 188 | tmpFileMap[version][d] = tmpFile{version: version, name: name, filename: file.Name(), d: d} 189 | } else { 190 | return nil, fmt.Errorf("duplicate migration file version %d : %q and %q", version, existing.filename, file.Name()) 191 | } 192 | tmpFiles = append(tmpFiles, &tmpFile{version, name, file.Name(), d}) 193 | } 194 | } 195 | 196 | // put tmpFiles into MigrationFile struct. 197 | parsedVersions := make(map[Version]bool) 198 | newFiles := make(MigrationFiles, 0) 199 | for _, file := range tmpFiles { 200 | if _, ok := parsedVersions[file.version]; !ok { 201 | migrationFile := MigrationFile{ 202 | Version: file.version, 203 | } 204 | 205 | var lookFordirection direction.Direction 206 | switch file.d { 207 | case direction.Up: 208 | migrationFile.UpFile = &File{ 209 | Path: path, 210 | FileName: file.filename, 211 | Version: file.version, 212 | Name: file.name, 213 | Content: nil, 214 | Direction: direction.Up, 215 | } 216 | lookFordirection = direction.Down 217 | case direction.Down: 218 | migrationFile.DownFile = &File{ 219 | Path: path, 220 | FileName: file.filename, 221 | Version: file.version, 222 | Name: file.name, 223 | Content: nil, 224 | Direction: direction.Down, 225 | } 226 | lookFordirection = direction.Up 227 | default: 228 | return nil, errors.New("Unsupported direction.Direction Type") 229 | } 230 | 231 | for _, file2 := range tmpFiles { 232 | if file2.version == file.version && file2.d == lookFordirection { 233 | switch lookFordirection { 234 | case direction.Up: 235 | migrationFile.UpFile = &File{ 236 | Path: path, 237 | FileName: file2.filename, 238 | Version: file.version, 239 | Name: file2.name, 240 | Content: nil, 241 | Direction: direction.Up, 242 | } 243 | case direction.Down: 244 | migrationFile.DownFile = &File{ 245 | Path: path, 246 | FileName: file2.filename, 247 | Version: file.version, 248 | Name: file2.name, 249 | Content: nil, 250 | Direction: direction.Down, 251 | } 252 | } 253 | break 254 | } 255 | } 256 | 257 | newFiles = append(newFiles, migrationFile) 258 | parsedVersions[file.version] = true 259 | } 260 | } 261 | 262 | sort.Sort(newFiles) 263 | return newFiles, nil 264 | } 265 | 266 | // parseFilenameSchema parses the filename. 267 | func parseFilenameSchema(filename string, filenameRegex *regexp.Regexp) (version Version, name string, d direction.Direction, err error) { 268 | matches := filenameRegex.FindStringSubmatch(filename) 269 | if len(matches) != 4 { 270 | return 0, "", 0, errors.New("Unable to parse filename schema") 271 | } 272 | 273 | v, err := strconv.ParseUint(matches[1], 10, 0) 274 | version = Version(v) 275 | if err != nil { 276 | return 0, "", 0, errors.New(fmt.Sprintf("Unable to parse version '%v' in filename schema", matches[0])) 277 | } 278 | 279 | if matches[3] == "up" { 280 | d = direction.Up 281 | } else if matches[3] == "down" { 282 | d = direction.Down 283 | } else { 284 | return 0, "", 0, errors.New(fmt.Sprintf("Unable to parse up|down '%v' in filename schema", matches[3])) 285 | } 286 | 287 | return version, matches[2], d, nil 288 | } 289 | 290 | // Len is the number of elements in the collection. Required by Sort Interface{}. 291 | func (mf MigrationFiles) Len() int { 292 | return len(mf) 293 | } 294 | 295 | // Less reports whether the element with index i should sort before the element 296 | // with index j. Required by Sort Interface{}. 297 | func (mf MigrationFiles) Less(i, j int) bool { 298 | return mf[i].Version < mf[j].Version 299 | } 300 | 301 | // Swap swaps the elements with indexes i and j. Required by Sort Interface{}. 302 | func (mf MigrationFiles) Swap(i, j int) { 303 | mf[i], mf[j] = mf[j], mf[i] 304 | } 305 | 306 | // LineColumnFromOffset reads data and returns line and column integer for a given offset. 307 | func LineColumnFromOffset(data []byte, offset int) (line, column int) { 308 | // TODO is there a better way? 309 | fs := token.NewFileSet() 310 | tf := fs.AddFile("", fs.Base(), len(data)) 311 | tf.SetLinesForContent(data) 312 | pos := tf.Position(tf.Pos(offset)) 313 | return pos.Line, pos.Column 314 | } 315 | 316 | // LinesBeforeAndAfter reads n lines before and after a given line. 317 | // Set lineNumbers to true, to prepend line numbers. 318 | func LinesBeforeAndAfter(data []byte, line, before, after int, lineNumbers bool) []byte { 319 | // TODO(gemnasium): Trim empty lines at the beginning and at the end 320 | // TODO(gemnasium): Trim offset whitespace at the beginning of each line, so that indentation is preserved 321 | startLine := line - before 322 | endLine := line + after 323 | lines := bytes.SplitN(data, []byte("\n"), endLine+1) 324 | 325 | if startLine < 0 { 326 | startLine = 0 327 | } 328 | if endLine > len(lines) { 329 | endLine = len(lines) 330 | } 331 | 332 | selectLines := lines[startLine:endLine] 333 | newLines := make([][]byte, 0) 334 | lineCounter := startLine + 1 335 | lineNumberDigits := len(strconv.Itoa(len(selectLines))) 336 | for _, l := range selectLines { 337 | lineCounterStr := strconv.Itoa(lineCounter) 338 | if len(lineCounterStr)%lineNumberDigits != 0 { 339 | lineCounterStr = strings.Repeat(" ", lineNumberDigits-len(lineCounterStr)%lineNumberDigits) + lineCounterStr 340 | } 341 | 342 | lNew := l 343 | if lineNumbers { 344 | lNew = append([]byte(lineCounterStr+": "), lNew...) 345 | } 346 | newLines = append(newLines, lNew) 347 | lineCounter += 1 348 | } 349 | 350 | return bytes.Join(newLines, []byte("\n")) 351 | } 352 | -------------------------------------------------------------------------------- /file/file_test.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path" 7 | "testing" 8 | 9 | "github.com/gemnasium/migrate/migrate/direction" 10 | ) 11 | 12 | func TestParseFilenameSchema(t *testing.T) { 13 | var tests = []struct { 14 | filename string 15 | filenameExtension string 16 | expectVersion Version 17 | expectName string 18 | expectDirection direction.Direction 19 | expectErr bool 20 | }{ 21 | {"001_test_file.up.sql", "sql", 1, "test_file", direction.Up, false}, 22 | {"001_test_file.down.sql", "sql", 1, "test_file", direction.Down, false}, 23 | {"10034_test_file.down.sql", "sql", 10034, "test_file", direction.Down, false}, 24 | {"-1_test_file.down.sql", "sql", 0, "", direction.Up, true}, 25 | {"test_file.down.sql", "sql", 0, "", direction.Up, true}, 26 | {"100_test_file.down", "sql", 0, "", direction.Up, true}, 27 | {"100_test_file.sql", "sql", 0, "", direction.Up, true}, 28 | {"100_test_file", "sql", 0, "", direction.Up, true}, 29 | {"test_file", "sql", 0, "", direction.Up, true}, 30 | {"100", "sql", 0, "", direction.Up, true}, 31 | {".sql", "sql", 0, "", direction.Up, true}, 32 | {"up.sql", "sql", 0, "", direction.Up, true}, 33 | {"down.sql", "sql", 0, "", direction.Up, true}, 34 | } 35 | 36 | for _, test := range tests { 37 | version, name, migrate, err := parseFilenameSchema(test.filename, FilenameRegex(test.filenameExtension)) 38 | if test.expectErr && err == nil { 39 | t.Fatal("Expected error, but got none.", test) 40 | } 41 | if !test.expectErr && err != nil { 42 | t.Fatal("Did not expect error, but got one:", err, test) 43 | } 44 | if err == nil { 45 | if version != test.expectVersion { 46 | t.Error("Wrong version number", test) 47 | } 48 | if name != test.expectName { 49 | t.Error("wrong name", test) 50 | } 51 | if migrate != test.expectDirection { 52 | t.Error("wrong migrate", test) 53 | } 54 | } 55 | } 56 | } 57 | 58 | func TestFiles(t *testing.T) { 59 | tmpdir, err := ioutil.TempDir("/tmp", "TestLookForMigrationFilesInSearchPath") 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | defer os.RemoveAll(tmpdir) 64 | 65 | if err := ioutil.WriteFile(path.Join(tmpdir, "nonsense.txt"), nil, 0755); err != nil { 66 | t.Fatal("Unable to write files in tmpdir", err) 67 | } 68 | ioutil.WriteFile(path.Join(tmpdir, "002_migrationfile.up.sql"), nil, 0755) 69 | ioutil.WriteFile(path.Join(tmpdir, "002_migrationfile.down.sql"), nil, 0755) 70 | 71 | ioutil.WriteFile(path.Join(tmpdir, "001_migrationfile.up.sql"), nil, 0755) 72 | ioutil.WriteFile(path.Join(tmpdir, "001_migrationfile.down.sql"), nil, 0755) 73 | 74 | ioutil.WriteFile(path.Join(tmpdir, "101_create_table.up.sql"), nil, 0755) 75 | ioutil.WriteFile(path.Join(tmpdir, "101_drop_tables.down.sql"), nil, 0755) 76 | 77 | ioutil.WriteFile(path.Join(tmpdir, "301_migrationfile.up.sql"), nil, 0755) 78 | 79 | ioutil.WriteFile(path.Join(tmpdir, "401_migrationfile.down.sql"), []byte("test"), 0755) 80 | 81 | files, err := ReadMigrationFiles(tmpdir, FilenameRegex("sql")) 82 | if err != nil { 83 | t.Fatal(err) 84 | } 85 | 86 | if len(files) == 0 { 87 | t.Fatal("No files returned.") 88 | } 89 | 90 | if len(files) != 5 { 91 | t.Fatal("Wrong number of files returned.") 92 | } 93 | 94 | // test sort order 95 | if files[0].Version != 1 || files[1].Version != 2 || files[2].Version != 101 || files[3].Version != 301 || files[4].Version != 401 { 96 | t.Error("Sort order is incorrect") 97 | t.Error(files) 98 | } 99 | 100 | // test UpFile and DownFile 101 | if files[0].UpFile == nil { 102 | t.Fatalf("Missing up file for version %v", files[0].Version) 103 | } 104 | if files[0].DownFile == nil { 105 | t.Fatalf("Missing down file for version %v", files[0].Version) 106 | } 107 | 108 | if files[1].UpFile == nil { 109 | t.Fatalf("Missing up file for version %v", files[1].Version) 110 | } 111 | if files[1].DownFile == nil { 112 | t.Fatalf("Missing down file for version %v", files[1].Version) 113 | } 114 | 115 | if files[2].UpFile == nil { 116 | t.Fatalf("Missing up file for version %v", files[2].Version) 117 | } 118 | if files[2].DownFile == nil { 119 | t.Fatalf("Missing down file for version %v", files[2].Version) 120 | } 121 | 122 | if files[3].UpFile == nil { 123 | t.Fatalf("Missing up file for version %v", files[3].Version) 124 | } 125 | if files[3].DownFile != nil { 126 | t.Fatalf("There should not be a down file for version %v", files[3].Version) 127 | } 128 | 129 | if files[4].UpFile != nil { 130 | t.Fatalf("There should not be a up file for version %v", files[4].Version) 131 | } 132 | if files[4].DownFile == nil { 133 | t.Fatalf("Missing down file for version %v", files[4].Version) 134 | } 135 | 136 | // test read 137 | if err := files[4].DownFile.ReadContent(); err != nil { 138 | t.Error("Unable to read file", err) 139 | } 140 | if files[4].DownFile.Content == nil { 141 | t.Fatal("Read content is nil") 142 | } 143 | if string(files[4].DownFile.Content) != "test" { 144 | t.Fatal("Read content is wrong") 145 | } 146 | 147 | // test names 148 | if files[0].UpFile.Name != "migrationfile" { 149 | t.Error("file name is not correct", files[0].UpFile.Name) 150 | } 151 | if files[0].UpFile.FileName != "001_migrationfile.up.sql" { 152 | t.Error("file name is not correct", files[0].UpFile.FileName) 153 | } 154 | 155 | // test file.Relative() 156 | // there should be the following versions: 157 | // 1(up&down), 2(up&down), 101(up&down), 301(up), 401(down) 158 | var tests = []struct { 159 | appliedVersions Versions 160 | relative int 161 | expectRange Versions 162 | }{ 163 | {Versions{}, 2, Versions{1, 2}}, 164 | {Versions{1}, 4, Versions{2, 101, 301}}, 165 | {Versions{1}, 0, nil}, 166 | {Versions{}, 1, Versions{1}}, 167 | {Versions{}, 0, nil}, 168 | {Versions{1, 2, 101}, -2, Versions{101, 2}}, 169 | {Versions{1, 2, 101, 301, 401}, -1, Versions{401}}, 170 | } 171 | 172 | for _, test := range tests { 173 | rangeFiles, err := files.Relative(test.relative, test.appliedVersions) 174 | if err != nil { 175 | t.Error("Unable to fetch range:", err) 176 | } 177 | if len(rangeFiles) != len(test.expectRange) { 178 | t.Fatalf("file.Relative(): expected %v files, got %v. For test %v.", len(test.expectRange), len(rangeFiles), test.expectRange) 179 | } 180 | 181 | for i, version := range test.expectRange { 182 | if rangeFiles[i].Version != version { 183 | t.Logf("rangeFiles: %v\n", rangeFiles) 184 | t.Fatal("file.Relative(): returned files dont match expectations", test.expectRange) 185 | } 186 | } 187 | } 188 | } 189 | 190 | func TestDuplicateFiles(t *testing.T) { 191 | dups := []string{ 192 | "001_migration.up.sql", 193 | "001_duplicate.up.sql", 194 | } 195 | 196 | root, cleanFn, err := makeFiles("TestDuplicateFiles", dups...) 197 | defer cleanFn() 198 | 199 | if err != nil { 200 | t.Fatal(err) 201 | } 202 | 203 | _, err = ReadMigrationFiles(root, FilenameRegex("sql")) 204 | if err == nil { 205 | t.Fatal("Expected duplicate migration file error") 206 | } 207 | } 208 | 209 | // makeFiles takes an identifier, and a list of file names and uses them to create a temporary 210 | // directory populated with files named with the names passed in. makeFiles returns the root 211 | // directory name, and a func suitable for a defer cleanup to remove the temporary files after 212 | // the calling function exits. 213 | func makeFiles(testname string, names ...string) (root string, cleanup func(), err error) { 214 | cleanup = func() {} 215 | root, err = ioutil.TempDir("/tmp", testname) 216 | if err != nil { 217 | return 218 | } 219 | cleanup = func() { os.RemoveAll(root) } 220 | if err = ioutil.WriteFile(path.Join(root, "nonsense.txt"), nil, 0755); err != nil { 221 | return 222 | } 223 | 224 | for _, name := range names { 225 | if err = ioutil.WriteFile(path.Join(root, name), nil, 0755); err != nil { 226 | return 227 | } 228 | } 229 | return 230 | } 231 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Package main is the CLI. 2 | // You can use the CLI via Terminal. 3 | // import "github.com/gemnasium/migrate/migrate" for usage within Go. 4 | package main 5 | 6 | import ( 7 | "flag" 8 | "fmt" 9 | "os" 10 | "strconv" 11 | "time" 12 | 13 | "github.com/fatih/color" 14 | _ "github.com/gemnasium/migrate/driver/bash" 15 | _ "github.com/gemnasium/migrate/driver/cassandra" 16 | _ "github.com/gemnasium/migrate/driver/crate" 17 | _ "github.com/gemnasium/migrate/driver/mysql" 18 | _ "github.com/gemnasium/migrate/driver/postgres" 19 | _ "github.com/gemnasium/migrate/driver/sqlite3" 20 | "github.com/gemnasium/migrate/file" 21 | "github.com/gemnasium/migrate/migrate" 22 | "github.com/gemnasium/migrate/migrate/direction" 23 | pipep "github.com/gemnasium/migrate/pipe" 24 | ) 25 | 26 | var url = flag.String("url", os.Getenv("MIGRATE_URL"), "") 27 | var migrationsPath = flag.String("path", "", "") 28 | var version = flag.Bool("version", false, "Show migrate version") 29 | 30 | func main() { 31 | flag.Usage = func() { 32 | helpCmd() 33 | } 34 | 35 | flag.Parse() 36 | command := flag.Arg(0) 37 | if *version { 38 | fmt.Println(Version) 39 | os.Exit(0) 40 | } 41 | 42 | if *migrationsPath == "" { 43 | *migrationsPath, _ = os.Getwd() 44 | } 45 | 46 | switch command { 47 | case "create": 48 | verifyMigrationsPath(*migrationsPath) 49 | name := flag.Arg(1) 50 | if name == "" { 51 | fmt.Println("Please specify name.") 52 | os.Exit(1) 53 | } 54 | 55 | migrationFile, err := migrate.Create(*url, *migrationsPath, name) 56 | if err != nil { 57 | fmt.Println(err) 58 | os.Exit(1) 59 | } 60 | 61 | fmt.Printf("Version %v migration files created in %v:\n", migrationFile.Version, *migrationsPath) 62 | fmt.Println(migrationFile.UpFile.FileName) 63 | fmt.Println(migrationFile.DownFile.FileName) 64 | 65 | case "migrate": 66 | verifyMigrationsPath(*migrationsPath) 67 | relativeN := flag.Arg(1) 68 | relativeNInt, err := strconv.Atoi(relativeN) 69 | if err != nil { 70 | fmt.Println("Unable to parse param .") 71 | os.Exit(1) 72 | } 73 | timerStart = time.Now() 74 | pipe := pipep.New() 75 | go migrate.Migrate(pipe, *url, *migrationsPath, relativeNInt) 76 | ok := writePipe(pipe) 77 | printTimer() 78 | if !ok { 79 | os.Exit(1) 80 | } 81 | 82 | case "goto": 83 | verifyMigrationsPath(*migrationsPath) 84 | toVersion := flag.Arg(1) 85 | toVersionInt, err := strconv.Atoi(toVersion) 86 | if err != nil || toVersionInt < 0 { 87 | fmt.Println("Unable to parse param .") 88 | os.Exit(1) 89 | } 90 | 91 | currentVersion, err := migrate.Version(*url, *migrationsPath) 92 | if err != nil { 93 | fmt.Println(err) 94 | os.Exit(1) 95 | } 96 | 97 | relativeNInt := toVersionInt - int(currentVersion) 98 | 99 | timerStart = time.Now() 100 | pipe := pipep.New() 101 | go migrate.Migrate(pipe, *url, *migrationsPath, relativeNInt) 102 | ok := writePipe(pipe) 103 | printTimer() 104 | if !ok { 105 | os.Exit(1) 106 | } 107 | 108 | case "up": 109 | verifyMigrationsPath(*migrationsPath) 110 | timerStart = time.Now() 111 | pipe := pipep.New() 112 | go migrate.Up(pipe, *url, *migrationsPath) 113 | ok := writePipe(pipe) 114 | printTimer() 115 | if !ok { 116 | os.Exit(1) 117 | } 118 | 119 | case "down": 120 | verifyMigrationsPath(*migrationsPath) 121 | timerStart = time.Now() 122 | pipe := pipep.New() 123 | go migrate.Down(pipe, *url, *migrationsPath) 124 | ok := writePipe(pipe) 125 | printTimer() 126 | if !ok { 127 | os.Exit(1) 128 | } 129 | 130 | case "redo": 131 | verifyMigrationsPath(*migrationsPath) 132 | timerStart = time.Now() 133 | pipe := pipep.New() 134 | go migrate.Redo(pipe, *url, *migrationsPath) 135 | ok := writePipe(pipe) 136 | printTimer() 137 | if !ok { 138 | os.Exit(1) 139 | } 140 | 141 | case "reset": 142 | verifyMigrationsPath(*migrationsPath) 143 | timerStart = time.Now() 144 | pipe := pipep.New() 145 | go migrate.Reset(pipe, *url, *migrationsPath) 146 | ok := writePipe(pipe) 147 | printTimer() 148 | if !ok { 149 | os.Exit(1) 150 | } 151 | 152 | case "version": 153 | verifyMigrationsPath(*migrationsPath) 154 | version, err := migrate.Version(*url, *migrationsPath) 155 | if err != nil { 156 | fmt.Println(err) 157 | os.Exit(1) 158 | } 159 | fmt.Println(version) 160 | 161 | default: 162 | helpCmd() 163 | os.Exit(1) 164 | case "help": 165 | helpCmd() 166 | } 167 | } 168 | 169 | func writePipe(pipe chan interface{}) (ok bool) { 170 | okFlag := true 171 | if pipe != nil { 172 | for { 173 | select { 174 | case item, more := <-pipe: 175 | if !more { 176 | return okFlag 177 | } else { 178 | switch item.(type) { 179 | 180 | case string: 181 | fmt.Println(item.(string)) 182 | 183 | case error: 184 | c := color.New(color.FgRed) 185 | c.Println(item.(error).Error(), "\n") 186 | okFlag = false 187 | 188 | case file.File: 189 | f := item.(file.File) 190 | c := color.New(color.FgBlue) 191 | if f.Direction == direction.Up { 192 | c.Print(">") 193 | } else if f.Direction == direction.Down { 194 | c.Print("<") 195 | } 196 | fmt.Printf(" %s\n", f.FileName) 197 | 198 | default: 199 | text := fmt.Sprint(item) 200 | fmt.Println(text) 201 | } 202 | } 203 | } 204 | } 205 | } 206 | return okFlag 207 | } 208 | 209 | func verifyMigrationsPath(path string) { 210 | if path == "" { 211 | fmt.Println("Please specify path") 212 | os.Exit(1) 213 | } 214 | } 215 | 216 | var timerStart time.Time 217 | 218 | func printTimer() { 219 | diff := time.Now().Sub(timerStart).Seconds() 220 | if diff > 60 { 221 | fmt.Printf("\n%.4f minutes\n", diff/60) 222 | } else { 223 | fmt.Printf("\n%.4f seconds\n", diff) 224 | } 225 | } 226 | 227 | func helpCmd() { 228 | os.Stderr.WriteString( 229 | `usage: migrate [-path=] -url= [] 230 | 231 | Commands: 232 | create Create a new migration 233 | up Apply all -up- migrations 234 | down Apply all -down- migrations 235 | reset Down followed by Up 236 | redo Roll back most recent migration, then apply it again 237 | version Show current migration version 238 | migrate Apply migrations -n|+n 239 | goto Migrate to version v 240 | help Show this help 241 | 242 | '-path' defaults to current working directory. 243 | `) 244 | } 245 | -------------------------------------------------------------------------------- /migrate/direction/direction.go: -------------------------------------------------------------------------------- 1 | // Package direction just holds convenience constants for Up and Down migrations. 2 | package direction 3 | 4 | type Direction int 5 | 6 | const ( 7 | Up Direction = +1 8 | Down = -1 9 | ) 10 | -------------------------------------------------------------------------------- /migrate/migrate.go: -------------------------------------------------------------------------------- 1 | // Package migrate is imported by other Go code. 2 | // It is the entry point to all migration functions. 3 | package migrate 4 | 5 | import ( 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "os/signal" 10 | "path" 11 | "strconv" 12 | "strings" 13 | "time" 14 | 15 | "github.com/gemnasium/migrate/driver" 16 | "github.com/gemnasium/migrate/file" 17 | "github.com/gemnasium/migrate/migrate/direction" 18 | pipep "github.com/gemnasium/migrate/pipe" 19 | ) 20 | 21 | // Up applies all available migrations. 22 | func Up(pipe chan interface{}, url, migrationsPath string) { 23 | d, files, versions, err := initDriverAndReadMigrationFilesAndGetVersions(url, migrationsPath) 24 | defer func() { 25 | if err != nil { 26 | pipe <- err 27 | 28 | } 29 | if d != nil { 30 | if err = d.Close(); err != nil { 31 | pipe <- err 32 | } 33 | } 34 | go pipep.Close(pipe, nil) 35 | }() 36 | if err != nil { 37 | return 38 | } 39 | 40 | applyMigrationFiles, err := files.Pending(versions) 41 | if err != nil { 42 | return 43 | } 44 | 45 | if len(applyMigrationFiles) > 0 { 46 | for _, f := range applyMigrationFiles { 47 | pipe1 := pipep.New() 48 | go d.Migrate(f, pipe1) 49 | if ok := pipep.WaitAndRedirect(pipe1, pipe, handleInterrupts()); !ok { 50 | break 51 | } 52 | } 53 | } 54 | } 55 | 56 | // UpSync is synchronous version of Up(). 57 | func UpSync(url, migrationsPath string) (err []error, ok bool) { 58 | pipe := pipep.New() 59 | go Up(pipe, url, migrationsPath) 60 | err = pipep.ReadErrors(pipe) 61 | return err, len(err) == 0 62 | } 63 | 64 | // Down rolls back all migrations. 65 | func Down(pipe chan interface{}, url, migrationsPath string) { 66 | d, files, versions, err := initDriverAndReadMigrationFilesAndGetVersions(url, migrationsPath) 67 | defer func() { 68 | if err != nil { 69 | pipe <- err 70 | 71 | } 72 | if d != nil { 73 | if err = d.Close(); err != nil { 74 | pipe <- err 75 | } 76 | } 77 | go pipep.Close(pipe, nil) 78 | }() 79 | if err != nil { 80 | return 81 | } 82 | 83 | applyMigrationFiles, err := files.Applied(versions) 84 | if err != nil { 85 | return 86 | } 87 | 88 | if len(applyMigrationFiles) > 0 { 89 | for _, f := range applyMigrationFiles { 90 | pipe1 := pipep.New() 91 | go d.Migrate(f, pipe1) 92 | if ok := pipep.WaitAndRedirect(pipe1, pipe, handleInterrupts()); !ok { 93 | break 94 | } 95 | } 96 | } 97 | } 98 | 99 | // DownSync is synchronous version of Down(). 100 | func DownSync(url, migrationsPath string) (err []error, ok bool) { 101 | pipe := pipep.New() 102 | go Down(pipe, url, migrationsPath) 103 | err = pipep.ReadErrors(pipe) 104 | return err, len(err) == 0 105 | } 106 | 107 | // Redo rolls back the most recently applied migration, then runs it again. 108 | func Redo(pipe chan interface{}, url, migrationsPath string) { 109 | pipe1 := pipep.New() 110 | go Migrate(pipe1, url, migrationsPath, -1) 111 | if ok := pipep.WaitAndRedirect(pipe1, pipe, handleInterrupts()); !ok { 112 | go pipep.Close(pipe, nil) 113 | return 114 | } 115 | go Migrate(pipe, url, migrationsPath, +1) 116 | } 117 | 118 | // RedoSync is synchronous version of Redo(). 119 | func RedoSync(url, migrationsPath string) (err []error, ok bool) { 120 | pipe := pipep.New() 121 | go Redo(pipe, url, migrationsPath) 122 | err = pipep.ReadErrors(pipe) 123 | return err, len(err) == 0 124 | } 125 | 126 | // Reset runs the down and up migration function. 127 | func Reset(pipe chan interface{}, url, migrationsPath string) { 128 | pipe1 := pipep.New() 129 | go Down(pipe1, url, migrationsPath) 130 | if ok := pipep.WaitAndRedirect(pipe1, pipe, handleInterrupts()); !ok { 131 | go pipep.Close(pipe, nil) 132 | return 133 | } 134 | go Up(pipe, url, migrationsPath) 135 | } 136 | 137 | // ResetSync is synchronous version of Reset(). 138 | func ResetSync(url, migrationsPath string) (err []error, ok bool) { 139 | pipe := pipep.New() 140 | go Reset(pipe, url, migrationsPath) 141 | err = pipep.ReadErrors(pipe) 142 | return err, len(err) == 0 143 | } 144 | 145 | // Migrate applies relative +n/-n migrations. 146 | func Migrate(pipe chan interface{}, url, migrationsPath string, relativeN int) { 147 | d, files, versions, err := initDriverAndReadMigrationFilesAndGetVersions(url, migrationsPath) 148 | defer func() { 149 | if err != nil { 150 | pipe <- err 151 | } 152 | if d != nil { 153 | if err = d.Close(); err != nil { 154 | pipe <- err 155 | } 156 | } 157 | go pipep.Close(pipe, nil) 158 | }() 159 | if err != nil { 160 | return 161 | } 162 | 163 | applyMigrationFiles, err := files.Relative(relativeN, versions) 164 | if err != nil { 165 | return 166 | } 167 | 168 | if len(applyMigrationFiles) > 0 && relativeN != 0 { 169 | for _, f := range applyMigrationFiles { 170 | pipe1 := pipep.New() 171 | go d.Migrate(f, pipe1) 172 | if ok := pipep.WaitAndRedirect(pipe1, pipe, handleInterrupts()); !ok { 173 | break 174 | } 175 | } 176 | } 177 | } 178 | 179 | // MigrateSync is synchronous version of Migrate(). 180 | func MigrateSync(url, migrationsPath string, relativeN int) (err []error, ok bool) { 181 | pipe := pipep.New() 182 | go Migrate(pipe, url, migrationsPath, relativeN) 183 | err = pipep.ReadErrors(pipe) 184 | return err, len(err) == 0 185 | } 186 | 187 | // Version returns the current migration version. 188 | func Version(url, migrationsPath string) (version file.Version, err error) { 189 | d, err := driver.New(url) 190 | if err != nil { 191 | return 0, err 192 | } 193 | return d.Version() 194 | } 195 | 196 | // Versions returns applied versions. 197 | func Versions(url, migrationsPath string) (versions file.Versions, err error) { 198 | d, err := driver.New(url) 199 | if err != nil { 200 | return file.Versions{}, err 201 | } 202 | return d.Versions() 203 | } 204 | 205 | // Create creates new migration files on disk. 206 | func Create(url, migrationsPath, name string) (*file.MigrationFile, error) { 207 | d, files, _, err := initDriverAndReadMigrationFilesAndGetVersions(url, migrationsPath) 208 | if err != nil { 209 | return nil, err 210 | } 211 | 212 | versionStr := time.Now().UTC().Format("20060102150405") 213 | v, _ := strconv.ParseUint(versionStr, 10, 64) 214 | version := file.Version(v) 215 | 216 | filenamef := "%d_%s.%s.%s" 217 | name = strings.Replace(name, " ", "_", -1) 218 | 219 | // if latest version has the same timestamp, increment version 220 | if len(files) > 0 { 221 | latest := files[len(files)-1].Version 222 | if latest >= version { 223 | version = latest + 1 224 | } 225 | } 226 | 227 | mfile := &file.MigrationFile{ 228 | Version: version, 229 | UpFile: &file.File{ 230 | Path: migrationsPath, 231 | FileName: fmt.Sprintf(filenamef, version, name, "up", d.FilenameExtension()), 232 | Name: name, 233 | Content: []byte(""), 234 | Direction: direction.Up, 235 | }, 236 | DownFile: &file.File{ 237 | Path: migrationsPath, 238 | FileName: fmt.Sprintf(filenamef, version, name, "down", d.FilenameExtension()), 239 | Name: name, 240 | Content: []byte(""), 241 | Direction: direction.Down, 242 | }, 243 | } 244 | 245 | if err := ioutil.WriteFile(path.Join(mfile.UpFile.Path, mfile.UpFile.FileName), mfile.UpFile.Content, 0644); err != nil { 246 | return nil, err 247 | } 248 | if err := ioutil.WriteFile(path.Join(mfile.DownFile.Path, mfile.DownFile.FileName), mfile.DownFile.Content, 0644); err != nil { 249 | return nil, err 250 | } 251 | 252 | return mfile, nil 253 | } 254 | 255 | // initDriverAndReadMigrationFilesAndGetVersionsAndGetVersion is a small helper 256 | // function that is common to most of the migration funcs. 257 | func initDriverAndReadMigrationFilesAndGetVersions(url, migrationsPath string) (driver.Driver, file.MigrationFiles, file.Versions, error) { 258 | d, err := driver.New(url) 259 | if err != nil { 260 | return nil, nil, file.Versions{}, err 261 | } 262 | files, err := file.ReadMigrationFiles(migrationsPath, file.FilenameRegex(d.FilenameExtension())) 263 | if err != nil { 264 | d.Close() // TODO what happens with errors from this func? 265 | return nil, nil, file.Versions{}, err 266 | } 267 | 268 | versions, err := d.Versions() 269 | if err != nil { 270 | d.Close() // TODO what happens with errors from this func? 271 | return nil, nil, file.Versions{}, err 272 | 273 | } 274 | 275 | return d, files, versions, nil 276 | } 277 | 278 | // NewPipe is a convenience function for pipe.New(). 279 | // This is helpful if the user just wants to import this package and nothing else. 280 | func NewPipe() chan interface{} { 281 | return pipep.New() 282 | } 283 | 284 | // interrupts is an internal variable that holds the state of 285 | // interrupt handling. 286 | var interrupts = true 287 | 288 | // Graceful enables interrupts checking. Once the first ^C is received 289 | // it will finish the currently running migration and abort execution 290 | // of the next migration. If ^C is received twice, it will stop 291 | // execution immediately. 292 | func Graceful() { 293 | interrupts = true 294 | } 295 | 296 | // NonGraceful disables interrupts checking. The first received ^C will 297 | // stop execution immediately. 298 | func NonGraceful() { 299 | interrupts = false 300 | } 301 | 302 | // interrupts returns a signal channel if interrupts checking is 303 | // enabled. nil otherwise. 304 | func handleInterrupts() chan os.Signal { 305 | if interrupts { 306 | c := make(chan os.Signal, 1) 307 | signal.Notify(c, os.Interrupt) 308 | return c 309 | } 310 | return nil 311 | } 312 | -------------------------------------------------------------------------------- /migrate/migrate_test.go: -------------------------------------------------------------------------------- 1 | package migrate 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path" 8 | "reflect" 9 | "testing" 10 | // Ensure imports for each driver we wish to test 11 | 12 | "github.com/gemnasium/migrate/driver" 13 | _ "github.com/gemnasium/migrate/driver/cassandra" 14 | _ "github.com/gemnasium/migrate/driver/mysql" 15 | _ "github.com/gemnasium/migrate/driver/postgres" 16 | _ "github.com/gemnasium/migrate/driver/sqlite3" 17 | "github.com/gemnasium/migrate/file" 18 | "github.com/gemnasium/migrate/migrate/direction" 19 | ) 20 | 21 | // Add Driver URLs here to test basic Up, Down, .. functions. 22 | var driverUrls = []string{ 23 | "postgres://postgres@" + os.Getenv("POSTGRES_PORT_5432_TCP_ADDR") + ":" + os.Getenv("POSTGRES_PORT_5432_TCP_PORT") + "/template1?sslmode=disable", 24 | "mysql://root@tcp(" + os.Getenv("MYSQL_PORT_3306_TCP_ADDR") + ":" + os.Getenv("MYSQL_PORT_3306_TCP_PORT") + ")/migratetest", 25 | "cassandra://" + os.Getenv("CASSANDRA_PORT_9042_TCP_ADDR") + ":" + os.Getenv("CASSANDRA_PORT_9042_TCP_PORT") + "/migrate?protocol=4", 26 | "sqlite3:///tmp/migrate.db", 27 | } 28 | 29 | func TestCreate(t *testing.T) { 30 | for _, driverUrl := range driverUrls { 31 | t.Logf("Test driver: %s", driverUrl) 32 | tmpdir, err := ioutil.TempDir("/tmp", "migrate-test") 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | file1, err := Create(driverUrl, tmpdir, "test_migration") 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | file2, err := Create(driverUrl, tmpdir, "another migration") 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | 46 | files, err := ioutil.ReadDir(tmpdir) 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | if len(files) != 4 { 51 | t.Fatal("Expected 2 new files, got", len(files)) 52 | } 53 | expectFiles := []string{ 54 | file1.UpFile.FileName, file1.DownFile.FileName, 55 | file2.UpFile.FileName, file2.DownFile.FileName, 56 | } 57 | for _, expectFile := range expectFiles { 58 | filepath := path.Join(tmpdir, expectFile) 59 | if _, err := os.Stat(filepath); os.IsNotExist(err) { 60 | t.Errorf("Can't find migration file: %s", filepath) 61 | } 62 | } 63 | 64 | if file1.Version == file2.Version { 65 | t.Errorf("files can't same version: %d", file1.Version) 66 | } 67 | } 68 | } 69 | 70 | func TestReset(t *testing.T) { 71 | for _, driverUrl := range driverUrls { 72 | t.Logf("Test driver: %s", driverUrl) 73 | tmpdir, err := ioutil.TempDir("/", "migrate-test") 74 | if err != nil { 75 | t.Fatal(err) 76 | } 77 | 78 | _, err = Create(driverUrl, tmpdir, "migration1") 79 | if err != nil { 80 | t.Fatal(err) 81 | } 82 | file, err := Create(driverUrl, tmpdir, "migration2") 83 | if err != nil { 84 | t.Fatal(err) 85 | } 86 | 87 | errs, ok := ResetSync(driverUrl, tmpdir) 88 | if !ok { 89 | t.Fatal(errs) 90 | } 91 | version, err := Version(driverUrl, tmpdir) 92 | if err != nil { 93 | t.Fatal(err) 94 | } 95 | if version != file.Version { 96 | versions, _ := Versions(driverUrl, tmpdir) 97 | t.Logf("Versions in db: %v", versions) 98 | t.Fatalf("Expected version %d, got %v", file.Version, version) 99 | } 100 | 101 | errs, ok = DownSync(driverUrl, tmpdir) 102 | if !ok { 103 | t.Fatal(errs) 104 | } 105 | } 106 | } 107 | 108 | func TestDown(t *testing.T) { 109 | for _, driverUrl := range driverUrls { 110 | t.Logf("Test driver: %s", driverUrl) 111 | tmpdir, err := ioutil.TempDir("/tmp", "migrate-test") 112 | if err != nil { 113 | t.Fatal(err) 114 | } 115 | 116 | Create(driverUrl, tmpdir, "migration1") 117 | file, _ := Create(driverUrl, tmpdir, "migration2") 118 | 119 | errs, ok := ResetSync(driverUrl, tmpdir) 120 | if !ok { 121 | t.Fatal(errs) 122 | } 123 | version, err := Version(driverUrl, tmpdir) 124 | if err != nil { 125 | t.Fatal(err) 126 | } 127 | if version != file.Version { 128 | t.Fatalf("Expected version %d, got %v", file.Version, version) 129 | } 130 | 131 | errs, ok = DownSync(driverUrl, tmpdir) 132 | if !ok { 133 | t.Fatal(errs) 134 | } 135 | version, err = Version(driverUrl, tmpdir) 136 | if err != nil { 137 | t.Fatal(err) 138 | } 139 | if version != 0 { 140 | t.Fatalf("Expected version 0, got %v", version) 141 | } 142 | } 143 | } 144 | 145 | func TestUp(t *testing.T) { 146 | for _, driverUrl := range driverUrls { 147 | t.Logf("Test driver: %s", driverUrl) 148 | tmpdir, err := ioutil.TempDir("/tmp", "migrate-test") 149 | if err != nil { 150 | t.Fatal(err) 151 | } 152 | 153 | Create(driverUrl, tmpdir, "migration1") 154 | file, _ := Create(driverUrl, tmpdir, "migration2") 155 | 156 | errs, ok := DownSync(driverUrl, tmpdir) 157 | if !ok { 158 | t.Fatal(errs) 159 | } 160 | version, err := Version(driverUrl, tmpdir) 161 | if err != nil { 162 | t.Fatal(err) 163 | } 164 | if version != 0 { 165 | t.Fatalf("Expected version 0, got %v", version) 166 | } 167 | 168 | errs, ok = UpSync(driverUrl, tmpdir) 169 | if !ok { 170 | t.Fatal(errs) 171 | } 172 | version, err = Version(driverUrl, tmpdir) 173 | if err != nil { 174 | t.Fatal(err) 175 | } 176 | if version != file.Version { 177 | t.Fatalf("Expected version %d, got %v", file.Version, version) 178 | } 179 | 180 | errs, ok = DownSync(driverUrl, tmpdir) 181 | if !ok { 182 | t.Fatal(errs) 183 | } 184 | } 185 | } 186 | 187 | func TestRedo(t *testing.T) { 188 | for _, driverUrl := range driverUrls { 189 | t.Logf("Test driver: %s", driverUrl) 190 | tmpdir, err := ioutil.TempDir("/tmp", "migrate-test") 191 | if err != nil { 192 | t.Fatal(err) 193 | } 194 | 195 | Create(driverUrl, tmpdir, "migration1") 196 | file, _ := Create(driverUrl, tmpdir, "migration2") 197 | 198 | errs, ok := ResetSync(driverUrl, tmpdir) 199 | if !ok { 200 | t.Fatal(errs) 201 | } 202 | version, err := Version(driverUrl, tmpdir) 203 | if err != nil { 204 | t.Fatal(err) 205 | } 206 | if version != file.Version { 207 | t.Fatalf("Expected version %d, got %v", file.Version, version) 208 | } 209 | 210 | errs, ok = RedoSync(driverUrl, tmpdir) 211 | if !ok { 212 | t.Fatal(errs) 213 | } 214 | version, err = Version(driverUrl, tmpdir) 215 | if err != nil { 216 | t.Fatal(err) 217 | } 218 | if version != file.Version { 219 | t.Fatalf("Expected version %d, got %v", file.Version, version) 220 | } 221 | if errs, ok := DownSync(driverUrl, tmpdir); !ok { 222 | t.Fatal(errs) 223 | } 224 | } 225 | } 226 | 227 | func TestMigrate(t *testing.T) { 228 | for _, driverUrl := range driverUrls { 229 | t.Logf("Test driver: %s", driverUrl) 230 | tmpdir, err := ioutil.TempDir("/tmp", "migrate-test") 231 | if err != nil { 232 | t.Fatal(err) 233 | } 234 | 235 | file1, err := Create(driverUrl, tmpdir, "migration1") 236 | if err != nil { 237 | t.Fatal(err) 238 | } 239 | 240 | file2, err := Create(driverUrl, tmpdir, "migration2") 241 | if err != nil { 242 | t.Fatal(err) 243 | } 244 | 245 | errs, ok := ResetSync(driverUrl, tmpdir) 246 | if !ok { 247 | t.Fatal(errs) 248 | } 249 | version, err := Version(driverUrl, tmpdir) 250 | if err != nil { 251 | t.Fatal(err) 252 | } 253 | if version != file2.Version { 254 | t.Fatalf("Expected version %d, got %v", file2.Version, version) 255 | } 256 | 257 | errs, ok = MigrateSync(driverUrl, tmpdir, -2) 258 | if !ok { 259 | t.Fatal(errs) 260 | } 261 | version, err = Version(driverUrl, tmpdir) 262 | if err != nil { 263 | t.Fatal(err) 264 | } 265 | if version != 0 { 266 | versions, _ := Versions(driverUrl, tmpdir) 267 | t.Logf("Versions in db: %v", versions) 268 | t.Fatalf("Expected version 0, got %v", version) 269 | } 270 | 271 | errs, ok = MigrateSync(driverUrl, tmpdir, +1) 272 | if !ok { 273 | t.Fatal(errs) 274 | } 275 | version, err = Version(driverUrl, tmpdir) 276 | if err != nil { 277 | t.Fatal(err) 278 | } 279 | if version != file1.Version { 280 | t.Fatalf("Expected version %d, got %v", file1.Version, version) 281 | } 282 | 283 | err = createOldMigrationFile(driverUrl, tmpdir) 284 | if err != nil { 285 | t.Fatal(err) 286 | } 287 | 288 | errs, ok = UpSync(driverUrl, tmpdir) 289 | if !ok { 290 | t.Fatal(errs) 291 | } 292 | expectedVersions := file.Versions{ 293 | file2.Version, 294 | file1.Version, 295 | 20060102150405, 296 | } 297 | 298 | versions, err := Versions(driverUrl, tmpdir) 299 | if err != nil { 300 | t.Errorf("Could not fetch versions: %s", err) 301 | } 302 | 303 | if !reflect.DeepEqual(versions, expectedVersions) { 304 | t.Errorf("Expected versions to be: %v, got: %v", expectedVersions, versions) 305 | } 306 | 307 | } 308 | } 309 | 310 | func createOldMigrationFile(url, migrationsPath string) error { 311 | version := file.Version(20060102150405) 312 | filenamef := "%d_%s.%s.%s" 313 | name := "old" 314 | d, err := driver.New(url) 315 | if err != nil { 316 | return err 317 | } 318 | 319 | mfile := &file.MigrationFile{ 320 | Version: version, 321 | UpFile: &file.File{ 322 | Path: migrationsPath, 323 | FileName: fmt.Sprintf(filenamef, version, name, "up", d.FilenameExtension()), 324 | Name: name, 325 | Content: []byte(""), 326 | Direction: direction.Up, 327 | }, 328 | DownFile: &file.File{ 329 | Path: migrationsPath, 330 | FileName: fmt.Sprintf(filenamef, version, name, "down", d.FilenameExtension()), 331 | Name: name, 332 | Content: []byte(""), 333 | Direction: direction.Down, 334 | }, 335 | } 336 | 337 | err = ioutil.WriteFile(path.Join(mfile.UpFile.Path, mfile.UpFile.FileName), mfile.UpFile.Content, 0644) 338 | return err 339 | } 340 | -------------------------------------------------------------------------------- /pipe/pipe.go: -------------------------------------------------------------------------------- 1 | // Package pipe has functions for pipe channel handling. 2 | package pipe 3 | 4 | import ( 5 | "os" 6 | ) 7 | 8 | // New creates a new pipe. A pipe is basically a channel. 9 | func New() chan interface{} { 10 | return make(chan interface{}, 0) 11 | } 12 | 13 | // Close closes a pipe and optionally sends an error 14 | func Close(pipe chan interface{}, err error) { 15 | if err != nil { 16 | pipe <- err 17 | } 18 | close(pipe) 19 | } 20 | 21 | // WaitAndRedirect waits for pipe to be closed and 22 | // redirects all messages from pipe to redirectPipe 23 | // while it waits. It also checks if there was an 24 | // interrupt send and will quit gracefully if yes. 25 | func WaitAndRedirect(pipe, redirectPipe chan interface{}, interrupt chan os.Signal) (ok bool) { 26 | errorReceived := false 27 | interruptsReceived := 0 28 | if pipe != nil && redirectPipe != nil { 29 | for { 30 | select { 31 | 32 | case <-interrupt: 33 | interruptsReceived += 1 34 | if interruptsReceived > 1 { 35 | os.Exit(5) 36 | } else { 37 | // add white space at beginning for ^C splitting 38 | redirectPipe <- " Aborting after this migration ... Hit again to force quit." 39 | } 40 | 41 | case item, ok := <-pipe: 42 | if !ok { 43 | return !errorReceived && interruptsReceived == 0 44 | } else { 45 | redirectPipe <- item 46 | switch item.(type) { 47 | case error: 48 | errorReceived = true 49 | } 50 | } 51 | } 52 | } 53 | } 54 | return !errorReceived && interruptsReceived == 0 55 | } 56 | 57 | // ReadErrors selects all received errors and returns them. 58 | // This is helpful for synchronous migration functions. 59 | func ReadErrors(pipe chan interface{}) []error { 60 | err := make([]error, 0) 61 | if pipe != nil { 62 | for { 63 | select { 64 | case item, ok := <-pipe: 65 | if !ok { 66 | return err 67 | } else { 68 | switch item.(type) { 69 | case error: 70 | err = append(err, item.(error)) 71 | } 72 | } 73 | } 74 | } 75 | } 76 | return err 77 | } 78 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | const Version string = "1.3.2" 4 | --------------------------------------------------------------------------------