Compare commits

..
302 Commits
Author SHA1 Message Date
Dale HuiandGitHub fd50054781 Merge pull request #145 from bobrovde/fix-clickhouse
Changed clickhouse migrations table struct
2018-12-30 13:42:39 -08:00
DBobrov 26064905d4 Changed migrations table struct of field version to fix support database.NilVersion and default version format. 2018-12-28 15:53:55 +03:00
Dale Hui 24176463f4 Update docker/docker checksum for Go 1.11.4
Addresses: https://github.com/golang-migrate/migrate/issues/138
2018-12-21 12:53:53 -08:00
Dale HuiandGitHub 418d41c2a1 Merge pull request #143 from coolaj86/url-opaque
use URL.Opaque when available
2018-12-20 21:37:54 -08:00
AJ ONeal 01291f7cb5 use URL.Opaque when available 2018-12-20 15:08:49 -07:00
Dale HuiandGitHub e8b8975ec3 Merge pull request #141 from fsouza/upgrade-mysql-driver
Upgrade go-sql-driver/mysql to v1.4.1
2018-12-19 11:31:31 -08:00
Francisco Souza 6a53d7d10d Upgrade go-sql-driver/mysql to v1.4.1
Fixes #139.
2018-12-18 18:01:52 -05:00
Dale HuiandGitHub 65870ff3e8 Merge pull request #126 from a-h/patch-1
Include details about migration locking in the FAQ
2018-12-14 17:36:07 -08:00
6f1479fa41 Update FAQ.md
Co-Authored-By: a-h <a-h@users.noreply.github.com>
2018-12-14 16:28:04 +00:00
Dale Hui f694b616aa Add supported major versions table to README
- Consolidates info about migrate major versions
    - https://github.com/golang-migrate/migrate/issues/134#issuecomment-446664565
2018-12-13 22:53:07 -08:00
Dale Hui 1bd41c2a0b README copy tweak
- helpers -> snippets
2018-12-13 22:41:26 -08:00
Dale HuiandGitHub f1d88ac6c1 Add guide for installing a specific version of migrate using the Go toolchain 2018-11-28 01:00:46 -08:00
Dale HuiandGitHub 0d5d918563 Fix CLI install with Go toolchain docs
`-d` option shouldn't be specified with `go get` when installing
2018-11-28 00:44:59 -08:00
Dale Hui 547bc41026 Merge branch 'deb' 2018-11-27 13:54:07 -08:00
Dale Hui d27dc63026 Move before_deploy steps to after_success to avoid re-building the binary before every deploy
- https://docs.travis-ci.com/user/job-lifecycle/#deploying-your-code
2018-11-27 13:48:57 -08:00
Dale Hui 83f5b9f644 Don't need to move and remove files before caching build data in Travis 2018-11-27 13:48:57 -08:00
Dale Hui addb3c1a79 .deb package changes
- Install binary to /usr/local/bin instead of /usr/bin
    - Support recent/stable Debian and Ubuntu distros that work with .deb packages
2018-11-27 13:48:57 -08:00
Dale HuiandGitHub 88e7eafcf9 Merge pull request #131 from vearutop/cli-layout
Refactor cli package to allow `go get` one-liner with a proper binary name
2018-11-27 12:15:59 -08:00
Viacheslav Poturaev 5f88d4bf22 Copy cli main package under migrate directory, keep deprecated original main package for backwards compatibility, fixes #70 2018-11-21 09:01:35 +01:00
Dale HuiandGitHub 7f00868584 Merge pull request #128 from andrei-m/fix-redshift
Fix Redshift migrations driver
2018-11-14 10:32:07 -08:00
Dale Hui 6ebc6c7dae Unlock in postgres db driver should also use the schema name 2018-11-14 02:18:57 -08:00
Dale Hui f38fe38d45 Use git describe --tags to set the version number when building the CLI 2018-11-12 02:23:08 -08:00
Dale Hui 323427e951 Update copyright in LICENSE 2018-11-08 01:45:19 -08:00
Dale Hui 06d3ebde1c Use strings.NewReader() instead of bytes.NewReader() when source is a string
- Group imports
2018-11-05 22:50:08 -08:00
Dale HuiandGitHub 1df8057f97 Merge pull request #127 from vporoshok/master
Add support for multi-schema migrations in Postgres
2018-11-05 22:34:54 -08:00
Evgeniy Bastrykov d2d449ad78 Review comments 2018-11-05 16:03:54 +04:00
Evgeniy Bastrykov 16d63e3a76 Add support for multi-schema migrations in Postgres
There is lock conflict on parallel migrations in different postgres
schemas. To avoid this conflicts function GenerateAdvisoryLockId added
variadic params to change lock id with schema name. Schema name taked
with postgres CURRENT_SCHEMA function. Null byte used as separator
between database and schema name, because any other symbol may be used
in both of it.

Closes #118
2018-11-05 12:56:38 +04:00
Andrei Mackenzie cc573a74dc Remove unused errors 2018-11-03 19:27:55 -04:00
Andrei Mackenzie 1d54cf5f18 Replace 'redshift' with 'redshift2'
This addresses https://github.com/golang-migrate/migrate/issues/90 . The
exported Redshift object no longer exports an embedde 'Driver' however,
so some more work is needed to make this backwards compatible.
2018-11-03 19:01:37 -04:00
Andrei Mackenzie 4e098f74cd Use 'DELETE FROM' over 'TRUNCATE' in Redshift
TRUNCATE commits the current transaction, which breaks the expection of
the 'Commit()' that follows.

See:
https://github.com/golang-migrate/migrate/issues/90
https://docs.aws.amazon.com/redshift/latest/dg/r_TRUNCATE.html
2018-11-03 18:52:09 -04:00
Andrei Mackenzie b0300df5e7 Use the most Redshift-like Postgres version available on Dockerhub
Redshift is based on Postgres version 8.0.2.
2018-11-03 18:49:31 -04:00
Andrei Mackenzie 9c132fe622 Update README with Redshift-specific info 2018-11-03 18:47:12 -04:00
Andrei Mackenzie 22334834ac Support the 'redshift' URL scheme
This brings the 'redshift2' package in alignment with the existing
'redshift' package.
2018-11-03 18:44:11 -04:00
Andrei Mackenzie 3373343f23 Remove advisory locking from the Redshift implementation
Redshift does not support advisory lock functions. The closest
capability is in-transaction table locks, which aren't quite right here
because the transaction scope is established within SetVersion, not
higher up above the Lock-before/Unlock-after SetVersion.

Local locking is left intact to satisfy expected "can't Lock twice
before Unlocking" behavior asserted in shared tests.
2018-11-03 18:31:41 -04:00
Andrei Mackenzie 1d0bedc454 Rename 'Postgres' to 'Redshift' in 'redshift2' 2018-11-03 18:09:53 -04:00
Andrei Mackenzie 8381ea0307 Clone the 'postgres' driver as 'redshift2'
Avoid stepping on the 'redshift' driver for the time being. Driver
registration is also modified to identify the clone as 'redshift2'
rather than 'postgres'.
2018-11-03 18:04:22 -04:00
Adrian HeskethandGitHub 2e0b820637 Include details about migration locking in the FAQ 2018-11-02 15:41:41 +00:00
Dale HuiandGitHub 9f5e1bd505 Merge pull request #124 from lopezator/master
cli: Update README.md with MacOS install info
2018-10-29 11:02:56 -07:00
Dale Hui 7db5c9a274 Remove references to dep from docs 2018-10-29 10:56:55 -07:00
David López 4fad212a25 cli: Update README.md with MacOS install info
Document in MacOS install section of README.md that brew packag is now available. References mattes/migrate#156 https://github.com/Homebrew/homebrew-core/pull/30032
2018-10-29 17:27:48 +01:00
Dale HuiandGitHub f6b3bf548d Merge pull request #121 from wmetaw/master
Add NewDB to spanner.go
2018-10-28 16:58:22 -07:00
Ryo Takashima ca888b96e3 Add NewDB to spanner.go 2018-10-28 22:00:05 +09:00
Dale HuiandGitHub 2fc1610248 Merge pull request #115 from weszeloos/add_support_for_tls_in_cassandra
Add support for TLS on Cassandra.
2018-10-16 15:09:30 -07:00
Wessel Oosthuizen bc28cee1dc Add support for TLS on Cassandra. 2018-10-16 11:18:38 +02:00
Dale Hui ea1928c904 Update dependencies 2018-10-15 13:26:18 -07:00
Dale HuiandGitHub 8c3f34e0c9 Merge pull request #114 from albenik/master
github.com/docker/docker package updated to api v1.37
2018-10-15 13:09:43 -07:00
Veniamin Albaev 956441176f github.com/docker/docker package updated to api v1.37
To avoid github.com/sirupsen/logrus package name case collision with go mod
2018-10-15 17:39:43 +03:00
Dale Hui 83d01b7e1c Run go mod tidy 2018-10-10 16:16:23 -07:00
Dale Hui fc2b2e9c64 Use v4 for Go module support 2018-10-10 16:16:23 -07:00
Dale Hui 60b45ba428 Revert "Docker builds need to use dep to manage dependencies"
This reverts commit 4937cd088f.
2018-10-10 16:16:23 -07:00
Dale Hui 107db3f407 Revert "Remove support for Go modules"
This reverts commit 5e96539f55.
2018-10-10 16:15:33 -07:00
Dale Hui 4937cd088f Docker builds need to use dep to manage dependencies 2018-10-10 16:11:44 -07:00
Dale Hui 5e96539f55 Remove support for Go modules 2018-10-10 13:34:03 -07:00
Dale HuiandGitHub 708d67fa02 Merge pull request #112 from xiamengyu/master
Fix typo in comments
2018-10-10 13:26:36 -07:00
xiamengyu1 a95b13d804 Fix typo 2018-10-10 16:26:27 +08:00
Dale HuiandGitHub 1e60261d3f Merge pull request #110 from MikeFitzgerald/master
Fix postgres drop
2018-10-04 00:30:33 -07:00
Mike Fitzgerald 0d4f1c0212 Fix postgres drop 2018-10-04 01:22:39 -04:00
Dale HuiandGitHub 4d30c4c87f Add Go version to but report template 2018-09-19 03:10:08 -07:00
Dale Hui 6c02d65563 Update commented out imports to use v3 2018-09-14 14:12:07 -07:00
Dale Hui 16dea48c82 Fix module support by opting in to modules 2018-09-14 01:14:42 -07:00
Dale Hui f517029483 Test latest version of Go as well, but allow failures 2018-09-13 16:14:43 -07:00
Dale Hui 6f233b3b58 Update dependencies 2018-09-13 16:14:43 -07:00
Dale HuiandGitHub f8c85735f4 Merge pull request #98 from Kay-Zee/Kay-Zee/close-sql-db-connections-on-driver-close
Add sql.DB to Postgres and Mysql structs to allow closing in Close()
2018-09-07 01:26:43 -07:00
Kay-Zee 1e4c50b70d Add sql.DB to Postgres and Mysql structs to allow closing in Close() 2018-09-06 17:16:02 -07:00
Dale Hui 93d53a5ae8 Use Go 1.11 (with modules enabled on alpine linux 3.8) for docker builds 2018-09-04 22:18:49 -07:00
Dale Hui 16f2b1736e Support Go 1.11 and drop support for Go 1.9
- Support modules in Go 1.11
2018-09-04 19:11:19 -07:00
Dale Hui 4d60607b0b Update issue templates 2018-08-29 12:14:54 -07:00
Dale Hui beb09d85fa Upgrade dep from v0.4.1 to v0.5.0 2018-08-15 12:08:25 -07:00
Dale HuiandGitHub 8ed902364c Merge pull request #91 from stabacov/master
Fixes MySQL custom TLS connection failure.
2018-08-15 00:03:14 -07:00
Tatsuhito KATO 02c83fb38b more descriptive. 2018-08-15 02:42:18 +00:00
Tatsuhito KATO 9049e49f9e x-tls-cert and x-tls-key must set both. 2018-08-14 07:43:14 +00:00
Tatsuhito KATO 9d36dd9841 Add detail of tls query. 2018-08-14 07:42:10 +00:00
Tatsuhito KATO 9905791932 Fixes MySQL custom TLS connection failure. 2018-08-13 11:43:19 +00:00
Dale Hui dd00ca926e Add support for postgres 10 and drop support for postgres 9.2
https://www.postgresql.org/support/versioning/
2018-08-12 15:54:21 -07:00
Dale HuiandGitHub 4092e160a4 Merge pull request #85 from HaraldNordgren/improve_tests
Update tests for 'Migrate', 'Steps' and 'UpAndDown' to assert exact migration sequence
2018-08-08 15:57:04 -07:00
Dale HuiandGitHub 6092802b3a Merge pull request #88 from HaraldNordgren/error_output
Improve error output for missing or malformed '-database' and '-source' parameters
2018-08-08 15:24:39 -07:00
Harald Nordgren 42f074062b Update tests for 'Migrate', 'Steps' and 'UpAndDown' to assert exact migration sequence 2018-08-08 22:24:37 +02:00
Harald Nordgren 112191042b Improve error output for missing or malformed '-database' and '-source' parameters 2018-08-08 21:27:12 +02:00
Dale Hui 1566dd06cd Pass the error through instead of overriding it 2018-08-07 16:22:05 -07:00
Dale HuiandGitHub 251bf76be4 Merge pull request #89 from HaraldNordgren/improve_documentation
Document that '-source' is a required parameter
2018-08-07 15:59:20 -07:00
Harald Nordgren a6eddccfe5 Document that '-source' is a required parameter 2018-08-07 09:53:36 +02:00
Dale Hui 3bd43bedad Reference mattes/migrate now that golang-migrate/migrate has been detached
https://github.com/golang-migrate/migrate/issues/81
2018-08-07 00:17:39 -07:00
Dale HuiandGitHub 4750e57ce1 Merge pull request #82 from UnitedTraders/clickhouse-multiline
Add support for the multiline clickhouse queries
2018-08-06 04:09:10 -06:00
Anton Markelov df7ce674e9 add support for the multiline clickhouse queries 2018-08-03 16:08:47 +10:00
Dale Hui e968e8a5f0 Merge branch 'update_pkgs' 2018-07-24 18:30:53 -07:00
Dale HuiandGitHub c8705dd876 Merge pull request #78 from dhui/mysql_dsn
Allow DB connection URLs to contain encoded reserved URL characters
2018-07-24 18:22:04 -07:00
Dale Hui c7a29e331f Another round of package updates couldn't hurt... 2018-07-24 18:19:22 -07:00
Dale Hui ffcbbd5fea Override dependency on docker/distribution to fix build
https://github.com/golang/dep/issues/1890#issuecomment-404036309
2018-07-24 18:13:11 -07:00
Dale Hui ebf9c0bf91 Update package dependencies 2018-07-24 18:13:11 -07:00
Dale Hui df658e8fa8 Don't urlencode the username and password for MySQL
Addresses: https://github.com/golang-migrate/migrate/pull/69
2018-07-24 17:56:17 -07:00
Dale Hui b5edb8e50c Improve error messaging when URL parsing fails
- Improve docs around escaping/encoding reserved URL characters
    - Add tests for net/url Parse() and reserved characters to document understanding
    - Addresses: https://github.com/golang-migrate/migrate/issues/77
2018-07-24 17:55:58 -07:00
Dale Hui 97eb0d191b Fix broken GenerateAdvisoryLockId() test
- Add it to the test suite to ensure that it doesn't break again
2018-07-24 17:55:58 -07:00
Dale Hui 857d7a620d Revert "Merge pull request #69 from bcho/fix/mysql-url-password-encode"
This reverts commit 78d696c1e5, reversing
changes made to 18583d5a91.
2018-07-24 17:55:58 -07:00
Dale HuiandGitHub fad64ed381 Merge pull request #76 from agquick/master
Support for multiple statements in Cassandra cql migration files
2018-07-24 16:02:44 -07:00
Alan Graham 4244ebea6a Add detail to README for Cassandra flag, improve if block 2018-07-24 19:30:44 +00:00
Alan Graham 327822c29a Added Cassandra URL flag to enable multi statement migrations 2018-07-20 13:27:54 +00:00
Dale HuiandGitHub feea45c205 Merge pull request #75 from LUSHDigital/master
Lock go-sql-driver/mysql to v1.4.0
2018-07-17 16:42:01 -07:00
Alan Graham c4b81d1210 Support for multiple statements in Cassandra cql migration files based on semi-colon and newline. 2018-07-17 15:19:54 +00:00
Dan Richards 3988074cae Lock digest. 2018-07-16 12:09:29 +01:00
Dan Richards ad2b69e115 Typo 2018-07-16 12:08:24 +01:00
Dan Richards f9eb960848 Just lock go-sql-driver/mysql. 2018-07-16 12:06:27 +01:00
Dan Richards 5927d6e5f4 Locked go-sql-driver/mysql to v1.4.0 2018-07-16 12:01:52 +01:00
Dale Hui 6bb3e494e5 Disable CGO to build binaries statically on the building platform
- Previously, only cross-compiled binaries were built statically
    - Addresses: https://github.com/golang-migrate/migrate/issues/45
2018-07-14 13:31:59 -07:00
Dale Hui 5d5d15e7a3 Link to comparison of transactional DDLs 2018-07-09 19:25:29 -07:00
Dale Hui b742ec3de4 Use unix line endings 2018-07-09 19:21:40 -07:00
Dale Hui 75f0aade0d Reduce docker image size by removing unnecessary chown
https://github.com/moby/moby/issues/5505
2018-07-09 14:52:59 -07:00
Dale HuiandGitHub 3872c8f41d Merge pull request #71 from qdentity/postgres-parse-error
Add postgres lib/pq error parsing
2018-07-09 12:26:24 -07:00
Damir Vandic ae2fd4fa7b Support postgres non-ascii error parsing 2018-07-09 11:39:29 +02:00
Damir Vandic 3eb26a65d3 Add postgres lib/pq error parsing 2018-07-07 12:17:38 +02:00
Dale Hui 157433893c Update CLI installation docs to reduce confusion
https://github.com/golang-migrate/migrate/issues/64#issuecomment-402300146
2018-07-05 10:55:44 -07:00
Dale Hui 236dac0c96 Add latest tag to docker image during build 2018-07-03 14:09:26 -07:00
Dale HuiandGitHub 78d696c1e5 Merge pull request #69 from bcho/fix/mysql-url-password-encode
prevent net/url encoding the user password
2018-06-29 14:17:45 -07:00
Chao Dengandhbc 48a4062f75 prevent net/url encoding the user password 2018-06-29 21:07:10 +08:00
Dale Hui 18583d5a91 Update docs
- Addresses: https://github.com/golang-migrate/migrate/issues/67
    - Add badge for supported Go versions
    - Suggest dep for dependency management
2018-06-27 11:45:54 -07:00
Dale HuiandGitHub 642913dbf4 Merge pull request #66 from dadlerj/cli-readme
Update CLI usage in readme
2018-06-26 13:25:18 -07:00
dadlerj 2ca5473e52 Update CLI usage in readme 2018-06-26 09:16:04 -07:00
Dale Hui 6070aa8281 Push latest image to docker hub 2018-06-19 01:42:37 -07:00
Dale HuiandGitHub 78dc468555 Merge pull request #47 from dhui/docker_badge
Add badge for num docker pulls
2018-06-19 01:33:06 -07:00
Dale Hui bcd996f3df Use environment variables since there's no way to pass secure data to deploy script provider 2018-06-19 00:39:54 -07:00
Dale Hui a1d959d0e9 Use latest version of docker 2018-06-18 23:07:54 -07:00
Dale Hui 71e97bccc7 Fix TravisCI conditional release 2018-06-18 21:42:31 -07:00
Dale Hui 3239b18671 Fix time format flag to be backwards compatible
https://github.com/golang-migrate/migrate/pull/17#issuecomment-397960867
2018-06-18 00:06:17 -07:00
Dale Hui c3794da4ed Make -ext flag required for the create command 2018-06-17 23:18:23 -07:00
Dale Hui 872a561495 Fix broken CLI build 2018-06-17 23:16:38 -07:00
Dale Hui d1adc8b15f Add golang/x/tools (godoc/vfs) as a dependency 2018-06-17 19:59:39 -07:00
Dale HuiandGitHub e7d20b35ee Merge pull request #57 from SamWhited/vfs
source/vfs: add virtual file system source
2018-06-17 19:22:06 -07:00
Dale Hui 21f566a60a Add docker hub credentials for uploading docker image 2018-06-17 17:42:03 -07:00
Sam Whited 7562e9ad5d source/vfs: add virtual file system source
Fixes #56
2018-06-17 19:35:13 -05:00
Dale Hui 0f16f7f6a2 Rename sources to reflect updated build constraint names 2018-06-12 16:45:56 +08:00
Dale Hui dfa9fc05cb Use correct build contraints (must be alphanumeric)
Fixes: https://github.com/golang-migrate/migrate/issues/52
2018-06-12 16:18:27 +08:00
Dale Hui c4e855b58c List source and database drivers in help text 2018-06-12 16:17:52 +08:00
Dale Hui 0d9f636356 Use dep to build docker image 2018-06-11 19:18:05 +08:00
Dale HuiandGitHub 387f79be13 Merge pull request #48 from dhui/static_build
Build binaries statically
2018-06-04 09:24:35 -07:00
Dale Hui 87f3e4a910 Build binaries statically
Addresses: https://github.com/golang-migrate/migrate/issues/45
2018-06-04 19:27:37 +08:00
Dale Hui 01ea635328 Add badge for num docker pulls 2018-06-04 18:54:43 +08:00
Dale HuiandGitHub 3d1bb8a4b4 Merge pull request #13 from Teddy-Schmitz/postgres-wait
postgres driver should wait for lock
2018-05-31 19:04:30 -07:00
Teddy Schmitz 046d1cb767 postgres driver should wait for lock 2018-06-01 09:11:24 +08:00
Dale Hui 3d7b06e24c Override google cloud package version (was pinned to v0.19.0 by github.com/fsouza/fake-gcs-server) 2018-05-31 15:26:18 -07:00
Dale Hui 063b69db2f Update OpenCensus dependency 2018-05-31 15:05:31 -07:00
Dale Hui c0b0829d5a Use "vanity" import to allow forks to build
https://docs.travis-ci.com/user/languages/go/#Go-Import-Path
2018-05-31 14:57:38 -07:00
Dale Hui 3cf42dd36d Don't need to re-solve package dependencies every time. Only use Gopkg.lock
Reference: https://github.com/golang/dep/blob/master/docs/ensure-mechanics.md#-no-vendor-and--vendor-only
2018-05-31 14:57:38 -07:00
Dale Hui 89e558ee4f Don't cache go source since dependencies are managed by dep now 2018-05-31 14:57:38 -07:00
Dale Hui f277ef7428 Update github.com/go-sql-driver/mysql and github.com/gocql/gocql 2018-05-31 14:57:38 -07:00
Dale Hui b7647a749e Use the master branch for go-sql-driver/mysql since there hasn't been a release in a yr and a half 2018-05-31 14:57:38 -07:00
Dale Hui 4cc6aeb274 Update gocql package 2018-05-31 14:57:38 -07:00
Dale Hui 3de903f51f Use latest minor revision of Go 1.10 in TravisCI 2018-05-31 14:57:38 -07:00
Dale Hui 4b4f49b1eb Update docs regarding usage of dep 2018-05-31 14:57:38 -07:00
Dale Hui 4407720b95 Remove "deps" target from Makefile 2018-05-31 14:57:38 -07:00
Dale Hui 36dc939f91 Use dep for TravisCI builds 2018-05-31 14:57:38 -07:00
Dale Hui 40cd326ed7 Use dep to manage dependencies 2018-05-31 14:57:38 -07:00
Dale Hui a890d44d2f CockroachDB driver improvements
- Correctly return database.ErrLocked when the DB is locked
    - Group imports
    - gofmt goodness
2018-05-31 14:44:48 -07:00
Dale HuiandGitHub c98426505e Merge pull request #43 from wmetaw/master
Fixed unkeyed fields in file.go for GoogleAppEngine
2018-05-31 14:23:14 -07:00
Ryo Takashima 6fa8039b55 Fixed unkeyed fields for GoogleAppEngine 2018-05-31 15:40:15 +09:00
Dale HuiandGitHub faf4307fcc Merge pull request #32 from kenjones-cisco/feature/cassandra-withinstance
Minor tweaks to remove duplication
2018-05-17 01:01:47 -07:00
kenjones 1512e41e41 Minor tweaks to remove duplication
Adds missing connection close for cassandra tests

Revert to default timeout of 600ms
2018-05-16 20:09:12 -04:00
Dale Hui 55a25c5e0e Cassandra DB driver test improvements
- shorten connect timeout to allow more connection retry attempts
    - close session after it's created
    - group imports
