Compare commits

..
112 Commits
Author SHA1 Message Date
Matthias Kadenbach 0d9689ab9f go get update 2017-05-17 19:37:36 -07:00
Matthias Kadenbach e696a861ba fix makefile 2017-05-17 19:26:24 -07:00
Matthias Kadenbach 801e84d4d2 fix make deps 2017-05-17 19:21:48 -07:00
Matthias Kadenbach ff7e060269 remove ql 2017-05-17 19:16:08 -07:00
Matthias Kadenbach d4d20346f2 build cli with all source and database drivers 2017-05-17 19:10:45 -07:00
Matthias KadenbachandGitHub 1b48b97aa4 Update README.md 2017-05-16 13:27:30 -07:00
Matthias KadenbachandGitHub d1e665bac7 Merge pull request #227 from fsouza/gcs-source
source/google-cloud-storage: implement the driver
2017-05-16 13:14:34 -07:00
Matthias KadenbachandGitHub dc109fc118 Merge pull request #228 from fsouza/s3-source
source/aws-s3: implement the driver
2017-05-16 13:13:00 -07:00
Matthias KadenbachandGitHub fb8698d89a Merge pull request #223 from Menta2L/master
ql database support
2017-05-16 13:09:22 -07:00
vesco 2821341551 make ExecuteQuery private 2017-05-16 12:07:40 +03:00
Matthias KadenbachandGitHub 2831b4c0d8 Merge pull request #222 from OrbotixInc/master
Added support for Redshift.
2017-05-15 18:31:13 -07:00
Francisco Souza c95cbb5931 source/aws-s3: implement the driver 2017-05-14 14:31:40 -04:00
Francisco Souza da89656fdc source/google-cloud-storage: implement the driver 2017-05-14 13:47:01 -04:00
root 4e5dc68b4e ql support 2017-05-12 11:11:16 +03:00
Jonathan Bryant 91ecd3cb6a Added support for Redshift.
* Added driver that wraps the standard PostgreSQL driver
* Added build.
* Added driver to documentation.
2017-05-11 15:47:40 -06:00
Matthias KadenbachandGitHub 448a36c5cb Merge pull request #210 from divan/master
Add missing import to example in README
2017-05-04 11:05:03 -07:00
Ivan DanilukandGitHub ed5a887579 Add import to example.
The example in README with file source doesn't work without importing "github.com/mattes/migrate/source/github" package.
2017-05-04 19:42:12 +02:00
Matthias KadenbachandGitHub e42058f15a Merge pull request #209 from pgmcgee/patch-1
Fix a typo in go-bindata/README.md
2017-05-04 09:33:40 -07:00
Parker McGeeandGitHub a7e1cacd36 Fix a typo
I was browsing this and noticed the typo that is causing syntax highlighting to be off.
2017-05-04 07:44:37 -04:00
Matthias KadenbachandGitHub 734803549d Merge pull request #204 from fln/master
Fix appending multiStatements=true parameter for mysql driver
2017-04-25 13:37:09 -07:00
Julius Kriukas 14f784961f Fix appending multiStatements=true parameter for mysql driver 2017-04-25 22:54:16 +03:00
Matthias Kadenbach 5c8da71dea update travis branch 2017-04-25 12:01:21 -07:00
Matthias Kadenbach a6fe4b73b3 update makefile 2017-04-25 11:59:52 -07:00
Matthias KadenbachandGitHub 256e8f045b Update README.md 2017-04-25 11:13:59 -07:00
Matthias KadenbachandGitHub 2e54bda0e1 Update README.md 2017-04-25 11:06:06 -07:00
Matthias Kadenbach 2ae8e6291c remove v1 migrate compat 2017-04-25 10:37:55 -07:00
Matthias KadenbachandGitHub 651f78cba8 Update README.md 2017-04-21 13:36:03 -07:00
Matthias KadenbachandGitHub d5674ed411 Create README.md 2017-04-21 13:30:59 -07:00
Matthias KadenbachandGitHub 4a2a810c20 Update README.md 2017-04-21 13:29:13 -07:00
Matthias KadenbachandGitHub 365b0565de Update README.md 2017-04-21 13:07:46 -07:00
Matthias KadenbachandGitHub 65d785d560 Update README.md 2017-04-21 13:02:10 -07:00
Matthias KadenbachandGitHub fff688c019 Create README.md 2017-04-21 13:00:19 -07:00
Matthias Kadenbach 6f07ac731b fix go toolchain build 2017-04-21 12:23:01 -07:00
Matthias Kadenbach f00c15d7b0 update go toolchain install 2017-04-20 16:03:43 -07:00
Matthias Kadenbach 1676a902f2 update readme 2017-04-20 15:22:06 -07:00
Matthias KadenbachandGitHub 576d323c52 Merge pull request #200 from Gobonoid/fix/ReadmeUpdate
Updates import path in README example
2017-04-19 10:04:37 -07:00
Gobonoid b4adf71696 Updates import path in README examplee 2017-04-19 11:48:58 +01:00
Matthias KadenbachandGitHub c0703e7a92 Merge pull request #199 from guzart/date-versions
Date-based versions
2017-04-16 12:38:48 -07:00
Arturo Guzman d789d50bd6 Support date-based versions 2017-04-13 20:57:22 -04:00
Matthias KadenbachandGitHub b83285bd59 Merge pull request #194 from Arkq/v3.0-prev
Support for alternative postgres URI scheme
2017-04-10 11:22:50 -07:00
Arkadiusz Bokowy bc93ed027f Support for alternative postgres URI scheme
According to the PostgreSQL documentation (section 32.1.1.2), postgres
library supports two URI schemes: postgresql:// and postgres://

