Compare commits
112
Commits
v1
..
cli-updates
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d9689ab9f | ||
|
|
e696a861ba | ||
|
|
801e84d4d2 | ||
|
|
ff7e060269 | ||
|
|
d4d20346f2 | ||
|
|
1b48b97aa4 | ||
|
|
d1e665bac7 | ||
|
|
dc109fc118 | ||
|
|
fb8698d89a | ||
|
|
2821341551 | ||
|
|
2831b4c0d8 | ||
|
|
c95cbb5931 | ||
|
|
da89656fdc | ||
|
|
4e5dc68b4e | ||
|
|
91ecd3cb6a | ||
|
|
448a36c5cb | ||
|
|
ed5a887579 | ||
|
|
e42058f15a | ||
|
|
a7e1cacd36 | ||
|
|
734803549d | ||
|
|
14f784961f | ||
|
|
5c8da71dea | ||
|
|
a6fe4b73b3 | ||
|
|
256e8f045b | ||
|
|
2e54bda0e1 | ||
|
|
2ae8e6291c | ||
|
|
651f78cba8 | ||
|
|
d5674ed411 | ||
|
|
4a2a810c20 | ||
|
|
365b0565de | ||
|
|
65d785d560 | ||
|
|
fff688c019 | ||
|
|
6f07ac731b | ||
|
|
f00c15d7b0 | ||
|
|
1676a902f2 | ||
|
|
576d323c52 | ||
|
|
b4adf71696 | ||
|
|
c0703e7a92 | ||
|
|
d789d50bd6 | ||
|
|
b83285bd59 | ||
|
|
bc93ed027f | ||
|
|
9f974b8996 | ||
|
|
c03b03c909 | ||
|
|
cecce18c51 | ||
|
|
1f37f41ee8 | ||
|
|
150ac7d708 | ||
|
|
be1ba9204a | ||
|
|
760bc3eb2d | ||
|
|
706f23115f | ||
|
|
171f9a256d | ||
|
|
e7b9b0d0ed | ||
|
|
5645143baa | ||
|
|
ec5c0ac3e0 | ||
|
|
511f9c1ffe | ||
|
|
6325327ba1 | ||
|
|
d60d590381 | ||
|
|
5aac011e49 | ||
|
|
320d573ccd | ||
|
|
c51bef38b6 | ||
|
|
28e0548cac | ||
|
|
d0acf6ecfe | ||
|
|
4b91f503eb | ||
|
|
eb02bc77fb | ||
|
|
55f54251eb | ||
|
|
e64b7cc233 | ||
|
|
4bb46a98c6 | ||
|
|
53b55288b3 | ||
|
|
f576a64f11 | ||
|
|
d574676702 | ||
|
|
f45821581c | ||
|
|
bb6ae0898d | ||
|
|
ea7fc641ba | ||
|
|
87e6482497 | ||
|
|
1e08e21470 | ||
|
|
ccb40ff3ff | ||
|
|
3291f3f891 | ||
|
|
2031939bfc | ||
|
|
6394299937 | ||
|
|
316f6c0cf3 | ||
|
|
53630f5ff1 | ||
|
|
3feee5958a | ||
|
|
209c056258 | ||
|
|
cd6e62049c | ||
|
|
526c0a918e | ||
|
|
de7ffa60cc | ||
|
|
81b9791178 | ||
|
|
c4ec6ac0f7 | ||
|
|
cfe3d2a83e | ||
|
|
b6bd4fe02d | ||
|
|
2da976155a | ||
|
|
e69e0de9c9 | ||
|
|
cfc9f10a6e | ||
|
|
8ba71b0754 | ||
|
|
46c16bb8f1 | ||
|
|
1b079a3cda | ||
|
|
c1b23fee45 | ||
|
|
199678e1bc | ||
|
|
d5daf6f6ae | ||
|
|
03d8d3233f | ||
|
|
5c6a21b014 | ||
|
|
3965df69d5 | ||
|
|
f47436e158 | ||
|
|
f474e4cae1 | ||
|
|
7979fdb832 | ||
|
|
b3f6bc47bb | ||
|
|
e6163082d7 | ||
|
|
fe1b5c06eb | ||
|
|
297a4227ef | ||
|
|
9f774d3e6a | ||
|
|
f7699f1dec | ||
|
|
3889c93b63 | ||
|
|
ee384ed8cc |
+5
-1
@@ -1,2 +1,6 @@
|
||||
.DS_Store
|
||||
test.db
|
||||
cli/build
|
||||
cli/cli
|
||||
cli/migrate
|
||||
.coverage
|
||||
.godoc.pid
|
||||
|
||||
+53
-13
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -1,3 +0,0 @@
|
||||
FROM scratch
|
||||
ADD migrate /migrate
|
||||
ENTRYPOINT ["/migrate"]
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Migrations
|
||||
|
||||
## Best practices: How to write migrations.
|
||||
|
||||
@TODO
|
||||
@@ -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)
|
||||
|
||||
@@ -1,132 +1,143 @@
|
||||
# You are looking at a deprecated version.
|
||||
# Use the [latest version](https://github.com/mattes/migrate) instead.
|
||||
|
||||
---
|
||||
[](https://travis-ci.org/mattes/migrate)
|
||||
[](https://godoc.org/github.com/mattes/migrate)
|
||||
[](https://coveralls.io/github/mattes/migrate?branch=v3.0-prev)
|
||||
[](https://packagecloud.io/mattes/migrate?filter=debs)
|
||||
|
||||
# migrate
|
||||
|
||||
[](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
@@ -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"]')"
|
||||
```
|
||||
@@ -0,0 +1,7 @@
|
||||
// +build aws-s3
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "github.com/mattes/migrate/source/aws-s3"
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
// +build github
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "github.com/mattes/migrate/source/github"
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
// +build go-bindata
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "github.com/mattes/migrate/source/go-bindata"
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
// +build google-cloud-storage
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "github.com/mattes/migrate/source/google-cloud-storage"
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
// +build mysql
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "github.com/mattes/migrate/database/mysql"
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
// +build postgres
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "github.com/mattes/migrate/database/postgres"
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
// +build ql
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "github.com/mattes/migrate/database/ql"
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
// +build redshift
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "github.com/mattes/migrate/database/redshift"
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package main
|
||||
|
||||
// Version is set in Makefile with build flags
|
||||
var Version = "dev"
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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>`.
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
@@ -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;;
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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 }
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package stub
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
dt "github.com/mattes/migrate/database/testing"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
s := &Stub{}
|
||||
d, err := s.Open("")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dt.Test(t, d, []byte("/* foobar migration */"))
|
||||
}
|
||||
@@ -0,0 +1,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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"
|
||||
@@ -1,11 +0,0 @@
|
||||
# Bash Driver
|
||||
|
||||
* Runs bash scripts. What you do in the scripts is up to you.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
migrate -url bash://xxx -path ./migrations create increment_xyz
|
||||
migrate -url bash://xxx -path ./migrations up
|
||||
migrate help # for more info
|
||||
```
|
||||
@@ -1,36 +0,0 @@
|
||||
// Package bash implements the Driver interface.
|
||||
package bash
|
||||
|
||||
import (
|
||||
"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{})
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package bash
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMigrate(t *testing.T) {
|
||||
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
# Cassandra Driver
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
migrate -url cassandra://host:port/keyspace -path ./db/migrations create add_field_to_table
|
||||
migrate -url cassandra://host:port/keyspace -path ./db/migrations up
|
||||
migrate help # for more info
|
||||
```
|
||||
|
||||
Url format
|
||||
- Authentication: `cassandra://username:password@host:port/keyspace`
|
||||
- Cassandra v3.x: `cassandra://host:port/keyspace?protocol=4`
|
||||
|
||||
|
||||
## Authors
|
||||
|
||||
* Paul Bergeron, https://github.com/dinedal
|
||||
* Johnny Bergström, https://github.com/balboah
|
||||
* pateld982, http://github.com/pateld982
|
||||
@@ -1,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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
# Crate driver
|
||||
|
||||
This is a driver for the [Crate](https://crate.io) database. It is based on the Crate
|
||||
sql driver by [herenow](https://github.com/herenow/go-crate).
|
||||
|
||||
This driver does not use transactions! This is not a limitation of the driver, but a
|
||||
limitation of Crate. So handle situations with failed migrations with care!
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
migrate -url http://host:port -path ./db/migrations create add_field_to_table
|
||||
migrate -url http://host:port -path ./db/migrations up
|
||||
migrate help # for more info
|
||||
```
|
||||
@@ -1,115 +0,0 @@
|
||||
// Package crate implements a driver for the Crate.io database
|
||||
package crate
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
_ "github.com/herenow/go-crate"
|
||||
"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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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{})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
# MySQL Driver
|
||||
|
||||
### See [issue #1](https://github.com/mattes/migrate/issues/1#issuecomment-58728186) before using this driver!
|
||||
|
||||
* Runs migrations in transcations.
|
||||
That means that if a migration failes, it will be safely rolled back.
|
||||
* Tries to return helpful error messages.
|
||||
* Stores migration version details in table ``schema_migrations``.
|
||||
This table will be auto-generated.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
migrate -url mysql://user@tcp(host:port)/database -path ./db/migrations create add_field_to_table
|
||||
migrate -url mysql://user@tcp(host:port)/database -path ./db/migrations up
|
||||
migrate help # for more info
|
||||
```
|
||||
|
||||
See full [DSN (Data Source Name) documentation](https://github.com/go-sql-driver/mysql/#dsn-data-source-name).
|
||||
|
||||
## Authors
|
||||
|
||||
* Matthias Kadenbach, https://github.com/mattes
|
||||
@@ -1,185 +0,0 @@
|
||||
// Package mysql implements the Driver interface.
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-sql-driver/mysql"
|
||||
"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{})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
# Neo4j Driver
|
||||
|
||||
* Runs migrations in transcations.
|
||||
That means that if a migration failes, it will be safely rolled back.
|
||||
|
||||
* Stores migration version details with the label ``SchemaMigrations``.
|
||||
An unique constraint for the field :SchemaMigrations(version) will be auto-generated.
|
||||
|
||||
* Neo4j cannot perform schema and data updates in a transaction, therefore it's necessary to use different migration files
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
migrate -url neo4j://user:password@host:port/db/data -path ./db/migrations create add_field_to_table
|
||||
migrate -url neo4j://user:password@host:port/db/data -path ./db/migrations up
|
||||
migrate help # for more info
|
||||
```
|
||||
## Author
|
||||
|
||||
* Carlos Forero, https://github.com/carlosforero
|
||||
@@ -1,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{})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
# PostgreSQL Driver
|
||||
|
||||
* Runs migrations in transactions.
|
||||
That means that if a migration fails, it will be safely rolled back.
|
||||
* Tries to return helpful error messages.
|
||||
* Stores migration version details in table ``schema_migrations``.
|
||||
This table will be auto-generated.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
migrate -url postgres://user@host:port/database -path ./db/migrations create add_field_to_table
|
||||
migrate -url postgres://user@host:port/database -path ./db/migrations up
|
||||
migrate help # for more info
|
||||
|
||||
# TODO(mattes): thinking about adding some custom flag to allow migration within schemas:
|
||||
-url="postgres://user@host:port/database?schema=name"
|
||||
|
||||
# see more docs: https://godoc.org/github.com/lib/pq#hdr-Connection_String_Parameters
|
||||
```
|
||||
|
||||
## Authors
|
||||
|
||||
* Matthias Kadenbach, https://github.com/mattes
|
||||
@@ -1,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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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{})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user