2018-05-16 15:12:36 -07:00
Dale HuiandGitHub f815731412 Merge pull request #30 from Decemberlabs/master
Add WithSession helper for Cassandra driver
2018-05-16 14:26:12 -07:00
Andrés Rodríguez d607064fd2 Switch Cassandra's package lock to instance lock 2018-05-16 16:26:23 -03:00
Andrés Rodríguez 8b0f876416 Rename Cassandra's WithSession to WithInstance 2018-05-16 16:23:21 -03:00
Andrés Rodríguez bbd8f410d9 Correct method name in comment 2018-05-16 10:54:56 -03:00
Dale Hui fb3d809e81 Cleanup CLI docs 2018-05-12 15:25:19 -07:00
Dale HuiandGitHub 95db494b30 Merge pull request #28 from tayhalla/patch-1
Update README.md
2018-05-12 14:58:12 -07:00
Taylor HallidayandGitHub 5ebdb74ba8 Update README.md
There's no homebrew support. Even the referenced TODO says so, so not sure why it's in the Readme. Let's encourage others to support the development of it rather than think the support for it is broken.
2018-05-12 07:22:21 -07:00
Dale HuiandGitHub 07c20c5357 Merge pull request #27 from Its-Alex/master
Fix dockerfile version and deps
2018-05-10 01:13:05 -07:00
ItsAlex 31daa8dad3 fix dockerfile version and deps
Signed-off-by: Alexandre MARRE <me@itsalex.fr>
2018-05-10 09:12:23 +02:00
Dale HuiandGitHub a0b03c3a68 Merge pull request #24 from Its-Alex/master
Dockerization
2018-05-09 11:22:34 -07:00
ItsAlex fb7bb4f87a add dockerization Fix #23
Signed-off-by: Alexandre MARRE <me@itsalex.fr>
2018-05-09 09:48:20 +02:00
Dale HuiandGitHub 81e9fff605 Merge pull request #22 from thurt/add-ref-to-github
add ref (SHA, branch, or tag) to the github migration source
2018-05-04 10:13:18 +02:00
thurt e6baca2ffb add ref (SHA, branch, or tag) to the github migration source 2018-05-04 03:16:12 +00:00
Dale HuiandGitHub 2b28a0bc0a Merge pull request #17 from bobmeister/cli_create_yyyymmddhhmmss_migration_files
-datetime option to create files in yyyymmddhhmmss format
2018-04-17 10:50:48 -07:00
Robert Haupt 1f389f1422 Removed commented code 2018-04-17 11:17:10 -06:00
Robert Haupt 8cdee40695 Use Go time format string if version other than timestamp is desired. 2018-04-17 11:09:08 -06:00
Robert Haupt c6c6874d72 Added -datetime option to create up/down files with version in yyyymmddhhmmss format. 2018-04-16 11:12:06 -06:00
Andrés Rodríguez 78c47074a3 Add WithSession helper for Cassandra driver
Also changes method receivers names for the Cassandra struct to "c"
since "p" makes no sense in this context.
2018-03-13 15:49:08 -03:00
Dale Hui 22f249514d Merge branch 'postgres-lock-fix'
Addresses: https://github.com/golang-migrate/migrate/pull/4
2018-02-20 16:12:54 -08:00
Dale HuiandGitHub 8f6826c9e3 Merge pull request #8 from rdallman/fix-lockyloo-golang
Fix lockyloo golang
2018-02-20 15:51:18 -08:00
Dale Hui 1519c59188 Rename Postgres.db to Postgres.conn 2018-02-20 15:40:20 -08:00
Dale HuiandGitHub a9176e9969 Merge branch 'master' into fix-lockyloo-golang 2018-02-20 15:37:20 -08:00
Dale Hui ab7dfb38ac Merge remote-tracking branch 'origin/master' into postgres-lock-fix 2018-02-20 15:32:34 -08:00
Dale Hui aa21a21647 Support Go 1.10 and drop support for 1.8
- Test using latest minor versions of the last 2 major (supported) releases
        - TravisCI's gimme doesn't support Go 1.10.x until a minor version is released
    - Explicitly use YAML strings so Go versions aren't interpreted as numbers
2018-02-20 15:20:43 -08:00
Reed Allman 094bad431e s/db/conn/ 2018-02-09 15:14:05 -08:00
Reed Allman a8ef2b8cfe update CI and build tags to go1.9 2018-02-09 00:32:47 -08:00
Reed Allman 636c911d33 fixes mysql lock failure
I believe this closes #297 as well.

I have been working on adding testing of migrations and it requires acquiring
the lock in mysql multiple times to go up and down. After nailing this down to
GET_LOCK returning a failure for every subsequent GET_LOCK call after the
first, I decided it was time to rtfm and lo and behold:

https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_release-lock

RELEASE_LOCK will not work if called from a different thread than GET_LOCK.
The simplest solution using the golang database/sql pkg appears to be to just
get a single conn to use for every operation. since migrations are happening
sequentially, I don't think this will be a performance hit (possible
improvement). now calling Lock() and Unlock() multiple times will work;
prior to this patch, every call to RELEASE_LOCK would fail. this required
minimal changes to use the *sql.Conn methods instead of the *sql.DB methods.

other changes:

* upped time out to 10s on GET_LOCK, 1s timeout can too easily leave us in a
state where we think we have the lock but it has timed out (during the
operation).
* fixes SetVersion which was not using the tx it was intending to, and fixed a
bug where the transaction could have been left open since Rollback or Commit
may never have been called.

I added a test but it does not seem to come up in the previous patch, at least
easily, I tried some shenanigans. At least, this behavior should be fixed with
this patch and hopefully the test / comment is advisory enough.

thank you for maintaining this library, it has been relatively easy to
integrate with and most stuff works (pg works great :)
2018-02-09 00:27:01 -08:00
Dale Hui 53dd06057f Add release badge 2018-01-21 02:07:25 -08:00
Dale Hui 1b840d9c79 Remove unused invalid formatter 2018-01-21 02:04:36 -08:00
Dale Hui 012f051f2e Use cached badges when possible and make badges reflect the state of the master branch 2018-01-21 01:40:41 -08:00
Dale Hui 8684cad808 Update README to reflect the current status of the sqlite driver 2018-01-21 00:20:10 -08:00
Dale Hui 15611d4559 Fix cli builds
- Removed sqlite3 support from binaries
        - It was never working anyways: https://github.com/mattes/migrate/issues/244
    - Don't use cgo as is requires a cross-compiler to build for other platforms
        - cgo was originally added for this reason: https://github.com/mattes/migrate/pull/238#issuecomment-306291988
2018-01-20 23:59:36 -08:00
Dale Hui d542e144dc Manually fetch github.com/kshvakov/clickhouse 2018-01-20 10:30:57 -08:00
Dale Hui f8fd159fbc Fork packagecloud releases 2018-01-20 02:10:22 -08:00
Dale Hui 2eb9570b52 Build release binaries (using G 1.9) and upload to Github using new key 2018-01-20 01:49:02 -08:00
Dale Hui f5ae4b42db Support creating migrations using sequences in addition to timestamps 2018-01-20 00:29:06 -08:00
Dale Hui 24dd870559 MySQL and Postgres db driver test improvements
- Log unexpected connection errors while waiting for the docker image to start
    - Don't leave connections open
2018-01-20 00:11:35 -08:00
Dale Hui 97ef1b8393 Remove redundant delay since the readyFn is sufficient 2018-01-20 00:11:35 -08:00
Dale Hui bdb332a5f8 Remove unused slowReader 2018-01-19 22:38:32 -08:00
Dale Hui 8cbdc28e66 Source code needs to be kept for goveralls to use but removed before caching in travis-ci 2018-01-19 20:53:20 -08:00
Dale Hui 4ebf080da8 Speed up travis-ci builds by caching dependencies 2018-01-19 20:32:15 -08:00
Dale Hui 53606261bf Make coverage directory configurable 2018-01-19 20:32:15 -08:00
Dale Hui 50d049d5f5 Only use coverage profiles from test runs to create combined coverage profile.
- Prevent issue where the combined coverage profile is combined with itself
2018-01-19 18:04:46 -08:00
Daniel CormierandDale Hui 395391525b Changed source.Up from untyped to a typed const
Changed source.Up from being an untyped string const to being a typed source.Direction const.
2018-01-19 16:24:25 -08:00
Dale Hui 59f3f6d6c2 Increase test timeout from default 10m to 20m since some tests are slow as molasses in January
- Example build being killed by Go's test binary: https://travis-ci.org/golang-migrate/migrate/jobs/331027558
2018-01-19 15:42:02 -08:00
Dale Hui 12cc536541 Drop support for Go 1.7
- Only the latest 2 major release version is supported. https://golang.org/doc/devel/release.html#policy
2018-01-19 15:36:13 -08:00
Dale Hui 51ec421ad8 Remove debugging print from mysql driver test 2018-01-19 15:13:07 -08:00
Dale Hui 1cf095c51d Update more references to github.com/mattes/migrate 2018-01-19 14:59:27 -08:00
Dale Hui 7c9a261dc0 Fix MySQL docker image connection testing 2018-01-19 14:50:31 -08:00
Dale Hui 31367fbc97 Add tip about dealing with docker client API version issues 2018-01-19 14:31:56 -08:00
Dale Hui 7159f40304 Update all *DockerContainer receivers to handle nil
- Use errors.New() instead of fmt.Errorf() if no formatter string is needed
    - Spacing changes due to `go fmt`
2018-01-19 14:22:55 -08:00
Dale Hui 042e307bef Ignore vendor dir 2018-01-19 10:57:17 -08:00
Dale Hui f584949470 Update imports to reference fork.
Thanks `make rewrite-import-paths`!
2018-01-19 10:56:55 -08:00
Dale Hui 303cd74e5f Fix Docker install on TravisCi by allowing the docker-ce package to be downgraded
- Started happening since TravisCI updated docker-ce https://docs.travis-ci.com/user/build-environment-updates/2017-12-12/
    - Can't use v17.09 since the git tag doesn't exist yet
2018-01-19 02:28:41 -08:00
Dale Hui 3b61abf20a Update some examples and badges in README to reference new fork 2018-01-19 02:07:34 -08:00
Matthias KadenbachandGitHub d23f71b03c Merge pull request #305 from zikes/master
use DELETE FROM to avoid schema changes within transaction
2017-12-08 13:48:26 -08:00
Jason Hutchinson c156f5f3a1 replace TRUNCATE with DELETE FROM to avoid schema changes within transaction 2017-11-02 16:59:26 -05:00
Teddy Schmitz 155a8b241c Use db.Conn to fix postgres lock/unlock 2017-10-27 10:56:10 +08:00
Matthias KadenbachandGitHub 69472d5f5c Merge pull request #301 from shaneodonnell/master
Issue 247: Removed '(always included)'
2017-10-24 11:00:00 -07:00
Shane O'Donnell 4ac3ee9d75 Removed '(always included)' 2017-10-23 13:27:24 -04:00
Matthias KadenbachandGitHub 5b98c13eff Merge pull request #281 from ashoda/support_multiple_statements
(Spanner) Add support for multiple statements in one migration step
2017-10-09 11:03:59 -07:00
Ashod Ayanyan 28cb030ffe Add support for multiple DDL/SQL statements in one migration file 2017-09-18 15:43:46 -07:00
Matthias KadenbachandGitHub be1b075605 add go 1.9 for tests 2017-09-18 14:55:49 -07:00
Matthias KadenbachandGitHub 62cddb9e74 Merge pull request #278 from JensRantil/cassandra-auth
feat(cassandra): support for user/pw authentication
2017-09-18 10:08:58 -07:00
Matthias KadenbachandGitHub eabba5b316 Merge pull request #277 from JensRantil/cleanup
Various `gofmt`ing
2017-09-18 10:08:34 -07:00
Jens Rantil a5e584ce22 feat(cassandra): support for user/pw authentication 2017-09-17 13:32:00 +02:00
Jens Rantil d25732a53a sty: avoid unnecessary parenthesis
Golang best-practise. Cleanup made by `gofmt`.
2017-09-17 13:30:08 +02:00
Jens Rantil 701bd68731 style(cassandra): whitespace fixes
According to `gofmt`.
2017-09-17 13:26:30 +02:00
Jens Rantil 51acd3f3ad style: order imports lexicographically
Best-practise according to `gofmt`.
2017-09-17 13:26:23 +02:00
Matthias KadenbachandGitHub 804f9c2d4f Merge pull request #273 from maknahar/master
Fixed a couple of typos
2017-08-14 14:01:06 -07:00
maknahar 81c0a17f20 Fixed a couple of typos 2017-08-14 17:55:33 +05:30
Matthias KadenbachandGitHub 2570d5866d Merge pull request #272 from twrobel3/bugfix/cockroachTxnSig
Update cockroachDB Txn signatures
2017-08-11 17:11:41 -07:00
Taylor Wrobel 2c0f4f588e Update cockroachDB Txn signatures
cockroach-go made a backwards-incompatible change to their transaction
function signatures, which was causing the cockroach instrumentation
to fail.

Updates the signature to match the cockraoch-go change.
2017-08-10 22:56:41 -07:00
Matthias KadenbachandGitHub 8b5365e62c Merge pull request #267 from nicolas33/FAQ-extension
Faq extension
2017-07-30 15:05:05 -07:00
Nicolas Sebrecht d837bcc60c FAQ: add required extension
Github-fix: #265
Signed-off-by: Nicolas Sebrecht <nicolas.sebrecht@1001pneus.fr>
2017-07-28 14:30:10 +02:00
Nicolas Sebrecht bfe502654c FAQ: update format from dos to unix
Signed-off-by: Nicolas Sebrecht <nicolas.sebrecht@1001pneus.fr>
2017-07-28 14:24:46 +02:00
Matthias KadenbachandGitHub 6632d110b6 Merge pull request #264 from twrobel3/feature/migrations-readme
Fill out MIGRATIONS.md with migration format and best practice notes
2017-07-25 23:05:42 -07:00
Taylor Wrobel 341c35b1b8 Fill out MIGRATIONS.md with migration format and best practice notes 2017-07-19 00:18:02 -07:00
Matthias KadenbachandGitHub 8e6084bc9c Merge pull request #263 from twrobel3/feature/crdb-readme
Add cockroachdb README and add force lock functionality
2017-07-12 08:48:03 -07:00
Taylor Wrobel 40a40613cb Add cockroachdb README and add ForceLock functionality
Adds a readme to the cockroachdb database package, following the
postgres readme style.

Also adds the ability to force acquisition of the migration lock
via a connect URL parameter/WithInstance config, to allow for fixing
cases where an implementation error causes the schema lock to not
be released.

Lastly, tweaks the CLI readme to include information on building a
CLI for databases other than postgres.
2017-07-11 21:52:02 -07:00
Watanabe MasaruandMatthias Kadenbach 23a2745a3a fix spanner ensure migration table 2017-07-09 10:57:10 -07:00
Matthias KadenbachandGitHub 88c6a0a752 Merge pull request #253 from kshvakov/master
Add ClickHouse driver
2017-07-08 10:11:23 -07:00
Kirill ShvakovandGitHub 1f62e3f142 Merge branch 'master' into master 2017-07-08 13:50:51 +03:00
Kirill Shvakov 8424e946e4 remove deprecated group from Travis file 2017-07-08 13:49:23 +03:00
Matthias KadenbachandGitHub 66a0448698 Merge pull request #255 from twrobel3/feature/cockroachdb
Add CockroachDB Support
2017-07-08 00:26:12 -07:00
Taylor Wrobel 2cab359222 Remove debugging output from cockroachDB tests 2017-07-07 23:13:55 -07:00
Taylor Wrobel aebc0c3cdd Merge port mapping helper functions into one 2017-07-07 23:12:07 -07:00
Taylor Wrobel c2925c40c7 Add tests for CockroachDB 2017-07-07 23:12:07 -07:00
Taylor Wrobel 88115dedbf Support running docker with commands and handling multiple exposed ports
Adds the ability to specify a series of commands to run as part
of the docker image execution, and allows for retrieving a mapping
of an exposed via the port bound within the container.
2017-07-07 23:12:07 -07:00
Taylor Wrobel 1d8a881941 Add CockroachDB Support
Adds support for CockroachDB.  Cockroach uses the postges wire
protocol and has a large amount of common SQL functionality shared
with Postgres, so much of the postgres code was able to be copied
and modified.

Since the protocol is used in determining the driver, and the
Postgres protocol is also used by Cockroach, new connect string
prefixes were added: cockroach:// cockroachdb:// and
crdb-postgres://.  These fake protocol strings are replaced in
the connect function with the correct `postgres://` protocol.

TODO: Tests needed (Cockroach has a docker image, so this shouldn't
be too hard)
2017-07-07 23:12:07 -07:00
Matthias KadenbachandGitHub 8439d7195f Merge pull request #262 from twrobel3/feature/fixTravisBuild
Update docker dependency version to fix Tavis builds
2017-07-07 22:45:46 -07:00
Matthias KadenbachandGitHub f0f8b2c85c Merge pull request #260 from martin-magakian/patch-1
Update README.md
2017-07-03 22:21:55 -07:00
Martin MagakianandGitHub 35af42a0dd Update README.md 2017-07-03 15:45:57 +02:00
Martin MagakianandGitHub 56a56527c6 Update README.md
Late edit I promise...
2017-07-03 15:45:41 +02:00
Martin MagakianandGitHub 1485e567f1 Update README.md
Some format :-p
2017-07-03 15:35:43 +02:00
Martin MagakianandGitHub 180c57747d Update README.md
Forget package
2017-07-03 15:34:43 +02:00
Martin MagakianandGitHub 2361a7aa24 Update README.md
Code snippet don't compile.
It's missing a step (see https://github.com/mattes/migrate/blob/master/source/go-bindata/go-bindata_test.go)
2017-07-03 15:29:15 +02:00
Taylor Wrobel 4f9422ec85 Update docker dependency version to fix Tavis builds
Tavis has updated its docker images, and as part of the update,
no longer includes support for the (deprevated) docker-engine
package, moving instead to docker-ce.

See https://blog.travis-ci.com/2017-06-21-trusty-updates-2017-Q2-launch
for details of the package change.

