├── .circleci └── config.yml ├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── ci.yaml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── .travis.yml ├── CONTRIBUTING.md ├── Dockerfile ├── Dockerfile.circleci ├── Dockerfile.github-actions ├── FAQ.md ├── GETTING_STARTED.md ├── LICENSE ├── MIGRATIONS.md ├── Makefile ├── README.md ├── SECURITY.md ├── cli ├── README.md ├── main.go └── version.go ├── cmd └── migrate │ ├── README.md │ ├── examples │ └── Dockerfile │ ├── main.go │ └── version.go ├── database ├── cassandra │ ├── README.md │ ├── cassandra.go │ ├── cassandra_test.go │ └── examples │ │ └── migrations │ │ ├── 1_simple_select.down.sql │ │ └── 1_simple_select.up.sql ├── clickhouse │ ├── README.md │ ├── clickhouse.go │ ├── clickhouse_test.go │ └── examples │ │ └── migrations │ │ ├── 001_init.down.sql │ │ ├── 001_init.up.sql │ │ ├── 002_create_table.down.sql │ │ ├── 002_create_table.up.sql │ │ ├── 003_create_database.down.sql │ │ └── 003_create_database.up.sql ├── cockroachdb │ ├── README.md │ ├── TUTORIAL.md │ ├── cockroachdb.go │ ├── cockroachdb_test.go │ └── examples │ │ └── migrations │ │ ├── 1085649617_create_users_table.down.sql │ │ ├── 1085649617_create_users_table.up.sql │ │ ├── 1185749658_add_city_to_users.down.sql │ │ ├── 1185749658_add_city_to_users.up.sql │ │ ├── 1285849751_add_index_on_user_emails.down.sql │ │ ├── 1285849751_add_index_on_user_emails.up.sql │ │ ├── 1385949617_create_books_table.down.sql │ │ ├── 1385949617_create_books_table.up.sql │ │ ├── 1485949617_create_movies_table.down.sql │ │ ├── 1485949617_create_movies_table.up.sql │ │ ├── 1585849751_just_a_comment.up.sql │ │ ├── 1685849751_another_comment.up.sql │ │ ├── 1785849751_another_comment.up.sql │ │ └── 1885849751_another_comment.up.sql ├── crate │ └── README.md ├── driver.go ├── driver_test.go ├── error.go ├── firebird │ ├── README.md │ ├── examples │ │ └── migrations │ │ │ ├── 1085649617_create_users_table.down.sql │ │ │ ├── 1085649617_create_users_table.up.sql │ │ │ ├── 1185749658_add_city_to_users.down.sql │ │ │ ├── 1185749658_add_city_to_users.up.sql │ │ │ ├── 1285849751_add_index_on_user_emails.down.sql │ │ │ ├── 1285849751_add_index_on_user_emails.up.sql │ │ │ ├── 1385949617_create_books_table.down.sql │ │ │ ├── 1385949617_create_books_table.up.sql │ │ │ ├── 1485949617_create_movies_table.down.sql │ │ │ └── 1485949617_create_movies_table.up.sql │ ├── firebird.go │ └── firebird_test.go ├── mongodb │ ├── README.md │ ├── examples │ │ └── migrations │ │ │ ├── 001_create_user.down.json │ │ │ ├── 001_create_user.up.json │ │ │ ├── 002_create_indexes.down.json │ │ │ ├── 002_create_indexes.up.json │ │ │ ├── 003_add_new_field.down.json │ │ │ ├── 003_add_new_field.up.json │ │ │ ├── 004_replace_field_value_from_another_field.down.json │ │ │ └── 004_replace_field_value_from_another_field.up.json │ ├── mongodb.go │ └── mongodb_test.go ├── multistmt │ ├── parse.go │ └── parse_test.go ├── mysql │ ├── README.md │ ├── examples │ │ └── migrations │ │ │ ├── 1_init.down.sql │ │ │ └── 1_init.up.sql │ ├── mysql.go │ └── mysql_test.go ├── neo4j │ ├── README.md │ ├── TUTORIAL.md │ ├── examples │ │ └── migrations │ │ │ ├── 1578421040_create_movies_constraint.down.cypher │ │ │ ├── 1578421040_create_movies_constraint.up.cypher │ │ │ ├── 1578421725_create_movies.down.cypher │ │ │ ├── 1578421725_create_movies.up.cypher │ │ │ └── 1578421726_multistatement_test.up.cypher │ ├── neo4j.go │ └── neo4j_test.go ├── parse_test.go ├── pgx │ ├── README.md │ ├── examples │ │ └── migrations │ │ │ ├── 1085649617_create_users_table.down.sql │ │ │ ├── 1085649617_create_users_table.up.sql │ │ │ ├── 1185749658_add_city_to_users.down.sql │ │ │ ├── 1185749658_add_city_to_users.up.sql │ │ │ ├── 1285849751_add_index_on_user_emails.down.sql │ │ │ ├── 1285849751_add_index_on_user_emails.up.sql │ │ │ ├── 1385949617_create_books_table.down.sql │ │ │ ├── 1385949617_create_books_table.up.sql │ │ │ ├── 1485949617_create_movies_table.down.sql │ │ │ ├── 1485949617_create_movies_table.up.sql │ │ │ ├── 1585849751_just_a_comment.up.sql │ │ │ ├── 1685849751_another_comment.up.sql │ │ │ ├── 1785849751_another_comment.up.sql │ │ │ └── 1885849751_another_comment.up.sql │ ├── pgx.go │ ├── pgx_test.go │ └── v5 │ │ ├── README.md │ │ ├── pgx.go │ │ └── pgx_test.go ├── postgres │ ├── README.md │ ├── TUTORIAL.md │ ├── examples │ │ └── migrations │ │ │ ├── 1085649617_create_users_table.down.sql │ │ │ ├── 1085649617_create_users_table.up.sql │ │ │ ├── 1185749658_add_city_to_users.down.sql │ │ │ ├── 1185749658_add_city_to_users.up.sql │ │ │ ├── 1285849751_add_index_on_user_emails.down.sql │ │ │ ├── 1285849751_add_index_on_user_emails.up.sql │ │ │ ├── 1385949617_create_books_table.down.sql │ │ │ ├── 1385949617_create_books_table.up.sql │ │ │ ├── 1485949617_create_movies_table.down.sql │ │ │ ├── 1485949617_create_movies_table.up.sql │ │ │ ├── 1585849751_just_a_comment.up.sql │ │ │ ├── 1685849751_another_comment.up.sql │ │ │ ├── 1785849751_another_comment.up.sql │ │ │ └── 1885849751_another_comment.up.sql │ ├── postgres.go │ └── postgres_test.go ├── ql │ ├── README.md │ ├── examples │ │ └── migrations │ │ │ ├── 33_create_table.down.sql │ │ │ ├── 33_create_table.up.sql │ │ │ ├── 44_alter_table.down.sql │ │ │ └── 44_alter_table.up.sql │ ├── ql.go │ └── ql_test.go ├── redshift │ ├── README.md │ ├── examples │ │ └── migrations │ │ │ ├── 1085649617_create_users_table.down.sql │ │ │ ├── 1085649617_create_users_table.up.sql │ │ │ ├── 1185749658_add_city_to_users.down.sql │ │ │ ├── 1185749658_add_city_to_users.up.sql │ │ │ ├── 1285849751_add_index_on_user_emails.down.sql │ │ │ ├── 1285849751_add_index_on_user_emails.up.sql │ │ │ ├── 1385949617_create_books_table.down.sql │ │ │ ├── 1385949617_create_books_table.up.sql │ │ │ ├── 1485949617_create_movies_table.down.sql │ │ │ ├── 1485949617_create_movies_table.up.sql │ │ │ ├── 1585849751_just_a_comment.up.sql │ │ │ ├── 1685849751_another_comment.up.sql │ │ │ ├── 1785849751_another_comment.up.sql │ │ │ └── 1885849751_another_comment.up.sql │ ├── redshift.go │ └── redshift_test.go ├── rqlite │ ├── README.md │ ├── examples │ │ └── migrations │ │ │ ├── 33_create_table.down.sql │ │ │ ├── 33_create_table.up.sql │ │ │ ├── 44_alter_table.down.sql │ │ │ └── 44_alter_table.up.sql │ ├── rqlite.go │ └── rqlite_test.go ├── shell │ └── README.md ├── snowflake │ ├── README.md │ └── snowflake.go ├── spanner │ ├── README.md │ ├── examples │ │ └── migrations │ │ │ ├── 1481574547_create_users_table.down.sql │ │ │ ├── 1481574547_create_users_table.up.sql │ │ │ ├── 1496539702_add_city_to_users.down.sql │ │ │ ├── 1496539702_add_city_to_users.up.sql │ │ │ ├── 1496601752_add_index_on_user_emails.down.sql │ │ │ ├── 1496601752_add_index_on_user_emails.up.sql │ │ │ ├── 1496602638_create_books_table.down.sql │ │ │ ├── 1496602638_create_books_table.up.sql │ │ │ ├── 1621360367_create_transactions_table.down.sql │ │ │ └── 1621360367_create_transactions_table.up.sql │ ├── spanner.go │ └── spanner_test.go ├── sqlcipher │ ├── README.md │ ├── examples │ │ └── migrations │ │ │ ├── 33_create_table.down.sql │ │ │ ├── 33_create_table.up.sql │ │ │ ├── 44_alter_table.down.sql │ │ │ └── 44_alter_table.up.sql │ ├── sqlcipher.go │ └── sqlcipher_test.go ├── sqlite │ ├── README.md │ ├── examples │ │ └── migrations │ │ │ ├── 33_create_table.down.sql │ │ │ ├── 33_create_table.up.sql │ │ │ ├── 44_alter_table.down.sql │ │ │ └── 44_alter_table.up.sql │ ├── sqlite.go │ └── sqlite_test.go ├── sqlite3 │ ├── README.md │ ├── examples │ │ └── migrations │ │ │ ├── 33_create_table.down.sql │ │ │ ├── 33_create_table.up.sql │ │ │ ├── 44_alter_table.down.sql │ │ │ └── 44_alter_table.up.sql │ ├── sqlite3.go │ └── sqlite3_test.go ├── sqlserver │ ├── README.md │ ├── examples │ │ └── migrations │ │ │ ├── 1085649617_create_users_table.down.sql │ │ │ ├── 1085649617_create_users_table.up.sql │ │ │ ├── 1185749658_add_city_to_users.down.sql │ │ │ ├── 1185749658_add_city_to_users.up.sql │ │ │ ├── 1285849751_add_index_on_user_emails.down.sql │ │ │ ├── 1285849751_add_index_on_user_emails.up.sql │ │ │ ├── 1385949617_create_books_table.down.sql │ │ │ ├── 1385949617_create_books_table.up.sql │ │ │ ├── 1485949617_create_movies_table.down.sql │ │ │ ├── 1485949617_create_movies_table.up.sql │ │ │ ├── 1585849751_just_a_comment.up.sql │ │ │ ├── 1685849751_another_comment.up.sql │ │ │ ├── 1785849751_another_comment.up.sql │ │ │ └── 1885849751_another_comment.up.sql │ ├── sqlserver.go │ └── sqlserver_test.go ├── stub │ ├── stub.go │ └── stub_test.go ├── testing │ ├── migrate_testing.go │ └── testing.go ├── util.go ├── util_test.go └── yugabytedb │ ├── README.md │ ├── examples │ └── migrations │ │ ├── 1085649617_create_users_table.down.sql │ │ ├── 1085649617_create_users_table.up.sql │ │ ├── 1185749658_add_city_to_users.down.sql │ │ ├── 1185749658_add_city_to_users.up.sql │ │ ├── 1285849751_add_index_on_user_emails.down.sql │ │ ├── 1285849751_add_index_on_user_emails.up.sql │ │ ├── 1385949617_create_books_table.down.sql │ │ ├── 1385949617_create_books_table.up.sql │ │ ├── 1485949617_create_movies_table.down.sql │ │ ├── 1485949617_create_movies_table.up.sql │ │ ├── 1585849751_just_a_comment.up.sql │ │ ├── 1685849751_another_comment.up.sql │ │ ├── 1785849751_another_comment.up.sql │ │ └── 1885849751_another_comment.up.sql │ ├── yugabytedb.go │ └── yugabytedb_test.go ├── dktesting ├── dktesting.go └── example_test.go ├── docker-deploy.sh ├── go.mod ├── go.sum ├── internal ├── cli │ ├── build_aws-s3.go │ ├── build_bitbucket.go │ ├── build_cassandra.go │ ├── build_clickhouse.go │ ├── build_cockroachdb.go │ ├── build_firebird.go │ ├── build_github.go │ ├── build_github_ee.go │ ├── build_gitlab.go │ ├── build_go-bindata.go │ ├── build_godoc-vfs.go │ ├── build_google-cloud-storage.go │ ├── build_mongodb.go │ ├── build_mysql.go │ ├── build_neo4j.go │ ├── build_pgx.go │ ├── build_pgxv5.go │ ├── build_postgres.go │ ├── build_ql.go │ ├── build_redshift.go │ ├── build_rqlite.go │ ├── build_snowflake.go │ ├── build_spanner.go │ ├── build_sqlcipher.go │ ├── build_sqlite.go │ ├── build_sqlite3.go │ ├── build_sqlserver.go │ ├── build_yugabytedb.go │ ├── commands.go │ ├── commands_test.go │ ├── log.go │ └── main.go └── url │ ├── url.go │ └── url_test.go ├── log.go ├── migrate.go ├── migrate_test.go ├── migration.go ├── migration_test.go ├── source ├── aws_s3 │ ├── README.md │ ├── s3.go │ └── s3_test.go ├── bitbucket │ ├── .gitignore │ ├── README.md │ ├── bitbucket.go │ └── bitbucket_test.go ├── driver.go ├── driver_test.go ├── errors.go ├── file │ ├── README.md │ ├── file.go │ └── file_test.go ├── github │ ├── .gitignore │ ├── README.md │ ├── examples │ │ └── migrations │ │ │ ├── 1085649617_create_users_table.down.sql │ │ │ ├── 1085649617_create_users_table.up.sql │ │ │ ├── 1185749658_add_city_to_users.down.sql │ │ │ ├── 1185749658_add_city_to_users.up.sql │ │ │ ├── 1285849751_add_index_on_user_emails.down.sql │ │ │ ├── 1285849751_add_index_on_user_emails.up.sql │ │ │ ├── 1385949617_create_books_table.down.sql │ │ │ ├── 1385949617_create_books_table.up.sql │ │ │ ├── 1485949617_create_movies_table.down.sql │ │ │ ├── 1485949617_create_movies_table.up.sql │ │ │ ├── 1585849751_just_a_comment.up.sql │ │ │ ├── 1685849751_another_comment.up.sql │ │ │ ├── 1785849751_another_comment.up.sql │ │ │ └── 1885849751_another_comment.up.sql │ ├── github.go │ └── github_test.go ├── github_ee │ ├── .gitignore │ ├── README.md │ ├── github_ee.go │ └── github_ee_test.go ├── gitlab │ ├── .gitignore │ ├── README.md │ ├── examples │ │ └── migrations │ │ │ ├── 1085649617_create_users_table.down.sql │ │ │ ├── 1085649617_create_users_table.up.sql │ │ │ ├── 1185749658_add_city_to_users.down.sql │ │ │ ├── 1185749658_add_city_to_users.up.sql │ │ │ ├── 1285849751_add_index_on_user_emails.down.sql │ │ │ ├── 1285849751_add_index_on_user_emails.up.sql │ │ │ ├── 1385949617_create_books_table.down.sql │ │ │ ├── 1385949617_create_books_table.up.sql │ │ │ ├── 1485949617_create_movies_table.down.sql │ │ │ ├── 1485949617_create_movies_table.up.sql │ │ │ ├── 1585849751_just_a_comment.up.sql │ │ │ ├── 1685849751_another_comment.up.sql │ │ │ ├── 1785849751_another_comment.up.sql │ │ │ └── 1885849751_another_comment.up.sql │ ├── gitlab.go │ └── gitlab_test.go ├── go_bindata │ ├── README.md │ ├── examples │ │ └── migrations │ │ │ └── bindata.go │ ├── go-bindata.go │ ├── go-bindata_test.go │ └── testdata │ │ └── bindata.go ├── godoc_vfs │ ├── vfs.go │ ├── vfs_example_test.go │ └── vfs_test.go ├── google_cloud_storage │ ├── README.md │ ├── storage.go │ └── storage_test.go ├── httpfs │ ├── README.md │ ├── driver.go │ ├── driver_test.go │ ├── partial_driver.go │ ├── partial_driver_test.go │ └── testdata │ │ ├── duplicates │ │ ├── 1_foobar.up.sql │ │ └── 1_foobaz.up.sql │ │ ├── no-migrations │ │ └── some-file │ │ └── sql │ │ ├── 1_foobar.down.sql │ │ ├── 1_foobar.up.sql │ │ ├── 3_foobar.up.sql │ │ ├── 4_foobar.down.sql │ │ ├── 4_foobar.up.sql │ │ ├── 5_foobar.down.sql │ │ ├── 7_foobar.down.sql │ │ ├── 7_foobar.up.sql │ │ ├── other-files-are-ignored │ │ └── subdirs-are-ignored │ │ └── some-file ├── iofs │ ├── README.md │ ├── doc.go │ ├── example_test.go │ ├── iofs.go │ ├── iofs_test.go │ └── testdata │ │ └── migrations │ │ ├── 1_foobar.down.sql │ │ ├── 1_foobar.up.sql │ │ ├── 3_foobar.up.sql │ │ ├── 4_foobar.down.sql │ │ ├── 4_foobar.up.sql │ │ ├── 5_foobar.down.sql │ │ ├── 7_foobar.down.sql │ │ └── 7_foobar.up.sql ├── migration.go ├── migration_test.go ├── parse.go ├── parse_test.go ├── pkger │ ├── README.md │ ├── pkger.go │ └── pkger_test.go ├── stub │ ├── stub.go │ └── stub_test.go └── testing │ └── testing.go ├── testing ├── docker.go ├── testing.go └── testing_test.go ├── util.go └── util_test.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Golang CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-go/ for more details 4 | version: 2.1 5 | 6 | jobs: 7 | "golang-1_15": &template 8 | machine: 9 | # https://circleci.com/docs/2.0/configuration-reference/#available-machine-images 10 | image: ubuntu-2004:202010-01 11 | # docker_layer_caching: true 12 | 13 | # https://circleci.com/docs/2.0/configuration-reference/#resource_class 14 | resource_class: medium 15 | 16 | # Leave working directory unspecified and use defaults: 17 | # https://circleci.com/blog/go-v1.11-modules-and-circleci/ 18 | # working_directory: /go/src/github.com/golang-migrate/migrate 19 | 20 | environment: 21 | GO111MODULE: "on" 22 | GO_VERSION: "1.15.x" 23 | 24 | steps: 25 | # - setup_remote_docker: 26 | # version: 19.03.13 27 | # docker_layer_caching: true 28 | - run: curl -sL -o ~/bin/gimme https://raw.githubusercontent.com/travis-ci/gimme/master/gimme 29 | - run: curl -sfL -o ~/bin/golangci-lint.sh https://install.goreleaser.com/github.com/golangci/golangci-lint.sh 30 | - run: chmod +x ~/bin/gimme ~/bin/golangci-lint.sh 31 | - run: eval "$(gimme $GO_VERSION)" 32 | - run: golangci-lint.sh -b ~/bin v1.37.0 33 | - checkout 34 | - restore_cache: 35 | keys: 36 | - go-mod-v1-{{ arch }}-{{ checksum "go.sum" }} 37 | - run: golangci-lint run 38 | - run: make test COVERAGE_DIR=/tmp/coverage 39 | - save_cache: 40 | key: go-mod-v1-{{ arch }}-{{ checksum "go.sum" }} 41 | paths: 42 | - "/go/pkg/mod" 43 | - run: go get github.com/mattn/goveralls 44 | - run: goveralls -service=circle-ci -coverprofile /tmp/coverage/combined.txt 45 | 46 | "golang-1_16": 47 | <<: *template 48 | environment: 49 | GO_VERSION: "1.16.x" 50 | 51 | workflows: 52 | version: 2 53 | build: 54 | jobs: 55 | - "golang-1_15" 56 | - "golang-1_16" 57 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Project 2 | FAQ.md 3 | README.md 4 | LICENSE 5 | .gitignore 6 | .travis.yml 7 | CONTRIBUTING.md 8 | MIGRATIONS.md 9 | docker-deploy.sh 10 | 11 | # Golang 12 | testing 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the Bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **Steps to Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. My migrations look like '...' 13 | 2. I ran migrate with the following options '....' 14 | 3. See error 15 | 16 | **Expected Behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Migrate Version** 20 | e.g. v3.4.0 21 | Obtained by running: `migrate -version` 22 | 23 | **Loaded Source Drivers** 24 | e.g. s3, github, go-bindata, gcs, file 25 | Obtained by running: `migrate -help` 26 | 27 | **Loaded Database Drivers** 28 | e.g. spanner, stub, clickhouse, cockroachdb, crdb-postgres, postgres, postgresql, pgx, redshift, cassandra, cockroach, mysql 29 | Obtained by running: `migrate -help` 30 | 31 | **Go Version** 32 | e.g. go version go1.11 linux/amd64 33 | Obtained by running: `go version` 34 | 35 | **Stacktrace** 36 | Please provide if available 37 | 38 | **Additional context** 39 | Add any other context about the problem here. 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | cli/build 3 | cli/cli 4 | cli/migrate 5 | .coverage 6 | .godoc.pid 7 | vendor/ 8 | .vscode/ 9 | .idea 10 | dist/ 11 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | # timeout for analysis, e.g. 30s, 5m, default is 1m 3 | timeout: 5m 4 | linters: 5 | enable: 6 | #- golint 7 | #- interfacer 8 | - unconvert 9 | #- dupl 10 | - goconst 11 | - gofmt 12 | - misspell 13 | - unparam 14 | - nakedret 15 | - prealloc 16 | - revive 17 | #- gosec 18 | linters-settings: 19 | misspell: 20 | locale: US 21 | revive: 22 | rules: 23 | - name: redundant-build-tag 24 | issues: 25 | max-same-issues: 0 26 | max-issues-per-linter: 0 27 | exclude-use-default: false 28 | exclude: 29 | # gosec: Duplicated errcheck checks 30 | - G104 31 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: migrate 2 | before: 3 | hooks: 4 | - go mod tidy 5 | builds: 6 | - env: 7 | - CGO_ENABLED=0 8 | goos: 9 | - linux 10 | - windows 11 | - darwin 12 | goarch: 13 | - amd64 14 | - arm 15 | - arm64 16 | - 386 17 | goarm: 18 | - 7 19 | main: ./cmd/migrate 20 | ldflags: 21 | - '-w -s -X main.Version={{ .Version }} -extldflags "static"' 22 | flags: 23 | - "-tags={{ .Env.DATABASE }} {{ .Env.SOURCE }}" 24 | - "-trimpath" 25 | nfpms: 26 | - homepage: "https://github.com/golang-migrate/migrate" 27 | maintainer: "dhui@users.noreply.github.com" 28 | license: MIT 29 | description: "Database migrations" 30 | formats: 31 | - deb 32 | file_name_template: "{{ .ProjectName }}.{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" 33 | dockers: 34 | - goos: linux 35 | goarch: amd64 36 | dockerfile: Dockerfile.github-actions 37 | use: buildx 38 | ids: 39 | - migrate 40 | image_templates: 41 | - 'migrate/migrate:{{ .Tag }}-amd64' 42 | build_flag_templates: 43 | - '--label=org.opencontainers.image.created={{ .Date }}' 44 | - '--label=org.opencontainers.image.title={{ .ProjectName }}' 45 | - '--label=org.opencontainers.image.revision={{ .FullCommit }}' 46 | - '--label=org.opencontainers.image.version={{ .Version }}' 47 | - "--label=org.opencontainers.image.source={{ .GitURL }}" 48 | - "--platform=linux/amd64" 49 | - goos: linux 50 | goarch: arm64 51 | dockerfile: Dockerfile.github-actions 52 | use: buildx 53 | ids: 54 | - migrate 55 | image_templates: 56 | - 'migrate/migrate:{{ .Tag }}-arm64' 57 | build_flag_templates: 58 | - '--label=org.opencontainers.image.created={{ .Date }}' 59 | - '--label=org.opencontainers.image.title={{ .ProjectName }}' 60 | - '--label=org.opencontainers.image.revision={{ .FullCommit }}' 61 | - '--label=org.opencontainers.image.version={{ .Version }}' 62 | - "--label=org.opencontainers.image.source={{ .GitURL }}" 63 | - "--platform=linux/arm64" 64 | 65 | docker_manifests: 66 | - name_template: 'migrate/migrate:{{ .Tag }}' 67 | image_templates: 68 | - 'migrate/migrate:{{ .Tag }}-amd64' 69 | - 'migrate/migrate:{{ .Tag }}-arm64' 70 | - name_template: 'migrate/migrate:{{ .Major }}' 71 | image_templates: 72 | - 'migrate/migrate:{{ .Tag }}-amd64' 73 | - 'migrate/migrate:{{ .Tag }}-arm64' 74 | - name_template: 'migrate/migrate:latest' 75 | image_templates: 76 | - 'migrate/migrate:{{ .Tag }}-amd64' 77 | - 'migrate/migrate:{{ .Tag }}-arm64' 78 | archives: 79 | - name_template: "{{ .ProjectName }}.{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" 80 | format_overrides: 81 | - goos: windows 82 | format: zip 83 | checksum: 84 | name_template: 'sha256sum.txt' 85 | release: 86 | draft: true 87 | prerelease: auto 88 | source: 89 | enabled: true 90 | format: zip 91 | changelog: 92 | skip: false 93 | sort: asc 94 | filters: 95 | exclude: 96 | - '^docs:' 97 | - '^test:' 98 | - Merge pull request 99 | - Merge branch 100 | - go mod tidy 101 | snapshot: 102 | name_template: "{{ .Tag }}-next" 103 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Development, Testing and Contributing 2 | 3 | 1. Make sure you have a running Docker daemon 4 | (Install for [MacOS](https://docs.docker.com/docker-for-mac/)) 5 | 1. Use a version of Go that supports [modules](https://golang.org/cmd/go/#hdr-Modules__module_versions__and_more) (e.g. Go 1.11+) 6 | 1. Fork this repo and `git clone` somewhere to `$GOPATH/src/github.com/golang-migrate/migrate` 7 | * Ensure that [Go modules are enabled](https://golang.org/cmd/go/#hdr-Preliminary_module_support) (e.g. your repo path or the `GO111MODULE` environment variable are set correctly) 8 | 1. Install [golangci-lint](https://github.com/golangci/golangci-lint#install) 9 | 1. Run the linter: `golangci-lint run` 10 | 1. Confirm tests are working: `make test-short` 11 | 1. Write awesome code ... 12 | 1. `make test` to run all tests against all database versions 13 | 1. Push code and open Pull Request 14 | 15 | Some more helpful commands: 16 | 17 | * You can specify which database/ source tests to run: 18 | `make test-short SOURCE='file go_bindata' DATABASE='postgres cassandra'` 19 | * After `make test`, run `make html-coverage` which opens a shiny test coverage overview. 20 | * `make build-cli` builds the CLI in directory `cli/build/`. 21 | * `make list-external-deps` lists all external dependencies for each package 22 | * `make docs && make open-docs` opens godoc in your browser, `make kill-docs` kills the godoc server. 23 | Repeatedly call `make docs` to refresh the server. 24 | * Set the `DOCKER_API_VERSION` environment variable to the latest supported version if you get errors regarding the docker client API version being too new. 25 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24-alpine3.21 AS builder 2 | ARG VERSION 3 | 4 | RUN apk add --no-cache git gcc musl-dev make 5 | 6 | WORKDIR /go/src/github.com/golang-migrate/migrate 7 | 8 | ENV GO111MODULE=on 9 | 10 | COPY go.mod go.sum ./ 11 | 12 | RUN go mod download 13 | 14 | COPY . ./ 15 | 16 | RUN make build-docker 17 | 18 | FROM alpine:3.21 19 | 20 | RUN apk add --no-cache ca-certificates 21 | 22 | COPY --from=builder /go/src/github.com/golang-migrate/migrate/build/migrate.linux-386 /usr/local/bin/migrate 23 | RUN ln -s /usr/local/bin/migrate /migrate 24 | 25 | ENTRYPOINT ["migrate"] 26 | CMD ["--help"] 27 | -------------------------------------------------------------------------------- /Dockerfile.circleci: -------------------------------------------------------------------------------- 1 | ARG DOCKER_IMAGE 2 | FROM $DOCKER_IMAGE 3 | 4 | RUN apk add --no-cache git gcc musl-dev make 5 | 6 | WORKDIR /go/src/github.com/golang-migrate/migrate 7 | 8 | ENV GO111MODULE=on 9 | ENV COVERAGE_DIR=/tmp/coverage 10 | 11 | COPY go.mod go.sum ./ 12 | 13 | RUN go mod download 14 | 15 | COPY . ./ 16 | 17 | CMD ["make", "test"] 18 | -------------------------------------------------------------------------------- /Dockerfile.github-actions: -------------------------------------------------------------------------------- 1 | FROM alpine:3.19 2 | 3 | RUN apk add --no-cache ca-certificates 4 | 5 | COPY migrate /usr/local/bin/migrate 6 | 7 | RUN ln -s /usr/local/bin/migrate /usr/bin/migrate 8 | RUN ln -s /usr/local/bin/migrate /migrate 9 | 10 | ENTRYPOINT ["migrate"] 11 | CMD ["--help"] 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Original Work 4 | Copyright (c) 2016 Matthias Kadenbach 5 | https://github.com/mattes/migrate 6 | 7 | Modified Work 8 | Copyright (c) 2018 Dale Hui 9 | https://github.com/golang-migrate/migrate 10 | 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining a copy 13 | of this software and associated documentation files (the "Software"), to deal 14 | in the Software without restriction, including without limitation the rights 15 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | copies of the Software, and to permit persons to whom the Software is 17 | furnished to do so, subject to the following conditions: 18 | 19 | The above copyright notice and this permission notice shall be included in 20 | all copies or substantial portions of the Software. 21 | 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 28 | THE SOFTWARE. 29 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | master | :white_check_mark: | 8 | | 4.x | :white_check_mark: | 9 | | 3.x | :x: | 10 | | < 3.0 | :x: | 11 | 12 | ## Reporting a Vulnerability 13 | 14 | We prefer [coordinated disclosures](https://en.wikipedia.org/wiki/Coordinated_vulnerability_disclosure). To start one, create a GitHub security advisory following [these instructions](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability) 15 | 16 | Please suggest potential impact and urgency in your reports. 17 | -------------------------------------------------------------------------------- /cli/README.md: -------------------------------------------------------------------------------- 1 | # Deprecated 2 | 3 | Use [cmd/migrate](../cmd/migrate) instead 4 | -------------------------------------------------------------------------------- /cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/golang-migrate/migrate/v4/internal/cli" 4 | 5 | // Deprecated, please use cmd/migrate 6 | func main() { 7 | cli.Main(Version) 8 | } 9 | -------------------------------------------------------------------------------- /cli/version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Version is set in Makefile with build flags 4 | var Version = "dev" 5 | -------------------------------------------------------------------------------- /cmd/migrate/examples/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:bionic 2 | 3 | RUN apt-get update && \ 4 | apt-get install -y \ 5 | apt-transport-https \ 6 | ca-certificates \ 7 | curl \ 8 | gnupg-agent 9 | 10 | RUN curl -sSL https://packagecloud.io/golang-migrate/migrate/gpgkey | apt-key add - 11 | RUN echo "deb https://packagecloud.io/golang-migrate/migrate/ubuntu/ bionic main" > /etc/apt/sources.list.d/migrate.list 12 | RUN apt-get update && \ 13 | apt-get install -y migrate 14 | 15 | RUN migrate -version 16 | -------------------------------------------------------------------------------- /cmd/migrate/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/golang-migrate/migrate/v4/internal/cli" 4 | 5 | func main() { 6 | cli.Main(Version) 7 | } 8 | -------------------------------------------------------------------------------- /cmd/migrate/version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Version is set in Makefile with build flags 4 | var Version = "dev" 5 | -------------------------------------------------------------------------------- /database/cassandra/README.md: -------------------------------------------------------------------------------- 1 | # Cassandra / ScyllaDB 2 | 3 | * `Drop()` method will not work on Cassandra 2.X because it rely on 4 | system_schema table which comes with 3.X 5 | * Other methods should work properly but are **not tested** 6 | * The Cassandra driver (gocql) does not natively support executing multiple statements in a single query. To allow for multiple statements in a single migration, you can use the `x-multi-statement` param. There are two important caveats: 7 | * This mode splits the migration text into separately-executed statements by a semi-colon `;`. Thus `x-multi-statement` cannot be used when a statement in the migration contains a string with a semi-colon. 8 | * The queries are not executed in any sort of transaction/batch, meaning you are responsible for fixing partial migrations. 9 | 10 | **ScyllaDB** 11 | 12 | * No additional configuration is required since it is a drop-in replacement for Cassandra. 13 | * The `Drop()` method` works for ScyllaDB 5.1 14 | 15 | 16 | ## Usage 17 | `cassandra://host:port/keyspace?param1=value¶m2=value2` 18 | 19 | 20 | | URL Query | Default value | Description | 21 | |------------|-------------|-----------| 22 | | `x-migrations-table` | schema_migrations | Name of the migrations table | 23 | | `x-multi-statement` | false | Enable multiple statements to be ran in a single migration (See note above) | 24 | | `port` | 9042 | The port to bind to | 25 | | `consistency` | ALL | Migration consistency 26 | | `protocol` | | Cassandra protocol version (3 or 4) 27 | | `timeout` | 1 minute | Migration timeout 28 | | `connect-timeout` | 600ms | Initial connection timeout to the cluster | 29 | | `username` | nil | Username to use when authenticating. | 30 | | `password` | nil | Password to use when authenticating. | 31 | | `sslcert` | | Cert file location. The file must contain PEM encoded data. | 32 | | `sslkey` | | Key file location. The file must contain PEM encoded data. | 33 | | `sslrootcert` | | The location of the root certificate file. The file must contain PEM encoded data. | 34 | | `sslmode` | | Whether or not to use SSL (disable\|require\|verify-ca\|verify-full) | 35 | | `disable-host-lookup`| false | Disable initial host lookup. | 36 | 37 | `timeout` is parsed using [time.ParseDuration(s string)](https://golang.org/pkg/time/#ParseDuration) 38 | 39 | 40 | ## Upgrading from v1 41 | 42 | 1. Write down the current migration version from schema_migrations 43 | 2. `DROP TABLE schema_migrations` 44 | 4. Download and install the latest migrate version. 45 | 5. Force the current migration version with `migrate force `. 46 | -------------------------------------------------------------------------------- /database/cassandra/cassandra_test.go: -------------------------------------------------------------------------------- 1 | package cassandra 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/golang-migrate/migrate/v4" 7 | "strconv" 8 | "testing" 9 | ) 10 | 11 | import ( 12 | "github.com/dhui/dktest" 13 | "github.com/gocql/gocql" 14 | ) 15 | 16 | import ( 17 | dt "github.com/golang-migrate/migrate/v4/database/testing" 18 | "github.com/golang-migrate/migrate/v4/dktesting" 19 | _ "github.com/golang-migrate/migrate/v4/source/file" 20 | ) 21 | 22 | var ( 23 | opts = dktest.Options{PortRequired: true, ReadyFunc: isReady} 24 | // Supported versions: http://cassandra.apache.org/download/ 25 | // Although Cassandra 2.x is supported by the Apache Foundation, 26 | // the migrate db driver only supports Cassandra 3.x since it uses 27 | // the system_schema keyspace. 28 | // last ScyllaDB version tested is 5.1.11 29 | specs = []dktesting.ContainerSpec{ 30 | {ImageName: "cassandra:3.0", Options: opts}, 31 | {ImageName: "cassandra:3.11", Options: opts}, 32 | {ImageName: "scylladb/scylla:5.1.11", Options: opts}, 33 | } 34 | ) 35 | 36 | func isReady(ctx context.Context, c dktest.ContainerInfo) bool { 37 | // Cassandra exposes 5 ports (7000, 7001, 7199, 9042 & 9160) 38 | // We only need the port bound to 9042 39 | ip, portStr, err := c.Port(9042) 40 | if err != nil { 41 | return false 42 | } 43 | port, err := strconv.Atoi(portStr) 44 | if err != nil { 45 | return false 46 | } 47 | 48 | cluster := gocql.NewCluster(ip) 49 | cluster.Port = port 50 | cluster.Consistency = gocql.All 51 | p, err := cluster.CreateSession() 52 | if err != nil { 53 | return false 54 | } 55 | defer p.Close() 56 | // Create keyspace for tests 57 | if err = p.Query("CREATE KEYSPACE testks WITH REPLICATION = {'class': 'SimpleStrategy', 'replication_factor':1}").Exec(); err != nil { 58 | return false 59 | } 60 | return true 61 | } 62 | 63 | func Test(t *testing.T) { 64 | t.Run("test", test) 65 | t.Run("testMigrate", testMigrate) 66 | 67 | t.Cleanup(func() { 68 | for _, spec := range specs { 69 | t.Log("Cleaning up ", spec.ImageName) 70 | if err := spec.Cleanup(); err != nil { 71 | t.Error("Error removing ", spec.ImageName, "error:", err) 72 | } 73 | } 74 | }) 75 | } 76 | 77 | func test(t *testing.T) { 78 | dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { 79 | ip, port, err := c.Port(9042) 80 | if err != nil { 81 | t.Fatal("Unable to get mapped port:", err) 82 | } 83 | addr := fmt.Sprintf("cassandra://%v:%v/testks", ip, port) 84 | p := &Cassandra{} 85 | d, err := p.Open(addr) 86 | if err != nil { 87 | t.Fatal(err) 88 | } 89 | defer func() { 90 | if err := d.Close(); err != nil { 91 | t.Error(err) 92 | } 93 | }() 94 | dt.Test(t, d, []byte("SELECT table_name from system_schema.tables")) 95 | }) 96 | } 97 | 98 | func testMigrate(t *testing.T) { 99 | dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { 100 | ip, port, err := c.Port(9042) 101 | if err != nil { 102 | t.Fatal("Unable to get mapped port:", err) 103 | } 104 | addr := fmt.Sprintf("cassandra://%v:%v/testks", ip, port) 105 | p := &Cassandra{} 106 | d, err := p.Open(addr) 107 | if err != nil { 108 | t.Fatal(err) 109 | } 110 | defer func() { 111 | if err := d.Close(); err != nil { 112 | t.Error(err) 113 | } 114 | }() 115 | 116 | m, err := migrate.NewWithDatabaseInstance("file://./examples/migrations", "testks", d) 117 | if err != nil { 118 | t.Fatal(err) 119 | } 120 | dt.TestMigrate(t, m) 121 | }) 122 | } 123 | -------------------------------------------------------------------------------- /database/cassandra/examples/migrations/1_simple_select.down.sql: -------------------------------------------------------------------------------- 1 | SELECT table_name from system_schema.tables -------------------------------------------------------------------------------- /database/cassandra/examples/migrations/1_simple_select.up.sql: -------------------------------------------------------------------------------- 1 | SELECT table_name from system_schema.tables -------------------------------------------------------------------------------- /database/clickhouse/README.md: -------------------------------------------------------------------------------- 1 | # ClickHouse 2 | 3 | `clickhouse://host:port?username=user&password=password&database=clicks&x-multi-statement=true` 4 | 5 | | URL Query | Description | 6 | |------------|-------------| 7 | | `x-migrations-table`| Name of the migrations table | 8 | | `x-migrations-table-engine`| Engine to use for the migrations table, defaults to TinyLog | 9 | | `x-cluster-name` | Name of cluster for creating `schema_migrations` table cluster wide | 10 | | `database` | The name of the database to connect to | 11 | | `username` | The user to sign in as | 12 | | `password` | The user's password | 13 | | `host` | The host to connect to. | 14 | | `port` | The port to bind to. | 15 | | `x-multi-statement` | false | Enable multiple statements to be ran in a single migration (See note below) | 16 | 17 | ## Notes 18 | 19 | * The Clickhouse driver does not natively support executing multiple statements in a single query. To allow for multiple statements in a single migration, you can use the `x-multi-statement` param. There are two important caveats: 20 | * This mode splits the migration text into separately-executed statements by a semi-colon `;`. Thus `x-multi-statement` cannot be used when a statement in the migration contains a string with a semi-colon. 21 | * The queries are not executed in any sort of transaction/batch, meaning you are responsible for fixing partial migrations. 22 | * Using the default TinyLog table engine for the schema_versions table prevents backing up the table if using the [clickhouse-backup](https://github.com/AlexAkulov/clickhouse-backup) tool. If backing up the database with make sure the migrations are run with `x-migrations-table-engine=MergeTree`. 23 | * Clickhouse cluster mode is not officially supported, since it's not tested right now, but you can try enabling `schema_migrations` table replication by specifying a `x-cluster-name`: 24 | * When `x-cluster-name` is specified, `x-migrations-table-engine` also should be specified. See the docs regarding [replicated table engines](https://clickhouse.tech/docs/en/engines/table-engines/mergetree-family/replication/#table_engines-replication). 25 | * When `x-cluster-name` is specified, only the `schema_migrations` table is replicated across the cluster. You still need to write your migrations so that the application tables are replicated within the cluster. 26 | * If you want to create database inside the migration, you should know, that table which will manage migrations `schema-migrations table` will be in `default` table, so you can't use `USE ` inside migration. In this case you may not specify the database in the connection string (example you can find [here](examples/migrations/003_create_database.up.sql)) 27 | -------------------------------------------------------------------------------- /database/clickhouse/examples/migrations/001_init.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS test_1; -------------------------------------------------------------------------------- /database/clickhouse/examples/migrations/001_init.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE test_1 ( 2 | Date Date 3 | ) Engine=Memory; -------------------------------------------------------------------------------- /database/clickhouse/examples/migrations/002_create_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS test_2; -------------------------------------------------------------------------------- /database/clickhouse/examples/migrations/002_create_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE test_2 ( 2 | Date Date 3 | ) Engine=Memory; -------------------------------------------------------------------------------- /database/clickhouse/examples/migrations/003_create_database.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS driver_ratings; 2 | DROP TABLE IF EXISTS user_ratings; 3 | DROP TABLE IF EXISTS orders; 4 | DROP TABLE IF EXISTS driver_ratings_queue; 5 | DROP TABLE IF EXISTS user_ratings_queue; 6 | DROP TABLE IF EXISTS orders_queue; 7 | DROP VIEW IF EXISTS user_ratings_queue_mv; 8 | DROP VIEW IF EXISTS driver_ratings_queue_mv; 9 | DROP VIEW IF EXISTS orders_queue_mv; 10 | DROP DATABASE IF EXISTS analytics; 11 | -------------------------------------------------------------------------------- /database/clickhouse/examples/migrations/003_create_database.up.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE IF NOT EXISTS analytics; 2 | 3 | CREATE TABLE IF NOT EXISTS analytics.driver_ratings( 4 | rate UInt8, 5 | userID Int64, 6 | driverID String, 7 | orderID String, 8 | inserted_time DateTime DEFAULT now() 9 | ) ENGINE = MergeTree 10 | PARTITION BY driverID 11 | ORDER BY (inserted_time); 12 | 13 | CREATE TABLE analytics.driver_ratings_queue( 14 | rate UInt8, 15 | userID Int64, 16 | driverID String, 17 | orderID String 18 | ) ENGINE = Kafka 19 | SETTINGS kafka_broker_list = 'broker:9092', 20 | kafka_topic_list = 'driver-ratings', 21 | kafka_group_name = 'rating_readers', 22 | kafka_format = 'Avro', 23 | kafka_max_block_size = 1048576; 24 | 25 | CREATE MATERIALIZED VIEW analytics.driver_ratings_queue_mv TO analytics.driver_ratings AS 26 | SELECT rate, userID, driverID, orderID 27 | FROM analytics.driver_ratings_queue; 28 | 29 | CREATE TABLE IF NOT EXISTS analytics.user_ratings( 30 | rate UInt8, 31 | userID Int64, 32 | driverID String, 33 | orderID String, 34 | inserted_time DateTime DEFAULT now() 35 | ) ENGINE = MergeTree 36 | PARTITION BY userID 37 | ORDER BY (inserted_time); 38 | 39 | CREATE TABLE analytics.user_ratings_queue( 40 | rate UInt8, 41 | userID Int64, 42 | driverID String, 43 | orderID String 44 | ) ENGINE = Kafka 45 | SETTINGS kafka_broker_list = 'broker:9092', 46 | kafka_topic_list = 'user-ratings', 47 | kafka_group_name = 'rating_readers', 48 | kafka_format = 'JSON', 49 | kafka_max_block_size = 1048576; 50 | 51 | CREATE MATERIALIZED VIEW analytics.user_ratings_queue_mv TO analytics.user_ratings AS 52 | SELECT rate, userID, driverID, orderID 53 | FROM analytics.user_ratings_queue; 54 | 55 | CREATE TABLE IF NOT EXISTS analytics.orders( 56 | from_place String, 57 | to_place String, 58 | userID Int64, 59 | driverID String, 60 | orderID String, 61 | inserted_time DateTime DEFAULT now() 62 | ) ENGINE = MergeTree 63 | PARTITION BY driverID 64 | ORDER BY (inserted_time); 65 | 66 | CREATE TABLE analytics.orders_queue( 67 | from_place String, 68 | to_place String, 69 | userID Int64, 70 | driverID String, 71 | orderID String 72 | ) ENGINE = Kafka 73 | SETTINGS kafka_broker_list = 'broker:9092', 74 | kafka_topic_list = 'orders', 75 | kafka_group_name = 'order_readers', 76 | kafka_format = 'Avro', 77 | kafka_max_block_size = 1048576; 78 | 79 | CREATE MATERIALIZED VIEW analytics.orders_queue_mv TO orders AS 80 | SELECT from_place, to_place, userID, driverID, orderID 81 | FROM analytics.orders_queue; 82 | -------------------------------------------------------------------------------- /database/cockroachdb/README.md: -------------------------------------------------------------------------------- 1 | # cockroachdb 2 | 3 | `cockroachdb://user:password@host:port/dbname?query` (`cockroach://`, and `crdb-postgres://` work, too) 4 | 5 | | URL Query | WithInstance Config | Description | 6 | |------------|---------------------|-------------| 7 | | `x-migrations-table` | `MigrationsTable` | Name of the migrations table | 8 | | `x-lock-table` | `LockTable` | Name of the table which maintains the migration lock | 9 | | `x-force-lock` | `ForceLock` | Force lock acquisition to fix faulty migrations which may not have released the schema lock (Boolean, default is `false`) | 10 | | `dbname` | `DatabaseName` | The name of the database to connect to | 11 | | `user` | | The user to sign in as | 12 | | `password` | | The user's password | 13 | | `host` | | The host to connect to. Values that start with / are for unix domain sockets. (default is localhost) | 14 | | `port` | | The port to bind to. (default is 5432) | 15 | | `connect_timeout` | | Maximum wait for connection, in seconds. Zero or not specified means wait indefinitely. | 16 | | `sslcert` | | Cert file location. The file must contain PEM encoded data. | 17 | | `sslkey` | | Key file location. The file must contain PEM encoded data. | 18 | | `sslrootcert` | | The location of the root certificate file. The file must contain PEM encoded data. | 19 | | `sslmode` | | Whether or not to use SSL (disable\|require\|verify-ca\|verify-full) | 20 | -------------------------------------------------------------------------------- /database/cockroachdb/examples/migrations/1085649617_create_users_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS users; 2 | -------------------------------------------------------------------------------- /database/cockroachdb/examples/migrations/1085649617_create_users_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users ( 2 | user_id INT UNIQUE, 3 | name STRING(40), 4 | email STRING(40) 5 | ); 6 | -------------------------------------------------------------------------------- /database/cockroachdb/examples/migrations/1185749658_add_city_to_users.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users DROP COLUMN IF EXISTS city; 2 | -------------------------------------------------------------------------------- /database/cockroachdb/examples/migrations/1185749658_add_city_to_users.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users ADD COLUMN city TEXT; 2 | -------------------------------------------------------------------------------- /database/cockroachdb/examples/migrations/1285849751_add_index_on_user_emails.down.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX IF EXISTS users_email_index; 2 | -------------------------------------------------------------------------------- /database/cockroachdb/examples/migrations/1285849751_add_index_on_user_emails.up.sql: -------------------------------------------------------------------------------- 1 | CREATE UNIQUE INDEX IF NOT EXISTS users_email_index ON users (email); 2 | 3 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 4 | -------------------------------------------------------------------------------- /database/cockroachdb/examples/migrations/1385949617_create_books_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS books; 2 | -------------------------------------------------------------------------------- /database/cockroachdb/examples/migrations/1385949617_create_books_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE books ( 2 | user_id INT, 3 | name STRING(40), 4 | author STRING(40) 5 | ); 6 | -------------------------------------------------------------------------------- /database/cockroachdb/examples/migrations/1485949617_create_movies_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS movies; 2 | -------------------------------------------------------------------------------- /database/cockroachdb/examples/migrations/1485949617_create_movies_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE movies ( 2 | user_id INT, 3 | name STRING(40), 4 | director STRING(40) 5 | ); 6 | -------------------------------------------------------------------------------- /database/cockroachdb/examples/migrations/1585849751_just_a_comment.up.sql: -------------------------------------------------------------------------------- 1 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 2 | -------------------------------------------------------------------------------- /database/cockroachdb/examples/migrations/1685849751_another_comment.up.sql: -------------------------------------------------------------------------------- 1 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 2 | -------------------------------------------------------------------------------- /database/cockroachdb/examples/migrations/1785849751_another_comment.up.sql: -------------------------------------------------------------------------------- 1 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 2 | -------------------------------------------------------------------------------- /database/cockroachdb/examples/migrations/1885849751_another_comment.up.sql: -------------------------------------------------------------------------------- 1 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 2 | -------------------------------------------------------------------------------- /database/crate/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golang-migrate/migrate/278833935c12dda022b1355f33a897d895501c45/database/crate/README.md -------------------------------------------------------------------------------- /database/driver_test.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "io" 5 | "testing" 6 | ) 7 | 8 | func ExampleDriver() { 9 | // see database/stub for an example 10 | 11 | // database/stub/stub.go has the driver implementation 12 | // database/stub/stub_test.go runs database/testing/test.go:Test 13 | } 14 | 15 | // Using database/stub here is not possible as it 16 | // results in an import cycle. 17 | type mockDriver struct { 18 | url string 19 | } 20 | 21 | func (m *mockDriver) Open(url string) (Driver, error) { 22 | return &mockDriver{ 23 | url: url, 24 | }, nil 25 | } 26 | 27 | func (m *mockDriver) Close() error { 28 | return nil 29 | } 30 | 31 | func (m *mockDriver) Lock() error { 32 | return nil 33 | } 34 | 35 | func (m *mockDriver) Unlock() error { 36 | return nil 37 | } 38 | 39 | func (m *mockDriver) Run(migration io.Reader) error { 40 | return nil 41 | } 42 | 43 | func (m *mockDriver) SetVersion(version int, dirty bool) error { 44 | return nil 45 | } 46 | 47 | func (m *mockDriver) Version() (version int, dirty bool, err error) { 48 | return 0, false, nil 49 | } 50 | 51 | func (m *mockDriver) Drop() error { 52 | return nil 53 | } 54 | 55 | func TestRegisterTwice(t *testing.T) { 56 | Register("mock", &mockDriver{}) 57 | 58 | var err interface{} 59 | func() { 60 | defer func() { 61 | err = recover() 62 | }() 63 | Register("mock", &mockDriver{}) 64 | }() 65 | 66 | if err == nil { 67 | t.Fatal("expected a panic when calling Register twice") 68 | } 69 | } 70 | 71 | func TestOpen(t *testing.T) { 72 | // Make sure the driver is registered. 73 | // But if the previous test already registered it just ignore the panic. 74 | // If we don't do this it will be impossible to run this test standalone. 75 | func() { 76 | defer func() { 77 | _ = recover() 78 | }() 79 | Register("mock", &mockDriver{}) 80 | }() 81 | 82 | cases := []struct { 83 | url string 84 | err bool 85 | }{ 86 | { 87 | "mock://user:pass@tcp(host:1337)/db", 88 | false, 89 | }, 90 | { 91 | "unknown://bla", 92 | true, 93 | }, 94 | } 95 | 96 | for _, c := range cases { 97 | t.Run(c.url, func(t *testing.T) { 98 | d, err := Open(c.url) 99 | 100 | if err == nil { 101 | if c.err { 102 | t.Fatal("expected an error for an unknown driver") 103 | } else { 104 | if md, ok := d.(*mockDriver); !ok { 105 | t.Fatalf("expected *mockDriver got %T", d) 106 | } else if md.url != c.url { 107 | t.Fatalf("expected %q got %q", c.url, md.url) 108 | } 109 | } 110 | } else if !c.err { 111 | t.Fatalf("did not expect %q", err) 112 | } 113 | }) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /database/error.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Error should be used for errors involving queries ran against the database 8 | type Error struct { 9 | // Optional: the line number 10 | Line uint 11 | 12 | // Query is a query excerpt 13 | Query []byte 14 | 15 | // Err is a useful/helping error message for humans 16 | Err string 17 | 18 | // OrigErr is the underlying error 19 | OrigErr error 20 | } 21 | 22 | func (e Error) Error() string { 23 | if len(e.Err) == 0 { 24 | return fmt.Sprintf("%v in line %v: %s", e.OrigErr, e.Line, e.Query) 25 | } 26 | return fmt.Sprintf("%v in line %v: %s (details: %v)", e.Err, e.Line, e.Query, e.OrigErr) 27 | } 28 | -------------------------------------------------------------------------------- /database/firebird/README.md: -------------------------------------------------------------------------------- 1 | # firebird 2 | 3 | `firebirdsql://user:password@servername[:port_number]/database_name_or_file[?params1=value1[¶m2=value2]...]` 4 | 5 | | URL Query | WithInstance Config | Description | 6 | |------------|---------------------|-------------| 7 | | `x-migrations-table` | `MigrationsTable` | Name of the migrations table | 8 | | `auth_plugin_name` | | Authentication plugin name. Srp256/Srp/Legacy_Auth are available. (default is Srp) | 9 | | `column_name_to_lower` | | Force column name to lower. (default is false) | 10 | | `role` | | Role name | 11 | | `tzname` | | Time Zone name. (For Firebird 4.0+) | 12 | | `wire_crypt` | | Enable wire data encryption or not. For Firebird 3.0+ (default is true) | 13 | -------------------------------------------------------------------------------- /database/firebird/examples/migrations/1085649617_create_users_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE users; 2 | -------------------------------------------------------------------------------- /database/firebird/examples/migrations/1085649617_create_users_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users ( 2 | user_id integer unique, 3 | name varchar(40), 4 | email varchar(40) 5 | ); 6 | -------------------------------------------------------------------------------- /database/firebird/examples/migrations/1185749658_add_city_to_users.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users DROP city; 2 | -------------------------------------------------------------------------------- /database/firebird/examples/migrations/1185749658_add_city_to_users.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users ADD city varchar(100); 2 | 3 | 4 | -------------------------------------------------------------------------------- /database/firebird/examples/migrations/1285849751_add_index_on_user_emails.down.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX users_email_index; 2 | -------------------------------------------------------------------------------- /database/firebird/examples/migrations/1285849751_add_index_on_user_emails.up.sql: -------------------------------------------------------------------------------- 1 | CREATE UNIQUE INDEX users_email_index ON users (email); 2 | 3 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 4 | -------------------------------------------------------------------------------- /database/firebird/examples/migrations/1385949617_create_books_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE books; 2 | -------------------------------------------------------------------------------- /database/firebird/examples/migrations/1385949617_create_books_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE books ( 2 | user_id integer, 3 | name varchar(40), 4 | author varchar(40) 5 | ); 6 | -------------------------------------------------------------------------------- /database/firebird/examples/migrations/1485949617_create_movies_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE movies; 2 | -------------------------------------------------------------------------------- /database/firebird/examples/migrations/1485949617_create_movies_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE movies ( 2 | user_id integer, 3 | name varchar(40), 4 | director varchar(40) 5 | ); 6 | -------------------------------------------------------------------------------- /database/mongodb/README.md: -------------------------------------------------------------------------------- 1 | # MongoDB 2 | 3 | * Driver work with mongo through [db.runCommands](https://docs.mongodb.com/manual/reference/command/) 4 | * Migrations support json format. It contains array of commands for `db.runCommand`. Every command is executed in separate request to database 5 | * All keys have to be in quotes `"` 6 | * [Examples](./examples) 7 | 8 | # Usage 9 | 10 | `mongodb://user:password@host:port/dbname?query` (`mongodb+srv://` also works, but behaves a bit differently. See [docs](https://docs.mongodb.com/manual/reference/connection-string/#dns-seedlist-connection-format) for more information) 11 | 12 | | URL Query | WithInstance Config | Description | 13 | |------------|---------------------|-------------| 14 | | `x-migrations-collection` | `MigrationsCollection` | Name of the migrations collection | 15 | | `x-transaction-mode` | `TransactionMode` | If set to `true` wrap commands in [transaction](https://docs.mongodb.com/manual/core/transactions). Available only for replica set. Driver is using [strconv.ParseBool](https://golang.org/pkg/strconv/#ParseBool) for parsing| 16 | | `x-advisory-locking` | `true` | Feature flag for advisory locking, if set to false, disable advisory locking | 17 | | `x-advisory-lock-collection` | `migrate_advisory_lock` | The name of the collection to use for advisory locking.| 18 | | `x-advisory-lock-timeout` | `15` | The max time in seconds that migrate will wait to acquire a lock before failing. | 19 | | `x-advisory-lock-timeout-interval` | `10` | The max time in seconds between attempts to acquire the advisory lock, the lock is attempted to be acquired using an exponential backoff algorithm. | 20 | | `dbname` | `DatabaseName` | The name of the database to connect to | 21 | | `user` | | The user to sign in as. Can be omitted | 22 | | `password` | | The user's password. Can be omitted | 23 | | `host` | | The host to connect to | 24 | | `port` | | The port to bind to | -------------------------------------------------------------------------------- /database/mongodb/examples/migrations/001_create_user.down.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "dropUser": "deminem" 4 | } 5 | ] -------------------------------------------------------------------------------- /database/mongodb/examples/migrations/001_create_user.up.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "createUser": "deminem", 4 | "pwd": "gogo", 5 | "roles": [ 6 | { 7 | "role": "readWrite", 8 | "db": "testMigration" 9 | } 10 | ] 11 | } 12 | ] -------------------------------------------------------------------------------- /database/mongodb/examples/migrations/002_create_indexes.down.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "dropIndexes": "mycollection", 4 | "index": "username_sort_by_asc_created" 5 | }, 6 | { 7 | "dropIndexes": "mycollection", 8 | "index": "unique_email" 9 | } 10 | ] -------------------------------------------------------------------------------- /database/mongodb/examples/migrations/002_create_indexes.up.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "createIndexes": "mycollection", 3 | "indexes": [ 4 | { 5 | "key": { 6 | "username": 1, 7 | "created": -1 8 | }, 9 | "name": "username_sort_by_asc_created", 10 | "background": true 11 | }, 12 | { 13 | "key": { 14 | "email": 1 15 | }, 16 | "name": "unique_email", 17 | "unique": true, 18 | "background": true 19 | } 20 | ] 21 | }] -------------------------------------------------------------------------------- /database/mongodb/examples/migrations/003_add_new_field.down.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "update": "users", 4 | "updates": [ 5 | { 6 | "q": {}, 7 | "u": { 8 | "$unset": { 9 | "status": "" 10 | } 11 | }, 12 | "multi": true 13 | } 14 | ] 15 | } 16 | ] 17 | -------------------------------------------------------------------------------- /database/mongodb/examples/migrations/003_add_new_field.up.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "update": "users", 4 | "updates": [ 5 | { 6 | "q": {}, 7 | "u": { 8 | "$set": { 9 | "status": "active" 10 | } 11 | }, 12 | "multi": true 13 | } 14 | ] 15 | } 16 | ] 17 | -------------------------------------------------------------------------------- /database/mongodb/examples/migrations/004_replace_field_value_from_another_field.down.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "update": "users", 4 | "updates": [ 5 | { 6 | "q": {}, 7 | "u": { 8 | "fullname": "" 9 | }, 10 | "multi": true 11 | } 12 | ] 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /database/mongodb/examples/migrations/004_replace_field_value_from_another_field.up.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "aggregate": "users", 4 | "pipeline": [ 5 | { 6 | "$project": { 7 | "_id": 1, 8 | "firstname": 1, 9 | "lastname": 1, 10 | "username": 1, 11 | "password": 1, 12 | "email": 1, 13 | "active": 1, 14 | "fullname": { "$concat": ["$firstname", " ", "$lastname"] } 15 | } 16 | }, 17 | { 18 | "$out": "users" 19 | } 20 | ], 21 | "cursor": {} 22 | } 23 | ] 24 | -------------------------------------------------------------------------------- /database/multistmt/parse.go: -------------------------------------------------------------------------------- 1 | // Package multistmt provides methods for parsing multi-statement database migrations 2 | package multistmt 3 | 4 | import ( 5 | "bufio" 6 | "bytes" 7 | "io" 8 | ) 9 | 10 | // StartBufSize is the default starting size of the buffer used to scan and parse multi-statement migrations 11 | var StartBufSize = 4096 12 | 13 | // Handler handles a single migration parsed from a multi-statement migration. 14 | // It's given the single migration to handle and returns whether or not further statements 15 | // from the multi-statement migration should be parsed and handled. 16 | type Handler func(migration []byte) bool 17 | 18 | func splitWithDelimiter(delimiter []byte) func(d []byte, atEOF bool) (int, []byte, error) { 19 | return func(d []byte, atEOF bool) (int, []byte, error) { 20 | // SplitFunc inspired by bufio.ScanLines() implementation 21 | if atEOF { 22 | if len(d) == 0 { 23 | return 0, nil, nil 24 | } 25 | return len(d), d, nil 26 | } 27 | if i := bytes.Index(d, delimiter); i >= 0 { 28 | return i + len(delimiter), d[:i+len(delimiter)], nil 29 | } 30 | return 0, nil, nil 31 | } 32 | } 33 | 34 | // Parse parses the given multi-statement migration 35 | func Parse(reader io.Reader, delimiter []byte, maxMigrationSize int, h Handler) error { 36 | scanner := bufio.NewScanner(reader) 37 | scanner.Buffer(make([]byte, 0, StartBufSize), maxMigrationSize) 38 | scanner.Split(splitWithDelimiter(delimiter)) 39 | for scanner.Scan() { 40 | cont := h(scanner.Bytes()) 41 | if !cont { 42 | break 43 | } 44 | } 45 | return scanner.Err() 46 | } 47 | -------------------------------------------------------------------------------- /database/multistmt/parse_test.go: -------------------------------------------------------------------------------- 1 | package multistmt_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/golang-migrate/migrate/v4/database/multistmt" 10 | ) 11 | 12 | const maxMigrationSize = 1024 13 | 14 | func TestParse(t *testing.T) { 15 | testCases := []struct { 16 | name string 17 | multiStmt string 18 | delimiter string 19 | expected []string 20 | expectedErr error 21 | }{ 22 | {name: "single statement, no delimiter", multiStmt: "single statement, no delimiter", delimiter: ";", 23 | expected: []string{"single statement, no delimiter"}, expectedErr: nil}, 24 | {name: "single statement, one delimiter", multiStmt: "single statement, one delimiter;", delimiter: ";", 25 | expected: []string{"single statement, one delimiter;"}, expectedErr: nil}, 26 | {name: "two statements, no trailing delimiter", multiStmt: "statement one; statement two", delimiter: ";", 27 | expected: []string{"statement one;", " statement two"}, expectedErr: nil}, 28 | {name: "two statements, with trailing delimiter", multiStmt: "statement one; statement two;", delimiter: ";", 29 | expected: []string{"statement one;", " statement two;"}, expectedErr: nil}, 30 | } 31 | 32 | for _, tc := range testCases { 33 | t.Run(tc.name, func(t *testing.T) { 34 | stmts := make([]string, 0, len(tc.expected)) 35 | err := multistmt.Parse(strings.NewReader(tc.multiStmt), []byte(tc.delimiter), maxMigrationSize, func(b []byte) bool { 36 | stmts = append(stmts, string(b)) 37 | return true 38 | }) 39 | assert.Equal(t, tc.expectedErr, err) 40 | assert.Equal(t, tc.expected, stmts) 41 | }) 42 | } 43 | } 44 | 45 | func TestParseDiscontinue(t *testing.T) { 46 | multiStmt := "statement one; statement two" 47 | delimiter := ";" 48 | expected := []string{"statement one;"} 49 | 50 | stmts := make([]string, 0, len(expected)) 51 | err := multistmt.Parse(strings.NewReader(multiStmt), []byte(delimiter), maxMigrationSize, func(b []byte) bool { 52 | stmts = append(stmts, string(b)) 53 | return false 54 | }) 55 | assert.Nil(t, err) 56 | assert.Equal(t, expected, stmts) 57 | } 58 | -------------------------------------------------------------------------------- /database/mysql/README.md: -------------------------------------------------------------------------------- 1 | # MySQL 2 | 3 | `mysql://user:password@tcp(host:port)/dbname?query` 4 | 5 | | URL Query | WithInstance Config | Description | 6 | |------------|---------------------|-------------| 7 | | `x-migrations-table` | `MigrationsTable` | Name of the migrations table | 8 | | `x-no-lock` | `NoLock` | Set to `true` to skip `GET_LOCK`/`RELEASE_LOCK` statements. Useful for [multi-master MySQL flavors](https://www.percona.com/doc/percona-xtradb-cluster/LATEST/features/pxc-strict-mode.html#explicit-table-locking). Only run migrations from one host when this is enabled. | 9 | | `x-statement-timeout` | `StatementTimeout` | Abort any statement that takes more than the specified number of milliseconds, functionally similar to [Server-side SELECT statement timeouts](https://dev.mysql.com/blog-archive/server-side-select-statement-timeouts/) but enforced by the client. Available for all versions of MySQL, not just >=5.7. | 10 | | `dbname` | `DatabaseName` | The name of the database to connect to | 11 | | `user` | | The user to sign in as | 12 | | `password` | | The user's password | 13 | | `host` | | The host to connect to. | 14 | | `port` | | The port to bind to. | 15 | | `tls` | | TLS / SSL encrypted connection parameter; see [go-sql-driver](https://github.com/go-sql-driver/mysql#tls). Use any name (e.g. `migrate`) if you want to use a custom TLS config (`x-tls-` queries). | 16 | | `x-tls-ca` | | The location of the CA (certificate authority) file. | 17 | | `x-tls-cert` | | The location of the client certificate file. Must be used with `x-tls-key`. | 18 | | `x-tls-key` | | The location of the private key file. Must be used with `x-tls-cert`. | 19 | | `x-tls-insecure-skip-verify` | | Whether or not to use SSL (true\|false) | 20 | 21 | ## Use with existing client 22 | 23 | If you use the MySQL driver with existing database client, you must create the client with parameter `multiStatements=true`: 24 | 25 | ```go 26 | package main 27 | 28 | import ( 29 | "database/sql" 30 | 31 | _ "github.com/go-sql-driver/mysql" 32 | "github.com/golang-migrate/migrate/v4" 33 | "github.com/golang-migrate/migrate/v4/database/mysql" 34 | _ "github.com/golang-migrate/migrate/v4/source/file" 35 | ) 36 | 37 | func main() { 38 | db, _ := sql.Open("mysql", "user:password@tcp(host:port)/dbname?multiStatements=true") 39 | driver, _ := mysql.WithInstance(db, &mysql.Config{}) 40 | m, _ := migrate.NewWithDatabaseInstance( 41 | "file:///migrations", 42 | "mysql", 43 | driver, 44 | ) 45 | 46 | m.Steps(2) 47 | } 48 | ``` 49 | 50 | ## Upgrading from v1 51 | 52 | 1. Write down the current migration version from schema_migrations 53 | 1. `DROP TABLE schema_migrations` 54 | 2. Wrap your existing migrations in transactions ([BEGIN/COMMIT](https://dev.mysql.com/doc/refman/5.7/en/commit.html)) if you use multiple statements within one migration. 55 | 3. Download and install the latest migrate version. 56 | 4. Force the current migration version with `migrate force `. 57 | -------------------------------------------------------------------------------- /database/mysql/examples/migrations/1_init.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS test; -------------------------------------------------------------------------------- /database/mysql/examples/migrations/1_init.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS test ( 2 | firstname VARCHAR(16) 3 | ); -------------------------------------------------------------------------------- /database/neo4j/README.md: -------------------------------------------------------------------------------- 1 | # neo4j 2 | The Neo4j driver (bolt) does not natively support executing multiple statements in a single query. To allow for multiple statements in a single migration, you can use the `x-multi-statement` param. 3 | This mode splits the migration text into separately-executed statements by a semi-colon `;`. Thus `x-multi-statement` cannot be used when a statement in the migration contains a string with a semi-colon. 4 | The queries **should** run in a single transaction, so partial migrations should not be a concern, but this is untested. 5 | 6 | 7 | `neo4j://user:password@host:port/` 8 | 9 | | URL Query | WithInstance Config | Description | 10 | |------------|---------------------|-------------| 11 | | `x-multi-statement` | `MultiStatement` | Enable multiple statements to be ran in a single migration (See note above) | 12 | | `user` | Contained within `AuthConfig` | The user to sign in as | 13 | | `password` | Contained within `AuthConfig` | The user's password | 14 | | `host` | | The host to connect to. Values that start with / are for unix domain sockets. (default is localhost) | 15 | | `port` | | The port to bind to. (default is 7687) | 16 | | | `MigrationsLabel` | Name of the migrations node label | 17 | 18 | ## Supported versions 19 | 20 | Only Neo4j v3.5+ is [supported](https://github.com/neo4j/neo4j-go-driver/issues/64#issuecomment-625133600) 21 | -------------------------------------------------------------------------------- /database/neo4j/TUTORIAL.md: -------------------------------------------------------------------------------- 1 | ## Create migrations 2 | Let's create nodes called `Users`: 3 | ``` 4 | migrate create -ext cypher -dir db/migrations -seq create_user_nodes 5 | ``` 6 | If there were no errors, we should have two files available under `db/migrations` folder: 7 | - 000001_create_user_nodes.down.cypher 8 | - 000001_create_user_nodes.up.cypher 9 | 10 | Note the `cypher` extension that we provided. 11 | 12 | In the `.up.cypher` file let's create the table: 13 | ``` 14 | CREATE (u1:User {name: "Peter"}) 15 | CREATE (u2:User {name: "Paul"}) 16 | CREATE (u3:User {name: "Mary"}) 17 | ``` 18 | And in the `.down.sql` let's delete it: 19 | ``` 20 | MATCH (u:User) WHERE u.name IN ["Peter", "Paul", "Mary"] DELETE u 21 | ``` 22 | Ideally your migrations should be idempotent. You can read more about idempotency in [getting started](GETTING_STARTED.md#create-migrations) 23 | 24 | ## Run migrations 25 | ``` 26 | migrate -database ${NEO4J_URL} -path db/migrations up 27 | ``` 28 | Let's check if the table was created properly by running `bin/cypher-shell -u neo4j -p password`, then `neo4j> MATCH (u:User)` 29 | The output you are supposed to see: 30 | ``` 31 | +-----------------------------------------------------------------+ 32 | | u | 33 | +-----------------------------------------------------------------+ 34 | | (:User {name: "Peter") | 35 | | (:User {name: "Paul") | 36 | | (:User {name: "Mary") | 37 | +-----------------------------------------------------------------+ 38 | ``` 39 | Great! Now let's check if running reverse migration also works: 40 | ``` 41 | migrate -database ${NEO4J_URL} -path db/migrations down 42 | ``` 43 | Make sure to check if your database changed as expected in this case as well. 44 | 45 | ## Database transactions 46 | 47 | To show database transactions usage, let's create another set of migrations by running: 48 | ``` 49 | migrate create -ext cypher -dir db/migrations -seq add_mood_to_users 50 | ``` 51 | Again, it should create for us two migrations files: 52 | - 000002_add_mood_to_users.down.cypher 53 | - 000002_add_mood_to_users.up.cypher 54 | 55 | In Neo4j, when we want our queries to be done in a transaction, we need to wrap it with `:BEGIN` and `:COMMIT` commands. 56 | Migration up: 57 | ``` 58 | :BEGIN 59 | 60 | MATCH (u:User) 61 | SET u.mood = "Cheery" 62 | 63 | :COMMIT 64 | ``` 65 | Migration down: 66 | ``` 67 | :BEGIN 68 | 69 | MATCH (u:User) 70 | SET u.mood = null 71 | 72 | :COMMIT 73 | ``` 74 | 75 | ## Optional: Run migrations within your Go app 76 | Here is a very simple app running migrations for the above configuration: 77 | ``` 78 | import ( 79 | "log" 80 | 81 | "github.com/golang-migrate/migrate/v4" 82 | _ "github.com/golang-migrate/migrate/v4/database/neo4j" 83 | _ "github.com/golang-migrate/migrate/v4/source/file" 84 | ) 85 | 86 | func main() { 87 | m, err := migrate.New( 88 | "file://db/migrations", 89 | "neo4j://neo4j:password@localhost:7687/") 90 | if err != nil { 91 | log.Fatal(err) 92 | } 93 | if err := m.Up(); err != nil { 94 | log.Fatal(err) 95 | } 96 | } 97 | ``` -------------------------------------------------------------------------------- /database/neo4j/examples/migrations/1578421040_create_movies_constraint.down.cypher: -------------------------------------------------------------------------------- 1 | DROP CONSTRAINT ON (m:Movie) ASSERT m.Name IS UNIQUE -------------------------------------------------------------------------------- /database/neo4j/examples/migrations/1578421040_create_movies_constraint.up.cypher: -------------------------------------------------------------------------------- 1 | CREATE CONSTRAINT ON (m:Movie) ASSERT m.Name IS UNIQUE -------------------------------------------------------------------------------- /database/neo4j/examples/migrations/1578421725_create_movies.down.cypher: -------------------------------------------------------------------------------- 1 | MATCH (m:Movie) 2 | DELETE m -------------------------------------------------------------------------------- /database/neo4j/examples/migrations/1578421725_create_movies.up.cypher: -------------------------------------------------------------------------------- 1 | CREATE (:Movie {name: "Footloose"}) 2 | CREATE (:Movie {name: "Ghost"}) -------------------------------------------------------------------------------- /database/neo4j/examples/migrations/1578421726_multistatement_test.up.cypher: -------------------------------------------------------------------------------- 1 | CREATE (:Movie {name: "Hollow Man"}); 2 | CREATE (:Movie {name: "Mystic River"}); 3 | ;;; -------------------------------------------------------------------------------- /database/pgx/README.md: -------------------------------------------------------------------------------- 1 | # pgx 2 | 3 | This package is for [pgx/v4](https://pkg.go.dev/github.com/jackc/pgx/v4). A backend for the newer [pgx/v5](https://pkg.go.dev/github.com/jackc/pgx/v5) is [also available](v5). 4 | 5 | `pgx://user:password@host:port/dbname?query` 6 | 7 | | URL Query | WithInstance Config | Description | 8 | |------------|---------------------|-------------| 9 | | `x-migrations-table` | `MigrationsTable` | Name of the migrations table | 10 | | `x-migrations-table-quoted` | `MigrationsTableQuoted` | By default, migrate quotes the migration table for SQL injection safety reasons. This option disable quoting and naively checks that you have quoted the migration table name. e.g. `"my_schema"."schema_migrations"` | 11 | | `x-statement-timeout` | `StatementTimeout` | Abort any statement that takes more than the specified number of milliseconds | 12 | | `x-multi-statement` | `MultiStatementEnabled` | Enable multi-statement execution (default: false) | 13 | | `x-multi-statement-max-size` | `MultiStatementMaxSize` | Maximum size of single statement in bytes (default: 10MB) | 14 | | `x-lock-strategy` | `LockStrategy` | Strategy used for locking during migration (default: advisory) | 15 | | `x-lock-table` | `LockTable` | Name of the table which maintains the migration lock (default: schema_lock) | 16 | | `dbname` | `DatabaseName` | The name of the database to connect to | 17 | | `search_path` | | This variable specifies the order in which schemas are searched when an object is referenced by a simple name with no schema specified. | 18 | | `user` | | The user to sign in as | 19 | | `password` | | The user's password | 20 | | `host` | | The host to connect to. Values that start with / are for unix domain sockets. (default is localhost) | 21 | | `port` | | The port to bind to. (default is 5432) | 22 | | `fallback_application_name` | | An application_name to fall back to if one isn't provided. | 23 | | `connect_timeout` | | Maximum wait for connection, in seconds. Zero or not specified means wait indefinitely. | 24 | | `sslcert` | | Cert file location. The file must contain PEM encoded data. | 25 | | `sslkey` | | Key file location. The file must contain PEM encoded data. | 26 | | `sslrootcert` | | The location of the root certificate file. The file must contain PEM encoded data. | 27 | | `sslmode` | | Whether or not to use SSL (disable\|require\|verify-ca\|verify-full) | 28 | 29 | 30 | ## Upgrading from v1 31 | 32 | 1. Write down the current migration version from schema_migrations 33 | 1. `DROP TABLE schema_migrations` 34 | 2. Wrap your existing migrations in transactions ([BEGIN/COMMIT](https://www.postgresql.org/docs/current/static/transaction-iso.html)) if you use multiple statements within one migration. 35 | 3. Download and install the latest migrate version. 36 | 4. Force the current migration version with `migrate force `. 37 | 38 | ## Multi-statement mode 39 | 40 | In PostgreSQL running multiple SQL statements in one `Exec` executes them inside a transaction. Sometimes this 41 | behavior is not desirable because some statements can be only run outside of transaction (e.g. 42 | `CREATE INDEX CONCURRENTLY`). If you want to use `CREATE INDEX CONCURRENTLY` without activating multi-statement mode 43 | you have to put such statements in a separate migration files. 44 | -------------------------------------------------------------------------------- /database/pgx/examples/migrations/1085649617_create_users_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS users; 2 | -------------------------------------------------------------------------------- /database/pgx/examples/migrations/1085649617_create_users_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users ( 2 | user_id integer unique, 3 | name varchar(40), 4 | email varchar(40) 5 | ); 6 | -------------------------------------------------------------------------------- /database/pgx/examples/migrations/1185749658_add_city_to_users.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users DROP COLUMN IF EXISTS city; 2 | -------------------------------------------------------------------------------- /database/pgx/examples/migrations/1185749658_add_city_to_users.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users ADD COLUMN city varchar(100); 2 | 3 | 4 | -------------------------------------------------------------------------------- /database/pgx/examples/migrations/1285849751_add_index_on_user_emails.down.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX IF EXISTS users_email_index; 2 | -------------------------------------------------------------------------------- /database/pgx/examples/migrations/1285849751_add_index_on_user_emails.up.sql: -------------------------------------------------------------------------------- 1 | CREATE UNIQUE INDEX CONCURRENTLY users_email_index ON users (email); 2 | 3 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 4 | -------------------------------------------------------------------------------- /database/pgx/examples/migrations/1385949617_create_books_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS books; 2 | -------------------------------------------------------------------------------- /database/pgx/examples/migrations/1385949617_create_books_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE books ( 2 | user_id integer, 3 | name varchar(40), 4 | author varchar(40) 5 | ); 6 | -------------------------------------------------------------------------------- /database/pgx/examples/migrations/1485949617_create_movies_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS movies; 2 | -------------------------------------------------------------------------------- /database/pgx/examples/migrations/1485949617_create_movies_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE movies ( 2 | user_id integer, 3 | name varchar(40), 4 | director varchar(40) 5 | ); 6 | -------------------------------------------------------------------------------- /database/pgx/examples/migrations/1585849751_just_a_comment.up.sql: -------------------------------------------------------------------------------- 1 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 2 | -------------------------------------------------------------------------------- /database/pgx/examples/migrations/1685849751_another_comment.up.sql: -------------------------------------------------------------------------------- 1 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 2 | -------------------------------------------------------------------------------- /database/pgx/examples/migrations/1785849751_another_comment.up.sql: -------------------------------------------------------------------------------- 1 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 2 | -------------------------------------------------------------------------------- /database/pgx/examples/migrations/1885849751_another_comment.up.sql: -------------------------------------------------------------------------------- 1 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 2 | -------------------------------------------------------------------------------- /database/pgx/v5/README.md: -------------------------------------------------------------------------------- 1 | # pgx 2 | 3 | This package is for [pgx/v5](https://pkg.go.dev/github.com/jackc/pgx/v5). A backend for the older [pgx/v4](https://pkg.go.dev/github.com/jackc/pgx/v4). is [also available](..). 4 | 5 | `pgx5://user:password@host:port/dbname?query` 6 | 7 | | URL Query | WithInstance Config | Description | 8 | |------------|---------------------|-------------| 9 | | `x-migrations-table` | `MigrationsTable` | Name of the migrations table | 10 | | `x-migrations-table-quoted` | `MigrationsTableQuoted` | By default, migrate quotes the migration table for SQL injection safety reasons. This option disable quoting and naively checks that you have quoted the migration table name. e.g. `"my_schema"."schema_migrations"` | 11 | | `x-statement-timeout` | `StatementTimeout` | Abort any statement that takes more than the specified number of milliseconds | 12 | | `x-multi-statement` | `MultiStatementEnabled` | Enable multi-statement execution (default: false) | 13 | | `x-multi-statement-max-size` | `MultiStatementMaxSize` | Maximum size of single statement in bytes (default: 10MB) | 14 | | `dbname` | `DatabaseName` | The name of the database to connect to | 15 | | `search_path` | | This variable specifies the order in which schemas are searched when an object is referenced by a simple name with no schema specified. | 16 | | `user` | | The user to sign in as | 17 | | `password` | | The user's password | 18 | | `host` | | The host to connect to. Values that start with / are for unix domain sockets. (default is localhost) | 19 | | `port` | | The port to bind to. (default is 5432) | 20 | | `fallback_application_name` | | An application_name to fall back to if one isn't provided. | 21 | | `connect_timeout` | | Maximum wait for connection, in seconds. Zero or not specified means wait indefinitely. | 22 | | `sslcert` | | Cert file location. The file must contain PEM encoded data. | 23 | | `sslkey` | | Key file location. The file must contain PEM encoded data. | 24 | | `sslrootcert` | | The location of the root certificate file. The file must contain PEM encoded data. | 25 | | `sslmode` | | Whether or not to use SSL (disable\|require\|verify-ca\|verify-full) | 26 | 27 | 28 | ## Upgrading from v1 29 | 30 | 1. Write down the current migration version from schema_migrations 31 | 1. `DROP TABLE schema_migrations` 32 | 2. Wrap your existing migrations in transactions ([BEGIN/COMMIT](https://www.postgresql.org/docs/current/static/transaction-iso.html)) if you use multiple statements within one migration. 33 | 3. Download and install the latest migrate version. 34 | 4. Force the current migration version with `migrate force `. 35 | 36 | ## Multi-statement mode 37 | 38 | In PostgreSQL running multiple SQL statements in one `Exec` executes them inside a transaction. Sometimes this 39 | behavior is not desirable because some statements can be only run outside of transaction (e.g. 40 | `CREATE INDEX CONCURRENTLY`). If you want to use `CREATE INDEX CONCURRENTLY` without activating multi-statement mode 41 | you have to put such statements in a separate migration files. 42 | -------------------------------------------------------------------------------- /database/postgres/README.md: -------------------------------------------------------------------------------- 1 | # postgres 2 | 3 | `postgres://user:password@host:port/dbname?query` (`postgresql://` works, too) 4 | 5 | | URL Query | WithInstance Config | Description | 6 | |------------|---------------------|-------------| 7 | | `x-migrations-table` | `MigrationsTable` | Name of the migrations table | 8 | | `x-migrations-table-quoted` | `MigrationsTableQuoted` | By default, migrate quotes the migration table for SQL injection safety reasons. This option disable quoting and naively checks that you have quoted the migration table name. e.g. `"my_schema"."schema_migrations"` | 9 | | `x-statement-timeout` | `StatementTimeout` | Abort any statement that takes more than the specified number of milliseconds | 10 | | `x-multi-statement` | `MultiStatementEnabled` | Enable multi-statement execution (default: false) | 11 | | `x-multi-statement-max-size` | `MultiStatementMaxSize` | Maximum size of single statement in bytes (default: 10MB) | 12 | | `dbname` | `DatabaseName` | The name of the database to connect to | 13 | | `search_path` | | This variable specifies the order in which schemas are searched when an object is referenced by a simple name with no schema specified. | 14 | | `user` | | The user to sign in as | 15 | | `password` | | The user's password | 16 | | `host` | | The host to connect to. Values that start with / are for unix domain sockets. (default is localhost) | 17 | | `port` | | The port to bind to. (default is 5432) | 18 | | `fallback_application_name` | | An application_name to fall back to if one isn't provided. | 19 | | `connect_timeout` | | Maximum wait for connection, in seconds. Zero or not specified means wait indefinitely. | 20 | | `sslcert` | | Cert file location. The file must contain PEM encoded data. | 21 | | `sslkey` | | Key file location. The file must contain PEM encoded data. | 22 | | `sslrootcert` | | The location of the root certificate file. The file must contain PEM encoded data. | 23 | | `sslmode` | | Whether or not to use SSL (disable\|require\|verify-ca\|verify-full) | 24 | 25 | 26 | ## Upgrading from v1 27 | 28 | 1. Write down the current migration version from schema_migrations 29 | 1. `DROP TABLE schema_migrations` 30 | 2. Wrap your existing migrations in transactions ([BEGIN/COMMIT](https://www.postgresql.org/docs/current/static/transaction-iso.html)) if you use multiple statements within one migration. 31 | 3. Download and install the latest migrate version. 32 | 4. Force the current migration version with `migrate force `. 33 | 34 | ## Multi-statement mode 35 | 36 | In PostgreSQL running multiple SQL statements in one `Exec` executes them inside a transaction. Sometimes this 37 | behavior is not desirable because some statements can be only run outside of transaction (e.g. 38 | `CREATE INDEX CONCURRENTLY`). If you want to use `CREATE INDEX CONCURRENTLY` without activating multi-statement mode 39 | you have to put such statements in a separate migration files. 40 | -------------------------------------------------------------------------------- /database/postgres/examples/migrations/1085649617_create_users_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS users; 2 | -------------------------------------------------------------------------------- /database/postgres/examples/migrations/1085649617_create_users_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users ( 2 | user_id integer unique, 3 | name varchar(40), 4 | email varchar(40) 5 | ); 6 | -------------------------------------------------------------------------------- /database/postgres/examples/migrations/1185749658_add_city_to_users.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users DROP COLUMN IF EXISTS city; 2 | -------------------------------------------------------------------------------- /database/postgres/examples/migrations/1185749658_add_city_to_users.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users ADD COLUMN city varchar(100); 2 | 3 | 4 | -------------------------------------------------------------------------------- /database/postgres/examples/migrations/1285849751_add_index_on_user_emails.down.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX IF EXISTS users_email_index; 2 | -------------------------------------------------------------------------------- /database/postgres/examples/migrations/1285849751_add_index_on_user_emails.up.sql: -------------------------------------------------------------------------------- 1 | CREATE UNIQUE INDEX CONCURRENTLY users_email_index ON users (email); 2 | 3 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 4 | -------------------------------------------------------------------------------- /database/postgres/examples/migrations/1385949617_create_books_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS books; 2 | -------------------------------------------------------------------------------- /database/postgres/examples/migrations/1385949617_create_books_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE books ( 2 | user_id integer, 3 | name varchar(40), 4 | author varchar(40) 5 | ); 6 | -------------------------------------------------------------------------------- /database/postgres/examples/migrations/1485949617_create_movies_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS movies; 2 | -------------------------------------------------------------------------------- /database/postgres/examples/migrations/1485949617_create_movies_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE movies ( 2 | user_id integer, 3 | name varchar(40), 4 | director varchar(40) 5 | ); 6 | -------------------------------------------------------------------------------- /database/postgres/examples/migrations/1585849751_just_a_comment.up.sql: -------------------------------------------------------------------------------- 1 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 2 | -------------------------------------------------------------------------------- /database/postgres/examples/migrations/1685849751_another_comment.up.sql: -------------------------------------------------------------------------------- 1 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 2 | -------------------------------------------------------------------------------- /database/postgres/examples/migrations/1785849751_another_comment.up.sql: -------------------------------------------------------------------------------- 1 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 2 | -------------------------------------------------------------------------------- /database/postgres/examples/migrations/1885849751_another_comment.up.sql: -------------------------------------------------------------------------------- 1 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 2 | -------------------------------------------------------------------------------- /database/ql/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golang-migrate/migrate/278833935c12dda022b1355f33a897d895501c45/database/ql/README.md -------------------------------------------------------------------------------- /database/ql/examples/migrations/33_create_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS pets; -------------------------------------------------------------------------------- /database/ql/examples/migrations/33_create_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE pets ( 2 | name string 3 | ); -------------------------------------------------------------------------------- /database/ql/examples/migrations/44_alter_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS pets; -------------------------------------------------------------------------------- /database/ql/examples/migrations/44_alter_table.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE pets ADD predator bool;; -------------------------------------------------------------------------------- /database/ql/ql_test.go: -------------------------------------------------------------------------------- 1 | package ql 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/golang-migrate/migrate/v4" 10 | dt "github.com/golang-migrate/migrate/v4/database/testing" 11 | _ "github.com/golang-migrate/migrate/v4/source/file" 12 | _ "modernc.org/ql/driver" 13 | ) 14 | 15 | func Test(t *testing.T) { 16 | dir := t.TempDir() 17 | t.Logf("DB path : %s\n", filepath.Join(dir, "ql.db")) 18 | p := &Ql{} 19 | addr := fmt.Sprintf("ql://%s", filepath.Join(dir, "ql.db")) 20 | d, err := p.Open(addr) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | db, err := sql.Open("ql", filepath.Join(dir, "ql.db")) 26 | if err != nil { 27 | return 28 | } 29 | defer func() { 30 | if err := db.Close(); err != nil { 31 | return 32 | } 33 | }() 34 | dt.Test(t, d, []byte("CREATE TABLE t (Qty int, Name string);")) 35 | } 36 | 37 | func TestMigrate(t *testing.T) { 38 | dir := t.TempDir() 39 | t.Logf("DB path : %s\n", filepath.Join(dir, "ql.db")) 40 | 41 | db, err := sql.Open("ql", filepath.Join(dir, "ql.db")) 42 | if err != nil { 43 | return 44 | } 45 | defer func() { 46 | if err := db.Close(); err != nil { 47 | return 48 | } 49 | }() 50 | 51 | driver, err := WithInstance(db, &Config{}) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | 56 | m, err := migrate.NewWithDatabaseInstance( 57 | "file://./examples/migrations", 58 | "ql", driver) 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | dt.TestMigrate(t, m) 63 | } 64 | -------------------------------------------------------------------------------- /database/redshift/README.md: -------------------------------------------------------------------------------- 1 | # Redshift 2 | 3 | `redshift://user:password@host:port/dbname?query` 4 | 5 | | URL Query | WithInstance Config | Description | 6 | |------------|---------------------|-------------| 7 | | `x-migrations-table` | `MigrationsTable` | Name of the migrations table | 8 | | `dbname` | `DatabaseName` | The name of the database to connect to | 9 | | `search_path` | | This variable specifies the order in which schemas are searched when an object is referenced by a simple name with no schema specified. | 10 | | `user` | | The user to sign in as | 11 | | `password` | | The user's password | 12 | | `host` | | The host to connect to. Values that start with / are for unix domain sockets. (default is localhost) | 13 | | `port` | | The port to bind to. (default is 5439) | 14 | | `fallback_application_name` | | An application_name to fall back to if one isn't provided. | 15 | | `connect_timeout` | | Maximum wait for connection, in seconds. Zero or not specified means wait indefinitely. | 16 | | `sslcert` | | Cert file location. The file must contain PEM encoded data. | 17 | | `sslkey` | | Key file location. The file must contain PEM encoded data. | 18 | | `sslrootcert` | | The location of the root certificate file. The file must contain PEM encoded data. | 19 | | `sslmode` | | Whether or not to use SSL (disable\|require\|verify-ca\|verify-full) | 20 | 21 | Redshift is PostgreSQL compatible but has some specific features (or lack thereof) that require slightly different behavior. 22 | -------------------------------------------------------------------------------- /database/redshift/examples/migrations/1085649617_create_users_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS users; 2 | -------------------------------------------------------------------------------- /database/redshift/examples/migrations/1085649617_create_users_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users ( 2 | user_id integer unique, 3 | name varchar(40), 4 | email varchar(40) 5 | ); 6 | -------------------------------------------------------------------------------- /database/redshift/examples/migrations/1185749658_add_city_to_users.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users DROP COLUMN IF EXISTS city; 2 | -------------------------------------------------------------------------------- /database/redshift/examples/migrations/1185749658_add_city_to_users.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users ADD COLUMN city varchar(100); 2 | 3 | 4 | -------------------------------------------------------------------------------- /database/redshift/examples/migrations/1285849751_add_index_on_user_emails.down.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX IF EXISTS users_email_index; 2 | -------------------------------------------------------------------------------- /database/redshift/examples/migrations/1285849751_add_index_on_user_emails.up.sql: -------------------------------------------------------------------------------- 1 | CREATE UNIQUE INDEX CONCURRENTLY users_email_index ON users (email); 2 | 3 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 4 | -------------------------------------------------------------------------------- /database/redshift/examples/migrations/1385949617_create_books_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS books; 2 | -------------------------------------------------------------------------------- /database/redshift/examples/migrations/1385949617_create_books_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE books ( 2 | user_id integer, 3 | name varchar(40), 4 | author varchar(40) 5 | ); 6 | -------------------------------------------------------------------------------- /database/redshift/examples/migrations/1485949617_create_movies_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS movies; 2 | -------------------------------------------------------------------------------- /database/redshift/examples/migrations/1485949617_create_movies_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE movies ( 2 | user_id integer, 3 | name varchar(40), 4 | director varchar(40) 5 | ); 6 | -------------------------------------------------------------------------------- /database/redshift/examples/migrations/1585849751_just_a_comment.up.sql: -------------------------------------------------------------------------------- 1 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 2 | -------------------------------------------------------------------------------- /database/redshift/examples/migrations/1685849751_another_comment.up.sql: -------------------------------------------------------------------------------- 1 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 2 | -------------------------------------------------------------------------------- /database/redshift/examples/migrations/1785849751_another_comment.up.sql: -------------------------------------------------------------------------------- 1 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 2 | -------------------------------------------------------------------------------- /database/redshift/examples/migrations/1885849751_another_comment.up.sql: -------------------------------------------------------------------------------- 1 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 2 | -------------------------------------------------------------------------------- /database/rqlite/README.md: -------------------------------------------------------------------------------- 1 | # rqlite 2 | 3 | `rqlite://admin:secret@server1.example.com:4001/?level=strong&timeout=5` 4 | 5 | The `rqlite` url scheme is used for both secure and insecure connections. If connecting to an insecure database, pass `x-connect-insecure` in your URL query, or use `WithInstance` to pass an established connection. 6 | 7 | The migrations table name is configurable through the `x-migrations-table` URL query parameter, or by using `WithInstance` and passing `MigrationsTable` through `Config`. 8 | 9 | Other connect parameters are directly passed through to the database driver. For examples of connection strings, see https://github.com/rqlite/gorqlite#examples. 10 | 11 | | URL Query | WithInstance Config | Description | 12 | |------------|---------------------|-------------| 13 | | `x-connect-insecure` | n/a: set on instance | Boolean to indicate whether to use an insecure connection. Defaults to `false`. | 14 | | `x-migrations-table` | `MigrationsTable` | Name of the migrations table. Defaults to `schema_migrations`. | 15 | 16 | ## Notes 17 | 18 | * Uses the https://github.com/rqlite/gorqlite driver 19 | -------------------------------------------------------------------------------- /database/rqlite/examples/migrations/33_create_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS pets; -------------------------------------------------------------------------------- /database/rqlite/examples/migrations/33_create_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE pets ( 2 | name string 3 | ); -------------------------------------------------------------------------------- /database/rqlite/examples/migrations/44_alter_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS pets; -------------------------------------------------------------------------------- /database/rqlite/examples/migrations/44_alter_table.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE pets ADD predator bool; 2 | -------------------------------------------------------------------------------- /database/shell/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golang-migrate/migrate/278833935c12dda022b1355f33a897d895501c45/database/shell/README.md -------------------------------------------------------------------------------- /database/snowflake/README.md: -------------------------------------------------------------------------------- 1 | # Snowflake 2 | 3 | `snowflake://user:password@accountname/schema/dbname?query` 4 | 5 | | URL Query | WithInstance Config | Description | 6 | |------------|---------------------|-------------| 7 | | `x-migrations-table` | `MigrationsTable` | Name of the migrations table | 8 | 9 | Snowflake is PostgreSQL compatible but has some specific features (or lack thereof) that require slightly different behavior. 10 | 11 | ## Status 12 | This driver is not officially supported as there are no tests for it. 13 | -------------------------------------------------------------------------------- /database/spanner/README.md: -------------------------------------------------------------------------------- 1 | # Google Cloud Spanner 2 | 3 | ## Usage 4 | 5 | See [Google Spanner Documentation](https://cloud.google.com/spanner/docs) for 6 | more details. 7 | 8 | The DSN must be given in the following format. 9 | 10 | `spanner://projects/{projectId}/instances/{instanceId}/databases/{databaseName}?param=true` 11 | 12 | as described in [README.md#database-urls](../../README.md#database-urls) 13 | 14 | | Param | WithInstance Config | Description | 15 | | ----- | ------------------- | ----------- | 16 | | `x-migrations-table` | `MigrationsTable` | Name of the migrations table | 17 | | `x-clean-statements` | `CleanStatements` | Whether to parse and clean DDL statements before running migration towards Spanner (Required for comments and multiple statements) | 18 | | `url` | `DatabaseName` | The full path to the Spanner database resource. If provided as part of `Config` it must not contain a scheme or query string to match the format `projects/{projectId}/instances/{instanceId}/databases/{databaseName}`| 19 | | `projectId` || The Google Cloud Platform project id 20 | | `instanceId` || The id of the instance running Spanner 21 | | `databaseName` || The name of the Spanner database 22 | 23 | > **Note:** Google Cloud Spanner migrations can take a considerable amount of 24 | > time. The migrations provided as part of the example take about 6 minutes to 25 | > run on a small instance. 26 | > 27 | > ```log 28 | > 1481574547/u create_users_table (21.354507597s) 29 | > 1496539702/u add_city_to_users (41.647359754s) 30 | > 1496601752/u add_index_on_user_emails (2m12.155787369s) 31 | > 1496602638/u create_books_table (2m30.77299181s) 32 | 33 | ## DDL with comments 34 | 35 | At the moment the GCP Spanner backed does not seem to allow for comments (See https://issuetracker.google.com/issues/159730604) 36 | so in order to be able to use migration with DDL containing comments `x-clean-statements` is required 37 | 38 | ## Multiple statements 39 | 40 | In order to be able to use more than 1 DDL statement in the same migration file, the file has to be parsed and therefore the `x-clean-statements` flag is required 41 | 42 | ## Testing 43 | 44 | To unit test the `spanner` driver, `SPANNER_DATABASE` needs to be set. You'll 45 | need to sign-up to Google Cloud Platform (GCP) and have a running Spanner 46 | instance since it is not possible to run Google Spanner outside GCP. 47 | -------------------------------------------------------------------------------- /database/spanner/examples/migrations/1481574547_create_users_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE Users 2 | -------------------------------------------------------------------------------- /database/spanner/examples/migrations/1481574547_create_users_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE Users ( 2 | UserId INT64, 3 | Name STRING(40), 4 | Email STRING(83) 5 | ) PRIMARY KEY(UserId) -------------------------------------------------------------------------------- /database/spanner/examples/migrations/1496539702_add_city_to_users.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE Users DROP COLUMN city -------------------------------------------------------------------------------- /database/spanner/examples/migrations/1496539702_add_city_to_users.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE Users ADD COLUMN city STRING(100) -------------------------------------------------------------------------------- /database/spanner/examples/migrations/1496601752_add_index_on_user_emails.down.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX UsersEmailIndex 2 | -------------------------------------------------------------------------------- /database/spanner/examples/migrations/1496601752_add_index_on_user_emails.up.sql: -------------------------------------------------------------------------------- 1 | CREATE UNIQUE INDEX UsersEmailIndex ON Users (Email) 2 | -------------------------------------------------------------------------------- /database/spanner/examples/migrations/1496602638_create_books_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE Books -------------------------------------------------------------------------------- /database/spanner/examples/migrations/1496602638_create_books_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE Books ( 2 | UserId INT64, 3 | Name STRING(40), 4 | Author STRING(40) 5 | ) PRIMARY KEY(UserId, Name), 6 | INTERLEAVE IN PARENT Users ON DELETE CASCADE 7 | -------------------------------------------------------------------------------- /database/spanner/examples/migrations/1621360367_create_transactions_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE Transactions 2 | -------------------------------------------------------------------------------- /database/spanner/examples/migrations/1621360367_create_transactions_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE Transactions ( 2 | UserId INT64, 3 | TransactionId STRING(40), 4 | Total NUMERIC 5 | ) PRIMARY KEY(UserId, TransactionId), 6 | INTERLEAVE IN PARENT Users ON DELETE CASCADE 7 | -------------------------------------------------------------------------------- /database/sqlcipher/README.md: -------------------------------------------------------------------------------- 1 | # sqlcipher 2 | 3 | This is just a copy of the [sqlite3](https://github.com/golang-migrate/migrate/blob/master/database/sqlite3) driver except that it imports `github.com/mutecomm/go-sqlcipher`. -------------------------------------------------------------------------------- /database/sqlcipher/examples/migrations/33_create_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS pets; -------------------------------------------------------------------------------- /database/sqlcipher/examples/migrations/33_create_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE pets ( 2 | name string 3 | ); -------------------------------------------------------------------------------- /database/sqlcipher/examples/migrations/44_alter_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS pets; -------------------------------------------------------------------------------- /database/sqlcipher/examples/migrations/44_alter_table.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE pets ADD predator bool; 2 | -------------------------------------------------------------------------------- /database/sqlcipher/sqlcipher_test.go: -------------------------------------------------------------------------------- 1 | package sqlcipher 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | 11 | "github.com/golang-migrate/migrate/v4" 12 | dt "github.com/golang-migrate/migrate/v4/database/testing" 13 | _ "github.com/golang-migrate/migrate/v4/source/file" 14 | _ "github.com/mutecomm/go-sqlcipher/v4" 15 | ) 16 | 17 | func Test(t *testing.T) { 18 | dir := t.TempDir() 19 | t.Logf("DB path : %s\n", filepath.Join(dir, "sqlite3.db")) 20 | p := &Sqlite{} 21 | addr := fmt.Sprintf("sqlite3://%s", filepath.Join(dir, "sqlite3.db")) 22 | d, err := p.Open(addr) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | dt.Test(t, d, []byte("CREATE TABLE t (Qty int, Name string);")) 27 | } 28 | 29 | func TestMigrate(t *testing.T) { 30 | dir := t.TempDir() 31 | t.Logf("DB path : %s\n", filepath.Join(dir, "sqlite3.db")) 32 | 33 | db, err := sql.Open("sqlite3", filepath.Join(dir, "sqlite3.db")) 34 | if err != nil { 35 | return 36 | } 37 | defer func() { 38 | if err := db.Close(); err != nil { 39 | return 40 | } 41 | }() 42 | driver, err := WithInstance(db, &Config{}) 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | 47 | m, err := migrate.NewWithDatabaseInstance( 48 | "file://./examples/migrations", 49 | "ql", driver) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | dt.TestMigrate(t, m) 54 | } 55 | 56 | func TestMigrationTable(t *testing.T) { 57 | dir := t.TempDir() 58 | 59 | t.Logf("DB path : %s\n", filepath.Join(dir, "sqlite3.db")) 60 | 61 | db, err := sql.Open("sqlite3", filepath.Join(dir, "sqlite3.db")) 62 | if err != nil { 63 | return 64 | } 65 | defer func() { 66 | if err := db.Close(); err != nil { 67 | return 68 | } 69 | }() 70 | 71 | config := &Config{ 72 | MigrationsTable: "my_migration_table", 73 | } 74 | driver, err := WithInstance(db, config) 75 | if err != nil { 76 | t.Fatal(err) 77 | } 78 | m, err := migrate.NewWithDatabaseInstance( 79 | "file://./examples/migrations", 80 | "ql", driver) 81 | if err != nil { 82 | t.Fatal(err) 83 | } 84 | t.Log("UP") 85 | err = m.Up() 86 | if err != nil { 87 | t.Fatal(err) 88 | } 89 | 90 | _, err = db.Query(fmt.Sprintf("SELECT * FROM %s", config.MigrationsTable)) 91 | if err != nil { 92 | t.Fatal(err) 93 | } 94 | } 95 | 96 | func TestNoTxWrap(t *testing.T) { 97 | dir := t.TempDir() 98 | t.Logf("DB path : %s\n", filepath.Join(dir, "sqlite3.db")) 99 | p := &Sqlite{} 100 | addr := fmt.Sprintf("sqlite3://%s?x-no-tx-wrap=true", filepath.Join(dir, "sqlite3.db")) 101 | d, err := p.Open(addr) 102 | if err != nil { 103 | t.Fatal(err) 104 | } 105 | // An explicit BEGIN statement would ordinarily fail without x-no-tx-wrap. 106 | // (Transactions in sqlite may not be nested.) 107 | dt.Test(t, d, []byte("BEGIN; CREATE TABLE t (Qty int, Name string); COMMIT;")) 108 | } 109 | 110 | func TestNoTxWrapInvalidValue(t *testing.T) { 111 | dir := t.TempDir() 112 | t.Logf("DB path : %s\n", filepath.Join(dir, "sqlite3.db")) 113 | p := &Sqlite{} 114 | addr := fmt.Sprintf("sqlite3://%s?x-no-tx-wrap=yeppers", filepath.Join(dir, "sqlite3.db")) 115 | _, err := p.Open(addr) 116 | if assert.Error(t, err) { 117 | assert.Contains(t, err.Error(), "x-no-tx-wrap") 118 | assert.Contains(t, err.Error(), "invalid syntax") 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /database/sqlite/README.md: -------------------------------------------------------------------------------- 1 | # sqlite 2 | 3 | `sqlite://path/to/database?query` 4 | 5 | Unlike other migrate database drivers, the sqlite driver will automatically wrap each migration in an implicit transaction by default. Migrations must not contain explicit `BEGIN` or `COMMIT` statements. This behavior may change in a future major release. (See below for a workaround.) 6 | 7 | The auxiliary query parameters listed below may be supplied to tailor migrate behavior. All auxiliary query parameters are optional. 8 | 9 | | URL Query | WithInstance Config | Description | 10 | |------------|---------------------|-------------| 11 | | `x-migrations-table` | `MigrationsTable` | Name of the migrations table. Defaults to `schema_migrations`. | 12 | | `x-no-tx-wrap` | `NoTxWrap` | Disable implicit transactions when `true`. Migrations may, and should, contain explicit `BEGIN` and `COMMIT` statements. | 13 | 14 | ## Notes 15 | 16 | * Uses the `modernc.org/sqlite` sqlite db driver (pure Go) 17 | * Has [limited `GOOS` and `GOARCH` support](https://pkg.go.dev/modernc.org/sqlite?utm_source=godoc#hdr-Supported_platforms_and_architectures) 18 | -------------------------------------------------------------------------------- /database/sqlite/examples/migrations/33_create_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS pets; -------------------------------------------------------------------------------- /database/sqlite/examples/migrations/33_create_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE pets ( 2 | name string 3 | ); -------------------------------------------------------------------------------- /database/sqlite/examples/migrations/44_alter_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS pets; -------------------------------------------------------------------------------- /database/sqlite/examples/migrations/44_alter_table.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE pets ADD predator bool; 2 | -------------------------------------------------------------------------------- /database/sqlite/sqlite_test.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | 11 | "github.com/golang-migrate/migrate/v4" 12 | dt "github.com/golang-migrate/migrate/v4/database/testing" 13 | _ "github.com/golang-migrate/migrate/v4/source/file" 14 | _ "modernc.org/sqlite" 15 | ) 16 | 17 | func Test(t *testing.T) { 18 | dir := t.TempDir() 19 | t.Logf("DB path : %s\n", filepath.Join(dir, "sqlite.db")) 20 | p := &Sqlite{} 21 | addr := fmt.Sprintf("sqlite://%s", filepath.Join(dir, "sqlite.db")) 22 | d, err := p.Open(addr) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | dt.Test(t, d, []byte("CREATE TABLE t (Qty int, Name string);")) 27 | } 28 | 29 | func TestMigrate(t *testing.T) { 30 | dir := t.TempDir() 31 | t.Logf("DB path : %s\n", filepath.Join(dir, "sqlite.db")) 32 | 33 | db, err := sql.Open("sqlite", filepath.Join(dir, "sqlite.db")) 34 | if err != nil { 35 | return 36 | } 37 | defer func() { 38 | if err := db.Close(); err != nil { 39 | return 40 | } 41 | }() 42 | driver, err := WithInstance(db, &Config{}) 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | 47 | m, err := migrate.NewWithDatabaseInstance( 48 | "file://./examples/migrations", 49 | "ql", driver) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | dt.TestMigrate(t, m) 54 | } 55 | 56 | func TestMigrationTable(t *testing.T) { 57 | dir := t.TempDir() 58 | 59 | t.Logf("DB path : %s\n", filepath.Join(dir, "sqlite.db")) 60 | 61 | db, err := sql.Open("sqlite", filepath.Join(dir, "sqlite.db")) 62 | if err != nil { 63 | return 64 | } 65 | defer func() { 66 | if err := db.Close(); err != nil { 67 | return 68 | } 69 | }() 70 | 71 | config := &Config{ 72 | MigrationsTable: "my_migration_table", 73 | } 74 | driver, err := WithInstance(db, config) 75 | if err != nil { 76 | t.Fatal(err) 77 | } 78 | m, err := migrate.NewWithDatabaseInstance( 79 | "file://./examples/migrations", 80 | "ql", driver) 81 | if err != nil { 82 | t.Fatal(err) 83 | } 84 | t.Log("UP") 85 | err = m.Up() 86 | if err != nil { 87 | t.Fatal(err) 88 | } 89 | 90 | _, err = db.Query(fmt.Sprintf("SELECT * FROM %s", config.MigrationsTable)) 91 | if err != nil { 92 | t.Fatal(err) 93 | } 94 | } 95 | 96 | func TestNoTxWrap(t *testing.T) { 97 | dir := t.TempDir() 98 | t.Logf("DB path : %s\n", filepath.Join(dir, "sqlite.db")) 99 | p := &Sqlite{} 100 | addr := fmt.Sprintf("sqlite://%s?x-no-tx-wrap=true", filepath.Join(dir, "sqlite.db")) 101 | d, err := p.Open(addr) 102 | if err != nil { 103 | t.Fatal(err) 104 | } 105 | // An explicit BEGIN statement would ordinarily fail without x-no-tx-wrap. 106 | // (Transactions in sqlite may not be nested.) 107 | dt.Test(t, d, []byte("BEGIN; CREATE TABLE t (Qty int, Name string); COMMIT;")) 108 | } 109 | 110 | func TestNoTxWrapInvalidValue(t *testing.T) { 111 | dir := t.TempDir() 112 | t.Logf("DB path : %s\n", filepath.Join(dir, "sqlite.db")) 113 | p := &Sqlite{} 114 | addr := fmt.Sprintf("sqlite://%s?x-no-tx-wrap=yeppers", filepath.Join(dir, "sqlite.db")) 115 | _, err := p.Open(addr) 116 | if assert.Error(t, err) { 117 | assert.Contains(t, err.Error(), "x-no-tx-wrap") 118 | assert.Contains(t, err.Error(), "invalid syntax") 119 | } 120 | } 121 | 122 | func TestMigrateWithDirectoryNameContainsWhitespaces(t *testing.T) { 123 | dir := t.TempDir() 124 | dbPath := filepath.Join(dir, "sqlite.db") 125 | t.Logf("DB path : %s\n", dbPath) 126 | p := &Sqlite{} 127 | addr := fmt.Sprintf("sqlite://file:%s", dbPath) 128 | d, err := p.Open(addr) 129 | if err != nil { 130 | t.Fatal(err) 131 | } 132 | dt.Test(t, d, []byte("CREATE TABLE t (Qty int, Name string);")) 133 | } 134 | -------------------------------------------------------------------------------- /database/sqlite3/README.md: -------------------------------------------------------------------------------- 1 | # sqlite3 2 | 3 | `sqlite3://path/to/database?query` 4 | 5 | Unlike other migrate database drivers, the sqlite3 driver will automatically wrap each migration in an implicit transaction by default. Migrations must not contain explicit `BEGIN` or `COMMIT` statements. This behavior may change in a future major release. (See below for a workaround.) 6 | 7 | Refer to [upstream documentation](https://github.com/mattn/go-sqlite3/blob/master/README.md#connection-string) for a complete list of query parameters supported by the sqlite3 database driver. The auxiliary query parameters listed below may be supplied to tailor migrate behavior. All auxiliary query parameters are optional. 8 | 9 | | URL Query | WithInstance Config | Description | 10 | |------------|---------------------|-------------| 11 | | `x-migrations-table` | `MigrationsTable` | Name of the migrations table. Defaults to `schema_migrations`. | 12 | | `x-no-tx-wrap` | `NoTxWrap` | Disable implicit transactions when `true`. Migrations may, and should, contain explicit `BEGIN` and `COMMIT` statements. | 13 | 14 | ## Notes 15 | 16 | * Uses the `github.com/mattn/go-sqlite3` sqlite db driver (cgo) 17 | -------------------------------------------------------------------------------- /database/sqlite3/examples/migrations/33_create_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS pets; -------------------------------------------------------------------------------- /database/sqlite3/examples/migrations/33_create_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE pets ( 2 | name string 3 | ); -------------------------------------------------------------------------------- /database/sqlite3/examples/migrations/44_alter_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS pets; -------------------------------------------------------------------------------- /database/sqlite3/examples/migrations/44_alter_table.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE pets ADD predator bool; 2 | -------------------------------------------------------------------------------- /database/sqlserver/README.md: -------------------------------------------------------------------------------- 1 | # Microsoft SQL Server 2 | 3 | `sqlserver://username:password@host/instance?param1=value¶m2=value` 4 | `sqlserver://username:password@host:port?param1=value¶m2=value` 5 | 6 | | URL Query | WithInstance Config | Description | 7 | |------------|---------------------|-------------| 8 | | `x-migrations-table` | `MigrationsTable` | Name of the migrations table | 9 | | `username` | | enter the SQL Server Authentication user id or the Windows Authentication user id in the DOMAIN\User format. On Windows, if user id is empty or missing Single-Sign-On is used. | 10 | | `password` | | The user's password. | 11 | | `host` | | The host to connect to. | 12 | | `port` | | The port to connect to. | 13 | | `instance` | | SQL Server instance name. | 14 | | `database` | `DatabaseName` | The name of the database to connect to | 15 | | `connection+timeout` | | in seconds (default is 0 for no timeout), set to 0 for no timeout. | 16 | | `dial+timeout` | | in seconds (default is 15), set to 0 for no timeout. | 17 | | `encrypt` | | `disable` - Data send between client and server is not encrypted. `false` - Data sent between client and server is not encrypted beyond the login packet (Default). `true` - Data sent between client and server is encrypted. | 18 | | `app+name` || The application name (default is go-mssqldb). | 19 | | `useMsi` | | `true` - Use Azure MSI Authentication for connecting to Sql Server. Must be running from an Azure VM/an instance with MSI enabled. `false` - Use password authentication (Default). See [here for Azure MSI Auth details](https://docs.microsoft.com/en-us/azure/app-service/app-service-web-tutorial-connect-msi). NOTE: Since this cannot be tested locally, this is not officially supported. 20 | 21 | See https://github.com/microsoft/go-mssqldb for full parameter list. 22 | 23 | ## Driver Support 24 | 25 | ### Which go-mssqldb driver to us? 26 | 27 | Please note that the deprecated `mssql` driver is not supported. Please use the newer `sqlserver` driver. 28 | See https://github.com/microsoft/go-mssqldb#deprecated for more information. 29 | 30 | ### Official Support by migrate 31 | 32 | Versions of MS SQL Server 2019 newer than CTP3.1 are not officially supported since there are issues testing against the Docker image. 33 | For more info, see: https://github.com/golang-migrate/migrate/issues/160#issuecomment-522433269 34 | -------------------------------------------------------------------------------- /database/sqlserver/examples/migrations/1085649617_create_users_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS users; 2 | -------------------------------------------------------------------------------- /database/sqlserver/examples/migrations/1085649617_create_users_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users ( 2 | user_id integer unique, 3 | name varchar(40), 4 | email varchar(40) 5 | ); 6 | -------------------------------------------------------------------------------- /database/sqlserver/examples/migrations/1185749658_add_city_to_users.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users DROP COLUMN IF EXISTS city; 2 | -------------------------------------------------------------------------------- /database/sqlserver/examples/migrations/1185749658_add_city_to_users.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users ADD city varchar(100); 2 | 3 | 4 | -------------------------------------------------------------------------------- /database/sqlserver/examples/migrations/1285849751_add_index_on_user_emails.down.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX IF EXISTS users_email_index; 2 | -------------------------------------------------------------------------------- /database/sqlserver/examples/migrations/1285849751_add_index_on_user_emails.up.sql: -------------------------------------------------------------------------------- 1 | CREATE UNIQUE INDEX users_email_index ON users (email); 2 | 3 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 4 | -------------------------------------------------------------------------------- /database/sqlserver/examples/migrations/1385949617_create_books_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS books; 2 | -------------------------------------------------------------------------------- /database/sqlserver/examples/migrations/1385949617_create_books_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE books ( 2 | user_id integer, 3 | name varchar(40), 4 | author varchar(40) 5 | ); 6 | -------------------------------------------------------------------------------- /database/sqlserver/examples/migrations/1485949617_create_movies_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS movies; 2 | -------------------------------------------------------------------------------- /database/sqlserver/examples/migrations/1485949617_create_movies_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE movies ( 2 | user_id integer, 3 | name varchar(40), 4 | director varchar(40) 5 | ); 6 | -------------------------------------------------------------------------------- /database/sqlserver/examples/migrations/1585849751_just_a_comment.up.sql: -------------------------------------------------------------------------------- 1 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 2 | -------------------------------------------------------------------------------- /database/sqlserver/examples/migrations/1685849751_another_comment.up.sql: -------------------------------------------------------------------------------- 1 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 2 | -------------------------------------------------------------------------------- /database/sqlserver/examples/migrations/1785849751_another_comment.up.sql: -------------------------------------------------------------------------------- 1 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 2 | -------------------------------------------------------------------------------- /database/sqlserver/examples/migrations/1885849751_another_comment.up.sql: -------------------------------------------------------------------------------- 1 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 2 | -------------------------------------------------------------------------------- /database/stub/stub.go: -------------------------------------------------------------------------------- 1 | package stub 2 | 3 | import ( 4 | "io" 5 | "reflect" 6 | 7 | "go.uber.org/atomic" 8 | 9 | "github.com/golang-migrate/migrate/v4/database" 10 | ) 11 | 12 | func init() { 13 | database.Register("stub", &Stub{}) 14 | } 15 | 16 | type Stub struct { 17 | Url string 18 | Instance interface{} 19 | CurrentVersion int 20 | MigrationSequence []string 21 | LastRunMigration []byte // todo: make []string 22 | IsDirty bool 23 | isLocked atomic.Bool 24 | 25 | Config *Config 26 | } 27 | 28 | func (s *Stub) Open(url string) (database.Driver, error) { 29 | return &Stub{ 30 | Url: url, 31 | CurrentVersion: database.NilVersion, 32 | MigrationSequence: make([]string, 0), 33 | Config: &Config{}, 34 | }, nil 35 | } 36 | 37 | type Config struct{} 38 | 39 | func WithInstance(instance interface{}, config *Config) (database.Driver, error) { 40 | return &Stub{ 41 | Instance: instance, 42 | CurrentVersion: database.NilVersion, 43 | MigrationSequence: make([]string, 0), 44 | Config: config, 45 | }, nil 46 | } 47 | 48 | func (s *Stub) Close() error { 49 | return nil 50 | } 51 | 52 | func (s *Stub) Lock() error { 53 | if !s.isLocked.CAS(false, true) { 54 | return database.ErrLocked 55 | } 56 | return nil 57 | } 58 | 59 | func (s *Stub) Unlock() error { 60 | if !s.isLocked.CAS(true, false) { 61 | return database.ErrNotLocked 62 | } 63 | return nil 64 | } 65 | 66 | func (s *Stub) Run(migration io.Reader) error { 67 | m, err := io.ReadAll(migration) 68 | if err != nil { 69 | return err 70 | } 71 | s.LastRunMigration = m 72 | s.MigrationSequence = append(s.MigrationSequence, string(m[:])) 73 | return nil 74 | } 75 | 76 | func (s *Stub) SetVersion(version int, state bool) error { 77 | s.CurrentVersion = version 78 | s.IsDirty = state 79 | return nil 80 | } 81 | 82 | func (s *Stub) Version() (version int, dirty bool, err error) { 83 | return s.CurrentVersion, s.IsDirty, nil 84 | } 85 | 86 | const DROP = "DROP" 87 | 88 | func (s *Stub) Drop() error { 89 | s.CurrentVersion = database.NilVersion 90 | s.LastRunMigration = nil 91 | s.MigrationSequence = append(s.MigrationSequence, DROP) 92 | return nil 93 | } 94 | 95 | func (s *Stub) EqualSequence(seq []string) bool { 96 | return reflect.DeepEqual(seq, s.MigrationSequence) 97 | } 98 | -------------------------------------------------------------------------------- /database/stub/stub_test.go: -------------------------------------------------------------------------------- 1 | package stub 2 | 3 | import ( 4 | "github.com/golang-migrate/migrate/v4" 5 | "github.com/golang-migrate/migrate/v4/source" 6 | "github.com/golang-migrate/migrate/v4/source/stub" 7 | "testing" 8 | 9 | dt "github.com/golang-migrate/migrate/v4/database/testing" 10 | ) 11 | 12 | func Test(t *testing.T) { 13 | s := &Stub{} 14 | d, err := s.Open("") 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | dt.Test(t, d, []byte("/* foobar migration */")) 19 | } 20 | 21 | func TestMigrate(t *testing.T) { 22 | s := &Stub{} 23 | d, err := s.Open("") 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | stubMigrations := source.NewMigrations() 29 | stubMigrations.Append(&source.Migration{Version: 1, Direction: source.Up, Identifier: "CREATE 1"}) 30 | stubMigrations.Append(&source.Migration{Version: 1, Direction: source.Down, Identifier: "DROP 1"}) 31 | src := &stub.Stub{} 32 | srcDrv, err := src.Open("") 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | srcDrv.(*stub.Stub).Migrations = stubMigrations 37 | m, err := migrate.NewWithInstance("stub", srcDrv, "", d) 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | 42 | dt.TestMigrate(t, m) 43 | } 44 | -------------------------------------------------------------------------------- /database/testing/migrate_testing.go: -------------------------------------------------------------------------------- 1 | // Package testing has the database tests. 2 | // All database drivers must pass the Test function. 3 | // This lives in it's own package so it stays a test dependency. 4 | package testing 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | import ( 11 | "github.com/golang-migrate/migrate/v4" 12 | ) 13 | 14 | // TestMigrate runs integration-tests between the Migrate layer and database implementations. 15 | func TestMigrate(t *testing.T, m *migrate.Migrate) { 16 | TestMigrateUp(t, m) 17 | TestMigrateDrop(t, m) 18 | } 19 | 20 | // Regression test for preventing a regression for #164 https://github.com/golang-migrate/migrate/pull/173 21 | // Similar to TestDrop(), but tests the dropping mechanism through the Migrate logic instead, to check for 22 | // double-locking during the Drop logic. 23 | func TestMigrateDrop(t *testing.T, m *migrate.Migrate) { 24 | if err := m.Drop(); err != nil { 25 | t.Fatal(err) 26 | } 27 | } 28 | 29 | func TestMigrateUp(t *testing.T, m *migrate.Migrate) { 30 | t.Log("UP") 31 | if err := m.Up(); err != nil { 32 | t.Fatal(err) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/util.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "fmt" 5 | "go.uber.org/atomic" 6 | "hash/crc32" 7 | "strings" 8 | ) 9 | 10 | const advisoryLockIDSalt uint = 1486364155 11 | 12 | // GenerateAdvisoryLockId inspired by rails migrations, see https://goo.gl/8o9bCT 13 | func GenerateAdvisoryLockId(databaseName string, additionalNames ...string) (string, error) { // nolint: golint 14 | if len(additionalNames) > 0 { 15 | databaseName = strings.Join(append(additionalNames, databaseName), "\x00") 16 | } 17 | sum := crc32.ChecksumIEEE([]byte(databaseName)) 18 | sum = sum * uint32(advisoryLockIDSalt) 19 | return fmt.Sprint(sum), nil 20 | } 21 | 22 | // CasRestoreOnErr CAS wrapper to automatically restore the lock state on error 23 | func CasRestoreOnErr(lock *atomic.Bool, o, n bool, casErr error, f func() error) error { 24 | if !lock.CAS(o, n) { 25 | return casErr 26 | } 27 | if err := f(); err != nil { 28 | // Automatically unlock/lock on error 29 | lock.Store(o) 30 | return err 31 | } 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /database/util_test.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "errors" 5 | "go.uber.org/atomic" 6 | "testing" 7 | ) 8 | 9 | func TestGenerateAdvisoryLockId(t *testing.T) { 10 | testcases := []struct { 11 | dbname string 12 | additional []string 13 | expectedID string // empty string signifies that an error is expected 14 | }{ 15 | { 16 | dbname: "database_name", 17 | expectedID: "1764327054", 18 | }, 19 | { 20 | dbname: "database_name", 21 | additional: []string{"schema_name_1"}, 22 | expectedID: "2453313553", 23 | }, 24 | { 25 | dbname: "database_name", 26 | additional: []string{"schema_name_2"}, 27 | expectedID: "235207038", 28 | }, 29 | { 30 | dbname: "database_name", 31 | additional: []string{"schema_name_1", "schema_name_2"}, 32 | expectedID: "3743845847", 33 | }, 34 | } 35 | 36 | for _, tc := range testcases { 37 | t.Run(tc.dbname, func(t *testing.T) { 38 | if id, err := GenerateAdvisoryLockId(tc.dbname, tc.additional...); err == nil { 39 | if id != tc.expectedID { 40 | t.Error("Generated incorrect ID:", id, "!=", tc.expectedID) 41 | } 42 | } else { 43 | if tc.expectedID != "" { 44 | t.Error("Got unexpected error:", err) 45 | } 46 | } 47 | }) 48 | } 49 | } 50 | 51 | func TestCasRestoreOnErr(t *testing.T) { 52 | casErr := errors.New("test lock CAS failure") 53 | fErr := errors.New("test callback error") 54 | 55 | testcases := []struct { 56 | name string 57 | lock *atomic.Bool 58 | from bool 59 | to bool 60 | expectLock bool 61 | fErr error 62 | expectError error 63 | }{ 64 | { 65 | name: "Test positive CAS lock", 66 | lock: atomic.NewBool(false), 67 | from: false, 68 | to: true, 69 | expectLock: true, 70 | fErr: nil, 71 | expectError: nil, 72 | }, 73 | { 74 | name: "Test negative CAS lock", 75 | lock: atomic.NewBool(true), 76 | from: false, 77 | to: true, 78 | expectLock: true, 79 | fErr: nil, 80 | expectError: casErr, 81 | }, 82 | { 83 | name: "Test negative with callback lock", 84 | lock: atomic.NewBool(false), 85 | from: false, 86 | to: true, 87 | expectLock: false, 88 | fErr: fErr, 89 | expectError: fErr, 90 | }, 91 | } 92 | 93 | for _, tc := range testcases { 94 | t.Run(tc.name, func(t *testing.T) { 95 | if err := CasRestoreOnErr(tc.lock, tc.from, tc.to, casErr, func() error { 96 | return tc.fErr 97 | }); err != tc.expectError { 98 | t.Error("Incorrect error value returned") 99 | } 100 | 101 | if tc.lock.Load() != tc.expectLock { 102 | t.Error("Incorrect state of lock") 103 | } 104 | }) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /database/yugabytedb/README.md: -------------------------------------------------------------------------------- 1 | # yugabytedb 2 | 3 | `yugabytedb://user:password@host:port/dbname?query` (`yugabyte://`, and `ysql://` work, too) 4 | 5 | | URL Query | WithInstance Config | Description | 6 | |------------|---------------------|-------------| 7 | | `x-migrations-table` | `MigrationsTable` | Name of the migrations table | 8 | | `x-lock-table` | `LockTable` | Name of the table which maintains the migration lock | 9 | | `x-force-lock` | `ForceLock` | Force lock acquisition to fix faulty migrations which may not have released the schema lock (Boolean, default is `false`) | 10 | | `x-max-retries` | `MaxRetries` | How many times retry queries on retryable errors (40001, 40P01, 08006, XX000). Default is 10 | 11 | | `x-max-retry-interval` | `MaxRetryInterval` | Interval between retries increases exponentially. This option specifies maximum duration between retries. Default is 15s | 12 | | `x-max-retry-elapsed-time` | `MaxRetryElapsedTime` | Total retries timeout. Default is 30s | 13 | | `dbname` | `DatabaseName` | The name of the database to connect to | 14 | | `user` | | The user to sign in as | 15 | | `password` | | The user's password | 16 | | `host` | | The host to connect to. Values that start with / are for unix domain sockets. (default is localhost) | 17 | | `port` | | The port to bind to. (default is 5432) | 18 | | `connect_timeout` | | Maximum wait for connection, in seconds. Zero or not specified means wait indefinitely. | 19 | | `sslcert` | | Cert file location. The file must contain PEM encoded data. | 20 | | `sslkey` | | Key file location. The file must contain PEM encoded data. | 21 | | `sslrootcert` | | The location of the root certificate file. The file must contain PEM encoded data. | 22 | | `sslmode` | | Whether or not to use SSL (disable\|require\|verify-ca\|verify-full) | 23 | -------------------------------------------------------------------------------- /database/yugabytedb/examples/migrations/1085649617_create_users_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS users; 2 | -------------------------------------------------------------------------------- /database/yugabytedb/examples/migrations/1085649617_create_users_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users ( 2 | user_id integer unique, 3 | name varchar(40), 4 | email varchar(40) 5 | ); 6 | -------------------------------------------------------------------------------- /database/yugabytedb/examples/migrations/1185749658_add_city_to_users.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users DROP COLUMN IF EXISTS city; 2 | -------------------------------------------------------------------------------- /database/yugabytedb/examples/migrations/1185749658_add_city_to_users.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users ADD COLUMN city varchar(100); 2 | 3 | 4 | -------------------------------------------------------------------------------- /database/yugabytedb/examples/migrations/1285849751_add_index_on_user_emails.down.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX IF EXISTS users_email_index; 2 | -------------------------------------------------------------------------------- /database/yugabytedb/examples/migrations/1285849751_add_index_on_user_emails.up.sql: -------------------------------------------------------------------------------- 1 | CREATE UNIQUE INDEX users_email_index ON users (email); 2 | 3 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 4 | -------------------------------------------------------------------------------- /database/yugabytedb/examples/migrations/1385949617_create_books_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS books; 2 | -------------------------------------------------------------------------------- /database/yugabytedb/examples/migrations/1385949617_create_books_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE books ( 2 | user_id integer, 3 | name varchar(40), 4 | author varchar(40) 5 | ); 6 | -------------------------------------------------------------------------------- /database/yugabytedb/examples/migrations/1485949617_create_movies_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS movies; 2 | -------------------------------------------------------------------------------- /database/yugabytedb/examples/migrations/1485949617_create_movies_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE movies ( 2 | user_id integer, 3 | name varchar(40), 4 | director varchar(40) 5 | ); 6 | -------------------------------------------------------------------------------- /database/yugabytedb/examples/migrations/1585849751_just_a_comment.up.sql: -------------------------------------------------------------------------------- 1 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 2 | -------------------------------------------------------------------------------- /database/yugabytedb/examples/migrations/1685849751_another_comment.up.sql: -------------------------------------------------------------------------------- 1 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 2 | -------------------------------------------------------------------------------- /database/yugabytedb/examples/migrations/1785849751_another_comment.up.sql: -------------------------------------------------------------------------------- 1 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 2 | -------------------------------------------------------------------------------- /database/yugabytedb/examples/migrations/1885849751_another_comment.up.sql: -------------------------------------------------------------------------------- 1 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 2 | -------------------------------------------------------------------------------- /dktesting/dktesting.go: -------------------------------------------------------------------------------- 1 | package dktesting 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/dhui/dktest" 9 | "github.com/docker/docker/api/types/image" 10 | "github.com/docker/docker/client" 11 | ) 12 | 13 | // ContainerSpec holds Docker testing setup specifications 14 | type ContainerSpec struct { 15 | ImageName string 16 | Options dktest.Options 17 | } 18 | 19 | // Cleanup cleanups the ContainerSpec after a test run by removing the ContainerSpec's image 20 | func (s *ContainerSpec) Cleanup() (retErr error) { 21 | // copied from dktest.RunContext() 22 | dc, err := client.NewClientWithOpts(client.FromEnv, client.WithVersion("1.41")) 23 | if err != nil { 24 | return err 25 | } 26 | defer func() { 27 | if err := dc.Close(); err != nil && retErr == nil { 28 | retErr = fmt.Errorf("error closing Docker client: %w", err) 29 | } 30 | }() 31 | cleanupTimeout := s.Options.CleanupTimeout 32 | if cleanupTimeout <= 0 { 33 | cleanupTimeout = dktest.DefaultCleanupTimeout 34 | } 35 | ctx, timeoutCancelFunc := context.WithTimeout(context.Background(), cleanupTimeout) 36 | defer timeoutCancelFunc() 37 | if _, err := dc.ImageRemove(ctx, s.ImageName, image.RemoveOptions{Force: true, PruneChildren: true}); err != nil { 38 | return err 39 | } 40 | return nil 41 | } 42 | 43 | // ParallelTest runs Docker tests in parallel 44 | func ParallelTest(t *testing.T, specs []ContainerSpec, 45 | testFunc func(*testing.T, dktest.ContainerInfo)) { 46 | 47 | for i, spec := range specs { 48 | spec := spec // capture range variable, see https://goo.gl/60w3p2 49 | 50 | // Only test against one version in short mode 51 | // TODO: order is random, maybe always pick first version instead? 52 | if i > 0 && testing.Short() { 53 | t.Logf("Skipping %v in short mode", spec.ImageName) 54 | } else { 55 | t.Run(spec.ImageName, func(t *testing.T) { 56 | t.Parallel() 57 | dktest.Run(t, spec.ImageName, spec.Options, testFunc) 58 | }) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /dktesting/example_test.go: -------------------------------------------------------------------------------- 1 | package dktesting_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | ) 7 | 8 | import ( 9 | "github.com/dhui/dktest" 10 | ) 11 | 12 | import ( 13 | "github.com/golang-migrate/migrate/v4/dktesting" 14 | ) 15 | 16 | func ExampleParallelTest() { 17 | t := &testing.T{} // Should actually be used in a Test 18 | 19 | var isReady = func(ctx context.Context, c dktest.ContainerInfo) bool { 20 | // Return true if the container is ready to run tests. 21 | // Don't block here though. Use the Context to timeout container ready checks. 22 | return true 23 | } 24 | 25 | dktesting.ParallelTest(t, []dktesting.ContainerSpec{{ImageName: "docker_image:9.6", 26 | Options: dktest.Options{ReadyFunc: isReady}}}, func(t *testing.T, c dktest.ContainerInfo) { 27 | // Run your test/s ... 28 | t.Fatal("...") 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /docker-deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin && \ 4 | docker build --build-arg VERSION="$TRAVIS_TAG" . -t migrate/migrate -t migrate/migrate:"$TRAVIS_TAG" && \ 5 | docker push migrate/migrate:"$TRAVIS_TAG" && docker push migrate/migrate 6 | -------------------------------------------------------------------------------- /internal/cli/build_aws-s3.go: -------------------------------------------------------------------------------- 1 | //go:build aws_s3 2 | 3 | package cli 4 | 5 | import ( 6 | _ "github.com/golang-migrate/migrate/v4/source/aws_s3" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/cli/build_bitbucket.go: -------------------------------------------------------------------------------- 1 | //go:build bitbucket 2 | 3 | package cli 4 | 5 | import ( 6 | _ "github.com/golang-migrate/migrate/v4/source/bitbucket" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/cli/build_cassandra.go: -------------------------------------------------------------------------------- 1 | //go:build cassandra 2 | 3 | package cli 4 | 5 | import ( 6 | _ "github.com/golang-migrate/migrate/v4/database/cassandra" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/cli/build_clickhouse.go: -------------------------------------------------------------------------------- 1 | //go:build clickhouse 2 | 3 | package cli 4 | 5 | import ( 6 | _ "github.com/ClickHouse/clickhouse-go" 7 | _ "github.com/golang-migrate/migrate/v4/database/clickhouse" 8 | ) 9 | -------------------------------------------------------------------------------- /internal/cli/build_cockroachdb.go: -------------------------------------------------------------------------------- 1 | //go:build cockroachdb 2 | 3 | package cli 4 | 5 | import ( 6 | _ "github.com/golang-migrate/migrate/v4/database/cockroachdb" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/cli/build_firebird.go: -------------------------------------------------------------------------------- 1 | //go:build firebird 2 | 3 | package cli 4 | 5 | import ( 6 | _ "github.com/golang-migrate/migrate/v4/database/firebird" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/cli/build_github.go: -------------------------------------------------------------------------------- 1 | //go:build github 2 | 3 | package cli 4 | 5 | import ( 6 | _ "github.com/golang-migrate/migrate/v4/source/github" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/cli/build_github_ee.go: -------------------------------------------------------------------------------- 1 | //go:build github 2 | 3 | package cli 4 | 5 | import ( 6 | _ "github.com/golang-migrate/migrate/v4/source/github_ee" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/cli/build_gitlab.go: -------------------------------------------------------------------------------- 1 | //go:build gitlab 2 | 3 | package cli 4 | 5 | import ( 6 | _ "github.com/golang-migrate/migrate/v4/source/gitlab" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/cli/build_go-bindata.go: -------------------------------------------------------------------------------- 1 | //go:build go_bindata 2 | 3 | package cli 4 | 5 | import ( 6 | _ "github.com/golang-migrate/migrate/v4/source/go_bindata" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/cli/build_godoc-vfs.go: -------------------------------------------------------------------------------- 1 | //go:build godoc_vfs 2 | 3 | package cli 4 | 5 | import ( 6 | _ "github.com/golang-migrate/migrate/v4/source/godoc_vfs" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/cli/build_google-cloud-storage.go: -------------------------------------------------------------------------------- 1 | //go:build google_cloud_storage 2 | 3 | package cli 4 | 5 | import ( 6 | _ "github.com/golang-migrate/migrate/v4/source/google_cloud_storage" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/cli/build_mongodb.go: -------------------------------------------------------------------------------- 1 | //go:build mongodb 2 | 3 | package cli 4 | 5 | import ( 6 | _ "github.com/golang-migrate/migrate/v4/database/mongodb" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/cli/build_mysql.go: -------------------------------------------------------------------------------- 1 | //go:build mysql 2 | 3 | package cli 4 | 5 | import ( 6 | _ "github.com/golang-migrate/migrate/v4/database/mysql" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/cli/build_neo4j.go: -------------------------------------------------------------------------------- 1 | //go:build neo4j 2 | 3 | package cli 4 | 5 | import ( 6 | _ "github.com/golang-migrate/migrate/v4/database/neo4j" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/cli/build_pgx.go: -------------------------------------------------------------------------------- 1 | //go:build pgx 2 | 3 | package cli 4 | 5 | import ( 6 | _ "github.com/golang-migrate/migrate/v4/database/pgx" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/cli/build_pgxv5.go: -------------------------------------------------------------------------------- 1 | //go:build pgx5 2 | 3 | package cli 4 | 5 | import ( 6 | _ "github.com/golang-migrate/migrate/v4/database/pgx/v5" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/cli/build_postgres.go: -------------------------------------------------------------------------------- 1 | //go:build postgres 2 | 3 | package cli 4 | 5 | import ( 6 | _ "github.com/golang-migrate/migrate/v4/database/postgres" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/cli/build_ql.go: -------------------------------------------------------------------------------- 1 | //go:build ql 2 | 3 | package cli 4 | 5 | import ( 6 | _ "github.com/golang-migrate/migrate/v4/database/ql" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/cli/build_redshift.go: -------------------------------------------------------------------------------- 1 | //go:build redshift 2 | 3 | package cli 4 | 5 | import ( 6 | _ "github.com/golang-migrate/migrate/v4/database/redshift" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/cli/build_rqlite.go: -------------------------------------------------------------------------------- 1 | //go:build rqlite 2 | 3 | package cli 4 | 5 | import ( 6 | _ "github.com/golang-migrate/migrate/v4/database/rqlite" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/cli/build_snowflake.go: -------------------------------------------------------------------------------- 1 | //go:build snowflake 2 | 3 | package cli 4 | 5 | import ( 6 | _ "github.com/golang-migrate/migrate/v4/database/snowflake" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/cli/build_spanner.go: -------------------------------------------------------------------------------- 1 | //go:build spanner 2 | 3 | package cli 4 | 5 | import ( 6 | _ "github.com/golang-migrate/migrate/v4/database/spanner" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/cli/build_sqlcipher.go: -------------------------------------------------------------------------------- 1 | //go:build sqlcipher 2 | 3 | package cli 4 | 5 | import ( 6 | _ "github.com/golang-migrate/migrate/v4/database/sqlcipher" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/cli/build_sqlite.go: -------------------------------------------------------------------------------- 1 | //go:build sqlite 2 | 3 | package cli 4 | 5 | import ( 6 | _ "github.com/golang-migrate/migrate/v4/database/sqlite" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/cli/build_sqlite3.go: -------------------------------------------------------------------------------- 1 | //go:build sqlite3 2 | 3 | package cli 4 | 5 | import ( 6 | _ "github.com/golang-migrate/migrate/v4/database/sqlite3" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/cli/build_sqlserver.go: -------------------------------------------------------------------------------- 1 | //go:build sqlserver 2 | 3 | package cli 4 | 5 | import ( 6 | _ "github.com/golang-migrate/migrate/v4/database/sqlserver" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/cli/build_yugabytedb.go: -------------------------------------------------------------------------------- 1 | //go:build yugabytedb 2 | 3 | package cli 4 | 5 | import ( 6 | _ "github.com/golang-migrate/migrate/v4/database/yugabytedb" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/cli/log.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | logpkg "log" 6 | "os" 7 | ) 8 | 9 | // Log represents the logger 10 | type Log struct { 11 | verbose bool 12 | } 13 | 14 | // Printf prints out formatted string into a log 15 | func (l *Log) Printf(format string, v ...interface{}) { 16 | if l.verbose { 17 | logpkg.Printf(format, v...) 18 | } else { 19 | fmt.Fprintf(os.Stderr, format, v...) 20 | } 21 | } 22 | 23 | // Println prints out args into a log 24 | func (l *Log) Println(args ...interface{}) { 25 | if l.verbose { 26 | logpkg.Println(args...) 27 | } else { 28 | fmt.Fprintln(os.Stderr, args...) 29 | } 30 | } 31 | 32 | // Verbose shows if verbose print enabled 33 | func (l *Log) Verbose() bool { 34 | return l.verbose 35 | } 36 | 37 | func (l *Log) fatal(args ...interface{}) { 38 | l.Println(args...) 39 | os.Exit(1) 40 | } 41 | 42 | func (l *Log) fatalErr(err error) { 43 | l.fatal("error:", err) 44 | } 45 | -------------------------------------------------------------------------------- /internal/url/url.go: -------------------------------------------------------------------------------- 1 | package url 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | ) 7 | 8 | var errNoScheme = errors.New("no scheme") 9 | var errEmptyURL = errors.New("URL cannot be empty") 10 | 11 | // schemeFromURL returns the scheme from a URL string 12 | func SchemeFromURL(url string) (string, error) { 13 | if url == "" { 14 | return "", errEmptyURL 15 | } 16 | 17 | i := strings.Index(url, ":") 18 | 19 | // No : or : is the first character. 20 | if i < 1 { 21 | return "", errNoScheme 22 | } 23 | 24 | return url[0:i], nil 25 | } 26 | -------------------------------------------------------------------------------- /internal/url/url_test.go: -------------------------------------------------------------------------------- 1 | package url 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestSchemeFromUrl(t *testing.T) { 8 | cases := []struct { 9 | name string 10 | urlStr string 11 | expected string 12 | expectErr error 13 | }{ 14 | { 15 | name: "Simple", 16 | urlStr: "protocol://path", 17 | expected: "protocol", 18 | }, 19 | { 20 | // See issue #264 21 | name: "MySQLWithPort", 22 | urlStr: "mysql://user:pass@tcp(host:1337)/db", 23 | expected: "mysql", 24 | }, 25 | { 26 | name: "Empty", 27 | urlStr: "", 28 | expectErr: errEmptyURL, 29 | }, 30 | { 31 | name: "NoScheme", 32 | urlStr: "hello", 33 | expectErr: errNoScheme, 34 | }, 35 | } 36 | 37 | for _, tc := range cases { 38 | t.Run(tc.name, func(t *testing.T) { 39 | s, err := SchemeFromURL(tc.urlStr) 40 | if err != tc.expectErr { 41 | t.Fatalf("expected %q, but received %q", tc.expectErr, err) 42 | } 43 | if s != tc.expected { 44 | t.Fatalf("expected %q, but received %q", tc.expected, s) 45 | } 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | package migrate 2 | 3 | // Logger is an interface so you can pass in your own 4 | // logging implementation. 5 | type Logger interface { 6 | 7 | // Printf is like fmt.Printf 8 | Printf(format string, v ...interface{}) 9 | 10 | // Verbose should return true when verbose logging output is wanted 11 | Verbose() bool 12 | } 13 | -------------------------------------------------------------------------------- /migration_test.go: -------------------------------------------------------------------------------- 1 | package migrate 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "strings" 8 | ) 9 | 10 | func ExampleNewMigration() { 11 | // Create a dummy migration body, this is coming from the source usually. 12 | body := io.NopCloser(strings.NewReader("dumy migration that creates users table")) 13 | 14 | // Create a new Migration that represents version 1486686016. 15 | // Once this migration has been applied to the database, the new 16 | // migration version will be 1486689359. 17 | migr, err := NewMigration(body, "create_users_table", 1486686016, 1486689359) 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | 22 | fmt.Print(migr.LogString()) 23 | // Output: 24 | // 1486686016/u create_users_table 25 | } 26 | 27 | func ExampleNewMigration_nilMigration() { 28 | // Create a new Migration that represents a NilMigration. 29 | // Once this migration has been applied to the database, the new 30 | // migration version will be 1486689359. 31 | migr, err := NewMigration(nil, "", 1486686016, 1486689359) 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | 36 | fmt.Print(migr.LogString()) 37 | // Output: 38 | // 1486686016/u 39 | } 40 | 41 | func ExampleNewMigration_nilVersion() { 42 | // Create a dummy migration body, this is coming from the source usually. 43 | body := io.NopCloser(strings.NewReader("dumy migration that deletes users table")) 44 | 45 | // Create a new Migration that represents version 1486686016. 46 | // This is the last available down migration, so the migration version 47 | // will be -1, meaning NilVersion once this migration ran. 48 | migr, err := NewMigration(body, "drop_users_table", 1486686016, -1) 49 | if err != nil { 50 | log.Fatal(err) 51 | } 52 | 53 | fmt.Print(migr.LogString()) 54 | // Output: 55 | // 1486686016/d drop_users_table 56 | } 57 | -------------------------------------------------------------------------------- /source/aws_s3/README.md: -------------------------------------------------------------------------------- 1 | # aws_s3 2 | 3 | `s3:///` 4 | -------------------------------------------------------------------------------- /source/bitbucket/.gitignore: -------------------------------------------------------------------------------- 1 | .bitbucket_test_secrets 2 | -------------------------------------------------------------------------------- /source/bitbucket/README.md: -------------------------------------------------------------------------------- 1 | # bitbucket 2 | 3 | This driver is catered for those that want to source migrations from bitbucket cloud(https://bitbucket.com). 4 | 5 | `bitbucket://user:password@owner/repo/path#ref` 6 | 7 | | URL Query | WithInstance Config | Description | 8 | |------------|---------------------|-------------| 9 | | user | | The username of the user connecting | 10 | | password | | User's password or an app password with repo read permission | 11 | | owner | | the repo owner | 12 | | repo | | the name of the repository | 13 | | path | | path in repo to migrations | 14 | | ref | | (optional) can be a SHA, branch, or tag | 15 | -------------------------------------------------------------------------------- /source/bitbucket/bitbucket_test.go: -------------------------------------------------------------------------------- 1 | package bitbucket 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "testing" 7 | 8 | st "github.com/golang-migrate/migrate/v4/source/testing" 9 | ) 10 | 11 | var BitbucketTestSecret = "" // username:password 12 | 13 | func init() { 14 | secrets, err := os.ReadFile(".bitbucket_test_secrets") 15 | if err == nil { 16 | BitbucketTestSecret = string(bytes.TrimSpace(secrets)[:]) 17 | } 18 | } 19 | 20 | func Test(t *testing.T) { 21 | if len(BitbucketTestSecret) == 0 { 22 | t.Skip("test requires .bitbucket_test_secrets") 23 | } 24 | 25 | b := &Bitbucket{} 26 | 27 | d, err := b.Open("bitbucket://" + BitbucketTestSecret + "@abhishekbipp/test-migration/migrations/test#master") 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | 32 | st.Test(t, d) 33 | } 34 | -------------------------------------------------------------------------------- /source/driver_test.go: -------------------------------------------------------------------------------- 1 | package source 2 | 3 | func ExampleDriver() { 4 | // see source/stub for an example 5 | 6 | // source/stub/stub.go has the driver implementation 7 | // source/stub/stub_test.go runs source/testing/test.go:Test 8 | } 9 | -------------------------------------------------------------------------------- /source/errors.go: -------------------------------------------------------------------------------- 1 | package source 2 | 3 | import "os" 4 | 5 | // ErrDuplicateMigration is an error type for reporting duplicate migration 6 | // files. 7 | type ErrDuplicateMigration struct { 8 | Migration 9 | os.FileInfo 10 | } 11 | 12 | // Error implements error interface. 13 | func (e ErrDuplicateMigration) Error() string { 14 | return "duplicate migration file: " + e.Name() 15 | } 16 | -------------------------------------------------------------------------------- /source/file/README.md: -------------------------------------------------------------------------------- 1 | # file 2 | 3 | `file:///absolute/path` 4 | `file://relative/path` 5 | -------------------------------------------------------------------------------- /source/file/file.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | nurl "net/url" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/golang-migrate/migrate/v4/source" 9 | "github.com/golang-migrate/migrate/v4/source/iofs" 10 | ) 11 | 12 | func init() { 13 | source.Register("file", &File{}) 14 | } 15 | 16 | type File struct { 17 | iofs.PartialDriver 18 | url string 19 | path string 20 | } 21 | 22 | func (f *File) Open(url string) (source.Driver, error) { 23 | p, err := parseURL(url) 24 | if err != nil { 25 | return nil, err 26 | } 27 | nf := &File{ 28 | url: url, 29 | path: p, 30 | } 31 | if err := nf.Init(os.DirFS(p), "."); err != nil { 32 | return nil, err 33 | } 34 | return nf, nil 35 | } 36 | 37 | func parseURL(url string) (string, error) { 38 | u, err := nurl.Parse(url) 39 | if err != nil { 40 | return "", err 41 | } 42 | // concat host and path to restore full path 43 | // host might be `.` 44 | p := u.Opaque 45 | if len(p) == 0 { 46 | p = u.Host + u.Path 47 | } 48 | 49 | if len(p) == 0 { 50 | // default to current directory if no path 51 | wd, err := os.Getwd() 52 | if err != nil { 53 | return "", err 54 | } 55 | p = wd 56 | 57 | } else if p[0:1] == "." || p[0:1] != "/" { 58 | // make path absolute if relative 59 | abs, err := filepath.Abs(p) 60 | if err != nil { 61 | return "", err 62 | } 63 | p = abs 64 | } 65 | return p, nil 66 | } 67 | -------------------------------------------------------------------------------- /source/github/.gitignore: -------------------------------------------------------------------------------- 1 | .github_test_secrets 2 | -------------------------------------------------------------------------------- /source/github/README.md: -------------------------------------------------------------------------------- 1 | # github 2 | 3 | This driver is catered for those that want to source migrations from [github.com](https://github.com). The URL scheme doesn't require a hostname, as it just simply defaults to `github.com`. 4 | 5 | Authenticated client: `github://user:personal-access-token@owner/repo/path#ref` 6 | 7 | Unauthenticated client: `github://owner/repo/path#ref` 8 | 9 | | URL Query | WithInstance Config | Description | 10 | |------------|---------------------|-------------| 11 | | user | | (optional) The username of the user connecting | 12 | | personal-access-token | | (optional) An access token from GitHub (https://github.com/settings/tokens) | 13 | | owner | | the repo owner | 14 | | repo | | the name of the repository | 15 | | path | | path in repo to migrations | 16 | | ref | | (optional) can be a SHA, branch, or tag | 17 | -------------------------------------------------------------------------------- /source/github/examples/migrations/1085649617_create_users_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS users; 2 | -------------------------------------------------------------------------------- /source/github/examples/migrations/1085649617_create_users_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users ( 2 | user_id integer unique, 3 | name varchar(40), 4 | email varchar(40) 5 | ); 6 | -------------------------------------------------------------------------------- /source/github/examples/migrations/1185749658_add_city_to_users.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users DROP COLUMN IF EXISTS city; 2 | -------------------------------------------------------------------------------- /source/github/examples/migrations/1185749658_add_city_to_users.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users ADD COLUMN city varchar(100); 2 | 3 | 4 | -------------------------------------------------------------------------------- /source/github/examples/migrations/1285849751_add_index_on_user_emails.down.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX IF EXISTS users_email_index; 2 | -------------------------------------------------------------------------------- /source/github/examples/migrations/1285849751_add_index_on_user_emails.up.sql: -------------------------------------------------------------------------------- 1 | CREATE UNIQUE INDEX CONCURRENTLY users_email_index ON users (email); 2 | 3 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 4 | -------------------------------------------------------------------------------- /source/github/examples/migrations/1385949617_create_books_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS books; 2 | -------------------------------------------------------------------------------- /source/github/examples/migrations/1385949617_create_books_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE books ( 2 | user_id integer, 3 | name varchar(40), 4 | author varchar(40) 5 | ); 6 | -------------------------------------------------------------------------------- /source/github/examples/migrations/1485949617_create_movies_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS movies; 2 | -------------------------------------------------------------------------------- /source/github/examples/migrations/1485949617_create_movies_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE movies ( 2 | user_id integer, 3 | name varchar(40), 4 | director varchar(40) 5 | ); 6 | -------------------------------------------------------------------------------- /source/github/examples/migrations/1585849751_just_a_comment.up.sql: -------------------------------------------------------------------------------- 1 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 2 | -------------------------------------------------------------------------------- /source/github/examples/migrations/1685849751_another_comment.up.sql: -------------------------------------------------------------------------------- 1 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 2 | -------------------------------------------------------------------------------- /source/github/examples/migrations/1785849751_another_comment.up.sql: -------------------------------------------------------------------------------- 1 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 2 | -------------------------------------------------------------------------------- /source/github/examples/migrations/1885849751_another_comment.up.sql: -------------------------------------------------------------------------------- 1 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 2 | -------------------------------------------------------------------------------- /source/github/github_test.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "testing" 8 | 9 | st "github.com/golang-migrate/migrate/v4/source/testing" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | var GithubTestSecret = "" // username:token 14 | 15 | func init() { 16 | secrets, err := os.ReadFile(".github_test_secrets") 17 | if err == nil { 18 | GithubTestSecret = string(bytes.TrimSpace(secrets)[:]) 19 | } 20 | } 21 | 22 | func Test(t *testing.T) { 23 | if len(GithubTestSecret) == 0 { 24 | t.Skip("test requires .github_test_secrets") 25 | } 26 | 27 | g := &Github{} 28 | d, err := g.Open("github://" + GithubTestSecret + "@mattes/migrate_test_tmp/test#452b8003e7") 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | 33 | st.Test(t, d) 34 | } 35 | 36 | func TestDefaultClient(t *testing.T) { 37 | g := &Github{} 38 | owner := "golang-migrate" 39 | repo := "migrate" 40 | path := "source/github/examples/migrations" 41 | 42 | url := fmt.Sprintf("github://%s/%s/%s", owner, repo, path) 43 | d, err := g.Open(url) 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | 48 | ver, err := d.First() 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | assert.Equal(t, uint(1085649617), ver) 53 | 54 | ver, err = d.Next(ver) 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | assert.Equal(t, uint(1185749658), ver) 59 | } 60 | -------------------------------------------------------------------------------- /source/github_ee/.gitignore: -------------------------------------------------------------------------------- 1 | .github_test_secrets 2 | -------------------------------------------------------------------------------- /source/github_ee/README.md: -------------------------------------------------------------------------------- 1 | # github ee 2 | 3 | ## GitHub Enterprise Edition 4 | 5 | This driver is catered for those who run GitHub Enterprise under private infrastructure. 6 | 7 | The below URL scheme illustrates how to source migration files from GitHub Enterprise. 8 | 9 | GitHub client for Go requires API and Uploads endpoint hosts in order to create an instance of GitHub Enterprise Client. We're making an assumption that the API and Uploads are available under `https://api.*` and `https://uploads.*` respectively. [GitHub Enterprise Installation Guide](https://help.github.com/en/enterprise/2.15/admin/installation/enabling-subdomain-isolation) recommends that you enable Subdomain isolation feature. 10 | 11 | `github-ee://user:personal-access-token@host/owner/repo/path?verify-tls=true#ref` 12 | 13 | | URL Query | WithInstance Config | Description | 14 | |------------|---------------------|-------------| 15 | | user | | The username of the user connecting | 16 | | personal-access-token | | Personal access token from your GitHub Enterprise instance | 17 | | owner | | the repo owner | 18 | | repo | | the name of the repository | 19 | | path | | path in repo to migrations | 20 | | ref | | (optional) can be a SHA, branch, or tag | 21 | | verify-tls | | (optional) defaults to `true`. This option sets `tls.Config.InsecureSkipVerify` accordingly | 22 | -------------------------------------------------------------------------------- /source/github_ee/github_ee.go: -------------------------------------------------------------------------------- 1 | package github_ee 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "net/http" 7 | nurl "net/url" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/golang-migrate/migrate/v4/source" 12 | gh "github.com/golang-migrate/migrate/v4/source/github" 13 | 14 | "github.com/google/go-github/v39/github" 15 | ) 16 | 17 | func init() { 18 | source.Register("github-ee", &GithubEE{}) 19 | } 20 | 21 | type GithubEE struct { 22 | source.Driver 23 | } 24 | 25 | func (g *GithubEE) Open(url string) (source.Driver, error) { 26 | verifyTLS := true 27 | 28 | u, err := nurl.Parse(url) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | if o := u.Query().Get("verify-tls"); o != "" { 34 | verifyTLS = parseBool(o, verifyTLS) 35 | } 36 | 37 | if u.User == nil { 38 | return nil, gh.ErrNoUserInfo 39 | } 40 | 41 | password, ok := u.User.Password() 42 | if !ok { 43 | return nil, gh.ErrNoUserInfo 44 | } 45 | 46 | ghc, err := g.createGithubClient(u.Host, u.User.Username(), password, verifyTLS) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | pe := strings.Split(strings.Trim(u.Path, "/"), "/") 52 | 53 | if len(pe) < 1 { 54 | return nil, gh.ErrInvalidRepo 55 | } 56 | 57 | cfg := &gh.Config{ 58 | Owner: pe[0], 59 | Repo: pe[1], 60 | Ref: u.Fragment, 61 | } 62 | 63 | if len(pe) > 2 { 64 | cfg.Path = strings.Join(pe[2:], "/") 65 | } 66 | 67 | i, err := gh.WithInstance(ghc, cfg) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | return &GithubEE{Driver: i}, nil 73 | } 74 | 75 | func (g *GithubEE) createGithubClient(host, username, password string, verifyTLS bool) (*github.Client, error) { 76 | tr := &github.BasicAuthTransport{ 77 | Username: username, 78 | Password: password, 79 | Transport: &http.Transport{ 80 | TLSClientConfig: &tls.Config{InsecureSkipVerify: !verifyTLS}, 81 | }, 82 | } 83 | 84 | apiHost := fmt.Sprintf("https://%s/api/v3", host) 85 | uploadHost := fmt.Sprintf("https://uploads.%s", host) 86 | 87 | return github.NewEnterpriseClient(apiHost, uploadHost, tr.Client()) 88 | } 89 | 90 | func parseBool(val string, fallback bool) bool { 91 | b, err := strconv.ParseBool(val) 92 | if err != nil { 93 | return fallback 94 | } 95 | 96 | return b 97 | } 98 | -------------------------------------------------------------------------------- /source/github_ee/github_ee_test.go: -------------------------------------------------------------------------------- 1 | package github_ee 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | nurl "net/url" 7 | "testing" 8 | ) 9 | 10 | func Test(t *testing.T) { 11 | ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 12 | if r.URL.Path != "/api/v3/repos/mattes/migrate_test_tmp/contents/test" { 13 | w.WriteHeader(http.StatusNotFound) 14 | return 15 | } 16 | 17 | if ref := r.URL.Query().Get("ref"); ref != "452b8003e7" { 18 | w.WriteHeader(http.StatusNotFound) 19 | return 20 | } 21 | 22 | w.Header().Set("Content-Type", "application/json") 23 | w.WriteHeader(http.StatusOK) 24 | 25 | _, err := w.Write([]byte("[]")) 26 | if err != nil { 27 | w.WriteHeader(http.StatusInternalServerError) 28 | return 29 | } 30 | })) 31 | defer ts.Close() 32 | 33 | u, err := nurl.Parse(ts.URL) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | g := &GithubEE{} 39 | _, err = g.Open("github-ee://foo:bar@" + u.Host + "/mattes/migrate_test_tmp/test?verify-tls=false#452b8003e7") 40 | 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /source/gitlab/.gitignore: -------------------------------------------------------------------------------- 1 | .gitlab_test_secrets 2 | -------------------------------------------------------------------------------- /source/gitlab/README.md: -------------------------------------------------------------------------------- 1 | # gitlab 2 | 3 | `gitlab://user:personal-access-token@gitlab_url/project_id/path#ref` 4 | 5 | | URL Query | WithInstance Config | Description | 6 | |------------|---------------------|-------------| 7 | | user | | The username of the user connecting | 8 | | personal-access-token | | An access token from Gitlab (https:///profile/personal_access_tokens) | 9 | | gitlab_url | | url of the gitlab server | 10 | | project_id | | id of the repository | 11 | | path | | path in repo to migrations | 12 | | ref | | (optional) can be a SHA, branch, or tag | 13 | -------------------------------------------------------------------------------- /source/gitlab/examples/migrations/1085649617_create_users_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS users; 2 | -------------------------------------------------------------------------------- /source/gitlab/examples/migrations/1085649617_create_users_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users ( 2 | user_id integer unique, 3 | name varchar(40), 4 | email varchar(40) 5 | ); 6 | -------------------------------------------------------------------------------- /source/gitlab/examples/migrations/1185749658_add_city_to_users.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users DROP COLUMN IF EXISTS city; 2 | -------------------------------------------------------------------------------- /source/gitlab/examples/migrations/1185749658_add_city_to_users.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users ADD COLUMN city varchar(100); 2 | 3 | 4 | -------------------------------------------------------------------------------- /source/gitlab/examples/migrations/1285849751_add_index_on_user_emails.down.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX IF EXISTS users_email_index; 2 | -------------------------------------------------------------------------------- /source/gitlab/examples/migrations/1285849751_add_index_on_user_emails.up.sql: -------------------------------------------------------------------------------- 1 | CREATE UNIQUE INDEX CONCURRENTLY users_email_index ON users (email); 2 | 3 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 4 | -------------------------------------------------------------------------------- /source/gitlab/examples/migrations/1385949617_create_books_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS books; 2 | -------------------------------------------------------------------------------- /source/gitlab/examples/migrations/1385949617_create_books_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE books ( 2 | user_id integer, 3 | name varchar(40), 4 | author varchar(40) 5 | ); 6 | -------------------------------------------------------------------------------- /source/gitlab/examples/migrations/1485949617_create_movies_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS movies; 2 | -------------------------------------------------------------------------------- /source/gitlab/examples/migrations/1485949617_create_movies_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE movies ( 2 | user_id integer, 3 | name varchar(40), 4 | director varchar(40) 5 | ); 6 | -------------------------------------------------------------------------------- /source/gitlab/examples/migrations/1585849751_just_a_comment.up.sql: -------------------------------------------------------------------------------- 1 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 2 | -------------------------------------------------------------------------------- /source/gitlab/examples/migrations/1685849751_another_comment.up.sql: -------------------------------------------------------------------------------- 1 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 2 | -------------------------------------------------------------------------------- /source/gitlab/examples/migrations/1785849751_another_comment.up.sql: -------------------------------------------------------------------------------- 1 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 2 | -------------------------------------------------------------------------------- /source/gitlab/examples/migrations/1885849751_another_comment.up.sql: -------------------------------------------------------------------------------- 1 | -- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. 2 | -------------------------------------------------------------------------------- /source/gitlab/gitlab_test.go: -------------------------------------------------------------------------------- 1 | package gitlab 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "testing" 7 | 8 | st "github.com/golang-migrate/migrate/v4/source/testing" 9 | ) 10 | 11 | var GitlabTestSecret = "" // username:token 12 | 13 | func init() { 14 | secrets, err := os.ReadFile(".gitlab_test_secrets") 15 | if err == nil { 16 | GitlabTestSecret = string(bytes.TrimSpace(secrets)[:]) 17 | } 18 | } 19 | 20 | func Test(t *testing.T) { 21 | if len(GitlabTestSecret) == 0 { 22 | t.Skip("test requires .gitlab_test_secrets") 23 | } 24 | 25 | g := &Gitlab{} 26 | d, err := g.Open("gitlab://" + GitlabTestSecret + "@gitlab.com/11197284/migrations") 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | 31 | st.Test(t, d) 32 | } 33 | -------------------------------------------------------------------------------- /source/go_bindata/README.md: -------------------------------------------------------------------------------- 1 | # go_bindata 2 | 3 | ## Usage 4 | 5 | 6 | 7 | ### Read bindata with NewWithSourceInstance 8 | 9 | ```shell 10 | go get -u github.com/jteeuwen/go-bindata/... 11 | cd examples/migrations && go-bindata -pkg migrations . 12 | ``` 13 | 14 | ```go 15 | import ( 16 | "github.com/golang-migrate/migrate/v4" 17 | "github.com/golang-migrate/migrate/v4/source/go_bindata" 18 | "github.com/golang-migrate/migrate/v4/source/go_bindata/examples/migrations" 19 | ) 20 | 21 | func main() { 22 | // wrap assets into Resource 23 | s := bindata.Resource(migrations.AssetNames(), 24 | func(name string) ([]byte, error) { 25 | return migrations.Asset(name) 26 | }) 27 | 28 | d, err := bindata.WithInstance(s) 29 | m, err := migrate.NewWithSourceInstance("go-bindata", d, "database://foobar") 30 | m.Up() // run your migrations and handle the errors above of course 31 | } 32 | ``` 33 | 34 | ### Read bindata with URL (todo) 35 | 36 | This will restore the assets in a tmp directory and then 37 | proxy to source/file. go-bindata must be in your `$PATH`. 38 | 39 | ``` 40 | migrate -source go-bindata://examples/migrations/bindata.go 41 | ``` 42 | 43 | 44 | -------------------------------------------------------------------------------- /source/go_bindata/go-bindata.go: -------------------------------------------------------------------------------- 1 | package bindata 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | 9 | "github.com/golang-migrate/migrate/v4/source" 10 | ) 11 | 12 | type AssetFunc func(name string) ([]byte, error) 13 | 14 | func Resource(names []string, afn AssetFunc) *AssetSource { 15 | return &AssetSource{ 16 | Names: names, 17 | AssetFunc: afn, 18 | } 19 | } 20 | 21 | type AssetSource struct { 22 | Names []string 23 | AssetFunc AssetFunc 24 | } 25 | 26 | func init() { 27 | source.Register("go-bindata", &Bindata{}) 28 | } 29 | 30 | type Bindata struct { 31 | path string 32 | assetSource *AssetSource 33 | migrations *source.Migrations 34 | } 35 | 36 | func (b *Bindata) Open(url string) (source.Driver, error) { 37 | return nil, fmt.Errorf("not yet implemented") 38 | } 39 | 40 | var ( 41 | ErrNoAssetSource = fmt.Errorf("expects *AssetSource") 42 | ) 43 | 44 | func WithInstance(instance interface{}) (source.Driver, error) { 45 | if _, ok := instance.(*AssetSource); !ok { 46 | return nil, ErrNoAssetSource 47 | } 48 | as := instance.(*AssetSource) 49 | 50 | bn := &Bindata{ 51 | path: "", 52 | assetSource: as, 53 | migrations: source.NewMigrations(), 54 | } 55 | 56 | for _, fi := range as.Names { 57 | m, err := source.DefaultParse(fi) 58 | if err != nil { 59 | continue // ignore files that we can't parse 60 | } 61 | 62 | if !bn.migrations.Append(m) { 63 | return nil, fmt.Errorf("unable to parse file %v", fi) 64 | } 65 | } 66 | 67 | return bn, nil 68 | } 69 | 70 | func (b *Bindata) Close() error { 71 | return nil 72 | } 73 | 74 | func (b *Bindata) First() (version uint, err error) { 75 | if v, ok := b.migrations.First(); !ok { 76 | return 0, &os.PathError{Op: "first", Path: b.path, Err: os.ErrNotExist} 77 | } else { 78 | return v, nil 79 | } 80 | } 81 | 82 | func (b *Bindata) Prev(version uint) (prevVersion uint, err error) { 83 | if v, ok := b.migrations.Prev(version); !ok { 84 | return 0, &os.PathError{Op: fmt.Sprintf("prev for version %v", version), Path: b.path, Err: os.ErrNotExist} 85 | } else { 86 | return v, nil 87 | } 88 | } 89 | 90 | func (b *Bindata) Next(version uint) (nextVersion uint, err error) { 91 | if v, ok := b.migrations.Next(version); !ok { 92 | return 0, &os.PathError{Op: fmt.Sprintf("next for version %v", version), Path: b.path, Err: os.ErrNotExist} 93 | } else { 94 | return v, nil 95 | } 96 | } 97 | 98 | func (b *Bindata) ReadUp(version uint) (r io.ReadCloser, identifier string, err error) { 99 | if m, ok := b.migrations.Up(version); ok { 100 | body, err := b.assetSource.AssetFunc(m.Raw) 101 | if err != nil { 102 | return nil, "", err 103 | } 104 | return io.NopCloser(bytes.NewReader(body)), m.Identifier, nil 105 | } 106 | return nil, "", &os.PathError{Op: fmt.Sprintf("read version %v", version), Path: b.path, Err: os.ErrNotExist} 107 | } 108 | 109 | func (b *Bindata) ReadDown(version uint) (r io.ReadCloser, identifier string, err error) { 110 | if m, ok := b.migrations.Down(version); ok { 111 | body, err := b.assetSource.AssetFunc(m.Raw) 112 | if err != nil { 113 | return nil, "", err 114 | } 115 | return io.NopCloser(bytes.NewReader(body)), m.Identifier, nil 116 | } 117 | return nil, "", &os.PathError{Op: fmt.Sprintf("read version %v", version), Path: b.path, Err: os.ErrNotExist} 118 | } 119 | -------------------------------------------------------------------------------- /source/go_bindata/go-bindata_test.go: -------------------------------------------------------------------------------- 1 | package bindata 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/golang-migrate/migrate/v4/source/go_bindata/testdata" 7 | st "github.com/golang-migrate/migrate/v4/source/testing" 8 | ) 9 | 10 | func Test(t *testing.T) { 11 | // wrap assets into Resource first 12 | s := Resource(testdata.AssetNames(), 13 | func(name string) ([]byte, error) { 14 | return testdata.Asset(name) 15 | }) 16 | 17 | d, err := WithInstance(s) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | st.Test(t, d) 22 | } 23 | 24 | func TestWithInstance(t *testing.T) { 25 | // wrap assets into Resource 26 | s := Resource(testdata.AssetNames(), 27 | func(name string) ([]byte, error) { 28 | return testdata.Asset(name) 29 | }) 30 | 31 | _, err := WithInstance(s) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | } 36 | 37 | func TestOpen(t *testing.T) { 38 | b := &Bindata{} 39 | _, err := b.Open("") 40 | if err == nil { 41 | t.Fatal("expected err, because it's not implemented yet") 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /source/godoc_vfs/vfs.go: -------------------------------------------------------------------------------- 1 | // Package godoc_vfs contains a driver that reads migrations from a virtual file 2 | // system. 3 | // 4 | // Implementations of the filesystem interface that read from zip files and 5 | // maps, as well as the definition of the filesystem interface can be found in 6 | // the golang.org/x/tools/godoc/vfs package. 7 | package godoc_vfs 8 | 9 | import ( 10 | "github.com/golang-migrate/migrate/v4/source" 11 | "github.com/golang-migrate/migrate/v4/source/httpfs" 12 | 13 | "golang.org/x/tools/godoc/vfs" 14 | vfs_httpfs "golang.org/x/tools/godoc/vfs/httpfs" 15 | ) 16 | 17 | func init() { 18 | source.Register("godoc-vfs", &VFS{}) 19 | } 20 | 21 | // VFS is an implementation of driver that returns migrations from a virtual 22 | // file system. 23 | type VFS struct { 24 | httpfs.PartialDriver 25 | fs vfs.FileSystem 26 | path string 27 | } 28 | 29 | // Open implements the source.Driver interface for VFS. 30 | // 31 | // Calling this function panics, instead use the WithInstance function. 32 | // See the package level documentation for an example. 33 | func (b *VFS) Open(url string) (source.Driver, error) { 34 | panic("not implemented") 35 | } 36 | 37 | // WithInstance creates a new driver from a virtual file system. 38 | // If a tree named searchPath exists in the virtual filesystem, WithInstance 39 | // searches for migration files there. 40 | // It defaults to "/". 41 | func WithInstance(fs vfs.FileSystem, searchPath string) (source.Driver, error) { 42 | if searchPath == "" { 43 | searchPath = "/" 44 | } 45 | 46 | bn := &VFS{ 47 | fs: fs, 48 | path: searchPath, 49 | } 50 | 51 | if err := bn.Init(vfs_httpfs.New(fs), searchPath); err != nil { 52 | return nil, err 53 | } 54 | 55 | return bn, nil 56 | } 57 | -------------------------------------------------------------------------------- /source/godoc_vfs/vfs_example_test.go: -------------------------------------------------------------------------------- 1 | package godoc_vfs_test 2 | 3 | import ( 4 | "github.com/golang-migrate/migrate/v4" 5 | "github.com/golang-migrate/migrate/v4/source/godoc_vfs" 6 | "golang.org/x/tools/godoc/vfs/mapfs" 7 | ) 8 | 9 | func Example_mapfs() { 10 | fs := mapfs.New(map[string]string{ 11 | "1_foobar.up.sql": "1 up", 12 | "1_foobar.down.sql": "1 down", 13 | "3_foobar.up.sql": "3 up", 14 | "4_foobar.up.sql": "4 up", 15 | "4_foobar.down.sql": "4 down", 16 | "5_foobar.down.sql": "5 down", 17 | "7_foobar.up.sql": "7 up", 18 | "7_foobar.down.sql": "7 down", 19 | }) 20 | 21 | d, err := godoc_vfs.WithInstance(fs, "") 22 | if err != nil { 23 | panic("bad migrations found!") 24 | } 25 | m, err := migrate.NewWithSourceInstance("godoc-vfs", d, "database://foobar") 26 | if err != nil { 27 | panic("error creating the migrations") 28 | } 29 | err = m.Up() 30 | if err != nil { 31 | panic("up failed") 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /source/godoc_vfs/vfs_test.go: -------------------------------------------------------------------------------- 1 | package godoc_vfs_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/golang-migrate/migrate/v4/source/godoc_vfs" 7 | st "github.com/golang-migrate/migrate/v4/source/testing" 8 | "golang.org/x/tools/godoc/vfs/mapfs" 9 | ) 10 | 11 | func TestVFS(t *testing.T) { 12 | fs := mapfs.New(map[string]string{ 13 | "1_foobar.up.sql": "1 up", 14 | "1_foobar.down.sql": "1 down", 15 | "3_foobar.up.sql": "3 up", 16 | "4_foobar.up.sql": "4 up", 17 | "4_foobar.down.sql": "4 down", 18 | "5_foobar.down.sql": "5 down", 19 | "7_foobar.up.sql": "7 up", 20 | "7_foobar.down.sql": "7 down", 21 | }) 22 | 23 | d, err := godoc_vfs.WithInstance(fs, "") 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | st.Test(t, d) 28 | } 29 | 30 | func TestOpen(t *testing.T) { 31 | defer func() { 32 | if r := recover(); r == nil { 33 | t.Error("Expected Open to panic") 34 | } 35 | }() 36 | b := &godoc_vfs.VFS{} 37 | if _, err := b.Open(""); err != nil { 38 | t.Error(err) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /source/google_cloud_storage/README.md: -------------------------------------------------------------------------------- 1 | # Google Cloud Storage 2 | 3 | 4 | ## Import 5 | 6 | ```go 7 | import ( 8 | _ "github.com/golang-migrate/migrate/v4/source/google_cloud_storage" 9 | ) 10 | ``` 11 | 12 | ## Connection String 13 | 14 | `gcs:///` 15 | -------------------------------------------------------------------------------- /source/google_cloud_storage/storage.go: -------------------------------------------------------------------------------- 1 | package googlecloudstorage 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/url" 7 | "os" 8 | "path" 9 | "strings" 10 | 11 | "cloud.google.com/go/storage" 12 | "context" 13 | "github.com/golang-migrate/migrate/v4/source" 14 | "google.golang.org/api/iterator" 15 | ) 16 | 17 | func init() { 18 | source.Register("gcs", &gcs{}) 19 | } 20 | 21 | type gcs struct { 22 | bucket *storage.BucketHandle 23 | prefix string 24 | migrations *source.Migrations 25 | } 26 | 27 | func (g *gcs) Open(folder string) (source.Driver, error) { 28 | u, err := url.Parse(folder) 29 | if err != nil { 30 | return nil, err 31 | } 32 | client, err := storage.NewClient(context.Background()) 33 | if err != nil { 34 | return nil, err 35 | } 36 | driver := gcs{ 37 | bucket: client.Bucket(u.Host), 38 | prefix: strings.Trim(u.Path, "/") + "/", 39 | migrations: source.NewMigrations(), 40 | } 41 | err = driver.loadMigrations() 42 | if err != nil { 43 | return nil, err 44 | } 45 | return &driver, nil 46 | } 47 | 48 | func (g *gcs) loadMigrations() error { 49 | iter := g.bucket.Objects(context.Background(), &storage.Query{ 50 | Prefix: g.prefix, 51 | Delimiter: "/", 52 | }) 53 | object, err := iter.Next() 54 | for ; err == nil; object, err = iter.Next() { 55 | _, fileName := path.Split(object.Name) 56 | m, parseErr := source.DefaultParse(fileName) 57 | if parseErr != nil { 58 | continue 59 | } 60 | if !g.migrations.Append(m) { 61 | return fmt.Errorf("unable to parse file %v", object.Name) 62 | } 63 | } 64 | if err != iterator.Done { 65 | return err 66 | } 67 | return nil 68 | } 69 | 70 | func (g *gcs) Close() error { 71 | return nil 72 | } 73 | 74 | func (g *gcs) First() (uint, error) { 75 | v, ok := g.migrations.First() 76 | if !ok { 77 | return 0, os.ErrNotExist 78 | } 79 | return v, nil 80 | } 81 | 82 | func (g *gcs) Prev(version uint) (uint, error) { 83 | v, ok := g.migrations.Prev(version) 84 | if !ok { 85 | return 0, os.ErrNotExist 86 | } 87 | return v, nil 88 | } 89 | 90 | func (g *gcs) Next(version uint) (uint, error) { 91 | v, ok := g.migrations.Next(version) 92 | if !ok { 93 | return 0, os.ErrNotExist 94 | } 95 | return v, nil 96 | } 97 | 98 | func (g *gcs) ReadUp(version uint) (io.ReadCloser, string, error) { 99 | if m, ok := g.migrations.Up(version); ok { 100 | return g.open(m) 101 | } 102 | return nil, "", os.ErrNotExist 103 | } 104 | 105 | func (g *gcs) ReadDown(version uint) (io.ReadCloser, string, error) { 106 | if m, ok := g.migrations.Down(version); ok { 107 | return g.open(m) 108 | } 109 | return nil, "", os.ErrNotExist 110 | } 111 | 112 | func (g *gcs) open(m *source.Migration) (io.ReadCloser, string, error) { 113 | objectPath := path.Join(g.prefix, m.Raw) 114 | reader, err := g.bucket.Object(objectPath).NewReader(context.Background()) 115 | if err != nil { 116 | return nil, "", err 117 | } 118 | return reader, m.Identifier, nil 119 | } 120 | -------------------------------------------------------------------------------- /source/google_cloud_storage/storage_test.go: -------------------------------------------------------------------------------- 1 | package googlecloudstorage 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/fsouza/fake-gcs-server/fakestorage" 7 | "github.com/golang-migrate/migrate/v4/source" 8 | st "github.com/golang-migrate/migrate/v4/source/testing" 9 | ) 10 | 11 | func Test(t *testing.T) { 12 | server := fakestorage.NewServer([]fakestorage.Object{ 13 | {BucketName: "some-bucket", Name: "staging/migrations/1_foobar.up.sql", Content: []byte("1 up")}, 14 | {BucketName: "some-bucket", Name: "staging/migrations/1_foobar.down.sql", Content: []byte("1 down")}, 15 | {BucketName: "some-bucket", Name: "prod/migrations/1_foobar.up.sql", Content: []byte("1 up")}, 16 | {BucketName: "some-bucket", Name: "prod/migrations/1_foobar.down.sql", Content: []byte("1 down")}, 17 | {BucketName: "some-bucket", Name: "prod/migrations/3_foobar.up.sql", Content: []byte("3 up")}, 18 | {BucketName: "some-bucket", Name: "prod/migrations/4_foobar.up.sql", Content: []byte("4 up")}, 19 | {BucketName: "some-bucket", Name: "prod/migrations/4_foobar.down.sql", Content: []byte("4 down")}, 20 | {BucketName: "some-bucket", Name: "prod/migrations/5_foobar.down.sql", Content: []byte("5 down")}, 21 | {BucketName: "some-bucket", Name: "prod/migrations/7_foobar.up.sql", Content: []byte("7 up")}, 22 | {BucketName: "some-bucket", Name: "prod/migrations/7_foobar.down.sql", Content: []byte("7 down")}, 23 | {BucketName: "some-bucket", Name: "prod/migrations/not-a-migration.txt"}, 24 | {BucketName: "some-bucket", Name: "prod/migrations/0-random-stuff/whatever.txt"}, 25 | }) 26 | defer server.Stop() 27 | driver := gcs{ 28 | bucket: server.Client().Bucket("some-bucket"), 29 | prefix: "prod/migrations/", 30 | migrations: source.NewMigrations(), 31 | } 32 | err := driver.loadMigrations() 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | st.Test(t, &driver) 37 | } 38 | -------------------------------------------------------------------------------- /source/httpfs/README.md: -------------------------------------------------------------------------------- 1 | # httpfs 2 | 3 | ## Usage 4 | 5 | This package could be used to create new migration source drivers that uses 6 | `http.FileSystem` to read migration files. 7 | 8 | Struct `httpfs.PartialDriver` partly implements the `source.Driver` interface. It has all 9 | the methods except for `Open()`. Embedding this struct and adding `Open()` method 10 | allows users of this package to create new migration sources. Example: 11 | 12 | ```go 13 | struct mydriver { 14 | httpfs.PartialDriver 15 | } 16 | 17 | func (d *mydriver) Open(url string) (source.Driver, error) { 18 | var fs http.FileSystem 19 | var path string 20 | var ds mydriver 21 | 22 | // acquire fs and path from url 23 | // set-up ds if necessary 24 | 25 | if err := ds.Init(fs, path); err != nil { 26 | return nil, err 27 | } 28 | return &ds, nil 29 | } 30 | ``` 31 | 32 | This package also provides a simple `source.Driver` implementation that works 33 | with `http.FileSystem` provided by the user of this package. It is created with 34 | `httpfs.New()` call. 35 | 36 | Example of using `http.Dir()` to read migrations from `sql` directory: 37 | 38 | ```go 39 | src, err := httpfs.New(http.Dir("sql")) 40 | if err != nil { 41 | // do something 42 | } 43 | m, err := migrate.NewWithSourceInstance("httpfs", src, "database://url") 44 | if err != nil { 45 | // do something 46 | } 47 | err = m.Up() 48 | ... 49 | ``` 50 | -------------------------------------------------------------------------------- /source/httpfs/driver.go: -------------------------------------------------------------------------------- 1 | package httpfs 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | 7 | "github.com/golang-migrate/migrate/v4/source" 8 | ) 9 | 10 | // driver is a migration source driver for reading migrations from 11 | // http.FileSystem instances. It implements source.Driver interface and can be 12 | // used as a migration source for the main migrate library. 13 | type driver struct { 14 | PartialDriver 15 | } 16 | 17 | // New creates a new migrate source driver from a http.FileSystem instance and a 18 | // relative path to migration files within the virtual FS. 19 | func New(fs http.FileSystem, path string) (source.Driver, error) { 20 | var d driver 21 | if err := d.Init(fs, path); err != nil { 22 | return nil, err 23 | } 24 | return &d, nil 25 | } 26 | 27 | // Open completes the implementetion of source.Driver interface. Other methods 28 | // are implemented by the embedded PartialDriver struct. 29 | func (d *driver) Open(url string) (source.Driver, error) { 30 | return nil, errors.New("Open() cannot be called on the httpfs passthrough driver") 31 | } 32 | -------------------------------------------------------------------------------- /source/httpfs/driver_test.go: -------------------------------------------------------------------------------- 1 | package httpfs_test 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/golang-migrate/migrate/v4/source/httpfs" 8 | st "github.com/golang-migrate/migrate/v4/source/testing" 9 | ) 10 | 11 | func TestNewOK(t *testing.T) { 12 | d, err := httpfs.New(http.Dir("testdata"), "sql") 13 | if err != nil { 14 | t.Errorf("New() expected not error, got: %s", err) 15 | } 16 | st.Test(t, d) 17 | } 18 | 19 | func TestNewErrors(t *testing.T) { 20 | d, err := httpfs.New(http.Dir("does-not-exist"), "") 21 | if err == nil { 22 | t.Errorf("New() expected to return error") 23 | } 24 | if d != nil { 25 | t.Errorf("New() expected to return nil driver") 26 | } 27 | } 28 | 29 | func TestOpen(t *testing.T) { 30 | d, err := httpfs.New(http.Dir("testdata/sql"), "") 31 | if err != nil { 32 | t.Error("New() expected no error") 33 | return 34 | } 35 | d, err = d.Open("") 36 | if d != nil { 37 | t.Error("Open() expected to return nil driver") 38 | } 39 | if err == nil { 40 | t.Error("Open() expected to return error") 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /source/httpfs/partial_driver_test.go: -------------------------------------------------------------------------------- 1 | package httpfs_test 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/golang-migrate/migrate/v4/source" 10 | "github.com/golang-migrate/migrate/v4/source/httpfs" 11 | st "github.com/golang-migrate/migrate/v4/source/testing" 12 | ) 13 | 14 | type driver struct{ httpfs.PartialDriver } 15 | 16 | func (d *driver) Open(url string) (source.Driver, error) { return nil, errors.New("X") } 17 | 18 | type driverExample struct { 19 | httpfs.PartialDriver 20 | } 21 | 22 | func (d *driverExample) Open(url string) (source.Driver, error) { 23 | parts := strings.Split(url, ":") 24 | dir := parts[0] 25 | path := "" 26 | if len(parts) >= 2 { 27 | path = parts[1] 28 | } 29 | 30 | var de driverExample 31 | return &de, de.Init(http.Dir(dir), path) 32 | } 33 | 34 | func TestDriverExample(t *testing.T) { 35 | d, err := (*driverExample)(nil).Open("testdata:sql") 36 | if err != nil { 37 | t.Errorf("Open() returned error: %s", err) 38 | } 39 | st.Test(t, d) 40 | } 41 | 42 | func TestPartialDriverInit(t *testing.T) { 43 | tests := []struct { 44 | name string 45 | fs http.FileSystem 46 | path string 47 | ok bool 48 | }{ 49 | { 50 | name: "valid dir and empty path", 51 | fs: http.Dir("testdata/sql"), 52 | ok: true, 53 | }, 54 | { 55 | name: "valid dir and non-empty path", 56 | fs: http.Dir("testdata"), 57 | path: "sql", 58 | ok: true, 59 | }, 60 | { 61 | name: "invalid dir", 62 | fs: http.Dir("does-not-exist"), 63 | }, 64 | { 65 | name: "file instead of dir", 66 | fs: http.Dir("testdata/sql/1_foobar.up.sql"), 67 | }, 68 | { 69 | name: "dir with duplicates", 70 | fs: http.Dir("testdata/duplicates"), 71 | }, 72 | } 73 | 74 | for _, test := range tests { 75 | t.Run(test.name, func(t *testing.T) { 76 | var d driver 77 | err := d.Init(test.fs, test.path) 78 | if test.ok { 79 | if err != nil { 80 | t.Errorf("Init() returned error %s", err) 81 | } 82 | st.Test(t, &d) 83 | if err = d.Close(); err != nil { 84 | t.Errorf("Init().Close() returned error %s", err) 85 | } 86 | } else { 87 | if err == nil { 88 | t.Errorf("Init() expected error but did not get one") 89 | } 90 | } 91 | }) 92 | } 93 | 94 | } 95 | 96 | func TestFirstWithNoMigrations(t *testing.T) { 97 | var d driver 98 | fs := http.Dir("testdata/no-migrations") 99 | 100 | if err := d.Init(fs, ""); err != nil { 101 | t.Errorf("No error on Init() expected, got: %v", err) 102 | } 103 | 104 | if _, err := d.First(); err == nil { 105 | t.Errorf("Expected error on First(), got: %v", err) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /source/httpfs/testdata/duplicates/1_foobar.up.sql: -------------------------------------------------------------------------------- 1 | 1 up 2 | -------------------------------------------------------------------------------- /source/httpfs/testdata/duplicates/1_foobaz.up.sql: -------------------------------------------------------------------------------- 1 | 1 up 2 | -------------------------------------------------------------------------------- /source/httpfs/testdata/no-migrations/some-file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golang-migrate/migrate/278833935c12dda022b1355f33a897d895501c45/source/httpfs/testdata/no-migrations/some-file -------------------------------------------------------------------------------- /source/httpfs/testdata/sql/1_foobar.down.sql: -------------------------------------------------------------------------------- 1 | 1 down 2 | -------------------------------------------------------------------------------- /source/httpfs/testdata/sql/1_foobar.up.sql: -------------------------------------------------------------------------------- 1 | 1 up 2 | -------------------------------------------------------------------------------- /source/httpfs/testdata/sql/3_foobar.up.sql: -------------------------------------------------------------------------------- 1 | 3 up 2 | -------------------------------------------------------------------------------- /source/httpfs/testdata/sql/4_foobar.down.sql: -------------------------------------------------------------------------------- 1 | 4 down 2 | -------------------------------------------------------------------------------- /source/httpfs/testdata/sql/4_foobar.up.sql: -------------------------------------------------------------------------------- 1 | 4 up 2 | -------------------------------------------------------------------------------- /source/httpfs/testdata/sql/5_foobar.down.sql: -------------------------------------------------------------------------------- 1 | 5 down 2 | -------------------------------------------------------------------------------- /source/httpfs/testdata/sql/7_foobar.down.sql: -------------------------------------------------------------------------------- 1 | 7 down 2 | -------------------------------------------------------------------------------- /source/httpfs/testdata/sql/7_foobar.up.sql: -------------------------------------------------------------------------------- 1 | 7 up 2 | -------------------------------------------------------------------------------- /source/httpfs/testdata/sql/other-files-are-ignored: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golang-migrate/migrate/278833935c12dda022b1355f33a897d895501c45/source/httpfs/testdata/sql/other-files-are-ignored -------------------------------------------------------------------------------- /source/httpfs/testdata/sql/subdirs-are-ignored/some-file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golang-migrate/migrate/278833935c12dda022b1355f33a897d895501c45/source/httpfs/testdata/sql/subdirs-are-ignored/some-file -------------------------------------------------------------------------------- /source/iofs/README.md: -------------------------------------------------------------------------------- 1 | # iofs 2 | 3 | https://pkg.go.dev/github.com/golang-migrate/migrate/v4/source/iofs 4 | -------------------------------------------------------------------------------- /source/iofs/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package iofs provides the Go 1.16+ io/fs#FS driver. 3 | 4 | It can accept various file systems (like embed.FS, archive/zip#Reader) implementing io/fs#FS. 5 | 6 | This driver cannot be used with Go versions 1.15 and below. 7 | 8 | Also, Opening with a URL scheme is not supported. 9 | */ 10 | package iofs 11 | -------------------------------------------------------------------------------- /source/iofs/example_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.16 2 | 3 | package iofs_test 4 | 5 | import ( 6 | "embed" 7 | "log" 8 | 9 | "github.com/golang-migrate/migrate/v4" 10 | _ "github.com/golang-migrate/migrate/v4/database/postgres" 11 | "github.com/golang-migrate/migrate/v4/source/iofs" 12 | ) 13 | 14 | //go:embed testdata/migrations/*.sql 15 | var fs embed.FS 16 | 17 | func Example() { 18 | d, err := iofs.New(fs, "testdata/migrations") 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | m, err := migrate.NewWithSourceInstance("iofs", d, "postgres://postgres@localhost/postgres?sslmode=disable") 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | err = m.Up() 27 | if err != nil { 28 | // ... 29 | } 30 | // ... 31 | } 32 | -------------------------------------------------------------------------------- /source/iofs/iofs_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.16 2 | 3 | package iofs_test 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/golang-migrate/migrate/v4/source/iofs" 9 | st "github.com/golang-migrate/migrate/v4/source/testing" 10 | ) 11 | 12 | func Test(t *testing.T) { 13 | // reuse the embed.FS set in example_test.go 14 | d, err := iofs.New(fs, "testdata/migrations") 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | 19 | st.Test(t, d) 20 | } 21 | -------------------------------------------------------------------------------- /source/iofs/testdata/migrations/1_foobar.down.sql: -------------------------------------------------------------------------------- 1 | 1 down 2 | -------------------------------------------------------------------------------- /source/iofs/testdata/migrations/1_foobar.up.sql: -------------------------------------------------------------------------------- 1 | 1 up 2 | -------------------------------------------------------------------------------- /source/iofs/testdata/migrations/3_foobar.up.sql: -------------------------------------------------------------------------------- 1 | 3 up 2 | -------------------------------------------------------------------------------- /source/iofs/testdata/migrations/4_foobar.down.sql: -------------------------------------------------------------------------------- 1 | 4 down 2 | -------------------------------------------------------------------------------- /source/iofs/testdata/migrations/4_foobar.up.sql: -------------------------------------------------------------------------------- 1 | 4 up 2 | -------------------------------------------------------------------------------- /source/iofs/testdata/migrations/5_foobar.down.sql: -------------------------------------------------------------------------------- 1 | 5 down 2 | -------------------------------------------------------------------------------- /source/iofs/testdata/migrations/7_foobar.down.sql: -------------------------------------------------------------------------------- 1 | 7 down 2 | -------------------------------------------------------------------------------- /source/iofs/testdata/migrations/7_foobar.up.sql: -------------------------------------------------------------------------------- 1 | 7 up 2 | -------------------------------------------------------------------------------- /source/migration.go: -------------------------------------------------------------------------------- 1 | package source 2 | 3 | import ( 4 | "sort" 5 | ) 6 | 7 | // Direction is either up or down. 8 | type Direction string 9 | 10 | const ( 11 | Down Direction = "down" 12 | Up Direction = "up" 13 | ) 14 | 15 | // Migration is a helper struct for source drivers that need to 16 | // build the full directory tree in memory. 17 | // Migration is fully independent from migrate.Migration. 18 | type Migration struct { 19 | // Version is the version of this migration. 20 | Version uint 21 | 22 | // Identifier can be any string that helps identifying 23 | // this migration in the source. 24 | Identifier string 25 | 26 | // Direction is either Up or Down. 27 | Direction Direction 28 | 29 | // Raw holds the raw location path to this migration in source. 30 | // ReadUp and ReadDown will use this. 31 | Raw string 32 | } 33 | 34 | // Migrations wraps Migration and has an internal index 35 | // to keep track of Migration order. 36 | type Migrations struct { 37 | index uintSlice 38 | migrations map[uint]map[Direction]*Migration 39 | } 40 | 41 | func NewMigrations() *Migrations { 42 | return &Migrations{ 43 | index: make(uintSlice, 0), 44 | migrations: make(map[uint]map[Direction]*Migration), 45 | } 46 | } 47 | 48 | func (i *Migrations) Append(m *Migration) (ok bool) { 49 | if m == nil { 50 | return false 51 | } 52 | 53 | if i.migrations[m.Version] == nil { 54 | i.migrations[m.Version] = make(map[Direction]*Migration) 55 | } 56 | 57 | // reject duplicate versions 58 | if _, dup := i.migrations[m.Version][m.Direction]; dup { 59 | return false 60 | } 61 | 62 | i.migrations[m.Version][m.Direction] = m 63 | i.buildIndex() 64 | 65 | return true 66 | } 67 | 68 | func (i *Migrations) buildIndex() { 69 | i.index = make(uintSlice, 0, len(i.migrations)) 70 | for version := range i.migrations { 71 | i.index = append(i.index, version) 72 | } 73 | sort.Slice(i.index, func(x, y int) bool { 74 | return i.index[x] < i.index[y] 75 | }) 76 | } 77 | 78 | func (i *Migrations) First() (version uint, ok bool) { 79 | if len(i.index) == 0 { 80 | return 0, false 81 | } 82 | return i.index[0], true 83 | } 84 | 85 | func (i *Migrations) Prev(version uint) (prevVersion uint, ok bool) { 86 | pos := i.findPos(version) 87 | if pos >= 1 && len(i.index) > pos-1 { 88 | return i.index[pos-1], true 89 | } 90 | return 0, false 91 | } 92 | 93 | func (i *Migrations) Next(version uint) (nextVersion uint, ok bool) { 94 | pos := i.findPos(version) 95 | if pos >= 0 && len(i.index) > pos+1 { 96 | return i.index[pos+1], true 97 | } 98 | return 0, false 99 | } 100 | 101 | func (i *Migrations) Up(version uint) (m *Migration, ok bool) { 102 | if _, ok := i.migrations[version]; ok { 103 | if mx, ok := i.migrations[version][Up]; ok { 104 | return mx, true 105 | } 106 | } 107 | return nil, false 108 | } 109 | 110 | func (i *Migrations) Down(version uint) (m *Migration, ok bool) { 111 | if _, ok := i.migrations[version]; ok { 112 | if mx, ok := i.migrations[version][Down]; ok { 113 | return mx, true 114 | } 115 | } 116 | return nil, false 117 | } 118 | 119 | func (i *Migrations) findPos(version uint) int { 120 | if len(i.index) > 0 { 121 | ix := i.index.Search(version) 122 | if ix < len(i.index) && i.index[ix] == version { 123 | return ix 124 | } 125 | } 126 | return -1 127 | } 128 | 129 | type uintSlice []uint 130 | 131 | func (s uintSlice) Search(x uint) int { 132 | return sort.Search(len(s), func(i int) bool { return s[i] >= x }) 133 | } 134 | -------------------------------------------------------------------------------- /source/migration_test.go: -------------------------------------------------------------------------------- 1 | package source 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestNewMigrations(t *testing.T) { 8 | // TODO 9 | } 10 | 11 | func TestAppend(t *testing.T) { 12 | // TODO 13 | } 14 | 15 | func TestBuildIndex(t *testing.T) { 16 | // TODO 17 | } 18 | 19 | func TestFirst(t *testing.T) { 20 | // TODO 21 | } 22 | 23 | func TestPrev(t *testing.T) { 24 | // TODO 25 | } 26 | 27 | func TestUp(t *testing.T) { 28 | // TODO 29 | } 30 | 31 | func TestDown(t *testing.T) { 32 | // TODO 33 | } 34 | 35 | func TestFindPos(t *testing.T) { 36 | m := Migrations{index: uintSlice{1, 2, 3}} 37 | if p := m.findPos(0); p != -1 { 38 | t.Errorf("expected -1, got %v", p) 39 | } 40 | if p := m.findPos(1); p != 0 { 41 | t.Errorf("expected 0, got %v", p) 42 | } 43 | if p := m.findPos(3); p != 2 { 44 | t.Errorf("expected 2, got %v", p) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /source/parse.go: -------------------------------------------------------------------------------- 1 | package source 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | ) 8 | 9 | var ( 10 | ErrParse = fmt.Errorf("no match") 11 | ) 12 | 13 | var ( 14 | DefaultParse = Parse 15 | DefaultRegex = Regex 16 | ) 17 | 18 | // Regex matches the following pattern: 19 | // 20 | // 123_name.up.ext 21 | // 123_name.down.ext 22 | var Regex = regexp.MustCompile(`^([0-9]+)_(.*)\.(` + string(Down) + `|` + string(Up) + `)\.(.*)$`) 23 | 24 | // Parse returns Migration for matching Regex pattern. 25 | func Parse(raw string) (*Migration, error) { 26 | m := Regex.FindStringSubmatch(raw) 27 | if len(m) == 5 { 28 | versionUint64, err := strconv.ParseUint(m[1], 10, 64) 29 | if err != nil { 30 | return nil, err 31 | } 32 | return &Migration{ 33 | Version: uint(versionUint64), 34 | Identifier: m[2], 35 | Direction: Direction(m[3]), 36 | Raw: raw, 37 | }, nil 38 | } 39 | return nil, ErrParse 40 | } 41 | -------------------------------------------------------------------------------- /source/parse_test.go: -------------------------------------------------------------------------------- 1 | package source 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestParse(t *testing.T) { 8 | tt := []struct { 9 | name string 10 | expectErr error 11 | expectMigration *Migration 12 | }{ 13 | { 14 | name: "1_foobar.up.sql", 15 | expectErr: nil, 16 | expectMigration: &Migration{ 17 | Version: 1, 18 | Identifier: "foobar", 19 | Direction: Up, 20 | Raw: "1_foobar.up.sql", 21 | }, 22 | }, 23 | { 24 | name: "1_foobar.down.sql", 25 | expectErr: nil, 26 | expectMigration: &Migration{ 27 | Version: 1, 28 | Identifier: "foobar", 29 | Direction: Down, 30 | Raw: "1_foobar.down.sql", 31 | }, 32 | }, 33 | { 34 | name: "1_f-o_ob+ar.up.sql", 35 | expectErr: nil, 36 | expectMigration: &Migration{ 37 | Version: 1, 38 | Identifier: "f-o_ob+ar", 39 | Direction: Up, 40 | Raw: "1_f-o_ob+ar.up.sql", 41 | }, 42 | }, 43 | { 44 | name: "1485385885_foobar.up.sql", 45 | expectErr: nil, 46 | expectMigration: &Migration{ 47 | Version: 1485385885, 48 | Identifier: "foobar", 49 | Direction: Up, 50 | Raw: "1485385885_foobar.up.sql", 51 | }, 52 | }, 53 | { 54 | name: "20170412214116_date_foobar.up.sql", 55 | expectErr: nil, 56 | expectMigration: &Migration{ 57 | Version: 20170412214116, 58 | Identifier: "date_foobar", 59 | Direction: Up, 60 | Raw: "20170412214116_date_foobar.up.sql", 61 | }, 62 | }, 63 | { 64 | name: "-1_foobar.up.sql", 65 | expectErr: ErrParse, 66 | expectMigration: nil, 67 | }, 68 | { 69 | name: "foobar.up.sql", 70 | expectErr: ErrParse, 71 | expectMigration: nil, 72 | }, 73 | { 74 | name: "1.up.sql", 75 | expectErr: ErrParse, 76 | expectMigration: nil, 77 | }, 78 | { 79 | name: "1_foobar.sql", 80 | expectErr: ErrParse, 81 | expectMigration: nil, 82 | }, 83 | { 84 | name: "1_foobar.up", 85 | expectErr: ErrParse, 86 | expectMigration: nil, 87 | }, 88 | { 89 | name: "1_foobar.down", 90 | expectErr: ErrParse, 91 | expectMigration: nil, 92 | }, 93 | } 94 | 95 | for i, v := range tt { 96 | f, err := Parse(v.name) 97 | 98 | if err != v.expectErr { 99 | t.Errorf("expected %v, got %v, in %v", v.expectErr, err, i) 100 | } 101 | 102 | if v.expectMigration != nil && *f != *v.expectMigration { 103 | t.Errorf("expected %+v, got %+v, in %v", *v.expectMigration, *f, i) 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /source/pkger/README.md: -------------------------------------------------------------------------------- 1 | # pkger 2 | ```go 3 | package main 4 | 5 | import ( 6 | "errors" 7 | "log" 8 | 9 | "github.com/golang-migrate/migrate/v4" 10 | "github.com/markbates/pkger" 11 | 12 | _ "github.com/golang-migrate/migrate/v4/database/postgres" 13 | _ "github.com/golang-migrate/migrate/v4/source/pkger" 14 | _ "github.com/lib/pq" 15 | ) 16 | 17 | func main() { 18 | pkger.Include("/module/path/to/migrations") 19 | m, err := migrate.New("pkger:///module/path/to/migrations", "postgres://postgres@localhost/postgres?sslmode=disable") 20 | if err != nil { 21 | log.Fatalln(err) 22 | } 23 | if err := m.Up(); errors.Is(err, migrate.ErrNoChange) { 24 | log.Println(err) 25 | } else if err != nil { 26 | log.Fatalln(err) 27 | } 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /source/pkger/pkger.go: -------------------------------------------------------------------------------- 1 | package pkger 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | stdurl "net/url" 7 | 8 | "github.com/golang-migrate/migrate/v4/source" 9 | "github.com/golang-migrate/migrate/v4/source/httpfs" 10 | "github.com/markbates/pkger" 11 | "github.com/markbates/pkger/pkging" 12 | ) 13 | 14 | func init() { 15 | source.Register("pkger", &Pkger{}) 16 | } 17 | 18 | // Pkger is a source.Driver that reads migrations from instances of 19 | // pkging.Pkger. 20 | type Pkger struct { 21 | httpfs.PartialDriver 22 | } 23 | 24 | // Open implements source.Driver. The path component of url will be used as the 25 | // relative location of migrations. The returned driver will use the package 26 | // scoped pkger.Open to access migrations. The relative root and any 27 | // migrations must be added to the global pkger.Pkger instance by calling 28 | // pkger.Apply. Refer to Pkger documentation for more information. 29 | func (p *Pkger) Open(url string) (source.Driver, error) { 30 | u, err := stdurl.Parse(url) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | // wrap pkger to implement http.FileSystem. 36 | fs := fsFunc(func(name string) (http.File, error) { 37 | f, err := pkger.Open(name) 38 | if err != nil { 39 | return nil, err 40 | } 41 | return f.(http.File), nil 42 | }) 43 | 44 | if err := p.Init(fs, u.Path); err != nil { 45 | return nil, fmt.Errorf("failed to init driver with relative path %q: %w", u.Path, err) 46 | } 47 | 48 | return p, nil 49 | } 50 | 51 | // WithInstance returns a source.Driver that is backed by an instance of 52 | // pkging.Pkger. The relative location of migrations is indicated by path. The 53 | // path must exist on the pkging.Pkger instance for the driver to initialize 54 | // successfully. 55 | func WithInstance(instance pkging.Pkger, path string) (source.Driver, error) { 56 | if instance == nil { 57 | return nil, fmt.Errorf("expected instance of pkging.Pkger") 58 | } 59 | 60 | // wrap pkger to implement http.FileSystem. 61 | fs := fsFunc(func(name string) (http.File, error) { 62 | f, err := instance.Open(name) 63 | if err != nil { 64 | return nil, err 65 | } 66 | return f.(http.File), nil 67 | }) 68 | 69 | var p Pkger 70 | 71 | if err := p.Init(fs, path); err != nil { 72 | return nil, fmt.Errorf("failed to init driver with relative path %q: %w", path, err) 73 | } 74 | 75 | return &p, nil 76 | } 77 | 78 | type fsFunc func(name string) (http.File, error) 79 | 80 | // Open implements http.FileSystem. 81 | func (f fsFunc) Open(name string) (http.File, error) { 82 | return f(name) 83 | } 84 | -------------------------------------------------------------------------------- /source/stub/stub.go: -------------------------------------------------------------------------------- 1 | package stub 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | 9 | "github.com/golang-migrate/migrate/v4/source" 10 | ) 11 | 12 | func init() { 13 | source.Register("stub", &Stub{}) 14 | } 15 | 16 | type Config struct{} 17 | 18 | // d, _ := source.Open("stub://") 19 | // d.(*stub.Stub).Migrations = 20 | 21 | type Stub struct { 22 | Url string 23 | Instance interface{} 24 | Migrations *source.Migrations 25 | Config *Config 26 | } 27 | 28 | func (s *Stub) Open(url string) (source.Driver, error) { 29 | return &Stub{ 30 | Url: url, 31 | Migrations: source.NewMigrations(), 32 | Config: &Config{}, 33 | }, nil 34 | } 35 | 36 | func WithInstance(instance interface{}, config *Config) (source.Driver, error) { 37 | return &Stub{ 38 | Instance: instance, 39 | Migrations: source.NewMigrations(), 40 | Config: config, 41 | }, nil 42 | } 43 | 44 | func (s *Stub) Close() error { 45 | return nil 46 | } 47 | 48 | func (s *Stub) First() (version uint, err error) { 49 | if v, ok := s.Migrations.First(); !ok { 50 | return 0, &os.PathError{Op: "first", Path: s.Url, Err: os.ErrNotExist} // TODO: s.Url can be empty when called with WithInstance 51 | } else { 52 | return v, nil 53 | } 54 | } 55 | 56 | func (s *Stub) Prev(version uint) (prevVersion uint, err error) { 57 | if v, ok := s.Migrations.Prev(version); !ok { 58 | return 0, &os.PathError{Op: fmt.Sprintf("prev for version %v", version), Path: s.Url, Err: os.ErrNotExist} 59 | } else { 60 | return v, nil 61 | } 62 | } 63 | 64 | func (s *Stub) Next(version uint) (nextVersion uint, err error) { 65 | if v, ok := s.Migrations.Next(version); !ok { 66 | return 0, &os.PathError{Op: fmt.Sprintf("next for version %v", version), Path: s.Url, Err: os.ErrNotExist} 67 | } else { 68 | return v, nil 69 | } 70 | } 71 | 72 | func (s *Stub) ReadUp(version uint) (r io.ReadCloser, identifier string, err error) { 73 | if m, ok := s.Migrations.Up(version); ok { 74 | return io.NopCloser(bytes.NewBufferString(m.Identifier)), fmt.Sprintf("%v.up.stub", version), nil 75 | } 76 | return nil, "", &os.PathError{Op: fmt.Sprintf("read up version %v", version), Path: s.Url, Err: os.ErrNotExist} 77 | } 78 | 79 | func (s *Stub) ReadDown(version uint) (r io.ReadCloser, identifier string, err error) { 80 | if m, ok := s.Migrations.Down(version); ok { 81 | return io.NopCloser(bytes.NewBufferString(m.Identifier)), fmt.Sprintf("%v.down.stub", version), nil 82 | } 83 | return nil, "", &os.PathError{Op: fmt.Sprintf("read down version %v", version), Path: s.Url, Err: os.ErrNotExist} 84 | } 85 | -------------------------------------------------------------------------------- /source/stub/stub_test.go: -------------------------------------------------------------------------------- 1 | package stub 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/golang-migrate/migrate/v4/source" 7 | st "github.com/golang-migrate/migrate/v4/source/testing" 8 | ) 9 | 10 | func Test(t *testing.T) { 11 | s := &Stub{} 12 | d, err := s.Open("") 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | 17 | m := source.NewMigrations() 18 | m.Append(&source.Migration{Version: 1, Direction: source.Up}) 19 | m.Append(&source.Migration{Version: 1, Direction: source.Down}) 20 | m.Append(&source.Migration{Version: 3, Direction: source.Up}) 21 | m.Append(&source.Migration{Version: 4, Direction: source.Up}) 22 | m.Append(&source.Migration{Version: 4, Direction: source.Down}) 23 | m.Append(&source.Migration{Version: 5, Direction: source.Down}) 24 | m.Append(&source.Migration{Version: 7, Direction: source.Up}) 25 | m.Append(&source.Migration{Version: 7, Direction: source.Down}) 26 | 27 | d.(*Stub).Migrations = m 28 | 29 | st.Test(t, d) 30 | } 31 | -------------------------------------------------------------------------------- /testing/testing.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "strconv" 7 | "testing" 8 | "time" 9 | 10 | dockertypes "github.com/docker/docker/api/types" 11 | ) 12 | 13 | type IsReadyFunc func(Instance) bool 14 | 15 | type TestFunc func(*testing.T, Instance) 16 | 17 | type Version struct { 18 | Image string 19 | ENV []string 20 | Cmd []string 21 | } 22 | 23 | func ParallelTest(t *testing.T, versions []Version, readyFn IsReadyFunc, testFn TestFunc) { 24 | timeout, err := strconv.Atoi(os.Getenv("MIGRATE_TEST_CONTAINER_BOOT_TIMEOUT")) 25 | if err != nil { 26 | timeout = 60 // Cassandra docker image can take ~30s to start 27 | } 28 | 29 | for i, version := range versions { 30 | version := version // capture range variable, see https://goo.gl/60w3p2 31 | 32 | // Only test against one version in short mode 33 | // TODO: order is random, maybe always pick first version instead? 34 | if i > 0 && testing.Short() { 35 | t.Logf("Skipping %v in short mode", version) 36 | 37 | } else { 38 | t.Run(version.Image, func(t *testing.T) { 39 | t.Parallel() 40 | 41 | // create new container 42 | container, err := NewDockerContainer(t, version.Image, version.ENV, version.Cmd) 43 | if err != nil { 44 | t.Fatalf("%v\n%s", err, containerLogs(t, container)) 45 | } 46 | 47 | // make sure to remove container once done 48 | defer func() { 49 | if err := container.Remove(); err != nil { 50 | t.Error(err) 51 | } 52 | }() 53 | // wait until database is ready 54 | tick := time.NewTicker(1000 * time.Millisecond) 55 | defer tick.Stop() 56 | timeout := time.NewTimer(time.Duration(timeout) * time.Second) 57 | defer timeout.Stop() 58 | outer: 59 | for { 60 | select { 61 | case <-tick.C: 62 | if readyFn(container) { 63 | break outer 64 | } 65 | 66 | case <-timeout.C: 67 | t.Fatalf("Docker: Container not ready, timeout for %v.\n%s", version, containerLogs(t, container)) 68 | } 69 | } 70 | 71 | // we can now run the tests 72 | testFn(t, container) 73 | }) 74 | } 75 | } 76 | } 77 | 78 | func containerLogs(t *testing.T, c *DockerContainer) []byte { 79 | r, err := c.Logs() 80 | if err != nil { 81 | t.Error(err) 82 | return nil 83 | } 84 | defer func() { 85 | if err := r.Close(); err != nil { 86 | t.Error(err) 87 | } 88 | }() 89 | b, err := io.ReadAll(r) 90 | if err != nil { 91 | t.Error(err) 92 | return nil 93 | } 94 | return b 95 | } 96 | 97 | type Instance interface { 98 | Host() string 99 | Port() uint 100 | PortFor(int) uint 101 | NetworkSettings() dockertypes.NetworkSettings 102 | KeepForDebugging() 103 | } 104 | -------------------------------------------------------------------------------- /testing/testing_test.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func ExampleParallelTest() { 8 | t := &testing.T{} // Should actually be used in a Test 9 | 10 | var isReady = func(i Instance) bool { 11 | // Return true if Instance is ready to run tests. 12 | // Don't block here though. 13 | return true 14 | } 15 | 16 | // t is *testing.T coming from parent Test(t *testing.T) 17 | ParallelTest(t, []Version{{Image: "docker_image:9.6"}}, isReady, 18 | func(t *testing.T, i Instance) { 19 | // Run your test/s ... 20 | t.Fatal("...") 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package migrate 2 | 3 | import ( 4 | "fmt" 5 | nurl "net/url" 6 | "strings" 7 | ) 8 | 9 | // MultiError holds multiple errors. 10 | // 11 | // Deprecated: Use github.com/hashicorp/go-multierror instead 12 | type MultiError struct { 13 | Errs []error 14 | } 15 | 16 | // NewMultiError returns an error type holding multiple errors. 17 | // 18 | // Deprecated: Use github.com/hashicorp/go-multierror instead 19 | func NewMultiError(errs ...error) MultiError { 20 | compactErrs := make([]error, 0) 21 | for _, e := range errs { 22 | if e != nil { 23 | compactErrs = append(compactErrs, e) 24 | } 25 | } 26 | return MultiError{compactErrs} 27 | } 28 | 29 | // Error implements error. Multiple errors are concatenated with 'and's. 30 | func (m MultiError) Error() string { 31 | var strs = make([]string, 0) 32 | for _, e := range m.Errs { 33 | if len(e.Error()) > 0 { 34 | strs = append(strs, e.Error()) 35 | } 36 | } 37 | return strings.Join(strs, " and ") 38 | } 39 | 40 | // suint safely converts int to uint 41 | // see https://goo.gl/wEcqof 42 | // see https://goo.gl/pai7Dr 43 | func suint(n int) uint { 44 | if n < 0 { 45 | panic(fmt.Sprintf("suint(%v) expects input >= 0", n)) 46 | } 47 | return uint(n) 48 | } 49 | 50 | // FilterCustomQuery filters all query values starting with `x-` 51 | func FilterCustomQuery(u *nurl.URL) *nurl.URL { 52 | ux := *u 53 | vx := make(nurl.Values) 54 | for k, v := range ux.Query() { 55 | if len(k) <= 1 || k[0:2] != "x-" { 56 | vx[k] = v 57 | } 58 | } 59 | ux.RawQuery = vx.Encode() 60 | return &ux 61 | } 62 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | package migrate 2 | 3 | import ( 4 | nurl "net/url" 5 | "testing" 6 | ) 7 | 8 | func TestSuintPanicsWithNegativeInput(t *testing.T) { 9 | defer func() { 10 | if r := recover(); r == nil { 11 | t.Fatal("expected suint to panic for -1") 12 | } 13 | }() 14 | suint(-1) 15 | } 16 | 17 | func TestSuint(t *testing.T) { 18 | if u := suint(0); u != 0 { 19 | t.Fatalf("expected 0, got %v", u) 20 | } 21 | } 22 | 23 | func TestFilterCustomQuery(t *testing.T) { 24 | n, err := nurl.Parse("foo://host?a=b&x-custom=foo&c=d&ok=y") 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | nx := FilterCustomQuery(n).Query() 29 | if nx.Get("x-custom") != "" { 30 | t.Fatalf("didn't expect x-custom") 31 | } 32 | if nx.Get("ok") != "y" { 33 | t.Fatalf("expected ok=y, got %v", nx.Get("ok")) 34 | } 35 | } 36 | --------------------------------------------------------------------------------