Compare commits

...
194 Commits
Author SHA1 Message Date
Matthias KadenbachandGitHub dfe3ec0fce Merge pull request #198 from rmulley/travis-go-1.8
TravisCI Updates
2017-07-09 11:00:29 -07:00
Matthias KadenbachandGitHub 01b25ee18b Merge branch 'v1' into travis-go-1.8 2017-07-09 10:59:35 -07:00
Matthias KadenbachandGitHub afc973c4e5 Merge pull request #216 from thingful/v1.3.1-neo4j-https
Register neo4j driver using a pseudo `neo4js` protocol scheme.
2017-05-09 10:37:06 -07:00
Sam Mulube 250543a561 Register neo4j driver using a pseudo neo4js protocol scheme.
Neo4J supports both `http` and `https` connections, however `https`
connections weren't supported previously as the `neo4j` URL scheme
currently used to identify the driver type is always substituted for
just `http`.

This PR also registers the driver under an alternate key: `neo4js` which
given the way the scheme substitution happens, should allow the driver
to correctly create an `https` connection URL. This will allow the
library to be used for cases where we don't have access to the plain
`http` endpoint.
2017-05-09 09:54:37 +01:00
Matthias KadenbachandGitHub 1dffba9fa5 Merge pull request #212 from mattes/v1-gopkg
v1: github.com/mattes/migrate -> gopkg.in/mattes/migrate.v1
2017-05-06 16:25:42 -07:00
Matthias Kadenbach 964e6bc070 rewrite in readmes 2017-05-06 16:18:32 -07:00
Matthias Kadenbach 1887c0f3e3 rewrite paths 2017-05-06 16:13:08 -07:00
Matthias Kadenbach 62dba4f8ea update travis branch 2017-04-25 12:02:57 -07:00
Matthias KadenbachandGitHub 78ca4eb007 Merge pull request #190 from alaczi/mssql_support
MSSQL support
2017-04-25 11:35:40 -07:00
Andras Laczi d21443f74b Remove go1.4 from travis as the ql package is not compatible and breaks the build 2017-04-25 09:35:03 +01:00
Andras Laczi 7f4000c0d6 Add wait-retry to mssql test 2017-04-24 13:15:17 +01:00
Andras Laczi ecd74e1ce6 Add go 1.8 to travis config
Fix getting the latest version
2017-04-24 13:15:12 +01:00
Matthias Kadenbach 9f14676773 Update Readme 2017-04-20 15:04:54 -07:00
Matthias KadenbachandGitHub 7dde434714 Update version.go 2017-04-20 11:50:49 -07:00
Robert Mulley fff063351d Pull latest patch versions for each go minor version 2017-04-13 13:01:59 -06:00
Robert Mulley 5498e70562 Bump docker-compose to latest, 1.11.2 2017-04-13 12:54:35 -06:00
Robert Mulley 0da98940d6 Add go 1.8 to test matrix 2017-04-13 12:44:27 -06:00
Matthias KadenbachandGitHub 4cfc044489 Merge pull request #193 from Arkq/master
Support for alternative postgres URI scheme
2017-04-03 10:33:35 -07:00
Arkadiusz Bokowy 83c703f09f Support for alternative postgres URI scheme
According to the PostgreSQL documentation (section 32.1.1.2), postgres
library supports two URI schemes: postgresql:// and postgres://

Reference:
https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING
2017-04-03 09:15:25 +02:00
Andras Laczi b7845a779e Add MSSQL support 2017-03-30 15:32:14 +01:00
Matthias KadenbachandGitHub 09640b136e Merge pull request #189 from nathj07/issue-188-sqlite-err-cast
fixing error cast for sqlite3
2017-03-29 11:48:54 -07:00
Nathan Davies 7ca71e567d fixing error cast for sqlite3
Seems the sqlite3 packaged reverted to using non-pointer Error. This patch corrects the use of it here.
2017-03-29 14:06:38 +01:00
Matthias KadenbachandGitHub c7653285b2 Merge pull request #185 from adrianduke/master
driver/sqlite3: Fix for upstream changes
2017-03-20 10:09:33 -07:00
Adrian Duke f4979054e2 driver/sqlite3 fix for upstream change sqlite3.Error{} Error() now takes a pointer receiver 2017-03-20 16:07:01 +00:00
Matthias KadenbachandGitHub 305a63a447 Merge pull request #181 from chrisaxiom/master
Call Close() in Version()
2017-03-02 13:39:55 -08:00
Chris S 528bc5bbbb close driver 2017-03-02 12:48:31 -08:00
Matthias KadenbachandGitHub 83e27c8b88 Merge pull request #176 from Synesis-LLC/stage
Golint. Rewrite cassandra driver
2017-02-27 10:12:48 -08:00
Pavel Sapezhko e8bfb1b2b3 Merge remote-tracking branch 'upstream/master' into stage 2017-02-27 17:56:15 +03:00
Matthias KadenbachandGitHub 2cd9bfcd04 Merge pull request #173 from sekimura/govet
Fix for go vet
2017-02-21 10:35:02 -08:00
Pavel Sapezhko 21ccec2aa1 allow create migration files without db connection 2017-02-21 09:13:59 +03:00
Masa Sekimura fe6711645a Fix for go vet
Avoid getting caught by go vet for using "\n" with Println

	main.go:187: Println call ends with newline
2017-02-20 23:39:51 +00:00
Pavel Sapezhko adf1cff659 drivers: cassandra: rewrite 2017-02-17 17:59:38 +03:00
Pavel Sapezhko da12e27062 fix some lint errors 2017-02-17 13:06:26 +03:00
Matthias KadenbachandGitHub d2ad6f8ecf Update README.md 2017-02-10 02:42:46 -08:00
Matthias KadenbachandGitHub bbb58aca8d Merge pull request #146 from FreGalle/postgres-version-table-check-schema
Postgres: version table existence check in current schema
2017-02-04 10:17:41 -08:00
Frederik Galle 5aed294d25 Check version table existence for current schema 2017-02-03 12:47:24 +01:00
Matthias Kadenbach e92f038f40 skip neo4j tests 2017-01-23 13:54:25 -08:00
Matthias KadenbachandGitHub 0b3426bf5d Merge pull request #25 from daddykotex/master
Ensure that interrupt signals are not relayed to the interrupt channel when the migration is over
2017-01-23 13:37:42 -08:00
Matthias Kadenbach 0861ff957d Update Readme 2017-01-23 13:36:22 -08:00
Matthias Kadenbach 32607ef715 Merge branch 'carlosforero-neo4j-driver' 2017-01-23 13:32:56 -08:00
Matthias Kadenbach c643ffd6d6 fix merge conflicts 2017-01-23 13:32:48 -08:00
Matthias KadenbachandGitHub b8b3883950 Merge pull request #101 from bfosberry/fixing-unsafe-cast
Fixing unsafe typecast
2017-01-23 13:26:59 -08:00
Matthias KadenbachandGitHub 3547fc54cb Merge pull request #144 from benma/patch-1
Update version.go
2017-01-19 09:32:29 -08:00
benmaandGitHub 64c41a8af0 Update version.go 2017-01-19 14:11:19 +01:00
Matthias KadenbachandGitHub 4191ac9cfe Merge pull request #142 from goenning/master
allow use of big version numbers
2017-01-18 15:48:54 -08:00
goenning f1738e78db allow use of big version numbers 2017-01-18 22:19:02 +00:00
Matthias KadenbachandGitHub de494e4b98 Merge pull request #128 from oliverkofoed/master
Add support for ScyllaDB (remove cql counter use)
2017-01-10 09:26:17 -08:00
Matthias KadenbachandGitHub 585509f3bc Merge pull request #138 from mattes/add-mongo-to-readme
add mongodb to readme
2017-01-09 10:43:35 -08:00
Matthias KadenbachandGitHub f630dc5971 Update README.md 2017-01-09 10:43:22 -08:00
Matthias KadenbachandGitHub 9a8f78ba3b Merge pull request #137 from drboyer/updown-colors
Change colors for up and down migration arrows
2017-01-09 10:39:55 -08:00
Matthias KadenbachandGitHub 63c6168c7f Merge pull request #136 from dimag-jfrog/prepare_pr_2_rebase
Driver for MongoDB (New)
2017-01-09 10:38:56 -08:00
Devin Boyer 0c80688879 Change colors for up and down migration arrows 2017-01-08 21:33:04 -05:00
dimag 1993f0783b Merge remote-tracking branch 'fork/prepare_pr_2_rebase' into prepare_pr_2_rebase
# Conflicts:
#	docker-compose.yml
2017-01-07 16:44:36 +02:00
dimag 70238f448b - Reverted repository from dimag-jfrog to mattes to prepare for pull request 2017-01-07 16:36:25 +02:00
dimag 914c38e379 - Fixed test with not-exported method identified as missing: unified the 2 errors into one
- Fixed error message formatting
- Ensuring uniqueness of the version field (added unique index)
2017-01-07 16:34:06 +02:00
dimag 0f28b01383 specifying version of mongo 2017-01-07 16:34:06 +02:00
dimag 6909594c0b Checking non-exported migration method 2017-01-07 16:33:10 +02:00
dimag 583921e8bf Added README.md 2017-01-07 16:33:10 +02:00
dimag 61ecf2c096 - Refactored project structure to be consistent with original project
- Added docker support
2017-01-07 16:33:10 +02:00
dimag 0eecb74230 Added explicit migration method signature validation 2017-01-07 16:30:40 +02:00
dimag b7b75f3c59 - registering method receiver directly for the driver
- currently each driver contains only a single method receiver:
   - enforcing method receiver pre-registration on go methods driver initialization
   - Method receiver name can be removed from files format
- passing the DbName parameter inside the method receiver for the go methods driver
2017-01-07 16:30:40 +02:00
dimag fe48ddfdcc - redefined mongo db driver to be autonomous and not depending on specific methods
- defined methods receivers registration by name and change the migration files format to include them
- added extensive testing
2017-01-07 16:30:40 +02:00
dimag 70b23c3f84 Relaxed constratint that method receiver has to be of Driver interface. 2017-01-07 16:30:40 +02:00
dimag 223908f9d2 - Reset all changes to the upstream branch
- Changed logic not to use custom filename parser:
  -Supporting up and down files only, no both direction files
  -Using method names as is