This updates the travis configuration to pin the docker library
version to 17.05.0 and installs the corresponding version of
docker-ce in the install script.
2017-06-24 10:37:08 -07:00
kshvakov 9d17987b8c Travis CI: using the previous Ubuntu Trusty images (https://blog.travis-ci.com/2017-06-21-trusty-updates-2017-Q2-launch) 2017-06-23 12:16:14 +03:00
kshvakov 94056e51cc postgres: fix SetVersion. All requests were executed in different connections so commit/rollback doesn't work correctly 2017-06-23 11:13:54 +03:00
Kirill Shvakov 8cd9761d29 fix: set current database 2017-06-21 23:10:43 +03:00
kshvakov e86f4a201c change include path from github.com/kshvakov to github.com/mattes 2017-06-21 17:24:55 +03:00
kshvakov 9341404de0 move ensureVersionTable to init 2017-06-21 17:14:43 +03:00
kshvakov b031b22c80 Add WithInstance 2017-06-21 17:12:05 +03:00
kshvakov 1440a070c3 Add WithInstance 2017-06-21 17:06:04 +03:00
kshvakov 14636b77fb Add ClickHouse driver 2017-06-21 15:58:46 +03:00
Matthias KadenbachandGitHub da4160ce04 Merge pull request #251 from christianklotz/spanner-driver
Add Spanner driver
2017-06-20 21:52:00 -07:00
Christian Klotz eccc3a26d2 Skip test if SPANNER_DATABASE isn’t set 2017-06-20 17:53:41 +01:00
Christian KlotzandGitHub 51afcccb6b Merge branch 'master' into spanner-driver 2017-06-14 21:02:59 +01:00
Stephanie NgandChristian Klotz 8f7d3efb1a add create command to cli 2017-06-14 21:59:11 +02:00
BenandChristian Klotz 13a818e3b5 Fix typo forgotton => forgotten 2017-06-14 21:59:11 +02:00
Matthias KadenbachandChristian Klotz 5e049b9fb7 enable cgo 2017-06-14 21:59:11 +02:00
maxvwandChristian Klotz 33d5038bd3 adapted the sqlite driver for v3 (#165) 2017-06-14 21:58:37 +02:00
Fuyuan BieandChristian Klotz 39bb76224d Fix parenthesis problem. 2017-06-14 21:57:37 +02:00
Fuyuan BieandChristian Klotz 9632882b69 Use both db name and migration table name for lock hash 2017-06-14 21:57:37 +02:00
Christian Klotz 803598293e Add basic Google Spanner test 2017-06-14 19:46:10 +02:00
Matthias KadenbachandGitHub 50068870a1 Merge pull request #249 from recurza/master
Add create command to CLI
2017-06-10 10:25:06 -07:00
Stephanie Ng 9a19439e31 add create command to cli 2017-06-10 20:20:35 +08:00
Matthias KadenbachandGitHub d8dd1bc427 Merge pull request #248 from biefy/master
Solve lock contention problem when upgrading multiple logical MySQL databases backed by a single physical database.
2017-06-09 17:08:54 -07:00
Matthias KadenbachandGitHub c1134be427 Merge pull request #242 from benmoss/master
Fix typo forgotton => forgotten
2017-06-05 13:11:58 -07:00
Matthias KadenbachandGitHub 2aab3394cf Merge pull request #243 from mattes/mattes-patch-1
enable cgo
2017-06-05 13:11:27 -07:00
Ben 39a5a37822 Fix typo forgotton => forgotten 2017-06-05 12:14:10 -04:00
Jolan MalassigneandChristian Klotz 89879968bb add cassandra driver and function to retrieve networkSettings to get port bound to 9042 2017-06-05 11:03:49 +01:00
Pavlina DrososandChristian Klotz 6ecd671cfc Add code sample for use with existing MySQL client with multistatements=true 2017-06-05 11:02:51 +01:00
MaxandChristian Klotz 2f50c9c52b added file cli/build_sqlite3.go 2017-06-05 11:02:51 +01:00
maxvwandChristian Klotz 386ce008d9 adapted the sqlite driver for v3 (#165) 2017-06-05 11:02:51 +01:00
Christian Klotz 3d6c78875f Add example migrations 2017-06-05 10:50:14 +01:00
Matthias KadenbachandGitHub a9bcd83ff0 enable cgo 2017-06-05 00:59:53 -07:00
Matthias KadenbachandGitHub bf3f0f133e Merge pull request #240 from tinysquare/master
Add cassandra driver
2017-06-04 21:27:48 -07:00
Matthias KadenbachandGitHub 2aa16f5f66 Merge branch 'master' into master 2017-06-04 21:27:38 -07:00
Matthias KadenbachandGitHub 63f788f07e Merge pull request #239 from pdrosos/update-mysql-documentation-for-use-with-existing-db-client
Add documentation for use with existing MySQL client with multiStatements=true
2017-06-04 21:23:57 -07:00
Matthias KadenbachandGitHub c3d61852ee Merge pull request #238 from maxvw/v3-sqlite
adapted the sqlite driver for v3 (#165)
2017-06-04 21:21:55 -07:00
Christian Klotz 2742b9c467 Provide WithInstance method
Also includes some refactor around package naming, treats own repo as first-class and uses alternative package names for third party imports, Google spanner libraries in this case.
2017-06-04 21:53:33 +01:00
Christian Klotz 748ae8f06a Add Spanner driver
Support for Google Cloud Spanner, closes #172. Includes example migrations ported over from the PostgreSQL driver.

In all examples provided by Google I’ve come across, camel-case is used for table names, columns, etc. Hence the examples use this naming convention.
2017-06-04 21:13:49 +01:00
Jolan Malassigne 28524d977c add cassandra driver and function to retrieve networkSettings to get port bound to 9042 2017-05-31 10:05:56 +02:00
Pavlina Drosos b5c2f0f2bb Add code sample for use with existing MySQL client with multistatements=true 2017-05-30 18:52:26 +03:00
Max decee4abb2 added file cli/build_sqlite3.go 2017-05-30 14:06:59 +02:00
maxvw 8b4ce58a1d adapted the sqlite driver for v3 (#165) 2017-05-27 08:58:01 +02:00
Matthias KadenbachandGitHub 3682bcf9d2 Update README.md 2017-05-18 19:47:23 -07:00
Matthias KadenbachandGitHub 857dc4b862 Update README.md 2017-05-18 19:45:53 -07:00
Matthias KadenbachandGitHub 247ca90b33 Merge pull request #234 from augustzf/master
Add command to make CLI example with Go toolchain work
2017-05-18 10:29:15 -07:00
Matthias KadenbachandGitHub 00b22e527d one line 2017-05-18 10:28:39 -07:00
August Z. Flatby 83035ec925 Add command to make CLI example with Go toolchain work 2017-05-18 09:27:39 +02:00
Matthias KadenbachandGitHub 035c07716c Merge pull request #231 from mattes/cli-updates
build cli with all source and database drivers
2017-05-17 19:37:49 -07:00
Fuyuan BieandGitHub bcdbe0f018 Fix parenthesis problem. 2017-05-15 22:01:45 -07:00
Fuyuan BieandGitHub 5f2a8b7f30 Use both db name and migration table name for lock hash 2017-05-15 21:50:41 -07:00
154 changed files with 6348 additions and 847 deletions
+13
View File
@@ -0,0 +1,13 @@
# Project
FAQ.md
README.md
LICENSE
Makefile
.gitignore
.travis.yml
CONTRIBUTING.md
MIGRATIONS.md
docker-deploy.sh
# Golang
testing
+39
View File
@@ -0,0 +1,39 @@
---
name: Bug report
about: Create a report to help us improve
---
**Describe the Bug**
A clear and concise description of what the bug is.
**Steps to Reproduce**
Steps to reproduce the behavior:
1. My migrations look like '...'
2. I ran migrate with the following options '....'
3. See error
**Expected Behavior**
A clear and concise description of what you expected to happen.
**Migrate Version**
e.g. v3.4.0
Obtained by running: `migrate -version`
**Loaded Source Drivers**
e.g. s3, github, go-bindata, gcs, file
Obtained by running: `migrate -help`
**Loaded Database Drivers**
e.g. spanner, stub, clickhouse, cockroachdb, crdb-postgres, postgres, postgresql, redshift, cassandra, cockroach, mysql
Obtained by running: `migrate -help`
**Go Version**
e.g. go version go1.11 linux/amd64
Obtained by running: `go version`
**Stacktrace**
Please provide if available
**Additional context**
Add any other context about the problem here.
+17
View File
@@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+1
View File
@@ -4,3 +4,4 @@ cli/cli
cli/migrate
.coverage
.godoc.pid
vendor/
+93 -24
View File
@@ -1,44 +1,60 @@
language: go
sudo: required
go:
- 1.7
- 1.8
matrix:
allow_failures:
- go: master
include:
# Supported versions of Go: https://golang.org/dl/
- go: "1.10.x"
- go: "1.11.x"
- go: master
go_import_path: github.com/golang-migrate/migrate
env:
- MIGRATE_TEST_CONTAINER_BOOT_DELAY=10
global:
- GO111MODULE=on
- MIGRATE_TEST_CONTAINER_BOOT_TIMEOUT=60
- DOCKER_USERNAME=golangmigrate
- secure: "oSOznzUrgr5h45qW4PONkREpisPAt40tnM+KFWtS/Ggu5UI2Ie0CmyYXWuBjbt7B97a4yN9Qzmn8FxJHJ7kk+ABOi3muhkxeIhr6esXbzHhX/Jhv0mj1xkzX7KoVN9oHBz3cOI/QeRyEAO68xjDHNE2kby4RTT9VBt6TQUakKVkqI5qkqLBTADepCjVC+9XhxVxUNyeWKU8ormaUfJBjoNVoDlwXekUPnJenfmfZqXxUInvBCfUyp7Pq+kurBORmg4yc6qOlRYuK67Xw+i5xpjbZouNlXPk0rq7pPy5zjhmZQ3kImoFPvNMeKViDcI6kSIJKtjdhms9/g/6MgXS9HlL5kFy8tYKbsyiHnHB1BsvaLAKXctbUZFDPstgMPADfnad2kZXPrNqIhfWKZrGRWidawCYJ1sKKwYxLMKrtA0umqgMoL90MmBOELhuGmvMV0cFJB+zo+K2YWjEiMGd8xRb5mC5aAy0ZcCehO46jGtpr217EJmMF8Ywr7cFqM2Shg5U2jev9qUpYiXwmPnJKDuoT2ZHuHmPgFIkYiWC5yeJnnmG5bed1sKBp93AFrJX+1Rx5oC4BpNegewmBZKpOSwls/D1uMAeQK3dPmQHLsT6o2VBLfeDGr+zY0R85ywwPZCv00vGol02zYoTqN7eFqr6Qhjr/qx5K1nnxJdFK3Ts="
# 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
cache:
directories:
- $GOPATH/pkg
before_install:
- echo "TRAVIS_GO_VERSION=${TRAVIS_GO_VERSION}"
# Download the binary to bin folder in $GOPATH
- if [[ "${TRAVIS_GO_VERSION}" == 1.10* ]]; then curl -L -s https://github.com/golang/dep/releases/download/v0.5.0/dep-linux-amd64 -o $GOPATH/bin/dep; fi
# Make the binary executable
- if [[ "${TRAVIS_GO_VERSION}" == 1.10* ]]; then chmod +x $GOPATH/bin/dep; fi
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
- if [[ "${TRAVIS_GO_VERSION}" == 1.10* ]]; then dep ensure -vendor-only; fi
- go get github.com/mattn/goveralls
script:
- make test
- make test COVERAGE_DIR=/tmp/coverage
after_success:
- goveralls -service=travis-ci -coverprofile .coverage/combined.txt
- goveralls -service=travis-ci -coverprofile /tmp/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
- fpm -s dir -t deb -n migrate -v "$(git describe --tags 2>/dev/null | cut -c 2-)" --license MIT -m dhui@users.noreply.github.com --url https://github.com/golang-migrate/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/local/bin/migrate
deploy:
- provider: releases
api_key:
secure: EFow50BI448HVb/uQ1Kk2Kq0xzmwIYq3V67YyymXIuqSCodvXEsMiBPUoLrxEknpPEIc67LEQTNdfHBgvyHk6oRINWAfie+7pr5tKrpOTF9ghyxoN1PlO8WKQCqwCvGMBCnc5ur5rvzp0bqfpV2rs5q9/nngy3kBuEvs12V7iho=
secure: hWH1HLPpzpfA8pXQ93T1qKQVFSpQp0as/JLQ7D91jHuJ8p+RxVeqblDrR6HQY/95R/nyiE9GJmvUolSuw5h449LSrGxPtVWhdh6EnkxlQHlen5XeMhVjRjFV0sE9qGe8v7uAkiTfRO61ktTWHrEAvw5qpyqnNISodmZS78XIasPODQbNlzwINhWhDTHIjXGb4FpizYaL3OGCanrxfR9fQyCaqKGGBjRq3Mfq8U6Yd4mApmsE+uJxgaZV8K5zBqpkSzQRWhcVGNL5DuLsU3gfSJOo7kZeA2G71SHffH577dBoqtCZ4VFv169CoUZehLWCb+7XKJZmHXVujCURATSySLGUOPc6EoLFAn3YtsCA04mS4bZVo5FZPWVwfhjmkhtDR4f6wscKp7r1HsFHSOgm59QfETQdrn4MnZ44H2Jd39axqndn5DvK9EcZVjPHynOPnueXP2u6mTuUgh2VyyWBCDO3CNo0fGlo7VJI69IkIWNSD87K9cHZWYMClyKZkUzS+PmRAhHRYbVd+9ZjKOmnU36kUHNDG/ft1D4ogsY+rhVtXB4lgWDM5adri+EIScYdYnB1/pQexLBigcJY9uE7nQTR0U6QgVNYvun7uRNs40E0c4voSfmPdFO0FlOD2y1oQhnaXfWLbu9nMcTcs4RFGrcC7NzkUN4/WjG8s285V6w=
skip_cleanup: true
on:
go: 1.8
repo: mattes/migrate
go: "1.11.x"
repo: golang-migrate/migrate
tags: true
file:
- cli/build/migrate.linux-amd64.tar.gz
@@ -48,14 +64,67 @@ deploy:
- dependency_tree.txt
- provider: packagecloud
repository: migrate
username: mattes
username: golang-migrate
token:
secure: RiHJ/+J9DvXUah/APYdWySWZ5uOOISYJ0wS7xddc7/BNStRVjzFzvJ9zmb67RkyZZrvGuVjPiL4T8mtDyCJCj47RmU/56wPdEHbar/FjsiUCgwvR19RlulkgbV4okBCePbwzMw6HNHRp14TzfQCPtnN4kef0lOI4gZJkImN7rtQ=
secure: aICwu3gJ1sJ1QVCD3elpg+Jxzt4P+Zj1uoh5f0sOwnjDNIZ4FwUT1cMrWloP8P2KD0iyCOawuZER27o/kQ21oX2OxHvQbYPReA2znLm7lHzCmypAAOHPxpgnQ4rMGHHJXd+OsxtdclGs67c+EbdBfoRRbK400Qz/vjPJEDeH4mh02ZHC2nw4Nk/wV4jjBIkIt9dGEx6NgOA17FCMa3MaPHlHeFIzU7IfTlDHbS0mCCYbg/wafWBWcbGqtZLWAYtJDmfjrAStmDLdAX5J5PsB7taGSGPZHmPmpGoVgrKt/tb9Xz1rFBGslTpGROOiO4CiMAvkEKFn8mxrBGjfSBqp7Dp3eeSalKXB1DJAbEXx2sEbMcvmnoR9o43meaAn+ZRts8lRL8S/skBloe6Nk8bx3NlJCGB9WPK1G56b7c/fZnJxQbrCw6hxDfbZwm8S2YPviFTo/z1BfZDhRsL74reKsN2kgnGo2W/k38vvzIpsssQ9DHN1b0TLCxolCNPtQ7oHcQ1ohcjP2UgYXk0FhqDoL+9LQva/DU4N9sKH0UbAaqsMVSErLeG8A4aauuFcVrWRBaDYyTag4dQqzTulEy7iru2kDDIBgSQ1gMW/yoBOIPK4oi6MtbTf1X39fzXFLS1cDd3LW61yAu3YrbjAetpfx2frIvrRAiL9TxWA1gnrs5o=
dist: ubuntu/xenial
package_glob: '*.deb'
skip_cleanup: true
on:
go: 1.8
repo: mattes/migrate
go: "1.11.x"
repo: golang-migrate/migrate
tags: true
- provider: packagecloud
repository: migrate
username: golang-migrate
token:
secure: aICwu3gJ1sJ1QVCD3elpg+Jxzt4P+Zj1uoh5f0sOwnjDNIZ4FwUT1cMrWloP8P2KD0iyCOawuZER27o/kQ21oX2OxHvQbYPReA2znLm7lHzCmypAAOHPxpgnQ4rMGHHJXd+OsxtdclGs67c+EbdBfoRRbK400Qz/vjPJEDeH4mh02ZHC2nw4Nk/wV4jjBIkIt9dGEx6NgOA17FCMa3MaPHlHeFIzU7IfTlDHbS0mCCYbg/wafWBWcbGqtZLWAYtJDmfjrAStmDLdAX5J5PsB7taGSGPZHmPmpGoVgrKt/tb9Xz1rFBGslTpGROOiO4CiMAvkEKFn8mxrBGjfSBqp7Dp3eeSalKXB1DJAbEXx2sEbMcvmnoR9o43meaAn+ZRts8lRL8S/skBloe6Nk8bx3NlJCGB9WPK1G56b7c/fZnJxQbrCw6hxDfbZwm8S2YPviFTo/z1BfZDhRsL74reKsN2kgnGo2W/k38vvzIpsssQ9DHN1b0TLCxolCNPtQ7oHcQ1ohcjP2UgYXk0FhqDoL+9LQva/DU4N9sKH0UbAaqsMVSErLeG8A4aauuFcVrWRBaDYyTag4dQqzTulEy7iru2kDDIBgSQ1gMW/yoBOIPK4oi6MtbTf1X39fzXFLS1cDd3LW61yAu3YrbjAetpfx2frIvrRAiL9TxWA1gnrs5o=
dist: ubuntu/bionic
package_glob: '*.deb'
skip_cleanup: true
on:
go: "1.11.x"
repo: golang-migrate/migrate
tags: true
- provider: packagecloud
repository: migrate
username: golang-migrate
token:
secure: aICwu3gJ1sJ1QVCD3elpg+Jxzt4P+Zj1uoh5f0sOwnjDNIZ4FwUT1cMrWloP8P2KD0iyCOawuZER27o/kQ21oX2OxHvQbYPReA2znLm7lHzCmypAAOHPxpgnQ4rMGHHJXd+OsxtdclGs67c+EbdBfoRRbK400Qz/vjPJEDeH4mh02ZHC2nw4Nk/wV4jjBIkIt9dGEx6NgOA17FCMa3MaPHlHeFIzU7IfTlDHbS0mCCYbg/wafWBWcbGqtZLWAYtJDmfjrAStmDLdAX5J5PsB7taGSGPZHmPmpGoVgrKt/tb9Xz1rFBGslTpGROOiO4CiMAvkEKFn8mxrBGjfSBqp7Dp3eeSalKXB1DJAbEXx2sEbMcvmnoR9o43meaAn+ZRts8lRL8S/skBloe6Nk8bx3NlJCGB9WPK1G56b7c/fZnJxQbrCw6hxDfbZwm8S2YPviFTo/z1BfZDhRsL74reKsN2kgnGo2W/k38vvzIpsssQ9DHN1b0TLCxolCNPtQ7oHcQ1ohcjP2UgYXk0FhqDoL+9LQva/DU4N9sKH0UbAaqsMVSErLeG8A4aauuFcVrWRBaDYyTag4dQqzTulEy7iru2kDDIBgSQ1gMW/yoBOIPK4oi6MtbTf1X39fzXFLS1cDd3LW61yAu3YrbjAetpfx2frIvrRAiL9TxWA1gnrs5o=
dist: ubuntu/cosmic
package_glob: '*.deb'
skip_cleanup: true
on:
go: "1.11.x"
repo: golang-migrate/migrate
tags: true
- provider: packagecloud
repository: migrate
username: golang-migrate
token:
secure: aICwu3gJ1sJ1QVCD3elpg+Jxzt4P+Zj1uoh5f0sOwnjDNIZ4FwUT1cMrWloP8P2KD0iyCOawuZER27o/kQ21oX2OxHvQbYPReA2znLm7lHzCmypAAOHPxpgnQ4rMGHHJXd+OsxtdclGs67c+EbdBfoRRbK400Qz/vjPJEDeH4mh02ZHC2nw4Nk/wV4jjBIkIt9dGEx6NgOA17FCMa3MaPHlHeFIzU7IfTlDHbS0mCCYbg/wafWBWcbGqtZLWAYtJDmfjrAStmDLdAX5J5PsB7taGSGPZHmPmpGoVgrKt/tb9Xz1rFBGslTpGROOiO4CiMAvkEKFn8mxrBGjfSBqp7Dp3eeSalKXB1DJAbEXx2sEbMcvmnoR9o43meaAn+ZRts8lRL8S/skBloe6Nk8bx3NlJCGB9WPK1G56b7c/fZnJxQbrCw6hxDfbZwm8S2YPviFTo/z1BfZDhRsL74reKsN2kgnGo2W/k38vvzIpsssQ9DHN1b0TLCxolCNPtQ7oHcQ1ohcjP2UgYXk0FhqDoL+9LQva/DU4N9sKH0UbAaqsMVSErLeG8A4aauuFcVrWRBaDYyTag4dQqzTulEy7iru2kDDIBgSQ1gMW/yoBOIPK4oi6MtbTf1X39fzXFLS1cDd3LW61yAu3YrbjAetpfx2frIvrRAiL9TxWA1gnrs5o=
dist: debian/stretch
package_glob: '*.deb'
skip_cleanup: true
on:
go: "1.11.x"
repo: golang-migrate/migrate
tags: true
- provider: packagecloud
repository: migrate
username: golang-migrate
token:
secure: aICwu3gJ1sJ1QVCD3elpg+Jxzt4P+Zj1uoh5f0sOwnjDNIZ4FwUT1cMrWloP8P2KD0iyCOawuZER27o/kQ21oX2OxHvQbYPReA2znLm7lHzCmypAAOHPxpgnQ4rMGHHJXd+OsxtdclGs67c+EbdBfoRRbK400Qz/vjPJEDeH4mh02ZHC2nw4Nk/wV4jjBIkIt9dGEx6NgOA17FCMa3MaPHlHeFIzU7IfTlDHbS0mCCYbg/wafWBWcbGqtZLWAYtJDmfjrAStmDLdAX5J5PsB7taGSGPZHmPmpGoVgrKt/tb9Xz1rFBGslTpGROOiO4CiMAvkEKFn8mxrBGjfSBqp7Dp3eeSalKXB1DJAbEXx2sEbMcvmnoR9o43meaAn+ZRts8lRL8S/skBloe6Nk8bx3NlJCGB9WPK1G56b7c/fZnJxQbrCw6hxDfbZwm8S2YPviFTo/z1BfZDhRsL74reKsN2kgnGo2W/k38vvzIpsssQ9DHN1b0TLCxolCNPtQ7oHcQ1ohcjP2UgYXk0FhqDoL+9LQva/DU4N9sKH0UbAaqsMVSErLeG8A4aauuFcVrWRBaDYyTag4dQqzTulEy7iru2kDDIBgSQ1gMW/yoBOIPK4oi6MtbTf1X39fzXFLS1cDd3LW61yAu3YrbjAetpfx2frIvrRAiL9TxWA1gnrs5o=
dist: debian/buster
package_glob: '*.deb'
skip_cleanup: true
on:
go: "1.11.x"
repo: golang-migrate/migrate
tags: true
- provider: script
script: ./docker-deploy.sh
on:
go: "1.11.x"
repo: golang-migrate/migrate
tags: true
+13 -13
View File
@@ -2,21 +2,21 @@
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
1. Use a version of Go that supports [modules](https://golang.org/cmd/go/#hdr-Modules__module_versions__and_more) (e.g. Go 1.11+)
1. Fork this repo and `git clone` somewhere to `$GOPATH/src/github.com/golang-migrate/migrate`
* Ensure that [Go modules are enabled](https://golang.org/cmd/go/#hdr-Preliminary_module_support) (e.g. your repo path or the `GO111MODULE` environment variable are set correctly)
1. Confirm tests are working: `make test-short`
1. Write awesome code ...
1. `make test` to run all tests against all database versions
1. 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`
* 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.
* `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.
* `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.
* Set the `DOCKER_API_VERSION` environment variable to the latest supported version if you get errors regarding the docker client API version being too new.
+23
View File
@@ -0,0 +1,23 @@
FROM golang:1.11-alpine3.8 AS downloader
ARG VERSION
RUN apk add --no-cache git gcc musl-dev
WORKDIR /go/src/github.com/golang-migrate/migrate
COPY . ./
ENV GO111MODULE=on
ENV DATABASES="postgres mysql redshift cassandra spanner cockroachdb clickhouse"
ENV SOURCES="file go_bindata github aws_s3 google_cloud_storage"
RUN go build -a -o build/migrate.linux-386 -ldflags="-X main.Version=${VERSION}" -tags "$DATABASES $SOURCES" ./cmd/migrate
FROM alpine:3.8
RUN apk add --no-cache ca-certificates
COPY --from=downloader /go/src/github.com/golang-migrate/migrate/build/migrate.linux-386 /migrate
ENTRYPOINT ["/migrate"]
CMD ["--help"]
+70 -67
View File
@@ -1,67 +1,70 @@
# 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.
# 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.extension next -> 2_second_migration.up.extension ...
1_first_migration.down.extension <- previous 2_second_migration.down.extension ...
```
#### 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.
#### What happens if two programs try and update the database at the same time?
Database-specific locking features are used by *some* database drivers to prevent multiple instances of migrate from running migrations at the same time
the same database at the same time. For example, the MySQL driver uses the `GET_LOCK` function, while the Postgres driver uses
the `pg_advisory_lock` function.
Generated
+679
View File
@@ -0,0 +1,679 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
digest = "1:4a799555f4a58b849ca3a3bed67c10ca484a2d3cedf4199773211d04b0109793"
name = "cloud.google.com/go"
packages = [
"civil",
"compute/metadata",
"iam",
"internal",
"internal/atomiccache",
"internal/fields",
"internal/optional",
"internal/protostruct",
"internal/trace",
"internal/version",
"longrunning",
"longrunning/autogen",
"spanner",
"spanner/admin/database/apiv1",
"storage",
]
pruneopts = "UT"
revision = "c728a003b238b26cef9ab6753a5dc424b331c3ad"
version = "v0.27.0"
[[projects]]
digest = "1:f9ae348e1f793dcf9ed930ed47136a67343dbd6809c5c91391322267f4476892"
name = "github.com/Microsoft/go-winio"
packages = ["."]
pruneopts = "UT"
revision = "97e4973ce50b2ff5f09635a57e2b88a037aae829"
version = "v0.4.11"
[[projects]]
digest = "1:1d58a675bc7df5691f81b4a28a39d2d218f020b23644a6c7771f7ebaab272ac8"
name = "github.com/aws/aws-sdk-go"
packages = [
"aws",
"aws/awserr",
"aws/awsutil",
"aws/client",
"aws/client/metadata",
"aws/corehandlers",
"aws/credentials",
"aws/credentials/ec2rolecreds",
"aws/credentials/endpointcreds",
"aws/credentials/stscreds",
"aws/csm",
"aws/defaults",
"aws/ec2metadata",
"aws/endpoints",
"aws/request",
"aws/session",
"aws/signer/v4",
"internal/s3err",
"internal/sdkio",
"internal/sdkrand",
"internal/sdkuri",
"internal/shareddefaults",
"private/protocol",
"private/protocol/eventstream",
"private/protocol/eventstream/eventstreamapi",
"private/protocol/query",
"private/protocol/query/queryutil",
"private/protocol/rest",
"private/protocol/restxml",
"private/protocol/xml/xmlutil",
"service/s3",
"service/s3/s3iface",
"service/sts",
]
pruneopts = "UT"
revision = "bf8067ceb6e7f51e150c218972dccfeeed892b85"
version = "v1.15.54"
[[projects]]
branch = "master"
digest = "1:5a75ff130630f57609ffca00d6a76dc31bb50ad6a03809848e0f9e13add31987"
name = "github.com/cockroachdb/cockroach-go"
packages = ["crdb"]
pruneopts = "UT"
revision = "e0a95dfd547cc9c3ebaaba1a12c2afe4bf621ac5"
[[projects]]
branch = "master"
digest = "1:3ba3d94d45f2d5c4b3411452474d96996b7e0dce730b689920f7c307511e5655"
name = "github.com/cznic/b"
packages = ["."]
pruneopts = "UT"
revision = "35e9bbe41f07452a183c517a5fc5f3c9f45eaa0f"
[[projects]]
branch = "master"
digest = "1:4688e1d4b22367c5bd24fe65f44b3e53a443a93fd19cf34169f1c8337d93dcf7"
name = "github.com/cznic/fileutil"
packages = ["."]
pruneopts = "UT"
revision = "6a051e75936f623600b67c2b1116b6b6c0ffb936"
[[projects]]
branch = "master"
digest = "1:2a4afef2ef0e4e4ab66b4f4f6a70bf6551ae8b727184771b5547e2b90245fe58"
name = "github.com/cznic/golex"
packages = ["lex"]
pruneopts = "UT"
revision = "4ab7c5e190e49208c823ce8ec803aa39e6a4b31a"
[[projects]]
digest = "1:a9e2943ed681d4758a43d1bd9e80d7e993b66ce5adc7aa5dcc083ba48a47a4eb"
name = "github.com/cznic/internal"
packages = [
"buffer",
"file",
"slice",
]
pruneopts = "UT"
revision = "cef02a853c3a93623c42eacd574e7ea05f55531b"
version = "1.0.0"
[[projects]]
digest = "1:ec7b58207ac8eee1c554b7947b3f5124f711238a80edb7224a53bae7263c95ae"
name = "github.com/cznic/lldb"
packages = ["."]
pruneopts = "UT"
revision = "bea8611dd5c407f3c5eab9f9c68e887a27dc6f0e"
version = "v1.1.0"
[[projects]]
branch = "master"
digest = "1:42951d361293c2f7068ab0411afeb57856afba843fe681cea1e2731b47021222"
name = "github.com/cznic/mathutil"
packages = ["."]
pruneopts = "UT"
revision = "ca4c9f2c136954238c3158b92de72078c7672ecc"
[[projects]]
digest = "1:336a34e066968a8e30ee73a0d4419f37d3e8286b94f996cddf46a5e82d13b2fa"
name = "github.com/cznic/ql"
packages = [
".",
"driver",
"vendored/github.com/camlistore/go4/lock",
]
pruneopts = "UT"
revision = "47bf73cf8ed137969f12b223b7016784cdd9d42e"
version = "v1.2.0"
[[projects]]
branch = "master"
digest = "1:06877d57543857a87186f43b6195bbdf5e8f4a0f51412fdf4baef504cead7973"
name = "github.com/cznic/sortutil"
packages = ["."]
pruneopts = "UT"
revision = "4c7342852e65c2088c981288f2c5610d10b9f7f4"
[[projects]]
branch = "master"
digest = "1:8a26e11e94f1c9f03ab9fc1e816b003629d41b26ab41eefcb7444fad5b24fc4b"
name = "github.com/cznic/strutil"
packages = ["."]
pruneopts = "UT"
revision = "529a34b1c186b483642a7a230c67521d9aa4b0fb"
[[projects]]
branch = "master"
digest = "1:bb8236408f88f411f565dffcf1e648fe34c7456820b707f96ccbfb89e1cd8eeb"
name = "github.com/cznic/zappy"
packages = ["."]
pruneopts = "UT"
revision = "2533cb5b45cc6c07421468ce262899ddc9d53fb7"
[[projects]]
branch = "master"
digest = "1:4ddc17aeaa82cb18c5f0a25d7c253a10682f518f4b2558a82869506eec223d76"
name = "github.com/docker/distribution"
packages = [
"digestset",
"reference",
]
pruneopts = "UT"
revision = "1cb4180b1a5b9c029b2f2beaeb38f0b5cf64e12e"
[[projects]]
digest = "1:47eae8fd493e859163b3cace5b72c83a7f42f7e9aad758b2c747a58331ec24ab"
name = "github.com/docker/docker"
packages = [
"api",
"api/types",
"api/types/blkiodev",
"api/types/container",
"api/types/events",
"api/types/filters",
"api/types/image",
"api/types/mount",
"api/types/network",
"api/types/registry",
"api/types/strslice",
"api/types/swarm",
"api/types/swarm/runtime",
"api/types/time",
"api/types/versions",
"api/types/volume",
"client",
]
pruneopts = "UT"
revision = "453f2b8b40b923862ce1c08c11531ff5042770f1"
[[projects]]
digest = "1:811c86996b1ca46729bad2724d4499014c4b9effd05ef8c71b852aad90deb0ce"
name = "github.com/docker/go-connections"
packages = [
"nat",
"sockets",
"tlsconfig",
]
pruneopts = "UT"
revision = "7395e3f8aa162843a74ed6d48e79627d9792ac55"
version = "v0.4.0"
[[projects]]
digest = "1:6f82cacd0af5921e99bf3f46748705239b36489464f4529a1589bc895764fb18"
name = "github.com/docker/go-units"
packages = ["."]
pruneopts = "UT"
revision = "47565b4f722fb6ceae66b95f853feed578a4a51c"
version = "v0.3.3"
[[projects]]
branch = "master"
digest = "1:67d0b50be0549e610017cb91e0b0b745ec0cad7c613bc8e18ff2d1c1fc8825a7"
name = "github.com/edsrzf/mmap-go"
packages = ["."]
pruneopts = "UT"
revision = "0bce6a6887123b67a60366d2c9fe2dfb74289d2e"
[[projects]]
digest = "1:812c304ad1cbb214459f7a198cc246a9c8a114a6a92063cef7b31c4ec42a5d6c"
name = "github.com/fsouza/fake-gcs-server"
packages = [
"fakestorage",
"internal/backend",
]
pruneopts = "UT"
revision = "9f9efdc50d6ad4314a4ff85646341f2a9b75ae5f"
version = "v1.3.0"
[[projects]]
digest = "1:15e27372d379b45b18ac917b9dafc45c45485239490ece18cca97a12f9591146"
name = "github.com/go-ini/ini"
packages = ["."]
pruneopts = "UT"
revision = "9c8236e659b76e87bf02044d06fde8683008ff3e"
version = "v1.39.0"
[[projects]]
digest = "1:adea5a94903eb4384abef30f3d878dc9ff6b6b5b0722da25b82e5169216dfb61"
name = "github.com/go-sql-driver/mysql"
packages = ["."]
pruneopts = "UT"
revision = "d523deb1b23d913de5bdada721a6071e71283618"
version = "v1.4.0"
[[projects]]
branch = "master"
digest = "1:d21f3471df3c4109c9b7c5569363a082b9fcde4fea49e4ce332ad307cfc4e359"
name = "github.com/gocql/gocql"
packages = [
".",
"internal/lru",
"internal/murmur",
"internal/streams",
]
pruneopts = "UT"
revision = "44e29ed5b8a4b4fff39d7ebaa19976c6f852075b"
[[projects]]
digest = "1:bbadccf3d3317ea03c0dac0b45b673b4b397c8f91a1d2eff550a3c51c4ad770e"
name = "github.com/gogo/protobuf"
packages = ["proto"]
pruneopts = "UT"
revision = "636bf0302bc95575d69441b25a2603156ffdddf1"
version = "v1.1.1"
[[projects]]
digest = "1:ec0bde6dc8cf3cb9254f79603bd8489c366a0a8584949cac5a73fa10b9f47c21"
name = "github.com/golang/protobuf"
packages = [
"proto",
"protoc-gen-go/descriptor",
"ptypes",
"ptypes/any",
"ptypes/duration",
"ptypes/empty",
"ptypes/struct",
"ptypes/timestamp",
]
pruneopts = "UT"
revision = "aa810b61a9c79d51363740d207bb46cf8e620ed5"
version = "v1.2.0"
[[projects]]
branch = "master"
digest = "1:4a0c6bb4805508a6287675fac876be2ac1182539ca8a32468d8128882e9d5009"
name = "github.com/golang/snappy"
packages = ["."]
pruneopts = "UT"
revision = "2e65f85255dbc3072edf28d6b5b8efc472979f5a"
[[projects]]
digest = "1:f3fb942d9a52aff79191a8318317147ab76ca609bced931b8be9ab1825569993"
name = "github.com/google/go-github"
packages = ["github"]
pruneopts = "UT"
revision = "747f93dd9b489bd56f7442b3c42bfea12800a75b"
version = "v17.0.0"
[[projects]]
digest = "1:a63cff6b5d8b95638bfe300385d93b2a6d9d687734b863da8e09dc834510a690"
name = "github.com/google/go-querystring"
packages = ["query"]
pruneopts = "UT"
revision = "44c6ddd0a2342c386950e880b658017258da92fc"
version = "v1.0.0"
[[projects]]
digest = "1:e145e9710a10bc114a6d3e2738aadf8de146adaa031854ffdf7bbfe15da85e63"
name = "github.com/googleapis/gax-go"
packages = ["."]
pruneopts = "UT"
revision = "317e0006254c44a0ac427cc52a0e083ff0b9622f"
version = "v2.0.0"
[[projects]]
digest = "1:c79fb010be38a59d657c48c6ba1d003a8aa651fa56b579d959d74573b7dff8e1"
name = "github.com/gorilla/context"
packages = ["."]
pruneopts = "UT"
revision = "08b5f424b9271eedf6f9f0ce86cb9396ed337a42"
version = "v1.1.1"
[[projects]]
digest = "1:e73f5b0152105f18bc131fba127d9949305c8693f8a762588a82a48f61756f5f"
name = "github.com/gorilla/mux"
packages = ["."]
pruneopts = "UT"
revision = "e3702bed27f0d39777b0b37b664b6280e8ef8fbf"
version = "v1.6.2"
[[projects]]
branch = "master"
digest = "1:364b908b9b27b97ab838f2f6f1b1f46281fa29b978a037d72a9b1d4f6d940190"
name = "github.com/hailocab/go-hostpool"
packages = ["."]
pruneopts = "UT"
revision = "e80d13ce29ede4452c43dea11e79b9bc8a15b478"
[[projects]]
digest = "1:8dfbec76b4834d96c37ba4c4969fc47c9a15e67c5a7eecfa031fbe67dab50429"
name = "github.com/jackc/pgx"
packages = [
".",
"chunkreader",
"internal/sanitize",
"pgio",
"pgproto3",
"pgtype",
]
pruneopts = "UT"
revision = "89f1e6ac7276b61d885db5e5aed6fcbedd1c7e31"
version = "v3.2.0"
[[projects]]
digest = "1:e22af8c7518e1eab6f2eab2b7d7558927f816262586cd6ed9f349c97a6c285c4"
name = "github.com/jmespath/go-jmespath"
packages = ["."]
pruneopts = "UT"
revision = "0b12d6b5"
[[projects]]
digest = "1:f02dbf2124e5dfb5305a6c3c8957a41d54a89d2a3ef60f030769f86e7851d5f5"
name = "github.com/kshvakov/clickhouse"
packages = [
".",
"lib/binary",
"lib/column",
"lib/data",
"lib/protocol",
"lib/types",
"lib/writebuffer",
]
pruneopts = "UT"
revision = "aeedb7b9d0584f393905447021238b5c6e9c5154"
version = "v1.3.4"
[[projects]]
digest = "1:8ef506fc2bb9ced9b151dafa592d4046063d744c646c1bbe801982ce87e4bc24"
name = "github.com/lib/pq"
packages = [
".",
"oid",
]
pruneopts = "UT"
revision = "4ded0e9383f75c197b3a2aaa6d590ac52df6fd79"
version = "v1.0.0"
[[projects]]
digest = "1:3cafc6a5a1b8269605d9df4c6956d43d8011fc57f266ca6b9d04da6c09dee548"
name = "github.com/mattn/go-sqlite3"
packages = ["."]
pruneopts = "UT"
revision = "25ecb14adfc7543176f7d85291ec7dba82c6f7e4"
version = "v1.9.0"
[[projects]]
digest = "1:ee4d4af67d93cc7644157882329023ce9a7bcfce956a079069a9405521c7cc8d"
name = "github.com/opencontainers/go-digest"
packages = ["."]
pruneopts = "UT"
revision = "279bed98673dd5bef374d3b6e4b09e2af76183bf"
version = "v1.0.0-rc1"
[[projects]]
digest = "1:11db38d694c130c800d0aefb502fb02519e514dc53d9804ce51d1ad25ec27db6"
name = "github.com/opencontainers/image-spec"
packages = [
"specs-go",
"specs-go/v1",
]
pruneopts = "UT"
revision = "d60099175f88c47cd379c4738d158884749ed235"
version = "v1.0.1"
[[projects]]
digest = "1:40e195917a951a8bf867cd05de2a46aaf1806c50cf92eebf4c16f78cd196f747"
name = "github.com/pkg/errors"
packages = ["."]
pruneopts = "UT"
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
version = "v0.8.0"
[[projects]]
digest = "1:8e7fcf54cc7c0c3aba7801812ae70343585b3cb663f6cb11ad50c0c28552f359"
name = "go.opencensus.io"
packages = [
".",
"internal",
"internal/tagencoding",
"plugin/ocgrpc",
"plugin/ochttp",
"plugin/ochttp/propagation/b3",
"stats",
"stats/internal",
"stats/view",
"tag",
"trace",
"trace/internal",
"trace/propagation",
"trace/tracestate",
]
pruneopts = "UT"
revision = "79993219becaa7e29e3b60cb67f5b8e82dee11d6"
version = "v0.17.0"
[[projects]]
branch = "master"
digest = "1:56975096c005d57a2c9c2faefbec772c44c29c084af35facbfca85961469ff6d"
name = "golang.org/x/net"
packages = [
"context",
"context/ctxhttp",
"http/httpguts",
"http2",
"http2/hpack",
"idna",
"internal/socks",
"internal/timeseries",
"proxy",
"trace",
]
pruneopts = "UT"
revision = "49bb7cea24b1df9410e1712aa6433dae904ff66a"
[[projects]]
branch = "master"
digest = "1:39273b3e6c96fed52bd8f7793fc463c40b1a27a7caa51302b81d45306b9646b4"
name = "golang.org/x/oauth2"
packages = [
".",
"google",
"internal",
"jws",
"jwt",
]
pruneopts = "UT"
revision = "c57b0facaced709681d9f90397429b9430a74754"
[[projects]]
branch = "master"
digest = "1:f5aa274a0377f85735edc7fedfb0811d3cbc20af91633797cb359e29c3272271"
name = "golang.org/x/sys"
packages = [
"unix",
"windows",
]
pruneopts = "UT"
revision = "fa43e7bc11baaae89f3f902b2b4d832b68234844"
[[projects]]
digest = "1:a2ab62866c75542dd18d2b069fec854577a20211d7c0ea6ae746072a1dccdd18"
name = "golang.org/x/text"
packages = [
"collate",
"collate/build",
"internal/colltab",
"internal/gen",
"internal/tag",
"internal/triegen",
"internal/ucd",
"language",
"secure/bidirule",
"transform",
"unicode/bidi",
"unicode/cldr",
"unicode/norm",
"unicode/rangetable",
]
pruneopts = "UT"
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
version = "v0.3.0"
[[projects]]
branch = "master"
digest = "1:fc393bc931c39e70d322b5158a90ef38e1a1617afdffb4efaaa4567f5c4f0410"
name = "golang.org/x/tools"
packages = [
"godoc/vfs",
"godoc/vfs/mapfs",
]
pruneopts = "UT"
revision = "8919434dde1e35f7b9ef589ecb80ec0833f021bc"
[[projects]]
branch = "master"
digest = "1:c8a31b4403741a053bf08594b91189ec63c96b041bdfddef06a272f42549f2cf"
name = "google.golang.org/api"
packages = [
"gensupport",
"googleapi",
"googleapi/internal/uritemplates",
"googleapi/transport",
"internal",
"iterator",
"option",
"storage/v1",
"transport",
"transport/grpc",
"transport/http",
"transport/http/internal/propagation",
]
pruneopts = "UT"
revision = "625cd1887957946515db468ce519bb71fa31fc7f"
[[projects]]
digest = "1:d2cfb607095cee410f25a3c04eed77f76c4721d1bb0cbc894f5d8624b33394c8"
name = "google.golang.org/appengine"
packages = [
".",
"cloudsql",
"internal",
"internal/app_identity",
"internal/base",
"internal/datastore",
"internal/log",
"internal/modules",
"internal/remote_api",
"internal/socket",
"internal/urlfetch",
"socket",
"urlfetch",
]
pruneopts = "UT"
revision = "ae0ab99deb4dc413a2b4bd6c8bdd0eb67f1e4d06"
version = "v1.2.0"
[[projects]]
branch = "master"
digest = "1:315f8fe35ad73727d15b20fc3a533e7ab9ea8f0c3ff6441753d04086e5ca5003"
name = "google.golang.org/genproto"
packages = [
"googleapis/api/annotations",
"googleapis/iam/v1",
"googleapis/longrunning",
"googleapis/rpc/code",
"googleapis/rpc/errdetails",
"googleapis/rpc/status",
"googleapis/spanner/admin/database/v1",
"googleapis/spanner/v1",
]
pruneopts = "UT"
revision = "af9cb2a35e7f169ec875002c1829c9b315cddc04"
[[projects]]
digest = "1:4ad047d772a7d4841687df9534a767a9e243885ed6d7d2ced6af5995a5dc44b3"
name = "google.golang.org/grpc"
packages = [
".",
"balancer",
"balancer/base",
"balancer/roundrobin",
"codes",
"connectivity",
"credentials",
"credentials/oauth",
"encoding",
"encoding/proto",
"grpclog",
"internal",
"internal/backoff",
"internal/channelz",
"internal/envconfig",
"internal/grpcrand",
"internal/transport",
"keepalive",
"metadata",
"naming",
"peer",
"resolver",
"resolver/dns",
"resolver/passthrough",
"stats",
"status",
"tap",
]
pruneopts = "UT"
revision = "8dea3dc473e90c8179e519d91302d0597c0ca1d1"
version = "v1.15.0"
[[projects]]
digest = "1:2d1fbdc6777e5408cabeb02bf336305e724b925ff4546ded0fa8715a7267922a"
name = "gopkg.in/inf.v0"
packages = ["."]
pruneopts = "UT"
revision = "d2d2541c53f18d2a059457998ce2876cc8e67cbf"
version = "v0.9.1"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
input-imports = [
"cloud.google.com/go/spanner",
"cloud.google.com/go/spanner/admin/database/apiv1",
"cloud.google.com/go/storage",
"github.com/aws/aws-sdk-go/aws",
"github.com/aws/aws-sdk-go/aws/session",
"github.com/aws/aws-sdk-go/service/s3",
"github.com/aws/aws-sdk-go/service/s3/s3iface",
"github.com/cockroachdb/cockroach-go/crdb",
"github.com/cznic/ql/driver",
"github.com/docker/docker/api/types",
"github.com/docker/docker/api/types/container",
"github.com/docker/docker/api/types/network",
"github.com/docker/docker/client",
"github.com/fsouza/fake-gcs-server/fakestorage",
"github.com/go-sql-driver/mysql",
"github.com/gocql/gocql",
"github.com/google/go-github/github",
"github.com/kshvakov/clickhouse",
"github.com/lib/pq",
"github.com/mattn/go-sqlite3",
"golang.org/x/net/context",
"golang.org/x/tools/godoc/vfs",
"golang.org/x/tools/godoc/vfs/mapfs",
"google.golang.org/api/iterator",
"google.golang.org/genproto/googleapis/spanner/admin/database/v1",
]
solver-name = "gps-cdcl"
solver-version = 1
+98
View File
@@ -0,0 +1,98 @@
# Gopkg.toml example
#
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true
[[constraint]]
name = "github.com/aws/aws-sdk-go"
version = "1.15.34"
[[constraint]]
branch = "master"
name = "github.com/cockroachdb/cockroach-go"
[[constraint]]
name = "github.com/cznic/ql"
version = "1.2.0"
[[constraint]]
name = "github.com/docker/docker"
revision = "453f2b8b40b923862ce1c08c11531ff5042770f1"
[[constraint]]
name = "github.com/fsouza/fake-gcs-server"
version = "1.2.0"
[[constraint]]
name = "github.com/go-sql-driver/mysql"
version = "v1.4.0"
[[constraint]]
branch = "master"
name = "github.com/gocql/gocql"
[[constraint]]
name = "github.com/google/go-github"
version = "17.0.0"
[[constraint]]
name = "github.com/kshvakov/clickhouse"
version = "1.3.4"
[[constraint]]
name = "github.com/lib/pq"
version = "1.0.0"
[[constraint]]
name = "github.com/mattn/go-sqlite3"
version = "1.9.0"
[[constraint]]
branch = "master"
name = "golang.org/x/net"
[[constraint]]
branch = "master"
name = "google.golang.org/api"
[[constraint]]
branch = "master"
name = "google.golang.org/genproto"
[[override]]
name = "cloud.google.com/go"
version = "0.27.0"
[[override]]
branch = "master"
name = "golang.org/x/tools"
[[override]]
branch = "master"
name = "github.com/docker/distribution"
[prune]
go-tests = true
unused-packages = true
+6 -1
View File
@@ -1,9 +1,14 @@
The MIT License (MIT)
Original Work
Copyright (c) 2016 Matthias Kadenbach
https://github.com/mattes/migrate
Modified Work
Copyright (c) 2018 Dale Hui
https://github.com/golang-migrate/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
in the Software without restriction, including without limitation the rights
+81 -5
View File
@@ -1,5 +1,81 @@
# Migrations
## Best practices: How to write migrations.
@TODO
# Migrations
## Migration Filename Format
A single logical migration is represented as two separate migration files, one
to migrate "up" to the specified version from the previous version, and a second
to migrate back "down" to the previous version. These migrations can be provided
by any one of the supported [migration sources](./README.md#migration-sources).
The ordering and direction of the migration files is determined by the filenames
used for them. `migrate` expects the filenames of migrations to have the format:
{version}_{title}.up.{extension}
{version}_{title}.down.{extension}
The `title` of each migration is unused, and is only for readability. Similarly,
the `extension` of the migration files is not checked by the library, and should
be an appropriate format for the database in use (`.sql` for SQL variants, for
instance).
Versions of migrations may be represented as any 64 bit unsigned integer.
All migrations are applied upward in order of increasing version number, and
downward by decreasing version number.
Common versioning schemes include incrementing integers:
1_initialize_schema.down.sql
1_initialize_schema.up.sql
2_add_table.down.sql
2_add_table.up.sql
...
Or timestamps at an appropriate resolution:
1500360784_initialize_schema.down.sql
1500360784_initialize_schema.up.sql
1500445949_add_table.down.sql
1500445949_add_table.up.sql
...
But any scheme resulting in distinct, incrementing integers as versions is valid.
It is suggested that the version number of corresponding `up` and `down` migration
files be equivalent for clarity, but they are allowed to differ so long as the
relative ordering of the migrations is preserved.
The migration files are permitted to be empty, so in the event that a migration
is a no-op or is irreversible, it is recommended to still include both migration
files, and either leaving them empty or adding a comment as appropriate.
## Migration Content Format
The format of the migration files themselves varies between database systems.
Different databases have different semantics around schema changes and when and
how they are allowed to occur
(for instance, [if schema changes can occur within a transaction](https://wiki.postgresql.org/wiki/Transactional_DDL_in_PostgreSQL:_A_Competitive_Analysis)).
As such, the `migrate` library has little to no checking around the format of
migration sources. The migration files are generally processed directly by the
drivers as raw operations.
## Reversibility of Migrations
Best practice for writing schema migration is that all migrations should be
reversible. It should in theory be possible for run migrations down and back up
through any and all versions with the state being fully cleaned and recreated
by doing so.
By adhering to this recommended practice, development and deployment of new code
is cleaner and easier (cleaning database state for a new feature should be as
easy as migrating down to a prior version, and back up to the latest).
As opposed to some other migration libraries, `migrate` represents up and down
migrations as separate files. This prevents any non-standard file syntax from
being introduced which may result in unintended behavior or errors, depending
on what database is processing the file.
While it is technically possible for an up or down migration to exist on its own
without an equivalently versioned counterpart, it is strongly recommended to
always include a down migration which cleans up the state of the corresponding
up migration.
+14 -19
View File
@@ -1,15 +1,16 @@
SOURCE ?= file go-bindata github aws-s3 google-cloud-storage
DATABASE ?= postgres mysql redshift
SOURCE ?= file go_bindata github aws_s3 google_cloud_storage godoc_vfs
DATABASE ?= postgres mysql redshift cassandra spanner cockroachdb clickhouse
VERSION ?= $(shell git describe --tags 2>/dev/null | cut -c 2-)
TEST_FLAGS ?=
REPO_OWNER ?= $(shell cd .. && basename "$$(pwd)")
COVERAGE_DIR ?= .coverage
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 ./cmd/migrate && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o ../../cli/build/migrate.linux-amd64 -ldflags='-X main.Version=$(VERSION) -extldflags "-static"' -tags '$(DATABASE) $(SOURCE)' .
cd ./cmd/migrate && CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -a -o ../../cli/build/migrate.darwin-amd64 -ldflags='-X main.Version=$(VERSION) -extldflags "-static"' -tags '$(DATABASE) $(SOURCE)' .
cd ./cmd/migrate && CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -a -o ../../cli/build/migrate.windows-amd64.exe -ldflags='-X main.Version=$(VERSION) -extldflags "-static"' -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
@@ -24,11 +25,11 @@ test-short:
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
@-rm -r $(COVERAGE_DIR)
@mkdir $(COVERAGE_DIR)
make test-with-flags TEST_FLAGS='-v -race -covermode atomic -coverprofile $$(COVERAGE_DIR)/_$$(RAND).txt -bench=. -benchmem -timeout 20m'
@echo 'mode: atomic' > $(COVERAGE_DIR)/combined.txt
@cat $(COVERAGE_DIR)/_*.txt | grep -v 'mode: atomic' >> $(COVERAGE_DIR)/combined.txt
test-with-flags:
@@ -37,6 +38,7 @@ test-with-flags:
@go test $(TEST_FLAGS) .
@go test $(TEST_FLAGS) ./cli/...
@go test $(TEST_FLAGS) ./database
@go test $(TEST_FLAGS) ./testing/...
@echo -n '$(SOURCE)' | tr -s ' ' '\n' | xargs -I{} go test $(TEST_FLAGS) ./source/{}
@@ -53,14 +55,7 @@ kill-orphaned-docker-containers:
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
go tool cover -html=$(COVERAGE_DIR)/combined.txt
list-external-deps:
@@ -114,7 +109,7 @@ define external_deps
endef
.PHONY: build-cli clean test-short test test-with-flags deps html-coverage \
.PHONY: build-cli clean test-short test test-with-flags html-coverage \
restore-import-paths rewrite-import-paths list-external-deps release \
docs kill-docs open-docs kill-orphaned-docker-containers
+61 -34
View File
@@ -1,7 +1,11 @@
[![Build Status](https://travis-ci.org/mattes/migrate.svg?branch=master)](https://travis-ci.org/mattes/migrate)
[![GoDoc](https://godoc.org/github.com/mattes/migrate?status.svg)](https://godoc.org/github.com/mattes/migrate)
[![Coverage Status](https://coveralls.io/repos/github/mattes/migrate/badge.svg?branch=v3.0-prev)](https://coveralls.io/github/mattes/migrate?branch=v3.0-prev)
[![packagecloud.io](https://img.shields.io/badge/deb-packagecloud.io-844fec.svg)](https://packagecloud.io/mattes/migrate?filter=debs)
[![Build Status](https://img.shields.io/travis/golang-migrate/migrate/master.svg)](https://travis-ci.org/golang-migrate/migrate)
[![GoDoc](https://godoc.org/github.com/golang-migrate/migrate?status.svg)](https://godoc.org/github.com/golang-migrate/migrate)
[![Coverage Status](https://img.shields.io/coveralls/github/golang-migrate/migrate/master.svg)](https://coveralls.io/github/golang-migrate/migrate?branch=master)
[![packagecloud.io](https://img.shields.io/badge/deb-packagecloud.io-844fec.svg)](https://packagecloud.io/golang-migrate/migrate?filter=debs)
[![Docker Pulls](https://img.shields.io/docker/pulls/migrate/migrate.svg)](https://hub.docker.com/r/migrate/migrate/)
![Supported Go Versions](https://img.shields.io/badge/Go-1.10%2C%201.11-lightgrey.svg)
[![GitHub Release](https://img.shields.io/github/release/golang-migrate/migrate.svg)](https://github.com/golang-migrate/migrate/releases)
# migrate
@@ -9,13 +13,11 @@ __Database migrations written in Go. Use as [CLI](#cli-usage) or import as [libr
* 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.
* 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)?
Forked from [mattes/migrate](https://github.com/mattes/migrate)
## Databases
@@ -24,26 +26,46 @@ 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))
* [Cassandra](database/cassandra)
* [SQLite](database/sqlite3) ([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))
* [Google Cloud Spanner](database/spanner)
* [CockroachDB](database/cockroachdb)
* [ClickHouse](database/clickhouse)
### Database URLs
Database connection strings are specified via URLs. The URL format is driver dependent but generally has the form: `dbdriver://username:password@host:port/dbname?option1=true&option2=false`
Any [reserved URL characters](https://en.wikipedia.org/wiki/Percent-encoding#Percent-encoding_reserved_characters) need to be escaped. Note, the `%` character also [needs to be escaped](https://en.wikipedia.org/wiki/Percent-encoding#Percent-encoding_the_percent_character)
Explicitly, the following characters need to be escaped:
`!`, `#`, `$`, `%`, `&`, `'`, `(`, `)`, `*`, `+`, `,`, `/`, `:`, `;`, `=`, `?`, `@`, `[`, `]`
It's easiest to always run the URL parts of your DB connection URL (e.g. username, password, etc) through an URL encoder. See the example Python snippets below:
```bash
$ python3 -c 'import urllib.parse; print(urllib.parse.quote(input("String to encode: "), ""))'
String to encode: FAKEpassword!#$%&'()*+,/:;=?@[]
FAKEpassword%21%23%24%25%26%27%28%29%2A%2B%2C%2F%3A%3B%3D%3F%40%5B%5D
$ python2 -c 'import urllib; print urllib.quote(raw_input("String to encode: "), "")'
String to encode: FAKEpassword!#$%&'()*+,/:;=?@[]
FAKEpassword%21%23%24%25%26%27%28%29%2A%2B%2C%2F%3A%3B%3D%3F%40%5B%5D
$
```
## 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))
* [Filesystem](source/file) - read from fileystem
* [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
* [AWS S3](source/aws_s3) - read from Amazon Web Services S3
* [Google Cloud Storage](source/google_cloud_storage) - read from Google Cloud Platform Storage
@@ -55,32 +77,35 @@ Source drivers read migrations from local or remote sources. [Add a new source?]
__[CLI Documentation](cli)__
([brew todo #156](https://github.com/mattes/migrate/issues/156))
### Basic usage:
```
$ brew install migrate --with-postgres
$ migrate -database postgres://localhost:5432/database up 2
$ migrate -source file://path/to/migrations -database postgres://localhost:5432/database up 2
```
### Docker usage
```
$ docker run -v {{ migration dir }}:/migrations --network host migrate/migrate
-path=/migrations/ -database postgres://localhost:5432/database up 2
```
## Use in your Go project
* 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)
* API is stable and frozen for this release (v3 & v4).
* Uses [Go modules](https://golang.org/cmd/go/#hdr-Modules__module_versions__and_more) to manage dependencies.
* 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 Documentation](https://godoc.org/github.com/golang-migrate/migrate)__
```go
import (
"github.com/mattes/migrate"
_ "github.com/mattes/migrate/database/postgres"
_ "github.com/mattes/migrate/source/github"
"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/github"
)
func main() {
@@ -97,9 +122,9 @@ Want to use an existing database client?
import (
"database/sql"
_ "github.com/lib/pq"
"github.com/mattes/migrate"
"github.com/mattes/migrate/database/postgres"
_ "github.com/mattes/migrate/source/file"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"
)
func main() {
@@ -123,7 +148,13 @@ Each migration has an up and down migration. [Why?](FAQ.md#why-two-separate-file
[Best practices: How to write migrations.](MIGRATIONS.md)
## Supported Major Versions
Version | Go Modules | Min Go Version | Import | Notes
--------|------------|----------------|--------|------
**master** | :white_check_mark: | 1.10.3 | `import "github.com/golang-migrate/migrate/v4"` | New features and bug fixes arrive here first |
**v4** | :white_check_mark: | 1.10.3 | `import "github.com/golang-migrate/migrate/v4"` | |
**v3** | :x: | 1.10 | `import "github.com/golang-migrate/migrate"` (with package manager) or `import "gopkg.in/golang-migrate/migrate.v3"` (not recommended) | Only security fixes are backported. Only use if your project is not using Go modules. |
## Development and Contributing
@@ -136,8 +167,4 @@ 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
Looking for alternatives? [https://awesome-go.com/#database](https://awesome-go.com/#database).
+34 -14
View File
@@ -2,38 +2,54 @@
## Installation
#### With Go toolchain
#### Download pre-build binary (Windows, MacOS, or Linux)
[Release Downloads](https://github.com/golang-migrate/migrate/releases)
```
$ go get -u -d github.com/mattes/migrate/cli
$ go build -tags 'postgres' -o /usr/local/bin/migrate github.com/mattes/migrate/cli
$ curl -L https://github.com/golang-migrate/migrate/releases/download/$version/migrate.$platform-amd64.tar.gz | tar xvz
```
#### MacOS
([todo #156](https://github.com/mattes/migrate/issues/156))
```
$ brew install migrate --with-postgres
$ brew install golang-migrate
```
#### 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
$ curl -L https://packagecloud.io/golang-migrate/migrate/gpgkey | apt-key add -
$ echo "deb https://packagecloud.io/golang-migrate/migrate/ubuntu/ $(lsb_release -sc) 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)
#### With Go toolchain
##### Unversioned
```
$ curl -L https://github.com/mattes/migrate/releases/download/$version/migrate.$platform-amd64.tar.gz | tar xvz
$ go get -tags 'postgres' -u github.com/golang-migrate/migrate/cmd/migrate
```
##### Versioned
```
$ go get -u -d github.com/golang-migrate/migrate/cmd/migrate
$ cd $GOPATH/src/github.com/golang-migrate/migrate/cmd/migrate
$ git checkout $TAG # e.g. v4.1.0
$ go build -tags 'postgres' -ldflags="-X main.Version=$(git describe --tags)" -o $GOPATH/bin/migrate github.com/golang-migrate/migrate/cmd/migrate
```
##### Notes:
1. Requires a version of Go that [supports modules](https://golang.org/cmd/go/#hdr-Preliminary_module_support). e.g. Go 1.11+
1. This example builds the cli which will only work with postgres. In order
to build the cli for use with other databases, replace the `postgres` build tag
with the appropriate database tag(s) for the databases desired. The tags
correspond to the names of the sub-packages underneath the
[`database`](../database) package.
1. Similarly to the database build tags, if you need to support other sources, use the appropriate build tag(s).
1. Support for build constraints will be removed in the future: https://github.com/golang-migrate/migrate/issues/60
## Usage
@@ -45,7 +61,7 @@ Usage: migrate OPTIONS COMMAND [arg...]
Options:
-source Location of the migrations (driver://url)
-path Shorthand for -source=file://path
-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)
@@ -54,6 +70,10 @@ Options:
-help Print usage
Commands:
create [-ext E] [-dir D] [-seq] [-digits N] [-format] NAME
Create a set of timestamped up/down migrations titled NAME, in directory D with extension E.
Use -seq option to generate sequential up/down migrations with N digits.
Use -format option to specify a Go time format string.
goto V Migrate to version V
up [N] Apply all or N up migrations
down [N] Apply all or N down migrations
@@ -66,7 +86,7 @@ Commands:
So let's say you want to run the first two migrations
```
$ migrate -database postgres://localhost:5432/database up 2
$ migrate -source file://path/to/migrations -database postgres://localhost:5432/database up 2
```
If your migrations are hosted on github
-7
View File
@@ -1,7 +0,0 @@
// +build aws-s3
package main
import (
_ "github.com/mattes/migrate/source/aws-s3"
)
-7
View File
@@ -1,7 +0,0 @@
// +build github
package main
import (
_ "github.com/mattes/migrate/source/github"
)
-7
View File
@@ -1,7 +0,0 @@
// +build go-bindata
package main
import (
_ "github.com/mattes/migrate/source/go-bindata"
)
-7
View File
@@ -1,7 +0,0 @@
// +build google-cloud-storage
package main
import (
_ "github.com/mattes/migrate/source/google-cloud-storage"
)
-7
View File
@@ -1,7 +0,0 @@
// +build mysql
package main
import (
_ "github.com/mattes/migrate/database/mysql"
)
-7
View File
@@ -1,7 +0,0 @@
// +build postgres
package main
import (
_ "github.com/mattes/migrate/database/postgres"
)
-7
View File
@@ -1,7 +0,0 @@
// +build ql
package main
import (
_ "github.com/mattes/migrate/database/ql"
)
-7
View File
@@ -1,7 +0,0 @@
// +build redshift
package main
import (
_ "github.com/mattes/migrate/database/redshift"
)
-81
View File
@@ -1,81 +0,0 @@
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)
}
}
+2 -2
View File
@@ -3,8 +3,8 @@ 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 && \
RUN curl -L https://packagecloud.io/golang-migrate/migrate/gpgkey | apt-key add - && \
echo "deb https://packagecloud.io/golang-migrate/migrate/ubuntu/ xenial main" > /etc/apt/sources.list.d/migrate.list && \
apt-get update && \
apt-get install -y migrate
+3 -205
View File
@@ -1,210 +1,8 @@
package main
import (
"flag"
"fmt"
"os"
"os/signal"
"strconv"
"syscall"
"time"
"github.com/mattes/migrate"
)
// set main log
var log = &Log{}
import "github.com/golang-migrate/migrate/v4/internal/cli"
// Deprecated, please use cmd/migrate
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)
}
cli.Main(Version)
}
+7
View File
@@ -0,0 +1,7 @@
package main
import "github.com/golang-migrate/migrate/v4/internal/cli"
func main() {
cli.Main(Version)
}
+4
View File
@@ -0,0 +1,4 @@
package main
// Version is set in Makefile with build flags
var Version = "dev"
+38
View File
@@ -0,0 +1,38 @@
# Cassandra
* Drop command will not work on Cassandra 2.X because it rely on
system_schema table which comes with 3.X
* Other commands should work properly but are **not tested**
* The Cassandra driver (gocql) does not natively support executing multipe statements in a single query. To allow for multiple statements in a single migration, you can use the `x-multi-statement` param. There are two important caveats:
* This mode splits the migration text into separately-executed statements by a semi-colon `;`. Thus `x-multi-statement` cannot be used when a statement in the migration contains a string with a semi-colon.
* The queries are not executed in any sort of transaction/batch, meaning you are responsible for fixing partial migrations.
## Usage
`cassandra://host:port/keyspace?param1=value&param2=value2`
| URL Query | Default value | Description |
|------------|-------------|-----------|
| `x-migrations-table` | schema_migrations | Name of the migrations table |
| `x-multi-statement` | false | Enable multiple statements to be ran in a single migration (See note above) |
| `port` | 9042 | The port to bind to |
| `consistency` | ALL | Migration consistency
| `protocol` | | Cassandra protocol version (3 or 4)
| `timeout` | 1 minute | Migration timeout
| `username` | nil | Username to use when authenticating. |
| `password` | nil | Password to use when authenticating. |
| `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) |
`timeout` is parsed using [time.ParseDuration(s string)](https://golang.org/pkg/time/#ParseDuration)
## Upgrading from v1
1. Write down the current migration version from schema_migrations
2. `DROP TABLE schema_migrations`
4. Download and install the latest migrate version.
5. Force the current migration version with `migrate force <current_version>`.
+274
View File
@@ -0,0 +1,274 @@
package cassandra
import (
"errors"
"fmt"
"io"
"io/ioutil"
nurl "net/url"
"strconv"
"strings"
"time"
"github.com/gocql/gocql"
"github.com/golang-migrate/migrate/v4/database"
)
func init() {
db := new(Cassandra)
database.Register("cassandra", db)
}
var DefaultMigrationsTable = "schema_migrations"
var (
ErrNilConfig = errors.New("no config")
ErrNoKeyspace = errors.New("no keyspace provided")
ErrDatabaseDirty = errors.New("database is dirty")
ErrClosedSession = errors.New("session is closed")
)
type Config struct {
MigrationsTable string
KeyspaceName string
MultiStatementEnabled bool
}
type Cassandra struct {
session *gocql.Session
isLocked bool
// Open and WithInstance need to guarantee that config is never nil
config *Config
}
func WithInstance(session *gocql.Session, config *Config) (database.Driver, error) {
if config == nil {
return nil, ErrNilConfig
} else if len(config.KeyspaceName) == 0 {
return nil, ErrNoKeyspace
}
if session.Closed() {
return nil, ErrClosedSession
}
if len(config.MigrationsTable) == 0 {
config.MigrationsTable = DefaultMigrationsTable
}
c := &Cassandra{
session: session,
config: config,
}
if err := c.ensureVersionTable(); err != nil {
return nil, err
}
return c, nil
}
func (c *Cassandra) Open(url string) (database.Driver, error) {
u, err := nurl.Parse(url)
if err != nil {
return nil, err
}
// Check for missing mandatory attributes
if len(u.Path) == 0 {
return nil, ErrNoKeyspace
}
cluster := gocql.NewCluster(u.Host)
cluster.Keyspace = strings.TrimPrefix(u.Path, "/")
cluster.Consistency = gocql.All
cluster.Timeout = 1 * time.Minute
if len(u.Query().Get("username")) > 0 && len(u.Query().Get("password")) > 0 {
authenticator := gocql.PasswordAuthenticator{
Username: u.Query().Get("username"),
Password: u.Query().Get("password"),
}
cluster.Authenticator = authenticator
}
// Retrieve query string configuration
if len(u.Query().Get("consistency")) > 0 {
var consistency gocql.Consistency
consistency, err = parseConsistency(u.Query().Get("consistency"))
if err != nil {
return nil, 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 nil, err
}
cluster.ProtoVersion = protoversion
}
if len(u.Query().Get("timeout")) > 0 {
var timeout time.Duration
timeout, err = time.ParseDuration(u.Query().Get("timeout"))
if err != nil {
return nil, err
}
cluster.Timeout = timeout
}
if len(u.Query().Get("sslmode")) > 0 && len(u.Query().Get("sslrootcert")) > 0 && len(u.Query().Get("sslcert")) > 0 && len(u.Query().Get("sslkey")) > 0 {
if u.Query().Get("sslmode") != "disable" {
cluster.SslOpts = &gocql.SslOptions{
CaPath: u.Query().Get("sslrootcert"),
CertPath: u.Query().Get("sslcert"),
KeyPath: u.Query().Get("sslkey"),
}
if u.Query().Get("sslmode") == "verify-full" {
cluster.SslOpts.EnableHostVerification = true
}
}
}
session, err := cluster.CreateSession()
if err != nil {
return nil, err
}
return WithInstance(session, &Config{
KeyspaceName: strings.TrimPrefix(u.Path, "/"),
MigrationsTable: u.Query().Get("x-migrations-table"),
MultiStatementEnabled: u.Query().Get("x-multi-statement") == "true",
})
}
func (c *Cassandra) Close() error {
c.session.Close()
return nil
}
func (c *Cassandra) Lock() error {
if c.isLocked {
return database.ErrLocked
}
c.isLocked = true
return nil
}
func (c *Cassandra) Unlock() error {
c.isLocked = false
return nil
}
func (c *Cassandra) Run(migration io.Reader) error {
migr, err := ioutil.ReadAll(migration)
if err != nil {
return err
}
// run migration
query := string(migr[:])
if c.config.MultiStatementEnabled {
// split query by semi-colon
queries := strings.Split(query, ";")
for _, q := range queries {
tq := strings.TrimSpace(q)
if tq == "" {
continue
}
if err := c.session.Query(tq).Exec(); err != nil {
// TODO: cast to Cassandra error and get line number
return database.Error{OrigErr: err, Err: "migration failed", Query: migr}
}
}
return nil
}
if err := c.session.Query(query).Exec(); err != nil {
// TODO: cast to Cassandra error and get line number
return database.Error{OrigErr: err, Err: "migration failed", Query: migr}
}
return nil
}
func (c *Cassandra) SetVersion(version int, dirty bool) error {
query := `TRUNCATE "` + c.config.MigrationsTable + `"`
if err := c.session.Query(query).Exec(); err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
if version >= 0 {
query = `INSERT INTO "` + c.config.MigrationsTable + `" (version, dirty) VALUES (?, ?)`
if err := c.session.Query(query, version, dirty).Exec(); err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
}
return nil
}
// Return current keyspace version
func (c *Cassandra) Version() (version int, dirty bool, err error) {
query := `SELECT version, dirty FROM "` + c.config.MigrationsTable + `" LIMIT 1`
err = c.session.Query(query).Scan(&version, &dirty)
switch {
case err == gocql.ErrNotFound:
return database.NilVersion, false, nil
case err != nil:
if _, ok := err.(*gocql.Error); ok {
return database.NilVersion, false, nil
}
return 0, false, &database.Error{OrigErr: err, Query: []byte(query)}
default:
return version, dirty, nil
}
}
func (c *Cassandra) Drop() error {
// select all tables in current schema
query := fmt.Sprintf(`SELECT table_name from system_schema.tables WHERE keyspace_name='%s'`, c.config.KeyspaceName)
iter := c.session.Query(query).Iter()
var tableName string
for iter.Scan(&tableName) {
err := c.session.Query(fmt.Sprintf(`DROP TABLE %s`, tableName)).Exec()
if err != nil {
return err
}
}
// Re-create the version table
return c.ensureVersionTable()
}
// Ensure version table exists
func (c *Cassandra) ensureVersionTable() error {
err := c.session.Query(fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (version bigint, dirty boolean, PRIMARY KEY(version))", c.config.MigrationsTable)).Exec()
if err != nil {
return err
}
if _, _, err = c.Version(); err != nil {
return err
}
return nil
}
// ParseConsistency wraps gocql.ParseConsistency
// to return an error instead of a panicking.
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
}
+56
View File
@@ -0,0 +1,56 @@
package cassandra
import (
"fmt"
"strconv"
"testing"
"github.com/gocql/gocql"
dt "github.com/golang-migrate/migrate/v4/database/testing"
mt "github.com/golang-migrate/migrate/v4/testing"
)
var versions = []mt.Version{
{Image: "cassandra:3.0.10"},
{Image: "cassandra:3.0"},
}
func isReady(i mt.Instance) bool {
// Cassandra exposes 5 ports (7000, 7001, 7199, 9042 & 9160)
// We only need the port bound to 9042, but we can only access to the first one
// through 'i.Port()' (which calls DockerContainer.firstPortMapping())
// So we need to get port mapping to retrieve correct port number bound to 9042
portMap := i.NetworkSettings().Ports
port, _ := strconv.Atoi(portMap["9042/tcp"][0].HostPort)
cluster := gocql.NewCluster(i.Host())
cluster.Port = port
cluster.Consistency = gocql.All
p, err := cluster.CreateSession()
if err != nil {
return false
}
defer p.Close()
// Create keyspace for tests
if err = p.Query("CREATE KEYSPACE testks WITH REPLICATION = {'class': 'SimpleStrategy', 'replication_factor':1}").Exec(); err != nil {
return false
}
return true
}
func Test(t *testing.T) {
mt.ParallelTest(t, versions, isReady,
func(t *testing.T, i mt.Instance) {
p := &Cassandra{}
portMap := i.NetworkSettings().Ports
port, _ := strconv.Atoi(portMap["9042/tcp"][0].HostPort)
addr := fmt.Sprintf("cassandra://%v:%v/testks", i.Host(), port)
d, err := p.Open(addr)
if err != nil {
t.Fatalf("%v", err)
}
defer d.Close()
dt.Test(t, d, []byte("SELECT table_name from system_schema.tables"))
})
}
+19
View File
@@ -0,0 +1,19 @@
# ClickHouse
`clickhouse://host:port?username=user&password=qwerty&database=clicks&x-multi-statement=true`
| URL Query | Description |
|------------|-------------|
| `x-migrations-table`| Name of the migrations table |
| `database` | The name of the database to connect to |
| `username` | The user to sign in as |
| `password` | The user's password |
| `host` | The host to connect to. |
| `port` | The port to bind to. |
| `x-multi-statement` | false | Enable multiple statements to be ran in a single migration (See note below) |
## Notes
* The Clickhouse driver does not natively support executing multipe statements in a single query. To allow for multiple statements in a single migration, you can use the `x-multi-statement` param. There are two important caveats:
* This mode splits the migration text into separately-executed statements by a semi-colon `;`. Thus `x-multi-statement` cannot be used when a statement in the migration contains a string with a semi-colon.
* The queries are not executed in any sort of transaction/batch, meaning you are responsible for fixing partial migrations.
+215
View File
@@ -0,0 +1,215 @@
package clickhouse
import (
"database/sql"
"fmt"
"io"
"io/ioutil"
"net/url"
"strings"
"time"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database"
)
var DefaultMigrationsTable = "schema_migrations"
var ErrNilConfig = fmt.Errorf("no config")
type Config struct {
DatabaseName string
MigrationsTable string
MultiStatementEnabled bool
}
func init() {
database.Register("clickhouse", &ClickHouse{})
}
func WithInstance(conn *sql.DB, config *Config) (database.Driver, error) {
if config == nil {
return nil, ErrNilConfig
}
if err := conn.Ping(); err != nil {
return nil, err
}
ch := &ClickHouse{
conn: conn,
config: config,
}
if err := ch.init(); err != nil {
return nil, err
}
return ch, nil
}
type ClickHouse struct {
conn *sql.DB
config *Config
}
func (ch *ClickHouse) Open(dsn string) (database.Driver, error) {
purl, err := url.Parse(dsn)
if err != nil {
return nil, err
}
q := migrate.FilterCustomQuery(purl)
q.Scheme = "tcp"
conn, err := sql.Open("clickhouse", q.String())
if err != nil {
return nil, err
}
ch = &ClickHouse{
conn: conn,
config: &Config{
MigrationsTable: purl.Query().Get("x-migrations-table"),
DatabaseName: purl.Query().Get("database"),
MultiStatementEnabled: purl.Query().Get("x-multi-statement") == "true",
},
}
if err := ch.init(); err != nil {
return nil, err
}
return ch, nil
}
func (ch *ClickHouse) init() error {
if len(ch.config.DatabaseName) == 0 {
if err := ch.conn.QueryRow("SELECT currentDatabase()").Scan(&ch.config.DatabaseName); err != nil {
return err
}
}
if len(ch.config.MigrationsTable) == 0 {
ch.config.MigrationsTable = DefaultMigrationsTable
}
return ch.ensureVersionTable()
}
func (ch *ClickHouse) Run(r io.Reader) error {
migration, err := ioutil.ReadAll(r)
if err != nil {
return err
}
if ch.config.MultiStatementEnabled {
// split query by semi-colon
queries := strings.Split(string(migration), ";")
for _, q := range queries {
tq := strings.TrimSpace(q)
if tq == "" {
continue
}
if _, err := ch.conn.Exec(string(q)); err != nil {
return database.Error{OrigErr: err, Err: "migration failed", Query: []byte(q)}
}
}
return nil
}
if _, err := ch.conn.Exec(string(migration)); err != nil {
return database.Error{OrigErr: err, Err: "migration failed", Query: migration}
}
return nil
}
func (ch *ClickHouse) Version() (int, bool, error) {
var (
version int
dirty uint8
query = "SELECT version, dirty FROM `" + ch.config.MigrationsTable + "` ORDER BY sequence DESC LIMIT 1"
)
if err := ch.conn.QueryRow(query).Scan(&version, &dirty); err != nil {
if err == sql.ErrNoRows {
return database.NilVersion, false, nil
}
return 0, false, &database.Error{OrigErr: err, Query: []byte(query)}
}
return version, dirty == 1, nil
}
func (ch *ClickHouse) SetVersion(version int, dirty bool) error {
var (
bool = func(v bool) uint8 {
if v {
return 1
}
return 0
}
tx, err = ch.conn.Begin()
)
if err != nil {
return err
}
query := "INSERT INTO " + ch.config.MigrationsTable + " (version, dirty, sequence) VALUES (?, ?, ?)"
if _, err := tx.Exec(query, version, bool(dirty), time.Now().UnixNano()); err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
return tx.Commit()
}
func (ch *ClickHouse) ensureVersionTable() error {
var (
table string
query = "SHOW TABLES FROM " + ch.config.DatabaseName + " LIKE '" + ch.config.MigrationsTable + "'"
)
// check if migration table exists
if err := ch.conn.QueryRow(query).Scan(&table); 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 ` + ch.config.MigrationsTable + ` (
version Int64,
dirty UInt8,
sequence UInt64
) Engine=TinyLog
`
if _, err := ch.conn.Exec(query); err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
return nil
}
func (ch *ClickHouse) Drop() error {
var (
query = "SHOW TABLES FROM " + ch.config.DatabaseName
tables, err = ch.conn.Query(query)
)
if err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
defer tables.Close()
for tables.Next() {
var table string
if err := tables.Scan(&table); err != nil {
return err
}
query = "DROP TABLE IF EXISTS " + ch.config.DatabaseName + "." + table
if _, err := ch.conn.Exec(query); err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
}
return ch.ensureVersionTable()
}
func (ch *ClickHouse) Lock() error { return nil }
func (ch *ClickHouse) Unlock() error { return nil }
func (ch *ClickHouse) Close() error { return ch.conn.Close() }
@@ -0,0 +1 @@
DROP TABLE IF EXISTS test_1;
@@ -0,0 +1,3 @@
CREATE TABLE test_1 (
Date Date
) Engine=Memory;
@@ -0,0 +1 @@
DROP TABLE IF EXISTS test_2;
@@ -0,0 +1,3 @@
CREATE TABLE test_2 (
Date Date
) Engine=Memory;
+19
View File
@@ -0,0 +1,19 @@
# cockroachdb
`cockroachdb://user:password@host:port/dbname?query` (`cockroach://`, and `crdb-postgres://` work, too)
| URL Query | WithInstance Config | Description |
|------------|---------------------|-------------|
| `x-migrations-table` | `MigrationsTable` | Name of the migrations table |
| `x-lock-table` | `LockTable` | Name of the table which maintains the migration lock |
| `x-force-lock` | `ForceLock` | Force lock acquisition to fix faulty migrations which may not have released the schema lock (Boolean, default is `false`) |
| `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. Values that start with / are for unix domain sockets. (default is localhost) |
| `port` | | The port to bind to. (default is 5432) |
| `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) |
+342
View File
@@ -0,0 +1,342 @@
package cockroachdb
import (
"context"
"database/sql"
"fmt"
"io"
"io/ioutil"
nurl "net/url"
"regexp"
"strconv"
)
import (
"github.com/cockroachdb/cockroach-go/crdb"
"github.com/lib/pq"
)
import (
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database"
)
func init() {
db := CockroachDb{}
database.Register("cockroach", &db)
database.Register("cockroachdb", &db)
database.Register("crdb-postgres", &db)
}
var DefaultMigrationsTable = "schema_migrations"
var DefaultLockTable = "schema_lock"
var (
ErrNilConfig = fmt.Errorf("no config")
ErrNoDatabaseName = fmt.Errorf("no database name")
)
type Config struct {
MigrationsTable string
LockTable string
ForceLock bool
DatabaseName string
}
type CockroachDb struct {
db *sql.DB
isLocked bool
// Open and WithInstance need to guarantee 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
}
if len(config.LockTable) == 0 {
config.LockTable = DefaultLockTable
}
px := &CockroachDb{
db: instance,
config: config,
}
if err := px.ensureVersionTable(); err != nil {
return nil, err
}
if err := px.ensureLockTable(); err != nil {
return nil, err
}
return px, nil
}
func (c *CockroachDb) Open(url string) (database.Driver, error) {
purl, err := nurl.Parse(url)
if err != nil {
return nil, err
}
// As Cockroach uses the postgres protocol, and 'postgres' is already a registered database, we need to replace the
// connect prefix, with the actual protocol, so that the library can differentiate between the implementations
re := regexp.MustCompile("^(cockroach(db)?|crdb-postgres)")
connectString := re.ReplaceAllString(migrate.FilterCustomQuery(purl).String(), "postgres")
db, err := sql.Open("postgres", connectString)
if err != nil {
return nil, err
}
migrationsTable := purl.Query().Get("x-migrations-table")
if len(migrationsTable) == 0 {
migrationsTable = DefaultMigrationsTable
}
lockTable := purl.Query().Get("x-lock-table")
if len(lockTable) == 0 {
lockTable = DefaultLockTable
}
forceLockQuery := purl.Query().Get("x-force-lock")
forceLock, err := strconv.ParseBool(forceLockQuery)
if err != nil {
forceLock = false
}
px, err := WithInstance(db, &Config{
DatabaseName: purl.Path,
MigrationsTable: migrationsTable,
LockTable: lockTable,
ForceLock: forceLock,
})
if err != nil {
return nil, err
}
return px, nil
}
func (c *CockroachDb) Close() error {
return c.db.Close()
}
// Locking is done manually with a separate lock table. Implementing advisory locks in CRDB is being discussed
// See: https://github.com/cockroachdb/cockroach/issues/13546
func (c *CockroachDb) Lock() error {
err := crdb.ExecuteTx(context.Background(), c.db, nil, func(tx *sql.Tx) error {
aid, err := database.GenerateAdvisoryLockId(c.config.DatabaseName)
if err != nil {
return err
}
query := "SELECT * FROM " + c.config.LockTable + " WHERE lock_id = $1"
rows, err := tx.Query(query, aid)
if err != nil {
return database.Error{OrigErr: err, Err: "failed to fetch migration lock", Query: []byte(query)}
}
defer rows.Close()
// If row exists at all, lock is present
locked := rows.Next()
if locked && !c.config.ForceLock {
return database.ErrLocked
}
query = "INSERT INTO " + c.config.LockTable + " (lock_id) VALUES ($1)"
if _, err := tx.Exec(query, aid); err != nil {
return database.Error{OrigErr: err, Err: "failed to set migration lock", Query: []byte(query)}
}
return nil
})
if err != nil {
return err
} else {
c.isLocked = true
return nil
}
}
// Locking is done manually with a separate lock table. Implementing advisory locks in CRDB is being discussed
// See: https://github.com/cockroachdb/cockroach/issues/13546
func (c *CockroachDb) Unlock() error {
aid, err := database.GenerateAdvisoryLockId(c.config.DatabaseName)
if err != nil {
return err
}
// In the event of an implementation (non-migration) error, it is possible for the lock to not be released. Until
// a better locking mechanism is added, a manual purging of the lock table may be required in such circumstances
query := "DELETE FROM " + c.config.LockTable + " WHERE lock_id = $1"
if _, err := c.db.Exec(query, aid); err != nil {
if e, ok := err.(*pq.Error); ok {
// 42P01 is "UndefinedTableError" in CockroachDB
// https://github.com/cockroachdb/cockroach/blob/master/pkg/sql/pgwire/pgerror/codes.go
if e.Code == "42P01" {
// On drops, the lock table is fully removed; This is fine, and is a valid "unlocked" state for the schema
c.isLocked = false
return nil
}
}
return database.Error{OrigErr: err, Err: "failed to release migration lock", Query: []byte(query)}
}
c.isLocked = false
return nil
}
func (c *CockroachDb) Run(migration io.Reader) error {
migr, err := ioutil.ReadAll(migration)
if err != nil {
return err
}
// run migration
query := string(migr[:])
if _, err := c.db.Exec(query); err != nil {
return database.Error{OrigErr: err, Err: "migration failed", Query: migr}
}
return nil
}
func (c *CockroachDb) SetVersion(version int, dirty bool) error {
return crdb.ExecuteTx(context.Background(), c.db, nil, func(tx *sql.Tx) error {
if _, err := tx.Exec(`DELETE FROM "` + c.config.MigrationsTable + `"`); err != nil {
return err
}
if version >= 0 {
if _, err := tx.Exec(`INSERT INTO "`+c.config.MigrationsTable+`" (version, dirty) VALUES ($1, $2)`, version, dirty); err != nil {
return err
}
}
return nil
})
}
func (c *CockroachDb) Version() (version int, dirty bool, err error) {
query := `SELECT version, dirty FROM "` + c.config.MigrationsTable + `" LIMIT 1`
err = c.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 {
// 42P01 is "UndefinedTableError" in CockroachDB
// https://github.com/cockroachdb/cockroach/blob/master/pkg/sql/pgwire/pgerror/codes.go
if e.Code == "42P01" {
return database.NilVersion, false, nil
}
}
return 0, false, &database.Error{OrigErr: err, Query: []byte(query)}
default:
return version, dirty, nil
}
}
func (c *CockroachDb) Drop() error {
// select all tables in current schema
query := `SELECT table_name FROM information_schema.tables WHERE table_schema=(SELECT current_schema())`
tables, err := c.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 := c.db.Exec(query); err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
}
if err := c.ensureVersionTable(); err != nil {
return err
}
}
return nil
}
func (c *CockroachDb) 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 := c.db.QueryRow(query, c.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 "` + c.config.MigrationsTable + `" (version INT NOT NULL PRIMARY KEY, dirty BOOL NOT NULL)`
if _, err := c.db.Exec(query); err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
return nil
}
func (c *CockroachDb) ensureLockTable() error {
// check if lock 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 := c.db.QueryRow(query, c.config.LockTable).Scan(&count); err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
if count == 1 {
return nil
}
// if not, create the empty lock table
query = `CREATE TABLE "` + c.config.LockTable + `" (lock_id INT NOT NULL PRIMARY KEY)`
if _, err := c.db.Exec(query); err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
return nil
}
+92
View File
@@ -0,0 +1,92 @@
package cockroachdb
// error codes https://github.com/lib/pq/blob/master/error.go
import (
"database/sql"
"fmt"
"io"
"strings"
"testing"
)
import (
dt "github.com/golang-migrate/migrate/v4/database/testing"
mt "github.com/golang-migrate/migrate/v4/testing"
"github.com/lib/pq"
)
var versions = []mt.Version{
{Image: "cockroachdb/cockroach:v1.0.2", Cmd: []string{"start", "--insecure"}},
}
func isReady(i mt.Instance) bool {
db, err := sql.Open("postgres", fmt.Sprintf("postgres://root@%v:%v?sslmode=disable", i.Host(), i.PortFor(26257)))
if err != nil {
return false
}
defer db.Close()
err = db.Ping()
if err == io.EOF {
_, err = db.Exec("CREATE DATABASE migrate")
return err == nil
} else if e, ok := err.(*pq.Error); ok {
if e.Code.Name() == "cannot_connect_now" {
return false
}
}
_, err = db.Exec("CREATE DATABASE migrate")
return err == nil
return true
}
func Test(t *testing.T) {
mt.ParallelTest(t, versions, isReady,
func(t *testing.T, i mt.Instance) {
c := &CockroachDb{}
addr := fmt.Sprintf("cockroach://root@%v:%v/migrate?sslmode=disable", i.Host(), i.PortFor(26257))
d, err := c.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) {
c := &CockroachDb{}
addr := fmt.Sprintf("cockroach://root@%v:%v/migrate?sslmode=disable", i.Host(), i.Port())
d, err := c.Open(addr)
if err != nil {
t.Fatalf("%v", err)
}
if err := d.Run(strings.NewReader("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.(*CockroachDb).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) {
c := &CockroachDb{}
addr := fmt.Sprintf("cockroach://root@%v:%v/migrate?sslmode=disable&x-custom=foobar", i.Host(), i.PortFor(26257))
_, err := c.Open(addr)
if err != nil {
t.Fatalf("%v", err)
}
})
}
@@ -0,0 +1 @@
DROP TABLE IF EXISTS users;
@@ -0,0 +1,5 @@
CREATE TABLE users (
user_id INT UNIQUE,
name STRING(40),
email STRING(40)
);
@@ -0,0 +1 @@
ALTER TABLE users DROP COLUMN IF EXISTS city;
@@ -0,0 +1 @@
ALTER TABLE users ADD COLUMN city TEXT;
@@ -0,0 +1 @@
DROP INDEX IF EXISTS users_email_index;
@@ -0,0 +1,3 @@
CREATE UNIQUE INDEX IF NOT EXISTS 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 INT,
name STRING(40),
author STRING(40)
);
@@ -0,0 +1 @@
DROP TABLE IF EXISTS movies;
@@ -0,0 +1,5 @@
CREATE TABLE movies (
user_id INT,
name STRING(40),
director STRING(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.
+17 -3
View File
@@ -32,6 +32,8 @@ var drivers = make(map[string]Driver)
// 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().
// 6. Create a migrate/cli/build_<driver-name>.go file
// 7. Add driver name in 'DATABASE' variable in Makefile
//
// Guidelines:
// * Don't try to correct user input. Don't assume things.
@@ -71,7 +73,7 @@ type Driver interface {
// 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 deletes everything in the database.
Drop() error
}
@@ -79,7 +81,8 @@ type Driver interface {
func Open(url string) (Driver, error) {
u, err := nurl.Parse(url)
if err != nil {
return nil, err
return nil, fmt.Errorf("Unable to parse URL. Did you escape all reserved URL characters? "+
"See: https://github.com/golang-migrate/migrate#database-urls Error: %v", err)
}
if u.Scheme == "" {
@@ -90,7 +93,7 @@ func Open(url string) (Driver, error) {
d, ok := drivers[u.Scheme]
driversMu.RUnlock()
if !ok {
return nil, fmt.Errorf("database driver: unknown driver %v (forgotton import?)", u.Scheme)
return nil, fmt.Errorf("database driver: unknown driver %v (forgotten import?)", u.Scheme)
}
return d.Open(url)
@@ -108,3 +111,14 @@ func Register(name string, driver Driver) {
}
drivers[name] = driver
}
// List lists the registered drivers
func List() []string {
driversMu.RLock()
defer driversMu.RUnlock()
names := make([]string, 0, len(drivers))
for n := range drivers {
names = append(names, n)
}
return names
}
+54 -24
View File
@@ -1,24 +1,54 @@
# 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>`.
# 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. |
| `tls` | | TLS / SSL encrypted connection parameter; see [go-sql-driver](https://github.com/go-sql-driver/mysql#tls). Use any name (e.g. `migrate`) if you want to use a custom TLS config (`x-tls-` queries). |
| `x-tls-ca` | | The location of the CA (certificate authority) file. |
| `x-tls-cert` | | The location of the client certicicate file. Must be used with `x-tls-key`. |
| `x-tls-key` | | The location of the private key file. Must be used with `x-tls-cert`. |
| `x-tls-insecure-skip-verify` | | Whether or not to use SSL (true\|false) |
## Use with existing client
If you use the MySQL driver with existing database client, you must create the client with parameter `multiStatements=true`:
```go
package main
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
"github.com/golang-migrate/migrate"
"github.com/golang-migrate/migrate/database/mysql"
_ "github.com/golang-migrate/migrate/source/file"
)
func main() {
db, _ := sql.Open("mysql", "user:password@tcp(host:port)/dbname?multiStatements=true")
driver, _ := mysql.WithInstance(db, &mysql.Config{})
m, _ := migrate.NewWithDatabaseInstance(
"file:///migrations",
"mysql",
driver,
)
m.Steps(2)
}
```
## 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>`.
+90 -31
View File
@@ -1,6 +1,9 @@
// +build go1.9
package mysql
import (
"context"
"crypto/tls"
"crypto/x509"
"database/sql"
@@ -10,10 +13,15 @@ import (
nurl "net/url"
"strconv"
"strings"
)
import (
"github.com/go-sql-driver/mysql"
"github.com/mattes/migrate"
"github.com/mattes/migrate/database"
)
import (
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database"
)
func init() {
@@ -23,10 +31,11 @@ func init() {
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")
ErrDatabaseDirty = fmt.Errorf("database is dirty")
ErrNilConfig = fmt.Errorf("no config")
ErrNoDatabaseName = fmt.Errorf("no database name")
ErrAppendPEM = fmt.Errorf("failed to append PEM")
ErrTLSCertKeyConfig = fmt.Errorf("To use TLS client authentication, both x-tls-cert and x-tls-key must not be empty")
)
type Config struct {
@@ -35,6 +44,9 @@ type Config struct {
}
type Mysql struct {
// mysql RELEASE_LOCK must be called from the same conn, so
// just do everything over a single conn anyway.
conn *sql.Conn
db *sql.DB
isLocked bool
@@ -67,7 +79,13 @@ func WithInstance(instance *sql.DB, config *Config) (database.Driver, error) {
config.MigrationsTable = DefaultMigrationsTable
}
conn, err := instance.Conn(context.Background())
if err != nil {
return nil, err
}
mx := &Mysql{
conn: conn,
db: instance,
config: config,
}
@@ -79,6 +97,25 @@ func WithInstance(instance *sql.DB, config *Config) (database.Driver, error) {
return mx, nil
}
// urlToMySQLConfig takes a net/url URL and returns a go-sql-driver/mysql Config.
// Manually sets username and password to avoid net/url from url-encoding the reserved URL characters
func urlToMySQLConfig(u nurl.URL) (*mysql.Config, error) {
origUserInfo := u.User
u.User = nil
c, err := mysql.ParseDSN(strings.TrimPrefix(u.String(), "mysql://"))
if err != nil {
return nil, err
}
if origUserInfo != nil {
c.User = origUserInfo.Username()
if p, ok := origUserInfo.Password(); ok {
c.Passwd = p
}
}
return c, nil
}
func (m *Mysql) Open(url string) (database.Driver, error) {
purl, err := nurl.Parse(url)
if err != nil {
@@ -89,12 +126,6 @@ func (m *Mysql) Open(url string) (database.Driver, error) {
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
@@ -114,9 +145,16 @@ func (m *Mysql) Open(url string) (database.Driver, error) {
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
clientCert := make([]tls.Certificate, 0, 1)
if ccert, ckey := purl.Query().Get("x-tls-cert"), purl.Query().Get("x-tls-key"); ccert != "" || ckey != "" {
if ccert == "" || ckey == "" {
return nil, ErrTLSCertKeyConfig
}
certs, err := tls.LoadX509KeyPair(ccert, ckey)
if err != nil {
return nil, err
}
clientCert = append(clientCert, certs)
}
insecureSkipVerify := false
@@ -130,12 +168,21 @@ func (m *Mysql) Open(url string) (database.Driver, error) {
mysql.RegisterTLSConfig(ctls, &tls.Config{
RootCAs: rootCertPool,
Certificates: []tls.Certificate{certs},
Certificates: clientCert,
InsecureSkipVerify: insecureSkipVerify,
})
}
}
c, err := urlToMySQLConfig(*migrate.FilterCustomQuery(purl))
if err != nil {
return nil, err
}
db, err := sql.Open("mysql", c.FormatDSN())
if err != nil {
return nil, err
}
mx, err := WithInstance(db, &Config{
DatabaseName: purl.Path,
MigrationsTable: migrationsTable,
@@ -148,7 +195,12 @@ func (m *Mysql) Open(url string) (database.Driver, error) {
}
func (m *Mysql) Close() error {
return m.db.Close()
connErr := m.conn.Close()
dbErr := m.db.Close()
if connErr != nil || dbErr != nil {
return fmt.Errorf("conn: %v, db: %v", connErr, dbErr)
}
return nil
}
func (m *Mysql) Lock() error {
@@ -156,14 +208,15 @@ func (m *Mysql) Lock() error {
return database.ErrLocked
}
aid, err := database.GenerateAdvisoryLockId(m.config.DatabaseName)
aid, err := database.GenerateAdvisoryLockId(
fmt.Sprintf("%s:%s", m.config.DatabaseName, m.config.MigrationsTable))
if err != nil {
return err
}
query := "SELECT GET_LOCK(?, 1)"
query := "SELECT GET_LOCK(?, 10)"
var success bool
if err := m.db.QueryRow(query, aid).Scan(&success); err != nil {
if err := m.conn.QueryRowContext(context.Background(), query, aid).Scan(&success); err != nil {
return &database.Error{OrigErr: err, Err: "try lock failed", Query: []byte(query)}
}
@@ -180,16 +233,21 @@ func (m *Mysql) Unlock() error {
return nil
}
aid, err := database.GenerateAdvisoryLockId(m.config.DatabaseName)
aid, err := database.GenerateAdvisoryLockId(
fmt.Sprintf("%s:%s", m.config.DatabaseName, m.config.MigrationsTable))
if err != nil {
return err
}
query := `SELECT RELEASE_LOCK(?)`
if _, err := m.db.Exec(query, aid); err != nil {
if _, err := m.conn.ExecContext(context.Background(), query, aid); err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
// NOTE: RELEASE_LOCK could return NULL or (or 0 if the code is changed),
// in which case isLocked should be true until the timeout expires -- synchronizing
// these states is likely not worth trying to do; reconsider the necessity of isLocked.
m.isLocked = false
return nil
}
@@ -201,7 +259,7 @@ func (m *Mysql) Run(migration io.Reader) error {
}
query := string(migr[:])
if _, err := m.db.Exec(query); err != nil {
if _, err := m.conn.ExecContext(context.Background(), query); err != nil {
return database.Error{OrigErr: err, Err: "migration failed", Query: migr}
}
@@ -209,19 +267,20 @@ func (m *Mysql) Run(migration io.Reader) error {
}
func (m *Mysql) SetVersion(version int, dirty bool) error {
tx, err := m.db.Begin()
tx, err := m.conn.BeginTx(context.Background(), &sql.TxOptions{})
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 {
if _, err := tx.ExecContext(context.Background(), query); err != nil {
tx.Rollback()
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 {
if _, err := tx.ExecContext(context.Background(), query, version, dirty); err != nil {
tx.Rollback()
return &database.Error{OrigErr: err, Query: []byte(query)}
}
@@ -236,7 +295,7 @@ func (m *Mysql) SetVersion(version int, dirty bool) error {
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)
err = m.conn.QueryRowContext(context.Background(), query).Scan(&version, &dirty)
switch {
case err == sql.ErrNoRows:
return database.NilVersion, false, nil
@@ -257,7 +316,7 @@ func (m *Mysql) Version() (version int, dirty bool, err error) {
func (m *Mysql) Drop() error {
// select all tables
query := `SHOW TABLES LIKE '%'`
tables, err := m.db.Query(query)
tables, err := m.conn.QueryContext(context.Background(), query)
if err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
@@ -279,7 +338,7 @@ func (m *Mysql) Drop() error {
// delete one by one ...
for _, t := range tableNames {
query = "DROP TABLE IF EXISTS `" + t + "` CASCADE"
if _, err := m.db.Exec(query); err != nil {
if _, err := m.conn.ExecContext(context.Background(), query); err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
}
@@ -295,7 +354,7 @@ 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 := m.conn.QueryRowContext(context.Background(), query).Scan(&result); err != nil {
if err != sql.ErrNoRows {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
@@ -305,7 +364,7 @@ func (m *Mysql) ensureVersionTable() error {
// 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 {
if _, err := m.conn.ExecContext(context.Background(), query); err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
return nil
+107 -12
View File
@@ -4,20 +4,24 @@ import (
"database/sql"
sqldriver "database/sql/driver"
"fmt"
// "io/ioutil"
// "log"
"net/url"
"testing"
)
// "github.com/go-sql-driver/mysql"
dt "github.com/mattes/migrate/database/testing"
mt "github.com/mattes/migrate/testing"
import (
"github.com/go-sql-driver/mysql"
)
import (
dt "github.com/golang-migrate/migrate/v4/database/testing"
mt "github.com/golang-migrate/migrate/v4/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"}},
{Image: "mysql:8", ENV: []string{"MYSQL_ROOT_PASSWORD=root", "MYSQL_DATABASE=public"}},
{Image: "mysql:5.7", ENV: []string{"MYSQL_ROOT_PASSWORD=root", "MYSQL_DATABASE=public"}},
{Image: "mysql:5.6", ENV: []string{"MYSQL_ROOT_PASSWORD=root", "MYSQL_DATABASE=public"}},
{Image: "mysql:5.5", ENV: []string{"MYSQL_ROOT_PASSWORD=root", "MYSQL_DATABASE=public"}},
}
func isReady(i mt.Instance) bool {
@@ -26,9 +30,13 @@ func isReady(i mt.Instance) bool {
return false
}
defer db.Close()
err = db.Ping()
if err == sqldriver.ErrBadConn {
if err = db.Ping(); err != nil {
switch err {
case sqldriver.ErrBadConn, mysql.ErrInvalidConn:
return false
default:
fmt.Println(err)
}
return false
}
@@ -46,6 +54,7 @@ func Test(t *testing.T) {
if err != nil {
t.Fatalf("%v", err)
}
defer d.Close()
dt.Test(t, d, []byte("SELECT 1"))
// check ensureVersionTable
@@ -58,3 +67,89 @@ func Test(t *testing.T) {
}
})
}
func TestLockWorks(t *testing.T) {
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"))
ms := d.(*Mysql)
err = ms.Lock()
if err != nil {
t.Fatal(err)
}
err = ms.Unlock()
if err != nil {
t.Fatal(err)
}
// make sure the 2nd lock works (RELEASE_LOCK is very finicky)
err = ms.Lock()
if err != nil {
t.Fatal(err)
}
err = ms.Unlock()
if err != nil {
t.Fatal(err)
}
})
}
func TestURLToMySQLConfig(t *testing.T) {
testcases := []struct {
name string
urlStr string
expectedDSN string // empty string signifies that an error is expected
}{
{name: "no user/password", urlStr: "mysql://tcp(127.0.0.1:3306)/myDB?multiStatements=true",
expectedDSN: "tcp(127.0.0.1:3306)/myDB?multiStatements=true"},
{name: "only user", urlStr: "mysql://username@tcp(127.0.0.1:3306)/myDB?multiStatements=true",
expectedDSN: "username@tcp(127.0.0.1:3306)/myDB?multiStatements=true"},
{name: "only user - with encoded :",
urlStr: "mysql://username%3A@tcp(127.0.0.1:3306)/myDB?multiStatements=true",
expectedDSN: "username:@tcp(127.0.0.1:3306)/myDB?multiStatements=true"},
{name: "only user - with encoded @",
urlStr: "mysql://username%40@tcp(127.0.0.1:3306)/myDB?multiStatements=true",
expectedDSN: "username@@tcp(127.0.0.1:3306)/myDB?multiStatements=true"},
{name: "user/password", urlStr: "mysql://username:password@tcp(127.0.0.1:3306)/myDB?multiStatements=true",
expectedDSN: "username:password@tcp(127.0.0.1:3306)/myDB?multiStatements=true"},
// Not supported yet: https://github.com/go-sql-driver/mysql/issues/591
// {name: "user/password - user with encoded :",
// urlStr: "mysql://username%3A:password@tcp(127.0.0.1:3306)/myDB?multiStatements=true",
// expectedDSN: "username::pasword@tcp(127.0.0.1:3306)/myDB?multiStatements=true"},
{name: "user/password - user with encoded @",
urlStr: "mysql://username%40:password@tcp(127.0.0.1:3306)/myDB?multiStatements=true",
expectedDSN: "username@:password@tcp(127.0.0.1:3306)/myDB?multiStatements=true"},
{name: "user/password - password with encoded :",
urlStr: "mysql://username:password%3A@tcp(127.0.0.1:3306)/myDB?multiStatements=true",
expectedDSN: "username:password:@tcp(127.0.0.1:3306)/myDB?multiStatements=true"},
{name: "user/password - password with encoded @",
urlStr: "mysql://username:password%40@tcp(127.0.0.1:3306)/myDB?multiStatements=true",
expectedDSN: "username:password@@tcp(127.0.0.1:3306)/myDB?multiStatements=true"},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
u, err := url.Parse(tc.urlStr)
if err != nil {
t.Fatal("Failed to parse url string:", tc.urlStr, "error:", err)
}
if config, err := urlToMySQLConfig(*u); err == nil {
dsn := config.FormatDSN()
if dsn != tc.expectedDSN {
t.Error("Got unexpected DSN:", dsn, "!=", tc.expectedDSN)
}
} else {
if tc.expectedDSN != "" {
t.Error("Got unexpected error:", err, "urlStr:", tc.urlStr)
}
}
})
}
}
+244
View File
@@ -0,0 +1,244 @@
package database_test
import (
"encoding/hex"
"net/url"
"strings"
"testing"
)
const reservedChars = "!#$%&'()*+,/:;=?@[]"
// TestUserUnencodedReservedURLChars documents the behavior of using unencoded reserved characters in usernames with
// net/url Parse()
func TestUserUnencodedReservedURLChars(t *testing.T) {
scheme := "database://"
baseUsername := "username"
urlSuffix := "password@localhost:12345/myDB?someParam=true"
urlSuffixAndSep := ":" + urlSuffix
testcases := []struct {
char string
parses bool
expectedUsername string // empty string means that the username failed to parse
encodedURL string
}{
{char: "!", parses: true, expectedUsername: baseUsername + "!",
encodedURL: scheme + baseUsername + "%21" + urlSuffixAndSep},
{char: "#", parses: true, expectedUsername: "",
encodedURL: scheme + baseUsername + "#" + urlSuffixAndSep},
{char: "$", parses: true, expectedUsername: baseUsername + "$",
encodedURL: scheme + baseUsername + "$" + urlSuffixAndSep},
{char: "%", parses: false},
{char: "&", parses: true, expectedUsername: baseUsername + "&",
encodedURL: scheme + baseUsername + "&" + urlSuffixAndSep},
{char: "'", parses: true, expectedUsername: "username'",
encodedURL: scheme + baseUsername + "%27" + urlSuffixAndSep},
{char: "(", parses: true, expectedUsername: "username(",
encodedURL: scheme + baseUsername + "%28" + urlSuffixAndSep},
{char: ")", parses: true, expectedUsername: "username)",
encodedURL: scheme + baseUsername + "%29" + urlSuffixAndSep},
{char: "*", parses: true, expectedUsername: "username*",
encodedURL: scheme + baseUsername + "%2A" + urlSuffixAndSep},
{char: "+", parses: true, expectedUsername: "username+",
encodedURL: scheme + baseUsername + "+" + urlSuffixAndSep},
{char: ",", parses: true, expectedUsername: "username,",
encodedURL: scheme + baseUsername + "," + urlSuffixAndSep},
{char: "/", parses: true, expectedUsername: "",
encodedURL: scheme + baseUsername + "/" + urlSuffixAndSep},
{char: ":", parses: true, expectedUsername: "username",
encodedURL: scheme + baseUsername + ":%3A" + urlSuffix},
{char: ";", parses: true, expectedUsername: "username;",
encodedURL: scheme + baseUsername + ";" + urlSuffixAndSep},
{char: "=", parses: true, expectedUsername: "username=",
encodedURL: scheme + baseUsername + "=" + urlSuffixAndSep},
{char: "?", parses: true, expectedUsername: "",
encodedURL: scheme + baseUsername + "?" + urlSuffixAndSep},
{char: "@", parses: true, expectedUsername: "username@",
encodedURL: scheme + baseUsername + "%40" + urlSuffixAndSep},
{char: "[", parses: false},
{char: "]", parses: false},
}
testedChars := make([]string, 0, len(reservedChars))
for _, tc := range testcases {
testedChars = append(testedChars, tc.char)
t.Run("reserved char "+tc.char, func(t *testing.T) {
s := scheme + baseUsername + tc.char + urlSuffixAndSep
u, err := url.Parse(s)
if err == nil {
if !tc.parses {
t.Error("Unexpectedly parsed reserved character. url:", s)
return
}
var username string
if u.User != nil {
username = u.User.Username()
}
if username != tc.expectedUsername {
t.Error("Got unexpected username:", username, "!=", tc.expectedUsername)
}
if s := u.String(); s != tc.encodedURL {
t.Error("Got unexpected encoded URL:", s, "!=", tc.encodedURL)
}
} else {
if tc.parses {
t.Error("Failed to parse reserved character. url:", s)
}
}
})
}
t.Run("All reserved chars tested", func(t *testing.T) {
if s := strings.Join(testedChars, ""); s != reservedChars {
t.Error("Not all reserved URL characters were tested:", s, "!=", reservedChars)
}
})
}
func TestUserEncodedReservedURLChars(t *testing.T) {
scheme := "database://"
baseUsername := "username"
urlSuffix := "password@localhost:12345/myDB?someParam=true"
urlSuffixAndSep := ":" + urlSuffix
for _, c := range reservedChars {
c := string(c)
t.Run("reserved char "+c, func(t *testing.T) {
encodedChar := "%" + hex.EncodeToString([]byte(c))
s := scheme + baseUsername + encodedChar + urlSuffixAndSep
expectedUsername := baseUsername + c
u, err := url.Parse(s)
if err != nil {
t.Fatal("Failed to parse url with encoded reserved character. url:", s)
}
if u.User == nil {
t.Fatal("Failed to parse userinfo with encoded reserve character. url:", s)
}
if username := u.User.Username(); username != expectedUsername {
t.Fatal("Got unexpected username:", username, "!=", expectedUsername)
}
})
}
}
// TestPasswordUnencodedReservedURLChars documents the behavior of using unencoded reserved characters in passwords
// with net/url Parse()
func TestPasswordUnencodedReservedURLChars(t *testing.T) {
username := "username"
schemeAndUsernameAndSep := "database://" + username + ":"
basePassword := "password"
urlSuffixAndSep := "@localhost:12345/myDB?someParam=true"
testcases := []struct {
char string
parses bool
expectedUsername string // empty string means that the username failed to parse
expectedPassword string // empty string means that the password failed to parse
encodedURL string
}{
{char: "!", parses: true, expectedUsername: username, expectedPassword: basePassword + "!",
encodedURL: schemeAndUsernameAndSep + basePassword + "%21" + urlSuffixAndSep},
{char: "#", parses: true, expectedUsername: "", expectedPassword: "",
encodedURL: schemeAndUsernameAndSep + basePassword + "#" + urlSuffixAndSep},
{char: "$", parses: true, expectedUsername: username, expectedPassword: basePassword + "$",
encodedURL: schemeAndUsernameAndSep + basePassword + "$" + urlSuffixAndSep},
{char: "%", parses: false},
{char: "&", parses: true, expectedUsername: username, expectedPassword: basePassword + "&",
encodedURL: schemeAndUsernameAndSep + basePassword + "&" + urlSuffixAndSep},
{char: "'", parses: true, expectedUsername: username, expectedPassword: "password'",
encodedURL: schemeAndUsernameAndSep + basePassword + "%27" + urlSuffixAndSep},
{char: "(", parses: true, expectedUsername: username, expectedPassword: "password(",
encodedURL: schemeAndUsernameAndSep + basePassword + "%28" + urlSuffixAndSep},
{char: ")", parses: true, expectedUsername: username, expectedPassword: "password)",
encodedURL: schemeAndUsernameAndSep + basePassword + "%29" + urlSuffixAndSep},
{char: "*", parses: true, expectedUsername: username, expectedPassword: "password*",
encodedURL: schemeAndUsernameAndSep + basePassword + "%2A" + urlSuffixAndSep},
{char: "+", parses: true, expectedUsername: username, expectedPassword: "password+",
encodedURL: schemeAndUsernameAndSep + basePassword + "+" + urlSuffixAndSep},
{char: ",", parses: true, expectedUsername: username, expectedPassword: "password,",
encodedURL: schemeAndUsernameAndSep + basePassword + "," + urlSuffixAndSep},
{char: "/", parses: true, expectedUsername: "", expectedPassword: "",
encodedURL: schemeAndUsernameAndSep + basePassword + "/" + urlSuffixAndSep},
{char: ":", parses: true, expectedUsername: username, expectedPassword: "password:",
encodedURL: schemeAndUsernameAndSep + basePassword + "%3A" + urlSuffixAndSep},
{char: ";", parses: true, expectedUsername: username, expectedPassword: "password;",
encodedURL: schemeAndUsernameAndSep + basePassword + ";" + urlSuffixAndSep},
{char: "=", parses: true, expectedUsername: username, expectedPassword: "password=",
encodedURL: schemeAndUsernameAndSep + basePassword + "=" + urlSuffixAndSep},
{char: "?", parses: true, expectedUsername: "", expectedPassword: "",
encodedURL: schemeAndUsernameAndSep + basePassword + "?" + urlSuffixAndSep},
{char: "@", parses: true, expectedUsername: username, expectedPassword: "password@",
encodedURL: schemeAndUsernameAndSep + basePassword + "%40" + urlSuffixAndSep},
{char: "[", parses: false},
{char: "]", parses: false},
}
testedChars := make([]string, 0, len(reservedChars))
for _, tc := range testcases {
testedChars = append(testedChars, tc.char)
t.Run("reserved char "+tc.char, func(t *testing.T) {
s := schemeAndUsernameAndSep + basePassword + tc.char + urlSuffixAndSep
u, err := url.Parse(s)
if err == nil {
if !tc.parses {
t.Error("Unexpectedly parsed reserved character. url:", s)
return
}
var username, password string
if u.User != nil {
username = u.User.Username()
password, _ = u.User.Password()
}
if username != tc.expectedUsername {
t.Error("Got unexpected username:", username, "!=", tc.expectedUsername)
}
if password != tc.expectedPassword {
t.Error("Got unexpected password:", password, "!=", tc.expectedPassword)
}
if s := u.String(); s != tc.encodedURL {
t.Error("Got unexpected encoded URL:", s, "!=", tc.encodedURL)
}
} else {
if tc.parses {
t.Error("Failed to parse reserved character. url:", s)
}
}
})
}
t.Run("All reserved chars tested", func(t *testing.T) {
if s := strings.Join(testedChars, ""); s != reservedChars {
t.Error("Not all reserved URL characters were tested:", s, "!=", reservedChars)
}
})
}
func TestPasswordEncodedReservedURLChars(t *testing.T) {
username := "username"
schemeAndUsernameAndSep := "database://" + username + ":"
basePassword := "password"
urlSuffixAndSep := "@localhost:12345/myDB?someParam=true"
for _, c := range reservedChars {
c := string(c)
t.Run("reserved char "+c, func(t *testing.T) {
encodedChar := "%" + hex.EncodeToString([]byte(c))
s := schemeAndUsernameAndSep + basePassword + encodedChar + urlSuffixAndSep
expectedPassword := basePassword + c
u, err := url.Parse(s)
if err != nil {
t.Fatal("Failed to parse url with encoded reserved character. url:", s)
}
if u.User == nil {
t.Fatal("Failed to parse userinfo with encoded reserve character. url:", s)
}
if n := u.User.Username(); n != username {
t.Fatal("Got unexpected username:", n, "!=", username)
}
if p, _ := u.User.Password(); p != expectedPassword {
t.Fatal("Got unexpected password:", p, "!=", expectedPassword)
}
})
}
}
+105 -26
View File
@@ -1,15 +1,20 @@
// +build go1.9
package postgres
import (
"context"
"database/sql"
"fmt"
"io"
"io/ioutil"
nurl "net/url"
"strconv"
"strings"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database"
"github.com/lib/pq"
"github.com/mattes/migrate"
"github.com/mattes/migrate/database"
)
func init() {
@@ -30,9 +35,12 @@ var (
type Config struct {
MigrationsTable string
DatabaseName string
SchemaName string
}
type Postgres struct {
// Locking and unlocking need to use the same connection
conn *sql.Conn
db *sql.DB
isLocked bool
@@ -61,11 +69,30 @@ func WithInstance(instance *sql.DB, config *Config) (database.Driver, error) {
config.DatabaseName = databaseName
query = `SELECT CURRENT_SCHEMA()`
var schemaName string
if err := instance.QueryRow(query).Scan(&schemaName); err != nil {
return nil, &database.Error{OrigErr: err, Query: []byte(query)}
}
if len(schemaName) == 0 {
return nil, ErrNoSchema
}
config.SchemaName = schemaName
if len(config.MigrationsTable) == 0 {
config.MigrationsTable = DefaultMigrationsTable
}
conn, err := instance.Conn(context.Background())
if err != nil {
return nil, err
}
px := &Postgres{
conn: conn,
db: instance,
config: config,
}
@@ -105,7 +132,12 @@ func (p *Postgres) Open(url string) (database.Driver, error) {
}
func (p *Postgres) Close() error {
return p.db.Close()
connErr := p.conn.Close()
dbErr := p.db.Close()
if connErr != nil || dbErr != nil {
return fmt.Errorf("conn: %v, db: %v", connErr, dbErr)
}
return nil
}
// https://www.postgresql.org/docs/9.6/static/explicit-locking.html#ADVISORY-LOCKS
@@ -114,25 +146,20 @@ func (p *Postgres) Lock() error {
return database.ErrLocked
}
aid, err := database.GenerateAdvisoryLockId(p.config.DatabaseName)
aid, err := database.GenerateAdvisoryLockId(p.config.DatabaseName, p.config.SchemaName)
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 {
query := `SELECT pg_advisory_lock($1)`
if _, err := p.conn.ExecContext(context.Background(), query, aid); err != nil {
return &database.Error{OrigErr: err, Err: "try lock failed", Query: []byte(query)}
}
if success {
p.isLocked = true
return nil
}
return database.ErrLocked
p.isLocked = true
return nil
}
func (p *Postgres) Unlock() error {
@@ -140,13 +167,13 @@ func (p *Postgres) Unlock() error {
return nil
}
aid, err := database.GenerateAdvisoryLockId(p.config.DatabaseName)
aid, err := database.GenerateAdvisoryLockId(p.config.DatabaseName, p.config.SchemaName)
if err != nil {
return err
}
query := `SELECT pg_advisory_unlock($1)`
if _, err := p.db.Exec(query, aid); err != nil {
if _, err := p.conn.ExecContext(context.Background(), query, aid); err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
p.isLocked = false
@@ -161,29 +188,81 @@ func (p *Postgres) Run(migration io.Reader) error {
// run migration
query := string(migr[:])
if _, err := p.db.Exec(query); err != nil {
// TODO: cast to postgress error and get line number
if _, err := p.conn.ExecContext(context.Background(), query); err != nil {
if pgErr, ok := err.(*pq.Error); ok {
var line uint
var col uint
var lineColOK bool
if pgErr.Position != "" {
if pos, err := strconv.ParseUint(pgErr.Position, 10, 64); err == nil {
line, col, lineColOK = computeLineFromPos(query, int(pos))
}
}
message := fmt.Sprintf("migration failed: %s", pgErr.Message)
if lineColOK {
message = fmt.Sprintf("%s (column %d)", message, col)
}
if pgErr.Detail != "" {
message = fmt.Sprintf("%s, %s", message, pgErr.Detail)
}
return database.Error{OrigErr: err, Err: message, Query: migr, Line: line}
}
return database.Error{OrigErr: err, Err: "migration failed", Query: migr}
}
return nil
}
func computeLineFromPos(s string, pos int) (line uint, col uint, ok bool) {
// replace crlf with lf
s = strings.Replace(s, "\r\n", "\n", -1)
// pg docs: pos uses index 1 for the first character, and positions are measured in characters not bytes
runes := []rune(s)
if pos > len(runes) {
return 0, 0, false
}
sel := runes[:pos]
line = uint(runesCount(sel, newLine) + 1)
col = uint(pos - 1 - runesLastIndex(sel, newLine))
return line, col, true
}
const newLine = '\n'
func runesCount(input []rune, target rune) int {
var count int
for _, r := range input {
if r == target {
count++
}
}
return count
}
func runesLastIndex(input []rune, target rune) int {
for i := len(input) - 1; i >= 0; i-- {
if input[i] == target {
return i
}
}
return -1
}
func (p *Postgres) SetVersion(version int, dirty bool) error {
tx, err := p.db.Begin()
tx, err := p.conn.BeginTx(context.Background(), &sql.TxOptions{})
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 {
if _, err := tx.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 {
if _, err := tx.Exec(query, version, dirty); err != nil {
tx.Rollback()
return &database.Error{OrigErr: err, Query: []byte(query)}
}
@@ -198,7 +277,7 @@ func (p *Postgres) SetVersion(version int, dirty bool) error {
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)
err = p.conn.QueryRowContext(context.Background(), query).Scan(&version, &dirty)
switch {
case err == sql.ErrNoRows:
return database.NilVersion, false, nil
@@ -218,8 +297,8 @@ func (p *Postgres) Version() (version int, dirty bool, err error) {
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)
query := `SELECT table_name FROM information_schema.tables WHERE table_schema=(SELECT current_schema()) AND table_type='BASE TABLE'`
tables, err := p.conn.QueryContext(context.Background(), query)
if err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
@@ -241,7 +320,7 @@ func (p *Postgres) Drop() error {
// delete one by one ...
for _, t := range tableNames {
query = `DROP TABLE IF EXISTS ` + t + ` CASCADE`
if _, err := p.db.Exec(query); err != nil {
if _, err := p.conn.ExecContext(context.Background(), query); err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
}
@@ -257,7 +336,7 @@ 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 {
if err := p.conn.QueryRowContext(context.Background(), query, p.config.MigrationsTable).Scan(&count); err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
if count == 1 {
@@ -266,7 +345,7 @@ func (p *Postgres) ensureVersionTable() error {
// 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 {
if _, err := p.conn.ExecContext(context.Background(), query); err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
return nil
+220 -19
View File
@@ -3,39 +3,47 @@ package postgres
// error codes https://github.com/lib/pq/blob/master/error.go
import (
"bytes"
"context"
"database/sql"
sqldriver "database/sql/driver"
"fmt"
"io"
"strconv"
"strings"
"testing"
)
"github.com/lib/pq"
dt "github.com/mattes/migrate/database/testing"
mt "github.com/mattes/migrate/testing"
import (
dt "github.com/golang-migrate/migrate/v4/database/testing"
mt "github.com/golang-migrate/migrate/v4/testing"
)
var versions = []mt.Version{
{Image: "postgres:10"},
{Image: "postgres:9.6"},
{Image: "postgres:9.5"},
{Image: "postgres:9.4"},
{Image: "postgres:9.3"},
{Image: "postgres:9.2"},
}
func pgConnectionString(host string, port uint) string {
return fmt.Sprintf("postgres://postgres@%s:%v/postgres?sslmode=disable", host, port)
}
func isReady(i mt.Instance) bool {
db, err := sql.Open("postgres", fmt.Sprintf("postgres://postgres@%v:%v/postgres?sslmode=disable", i.Host(), i.Port()))
db, err := sql.Open("postgres", pgConnectionString(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" {
if err = db.Ping(); err != nil {
switch err {
case sqldriver.ErrBadConn, io.EOF:
return false
default:
fmt.Println(err)
}
return false
}
return true
@@ -45,11 +53,12 @@ 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())
addr := pgConnectionString(i.Host(), i.Port())
d, err := p.Open(addr)
if err != nil {
t.Fatalf("%v", err)
}
defer d.Close()
dt.Test(t, d, []byte("SELECT 1"))
})
}
@@ -58,18 +67,19 @@ 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())
addr := pgConnectionString(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 {
defer d.Close()
if err := d.Run(strings.NewReader("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 {
if err := d.(*Postgres).conn.QueryRowContext(context.Background(), "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 {
@@ -78,15 +88,37 @@ func TestMultiStatement(t *testing.T) {
})
}
func TestErrorParsing(t *testing.T) {
mt.ParallelTest(t, versions, isReady,
func(t *testing.T, i mt.Instance) {
p := &Postgres{}
addr := pgConnectionString(i.Host(), i.Port())
d, err := p.Open(addr)
if err != nil {
t.Fatalf("%v", err)
}
defer d.Close()
wantErr := `migration failed: syntax error at or near "TABLEE" (column 37) in line 1: CREATE TABLE foo ` +
`(foo text); CREATE TABLEE bar (bar text); (details: pq: syntax error at or near "TABLEE")`
if err := d.Run(strings.NewReader("CREATE TABLE foo (foo text); CREATE TABLEE bar (bar text);")); err == nil {
t.Fatal("expected err but got nil")
} else if err.Error() != wantErr {
t.Fatalf("expected '%s' but got '%s'", wantErr, err.Error())
}
})
}
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)
d, err := p.Open(addr)
if err != nil {
t.Fatalf("%v", err)
}
defer d.Close()
})
}
@@ -94,14 +126,15 @@ 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())
addr := pgConnectionString(i.Host(), i.Port())
d, err := p.Open(addr)
if err != nil {
t.Fatalf("%v", err)
}
defer d.Close()
// create foobar schema
if err := d.Run(bytes.NewReader([]byte("CREATE SCHEMA foobar AUTHORIZATION postgres"))); err != nil {
if err := d.Run(strings.NewReader("CREATE SCHEMA foobar AUTHORIZATION postgres")); err != nil {
t.Fatal(err)
}
if err := d.SetVersion(1, false); err != nil {
@@ -113,6 +146,7 @@ func TestWithSchema(t *testing.T) {
if err != nil {
t.Fatalf("%v", err)
}
defer d2.Close()
version, _, err := d2.Version()
if err != nil {
@@ -145,6 +179,173 @@ func TestWithSchema(t *testing.T) {
})
}
func TestParallelSchema(t *testing.T) {
mt.ParallelTest(t, versions, isReady,
func(t *testing.T, i mt.Instance) {
p := &Postgres{}
addr := pgConnectionString(i.Host(), i.Port())
d, err := p.Open(addr)
if err != nil {
t.Fatalf("%v", err)
}
defer d.Close()
// create foo and bar schemas
if err := d.Run(strings.NewReader("CREATE SCHEMA foo AUTHORIZATION postgres")); err != nil {
t.Fatal(err)
}
if err := d.Run(strings.NewReader("CREATE SCHEMA bar AUTHORIZATION postgres")); err != nil {
t.Fatal(err)
}
// re-connect using that schemas
dfoo, err := p.Open(fmt.Sprintf("postgres://postgres@%v:%v/postgres?sslmode=disable&search_path=foo", i.Host(), i.Port()))
if err != nil {
t.Fatalf("%v", err)
}
defer dfoo.Close()
dbar, err := p.Open(fmt.Sprintf("postgres://postgres@%v:%v/postgres?sslmode=disable&search_path=bar", i.Host(), i.Port()))
if err != nil {
t.Fatalf("%v", err)
}
defer dbar.Close()
if err := dfoo.Lock(); err != nil {
t.Fatal(err)
}
if err := dbar.Lock(); err != nil {
t.Fatal(err)
}
if err := dbar.Unlock(); err != nil {
t.Fatal(err)
}
if err := dfoo.Unlock(); err != nil {
t.Fatal(err)
}
})
}
func TestWithInstance(t *testing.T) {
}
func TestPostgres_Lock(t *testing.T) {
mt.ParallelTest(t, versions, isReady,
func(t *testing.T, i mt.Instance) {
p := &Postgres{}
addr := pgConnectionString(i.Host(), i.Port())
d, err := p.Open(addr)
if err != nil {
t.Fatalf("%v", err)
}
dt.Test(t, d, []byte("SELECT 1"))
ps := d.(*Postgres)
err = ps.Lock()
if err != nil {
t.Fatal(err)
}
err = ps.Unlock()
if err != nil {
t.Fatal(err)
}
err = ps.Lock()
if err != nil {
t.Fatal(err)
}
err = ps.Unlock()
if err != nil {
t.Fatal(err)
}
})
}
func Test_computeLineFromPos(t *testing.T) {
testcases := []struct {
pos int
wantLine uint
wantCol uint
input string
wantOk bool
}{
{
15, 2, 6, "SELECT *\nFROM foo", true, // foo table does not exists
},
{
16, 3, 6, "SELECT *\n\nFROM foo", true, // foo table does not exists, empty line
},
{
25, 3, 7, "SELECT *\nFROM foo\nWHERE x", true, // x column error
},
{
27, 5, 7, "SELECT *\n\nFROM foo\n\nWHERE x", true, // x column error, empty lines
},
{
10, 2, 1, "SELECT *\nFROMM foo", true, // FROMM typo
},
{
11, 3, 1, "SELECT *\n\nFROMM foo", true, // FROMM typo, empty line
},
{
17, 2, 8, "SELECT *\nFROM foo", true, // last character
},
{
18, 0, 0, "SELECT *\nFROM foo", false, // invalid position
},
}
for i, tc := range testcases {
t.Run("tc"+strconv.Itoa(i), func(t *testing.T) {
run := func(crlf bool, nonASCII bool) {
var name string
if crlf {
name = "crlf"
} else {
name = "lf"
}
if nonASCII {
name += "-nonascii"
} else {
name += "-ascii"
}
t.Run(name, func(t *testing.T) {
input := tc.input
if crlf {
input = strings.Replace(input, "\n", "\r\n", -1)
}
if nonASCII {
input = strings.Replace(input, "FROM", "FRÖM", -1)
}
gotLine, gotCol, gotOK := computeLineFromPos(input, tc.pos)
if tc.wantOk {
t.Logf("pos %d, want %d:%d, %#v", tc.pos, tc.wantLine, tc.wantCol, input)
}
if gotOK != tc.wantOk {
t.Fatalf("expected ok %v but got %v", tc.wantOk, gotOK)
}
if gotLine != tc.wantLine {
t.Fatalf("expected line %d but got %d", tc.wantLine, gotLine)
}
if gotCol != tc.wantCol {
t.Fatalf("expected col %d but got %d", tc.wantCol, gotCol)
}
})
}
run(false, false)
run(true, false)
run(false, true)
run(true, true)
})
}
}
+2 -2
View File
@@ -10,8 +10,8 @@ import (
nurl "net/url"
_ "github.com/cznic/ql/driver"
"github.com/mattes/migrate"
"github.com/mattes/migrate/database"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database"
)
func init() {
+3 -3
View File
@@ -9,9 +9,9 @@ import (
"testing"
_ "github.com/cznic/ql/driver"
"github.com/mattes/migrate"
dt "github.com/mattes/migrate/database/testing"
_ "github.com/mattes/migrate/source/file"
"github.com/golang-migrate/migrate/v4"
dt "github.com/golang-migrate/migrate/v4/database/testing"
_ "github.com/golang-migrate/migrate/v4/source/file"
)
func Test(t *testing.T) {
+18 -3
View File
@@ -1,6 +1,21 @@
Redshift
===
# Redshift
This provides a Redshift driver for migrations. It is used whenever the URL of the database starts with `redshift://`.
`redshift://user:password@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 |
| `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 5439) |
| `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) |
Redshift is PostgreSQL compatible but has some specific features (or lack thereof) that require slightly different behavior.
@@ -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.
+286 -22
View File
@@ -1,46 +1,310 @@
// +build go1.9
package redshift
import (
"net/url"
"context"
"database/sql"
"fmt"
"io"
"io/ioutil"
nurl "net/url"
"strconv"
"strings"
"github.com/mattes/migrate/database"
"github.com/mattes/migrate/database/postgres"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database"
"github.com/lib/pq"
)
// init registers the driver under the name 'redshift'
func init() {
db := new(Redshift)
db.Driver = new(postgres.Postgres)
db := Redshift{}
database.Register("redshift", &db)
}
database.Register("redshift", db)
var DefaultMigrationsTable = "schema_migrations"
var (
ErrNilConfig = fmt.Errorf("no config")
ErrNoDatabaseName = fmt.Errorf("no database name")
)
type Config struct {
MigrationsTable string
DatabaseName string
}
// 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
isLocked bool
conn *sql.Conn
db *sql.DB
// Open and WithInstance need to garantuee that config is never nil
config *Config
}
// 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)
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
}
conn, err := instance.Conn(context.Background())
if err != nil {
return nil, err
}
parsed.Scheme = "postgres"
psql, err := driver.Driver.Open(parsed.String())
px := &Redshift{
conn: conn,
db: instance,
config: config,
}
if err := px.ensureVersionTable(); err != nil {
return nil, err
}
return px, nil
}
func (p *Redshift) Open(url string) (database.Driver, error) {
purl, err := nurl.Parse(url)
if err != nil {
return nil, err
}
purl.Scheme = "postgres"
db, err := sql.Open("postgres", migrate.FilterCustomQuery(purl).String())
if err != nil {
return nil, err
}
return &Redshift{Driver: psql}, nil
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
}
// Lock implements the database.Driver interface by not locking and returning nil.
func (driver *Redshift) Lock() error { return nil }
func (p *Redshift) Close() error {
connErr := p.conn.Close()
dbErr := p.db.Close()
if connErr != nil || dbErr != nil {
return fmt.Errorf("conn: %v, db: %v", connErr, dbErr)
}
return nil
}
// Unlock implements the database.Driver interface by not unlocking and returning nil.
func (driver *Redshift) Unlock() error { return nil }
// Redshift does not support advisory lock functions: https://docs.aws.amazon.com/redshift/latest/dg/c_unsupported-postgresql-functions.html
func (p *Redshift) Lock() error {
if p.isLocked {
return database.ErrLocked
}
p.isLocked = true
return nil
}
func (p *Redshift) Unlock() error {
p.isLocked = false
return nil
}
func (p *Redshift) Run(migration io.Reader) error {
migr, err := ioutil.ReadAll(migration)
if err != nil {
return err
}
// run migration
query := string(migr[:])
if _, err := p.conn.ExecContext(context.Background(), query); err != nil {
if pgErr, ok := err.(*pq.Error); ok {
var line uint
var col uint
var lineColOK bool
if pgErr.Position != "" {
if pos, err := strconv.ParseUint(pgErr.Position, 10, 64); err == nil {
line, col, lineColOK = computeLineFromPos(query, int(pos))
}
}
message := fmt.Sprintf("migration failed: %s", pgErr.Message)
if lineColOK {
message = fmt.Sprintf("%s (column %d)", message, col)
}
if pgErr.Detail != "" {
message = fmt.Sprintf("%s, %s", message, pgErr.Detail)
}
return database.Error{OrigErr: err, Err: message, Query: migr, Line: line}
}
return database.Error{OrigErr: err, Err: "migration failed", Query: migr}
}
return nil
}
func computeLineFromPos(s string, pos int) (line uint, col uint, ok bool) {
// replace crlf with lf
s = strings.Replace(s, "\r\n", "\n", -1)
// pg docs: pos uses index 1 for the first character, and positions are measured in characters not bytes
runes := []rune(s)
if pos > len(runes) {
return 0, 0, false
}
sel := runes[:pos]
line = uint(runesCount(sel, newLine) + 1)
col = uint(pos - 1 - runesLastIndex(sel, newLine))
return line, col, true
}
const newLine = '\n'
func runesCount(input []rune, target rune) int {
var count int
for _, r := range input {
if r == target {
count++
}
}
return count
}
func runesLastIndex(input []rune, target rune) int {
for i := len(input) - 1; i >= 0; i-- {
if input[i] == target {
return i
}
}
return -1
}
func (p *Redshift) SetVersion(version int, dirty bool) error {
tx, err := p.conn.BeginTx(context.Background(), &sql.TxOptions{})
if err != nil {
return &database.Error{OrigErr: err, Err: "transaction start failed"}
}
query := `DELETE FROM "` + p.config.MigrationsTable + `"`
if _, err := tx.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 := tx.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 *Redshift) Version() (version int, dirty bool, err error) {
query := `SELECT version, dirty FROM "` + p.config.MigrationsTable + `" LIMIT 1`
err = p.conn.QueryRowContext(context.Background(), 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 *Redshift) Drop() error {
// select all tables in current schema
query := `SELECT table_name FROM information_schema.tables WHERE table_schema=(SELECT current_schema()) AND table_type='BASE TABLE'`
tables, err := p.conn.QueryContext(context.Background(), 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.conn.ExecContext(context.Background(), query); err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
}
if err := p.ensureVersionTable(); err != nil {
return err
}
}
return nil
}
func (p *Redshift) 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.conn.QueryRowContext(context.Background(), 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.conn.ExecContext(context.Background(), query); err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
return nil
}
+304
View File
@@ -0,0 +1,304 @@
package redshift
// error codes https://github.com/lib/pq/blob/master/error.go
import (
"bytes"
"context"
"database/sql"
sqldriver "database/sql/driver"
"fmt"
"io"
"strconv"
"strings"
"testing"
dt "github.com/golang-migrate/migrate/v4/database/testing"
mt "github.com/golang-migrate/migrate/v4/testing"
)
var versions = []mt.Version{
{Image: "postgres:8"},
}
func redshiftConnectionString(host string, port uint) string {
return connectionString("redshift", host, port)
}
func pgConnectionString(host string, port uint) string {
return connectionString("postgres", host, port)
}
func connectionString(schema, host string, port uint) string {
return fmt.Sprintf("%s://postgres@%s:%v/postgres?sslmode=disable", schema, host, port)
}
func isReady(i mt.Instance) bool {
db, err := sql.Open("postgres", pgConnectionString(i.Host(), i.Port()))
if err != nil {
return false
}
defer db.Close()
if err = db.Ping(); err != nil {
switch err {
case sqldriver.ErrBadConn, io.EOF:
return false
default:
fmt.Println(err)
}
return false
}
return true
}
func Test(t *testing.T) {
mt.ParallelTest(t, versions, isReady,
func(t *testing.T, i mt.Instance) {
p := &Redshift{}
addr := redshiftConnectionString(i.Host(), i.Port())
d, err := p.Open(addr)
if err != nil {
t.Fatalf("%v", err)
}
defer d.Close()
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 := &Redshift{}
addr := redshiftConnectionString(i.Host(), i.Port())
d, err := p.Open(addr)
if err != nil {
t.Fatalf("%v", err)
}
defer d.Close()
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.(*Redshift).conn.QueryRowContext(context.Background(), "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 TestErrorParsing(t *testing.T) {
mt.ParallelTest(t, versions, isReady,
func(t *testing.T, i mt.Instance) {
p := &Redshift{}
addr := redshiftConnectionString(i.Host(), i.Port())
d, err := p.Open(addr)
if err != nil {
t.Fatalf("%v", err)
}
defer d.Close()
wantErr := `migration failed: syntax error at or near "TABLEE" (column 37) in line 1: CREATE TABLE foo ` +
`(foo text); CREATE TABLEE bar (bar text); (details: pq: syntax error at or near "TABLEE")`
if err := d.Run(bytes.NewReader([]byte("CREATE TABLE foo (foo text); CREATE TABLEE bar (bar text);"))); err == nil {
t.Fatal("expected err but got nil")
} else if err.Error() != wantErr {
t.Fatalf("expected '%s' but got '%s'", wantErr, err.Error())
}
})
}
func TestFilterCustomQuery(t *testing.T) {
mt.ParallelTest(t, versions, isReady,
func(t *testing.T, i mt.Instance) {
p := &Redshift{}
addr := fmt.Sprintf("postgres://postgres@%v:%v/postgres?sslmode=disable&x-custom=foobar", i.Host(), i.Port())
d, err := p.Open(addr)
if err != nil {
t.Fatalf("%v", err)
}
defer d.Close()
})
}
func TestWithSchema(t *testing.T) {
mt.ParallelTest(t, versions, isReady,
func(t *testing.T, i mt.Instance) {
p := &Redshift{}
addr := redshiftConnectionString(i.Host(), i.Port())
d, err := p.Open(addr)
if err != nil {
t.Fatalf("%v", err)
}
defer d.Close()
// 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)
}
defer d2.Close()
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) {
}
func TestRedshift_Lock(t *testing.T) {
mt.ParallelTest(t, versions, isReady,
func(t *testing.T, i mt.Instance) {
p := &Redshift{}
addr := pgConnectionString(i.Host(), i.Port())
d, err := p.Open(addr)
if err != nil {
t.Fatalf("%v", err)
}
dt.Test(t, d, []byte("SELECT 1"))
ps := d.(*Redshift)
err = ps.Lock()
if err != nil {
t.Fatal(err)
}
err = ps.Unlock()
if err != nil {
t.Fatal(err)
}
err = ps.Lock()
if err != nil {
t.Fatal(err)
}
err = ps.Unlock()
if err != nil {
t.Fatal(err)
}
})
}
func Test_computeLineFromPos(t *testing.T) {
testcases := []struct {
pos int
wantLine uint
wantCol uint
input string
wantOk bool
}{
{
15, 2, 6, "SELECT *\nFROM foo", true, // foo table does not exists
},
{
16, 3, 6, "SELECT *\n\nFROM foo", true, // foo table does not exists, empty line
},
{
25, 3, 7, "SELECT *\nFROM foo\nWHERE x", true, // x column error
},
{
27, 5, 7, "SELECT *\n\nFROM foo\n\nWHERE x", true, // x column error, empty lines
},
{
10, 2, 1, "SELECT *\nFROMM foo", true, // FROMM typo
},
{
11, 3, 1, "SELECT *\n\nFROMM foo", true, // FROMM typo, empty line
},
{
17, 2, 8, "SELECT *\nFROM foo", true, // last character
},
{
18, 0, 0, "SELECT *\nFROM foo", false, // invalid position
},
}
for i, tc := range testcases {
t.Run("tc"+strconv.Itoa(i), func(t *testing.T) {
run := func(crlf bool, nonASCII bool) {
var name string
if crlf {
name = "crlf"
} else {
name = "lf"
}
if nonASCII {
name += "-nonascii"
} else {
name += "-ascii"
}
t.Run(name, func(t *testing.T) {
input := tc.input
if crlf {
input = strings.Replace(input, "\n", "\r\n", -1)
}
if nonASCII {
input = strings.Replace(input, "FROM", "FRÖM", -1)
}
gotLine, gotCol, gotOK := computeLineFromPos(input, tc.pos)
if tc.wantOk {
t.Logf("pos %d, want %d:%d, %#v", tc.pos, tc.wantLine, tc.wantCol, input)
}
if gotOK != tc.wantOk {
t.Fatalf("expected ok %v but got %v", tc.wantOk, gotOK)
}
if gotLine != tc.wantLine {
t.Fatalf("expected line %d but got %d", tc.wantLine, gotLine)
}
if gotCol != tc.wantCol {
t.Fatalf("expected col %d but got %d", tc.wantCol, gotCol)
}
})
}
run(false, false)
run(true, false)
run(false, true)
run(true, true)
})
}
}
+35
View File
@@ -0,0 +1,35 @@
# Google Cloud Spanner
## Usage
The DSN must be given in the following format.
`spanner://projects/{projectId}/instances/{instanceId}/databases/{databaseName}`
See [Google Spanner Documentation](https://cloud.google.com/spanner/docs) for details.
| Param | WithInstance Config | Description |
| ----- | ------------------- | ----------- |
| `x-migrations-table` | `MigrationsTable` | Name of the migrations table |
| `url` | `DatabaseName` | The full path to the Spanner database resource. If provided as part of `Config` it must not contain a scheme or query string to match the format `projects/{projectId}/instances/{instanceId}/databases/{databaseName}`|
| `projectId` || The Google Cloud Platform project id
| `instanceId` || The id of the instance running Spanner
| `databaseName` || The name of the Spanner database
> **Note:** Google Cloud Spanner migrations can take a considerable amount of
> time. The migrations provided as part of the example take about 6 minutes to
> run on a small instance.
>
> ```log
> 1481574547/u create_users_table (21.354507597s)
> 1496539702/u add_city_to_users (41.647359754s)
> 1496601752/u add_index_on_user_emails (2m12.155787369s)
> 1496602638/u create_books_table (2m30.77299181s)
## Testing
To unit test the `spanner` driver, `SPANNER_DATABASE` needs to be set. You'll
need to sign-up to Google Cloud Platform (GCP) and have a running Spanner
instance since it is not possible to run Google Spanner outside GCP.
@@ -0,0 +1 @@
DROP TABLE Users
@@ -0,0 +1,5 @@
CREATE TABLE Users (
UserId INT64,
Name STRING(40),
Email STRING(83)
) PRIMARY KEY(UserId)
@@ -0,0 +1 @@
ALTER TABLE Users DROP COLUMN city
@@ -0,0 +1 @@
ALTER TABLE Users ADD COLUMN city STRING(100)
@@ -0,0 +1 @@
DROP INDEX UsersEmailIndex
@@ -0,0 +1 @@
CREATE UNIQUE INDEX UsersEmailIndex ON Users (Email)
@@ -0,0 +1 @@
DROP TABLE Books
@@ -0,0 +1,6 @@
CREATE TABLE Books (
UserId INT64,
Name STRING(40),
Author STRING(40)
) PRIMARY KEY(UserId, Name),
INTERLEAVE IN PARENT Users ON DELETE CASCADE
+301
View File
@@ -0,0 +1,301 @@
package spanner
import (
"fmt"
"io"
"io/ioutil"
"log"
nurl "net/url"
"regexp"
"strings"
"golang.org/x/net/context"
"cloud.google.com/go/spanner"
sdb "cloud.google.com/go/spanner/admin/database/apiv1"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database"
"google.golang.org/api/iterator"
adminpb "google.golang.org/genproto/googleapis/spanner/admin/database/v1"
)
func init() {
db := Spanner{}
database.Register("spanner", &db)
}
// DefaultMigrationsTable is used if no custom table is specified
const DefaultMigrationsTable = "SchemaMigrations"
// Driver errors
var (
ErrNilConfig = fmt.Errorf("no config")
ErrNoDatabaseName = fmt.Errorf("no database name")
ErrNoSchema = fmt.Errorf("no schema")
ErrDatabaseDirty = fmt.Errorf("database is dirty")
)
// Config used for a Spanner instance
type Config struct {
MigrationsTable string
DatabaseName string
}
// Spanner implements database.Driver for Google Cloud Spanner
type Spanner struct {
db *DB
config *Config
}
type DB struct {
admin *sdb.DatabaseAdminClient
data *spanner.Client
}
func NewDB(admin sdb.DatabaseAdminClient, data spanner.Client) *DB {
return &DB{
admin: &admin,
data: &data,
}
}
// WithInstance implements database.Driver
func WithInstance(instance *DB, config *Config) (database.Driver, error) {
if config == nil {
return nil, ErrNilConfig
}
if len(config.DatabaseName) == 0 {
return nil, ErrNoDatabaseName
}
if len(config.MigrationsTable) == 0 {
config.MigrationsTable = DefaultMigrationsTable
}
sx := &Spanner{
db: instance,
config: config,
}
if err := sx.ensureVersionTable(); err != nil {
return nil, err
}
return sx, nil
}
// Open implements database.Driver
func (s *Spanner) Open(url string) (database.Driver, error) {
purl, err := nurl.Parse(url)
if err != nil {
return nil, err
}
ctx := context.Background()
adminClient, err := sdb.NewDatabaseAdminClient(ctx)
if err != nil {
return nil, err
}
dbname := strings.Replace(migrate.FilterCustomQuery(purl).String(), "spanner://", "", 1)
dataClient, err := spanner.NewClient(ctx, dbname)
if err != nil {
log.Fatal(err)
}
migrationsTable := purl.Query().Get("x-migrations-table")
if len(migrationsTable) == 0 {
migrationsTable = DefaultMigrationsTable
}
db := &DB{admin: adminClient, data: dataClient}
return WithInstance(db, &Config{
DatabaseName: dbname,
MigrationsTable: migrationsTable,
})
}
// Close implements database.Driver
func (s *Spanner) Close() error {
s.db.data.Close()
return s.db.admin.Close()
}
// Lock implements database.Driver but doesn't do anything because Spanner only
// enqueues the UpdateDatabaseDdlRequest.
func (s *Spanner) Lock() error {
return nil
}
// Unlock implements database.Driver but no action required, see Lock.
func (s *Spanner) Unlock() error {
return nil
}
// Run implements database.Driver
func (s *Spanner) Run(migration io.Reader) error {
migr, err := ioutil.ReadAll(migration)
if err != nil {
return err
}
// run migration
stmts := migrationStatements(migr)
ctx := context.Background()
op, err := s.db.admin.UpdateDatabaseDdl(ctx, &adminpb.UpdateDatabaseDdlRequest{
Database: s.config.DatabaseName,
Statements: stmts,
})
if err != nil {
return &database.Error{OrigErr: err, Err: "migration failed", Query: migr}
}
if err := op.Wait(ctx); err != nil {
return &database.Error{OrigErr: err, Err: "migration failed", Query: migr}
}
return nil
}
// SetVersion implements database.Driver
func (s *Spanner) SetVersion(version int, dirty bool) error {
ctx := context.Background()
_, err := s.db.data.ReadWriteTransaction(ctx,
func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
m := []*spanner.Mutation{
spanner.Delete(s.config.MigrationsTable, spanner.AllKeys()),
spanner.Insert(s.config.MigrationsTable,
[]string{"Version", "Dirty"},
[]interface{}{version, dirty},
)}
return txn.BufferWrite(m)
})
if err != nil {
return &database.Error{OrigErr: err}
}
return nil
}
// Version implements database.Driver
func (s *Spanner) Version() (version int, dirty bool, err error) {
ctx := context.Background()
stmt := spanner.Statement{
SQL: `SELECT Version, Dirty FROM ` + s.config.MigrationsTable + ` LIMIT 1`,
}
iter := s.db.data.Single().Query(ctx, stmt)
defer iter.Stop()
row, err := iter.Next()
switch err {
case iterator.Done:
return database.NilVersion, false, nil
case nil:
var v int64
if err = row.Columns(&v, &dirty); err != nil {
return 0, false, &database.Error{OrigErr: err, Query: []byte(stmt.SQL)}
}
version = int(v)
default:
return 0, false, &database.Error{OrigErr: err, Query: []byte(stmt.SQL)}
}
return version, dirty, nil
}
// Drop implements database.Driver. Retrieves the database schema first and
// creates statements to drop the indexes and tables accordingly.
// Note: The drop statements are created in reverse order to how they're
// provided in the schema. Assuming the schema describes how the database can
// be "build up", it seems logical to "unbuild" the database simply by going the
// opposite direction. More testing
func (s *Spanner) Drop() error {
ctx := context.Background()
res, err := s.db.admin.GetDatabaseDdl(ctx, &adminpb.GetDatabaseDdlRequest{
Database: s.config.DatabaseName,
})
if err != nil {
return &database.Error{OrigErr: err, Err: "drop failed"}
}
if len(res.Statements) == 0 {
return nil
}
r := regexp.MustCompile(`(CREATE TABLE\s(\S+)\s)|(CREATE.+INDEX\s(\S+)\s)`)
stmts := make([]string, 0)
for i := len(res.Statements) - 1; i >= 0; i-- {
s := res.Statements[i]
m := r.FindSubmatch([]byte(s))
if len(m) == 0 {
continue
} else if tbl := m[2]; len(tbl) > 0 {
stmts = append(stmts, fmt.Sprintf(`DROP TABLE %s`, tbl))
} else if idx := m[4]; len(idx) > 0 {
stmts = append(stmts, fmt.Sprintf(`DROP INDEX %s`, idx))
}
}
op, err := s.db.admin.UpdateDatabaseDdl(ctx, &adminpb.UpdateDatabaseDdlRequest{
Database: s.config.DatabaseName,
Statements: stmts,
})
if err != nil {
return &database.Error{OrigErr: err, Query: []byte(strings.Join(stmts, "; "))}
}
if err := op.Wait(ctx); err != nil {
return &database.Error{OrigErr: err, Query: []byte(strings.Join(stmts, "; "))}
}
if err := s.ensureVersionTable(); err != nil {
return err
}
return nil
}
func (s *Spanner) ensureVersionTable() error {
ctx := context.Background()
tbl := s.config.MigrationsTable
iter := s.db.data.Single().Read(ctx, tbl, spanner.AllKeys(), []string{"Version"})
if err := iter.Do(func(r *spanner.Row) error { return nil }); err == nil {
return nil
}
stmt := fmt.Sprintf(`CREATE TABLE %s (
Version INT64 NOT NULL,
Dirty BOOL NOT NULL
) PRIMARY KEY(Version)`, tbl)
op, err := s.db.admin.UpdateDatabaseDdl(ctx, &adminpb.UpdateDatabaseDdlRequest{
Database: s.config.DatabaseName,
Statements: []string{stmt},
})
if err != nil {
return &database.Error{OrigErr: err, Query: []byte(stmt)}
}
if err := op.Wait(ctx); err != nil {
return &database.Error{OrigErr: err, Query: []byte(stmt)}
}
return nil
}
func migrationStatements(migration []byte) []string {
regex := regexp.MustCompile(";$")
migrationString := string(migration[:])
migrationString = strings.TrimSpace(migrationString)
migrationString = regex.ReplaceAllString(migrationString, "")
statements := strings.Split(migrationString, ";")
return statements
}
+28
View File
@@ -0,0 +1,28 @@
package spanner
import (
"fmt"
"os"
"testing"
dt "github.com/golang-migrate/migrate/v4/database/testing"
)
func Test(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
db, ok := os.LookupEnv("SPANNER_DATABASE")
if !ok {
t.Skip("SPANNER_DATABASE not set, skipping test.")
}
s := &Spanner{}
addr := fmt.Sprintf("spanner://%v", db)
d, err := s.Open(addr)
if err != nil {
t.Fatalf("%v", err)
}
dt.Test(t, d, []byte("SELECT 1"))
}
@@ -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;
+214
View File
@@ -0,0 +1,214 @@
package sqlite3
import (
"database/sql"
"fmt"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database"
_ "github.com/mattn/go-sqlite3"
"io"
"io/ioutil"
nurl "net/url"
"strings"
)
func init() {
database.Register("sqlite3", &Sqlite{})
}
var DefaultMigrationsTable = "schema_migrations"
var (
ErrDatabaseDirty = fmt.Errorf("database is dirty")
ErrNilConfig = fmt.Errorf("no config")
ErrNoDatabaseName = fmt.Errorf("no database name")
)
type Config struct {
MigrationsTable string
DatabaseName string
}
type Sqlite 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 := &Sqlite{
db: instance,
config: config,
}
if err := mx.ensureVersionTable(); err != nil {
return nil, err
}
return mx, nil
}
func (m *Sqlite) ensureVersionTable() error {
query := fmt.Sprintf(`
CREATE TABLE IF NOT EXISTS %s (version uint64,dirty bool);
CREATE UNIQUE INDEX IF NOT EXISTS version_unique ON %s (version);
`, DefaultMigrationsTable, DefaultMigrationsTable)
if _, err := m.db.Exec(query); err != nil {
return err
}
return nil
}
func (m *Sqlite) Open(url string) (database.Driver, error) {
purl, err := nurl.Parse(url)
if err != nil {
return nil, err
}
dbfile := strings.Replace(migrate.FilterCustomQuery(purl).String(), "sqlite3://", "", 1)
db, err := sql.Open("sqlite3", 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 *Sqlite) Close() error {
return m.db.Close()
}
func (m *Sqlite) Drop() error {
query := `SELECT name FROM sqlite_master WHERE type = '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 {
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
}
query := "VACUUM"
_, err = m.db.Query(query)
if err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
}
return nil
}
func (m *Sqlite) Lock() error {
if m.isLocked {
return database.ErrLocked
}
m.isLocked = true
return nil
}
func (m *Sqlite) Unlock() error {
if !m.isLocked {
return nil
}
m.isLocked = false
return nil
}
func (m *Sqlite) Run(migration io.Reader) error {
migr, err := ioutil.ReadAll(migration)
if err != nil {
return err
}
query := string(migr[:])
return m.executeQuery(query)
}
func (m *Sqlite) 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 *Sqlite) SetVersion(version int, dirty bool) error {
tx, err := m.db.Begin()
if err != nil {
return &database.Error{OrigErr: err, Err: "transaction start failed"}
}
query := "DELETE FROM " + 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 *Sqlite) 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
}
+61
View File
@@ -0,0 +1,61 @@
package sqlite3
import (
"database/sql"
"fmt"
"github.com/golang-migrate/migrate/v4"
dt "github.com/golang-migrate/migrate/v4/database/testing"
_ "github.com/golang-migrate/migrate/v4/source/file"
_ "github.com/mattn/go-sqlite3"
"io/ioutil"
"os"
"path/filepath"
"testing"
)
func Test(t *testing.T) {
dir, err := ioutil.TempDir("", "sqlite3-driver-test")
if err != nil {
return
}
defer func() {
os.RemoveAll(dir)
}()
fmt.Printf("DB path : %s\n", filepath.Join(dir, "sqlite3.db"))
p := &Sqlite{}
addr := fmt.Sprintf("sqlite3://%s", filepath.Join(dir, "sqlite3.db"))
d, err := p.Open(addr)
if err != nil {
t.Fatalf("%v", err)
}
db, err := sql.Open("sqlite3", filepath.Join(dir, "sqlite3.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)
}
}
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"io/ioutil"
"reflect"
"github.com/mattes/migrate/database"
"github.com/golang-migrate/migrate/v4/database"
)
func init() {
+1 -1
View File
@@ -3,7 +3,7 @@ package stub
import (
"testing"
dt "github.com/mattes/migrate/database/testing"
dt "github.com/golang-migrate/migrate/v4/database/testing"
)
func Test(t *testing.T) {

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