Reference:
https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING
2017-04-04 08:29:33 +02:00
Matthias Kadenbach 9f974b8996 Update Readme 2017-03-12 18:00:22 -07:00
Matthias Kadenbach c03b03c909 return status 0 for ErrNoChange #182 2017-03-12 12:33:13 -07:00
Matthias Kadenbach cecce18c51 fix source/file relative paths, ref #178 thanks @matyunin 2017-03-03 12:02:32 -08:00
Matthias Kadenbach 1f37f41ee8 remove cleanpath, fix mysql ensureVersionTable 2017-03-01 11:48:38 -08:00
Matthias Kadenbach 150ac7d708 add mysql custom TLS config
closes https://github.com/mattes/migrate/pull/117
2017-02-28 16:23:29 -08:00
Matthias Kadenbach be1ba9204a add mysql driver, add ENV to docker containers 2017-02-28 15:10:56 -08:00
Matthias Kadenbach 760bc3eb2d add note in readme 2017-02-23 14:50:48 -08:00
Matthias Kadenbach 706f23115f add v1compat 2017-02-23 14:48:44 -08:00
Matthias KadenbachandGitHub 171f9a256d Update FAQ.md 2017-02-23 14:27:04 -08:00
Matthias KadenbachandGitHub e7b9b0d0ed Update README.md 2017-02-23 13:36:34 -08:00
Matthias Kadenbach 5645143baa add example dockerfile 2017-02-23 13:13:02 -08:00
Matthias Kadenbach ec5c0ac3e0 fix version string 2017-02-23 10:36:33 -08:00
Matthias Kadenbach 511f9c1ffe only deploy on version go 1.8 2017-02-23 10:30:18 -08:00
Matthias Kadenbach 6325327ba1 fix dir 2017-02-22 17:58:25 -08:00
Matthias Kadenbach d60d590381 remove escaped new line 2017-02-22 17:48:50 -08:00
Matthias Kadenbach 5aac011e49 remove dups 2017-02-22 17:38:40 -08:00
Matthias Kadenbach 320d573ccd fix sudo whitespace 2017-02-22 17:27:18 -08:00
Matthias Kadenbach c51bef38b6 sudo 2017-02-22 17:15:40 -08:00
Matthias Kadenbach 28e0548cac fix github
see https://github.com/google/go-github/commit/23d6cb9cacb5aa314e93d600fe20a48496a718d4
2017-02-22 16:56:14 -08:00
Matthias Kadenbach d0acf6ecfe build deb package and push to packagecloud 2017-02-22 16:41:11 -08:00
Matthias Kadenbach 4b91f503eb make Force doc clear 2017-02-19 16:22:48 -08:00
Matthias Kadenbach eb02bc77fb allow to force NilVersion 2017-02-19 16:06:11 -08:00
Matthias KadenbachandGitHub 55f54251eb Update FAQ.md 2017-02-19 15:56:45 -08:00
Matthias Kadenbach e64b7cc233 explain dirty version error 2017-02-19 15:51:12 -08:00
Matthias Kadenbach 4bb46a98c6 fix github
see https://github.com/google/go-github/commit/2a4b92075e5c5e42ee0e55819b70f49b02c1ac64
2017-02-19 15:31:57 -08:00
Matthias Kadenbach 53b55288b3 bring back dirty state from v2 2017-02-19 15:15:00 -08:00
Matthias KadenbachandGitHub f576a64f11 Update README.md 2017-02-18 10:41:17 -08:00
Matthias Kadenbach d574676702 drop custom query params 2017-02-17 16:59:47 -08:00
Matthias Kadenbach f45821581c add multi statement test 2017-02-16 22:10:01 -08:00
Matthias KadenbachandGitHub bb6ae0898d Update README.md 2017-02-16 21:13:12 -08:00
Matthias KadenbachandGitHub ea7fc641ba Update README.md 2017-02-16 21:12:33 -08:00
Matthias KadenbachandGitHub 87e6482497 travis build with go 1.8 2017-02-16 20:46:12 -08:00
Matthias KadenbachandGitHub 1e08e21470 update with issues 2017-02-16 20:41:17 -08:00
Matthias Kadenbach ccb40ff3ff Update Readme 2017-02-16 11:29:18 -08:00
Matthias Kadenbach 3291f3f891 add comment 2017-02-16 11:18:02 -08:00
Matthias Kadenbach 2031939bfc allow postgres migrations_table config 2017-02-16 11:06:11 -08:00
Matthias Kadenbach 6394299937 clean cli -path 2017-02-15 11:07:28 -08:00
Matthias Kadenbach 316f6c0cf3 switch to binary search 2017-02-15 10:48:43 -08:00
Matthias Kadenbach 53630f5ff1 add NewWithDatabaseInstance example 2017-02-14 23:25:19 -08:00
Matthias Kadenbach 3feee5958a updated postgres driver, add database.Error struct 2017-02-14 23:12:16 -08:00
Matthias Kadenbach 209c056258 add database lock timeouts in migrate and in tests 2017-02-11 19:15:54 -08:00
Matthias Kadenbach cd6e62049c Update documentation 2017-02-11 11:13:27 -08:00
Matthias Kadenbach 526c0a918e add packagecloud.io badge 2017-02-09 20:09:11 -08:00
Matthias Kadenbach de7ffa60cc update readme 2017-02-09 20:07:04 -08:00
Matthias Kadenbach 81b9791178 move contributing in own file 2017-02-09 20:03:56 -08:00
Matthias Kadenbach c4ec6ac0f7 move CLI usage in own README 2017-02-09 20:00:38 -08:00
Matthias Kadenbach cfe3d2a83e add example for parallelTest 2017-02-09 19:52:50 -08:00
Matthias Kadenbach b6bd4fe02d add comments for source 2017-02-09 19:42:48 -08:00
Matthias Kadenbach 2da976155a add comments for database 2017-02-09 19:04:54 -08:00
Matthias Kadenbach e69e0de9c9 fix old ref to LogString 2017-02-09 18:26:58 -08:00
Matthias Kadenbach cfc9f10a6e add some more comments 2017-02-09 18:23:08 -08:00
Matthias Kadenbach 8ba71b0754 rename nameFromUrl to schemeFromUrl 2017-02-09 18:19:13 -08:00
Matthias Kadenbach 46c16bb8f1 add test for suint 2017-02-09 18:16:16 -08:00
Matthias Kadenbach 1b079a3cda rename Migration.LongString to LogString 2017-02-09 18:06:38 -08:00
Matthias Kadenbach c1b23fee45 add Migrate.Run 2017-02-09 18:04:36 -08:00
Matthias Kadenbach 199678e1bc add code comments for package migrate 2017-02-09 16:59:30 -08:00
Matthias Kadenbach d5daf6f6ae add .godoc.pid to gitignore 2017-02-09 13:47:28 -08:00
Matthias Kadenbach 03d8d3233f fix line break in makefile 2017-02-09 12:03:03 -08:00
Matthias Kadenbach 5c6a21b014 add make docs 2017-02-09 11:59:49 -08:00
Matthias Kadenbach 3965df69d5 fix indent in makefile 2017-02-08 20:13:17 -08:00
Matthias Kadenbach f47436e158 add make release 2017-02-08 19:54:50 -08:00
Matthias Kadenbach f474e4cae1 travis builds dep tree info 2017-02-08 19:44:12 -08:00
Matthias Kadenbach 7979fdb832 add list-external-deps 2017-02-08 19:34:53 -08:00
Matthias Kadenbach b3f6bc47bb update readme with dev, testing, and contribution section 2017-02-08 18:27:12 -08:00
Matthias Kadenbach e6163082d7 add restore-import-paths and rewrite-import-paths 2017-02-08 18:01:01 -08:00
Matthias Kadenbach fe1b5c06eb add go coverage 2017-02-08 16:17:04 -08:00
Matthias Kadenbach 297a4227ef fixing travis 2017-02-08 01:49:57 -08:00
Matthias Kadenbach 9f774d3e6a fix typo in go-bindata README 2017-02-07 23:00:25 -08:00
Matthias Kadenbach f7699f1dec update go-bindata with latest source.Migration helper 2017-02-07 22:59:12 -08:00
Matthias Kadenbach 3889c93b63 add Readme draft 2017-02-07 22:40:45 -08:00
Matthias Kadenbach ee384ed8cc initial version 3.0.0 preview
hard work
2017-02-07 22:01:29 -08:00
161 changed files with 7697 additions and 5369 deletions
+5 -1
View File
@@ -1,2 +1,6 @@
.DS_Store
test.db
cli/build
cli/cli
cli/migrate
.coverage
.godoc.pid
+53 -13
View File
@@ -2,20 +2,60 @@ language: go
sudo: required
go:
- 1.4.x
- 1.5.x
- 1.6.x
- 1.7.x
- 1.8.x
- 1.7
- 1.8
env:
- MIGRATE_TEST_CONTAINER_BOOT_DELAY=10
# TODO: https://docs.docker.com/engine/installation/linux/ubuntu/
# pre-provision with travis docker setup and pin down docker version in install step
services:
- docker
- docker
before_install:
- curl -L https://github.com/docker/compose/releases/download/1.11.2/docker-compose-`uname -s`-`uname -m` > docker-compose
- chmod +x docker-compose
- sudo mv docker-compose /usr/local/bin
- sed -i -e 's/golang/golang:'"$TRAVIS_GO_VERSION"'/' docker-compose.yml
install:
- make deps
- (cd $GOPATH/src/github.com/docker/docker && git fetch --all --tags --prune && git checkout v1.13.0)
- sudo apt-get update && sudo apt-get install docker-engine=1.13.0*
- go get github.com/mattn/goveralls
script:
- make test
after_success:
- goveralls -service=travis-ci -coverprofile .coverage/combined.txt
- make list-external-deps > dependency_tree.txt && cat dependency_tree.txt
before_deploy:
- make build-cli
- gem install --no-ri --no-rdoc fpm
- fpm -s dir -t deb -n migrate -v "$(git describe --tags 2>/dev/null | cut -c 2-)" --license MIT -m matthias.kadenbach@gmail.com --url https://github.com/mattes/migrate --description='Database migrations' -a amd64 -p migrate.$(git describe --tags 2>/dev/null | cut -c 2-).deb --deb-no-default-config-files -f -C cli/build migrate.linux-amd64=/usr/bin/migrate
deploy:
- provider: releases
api_key:
secure: EFow50BI448HVb/uQ1Kk2Kq0xzmwIYq3V67YyymXIuqSCodvXEsMiBPUoLrxEknpPEIc67LEQTNdfHBgvyHk6oRINWAfie+7pr5tKrpOTF9ghyxoN1PlO8WKQCqwCvGMBCnc5ur5rvzp0bqfpV2rs5q9/nngy3kBuEvs12V7iho=
skip_cleanup: true
on:
go: 1.8
repo: mattes/migrate
tags: true
file:
- cli/build/migrate.linux-amd64.tar.gz
- cli/build/migrate.darwin-amd64.tar.gz
- cli/build/migrate.windows-amd64.exe.tar.gz
- cli/build/sha256sum.txt
- dependency_tree.txt
- provider: packagecloud
repository: migrate
username: mattes
token:
secure: RiHJ/+J9DvXUah/APYdWySWZ5uOOISYJ0wS7xddc7/BNStRVjzFzvJ9zmb67RkyZZrvGuVjPiL4T8mtDyCJCj47RmU/56wPdEHbar/FjsiUCgwvR19RlulkgbV4okBCePbwzMw6HNHRp14TzfQCPtnN4kef0lOI4gZJkImN7rtQ=
dist: ubuntu/xenial
package_glob: '*.deb'
skip_cleanup: true
on:
go: 1.8
repo: mattes/migrate
tags: true
script: make test
+22
View File
@@ -0,0 +1,22 @@
# Development, Testing and Contributing
1. Make sure you have a running Docker daemon
(Install for [MacOS](https://docs.docker.com/docker-for-mac/))
2. Fork this repo and `git clone` somewhere to `$GOPATH/src/github.com/%you%/migrate`
3. `make rewrite-import-paths` to update imports to your local fork
4. Confirm tests are working: `make test-short`
5. Write awesome code ...
6. `make test` to run all tests against all database versions
7. `make restore-import-paths` to restore import paths
8. Push code and open Pull Request
Some more helpful commands:
* You can specify which database/ source tests to run:
`make test-short SOURCE='file go-bindata' DATABASE='postgres cassandra'`
* After `make test`, run `make html-coverage` which opens a shiny test coverage overview.
* Missing imports? `make deps`
* `make build-cli` builds the CLI in directory `cli/build/`.
* `make list-external-deps` lists all external dependencies for each package
* `make docs && make open-docs` opens godoc in your browser, `make kill-docs` kills the godoc server.
Repeatedly call `make docs` to refresh the server.
-3
View File
@@ -1,3 +0,0 @@
FROM scratch
ADD migrate /migrate
ENTRYPOINT ["/migrate"]
+67
View File
@@ -0,0 +1,67 @@
# FAQ
#### How is the code base structured?
```
/ package migrate (the heart of everything)
/cli the CLI wrapper
/database database driver and sub directories have the actual driver implementations
/source source driver and sub directories have the actual driver implementations
```
#### Why is there no `source/driver.go:Last()`?
It's not needed. And unless the source has a "native" way to read a directory in reversed order,
it might be expensive to do a full directory scan in order to get the last element.
#### What is a NilMigration? NilVersion?
NilMigration defines a migration without a body. NilVersion is defined as const -1.
#### What is the difference between uint(version) and int(targetVersion)?
version refers to an existing migration version coming from a source and therefor can never be negative.
targetVersion can either be a version OR represent a NilVersion, which equals -1.
#### What's the difference between Next/Previous and Up/Down?
```
1_first_migration.up next -> 2_second_migration.up ...
1_first_migration.down <- previous 2_second_migration.down ...
```
#### Why two separate files (up and down) for a migration?
It makes all of our lives easier. No new markup/syntax to learn for users
and existing database utility tools continue to work as expected.
#### How many migrations can migrate handle?
Whatever the maximum positive signed integer value is for your platform.
For 32bit it would be 2,147,483,647 migrations. Migrate only keeps references to
the currently run and pre-fetched migrations in memory. Please note that some
source drivers need to do build a full "directory" tree first, which puts some
heat on the memory consumption.
#### Are the table tests in migrate_test.go bloated?
Yes and no. There are duplicate test cases for sure but they don't hurt here. In fact
the tests are very visual now and might help new users understand expected behaviors quickly.
Migrate from version x to y and y is the last migration? Just check out the test for
that particular case and know what's going on instantly.
#### What is Docker being used for?
Only for testing. See [testing/docker.go](testing/docker.go)
#### Why not just use docker-compose?
It doesn't give us enough runtime control for testing. We want to be able to bring up containers fast
and whenever we want, not just once at the beginning of all tests.
#### Can I maintain my driver in my own repository?
Yes, technically thats possible. We want to encourage you to contribute your driver to this respository though.
The driver's functionality is dictated by migrate's interfaces. That means there should really
just be one driver for a database/ source. We want to prevent a future where several drivers doing the exact same thing,
just implemented a bit differently, co-exist somewhere on Github. If users have to do research first to find the
"best" available driver for a database in order to get started, we would have failed as an open source community.
#### Can I mix multiple sources during a batch of migrations?
No.
#### What does "dirty" database mean?
Before a migration runs, each database sets a dirty flag. Execution stops if a migration fails and the dirty state persists,
which prevents attempts to run more migrations on top of a failed migration. You need to manually fix the error
and then "force" the expected version.
+4 -2
View File
@@ -1,6 +1,8 @@
The MIT License (MIT)
Copyright (c) 2014 Matthias Kadenbach
Copyright (c) 2016 Matthias Kadenbach
https://github.com/mattes/migrate
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -18,4 +20,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
THE SOFTWARE.
+5
View File
@@ -0,0 +1,5 @@
# Migrations
## Best practices: How to write migrations.
@TODO
+113 -20
View File
@@ -1,30 +1,123 @@
TESTFLAGS?=
IMAGE=mattes/migrate
DCR=docker-compose run --rm
GOTEST=go test $(TESTFLAGS) `go list ./... | grep -v "/vendor/"`
SOURCE ?= file go-bindata github aws-s3 google-cloud-storage
DATABASE ?= postgres mysql redshift
VERSION ?= $(shell git describe --tags 2>/dev/null | cut -c 2-)
TEST_FLAGS ?=
REPO_OWNER ?= $(shell cd .. && basename "$$(pwd)")
build-cli: clean
-mkdir ./cli/build
cd ./cli && GOOS=linux GOARCH=amd64 go build -a -o build/migrate.linux-amd64 -ldflags='-X main.Version=$(VERSION)' -tags '$(DATABASE) $(SOURCE)' .
cd ./cli && GOOS=darwin GOARCH=amd64 go build -a -o build/migrate.darwin-amd64 -ldflags='-X main.Version=$(VERSION)' -tags '$(DATABASE) $(SOURCE)' .
cd ./cli && GOOS=windows GOARCH=amd64 go build -a -o build/migrate.windows-amd64.exe -ldflags='-X main.Version=$(VERSION)' -tags '$(DATABASE) $(SOURCE)' .
cd ./cli/build && find . -name 'migrate*' | xargs -I{} tar czf {}.tar.gz {}
cd ./cli/build && shasum -a 256 * > sha256sum.txt
cat ./cli/build/sha256sum.txt
.PHONY: clean test build release docker-build docker-push run
all: release
clean:
rm -f migrate
-rm -r ./cli/build
fmt:
@gofmt -s -w `go list -f {{.Dir}} ./... | grep -v "/vendor/"`
test: fmt
$(DCR) go-test
test-short:
make test-with-flags --ignore-errors TEST_FLAGS='-short'
go-test: fmt
@$(GOTEST)
build:
$(DCR) go-build
test:
@-rm -r .coverage
@mkdir .coverage
make test-with-flags TEST_FLAGS='-v -race -covermode atomic -coverprofile .coverage/_$$(RAND).txt -bench=. -benchmem'
@echo 'mode: atomic' > .coverage/combined.txt
@cat .coverage/*.txt | grep -v 'mode: atomic' >> .coverage/combined.txt
release: test build docker-build docker-push
docker-build:
docker build --rm -t $(IMAGE) .
test-with-flags:
@echo SOURCE: $(SOURCE)
@echo DATABASE: $(DATABASE)
@go test $(TEST_FLAGS) .
@go test $(TEST_FLAGS) ./cli/...
@go test $(TEST_FLAGS) ./testing/...
@echo -n '$(SOURCE)' | tr -s ' ' '\n' | xargs -I{} go test $(TEST_FLAGS) ./source/{}
@go test $(TEST_FLAGS) ./source/testing/...
@go test $(TEST_FLAGS) ./source/stub/...
@echo -n '$(DATABASE)' | tr -s ' ' '\n' | xargs -I{} go test $(TEST_FLAGS) ./database/{}
@go test $(TEST_FLAGS) ./database/testing/...
@go test $(TEST_FLAGS) ./database/stub/...
kill-orphaned-docker-containers:
docker rm -f $(shell docker ps -aq --filter label=migrate_test)
html-coverage:
go tool cover -html=.coverage/combined.txt
deps:
-go get -v -u ./...
-go test -v -i ./...
# TODO: why is this not being fetched with the command above?
-go get -u github.com/fsouza/fake-gcs-server/fakestorage
list-external-deps:
$(call external_deps,'.')
$(call external_deps,'./cli/...')
$(call external_deps,'./testing/...')
$(foreach v, $(SOURCE), $(call external_deps,'./source/$(v)/...'))
$(call external_deps,'./source/testing/...')
$(call external_deps,'./source/stub/...')
$(foreach v, $(DATABASE), $(call external_deps,'./database/$(v)/...'))
$(call external_deps,'./database/testing/...')
$(call external_deps,'./database/stub/...')
restore-import-paths:
find . -name '*.go' -type f -execdir sed -i '' s%\"github.com/$(REPO_OWNER)/migrate%\"github.com/mattes/migrate%g '{}' \;
rewrite-import-paths:
find . -name '*.go' -type f -execdir sed -i '' s%\"github.com/mattes/migrate%\"github.com/$(REPO_OWNER)/migrate%g '{}' \;
# example: fswatch -0 --exclude .godoc.pid --event Updated . | xargs -0 -n1 -I{} make docs
docs:
-make kill-docs
nohup godoc -play -http=127.0.0.1:6064 </dev/null >/dev/null 2>&1 & echo $$! > .godoc.pid
cat .godoc.pid
kill-docs:
@cat .godoc.pid
kill -9 $$(cat .godoc.pid)
rm .godoc.pid
open-docs:
open http://localhost:6064/pkg/github.com/$(REPO_OWNER)/migrate
# example: make release V=0.0.0
release:
git tag v$(V)
@read -p "Press enter to confirm and push to origin ..." && git push origin v$(V)
define external_deps
@echo '-- $(1)'; go list -f '{{join .Deps "\n"}}' $(1) | grep -v github.com/$(REPO_OWNER)/migrate | xargs go list -f '{{if not .Standard}}{{.ImportPath}}{{end}}'
endef
.PHONY: build-cli clean test-short test test-with-flags deps html-coverage \
restore-import-paths rewrite-import-paths list-external-deps release \
docs kill-docs open-docs kill-orphaned-docker-containers
SHELL = /bin/bash
RAND = $(shell echo $$RANDOM)
docker-push:
docker push $(IMAGE)
+115 -104
View File
@@ -1,132 +1,143 @@
# You are looking at a deprecated version.
# Use the [latest version](https://github.com/mattes/migrate) instead.
---
[![Build Status](https://travis-ci.org/mattes/migrate.svg?branch=master)](https://travis-ci.org/mattes/migrate)
[![GoDoc](https://godoc.org/github.com/mattes/migrate?status.svg)](https://godoc.org/github.com/mattes/migrate)
[![Coverage Status](https://coveralls.io/repos/github/mattes/migrate/badge.svg?branch=v3.0-prev)](https://coveralls.io/github/mattes/migrate?branch=v3.0-prev)
[![packagecloud.io](https://img.shields.io/badge/deb-packagecloud.io-844fec.svg)](https://packagecloud.io/mattes/migrate?filter=debs)
# migrate
[![Build Status](https://travis-ci.org/mattes/migrate.svg?branch=v1)](https://travis-ci.org/mattes/migrate)
__Database migrations written in Go. Use as [CLI](#cli-usage) or import as [library](#use-in-your-go-project).__
A migration helper written in Go. Use it in your existing Golang code
or run commands via the CLI.
* Migrate reads migrations from [sources](#migration-sources)
and applies them in correct order to a [database](#databases).
* Drivers are "dumb", migrate glues everything together and makes sure the logic is bulletproof.
(Keeps the drivers lightweight, too.)
* Database drivers don't assume things or try to correct user input. When in doubt, fail.
Looking for [v1](https://github.com/mattes/migrate/tree/v1)?
## Databases
Database drivers run migrations. [Add a new database?](database/driver.go)
* [PostgreSQL](database/postgres)
* [Redshift](database/redshift)
* [Ql](database/ql)
* [Cassandra](database/cassandra) ([todo #164](https://github.com/mattes/migrate/issues/164))
* [SQLite](database/sqlite) ([todo #165](https://github.com/mattes/migrate/issues/165))
* [MySQL/ MariaDB](database/mysql)
* [Neo4j](database/neo4j) ([todo #167](https://github.com/mattes/migrate/issues/167))
* [MongoDB](database/mongodb) ([todo #169](https://github.com/mattes/migrate/issues/169))
* [CrateDB](database/crate) ([todo #170](https://github.com/mattes/migrate/issues/170))
* [Shell](database/shell) ([todo #171](https://github.com/mattes/migrate/issues/171))
* [Google Cloud Spanner](database/spanner) ([todo #172](https://github.com/mattes/migrate/issues/172))
## Migration Sources
Source drivers read migrations from local or remote sources. [Add a new source?](source/driver.go)
* [Filesystem](source/file) - read from fileystem (always included)
* [Go-Bindata](source/go-bindata) - read from embedded binary data ([jteeuwen/go-bindata](https://github.com/jteeuwen/go-bindata))
* [Github](source/github) - read from remote Github repositories
* [AWS S3](source/aws-s3) - read from Amazon Web Services S3
* [Google Cloud Storage](source/google-cloud-storage) - read from Google Cloud Platform Storage
## CLI usage
* Simple wrapper around this library.
* Handles ctrl+c (SIGINT) gracefully.
* No config search paths, no config files, no magic ENV var injections.
__[CLI Documentation](cli)__
([brew todo #156](https://github.com/mattes/migrate/issues/156))
```
GoCode import gopkg.in/mattes/migrate.v1/migrate
CLI go get -u gopkg.in/mattes/migrate.v1
```
__Features__
* Super easy to implement [Driver interface](http://godoc.org/gopkg.in/mattes/migrate.v1/driver#Driver).
* Gracefully quit running migrations on ``^C``.
* No magic search paths routines, no hard-coded config files.
* CLI is build on top of the ``migrate package``.
## Available Drivers
* [PostgreSQL](driver/postgres)
* [Cassandra](driver/cassandra)
* [SQLite](driver/sqlite3)
* [MySQL](driver/mysql) ([experimental](https://github.com/mattes/migrate/issues/1#issuecomment-58728186))
* [Neo4j](driver/neo4j)
* [Ql](driver/ql)
* [MongoDB](driver/mongodb)
* [CrateDB](driver/crate)
* [MSSQL](driver/mssql)
Need another driver? Just implement the [Driver interface](http://godoc.org/gopkg.in/mattes/migrate.v1/driver#Driver) and open a PR.
## Usage from Terminal
```bash
# install
go get gopkg.in/mattes/migrate.v1
# create new migration file in path
migrate -url driver://url -path ./migrations create migration_file_xyz
# apply all available migrations
migrate -url driver://url -path ./migrations up
# roll back all migrations
migrate -url driver://url -path ./migrations down
# roll back the most recently applied migration, then run it again.
migrate -url driver://url -path ./migrations redo
# run down and then up command
migrate -url driver://url -path ./migrations reset
# show the current migration version
migrate -url driver://url -path ./migrations version
# apply the next n migrations
migrate -url driver://url -path ./migrations migrate +1
migrate -url driver://url -path ./migrations migrate +2
migrate -url driver://url -path ./migrations migrate +n
# roll back the previous n migrations
migrate -url driver://url -path ./migrations migrate -1
migrate -url driver://url -path ./migrations migrate -2
migrate -url driver://url -path ./migrations migrate -n
# go to specific migration
migrate -url driver://url -path ./migrations goto 1
migrate -url driver://url -path ./migrations goto 10
migrate -url driver://url -path ./migrations goto v
$ brew install migrate --with-postgres
$ migrate -database postgres://localhost:5432/database up 2
```
## Usage in Go
## Use in your Go project
See GoDoc here: http://godoc.org/gopkg.in/mattes/migrate.v1/migrate
* API is stable and frozen for this release (v3.x).
* Package migrate has no external dependencies.
* Only import the drivers you need.
(check [dependency_tree.txt](https://github.com/mattes/migrate/releases) for each driver)
* To help prevent database corruptions, it supports graceful stops via `GracefulStop chan bool`.
* Bring your own logger.
* Uses `io.Reader` streams internally for low memory overhead.
* Thread-safe and no goroutine leaks.
__[Go Documentation](https://godoc.org/github.com/mattes/migrate)__
```go
import "gopkg.in/mattes/migrate.v1/migrate"
import (
"github.com/mattes/migrate"
_ "github.com/mattes/migrate/database/postgres"
_ "github.com/mattes/migrate/source/github"
)
// Import any required drivers so that they are registered and available
import _ "gopkg.in/mattes/migrate.v1/driver/mysql"
// use synchronous versions of migration functions ...
allErrors, ok := migrate.UpSync("driver://url", "./path")
if !ok {
fmt.Println("Oh no ...")
// do sth with allErrors slice
func main() {
m, err := migrate.New(
"github://mattes:personal-access-token@mattes/migrate_test",
"postgres://localhost:5432/database?sslmode=enable")
m.Steps(2)
}
```
// use the asynchronous version of migration functions ...
pipe := migrate.NewPipe()
go migrate.Up(pipe, "driver://url", "./path")
// pipe is basically just a channel
// write your own channel listener. see writePipe() in main.go as an example.
Want to use an existing database client?
```go
import (
"database/sql"
_ "github.com/lib/pq"
"github.com/mattes/migrate"
"github.com/mattes/migrate/database/postgres"
_ "github.com/mattes/migrate/source/file"
)
func main() {
db, err := sql.Open("postgres", "postgres://localhost:5432/database?sslmode=enable")
driver, err := postgres.WithInstance(db, &postgres.Config{})
m, err := migrate.NewWithDatabaseInstance(
"file:///migrations",
"postgres", driver)
m.Steps(2)
}
```
## Migration files
The format of migration files looks like this:
Each migration has an up and down migration. [Why?](FAQ.md#why-two-separate-files-up-and-down-for-a-migration)
```
1481574547_initial_plan_to_do_sth.up.sql # up migration instructions
1481574547_initial_plan_to_do_sth.down.sql # down migration instructions
1482438365_xxx.up.sql
1482438365_xxx.down.sql
...
1481574547_create_users_table.up.sql
1481574547_create_users_table.down.sql
```
Why two files? This way you could still do sth like
``psql -f ./db/migrations/1481574547_initial_plan_to_do_sth.up.sql`` and there is no
need for any custom markup language to divide up and down migrations. Please note
that the filename extension depends on the driver.
[Best practices: How to write migrations.](MIGRATIONS.md)
## Alternatives
* https://bitbucket.org/liamstask/goose
* https://github.com/tanel/dbmigrate
* https://github.com/BurntSushi/migration
* https://github.com/DavidHuie/gomigrate
* https://github.com/rubenv/sql-migrate
## Development and Contributing
Yes, please! [`Makefile`](Makefile) is your friend,
read the [development guide](CONTRIBUTING.md).
Also have a look at the [FAQ](FAQ.md).
---
__Alternatives__
https://bitbucket.org/liamstask/goose, https://github.com/tanel/dbmigrate,
https://github.com/BurntSushi/migration, https://github.com/DavidHuie/gomigrate,
https://github.com/rubenv/sql-migrate
+105
View File
@@ -0,0 +1,105 @@
# migrate CLI
## Installation
#### With Go toolchain
```
$ go get -u -d github.com/mattes/migrate/cli
$ go build -tags 'postgres' -o /usr/local/bin/migrate github.com/mattes/migrate/cli
```
#### MacOS
([todo #156](https://github.com/mattes/migrate/issues/156))
```
$ brew install migrate --with-postgres
```
#### Linux (*.deb package)
```
$ curl -L https://packagecloud.io/mattes/migrate/gpgkey | apt-key add -
$ echo "deb https://packagecloud.io/mattes/migrate/ubuntu/ xenial main" > /etc/apt/sources.list.d/migrate.list
$ apt-get update
$ apt-get install -y migrate
```
#### Download pre-build binary (Windows, MacOS, or Linux)
[Release Downloads](https://github.com/mattes/migrate/releases)
```
$ curl -L https://github.com/mattes/migrate/releases/download/$version/migrate.$platform-amd64.tar.gz | tar xvz
```
## Usage
```
$ migrate -help
Usage: migrate OPTIONS COMMAND [arg...]
migrate [ -version | -help ]
Options:
-source Location of the migrations (driver://url)
-path Shorthand for -source=file://path
-database Run migrations against this database (driver://url)
-prefetch N Number of migrations to load in advance before executing (default 10)
-lock-timeout N Allow N seconds to acquire database lock (default 15)
-verbose Print verbose logging
-version Print version
-help Print usage
Commands:
goto V Migrate to version V
up [N] Apply all or N up migrations
down [N] Apply all or N down migrations
drop Drop everyting inside database
force V Set version V but don't run migration (ignores dirty state)
version Print current migration version
```
So let's say you want to run the first two migrations
```
$ migrate -database postgres://localhost:5432/database up 2
```
If your migrations are hosted on github
```
$ migrate -source github://mattes:personal-access-token@mattes/migrate_test \
-database postgres://localhost:5432/database down 2
```
The CLI will gracefully stop at a safe point when SIGINT (ctrl+c) is received.
Send SIGKILL for immediate halt.
## Reading CLI arguments from somewhere else
##### ENV variables
```
$ migrate -database "$MY_MIGRATE_DATABASE"
```
##### JSON files
Check out https://stedolan.github.io/jq/
```
$ migrate -database "$(cat config.json | jq '.database')"
```
##### YAML files
````
$ migrate -database "$(cat config/database.yml | ruby -ryaml -e "print YAML.load(STDIN.read)['database']")"
$ migrate -database "$(cat config/database.yml | python -c 'import yaml,sys;print yaml.safe_load(sys.stdin)["database"]')"
```
+7
View File
@@ -0,0 +1,7 @@
// +build aws-s3
package main
import (
_ "github.com/mattes/migrate/source/aws-s3"
)
+7
View File
@@ -0,0 +1,7 @@
// +build github
package main
import (
_ "github.com/mattes/migrate/source/github"
)
+7
View File
@@ -0,0 +1,7 @@
// +build go-bindata
package main
import (
_ "github.com/mattes/migrate/source/go-bindata"
)
+7
View File
@@ -0,0 +1,7 @@
// +build google-cloud-storage
package main
import (
_ "github.com/mattes/migrate/source/google-cloud-storage"
)
+7
View File
@@ -0,0 +1,7 @@
// +build mysql
package main
import (
_ "github.com/mattes/migrate/database/mysql"
)
+7
View File
@@ -0,0 +1,7 @@
// +build postgres
package main
import (
_ "github.com/mattes/migrate/database/postgres"
)
+7
View File
@@ -0,0 +1,7 @@
// +build ql
package main
import (
_ "github.com/mattes/migrate/database/ql"
)
+7
View File
@@ -0,0 +1,7 @@
// +build redshift
package main
import (
_ "github.com/mattes/migrate/database/redshift"
)
+81
View File
@@ -0,0 +1,81 @@
package main
import (
"github.com/mattes/migrate"
_ "github.com/mattes/migrate/database/stub" // TODO remove again
_ "github.com/mattes/migrate/source/file"
)
func gotoCmd(m *migrate.Migrate, v uint) {
if err := m.Migrate(v); err != nil {
if err != migrate.ErrNoChange {
log.fatalErr(err)
} else {
log.Println(err)
}
}
}
func upCmd(m *migrate.Migrate, limit int) {
if limit >= 0 {
if err := m.Steps(limit); err != nil {
if err != migrate.ErrNoChange {
log.fatalErr(err)
} else {
log.Println(err)
}
}
} else {
if err := m.Up(); err != nil {
if err != migrate.ErrNoChange {
log.fatalErr(err)
} else {
log.Println(err)
}
}
}
}
func downCmd(m *migrate.Migrate, limit int) {
if limit >= 0 {
if err := m.Steps(-limit); err != nil {
if err != migrate.ErrNoChange {
log.fatalErr(err)
} else {
log.Println(err)
}
}
} else {
if err := m.Down(); err != nil {
if err != migrate.ErrNoChange {
log.fatalErr(err)
} else {
log.Println(err)
}
}
}
}
func dropCmd(m *migrate.Migrate) {
if err := m.Drop(); err != nil {
log.fatalErr(err)
}
}
func forceCmd(m *migrate.Migrate, v int) {
if err := m.Force(v); err != nil {
log.fatalErr(err)
}
}
func versionCmd(m *migrate.Migrate) {
v, dirty, err := m.Version()
if err != nil {
log.fatalErr(err)
}
if dirty {
log.Printf("%v (dirty)\n", v)
} else {
log.Println(v)
}
}
+12
View File
@@ -0,0 +1,12 @@
FROM ubuntu:xenial
RUN apt-get update && \
apt-get install -y curl apt-transport-https
RUN curl -L https://packagecloud.io/mattes/migrate/gpgkey | apt-key add - && \
echo "deb https://packagecloud.io/mattes/migrate/ubuntu/ xenial main" > /etc/apt/sources.list.d/migrate.list && \
apt-get update && \
apt-get install -y migrate
RUN migrate -version
+45
View File
@@ -0,0 +1,45 @@
package main
import (
"fmt"
logpkg "log"
"os"
)
type Log struct {
verbose bool
}
func (l *Log) Printf(format string, v ...interface{}) {
if l.verbose {
logpkg.Printf(format, v...)
} else {
fmt.Fprintf(os.Stderr, format, v...)
}
}
func (l *Log) Println(args ...interface{}) {
if l.verbose {
logpkg.Println(args...)
} else {
fmt.Fprintln(os.Stderr, args...)
}
}
func (l *Log) Verbose() bool {
return l.verbose
}
func (l *Log) fatalf(format string, v ...interface{}) {
l.Printf(format, v...)
os.Exit(1)
}
func (l *Log) fatal(args ...interface{}) {
l.Println(args...)
os.Exit(1)
}
func (l *Log) fatalErr(err error) {
l.fatal("error:", err)
}
+210
View File
@@ -0,0 +1,210 @@
package main
import (
"flag"
"fmt"
"os"
"os/signal"
"strconv"
"syscall"
"time"
"github.com/mattes/migrate"
)
// set main log
var log = &Log{}
func main() {
helpPtr := flag.Bool("help", false, "")
versionPtr := flag.Bool("version", false, "")
verbosePtr := flag.Bool("verbose", false, "")
prefetchPtr := flag.Uint("prefetch", 10, "")
lockTimeoutPtr := flag.Uint("lock-timeout", 15, "")
pathPtr := flag.String("path", "", "")
databasePtr := flag.String("database", "", "")
sourcePtr := flag.String("source", "", "")
flag.Usage = func() {
fmt.Fprint(os.Stderr,
`Usage: migrate OPTIONS COMMAND [arg...]
migrate [ -version | -help ]
Options:
-source Location of the migrations (driver://url)
-path Shorthand for -source=file://path
-database Run migrations against this database (driver://url)
-prefetch N Number of migrations to load in advance before executing (default 10)
-lock-timeout N Allow N seconds to acquire database lock (default 15)
-verbose Print verbose logging
-version Print version
-help Print usage
Commands:
goto V Migrate to version V
up [N] Apply all or N up migrations
down [N] Apply all or N down migrations
drop Drop everyting inside database
force V Set version V but don't run migration (ignores dirty state)
version Print current migration version
`)
}
flag.Parse()
// initialize logger
log.verbose = *verbosePtr
// show cli version
if *versionPtr {
fmt.Fprintln(os.Stderr, Version)
os.Exit(0)
}
// show help
if *helpPtr {
flag.Usage()
os.Exit(0)
}
// translate -path into -source if given
if *sourcePtr == "" && *pathPtr != "" {
*sourcePtr = fmt.Sprintf("file://%v", *pathPtr)
}
// initialize migrate
// don't catch migraterErr here and let each command decide
// how it wants to handle the error
migrater, migraterErr := migrate.New(*sourcePtr, *databasePtr)
defer func() {
if migraterErr == nil {
migrater.Close()
}
}()
if migraterErr == nil {
migrater.Log = log
migrater.PrefetchMigrations = *prefetchPtr
migrater.LockTimeout = time.Duration(int64(*lockTimeoutPtr)) * time.Second
// handle Ctrl+c
signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGINT)
go func() {
for range signals {
log.Println("Stopping after this running migration ...")
migrater.GracefulStop <- true
return
}
}()
}
startTime := time.Now()
switch flag.Arg(0) {
case "goto":
if migraterErr != nil {
log.fatalErr(migraterErr)
}
if flag.Arg(1) == "" {
log.fatal("error: please specify version argument V")
}
v, err := strconv.ParseUint(flag.Arg(1), 10, 64)
if err != nil {
log.fatal("error: can't read version argument V")
}
gotoCmd(migrater, uint(v))
if log.verbose {
log.Println("Finished after", time.Now().Sub(startTime))
}
case "up":
if migraterErr != nil {
log.fatalErr(migraterErr)
}
limit := -1
if flag.Arg(1) != "" {
n, err := strconv.ParseUint(flag.Arg(1), 10, 64)
if err != nil {
log.fatal("error: can't read limit argument N")
}
limit = int(n)
}
upCmd(migrater, limit)
if log.verbose {
log.Println("Finished after", time.Now().Sub(startTime))
}
case "down":
if migraterErr != nil {
log.fatalErr(migraterErr)
}
limit := -1
if flag.Arg(1) != "" {
n, err := strconv.ParseUint(flag.Arg(1), 10, 64)
if err != nil {
log.fatal("error: can't read limit argument N")
}
limit = int(n)
}
downCmd(migrater, limit)
if log.verbose {
log.Println("Finished after", time.Now().Sub(startTime))
}
case "drop":
if migraterErr != nil {
log.fatalErr(migraterErr)
}
dropCmd(migrater)
if log.verbose {
log.Println("Finished after", time.Now().Sub(startTime))
}
case "force":
if migraterErr != nil {
log.fatalErr(migraterErr)
}
if flag.Arg(1) == "" {
log.fatal("error: please specify version argument V")
}
v, err := strconv.ParseInt(flag.Arg(1), 10, 64)
if err != nil {
log.fatal("error: can't read version argument V")
}
if v < -1 {
log.fatal("error: argument V must be >= -1")
}
forceCmd(migrater, int(v))
if log.verbose {
log.Println("Finished after", time.Now().Sub(startTime))
}
case "version":
if migraterErr != nil {
log.fatalErr(migraterErr)
}
versionCmd(migrater)
default:
flag.Usage()
os.Exit(0)
}
}
+4
View File
@@ -0,0 +1,4 @@
package main
// Version is set in Makefile with build flags
var Version = "dev"
View File
View File
+110
View File
@@ -0,0 +1,110 @@
// Package database provides the Database interface.
// All database drivers must implement this interface, register themselves,
// optionally provide a `WithInstance` function and pass the tests
// in package database/testing.
package database
import (
"fmt"
"io"
nurl "net/url"
"sync"
)
var (
ErrLocked = fmt.Errorf("can't acquire lock")
)
const NilVersion int = -1
var driversMu sync.RWMutex
var drivers = make(map[string]Driver)
// Driver is the interface every database driver must implement.
//
// How to implement a database driver?
// 1. Implement this interface.
// 2. Optionally, add a function named `WithInstance`.
// This function should accept an existing DB instance and a Config{} struct
// and return a driver instance.
// 3. Add a test that calls database/testing.go:Test()
// 4. Add own tests for Open(), WithInstance() (when provided) and Close().
// All other functions are tested by tests in database/testing.
// Saves you some time and makes sure all database drivers behave the same way.
// 5. Call Register in init().
//
// Guidelines:
// * Don't try to correct user input. Don't assume things.
// When in doubt, return an error and explain the situation to the user.
// * All configuration input must come from the URL string in func Open()
// or the Config{} struct in WithInstance. Don't os.Getenv().
type Driver interface {
// Open returns a new driver instance configured with parameters
// coming from the URL string. Migrate will call this function
// only once per instance.
Open(url string) (Driver, error)
// Close closes the underlying database instance managed by the driver.
// Migrate will call this function only once per instance.
Close() error
// Lock should acquire a database lock so that only one migration process
// can run at a time. Migrate will call this function before Run is called.
// If the implementation can't provide this functionality, return nil.
// Return database.ErrLocked if database is already locked.
Lock() error
// Unlock should release the lock. Migrate will call this function after
// all migrations have been run.
Unlock() error
// Run applies a migration to the database. migration is garantueed to be not nil.
Run(migration io.Reader) error
// SetVersion saves version and dirty state.
// Migrate will call this function before and after each call to Run.
// version must be >= -1. -1 means NilVersion.
SetVersion(version int, dirty bool) error
// Version returns the currently active version and if the database is dirty.
// When no migration has been applied, it must return version -1.
// Dirty means, a previous migration failed and user interaction is required.
Version() (version int, dirty bool, err error)
// Drop deletes everyting in the database.
Drop() error
}
// Open returns a new driver instance.
func Open(url string) (Driver, error) {
u, err := nurl.Parse(url)
if err != nil {
return nil, err
}
if u.Scheme == "" {
return nil, fmt.Errorf("database driver: invalid URL scheme")
}
driversMu.RLock()
d, ok := drivers[u.Scheme]
driversMu.RUnlock()
if !ok {
return nil, fmt.Errorf("database driver: unknown driver %v (forgotton import?)", u.Scheme)
}
return d.Open(url)
}
// Register globally registers a driver.
func Register(name string, driver Driver) {
driversMu.Lock()
defer driversMu.Unlock()
if driver == nil {
panic("Register driver is nil")
}
if _, dup := drivers[name]; dup {
panic("Register called twice for driver " + name)
}
drivers[name] = driver
}
+8
View File
@@ -0,0 +1,8 @@
package database
func ExampleDriver() {
// see database/stub for an example
// database/stub/stub.go has the driver implementation
// database/stub/stub_test.go runs database/testing/test.go:Test
}
+27
View File
@@ -0,0 +1,27 @@
package database
import (
"fmt"
)
// Error should be used for errors involving queries ran against the database
type Error struct {
// Optional: the line number
Line uint
// Query is a query excerpt
Query []byte
// Err is a useful/helping error message for humans
Err string
// OrigErr is the underlying error
OrigErr error
}
func (e Error) Error() string {
if len(e.Err) == 0 {
return fmt.Sprintf("%v in line %v: %s", e.OrigErr, e.Line, e.Query)
}
return fmt.Sprintf("%v in line %v: %s (details: %v)", e.Err, e.Line, e.Query, e.OrigErr)
}
View File
+24
View File
@@ -0,0 +1,24 @@
# mysql
`mysql://user:password@tcp(host:port)/dbname?query`
| URL Query | WithInstance Config | Description |
|------------|---------------------|-------------|
| `x-migrations-table` | `MigrationsTable` | Name of the migrations table |
| `dbname` | `DatabaseName` | The name of the database to connect to |
| `user` | | The user to sign in as |
| `password` | | The user's password |
| `host` | | The host to connect to. |
| `port` | | The port to bind to. |
| `x-tls-ca` | | The location of the root certificate file. |
| `x-tls-cert` | | Cert file location. |
| `x-tls-key` | | Key file location. |
| `x-tls-insecure-skip-verify` | | Whether or not to use SSL (true\|false) |
## Upgrading from v1
1. Write down the current migration version from schema_migrations
1. `DROP TABLE schema_migrations`
2. Wrap your existing migrations in transactions ([BEGIN/COMMIT](https://dev.mysql.com/doc/refman/5.7/en/commit.html)) if you use multiple statements within one migration.
3. Download and install the latest migrate version.
4. Force the current migration version with `migrate force <current_version>`.
+327
View File
@@ -0,0 +1,327 @@
package mysql
import (
"crypto/tls"
"crypto/x509"
"database/sql"
"fmt"
"io"
"io/ioutil"
nurl "net/url"
"strconv"
"strings"
"github.com/go-sql-driver/mysql"
"github.com/mattes/migrate"
"github.com/mattes/migrate/database"
)
func init() {
database.Register("mysql", &Mysql{})
}
var DefaultMigrationsTable = "schema_migrations"
var (
ErrDatabaseDirty = fmt.Errorf("database is dirty")
ErrNilConfig = fmt.Errorf("no config")
ErrNoDatabaseName = fmt.Errorf("no database name")
ErrAppendPEM = fmt.Errorf("failed to append PEM")
)
type Config struct {
MigrationsTable string
DatabaseName string
}
type Mysql struct {
db *sql.DB
isLocked bool
config *Config
}
// instance must have `multiStatements` set to true
func WithInstance(instance *sql.DB, config *Config) (database.Driver, error) {
if config == nil {
return nil, ErrNilConfig
}
if err := instance.Ping(); err != nil {
return nil, err
}
query := `SELECT DATABASE()`
var databaseName sql.NullString
if err := instance.QueryRow(query).Scan(&databaseName); err != nil {
return nil, &database.Error{OrigErr: err, Query: []byte(query)}
}
if len(databaseName.String) == 0 {
return nil, ErrNoDatabaseName
}
config.DatabaseName = databaseName.String
if len(config.MigrationsTable) == 0 {
config.MigrationsTable = DefaultMigrationsTable
}
mx := &Mysql{
db: instance,
config: config,
}
if err := mx.ensureVersionTable(); err != nil {
return nil, err
}
return mx, nil
}
func (m *Mysql) Open(url string) (database.Driver, error) {
purl, err := nurl.Parse(url)
if err != nil {
return nil, err
}
q := purl.Query()
q.Set("multiStatements", "true")
purl.RawQuery = q.Encode()
db, err := sql.Open("mysql", strings.Replace(
migrate.FilterCustomQuery(purl).String(), "mysql://", "", 1))
if err != nil {
return nil, err
}
migrationsTable := purl.Query().Get("x-migrations-table")
if len(migrationsTable) == 0 {
migrationsTable = DefaultMigrationsTable
}
// use custom TLS?
ctls := purl.Query().Get("tls")
if len(ctls) > 0 {
if _, isBool := readBool(ctls); !isBool && strings.ToLower(ctls) != "skip-verify" {
rootCertPool := x509.NewCertPool()
pem, err := ioutil.ReadFile(purl.Query().Get("x-tls-ca"))
if err != nil {
return nil, err
}
if ok := rootCertPool.AppendCertsFromPEM(pem); !ok {
return nil, ErrAppendPEM
}
certs, err := tls.LoadX509KeyPair(purl.Query().Get("x-tls-cert"), purl.Query().Get("x-tls-key"))
if err != nil {
return nil, err
}
insecureSkipVerify := false
if len(purl.Query().Get("x-tls-insecure-skip-verify")) > 0 {
x, err := strconv.ParseBool(purl.Query().Get("x-tls-insecure-skip-verify"))
if err != nil {
return nil, err
}
insecureSkipVerify = x
}
mysql.RegisterTLSConfig(ctls, &tls.Config{
RootCAs: rootCertPool,
Certificates: []tls.Certificate{certs},
InsecureSkipVerify: insecureSkipVerify,
})
}
}
mx, err := WithInstance(db, &Config{
DatabaseName: purl.Path,
MigrationsTable: migrationsTable,
})
if err != nil {
return nil, err
}
return mx, nil
}
func (m *Mysql) Close() error {
return m.db.Close()
}
func (m *Mysql) Lock() error {
if m.isLocked {
return database.ErrLocked
}
aid, err := database.GenerateAdvisoryLockId(m.config.DatabaseName)
if err != nil {
return err
}
query := "SELECT GET_LOCK(?, 1)"
var success bool
if err := m.db.QueryRow(query, aid).Scan(&success); err != nil {
return &database.Error{OrigErr: err, Err: "try lock failed", Query: []byte(query)}
}
if success {
m.isLocked = true
return nil
}
return database.ErrLocked
}
func (m *Mysql) Unlock() error {
if !m.isLocked {
return nil
}
aid, err := database.GenerateAdvisoryLockId(m.config.DatabaseName)
if err != nil {
return err
}
query := `SELECT RELEASE_LOCK(?)`
if _, err := m.db.Exec(query, aid); err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
m.isLocked = false
return nil
}
func (m *Mysql) Run(migration io.Reader) error {
migr, err := ioutil.ReadAll(migration)
if err != nil {
return err
}
query := string(migr[:])
if _, err := m.db.Exec(query); err != nil {
return database.Error{OrigErr: err, Err: "migration failed", Query: migr}
}
return nil
}
func (m *Mysql) SetVersion(version int, dirty bool) error {
tx, err := m.db.Begin()
if err != nil {
return &database.Error{OrigErr: err, Err: "transaction start failed"}
}
query := "TRUNCATE `" + m.config.MigrationsTable + "`"
if _, err := m.db.Exec(query); err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
if version >= 0 {
query := "INSERT INTO `" + m.config.MigrationsTable + "` (version, dirty) VALUES (?, ?)"
if _, err := m.db.Exec(query, version, dirty); err != nil {
tx.Rollback()
return &database.Error{OrigErr: err, Query: []byte(query)}
}
}
if err := tx.Commit(); err != nil {
return &database.Error{OrigErr: err, Err: "transaction commit failed"}
}
return nil
}
func (m *Mysql) Version() (version int, dirty bool, err error) {
query := "SELECT version, dirty FROM `" + m.config.MigrationsTable + "` LIMIT 1"
err = m.db.QueryRow(query).Scan(&version, &dirty)
switch {
case err == sql.ErrNoRows:
return database.NilVersion, false, nil
case err != nil:
if e, ok := err.(*mysql.MySQLError); ok {
if e.Number == 0 {
return database.NilVersion, false, nil
}
}
return 0, false, &database.Error{OrigErr: err, Query: []byte(query)}
default:
return version, dirty, nil
}
}
func (m *Mysql) Drop() error {
// select all tables
query := `SHOW TABLES LIKE '%'`
tables, err := m.db.Query(query)
if err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
defer tables.Close()
// delete one table after another
tableNames := make([]string, 0)
for tables.Next() {
var tableName string
if err := tables.Scan(&tableName); err != nil {
return err
}
if len(tableName) > 0 {
tableNames = append(tableNames, tableName)
}
}
if len(tableNames) > 0 {
// delete one by one ...
for _, t := range tableNames {
query = "DROP TABLE IF EXISTS `" + t + "` CASCADE"
if _, err := m.db.Exec(query); err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
}
if err := m.ensureVersionTable(); err != nil {
return err
}
}
return nil
}
func (m *Mysql) ensureVersionTable() error {
// check if migration table exists
var result string
query := `SHOW TABLES LIKE "` + m.config.MigrationsTable + `"`
if err := m.db.QueryRow(query).Scan(&result); err != nil {
if err != sql.ErrNoRows {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
} else {
return nil
}
// if not, create the empty migration table
query = "CREATE TABLE `" + m.config.MigrationsTable + "` (version bigint not null primary key, dirty boolean not null)"
if _, err := m.db.Exec(query); err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
return nil
}
// Returns the bool value of the input.
// The 2nd return value indicates if the input was a valid bool value
// See https://github.com/go-sql-driver/mysql/blob/a059889267dc7170331388008528b3b44479bffb/utils.go#L71
func readBool(input string) (value bool, valid bool) {
switch input {
case "1", "true", "TRUE", "True":
return true, true
case "0", "false", "FALSE", "False":
return false, true
}
// Not a valid bool value
return
}
+60
View File
@@ -0,0 +1,60 @@
package mysql
import (
"database/sql"
sqldriver "database/sql/driver"
"fmt"
// "io/ioutil"
// "log"
"testing"
// "github.com/go-sql-driver/mysql"
dt "github.com/mattes/migrate/database/testing"
mt "github.com/mattes/migrate/testing"
)
var versions = []mt.Version{
{"mysql:8", []string{"MYSQL_ROOT_PASSWORD=root", "MYSQL_DATABASE=public"}},
{"mysql:5.7", []string{"MYSQL_ROOT_PASSWORD=root", "MYSQL_DATABASE=public"}},
{"mysql:5.6", []string{"MYSQL_ROOT_PASSWORD=root", "MYSQL_DATABASE=public"}},
{"mysql:5.5", []string{"MYSQL_ROOT_PASSWORD=root", "MYSQL_DATABASE=public"}},
}
func isReady(i mt.Instance) bool {
db, err := sql.Open("mysql", fmt.Sprintf("root:root@tcp(%v:%v)/public", i.Host(), i.Port()))
if err != nil {
return false
}
defer db.Close()
err = db.Ping()
if err == sqldriver.ErrBadConn {
return false
}
return true
}
func Test(t *testing.T) {
// mysql.SetLogger(mysql.Logger(log.New(ioutil.Discard, "", log.Ltime)))
mt.ParallelTest(t, versions, isReady,
func(t *testing.T, i mt.Instance) {
p := &Mysql{}
addr := fmt.Sprintf("mysql://root:root@tcp(%v:%v)/public", i.Host(), i.Port())
d, err := p.Open(addr)
if err != nil {
t.Fatalf("%v", err)
}
dt.Test(t, d, []byte("SELECT 1"))
// check ensureVersionTable
if err := d.(*Mysql).ensureVersionTable(); err != nil {
t.Fatal(err)
}
// check again
if err := d.(*Mysql).ensureVersionTable(); err != nil {
t.Fatal(err)
}
})
}
View File
+28
View File
@@ -0,0 +1,28 @@
# postgres
`postgres://user:password@host:port/dbname?query` (`postgresql://` works, too)
| URL Query | WithInstance Config | Description |
|------------|---------------------|-------------|
| `x-migrations-table` | `MigrationsTable` | Name of the migrations table |
| `dbname` | `DatabaseName` | The name of the database to connect to |
| `search_path` | | This variable specifies the order in which schemas are searched when an object is referenced by a simple name with no schema specified. |
| `user` | | The user to sign in as |
| `password` | | The user's password |
| `host` | | The host to connect to. Values that start with / are for unix domain sockets. (default is localhost) |
| `port` | | The port to bind to. (default is 5432) |
| `fallback_application_name` | | An application_name to fall back to if one isn't provided. |
| `connect_timeout` | | Maximum wait for connection, in seconds. Zero or not specified means wait indefinitely. |
| `sslcert` | | Cert file location. The file must contain PEM encoded data. |
| `sslkey` | | Key file location. The file must contain PEM encoded data. |
| `sslrootcert` | | The location of the root certificate file. The file must contain PEM encoded data. |
| `sslmode` | | Whether or not to use SSL (disable\|require\|verify-ca\|verify-full) |
## Upgrading from v1
1. Write down the current migration version from schema_migrations
1. `DROP TABLE schema_migrations`
2. Wrap your existing migrations in transactions ([BEGIN/COMMIT](https://www.postgresql.org/docs/current/static/transaction-iso.html)) if you use multiple statements within one migration.
3. Download and install the latest migrate version.
4. Force the current migration version with `migrate force <current_version>`.
@@ -0,0 +1 @@
DROP TABLE IF EXISTS users;
@@ -0,0 +1,5 @@
CREATE TABLE users (
user_id integer unique,
name varchar(40),
email varchar(40)
);
@@ -0,0 +1 @@
ALTER TABLE users DROP COLUMN IF EXISTS city;
@@ -0,0 +1,3 @@
ALTER TABLE users ADD COLUMN city varchar(100);
@@ -0,0 +1 @@
DROP INDEX IF EXISTS users_email_index;
@@ -0,0 +1,3 @@
CREATE UNIQUE INDEX CONCURRENTLY users_email_index ON users (email);
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
@@ -0,0 +1 @@
DROP TABLE IF EXISTS books;
@@ -0,0 +1,5 @@
CREATE TABLE books (
user_id integer,
name varchar(40),
author varchar(40)
);
@@ -0,0 +1 @@
DROP TABLE IF EXISTS movies;
@@ -0,0 +1,5 @@
CREATE TABLE movies (
user_id integer,
name varchar(40),
director varchar(40)
);
@@ -0,0 +1 @@
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
@@ -0,0 +1 @@
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
@@ -0,0 +1 @@
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
@@ -0,0 +1 @@
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
+273
View File
@@ -0,0 +1,273 @@
package postgres
import (
"database/sql"
"fmt"
"io"
"io/ioutil"
nurl "net/url"
"github.com/lib/pq"
"github.com/mattes/migrate"
"github.com/mattes/migrate/database"
)
func init() {
db := Postgres{}
database.Register("postgres", &db)
database.Register("postgresql", &db)
}
var DefaultMigrationsTable = "schema_migrations"
var (
ErrNilConfig = fmt.Errorf("no config")
ErrNoDatabaseName = fmt.Errorf("no database name")
ErrNoSchema = fmt.Errorf("no schema")
ErrDatabaseDirty = fmt.Errorf("database is dirty")
)
type Config struct {
MigrationsTable string
DatabaseName string
}
type Postgres struct {
db *sql.DB
isLocked bool
// Open and WithInstance need to garantuee that config is never nil
config *Config
}
func WithInstance(instance *sql.DB, config *Config) (database.Driver, error) {
if config == nil {
return nil, ErrNilConfig
}
if err := instance.Ping(); err != nil {
return nil, err
}
query := `SELECT CURRENT_DATABASE()`
var databaseName string
if err := instance.QueryRow(query).Scan(&databaseName); err != nil {
return nil, &database.Error{OrigErr: err, Query: []byte(query)}
}
if len(databaseName) == 0 {
return nil, ErrNoDatabaseName
}
config.DatabaseName = databaseName
if len(config.MigrationsTable) == 0 {
config.MigrationsTable = DefaultMigrationsTable
}
px := &Postgres{
db: instance,
config: config,
}
if err := px.ensureVersionTable(); err != nil {
return nil, err
}
return px, nil
}
func (p *Postgres) Open(url string) (database.Driver, error) {
purl, err := nurl.Parse(url)
if err != nil {
return nil, err
}
db, err := sql.Open("postgres", migrate.FilterCustomQuery(purl).String())
if err != nil {
return nil, err
}
migrationsTable := purl.Query().Get("x-migrations-table")
if len(migrationsTable) == 0 {
migrationsTable = DefaultMigrationsTable
}
px, err := WithInstance(db, &Config{
DatabaseName: purl.Path,
MigrationsTable: migrationsTable,
})
if err != nil {
return nil, err
}
return px, nil
}
func (p *Postgres) Close() error {
return p.db.Close()
}
// https://www.postgresql.org/docs/9.6/static/explicit-locking.html#ADVISORY-LOCKS
func (p *Postgres) Lock() error {
if p.isLocked {
return database.ErrLocked
}
aid, err := database.GenerateAdvisoryLockId(p.config.DatabaseName)
if err != nil {
return err
}
// This will either obtain the lock immediately and return true,
// or return false if the lock cannot be acquired immediately.
query := `SELECT pg_try_advisory_lock($1)`
var success bool
if err := p.db.QueryRow(query, aid).Scan(&success); err != nil {
return &database.Error{OrigErr: err, Err: "try lock failed", Query: []byte(query)}
}
if success {
p.isLocked = true
return nil
}
return database.ErrLocked
}
func (p *Postgres) Unlock() error {
if !p.isLocked {
return nil
}
aid, err := database.GenerateAdvisoryLockId(p.config.DatabaseName)
if err != nil {
return err
}
query := `SELECT pg_advisory_unlock($1)`
if _, err := p.db.Exec(query, aid); err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
p.isLocked = false
return nil
}
func (p *Postgres) Run(migration io.Reader) error {
migr, err := ioutil.ReadAll(migration)
if err != nil {
return err
}
// run migration
query := string(migr[:])
if _, err := p.db.Exec(query); err != nil {
// TODO: cast to postgress error and get line number
return database.Error{OrigErr: err, Err: "migration failed", Query: migr}
}
return nil
}
func (p *Postgres) SetVersion(version int, dirty bool) error {
tx, err := p.db.Begin()
if err != nil {
return &database.Error{OrigErr: err, Err: "transaction start failed"}
}
query := `TRUNCATE "` + p.config.MigrationsTable + `"`
if _, err := p.db.Exec(query); err != nil {
tx.Rollback()
return &database.Error{OrigErr: err, Query: []byte(query)}
}
if version >= 0 {
query = `INSERT INTO "` + p.config.MigrationsTable + `" (version, dirty) VALUES ($1, $2)`
if _, err := p.db.Exec(query, version, dirty); err != nil {
tx.Rollback()
return &database.Error{OrigErr: err, Query: []byte(query)}
}
}
if err := tx.Commit(); err != nil {
return &database.Error{OrigErr: err, Err: "transaction commit failed"}
}
return nil
}
func (p *Postgres) Version() (version int, dirty bool, err error) {
query := `SELECT version, dirty FROM "` + p.config.MigrationsTable + `" LIMIT 1`
err = p.db.QueryRow(query).Scan(&version, &dirty)
switch {
case err == sql.ErrNoRows:
return database.NilVersion, false, nil
case err != nil:
if e, ok := err.(*pq.Error); ok {
if e.Code.Name() == "undefined_table" {
return database.NilVersion, false, nil
}
}
return 0, false, &database.Error{OrigErr: err, Query: []byte(query)}
default:
return version, dirty, nil
}
}
func (p *Postgres) Drop() error {
// select all tables in current schema
query := `SELECT table_name FROM information_schema.tables WHERE table_schema=(SELECT current_schema())`
tables, err := p.db.Query(query)
if err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
defer tables.Close()
// delete one table after another
tableNames := make([]string, 0)
for tables.Next() {
var tableName string
if err := tables.Scan(&tableName); err != nil {
return err
}
if len(tableName) > 0 {
tableNames = append(tableNames, tableName)
}
}
if len(tableNames) > 0 {
// delete one by one ...
for _, t := range tableNames {
query = `DROP TABLE IF EXISTS ` + t + ` CASCADE`
if _, err := p.db.Exec(query); err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
}
if err := p.ensureVersionTable(); err != nil {
return err
}
}
return nil
}
func (p *Postgres) ensureVersionTable() error {
// check if migration table exists
var count int
query := `SELECT COUNT(1) FROM information_schema.tables WHERE table_name = $1 AND table_schema = (SELECT current_schema()) LIMIT 1`
if err := p.db.QueryRow(query, p.config.MigrationsTable).Scan(&count); err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
if count == 1 {
return nil
}
// if not, create the empty migration table
query = `CREATE TABLE "` + p.config.MigrationsTable + `" (version bigint not null primary key, dirty boolean not null)`
if _, err := p.db.Exec(query); err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
return nil
}
+150
View File
@@ -0,0 +1,150 @@
package postgres
// error codes https://github.com/lib/pq/blob/master/error.go
import (
"bytes"
"database/sql"
"fmt"
"io"
"testing"
"github.com/lib/pq"
dt "github.com/mattes/migrate/database/testing"
mt "github.com/mattes/migrate/testing"
)
var versions = []mt.Version{
{Image: "postgres:9.6"},
{Image: "postgres:9.5"},
{Image: "postgres:9.4"},
{Image: "postgres:9.3"},
{Image: "postgres:9.2"},
}
func isReady(i mt.Instance) bool {
db, err := sql.Open("postgres", fmt.Sprintf("postgres://postgres@%v:%v/postgres?sslmode=disable", i.Host(), i.Port()))
if err != nil {
return false
}
defer db.Close()
err = db.Ping()
if err == io.EOF {
return false
} else if e, ok := err.(*pq.Error); ok {
if e.Code.Name() == "cannot_connect_now" {
return false
}
}
return true
}
func Test(t *testing.T) {
mt.ParallelTest(t, versions, isReady,
func(t *testing.T, i mt.Instance) {
p := &Postgres{}
addr := fmt.Sprintf("postgres://postgres@%v:%v/postgres?sslmode=disable", i.Host(), i.Port())
d, err := p.Open(addr)
if err != nil {
t.Fatalf("%v", err)
}
dt.Test(t, d, []byte("SELECT 1"))
})
}
func TestMultiStatement(t *testing.T) {
mt.ParallelTest(t, versions, isReady,
func(t *testing.T, i mt.Instance) {
p := &Postgres{}
addr := fmt.Sprintf("postgres://postgres@%v:%v/postgres?sslmode=disable", i.Host(), i.Port())
d, err := p.Open(addr)
if err != nil {
t.Fatalf("%v", err)
}
if err := d.Run(bytes.NewReader([]byte("CREATE TABLE foo (foo text); CREATE TABLE bar (bar text);"))); err != nil {
t.Fatalf("expected err to be nil, got %v", err)
}
// make sure second table exists
var exists bool
if err := d.(*Postgres).db.QueryRow("SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'bar' AND table_schema = (SELECT current_schema()))").Scan(&exists); err != nil {
t.Fatal(err)
}
if !exists {
t.Fatalf("expected table bar to exist")
}
})
}
func TestFilterCustomQuery(t *testing.T) {
mt.ParallelTest(t, versions, isReady,
func(t *testing.T, i mt.Instance) {
p := &Postgres{}
addr := fmt.Sprintf("postgres://postgres@%v:%v/postgres?sslmode=disable&x-custom=foobar", i.Host(), i.Port())
_, err := p.Open(addr)
if err != nil {
t.Fatalf("%v", err)
}
})
}
func TestWithSchema(t *testing.T) {
mt.ParallelTest(t, versions, isReady,
func(t *testing.T, i mt.Instance) {
p := &Postgres{}
addr := fmt.Sprintf("postgres://postgres@%v:%v/postgres?sslmode=disable", i.Host(), i.Port())
d, err := p.Open(addr)
if err != nil {
t.Fatalf("%v", err)
}
// create foobar schema
if err := d.Run(bytes.NewReader([]byte("CREATE SCHEMA foobar AUTHORIZATION postgres"))); err != nil {
t.Fatal(err)
}
if err := d.SetVersion(1, false); err != nil {
t.Fatal(err)
}
// re-connect using that schema
d2, err := p.Open(fmt.Sprintf("postgres://postgres@%v:%v/postgres?sslmode=disable&search_path=foobar", i.Host(), i.Port()))
if err != nil {
t.Fatalf("%v", err)
}
version, _, err := d2.Version()
if err != nil {
t.Fatal(err)
}
if version != -1 {
t.Fatal("expected NilVersion")
}
// now update version and compare
if err := d2.SetVersion(2, false); err != nil {
t.Fatal(err)
}
version, _, err = d2.Version()
if err != nil {
t.Fatal(err)
}
if version != 2 {
t.Fatal("expected version 2")
}
// meanwhile, the public schema still has the other version
version, _, err = d.Version()
if err != nil {
t.Fatal(err)
}
if version != 1 {
t.Fatal("expected version 2")
}
})
}
func TestWithInstance(t *testing.T) {
}
View File
@@ -0,0 +1 @@
DROP TABLE IF EXISTS pets;
@@ -0,0 +1,3 @@
CREATE TABLE pets (
name string
);
@@ -0,0 +1 @@
DROP TABLE IF EXISTS pets;
@@ -0,0 +1 @@
ALTER TABLE pets ADD predator bool;;
+212
View File
@@ -0,0 +1,212 @@
package ql
import (
"database/sql"
"fmt"
"io"
"io/ioutil"
"strings"
nurl "net/url"
_ "github.com/cznic/ql/driver"
"github.com/mattes/migrate"
"github.com/mattes/migrate/database"
)
func init() {
database.Register("ql", &Ql{})
}
var DefaultMigrationsTable = "schema_migrations"
var (
ErrDatabaseDirty = fmt.Errorf("database is dirty")
ErrNilConfig = fmt.Errorf("no config")
ErrNoDatabaseName = fmt.Errorf("no database name")
ErrAppendPEM = fmt.Errorf("failed to append PEM")
)
type Config struct {
MigrationsTable string
DatabaseName string
}
type Ql struct {
db *sql.DB
isLocked bool
config *Config
}
func WithInstance(instance *sql.DB, config *Config) (database.Driver, error) {
if config == nil {
return nil, ErrNilConfig
}
if err := instance.Ping(); err != nil {
return nil, err
}
if len(config.MigrationsTable) == 0 {
config.MigrationsTable = DefaultMigrationsTable
}
mx := &Ql{
db: instance,
config: config,
}
if err := mx.ensureVersionTable(); err != nil {
return nil, err
}
return mx, nil
}
func (m *Ql) ensureVersionTable() error {
tx, err := m.db.Begin()
if err != nil {
return err
}
if _, err := tx.Exec(fmt.Sprintf(`
CREATE TABLE IF NOT EXISTS %s (version uint64,dirty bool);
CREATE UNIQUE INDEX IF NOT EXISTS version_unique ON %s (version);
`, m.config.MigrationsTable, m.config.MigrationsTable)); err != nil {
if err := tx.Rollback(); err != nil {
return err
}
return err
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
func (m *Ql) Open(url string) (database.Driver, error) {
purl, err := nurl.Parse(url)
if err != nil {
return nil, err
}
dbfile := strings.Replace(migrate.FilterCustomQuery(purl).String(), "ql://", "", 1)
db, err := sql.Open("ql", dbfile)
if err != nil {
return nil, err
}
migrationsTable := purl.Query().Get("x-migrations-table")
if len(migrationsTable) == 0 {
migrationsTable = DefaultMigrationsTable
}
mx, err := WithInstance(db, &Config{
DatabaseName: purl.Path,
MigrationsTable: migrationsTable,
})
if err != nil {
return nil, err
}
return mx, nil
}
func (m *Ql) Close() error {
return m.db.Close()
}
func (m *Ql) Drop() error {
query := `SELECT Name FROM __Table`
tables, err := m.db.Query(query)
if err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
defer tables.Close()
tableNames := make([]string, 0)
for tables.Next() {
var tableName string
if err := tables.Scan(&tableName); err != nil {
return err
}
if len(tableName) > 0 {
if strings.HasPrefix(tableName, "__") == false {
tableNames = append(tableNames, tableName)
}
}
}
if len(tableNames) > 0 {
for _, t := range tableNames {
query := "DROP TABLE " + t
err = m.executeQuery(query)
if err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
}
if err := m.ensureVersionTable(); err != nil {
return err
}
}
return nil
}
func (m *Ql) Lock() error {
if m.isLocked {
return database.ErrLocked
}
m.isLocked = true
return nil
}
func (m *Ql) Unlock() error {
if !m.isLocked {
return nil
}
m.isLocked = false
return nil
}
func (m *Ql) Run(migration io.Reader) error {
migr, err := ioutil.ReadAll(migration)
if err != nil {
return err
}
query := string(migr[:])
return m.executeQuery(query)
}
func (m *Ql) executeQuery(query string) error {
tx, err := m.db.Begin()
if err != nil {
return &database.Error{OrigErr: err, Err: "transaction start failed"}
}
if _, err := tx.Exec(query); err != nil {
tx.Rollback()
return &database.Error{OrigErr: err, Query: []byte(query)}
}
if err := tx.Commit(); err != nil {
return &database.Error{OrigErr: err, Err: "transaction commit failed"}
}
return nil
}
func (m *Ql) SetVersion(version int, dirty bool) error {
tx, err := m.db.Begin()
if err != nil {
return &database.Error{OrigErr: err, Err: "transaction start failed"}
}
query := "TRUNCATE TABLE " + m.config.MigrationsTable
if _, err := tx.Exec(query); err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
if version >= 0 {
query := fmt.Sprintf(`INSERT INTO %s (version, dirty) VALUES (%d, %t)`, m.config.MigrationsTable, version, dirty)
if _, err := tx.Exec(query); err != nil {
tx.Rollback()
return &database.Error{OrigErr: err, Query: []byte(query)}
}
}
if err := tx.Commit(); err != nil {
return &database.Error{OrigErr: err, Err: "transaction commit failed"}
}
return nil
}
func (m *Ql) Version() (version int, dirty bool, err error) {
query := "SELECT version, dirty FROM " + m.config.MigrationsTable + " LIMIT 1"
err = m.db.QueryRow(query).Scan(&version, &dirty)
if err != nil {
return database.NilVersion, false, nil
}
return version, dirty, nil
}
+62
View File
@@ -0,0 +1,62 @@
package ql
import (
"database/sql"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
_ "github.com/cznic/ql/driver"
"github.com/mattes/migrate"
dt "github.com/mattes/migrate/database/testing"
_ "github.com/mattes/migrate/source/file"
)
func Test(t *testing.T) {
dir, err := ioutil.TempDir("", "ql-driver-test")
if err != nil {
return
}
defer func() {
os.RemoveAll(dir)
}()
fmt.Printf("DB path : %s\n", filepath.Join(dir, "ql.db"))
p := &Ql{}
addr := fmt.Sprintf("ql://%s", filepath.Join(dir, "ql.db"))
d, err := p.Open(addr)
if err != nil {
t.Fatalf("%v", err)
}
db, err := sql.Open("ql", filepath.Join(dir, "ql.db"))
if err != nil {
return
}
defer func() {
if err := db.Close(); err != nil {
return
}
}()
dt.Test(t, d, []byte("CREATE TABLE t (Qty int, Name string);"))
driver, err := WithInstance(db, &Config{})
if err != nil {
t.Fatalf("%v", err)
}
if err := d.Drop(); err != nil {
t.Fatal(err)
}
m, err := migrate.NewWithDatabaseInstance(
"file://./migration",
"ql", driver)
if err != nil {
t.Fatalf("%v", err)
}
fmt.Println("UP")
err = m.Up()
if err != nil {
t.Fatalf("%v", err)
}
}
+6
View File
@@ -0,0 +1,6 @@
Redshift
===
This provides a Redshift driver for migrations. It is used whenever the URL of the database starts with `redshift://`.
Redshift is PostgreSQL compatible but has some specific features (or lack thereof) that require slightly different behavior.
+46
View File
@@ -0,0 +1,46 @@
package redshift
import (
"net/url"
"github.com/mattes/migrate/database"
"github.com/mattes/migrate/database/postgres"
)
// init registers the driver under the name 'redshift'
func init() {
db := new(Redshift)
db.Driver = new(postgres.Postgres)
database.Register("redshift", db)
}
// Redshift is a wrapper around the PostgreSQL driver which implements Redshift-specific behavior.
//
// Currently, the only different behaviour is the lack of locking in Redshift. The (Un)Lock() method(s) have been overridden from the PostgreSQL adapter to simply return nil.
type Redshift struct {
// The wrapped PostgreSQL driver.
database.Driver
}
// Open implements the database.Driver interface by parsing the URL, switching the scheme from "redshift" to "postgres", and delegating to the underlying PostgreSQL driver.
func (driver *Redshift) Open(dsn string) (database.Driver, error) {
parsed, err := url.Parse(dsn)
if err != nil {
return nil, err
}
parsed.Scheme = "postgres"
psql, err := driver.Driver.Open(parsed.String())
if err != nil {
return nil, err
}
return &Redshift{Driver: psql}, nil
}
// Lock implements the database.Driver interface by not locking and returning nil.
func (driver *Redshift) Lock() error { return nil }
// Unlock implements the database.Driver interface by not unlocking and returning nil.
func (driver *Redshift) Unlock() error { return nil }
View File
View File
+95
View File
@@ -0,0 +1,95 @@
package stub
import (
"io"
"io/ioutil"
"reflect"
"github.com/mattes/migrate/database"
)
func init() {
database.Register("stub", &Stub{})
}
type Stub struct {
Url string
Instance interface{}
CurrentVersion int
MigrationSequence []string
LastRunMigration []byte // todo: make []string
IsDirty bool
IsLocked bool
Config *Config
}
func (s *Stub) Open(url string) (database.Driver, error) {
return &Stub{
Url: url,
CurrentVersion: -1,
MigrationSequence: make([]string, 0),
Config: &Config{},
}, nil
}
type Config struct{}
func WithInstance(instance interface{}, config *Config) (database.Driver, error) {
return &Stub{
Instance: instance,
CurrentVersion: -1,
MigrationSequence: make([]string, 0),
Config: config,
}, nil
}
func (s *Stub) Close() error {
return nil
}
func (s *Stub) Lock() error {
if s.IsLocked {
return database.ErrLocked
}
s.IsLocked = true
return nil
}
func (s *Stub) Unlock() error {
s.IsLocked = false
return nil
}
func (s *Stub) Run(migration io.Reader) error {
m, err := ioutil.ReadAll(migration)
if err != nil {
return err
}
s.LastRunMigration = m
s.MigrationSequence = append(s.MigrationSequence, string(m[:]))
return nil
}
func (s *Stub) SetVersion(version int, state bool) error {
s.CurrentVersion = version
s.IsDirty = state
return nil
}
func (s *Stub) Version() (version int, dirty bool, err error) {
return s.CurrentVersion, s.IsDirty, nil
}
const DROP = "DROP"
func (s *Stub) Drop() error {
s.CurrentVersion = -1
s.LastRunMigration = nil
s.MigrationSequence = append(s.MigrationSequence, DROP)
return nil
}
func (s *Stub) EqualSequence(seq []string) bool {
return reflect.DeepEqual(seq, s.MigrationSequence)
}
+16
View File
@@ -0,0 +1,16 @@
package stub
import (
"testing"
dt "github.com/mattes/migrate/database/testing"
)
func Test(t *testing.T) {
s := &Stub{}
d, err := s.Open("")
if err != nil {
t.Fatal(err)
}
dt.Test(t, d, []byte("/* foobar migration */"))
}
+138
View File
@@ -0,0 +1,138 @@
// Package testing has the database tests.
// All database drivers must pass the Test function.
// This lives in it's own package so it stays a test dependency.
package testing
import (
"bytes"
"fmt"
"io"
"testing"
"time"
"github.com/mattes/migrate/database"
)
// Test runs tests against database implementations.
func Test(t *testing.T, d database.Driver, migration []byte) {
if migration == nil {
panic("test must provide migration reader")
}
TestNilVersion(t, d) // test first
TestLockAndUnlock(t, d)
TestRun(t, d, bytes.NewReader(migration))
TestDrop(t, d)
TestSetVersion(t, d) // also tests Version()
}
func TestNilVersion(t *testing.T, d database.Driver) {
v, _, err := d.Version()
if err != nil {
t.Fatal(err)
}
if v != database.NilVersion {
t.Fatalf("Version: expected version to be NilVersion (-1), got %v", v)
}
}
func TestLockAndUnlock(t *testing.T, d database.Driver) {
// add a timeout, in case there is a deadlock
done := make(chan bool, 1)
go func() {
timeout := time.After(15 * time.Second)
for {
select {
case <-done:
return
case <-timeout:
panic(fmt.Sprintf("Timeout after 15 seconds. Looks like a deadlock in Lock/UnLock.\n%#v", d))
}
}
}()
defer func() {
done <- true
}()
// run the locking test ...
if err := d.Lock(); err != nil {
t.Fatal(err)
}
// try to acquire lock again
if err := d.Lock(); err == nil {
t.Fatal("Lock: expected err not to be nil")
}
// unlock
if err := d.Unlock(); err != nil {
t.Fatal(err)
}
// try to lock
if err := d.Lock(); err != nil {
t.Fatal(err)
}
if err := d.Unlock(); err != nil {
t.Fatal(err)
}
}
func TestRun(t *testing.T, d database.Driver, migration io.Reader) {
if migration == nil {
panic("migration can't be nil")
}
if err := d.Run(migration); err != nil {
t.Fatal(err)
}
}
func TestDrop(t *testing.T, d database.Driver) {
if err := d.Drop(); err != nil {
t.Fatal(err)
}
}
func TestSetVersion(t *testing.T, d database.Driver) {
if err := d.SetVersion(1, true); err != nil {
t.Fatal(err)
}
// call again
if err := d.SetVersion(1, true); err != nil {
t.Fatal(err)
}
v, dirty, err := d.Version()
if err != nil {
t.Fatal(err)
}
if !dirty {
t.Fatal("expected dirty")
}
if v != 1 {
t.Fatal("expected version to be 1")
}
if err := d.SetVersion(2, false); err != nil {
t.Fatal(err)
}
// call again
if err := d.SetVersion(2, false); err != nil {
t.Fatal(err)
}
v, dirty, err = d.Version()
if err != nil {
t.Fatal(err)
}
if dirty {
t.Fatal("expected not dirty")
}
if v != 2 {
t.Fatal("expected version to be 2")
}
}
+15
View File
@@ -0,0 +1,15 @@
package database
import (
"fmt"
"hash/crc32"
)
const advisoryLockIdSalt uint = 1486364155
// inspired by rails migrations, see https://goo.gl/8o9bCT
func GenerateAdvisoryLockId(databaseName string) (string, error) {
sum := crc32.ChecksumIEEE([]byte(databaseName))
sum = sum * uint32(advisoryLockIdSalt)
return fmt.Sprintf("%v", sum), nil
}
+12
View File
@@ -0,0 +1,12 @@
package database
func TestGenerateAdvisoryLockId(t *testing.T) {
id, err := p.generateAdvisoryLockId("database_name")
if err != nil {
t.Errorf("expected err to be nil, got %v", err)
}
if len(id) == 0 {
t.Errorf("expected generated id not to be empty")
}
t.Logf("generated id: %v", id)
}
-38
View File
@@ -1,38 +0,0 @@
go: &go
image: golang
working_dir: /go/src/gopkg.in/mattes/migrate.v1
volumes:
- $GOPATH/src/github.com/mattes/migrate:/go/src/gopkg.in/mattes/migrate.v1
go-test:
<<: *go
command: sh -c 'go get -t -v ./... && go test -v ./...'
links:
- mssql
- postgres
- mysql
- cassandra
- crate
- mongo
go-build:
<<: *go
command: sh -c 'go get -v && go build -ldflags ''-s'' -o migrater'
environment:
CGO_ENABLED: 1
postgres:
image: postgres
mysql:
image: mysql
environment:
MYSQL_DATABASE: migratetest
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
cassandra:
image: cassandra:2.2
crate:
image: crate
mongo:
image: mongo:3.2.6
mssql:
image: microsoft/mssql-server-linux
environment:
SA_PASSWORD: Passw0rd
ACCEPT_EULA: "Y"
-11
View File
@@ -1,11 +0,0 @@
# Bash Driver
* Runs bash scripts. What you do in the scripts is up to you.
## Usage
```bash
migrate -url bash://xxx -path ./migrations create increment_xyz
migrate -url bash://xxx -path ./migrations up
migrate help # for more info
```
-36
View File
@@ -1,36 +0,0 @@
// Package bash implements the Driver interface.
package bash
import (
"gopkg.in/mattes/migrate.v1/driver"
"gopkg.in/mattes/migrate.v1/file"
)
type Driver struct {
}
func (driver *Driver) Initialize(url string) error {
return nil
}
func (driver *Driver) Close() error {
return nil
}
func (driver *Driver) FilenameExtension() string {
return "sh"
}
func (driver *Driver) Migrate(f file.File, pipe chan interface{}) {
defer close(pipe)
pipe <- f
return
}
func (driver *Driver) Version() (uint64, error) {
return uint64(0), nil
}
func init() {
driver.RegisterDriver("bash", &Driver{})
}
-7
View File
@@ -1,7 +0,0 @@
package bash
import "testing"
func TestMigrate(t *testing.T) {
}
-20
View File
@@ -1,20 +0,0 @@
# Cassandra Driver
## Usage
```bash
migrate -url cassandra://host:port/keyspace -path ./db/migrations create add_field_to_table
migrate -url cassandra://host:port/keyspace -path ./db/migrations up
migrate help # for more info
```
Url format
- Authentication: `cassandra://username:password@host:port/keyspace`
- Cassandra v3.x: `cassandra://host:port/keyspace?protocol=4`
## Authors
* Paul Bergeron, https://github.com/dinedal
* Johnny Bergström, https://github.com/balboah
* pateld982, http://github.com/pateld982
-199
View File
@@ -1,199 +0,0 @@
// Package cassandra implements the Driver interface.
package cassandra
import (
"fmt"
"net/url"
"strconv"
"strings"
"time"
"github.com/gocql/gocql"
"gopkg.in/mattes/migrate.v1/driver"
"gopkg.in/mattes/migrate.v1/file"
"gopkg.in/mattes/migrate.v1/migrate/direction"
)
// Driver implements migrate Driver interface
type Driver struct {
session *gocql.Session
}
const (
tableName = "schema_migrations"
)
// Initialize will be called first
func (driver *Driver) Initialize(rawurl string) error {
u, err := url.Parse(rawurl)
if err != nil {
return fmt.Errorf("failed to parse connectil url: %v", err)
}
if u.Path == "" {
return fmt.Errorf("no keyspace provided in connection url")
}
cluster := gocql.NewCluster(u.Host)
cluster.Keyspace = u.Path[1:len(u.Path)]
cluster.Consistency = gocql.All
cluster.Timeout = 1 * time.Minute
if len(u.Query().Get("consistency")) > 0 {
var consistency gocql.Consistency
consistency, err = parseConsistency(u.Query().Get("consistency"))
if err != nil {
return err
}
cluster.Consistency = consistency
}
if len(u.Query().Get("protocol")) > 0 {
var protoversion int
protoversion, err = strconv.Atoi(u.Query().Get("protocol"))
if err != nil {
return err
}
cluster.ProtoVersion = protoversion
}
// Check if url user struct is null
if u.User != nil {
password, passwordSet := u.User.Password()
if passwordSet == false {
return fmt.Errorf("Missing password. Please provide password")
}
cluster.Authenticator = gocql.PasswordAuthenticator{
Username: u.User.Username(),
Password: password,
}
}
driver.session, err = cluster.CreateSession()
if err != nil {
return err
}
if err := driver.ensureVersionTableExists(); err != nil {
return err
}
return nil
}
// Close last function to be called. Closes cassandra session
func (driver *Driver) Close() error {
driver.session.Close()
return nil
}
func (driver *Driver) ensureVersionTableExists() error {
err := driver.session.Query(fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (id uuid primary key, version bigint)", tableName)).Exec()
if err != nil {
return err
}
if _, err = driver.Version(); err != nil {
return err
}
return nil
}
// FilenameExtension return extension of migrations files
func (driver *Driver) FilenameExtension() string {
return "cql"
}
func (driver *Driver) updateVersion(version uint64, dir direction.Direction) error {
var ids []string
var id string
var err error
iter := driver.session.Query(fmt.Sprintf("SELECT id FROM %s WHERE version >= ? ALLOW FILTERING", tableName), version).Iter()
for iter.Scan(&id) {
ids = append(ids, id)
}
if len(ids) > 0 {
err = driver.session.Query(fmt.Sprintf("DELETE FROM %s WHERE id IN ?", tableName), ids).Exec()
if err != nil {
return err
}
}
if dir == direction.Up {
return driver.session.Query(fmt.Sprintf("INSERT INTO %s (id, version) VALUES (uuid(), ?)", tableName), version).Exec()
}
return nil
}
// Migrate run migration file. Restore previous version in case of fail
func (driver *Driver) Migrate(f file.File, pipe chan interface{}) {
var err error
previousVersion, err := driver.Version()
if err != nil {
close(pipe)
return
}
defer func() {
if err != nil {
// Invert version direction if we couldn't apply the changes for some reason.
if updErr := driver.updateVersion(previousVersion, direction.Up); updErr != nil {
pipe <- updErr
}
pipe <- err
}
close(pipe)
}()
pipe <- f
if err = driver.updateVersion(f.Version, f.Direction); err != nil {
return
}
if err = f.ReadContent(); err != nil {
return
}
for _, query := range strings.Split(string(f.Content), ";") {
query = strings.TrimSpace(query)
if len(query) == 0 {
continue
}
if err = driver.session.Query(query).Exec(); err != nil {
return
}
}
}
// Version return current version
func (driver *Driver) Version() (uint64, error) {
var version int64
err := driver.session.Query(fmt.Sprintf("SELECT max(version) FROM %s", tableName)).Scan(&version)
return uint64(version), err
}
func init() {
driver.RegisterDriver("cassandra", &Driver{})
}
// ParseConsistency wraps gocql.ParseConsistency to return an error
// instead of a panicing.
func parseConsistency(consistencyStr string) (consistency gocql.Consistency, err error) {
defer func() {
if r := recover(); r != nil {
var ok bool
err, ok = r.(error)
if !ok {
err = fmt.Errorf("Failed to parse consistency \"%s\": %v", consistencyStr, r)
}
}
}()
consistency = gocql.ParseConsistency(consistencyStr)
return consistency, nil
}
-154
View File
@@ -1,154 +0,0 @@
package cassandra
import (
"net/url"
"os"
"testing"
"time"
"github.com/gocql/gocql"
"gopkg.in/mattes/migrate.v1/file"
"gopkg.in/mattes/migrate.v1/migrate/direction"
pipep "gopkg.in/mattes/migrate.v1/pipe"
)
func TestMigrate(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
var session *gocql.Session
host := os.Getenv("CASSANDRA_PORT_9042_TCP_ADDR")
port := os.Getenv("CASSANDRA_PORT_9042_TCP_PORT")
driverURL := "cassandra://" + host + ":" + port + "/system"
// prepare a clean test database
u, err := url.Parse(driverURL)
if err != nil {
t.Fatal(err)
}
cluster := gocql.NewCluster(u.Host)
cluster.Keyspace = u.Path[1:len(u.Path)]
cluster.Consistency = gocql.All
cluster.Timeout = 1 * time.Minute
session, err = cluster.CreateSession()
if err != nil {
t.Fatal(err)
}
if err = session.Query(`DROP KEYSPACE IF EXISTS migrate;`).Exec(); err != nil {
t.Fatal(err)
}
if err = session.Query(`CREATE KEYSPACE IF NOT EXISTS migrate WITH REPLICATION = {'class': 'SimpleStrategy', 'replication_factor': 1};`).Exec(); err != nil {
t.Fatal(err)
}
cluster.Keyspace = "migrate"
session, err = cluster.CreateSession()
if err != nil {
t.Fatal(err)
}
driverURL = "cassandra://" + host + ":" + port + "/migrate"
d := &Driver{}
if err := d.Initialize(driverURL); err != nil {
t.Fatal(err)
}
files := []file.File{
{
Path: "/foobar",
FileName: "001_foobar.up.sql",
Version: 1,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
CREATE TABLE yolo (
id varint primary key,
msg text
);
`),
},
{
Path: "/foobar",
FileName: "002_foobar.down.sql",
Version: 1,
Name: "foobar",
Direction: direction.Down,
Content: []byte(`
DROP TABLE yolo;
`),
},
{
Path: "/foobar",
FileName: "002_foobar.up.sql",
Version: 2,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
CREATE TABLE error (
id THIS WILL CAUSE AN ERROR
)
`),
},
}
pipe := pipep.New()
go d.Migrate(files[0], pipe)
errs := pipep.ReadErrors(pipe)
if len(errs) > 0 {
t.Fatal(errs)
}
pipe = pipep.New()
go d.Migrate(files[1], pipe)
errs = pipep.ReadErrors(pipe)
if len(errs) > 0 {
t.Fatal(errs)
}
pipe = pipep.New()
go d.Migrate(files[2], pipe)
errs = pipep.ReadErrors(pipe)
if len(errs) == 0 {
t.Error("Expected test case to fail")
}
if err := d.Close(); err != nil {
t.Fatal(err)
}
}
func TestInitializeReturnsErrorsForBadUrls(t *testing.T) {
var session *gocql.Session
host := os.Getenv("CASSANDRA_PORT_9042_TCP_ADDR")
port := os.Getenv("CASSANDRA_PORT_9042_TCP_PORT")
cluster := gocql.NewCluster(host)
cluster.Consistency = gocql.All
cluster.Timeout = 1 * time.Minute
session, err := cluster.CreateSession()
if err != nil {
t.Fatal(err)
}
defer session.Close()
if err := session.Query(`CREATE KEYSPACE IF NOT EXISTS migrate WITH REPLICATION = {'class': 'SimpleStrategy', 'replication_factor': 1};`).Exec(); err != nil {
t.Fatal(err)
}
d := &Driver{}
invalidURL := "sdf://asdf://as?df?a"
if err := d.Initialize(invalidURL); err == nil {
t.Errorf("expected an error to be returned if url could not be parsed")
}
noKeyspace := "cassandra://" + host + ":" + port
if err := d.Initialize(noKeyspace); err == nil {
t.Errorf("expected an error to be returned if no keyspace provided")
}
}
-15
View File
@@ -1,15 +0,0 @@
# Crate driver
This is a driver for the [Crate](https://crate.io) database. It is based on the Crate
sql driver by [herenow](https://github.com/herenow/go-crate).
This driver does not use transactions! This is not a limitation of the driver, but a
limitation of Crate. So handle situations with failed migrations with care!
## Usage
```bash
migrate -url http://host:port -path ./db/migrations create add_field_to_table
migrate -url http://host:port -path ./db/migrations up
migrate help # for more info
```
-115
View File
@@ -1,115 +0,0 @@
// Package crate implements a driver for the Crate.io database
package crate
import (
"database/sql"
"fmt"
"strings"
_ "github.com/herenow/go-crate"
"gopkg.in/mattes/migrate.v1/driver"
"gopkg.in/mattes/migrate.v1/file"
"gopkg.in/mattes/migrate.v1/migrate/direction"
)
func init() {
driver.RegisterDriver("crate", &Driver{})
}
type Driver struct {
db *sql.DB
}
const tableName = "schema_migrations"
func (driver *Driver) Initialize(url string) error {
url = strings.Replace(url, "crate", "http", 1)
db, err := sql.Open("crate", url)
if err != nil {
return err
}
if err := db.Ping(); err != nil {
return err
}
driver.db = db
if err := driver.ensureVersionTableExists(); err != nil {
return err
}
return nil
}
func (driver *Driver) Close() error {
if err := driver.db.Close(); err != nil {
return err
}
return nil
}
func (driver *Driver) FilenameExtension() string {
return "sql"
}
func (driver *Driver) Version() (uint64, error) {
var version uint64
err := driver.db.QueryRow("SELECT version FROM " + tableName + " ORDER BY version DESC LIMIT 1").Scan(&version)
switch {
case err == sql.ErrNoRows:
return 0, nil
case err != nil:
return 0, err
default:
return version, nil
}
}
func (driver *Driver) Migrate(f file.File, pipe chan interface{}) {
defer close(pipe)
pipe <- f
if err := f.ReadContent(); err != nil {
pipe <- err
return
}
lines := splitContent(string(f.Content))
for _, line := range lines {
_, err := driver.db.Exec(line)
if err != nil {
pipe <- err
return
}
}
if f.Direction == direction.Up {
if _, err := driver.db.Exec("INSERT INTO "+tableName+" (version) VALUES (?)", f.Version); err != nil {
pipe <- err
return
}
} else if f.Direction == direction.Down {
if _, err := driver.db.Exec("DELETE FROM "+tableName+" WHERE version=?", f.Version); err != nil {
pipe <- err
return
}
}
}
func splitContent(content string) []string {
lines := strings.Split(content, ";")
resultLines := make([]string, 0, len(lines))
for i := range lines {
line := strings.Replace(lines[i], ";", "", -1)
line = strings.TrimSpace(line)
if line != "" {
resultLines = append(resultLines, line)
}
}
return resultLines
}
func (driver *Driver) ensureVersionTableExists() error {
if _, err := driver.db.Exec(fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (version INTEGER PRIMARY KEY)", tableName)); err != nil {
return err
}
return nil
}
-111
View File
@@ -1,111 +0,0 @@
package crate
import (
"fmt"
"os"
"testing"
"gopkg.in/mattes/migrate.v1/file"
"gopkg.in/mattes/migrate.v1/migrate/direction"
pipep "gopkg.in/mattes/migrate.v1/pipe"
)
func TestContentSplit(t *testing.T) {
content := `CREATE TABLE users (user_id STRING primary key, first_name STRING, last_name STRING, email STRING, password_hash STRING) CLUSTERED INTO 3 shards WITH (number_of_replicas = 0);
CREATE TABLE units (unit_id STRING primary key, name STRING, members array(string)) CLUSTERED INTO 3 shards WITH (number_of_replicas = 0);
CREATE TABLE available_connectors (technology_id STRING primary key, description STRING, icon STRING, link STRING, configuration_parameters array(object as (name STRING, type STRING))) CLUSTERED INTO 3 shards WITH (number_of_replicas = 0);
`
lines := splitContent(content)
if len(lines) != 3 {
t.Errorf("Expected 3 lines, but got %d", len(lines))
}
if lines[0] != "CREATE TABLE users (user_id STRING primary key, first_name STRING, last_name STRING, email STRING, password_hash STRING) CLUSTERED INTO 3 shards WITH (number_of_replicas = 0)" {
t.Error("Line does not match expected output")
}
if lines[1] != "CREATE TABLE units (unit_id STRING primary key, name STRING, members array(string)) CLUSTERED INTO 3 shards WITH (number_of_replicas = 0)" {
t.Error("Line does not match expected output")
}
if lines[2] != "CREATE TABLE available_connectors (technology_id STRING primary key, description STRING, icon STRING, link STRING, configuration_parameters array(object as (name STRING, type STRING))) CLUSTERED INTO 3 shards WITH (number_of_replicas = 0)" {
t.Error("Line does not match expected output")
}
}
func TestMigrate(t *testing.T) {
host := os.Getenv("CRATE_PORT_4200_TCP_ADDR")
port := os.Getenv("CRATE_PORT_4200_TCP_PORT")
url := fmt.Sprintf("crate://%s:%s", host, port)
driver := &Driver{}
if err := driver.Initialize(url); err != nil {
t.Fatal(err)
}
successFiles := []file.File{
{
Path: "/foobar",
FileName: "001_foobar.up.sql",
Version: 1,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
CREATE TABLE yolo (
id integer primary key,
msg string
);
`),
},
{
Path: "/foobar",
FileName: "002_foobar.down.sql",
Version: 1,
Name: "foobar",
Direction: direction.Down,
Content: []byte(`
DROP TABLE yolo;
`),
},
}
failFiles := []file.File{
{
Path: "/foobar",
FileName: "002_foobar.up.sql",
Version: 2,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
CREATE TABLE error (
id THIS WILL CAUSE AN ERROR
)
`),
},
}
for _, file := range successFiles {
pipe := pipep.New()
go driver.Migrate(file, pipe)
errs := pipep.ReadErrors(pipe)
if len(errs) > 0 {
t.Fatal(errs)
}
}
for _, file := range failFiles {
pipe := pipep.New()
go driver.Migrate(file, pipe)
errs := pipep.ReadErrors(pipe)
if len(errs) == 0 {
t.Fatal("Migration should have failed but succeeded")
}
}
if err := driver.Close(); err != nil {
t.Fatal(err)
}
}
-82
View File
@@ -1,82 +0,0 @@
// Package driver holds the driver interface.
package driver
import (
"fmt"
neturl "net/url" // alias to allow `url string` func signature in New
"gopkg.in/mattes/migrate.v1/file"
)
// Driver is the interface type that needs to implemented by all drivers.
type Driver interface {
// Initialize is the first function to be called.
// Check the url string and open and verify any connection
// that has to be made.
Initialize(url string) error
// Close is the last function to be called.
// Close any open connection here.
Close() error
// FilenameExtension returns the extension of the migration files.
// The returned string must not begin with a dot.
FilenameExtension() string
// Migrate is the heart of the driver.
// It will receive a file which the driver should apply
// to its backend or whatever. The migration function should use
// the pipe channel to return any errors or other useful information.
Migrate(file file.File, pipe chan interface{})
// Version returns the current migration version.
Version() (uint64, error)
}
// New returns Driver and calls Initialize on it
func New(url string) (Driver, error) {
u, err := neturl.Parse(url)
if err != nil {
return nil, err
}
d := GetDriver(u.Scheme)
if d == nil {
return nil, fmt.Errorf("Driver '%s' not found", u.Scheme)
}
verifyFilenameExtension(u.Scheme, d)
if err := d.Initialize(url); err != nil {
return nil, err
}
return d, nil
}
// FilenameExtensionFromURL return extension for migration files. Used for create migrations
func FilenameExtensionFromURL(url string) (string, error) {
u, err := neturl.Parse(url)
if err != nil {
return "", err
}
d := GetDriver(u.Scheme)
if d == nil {
return "", fmt.Errorf("Driver '%s' not found", u.Scheme)
}
verifyFilenameExtension(u.Scheme, d)
return d.FilenameExtension(), nil
}
// verifyFilenameExtension panics if the driver's filename extension
// is not correct or empty.
func verifyFilenameExtension(driverName string, d Driver) {
f := d.FilenameExtension()
if f == "" {
panic(fmt.Sprintf("%s.FilenameExtension() returns empty string.", driverName))
}
if f[0:1] == "." {
panic(fmt.Sprintf("%s.FilenameExtension() returned string must not start with a dot.", driverName))
}
}
-104
View File
@@ -1,104 +0,0 @@
# MongoDB Driver
* Runs pre-registered Golang methods that receive a single `*mgo.Session` parameter and return `error` on failure.
* Stores migration version details in collection ``db_migrations``.
This collection will be auto-generated.
* Migrations do not run in transactions, there are no built-in transactions in MongoDB.
That means that if a migration fails, it will not be rolled back.
* There is no out-of-the-box support for command-line interface via terminal.
## Usage in Go
```go
import "gopkg.in/mattes/migrate.v1/migrate"
// Import your migration methods package so that they are registered and available for the MongoDB driver.
// There is no need to import the MongoDB driver explicitly, as it should already be imported by your migration methods package.
import _ "my_mongo_db_migrator"
// use synchronous versions of migration functions ...
allErrors, ok := migrate.UpSync("mongodb://host:port", "./path")
if !ok {
fmt.Println("Oh no ...")
// do sth with allErrors slice
}
// use the asynchronous version of migration functions ...
pipe := migrate.NewPipe()
go migrate.Up(pipe, "mongodb://host:port", "./path")
// pipe is basically just a channel
// write your own channel listener. see writePipe() in main.go as an example.
```
## Migration files format
The migration files should have an ".mgo" extension and contain a list of registered methods names.
Migration methods should satisfy the following:
* They should be exported (their name should start with a capital letter)
* Their type should be `func (*mgo.Session) error`
Recommended (but not required) naming conventions for migration methods:
* Prefix with V<version> : for example V001 for version 1.
* Suffix with "_up" or "_down" for up and down migrations correspondingly.
001_first_release.up.mgo
```
V001_some_migration_operation_up
V001_some_other_operation_up
...
```
001_first_release.down.mgo
```
V001_some_other_operation_down
V001_some_migration_operation_down
...
```
## Methods registration
For a detailed example see: [sample_mongodb_migrator.go](https://github.com/mattes/migrate/blob/v1/driver/mongodb/example/sample_mongdb_migrator.go)
```go
package my_mongo_db_migrator
import (
"gopkg.in/mattes/migrate.v1/driver/mongodb"
"gopkg.in/mattes/migrate.v1/driver/mongodb/gomethods"
"gopkg.in/mgo.v2"
)
// common boilerplate
type MyMongoDbMigrator struct {
}
func (r *MyMongoDbMigrator) DbName() string {
return "<target_db_name_for_migration>"
}
var _ mongodb.MethodsReceiver = (*MyMongoDbMigrator)(nil)
func init() {
gomethods.RegisterMethodsReceiverForDriver("mongodb", &MyMongoDbMigrator{})
}
// Here goes the application-specific migration logic
func (r *MyMongoDbMigrator) V001_some_migration_operation_up(session *mgo.Session) error {
// do something
return nil
}
func (r *MyMongoDbMigrator) V001_some_migration_operation_down(session *mgo.Session) error {
// revert some_migration_operation_up from above
return nil
}
```
## Authors
* Demitry Gershovich, https://github.com/dimag-jfrog
-311
View File
@@ -1,311 +0,0 @@
package example
import (
"testing"
"gopkg.in/mattes/migrate.v1/file"
"gopkg.in/mattes/migrate.v1/migrate/direction"
"os"
"reflect"
"time"
"gopkg.in/mattes/migrate.v1/driver"
"gopkg.in/mattes/migrate.v1/driver/mongodb"
"gopkg.in/mattes/migrate.v1/driver/mongodb/gomethods"
pipep "gopkg.in/mattes/migrate.v1/pipe"
)
type ExpectedMigrationResult struct {
Organizations []Organization
Organizations_v2 []Organization_v2
Users []User
Errors []error
}
func RunMigrationAndAssertResult(
t *testing.T,
title string,
d *mongodb.Driver,
file file.File,
expected *ExpectedMigrationResult) {
actualOrganizations := []Organization{}
actualOrganizations_v2 := []Organization_v2{}
actualUsers := []User{}
var err error
var pipe chan interface{}
var errs []error
pipe = pipep.New()
go d.Migrate(file, pipe)
errs = pipep.ReadErrors(pipe)
session := d.Session
if len(expected.Organizations) > 0 {
err = session.DB(DB_NAME).C(ORGANIZATIONS_C).Find(nil).All(&actualOrganizations)
} else {
err = session.DB(DB_NAME).C(ORGANIZATIONS_C).Find(nil).All(&actualOrganizations_v2)
}
if err != nil {
t.Fatal("Failed to query Organizations collection")
}
err = session.DB(DB_NAME).C(USERS_C).Find(nil).All(&actualUsers)
if err != nil {
t.Fatal("Failed to query Users collection")
}
if !reflect.DeepEqual(expected.Errors, errs) {
t.Fatalf("Migration '%s': FAILED\nexpected errors %v\nbut got %v", title, expected.Errors, errs)
}
if !reflect.DeepEqual(expected.Organizations, actualOrganizations) {
t.Fatalf("Migration '%s': FAILED\nexpected organizations %v\nbut got %v", title, expected.Organizations, actualOrganizations)
}
if !reflect.DeepEqual(expected.Organizations_v2, actualOrganizations_v2) {
t.Fatalf("Migration '%s': FAILED\nexpected organizations v2 %v\nbut got %v", title, expected.Organizations_v2, actualOrganizations_v2)
}
if !reflect.DeepEqual(expected.Users, actualUsers) {
t.Fatalf("Migration '%s': FAILED\nexpected users %v\nbut got %v", title, expected.Users, actualUsers)
}
// t.Logf("Migration '%s': PASSED", title)
}
func TestMigrate(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Fatalf("Test failed on panic: %v", r)
}
}()
host := os.Getenv("MONGO_PORT_27017_TCP_ADDR")
port := os.Getenv("MONGO_PORT_27017_TCP_PORT")
driverUrl := "mongodb://" + host + ":" + port
d0 := driver.GetDriver("mongodb")
d, ok := d0.(*mongodb.Driver)
if !ok {
t.Fatal("MongoDbGoMethodsDriver has not registered")
}
if err := d.Initialize(driverUrl); err != nil {
t.Fatal(err)
}
// Reset DB
d.Session.DB(DB_NAME).C(mongodb.MIGRATE_C).DropCollection()
d.Session.DB(DB_NAME).C(ORGANIZATIONS_C).DropCollection()
d.Session.DB(DB_NAME).C(USERS_C).DropCollection()
date1, _ := time.Parse(SHORT_DATE_LAYOUT, "1994-Jul-05")
date2, _ := time.Parse(SHORT_DATE_LAYOUT, "1998-Sep-04")
date3, _ := time.Parse(SHORT_DATE_LAYOUT, "2008-Apr-28")
migrations := []struct {
name string
file file.File
expectedResult ExpectedMigrationResult
}{
{
name: "v0 -> v1",
file: file.File{
Path: "/foobar",
FileName: "001_foobar.up.gm",
Version: 1,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
V001_init_organizations_up
V001_init_users_up
`),
},
expectedResult: ExpectedMigrationResult{
Organizations: []Organization{
{Id: OrganizationIds[0], Name: "Amazon", Location: "Seattle", DateFounded: date1},
{Id: OrganizationIds[1], Name: "Google", Location: "Mountain View", DateFounded: date2},
{Id: OrganizationIds[2], Name: "JFrog", Location: "Santa Clara", DateFounded: date3},
},
Organizations_v2: []Organization_v2{},
Users: []User{
{Id: UserIds[0], Name: "Alex"},
{Id: UserIds[1], Name: "Beatrice"},
{Id: UserIds[2], Name: "Cleo"},
},
Errors: []error{},
},
},
{
name: "v1 -> v2",
file: file.File{
Path: "/foobar",
FileName: "002_foobar.up.gm",
Version: 2,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
V002_organizations_rename_location_field_to_headquarters_up
V002_change_user_cleo_to_cleopatra_up
`),
},
expectedResult: ExpectedMigrationResult{
Organizations: []Organization{},
Organizations_v2: []Organization_v2{
{Id: OrganizationIds[0], Name: "Amazon", Headquarters: "Seattle", DateFounded: date1},
{Id: OrganizationIds[1], Name: "Google", Headquarters: "Mountain View", DateFounded: date2},
{Id: OrganizationIds[2], Name: "JFrog", Headquarters: "Santa Clara", DateFounded: date3},
},
Users: []User{
{Id: UserIds[0], Name: "Alex"},
{Id: UserIds[1], Name: "Beatrice"},
{Id: UserIds[2], Name: "Cleopatra"},
},
Errors: []error{},
},
},
{
name: "v2 -> v1",
file: file.File{
Path: "/foobar",
FileName: "002_foobar.down.gm",
Version: 2,
Name: "foobar",
Direction: direction.Down,
Content: []byte(`
V002_change_user_cleo_to_cleopatra_down
V002_organizations_rename_location_field_to_headquarters_down
`),
},
expectedResult: ExpectedMigrationResult{
Organizations: []Organization{
{Id: OrganizationIds[0], Name: "Amazon", Location: "Seattle", DateFounded: date1},
{Id: OrganizationIds[1], Name: "Google", Location: "Mountain View", DateFounded: date2},
{Id: OrganizationIds[2], Name: "JFrog", Location: "Santa Clara", DateFounded: date3},
},
Organizations_v2: []Organization_v2{},
Users: []User{
{Id: UserIds[0], Name: "Alex"},
{Id: UserIds[1], Name: "Beatrice"},
{Id: UserIds[2], Name: "Cleo"},
},
Errors: []error{},
},
},
{
name: "v1 -> v0",
file: file.File{
Path: "/foobar",
FileName: "001_foobar.down.gm",
Version: 1,
Name: "foobar",
Direction: direction.Down,
Content: []byte(`
V001_init_users_down
V001_init_organizations_down
`),
},
expectedResult: ExpectedMigrationResult{
Organizations: []Organization{},
Organizations_v2: []Organization_v2{},
Users: []User{},
Errors: []error{},
},
},
{
name: "v0 -> v1: missing method aborts migration",
file: file.File{
Path: "/foobar",
FileName: "001_foobar.up.gm",
Version: 1,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
V001_init_organizations_up
V001_init_users_up
v001_non_existing_method_up
`),
},
expectedResult: ExpectedMigrationResult{
Organizations: []Organization{},
Organizations_v2: []Organization_v2{},
Users: []User{},
Errors: []error{gomethods.MethodNotFoundError("v001_non_existing_method_up")},
},
},
{
name: "v0 -> v1: not exported method aborts migration",
file: file.File{
Path: "/foobar",
FileName: "001_foobar.up.gm",
Version: 1,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
V001_init_organizations_up
v001_not_exported_method_up
V001_init_users_up
`),
},
expectedResult: ExpectedMigrationResult{
Organizations: []Organization{},
Organizations_v2: []Organization_v2{},
Users: []User{},
Errors: []error{gomethods.MethodNotFoundError("v001_not_exported_method_up")},
},
},
{
name: "v0 -> v1: wrong signature method aborts migration",
file: file.File{
Path: "/foobar",
FileName: "001_foobar.up.gm",
Version: 1,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
V001_init_organizations_up
V001_method_with_wrong_signature_up
V001_init_users_up
`),
},
expectedResult: ExpectedMigrationResult{
Organizations: []Organization{},
Organizations_v2: []Organization_v2{},
Users: []User{},
Errors: []error{gomethods.WrongMethodSignatureError("V001_method_with_wrong_signature_up")},
},
},
{
name: "v1 -> v0: wrong signature method aborts migration",
file: file.File{
Path: "/foobar",
FileName: "001_foobar.down.gm",
Version: 1,
Name: "foobar",
Direction: direction.Down,
Content: []byte(`
V001_init_users_down
V001_method_with_wrong_signature_down
V001_init_organizations_down
`),
},
expectedResult: ExpectedMigrationResult{
Organizations: []Organization{},
Organizations_v2: []Organization_v2{},
Users: []User{},
Errors: []error{gomethods.WrongMethodSignatureError("V001_method_with_wrong_signature_down")},
},
},
}
for _, m := range migrations {
RunMigrationAndAssertResult(t, m.name, d, m.file, &m.expectedResult)
}
if err := d.Close(); err != nil {
t.Fatal(err)
}
}
@@ -1,154 +0,0 @@
package example
import (
"time"
"gopkg.in/mattes/migrate.v1/driver/mongodb/gomethods"
_ "gopkg.in/mattes/migrate.v1/driver/mongodb/gomethods"
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
"gopkg.in/mattes/migrate.v1/driver/mongodb"
)
type SampleMongoDbMigrator struct {
}
func (r *SampleMongoDbMigrator) DbName() string {
return DB_NAME
}
var _ mongodb.MethodsReceiver = (*SampleMongoDbMigrator)(nil)
func init() {
gomethods.RegisterMethodsReceiverForDriver("mongodb", &SampleMongoDbMigrator{})
}
// Here goes the specific mongodb golang methods migration logic
const (
DB_NAME = "test"
SHORT_DATE_LAYOUT = "2000-Jan-01"
USERS_C = "users"
ORGANIZATIONS_C = "organizations"
)
type Organization struct {
Id bson.ObjectId `bson:"_id,omitempty"`
Name string `bson:"name"`
Location string `bson:"location"`
DateFounded time.Time `bson:"date_founded"`
}
type Organization_v2 struct {
Id bson.ObjectId `bson:"_id,omitempty"`
Name string `bson:"name"`
Headquarters string `bson:"headquarters"`
DateFounded time.Time `bson:"date_founded"`
}
type User struct {
Id bson.ObjectId `bson:"_id"`
Name string `bson:"name"`
}
var OrganizationIds []bson.ObjectId = []bson.ObjectId{
bson.NewObjectId(),
bson.NewObjectId(),
bson.NewObjectId(),
}
var UserIds []bson.ObjectId = []bson.ObjectId{
bson.NewObjectId(),
bson.NewObjectId(),
bson.NewObjectId(),
}
func (r *SampleMongoDbMigrator) V001_init_organizations_up(session *mgo.Session) error {
date1, _ := time.Parse(SHORT_DATE_LAYOUT, "1994-Jul-05")
date2, _ := time.Parse(SHORT_DATE_LAYOUT, "1998-Sep-04")
date3, _ := time.Parse(SHORT_DATE_LAYOUT, "2008-Apr-28")
orgs := []Organization{
{Id: OrganizationIds[0], Name: "Amazon", Location: "Seattle", DateFounded: date1},
{Id: OrganizationIds[1], Name: "Google", Location: "Mountain View", DateFounded: date2},
{Id: OrganizationIds[2], Name: "JFrog", Location: "Santa Clara", DateFounded: date3},
}
for _, org := range orgs {
err := session.DB(DB_NAME).C(ORGANIZATIONS_C).Insert(org)
if err != nil {
return err
}
}
return nil
}
func (r *SampleMongoDbMigrator) V001_init_organizations_down(session *mgo.Session) error {
return session.DB(DB_NAME).C(ORGANIZATIONS_C).DropCollection()
}
func (r *SampleMongoDbMigrator) V001_init_users_up(session *mgo.Session) error {
users := []User{
{Id: UserIds[0], Name: "Alex"},
{Id: UserIds[1], Name: "Beatrice"},
{Id: UserIds[2], Name: "Cleo"},
}
for _, user := range users {
err := session.DB(DB_NAME).C(USERS_C).Insert(user)
if err != nil {
return err
}
}
return nil
}
func (r *SampleMongoDbMigrator) V001_init_users_down(session *mgo.Session) error {
return session.DB(DB_NAME).C(USERS_C).DropCollection()
}
func (r *SampleMongoDbMigrator) V002_organizations_rename_location_field_to_headquarters_up(session *mgo.Session) error {
c := session.DB(DB_NAME).C(ORGANIZATIONS_C)
_, err := c.UpdateAll(nil, bson.M{"$rename": bson.M{"location": "headquarters"}})
return err
}
func (r *SampleMongoDbMigrator) V002_organizations_rename_location_field_to_headquarters_down(session *mgo.Session) error {
c := session.DB(DB_NAME).C(ORGANIZATIONS_C)
_, err := c.UpdateAll(nil, bson.M{"$rename": bson.M{"headquarters": "location"}})
return err
}
func (r *SampleMongoDbMigrator) V002_change_user_cleo_to_cleopatra_up(session *mgo.Session) error {
c := session.DB(DB_NAME).C(USERS_C)
colQuerier := bson.M{"name": "Cleo"}
change := bson.M{"$set": bson.M{"name": "Cleopatra"}}
return c.Update(colQuerier, change)
}
func (r *SampleMongoDbMigrator) V002_change_user_cleo_to_cleopatra_down(session *mgo.Session) error {
c := session.DB(DB_NAME).C(USERS_C)
colQuerier := bson.M{"name": "Cleopatra"}
change := bson.M{"$set": bson.M{"name": "Cleo"}}
return c.Update(colQuerier, change)
}
// Wrong signature methods for testing
func (r *SampleMongoDbMigrator) v001_not_exported_method_up(session *mgo.Session) error {
return nil
}
func (r *SampleMongoDbMigrator) V001_method_with_wrong_signature_up(s string) error {
return nil
}
func (r *SampleMongoDbMigrator) V001_method_with_wrong_signature_down(session *mgo.Session) (bool, error) {
return true, nil
}
@@ -1,150 +0,0 @@
package gomethods
import (
"bufio"
"fmt"
"os"
"path"
"strings"
"gopkg.in/mattes/migrate.v1/driver"
"gopkg.in/mattes/migrate.v1/file"
)
type MethodNotFoundError string
func (e MethodNotFoundError) Error() string {
return fmt.Sprintf("Method '%s' was not found. It is either not existing or has not been exported (starts with lowercase).", string(e))
}
type WrongMethodSignatureError string
func (e WrongMethodSignatureError) Error() string {
return fmt.Sprintf("Method '%s' has wrong signature", string(e))
}
type MethodInvocationFailedError struct {
MethodName string
Err error
}
func (e *MethodInvocationFailedError) Error() string {
return fmt.Sprintf("Method '%s' returned an error: %v", e.MethodName, e.Err)
}
type MigrationMethodInvoker interface {
Validate(methodName string) error
Invoke(methodName string) error
}
type GoMethodsDriver interface {
driver.Driver
MigrationMethodInvoker
MethodsReceiver() interface{}
SetMethodsReceiver(r interface{}) error
}
type Migrator struct {
RollbackOnFailure bool
MethodInvoker MigrationMethodInvoker
}
func (m *Migrator) Migrate(f file.File, pipe chan interface{}) error {
methods, err := m.getMigrationMethods(f)
if err != nil {
pipe <- err
return err
}
for i, methodName := range methods {
pipe <- methodName
err := m.MethodInvoker.Invoke(methodName)
if err != nil {
pipe <- err
if !m.RollbackOnFailure {
return err
}
// on failure, try to rollback methods in this migration
for j := i - 1; j >= 0; j-- {
rollbackToMethodName := getRollbackToMethod(methods[j])
if rollbackToMethodName == "" {
continue
}
if err := m.MethodInvoker.Validate(rollbackToMethodName); err != nil {
continue
}
pipe <- rollbackToMethodName
err = m.MethodInvoker.Invoke(rollbackToMethodName)
if err != nil {
pipe <- err
break
}
}
return err
}
}
return nil
}
func getRollbackToMethod(methodName string) string {
if strings.HasSuffix(methodName, "_up") {
return strings.TrimSuffix(methodName, "_up") + "_down"
} else if strings.HasSuffix(methodName, "_down") {
return strings.TrimSuffix(methodName, "_down") + "_up"
} else {
return ""
}
}
func getFileLines(file file.File) ([]string, error) {
if len(file.Content) == 0 {
lines := make([]string, 0)
file, err := os.Open(path.Join(file.Path, file.FileName))
if err != nil {
return nil, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
return lines, nil
} else {
s := string(file.Content)
return strings.Split(s, "\n"), nil
}
}
func (m *Migrator) getMigrationMethods(f file.File) (methods []string, err error) {
var lines []string
lines, err = getFileLines(f)
if err != nil {
return nil, err
}
for _, line := range lines {
line := strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "--") {
// an empty line or a comment, ignore
continue
}
methodName := line
if err := m.MethodInvoker.Validate(methodName); err != nil {
return nil, err
}
methods = append(methods, methodName)
}
return methods, nil
}
@@ -1,247 +0,0 @@
package gomethods
import (
"reflect"
"testing"
"gopkg.in/mattes/migrate.v1/file"
"gopkg.in/mattes/migrate.v1/migrate/direction"
pipep "gopkg.in/mattes/migrate.v1/pipe"
)
type FakeGoMethodsInvoker struct {
InvokedMethods []string
}
func (invoker *FakeGoMethodsInvoker) Validate(methodName string) error {
if methodName == "V001_some_non_existing_method_up" {
return MethodNotFoundError(methodName)
}
return nil
}
func (invoker *FakeGoMethodsInvoker) Invoke(methodName string) error {
invoker.InvokedMethods = append(invoker.InvokedMethods, methodName)
if methodName == "V001_some_failing_method_up" || methodName == "V001_some_failing_method_down" {
return &MethodInvocationFailedError{
MethodName: methodName,
Err: SomeError{},
}
}
return nil
}
type SomeError struct{}
func (e SomeError) Error() string { return "Some error happened" }
func TestMigrate(t *testing.T) {
cases := []struct {
name string
file file.File
expectedInvokedMethods []string
expectedErrors []error
expectRollback bool
}{
{
name: "up migration invokes up methods",
file: file.File{
Path: "/foobar",
FileName: "001_foobar.up.gm",
Version: 1,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
V001_init_organizations_up
V001_init_users_up
`),
},
expectedInvokedMethods: []string{"V001_init_organizations_up", "V001_init_users_up"},
expectedErrors: []error{},
},
{
name: "down migration invoked down methods",
file: file.File{
Path: "/foobar",
FileName: "001_foobar.down.gm",
Version: 1,
Name: "foobar",
Direction: direction.Down,
Content: []byte(`
V001_init_users_down
V001_init_organizations_down
`),
},
expectedInvokedMethods: []string{"V001_init_users_down", "V001_init_organizations_down"},
expectedErrors: []error{},
},
{
name: "up migration: non-existing method causes migration not to execute",
file: file.File{
Path: "/foobar",
FileName: "001_foobar.up.gm",
Version: 1,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
V001_init_organizations_up
V001_init_users_up
V001_some_non_existing_method_up
`),
},
expectedInvokedMethods: []string{},
expectedErrors: []error{MethodNotFoundError("V001_some_non_existing_method_up")},
},
{
name: "up migration: failing method stops execution",
file: file.File{
Path: "/foobar",
FileName: "001_foobar.up.gm",
Version: 1,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
V001_init_organizations_up
V001_some_failing_method_up
V001_init_users_up
`),
},
expectedInvokedMethods: []string{
"V001_init_organizations_up",
"V001_some_failing_method_up",
},
expectedErrors: []error{&MethodInvocationFailedError{
MethodName: "V001_some_failing_method_up",
Err: SomeError{},
}},
},
{
name: "down migration: failing method stops migration",
file: file.File{
Path: "/foobar",
FileName: "001_foobar.down.gm",
Version: 1,
Name: "foobar",
Direction: direction.Down,
Content: []byte(`
V001_init_users_down
V001_some_failing_method_down
V001_init_organizations_down
`),
},
expectedInvokedMethods: []string{
"V001_init_users_down",
"V001_some_failing_method_down",
},
expectedErrors: []error{&MethodInvocationFailedError{
MethodName: "V001_some_failing_method_down",
Err: SomeError{},
}},
},
{
name: "up migration: failing method causes rollback in rollback mode",
expectRollback: true,
file: file.File{
Path: "/foobar",
FileName: "001_foobar.up.gm",
Version: 1,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
V001_init_organizations_up
V001_init_users_up
V001_some_failing_method_up
`),
},
expectedInvokedMethods: []string{
"V001_init_organizations_up",
"V001_init_users_up",
"V001_some_failing_method_up",
"V001_init_users_down",
"V001_init_organizations_down",
},
expectedErrors: []error{&MethodInvocationFailedError{
MethodName: "V001_some_failing_method_up",
Err: SomeError{},
}},
},
{
name: "down migration: failing method causes rollback in rollback mode",
expectRollback: true,
file: file.File{
Path: "/foobar",
FileName: "001_foobar.down.gm",
Version: 1,
Name: "foobar",
Direction: direction.Down,
Content: []byte(`
V001_init_users_down
V001_some_failing_method_down
V001_init_organizations_down
`),
},
expectedInvokedMethods: []string{
"V001_init_users_down",
"V001_some_failing_method_down",
"V001_init_users_up",
},
expectedErrors: []error{&MethodInvocationFailedError{
MethodName: "V001_some_failing_method_down",
Err: SomeError{},
}},
},
}
for _, c := range cases {
migrator := Migrator{}
fakeInvoker := &FakeGoMethodsInvoker{InvokedMethods: []string{}}
migrator.MethodInvoker = fakeInvoker
migrator.RollbackOnFailure = c.expectRollback
pipe := pipep.New()
go func() {
migrator.Migrate(c.file, pipe)
close(pipe)
}()
errs := pipep.ReadErrors(pipe)
var failed bool
if !reflect.DeepEqual(fakeInvoker.InvokedMethods, c.expectedInvokedMethods) {
failed = true
t.Errorf("case '%s': FAILED\nexpected invoked methods %v\nbut got %v", c.name, c.expectedInvokedMethods, fakeInvoker.InvokedMethods)
}
if !reflect.DeepEqual(errs, c.expectedErrors) {
failed = true
t.Errorf("case '%s': FAILED\nexpected errors %v\nbut got %v", c.name, c.expectedErrors, errs)
}
if !failed {
//t.Logf("case '%s': PASSED", c.name)
}
}
}
func TestGetRollbackToMethod(t *testing.T) {
cases := []struct {
method string
expectedRollbackMethod string
}{
{"some_method_up", "some_method_down"},
{"some_method_down", "some_method_up"},
{"up_down_up", "up_down_down"},
{"down_up", "down_down"},
{"down_down", "down_up"},
{"some_method", ""},
}
for _, c := range cases {
actualRollbackMethod := getRollbackToMethod(c.method)
if actualRollbackMethod != c.expectedRollbackMethod {
t.Errorf("Expected rollback method to be %s but got %s", c.expectedRollbackMethod, actualRollbackMethod)
}
}
}
@@ -1,40 +0,0 @@
package gomethods
import (
"fmt"
"sync"
"gopkg.in/mattes/migrate.v1/driver"
)
var methodsReceiversMu sync.Mutex
// Registers a methods receiver for go methods driver
// Users of gomethods migration drivers should call this method
// to register objects with their migration methods before executing the migration
func RegisterMethodsReceiverForDriver(driverName string, receiver interface{}) {
methodsReceiversMu.Lock()
defer methodsReceiversMu.Unlock()
if receiver == nil {
panic("Go methods: Register receiver object is nil")
}
driver := driver.GetDriver(driverName)
if driver == nil {
panic("Go methods: Trying to register receiver for not registered driver " + driverName)
}
methodsDriver, ok := driver.(GoMethodsDriver)
if !ok {
panic("Go methods: Trying to register receiver for non go methods driver " + driverName)
}
if methodsDriver.MethodsReceiver() != nil {
panic("Go methods: Methods receiver already registered for driver " + driverName)
}
if err := methodsDriver.SetMethodsReceiver(receiver); err != nil {
panic(fmt.Sprintf("Go methods: Failed to set methods receiver for driver %s\nError: %v",
driverName, err))
}
}
-194
View File
@@ -1,194 +0,0 @@
package mongodb
import (
"errors"
"reflect"
"strings"
"gopkg.in/mattes/migrate.v1/driver"
"gopkg.in/mattes/migrate.v1/driver/mongodb/gomethods"
"gopkg.in/mattes/migrate.v1/file"
"gopkg.in/mattes/migrate.v1/migrate/direction"
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
)
type UnregisteredMethodsReceiverError string
func (e UnregisteredMethodsReceiverError) Error() string {
return "Unregistered methods receiver for driver: " + string(e)
}
type WrongMethodsReceiverTypeError string
func (e WrongMethodsReceiverTypeError) Error() string {
return "Wrong methods receiver type for driver: " + string(e)
}
const MIGRATE_C = "db_migrations"
const DRIVER_NAME = "gomethods.mongodb"
type Driver struct {
Session *mgo.Session
methodsReceiver MethodsReceiver
migrator gomethods.Migrator
}
var _ gomethods.GoMethodsDriver = (*Driver)(nil)
type MethodsReceiver interface {
DbName() string
}
func (d *Driver) MethodsReceiver() interface{} {
return d.methodsReceiver
}
func (d *Driver) SetMethodsReceiver(r interface{}) error {
r1, ok := r.(MethodsReceiver)
if !ok {
return WrongMethodsReceiverTypeError(DRIVER_NAME)
}
d.methodsReceiver = r1
return nil
}
func init() {
driver.RegisterDriver("mongodb", &Driver{})
}
type DbMigration struct {
Id bson.ObjectId `bson:"_id"`
Version uint64 `bson:"version"`
}
func (driver *Driver) Initialize(url string) error {
if driver.methodsReceiver == nil {
return UnregisteredMethodsReceiverError(DRIVER_NAME)
}
urlWithoutScheme := strings.SplitN(url, "mongodb://", 2)
if len(urlWithoutScheme) != 2 {
return errors.New("invalid mongodb:// scheme")
}
session, err := mgo.Dial(url)
if err != nil {
return err
}
session.SetMode(mgo.Monotonic, true)
c := session.DB(driver.methodsReceiver.DbName()).C(MIGRATE_C)
err = c.EnsureIndex(mgo.Index{
Key: []string{"version"},
Unique: true,
})
if err != nil {
return err
}
driver.Session = session
driver.migrator = gomethods.Migrator{MethodInvoker: driver}
return nil
}
func (driver *Driver) Close() error {
if driver.Session != nil {
driver.Session.Close()
}
return nil
}
func (driver *Driver) FilenameExtension() string {
return "mgo"
}
func (driver *Driver) Version() (uint64, error) {
var latestMigration DbMigration
c := driver.Session.DB(driver.methodsReceiver.DbName()).C(MIGRATE_C)
err := c.Find(bson.M{}).Sort("-version").One(&latestMigration)
switch {
case err == mgo.ErrNotFound:
return 0, nil
case err != nil:
return 0, err
default:
return latestMigration.Version, nil
}
}
func (driver *Driver) Migrate(f file.File, pipe chan interface{}) {
defer close(pipe)
pipe <- f
err := driver.migrator.Migrate(f, pipe)
if err != nil {
return
}
migrate_c := driver.Session.DB(driver.methodsReceiver.DbName()).C(MIGRATE_C)
if f.Direction == direction.Up {
id := bson.NewObjectId()
dbMigration := DbMigration{Id: id, Version: f.Version}
err := migrate_c.Insert(dbMigration)
if err != nil {
pipe <- err
return
}
} else if f.Direction == direction.Down {
err := migrate_c.Remove(bson.M{"version": f.Version})
if err != nil {
pipe <- err
return
}
}
}
func (driver *Driver) Validate(methodName string) error {
methodWithReceiver, ok := reflect.TypeOf(driver.methodsReceiver).MethodByName(methodName)
if !ok {
return gomethods.MethodNotFoundError(methodName)
}
if methodWithReceiver.PkgPath != "" {
return gomethods.MethodNotFoundError(methodName)
}
methodFunc := reflect.ValueOf(driver.methodsReceiver).MethodByName(methodName)
methodTemplate := func(*mgo.Session) error { return nil }
if methodFunc.Type() != reflect.TypeOf(methodTemplate) {
return gomethods.WrongMethodSignatureError(methodName)
}
return nil
}
func (driver *Driver) Invoke(methodName string) error {
name := methodName
migrateMethod := reflect.ValueOf(driver.methodsReceiver).MethodByName(name)
if !migrateMethod.IsValid() {
return gomethods.MethodNotFoundError(methodName)
}
retValues := migrateMethod.Call([]reflect.Value{reflect.ValueOf(driver.Session)})
if len(retValues) != 1 {
return gomethods.WrongMethodSignatureError(name)
}
if !retValues[0].IsNil() {
err, ok := retValues[0].Interface().(error)
if !ok {
return gomethods.WrongMethodSignatureError(name)
}
return &gomethods.MethodInvocationFailedError{MethodName: name, Err: err}
}
return nil
}
-22
View File
@@ -1,22 +0,0 @@
# MSSQL Driver
* Runs migrations in transactions.
That means that if a migration fails, it will be safely rolled back.
* Tries to return helpful error messages.
* Stores migration version details in table ``schema_migrations``.
This table will be auto-generated.
## Usage
```bash
migrate -url="sqlserver://sa:Passw0rd@localhost:1433?database=master" -path ./db/migrations create add_field_to_table
migrate -url="sqlserver://sa:Passw0rd@localhost:1433?database=master" -path ./db/migrations up
migrate help # for more info
# see more docs: https://github.com/denisenkom/go-mssqldb
```
## Authors
* Andras Laczi, https://github.com/alaczi
-128
View File
@@ -1,128 +0,0 @@
// Package mssql implements the Driver interface.
package mssql
import (
"database/sql"
"errors"
"fmt"
"github.com/denisenkom/go-mssqldb"
"gopkg.in/mattes/migrate.v1/driver"
"gopkg.in/mattes/migrate.v1/file"
"gopkg.in/mattes/migrate.v1/migrate/direction"
)
type Driver struct {
db *sql.DB
}
const tableName = "schema_migrations"
func (driver *Driver) Initialize(url string) error {
db, err := sql.Open("mssql", url)
if err != nil {
return err
}
if err := db.Ping(); err != nil {
return err
}
driver.db = db
if err := driver.ensureVersionTableExists(); err != nil {
return err
}
return nil
}
func (driver *Driver) Close() error {
if err := driver.db.Close(); err != nil {
return err
}
return nil
}
func (driver *Driver) ensureVersionTableExists() error {
if _, err := driver.db.Exec("if object_id('" + tableName + "') is null create table [" + tableName + "] ([version] BIGINT NOT NULL PRIMARY KEY);"); err != nil {
return err
}
return nil
}
func (driver *Driver) FilenameExtension() string {
return "sql"
}
func (driver *Driver) Migrate(f file.File, pipe chan interface{}) {
defer close(pipe)
pipe <- f
tx, err := driver.db.Begin()
if err != nil {
pipe <- err
return
}
if f.Direction == direction.Up {
if _, err := tx.Exec("INSERT INTO "+tableName+" (version) VALUES (?)", f.Version); err != nil {
pipe <- err
if err := tx.Rollback(); err != nil {
pipe <- err
}
return
}
} else if f.Direction == direction.Down {
if _, err := tx.Exec("DELETE FROM "+tableName+" WHERE version=?", f.Version); err != nil {
pipe <- err
if err := tx.Rollback(); err != nil {
pipe <- err
}
return
}
}
if err := f.ReadContent(); err != nil {
pipe <- err
return
}
if _, err := tx.Exec(string(f.Content)); err != nil {
switch msErr := err.(type) {
case *mssql.Error:
errorPart := file.LinesBeforeAndAfter(f.Content, int(msErr.LineNo), 5, 5, true)
pipe <- errors.New(fmt.Sprintf("%v: %s in line %v:\n\n%s", msErr.Number, msErr.Message, msErr.LineNo, string(errorPart)))
if err := tx.Rollback(); err != nil {
pipe <- err
}
return
default:
pipe <- err
if err := tx.Rollback(); err != nil {
pipe <- err
}
return
}
}
if err := tx.Commit(); err != nil {
pipe <- err
return
}
}
func (driver *Driver) Version() (uint64, error) {
var version uint64
err := driver.db.QueryRow("SELECT version FROM " + tableName + " ORDER BY version DESC OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY").Scan(&version)
switch {
case err == sql.ErrNoRows:
return 0, nil
case err != nil:
return 0, err
default:
return version, nil
}
}
func init() {
driver.RegisterDriver("mssql", &Driver{})
driver.RegisterDriver("sqlserver", &Driver{})
}
-168
View File
@@ -1,168 +0,0 @@
package mssql
import (
"database/sql"
"fmt"
"os"
"testing"
"time"
"gopkg.in/mattes/migrate.v1/file"
"gopkg.in/mattes/migrate.v1/migrate/direction"
pipep "gopkg.in/mattes/migrate.v1/pipe"
)
// TestMigrate runs some additional tests on Migrate().
// Basic testing is already done in migrate/migrate_test.go
func TestMigrate(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
host := os.Getenv("MSSQL_PORT_1433_TCP_ADDR")
port := os.Getenv("MSSQL_PORT_1433_TCP_PORT")
driverURL := fmt.Sprintf("server=%s;user id=%s;password=%s;port=%s;database=%s;encrypt=disable;log=2;TrustServerCertificate=true",
host,
"sa",
"Passw0rd",
port,
"master",
)
// retry connection for 2 minutes
until := time.Now().Add(time.Second * 120)
ticker := time.NewTicker(time.Second)
var connection *sql.DB
var err error
// prepare clean database
for tick := range ticker.C {
if tick.After(until) {
ticker.Stop()
break
}
connection, err = sql.Open("mssql", driverURL)
err = connection.Ping()
if err == nil {
ticker.Stop()
break
}
}
if err != nil {
t.Fatal("Failed to connect to mssql docker container after 2 minutes", err)
}
if _, err := connection.Exec(`
DROP TABLE IF EXISTS yolo;
DROP TABLE IF EXISTS ` + tableName + `;`); err != nil {
t.Fatal(err)
}
d := &Driver{}
if err := d.Initialize(driverURL); err != nil {
t.Fatal(err)
}
// testing idempotency: second call should be a no-op, since table already exists
if err := d.Initialize(driverURL); err != nil {
t.Fatal(err)
}
files := []file.File{
{
Path: "/foobar",
FileName: "001_foobar.up.sql",
Version: 1,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
CREATE TABLE yolo (
id BIGINT IDENTITY NOT NULL PRIMARY KEY
);
`),
},
{
Path: "/foobar",
FileName: "002_foobar.down.sql",
Version: 1,
Name: "foobar",
Direction: direction.Down,
Content: []byte(`
DROP TABLE yolo;
`),
},
{
Path: "/foobar",
FileName: "002_foobar.up.sql",
Version: 2,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
CREATE TABLE error (
id THIS WILL CAUSE AN ERROR
)
`),
},
{
Path: "/foobar",
FileName: "20170118205923_demo.up.sql",
Version: 20170118205923,
Name: "demo",
Direction: direction.Up,
Content: []byte(`
CREATE TABLE demo (
id BIGINT IDENTITY NOT NULL PRIMARY KEY
)
`),
},
{
Path: "/foobar",
FileName: "20170118205923_demo.down.sql",
Version: 20170118205923,
Name: "demo",
Direction: direction.Down,
Content: []byte(`
DROP TABLE demo
`),
},
}
pipe := pipep.New()
go d.Migrate(files[0], pipe)
errs := pipep.ReadErrors(pipe)
if len(errs) > 0 {
t.Fatal(errs)
}
pipe = pipep.New()
go d.Migrate(files[1], pipe)
errs = pipep.ReadErrors(pipe)
if len(errs) > 0 {
t.Fatal(errs)
}
pipe = pipep.New()
go d.Migrate(files[2], pipe)
errs = pipep.ReadErrors(pipe)
if len(errs) == 0 {
t.Error("Expected test case to fail")
}
pipe = pipep.New()
go d.Migrate(files[3], pipe)
errs = pipep.ReadErrors(pipe)
if len(errs) > 0 {
t.Fatal(errs)
}
pipe = pipep.New()
go d.Migrate(files[4], pipe)
errs = pipep.ReadErrors(pipe)
if len(errs) > 0 {
t.Fatal(errs)
}
if err := d.Close(); err != nil {
t.Fatal(err)
}
}
-24
View File
@@ -1,24 +0,0 @@
# MySQL Driver
### See [issue #1](https://github.com/mattes/migrate/issues/1#issuecomment-58728186) before using this driver!
* Runs migrations in transcations.
That means that if a migration failes, it will be safely rolled back.
* Tries to return helpful error messages.
* Stores migration version details in table ``schema_migrations``.
This table will be auto-generated.
## Usage
```bash
migrate -url mysql://user@tcp(host:port)/database -path ./db/migrations create add_field_to_table
migrate -url mysql://user@tcp(host:port)/database -path ./db/migrations up
migrate help # for more info
```
See full [DSN (Data Source Name) documentation](https://github.com/go-sql-driver/mysql/#dsn-data-source-name).
## Authors
* Matthias Kadenbach, https://github.com/mattes
-185
View File
@@ -1,185 +0,0 @@
// Package mysql implements the Driver interface.
package mysql
import (
"bufio"
"bytes"
"database/sql"
"errors"
"fmt"
"regexp"
"strconv"
"strings"
"github.com/go-sql-driver/mysql"
"gopkg.in/mattes/migrate.v1/driver"
"gopkg.in/mattes/migrate.v1/file"
"gopkg.in/mattes/migrate.v1/migrate/direction"
)
type Driver struct {
db *sql.DB
}
const tableName = "schema_migrations"
func (driver *Driver) Initialize(url string) error {
urlWithoutScheme := strings.SplitN(url, "mysql://", 2)
if len(urlWithoutScheme) != 2 {
return errors.New("invalid mysql:// scheme")
}
db, err := sql.Open("mysql", urlWithoutScheme[1])
if err != nil {
return err
}
if err := db.Ping(); err != nil {
return err
}
driver.db = db
if err := driver.ensureVersionTableExists(); err != nil {
return err
}
return nil
}
func (driver *Driver) Close() error {
if err := driver.db.Close(); err != nil {
return err
}
return nil
}
func (driver *Driver) ensureVersionTableExists() error {
_, err := driver.db.Exec("CREATE TABLE IF NOT EXISTS " + tableName + " (version int not null primary key);")
if _, isWarn := err.(mysql.MySQLWarnings); err != nil && !isWarn {
return err
}
return nil
}
func (driver *Driver) FilenameExtension() string {
return "sql"
}
func (driver *Driver) Migrate(f file.File, pipe chan interface{}) {
defer close(pipe)
pipe <- f
// http://go-database-sql.org/modifying.html, Working with Transactions
// You should not mingle the use of transaction-related functions such as Begin() and Commit() with SQL statements such as BEGIN and COMMIT in your SQL code.
tx, err := driver.db.Begin()
if err != nil {
pipe <- err
return
}
if f.Direction == direction.Up {
if _, err := tx.Exec("INSERT INTO "+tableName+" (version) VALUES (?)", f.Version); err != nil {
pipe <- err
if err := tx.Rollback(); err != nil {
pipe <- err
}
return
}
} else if f.Direction == direction.Down {
if _, err := tx.Exec("DELETE FROM "+tableName+" WHERE version = ?", f.Version); err != nil {
pipe <- err
if err := tx.Rollback(); err != nil {
pipe <- err
}
return
}
}
if err := f.ReadContent(); err != nil {
pipe <- err
return
}
// TODO this is not good! unfortunately there is no mysql driver that
// supports multiple statements per query.
sqlStmts := bytes.Split(f.Content, []byte(";"))
for _, sqlStmt := range sqlStmts {
sqlStmt = bytes.TrimSpace(sqlStmt)
if len(sqlStmt) > 0 {
if _, err := tx.Exec(string(sqlStmt)); err != nil {
mysqlErr, isErr := err.(*mysql.MySQLError)
if isErr {
re, err := regexp.Compile(`at line ([0-9]+)$`)
if err != nil {
pipe <- err
if err := tx.Rollback(); err != nil {
pipe <- err
}
}
var lineNo int
lineNoRe := re.FindStringSubmatch(mysqlErr.Message)
if len(lineNoRe) == 2 {
lineNo, err = strconv.Atoi(lineNoRe[1])
}
if err == nil {
// get white-space offset
// TODO this is broken, because we use sqlStmt instead of f.Content
wsLineOffset := 0
b := bufio.NewReader(bytes.NewBuffer(sqlStmt))
for {
line, _, err := b.ReadLine()
if err != nil {
break
}
if bytes.TrimSpace(line) == nil {
wsLineOffset += 1
} else {
break
}
}
message := mysqlErr.Error()
message = re.ReplaceAllString(message, fmt.Sprintf("at line %v", lineNo+wsLineOffset))
errorPart := file.LinesBeforeAndAfter(sqlStmt, lineNo, 5, 5, true)
pipe <- errors.New(fmt.Sprintf("%s\n\n%s", message, string(errorPart)))
} else {
pipe <- errors.New(mysqlErr.Error())
}
if err := tx.Rollback(); err != nil {
pipe <- err
}
return
}
}
}
}
if err := tx.Commit(); err != nil {
pipe <- err
return
}
}
func (driver *Driver) Version() (uint64, error) {
var version uint64
err := driver.db.QueryRow("SELECT version FROM " + tableName + " ORDER BY version DESC").Scan(&version)
switch {
case err == sql.ErrNoRows:
return 0, nil
case err != nil:
return 0, err
default:
return version, nil
}
}
func init() {
driver.RegisterDriver("mysql", &Driver{})
}
-106
View File
@@ -1,106 +0,0 @@
package mysql
import (
"database/sql"
"os"
"strings"
"testing"
"gopkg.in/mattes/migrate.v1/file"
"gopkg.in/mattes/migrate.v1/migrate/direction"
pipep "gopkg.in/mattes/migrate.v1/pipe"
)
// TestMigrate runs some additional tests on Migrate().
// Basic testing is already done in migrate/migrate_test.go
func TestMigrate(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
host := os.Getenv("MYSQL_PORT_3306_TCP_ADDR")
port := os.Getenv("MYSQL_PORT_3306_TCP_PORT")
driverUrl := "mysql://root@tcp(" + host + ":" + port + ")/migratetest"
// prepare clean database
connection, err := sql.Open("mysql", strings.SplitN(driverUrl, "mysql://", 2)[1])
if err != nil {
t.Fatal(err)
}
if _, err := connection.Exec(`DROP TABLE IF EXISTS yolo, yolo1, ` + tableName); err != nil {
t.Fatal(err)
}
d := &Driver{}
if err := d.Initialize(driverUrl); err != nil {
t.Fatal(err)
}
files := []file.File{
{
Path: "/foobar",
FileName: "001_foobar.up.sql",
Version: 1,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
CREATE TABLE yolo (
id int(11) not null primary key auto_increment
);
CREATE TABLE yolo1 (
id int(11) not null primary key auto_increment
);
`),
},
{
Path: "/foobar",
FileName: "002_foobar.down.sql",
Version: 1,
Name: "foobar",
Direction: direction.Down,
Content: []byte(`
DROP TABLE yolo;
`),
},
{
Path: "/foobar",
FileName: "002_foobar.up.sql",
Version: 2,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
// a comment
CREATE TABLE error (
id THIS WILL CAUSE AN ERROR
);
`),
},
}
pipe := pipep.New()
go d.Migrate(files[0], pipe)
errs := pipep.ReadErrors(pipe)
if len(errs) > 0 {
t.Fatal(errs)
}
pipe = pipep.New()
go d.Migrate(files[1], pipe)
errs = pipep.ReadErrors(pipe)
if len(errs) > 0 {
t.Fatal(errs)
}
pipe = pipep.New()
go d.Migrate(files[2], pipe)
errs = pipep.ReadErrors(pipe)
if len(errs) == 0 {
t.Error("Expected test case to fail")
}
if err := d.Close(); err != nil {
t.Fatal(err)
}
}
-20
View File
@@ -1,20 +0,0 @@
# Neo4j Driver
* Runs migrations in transcations.
That means that if a migration failes, it will be safely rolled back.
* Stores migration version details with the label ``SchemaMigrations``.
An unique constraint for the field :SchemaMigrations(version) will be auto-generated.
* Neo4j cannot perform schema and data updates in a transaction, therefore it's necessary to use different migration files
## Usage
```bash
migrate -url neo4j://user:password@host:port/db/data -path ./db/migrations create add_field_to_table
migrate -url neo4j://user:password@host:port/db/data -path ./db/migrations up
migrate help # for more info
```
## Author
* Carlos Forero, https://github.com/carlosforero
-167
View File
@@ -1,167 +0,0 @@
// Package neo4j implements the Driver interface.
package neo4j
import (
"bytes"
"errors"
"fmt"
"github.com/jmcvetta/neoism"
"gopkg.in/mattes/migrate.v1/driver"
"gopkg.in/mattes/migrate.v1/file"
"gopkg.in/mattes/migrate.v1/migrate/direction"
"strings"
)
type Driver struct {
db *neoism.Database
}
const labelName = "SchemaMigration"
func (driver *Driver) Initialize(url string) error {
url = strings.Replace(url, "neo4j", "http", 1)
db, err := neoism.Connect(url)
if err != nil {
return err
}
driver.db = db
if err := driver.ensureVersionConstraintExists(); err != nil {
return err
}
return nil
}
func (driver *Driver) Close() error {
driver.db = nil
return nil
}
func (driver *Driver) FilenameExtension() string {
return "cql"
}
func (driver *Driver) ensureVersionConstraintExists() error {
uc, _ := driver.db.UniqueConstraints("SchemaMigration", "version")
if len(uc) == 0 {
_, err := driver.db.CreateUniqueConstraint("SchemaMigration", "version")
return err
}
return nil
}
func (driver *Driver) setVersion(d direction.Direction, v uint64, invert bool) error {
cqUp := neoism.CypherQuery{
Statement: `CREATE (n:SchemaMigration {version: {Version}}) RETURN n`,
Parameters: neoism.Props{"Version": v},
}
cqDown := neoism.CypherQuery{
Statement: `MATCH (n:SchemaMigration {version: {Version}}) DELETE n`,
Parameters: neoism.Props{"Version": v},
}
var cq neoism.CypherQuery
switch d {
case direction.Up:
if invert {
cq = cqDown
} else {
cq = cqUp
}
case direction.Down:
if invert {
cq = cqUp
} else {
cq = cqDown
}
}
return driver.db.Cypher(&cq)
}
func (driver *Driver) Migrate(f file.File, pipe chan interface{}) {
var err error
defer func() {
if err != nil {
// Invert version direction if we couldn't apply the changes for some reason.
if err := driver.setVersion(f.Direction, f.Version, true); err != nil {
pipe <- err
}
pipe <- err
}
close(pipe)
}()
pipe <- f
if err = driver.setVersion(f.Direction, f.Version, false); err != nil {
pipe <- err
return
}
if err = f.ReadContent(); err != nil {
pipe <- err
return
}
cQueries := []*neoism.CypherQuery{}
// Neoism doesn't support multiple statements per query.
cqlStmts := bytes.Split(f.Content, []byte(";"))
for _, cqlStmt := range cqlStmts {
cqlStmt = bytes.TrimSpace(cqlStmt)
if len(cqlStmt) > 0 {
cq := neoism.CypherQuery{Statement: string(cqlStmt)}
cQueries = append(cQueries, &cq)
}
}
var tx *neoism.Tx
tx, err = driver.db.Begin(cQueries)
if err != nil {
pipe <- err
for _, err := range tx.Errors {
pipe <- errors.New(fmt.Sprintf("%v", err.Message))
}
if err = tx.Rollback(); err != nil {
pipe <- err
}
return
}
if err = tx.Commit(); err != nil {
pipe <- err
for _, err := range tx.Errors {
pipe <- errors.New(fmt.Sprintf("%v", err.Message))
}
return
}
}
func (driver *Driver) Version() (uint64, error) {
res := []struct {
Version uint64 `json:"n.version"`
}{}
cq := neoism.CypherQuery{
Statement: `MATCH (n:SchemaMigration)
RETURN n.version ORDER BY n.version DESC LIMIT 1`,
Result: &res,
}
if err := driver.db.Cypher(&cq); err != nil || len(res) == 0 {
return 0, err
}
return res[0].Version, nil
}
func init() {
driver.RegisterDriver("neo4j", &Driver{})
driver.RegisterDriver("neo4js", &Driver{})
}
-100
View File
@@ -1,100 +0,0 @@
package neo4j
import (
"os"
"testing"
"github.com/jmcvetta/neoism"
"gopkg.in/mattes/migrate.v1/file"
"gopkg.in/mattes/migrate.v1/migrate/direction"
pipep "gopkg.in/mattes/migrate.v1/pipe"
)
// TestMigrate runs some additional tests on Migrate().
// Basic testing is already done in migrate/migrate_test.go
func TestMigrate(t *testing.T) {
t.Skip("TODO: fix test: neo4j_test.go:26: Get http://neo4j:test@/db/data/: http: no Host in request URL")
host := os.Getenv("NEO4J_PORT_7474_TCP_ADDR")
port := os.Getenv("NEO4J_PORT_7474_TCP_PORT")
driverUrl := "http://neo4j:test@" + host + ":" + port + "/db/data"
// prepare clean database
db, err := neoism.Connect(driverUrl)
if err != nil {
t.Fatal(err)
}
cq := neoism.CypherQuery{
Statement: `DROP INDEX ON :Yolo(name)`,
}
// If an error dropping the index then ignore it
db.Cypher(&cq)
driverUrl = "neo4j://neo4j:test@" + host + ":" + port + "/db/data"
d := &Driver{}
if err := d.Initialize(driverUrl); err != nil {
t.Fatal(err)
}
files := []file.File{
{
Path: "/foobar",
FileName: "001_foobar.up.cql",
Version: 1,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
CREATE INDEX ON :Yolo(name)
`),
},
{
Path: "/foobar",
FileName: "001_foobar.down.cql",
Version: 1,
Name: "foobar",
Direction: direction.Down,
Content: []byte(`
DROP INDEX ON :Yolo(name)
`),
},
{
Path: "/foobar",
FileName: "002_foobar.up.cql",
Version: 2,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
CREATE INDEX :Yolo(name) THIS WILL CAUSE AN ERROR
`),
},
}
pipe := pipep.New()
go d.Migrate(files[0], pipe)
errs := pipep.ReadErrors(pipe)
if len(errs) > 0 {
t.Fatal(errs)
}
pipe = pipep.New()
go d.Migrate(files[1], pipe)
errs = pipep.ReadErrors(pipe)
if len(errs) > 0 {
t.Fatal(errs)
}
pipe = pipep.New()
go d.Migrate(files[2], pipe)
errs = pipep.ReadErrors(pipe)
if len(errs) == 0 {
t.Error("Expected test case to fail")
}
if err := d.Close(); err != nil {
t.Fatal(err)
}
}
-25
View File
@@ -1,25 +0,0 @@
# PostgreSQL Driver
* Runs migrations in transactions.
That means that if a migration fails, it will be safely rolled back.
* Tries to return helpful error messages.
* Stores migration version details in table ``schema_migrations``.
This table will be auto-generated.
## Usage
```bash
migrate -url postgres://user@host:port/database -path ./db/migrations create add_field_to_table
migrate -url postgres://user@host:port/database -path ./db/migrations up
migrate help # for more info
# TODO(mattes): thinking about adding some custom flag to allow migration within schemas:
-url="postgres://user@host:port/database?schema=name"
# see more docs: https://godoc.org/github.com/lib/pq#hdr-Connection_String_Parameters
```
## Authors
* Matthias Kadenbach, https://github.com/mattes
-145
View File
@@ -1,145 +0,0 @@
// Package postgres implements the Driver interface.
package postgres
import (
"database/sql"
"errors"
"fmt"
"strconv"
"github.com/lib/pq"
"gopkg.in/mattes/migrate.v1/driver"
"gopkg.in/mattes/migrate.v1/file"
"gopkg.in/mattes/migrate.v1/migrate/direction"
)
type Driver struct {
db *sql.DB
}
const tableName = "schema_migrations"
func (driver *Driver) Initialize(url string) error {
db, err := sql.Open("postgres", url)
if err != nil {
return err
}
if err := db.Ping(); err != nil {
return err
}
driver.db = db
if err := driver.ensureVersionTableExists(); err != nil {
return err
}
return nil
}
func (driver *Driver) Close() error {
if err := driver.db.Close(); err != nil {
return err
}
return nil
}
func (driver *Driver) ensureVersionTableExists() error {
r := driver.db.QueryRow("SELECT count(*) FROM information_schema.tables WHERE table_name = $1 AND table_schema = (SELECT current_schema());", tableName)
c := 0
if err := r.Scan(&c); err != nil {
return err
}
if c > 0 {
return nil
}
if _, err := driver.db.Exec("CREATE TABLE IF NOT EXISTS " + tableName + " (version bigint not null primary key);"); err != nil {
return err
}
return nil
}
func (driver *Driver) FilenameExtension() string {
return "sql"
}
func (driver *Driver) Migrate(f file.File, pipe chan interface{}) {
defer close(pipe)
pipe <- f
tx, err := driver.db.Begin()
if err != nil {
pipe <- err
return
}
if f.Direction == direction.Up {
if _, err := tx.Exec("INSERT INTO "+tableName+" (version) VALUES ($1)", f.Version); err != nil {
pipe <- err
if err := tx.Rollback(); err != nil {
pipe <- err
}
return
}
} else if f.Direction == direction.Down {
if _, err := tx.Exec("DELETE FROM "+tableName+" WHERE version=$1", f.Version); err != nil {
pipe <- err
if err := tx.Rollback(); err != nil {
pipe <- err
}
return
}
}
if err := f.ReadContent(); err != nil {
pipe <- err
return
}
if _, err := tx.Exec(string(f.Content)); err != nil {
switch pqErr := err.(type) {
case *pq.Error:
offset, err := strconv.Atoi(pqErr.Position)
if err == nil && offset >= 0 {
lineNo, columnNo := file.LineColumnFromOffset(f.Content, offset-1)
errorPart := file.LinesBeforeAndAfter(f.Content, lineNo, 5, 5, true)
pipe <- errors.New(fmt.Sprintf("%s %v: %s in line %v, column %v:\n\n%s", pqErr.Severity, pqErr.Code, pqErr.Message, lineNo, columnNo, string(errorPart)))
} else {
pipe <- errors.New(fmt.Sprintf("%s %v: %s", pqErr.Severity, pqErr.Code, pqErr.Message))
}
if err := tx.Rollback(); err != nil {
pipe <- err
}
return
default:
pipe <- err
if err := tx.Rollback(); err != nil {
pipe <- err
}
return
}
}
if err := tx.Commit(); err != nil {
pipe <- err
return
}
}
func (driver *Driver) Version() (uint64, error) {
var version uint64
err := driver.db.QueryRow("SELECT version FROM " + tableName + " ORDER BY version DESC LIMIT 1").Scan(&version)
switch {
case err == sql.ErrNoRows:
return 0, nil
case err != nil:
return 0, err
default:
return version, nil
}
}
func init() {
drv := Driver{}
driver.RegisterDriver("postgres", &drv)
driver.RegisterDriver("postgresql", &drv)
}
-142
View File
@@ -1,142 +0,0 @@
package postgres
import (
"database/sql"
"os"
"testing"
"gopkg.in/mattes/migrate.v1/file"
"gopkg.in/mattes/migrate.v1/migrate/direction"
pipep "gopkg.in/mattes/migrate.v1/pipe"
)
// TestMigrate runs some additional tests on Migrate().
// Basic testing is already done in migrate/migrate_test.go
func TestMigrate(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
host := os.Getenv("POSTGRES_PORT_5432_TCP_ADDR")
port := os.Getenv("POSTGRES_PORT_5432_TCP_PORT")
driverURL := "postgres://postgres@" + host + ":" + port + "/template1?sslmode=disable"
// prepare clean database
connection, err := sql.Open("postgres", driverURL)
if err != nil {
t.Fatal(err)
}
if _, err := connection.Exec(`
DROP TABLE IF EXISTS yolo;
DROP TABLE IF EXISTS ` + tableName + `;`); err != nil {
t.Fatal(err)
}
d := &Driver{}
if err := d.Initialize(driverURL); err != nil {
t.Fatal(err)
}
// testing idempotency: second call should be a no-op, since table already exists
if err := d.Initialize(driverURL); err != nil {
t.Fatal(err)
}
files := []file.File{
{
Path: "/foobar",
FileName: "001_foobar.up.sql",
Version: 1,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
CREATE TABLE yolo (
id serial not null primary key
);
`),
},
{
Path: "/foobar",
FileName: "002_foobar.down.sql",
Version: 1,
Name: "foobar",
Direction: direction.Down,
Content: []byte(`
DROP TABLE yolo;
`),
},
{
Path: "/foobar",
FileName: "002_foobar.up.sql",
Version: 2,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
CREATE TABLE error (
id THIS WILL CAUSE AN ERROR
)
`),
},
{
Path: "/foobar",
FileName: "20170118205923_demo.up.sql",
Version: 20170118205923,
Name: "demo",
Direction: direction.Up,
Content: []byte(`
CREATE TABLE demo (
id serial not null primary key
)
`),
},
{
Path: "/foobar",
FileName: "20170118205923_demo.down.sql",
Version: 20170118205923,
Name: "demo",
Direction: direction.Down,
Content: []byte(`
DROP TABLE demo
`),
},
}
pipe := pipep.New()
go d.Migrate(files[0], pipe)
errs := pipep.ReadErrors(pipe)
if len(errs) > 0 {
t.Fatal(errs)
}
pipe = pipep.New()
go d.Migrate(files[1], pipe)
errs = pipep.ReadErrors(pipe)
if len(errs) > 0 {
t.Fatal(errs)
}
pipe = pipep.New()
go d.Migrate(files[2], pipe)
errs = pipep.ReadErrors(pipe)
if len(errs) == 0 {
t.Error("Expected test case to fail")
}
pipe = pipep.New()
go d.Migrate(files[3], pipe)
errs = pipep.ReadErrors(pipe)
if len(errs) > 0 {
t.Fatal(errs)
}
pipe = pipep.New()
go d.Migrate(files[4], pipe)
errs = pipep.ReadErrors(pipe)
if len(errs) > 0 {
t.Fatal(errs)
}
if err := d.Close(); err != nil {
t.Fatal(err)
}
}
-17
View File
@@ -1,17 +0,0 @@
# Ql Driver
* Supports both in-memory and file ql databases, with the URL schemes `ql+file://` and `ql+memory://`
* In-memory driver is not of much use, since the database is destroyed once the migration operation finishes, but is included for completeness.
* Stores migration version details in automatically generated table ``schema_migrations``.
## Usage
```bash
migrate -url ql+file://./data.db -path ./migrations create add_field_to_table
migrate -url ql+file://./data.db -path ./db/migrations up
migrate help # for more info
```
## Authors
* Sam Willcocks, https://github.com/wlcx
-131
View File
@@ -1,131 +0,0 @@
package ql
import (
"database/sql"
"gopkg.in/mattes/migrate.v1/driver"
"gopkg.in/mattes/migrate.v1/file"
"gopkg.in/mattes/migrate.v1/migrate/direction"
"fmt"
"strings"
_ "github.com/cznic/ql/driver"
)
const tableName = "schema_migration"
type Driver struct {
db *sql.DB
}
func (d *Driver) Initialize(url string) (err error) {
d.db, err = sql.Open("ql", strings.TrimPrefix(url, "ql+"))
if err != nil {
return
}
if err = d.db.Ping(); err != nil {
return
}
if err = d.ensureVersionTableExists(); err != nil {
return
}
return
}
func (d *Driver) Close() error {
if err := d.db.Close(); err != nil {
return err
}
return nil
}
func (d *Driver) FilenameExtension() string {
return "sql"
}
func (d *Driver) Migrate(f file.File, pipe chan interface{}) {
defer close(pipe)
pipe <- f
tx, err := d.db.Begin()
if err != nil {
pipe <- err
return
}
switch f.Direction {
case direction.Up:
if _, err := tx.Exec("INSERT INTO "+tableName+" (version) VALUES (uint($1))", f.Version); err != nil {
pipe <- err
if err := tx.Rollback(); err != nil {
pipe <- err
}
return
}
case direction.Down:
if _, err := tx.Exec("DELETE FROM "+tableName+" WHERE version=uint($1)", f.Version); err != nil {
pipe <- err
if err := tx.Rollback(); err != nil {
pipe <- err
}
return
}
}
if err := f.ReadContent(); err != nil {
pipe <- err
return
}
if _, err := tx.Exec(string(f.Content)); err != nil {
pipe <- err
if err := tx.Rollback(); err != nil {
pipe <- err
}
return
}
if err := tx.Commit(); err != nil {
pipe <- err
return
}
}
func (d *Driver) Version() (uint64, error) {
var version uint64
err := d.db.QueryRow("SELECT version FROM " + tableName + " ORDER BY version DESC LIMIT 1").Scan(&version)
switch {
case err == sql.ErrNoRows:
return 0, nil
case err != nil:
return 0, err
default:
return version, nil
}
}
func (d *Driver) ensureVersionTableExists() error {
tx, err := d.db.Begin()
if err != nil {
return err
}
if _, err := tx.Exec(fmt.Sprintf(`
CREATE TABLE IF NOT EXISTS %s (version uint64);
CREATE UNIQUE INDEX IF NOT EXISTS version_unique ON %s (version);
`, tableName, tableName)); err != nil {
if err := tx.Rollback(); err != nil {
return err
}
return err
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
func init() {
driver.RegisterDriver("ql+file", &Driver{})
driver.RegisterDriver("ql+memory", &Driver{})
}
-44
View File
@@ -1,44 +0,0 @@
package driver
import (
"sort"
"sync"
)
var driversMu sync.Mutex
var drivers = make(map[string]Driver)
// RegisterDriver register a driver so it can be created from its name. Drivers should
// call this from an init() function so that they registers themselves on
// import
func RegisterDriver(name string, driver Driver) {
driversMu.Lock()
defer driversMu.Unlock()
if driver == nil {
panic("driver: Register driver is nil")
}
if _, dup := drivers[name]; dup {
panic("sql: Register called twice for driver " + name)
}
drivers[name] = driver
}
// GetDriver retrieves a registered driver by name
func GetDriver(name string) Driver {
driversMu.Lock()
defer driversMu.Unlock()
driver := drivers[name]
return driver
}
// Drivers returns a sorted list of the names of the registered drivers.
func Drivers() []string {
driversMu.Lock()
defer driversMu.Unlock()
var list []string
for name := range drivers {
list = append(list, name)
}
sort.Strings(list)
return list
}
-21
View File
@@ -1,21 +0,0 @@
# Sqlite3 Driver
* Runs migrations in transcations.
That means that if a migration failes, it will be safely rolled back.
* Tries to return helpful error messages.
* Stores migration version details in table ``schema_migrations``.
This table will be auto-generated.
## Usage
```bash
migrate -url sqlite3://database.sqlite -path ./db/migrations create add_field_to_table
migrate -url sqlite3://database.sqlite -path ./db/migrations up
migrate help # for more info
```
## Authors
* Matthias Kadenbach, https://github.com/mattes
* Caesar Wirth, https://github.com/cjwirth

Some files were not shown because too many files have changed in this diff Show More