- Added complete test to the mongo db migration scenarios
2017-01-07 16:30:40 +02:00
dimag 1838852d4d Changes that interfere with the Migrate open source code:
- Added go methods migrator, mongo db template: different from the usual driver
model.
- Added support for bidirectional files (for go methods), appending _up or _down upon context
- Added DriverWithFilnameParser for providing custom filename parser functionality that knows to parse bi-directional file names.
2017-01-07 16:29:19 +02:00
dimag 175550643c changed import paths to point to dimag-jfrog 2017-01-07 16:24:14 +02:00
dimag 34a3e58ab9 - Reverted repository from dimag-jfrog to mattes to prepare for pull request 2017-01-07 16:16:00 +02:00
dimag dec52a778f - Using double quotes for parameter in docker-compose - to be more aligned with master repository 2017-01-07 16:12:48 +02:00
dimag 8865262a03 - Fixed test with not-exported method identified as missing: unified the 2 errors into one
- Fixed error message formatting
- Ensuring uniqueness of the version field (added unique index)
2017-01-07 16:08:09 +02:00
Matthias KadenbachandGitHub eb0eec4471 Merge pull request #135 from ansel1/master
Avoid DDL when checking for versions table #134
2017-01-06 14:24:59 -08:00
Russ Egan da6c505f41 Avoid DDL when checking for versions table #134 2017-01-06 15:52:54 -05:00
Matthias KadenbachandGitHub 966ba843e0 Merge pull request #133 from wlcx/add-ql-driver
Add Ql driver
2017-01-05 14:04:34 -08:00
Sam Willcocks 5773f75698 Add Ql driver
Adds a driver for the embedded Ql database (https://github.com/cznic/ql)
2017-01-05 15:09:41 +00:00
Matthias KadenbachandGitHub 3e3cef3763 Merge pull request #131 from dacamp/patch-1
Update README to reflect epoch filenames
2016-12-22 14:18:38 -08:00
David CampbellandGitHub d4796c8104 Update README to reflect epoch filenames 2016-12-22 12:30:34 -08:00
Matthias KadenbachandGitHub cd15e67614 Merge pull request #104 from connctd/crate
Driver for the Crate.io database
2016-12-21 20:57:04 -08:00
Matthias KadenbachandGitHub 11b4db8a90 Merge pull request #73 from uber/create_epoch
prefix migration files with epoch
2016-12-21 20:51:44 -08:00
Matthias KadenbachandGitHub 474d0f5a60 Merge pull request #90 from jmhodges/fail_on_missing_command
use a non-zero exit code when no command is given
2016-12-21 20:48:03 -08:00
Matthias KadenbachandGitHub b4d9b6ae24 Merge pull request #59 from bfitzsimmons/patch-1
Fixes typos.
2016-12-21 20:44:14 -08:00
Matthias KadenbachandGitHub e97cecb0cf Merge pull request #124 from dacamp/skip
Skip tests requiring a DB connection with the '-short' flag
2016-12-21 20:42:53 -08:00
Matthias KadenbachandGitHub 492671437f Merge pull request #125 from dacamp/travis
Bump travis versions to use Go 1.6 and 1.7
2016-12-21 20:41:32 -08:00
Matthias KadenbachandGitHub cb2fa36c8b Merge pull request #81 from uzimonkey/master
Release OS signals when you're finished with them.
2016-12-21 20:32:53 -08:00
Matthias KadenbachandGitHub b94dd6fc29 Merge pull request #96 from meehow/patch-1
typo fixed
2016-12-21 20:32:19 -08:00
Matthias KadenbachandGitHub 7910a55d0e Merge pull request #103 from cgilling/better_cassandra_uri_errors
cassandra: add error checking for bad connection urls
2016-12-21 20:30:43 -08:00
Matthias KadenbachandGitHub ae018f8ade Merge pull request #119 from trietphm/master
Add Cassandra url format example
2016-12-21 20:27:54 -08:00
Matthias KadenbachandGitHub 33aebee975 Merge pull request #129 from mattes/mattes-docs-1
Update README.md
2016-12-21 20:14:05 -08:00
Matthias KadenbachandGitHub 478fb42df5 Update README.md 2016-12-21 20:13:57 -08:00
Matthias KadenbachandGitHub 9c7172291e Merge pull request #114 from skeswa/feature/consistency-in-conn-str
Added consistency parameter to cassandra connection string
2016-12-21 20:01:06 -08:00
Oliver Kofoed 9e11d704a4 Add support for ScyllaDB (remove cql counter use) 2016-12-18 19:25:52 -08:00
David Campbell d290bf9460 - docker command should remain 'go-test' 2016-10-23 15:11:04 -07:00
David Campbell 04e1cb0cfc Bump travis versions to use Go 1.6 and 1.7 2016-10-23 15:05:54 -07:00
David Campbell 9a558eed89 Skip tests requiring a DB connection with the '-short' flag
- go-test target for local testing
2016-10-23 15:01:12 -07:00
Triet PhamandGitHub 74b161fdb2 Add Cassandra url format 2016-08-31 10:20:16 +07:00
dimag 32cb825110 specifying version of mongo 2016-08-10 21:33:36 +03:00
dimag 9bd4c8c83c Checking non-exported migration method 2016-08-10 21:25:36 +03:00
dimag e9ede5506a Added README.md 2016-08-10 20:39:41 +03:00
dimag e27b9c2a7b - Refactored project structure to be consistent with original project
- Added docker support
2016-08-10 17:15:17 +03:00
dimag 834a096bf8 Added explicit migration method signature validation 2016-08-10 14:13:53 +03:00
dimag 866ff82ffe - registering method receiver directly for the driver
- currently each driver contains only a single method receiver:
   - enforcing method receiver pre-registration on go methods driver initialization
   - Method receiver name can be removed from files format
- passing the DbName parameter inside the method receiver for the go methods driver
2016-08-09 21:16:21 +03:00
dimag 570e4b42be - redefined mongo db driver to be autonomous and not depending on specific methods
- defined methods receivers registration by name and change the migration files format to include them
- added extensive testing
2016-08-09 16:47:34 +03:00
dimag c71fea90d6 Relaxed constratint that method receiver has to be of Driver interface. 2016-08-08 21:34:52 +03:00
dimag 5f7b2caee8 - Reset all changes to the upstream branch
- Changed logic not to use custom filename parser:
  -Supporting up and down files only, no both direction files
  -Using method names as is
- Added complete test to the mongo db migration scenarios
2016-08-08 21:07:37 +03:00
dimag 64101a13e9 Changes that interfere with the Migrate open source code:
- Added go methods migrator, mongo db template: different from the usual driver
model.
- Added support for bidirectional files (for go methods), appending _up or _down upon context
- Added DriverWithFilnameParser for providing custom filename parser functionality that knows to parse bi-directional file names.
2016-08-08 16:29:25 +03:00
dimag 8630ea78b7 changed import paths to point to dimag-jfrog 2016-08-04 15:52:31 +03:00
Sandile Keswa aa4dc1252b Oops! Correcting a renaming error. 2016-07-25 13:25:45 -04:00
Sandile Keswa 0cd470ee98 Adopted @nlacey's suggestion. 2016-07-25 12:02:50 -04:00
Sandile Keswa cba0cc9847 fixed the overflow 2016-07-23 15:04:11 -04:00
Sandile Keswa 42b45a6de8 added consistency as an option 2016-07-23 14:47:11 -04:00
Till Klocke cd5f75d47c Splitting of sql is now more robust and better tested 2016-06-16 14:34:50 +02:00
Till Klocke 6612806e06 Crate driver was using wrong url scheme. The migrate CLI never had a chance to select the correct driver 2016-06-16 13:47:21 +02:00
Till Klocke a7fa9eb3fb Added README.md for Crate driver 2016-06-16 13:08:04 +02:00
Till Klocke 907d8cd935 Added driver for crate.io database 2016-06-16 13:03:01 +02:00
Chris Gilling b5ef068704 cassandra: add error checking for bad connection urls 2016-06-12 14:59:23 -07:00
Brendan Fosberry b68c58c136 Fixing unsafe typecast
If a non pq error is returned, we rollback and return the error
2016-06-03 13:41:05 -05:00
meehow f230ced201 typo fixed 2016-05-15 08:12:06 +02:00
Jeff Hodges 91a3364232 use a non-zero exit code when no command is given 2016-04-08 14:26:39 -07:00
Matthias Kadenbach ce1b59bb81 Merge pull request #89 from arbarlow/master
Add support for Cassandra >= 3 with protocol version 4
2016-03-21 14:09:30 -07:00
Alex Barlow 12c19e08b7 Add support for Cassandra >= 3 with protocol version 4 2016-03-21 17:36:03 +00:00
Michael Morin 7f9f7e5978 Release OS signals when you're finished with them. 2016-02-11 22:35:49 -05:00
Chris McNabbandDavid Campbell 90bab2409b gocql doesn't support cassandra > 2.2 2016-02-01 12:01:09 -08:00
Chris McNabbandDavid Campbell 10d89631a3 https://github.com/mattn/go-sqlite3 is cgo, so CGO_ENABLED must be true 2016-02-01 12:01:09 -08:00
Matthias Kadenbach 5593c9892b Merge pull request #78 from raizyr/sqlite3_is_cgo
https://github.com/mattn/go-sqlite3 is cgo, so CGO_ENABLED must be true
2016-01-11 23:38:19 -08:00
Matthias Kadenbach 081a4e2cae Merge pull request #79 from raizyr/cassandra_2_2
gocql doesn't support cassandra > 2.2
2016-01-11 23:25:52 -08:00
Chris McNabb 5a8219c970 gocql doesn't support cassandra > 2.2 2016-01-10 21:39:37 -06:00
Chris McNabb 5a245a959a https://github.com/mattn/go-sqlite3 is cgo, so CGO_ENABLED must be true 2016-01-10 08:09:34 -06:00
David Campbell 9694775972 prefix migration files with epoch
Using epoch prefixed filenames, multiple developers can commit
migrations while minimizing versioning conflicts.

This commit remains backwards compatible with all previous migrations
and requires no changes to the `schema_migrations` table.

This commit also cleans up migrate-test temp directories
2015-12-20 16:18:25 -08:00
Carlos Forero e303d64469 Add driver for Neo4j 2015-12-08 19:39:22 -05:00
Matthias Kadenbach 6028882529 Merge pull request #66 from jamiecuthill/patch-1
Error in example mysql driver import statement
2015-12-04 09:50:58 -08:00
Jamie Cuthill 2f6745a42f Error in example mysql driver import statement 2015-11-13 11:57:52 +00:00
Matthias Kadenbach 8fe1a56731 Merge pull request #65 from brainly/import-drivers-in-cli
Import drivers in CLI
2015-11-12 14:39:06 -08:00
Oleksii Fedorov f5053fbe29 Import drivers in CLI 2015-11-12 11:38:53 +01:00
Matthias Kadenbach 2a2bc4fec8 Merge pull request #54 from gemnasium/dont-load-all-drivers-43
Don't load all drivers
2015-11-11 10:04:20 -08:00
Brant Fitzsimmons ade683e8b5 Fixes typos. 2015-11-01 13:14:23 -05:00
Philippe Lafoucrière 65674ac4cc Dropping support for go 1.3 2015-10-22 19:29:14 -04:00
Philippe Lafoucrière 9754378aa5 Move the registry to driver package
We can't test driver.New methods because of circular imports.
That's probably why the initial code had a map[string]interface{} as
registry: it removes a dependency import.
I prefer the remove the registry and have a registry returning a real
Driver. It will ease the development later.
2015-10-22 16:29:26 -04:00
Philippe Lafoucrière 080f49baf4 Fix error message 2015-10-18 22:30:51 -04:00
Philippe Lafoucrière 3bc76ee0bb remove leftover
[skipci]
2015-10-15 14:19:14 -04:00
Philippe Lafoucrière 46c831b8e9 Don't trust travis go 2015-10-15 14:18:23 -04:00
Philippe Lafoucrière a35e2a3c7a Don't use -cover for now, it's not working 2015-10-15 13:48:04 -04:00
Philippe Lafoucrière 8ae2c91ef4 Fix travis 2015-10-15 13:43:48 -04:00
Philippe Lafoucrière b4e84ac8f9 Use go version defined in the travis.yml matrix 2015-10-15 13:34:52 -04:00
Philippe Lafoucrière ef698d8132 go get all deps deeply 2015-10-15 12:59:31 -04:00
Philippe Lafoucrière 5cf4286653 fix yaml in travis.yml 2015-10-15 12:53:53 -04:00
Philippe Lafoucrière 2957444991 Fix test with optionated drivers 2015-10-15 12:49:43 -04:00
Philippe Lafoucrière 0c7f7b79e0 Merge branch 'master' of https://github.com/mattes/migrate into dont-load-all-drivers-43 2015-10-14 20:35:35 -04:00
Matthias Kadenbach e351ab27b9 Merge pull request #51 from raphaeljlps/master
Fixes migrate --help command
2015-10-01 10:26:19 -07:00
Raphael Jardim Lopes 432a0b56d5 fixes issue #3 2015-10-01 10:41:04 -03:00
Matthias Kadenbach 531fdf64e5 Merge pull request #50 from buddhamagnet/streamline-driver-code
Streamline driver code
2015-09-24 10:33:18 -07:00
buddhamagnet 7a0e24ebf3 add tests for driver registration 2015-09-15 23:08:29 +01:00
buddhamagnet 0f9ca3b392 exclude test sqlite3 db 2015-09-15 23:08:11 +01:00
buddhamagnet 02c5fc24d3 streamline driver code 2015-09-15 22:56:29 +01:00
Matthias Kadenbach e857fcc785 Merge pull request #48 from johnweldon/45-migration-file-id-clash
Proposed fix for migration file name clashes
2015-09-15 11:24:00 -07:00
John Weldon 07fedf8445 Add some docstring explanation to makeFiles 2015-09-14 16:41:19 -07:00
Matthias Kadenbach c848ca946e Merge pull request #49 from johnweldon/remove-go1.2-dependency
Fix travis build file to match reality
2015-09-14 10:18:21 -07:00
John Weldon 879cc40d38 Fix travis build file to match reality
This project depends on github.com/gocql/gocql, which does not support
go 1.2.

Update the .travis.yml file to match that reality.

Add go 1.4 and 1.5 to the test matrix.
2015-09-11 18:30:08 -07:00
John Weldon ca48601396 Proposed fix for #45
This is the bare minimum proposed fix for #45

By using a nested map the way I do in this PR we could also simplify the
logic further down where we look for matching direction.Up or
direction.Down migrations.
2015-09-05 17:48:33 -07:00
John Weldon c7bacb831b Test to demonstrate issue #45
Added a unit test to demonstrate the problem in issue #45

The introduced makeFiles() function could be used to simplify the other
tests too, but I didn't touch more than necessary for this commit.
2015-09-05 17:46:58 -07:00
Matthias Kadenbach 68a658aff4 Merge pull request #44 from faxal/master
Get default URL from environment variable MIGRATE_URL
2015-06-24 10:09:51 -07:00
fazal d8df2cd1cc Get default URL from environment variable MIGRATE_URL 2015-06-17 13:07:18 +05:00
Dave Jeffrey 0741616d2e Don't load in all drivers by default #40
Requires activating drivers with a _ style import, e.g.
import "_ github.com/mattes/migrate/driver/postgres"
2015-06-11 11:11:28 +01:00
Matthias Kadenbach 9bb037339f Update README.md 2015-03-08 10:58:38 +01:00
Matthias Kadenbach 7dcd8fe1d7 Merge pull request #36 from cjwirth/sqlite3
Add driver for sqlite3
2015-03-08 10:57:11 +01:00
Caesar Wirth 8d39928220 Add driver for sqlite3 2015-03-08 11:29:20 +09:00
Matthias Kadenbach cd2c561564 Merge pull request #32 from c4milo/patch-1
Adds sql-migrate as another alternative
2015-02-09 21:00:56 +01:00
Camilo Aguilar 0c9a8408c7 Adds sql-migrate as another alternative 2015-02-09 14:18:33 -05:00
mattes e72d592ef5 add dot 2015-02-04 21:49:59 +01:00
mattes bf81444464 add pateld982 to readme 2015-02-04 21:49:15 +01:00
Matthias Kadenbach cac6e08b44 Merge pull request #30 from pateld982/master
Add authentication support for cassandra
2015-02-04 21:47:02 +01:00
Matthias Kadenbach f12373eb83 Merge pull request #27 from ngauthier/proper-exit-codes
ok flag is misleading, it's really if channel is closed, so return okFlag
2015-02-04 21:40:26 +01:00
Matthias Kadenbach 3502e0252c Merge pull request #28 from ngauthier/fix-err-handling-typo
fix check on err when using err2
2015-02-04 20:40:45 +01:00
Dpatel ee6a71da4c Add login support for cassandra 2015-01-27 17:40:46 -05:00
Nick Gauthier 1333184c7b fix check on err when using err2 2015-01-15 12:44:51 -05:00
Nick Gauthier ef6572b9aa ok flag is misleading, it's really if channel is closed, so return okFlag 2015-01-15 12:44:18 -05:00
David Francoeur 820c58b7f2 Merge branch 'master' of github.com:mattes/migrate 2014-12-28 15:38:33 -05:00
David Francoeur ab520d7b6b Ensure that os signals are not relayed to the interrupt channel when the
function returns
2014-12-28 15:36:46 -05:00
mattes 2cf4b2c33e rename errorFlag to okFlag to make it even more clearer #22 2014-12-19 12:10:32 -08:00
Matthias Kadenbach bcba31af11 Merge pull request #23 from ngauthier/return-errors-from-pipes-and-exit
return ok bool instead of error bool
2014-12-19 12:06:46 -08:00
Nick Gauthier f61157f6a6 return ok bool instead of error bool. fixes #22 2014-12-19 08:19:42 -05:00
mattes 48fe666dbe Update Readme 2014-12-19 02:50:58 -08:00
mattes 05e520bd44 replace bool with errorFlag #22 2014-12-19 02:41:11 -08:00
mattes 3ce2dc92c2 fixing #22 2014-12-19 02:40:11 -08:00
Matthias Kadenbach 6b535262fd Merge pull request #21 from divoxx/mysql_version_table_issue
WIP: Prevents MySQL driver to failing when version table alredy exists
2014-12-05 13:07:42 -08:00
Rodrigo Kochenburger 94efc48c49 Don't stop when a mysql warning happens 2014-12-04 14:33:52 -08:00
Rodrigo Kochenburger 6bce23e36a Don't consider warnings as errors 2014-12-04 13:59:40 -08:00
Rodrigo Kochenburger c94461932f Prevents MySQL driver to failing when version table alredy exists
The MySQL driver is using `CREATE TABLE IF NOT EXISTS`, which causes
the MySQL database to raise warnings. The golang driver collects
the warnings and return a composite object (mysql.MySQLWarnings) as
an error that needs to be properly handled.

This change stops the driver from failing in case there are only
warnings.
2014-12-04 13:49:26 -08:00
Matthias Kadenbach 150ce9b524 Merge pull request #20 from balboah/master
Cassandra driver tweaks
2014-11-17 12:42:36 -08:00
Johnny Bergström 84ea5f6842 Update Cassandra authors
Adding myself :)
2014-11-17 12:56:51 +01:00
Johnny Bergström 5f1ca5dadc Multiple statements per migration file
Makes it possible to run several queries in each file.
2014-11-17 11:54:21 +01:00
Johnny Bergström d5f5eb65a6 Refactor version tracking
Make sure to rollback the version number in case we couldn't apply the changes.
Some deduplication of code.
2014-11-17 11:42:19 +01:00
Matthias Kadenbach 7ae5048cb3 Merge pull request #19 from balboah/master
Fix typo in printing migrations path
2014-11-05 12:12:51 -08:00
Johnny Bergström 357fbc15d9 Fix typo in printing migrations path
We want to print the value pointed to, not the pointer.
2014-11-05 12:19:22 +01:00
mattes c1f0a1a044 fix mysql 2014-10-17 11:50:39 -07:00
mattes 5b6715333f fix mysql tests 2014-10-17 11:39:01 -07:00
mattes 7b1a07fa97 mysql scheme fix 2014-10-17 11:34:43 -07:00
50 changed files with 3415 additions and 447 deletions
+2 -1
View File
@@ -1 +1,2 @@
.DS_Store
.DS_Store
test.db
+14 -13
View File
@@ -1,20 +1,21 @@
language: go
sudo: required
go:
- 1.2
- 1.3
- tip
addons:
postgresql: "9.3"
- 1.4.x
- 1.5.x
- 1.6.x
- 1.7.x
- 1.8.x
services:
- cassandra
- docker
before_script:
- >
/usr/local/cassandra/bin/cqlsh -e "CREATE KEYSPACE migratetest WITH REPLICATION = {'class': 'SimpleStrategy', 'replication_factor' : 1};"
- psql -c 'create database migratetest;' -U postgres
- mysql -e 'create database migratetest;'
before_install:
- curl -L https://github.com/docker/compose/releases/download/1.11.2/docker-compose-`uname -s`-`uname -m` > docker-compose
- chmod +x docker-compose
- sudo mv docker-compose /usr/local/bin
- sed -i -e 's/golang/golang:'"$TRAVIS_GO_VERSION"'/' docker-compose.yml
script: go test -p 1 ./...
script: make test
+3
View File
@@ -0,0 +1,3 @@
FROM scratch
ADD migrate /migrate
ENTRYPOINT ["/migrate"]
+30
View File
@@ -0,0 +1,30 @@
TESTFLAGS?=
IMAGE=mattes/migrate
DCR=docker-compose run --rm
GOTEST=go test $(TESTFLAGS) `go list ./... | grep -v "/vendor/"`
.PHONY: clean test build release docker-build docker-push run
all: release
clean:
rm -f migrate
fmt:
@gofmt -s -w `go list -f {{.Dir}} ./... | grep -v "/vendor/"`
test: fmt
$(DCR) go-test
go-test: fmt
@$(GOTEST)
build:
$(DCR) go-build
release: test build docker-build docker-push
docker-build:
docker build --rm -t $(IMAGE) .
docker-push:
docker push $(IMAGE)
+33 -19
View File
@@ -1,19 +1,23 @@
# You are looking at a deprecated version.
# Use the [latest version](https://github.com/mattes/migrate) instead.
---
# migrate
[![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)
[![Build Status](https://travis-ci.org/mattes/migrate.svg?branch=v1)](https://travis-ci.org/mattes/migrate)
A migration helper written in Go. Use it in your existing Golang code
or run commands via the CLI.
```
GoCode import github.com/mattes/migrate/migrate
CLI go get -u github.com/mattes/migrate
GoCode import gopkg.in/mattes/migrate.v1/migrate
CLI go get -u gopkg.in/mattes/migrate.v1
```
__Features__
* Super easy to implement [Driver interface](http://godoc.org/github.com/mattes/migrate/driver#Driver).
* Super easy to implement [Driver interface](http://godoc.org/gopkg.in/mattes/migrate.v1/driver#Driver).
* Gracefully quit running migrations on ``^C``.
* No magic search paths routines, no hard-coded config files.
* CLI is build on top of the ``migrate package``.
@@ -21,20 +25,24 @@ __Features__
## Available Drivers
* [PostgreSQL](https://github.com/mattes/migrate/tree/master/driver/postgres)
* [Cassandra](https://github.com/mattes/migrate/tree/master/driver/cassandra)
* SQLite ([planned](https://github.com/mattes/migrate/issues/2))
* [MySQL](https://github.com/mattes/migrate/tree/master/driver/mysql) ([experimental](https://github.com/mattes/migrate/issues/1#issuecomment-58728186))
* Bash (planned)
* [PostgreSQL](driver/postgres)
* [Cassandra](driver/cassandra)
* [SQLite](driver/sqlite3)
* [MySQL](driver/mysql) ([experimental](https://github.com/mattes/migrate/issues/1#issuecomment-58728186))
* [Neo4j](driver/neo4j)
* [Ql](driver/ql)
* [MongoDB](driver/mongodb)
* [CrateDB](driver/crate)
* [MSSQL](driver/mssql)
Need another driver? Just implement the [Driver interface](http://godoc.org/github.com/mattes/migrate/driver#Driver) and open a PR.
Need another driver? Just implement the [Driver interface](http://godoc.org/gopkg.in/mattes/migrate.v1/driver#Driver) and open a PR.
## Usage from Terminal
```bash
# install
go get github.com/mattes/migrate
go get gopkg.in/mattes/migrate.v1
# create new migration file in path
migrate -url driver://url -path ./migrations create migration_file_xyz
@@ -65,16 +73,21 @@ migrate -url driver://url -path ./migrations migrate -2
migrate -url driver://url -path ./migrations migrate -n
# go to specific migration
migrate -url driver://url -path ./migrations goto 1
migrate -url driver://url -path ./migrations goto 10
migrate -url driver://url -path ./migrations goto v
```
## Usage in Go
See GoDoc here: http://godoc.org/github.com/mattes/migrate/migrate
See GoDoc here: http://godoc.org/gopkg.in/mattes/migrate.v1/migrate
```go
import "github.com/mattes/migrate/migrate"
import "gopkg.in/mattes/migrate.v1/migrate"
// Import any required drivers so that they are registered and available
import _ "gopkg.in/mattes/migrate.v1/driver/mysql"
// use synchronous versions of migration functions ...
allErrors, ok := migrate.UpSync("driver://url", "./path")
@@ -95,15 +108,15 @@ go migrate.Up(pipe, "driver://url", "./path")
The format of migration files looks like this:
```
001_initial_plan_to_do_sth.up.sql # up migration instructions
001_initial_plan_to_do_sth.down.sql # down migration instructions
002_xxx.up.sql
002_xxx.down.sql
1481574547_initial_plan_to_do_sth.up.sql # up migration instructions
1481574547_initial_plan_to_do_sth.down.sql # down migration instructions
1482438365_xxx.up.sql
1482438365_xxx.down.sql
...
```
Why two files? This way you could still do sth like
``psql -f ./db/migrations/001_initial_plan_to_do_sth.up.sql`` and there is no
``psql -f ./db/migrations/1481574547_initial_plan_to_do_sth.up.sql`` and there is no
need for any custom markup language to divide up and down migrations. Please note
that the filename extension depends on the driver.
@@ -114,5 +127,6 @@ that the filename extension depends on the driver.
* https://github.com/tanel/dbmigrate
* https://github.com/BurntSushi/migration
* https://github.com/DavidHuie/gomigrate
* https://github.com/rubenv/sql-migrate
+38
View File
@@ -0,0 +1,38 @@
go: &go
image: golang
working_dir: /go/src/gopkg.in/mattes/migrate.v1
volumes:
- $GOPATH/src/github.com/mattes/migrate:/go/src/gopkg.in/mattes/migrate.v1
go-test:
<<: *go
command: sh -c 'go get -t -v ./... && go test -v ./...'
links:
- mssql
- postgres
- mysql
- cassandra
- crate
- mongo
go-build:
<<: *go
command: sh -c 'go get -v && go build -ldflags ''-s'' -o migrater'
environment:
CGO_ENABLED: 1
postgres:
image: postgres
mysql:
image: mysql
environment:
MYSQL_DATABASE: migratetest
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
cassandra:
image: cassandra:2.2
crate:
image: crate
mongo:
image: mongo:3.2.6
mssql:
image: microsoft/mssql-server-linux
environment:
SA_PASSWORD: Passw0rd
ACCEPT_EULA: "Y"
+1 -1
View File
@@ -8,4 +8,4 @@
migrate -url bash://xxx -path ./migrations create increment_xyz
migrate -url bash://xxx -path ./migrations up
migrate help # for more info
```
```
+6 -2
View File
@@ -2,8 +2,8 @@
package bash
import (
"github.com/mattes/migrate/file"
_ "github.com/mattes/migrate/migrate/direction"
"gopkg.in/mattes/migrate.v1/driver"
"gopkg.in/mattes/migrate.v1/file"
)
type Driver struct {
@@ -30,3 +30,7 @@ func (driver *Driver) Migrate(f file.File, pipe chan interface{}) {
func (driver *Driver) Version() (uint64, error) {
return uint64(0), nil
}
func init() {
driver.RegisterDriver("bash", &Driver{})
}
+1 -3
View File
@@ -1,8 +1,6 @@
package bash
import (
"testing"
)
import "testing"
func TestMigrate(t *testing.T) {
+8 -1
View File
@@ -8,6 +8,13 @@ migrate -url cassandra://host:port/keyspace -path ./db/migrations up
migrate help # for more info
```
Url format
- Authentication: `cassandra://username:password@host:port/keyspace`
- Cassandra v3.x: `cassandra://host:port/keyspace?protocol=4`
## Authors
* Paul Bergeron, https://github.com/dinedal
* Paul Bergeron, https://github.com/dinedal
* Johnny Bergström, https://github.com/balboah
* pateld982, http://github.com/pateld982
+136 -40
View File
@@ -2,34 +2,78 @@
package cassandra
import (
"fmt"
"net/url"
"strconv"
"strings"
"time"
"github.com/gocql/gocql"
"github.com/mattes/migrate/file"
"github.com/mattes/migrate/migrate/direction"
"gopkg.in/mattes/migrate.v1/driver"
"gopkg.in/mattes/migrate.v1/file"
"gopkg.in/mattes/migrate.v1/migrate/direction"
)
// Driver implements migrate Driver interface
type Driver struct {
session *gocql.Session
}
const tableName = "schema_migrations"
const versionRow = 1
const (
tableName = "schema_migrations"
)
// Cassandra Driver URL format:
// cassandra://host:port/keyspace
//
// Example:
// cassandra://localhost/SpaceOfKeys
// Initialize will be called first
func (driver *Driver) Initialize(rawurl string) error {
u, err := url.Parse(rawurl)
if err != nil {
return fmt.Errorf("failed to parse connectil url: %v", err)
}
if u.Path == "" {
return fmt.Errorf("no keyspace provided in connection url")
}
cluster := gocql.NewCluster(u.Host)
cluster.Keyspace = u.Path[1:len(u.Path)]
cluster.Consistency = gocql.All
cluster.Timeout = 1 * time.Minute
if len(u.Query().Get("consistency")) > 0 {
var consistency gocql.Consistency
consistency, err = parseConsistency(u.Query().Get("consistency"))
if err != nil {
return err
}
cluster.Consistency = consistency
}
if len(u.Query().Get("protocol")) > 0 {
var protoversion int
protoversion, err = strconv.Atoi(u.Query().Get("protocol"))
if err != nil {
return err
}
cluster.ProtoVersion = protoversion
}
// Check if url user struct is null
if u.User != nil {
password, passwordSet := u.User.Password()
if passwordSet == false {
return fmt.Errorf("Missing password. Please provide password")
}
cluster.Authenticator = gocql.PasswordAuthenticator{
Username: u.User.Username(),
Password: password,
}
}
driver.session, err = cluster.CreateSession()
if err != nil {
@@ -42,62 +86,114 @@ func (driver *Driver) Initialize(rawurl string) error {
return nil
}
// Close last function to be called. Closes cassandra session
func (driver *Driver) Close() error {
driver.session.Close()
return nil
}
func (driver *Driver) ensureVersionTableExists() error {
err := driver.session.Query("CREATE TABLE IF NOT EXISTS " + tableName + " (version counter, versionRow bigint primary key);").Exec()
err := driver.session.Query(fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (id uuid primary key, version bigint)", tableName)).Exec()
if err != nil {
return err
}
_, err = driver.Version()
if err != nil {
driver.session.Query("UPDATE "+tableName+" SET version = version + 1 where versionRow = ?", versionRow).Exec()
if _, err = driver.Version(); err != nil {
return err
}
return nil
}
// FilenameExtension return extension of migrations files
func (driver *Driver) FilenameExtension() string {
return "cql"
}
func (driver *Driver) updateVersion(version uint64, dir direction.Direction) error {
var ids []string
var id string
var err error
iter := driver.session.Query(fmt.Sprintf("SELECT id FROM %s WHERE version >= ? ALLOW FILTERING", tableName), version).Iter()
for iter.Scan(&id) {
ids = append(ids, id)
}
if len(ids) > 0 {
err = driver.session.Query(fmt.Sprintf("DELETE FROM %s WHERE id IN ?", tableName), ids).Exec()
if err != nil {
return err
}
}
if dir == direction.Up {
return driver.session.Query(fmt.Sprintf("INSERT INTO %s (id, version) VALUES (uuid(), ?)", tableName), version).Exec()
}
return nil
}
// Migrate run migration file. Restore previous version in case of fail
func (driver *Driver) Migrate(f file.File, pipe chan interface{}) {
defer close(pipe)
pipe <- f
if f.Direction == direction.Up {
err := driver.session.Query("UPDATE "+tableName+" SET version = version + 1 where versionRow = ?", versionRow).Exec()
if err != nil {
pipe <- err
return
}
} else if f.Direction == direction.Down {
err := driver.session.Query("UPDATE "+tableName+" SET version = version - 1 where versionRow = ?", versionRow).Exec()
if err != nil {
pipe <- err
return
}
}
if err := f.ReadContent(); err != nil {
pipe <- err
return
}
err := driver.session.Query(string(f.Content)).Exec()
var err error
previousVersion, err := driver.Version()
if err != nil {
pipe <- err
close(pipe)
return
}
defer func() {
if err != nil {
// Invert version direction if we couldn't apply the changes for some reason.
if updErr := driver.updateVersion(previousVersion, direction.Up); updErr != nil {
pipe <- updErr
}
pipe <- err
}
close(pipe)
}()
pipe <- f
if err = driver.updateVersion(f.Version, f.Direction); err != nil {
return
}
if err = f.ReadContent(); err != nil {
return
}
for _, query := range strings.Split(string(f.Content), ";") {
query = strings.TrimSpace(query)
if len(query) == 0 {
continue
}
if err = driver.session.Query(query).Exec(); err != nil {
return
}
}
}
// Version return current version
func (driver *Driver) Version() (uint64, error) {
var version int64
err := driver.session.Query("SELECT version FROM "+tableName+" WHERE versionRow = ?", versionRow).Scan(&version)
return uint64(version) - 1, err
err := driver.session.Query(fmt.Sprintf("SELECT max(version) FROM %s", tableName)).Scan(&version)
return uint64(version), err
}
func init() {
driver.RegisterDriver("cassandra", &Driver{})
}
// ParseConsistency wraps gocql.ParseConsistency to return an error
// instead of a panicing.
func parseConsistency(consistencyStr string) (consistency gocql.Consistency, err error) {
defer func() {
if r := recover(); r != nil {
var ok bool
err, ok = r.(error)
if !ok {
err = fmt.Errorf("Failed to parse consistency \"%s\": %v", consistencyStr, r)
}
}
}()
consistency = gocql.ParseConsistency(consistencyStr)
return consistency, nil
}
+55 -10
View File
@@ -2,21 +2,29 @@ package cassandra
import (
"net/url"
"os"
"testing"
"time"
"github.com/gocql/gocql"
"github.com/mattes/migrate/file"
"github.com/mattes/migrate/migrate/direction"
pipep "github.com/mattes/migrate/pipe"
"gopkg.in/mattes/migrate.v1/file"
"gopkg.in/mattes/migrate.v1/migrate/direction"
pipep "gopkg.in/mattes/migrate.v1/pipe"
)
func TestMigrate(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
var session *gocql.Session
driverUrl := "cassandra://localhost/migratetest"
host := os.Getenv("CASSANDRA_PORT_9042_TCP_ADDR")
port := os.Getenv("CASSANDRA_PORT_9042_TCP_PORT")
driverURL := "cassandra://" + host + ":" + port + "/system"
// prepare a clean test database
u, err := url.Parse(driverUrl)
u, err := url.Parse(driverURL)
if err != nil {
t.Fatal(err)
}
@@ -27,20 +35,25 @@ func TestMigrate(t *testing.T) {
cluster.Timeout = 1 * time.Minute
session, err = cluster.CreateSession()
if err != nil {
t.Fatal(err)
}
if err := session.Query(`DROP TABLE IF EXISTS yolo`).Exec(); err != nil {
if err = session.Query(`DROP KEYSPACE IF EXISTS migrate;`).Exec(); err != nil {
t.Fatal(err)
}
if err := session.Query(`DROP TABLE IF EXISTS ` + tableName).Exec(); err != nil {
if err = session.Query(`CREATE KEYSPACE IF NOT EXISTS migrate WITH REPLICATION = {'class': 'SimpleStrategy', 'replication_factor': 1};`).Exec(); err != nil {
t.Fatal(err)
}
cluster.Keyspace = "migrate"
session, err = cluster.CreateSession()
if err != nil {
t.Fatal(err)
}
driverURL = "cassandra://" + host + ":" + port + "/migrate"
d := &Driver{}
if err := d.Initialize(driverUrl); err != nil {
if err := d.Initialize(driverURL); err != nil {
t.Fatal(err)
}
@@ -53,8 +66,10 @@ func TestMigrate(t *testing.T) {
Direction: direction.Up,
Content: []byte(`
CREATE TABLE yolo (
id varint primary key
id varint primary key,
msg text
);
`),
},
{
@@ -105,5 +120,35 @@ func TestMigrate(t *testing.T) {
if err := d.Close(); err != nil {
t.Fatal(err)
}
}
func TestInitializeReturnsErrorsForBadUrls(t *testing.T) {
var session *gocql.Session
host := os.Getenv("CASSANDRA_PORT_9042_TCP_ADDR")
port := os.Getenv("CASSANDRA_PORT_9042_TCP_PORT")
cluster := gocql.NewCluster(host)
cluster.Consistency = gocql.All
cluster.Timeout = 1 * time.Minute
session, err := cluster.CreateSession()
if err != nil {
t.Fatal(err)
}
defer session.Close()
if err := session.Query(`CREATE KEYSPACE IF NOT EXISTS migrate WITH REPLICATION = {'class': 'SimpleStrategy', 'replication_factor': 1};`).Exec(); err != nil {
t.Fatal(err)
}
d := &Driver{}
invalidURL := "sdf://asdf://as?df?a"
if err := d.Initialize(invalidURL); err == nil {
t.Errorf("expected an error to be returned if url could not be parsed")
}
noKeyspace := "cassandra://" + host + ":" + port
if err := d.Initialize(noKeyspace); err == nil {
t.Errorf("expected an error to be returned if no keyspace provided")
}
}
+15
View File
@@ -0,0 +1,15 @@
# Crate driver
This is a driver for the [Crate](https://crate.io) database. It is based on the Crate
sql driver by [herenow](https://github.com/herenow/go-crate).
This driver does not use transactions! This is not a limitation of the driver, but a
limitation of Crate. So handle situations with failed migrations with care!
## Usage
```bash
migrate -url http://host:port -path ./db/migrations create add_field_to_table
migrate -url http://host:port -path ./db/migrations up
migrate help # for more info
```
+115
View File
@@ -0,0 +1,115 @@
// Package crate implements a driver for the Crate.io database
package crate
import (
"database/sql"
"fmt"
"strings"
_ "github.com/herenow/go-crate"
"gopkg.in/mattes/migrate.v1/driver"
"gopkg.in/mattes/migrate.v1/file"
"gopkg.in/mattes/migrate.v1/migrate/direction"
)
func init() {
driver.RegisterDriver("crate", &Driver{})
}
type Driver struct {
db *sql.DB
}
const tableName = "schema_migrations"
func (driver *Driver) Initialize(url string) error {
url = strings.Replace(url, "crate", "http", 1)
db, err := sql.Open("crate", url)
if err != nil {
return err
}
if err := db.Ping(); err != nil {
return err
}
driver.db = db
if err := driver.ensureVersionTableExists(); err != nil {
return err
}
return nil
}
func (driver *Driver) Close() error {
if err := driver.db.Close(); err != nil {
return err
}
return nil
}
func (driver *Driver) FilenameExtension() string {
return "sql"
}
func (driver *Driver) Version() (uint64, error) {
var version uint64
err := driver.db.QueryRow("SELECT version FROM " + tableName + " ORDER BY version DESC LIMIT 1").Scan(&version)
switch {
case err == sql.ErrNoRows:
return 0, nil
case err != nil:
return 0, err
default:
return version, nil
}
}
func (driver *Driver) Migrate(f file.File, pipe chan interface{}) {
defer close(pipe)
pipe <- f
if err := f.ReadContent(); err != nil {
pipe <- err
return
}
lines := splitContent(string(f.Content))
for _, line := range lines {
_, err := driver.db.Exec(line)
if err != nil {
pipe <- err
return
}
}
if f.Direction == direction.Up {
if _, err := driver.db.Exec("INSERT INTO "+tableName+" (version) VALUES (?)", f.Version); err != nil {
pipe <- err
return
}
} else if f.Direction == direction.Down {
if _, err := driver.db.Exec("DELETE FROM "+tableName+" WHERE version=?", f.Version); err != nil {
pipe <- err
return
}
}
}
func splitContent(content string) []string {
lines := strings.Split(content, ";")
resultLines := make([]string, 0, len(lines))
for i := range lines {
line := strings.Replace(lines[i], ";", "", -1)
line = strings.TrimSpace(line)
if line != "" {
resultLines = append(resultLines, line)
}
}
return resultLines
}
func (driver *Driver) ensureVersionTableExists() error {
if _, err := driver.db.Exec(fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (version INTEGER PRIMARY KEY)", tableName)); err != nil {
return err
}
return nil
}
+111
View File
@@ -0,0 +1,111 @@
package crate
import (
"fmt"
"os"
"testing"
"gopkg.in/mattes/migrate.v1/file"
"gopkg.in/mattes/migrate.v1/migrate/direction"
pipep "gopkg.in/mattes/migrate.v1/pipe"
)
func TestContentSplit(t *testing.T) {
content := `CREATE TABLE users (user_id STRING primary key, first_name STRING, last_name STRING, email STRING, password_hash STRING) CLUSTERED INTO 3 shards WITH (number_of_replicas = 0);
CREATE TABLE units (unit_id STRING primary key, name STRING, members array(string)) CLUSTERED INTO 3 shards WITH (number_of_replicas = 0);
CREATE TABLE available_connectors (technology_id STRING primary key, description STRING, icon STRING, link STRING, configuration_parameters array(object as (name STRING, type STRING))) CLUSTERED INTO 3 shards WITH (number_of_replicas = 0);
`
lines := splitContent(content)
if len(lines) != 3 {
t.Errorf("Expected 3 lines, but got %d", len(lines))
}
if lines[0] != "CREATE TABLE users (user_id STRING primary key, first_name STRING, last_name STRING, email STRING, password_hash STRING) CLUSTERED INTO 3 shards WITH (number_of_replicas = 0)" {
t.Error("Line does not match expected output")
}
if lines[1] != "CREATE TABLE units (unit_id STRING primary key, name STRING, members array(string)) CLUSTERED INTO 3 shards WITH (number_of_replicas = 0)" {
t.Error("Line does not match expected output")
}
if lines[2] != "CREATE TABLE available_connectors (technology_id STRING primary key, description STRING, icon STRING, link STRING, configuration_parameters array(object as (name STRING, type STRING))) CLUSTERED INTO 3 shards WITH (number_of_replicas = 0)" {
t.Error("Line does not match expected output")
}
}
func TestMigrate(t *testing.T) {
host := os.Getenv("CRATE_PORT_4200_TCP_ADDR")
port := os.Getenv("CRATE_PORT_4200_TCP_PORT")
url := fmt.Sprintf("crate://%s:%s", host, port)
driver := &Driver{}
if err := driver.Initialize(url); err != nil {
t.Fatal(err)
}
successFiles := []file.File{
{
Path: "/foobar",
FileName: "001_foobar.up.sql",
Version: 1,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
CREATE TABLE yolo (
id integer primary key,
msg string
);
`),
},
{
Path: "/foobar",
FileName: "002_foobar.down.sql",
Version: 1,
Name: "foobar",
Direction: direction.Down,
Content: []byte(`
DROP TABLE yolo;
`),
},
}
failFiles := []file.File{
{
Path: "/foobar",
FileName: "002_foobar.up.sql",
Version: 2,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
CREATE TABLE error (
id THIS WILL CAUSE AN ERROR
)
`),
},
}
for _, file := range successFiles {
pipe := pipep.New()
go driver.Migrate(file, pipe)
errs := pipep.ReadErrors(pipe)
if len(errs) > 0 {
t.Fatal(errs)
}
}
for _, file := range failFiles {
pipe := pipep.New()
go driver.Migrate(file, pipe)
errs := pipep.ReadErrors(pipe)
if len(errs) == 0 {
t.Fatal("Migration should have failed but succeeded")
}
}
if err := driver.Close(); err != nil {
t.Fatal(err)
}
}
+27 -41
View File
@@ -2,15 +2,10 @@
package driver
import (
"errors"
"fmt"
neturl "net/url" // alias to allow `url string` func signature in New
"github.com/mattes/migrate/driver/bash"
"github.com/mattes/migrate/driver/cassandra"
"github.com/mattes/migrate/driver/mysql"
"github.com/mattes/migrate/driver/postgres"
"github.com/mattes/migrate/file"
"gopkg.in/mattes/migrate.v1/file"
)
// Driver is the interface type that needs to implemented by all drivers.
@@ -46,44 +41,35 @@ func New(url string) (Driver, error) {
return nil, err
}
switch u.Scheme {
case "postgres":
d := &postgres.Driver{}
verifyFilenameExtension("postgres", d)
if err := d.Initialize(url); err != nil {
return nil, err
}
return d, nil
case "mysql":
d := &mysql.Driver{}
verifyFilenameExtension("mysql", d)
if err := d.Initialize(url); err != nil {
return nil, err
}
return d, nil
case "bash":
d := &bash.Driver{}
verifyFilenameExtension("bash", d)
if err := d.Initialize(url); err != nil {
return nil, err
}
return d, nil
case "cassandra":
d := &cassandra.Driver{}
verifyFilenameExtension("cassanda", d)
if err := d.Initialize(url); err != nil {
return nil, err
}
return d, nil
default:
return nil, errors.New(fmt.Sprintf("Driver '%s' not found.", u.Scheme))
d := GetDriver(u.Scheme)
if d == nil {
return nil, fmt.Errorf("Driver '%s' not found", u.Scheme)
}
verifyFilenameExtension(u.Scheme, d)
if err := d.Initialize(url); err != nil {
return nil, err
}
return d, nil
}
// verifyFilenameExtension panics if the drivers filename extension
// FilenameExtensionFromURL return extension for migration files. Used for create migrations
func FilenameExtensionFromURL(url string) (string, error) {
u, err := neturl.Parse(url)
if err != nil {
return "", err
}
d := GetDriver(u.Scheme)
if d == nil {
return "", fmt.Errorf("Driver '%s' not found", u.Scheme)
}
verifyFilenameExtension(u.Scheme, d)
return d.FilenameExtension(), nil
}
// verifyFilenameExtension panics if the driver's filename extension
// is not correct or empty.
func verifyFilenameExtension(driverName string, d Driver) {
f := d.FilenameExtension()
-11
View File
@@ -1,11 +0,0 @@
package driver
import (
"testing"
)
func TestNew(t *testing.T) {
if _, err := New("unknown://url"); err == nil {
t.Error("no error although driver unknown")
}
}
+104
View File
@@ -0,0 +1,104 @@
# MongoDB Driver
* Runs pre-registered Golang methods that receive a single `*mgo.Session` parameter and return `error` on failure.
* Stores migration version details in collection ``db_migrations``.
This collection will be auto-generated.
* Migrations do not run in transactions, there are no built-in transactions in MongoDB.
That means that if a migration fails, it will not be rolled back.
* There is no out-of-the-box support for command-line interface via terminal.
## Usage in Go
```go
import "gopkg.in/mattes/migrate.v1/migrate"
// Import your migration methods package so that they are registered and available for the MongoDB driver.
// There is no need to import the MongoDB driver explicitly, as it should already be imported by your migration methods package.
import _ "my_mongo_db_migrator"
// use synchronous versions of migration functions ...
allErrors, ok := migrate.UpSync("mongodb://host:port", "./path")
if !ok {
fmt.Println("Oh no ...")
// do sth with allErrors slice
}
// use the asynchronous version of migration functions ...
pipe := migrate.NewPipe()
go migrate.Up(pipe, "mongodb://host:port", "./path")
// pipe is basically just a channel
// write your own channel listener. see writePipe() in main.go as an example.
```
## Migration files format
The migration files should have an ".mgo" extension and contain a list of registered methods names.
Migration methods should satisfy the following:
* They should be exported (their name should start with a capital letter)
* Their type should be `func (*mgo.Session) error`
Recommended (but not required) naming conventions for migration methods:
* Prefix with V<version> : for example V001 for version 1.
* Suffix with "_up" or "_down" for up and down migrations correspondingly.
001_first_release.up.mgo
```
V001_some_migration_operation_up
V001_some_other_operation_up
...
```
001_first_release.down.mgo
```
V001_some_other_operation_down
V001_some_migration_operation_down
...
```
## Methods registration
For a detailed example see: [sample_mongodb_migrator.go](https://github.com/mattes/migrate/blob/v1/driver/mongodb/example/sample_mongdb_migrator.go)
```go
package my_mongo_db_migrator
import (
"gopkg.in/mattes/migrate.v1/driver/mongodb"
"gopkg.in/mattes/migrate.v1/driver/mongodb/gomethods"
"gopkg.in/mgo.v2"
)
// common boilerplate
type MyMongoDbMigrator struct {
}
func (r *MyMongoDbMigrator) DbName() string {
return "<target_db_name_for_migration>"
}
var _ mongodb.MethodsReceiver = (*MyMongoDbMigrator)(nil)
func init() {
gomethods.RegisterMethodsReceiverForDriver("mongodb", &MyMongoDbMigrator{})
}
// Here goes the application-specific migration logic
func (r *MyMongoDbMigrator) V001_some_migration_operation_up(session *mgo.Session) error {
// do something
return nil
}
func (r *MyMongoDbMigrator) V001_some_migration_operation_down(session *mgo.Session) error {
// revert some_migration_operation_up from above
return nil
}
```
## Authors
* Demitry Gershovich, https://github.com/dimag-jfrog
+311
View File
@@ -0,0 +1,311 @@
package example
import (
"testing"
"gopkg.in/mattes/migrate.v1/file"
"gopkg.in/mattes/migrate.v1/migrate/direction"
"os"
"reflect"
"time"
"gopkg.in/mattes/migrate.v1/driver"
"gopkg.in/mattes/migrate.v1/driver/mongodb"
"gopkg.in/mattes/migrate.v1/driver/mongodb/gomethods"
pipep "gopkg.in/mattes/migrate.v1/pipe"
)
type ExpectedMigrationResult struct {
Organizations []Organization
Organizations_v2 []Organization_v2
Users []User
Errors []error
}
func RunMigrationAndAssertResult(
t *testing.T,
title string,
d *mongodb.Driver,
file file.File,
expected *ExpectedMigrationResult) {
actualOrganizations := []Organization{}
actualOrganizations_v2 := []Organization_v2{}
actualUsers := []User{}
var err error
var pipe chan interface{}
var errs []error
pipe = pipep.New()
go d.Migrate(file, pipe)
errs = pipep.ReadErrors(pipe)
session := d.Session
if len(expected.Organizations) > 0 {
err = session.DB(DB_NAME).C(ORGANIZATIONS_C).Find(nil).All(&actualOrganizations)
} else {
err = session.DB(DB_NAME).C(ORGANIZATIONS_C).Find(nil).All(&actualOrganizations_v2)
}
if err != nil {
t.Fatal("Failed to query Organizations collection")
}
err = session.DB(DB_NAME).C(USERS_C).Find(nil).All(&actualUsers)
if err != nil {
t.Fatal("Failed to query Users collection")
}
if !reflect.DeepEqual(expected.Errors, errs) {
t.Fatalf("Migration '%s': FAILED\nexpected errors %v\nbut got %v", title, expected.Errors, errs)
}
if !reflect.DeepEqual(expected.Organizations, actualOrganizations) {
t.Fatalf("Migration '%s': FAILED\nexpected organizations %v\nbut got %v", title, expected.Organizations, actualOrganizations)
}
if !reflect.DeepEqual(expected.Organizations_v2, actualOrganizations_v2) {
t.Fatalf("Migration '%s': FAILED\nexpected organizations v2 %v\nbut got %v", title, expected.Organizations_v2, actualOrganizations_v2)
}
if !reflect.DeepEqual(expected.Users, actualUsers) {
t.Fatalf("Migration '%s': FAILED\nexpected users %v\nbut got %v", title, expected.Users, actualUsers)
}
// t.Logf("Migration '%s': PASSED", title)
}
func TestMigrate(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Fatalf("Test failed on panic: %v", r)
}
}()
host := os.Getenv("MONGO_PORT_27017_TCP_ADDR")
port := os.Getenv("MONGO_PORT_27017_TCP_PORT")
driverUrl := "mongodb://" + host + ":" + port
d0 := driver.GetDriver("mongodb")
d, ok := d0.(*mongodb.Driver)
if !ok {
t.Fatal("MongoDbGoMethodsDriver has not registered")
}
if err := d.Initialize(driverUrl); err != nil {
t.Fatal(err)
}
// Reset DB
d.Session.DB(DB_NAME).C(mongodb.MIGRATE_C).DropCollection()
d.Session.DB(DB_NAME).C(ORGANIZATIONS_C).DropCollection()
d.Session.DB(DB_NAME).C(USERS_C).DropCollection()
date1, _ := time.Parse(SHORT_DATE_LAYOUT, "1994-Jul-05")
date2, _ := time.Parse(SHORT_DATE_LAYOUT, "1998-Sep-04")
date3, _ := time.Parse(SHORT_DATE_LAYOUT, "2008-Apr-28")
migrations := []struct {
name string
file file.File
expectedResult ExpectedMigrationResult
}{
{
name: "v0 -> v1",
file: file.File{
Path: "/foobar",
FileName: "001_foobar.up.gm",
Version: 1,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
V001_init_organizations_up
V001_init_users_up
`),
},
expectedResult: ExpectedMigrationResult{
Organizations: []Organization{
{Id: OrganizationIds[0], Name: "Amazon", Location: "Seattle", DateFounded: date1},
{Id: OrganizationIds[1], Name: "Google", Location: "Mountain View", DateFounded: date2},
{Id: OrganizationIds[2], Name: "JFrog", Location: "Santa Clara", DateFounded: date3},
},
Organizations_v2: []Organization_v2{},
Users: []User{
{Id: UserIds[0], Name: "Alex"},
{Id: UserIds[1], Name: "Beatrice"},
{Id: UserIds[2], Name: "Cleo"},
},
Errors: []error{},
},
},
{
name: "v1 -> v2",
file: file.File{
Path: "/foobar",
FileName: "002_foobar.up.gm",
Version: 2,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
V002_organizations_rename_location_field_to_headquarters_up
V002_change_user_cleo_to_cleopatra_up
`),
},
expectedResult: ExpectedMigrationResult{
Organizations: []Organization{},
Organizations_v2: []Organization_v2{
{Id: OrganizationIds[0], Name: "Amazon", Headquarters: "Seattle", DateFounded: date1},
{Id: OrganizationIds[1], Name: "Google", Headquarters: "Mountain View", DateFounded: date2},
{Id: OrganizationIds[2], Name: "JFrog", Headquarters: "Santa Clara", DateFounded: date3},
},
Users: []User{
{Id: UserIds[0], Name: "Alex"},
{Id: UserIds[1], Name: "Beatrice"},
{Id: UserIds[2], Name: "Cleopatra"},
},
Errors: []error{},
},
},
{
name: "v2 -> v1",
file: file.File{
Path: "/foobar",
FileName: "002_foobar.down.gm",
Version: 2,
Name: "foobar",
Direction: direction.Down,
Content: []byte(`
V002_change_user_cleo_to_cleopatra_down
V002_organizations_rename_location_field_to_headquarters_down
`),
},
expectedResult: ExpectedMigrationResult{
Organizations: []Organization{
{Id: OrganizationIds[0], Name: "Amazon", Location: "Seattle", DateFounded: date1},
{Id: OrganizationIds[1], Name: "Google", Location: "Mountain View", DateFounded: date2},
{Id: OrganizationIds[2], Name: "JFrog", Location: "Santa Clara", DateFounded: date3},
},
Organizations_v2: []Organization_v2{},
Users: []User{
{Id: UserIds[0], Name: "Alex"},
{Id: UserIds[1], Name: "Beatrice"},
{Id: UserIds[2], Name: "Cleo"},
},
Errors: []error{},
},
},
{
name: "v1 -> v0",
file: file.File{
Path: "/foobar",
FileName: "001_foobar.down.gm",
Version: 1,
Name: "foobar",
Direction: direction.Down,
Content: []byte(`
V001_init_users_down
V001_init_organizations_down
`),
},
expectedResult: ExpectedMigrationResult{
Organizations: []Organization{},
Organizations_v2: []Organization_v2{},
Users: []User{},
Errors: []error{},
},
},
{
name: "v0 -> v1: missing method aborts migration",
file: file.File{
Path: "/foobar",
FileName: "001_foobar.up.gm",
Version: 1,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
V001_init_organizations_up
V001_init_users_up
v001_non_existing_method_up
`),
},
expectedResult: ExpectedMigrationResult{
Organizations: []Organization{},
Organizations_v2: []Organization_v2{},
Users: []User{},
Errors: []error{gomethods.MethodNotFoundError("v001_non_existing_method_up")},
},
},
{
name: "v0 -> v1: not exported method aborts migration",
file: file.File{
Path: "/foobar",
FileName: "001_foobar.up.gm",
Version: 1,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
V001_init_organizations_up
v001_not_exported_method_up
V001_init_users_up
`),
},
expectedResult: ExpectedMigrationResult{
Organizations: []Organization{},
Organizations_v2: []Organization_v2{},
Users: []User{},
Errors: []error{gomethods.MethodNotFoundError("v001_not_exported_method_up")},
},
},
{
name: "v0 -> v1: wrong signature method aborts migration",
file: file.File{
Path: "/foobar",
FileName: "001_foobar.up.gm",
Version: 1,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
V001_init_organizations_up
V001_method_with_wrong_signature_up
V001_init_users_up
`),
},
expectedResult: ExpectedMigrationResult{
Organizations: []Organization{},
Organizations_v2: []Organization_v2{},
Users: []User{},
Errors: []error{gomethods.WrongMethodSignatureError("V001_method_with_wrong_signature_up")},
},
},
{
name: "v1 -> v0: wrong signature method aborts migration",
file: file.File{
Path: "/foobar",
FileName: "001_foobar.down.gm",
Version: 1,
Name: "foobar",
Direction: direction.Down,
Content: []byte(`
V001_init_users_down
V001_method_with_wrong_signature_down
V001_init_organizations_down
`),
},
expectedResult: ExpectedMigrationResult{
Organizations: []Organization{},
Organizations_v2: []Organization_v2{},
Users: []User{},
Errors: []error{gomethods.WrongMethodSignatureError("V001_method_with_wrong_signature_down")},
},
},
}
for _, m := range migrations {
RunMigrationAndAssertResult(t, m.name, d, m.file, &m.expectedResult)
}
if err := d.Close(); err != nil {
t.Fatal(err)
}
}
@@ -0,0 +1,154 @@
package example
import (
"time"
"gopkg.in/mattes/migrate.v1/driver/mongodb/gomethods"
_ "gopkg.in/mattes/migrate.v1/driver/mongodb/gomethods"
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
"gopkg.in/mattes/migrate.v1/driver/mongodb"
)
type SampleMongoDbMigrator struct {
}
func (r *SampleMongoDbMigrator) DbName() string {
return DB_NAME
}
var _ mongodb.MethodsReceiver = (*SampleMongoDbMigrator)(nil)
func init() {
gomethods.RegisterMethodsReceiverForDriver("mongodb", &SampleMongoDbMigrator{})
}
// Here goes the specific mongodb golang methods migration logic
const (
DB_NAME = "test"
SHORT_DATE_LAYOUT = "2000-Jan-01"
USERS_C = "users"
ORGANIZATIONS_C = "organizations"
)
type Organization struct {
Id bson.ObjectId `bson:"_id,omitempty"`
Name string `bson:"name"`
Location string `bson:"location"`
DateFounded time.Time `bson:"date_founded"`
}
type Organization_v2 struct {
Id bson.ObjectId `bson:"_id,omitempty"`
Name string `bson:"name"`
Headquarters string `bson:"headquarters"`
DateFounded time.Time `bson:"date_founded"`
}
type User struct {
Id bson.ObjectId `bson:"_id"`
Name string `bson:"name"`
}
var OrganizationIds []bson.ObjectId = []bson.ObjectId{
bson.NewObjectId(),
bson.NewObjectId(),
bson.NewObjectId(),
}
var UserIds []bson.ObjectId = []bson.ObjectId{
bson.NewObjectId(),
bson.NewObjectId(),
bson.NewObjectId(),
}
func (r *SampleMongoDbMigrator) V001_init_organizations_up(session *mgo.Session) error {
date1, _ := time.Parse(SHORT_DATE_LAYOUT, "1994-Jul-05")
date2, _ := time.Parse(SHORT_DATE_LAYOUT, "1998-Sep-04")
date3, _ := time.Parse(SHORT_DATE_LAYOUT, "2008-Apr-28")
orgs := []Organization{
{Id: OrganizationIds[0], Name: "Amazon", Location: "Seattle", DateFounded: date1},
{Id: OrganizationIds[1], Name: "Google", Location: "Mountain View", DateFounded: date2},
{Id: OrganizationIds[2], Name: "JFrog", Location: "Santa Clara", DateFounded: date3},
}
for _, org := range orgs {
err := session.DB(DB_NAME).C(ORGANIZATIONS_C).Insert(org)
if err != nil {
return err
}
}
return nil
}
func (r *SampleMongoDbMigrator) V001_init_organizations_down(session *mgo.Session) error {
return session.DB(DB_NAME).C(ORGANIZATIONS_C).DropCollection()
}
func (r *SampleMongoDbMigrator) V001_init_users_up(session *mgo.Session) error {
users := []User{
{Id: UserIds[0], Name: "Alex"},
{Id: UserIds[1], Name: "Beatrice"},
{Id: UserIds[2], Name: "Cleo"},
}
for _, user := range users {
err := session.DB(DB_NAME).C(USERS_C).Insert(user)
if err != nil {
return err
}
}
return nil
}
func (r *SampleMongoDbMigrator) V001_init_users_down(session *mgo.Session) error {
return session.DB(DB_NAME).C(USERS_C).DropCollection()
}
func (r *SampleMongoDbMigrator) V002_organizations_rename_location_field_to_headquarters_up(session *mgo.Session) error {
c := session.DB(DB_NAME).C(ORGANIZATIONS_C)
_, err := c.UpdateAll(nil, bson.M{"$rename": bson.M{"location": "headquarters"}})
return err
}
func (r *SampleMongoDbMigrator) V002_organizations_rename_location_field_to_headquarters_down(session *mgo.Session) error {
c := session.DB(DB_NAME).C(ORGANIZATIONS_C)
_, err := c.UpdateAll(nil, bson.M{"$rename": bson.M{"headquarters": "location"}})
return err
}
func (r *SampleMongoDbMigrator) V002_change_user_cleo_to_cleopatra_up(session *mgo.Session) error {
c := session.DB(DB_NAME).C(USERS_C)
colQuerier := bson.M{"name": "Cleo"}
change := bson.M{"$set": bson.M{"name": "Cleopatra"}}
return c.Update(colQuerier, change)
}
func (r *SampleMongoDbMigrator) V002_change_user_cleo_to_cleopatra_down(session *mgo.Session) error {
c := session.DB(DB_NAME).C(USERS_C)
colQuerier := bson.M{"name": "Cleopatra"}
change := bson.M{"$set": bson.M{"name": "Cleo"}}
return c.Update(colQuerier, change)
}
// Wrong signature methods for testing
func (r *SampleMongoDbMigrator) v001_not_exported_method_up(session *mgo.Session) error {
return nil
}
func (r *SampleMongoDbMigrator) V001_method_with_wrong_signature_up(s string) error {
return nil
}
func (r *SampleMongoDbMigrator) V001_method_with_wrong_signature_down(session *mgo.Session) (bool, error) {
return true, nil
}
@@ -0,0 +1,150 @@
package gomethods
import (
"bufio"
"fmt"
"os"
"path"
"strings"
"gopkg.in/mattes/migrate.v1/driver"
"gopkg.in/mattes/migrate.v1/file"
)
type MethodNotFoundError string
func (e MethodNotFoundError) Error() string {
return fmt.Sprintf("Method '%s' was not found. It is either not existing or has not been exported (starts with lowercase).", string(e))
}
type WrongMethodSignatureError string
func (e WrongMethodSignatureError) Error() string {
return fmt.Sprintf("Method '%s' has wrong signature", string(e))
}
type MethodInvocationFailedError struct {
MethodName string
Err error
}
func (e *MethodInvocationFailedError) Error() string {
return fmt.Sprintf("Method '%s' returned an error: %v", e.MethodName, e.Err)
}
type MigrationMethodInvoker interface {
Validate(methodName string) error
Invoke(methodName string) error
}
type GoMethodsDriver interface {
driver.Driver
MigrationMethodInvoker
MethodsReceiver() interface{}
SetMethodsReceiver(r interface{}) error
}
type Migrator struct {
RollbackOnFailure bool
MethodInvoker MigrationMethodInvoker
}
func (m *Migrator) Migrate(f file.File, pipe chan interface{}) error {
methods, err := m.getMigrationMethods(f)
if err != nil {
pipe <- err
return err
}
for i, methodName := range methods {
pipe <- methodName
err := m.MethodInvoker.Invoke(methodName)
if err != nil {
pipe <- err
if !m.RollbackOnFailure {
return err
}
// on failure, try to rollback methods in this migration
for j := i - 1; j >= 0; j-- {
rollbackToMethodName := getRollbackToMethod(methods[j])
if rollbackToMethodName == "" {
continue
}
if err := m.MethodInvoker.Validate(rollbackToMethodName); err != nil {
continue
}
pipe <- rollbackToMethodName
err = m.MethodInvoker.Invoke(rollbackToMethodName)
if err != nil {
pipe <- err
break
}
}
return err
}
}
return nil
}
func getRollbackToMethod(methodName string) string {
if strings.HasSuffix(methodName, "_up") {
return strings.TrimSuffix(methodName, "_up") + "_down"
} else if strings.HasSuffix(methodName, "_down") {
return strings.TrimSuffix(methodName, "_down") + "_up"
} else {
return ""
}
}
func getFileLines(file file.File) ([]string, error) {
if len(file.Content) == 0 {
lines := make([]string, 0)
file, err := os.Open(path.Join(file.Path, file.FileName))
if err != nil {
return nil, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
return lines, nil
} else {
s := string(file.Content)
return strings.Split(s, "\n"), nil
}
}
func (m *Migrator) getMigrationMethods(f file.File) (methods []string, err error) {
var lines []string
lines, err = getFileLines(f)
if err != nil {
return nil, err
}
for _, line := range lines {
line := strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "--") {
// an empty line or a comment, ignore
continue
}
methodName := line
if err := m.MethodInvoker.Validate(methodName); err != nil {
return nil, err
}
methods = append(methods, methodName)
}
return methods, nil
}
@@ -0,0 +1,247 @@
package gomethods
import (
"reflect"
"testing"
"gopkg.in/mattes/migrate.v1/file"
"gopkg.in/mattes/migrate.v1/migrate/direction"
pipep "gopkg.in/mattes/migrate.v1/pipe"
)
type FakeGoMethodsInvoker struct {
InvokedMethods []string
}
func (invoker *FakeGoMethodsInvoker) Validate(methodName string) error {
if methodName == "V001_some_non_existing_method_up" {
return MethodNotFoundError(methodName)
}
return nil
}
func (invoker *FakeGoMethodsInvoker) Invoke(methodName string) error {
invoker.InvokedMethods = append(invoker.InvokedMethods, methodName)
if methodName == "V001_some_failing_method_up" || methodName == "V001_some_failing_method_down" {
return &MethodInvocationFailedError{
MethodName: methodName,
Err: SomeError{},
}
}
return nil
}
type SomeError struct{}
func (e SomeError) Error() string { return "Some error happened" }
func TestMigrate(t *testing.T) {
cases := []struct {
name string
file file.File
expectedInvokedMethods []string
expectedErrors []error
expectRollback bool
}{
{
name: "up migration invokes up methods",
file: file.File{
Path: "/foobar",
FileName: "001_foobar.up.gm",
Version: 1,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
V001_init_organizations_up
V001_init_users_up
`),
},
expectedInvokedMethods: []string{"V001_init_organizations_up", "V001_init_users_up"},
expectedErrors: []error{},
},
{
name: "down migration invoked down methods",
file: file.File{
Path: "/foobar",
FileName: "001_foobar.down.gm",
Version: 1,
Name: "foobar",
Direction: direction.Down,
Content: []byte(`
V001_init_users_down
V001_init_organizations_down
`),
},
expectedInvokedMethods: []string{"V001_init_users_down", "V001_init_organizations_down"},
expectedErrors: []error{},
},
{
name: "up migration: non-existing method causes migration not to execute",
file: file.File{
Path: "/foobar",
FileName: "001_foobar.up.gm",
Version: 1,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
V001_init_organizations_up
V001_init_users_up
V001_some_non_existing_method_up
`),
},
expectedInvokedMethods: []string{},
expectedErrors: []error{MethodNotFoundError("V001_some_non_existing_method_up")},
},
{
name: "up migration: failing method stops execution",
file: file.File{
Path: "/foobar",
FileName: "001_foobar.up.gm",
Version: 1,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
V001_init_organizations_up
V001_some_failing_method_up
V001_init_users_up
`),
},
expectedInvokedMethods: []string{
"V001_init_organizations_up",
"V001_some_failing_method_up",
},
expectedErrors: []error{&MethodInvocationFailedError{
MethodName: "V001_some_failing_method_up",
Err: SomeError{},
}},
},
{
name: "down migration: failing method stops migration",
file: file.File{
Path: "/foobar",
FileName: "001_foobar.down.gm",
Version: 1,
Name: "foobar",
Direction: direction.Down,
Content: []byte(`
V001_init_users_down
V001_some_failing_method_down
V001_init_organizations_down
`),
},
expectedInvokedMethods: []string{
"V001_init_users_down",
"V001_some_failing_method_down",
},
expectedErrors: []error{&MethodInvocationFailedError{
MethodName: "V001_some_failing_method_down",
Err: SomeError{},
}},
},
{
name: "up migration: failing method causes rollback in rollback mode",
expectRollback: true,
file: file.File{
Path: "/foobar",
FileName: "001_foobar.up.gm",
Version: 1,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
V001_init_organizations_up
V001_init_users_up
V001_some_failing_method_up
`),
},
expectedInvokedMethods: []string{
"V001_init_organizations_up",
"V001_init_users_up",
"V001_some_failing_method_up",
"V001_init_users_down",
"V001_init_organizations_down",
},
expectedErrors: []error{&MethodInvocationFailedError{
MethodName: "V001_some_failing_method_up",
Err: SomeError{},
}},
},
{
name: "down migration: failing method causes rollback in rollback mode",
expectRollback: true,
file: file.File{
Path: "/foobar",
FileName: "001_foobar.down.gm",
Version: 1,
Name: "foobar",
Direction: direction.Down,
Content: []byte(`
V001_init_users_down
V001_some_failing_method_down
V001_init_organizations_down
`),
},
expectedInvokedMethods: []string{
"V001_init_users_down",
"V001_some_failing_method_down",
"V001_init_users_up",
},
expectedErrors: []error{&MethodInvocationFailedError{
MethodName: "V001_some_failing_method_down",
Err: SomeError{},
}},
},
}
for _, c := range cases {
migrator := Migrator{}
fakeInvoker := &FakeGoMethodsInvoker{InvokedMethods: []string{}}
migrator.MethodInvoker = fakeInvoker
migrator.RollbackOnFailure = c.expectRollback
pipe := pipep.New()
go func() {
migrator.Migrate(c.file, pipe)
close(pipe)
}()
errs := pipep.ReadErrors(pipe)
var failed bool
if !reflect.DeepEqual(fakeInvoker.InvokedMethods, c.expectedInvokedMethods) {
failed = true
t.Errorf("case '%s': FAILED\nexpected invoked methods %v\nbut got %v", c.name, c.expectedInvokedMethods, fakeInvoker.InvokedMethods)
}
if !reflect.DeepEqual(errs, c.expectedErrors) {
failed = true
t.Errorf("case '%s': FAILED\nexpected errors %v\nbut got %v", c.name, c.expectedErrors, errs)
}
if !failed {
//t.Logf("case '%s': PASSED", c.name)
}
}
}
func TestGetRollbackToMethod(t *testing.T) {
cases := []struct {
method string
expectedRollbackMethod string
}{
{"some_method_up", "some_method_down"},
{"some_method_down", "some_method_up"},
{"up_down_up", "up_down_down"},
{"down_up", "down_down"},
{"down_down", "down_up"},
{"some_method", ""},
}
for _, c := range cases {
actualRollbackMethod := getRollbackToMethod(c.method)
if actualRollbackMethod != c.expectedRollbackMethod {
t.Errorf("Expected rollback method to be %s but got %s", c.expectedRollbackMethod, actualRollbackMethod)
}
}
}
@@ -0,0 +1,40 @@
package gomethods
import (
"fmt"
"sync"
"gopkg.in/mattes/migrate.v1/driver"
)
var methodsReceiversMu sync.Mutex
// Registers a methods receiver for go methods driver
// Users of gomethods migration drivers should call this method
// to register objects with their migration methods before executing the migration
func RegisterMethodsReceiverForDriver(driverName string, receiver interface{}) {
methodsReceiversMu.Lock()
defer methodsReceiversMu.Unlock()
if receiver == nil {
panic("Go methods: Register receiver object is nil")
}
driver := driver.GetDriver(driverName)
if driver == nil {
panic("Go methods: Trying to register receiver for not registered driver " + driverName)
}
methodsDriver, ok := driver.(GoMethodsDriver)
if !ok {
panic("Go methods: Trying to register receiver for non go methods driver " + driverName)
}
if methodsDriver.MethodsReceiver() != nil {
panic("Go methods: Methods receiver already registered for driver " + driverName)
}
if err := methodsDriver.SetMethodsReceiver(receiver); err != nil {
panic(fmt.Sprintf("Go methods: Failed to set methods receiver for driver %s\nError: %v",
driverName, err))
}
}
+194
View File
@@ -0,0 +1,194 @@
package mongodb
import (
"errors"
"reflect"
"strings"
"gopkg.in/mattes/migrate.v1/driver"
"gopkg.in/mattes/migrate.v1/driver/mongodb/gomethods"
"gopkg.in/mattes/migrate.v1/file"
"gopkg.in/mattes/migrate.v1/migrate/direction"
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
)
type UnregisteredMethodsReceiverError string
func (e UnregisteredMethodsReceiverError) Error() string {
return "Unregistered methods receiver for driver: " + string(e)
}
type WrongMethodsReceiverTypeError string
func (e WrongMethodsReceiverTypeError) Error() string {
return "Wrong methods receiver type for driver: " + string(e)
}
const MIGRATE_C = "db_migrations"
const DRIVER_NAME = "gomethods.mongodb"
type Driver struct {
Session *mgo.Session
methodsReceiver MethodsReceiver
migrator gomethods.Migrator
}
var _ gomethods.GoMethodsDriver = (*Driver)(nil)
type MethodsReceiver interface {
DbName() string
}
func (d *Driver) MethodsReceiver() interface{} {
return d.methodsReceiver
}
func (d *Driver) SetMethodsReceiver(r interface{}) error {
r1, ok := r.(MethodsReceiver)
if !ok {
return WrongMethodsReceiverTypeError(DRIVER_NAME)
}
d.methodsReceiver = r1
return nil
}
func init() {
driver.RegisterDriver("mongodb", &Driver{})
}
type DbMigration struct {
Id bson.ObjectId `bson:"_id"`
Version uint64 `bson:"version"`
}
func (driver *Driver) Initialize(url string) error {
if driver.methodsReceiver == nil {
return UnregisteredMethodsReceiverError(DRIVER_NAME)
}
urlWithoutScheme := strings.SplitN(url, "mongodb://", 2)
if len(urlWithoutScheme) != 2 {
return errors.New("invalid mongodb:// scheme")
}
session, err := mgo.Dial(url)
if err != nil {
return err
}
session.SetMode(mgo.Monotonic, true)
c := session.DB(driver.methodsReceiver.DbName()).C(MIGRATE_C)
err = c.EnsureIndex(mgo.Index{
Key: []string{"version"},
Unique: true,
})
if err != nil {
return err
}
driver.Session = session
driver.migrator = gomethods.Migrator{MethodInvoker: driver}
return nil
}
func (driver *Driver) Close() error {
if driver.Session != nil {
driver.Session.Close()
}
return nil
}
func (driver *Driver) FilenameExtension() string {
return "mgo"
}
func (driver *Driver) Version() (uint64, error) {
var latestMigration DbMigration
c := driver.Session.DB(driver.methodsReceiver.DbName()).C(MIGRATE_C)
err := c.Find(bson.M{}).Sort("-version").One(&latestMigration)
switch {
case err == mgo.ErrNotFound:
return 0, nil
case err != nil:
return 0, err
default:
return latestMigration.Version, nil
}
}
func (driver *Driver) Migrate(f file.File, pipe chan interface{}) {
defer close(pipe)
pipe <- f
err := driver.migrator.Migrate(f, pipe)
if err != nil {
return
}
migrate_c := driver.Session.DB(driver.methodsReceiver.DbName()).C(MIGRATE_C)
if f.Direction == direction.Up {
id := bson.NewObjectId()
dbMigration := DbMigration{Id: id, Version: f.Version}
err := migrate_c.Insert(dbMigration)
if err != nil {
pipe <- err
return
}
} else if f.Direction == direction.Down {
err := migrate_c.Remove(bson.M{"version": f.Version})
if err != nil {
pipe <- err
return
}
}
}
func (driver *Driver) Validate(methodName string) error {
methodWithReceiver, ok := reflect.TypeOf(driver.methodsReceiver).MethodByName(methodName)
if !ok {
return gomethods.MethodNotFoundError(methodName)
}
if methodWithReceiver.PkgPath != "" {
return gomethods.MethodNotFoundError(methodName)
}
methodFunc := reflect.ValueOf(driver.methodsReceiver).MethodByName(methodName)
methodTemplate := func(*mgo.Session) error { return nil }
if methodFunc.Type() != reflect.TypeOf(methodTemplate) {
return gomethods.WrongMethodSignatureError(methodName)
}
return nil
}
func (driver *Driver) Invoke(methodName string) error {
name := methodName
migrateMethod := reflect.ValueOf(driver.methodsReceiver).MethodByName(name)
if !migrateMethod.IsValid() {
return gomethods.MethodNotFoundError(methodName)
}
retValues := migrateMethod.Call([]reflect.Value{reflect.ValueOf(driver.Session)})
if len(retValues) != 1 {
return gomethods.WrongMethodSignatureError(name)
}
if !retValues[0].IsNil() {
err, ok := retValues[0].Interface().(error)
if !ok {
return gomethods.WrongMethodSignatureError(name)
}
return &gomethods.MethodInvocationFailedError{MethodName: name, Err: err}
}
return nil
}
+22
View File
@@ -0,0 +1,22 @@
# MSSQL Driver
* Runs migrations in transactions.
That means that if a migration fails, it will be safely rolled back.
* Tries to return helpful error messages.
* Stores migration version details in table ``schema_migrations``.
This table will be auto-generated.
## Usage
```bash
migrate -url="sqlserver://sa:Passw0rd@localhost:1433?database=master" -path ./db/migrations create add_field_to_table
migrate -url="sqlserver://sa:Passw0rd@localhost:1433?database=master" -path ./db/migrations up
migrate help # for more info
# see more docs: https://github.com/denisenkom/go-mssqldb
```
## Authors
* Andras Laczi, https://github.com/alaczi
+128
View File
@@ -0,0 +1,128 @@
// Package mssql implements the Driver interface.
package mssql
import (
"database/sql"
"errors"
"fmt"
"github.com/denisenkom/go-mssqldb"
"gopkg.in/mattes/migrate.v1/driver"
"gopkg.in/mattes/migrate.v1/file"
"gopkg.in/mattes/migrate.v1/migrate/direction"
)
type Driver struct {
db *sql.DB
}
const tableName = "schema_migrations"
func (driver *Driver) Initialize(url string) error {
db, err := sql.Open("mssql", url)
if err != nil {
return err
}
if err := db.Ping(); err != nil {
return err
}
driver.db = db
if err := driver.ensureVersionTableExists(); err != nil {
return err
}
return nil
}
func (driver *Driver) Close() error {
if err := driver.db.Close(); err != nil {
return err
}
return nil
}
func (driver *Driver) ensureVersionTableExists() error {
if _, err := driver.db.Exec("if object_id('" + tableName + "') is null create table [" + tableName + "] ([version] BIGINT NOT NULL PRIMARY KEY);"); err != nil {
return err
}
return nil
}
func (driver *Driver) FilenameExtension() string {
return "sql"
}
func (driver *Driver) Migrate(f file.File, pipe chan interface{}) {
defer close(pipe)
pipe <- f
tx, err := driver.db.Begin()
if err != nil {
pipe <- err
return
}
if f.Direction == direction.Up {
if _, err := tx.Exec("INSERT INTO "+tableName+" (version) VALUES (?)", f.Version); err != nil {
pipe <- err
if err := tx.Rollback(); err != nil {
pipe <- err
}
return
}
} else if f.Direction == direction.Down {
if _, err := tx.Exec("DELETE FROM "+tableName+" WHERE version=?", f.Version); err != nil {
pipe <- err
if err := tx.Rollback(); err != nil {
pipe <- err
}
return
}
}
if err := f.ReadContent(); err != nil {
pipe <- err
return
}
if _, err := tx.Exec(string(f.Content)); err != nil {
switch msErr := err.(type) {
case *mssql.Error:
errorPart := file.LinesBeforeAndAfter(f.Content, int(msErr.LineNo), 5, 5, true)
pipe <- errors.New(fmt.Sprintf("%v: %s in line %v:\n\n%s", msErr.Number, msErr.Message, msErr.LineNo, string(errorPart)))
if err := tx.Rollback(); err != nil {
pipe <- err
}
return
default:
pipe <- err
if err := tx.Rollback(); err != nil {
pipe <- err
}
return
}
}
if err := tx.Commit(); err != nil {
pipe <- err
return
}
}
func (driver *Driver) Version() (uint64, error) {
var version uint64
err := driver.db.QueryRow("SELECT version FROM " + tableName + " ORDER BY version DESC OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY").Scan(&version)
switch {
case err == sql.ErrNoRows:
return 0, nil
case err != nil:
return 0, err
default:
return version, nil
}
}
func init() {
driver.RegisterDriver("mssql", &Driver{})
driver.RegisterDriver("sqlserver", &Driver{})
}
+168
View File
@@ -0,0 +1,168 @@
package mssql
import (
"database/sql"
"fmt"
"os"
"testing"
"time"
"gopkg.in/mattes/migrate.v1/file"
"gopkg.in/mattes/migrate.v1/migrate/direction"
pipep "gopkg.in/mattes/migrate.v1/pipe"
)
// TestMigrate runs some additional tests on Migrate().
// Basic testing is already done in migrate/migrate_test.go
func TestMigrate(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
host := os.Getenv("MSSQL_PORT_1433_TCP_ADDR")
port := os.Getenv("MSSQL_PORT_1433_TCP_PORT")
driverURL := fmt.Sprintf("server=%s;user id=%s;password=%s;port=%s;database=%s;encrypt=disable;log=2;TrustServerCertificate=true",
host,
"sa",
"Passw0rd",
port,
"master",
)
// retry connection for 2 minutes
until := time.Now().Add(time.Second * 120)
ticker := time.NewTicker(time.Second)
var connection *sql.DB
var err error
// prepare clean database
for tick := range ticker.C {
if tick.After(until) {
ticker.Stop()
break
}
connection, err = sql.Open("mssql", driverURL)
err = connection.Ping()
if err == nil {
ticker.Stop()
break
}
}
if err != nil {
t.Fatal("Failed to connect to mssql docker container after 2 minutes", err)
}
if _, err := connection.Exec(`
DROP TABLE IF EXISTS yolo;
DROP TABLE IF EXISTS ` + tableName + `;`); err != nil {
t.Fatal(err)
}
d := &Driver{}
if err := d.Initialize(driverURL); err != nil {
t.Fatal(err)
}
// testing idempotency: second call should be a no-op, since table already exists
if err := d.Initialize(driverURL); err != nil {
t.Fatal(err)
}
files := []file.File{
{
Path: "/foobar",
FileName: "001_foobar.up.sql",
Version: 1,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
CREATE TABLE yolo (
id BIGINT IDENTITY NOT NULL PRIMARY KEY
);
`),
},
{
Path: "/foobar",
FileName: "002_foobar.down.sql",
Version: 1,
Name: "foobar",
Direction: direction.Down,
Content: []byte(`
DROP TABLE yolo;
`),
},
{
Path: "/foobar",
FileName: "002_foobar.up.sql",
Version: 2,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
CREATE TABLE error (
id THIS WILL CAUSE AN ERROR
)
`),
},
{
Path: "/foobar",
FileName: "20170118205923_demo.up.sql",
Version: 20170118205923,
Name: "demo",
Direction: direction.Up,
Content: []byte(`
CREATE TABLE demo (
id BIGINT IDENTITY NOT NULL PRIMARY KEY
)
`),
},
{
Path: "/foobar",
FileName: "20170118205923_demo.down.sql",
Version: 20170118205923,
Name: "demo",
Direction: direction.Down,
Content: []byte(`
DROP TABLE demo
`),
},
}
pipe := pipep.New()
go d.Migrate(files[0], pipe)
errs := pipep.ReadErrors(pipe)
if len(errs) > 0 {
t.Fatal(errs)
}
pipe = pipep.New()
go d.Migrate(files[1], pipe)
errs = pipep.ReadErrors(pipe)
if len(errs) > 0 {
t.Fatal(errs)
}
pipe = pipep.New()
go d.Migrate(files[2], pipe)
errs = pipep.ReadErrors(pipe)
if len(errs) == 0 {
t.Error("Expected test case to fail")
}
pipe = pipep.New()
go d.Migrate(files[3], pipe)
errs = pipep.ReadErrors(pipe)
if len(errs) > 0 {
t.Fatal(errs)
}
pipe = pipep.New()
go d.Migrate(files[4], pipe)
errs = pipep.ReadErrors(pipe)
if len(errs) > 0 {
t.Fatal(errs)
}
if err := d.Close(); err != nil {
t.Fatal(err)
}
}
+1 -1
View File
@@ -21,4 +21,4 @@ See full [DSN (Data Source Name) documentation](https://github.com/go-sql-driver
## Authors
* Matthias Kadenbach, https://github.com/mattes
* Matthias Kadenbach, https://github.com/mattes
+65 -47
View File
@@ -7,12 +7,14 @@ import (
"database/sql"
"errors"
"fmt"
"github.com/go-sql-driver/mysql"
"github.com/mattes/migrate/file"
"github.com/mattes/migrate/migrate/direction"
"regexp"
"strconv"
"strings"
"github.com/go-sql-driver/mysql"
"gopkg.in/mattes/migrate.v1/driver"
"gopkg.in/mattes/migrate.v1/file"
"gopkg.in/mattes/migrate.v1/migrate/direction"
)
type Driver struct {
@@ -22,7 +24,12 @@ type Driver struct {
const tableName = "schema_migrations"
func (driver *Driver) Initialize(url string) error {
db, err := sql.Open("mysql", strings.TrimLeft(url, "mysql://"))
urlWithoutScheme := strings.SplitN(url, "mysql://", 2)
if len(urlWithoutScheme) != 2 {
return errors.New("invalid mysql:// scheme")
}
db, err := sql.Open("mysql", urlWithoutScheme[1])
if err != nil {
return err
}
@@ -45,9 +52,12 @@ func (driver *Driver) Close() error {
}
func (driver *Driver) ensureVersionTableExists() error {
if _, err := driver.db.Exec("CREATE TABLE IF NOT EXISTS " + tableName + " (version int not null primary key);"); err != nil {
_, err := driver.db.Exec("CREATE TABLE IF NOT EXISTS " + tableName + " (version int not null primary key);")
if _, isWarn := err.(mysql.MySQLWarnings); err != nil && !isWarn {
return err
}
return nil
}
@@ -95,54 +105,58 @@ func (driver *Driver) Migrate(f file.File, pipe chan interface{}) {
sqlStmts := bytes.Split(f.Content, []byte(";"))
for _, sqlStmt := range sqlStmts {
if len(bytes.TrimSpace(sqlStmt)) > 0 {
sqlStmt = bytes.TrimSpace(sqlStmt)
if len(sqlStmt) > 0 {
if _, err := tx.Exec(string(sqlStmt)); err != nil {
mysqlErr := err.(*mysql.MySQLError)
mysqlErr, isErr := err.(*mysql.MySQLError)
if isErr {
re, err := regexp.Compile(`at line ([0-9]+)$`)
if err != nil {
pipe <- err
if err := tx.Rollback(); err != nil {
pipe <- err
}
}
var lineNo int
lineNoRe := re.FindStringSubmatch(mysqlErr.Message)
if len(lineNoRe) == 2 {
lineNo, err = strconv.Atoi(lineNoRe[1])
}
if err == nil {
// get white-space offset
// TODO this is broken, because we use sqlStmt instead of f.Content
wsLineOffset := 0
b := bufio.NewReader(bytes.NewBuffer(sqlStmt))
for {
line, _, err := b.ReadLine()
if err != nil {
break
}
if bytes.TrimSpace(line) == nil {
wsLineOffset += 1
} else {
break
}
}
message := mysqlErr.Error()
message = re.ReplaceAllString(message, fmt.Sprintf("at line %v", lineNo+wsLineOffset))
errorPart := file.LinesBeforeAndAfter(sqlStmt, lineNo, 5, 5, true)
pipe <- errors.New(fmt.Sprintf("%s\n\n%s", message, string(errorPart)))
} else {
pipe <- errors.New(mysqlErr.Error())
}
re, err := regexp.Compile(`at line ([0-9]+)$`)
if err != nil {
pipe <- err
if err := tx.Rollback(); err != nil {
pipe <- err
}
return
}
var lineNo int
lineNoRe := re.FindStringSubmatch(mysqlErr.Message)
if len(lineNoRe) == 2 {
lineNo, err = strconv.Atoi(lineNoRe[1])
}
if err == nil {
// get white-space offset
// TODO this is broken, because we use sqlStmt instead of f.Content
wsLineOffset := 0
b := bufio.NewReader(bytes.NewBuffer(sqlStmt))
for {
line, _, err := b.ReadLine()
if err != nil {
break
}
if bytes.TrimSpace(line) == nil {
wsLineOffset += 1
} else {
break
}
}
message := mysqlErr.Error()
message = re.ReplaceAllString(message, fmt.Sprintf("at line %v", lineNo+wsLineOffset))
errorPart := file.LinesBeforeAndAfter(sqlStmt, lineNo, 5, 5, true)
pipe <- errors.New(fmt.Sprintf("%s\n\n%s", message, string(errorPart)))
} else {
pipe <- errors.New(mysqlErr.Error())
}
if err := tx.Rollback(); err != nil {
pipe <- err
}
return
}
}
}
@@ -165,3 +179,7 @@ func (driver *Driver) Version() (uint64, error) {
return version, nil
}
}
func init() {
driver.RegisterDriver("mysql", &Driver{})
}
+13 -5
View File
@@ -2,19 +2,27 @@ package mysql
import (
"database/sql"
"github.com/mattes/migrate/file"
"github.com/mattes/migrate/migrate/direction"
pipep "github.com/mattes/migrate/pipe"
"os"
"strings"
"testing"
"gopkg.in/mattes/migrate.v1/file"
"gopkg.in/mattes/migrate.v1/migrate/direction"
pipep "gopkg.in/mattes/migrate.v1/pipe"
)
// TestMigrate runs some additional tests on Migrate().
// Basic testing is already done in migrate/migrate_test.go
func TestMigrate(t *testing.T) {
driverUrl := "root@tcp(127.0.0.1:3306)/migratetest"
if testing.Short() {
t.Skip("skipping test in short mode.")
}
host := os.Getenv("MYSQL_PORT_3306_TCP_ADDR")
port := os.Getenv("MYSQL_PORT_3306_TCP_PORT")
driverUrl := "mysql://root@tcp(" + host + ":" + port + ")/migratetest"
// prepare clean database
connection, err := sql.Open("mysql", driverUrl)
connection, err := sql.Open("mysql", strings.SplitN(driverUrl, "mysql://", 2)[1])
if err != nil {
t.Fatal(err)
}
+20
View File
@@ -0,0 +1,20 @@
# Neo4j Driver
* Runs migrations in transcations.
That means that if a migration failes, it will be safely rolled back.
* Stores migration version details with the label ``SchemaMigrations``.
An unique constraint for the field :SchemaMigrations(version) will be auto-generated.
* Neo4j cannot perform schema and data updates in a transaction, therefore it's necessary to use different migration files
## Usage
```bash
migrate -url neo4j://user:password@host:port/db/data -path ./db/migrations create add_field_to_table
migrate -url neo4j://user:password@host:port/db/data -path ./db/migrations up
migrate help # for more info
```
## Author
* Carlos Forero, https://github.com/carlosforero
+167
View File
@@ -0,0 +1,167 @@
// Package neo4j implements the Driver interface.
package neo4j
import (
"bytes"
"errors"
"fmt"
"github.com/jmcvetta/neoism"
"gopkg.in/mattes/migrate.v1/driver"
"gopkg.in/mattes/migrate.v1/file"
"gopkg.in/mattes/migrate.v1/migrate/direction"
"strings"
)
type Driver struct {
db *neoism.Database
}
const labelName = "SchemaMigration"
func (driver *Driver) Initialize(url string) error {
url = strings.Replace(url, "neo4j", "http", 1)
db, err := neoism.Connect(url)
if err != nil {
return err
}
driver.db = db
if err := driver.ensureVersionConstraintExists(); err != nil {
return err
}
return nil
}
func (driver *Driver) Close() error {
driver.db = nil
return nil
}
func (driver *Driver) FilenameExtension() string {
return "cql"
}
func (driver *Driver) ensureVersionConstraintExists() error {
uc, _ := driver.db.UniqueConstraints("SchemaMigration", "version")
if len(uc) == 0 {
_, err := driver.db.CreateUniqueConstraint("SchemaMigration", "version")
return err
}
return nil
}
func (driver *Driver) setVersion(d direction.Direction, v uint64, invert bool) error {
cqUp := neoism.CypherQuery{
Statement: `CREATE (n:SchemaMigration {version: {Version}}) RETURN n`,
Parameters: neoism.Props{"Version": v},
}
cqDown := neoism.CypherQuery{
Statement: `MATCH (n:SchemaMigration {version: {Version}}) DELETE n`,
Parameters: neoism.Props{"Version": v},
}
var cq neoism.CypherQuery
switch d {
case direction.Up:
if invert {
cq = cqDown
} else {
cq = cqUp
}
case direction.Down:
if invert {
cq = cqUp
} else {
cq = cqDown
}
}
return driver.db.Cypher(&cq)
}
func (driver *Driver) Migrate(f file.File, pipe chan interface{}) {
var err error
defer func() {
if err != nil {
// Invert version direction if we couldn't apply the changes for some reason.
if err := driver.setVersion(f.Direction, f.Version, true); err != nil {
pipe <- err
}
pipe <- err
}
close(pipe)
}()
pipe <- f
if err = driver.setVersion(f.Direction, f.Version, false); err != nil {
pipe <- err
return
}
if err = f.ReadContent(); err != nil {
pipe <- err
return
}
cQueries := []*neoism.CypherQuery{}
// Neoism doesn't support multiple statements per query.
cqlStmts := bytes.Split(f.Content, []byte(";"))
for _, cqlStmt := range cqlStmts {
cqlStmt = bytes.TrimSpace(cqlStmt)
if len(cqlStmt) > 0 {
cq := neoism.CypherQuery{Statement: string(cqlStmt)}
cQueries = append(cQueries, &cq)
}
}
var tx *neoism.Tx
tx, err = driver.db.Begin(cQueries)
if err != nil {
pipe <- err
for _, err := range tx.Errors {
pipe <- errors.New(fmt.Sprintf("%v", err.Message))
}
if err = tx.Rollback(); err != nil {
pipe <- err
}
return
}
if err = tx.Commit(); err != nil {
pipe <- err
for _, err := range tx.Errors {
pipe <- errors.New(fmt.Sprintf("%v", err.Message))
}
return
}
}
func (driver *Driver) Version() (uint64, error) {
res := []struct {
Version uint64 `json:"n.version"`
}{}
cq := neoism.CypherQuery{
Statement: `MATCH (n:SchemaMigration)
RETURN n.version ORDER BY n.version DESC LIMIT 1`,
Result: &res,
}
if err := driver.db.Cypher(&cq); err != nil || len(res) == 0 {
return 0, err
}
return res[0].Version, nil
}
func init() {
driver.RegisterDriver("neo4j", &Driver{})
driver.RegisterDriver("neo4js", &Driver{})
}
+100
View File
@@ -0,0 +1,100 @@
package neo4j
import (
"os"
"testing"
"github.com/jmcvetta/neoism"
"gopkg.in/mattes/migrate.v1/file"
"gopkg.in/mattes/migrate.v1/migrate/direction"
pipep "gopkg.in/mattes/migrate.v1/pipe"
)
// TestMigrate runs some additional tests on Migrate().
// Basic testing is already done in migrate/migrate_test.go
func TestMigrate(t *testing.T) {
t.Skip("TODO: fix test: neo4j_test.go:26: Get http://neo4j:test@/db/data/: http: no Host in request URL")
host := os.Getenv("NEO4J_PORT_7474_TCP_ADDR")
port := os.Getenv("NEO4J_PORT_7474_TCP_PORT")
driverUrl := "http://neo4j:test@" + host + ":" + port + "/db/data"
// prepare clean database
db, err := neoism.Connect(driverUrl)
if err != nil {
t.Fatal(err)
}
cq := neoism.CypherQuery{
Statement: `DROP INDEX ON :Yolo(name)`,
}
// If an error dropping the index then ignore it
db.Cypher(&cq)
driverUrl = "neo4j://neo4j:test@" + host + ":" + port + "/db/data"
d := &Driver{}
if err := d.Initialize(driverUrl); err != nil {
t.Fatal(err)
}
files := []file.File{
{
Path: "/foobar",
FileName: "001_foobar.up.cql",
Version: 1,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
CREATE INDEX ON :Yolo(name)
`),
},
{
Path: "/foobar",
FileName: "001_foobar.down.cql",
Version: 1,
Name: "foobar",
Direction: direction.Down,
Content: []byte(`
DROP INDEX ON :Yolo(name)
`),
},
{
Path: "/foobar",
FileName: "002_foobar.up.cql",
Version: 2,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
CREATE INDEX :Yolo(name) THIS WILL CAUSE AN ERROR
`),
},
}
pipe := pipep.New()
go d.Migrate(files[0], pipe)
errs := pipep.ReadErrors(pipe)
if len(errs) > 0 {
t.Fatal(errs)
}
pipe = pipep.New()
go d.Migrate(files[1], pipe)
errs = pipep.ReadErrors(pipe)
if len(errs) > 0 {
t.Fatal(errs)
}
pipe = pipep.New()
go d.Migrate(files[2], pipe)
errs = pipep.ReadErrors(pipe)
if len(errs) == 0 {
t.Error("Expected test case to fail")
}
if err := d.Close(); err != nil {
t.Fatal(err)
}
}
+6 -4
View File
@@ -1,7 +1,7 @@
# PostgreSQL Driver
* Runs migrations in transcations.
That means that if a migration failes, it will be safely rolled back.
* Runs migrations in transactions.
That means that if a migration fails, it will be safely rolled back.
* Tries to return helpful error messages.
* Stores migration version details in table ``schema_migrations``.
This table will be auto-generated.
@@ -15,9 +15,11 @@ migrate -url postgres://user@host:port/database -path ./db/migrations up
migrate help # for more info
# TODO(mattes): thinking about adding some custom flag to allow migration within schemas:
-url="postgres://user@host:port/database?schema=name"
-url="postgres://user@host:port/database?schema=name"
# see more docs: https://godoc.org/github.com/lib/pq#hdr-Connection_String_Parameters
```
## Authors
* Matthias Kadenbach, https://github.com/mattes
* Matthias Kadenbach, https://github.com/mattes
+39 -15
View File
@@ -5,10 +5,12 @@ import (
"database/sql"
"errors"
"fmt"
"github.com/lib/pq"
"github.com/mattes/migrate/file"
"github.com/mattes/migrate/migrate/direction"
"strconv"
"github.com/lib/pq"
"gopkg.in/mattes/migrate.v1/driver"
"gopkg.in/mattes/migrate.v1/file"
"gopkg.in/mattes/migrate.v1/migrate/direction"
)
type Driver struct {
@@ -41,7 +43,15 @@ func (driver *Driver) Close() error {
}
func (driver *Driver) ensureVersionTableExists() error {
if _, err := driver.db.Exec("CREATE TABLE IF NOT EXISTS " + tableName + " (version int not null primary key);"); err != nil {
r := driver.db.QueryRow("SELECT count(*) FROM information_schema.tables WHERE table_name = $1 AND table_schema = (SELECT current_schema());", tableName)
c := 0
if err := r.Scan(&c); err != nil {
return err
}
if c > 0 {
return nil
}
if _, err := driver.db.Exec("CREATE TABLE IF NOT EXISTS " + tableName + " (version bigint not null primary key);"); err != nil {
return err
}
return nil
@@ -85,20 +95,28 @@ func (driver *Driver) Migrate(f file.File, pipe chan interface{}) {
}
if _, err := tx.Exec(string(f.Content)); err != nil {
pqErr := err.(*pq.Error)
offset, err := strconv.Atoi(pqErr.Position)
if err == nil && offset >= 0 {
lineNo, columnNo := file.LineColumnFromOffset(f.Content, offset-1)
errorPart := file.LinesBeforeAndAfter(f.Content, lineNo, 5, 5, true)
pipe <- errors.New(fmt.Sprintf("%s %v: %s in line %v, column %v:\n\n%s", pqErr.Severity, pqErr.Code, pqErr.Message, lineNo, columnNo, string(errorPart)))
} else {
pipe <- errors.New(fmt.Sprintf("%s %v: %s", pqErr.Severity, pqErr.Code, pqErr.Message))
}
switch pqErr := err.(type) {
case *pq.Error:
offset, err := strconv.Atoi(pqErr.Position)
if err == nil && offset >= 0 {
lineNo, columnNo := file.LineColumnFromOffset(f.Content, offset-1)
errorPart := file.LinesBeforeAndAfter(f.Content, lineNo, 5, 5, true)
pipe <- errors.New(fmt.Sprintf("%s %v: %s in line %v, column %v:\n\n%s", pqErr.Severity, pqErr.Code, pqErr.Message, lineNo, columnNo, string(errorPart)))
} else {
pipe <- errors.New(fmt.Sprintf("%s %v: %s", pqErr.Severity, pqErr.Code, pqErr.Message))
}
if err := tx.Rollback(); err != nil {
if err := tx.Rollback(); err != nil {
pipe <- err
}
return
default:
pipe <- err
if err := tx.Rollback(); err != nil {
pipe <- err
}
return
}
return
}
if err := tx.Commit(); err != nil {
@@ -119,3 +137,9 @@ func (driver *Driver) Version() (uint64, error) {
return version, nil
}
}
func init() {
drv := Driver{}
driver.RegisterDriver("postgres", &drv)
driver.RegisterDriver("postgresql", &drv)
}
+55 -6
View File
@@ -2,19 +2,27 @@ package postgres
import (
"database/sql"
"github.com/mattes/migrate/file"
"github.com/mattes/migrate/migrate/direction"
pipep "github.com/mattes/migrate/pipe"
"os"
"testing"
"gopkg.in/mattes/migrate.v1/file"
"gopkg.in/mattes/migrate.v1/migrate/direction"
pipep "gopkg.in/mattes/migrate.v1/pipe"
)
// TestMigrate runs some additional tests on Migrate().
// Basic testing is already done in migrate/migrate_test.go
func TestMigrate(t *testing.T) {
driverUrl := "postgres://localhost/migratetest?sslmode=disable"
if testing.Short() {
t.Skip("skipping test in short mode.")
}
host := os.Getenv("POSTGRES_PORT_5432_TCP_ADDR")
port := os.Getenv("POSTGRES_PORT_5432_TCP_PORT")
driverURL := "postgres://postgres@" + host + ":" + port + "/template1?sslmode=disable"
// prepare clean database
connection, err := sql.Open("postgres", driverUrl)
connection, err := sql.Open("postgres", driverURL)
if err != nil {
t.Fatal(err)
}
@@ -25,7 +33,12 @@ func TestMigrate(t *testing.T) {
}
d := &Driver{}
if err := d.Initialize(driverUrl); err != nil {
if err := d.Initialize(driverURL); err != nil {
t.Fatal(err)
}
// testing idempotency: second call should be a no-op, since table already exists
if err := d.Initialize(driverURL); err != nil {
t.Fatal(err)
}
@@ -64,6 +77,28 @@ func TestMigrate(t *testing.T) {
)
`),
},
{
Path: "/foobar",
FileName: "20170118205923_demo.up.sql",
Version: 20170118205923,
Name: "demo",
Direction: direction.Up,
Content: []byte(`
CREATE TABLE demo (
id serial not null primary key
)
`),
},
{
Path: "/foobar",
FileName: "20170118205923_demo.down.sql",
Version: 20170118205923,
Name: "demo",
Direction: direction.Down,
Content: []byte(`
DROP TABLE demo
`),
},
}
pipe := pipep.New()
@@ -87,6 +122,20 @@ func TestMigrate(t *testing.T) {
t.Error("Expected test case to fail")
}
pipe = pipep.New()
go d.Migrate(files[3], pipe)
errs = pipep.ReadErrors(pipe)
if len(errs) > 0 {
t.Fatal(errs)
}
pipe = pipep.New()
go d.Migrate(files[4], pipe)
errs = pipep.ReadErrors(pipe)
if len(errs) > 0 {
t.Fatal(errs)
}
if err := d.Close(); err != nil {
t.Fatal(err)
}
+17
View File
@@ -0,0 +1,17 @@
# Ql Driver
* Supports both in-memory and file ql databases, with the URL schemes `ql+file://` and `ql+memory://`
* In-memory driver is not of much use, since the database is destroyed once the migration operation finishes, but is included for completeness.
* Stores migration version details in automatically generated table ``schema_migrations``.
## Usage
```bash
migrate -url ql+file://./data.db -path ./migrations create add_field_to_table
migrate -url ql+file://./data.db -path ./db/migrations up
migrate help # for more info
```
## Authors
* Sam Willcocks, https://github.com/wlcx
+131
View File
@@ -0,0 +1,131 @@
package ql
import (
"database/sql"
"gopkg.in/mattes/migrate.v1/driver"
"gopkg.in/mattes/migrate.v1/file"
"gopkg.in/mattes/migrate.v1/migrate/direction"
"fmt"
"strings"
_ "github.com/cznic/ql/driver"
)
const tableName = "schema_migration"
type Driver struct {
db *sql.DB
}
func (d *Driver) Initialize(url string) (err error) {
d.db, err = sql.Open("ql", strings.TrimPrefix(url, "ql+"))
if err != nil {
return
}
if err = d.db.Ping(); err != nil {
return
}
if err = d.ensureVersionTableExists(); err != nil {
return
}
return
}
func (d *Driver) Close() error {
if err := d.db.Close(); err != nil {
return err
}
return nil
}
func (d *Driver) FilenameExtension() string {
return "sql"
}
func (d *Driver) Migrate(f file.File, pipe chan interface{}) {
defer close(pipe)
pipe <- f
tx, err := d.db.Begin()
if err != nil {
pipe <- err
return
}
switch f.Direction {
case direction.Up:
if _, err := tx.Exec("INSERT INTO "+tableName+" (version) VALUES (uint($1))", f.Version); err != nil {
pipe <- err
if err := tx.Rollback(); err != nil {
pipe <- err
}
return
}
case direction.Down:
if _, err := tx.Exec("DELETE FROM "+tableName+" WHERE version=uint($1)", f.Version); err != nil {
pipe <- err
if err := tx.Rollback(); err != nil {
pipe <- err
}
return
}
}
if err := f.ReadContent(); err != nil {
pipe <- err
return
}
if _, err := tx.Exec(string(f.Content)); err != nil {
pipe <- err
if err := tx.Rollback(); err != nil {
pipe <- err
}
return
}
if err := tx.Commit(); err != nil {
pipe <- err
return
}
}
func (d *Driver) Version() (uint64, error) {
var version uint64
err := d.db.QueryRow("SELECT version FROM " + tableName + " ORDER BY version DESC LIMIT 1").Scan(&version)
switch {
case err == sql.ErrNoRows:
return 0, nil
case err != nil:
return 0, err
default:
return version, nil
}
}
func (d *Driver) ensureVersionTableExists() error {
tx, err := d.db.Begin()
if err != nil {
return err
}
if _, err := tx.Exec(fmt.Sprintf(`
CREATE TABLE IF NOT EXISTS %s (version uint64);
CREATE UNIQUE INDEX IF NOT EXISTS version_unique ON %s (version);
`, tableName, tableName)); err != nil {
if err := tx.Rollback(); err != nil {
return err
}
return err
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
func init() {
driver.RegisterDriver("ql+file", &Driver{})
driver.RegisterDriver("ql+memory", &Driver{})
}
+44
View File
@@ -0,0 +1,44 @@
package driver
import (
"sort"
"sync"
)
var driversMu sync.Mutex
var drivers = make(map[string]Driver)
// RegisterDriver register a driver so it can be created from its name. Drivers should
// call this from an init() function so that they registers themselves on
// import
func RegisterDriver(name string, driver Driver) {
driversMu.Lock()
defer driversMu.Unlock()
if driver == nil {
panic("driver: Register driver is nil")
}
if _, dup := drivers[name]; dup {
panic("sql: Register called twice for driver " + name)
}
drivers[name] = driver
}
// GetDriver retrieves a registered driver by name
func GetDriver(name string) Driver {
driversMu.Lock()
defer driversMu.Unlock()
driver := drivers[name]
return driver
}
// Drivers returns a sorted list of the names of the registered drivers.
func Drivers() []string {
driversMu.Lock()
defer driversMu.Unlock()
var list []string
for name := range drivers {
list = append(list, name)
}
sort.Strings(list)
return list
}
+21
View File
@@ -0,0 +1,21 @@
# Sqlite3 Driver
* Runs migrations in transcations.
That means that if a migration failes, it will be safely rolled back.
* Tries to return helpful error messages.
* Stores migration version details in table ``schema_migrations``.
This table will be auto-generated.
## Usage
```bash
migrate -url sqlite3://database.sqlite -path ./db/migrations create add_field_to_table
migrate -url sqlite3://database.sqlite -path ./db/migrations up
migrate help # for more info
```
## Authors
* Matthias Kadenbach, https://github.com/mattes
* Caesar Wirth, https://github.com/cjwirth
+131
View File
@@ -0,0 +1,131 @@
// Package sqlite3 implements the Driver interface.
package sqlite3
import (
"database/sql"
"errors"
"fmt"
"strings"
"github.com/mattn/go-sqlite3"
"gopkg.in/mattes/migrate.v1/driver"
"gopkg.in/mattes/migrate.v1/file"
"gopkg.in/mattes/migrate.v1/migrate/direction"
)
type Driver struct {
db *sql.DB
}
const tableName = "schema_migration"
func (driver *Driver) Initialize(url string) error {
filename := strings.SplitN(url, "sqlite3://", 2)
if len(filename) != 2 {
return errors.New("invalid sqlite3:// scheme")
}
db, err := sql.Open("sqlite3", filename[1])
if err != nil {
return err
}
if err := db.Ping(); err != nil {
return err
}
driver.db = db
if err := driver.ensureVersionTableExists(); err != nil {
return err
}
return nil
}
func (driver *Driver) Close() error {
if err := driver.db.Close(); err != nil {
return err
}
return nil
}
func (driver *Driver) ensureVersionTableExists() error {
if _, err := driver.db.Exec("CREATE TABLE IF NOT EXISTS " + tableName + " (version INTEGER PRIMARY KEY AUTOINCREMENT);"); err != nil {
return err
}
return nil
}
func (driver *Driver) FilenameExtension() string {
return "sql"
}
func (driver *Driver) Migrate(f file.File, pipe chan interface{}) {
defer close(pipe)
pipe <- f
tx, err := driver.db.Begin()
if err != nil {
pipe <- err
return
}
if f.Direction == direction.Up {
if _, err := tx.Exec("INSERT INTO "+tableName+" (version) VALUES (?)", f.Version); err != nil {
pipe <- err
if err := tx.Rollback(); err != nil {
pipe <- err
}
return
}
} else if f.Direction == direction.Down {
if _, err := tx.Exec("DELETE FROM "+tableName+" WHERE version=?", f.Version); err != nil {
pipe <- err
if err := tx.Rollback(); err != nil {
pipe <- err
}
return
}
}
if err := f.ReadContent(); err != nil {
pipe <- err
return
}
if _, err := tx.Exec(string(f.Content)); err != nil {
sqliteErr, isErr := err.(sqlite3.Error)
if isErr {
// The sqlite3 library only provides error codes, not position information. Output what we do know
pipe <- fmt.Errorf("SQLite Error (%s); Extended (%s)\nError: %s", sqliteErr.Code.Error(), sqliteErr.ExtendedCode.Error(), sqliteErr.Error())
} else {
pipe <- fmt.Errorf("An error occurred: %s", err.Error())
}
if err := tx.Rollback(); err != nil {
pipe <- err
}
return
}
if err := tx.Commit(); err != nil {
pipe <- err
return
}
}
func (driver *Driver) Version() (uint64, error) {
var version uint64
err := driver.db.QueryRow("SELECT version FROM " + tableName + " ORDER BY version DESC LIMIT 1").Scan(&version)
switch {
case err == sql.ErrNoRows:
return 0, nil
case err != nil:
return 0, err
default:
return version, nil
}
}
func init() {
driver.RegisterDriver("sqlite3", &Driver{})
}
+99
View File
@@ -0,0 +1,99 @@
package sqlite3
import (
"database/sql"
"testing"
"gopkg.in/mattes/migrate.v1/file"
"gopkg.in/mattes/migrate.v1/migrate/direction"
pipep "gopkg.in/mattes/migrate.v1/pipe"
)
// TestMigrate runs some additional tests on Migrate()
// Basic testing is already done in migrate/migrate_test.go
func TestMigrate(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
driverFile := ":memory:"
driverURL := "sqlite3://" + driverFile
// prepare clean database
connection, err := sql.Open("sqlite3", driverFile)
if err != nil {
t.Fatal(err)
}
if _, err := connection.Exec(`
DROP TABLE IF EXISTS yolo;
DROP TABLE IF EXISTS ` + tableName + `;`); err != nil {
t.Fatal(err)
}
d := &Driver{}
if err := d.Initialize(driverURL); err != nil {
t.Fatal(err)
}
files := []file.File{
{
Path: "/foobar",
FileName: "001_foobar.up.sql",
Version: 1,
Name: "foobar",
Direction: direction.Up,
Content: []byte(`
CREATE TABLE yolo (
id INTEGER PRIMARY KEY AUTOINCREMENT
);
`),
},
{
Path: "/foobar",
FileName: "002_foobar.down.sql",
Version: 1,
Name: "foobar",
Direction: direction.Down,
Content: []byte(`
DROP TABLE yolo;
`),
},
{
Path: "/foobar",
FileName: "002_foobar.up.sql",
Version: 1,
Name: "foobar",
Direction: direction.Down,
Content: []byte(`
CREATE TABLE error (
THIS; WILL CAUSE; AN ERROR;
)
`),
},
}
pipe := pipep.New()
go d.Migrate(files[0], pipe)
errs := pipep.ReadErrors(pipe)
if len(errs) > 0 {
t.Fatal(errs)
}
pipe = pipep.New()
go d.Migrate(files[1], pipe)
errs = pipep.ReadErrors(pipe)
if len(errs) > 0 {
t.Fatal(errs)
}
pipe = pipep.New()
go d.Migrate(files[2], pipe)
errs = pipep.ReadErrors(pipe)
if len(errs) == 0 {
t.Error("Expected test case to fail")
}
if err := d.Close(); err != nil {
t.Fatal(err)
}
}
+16 -6
View File
@@ -5,7 +5,6 @@ import (
"bytes"
"errors"
"fmt"
"github.com/mattes/migrate/migrate/direction"
"go/token"
"io/ioutil"
"path"
@@ -13,6 +12,8 @@ import (
"sort"
"strconv"
"strings"
"gopkg.in/mattes/migrate.v1/migrate/direction"
)
var filenameRegex = `^([0-9]+)_(.*)\.(up|down)\.%s$`
@@ -137,10 +138,10 @@ func (mf *MigrationFiles) From(version uint64, relativeN int) (Files, error) {
if d == direction.Up && migrationFile.Version > version && migrationFile.UpFile != nil {
files = append(files, *migrationFile.UpFile)
counter -= 1
counter--
} else if d == direction.Down && migrationFile.Version <= version && migrationFile.DownFile != nil {
files = append(files, *migrationFile.DownFile)
counter -= 1
counter--
}
} else {
break
@@ -163,9 +164,18 @@ func ReadMigrationFiles(path string, filenameRegex *regexp.Regexp) (files Migrat
d direction.Direction
}
tmpFiles := make([]*tmpFile, 0)
tmpFileMap := map[uint64]map[direction.Direction]tmpFile{}
for _, file := range ioFiles {
version, name, d, err := parseFilenameSchema(file.Name(), filenameRegex)
if err == nil {
if _, ok := tmpFileMap[version]; !ok {
tmpFileMap[version] = map[direction.Direction]tmpFile{}
}
if existing, ok := tmpFileMap[version][d]; !ok {
tmpFileMap[version][d] = tmpFile{version: version, name: name, filename: file.Name(), d: d}
} else {
return nil, fmt.Errorf("duplicate migration file version %d : %q and %q", version, existing.filename, file.Name())
}
tmpFiles = append(tmpFiles, &tmpFile{version, name, file.Name(), d})
}
}
@@ -249,7 +259,7 @@ func parseFilenameSchema(filename string, filenameRegex *regexp.Regexp) (version
version, err = strconv.ParseUint(matches[1], 10, 0)
if err != nil {
return 0, "", 0, errors.New(fmt.Sprintf("Unable to parse version '%v' in filename schema", matches[0]))
return 0, "", 0, fmt.Errorf("Unable to parse version '%v' in filename schema", matches[0])
}
if matches[3] == "up" {
@@ -257,7 +267,7 @@ func parseFilenameSchema(filename string, filenameRegex *regexp.Regexp) (version
} else if matches[3] == "down" {
d = direction.Down
} else {
return 0, "", 0, errors.New(fmt.Sprintf("Unable to parse up|down '%v' in filename schema", matches[3]))
return 0, "", 0, fmt.Errorf("Unable to parse up|down '%v' in filename schema", matches[3])
}
return version, matches[2], d, nil
@@ -324,7 +334,7 @@ func LinesBeforeAndAfter(data []byte, line, before, after int, lineNumbers bool)
lNew = append([]byte(lineCounterStr+": "), lNew...)
}
newLines = append(newLines, lNew)
lineCounter += 1
lineCounter++
}
return bytes.Join(newLines, []byte("\n"))
+48 -4
View File
@@ -1,11 +1,12 @@
package file
import (
"github.com/mattes/migrate/migrate/direction"
"io/ioutil"
"os"
"path"
"testing"
"gopkg.in/mattes/migrate.v1/migrate/direction"
)
func TestParseFilenameSchema(t *testing.T) {
@@ -61,7 +62,7 @@ func TestFiles(t *testing.T) {
}
defer os.RemoveAll(tmpdir)
if err := ioutil.WriteFile(path.Join(tmpdir, "nonsense.txt"), nil, 0755); err != nil {
if err = ioutil.WriteFile(path.Join(tmpdir, "nonsense.txt"), nil, 0755); err != nil {
t.Fatal("Unable to write files in tmpdir", err)
}
ioutil.WriteFile(path.Join(tmpdir, "002_migrationfile.up.sql"), nil, 0755)
@@ -133,7 +134,7 @@ func TestFiles(t *testing.T) {
}
// test read
if err := files[4].DownFile.ReadContent(); err != nil {
if err = files[4].DownFile.ReadContent(); err != nil {
t.Error("Unable to read file", err)
}
if files[4].DownFile.Content == nil {
@@ -169,7 +170,8 @@ func TestFiles(t *testing.T) {
}
for _, test := range tests {
rangeFiles, err := files.From(test.from, test.relative)
var rangeFiles Files
rangeFiles, err = files.From(test.from, test.relative)
if err != nil {
t.Error("Unable to fetch range:", err)
}
@@ -209,3 +211,45 @@ func TestFiles(t *testing.T) {
}
}
func TestDuplicateFiles(t *testing.T) {
dups := []string{
"001_migration.up.sql",
"001_duplicate.up.sql",
}
root, cleanFn, err := makeFiles("TestDuplicateFiles", dups...)
defer cleanFn()
if err != nil {
t.Fatal(err)
}
_, err = ReadMigrationFiles(root, FilenameRegex("sql"))
if err == nil {
t.Fatal("Expected duplicate migration file error")
}
}
// makeFiles takes an identifier, and a list of file names and uses them to create a temporary
// directory populated with files named with the names passed in. makeFiles returns the root
// directory name, and a func suitable for a defer cleanup to remove the temporary files after
// the calling function exits.
func makeFiles(testname string, names ...string) (root string, cleanup func(), err error) {
cleanup = func() {}
root, err = ioutil.TempDir("/tmp", testname)
if err != nil {
return
}
cleanup = func() { os.RemoveAll(root) }
if err = ioutil.WriteFile(path.Join(root, "nonsense.txt"), nil, 0755); err != nil {
return
}
for _, name := range names {
if err = ioutil.WriteFile(path.Join(root, name), nil, 0755); err != nil {
return
}
}
return
}
+75 -40
View File
@@ -1,29 +1,42 @@
// Package main is the CLI.
// You can use the CLI via Terminal.
// import "github.com/mattes/migrate/migrate" for usage within Go.
// import "gopkg.in/mattes/migrate.v1/migrate" for usage within Go.
package main
import (
"flag"
"fmt"
"github.com/fatih/color"
"github.com/mattes/migrate/file"
"github.com/mattes/migrate/migrate"
"github.com/mattes/migrate/migrate/direction"
pipep "github.com/mattes/migrate/pipe"
"os"
"strconv"
"time"
"github.com/fatih/color"
_ "gopkg.in/mattes/migrate.v1/driver/bash"
_ "gopkg.in/mattes/migrate.v1/driver/cassandra"
_ "gopkg.in/mattes/migrate.v1/driver/crate"
_ "gopkg.in/mattes/migrate.v1/driver/mssql"
_ "gopkg.in/mattes/migrate.v1/driver/mysql"
_ "gopkg.in/mattes/migrate.v1/driver/neo4j"
_ "gopkg.in/mattes/migrate.v1/driver/postgres"
_ "gopkg.in/mattes/migrate.v1/driver/ql"
_ "gopkg.in/mattes/migrate.v1/driver/sqlite3"
"gopkg.in/mattes/migrate.v1/file"
"gopkg.in/mattes/migrate.v1/migrate"
"gopkg.in/mattes/migrate.v1/migrate/direction"
pipep "gopkg.in/mattes/migrate.v1/pipe"
)
var url = flag.String("url", "", "")
var url = flag.String("url", os.Getenv("MIGRATE_URL"), "")
var migrationsPath = flag.String("path", "", "")
var version = flag.Bool("version", false, "Show migrate version")
func main() {
flag.Usage = func() {
helpCmd()
}
flag.Parse()
command := flag.Arg(0)
if *version {
fmt.Println(Version)
os.Exit(0)
@@ -48,7 +61,7 @@ func main() {
os.Exit(1)
}
fmt.Printf("Version %v migration files created in %v:\n", migrationFile.Version, migrationsPath)
fmt.Printf("Version %v migration files created in %v:\n", migrationFile.Version, *migrationsPath)
fmt.Println(migrationFile.UpFile.FileName)
fmt.Println(migrationFile.DownFile.FileName)
@@ -63,8 +76,11 @@ func main() {
timerStart = time.Now()
pipe := pipep.New()
go migrate.Migrate(pipe, *url, *migrationsPath, relativeNInt)
writePipe(pipe)
ok := writePipe(pipe)
printTimer()
if !ok {
os.Exit(1)
}
case "goto":
verifyMigrationsPath(*migrationsPath)
@@ -86,40 +102,55 @@ func main() {
timerStart = time.Now()
pipe := pipep.New()
go migrate.Migrate(pipe, *url, *migrationsPath, relativeNInt)
writePipe(pipe)
ok := writePipe(pipe)
printTimer()
if !ok {
os.Exit(1)
}
case "up":
verifyMigrationsPath(*migrationsPath)
timerStart = time.Now()
pipe := pipep.New()
go migrate.Up(pipe, *url, *migrationsPath)
writePipe(pipe)
ok := writePipe(pipe)
printTimer()
if !ok {
os.Exit(1)
}
case "down":
verifyMigrationsPath(*migrationsPath)
timerStart = time.Now()
pipe := pipep.New()
go migrate.Down(pipe, *url, *migrationsPath)
writePipe(pipe)
ok := writePipe(pipe)
printTimer()
if !ok {
os.Exit(1)
}
case "redo":
verifyMigrationsPath(*migrationsPath)
timerStart = time.Now()
pipe := pipep.New()
go migrate.Redo(pipe, *url, *migrationsPath)
writePipe(pipe)
ok := writePipe(pipe)
printTimer()
if !ok {
os.Exit(1)
}
case "reset":
verifyMigrationsPath(*migrationsPath)
timerStart = time.Now()
pipe := pipep.New()
go migrate.Reset(pipe, *url, *migrationsPath)
writePipe(pipe)
ok := writePipe(pipe)
printTimer()
if !ok {
os.Exit(1)
}
case "version":
verifyMigrationsPath(*migrationsPath)
@@ -131,47 +162,51 @@ func main() {
fmt.Println(version)
default:
fallthrough
helpCmd()
os.Exit(1)
case "help":
helpCmd()
}
}
func writePipe(pipe chan interface{}) {
func writePipe(pipe chan interface{}) (ok bool) {
okFlag := true
if pipe != nil {
for {
select {
case item, ok := <-pipe:
if !ok {
return
} else {
switch item.(type) {
case item, more := <-pipe:
if !more {
return okFlag
}
switch item.(type) {
case string:
fmt.Println(item.(string))
case string:
fmt.Println(item.(string))
case error:
case error:
c := color.New(color.FgRed)
c.Printf("%s\n\n", item.(error).Error())
okFlag = false
case file.File:
f := item.(file.File)
if f.Direction == direction.Up {
c := color.New(color.FgGreen)
c.Print(">")
} else if f.Direction == direction.Down {
c := color.New(color.FgRed)
c.Println(item.(error).Error(), "\n")
case file.File:
f := item.(file.File)
c := color.New(color.FgBlue)
if f.Direction == direction.Up {
c.Print(">")
} else if f.Direction == direction.Down {
c.Print("<")
}
fmt.Printf(" %s\n", f.FileName)
default:
text := fmt.Sprint(item)
fmt.Println(text)
c.Print("<")
}
fmt.Printf(" %s\n", f.FileName)
default:
text := fmt.Sprint(item)
fmt.Println(text)
}
}
}
}
return okFlag
}
func verifyMigrationsPath(path string) {
+5 -2
View File
@@ -1,9 +1,12 @@
// Package direction just holds convenience constants for Up and Down migrations.
package direction
// Direction - type that indicates direction of migration(up or down)
type Direction int
const (
Up Direction = +1
Down = -1
// Up - up migration
Up Direction = +1
// Down - down migration
Down Direction = -1
)
+66 -46
View File
@@ -10,11 +10,12 @@ import (
"path"
"strconv"
"strings"
"time"
"github.com/mattes/migrate/driver"
"github.com/mattes/migrate/file"
"github.com/mattes/migrate/migrate/direction"
pipep "github.com/mattes/migrate/pipe"
"gopkg.in/mattes/migrate.v1/driver"
"gopkg.in/mattes/migrate.v1/file"
"gopkg.in/mattes/migrate.v1/migrate/direction"
pipep "gopkg.in/mattes/migrate.v1/pipe"
)
// Up applies all available migrations
@@ -27,33 +28,35 @@ func Up(pipe chan interface{}, url, migrationsPath string) {
applyMigrationFiles, err := files.ToLastFrom(version)
if err != nil {
if err2 := d.Close(); err != nil {
if err2 := d.Close(); err2 != nil {
pipe <- err2
}
go pipep.Close(pipe, err)
return
}
signals := handleInterrupts()
defer signal.Stop(signals)
if len(applyMigrationFiles) > 0 {
for _, f := range applyMigrationFiles {
pipe1 := pipep.New()
go d.Migrate(f, pipe1)
if ok := pipep.WaitAndRedirect(pipe1, pipe, handleInterrupts()); !ok {
if ok := pipep.WaitAndRedirect(pipe1, pipe, signals); !ok {
break
}
}
if err2 := d.Close(); err != nil {
pipe <- err2
}
go pipep.Close(pipe, nil)
return
} else {
if err2 := d.Close(); err != nil {
pipe <- err2
if err := d.Close(); err != nil {
pipe <- err
}
go pipep.Close(pipe, nil)
return
}
if err := d.Close(); err != nil {
pipe <- err
}
go pipep.Close(pipe, nil)
return
}
// UpSync is synchronous version of Up
@@ -74,33 +77,35 @@ func Down(pipe chan interface{}, url, migrationsPath string) {
applyMigrationFiles, err := files.ToFirstFrom(version)
if err != nil {
if err2 := d.Close(); err != nil {
if err2 := d.Close(); err2 != nil {
pipe <- err2
}
go pipep.Close(pipe, err)
return
}
signals := handleInterrupts()
defer signal.Stop(signals)
if len(applyMigrationFiles) > 0 {
for _, f := range applyMigrationFiles {
pipe1 := pipep.New()
go d.Migrate(f, pipe1)
if ok := pipep.WaitAndRedirect(pipe1, pipe, handleInterrupts()); !ok {
if ok := pipep.WaitAndRedirect(pipe1, pipe, signals); !ok {
break
}
}
if err2 := d.Close(); err != nil {
pipe <- err2
}
go pipep.Close(pipe, nil)
return
} else {
if err2 := d.Close(); err != nil {
if err2 := d.Close(); err2 != nil {
pipe <- err2
}
go pipep.Close(pipe, nil)
return
}
if err2 := d.Close(); err2 != nil {
pipe <- err2
}
go pipep.Close(pipe, nil)
return
}
// DownSync is synchronous version of Down
@@ -115,12 +120,15 @@ func DownSync(url, migrationsPath string) (err []error, ok bool) {
func Redo(pipe chan interface{}, url, migrationsPath string) {
pipe1 := pipep.New()
go Migrate(pipe1, url, migrationsPath, -1)
if ok := pipep.WaitAndRedirect(pipe1, pipe, handleInterrupts()); !ok {
signals := handleInterrupts()
defer signal.Stop(signals)
if ok := pipep.WaitAndRedirect(pipe1, pipe, signals); !ok {
go pipep.Close(pipe, nil)
return
} else {
go Migrate(pipe, url, migrationsPath, +1)
}
go Migrate(pipe, url, migrationsPath, +1)
}
// RedoSync is synchronous version of Redo
@@ -135,12 +143,15 @@ func RedoSync(url, migrationsPath string) (err []error, ok bool) {
func Reset(pipe chan interface{}, url, migrationsPath string) {
pipe1 := pipep.New()
go Down(pipe1, url, migrationsPath)
if ok := pipep.WaitAndRedirect(pipe1, pipe, handleInterrupts()); !ok {
signals := handleInterrupts()
defer signal.Stop(signals)
if ok := pipep.WaitAndRedirect(pipe1, pipe, signals); !ok {
go pipep.Close(pipe, nil)
return
} else {
go Up(pipe, url, migrationsPath)
}
go Up(pipe, url, migrationsPath)
}
// ResetSync is synchronous version of Reset
@@ -161,28 +172,31 @@ func Migrate(pipe chan interface{}, url, migrationsPath string, relativeN int) {
applyMigrationFiles, err := files.From(version, relativeN)
if err != nil {
if err2 := d.Close(); err != nil {
if err2 := d.Close(); err2 != nil {
pipe <- err2
}
go pipep.Close(pipe, err)
return
}
signals := handleInterrupts()
defer signal.Stop(signals)
if len(applyMigrationFiles) > 0 && relativeN != 0 {
for _, f := range applyMigrationFiles {
pipe1 := pipep.New()
go d.Migrate(f, pipe1)
if ok := pipep.WaitAndRedirect(pipe1, pipe, handleInterrupts()); !ok {
if ok := pipep.WaitAndRedirect(pipe1, pipe, signals); !ok {
break
}
}
if err2 := d.Close(); err != nil {
if err2 := d.Close(); err2 != nil {
pipe <- err2
}
go pipep.Close(pipe, nil)
return
}
if err2 := d.Close(); err != nil {
if err2 := d.Close(); err2 != nil {
pipe <- err2
}
go pipep.Close(pipe, nil)
@@ -203,29 +217,35 @@ func Version(url, migrationsPath string) (version uint64, err error) {
if err != nil {
return 0, err
}
defer func() {
err = d.Close()
}()
return d.Version()
}
// Create creates new migration files on disk
func Create(url, migrationsPath, name string) (*file.MigrationFile, error) {
d, err := driver.New(url)
if err != nil {
return nil, err
}
files, err := file.ReadMigrationFiles(migrationsPath, file.FilenameRegex(d.FilenameExtension()))
ext, err := driver.FilenameExtensionFromURL(url)
if err != nil {
return nil, err
}
version := uint64(0)
if len(files) > 0 {
lastFile := files[len(files)-1]
version = lastFile.Version
files, err := file.ReadMigrationFiles(migrationsPath, file.FilenameRegex(ext))
if err != nil {
return nil, err
}
version += 1
version := uint64(time.Now().Unix())
for _, f := range files {
if f.Version == version {
version++
}
}
versionStr := strconv.FormatUint(version, 10)
length := 4 // TODO(mattes) check existing files and try to guess length
length := 10
if len(versionStr)%length != 0 {
versionStr = strings.Repeat("0", length-len(versionStr)%length) + versionStr
}
@@ -237,14 +257,14 @@ func Create(url, migrationsPath, name string) (*file.MigrationFile, error) {
Version: version,
UpFile: &file.File{
Path: migrationsPath,
FileName: fmt.Sprintf(filenamef, versionStr, name, "up", d.FilenameExtension()),
FileName: fmt.Sprintf(filenamef, versionStr, name, "up", ext),
Name: name,
Content: []byte(""),
Direction: direction.Up,
},
DownFile: &file.File{
Path: migrationsPath,
FileName: fmt.Sprintf(filenamef, versionStr, name, "down", d.FilenameExtension()),
FileName: fmt.Sprintf(filenamef, versionStr, name, "down", ext),
Name: name,
Content: []byte(""),
Direction: direction.Down,
+163 -116
View File
@@ -2,26 +2,44 @@ package migrate
import (
"io/ioutil"
"os"
"regexp"
"testing"
// Ensure imports for each driver we wish to test
_ "gopkg.in/mattes/migrate.v1/driver/postgres"
_ "gopkg.in/mattes/migrate.v1/driver/ql"
_ "gopkg.in/mattes/migrate.v1/driver/sqlite3"
)
// Add Driver URLs here to test basic Up, Down, .. functions.
var driverUrls = []string{
"postgres://localhost/migratetest?sslmode=disable",
var driverURLs = []string{
"postgres://postgres@" + os.Getenv("POSTGRES_PORT_5432_TCP_ADDR") + ":" + os.Getenv("POSTGRES_PORT_5432_TCP_PORT") + "/template1?sslmode=disable",
"ql+file://./test.db",
}
func tearDown(driverURL, tmpdir string) {
DownSync(driverURL, tmpdir)
os.RemoveAll(tmpdir)
}
func TestCreate(t *testing.T) {
for _, driverUrl := range driverUrls {
t.Logf("Test driver: %s", driverUrl)
if testing.Short() {
t.Skip("skipping test in short mode.")
}
for _, driverURL := range driverURLs {
t.Logf("Test driver: %s", driverURL)
tmpdir, err := ioutil.TempDir("/tmp", "migrate-test")
if err != nil {
t.Fatal(err)
}
defer tearDown(driverURL, tmpdir)
if _, err := Create(driverUrl, tmpdir, "test_migration"); err != nil {
if _, err = Create(driverURL, tmpdir, "test_migration"); err != nil {
t.Fatal(err)
}
if _, err := Create(driverUrl, tmpdir, "another migration"); err != nil {
if _, err = Create(driverURL, tmpdir, "another migration"); err != nil {
t.Fatal(err)
}
@@ -30,208 +48,237 @@ func TestCreate(t *testing.T) {
t.Fatal(err)
}
if len(files) != 4 {
t.Fatal("Expected 2 new files, got", len(files))
t.Fatal("Expected 4 new files, got", len(files))
}
fileNameRegexp := regexp.MustCompile(`^\d{10}_(.*.[up|down].sql)`)
expectFiles := []string{
"0001_test_migration.up.sql", "0001_test_migration.down.sql",
"0002_another_migration.up.sql", "0002_another_migration.down.sql",
"test_migration.up.sql", "test_migration.down.sql",
"another_migration.up.sql", "another_migration.down.sql",
}
foundCounter := 0
for _, expectFile := range expectFiles {
for _, file := range files {
if expectFile == file.Name() {
foundCounter += 1
break
var foundCounter int
for _, file := range files {
if x := fileNameRegexp.FindStringSubmatch(file.Name()); len(x) != 2 {
t.Errorf("expected %v to match %v", file.Name(), fileNameRegexp)
} else {
for _, expect := range expectFiles {
if expect == x[1] {
foundCounter++
break
}
}
}
}
if foundCounter != len(expectFiles) {
t.Error("not all expected files have been found")
t.Errorf("expected %v files, got %v", len(expectFiles), foundCounter)
}
}
}
func TestReset(t *testing.T) {
for _, driverUrl := range driverUrls {
t.Logf("Test driver: %s", driverUrl)
if testing.Short() {
t.Skip("skipping test in short mode.")
}
for _, driverURL := range driverURLs {
t.Logf("Test driver: %s", driverURL)
tmpdir, err := ioutil.TempDir("/tmp", "migrate-test")
if err != nil {
t.Fatal(err)
}
defer tearDown(driverURL, tmpdir)
Create(driverUrl, tmpdir, "migration1")
Create(driverUrl, tmpdir, "migration2")
errs, ok := ResetSync(driverUrl, tmpdir)
if !ok {
t.Fatal(errs)
}
version, err := Version(driverUrl, tmpdir)
Create(driverURL, tmpdir, "migration1")
f, err := Create(driverURL, tmpdir, "migration2")
if err != nil {
t.Fatal(err)
}
if version != 2 {
t.Fatalf("Expected version 2, got %v", version)
if err, ok := ResetSync(driverURL, tmpdir); !ok {
t.Fatal(err)
}
if version, err := Version(driverURL, tmpdir); err != nil {
t.Fatal(err)
} else if version != f.Version {
t.Fatalf("Expected version %v, got %v", version, f.Version)
}
}
}
func TestDown(t *testing.T) {
for _, driverUrl := range driverUrls {
t.Logf("Test driver: %s", driverUrl)
if testing.Short() {
t.Skip("skipping test in short mode.")
}
for _, driverURL := range driverURLs {
t.Logf("Test driver: %s", driverURL)
tmpdir, err := ioutil.TempDir("/tmp", "migrate-test")
if err != nil {
t.Fatal(err)
}
defer tearDown(driverURL, tmpdir)
Create(driverUrl, tmpdir, "migration1")
Create(driverUrl, tmpdir, "migration2")
initVersion, _ := Version(driverURL, tmpdir)
errs, ok := ResetSync(driverUrl, tmpdir)
if !ok {
t.Fatal(errs)
}
version, err := Version(driverUrl, tmpdir)
if err != nil {
firstMigration, _ := Create(driverURL, tmpdir, "migration1")
secondMigration, _ := Create(driverURL, tmpdir, "migration2")
t.Logf("init %v first %v second %v", initVersion, firstMigration.Version, secondMigration.Version)
if err, ok := ResetSync(driverURL, tmpdir); !ok {
t.Fatal(err)
}
if version != 2 {
t.Fatalf("Expected version 2, got %v", version)
if version, err := Version(driverURL, tmpdir); err != nil {
t.Fatal(err)
} else if version != secondMigration.Version {
t.Fatalf("Expected version %v, got %v", version, secondMigration.Version)
}
errs, ok = DownSync(driverUrl, tmpdir)
if !ok {
t.Fatal(errs)
}
version, err = Version(driverUrl, tmpdir)
if err != nil {
if err, ok := DownSync(driverURL, tmpdir); !ok {
t.Fatal(err)
}
if version != 0 {
t.Fatalf("Expected version 0, got %v", version)
if version, err := Version(driverURL, tmpdir); err != nil {
t.Fatal(err)
} else if version != 0 {
t.Fatalf("Expected 0, got %v", version)
}
}
}
func TestUp(t *testing.T) {
for _, driverUrl := range driverUrls {
t.Logf("Test driver: %s", driverUrl)
if testing.Short() {
t.Skip("skipping test in short mode.")
}
for _, driverURL := range driverURLs {
t.Logf("Test driver: %s", driverURL)
tmpdir, err := ioutil.TempDir("/tmp", "migrate-test")
if err != nil {
t.Fatal(err)
}
defer tearDown(driverURL, tmpdir)
Create(driverUrl, tmpdir, "migration1")
Create(driverUrl, tmpdir, "migration2")
initVersion, _ := Version(driverURL, tmpdir)
errs, ok := DownSync(driverUrl, tmpdir)
if !ok {
t.Fatal(errs)
}
version, err := Version(driverUrl, tmpdir)
if err != nil {
firstMigration, _ := Create(driverURL, tmpdir, "migration1")
secondMigration, _ := Create(driverURL, tmpdir, "migration2")
t.Logf("init %v first %v second %v", initVersion, firstMigration.Version, secondMigration.Version)
if err, ok := DownSync(driverURL, tmpdir); !ok {
t.Fatal(err)
}
if version != 0 {
t.Fatalf("Expected version 0, got %v", version)
if version, err := Version(driverURL, tmpdir); err != nil {
t.Fatal(err)
} else if version != initVersion {
t.Fatalf("Expected initial version %v, got %v", initVersion, version)
}
errs, ok = UpSync(driverUrl, tmpdir)
if !ok {
t.Fatal(errs)
}
version, err = Version(driverUrl, tmpdir)
if err != nil {
if err, ok := UpSync(driverURL, tmpdir); !ok {
t.Fatal(err)
}
if version != 2 {
t.Fatalf("Expected version 2, got %v", version)
if version, err := Version(driverURL, tmpdir); err != nil {
t.Fatal(err)
} else if version != secondMigration.Version {
t.Fatalf("Expected migrated version %v, got %v", secondMigration.Version, version)
}
}
}
func TestRedo(t *testing.T) {
for _, driverUrl := range driverUrls {
t.Logf("Test driver: %s", driverUrl)
if testing.Short() {
t.Skip("skipping test in short mode.")
}
for _, driverURL := range driverURLs {
t.Logf("Test driver: %s", driverURL)
tmpdir, err := ioutil.TempDir("/tmp", "migrate-test")
if err != nil {
t.Fatal(err)
}
defer tearDown(driverURL, tmpdir)
Create(driverUrl, tmpdir, "migration1")
Create(driverUrl, tmpdir, "migration2")
initVersion, _ := Version(driverURL, tmpdir)
errs, ok := ResetSync(driverUrl, tmpdir)
if !ok {
t.Fatal(errs)
}
version, err := Version(driverUrl, tmpdir)
if err != nil {
firstMigration, _ := Create(driverURL, tmpdir, "migration1")
secondMigration, _ := Create(driverURL, tmpdir, "migration2")
t.Logf("init %v first %v second %v", initVersion, firstMigration.Version, secondMigration.Version)
if err, ok := ResetSync(driverURL, tmpdir); !ok {
t.Fatal(err)
}
if version != 2 {
t.Fatalf("Expected version 2, got %v", version)
if version, err := Version(driverURL, tmpdir); err != nil {
t.Fatal(err)
} else if version != secondMigration.Version {
t.Fatalf("Expected migrated version %v, got %v", secondMigration.Version, version)
}
errs, ok = RedoSync(driverUrl, tmpdir)
if !ok {
t.Fatal(errs)
}
version, err = Version(driverUrl, tmpdir)
if err != nil {
if err, ok := RedoSync(driverURL, tmpdir); !ok {
t.Fatal(err)
}
if version != 2 {
t.Fatalf("Expected version 2, got %v", version)
if version, err := Version(driverURL, tmpdir); err != nil {
t.Fatal(err)
} else if version != secondMigration.Version {
t.Fatalf("Expected migrated version %v, got %v", secondMigration.Version, version)
}
}
}
func TestMigrate(t *testing.T) {
for _, driverUrl := range driverUrls {
t.Logf("Test driver: %s", driverUrl)
if testing.Short() {
t.Skip("skipping test in short mode.")
}
for _, driverURL := range driverURLs {
t.Logf("Test driver: %s", driverURL)
tmpdir, err := ioutil.TempDir("/tmp", "migrate-test")
if err != nil {
t.Fatal(err)
}
defer tearDown(driverURL, tmpdir)
Create(driverUrl, tmpdir, "migration1")
Create(driverUrl, tmpdir, "migration2")
initVersion, _ := Version(driverURL, tmpdir)
errs, ok := ResetSync(driverUrl, tmpdir)
if !ok {
t.Fatal(errs)
}
version, err := Version(driverUrl, tmpdir)
if err != nil {
firstMigration, _ := Create(driverURL, tmpdir, "migration1")
secondMigration, _ := Create(driverURL, tmpdir, "migration2")
t.Logf("init %v first %v second %v", initVersion, firstMigration.Version, secondMigration.Version)
if err, ok := ResetSync(driverURL, tmpdir); !ok {
t.Fatal(err)
}
if version != 2 {
t.Fatalf("Expected version 2, got %v", version)
if version, err := Version(driverURL, tmpdir); err != nil {
t.Fatal(err)
} else if version != secondMigration.Version {
t.Fatalf("Expected migrated version %v, got %v", secondMigration.Version, version)
}
errs, ok = MigrateSync(driverUrl, tmpdir, -2)
if !ok {
t.Fatal(errs)
}
version, err = Version(driverUrl, tmpdir)
if err != nil {
if err, ok := MigrateSync(driverURL, tmpdir, -2); !ok {
t.Fatal(err)
}
if version != 0 {
t.Fatalf("Expected version 0, got %v", version)
}
errs, ok = MigrateSync(driverUrl, tmpdir, +1)
if !ok {
t.Fatal(errs)
if version, err := Version(driverURL, tmpdir); err != nil {
t.Fatal(err)
} else if version != 0 {
t.Fatalf("Expected 0, got %v", version)
}
version, err = Version(driverUrl, tmpdir)
if err != nil {
if err, ok := MigrateSync(driverURL, tmpdir, +1); !ok {
t.Fatal(err)
}
if version != 1 {
t.Fatalf("Expected version 1, got %v", version)
if version, err := Version(driverURL, tmpdir); err != nil {
t.Fatal(err)
} else if version != firstMigration.Version {
t.Fatalf("Expected first version %v, got %v", firstMigration.Version, version)
}
}
}
+18 -12
View File
@@ -3,6 +3,7 @@ package pipe
import (
"os"
"os/signal"
)
// New creates a new pipe. A pipe is basically a channel.
@@ -25,12 +26,13 @@ func Close(pipe chan interface{}, err error) {
func WaitAndRedirect(pipe, redirectPipe chan interface{}, interrupt chan os.Signal) (ok bool) {
errorReceived := false
interruptsReceived := 0
defer stopNotifyInterruptChannel(interrupt)
if pipe != nil && redirectPipe != nil {
for {
select {
case <-interrupt:
interruptsReceived += 1
interruptsReceived++
if interruptsReceived > 1 {
os.Exit(5)
} else {
@@ -41,12 +43,11 @@ func WaitAndRedirect(pipe, redirectPipe chan interface{}, interrupt chan os.Sign
case item, ok := <-pipe:
if !ok {
return !errorReceived && interruptsReceived == 0
} else {
redirectPipe <- item
switch item.(type) {
case error:
errorReceived = true
}
}
redirectPipe <- item
switch item.(type) {
case error:
errorReceived = true
}
}
}
@@ -64,14 +65,19 @@ func ReadErrors(pipe chan interface{}) []error {
case item, ok := <-pipe:
if !ok {
return err
} else {
switch item.(type) {
case error:
err = append(err, item.(error))
}
}
switch item.(type) {
case error:
err = append(err, item.(error))
}
}
}
}
return err
}
func stopNotifyInterruptChannel(interruptChannel chan os.Signal) {
if interruptChannel != nil {
signal.Stop(interruptChannel)
}
}
+2 -1
View File
@@ -1,3 +1,4 @@
package main
const Version string = "1.2.0"
// Version - version of tool
const Version string = "1.3.1"