├── .dockerignore ├── .github └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .golangci.yaml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── bindata_test.go ├── doc.go ├── go.mod ├── go.sum ├── init_test.go ├── migrate.go ├── migrate_test.go ├── sort_test.go ├── sql-migrate ├── command_common.go ├── command_down.go ├── command_new.go ├── command_redo.go ├── command_skip.go ├── command_status.go ├── command_up.go ├── config.go ├── godror.go ├── main.go ├── main_test.go ├── mssql.go └── oracle.go ├── sqlparse ├── LICENSE ├── README.md ├── sqlparse.go └── sqlparse_test.go ├── test-integration ├── dbconfig.yml ├── mysql-env.sh ├── mysql-flag.sh ├── mysql.sh ├── postgres.sh └── sqlite.sh ├── test-migrations ├── 1_initial.sql └── 2_record.sql └── toapply_test.go /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !go.mod 3 | !go.sum 4 | !*.go 5 | !sql-migrate/*.go 6 | !sqlparse/*.go 7 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | releases-matrix: 9 | name: Release Go Binary 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | goos: [linux, windows, darwin] 14 | goarch: [amd64] 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | - uses: wangyoucao577/go-release-action@v1.53 19 | with: 20 | github_token: ${{ secrets.GITHUB_TOKEN }} 21 | goos: ${{ matrix.goos }} 22 | goarch: ${{ matrix.goarch }} 23 | goversion: 1.23 24 | pre_command: export CGO_ENABLED=0 25 | project_path: "./sql-migrate" 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | jobs: 8 | test: 9 | strategy: 10 | fail-fast: true 11 | matrix: 12 | go-version: ["1.23", "1.24"] 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: checkout 16 | uses: actions/checkout@v4 17 | - name: setup-go 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version: ${{ matrix.go-version }} 21 | cache: true 22 | cache-dependency-path: go.sum 23 | - name: go build 24 | run: go build -o ./bin/sql-migrate ./sql-migrate && ./bin/sql-migrate --help 25 | - name: go test 26 | run: go test ./... 27 | lint: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: checkout 31 | uses: actions/checkout@v4 32 | - name: setup-go 33 | uses: actions/setup-go@v5 34 | with: 35 | go-version: "1.24" 36 | cache: true 37 | cache-dependency-path: go.sum 38 | - name: golangci-lint 39 | uses: golangci/golangci-lint-action@v7 40 | with: 41 | version: v2.1 42 | - name: go mod tidy 43 | run: go mod tidy 44 | - name: check for any changes 45 | run: | 46 | [[ $(git status --porcelain) == "" ]] || (echo "changes detected" && exit 1) 47 | integration: 48 | needs: 49 | - test 50 | - lint 51 | runs-on: ubuntu-latest 52 | strategy: 53 | fail-fast: true 54 | matrix: 55 | go-version: ["1.23", "1.24"] 56 | services: 57 | mysql: 58 | image: mysql:8.0 59 | env: 60 | MYSQL_ALLOW_EMPTY_PASSWORD: "1" 61 | MYSQL_ROOT_PASSWORD: "" 62 | MYSQL_DATABASE: "test" 63 | ports: 64 | - 3306:3306 65 | options: >- 66 | --health-cmd="mysqladmin ping" 67 | --health-interval=10s 68 | --health-timeout=5s 69 | --health-retries=3 70 | postgres: 71 | image: postgres:15 72 | env: 73 | POSTGRES_PASSWORD: "password" 74 | ports: 75 | - 5432:5432 76 | options: >- 77 | --health-cmd pg_isready 78 | --health-interval 10s 79 | --health-timeout 5s 80 | --health-retries 5 81 | env: 82 | MYSQL_HOST: "127.0.0.1" 83 | PGHOST: "127.0.0.1" 84 | PGUSER: "postgres" 85 | PGPASSWORD: "password" 86 | steps: 87 | - name: checkout 88 | uses: actions/checkout@v4 89 | - name: setup-go 90 | uses: actions/setup-go@v5 91 | with: 92 | go-version: ${{ matrix.go-version }} 93 | cache: true 94 | cache-dependency-path: go.sum 95 | - name: setup databases 96 | run: | 97 | mysql --user=root -e 'CREATE DATABASE IF NOT EXISTS test;' 98 | mysql --user=root -e 'CREATE DATABASE IF NOT EXISTS test_env;' 99 | psql -U postgres -c 'CREATE DATABASE test;' 100 | - name: install sql-migrate 101 | run: go install ./... 102 | - name: postgres 103 | run: bash ./test-integration/postgres.sh 104 | - name: mysql 105 | run: bash ./test-integration/mysql.sh 106 | - name: mysql-flag 107 | run: bash ./test-integration/mysql-flag.sh 108 | - name: mysql-env 109 | run: bash ./test-integration/mysql-env.sh 110 | - name: sqlite 111 | run: bash ./test-integration/sqlite.sh 112 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | *.test 3 | .idea 4 | /vendor/ 5 | 6 | /sql-migrate/test.db 7 | /test.db 8 | .vscode/ 9 | bin/ 10 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | run: 3 | tests: true 4 | linters: 5 | default: none 6 | enable: 7 | - asciicheck 8 | - depguard 9 | - errcheck 10 | - errorlint 11 | - exhaustive 12 | - gocritic 13 | - govet 14 | - ineffassign 15 | - nolintlint 16 | - revive 17 | - staticcheck 18 | - unparam 19 | - unused 20 | - whitespace 21 | settings: 22 | depguard: 23 | rules: 24 | main: 25 | allow: 26 | - $gostd 27 | - github.com/denisenkom/go-mssqldb 28 | - github.com/go-sql-driver/mysql 29 | - github.com/go-gorp/gorp/v3 30 | - github.com/lib/pq 31 | - github.com/mattn/go-sqlite3 32 | - github.com/mitchellh/cli 33 | - github.com/olekukonko/tablewriter 34 | - github.com/rubenv/sql-migrate 35 | - gopkg.in/check.v1 36 | - gopkg.in/yaml.v2 37 | exhaustive: 38 | default-signifies-exhaustive: true 39 | gocritic: 40 | disabled-checks: 41 | - ifElseChain 42 | govet: 43 | disable: 44 | - fieldalignment 45 | enable-all: true 46 | nolintlint: 47 | require-explanation: true 48 | require-specific: true 49 | allow-no-explanation: 50 | - depguard 51 | allow-unused: false 52 | revive: 53 | enable-all-rules: false 54 | rules: 55 | - name: atomic 56 | - name: blank-imports 57 | - name: bool-literal-in-expr 58 | - name: call-to-gc 59 | - name: constant-logical-expr 60 | - name: context-as-argument 61 | - name: context-keys-type 62 | - name: dot-imports 63 | - name: duplicated-imports 64 | - name: empty-block 65 | - name: empty-lines 66 | - name: error-naming 67 | - name: error-return 68 | - name: error-strings 69 | - name: errorf 70 | - name: exported 71 | - name: identical-branches 72 | - name: imports-blocklist 73 | - name: increment-decrement 74 | - name: indent-error-flow 75 | - name: modifies-parameter 76 | - name: modifies-value-receiver 77 | - name: package-comments 78 | - name: range 79 | - name: range-val-address 80 | - name: range-val-in-closure 81 | - name: receiver-naming 82 | - name: string-format 83 | - name: string-of-int 84 | - name: struct-tag 85 | - name: time-naming 86 | - name: unconditional-recursion 87 | - name: unexported-naming 88 | - name: unexported-return 89 | - name: superfluous-else 90 | - name: unreachable-code 91 | - name: var-declaration 92 | - name: waitgroup-by-value 93 | - name: unused-receiver 94 | - name: unnecessary-stmt 95 | - name: unused-parameter 96 | exclusions: 97 | generated: lax 98 | presets: 99 | - comments 100 | - common-false-positives 101 | - legacy 102 | - std-error-handling 103 | rules: 104 | - path: (.+)\.go$ 105 | text: declaration of "err" shadows declaration at 106 | - path: (.+)\.go$ 107 | text: 'error-strings: error strings should not be capitalized or end with punctuation or a newline' 108 | - path: (.+)\.go$ 109 | text: 'ST1005: error strings should not end with punctuation or newline' 110 | - path: (.+)\.go$ 111 | text: 'ST1005: error strings should not be capitalized' 112 | paths: 113 | - third_party$ 114 | - builtin$ 115 | - examples$ 116 | issues: 117 | max-issues-per-linter: 10000 118 | max-same-issues: 10000 119 | formatters: 120 | enable: 121 | - gofmt 122 | - gofumpt 123 | - goimports 124 | settings: 125 | goimports: 126 | local-prefixes: 127 | - github.com/rubenv/sql-migrate 128 | exclusions: 129 | generated: lax 130 | paths: 131 | - third_party$ 132 | - builtin$ 133 | - examples$ 134 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG GO_VERSION=1.20.6 2 | ARG ALPINE_VERSION=3.12 3 | 4 | ### Vendor 5 | FROM golang:${GO_VERSION} as vendor 6 | COPY . /project 7 | WORKDIR /project 8 | RUN go mod tidy && go mod vendor 9 | 10 | ### Build binary 11 | FROM golang:${GO_VERSION} as build-binary 12 | COPY . /project 13 | COPY --from=vendor /project/vendor /project/vendor 14 | WORKDIR /project 15 | RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 GO111MODULE=on go build \ 16 | -v \ 17 | -mod vendor \ 18 | -o /project/bin/sql-migrate \ 19 | /project/sql-migrate 20 | 21 | ### Image 22 | FROM alpine:${ALPINE_VERSION} as image 23 | COPY --from=build-binary /project/bin/sql-migrate /usr/local/bin/sql-migrate 24 | RUN chmod +x /usr/local/bin/sql-migrate 25 | ENTRYPOINT ["sql-migrate"] 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (C) 2014-2021 by Ruben Vermeersch 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test lint build 2 | 3 | test: 4 | go test ./... 5 | 6 | lint: 7 | golangci-lint run --fix --config .golangci.yaml 8 | 9 | build: 10 | mkdir -p bin 11 | go build -o ./bin/sql-migrate ./sql-migrate 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sql-migrate 2 | 3 | > SQL Schema migration tool for [Go](https://golang.org/). Based on [gorp](https://github.com/go-gorp/gorp) and [goose](https://bitbucket.org/liamstask/goose). 4 | 5 | [![Test](https://github.com/rubenv/sql-migrate/actions/workflows/test.yml/badge.svg)](https://github.com/rubenv/sql-migrate/actions/workflows/test.yml) [![Go Reference](https://pkg.go.dev/badge/github.com/rubenv/sql-migrate.svg)](https://pkg.go.dev/github.com/rubenv/sql-migrate) 6 | 7 | ## Features 8 | 9 | - Usable as a CLI tool or as a library 10 | - Supports SQLite, PostgreSQL, MySQL, MSSQL and Oracle databases (through [gorp](https://github.com/go-gorp/gorp)) 11 | - Can embed migrations into your application 12 | - Migrations are defined with SQL for full flexibility 13 | - Atomic migrations 14 | - Up/down migrations to allow rollback 15 | - Supports multiple database types in one project 16 | - Works great with other libraries such as [sqlx](https://jmoiron.github.io/sqlx/) 17 | - Supported on go1.13+ 18 | 19 | ## Installation 20 | 21 | To install the library and command line program, use the following: 22 | 23 | ```bash 24 | go get -v github.com/rubenv/sql-migrate/... 25 | ``` 26 | 27 | For Go version from 1.18, use: 28 | 29 | ```bash 30 | go install github.com/rubenv/sql-migrate/...@latest 31 | ``` 32 | 33 | ## Usage 34 | 35 | ### As a standalone tool 36 | 37 | ``` 38 | $ sql-migrate --help 39 | usage: sql-migrate [--version] [--help] [] 40 | 41 | Available commands are: 42 | down Undo a database migration 43 | new Create a new migration 44 | redo Reapply the last migration 45 | status Show migration status 46 | up Migrates the database to the most recent version available 47 | ``` 48 | 49 | Each command requires a configuration file (which defaults to `dbconfig.yml`, but can be specified with the `-config` flag). This config file should specify one or more environments: 50 | 51 | ```yml 52 | development: 53 | dialect: sqlite3 54 | datasource: test.db 55 | dir: migrations/sqlite3 56 | 57 | production: 58 | dialect: postgres 59 | datasource: dbname=myapp sslmode=disable 60 | dir: migrations/postgres 61 | table: migrations 62 | ``` 63 | 64 | (See more examples for different set ups [here](test-integration/dbconfig.yml)) 65 | 66 | Also one can obtain env variables in datasource field via `os.ExpandEnv` embedded call for the field. 67 | This may be useful if one doesn't want to store credentials in file: 68 | 69 | ```yml 70 | production: 71 | dialect: postgres 72 | datasource: host=prodhost dbname=proddb user=${DB_USER} password=${DB_PASSWORD} sslmode=require 73 | dir: migrations 74 | table: migrations 75 | ``` 76 | 77 | The `table` setting is optional and will default to `gorp_migrations`. 78 | 79 | The environment that will be used can be specified with the `-env` flag (defaults to `development`). 80 | 81 | Use the `--help` flag in combination with any of the commands to get an overview of its usage: 82 | 83 | ``` 84 | $ sql-migrate up --help 85 | Usage: sql-migrate up [options] ... 86 | 87 | Migrates the database to the most recent version available. 88 | 89 | Options: 90 | 91 | -config=dbconfig.yml Configuration file to use. 92 | -env="development" Environment. 93 | -limit=0 Limit the number of migrations (0 = unlimited). 94 | -version Run migrate up to a specific version, eg: the version number of migration 1_initial.sql is 1. 95 | -dryrun Don't apply migrations, just print them. 96 | ``` 97 | 98 | The `new` command creates a new empty migration template using the following pattern `-.sql`. 99 | 100 | The `up` command applies all available migrations. By contrast, `down` will only apply one migration by default. This behavior can be changed for both by using the `-limit` parameter, and the `-version` parameter. Note `-version` has higher priority than `-limit` if you try to use them both. 101 | 102 | The `redo` command will unapply the last migration and reapply it. This is useful during development, when you're writing migrations. 103 | 104 | Use the `status` command to see the state of the applied migrations: 105 | 106 | ```bash 107 | $ sql-migrate status 108 | +---------------+-----------------------------------------+ 109 | | MIGRATION | APPLIED | 110 | +---------------+-----------------------------------------+ 111 | | 1_initial.sql | 2014-09-13 08:19:06.788354925 +0000 UTC | 112 | | 2_record.sql | no | 113 | +---------------+-----------------------------------------+ 114 | ``` 115 | 116 | #### Running Test Integrations 117 | 118 | You can see how to run setups for different setups by executing the `.sh` files in [test-integration](test-integration/) 119 | 120 | ```bash 121 | # Run mysql-env.sh example (you need to be in the project root directory) 122 | 123 | ./test-integration/mysql-env.sh 124 | ``` 125 | 126 | ### MySQL Caveat 127 | 128 | If you are using MySQL, you must append `?parseTime=true` to the `datasource` configuration. For example: 129 | 130 | ```yml 131 | production: 132 | dialect: mysql 133 | datasource: root@/dbname?parseTime=true 134 | dir: migrations/mysql 135 | table: migrations 136 | ``` 137 | 138 | See [here](https://github.com/go-sql-driver/mysql#parsetime) for more information. 139 | 140 | ### Oracle (oci8) 141 | 142 | Oracle Driver is [oci8](https://github.com/mattn/go-oci8), it is not pure Go code and relies on Oracle Office Client ([Instant Client](https://www.oracle.com/database/technologies/instant-client/downloads.html)), more detailed information is in the [oci8 repo](https://github.com/mattn/go-oci8). 143 | 144 | #### Install with Oracle support 145 | 146 | To install the library and command line program, use the following: 147 | 148 | ```bash 149 | go get -tags oracle -v github.com/rubenv/sql-migrate/... 150 | ``` 151 | 152 | ```yml 153 | development: 154 | dialect: oci8 155 | datasource: user/password@localhost:1521/sid 156 | dir: migrations/oracle 157 | table: migrations 158 | ``` 159 | 160 | ### Oracle (godror) 161 | 162 | Oracle Driver is [godror](https://github.com/godror/godror), it is not pure Go code and relies on Oracle Office Client ([Instant Client](https://www.oracle.com/database/technologies/instant-client/downloads.html)), more detailed information is in the [godror repository](https://github.com/godror/godror). 163 | 164 | #### Install with Oracle support 165 | 166 | To install the library and command line program, use the following: 167 | 168 | 1. Install sql-migrate 169 | 170 | ```bash 171 | go get -tags godror -v github.com/rubenv/sql-migrate/... 172 | ``` 173 | 174 | 2. Download Oracle Office Client(e.g. macos, click [Instant Client](https://www.oracle.com/database/technologies/instant-client/downloads.html) if you are other system) 175 | 176 | ```bash 177 | wget https://download.oracle.com/otn_software/mac/instantclient/193000/instantclient-basic-macos.x64-19.3.0.0.0dbru.zip 178 | ``` 179 | 180 | 3. Configure environment variables `LD_LIBRARY_PATH` 181 | 182 | ``` 183 | export LD_LIBRARY_PATH=your_oracle_office_path/instantclient_19_3 184 | ``` 185 | 186 | ```yml 187 | development: 188 | dialect: godror 189 | datasource: user/password@localhost:1521/sid 190 | dir: migrations/oracle 191 | table: migrations 192 | ``` 193 | 194 | ### As a library 195 | 196 | Import sql-migrate into your application: 197 | 198 | ```go 199 | import "github.com/rubenv/sql-migrate" 200 | ``` 201 | 202 | Set up a source of migrations, this can be from memory, from a set of files, from bindata (more on that later), or from any library that implements [`http.FileSystem`](https://godoc.org/net/http#FileSystem): 203 | 204 | ```go 205 | // Hardcoded strings in memory: 206 | migrations := &migrate.MemoryMigrationSource{ 207 | Migrations: []*migrate.Migration{ 208 | &migrate.Migration{ 209 | Id: "123", 210 | Up: []string{"CREATE TABLE people (id int)"}, 211 | Down: []string{"DROP TABLE people"}, 212 | }, 213 | }, 214 | } 215 | 216 | // OR: Read migrations from a folder: 217 | migrations := &migrate.FileMigrationSource{ 218 | Dir: "db/migrations", 219 | } 220 | 221 | // OR: Use migrations from a packr box 222 | // Note: Packr is no longer supported, your best option these days is [embed](https://pkg.go.dev/embed) 223 | migrations := &migrate.PackrMigrationSource{ 224 | Box: packr.New("migrations", "./migrations"), 225 | } 226 | 227 | // OR: Use pkger which implements `http.FileSystem` 228 | migrationSource := &migrate.HttpFileSystemMigrationSource{ 229 | FileSystem: pkger.Dir("/db/migrations"), 230 | } 231 | 232 | // OR: Use migrations from bindata: 233 | migrations := &migrate.AssetMigrationSource{ 234 | Asset: Asset, 235 | AssetDir: AssetDir, 236 | Dir: "migrations", 237 | } 238 | 239 | // OR: Read migrations from a `http.FileSystem` 240 | migrationSource := &migrate.HttpFileSystemMigrationSource{ 241 | FileSystem: httpFS, 242 | } 243 | ``` 244 | 245 | Then use the `Exec` function to upgrade your database: 246 | 247 | ```go 248 | db, err := sql.Open("sqlite3", filename) 249 | if err != nil { 250 | // Handle errors! 251 | } 252 | 253 | n, err := migrate.Exec(db, "sqlite3", migrations, migrate.Up) 254 | if err != nil { 255 | // Handle errors! 256 | } 257 | fmt.Printf("Applied %d migrations!\n", n) 258 | ``` 259 | 260 | Note that `n` can be greater than `0` even if there is an error: any migration that succeeded will remain applied even if a later one fails. 261 | 262 | Check [the GoDoc reference](https://godoc.org/github.com/rubenv/sql-migrate) for the full documentation. 263 | 264 | ## Writing migrations 265 | 266 | Migrations are defined in SQL files, which contain a set of SQL statements. Special comments are used to distinguish up and down migrations. 267 | 268 | ```sql 269 | -- +migrate Up 270 | -- SQL in section 'Up' is executed when this migration is applied 271 | CREATE TABLE people (id int); 272 | 273 | 274 | -- +migrate Down 275 | -- SQL section 'Down' is executed when this migration is rolled back 276 | DROP TABLE people; 277 | ``` 278 | 279 | You can put multiple statements in each block, as long as you end them with a semicolon (`;`). 280 | 281 | You can alternatively set up a separator string that matches an entire line by setting `sqlparse.LineSeparator`. This 282 | can be used to imitate, for example, MS SQL Query Analyzer functionality where commands can be separated by a line with 283 | contents of `GO`. If `sqlparse.LineSeparator` is matched, it will not be included in the resulting migration scripts. 284 | 285 | If you have complex statements which contain semicolons, use `StatementBegin` and `StatementEnd` to indicate boundaries: 286 | 287 | ```sql 288 | -- +migrate Up 289 | CREATE TABLE people (id int); 290 | 291 | -- +migrate StatementBegin 292 | CREATE OR REPLACE FUNCTION do_something() 293 | returns void AS $$ 294 | DECLARE 295 | create_query text; 296 | BEGIN 297 | -- Do something here 298 | END; 299 | $$ 300 | language plpgsql; 301 | -- +migrate StatementEnd 302 | 303 | -- +migrate Down 304 | DROP FUNCTION do_something(); 305 | DROP TABLE people; 306 | ``` 307 | 308 | The order in which migrations are applied is defined through the filename: sql-migrate will sort migrations based on their name. It's recommended to use an increasing version number or a timestamp as the first part of the filename. 309 | 310 | Normally each migration is run within a transaction in order to guarantee that it is fully atomic. However some SQL commands (for example creating an index concurrently in PostgreSQL) cannot be executed inside a transaction. In order to execute such a command in a migration, the migration can be run using the `notransaction` option: 311 | 312 | ```sql 313 | -- +migrate Up notransaction 314 | CREATE UNIQUE INDEX CONCURRENTLY people_unique_id_idx ON people (id); 315 | 316 | -- +migrate Down 317 | DROP INDEX people_unique_id_idx; 318 | ``` 319 | 320 | ## Embedding migrations with [embed](https://pkg.go.dev/embed) 321 | 322 | If you like your Go applications self-contained (that is: a single binary): use [embed](https://pkg.go.dev/embed) to embed the migration files. 323 | 324 | Just write your migration files as usual, as a set of SQL files in a folder. 325 | 326 | Import the embed package into your application and point it to your migrations: 327 | 328 | ```go 329 | import "embed" 330 | 331 | //go:embed migrations/* 332 | var dbMigrations embed.FS 333 | ``` 334 | 335 | Use the `EmbedFileSystemMigrationSource` in your application to find the migrations: 336 | 337 | ```go 338 | migrations := migrate.EmbedFileSystemMigrationSource{ 339 | FileSystem: dbMigrations, 340 | Root: "migrations", 341 | } 342 | ``` 343 | 344 | Other options such as [packr](https://github.com/gobuffalo/packr) or [go-bindata](https://github.com/shuLhan/go-bindata) are no longer recommended. 345 | 346 | ## Embedding migrations with libraries that implement `http.FileSystem` 347 | 348 | You can also embed migrations with any library that implements `http.FileSystem`, like [`vfsgen`](https://github.com/shurcooL/vfsgen), [`parcello`](https://github.com/phogolabs/parcello), or [`go-resources`](https://github.com/omeid/go-resources). 349 | 350 | ```go 351 | migrationSource := &migrate.HttpFileSystemMigrationSource{ 352 | FileSystem: httpFS, 353 | } 354 | ``` 355 | 356 | ## Extending 357 | 358 | Adding a new migration source means implementing `MigrationSource`. 359 | 360 | ```go 361 | type MigrationSource interface { 362 | FindMigrations() ([]*Migration, error) 363 | } 364 | ``` 365 | 366 | The resulting slice of migrations will be executed in the given order, so it should usually be sorted by the `Id` field. 367 | 368 | ## Usage with [sqlx](https://jmoiron.github.io/sqlx/) 369 | 370 | This library is compatible with sqlx. When calling migrate just dereference the DB from your `*sqlx.DB`: 371 | 372 | ``` 373 | n, err := migrate.Exec(db.DB, "sqlite3", migrations, migrate.Up) 374 | // ^^^ <-- Here db is a *sqlx.DB, the db.DB field is the plain sql.DB 375 | if err != nil { 376 | // Handle errors! 377 | } 378 | ``` 379 | 380 | ## Questions or Feedback? 381 | 382 | You can use Github Issues for feedback or questions. 383 | 384 | ## License 385 | 386 | This library is distributed under the [MIT](LICENSE) license. 387 | -------------------------------------------------------------------------------- /bindata_test.go: -------------------------------------------------------------------------------- 1 | package migrate 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "fmt" 7 | "io" 8 | "strings" 9 | ) 10 | 11 | func bindata_read(data []byte, name string) ([]byte, error) { 12 | gz, err := gzip.NewReader(bytes.NewBuffer(data)) 13 | if err != nil { 14 | return nil, fmt.Errorf("Read %q: %w", name, err) 15 | } 16 | 17 | var buf bytes.Buffer 18 | _, err = io.Copy(&buf, gz) 19 | if err := gz.Close(); err != nil { 20 | return nil, err 21 | } 22 | 23 | if err != nil { 24 | return nil, fmt.Errorf("Read %q: %w", name, err) 25 | } 26 | 27 | return buf.Bytes(), nil 28 | } 29 | 30 | func test_migrations_1_initial_sql() ([]byte, error) { 31 | return bindata_read([]byte{ 32 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x00, 0xff, 0x8c, 0xcd, 33 | 0x3d, 0x0e, 0x82, 0x40, 0x10, 0x05, 0xe0, 0x7e, 0x4e, 0xf1, 0x3a, 0x34, 34 | 0x86, 0x13, 0x50, 0xa1, 0xd0, 0x91, 0xa8, 0x08, 0x07, 0x40, 0x76, 0x22, 35 | 0x13, 0xd7, 0xdd, 0x09, 0xac, 0xc1, 0xe3, 0xbb, 0xc4, 0x68, 0xb4, 0xb3, 36 | 0x7c, 0x6f, 0x7e, 0xbe, 0x34, 0xc5, 0xe6, 0x26, 0x97, 0xb1, 0x0b, 0x8c, 37 | 0x56, 0x29, 0xc6, 0xd3, 0xb1, 0x82, 0x38, 0x4c, 0xdc, 0x07, 0xf1, 0x0e, 38 | 0x49, 0xab, 0x09, 0x64, 0x02, 0x3f, 0xb8, 0xbf, 0x07, 0x36, 0x98, 0x07, 39 | 0x76, 0x08, 0x43, 0xac, 0x5e, 0x77, 0xcb, 0x52, 0x0c, 0x9d, 0xaa, 0x15, 40 | 0x36, 0xb4, 0xab, 0xcb, 0xbc, 0x29, 0xd1, 0xe4, 0xdb, 0xaa, 0x84, 0xb2, 41 | 0x57, 0xcb, 0x58, 0x89, 0x89, 0x2f, 0xc3, 0x3a, 0x23, 0xa2, 0x6f, 0xb0, 42 | 0xf0, 0xb3, 0x7b, 0x93, 0x1f, 0x6f, 0x29, 0xff, 0x12, 0x47, 0x6f, 0x6d, 43 | 0x9c, 0x9e, 0xbb, 0xfe, 0x4a, 0x45, 0xbd, 0x3f, 0xfc, 0x98, 0x19, 0x3d, 44 | 0x03, 0x00, 0x00, 0xff, 0xff, 0x0d, 0x70, 0x5e, 0xf9, 0xda, 0x00, 0x00, 45 | 0x00, 46 | }, 47 | "test-migrations/1_initial.sql", 48 | ) 49 | } 50 | 51 | func test_migrations_2_record_sql() ([]byte, error) { 52 | return bindata_read([]byte{ 53 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x00, 0xff, 0xd2, 0xd5, 54 | 0x55, 0xd0, 0xce, 0xcd, 0x4c, 0x2f, 0x4a, 0x2c, 0x49, 0x55, 0x08, 0x2d, 55 | 0xe0, 0xf2, 0xf4, 0x0b, 0x76, 0x0d, 0x0a, 0x51, 0xf0, 0xf4, 0x0b, 0xf1, 56 | 0x57, 0x28, 0x48, 0xcd, 0x2f, 0xc8, 0x49, 0x55, 0xd0, 0xc8, 0x4c, 0xd1, 57 | 0x54, 0x08, 0x73, 0xf4, 0x09, 0x75, 0x0d, 0x56, 0xd0, 0x30, 0xd4, 0xb4, 58 | 0xe6, 0xe2, 0x42, 0xd6, 0xe3, 0x92, 0x5f, 0x9e, 0xc7, 0xe5, 0xe2, 0xea, 59 | 0xe3, 0x1a, 0xe2, 0xaa, 0xe0, 0x16, 0xe4, 0xef, 0x0b, 0xd3, 0x15, 0xee, 60 | 0xe1, 0x1a, 0xe4, 0xaa, 0x90, 0x99, 0x62, 0x6b, 0x68, 0xcd, 0x05, 0x08, 61 | 0x00, 0x00, 0xff, 0xff, 0xf4, 0x3a, 0x7b, 0xae, 0x64, 0x00, 0x00, 0x00, 62 | }, 63 | "test-migrations/2_record.sql", 64 | ) 65 | } 66 | 67 | // Asset loads and returns the asset for the given name. 68 | // It returns an error if the asset could not be found or 69 | // could not be loaded. 70 | func Asset(name string) ([]byte, error) { 71 | canonicalName := strings.ReplaceAll(name, "\\", "/") 72 | if f, ok := _bindata[canonicalName]; ok { 73 | return f() 74 | } 75 | return nil, fmt.Errorf("Asset %s not found", name) 76 | } 77 | 78 | // AssetNames returns the names of the assets. 79 | func AssetNames() []string { 80 | names := make([]string, 0, len(_bindata)) 81 | for name := range _bindata { 82 | names = append(names, name) 83 | } 84 | return names 85 | } 86 | 87 | // _bindata is a table, holding each asset generator, mapped to its name. 88 | var _bindata = map[string]func() ([]byte, error){ 89 | "test-migrations/1_initial.sql": test_migrations_1_initial_sql, 90 | "test-migrations/2_record.sql": test_migrations_2_record_sql, 91 | } 92 | 93 | // AssetDir returns the file names below a certain 94 | // directory embedded in the file by go-bindata. 95 | // For example if you run go-bindata on data/... and data contains the 96 | // following hierarchy: 97 | // 98 | // data/ 99 | // foo.txt 100 | // img/ 101 | // a.png 102 | // b.png 103 | // 104 | // then AssetDir("data") would return []string{"foo.txt", "img"} 105 | // AssetDir("data/img") would return []string{"a.png", "b.png"} 106 | // AssetDir("foo.txt") and AssetDir("notexist") would return an error 107 | // AssetDir("") will return []string{"data"}. 108 | func AssetDir(name string) ([]string, error) { 109 | node := _bintree 110 | if len(name) != 0 { 111 | canonicalName := strings.ReplaceAll(name, "\\", "/") 112 | pathList := strings.Split(canonicalName, "/") 113 | for _, p := range pathList { 114 | node = node.Children[p] 115 | if node == nil { 116 | return nil, fmt.Errorf("Asset %s not found", name) 117 | } 118 | } 119 | } 120 | if node.Func != nil { 121 | return nil, fmt.Errorf("Asset %s not found", name) 122 | } 123 | rv := make([]string, 0, len(node.Children)) 124 | for name := range node.Children { 125 | rv = append(rv, name) 126 | } 127 | return rv, nil 128 | } 129 | 130 | type _bintree_t struct { 131 | Func func() ([]byte, error) 132 | Children map[string]*_bintree_t 133 | } 134 | 135 | var _bintree = &_bintree_t{nil, map[string]*_bintree_t{ 136 | "test-migrations": {nil, map[string]*_bintree_t{ 137 | "1_initial.sql": {test_migrations_1_initial_sql, map[string]*_bintree_t{}}, 138 | "2_record.sql": {test_migrations_2_record_sql, map[string]*_bintree_t{}}, 139 | }}, 140 | }} 141 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | SQL Schema migration tool for Go. 3 | 4 | Key features: 5 | 6 | - Usable as a CLI tool or as a library 7 | - Supports SQLite, PostgreSQL, MySQL, MSSQL and Oracle databases (through gorp) 8 | - Can embed migrations into your application 9 | - Migrations are defined with SQL for full flexibility 10 | - Atomic migrations 11 | - Up/down migrations to allow rollback 12 | - Supports multiple database types in one project 13 | 14 | # Installation 15 | 16 | To install the library and command line program, use the following: 17 | 18 | go get -v github.com/rubenv/sql-migrate/... 19 | 20 | # Command-line tool 21 | 22 | The main command is called sql-migrate. 23 | 24 | $ sql-migrate --help 25 | usage: sql-migrate [--version] [--help] [] 26 | 27 | Available commands are: 28 | down Undo a database migration 29 | new Create a new migration 30 | redo Reapply the last migration 31 | status Show migration status 32 | up Migrates the database to the most recent version available 33 | 34 | Each command requires a configuration file (which defaults to dbconfig.yml, but can be specified with the -config flag). This config file should specify one or more environments: 35 | 36 | development: 37 | dialect: sqlite3 38 | datasource: test.db 39 | dir: migrations/sqlite3 40 | 41 | production: 42 | dialect: postgres 43 | datasource: dbname=myapp sslmode=disable 44 | dir: migrations/postgres 45 | table: migrations 46 | 47 | The `table` setting is optional and will default to `gorp_migrations`. 48 | 49 | The environment that will be used can be specified with the -env flag (defaults to development). 50 | 51 | Use the --help flag in combination with any of the commands to get an overview of its usage: 52 | 53 | $ sql-migrate up --help 54 | Usage: sql-migrate up [options] ... 55 | 56 | Migrates the database to the most recent version available. 57 | 58 | Options: 59 | 60 | -config=config.yml Configuration file to use. 61 | -env="development" Environment. 62 | -limit=0 Limit the number of migrations (0 = unlimited). 63 | -dryrun Don't apply migrations, just print them. 64 | 65 | The up command applies all available migrations. By contrast, down will only apply one migration by default. This behavior can be changed for both by using the -limit parameter. 66 | 67 | The redo command will unapply the last migration and reapply it. This is useful during development, when you're writing migrations. 68 | 69 | Use the status command to see the state of the applied migrations: 70 | 71 | $ sql-migrate status 72 | +---------------+-----------------------------------------+ 73 | | MIGRATION | APPLIED | 74 | +---------------+-----------------------------------------+ 75 | | 1_initial.sql | 2014-09-13 08:19:06.788354925 +0000 UTC | 76 | | 2_record.sql | no | 77 | +---------------+-----------------------------------------+ 78 | 79 | # MySQL Caveat 80 | 81 | If you are using MySQL, you must append ?parseTime=true to the datasource configuration. For example: 82 | 83 | production: 84 | dialect: mysql 85 | datasource: root@/dbname?parseTime=true 86 | dir: migrations/mysql 87 | table: migrations 88 | 89 | See https://github.com/go-sql-driver/mysql#parsetime for more information. 90 | 91 | # Library 92 | 93 | Import sql-migrate into your application: 94 | 95 | import "github.com/rubenv/sql-migrate" 96 | 97 | Set up a source of migrations, this can be from memory, from a set of files or from bindata (more on that later): 98 | 99 | // Hardcoded strings in memory: 100 | migrations := &migrate.MemoryMigrationSource{ 101 | Migrations: []*migrate.Migration{ 102 | &migrate.Migration{ 103 | Id: "123", 104 | Up: []string{"CREATE TABLE people (id int)"}, 105 | Down: []string{"DROP TABLE people"}, 106 | }, 107 | }, 108 | } 109 | 110 | // OR: Read migrations from a folder: 111 | migrations := &migrate.FileMigrationSource{ 112 | Dir: "db/migrations", 113 | } 114 | 115 | // OR: Use migrations from bindata: 116 | migrations := &migrate.AssetMigrationSource{ 117 | Asset: Asset, 118 | AssetDir: AssetDir, 119 | Dir: "migrations", 120 | } 121 | 122 | Then use the Exec function to upgrade your database: 123 | 124 | db, err := sql.Open("sqlite3", filename) 125 | if err != nil { 126 | // Handle errors! 127 | } 128 | 129 | n, err := migrate.Exec(db, "sqlite3", migrations, migrate.Up) 130 | if err != nil { 131 | // Handle errors! 132 | } 133 | fmt.Printf("Applied %d migrations!\n", n) 134 | 135 | Note that n can be greater than 0 even if there is an error: any migration that succeeded will remain applied even if a later one fails. 136 | 137 | The full set of capabilities can be found in the API docs below. 138 | 139 | # Writing migrations 140 | 141 | Migrations are defined in SQL files, which contain a set of SQL statements. Special comments are used to distinguish up and down migrations. 142 | 143 | -- +migrate Up 144 | -- SQL in section 'Up' is executed when this migration is applied 145 | CREATE TABLE people (id int); 146 | 147 | 148 | -- +migrate Down 149 | -- SQL section 'Down' is executed when this migration is rolled back 150 | DROP TABLE people; 151 | 152 | You can put multiple statements in each block, as long as you end them with a semicolon (;). 153 | 154 | If you have complex statements which contain semicolons, use StatementBegin and StatementEnd to indicate boundaries: 155 | 156 | -- +migrate Up 157 | CREATE TABLE people (id int); 158 | 159 | -- +migrate StatementBegin 160 | CREATE OR REPLACE FUNCTION do_something() 161 | returns void AS $$ 162 | DECLARE 163 | create_query text; 164 | BEGIN 165 | -- Do something here 166 | END; 167 | $$ 168 | language plpgsql; 169 | -- +migrate StatementEnd 170 | 171 | -- +migrate Down 172 | DROP FUNCTION do_something(); 173 | DROP TABLE people; 174 | 175 | The order in which migrations are applied is defined through the filename: sql-migrate will sort migrations based on their name. It's recommended to use an increasing version number or a timestamp as the first part of the filename. 176 | 177 | Normally each migration is run within a transaction in order to guarantee that it is fully atomic. However some SQL commands (for example creating an index concurrently in PostgreSQL) cannot be executed inside a transaction. In order to execute such a command in a migration, the migration can be run using the notransaction option: 178 | 179 | -- +migrate Up notransaction 180 | CREATE UNIQUE INDEX people_unique_id_idx CONCURRENTLY ON people (id); 181 | 182 | -- +migrate Down 183 | DROP INDEX people_unique_id_idx; 184 | 185 | # Embedding migrations with packr 186 | 187 | If you like your Go applications self-contained (that is: a single binary): use packr (https://github.com/gobuffalo/packr) to embed the migration files. 188 | 189 | Just write your migration files as usual, as a set of SQL files in a folder. 190 | 191 | Use the PackrMigrationSource in your application to find the migrations: 192 | 193 | migrations := &migrate.PackrMigrationSource{ 194 | Box: packr.NewBox("./migrations"), 195 | } 196 | 197 | If you already have a box and would like to use a subdirectory: 198 | 199 | migrations := &migrate.PackrMigrationSource{ 200 | Box: myBox, 201 | Dir: "./migrations", 202 | } 203 | 204 | # Embedding migrations with bindata 205 | 206 | As an alternative, but slightly less maintained, you can use bindata (https://github.com/shuLhan/go-bindata) to embed the migration files. 207 | 208 | Just write your migration files as usual, as a set of SQL files in a folder. 209 | 210 | Then use bindata to generate a .go file with the migrations embedded: 211 | 212 | go-bindata -pkg myapp -o bindata.go db/migrations/ 213 | 214 | The resulting bindata.go file will contain your migrations. Remember to regenerate your bindata.go file whenever you add/modify a migration (go generate will help here, once it arrives). 215 | 216 | Use the AssetMigrationSource in your application to find the migrations: 217 | 218 | migrations := &migrate.AssetMigrationSource{ 219 | Asset: Asset, 220 | AssetDir: AssetDir, 221 | Dir: "db/migrations", 222 | } 223 | 224 | Both Asset and AssetDir are functions provided by bindata. 225 | 226 | Then proceed as usual. 227 | 228 | # Extending 229 | 230 | Adding a new migration source means implementing MigrationSource. 231 | 232 | type MigrationSource interface { 233 | FindMigrations() ([]*Migration, error) 234 | } 235 | 236 | The resulting slice of migrations will be executed in the given order, so it should usually be sorted by the Id field. 237 | */ 238 | package migrate 239 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rubenv/sql-migrate 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/denisenkom/go-mssqldb v0.9.0 7 | github.com/go-gorp/gorp/v3 v3.1.0 8 | github.com/go-sql-driver/mysql v1.6.0 9 | github.com/godror/godror v0.40.4 10 | github.com/lib/pq v1.10.7 11 | github.com/mattn/go-oci8 v0.1.1 12 | github.com/mattn/go-sqlite3 v1.14.19 13 | github.com/mitchellh/cli v1.1.5 14 | github.com/olekukonko/tablewriter v0.0.5 15 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c 16 | gopkg.in/yaml.v2 v2.4.0 17 | ) 18 | 19 | require ( 20 | github.com/Masterminds/goutils v1.1.1 // indirect 21 | github.com/Masterminds/semver/v3 v3.2.0 // indirect 22 | github.com/Masterminds/sprig/v3 v3.2.3 // indirect 23 | github.com/armon/go-radix v1.0.0 // indirect 24 | github.com/bgentry/speakeasy v0.1.0 // indirect 25 | github.com/fatih/color v1.13.0 // indirect 26 | github.com/go-logfmt/logfmt v0.6.0 // indirect 27 | github.com/godror/knownpb v0.1.1 // indirect 28 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect 29 | github.com/google/uuid v1.3.0 // indirect 30 | github.com/hashicorp/errwrap v1.1.0 // indirect 31 | github.com/hashicorp/go-multierror v1.1.1 // indirect 32 | github.com/huandu/xstrings v1.4.0 // indirect 33 | github.com/imdario/mergo v0.3.13 // indirect 34 | github.com/kr/pretty v0.3.1 // indirect 35 | github.com/kr/text v0.2.0 // indirect 36 | github.com/mattn/go-colorable v0.1.13 // indirect 37 | github.com/mattn/go-isatty v0.0.17 // indirect 38 | github.com/mattn/go-runewidth v0.0.9 // indirect 39 | github.com/mitchellh/copystructure v1.2.0 // indirect 40 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 41 | github.com/posener/complete v1.2.3 // indirect 42 | github.com/rogpeppe/go-internal v1.9.0 // indirect 43 | github.com/shopspring/decimal v1.3.1 // indirect 44 | github.com/spf13/cast v1.5.0 // indirect 45 | golang.org/x/crypto v0.37.0 // indirect 46 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect 47 | golang.org/x/sys v0.32.0 // indirect 48 | google.golang.org/protobuf v1.33.0 // indirect 49 | ) 50 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= 2 | github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= 3 | github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= 4 | github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= 5 | github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= 6 | github.com/Masterminds/sprig/v3 v3.2.1/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= 7 | github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= 8 | github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= 9 | github.com/UNO-SOFT/zlog v0.8.1 h1:TEFkGJHtUfTRgMkLZiAjLSHALjwSBdw6/zByMC5GJt4= 10 | github.com/UNO-SOFT/zlog v0.8.1/go.mod h1:yqFOjn3OhvJ4j7ArJqQNA+9V+u6t9zSAyIZdWdMweWc= 11 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 12 | github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= 13 | github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 14 | github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= 15 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 16 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 17 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 19 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/denisenkom/go-mssqldb v0.9.0 h1:RSohk2RsiZqLZ0zCjtfn3S4Gp4exhpBWHyQ7D0yGjAk= 21 | github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= 22 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 23 | github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= 24 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 25 | github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= 26 | github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= 27 | github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= 28 | github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= 29 | github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= 30 | github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 31 | github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= 32 | github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 33 | github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= 34 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 35 | github.com/godror/godror v0.40.4 h1:X1e7hUd02GDaLWKZj40Z7L0CP0W9TrGgmPQZw6+anBg= 36 | github.com/godror/godror v0.40.4/go.mod h1:i8YtVTHUJKfFT3wTat4A9UoqScUtZXiYB9Rf3SVARgc= 37 | github.com/godror/knownpb v0.1.1 h1:A4J7jdx7jWBhJm18NntafzSC//iZDHkDi1+juwQ5pTI= 38 | github.com/godror/knownpb v0.1.1/go.mod h1:4nRFbQo1dDuwKnblRXDxrfCFYeT4hjg3GjMqef58eRE= 39 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= 40 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 41 | github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= 42 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 43 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 44 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 45 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 46 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 47 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 48 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 49 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 50 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 51 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 52 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 53 | github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 54 | github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 55 | github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 56 | github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= 57 | github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 58 | github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= 59 | github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= 60 | github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= 61 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 62 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 63 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 64 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 65 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 66 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 67 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 68 | github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= 69 | github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 70 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 71 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 72 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 73 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 74 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 75 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 76 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 77 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 78 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 79 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 80 | github.com/mattn/go-oci8 v0.1.1 h1:aEUDxNAyDG0tv8CA3TArnDQNyc4EhnWlsfxRgDHABHM= 81 | github.com/mattn/go-oci8 v0.1.1/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI= 82 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= 83 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 84 | github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI= 85 | github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= 86 | github.com/mitchellh/cli v1.1.5 h1:OxRIeJXpAMztws/XHlN2vu6imG5Dpq+j61AzAX5fLng= 87 | github.com/mitchellh/cli v1.1.5/go.mod h1:v8+iFts2sPIKUV1ltktPXMCC8fumSKFItNcD2cLtRR4= 88 | github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= 89 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 90 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 91 | github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 92 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 93 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 94 | github.com/oklog/ulid/v2 v2.0.2 h1:r4fFzBm+bv0wNKNh5eXTwU7i85y5x+uwkxCUTNVQqLc= 95 | github.com/oklog/ulid/v2 v2.0.2/go.mod h1:mtBL0Qe/0HAx6/a4Z30qxVIAL1eQDweXq5lxOEiwQ68= 96 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 97 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 98 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 99 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 100 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 101 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 102 | github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= 103 | github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= 104 | github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= 105 | github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= 106 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 107 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 108 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 109 | github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= 110 | github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 111 | github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 112 | github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= 113 | github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= 114 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 115 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 116 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 117 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 118 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 119 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 120 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 121 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 122 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 123 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 124 | golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 125 | golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 126 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 127 | golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= 128 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 129 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 130 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= 131 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= 132 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 133 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 134 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 135 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 136 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 137 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= 138 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 139 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= 140 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 141 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 142 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 143 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 144 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 145 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 146 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 147 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 148 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 149 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 150 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 151 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 152 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 153 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 154 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 155 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 156 | golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 157 | golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= 158 | golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= 159 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 160 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 161 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 162 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 163 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 164 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 165 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 166 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 167 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 168 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 169 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 170 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 171 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 172 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 173 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 174 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 175 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 176 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 177 | gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 178 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 179 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 180 | -------------------------------------------------------------------------------- /init_test.go: -------------------------------------------------------------------------------- 1 | package migrate 2 | 3 | import ( 4 | "testing" 5 | 6 | //revive:disable-next-line:dot-imports 7 | . "gopkg.in/check.v1" 8 | ) 9 | 10 | func Test(t *testing.T) { TestingT(t) } 11 | -------------------------------------------------------------------------------- /migrate.go: -------------------------------------------------------------------------------- 1 | package migrate 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "database/sql" 7 | "embed" 8 | "errors" 9 | "fmt" 10 | "io" 11 | "net/http" 12 | "os" 13 | "path" 14 | "regexp" 15 | "sort" 16 | "strconv" 17 | "strings" 18 | "time" 19 | 20 | "github.com/go-gorp/gorp/v3" 21 | 22 | "github.com/rubenv/sql-migrate/sqlparse" 23 | ) 24 | 25 | type MigrationDirection int 26 | 27 | const ( 28 | Up MigrationDirection = iota 29 | Down 30 | ) 31 | 32 | // MigrationSet provides database parameters for a migration execution 33 | type MigrationSet struct { 34 | // TableName name of the table used to store migration info. 35 | TableName string 36 | // SchemaName schema that the migration table be referenced. 37 | SchemaName string 38 | // IgnoreUnknown skips the check to see if there is a migration 39 | // ran in the database that is not in MigrationSource. 40 | // 41 | // This should be used sparingly as it is removing a safety check. 42 | IgnoreUnknown bool 43 | // DisableCreateTable disable the creation of the migration table 44 | DisableCreateTable bool 45 | } 46 | 47 | var migSet = MigrationSet{} 48 | 49 | // NewMigrationSet returns a parametrized Migration object 50 | func (ms MigrationSet) getTableName() string { 51 | if ms.TableName == "" { 52 | return "gorp_migrations" 53 | } 54 | return ms.TableName 55 | } 56 | 57 | var numberPrefixRegex = regexp.MustCompile(`^(\d+).*$`) 58 | 59 | // PlanError happens where no migration plan could be created between the sets 60 | // of already applied migrations and the currently found. For example, when the database 61 | // contains a migration which is not among the migrations list found for an operation. 62 | type PlanError struct { 63 | Migration *Migration 64 | ErrorMessage string 65 | } 66 | 67 | func newPlanError(migration *Migration, errorMessage string) error { 68 | return &PlanError{ 69 | Migration: migration, 70 | ErrorMessage: errorMessage, 71 | } 72 | } 73 | 74 | func (p *PlanError) Error() string { 75 | return fmt.Sprintf("Unable to create migration plan because of %s: %s", 76 | p.Migration.Id, p.ErrorMessage) 77 | } 78 | 79 | // TxError is returned when any error is encountered during a database 80 | // transaction. It contains the relevant *Migration and notes it's Id in the 81 | // Error function output. 82 | type TxError struct { 83 | Migration *Migration 84 | Err error 85 | } 86 | 87 | func newTxError(migration *PlannedMigration, err error) error { 88 | return &TxError{ 89 | Migration: migration.Migration, 90 | Err: err, 91 | } 92 | } 93 | 94 | func (e *TxError) Error() string { 95 | return e.Err.Error() + " handling " + e.Migration.Id 96 | } 97 | 98 | // Set the name of the table used to store migration info. 99 | // 100 | // Should be called before any other call such as (Exec, ExecMax, ...). 101 | func SetTable(name string) { 102 | if name != "" { 103 | migSet.TableName = name 104 | } 105 | } 106 | 107 | // SetSchema sets the name of a schema that the migration table be referenced. 108 | func SetSchema(name string) { 109 | if name != "" { 110 | migSet.SchemaName = name 111 | } 112 | } 113 | 114 | // SetDisableCreateTable sets the boolean to disable the creation of the migration table 115 | func SetDisableCreateTable(disable bool) { 116 | migSet.DisableCreateTable = disable 117 | } 118 | 119 | // SetIgnoreUnknown sets the flag that skips database check to see if there is a 120 | // migration in the database that is not in migration source. 121 | // 122 | // This should be used sparingly as it is removing a safety check. 123 | func SetIgnoreUnknown(v bool) { 124 | migSet.IgnoreUnknown = v 125 | } 126 | 127 | type Migration struct { 128 | Id string 129 | Up []string 130 | Down []string 131 | 132 | DisableTransactionUp bool 133 | DisableTransactionDown bool 134 | } 135 | 136 | func (m Migration) Less(other *Migration) bool { 137 | switch { 138 | case m.isNumeric() && other.isNumeric() && m.VersionInt() != other.VersionInt(): 139 | return m.VersionInt() < other.VersionInt() 140 | case m.isNumeric() && !other.isNumeric(): 141 | return true 142 | case !m.isNumeric() && other.isNumeric(): 143 | return false 144 | default: 145 | return m.Id < other.Id 146 | } 147 | } 148 | 149 | func (m Migration) isNumeric() bool { 150 | return len(m.NumberPrefixMatches()) > 0 151 | } 152 | 153 | func (m Migration) NumberPrefixMatches() []string { 154 | return numberPrefixRegex.FindStringSubmatch(m.Id) 155 | } 156 | 157 | func (m Migration) VersionInt() int64 { 158 | v := m.NumberPrefixMatches()[1] 159 | value, err := strconv.ParseInt(v, 10, 64) 160 | if err != nil { 161 | panic(fmt.Sprintf("Could not parse %q into int64: %s", v, err)) 162 | } 163 | return value 164 | } 165 | 166 | type PlannedMigration struct { 167 | *Migration 168 | 169 | DisableTransaction bool 170 | Queries []string 171 | } 172 | 173 | type byId []*Migration 174 | 175 | func (b byId) Len() int { return len(b) } 176 | func (b byId) Swap(i, j int) { b[i], b[j] = b[j], b[i] } 177 | func (b byId) Less(i, j int) bool { return b[i].Less(b[j]) } 178 | 179 | type MigrationRecord struct { 180 | Id string `db:"id"` 181 | AppliedAt time.Time `db:"applied_at"` 182 | } 183 | 184 | type OracleDialect struct { 185 | gorp.OracleDialect 186 | } 187 | 188 | func (OracleDialect) IfTableNotExists(command, _, _ string) string { 189 | return command 190 | } 191 | 192 | func (OracleDialect) IfSchemaNotExists(command, _ string) string { 193 | return command 194 | } 195 | 196 | func (OracleDialect) IfTableExists(command, _, _ string) string { 197 | return command 198 | } 199 | 200 | var MigrationDialects = map[string]gorp.Dialect{ 201 | "sqlite3": gorp.SqliteDialect{}, 202 | "postgres": gorp.PostgresDialect{}, 203 | "mysql": gorp.MySQLDialect{Engine: "InnoDB", Encoding: "UTF8"}, 204 | "mssql": gorp.SqlServerDialect{}, 205 | "oci8": OracleDialect{}, 206 | "godror": OracleDialect{}, 207 | "snowflake": gorp.SnowflakeDialect{}, 208 | } 209 | 210 | type MigrationSource interface { 211 | // Finds the migrations. 212 | // 213 | // The resulting slice of migrations should be sorted by Id. 214 | FindMigrations() ([]*Migration, error) 215 | } 216 | 217 | // A hardcoded set of migrations, in-memory. 218 | type MemoryMigrationSource struct { 219 | Migrations []*Migration 220 | } 221 | 222 | var _ MigrationSource = (*MemoryMigrationSource)(nil) 223 | 224 | func (m MemoryMigrationSource) FindMigrations() ([]*Migration, error) { 225 | // Make sure migrations are sorted. In order to make the MemoryMigrationSource safe for 226 | // concurrent use we should not mutate it in place. So `FindMigrations` would sort a copy 227 | // of the m.Migrations. 228 | migrations := make([]*Migration, len(m.Migrations)) 229 | copy(migrations, m.Migrations) 230 | sort.Sort(byId(migrations)) 231 | return migrations, nil 232 | } 233 | 234 | // A set of migrations loaded from an http.FileServer 235 | 236 | type HttpFileSystemMigrationSource struct { 237 | FileSystem http.FileSystem 238 | } 239 | 240 | var _ MigrationSource = (*HttpFileSystemMigrationSource)(nil) 241 | 242 | func (f HttpFileSystemMigrationSource) FindMigrations() ([]*Migration, error) { 243 | return findMigrations(f.FileSystem, "/") 244 | } 245 | 246 | // A set of migrations loaded from a directory. 247 | type FileMigrationSource struct { 248 | Dir string 249 | } 250 | 251 | var _ MigrationSource = (*FileMigrationSource)(nil) 252 | 253 | func (f FileMigrationSource) FindMigrations() ([]*Migration, error) { 254 | filesystem := http.Dir(f.Dir) 255 | return findMigrations(filesystem, "/") 256 | } 257 | 258 | func findMigrations(dir http.FileSystem, root string) ([]*Migration, error) { 259 | migrations := make([]*Migration, 0) 260 | 261 | file, err := dir.Open(root) 262 | if err != nil { 263 | return nil, err 264 | } 265 | 266 | files, err := file.Readdir(0) 267 | if err != nil { 268 | return nil, err 269 | } 270 | 271 | for _, info := range files { 272 | if strings.HasSuffix(info.Name(), ".sql") { 273 | migration, err := migrationFromFile(dir, root, info) 274 | if err != nil { 275 | return nil, err 276 | } 277 | 278 | migrations = append(migrations, migration) 279 | } 280 | } 281 | 282 | // Make sure migrations are sorted 283 | sort.Sort(byId(migrations)) 284 | 285 | return migrations, nil 286 | } 287 | 288 | func migrationFromFile(dir http.FileSystem, root string, info os.FileInfo) (*Migration, error) { 289 | path := path.Join(root, info.Name()) 290 | file, err := dir.Open(path) 291 | if err != nil { 292 | return nil, fmt.Errorf("Error while opening %s: %w", info.Name(), err) 293 | } 294 | defer func() { _ = file.Close() }() 295 | 296 | migration, err := ParseMigration(info.Name(), file) 297 | if err != nil { 298 | return nil, fmt.Errorf("Error while parsing %s: %w", info.Name(), err) 299 | } 300 | return migration, nil 301 | } 302 | 303 | // Migrations from a bindata asset set. 304 | type AssetMigrationSource struct { 305 | // Asset should return content of file in path if exists 306 | Asset func(path string) ([]byte, error) 307 | 308 | // AssetDir should return list of files in the path 309 | AssetDir func(path string) ([]string, error) 310 | 311 | // Path in the bindata to use. 312 | Dir string 313 | } 314 | 315 | var _ MigrationSource = (*AssetMigrationSource)(nil) 316 | 317 | func (a AssetMigrationSource) FindMigrations() ([]*Migration, error) { 318 | migrations := make([]*Migration, 0) 319 | 320 | files, err := a.AssetDir(a.Dir) 321 | if err != nil { 322 | return nil, err 323 | } 324 | 325 | for _, name := range files { 326 | if strings.HasSuffix(name, ".sql") { 327 | file, err := a.Asset(path.Join(a.Dir, name)) 328 | if err != nil { 329 | return nil, err 330 | } 331 | 332 | migration, err := ParseMigration(name, bytes.NewReader(file)) 333 | if err != nil { 334 | return nil, err 335 | } 336 | 337 | migrations = append(migrations, migration) 338 | } 339 | } 340 | 341 | // Make sure migrations are sorted 342 | sort.Sort(byId(migrations)) 343 | 344 | return migrations, nil 345 | } 346 | 347 | // A set of migrations loaded from an go1.16 embed.FS 348 | type EmbedFileSystemMigrationSource struct { 349 | FileSystem embed.FS 350 | 351 | Root string 352 | } 353 | 354 | var _ MigrationSource = (*EmbedFileSystemMigrationSource)(nil) 355 | 356 | func (f EmbedFileSystemMigrationSource) FindMigrations() ([]*Migration, error) { 357 | return findMigrations(http.FS(f.FileSystem), f.Root) 358 | } 359 | 360 | // Avoids pulling in the packr library for everyone, mimicks the bits of 361 | // packr.Box that we need. 362 | type PackrBox interface { 363 | List() []string 364 | Find(name string) ([]byte, error) 365 | } 366 | 367 | // Migrations from a packr box. 368 | type PackrMigrationSource struct { 369 | Box PackrBox 370 | 371 | // Path in the box to use. 372 | Dir string 373 | } 374 | 375 | var _ MigrationSource = (*PackrMigrationSource)(nil) 376 | 377 | func (p PackrMigrationSource) FindMigrations() ([]*Migration, error) { 378 | migrations := make([]*Migration, 0) 379 | items := p.Box.List() 380 | 381 | prefix := "" 382 | dir := path.Clean(p.Dir) 383 | if dir != "." { 384 | prefix = fmt.Sprintf("%s/", dir) 385 | } 386 | 387 | for _, item := range items { 388 | if !strings.HasPrefix(item, prefix) { 389 | continue 390 | } 391 | name := strings.TrimPrefix(item, prefix) 392 | if strings.Contains(name, "/") { 393 | continue 394 | } 395 | 396 | if strings.HasSuffix(name, ".sql") { 397 | file, err := p.Box.Find(item) 398 | if err != nil { 399 | return nil, err 400 | } 401 | 402 | migration, err := ParseMigration(name, bytes.NewReader(file)) 403 | if err != nil { 404 | return nil, err 405 | } 406 | 407 | migrations = append(migrations, migration) 408 | } 409 | } 410 | 411 | // Make sure migrations are sorted 412 | sort.Sort(byId(migrations)) 413 | 414 | return migrations, nil 415 | } 416 | 417 | // Migration parsing 418 | func ParseMigration(id string, r io.ReadSeeker) (*Migration, error) { 419 | m := &Migration{ 420 | Id: id, 421 | } 422 | 423 | parsed, err := sqlparse.ParseMigration(r) 424 | if err != nil { 425 | return nil, fmt.Errorf("Error parsing migration (%s): %w", id, err) 426 | } 427 | 428 | m.Up = parsed.UpStatements 429 | m.Down = parsed.DownStatements 430 | 431 | m.DisableTransactionUp = parsed.DisableTransactionUp 432 | m.DisableTransactionDown = parsed.DisableTransactionDown 433 | 434 | return m, nil 435 | } 436 | 437 | type SqlExecutor interface { 438 | Exec(query string, args ...interface{}) (sql.Result, error) 439 | Insert(list ...interface{}) error 440 | Delete(list ...interface{}) (int64, error) 441 | } 442 | 443 | // Execute a set of migrations 444 | // 445 | // Returns the number of applied migrations. 446 | func Exec(db *sql.DB, dialect string, m MigrationSource, dir MigrationDirection) (int, error) { 447 | return ExecMaxContext(context.Background(), db, dialect, m, dir, 0) 448 | } 449 | 450 | // Returns the number of applied migrations. 451 | func (ms MigrationSet) Exec(db *sql.DB, dialect string, m MigrationSource, dir MigrationDirection) (int, error) { 452 | return ms.ExecMaxContext(context.Background(), db, dialect, m, dir, 0) 453 | } 454 | 455 | // Execute a set of migrations with an input context. 456 | // 457 | // Returns the number of applied migrations. 458 | func ExecContext(ctx context.Context, db *sql.DB, dialect string, m MigrationSource, dir MigrationDirection) (int, error) { 459 | return ExecMaxContext(ctx, db, dialect, m, dir, 0) 460 | } 461 | 462 | // Returns the number of applied migrations. 463 | func (ms MigrationSet) ExecContext(ctx context.Context, db *sql.DB, dialect string, m MigrationSource, dir MigrationDirection) (int, error) { 464 | return ms.ExecMaxContext(ctx, db, dialect, m, dir, 0) 465 | } 466 | 467 | // Execute a set of migrations 468 | // 469 | // Will apply at most `max` migrations. Pass 0 for no limit (or use Exec). 470 | // 471 | // Returns the number of applied migrations. 472 | func ExecMax(db *sql.DB, dialect string, m MigrationSource, dir MigrationDirection, max int) (int, error) { 473 | return migSet.ExecMax(db, dialect, m, dir, max) 474 | } 475 | 476 | // Execute a set of migrations with an input context. 477 | // 478 | // Will apply at most `max` migrations. Pass 0 for no limit (or use Exec). 479 | // 480 | // Returns the number of applied migrations. 481 | func ExecMaxContext(ctx context.Context, db *sql.DB, dialect string, m MigrationSource, dir MigrationDirection, max int) (int, error) { 482 | return migSet.ExecMaxContext(ctx, db, dialect, m, dir, max) 483 | } 484 | 485 | // Execute a set of migrations 486 | // 487 | // Will apply at the target `version` of migration. Cannot be a negative value. 488 | // 489 | // Returns the number of applied migrations. 490 | func ExecVersion(db *sql.DB, dialect string, m MigrationSource, dir MigrationDirection, version int64) (int, error) { 491 | return ExecVersionContext(context.Background(), db, dialect, m, dir, version) 492 | } 493 | 494 | // Execute a set of migrations with an input context. 495 | // 496 | // Will apply at the target `version` of migration. Cannot be a negative value. 497 | // 498 | // Returns the number of applied migrations. 499 | func ExecVersionContext(ctx context.Context, db *sql.DB, dialect string, m MigrationSource, dir MigrationDirection, version int64) (int, error) { 500 | if version < 0 { 501 | return 0, fmt.Errorf("target version %d should not be negative", version) 502 | } 503 | return migSet.ExecVersionContext(ctx, db, dialect, m, dir, version) 504 | } 505 | 506 | // Returns the number of applied migrations. 507 | func (ms MigrationSet) ExecMax(db *sql.DB, dialect string, m MigrationSource, dir MigrationDirection, max int) (int, error) { 508 | return ms.ExecMaxContext(context.Background(), db, dialect, m, dir, max) 509 | } 510 | 511 | // Returns the number of applied migrations, but applies with an input context. 512 | func (ms MigrationSet) ExecMaxContext(ctx context.Context, db *sql.DB, dialect string, m MigrationSource, dir MigrationDirection, max int) (int, error) { 513 | migrations, dbMap, err := ms.PlanMigration(db, dialect, m, dir, max) 514 | if err != nil { 515 | return 0, err 516 | } 517 | return ms.applyMigrations(ctx, dir, migrations, dbMap) 518 | } 519 | 520 | // Returns the number of applied migrations. 521 | func (ms MigrationSet) ExecVersion(db *sql.DB, dialect string, m MigrationSource, dir MigrationDirection, version int64) (int, error) { 522 | return ms.ExecVersionContext(context.Background(), db, dialect, m, dir, version) 523 | } 524 | 525 | func (ms MigrationSet) ExecVersionContext(ctx context.Context, db *sql.DB, dialect string, m MigrationSource, dir MigrationDirection, version int64) (int, error) { 526 | migrations, dbMap, err := ms.PlanMigrationToVersion(db, dialect, m, dir, version) 527 | if err != nil { 528 | return 0, err 529 | } 530 | return ms.applyMigrations(ctx, dir, migrations, dbMap) 531 | } 532 | 533 | // Applies the planned migrations and returns the number of applied migrations. 534 | func (MigrationSet) applyMigrations(ctx context.Context, dir MigrationDirection, migrations []*PlannedMigration, dbMap *gorp.DbMap) (int, error) { 535 | applied := 0 536 | for _, migration := range migrations { 537 | var executor SqlExecutor 538 | var err error 539 | 540 | if migration.DisableTransaction { 541 | executor = dbMap.WithContext(ctx) 542 | } else { 543 | e, err := dbMap.Begin() 544 | if err != nil { 545 | return applied, newTxError(migration, err) 546 | } 547 | executor = e.WithContext(ctx) 548 | } 549 | 550 | for _, stmt := range migration.Queries { 551 | // remove the semicolon from stmt, fix ORA-00922 issue in database oracle 552 | stmt = strings.TrimSuffix(stmt, "\n") 553 | stmt = strings.TrimSuffix(stmt, " ") 554 | stmt = strings.TrimSuffix(stmt, ";") 555 | if _, err := executor.Exec(stmt); err != nil { 556 | if trans, ok := executor.(*gorp.Transaction); ok { 557 | _ = trans.Rollback() 558 | } 559 | 560 | return applied, newTxError(migration, err) 561 | } 562 | } 563 | 564 | switch dir { 565 | case Up: 566 | err = executor.Insert(&MigrationRecord{ 567 | Id: migration.Id, 568 | AppliedAt: time.Now(), 569 | }) 570 | if err != nil { 571 | if trans, ok := executor.(*gorp.Transaction); ok { 572 | _ = trans.Rollback() 573 | } 574 | 575 | return applied, newTxError(migration, err) 576 | } 577 | case Down: 578 | _, err := executor.Delete(&MigrationRecord{ 579 | Id: migration.Id, 580 | }) 581 | if err != nil { 582 | if trans, ok := executor.(*gorp.Transaction); ok { 583 | _ = trans.Rollback() 584 | } 585 | 586 | return applied, newTxError(migration, err) 587 | } 588 | default: 589 | panic("Not possible") 590 | } 591 | 592 | if trans, ok := executor.(*gorp.Transaction); ok { 593 | if err := trans.Commit(); err != nil { 594 | return applied, newTxError(migration, err) 595 | } 596 | } 597 | 598 | applied++ 599 | } 600 | 601 | return applied, nil 602 | } 603 | 604 | // Plan a migration. 605 | func PlanMigration(db *sql.DB, dialect string, m MigrationSource, dir MigrationDirection, max int) ([]*PlannedMigration, *gorp.DbMap, error) { 606 | return migSet.PlanMigration(db, dialect, m, dir, max) 607 | } 608 | 609 | // Plan a migration to version. 610 | func PlanMigrationToVersion(db *sql.DB, dialect string, m MigrationSource, dir MigrationDirection, version int64) ([]*PlannedMigration, *gorp.DbMap, error) { 611 | return migSet.PlanMigrationToVersion(db, dialect, m, dir, version) 612 | } 613 | 614 | // Plan a migration. 615 | func (ms MigrationSet) PlanMigration(db *sql.DB, dialect string, m MigrationSource, dir MigrationDirection, max int) ([]*PlannedMigration, *gorp.DbMap, error) { 616 | return ms.planMigrationCommon(db, dialect, m, dir, max, -1) 617 | } 618 | 619 | // Plan a migration to version. 620 | func (ms MigrationSet) PlanMigrationToVersion(db *sql.DB, dialect string, m MigrationSource, dir MigrationDirection, version int64) ([]*PlannedMigration, *gorp.DbMap, error) { 621 | return ms.planMigrationCommon(db, dialect, m, dir, 0, version) 622 | } 623 | 624 | // A common method to plan a migration. 625 | func (ms MigrationSet) planMigrationCommon(db *sql.DB, dialect string, m MigrationSource, dir MigrationDirection, max int, version int64) ([]*PlannedMigration, *gorp.DbMap, error) { 626 | dbMap, err := ms.getMigrationDbMap(db, dialect) 627 | if err != nil { 628 | return nil, nil, err 629 | } 630 | 631 | migrations, err := m.FindMigrations() 632 | if err != nil { 633 | return nil, nil, err 634 | } 635 | 636 | var migrationRecords []MigrationRecord 637 | _, err = dbMap.Select(&migrationRecords, fmt.Sprintf("SELECT * FROM %s", dbMap.Dialect.QuotedTableForQuery(ms.SchemaName, ms.getTableName()))) 638 | if err != nil { 639 | return nil, nil, err 640 | } 641 | 642 | // Sort migrations that have been run by Id. 643 | var existingMigrations []*Migration 644 | for _, migrationRecord := range migrationRecords { 645 | existingMigrations = append(existingMigrations, &Migration{ 646 | Id: migrationRecord.Id, 647 | }) 648 | } 649 | sort.Sort(byId(existingMigrations)) 650 | 651 | // Make sure all migrations in the database are among the found migrations which 652 | // are to be applied. 653 | if !ms.IgnoreUnknown { 654 | migrationsSearch := make(map[string]struct{}) 655 | for _, migration := range migrations { 656 | migrationsSearch[migration.Id] = struct{}{} 657 | } 658 | for _, existingMigration := range existingMigrations { 659 | if _, ok := migrationsSearch[existingMigration.Id]; !ok { 660 | return nil, nil, newPlanError(existingMigration, "unknown migration in database") 661 | } 662 | } 663 | } 664 | 665 | // Get last migration that was run 666 | record := &Migration{} 667 | if len(existingMigrations) > 0 { 668 | record = existingMigrations[len(existingMigrations)-1] 669 | } 670 | 671 | result := make([]*PlannedMigration, 0) 672 | 673 | // Add missing migrations up to the last run migration. 674 | // This can happen for example when merges happened. 675 | if len(existingMigrations) > 0 { 676 | result = append(result, ToCatchup(migrations, existingMigrations, record)...) 677 | } 678 | 679 | // Figure out which migrations to apply 680 | toApply := ToApply(migrations, record.Id, dir) 681 | toApplyCount := len(toApply) 682 | 683 | if version >= 0 { 684 | targetIndex := 0 685 | for targetIndex < len(toApply) { 686 | tempVersion := toApply[targetIndex].VersionInt() 687 | if dir == Up && tempVersion > version || dir == Down && tempVersion < version { 688 | return nil, nil, newPlanError(&Migration{}, fmt.Errorf("unknown migration with version id %d in database", version).Error()) 689 | } 690 | if tempVersion == version { 691 | toApplyCount = targetIndex + 1 692 | break 693 | } 694 | targetIndex++ 695 | } 696 | if targetIndex == len(toApply) { 697 | return nil, nil, newPlanError(&Migration{}, fmt.Errorf("unknown migration with version id %d in database", version).Error()) 698 | } 699 | } else if max > 0 && max < toApplyCount { 700 | toApplyCount = max 701 | } 702 | for _, v := range toApply[0:toApplyCount] { 703 | switch dir { 704 | case Up: 705 | result = append(result, &PlannedMigration{ 706 | Migration: v, 707 | Queries: v.Up, 708 | DisableTransaction: v.DisableTransactionUp, 709 | }) 710 | case Down: 711 | result = append(result, &PlannedMigration{ 712 | Migration: v, 713 | Queries: v.Down, 714 | DisableTransaction: v.DisableTransactionDown, 715 | }) 716 | } 717 | } 718 | 719 | return result, dbMap, nil 720 | } 721 | 722 | // Skip a set of migrations 723 | // 724 | // Will skip at most `max` migrations. Pass 0 for no limit. 725 | // 726 | // Returns the number of skipped migrations. 727 | func SkipMax(db *sql.DB, dialect string, m MigrationSource, dir MigrationDirection, max int) (int, error) { 728 | migrations, dbMap, err := PlanMigration(db, dialect, m, dir, max) 729 | if err != nil { 730 | return 0, err 731 | } 732 | 733 | // Skip migrations 734 | applied := 0 735 | for _, migration := range migrations { 736 | var executor SqlExecutor 737 | 738 | if migration.DisableTransaction { 739 | executor = dbMap 740 | } else { 741 | executor, err = dbMap.Begin() 742 | if err != nil { 743 | return applied, newTxError(migration, err) 744 | } 745 | } 746 | 747 | err = executor.Insert(&MigrationRecord{ 748 | Id: migration.Id, 749 | AppliedAt: time.Now(), 750 | }) 751 | if err != nil { 752 | if trans, ok := executor.(*gorp.Transaction); ok { 753 | _ = trans.Rollback() 754 | } 755 | 756 | return applied, newTxError(migration, err) 757 | } 758 | 759 | if trans, ok := executor.(*gorp.Transaction); ok { 760 | if err := trans.Commit(); err != nil { 761 | return applied, newTxError(migration, err) 762 | } 763 | } 764 | 765 | applied++ 766 | } 767 | 768 | return applied, nil 769 | } 770 | 771 | // Filter a slice of migrations into ones that should be applied. 772 | func ToApply(migrations []*Migration, current string, direction MigrationDirection) []*Migration { 773 | index := -1 774 | if current != "" { 775 | for index < len(migrations)-1 { 776 | index++ 777 | if migrations[index].Id == current { 778 | break 779 | } 780 | } 781 | } 782 | 783 | switch direction { 784 | case Up: 785 | return migrations[index+1:] 786 | case Down: 787 | if index == -1 { 788 | return []*Migration{} 789 | } 790 | toApply := make([]*Migration, index+1) 791 | for i := 0; i < index+1; i++ { 792 | toApply[index-i] = migrations[i] 793 | } 794 | return toApply 795 | } 796 | 797 | panic("Not possible") 798 | } 799 | 800 | func ToCatchup(migrations, existingMigrations []*Migration, lastRun *Migration) []*PlannedMigration { 801 | missing := make([]*PlannedMigration, 0) 802 | for _, migration := range migrations { 803 | found := false 804 | for _, existing := range existingMigrations { 805 | if existing.Id == migration.Id { 806 | found = true 807 | break 808 | } 809 | } 810 | if !found && migration.Less(lastRun) { 811 | missing = append(missing, &PlannedMigration{ 812 | Migration: migration, 813 | Queries: migration.Up, 814 | DisableTransaction: migration.DisableTransactionUp, 815 | }) 816 | } 817 | } 818 | return missing 819 | } 820 | 821 | func GetMigrationRecords(db *sql.DB, dialect string) ([]*MigrationRecord, error) { 822 | return migSet.GetMigrationRecords(db, dialect) 823 | } 824 | 825 | func (ms MigrationSet) GetMigrationRecords(db *sql.DB, dialect string) ([]*MigrationRecord, error) { 826 | dbMap, err := ms.getMigrationDbMap(db, dialect) 827 | if err != nil { 828 | return nil, err 829 | } 830 | 831 | var records []*MigrationRecord 832 | query := fmt.Sprintf("SELECT * FROM %s ORDER BY %s ASC", dbMap.Dialect.QuotedTableForQuery(ms.SchemaName, ms.getTableName()), dbMap.Dialect.QuoteField("id")) 833 | _, err = dbMap.Select(&records, query) 834 | if err != nil { 835 | return nil, err 836 | } 837 | 838 | return records, nil 839 | } 840 | 841 | func (ms MigrationSet) getMigrationDbMap(db *sql.DB, dialect string) (*gorp.DbMap, error) { 842 | d, ok := MigrationDialects[dialect] 843 | if !ok { 844 | return nil, fmt.Errorf("Unknown dialect: %s", dialect) 845 | } 846 | 847 | // When using the mysql driver, make sure that the parseTime option is 848 | // configured, otherwise it won't map time columns to time.Time. See 849 | // https://github.com/rubenv/sql-migrate/issues/2 850 | if dialect == "mysql" { 851 | var out *time.Time 852 | err := db.QueryRow("SELECT NOW()").Scan(&out) 853 | if err != nil { 854 | if err.Error() == "sql: Scan error on column index 0: unsupported driver -> Scan pair: []uint8 -> *time.Time" || 855 | err.Error() == "sql: Scan error on column index 0: unsupported Scan, storing driver.Value type []uint8 into type *time.Time" || 856 | err.Error() == "sql: Scan error on column index 0, name \"NOW()\": unsupported Scan, storing driver.Value type []uint8 into type *time.Time" { 857 | return nil, errors.New(`Cannot parse dates. 858 | 859 | Make sure that the parseTime option is supplied to your database connection. 860 | Check https://github.com/go-sql-driver/mysql#parsetime for more info.`) 861 | } 862 | return nil, err 863 | } 864 | } 865 | 866 | // Create migration database map 867 | dbMap := &gorp.DbMap{Db: db, Dialect: d} 868 | table := dbMap.AddTableWithNameAndSchema(MigrationRecord{}, ms.SchemaName, ms.getTableName()).SetKeys(false, "Id") 869 | 870 | if dialect == "oci8" || dialect == "godror" { 871 | table.ColMap("Id").SetMaxSize(4000) 872 | } 873 | 874 | if ms.DisableCreateTable { 875 | return dbMap, nil 876 | } 877 | 878 | err := dbMap.CreateTablesIfNotExists() 879 | if err != nil { 880 | // Oracle database does not support `if not exists`, so use `ORA-00955:` error code 881 | // to check if the table exists. 882 | if (dialect == "oci8" || dialect == "godror") && strings.Contains(err.Error(), "ORA-00955:") { 883 | return dbMap, nil 884 | } 885 | return nil, err 886 | } 887 | 888 | return dbMap, nil 889 | } 890 | 891 | // TODO: Run migration + record insert in transaction. 892 | -------------------------------------------------------------------------------- /migrate_test.go: -------------------------------------------------------------------------------- 1 | package migrate 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "embed" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/go-gorp/gorp/v3" 11 | //revive:disable-next-line:dot-imports 12 | . "gopkg.in/check.v1" 13 | 14 | _ "github.com/mattn/go-sqlite3" 15 | ) 16 | 17 | var sqliteMigrations = []*Migration{ 18 | { 19 | Id: "123", 20 | Up: []string{"CREATE TABLE people (id int)"}, 21 | Down: []string{"DROP TABLE people"}, 22 | }, 23 | { 24 | Id: "124", 25 | Up: []string{"ALTER TABLE people ADD COLUMN first_name text"}, 26 | Down: []string{"SELECT 0"}, // Not really supported 27 | }, 28 | } 29 | 30 | type SqliteMigrateSuite struct { 31 | Db *sql.DB 32 | DbMap *gorp.DbMap 33 | } 34 | 35 | var _ = Suite(&SqliteMigrateSuite{}) 36 | 37 | func (s *SqliteMigrateSuite) SetUpTest(c *C) { 38 | var err error 39 | db, err := sql.Open("sqlite3", ":memory:") 40 | c.Assert(err, IsNil) 41 | 42 | s.Db = db 43 | s.DbMap = &gorp.DbMap{Db: db, Dialect: &gorp.SqliteDialect{}} 44 | } 45 | 46 | func (s *SqliteMigrateSuite) TestRunMigration(c *C) { 47 | migrations := &MemoryMigrationSource{ 48 | Migrations: sqliteMigrations[:1], 49 | } 50 | 51 | // Executes one migration 52 | n, err := Exec(s.Db, "sqlite3", migrations, Up) 53 | c.Assert(err, IsNil) 54 | c.Assert(n, Equals, 1) 55 | 56 | // Can use table now 57 | _, err = s.DbMap.Exec("SELECT * FROM people") 58 | c.Assert(err, IsNil) 59 | 60 | // Shouldn't apply migration again 61 | n, err = Exec(s.Db, "sqlite3", migrations, Up) 62 | c.Assert(err, IsNil) 63 | c.Assert(n, Equals, 0) 64 | } 65 | 66 | func (s *SqliteMigrateSuite) TestRunMigrationEscapeTable(c *C) { 67 | migrations := &MemoryMigrationSource{ 68 | Migrations: sqliteMigrations[:1], 69 | } 70 | 71 | SetTable(`my migrations`) 72 | 73 | // Executes one migration 74 | n, err := Exec(s.Db, "sqlite3", migrations, Up) 75 | c.Assert(err, IsNil) 76 | c.Assert(n, Equals, 1) 77 | } 78 | 79 | func (s *SqliteMigrateSuite) TestMigrateMultiple(c *C) { 80 | migrations := &MemoryMigrationSource{ 81 | Migrations: sqliteMigrations[:2], 82 | } 83 | 84 | // Executes two migrations 85 | n, err := Exec(s.Db, "sqlite3", migrations, Up) 86 | c.Assert(err, IsNil) 87 | c.Assert(n, Equals, 2) 88 | 89 | // Can use column now 90 | _, err = s.DbMap.Exec("SELECT first_name FROM people") 91 | c.Assert(err, IsNil) 92 | } 93 | 94 | func (s *SqliteMigrateSuite) TestMigrateIncremental(c *C) { 95 | migrations := &MemoryMigrationSource{ 96 | Migrations: sqliteMigrations[:1], 97 | } 98 | 99 | // Executes one migration 100 | n, err := Exec(s.Db, "sqlite3", migrations, Up) 101 | c.Assert(err, IsNil) 102 | c.Assert(n, Equals, 1) 103 | 104 | // Execute a new migration 105 | migrations = &MemoryMigrationSource{ 106 | Migrations: sqliteMigrations[:2], 107 | } 108 | n, err = Exec(s.Db, "sqlite3", migrations, Up) 109 | c.Assert(err, IsNil) 110 | c.Assert(n, Equals, 1) 111 | 112 | // Can use column now 113 | _, err = s.DbMap.Exec("SELECT first_name FROM people") 114 | c.Assert(err, IsNil) 115 | } 116 | 117 | func (s *SqliteMigrateSuite) TestFileMigrate(c *C) { 118 | migrations := &FileMigrationSource{ 119 | Dir: "test-migrations", 120 | } 121 | 122 | // Executes two migrations 123 | n, err := Exec(s.Db, "sqlite3", migrations, Up) 124 | c.Assert(err, IsNil) 125 | c.Assert(n, Equals, 2) 126 | 127 | // Has data 128 | id, err := s.DbMap.SelectInt("SELECT id FROM people") 129 | c.Assert(err, IsNil) 130 | c.Assert(id, Equals, int64(1)) 131 | } 132 | 133 | func (s *SqliteMigrateSuite) TestHttpFileSystemMigrate(c *C) { 134 | migrations := &HttpFileSystemMigrationSource{ 135 | FileSystem: http.Dir("test-migrations"), 136 | } 137 | 138 | // Executes two migrations 139 | n, err := Exec(s.Db, "sqlite3", migrations, Up) 140 | c.Assert(err, IsNil) 141 | c.Assert(n, Equals, 2) 142 | 143 | // Has data 144 | id, err := s.DbMap.SelectInt("SELECT id FROM people") 145 | c.Assert(err, IsNil) 146 | c.Assert(id, Equals, int64(1)) 147 | } 148 | 149 | func (s *SqliteMigrateSuite) TestAssetMigrate(c *C) { 150 | migrations := &AssetMigrationSource{ 151 | Asset: Asset, 152 | AssetDir: AssetDir, 153 | Dir: "test-migrations", 154 | } 155 | 156 | // Executes two migrations 157 | n, err := Exec(s.Db, "sqlite3", migrations, Up) 158 | c.Assert(err, IsNil) 159 | c.Assert(n, Equals, 2) 160 | 161 | // Has data 162 | id, err := s.DbMap.SelectInt("SELECT id FROM people") 163 | c.Assert(err, IsNil) 164 | c.Assert(id, Equals, int64(1)) 165 | } 166 | 167 | func (s *SqliteMigrateSuite) TestMigrateMax(c *C) { 168 | migrations := &FileMigrationSource{ 169 | Dir: "test-migrations", 170 | } 171 | 172 | // Executes one migration 173 | n, err := ExecMax(s.Db, "sqlite3", migrations, Up, 1) 174 | c.Assert(err, IsNil) 175 | c.Assert(n, Equals, 1) 176 | 177 | id, err := s.DbMap.SelectInt("SELECT COUNT(*) FROM people") 178 | c.Assert(err, IsNil) 179 | c.Assert(id, Equals, int64(0)) 180 | } 181 | 182 | func (s *SqliteMigrateSuite) TestMigrateVersionInt(c *C) { 183 | migrations := &FileMigrationSource{ 184 | Dir: "test-migrations", 185 | } 186 | 187 | // Executes migration with target version 1 188 | n, err := ExecVersion(s.Db, "sqlite3", migrations, Up, 1) 189 | c.Assert(err, IsNil) 190 | c.Assert(n, Equals, 1) 191 | 192 | id, err := s.DbMap.SelectInt("SELECT COUNT(*) FROM people") 193 | c.Assert(err, IsNil) 194 | c.Assert(id, Equals, int64(0)) 195 | } 196 | 197 | func (s *SqliteMigrateSuite) TestMigrateVersionInt2(c *C) { 198 | migrations := &FileMigrationSource{ 199 | Dir: "test-migrations", 200 | } 201 | 202 | // Executes migration with target version 2 203 | n, err := ExecVersion(s.Db, "sqlite3", migrations, Up, 2) 204 | c.Assert(err, IsNil) 205 | c.Assert(n, Equals, 2) 206 | 207 | id, err := s.DbMap.SelectInt("SELECT COUNT(*) FROM people") 208 | c.Assert(err, IsNil) 209 | c.Assert(id, Equals, int64(1)) 210 | } 211 | 212 | func (s *SqliteMigrateSuite) TestMigrateVersionIntFailedWithNotExistingVerion(c *C) { 213 | migrations := &FileMigrationSource{ 214 | Dir: "test-migrations", 215 | } 216 | 217 | // Executes migration with not existing version 3 218 | _, err := ExecVersion(s.Db, "sqlite3", migrations, Up, 3) 219 | c.Assert(err, NotNil) 220 | } 221 | 222 | func (s *SqliteMigrateSuite) TestMigrateVersionIntFailedWithInvalidVerion(c *C) { 223 | migrations := &FileMigrationSource{ 224 | Dir: "test-migrations", 225 | } 226 | 227 | // Executes migration with invalid version -1 228 | _, err := ExecVersion(s.Db, "sqlite3", migrations, Up, -1) 229 | c.Assert(err, NotNil) 230 | } 231 | 232 | func (s *SqliteMigrateSuite) TestMigrateDown(c *C) { 233 | migrations := &FileMigrationSource{ 234 | Dir: "test-migrations", 235 | } 236 | 237 | n, err := Exec(s.Db, "sqlite3", migrations, Up) 238 | c.Assert(err, IsNil) 239 | c.Assert(n, Equals, 2) 240 | 241 | // Has data 242 | id, err := s.DbMap.SelectInt("SELECT id FROM people") 243 | c.Assert(err, IsNil) 244 | c.Assert(id, Equals, int64(1)) 245 | 246 | // Undo the last one 247 | n, err = ExecMax(s.Db, "sqlite3", migrations, Down, 1) 248 | c.Assert(err, IsNil) 249 | c.Assert(n, Equals, 1) 250 | 251 | // No more data 252 | id, err = s.DbMap.SelectInt("SELECT COUNT(*) FROM people") 253 | c.Assert(err, IsNil) 254 | c.Assert(id, Equals, int64(0)) 255 | 256 | // Remove the table. 257 | n, err = ExecMax(s.Db, "sqlite3", migrations, Down, 1) 258 | c.Assert(err, IsNil) 259 | c.Assert(n, Equals, 1) 260 | 261 | // Cannot query it anymore 262 | _, err = s.DbMap.SelectInt("SELECT COUNT(*) FROM people") 263 | c.Assert(err, Not(IsNil)) 264 | 265 | // Nothing left to do. 266 | n, err = ExecMax(s.Db, "sqlite3", migrations, Down, 1) 267 | c.Assert(err, IsNil) 268 | c.Assert(n, Equals, 0) 269 | } 270 | 271 | func (s *SqliteMigrateSuite) TestMigrateDownFull(c *C) { 272 | migrations := &FileMigrationSource{ 273 | Dir: "test-migrations", 274 | } 275 | 276 | n, err := Exec(s.Db, "sqlite3", migrations, Up) 277 | c.Assert(err, IsNil) 278 | c.Assert(n, Equals, 2) 279 | 280 | // Has data 281 | id, err := s.DbMap.SelectInt("SELECT id FROM people") 282 | c.Assert(err, IsNil) 283 | c.Assert(id, Equals, int64(1)) 284 | 285 | // Undo the last one 286 | n, err = Exec(s.Db, "sqlite3", migrations, Down) 287 | c.Assert(err, IsNil) 288 | c.Assert(n, Equals, 2) 289 | 290 | // Cannot query it anymore 291 | _, err = s.DbMap.SelectInt("SELECT COUNT(*) FROM people") 292 | c.Assert(err, Not(IsNil)) 293 | 294 | // Nothing left to do. 295 | n, err = Exec(s.Db, "sqlite3", migrations, Down) 296 | c.Assert(err, IsNil) 297 | c.Assert(n, Equals, 0) 298 | } 299 | 300 | func (s *SqliteMigrateSuite) TestMigrateTransaction(c *C) { 301 | migrations := &MemoryMigrationSource{ 302 | Migrations: []*Migration{ 303 | sqliteMigrations[0], 304 | sqliteMigrations[1], 305 | { 306 | Id: "125", 307 | Up: []string{"INSERT INTO people (id, first_name) VALUES (1, 'Test')", "SELECT fail"}, 308 | Down: []string{}, // Not important here 309 | }, 310 | }, 311 | } 312 | 313 | // Should fail, transaction should roll back the INSERT. 314 | n, err := Exec(s.Db, "sqlite3", migrations, Up) 315 | c.Assert(err, Not(IsNil)) 316 | c.Assert(n, Equals, 2) 317 | 318 | // INSERT should be rolled back 319 | count, err := s.DbMap.SelectInt("SELECT COUNT(*) FROM people") 320 | c.Assert(err, IsNil) 321 | c.Assert(count, Equals, int64(0)) 322 | } 323 | 324 | func (s *SqliteMigrateSuite) TestPlanMigration(c *C) { 325 | migrations := &MemoryMigrationSource{ 326 | Migrations: []*Migration{ 327 | { 328 | Id: "1_create_table.sql", 329 | Up: []string{"CREATE TABLE people (id int)"}, 330 | Down: []string{"DROP TABLE people"}, 331 | }, 332 | { 333 | Id: "2_alter_table.sql", 334 | Up: []string{"ALTER TABLE people ADD COLUMN first_name text"}, 335 | Down: []string{"SELECT 0"}, // Not really supported 336 | }, 337 | { 338 | Id: "10_add_last_name.sql", 339 | Up: []string{"ALTER TABLE people ADD COLUMN last_name text"}, 340 | Down: []string{"ALTER TABLE people DROP COLUMN last_name"}, 341 | }, 342 | }, 343 | } 344 | n, err := Exec(s.Db, "sqlite3", migrations, Up) 345 | c.Assert(err, IsNil) 346 | c.Assert(n, Equals, 3) 347 | 348 | migrations.Migrations = append(migrations.Migrations, &Migration{ 349 | Id: "11_add_middle_name.sql", 350 | Up: []string{"ALTER TABLE people ADD COLUMN middle_name text"}, 351 | Down: []string{"ALTER TABLE people DROP COLUMN middle_name"}, 352 | }) 353 | 354 | plannedMigrations, _, err := PlanMigration(s.Db, "sqlite3", migrations, Up, 0) 355 | c.Assert(err, IsNil) 356 | c.Assert(plannedMigrations, HasLen, 1) 357 | c.Assert(plannedMigrations[0].Migration, Equals, migrations.Migrations[3]) 358 | 359 | plannedMigrations, _, err = PlanMigration(s.Db, "sqlite3", migrations, Down, 0) 360 | c.Assert(err, IsNil) 361 | c.Assert(plannedMigrations, HasLen, 3) 362 | c.Assert(plannedMigrations[0].Migration, Equals, migrations.Migrations[2]) 363 | c.Assert(plannedMigrations[1].Migration, Equals, migrations.Migrations[1]) 364 | c.Assert(plannedMigrations[2].Migration, Equals, migrations.Migrations[0]) 365 | } 366 | 367 | func (s *SqliteMigrateSuite) TestSkipMigration(c *C) { 368 | migrations := &MemoryMigrationSource{ 369 | Migrations: []*Migration{ 370 | { 371 | Id: "1_create_table.sql", 372 | Up: []string{"CREATE TABLE people (id int)"}, 373 | Down: []string{"DROP TABLE people"}, 374 | }, 375 | { 376 | Id: "2_alter_table.sql", 377 | Up: []string{"ALTER TABLE people ADD COLUMN first_name text"}, 378 | Down: []string{"SELECT 0"}, // Not really supported 379 | }, 380 | { 381 | Id: "10_add_last_name.sql", 382 | Up: []string{"ALTER TABLE people ADD COLUMN last_name text"}, 383 | Down: []string{"ALTER TABLE people DROP COLUMN last_name"}, 384 | }, 385 | }, 386 | } 387 | n, err := SkipMax(s.Db, "sqlite3", migrations, Up, 0) 388 | // there should be no errors 389 | c.Assert(err, IsNil) 390 | // we should have detected and skipped 3 migrations 391 | c.Assert(n, Equals, 3) 392 | // should not actually have the tables now since it was skipped 393 | // so this query should fail 394 | _, err = s.DbMap.Exec("SELECT * FROM people") 395 | c.Assert(err, NotNil) 396 | // run the migrations again, should execute none of them since we pegged the db level 397 | // in the skip command 398 | n2, err2 := Exec(s.Db, "sqlite3", migrations, Up) 399 | // there should be no errors 400 | c.Assert(err2, IsNil) 401 | // we should not have executed any migrations 402 | c.Assert(n2, Equals, 0) 403 | } 404 | 405 | func (s *SqliteMigrateSuite) TestPlanMigrationWithHoles(c *C) { 406 | up := "SELECT 0" 407 | down := "SELECT 1" 408 | migrations := &MemoryMigrationSource{ 409 | Migrations: []*Migration{ 410 | { 411 | Id: "1", 412 | Up: []string{up}, 413 | Down: []string{down}, 414 | }, 415 | { 416 | Id: "3", 417 | Up: []string{up}, 418 | Down: []string{down}, 419 | }, 420 | }, 421 | } 422 | n, err := Exec(s.Db, "sqlite3", migrations, Up) 423 | c.Assert(err, IsNil) 424 | c.Assert(n, Equals, 2) 425 | 426 | migrations.Migrations = append(migrations.Migrations, &Migration{ 427 | Id: "2", 428 | Up: []string{up}, 429 | Down: []string{down}, 430 | }) 431 | 432 | migrations.Migrations = append(migrations.Migrations, &Migration{ 433 | Id: "4", 434 | Up: []string{up}, 435 | Down: []string{down}, 436 | }) 437 | 438 | migrations.Migrations = append(migrations.Migrations, &Migration{ 439 | Id: "5", 440 | Up: []string{up}, 441 | Down: []string{down}, 442 | }) 443 | 444 | // apply all the missing migrations 445 | plannedMigrations, _, err := PlanMigration(s.Db, "sqlite3", migrations, Up, 0) 446 | c.Assert(err, IsNil) 447 | c.Assert(plannedMigrations, HasLen, 3) 448 | c.Assert(plannedMigrations[0].Id, Equals, "2") 449 | c.Assert(plannedMigrations[0].Queries[0], Equals, up) 450 | c.Assert(plannedMigrations[1].Id, Equals, "4") 451 | c.Assert(plannedMigrations[1].Queries[0], Equals, up) 452 | c.Assert(plannedMigrations[2].Id, Equals, "5") 453 | c.Assert(plannedMigrations[2].Queries[0], Equals, up) 454 | 455 | // first catch up to current target state 123, then migrate down 1 step to 12 456 | plannedMigrations, _, err = PlanMigration(s.Db, "sqlite3", migrations, Down, 1) 457 | c.Assert(err, IsNil) 458 | c.Assert(plannedMigrations, HasLen, 2) 459 | c.Assert(plannedMigrations[0].Id, Equals, "2") 460 | c.Assert(plannedMigrations[0].Queries[0], Equals, up) 461 | c.Assert(plannedMigrations[1].Id, Equals, "3") 462 | c.Assert(plannedMigrations[1].Queries[0], Equals, down) 463 | 464 | // first catch up to current target state 123, then migrate down 2 steps to 1 465 | plannedMigrations, _, err = PlanMigration(s.Db, "sqlite3", migrations, Down, 2) 466 | c.Assert(err, IsNil) 467 | c.Assert(plannedMigrations, HasLen, 3) 468 | c.Assert(plannedMigrations[0].Id, Equals, "2") 469 | c.Assert(plannedMigrations[0].Queries[0], Equals, up) 470 | c.Assert(plannedMigrations[1].Id, Equals, "3") 471 | c.Assert(plannedMigrations[1].Queries[0], Equals, down) 472 | c.Assert(plannedMigrations[2].Id, Equals, "2") 473 | c.Assert(plannedMigrations[2].Queries[0], Equals, down) 474 | } 475 | 476 | func (*SqliteMigrateSuite) TestLess(c *C) { 477 | c.Assert((Migration{Id: "1"}).Less(&Migration{Id: "2"}), Equals, true) // 1 less than 2 478 | c.Assert((Migration{Id: "2"}).Less(&Migration{Id: "1"}), Equals, false) // 2 not less than 1 479 | c.Assert((Migration{Id: "1"}).Less(&Migration{Id: "a"}), Equals, true) // 1 less than a 480 | c.Assert((Migration{Id: "a"}).Less(&Migration{Id: "1"}), Equals, false) // a not less than 1 481 | c.Assert((Migration{Id: "a"}).Less(&Migration{Id: "a"}), Equals, false) // a not less than a 482 | c.Assert((Migration{Id: "1-a"}).Less(&Migration{Id: "1-b"}), Equals, true) // 1-a less than 1-b 483 | c.Assert((Migration{Id: "1-b"}).Less(&Migration{Id: "1-a"}), Equals, false) // 1-b not less than 1-a 484 | c.Assert((Migration{Id: "1"}).Less(&Migration{Id: "10"}), Equals, true) // 1 less than 10 485 | c.Assert((Migration{Id: "10"}).Less(&Migration{Id: "1"}), Equals, false) // 10 not less than 1 486 | c.Assert((Migration{Id: "1_foo"}).Less(&Migration{Id: "10_bar"}), Equals, true) // 1_foo not less than 1 487 | c.Assert((Migration{Id: "10_bar"}).Less(&Migration{Id: "1_foo"}), Equals, false) // 10 not less than 1 488 | // 20160126_1100 less than 20160126_1200 489 | c.Assert((Migration{Id: "20160126_1100"}). 490 | Less(&Migration{Id: "20160126_1200"}), Equals, true) 491 | // 20160126_1200 not less than 20160126_1100 492 | c.Assert((Migration{Id: "20160126_1200"}). 493 | Less(&Migration{Id: "20160126_1100"}), Equals, false) 494 | } 495 | 496 | func (s *SqliteMigrateSuite) TestPlanMigrationWithUnknownDatabaseMigrationApplied(c *C) { 497 | migrations := &MemoryMigrationSource{ 498 | Migrations: []*Migration{ 499 | { 500 | Id: "1_create_table.sql", 501 | Up: []string{"CREATE TABLE people (id int)"}, 502 | Down: []string{"DROP TABLE people"}, 503 | }, 504 | { 505 | Id: "2_alter_table.sql", 506 | Up: []string{"ALTER TABLE people ADD COLUMN first_name text"}, 507 | Down: []string{"SELECT 0"}, // Not really supported 508 | }, 509 | { 510 | Id: "10_add_last_name.sql", 511 | Up: []string{"ALTER TABLE people ADD COLUMN last_name text"}, 512 | Down: []string{"ALTER TABLE people DROP COLUMN last_name"}, 513 | }, 514 | }, 515 | } 516 | n, err := Exec(s.Db, "sqlite3", migrations, Up) 517 | c.Assert(err, IsNil) 518 | c.Assert(n, Equals, 3) 519 | 520 | // Note that migration 10_add_last_name.sql is missing from the new migrations source 521 | // so it is considered an "unknown" migration for the planner. 522 | migrations.Migrations = append(migrations.Migrations[:2], &Migration{ 523 | Id: "10_add_middle_name.sql", 524 | Up: []string{"ALTER TABLE people ADD COLUMN middle_name text"}, 525 | Down: []string{"ALTER TABLE people DROP COLUMN middle_name"}, 526 | }) 527 | 528 | _, _, err = PlanMigration(s.Db, "sqlite3", migrations, Up, 0) 529 | c.Assert(err, NotNil, Commentf("Up migrations should not have been applied when there "+ 530 | "is an unknown migration in the database")) 531 | c.Assert(err, FitsTypeOf, &PlanError{}) 532 | 533 | _, _, err = PlanMigration(s.Db, "sqlite3", migrations, Down, 0) 534 | c.Assert(err, NotNil, Commentf("Down migrations should not have been applied when there "+ 535 | "is an unknown migration in the database")) 536 | c.Assert(err, FitsTypeOf, &PlanError{}) 537 | } 538 | 539 | func (s *SqliteMigrateSuite) TestPlanMigrationWithIgnoredUnknownDatabaseMigrationApplied(c *C) { 540 | migrations := &MemoryMigrationSource{ 541 | Migrations: []*Migration{ 542 | { 543 | Id: "1_create_table.sql", 544 | Up: []string{"CREATE TABLE people (id int)"}, 545 | Down: []string{"DROP TABLE people"}, 546 | }, 547 | { 548 | Id: "2_alter_table.sql", 549 | Up: []string{"ALTER TABLE people ADD COLUMN first_name text"}, 550 | Down: []string{"SELECT 0"}, // Not really supported 551 | }, 552 | { 553 | Id: "10_add_last_name.sql", 554 | Up: []string{"ALTER TABLE people ADD COLUMN last_name text"}, 555 | Down: []string{"ALTER TABLE people DROP COLUMN last_name"}, 556 | }, 557 | }, 558 | } 559 | SetIgnoreUnknown(true) 560 | n, err := Exec(s.Db, "sqlite3", migrations, Up) 561 | c.Assert(err, IsNil) 562 | c.Assert(n, Equals, 3) 563 | 564 | // Note that migration 10_add_last_name.sql is missing from the new migrations source 565 | // so it is considered an "unknown" migration for the planner. 566 | migrations.Migrations = append(migrations.Migrations[:2], &Migration{ 567 | Id: "10_add_middle_name.sql", 568 | Up: []string{"ALTER TABLE people ADD COLUMN middle_name text"}, 569 | Down: []string{"ALTER TABLE people DROP COLUMN middle_name"}, 570 | }) 571 | 572 | _, _, err = PlanMigration(s.Db, "sqlite3", migrations, Up, 0) 573 | c.Assert(err, IsNil) 574 | 575 | _, _, err = PlanMigration(s.Db, "sqlite3", migrations, Down, 0) 576 | c.Assert(err, IsNil) 577 | SetIgnoreUnknown(false) // Make sure we are not breaking other tests as this is globaly set 578 | } 579 | 580 | func (s *SqliteMigrateSuite) TestPlanMigrationToVersion(c *C) { 581 | migrations := &MemoryMigrationSource{ 582 | Migrations: []*Migration{ 583 | { 584 | Id: "1_create_table.sql", 585 | Up: []string{"CREATE TABLE people (id int)"}, 586 | Down: []string{"DROP TABLE people"}, 587 | }, 588 | { 589 | Id: "2_alter_table.sql", 590 | Up: []string{"ALTER TABLE people ADD COLUMN first_name text"}, 591 | Down: []string{"SELECT 0"}, // Not really supported 592 | }, 593 | { 594 | Id: "10_add_last_name.sql", 595 | Up: []string{"ALTER TABLE people ADD COLUMN last_name text"}, 596 | Down: []string{"ALTER TABLE people DROP COLUMN last_name"}, 597 | }, 598 | }, 599 | } 600 | n, err := Exec(s.Db, "sqlite3", migrations, Up) 601 | c.Assert(err, IsNil) 602 | c.Assert(n, Equals, 3) 603 | 604 | migrations.Migrations = append(migrations.Migrations, &Migration{ 605 | Id: "11_add_middle_name.sql", 606 | Up: []string{"ALTER TABLE people ADD COLUMN middle_name text"}, 607 | Down: []string{"ALTER TABLE people DROP COLUMN middle_name"}, 608 | }) 609 | 610 | plannedMigrations, _, err := PlanMigrationToVersion(s.Db, "sqlite3", migrations, Up, 11) 611 | c.Assert(err, IsNil) 612 | c.Assert(plannedMigrations, HasLen, 1) 613 | c.Assert(plannedMigrations[0].Migration, Equals, migrations.Migrations[3]) 614 | 615 | plannedMigrations, _, err = PlanMigrationToVersion(s.Db, "sqlite3", migrations, Down, 1) 616 | c.Assert(err, IsNil) 617 | c.Assert(plannedMigrations, HasLen, 3) 618 | c.Assert(plannedMigrations[0].Migration, Equals, migrations.Migrations[2]) 619 | c.Assert(plannedMigrations[1].Migration, Equals, migrations.Migrations[1]) 620 | c.Assert(plannedMigrations[2].Migration, Equals, migrations.Migrations[0]) 621 | } 622 | 623 | // TestExecWithUnknownMigrationInDatabase makes sure that problems found with planning the 624 | // migrations are propagated and returned by Exec. 625 | func (s *SqliteMigrateSuite) TestExecWithUnknownMigrationInDatabase(c *C) { 626 | migrations := &MemoryMigrationSource{ 627 | Migrations: sqliteMigrations[:2], 628 | } 629 | 630 | // Executes two migrations 631 | n, err := Exec(s.Db, "sqlite3", migrations, Up) 632 | c.Assert(err, IsNil) 633 | c.Assert(n, Equals, 2) 634 | 635 | // Then create a new migration source with one of the migrations missing 636 | newSqliteMigrations := []*Migration{ 637 | { 638 | Id: "124_other", 639 | Up: []string{"ALTER TABLE people ADD COLUMN middle_name text"}, 640 | Down: []string{"ALTER TABLE people DROP COLUMN middle_name"}, 641 | }, 642 | { 643 | Id: "125", 644 | Up: []string{"ALTER TABLE people ADD COLUMN age int"}, 645 | Down: []string{"ALTER TABLE people DROP COLUMN age"}, 646 | }, 647 | } 648 | migrations = &MemoryMigrationSource{ 649 | Migrations: append(sqliteMigrations[:1], newSqliteMigrations...), 650 | } 651 | 652 | n, err = Exec(s.Db, "sqlite3", migrations, Up) 653 | c.Assert(err, NotNil, Commentf("Migrations should not have been applied when there "+ 654 | "is an unknown migration in the database")) 655 | c.Assert(err, FitsTypeOf, &PlanError{}) 656 | c.Assert(n, Equals, 0) 657 | 658 | // Make sure the new columns are not actually created 659 | _, err = s.DbMap.Exec("SELECT middle_name FROM people") 660 | c.Assert(err, NotNil) 661 | _, err = s.DbMap.Exec("SELECT age FROM people") 662 | c.Assert(err, NotNil) 663 | } 664 | 665 | func (s *SqliteMigrateSuite) TestRunMigrationObjDefaultTable(c *C) { 666 | migrations := &MemoryMigrationSource{ 667 | Migrations: sqliteMigrations[:1], 668 | } 669 | 670 | ms := MigrationSet{} 671 | // Executes one migration 672 | n, err := ms.Exec(s.Db, "sqlite3", migrations, Up) 673 | c.Assert(err, IsNil) 674 | c.Assert(n, Equals, 1) 675 | 676 | // Can use table now 677 | _, err = s.DbMap.Exec("SELECT * FROM people") 678 | c.Assert(err, IsNil) 679 | 680 | // Uses default tableName 681 | _, err = s.DbMap.Exec("SELECT * FROM gorp_migrations") 682 | c.Assert(err, IsNil) 683 | 684 | // Shouldn't apply migration again 685 | n, err = ms.Exec(s.Db, "sqlite3", migrations, Up) 686 | c.Assert(err, IsNil) 687 | c.Assert(n, Equals, 0) 688 | } 689 | 690 | func (s *SqliteMigrateSuite) TestRunMigrationObjOtherTable(c *C) { 691 | migrations := &MemoryMigrationSource{ 692 | Migrations: sqliteMigrations[:1], 693 | } 694 | 695 | ms := MigrationSet{TableName: "other_migrations"} 696 | // Executes one migration 697 | n, err := ms.Exec(s.Db, "sqlite3", migrations, Up) 698 | c.Assert(err, IsNil) 699 | c.Assert(n, Equals, 1) 700 | 701 | // Can use table now 702 | _, err = s.DbMap.Exec("SELECT * FROM people") 703 | c.Assert(err, IsNil) 704 | 705 | // Uses default tableName 706 | _, err = s.DbMap.Exec("SELECT * FROM other_migrations") 707 | c.Assert(err, IsNil) 708 | 709 | // Shouldn't apply migration again 710 | n, err = ms.Exec(s.Db, "sqlite3", migrations, Up) 711 | c.Assert(err, IsNil) 712 | c.Assert(n, Equals, 0) 713 | } 714 | 715 | func (*SqliteMigrateSuite) TestSetDisableCreateTable(c *C) { 716 | c.Assert(migSet.DisableCreateTable, Equals, false) 717 | 718 | SetDisableCreateTable(true) 719 | c.Assert(migSet.DisableCreateTable, Equals, true) 720 | 721 | SetDisableCreateTable(false) 722 | c.Assert(migSet.DisableCreateTable, Equals, false) 723 | } 724 | 725 | func (s *SqliteMigrateSuite) TestGetMigrationDbMapWithDisableCreateTable(c *C) { 726 | SetDisableCreateTable(false) 727 | 728 | _, err := migSet.getMigrationDbMap(s.Db, "postgres") 729 | c.Assert(err, IsNil) 730 | } 731 | 732 | // If ms.DisableCreateTable == true, then the the migrations table should not be 733 | // created, regardless of the global migSet.DisableCreateTable setting. 734 | func (s *SqliteMigrateSuite) TestGetMigrationObjDbMapWithDisableCreateTableTrue(c *C) { 735 | SetDisableCreateTable(false) 736 | ms := MigrationSet{ 737 | DisableCreateTable: true, 738 | TableName: "silly_example_table", 739 | } 740 | c.Assert(migSet.DisableCreateTable, Equals, false) 741 | c.Assert(ms.DisableCreateTable, Equals, true) 742 | 743 | dbMap, err := ms.getMigrationDbMap(s.Db, "sqlite3") 744 | c.Assert(err, IsNil) 745 | c.Assert(dbMap, NotNil) 746 | 747 | tableNameIfExists, err := s.DbMap.SelectNullStr( 748 | "SELECT name FROM sqlite_master WHERE type='table' AND name=$1", 749 | ms.TableName, 750 | ) 751 | c.Assert(err, IsNil) 752 | c.Assert(tableNameIfExists.Valid, Equals, false) 753 | } 754 | 755 | // If ms.DisableCreateTable == false, then the the migrations table should not be 756 | // created, regardless of the global migSet.DisableCreateTable setting. 757 | func (s *SqliteMigrateSuite) TestGetMigrationObjDbMapWithDisableCreateTableFalse(c *C) { 758 | SetDisableCreateTable(true) 759 | defer SetDisableCreateTable(false) // reset the global state when the test ends. 760 | ms := MigrationSet{ 761 | DisableCreateTable: false, 762 | TableName: "silly_example_table", 763 | } 764 | c.Assert(migSet.DisableCreateTable, Equals, true) 765 | c.Assert(ms.DisableCreateTable, Equals, false) 766 | 767 | dbMap, err := ms.getMigrationDbMap(s.Db, "sqlite3") 768 | c.Assert(err, IsNil) 769 | c.Assert(dbMap, NotNil) 770 | 771 | tableNameIfExists, err := s.DbMap.SelectNullStr( 772 | "SELECT name FROM sqlite_master WHERE type='table' AND name=$1", 773 | ms.TableName, 774 | ) 775 | c.Assert(err, IsNil) 776 | c.Assert(tableNameIfExists.Valid, Equals, true) 777 | c.Assert(tableNameIfExists.String, Equals, ms.TableName) 778 | } 779 | 780 | func (s *SqliteMigrateSuite) TestContextTimeout(c *C) { 781 | // This statement will run for a long time: 1,000,000 iterations of the fibonacci sequence 782 | fibonacciLoopStmt := `WITH RECURSIVE 783 | fibo (curr, next) 784 | AS 785 | ( SELECT 1,1 786 | UNION ALL 787 | SELECT next, curr+next FROM fibo 788 | LIMIT 1000000 ) 789 | SELECT group_concat(curr) FROM fibo; 790 | ` 791 | migrations := &MemoryMigrationSource{ 792 | Migrations: []*Migration{ 793 | sqliteMigrations[0], 794 | sqliteMigrations[1], 795 | { 796 | Id: "125", 797 | Up: []string{fibonacciLoopStmt}, 798 | Down: []string{}, // Not important here 799 | }, 800 | { 801 | Id: "125", 802 | Up: []string{"INSERT INTO people (id, first_name) VALUES (1, 'Test')", "SELECT fail"}, 803 | Down: []string{}, // Not important here 804 | }, 805 | }, 806 | } 807 | 808 | // Should never run the insert 809 | ctx, cancelFunc := context.WithTimeout(context.Background(), 10*time.Millisecond) 810 | defer cancelFunc() 811 | n, err := ExecContext(ctx, s.Db, "sqlite3", migrations, Up) 812 | c.Assert(err, Not(IsNil)) 813 | c.Assert(n, Equals, 2) 814 | } 815 | 816 | //go:embed test-migrations/* 817 | var testEmbedFS embed.FS 818 | 819 | func (s *SqliteMigrateSuite) TestEmbedSource(c *C) { 820 | migrations := EmbedFileSystemMigrationSource{ 821 | FileSystem: testEmbedFS, 822 | Root: "test-migrations", 823 | } 824 | 825 | // Executes two migrations 826 | n, err := Exec(s.Db, "sqlite3", migrations, Up) 827 | c.Assert(err, IsNil) 828 | c.Assert(n, Equals, 2) 829 | 830 | // Has data 831 | id, err := s.DbMap.SelectInt("SELECT id FROM people") 832 | c.Assert(err, IsNil) 833 | c.Assert(id, Equals, int64(1)) 834 | } 835 | -------------------------------------------------------------------------------- /sort_test.go: -------------------------------------------------------------------------------- 1 | package migrate 2 | 3 | import ( 4 | "sort" 5 | 6 | //revive:disable-next-line:dot-imports 7 | . "gopkg.in/check.v1" 8 | ) 9 | 10 | type SortSuite struct{} 11 | 12 | var _ = Suite(&SortSuite{}) 13 | 14 | func (*SortSuite) TestSortMigrations(c *C) { 15 | migrations := byId([]*Migration{ 16 | {Id: "10_abc", Up: nil, Down: nil}, 17 | {Id: "120_cde", Up: nil, Down: nil}, 18 | {Id: "1_abc", Up: nil, Down: nil}, 19 | {Id: "efg", Up: nil, Down: nil}, 20 | {Id: "2_cde", Up: nil, Down: nil}, 21 | {Id: "35_cde", Up: nil, Down: nil}, 22 | {Id: "3_efg", Up: nil, Down: nil}, 23 | {Id: "4_abc", Up: nil, Down: nil}, 24 | }) 25 | 26 | sort.Sort(migrations) 27 | c.Assert(migrations, HasLen, 8) 28 | c.Assert(migrations[0].Id, Equals, "1_abc") 29 | c.Assert(migrations[1].Id, Equals, "2_cde") 30 | c.Assert(migrations[2].Id, Equals, "3_efg") 31 | c.Assert(migrations[3].Id, Equals, "4_abc") 32 | c.Assert(migrations[4].Id, Equals, "10_abc") 33 | c.Assert(migrations[5].Id, Equals, "35_cde") 34 | c.Assert(migrations[6].Id, Equals, "120_cde") 35 | c.Assert(migrations[7].Id, Equals, "efg") 36 | } 37 | -------------------------------------------------------------------------------- /sql-migrate/command_common.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | migrate "github.com/rubenv/sql-migrate" 7 | ) 8 | 9 | func ApplyMigrations(dir migrate.MigrationDirection, dryrun bool, limit int, version int64) error { 10 | env, err := GetEnvironment() 11 | if err != nil { 12 | return fmt.Errorf("Could not parse config: %w", err) 13 | } 14 | 15 | db, dialect, err := GetConnection(env) 16 | if err != nil { 17 | return err 18 | } 19 | defer db.Close() 20 | 21 | source := migrate.FileMigrationSource{ 22 | Dir: env.Dir, 23 | } 24 | 25 | if dryrun { 26 | var migrations []*migrate.PlannedMigration 27 | 28 | if version >= 0 { 29 | migrations, _, err = migrate.PlanMigrationToVersion(db, dialect, source, dir, version) 30 | } else { 31 | migrations, _, err = migrate.PlanMigration(db, dialect, source, dir, limit) 32 | } 33 | 34 | if err != nil { 35 | return fmt.Errorf("Cannot plan migration: %w", err) 36 | } 37 | 38 | for _, m := range migrations { 39 | PrintMigration(m, dir) 40 | } 41 | } else { 42 | var n int 43 | 44 | if version >= 0 { 45 | n, err = migrate.ExecVersion(db, dialect, source, dir, version) 46 | } else { 47 | n, err = migrate.ExecMax(db, dialect, source, dir, limit) 48 | } 49 | 50 | if err != nil { 51 | return fmt.Errorf("Migration failed: %w", err) 52 | } 53 | 54 | if n == 1 { 55 | ui.Output("Applied 1 migration") 56 | } else { 57 | ui.Output(fmt.Sprintf("Applied %d migrations", n)) 58 | } 59 | } 60 | 61 | return nil 62 | } 63 | 64 | func PrintMigration(m *migrate.PlannedMigration, dir migrate.MigrationDirection) { 65 | switch dir { 66 | case migrate.Up: 67 | ui.Output(fmt.Sprintf("==> Would apply migration %s (up)", m.Id)) 68 | for _, q := range m.Up { 69 | ui.Output(q) 70 | } 71 | case migrate.Down: 72 | ui.Output(fmt.Sprintf("==> Would apply migration %s (down)", m.Id)) 73 | for _, q := range m.Down { 74 | ui.Output(q) 75 | } 76 | default: 77 | panic("Not reached") 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /sql-migrate/command_down.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "strings" 6 | 7 | migrate "github.com/rubenv/sql-migrate" 8 | ) 9 | 10 | type DownCommand struct{} 11 | 12 | func (*DownCommand) Help() string { 13 | helpText := ` 14 | Usage: sql-migrate down [options] ... 15 | 16 | Undo a database migration. 17 | 18 | Options: 19 | 20 | -config=dbconfig.yml Configuration file to use. 21 | -env="development" Environment. 22 | -limit=1 Limit the number of migrations (0 = unlimited). 23 | -version Run migrate down to a specific version, eg: the version number of migration 1_initial.sql is 1. 24 | -dryrun Don't apply migrations, just print them. 25 | 26 | ` 27 | return strings.TrimSpace(helpText) 28 | } 29 | 30 | func (*DownCommand) Synopsis() string { 31 | return "Undo a database migration" 32 | } 33 | 34 | func (c *DownCommand) Run(args []string) int { 35 | var limit int 36 | var version int64 37 | var dryrun bool 38 | 39 | cmdFlags := flag.NewFlagSet("down", flag.ContinueOnError) 40 | cmdFlags.Usage = func() { ui.Output(c.Help()) } 41 | cmdFlags.IntVar(&limit, "limit", 1, "Max number of migrations to apply.") 42 | cmdFlags.Int64Var(&version, "version", -1, "Migrate down to a specific version.") 43 | cmdFlags.BoolVar(&dryrun, "dryrun", false, "Don't apply migrations, just print them.") 44 | ConfigFlags(cmdFlags) 45 | 46 | if err := cmdFlags.Parse(args); err != nil { 47 | return 1 48 | } 49 | 50 | err := ApplyMigrations(migrate.Down, dryrun, limit, version) 51 | if err != nil { 52 | ui.Error(err.Error()) 53 | return 1 54 | } 55 | 56 | return 0 57 | } 58 | -------------------------------------------------------------------------------- /sql-migrate/command_new.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "path" 9 | "strings" 10 | "text/template" 11 | "time" 12 | ) 13 | 14 | var templateContent = ` 15 | -- +migrate Up 16 | 17 | -- +migrate Down 18 | ` 19 | var tpl = template.Must(template.New("new_migration").Parse(templateContent)) 20 | 21 | type NewCommand struct{} 22 | 23 | func (*NewCommand) Help() string { 24 | helpText := ` 25 | Usage: sql-migrate new [options] name 26 | 27 | Create a new a database migration. 28 | 29 | Options: 30 | 31 | -config=dbconfig.yml Configuration file to use. 32 | -env="development" Environment. 33 | name The name of the migration 34 | ` 35 | return strings.TrimSpace(helpText) 36 | } 37 | 38 | func (*NewCommand) Synopsis() string { 39 | return "Create a new migration" 40 | } 41 | 42 | func (c *NewCommand) Run(args []string) int { 43 | cmdFlags := flag.NewFlagSet("new", flag.ContinueOnError) 44 | cmdFlags.Usage = func() { ui.Output(c.Help()) } 45 | ConfigFlags(cmdFlags) 46 | 47 | if len(args) < 1 { 48 | err := errors.New("A name for the migration is needed") 49 | ui.Error(err.Error()) 50 | return 1 51 | } 52 | 53 | if err := cmdFlags.Parse(args); err != nil { 54 | return 1 55 | } 56 | 57 | if err := CreateMigration(cmdFlags.Arg(0)); err != nil { 58 | ui.Error(err.Error()) 59 | return 1 60 | } 61 | return 0 62 | } 63 | 64 | func CreateMigration(name string) error { 65 | env, err := GetEnvironment() 66 | if err != nil { 67 | return err 68 | } 69 | 70 | if _, err := os.Stat(env.Dir); os.IsNotExist(err) { 71 | return err 72 | } 73 | 74 | fileName := fmt.Sprintf("%s-%s.sql", time.Now().Format("20060102150405"), strings.TrimSpace(name)) 75 | pathName := path.Join(env.Dir, fileName) 76 | f, err := os.Create(pathName) 77 | if err != nil { 78 | return err 79 | } 80 | defer func() { _ = f.Close() }() 81 | 82 | if err := tpl.Execute(f, nil); err != nil { 83 | return err 84 | } 85 | 86 | ui.Output(fmt.Sprintf("Created migration %s", pathName)) 87 | return nil 88 | } 89 | -------------------------------------------------------------------------------- /sql-migrate/command_redo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "strings" 7 | 8 | migrate "github.com/rubenv/sql-migrate" 9 | ) 10 | 11 | type RedoCommand struct{} 12 | 13 | func (*RedoCommand) Help() string { 14 | helpText := ` 15 | Usage: sql-migrate redo [options] ... 16 | 17 | Reapply the last migration. 18 | 19 | Options: 20 | 21 | -config=dbconfig.yml Configuration file to use. 22 | -env="development" Environment. 23 | -dryrun Don't apply migrations, just print them. 24 | 25 | ` 26 | return strings.TrimSpace(helpText) 27 | } 28 | 29 | func (*RedoCommand) Synopsis() string { 30 | return "Reapply the last migration" 31 | } 32 | 33 | func (c *RedoCommand) Run(args []string) int { 34 | var dryrun bool 35 | 36 | cmdFlags := flag.NewFlagSet("redo", flag.ContinueOnError) 37 | cmdFlags.Usage = func() { ui.Output(c.Help()) } 38 | cmdFlags.BoolVar(&dryrun, "dryrun", false, "Don't apply migrations, just print them.") 39 | ConfigFlags(cmdFlags) 40 | 41 | if err := cmdFlags.Parse(args); err != nil { 42 | return 1 43 | } 44 | 45 | env, err := GetEnvironment() 46 | if err != nil { 47 | ui.Error(fmt.Sprintf("Could not parse config: %s", err)) 48 | return 1 49 | } 50 | 51 | db, dialect, err := GetConnection(env) 52 | if err != nil { 53 | ui.Error(err.Error()) 54 | return 1 55 | } 56 | defer db.Close() 57 | 58 | source := migrate.FileMigrationSource{ 59 | Dir: env.Dir, 60 | } 61 | 62 | migrations, _, err := migrate.PlanMigration(db, dialect, source, migrate.Down, 1) 63 | if err != nil { 64 | ui.Error(fmt.Sprintf("Migration (redo) failed: %v", err)) 65 | return 1 66 | } else if len(migrations) == 0 { 67 | ui.Output("Nothing to do!") 68 | return 0 69 | } 70 | 71 | if dryrun { 72 | PrintMigration(migrations[0], migrate.Down) 73 | PrintMigration(migrations[0], migrate.Up) 74 | } else { 75 | _, err := migrate.ExecMax(db, dialect, source, migrate.Down, 1) 76 | if err != nil { 77 | ui.Error(fmt.Sprintf("Migration (down) failed: %s", err)) 78 | return 1 79 | } 80 | 81 | _, err = migrate.ExecMax(db, dialect, source, migrate.Up, 1) 82 | if err != nil { 83 | ui.Error(fmt.Sprintf("Migration (up) failed: %s", err)) 84 | return 1 85 | } 86 | 87 | ui.Output(fmt.Sprintf("Reapplied migration %s.", migrations[0].Id)) 88 | } 89 | 90 | return 0 91 | } 92 | -------------------------------------------------------------------------------- /sql-migrate/command_skip.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "strings" 7 | 8 | migrate "github.com/rubenv/sql-migrate" 9 | ) 10 | 11 | type SkipCommand struct{} 12 | 13 | func (*SkipCommand) Help() string { 14 | helpText := ` 15 | Usage: sql-migrate skip [options] ... 16 | 17 | Set the database level to the most recent version available, without actually running the migrations. 18 | 19 | Options: 20 | 21 | -config=dbconfig.yml Configuration file to use. 22 | -env="development" Environment. 23 | -limit=0 Limit the number of migrations (0 = unlimited). 24 | 25 | ` 26 | return strings.TrimSpace(helpText) 27 | } 28 | 29 | func (*SkipCommand) Synopsis() string { 30 | return "Sets the database level to the most recent version available, without running the migrations" 31 | } 32 | 33 | func (c *SkipCommand) Run(args []string) int { 34 | var limit int 35 | 36 | cmdFlags := flag.NewFlagSet("up", flag.ContinueOnError) 37 | cmdFlags.Usage = func() { ui.Output(c.Help()) } 38 | cmdFlags.IntVar(&limit, "limit", 0, "Max number of migrations to skip.") 39 | ConfigFlags(cmdFlags) 40 | 41 | if err := cmdFlags.Parse(args); err != nil { 42 | return 1 43 | } 44 | 45 | err := SkipMigrations(migrate.Up, limit) 46 | if err != nil { 47 | ui.Error(err.Error()) 48 | return 1 49 | } 50 | 51 | return 0 52 | } 53 | 54 | func SkipMigrations(dir migrate.MigrationDirection, limit int) error { 55 | env, err := GetEnvironment() 56 | if err != nil { 57 | return fmt.Errorf("Could not parse config: %w", err) 58 | } 59 | 60 | db, dialect, err := GetConnection(env) 61 | if err != nil { 62 | return err 63 | } 64 | defer db.Close() 65 | 66 | source := migrate.FileMigrationSource{ 67 | Dir: env.Dir, 68 | } 69 | 70 | n, err := migrate.SkipMax(db, dialect, source, dir, limit) 71 | if err != nil { 72 | return fmt.Errorf("Migration failed: %w", err) 73 | } 74 | 75 | switch n { 76 | case 0: 77 | ui.Output("All migrations have already been applied") 78 | case 1: 79 | ui.Output("Skipped 1 migration") 80 | default: 81 | ui.Output(fmt.Sprintf("Skipped %d migrations", n)) 82 | } 83 | 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /sql-migrate/command_status.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | "github.com/olekukonko/tablewriter" 11 | 12 | migrate "github.com/rubenv/sql-migrate" 13 | ) 14 | 15 | type StatusCommand struct{} 16 | 17 | func (*StatusCommand) Help() string { 18 | helpText := ` 19 | Usage: sql-migrate status [options] ... 20 | 21 | Show migration status. 22 | 23 | Options: 24 | 25 | -config=dbconfig.yml Configuration file to use. 26 | -env="development" Environment. 27 | 28 | ` 29 | return strings.TrimSpace(helpText) 30 | } 31 | 32 | func (*StatusCommand) Synopsis() string { 33 | return "Show migration status" 34 | } 35 | 36 | func (c *StatusCommand) Run(args []string) int { 37 | cmdFlags := flag.NewFlagSet("status", flag.ContinueOnError) 38 | cmdFlags.Usage = func() { ui.Output(c.Help()) } 39 | ConfigFlags(cmdFlags) 40 | 41 | if err := cmdFlags.Parse(args); err != nil { 42 | return 1 43 | } 44 | 45 | env, err := GetEnvironment() 46 | if err != nil { 47 | ui.Error(fmt.Sprintf("Could not parse config: %s", err)) 48 | return 1 49 | } 50 | 51 | db, dialect, err := GetConnection(env) 52 | if err != nil { 53 | ui.Error(err.Error()) 54 | return 1 55 | } 56 | defer db.Close() 57 | 58 | source := migrate.FileMigrationSource{ 59 | Dir: env.Dir, 60 | } 61 | migrations, err := source.FindMigrations() 62 | if err != nil { 63 | ui.Error(err.Error()) 64 | return 1 65 | } 66 | 67 | records, err := migrate.GetMigrationRecords(db, dialect) 68 | if err != nil { 69 | ui.Error(err.Error()) 70 | return 1 71 | } 72 | 73 | table := tablewriter.NewWriter(os.Stdout) 74 | table.SetHeader([]string{"Migration", "Applied"}) 75 | table.SetColWidth(60) 76 | 77 | rows := make(map[string]*statusRow) 78 | 79 | for _, m := range migrations { 80 | rows[m.Id] = &statusRow{ 81 | Id: m.Id, 82 | Migrated: false, 83 | } 84 | } 85 | 86 | for _, r := range records { 87 | if rows[r.Id] == nil { 88 | ui.Warn(fmt.Sprintf("Could not find migration file: %v", r.Id)) 89 | continue 90 | } 91 | 92 | rows[r.Id].Migrated = true 93 | rows[r.Id].AppliedAt = r.AppliedAt 94 | } 95 | 96 | for _, m := range migrations { 97 | if rows[m.Id] != nil && rows[m.Id].Migrated { 98 | table.Append([]string{ 99 | m.Id, 100 | rows[m.Id].AppliedAt.String(), 101 | }) 102 | } else { 103 | table.Append([]string{ 104 | m.Id, 105 | "no", 106 | }) 107 | } 108 | } 109 | 110 | table.Render() 111 | 112 | return 0 113 | } 114 | 115 | type statusRow struct { 116 | Id string 117 | Migrated bool 118 | AppliedAt time.Time 119 | } 120 | -------------------------------------------------------------------------------- /sql-migrate/command_up.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "strings" 6 | 7 | migrate "github.com/rubenv/sql-migrate" 8 | ) 9 | 10 | type UpCommand struct{} 11 | 12 | func (*UpCommand) Help() string { 13 | helpText := ` 14 | Usage: sql-migrate up [options] ... 15 | 16 | Migrates the database to the most recent version available. 17 | 18 | Options: 19 | 20 | -config=dbconfig.yml Configuration file to use. 21 | -env="development" Environment. 22 | -limit=0 Limit the number of migrations (0 = unlimited). 23 | -version Run migrate up to a specific version, eg: the version number of migration 1_initial.sql is 1. 24 | -dryrun Don't apply migrations, just print them. 25 | 26 | ` 27 | return strings.TrimSpace(helpText) 28 | } 29 | 30 | func (*UpCommand) Synopsis() string { 31 | return "Migrates the database to the most recent version available" 32 | } 33 | 34 | func (c *UpCommand) Run(args []string) int { 35 | var limit int 36 | var version int64 37 | var dryrun bool 38 | 39 | cmdFlags := flag.NewFlagSet("up", flag.ContinueOnError) 40 | cmdFlags.Usage = func() { ui.Output(c.Help()) } 41 | cmdFlags.IntVar(&limit, "limit", 0, "Max number of migrations to apply.") 42 | cmdFlags.Int64Var(&version, "version", -1, "Migrate up to a specific version.") 43 | cmdFlags.BoolVar(&dryrun, "dryrun", false, "Don't apply migrations, just print them.") 44 | ConfigFlags(cmdFlags) 45 | 46 | if err := cmdFlags.Parse(args); err != nil { 47 | return 1 48 | } 49 | 50 | err := ApplyMigrations(migrate.Up, dryrun, limit, version) 51 | if err != nil { 52 | ui.Error(err.Error()) 53 | return 1 54 | } 55 | 56 | return 0 57 | } 58 | -------------------------------------------------------------------------------- /sql-migrate/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "os" 9 | "runtime/debug" 10 | 11 | "github.com/go-gorp/gorp/v3" 12 | "gopkg.in/yaml.v2" 13 | 14 | migrate "github.com/rubenv/sql-migrate" 15 | 16 | _ "github.com/go-sql-driver/mysql" 17 | _ "github.com/lib/pq" 18 | _ "github.com/mattn/go-sqlite3" 19 | ) 20 | 21 | var dialects = map[string]gorp.Dialect{ 22 | "sqlite3": gorp.SqliteDialect{}, 23 | "postgres": gorp.PostgresDialect{}, 24 | "mysql": gorp.MySQLDialect{Engine: "InnoDB", Encoding: "UTF8"}, 25 | } 26 | 27 | var ( 28 | ConfigFile string 29 | ConfigEnvironment string 30 | ) 31 | 32 | func ConfigFlags(f *flag.FlagSet) { 33 | f.StringVar(&ConfigFile, "config", "dbconfig.yml", "Configuration file to use.") 34 | f.StringVar(&ConfigEnvironment, "env", "development", "Environment to use.") 35 | } 36 | 37 | type Environment struct { 38 | Dialect string `yaml:"dialect"` 39 | DataSource string `yaml:"datasource"` 40 | Dir string `yaml:"dir"` 41 | TableName string `yaml:"table"` 42 | SchemaName string `yaml:"schema"` 43 | IgnoreUnknown bool `yaml:"ignoreunknown"` 44 | } 45 | 46 | func ReadConfig() (map[string]*Environment, error) { 47 | file, err := os.ReadFile(ConfigFile) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | config := make(map[string]*Environment) 53 | err = yaml.Unmarshal(file, config) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | return config, nil 59 | } 60 | 61 | func GetEnvironment() (*Environment, error) { 62 | config, err := ReadConfig() 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | env := config[ConfigEnvironment] 68 | if env == nil { 69 | return nil, errors.New("No environment: " + ConfigEnvironment) 70 | } 71 | 72 | if env.Dialect == "" { 73 | return nil, errors.New("No dialect specified") 74 | } 75 | 76 | if env.DataSource == "" { 77 | return nil, errors.New("No data source specified") 78 | } 79 | env.DataSource = os.ExpandEnv(env.DataSource) 80 | 81 | if env.Dir == "" { 82 | env.Dir = "migrations" 83 | } 84 | 85 | if env.TableName != "" { 86 | migrate.SetTable(env.TableName) 87 | } 88 | 89 | if env.SchemaName != "" { 90 | migrate.SetSchema(env.SchemaName) 91 | } 92 | 93 | migrate.SetIgnoreUnknown(env.IgnoreUnknown) 94 | 95 | return env, nil 96 | } 97 | 98 | func GetConnection(env *Environment) (*sql.DB, string, error) { 99 | db, err := sql.Open(env.Dialect, env.DataSource) 100 | if err != nil { 101 | return nil, "", fmt.Errorf("Cannot connect to database: %w", err) 102 | } 103 | 104 | // Make sure we only accept dialects that were compiled in. 105 | _, exists := dialects[env.Dialect] 106 | if !exists { 107 | return nil, "", fmt.Errorf("Unsupported dialect: %s", env.Dialect) 108 | } 109 | 110 | return db, env.Dialect, nil 111 | } 112 | 113 | // GetVersion returns the version. 114 | func GetVersion() string { 115 | if buildInfo, ok := debug.ReadBuildInfo(); ok && buildInfo.Main.Version != "(devel)" { 116 | return buildInfo.Main.Version 117 | } 118 | return "dev" 119 | } 120 | -------------------------------------------------------------------------------- /sql-migrate/godror.go: -------------------------------------------------------------------------------- 1 | //go:build godror 2 | // +build godror 3 | 4 | // godror is another oracle driver 5 | // repo: https://github.com/godror/godror 6 | // 7 | // godror package don't cofigure pkg config on your machine, 8 | // it mean that we don't need to specify oracle office client 9 | // at compile process and just config oracle client at runtime. 10 | package main 11 | 12 | import ( 13 | _ "github.com/godror/godror" 14 | migrate "github.com/rubenv/sql-migrate" 15 | ) 16 | 17 | func init() { 18 | dialects["godror"] = migrate.OracleDialect{} 19 | } 20 | -------------------------------------------------------------------------------- /sql-migrate/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/mitchellh/cli" 8 | ) 9 | 10 | func main() { 11 | os.Exit(realMain()) 12 | } 13 | 14 | var ui cli.Ui 15 | 16 | func realMain() int { 17 | ui = &cli.BasicUi{Writer: os.Stdout, ErrorWriter: os.Stderr} 18 | 19 | cli := &cli.CLI{ 20 | Args: os.Args[1:], 21 | Commands: map[string]cli.CommandFactory{ 22 | "up": func() (cli.Command, error) { 23 | return &UpCommand{}, nil 24 | }, 25 | "down": func() (cli.Command, error) { 26 | return &DownCommand{}, nil 27 | }, 28 | "redo": func() (cli.Command, error) { 29 | return &RedoCommand{}, nil 30 | }, 31 | "status": func() (cli.Command, error) { 32 | return &StatusCommand{}, nil 33 | }, 34 | "new": func() (cli.Command, error) { 35 | return &NewCommand{}, nil 36 | }, 37 | "skip": func() (cli.Command, error) { 38 | return &SkipCommand{}, nil 39 | }, 40 | }, 41 | HelpFunc: cli.BasicHelpFunc("sql-migrate"), 42 | HelpWriter: os.Stdout, 43 | ErrorWriter: os.Stderr, 44 | Version: GetVersion(), 45 | } 46 | 47 | exitCode, err := cli.Run() 48 | if err != nil { 49 | _, _ = fmt.Fprintf(os.Stderr, "Error executing CLI: %s\n", err.Error()) 50 | return 1 51 | } 52 | 53 | return exitCode 54 | } 55 | -------------------------------------------------------------------------------- /sql-migrate/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | -------------------------------------------------------------------------------- /sql-migrate/mssql.go: -------------------------------------------------------------------------------- 1 | //go:build go1.3 2 | // +build go1.3 3 | 4 | package main 5 | 6 | import ( 7 | _ "github.com/denisenkom/go-mssqldb" 8 | "github.com/go-gorp/gorp/v3" 9 | ) 10 | 11 | func init() { 12 | dialects["mssql"] = gorp.SqlServerDialect{} 13 | } 14 | -------------------------------------------------------------------------------- /sql-migrate/oracle.go: -------------------------------------------------------------------------------- 1 | //go:build oracle 2 | // +build oracle 3 | 4 | package main 5 | 6 | import ( 7 | _ "github.com/mattn/go-oci8" 8 | migrate "github.com/rubenv/sql-migrate" 9 | ) 10 | 11 | func init() { 12 | dialects["oci8"] = migrate.OracleDialect{} 13 | } 14 | -------------------------------------------------------------------------------- /sqlparse/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (C) 2014-2017 by Ruben Vermeersch 4 | Copyright (C) 2012-2014 by Liam Staskawicz 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /sqlparse/README.md: -------------------------------------------------------------------------------- 1 | # SQL migration parser 2 | 3 | Based on the [goose](https://bitbucket.org/liamstask/goose) migration parser. 4 | 5 | ## License 6 | 7 | This library is distributed under the [MIT](LICENSE) license. 8 | -------------------------------------------------------------------------------- /sqlparse/sqlparse.go: -------------------------------------------------------------------------------- 1 | package sqlparse 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "strings" 9 | ) 10 | 11 | const ( 12 | sqlCmdPrefix = "-- +migrate " 13 | optionNoTransaction = "notransaction" 14 | ) 15 | 16 | type ParsedMigration struct { 17 | UpStatements []string 18 | DownStatements []string 19 | 20 | DisableTransactionUp bool 21 | DisableTransactionDown bool 22 | } 23 | 24 | // LineSeparator can be used to split migrations by an exact line match. This line 25 | // will be removed from the output. If left blank, it is not considered. It is defaulted 26 | // to blank so you will have to set it manually. 27 | // Use case: in MSSQL, it is convenient to separate commands by GO statements like in 28 | // SQL Query Analyzer. 29 | var LineSeparator = "" 30 | 31 | func errNoTerminator() error { 32 | if len(LineSeparator) == 0 { 33 | return fmt.Errorf(`ERROR: The last statement must be ended by a semicolon or '-- +migrate StatementEnd' marker. 34 | See https://github.com/rubenv/sql-migrate for details.`) 35 | } 36 | 37 | return fmt.Errorf(`ERROR: The last statement must be ended by a semicolon, a line whose contents are %q, or '-- +migrate StatementEnd' marker. 38 | See https://github.com/rubenv/sql-migrate for details.`, LineSeparator) 39 | } 40 | 41 | // Checks the line to see if the line has a statement-ending semicolon 42 | // or if the line contains a double-dash comment. 43 | func endsWithSemicolon(line string) bool { 44 | prev := "" 45 | scanner := bufio.NewScanner(strings.NewReader(line)) 46 | scanner.Split(bufio.ScanWords) 47 | 48 | for scanner.Scan() { 49 | word := scanner.Text() 50 | if strings.HasPrefix(word, "--") { 51 | break 52 | } 53 | prev = word 54 | } 55 | 56 | return strings.HasSuffix(prev, ";") 57 | } 58 | 59 | type migrationDirection int 60 | 61 | const ( 62 | directionNone migrationDirection = iota 63 | directionUp 64 | directionDown 65 | ) 66 | 67 | type migrateCommand struct { 68 | Command string 69 | Options []string 70 | } 71 | 72 | func (c *migrateCommand) HasOption(opt string) bool { 73 | for _, specifiedOption := range c.Options { 74 | if specifiedOption == opt { 75 | return true 76 | } 77 | } 78 | 79 | return false 80 | } 81 | 82 | func parseCommand(line string) (*migrateCommand, error) { 83 | cmd := &migrateCommand{} 84 | 85 | if !strings.HasPrefix(line, sqlCmdPrefix) { 86 | return nil, fmt.Errorf("ERROR: not a sql-migrate command") 87 | } 88 | 89 | fields := strings.Fields(line[len(sqlCmdPrefix):]) 90 | if len(fields) == 0 { 91 | return nil, fmt.Errorf(`ERROR: incomplete migration command`) 92 | } 93 | 94 | cmd.Command = fields[0] 95 | 96 | cmd.Options = fields[1:] 97 | 98 | return cmd, nil 99 | } 100 | 101 | // Split the given sql script into individual statements. 102 | // 103 | // The base case is to simply split on semicolons, as these 104 | // naturally terminate a statement. 105 | // 106 | // However, more complex cases like pl/pgsql can have semicolons 107 | // within a statement. For these cases, we provide the explicit annotations 108 | // 'StatementBegin' and 'StatementEnd' to allow the script to 109 | // tell us to ignore semicolons. 110 | func ParseMigration(r io.ReadSeeker) (*ParsedMigration, error) { 111 | p := &ParsedMigration{} 112 | 113 | _, err := r.Seek(0, 0) 114 | if err != nil { 115 | return nil, err 116 | } 117 | 118 | var buf bytes.Buffer 119 | scanner := bufio.NewScanner(r) 120 | scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024) 121 | 122 | statementEnded := false 123 | ignoreSemicolons := false 124 | currentDirection := directionNone 125 | 126 | for scanner.Scan() { 127 | line := scanner.Text() 128 | // ignore comment except beginning with '-- +' 129 | if strings.HasPrefix(line, "-- ") && !strings.HasPrefix(line, "-- +") { 130 | continue 131 | } 132 | 133 | // handle any migrate-specific commands 134 | if strings.HasPrefix(line, sqlCmdPrefix) { 135 | cmd, err := parseCommand(line) 136 | if err != nil { 137 | return nil, err 138 | } 139 | 140 | switch cmd.Command { 141 | case "Up": 142 | if len(strings.TrimSpace(buf.String())) > 0 { 143 | return nil, errNoTerminator() 144 | } 145 | currentDirection = directionUp 146 | if cmd.HasOption(optionNoTransaction) { 147 | p.DisableTransactionUp = true 148 | } 149 | 150 | case "Down": 151 | if len(strings.TrimSpace(buf.String())) > 0 { 152 | return nil, errNoTerminator() 153 | } 154 | currentDirection = directionDown 155 | if cmd.HasOption(optionNoTransaction) { 156 | p.DisableTransactionDown = true 157 | } 158 | 159 | case "StatementBegin": 160 | if currentDirection != directionNone { 161 | ignoreSemicolons = true 162 | } 163 | 164 | case "StatementEnd": 165 | if currentDirection != directionNone { 166 | statementEnded = ignoreSemicolons 167 | ignoreSemicolons = false 168 | } 169 | } 170 | } 171 | 172 | if currentDirection == directionNone { 173 | continue 174 | } 175 | 176 | isLineSeparator := !ignoreSemicolons && len(LineSeparator) > 0 && line == LineSeparator 177 | 178 | if !isLineSeparator && !strings.HasPrefix(line, "-- +") { 179 | if _, err := buf.WriteString(line + "\n"); err != nil { 180 | return nil, err 181 | } 182 | } 183 | 184 | // Wrap up the two supported cases: 1) basic with semicolon; 2) psql statement 185 | // Lines that end with semicolon that are in a statement block 186 | // do not conclude statement. 187 | if (!ignoreSemicolons && (endsWithSemicolon(line) || isLineSeparator)) || statementEnded { 188 | statementEnded = false 189 | switch currentDirection { 190 | case directionUp: 191 | p.UpStatements = append(p.UpStatements, buf.String()) 192 | 193 | case directionDown: 194 | p.DownStatements = append(p.DownStatements, buf.String()) 195 | 196 | default: 197 | panic("impossible state") 198 | } 199 | 200 | buf.Reset() 201 | } 202 | } 203 | 204 | if err := scanner.Err(); err != nil { 205 | return nil, err 206 | } 207 | 208 | // diagnose likely migration script errors 209 | if ignoreSemicolons { 210 | return nil, fmt.Errorf("ERROR: saw '-- +migrate StatementBegin' with no matching '-- +migrate StatementEnd'") 211 | } 212 | 213 | if currentDirection == directionNone { 214 | return nil, fmt.Errorf(`ERROR: no Up/Down annotations found, so no statements were executed. 215 | See https://github.com/rubenv/sql-migrate for details.`) 216 | } 217 | 218 | // allow comment without sql instruction. Example: 219 | // -- +migrate Down 220 | // -- nothing to downgrade! 221 | if len(strings.TrimSpace(buf.String())) > 0 && !strings.HasPrefix(buf.String(), "-- +") { 222 | return nil, errNoTerminator() 223 | } 224 | 225 | return p, nil 226 | } 227 | -------------------------------------------------------------------------------- /sqlparse/sqlparse_test.go: -------------------------------------------------------------------------------- 1 | package sqlparse 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | //revive:disable-next-line:dot-imports 8 | . "gopkg.in/check.v1" 9 | ) 10 | 11 | func Test(t *testing.T) { TestingT(t) } 12 | 13 | type SqlParseSuite struct{} 14 | 15 | var _ = Suite(&SqlParseSuite{}) 16 | 17 | func (*SqlParseSuite) TestSemicolons(c *C) { 18 | type testData struct { 19 | line string 20 | result bool 21 | } 22 | 23 | tests := []testData{ 24 | { 25 | line: "END;", 26 | result: true, 27 | }, 28 | { 29 | line: "END; -- comment", 30 | result: true, 31 | }, 32 | { 33 | line: "END ; -- comment", 34 | result: true, 35 | }, 36 | { 37 | line: "END -- comment", 38 | result: false, 39 | }, 40 | { 41 | line: "END -- comment ;", 42 | result: false, 43 | }, 44 | { 45 | line: "END \" ; \" -- comment", 46 | result: false, 47 | }, 48 | } 49 | 50 | for _, test := range tests { 51 | r := endsWithSemicolon(test.line) 52 | c.Assert(r, Equals, test.result) 53 | } 54 | } 55 | 56 | func (*SqlParseSuite) TestSplitStatements(c *C) { 57 | type testData struct { 58 | sql string 59 | upCount int 60 | downCount int 61 | } 62 | 63 | tests := []testData{ 64 | { 65 | sql: functxt, 66 | upCount: 2, 67 | downCount: 2, 68 | }, 69 | { 70 | sql: multitxt, 71 | upCount: 2, 72 | downCount: 2, 73 | }, 74 | } 75 | 76 | for _, test := range tests { 77 | migration, err := ParseMigration(strings.NewReader(test.sql)) 78 | c.Assert(err, IsNil) 79 | c.Assert(migration.UpStatements, HasLen, test.upCount) 80 | c.Assert(migration.DownStatements, HasLen, test.downCount) 81 | } 82 | } 83 | 84 | func (*SqlParseSuite) TestIntentionallyBadStatements(c *C) { 85 | for _, test := range intentionallyBad { 86 | _, err := ParseMigration(strings.NewReader(test)) 87 | c.Assert(err, NotNil) 88 | } 89 | } 90 | 91 | func (*SqlParseSuite) TestJustComment(c *C) { 92 | for _, test := range justAComment { 93 | _, err := ParseMigration(strings.NewReader(test)) 94 | c.Assert(err, NotNil) 95 | } 96 | } 97 | 98 | func (*SqlParseSuite) TestCustomTerminator(c *C) { 99 | LineSeparator = "GO" 100 | defer func() { LineSeparator = "" }() 101 | 102 | type testData struct { 103 | sql string 104 | upCount int 105 | downCount int 106 | } 107 | 108 | tests := []testData{ 109 | { 110 | sql: functxtSplitByGO, 111 | upCount: 2, 112 | downCount: 2, 113 | }, 114 | { 115 | sql: multitxtSplitByGO, 116 | upCount: 2, 117 | downCount: 2, 118 | }, 119 | } 120 | 121 | for _, test := range tests { 122 | migration, err := ParseMigration(strings.NewReader(test.sql)) 123 | c.Assert(err, IsNil) 124 | c.Assert(migration.UpStatements, HasLen, test.upCount) 125 | c.Assert(migration.DownStatements, HasLen, test.downCount) 126 | } 127 | } 128 | 129 | var functxt = `-- +migrate Up 130 | CREATE TABLE IF NOT EXISTS histories ( 131 | id BIGSERIAL PRIMARY KEY, 132 | current_value varchar(2000) NOT NULL, 133 | created_at timestamp with time zone NOT NULL 134 | ); 135 | 136 | -- +migrate StatementBegin 137 | CREATE OR REPLACE FUNCTION histories_partition_creation( DATE, DATE ) 138 | returns void AS $$ 139 | DECLARE 140 | create_query text; 141 | BEGIN 142 | FOR create_query IN SELECT 143 | 'CREATE TABLE IF NOT EXISTS histories_' 144 | || TO_CHAR( d, 'YYYY_MM' ) 145 | || ' ( CHECK( created_at >= timestamp ''' 146 | || TO_CHAR( d, 'YYYY-MM-DD 00:00:00' ) 147 | || ''' AND created_at < timestamp ''' 148 | || TO_CHAR( d + INTERVAL '1 month', 'YYYY-MM-DD 00:00:00' ) 149 | || ''' ) ) inherits ( histories );' 150 | FROM generate_series( $1, $2, '1 month' ) AS d 151 | LOOP 152 | EXECUTE create_query; 153 | END LOOP; -- LOOP END 154 | END; -- FUNCTION END 155 | $$ 156 | language plpgsql; 157 | -- +migrate StatementEnd 158 | 159 | -- +migrate Down 160 | drop function histories_partition_creation(DATE, DATE); 161 | drop TABLE histories; 162 | ` 163 | 164 | // test multiple up/down transitions in a single script 165 | var multitxt = `-- +migrate Up 166 | CREATE TABLE post ( 167 | id int NOT NULL, 168 | title text, 169 | body text, 170 | PRIMARY KEY(id) 171 | ); 172 | 173 | -- +migrate Down 174 | DROP TABLE post; 175 | 176 | -- +migrate Up 177 | CREATE TABLE fancier_post ( 178 | id int NOT NULL, 179 | title text, 180 | body text, 181 | created_on timestamp without time zone, 182 | PRIMARY KEY(id) 183 | ); 184 | 185 | -- +migrate Down 186 | DROP TABLE fancier_post; 187 | ` 188 | 189 | // raise error when statements are not explicitly ended 190 | var intentionallyBad = []string{ 191 | // first statement missing terminator 192 | `-- +migrate Up 193 | CREATE TABLE post ( 194 | id int NOT NULL, 195 | title text, 196 | body text, 197 | PRIMARY KEY(id) 198 | ) 199 | 200 | -- +migrate Down 201 | DROP TABLE post; 202 | 203 | -- +migrate Up 204 | CREATE TABLE fancier_post ( 205 | id int NOT NULL, 206 | title text, 207 | body text, 208 | created_on timestamp without time zone, 209 | PRIMARY KEY(id) 210 | ); 211 | 212 | -- +migrate Down 213 | DROP TABLE fancier_post; 214 | `, 215 | 216 | // second half of first statement missing terminator 217 | `-- +migrate Up 218 | CREATE TABLE post ( 219 | id int NOT NULL, 220 | title text, 221 | body text, 222 | PRIMARY KEY(id) 223 | ); 224 | 225 | SELECT 'No ending semicolon' 226 | 227 | -- +migrate Down 228 | DROP TABLE post; 229 | 230 | -- +migrate Up 231 | CREATE TABLE fancier_post ( 232 | id int NOT NULL, 233 | title text, 234 | body text, 235 | created_on timestamp without time zone, 236 | PRIMARY KEY(id) 237 | ); 238 | 239 | -- +migrate Down 240 | DROP TABLE fancier_post; 241 | `, 242 | 243 | // second statement missing terminator 244 | `-- +migrate Up 245 | CREATE TABLE post ( 246 | id int NOT NULL, 247 | title text, 248 | body text, 249 | PRIMARY KEY(id) 250 | ); 251 | 252 | -- +migrate Down 253 | DROP TABLE post 254 | 255 | -- +migrate Up 256 | CREATE TABLE fancier_post ( 257 | id int NOT NULL, 258 | title text, 259 | body text, 260 | created_on timestamp without time zone, 261 | PRIMARY KEY(id) 262 | ); 263 | 264 | -- +migrate Down 265 | DROP TABLE fancier_post; 266 | `, 267 | 268 | // trailing text after explicit StatementEnd 269 | `-- +migrate Up 270 | -- +migrate StatementBegin 271 | CREATE TABLE post ( 272 | id int NOT NULL, 273 | title text, 274 | body text, 275 | PRIMARY KEY(id) 276 | ); 277 | -- +migrate StatementBegin 278 | SELECT 'no semicolon' 279 | 280 | -- +migrate Down 281 | DROP TABLE post; 282 | 283 | -- +migrate Up 284 | CREATE TABLE fancier_post ( 285 | id int NOT NULL, 286 | title text, 287 | body text, 288 | created_on timestamp without time zone, 289 | PRIMARY KEY(id) 290 | ); 291 | 292 | -- +migrate Down 293 | DROP TABLE fancier_post; 294 | `, 295 | } 296 | 297 | // Same as functxt above but split by GO lines 298 | var functxtSplitByGO = `-- +migrate Up 299 | CREATE TABLE IF NOT EXISTS histories ( 300 | id BIGSERIAL PRIMARY KEY, 301 | current_value varchar(2000) NOT NULL, 302 | created_at timestamp with time zone NOT NULL 303 | ) 304 | GO 305 | 306 | -- +migrate StatementBegin 307 | CREATE OR REPLACE FUNCTION histories_partition_creation( DATE, DATE ) 308 | returns void AS $$ 309 | DECLARE 310 | create_query text; 311 | BEGIN 312 | FOR create_query IN SELECT 313 | 'CREATE TABLE IF NOT EXISTS histories_' 314 | || TO_CHAR( d, 'YYYY_MM' ) 315 | || ' ( CHECK( created_at >= timestamp ''' 316 | || TO_CHAR( d, 'YYYY-MM-DD 00:00:00' ) 317 | || ''' AND created_at < timestamp ''' 318 | || TO_CHAR( d + INTERVAL '1 month', 'YYYY-MM-DD 00:00:00' ) 319 | || ''' ) ) inherits ( histories );' 320 | FROM generate_series( $1, $2, '1 month' ) AS d 321 | LOOP 322 | EXECUTE create_query; 323 | END LOOP; -- LOOP END 324 | END; -- FUNCTION END 325 | $$ 326 | GO 327 | /* while GO wouldn't be used in a statement like this, I'm including it for the test */ 328 | language plpgsql 329 | -- +migrate StatementEnd 330 | 331 | -- +migrate Down 332 | drop function histories_partition_creation(DATE, DATE) 333 | GO 334 | drop TABLE histories 335 | GO 336 | ` 337 | 338 | // test multiple up/down transitions in a single script, split by GO lines 339 | var multitxtSplitByGO = `-- +migrate Up 340 | CREATE TABLE post ( 341 | id int NOT NULL, 342 | title text, 343 | body text, 344 | PRIMARY KEY(id) 345 | ) 346 | GO 347 | 348 | -- +migrate Down 349 | DROP TABLE post 350 | GO 351 | 352 | -- +migrate Up 353 | CREATE TABLE fancier_post ( 354 | id int NOT NULL, 355 | title text, 356 | body text, 357 | created_on timestamp without time zone, 358 | PRIMARY KEY(id) 359 | ) 360 | GO 361 | 362 | -- +migrate Down 363 | DROP TABLE fancier_post 364 | GO 365 | ` 366 | 367 | // test a comment without sql instruction 368 | var justAComment = []string{ 369 | `-- +migrate Up 370 | CREATE TABLE post ( 371 | id int NOT NULL, 372 | title text, 373 | body text, 374 | PRIMARY KEY(id) 375 | ) 376 | 377 | -- +migrate Down 378 | -- no migration here 379 | `, 380 | } 381 | -------------------------------------------------------------------------------- /test-integration/dbconfig.yml: -------------------------------------------------------------------------------- 1 | postgres: 2 | dialect: postgres 3 | datasource: dbname=test sslmode=disable 4 | dir: test-migrations 5 | 6 | mysql: 7 | dialect: mysql 8 | datasource: root@/test?parseTime=true 9 | dir: test-migrations 10 | 11 | mysql_noflag: 12 | dialect: mysql 13 | datasource: root@/test 14 | dir: test-migrations 15 | 16 | mysql_env: 17 | dialect: mysql 18 | datasource: ${MYSQL_USER}:${MYSQL_PASSWORD}@tcp(${MYSQL_HOST}:${MYSQL_PORT})/${DATABASE_NAME}?parseTime=true 19 | dir: test-migrations 20 | 21 | sqlite: 22 | dialect: sqlite3 23 | datasource: test.db 24 | dir: test-migrations 25 | table: migrations 26 | -------------------------------------------------------------------------------- /test-integration/mysql-env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Tweak PATH for Travis 4 | export PATH=$PATH:$HOME/gopath/bin 5 | 6 | export MYSQL_USER=root 7 | export DATABASE_NAME=test_env 8 | export MYSQL_PASSWORD= 9 | export MYSQL_HOST=localhost 10 | export MYSQL_PORT=3306 11 | 12 | OPTIONS="-config=test-integration/dbconfig.yml -env mysql_env" 13 | 14 | set -ex 15 | 16 | sql-migrate status $OPTIONS 17 | sql-migrate up $OPTIONS 18 | sql-migrate down $OPTIONS 19 | sql-migrate redo $OPTIONS 20 | sql-migrate status $OPTIONS 21 | -------------------------------------------------------------------------------- /test-integration/mysql-flag.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Tweak PATH for Travis 4 | export PATH=$PATH:$HOME/gopath/bin 5 | 6 | OPTIONS="-config=test-integration/dbconfig.yml -env mysql_noflag" 7 | 8 | set -ex 9 | 10 | sql-migrate status $OPTIONS 2>&1 | grep -q "Make sure that the parseTime option is supplied" 11 | 12 | -------------------------------------------------------------------------------- /test-integration/mysql.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Tweak PATH for Travis 4 | export PATH=$PATH:$HOME/gopath/bin 5 | 6 | OPTIONS="-config=test-integration/dbconfig.yml -env mysql" 7 | 8 | set -ex 9 | 10 | sql-migrate status $OPTIONS 11 | sql-migrate up $OPTIONS 12 | sql-migrate down $OPTIONS 13 | sql-migrate redo $OPTIONS 14 | sql-migrate status $OPTIONS 15 | -------------------------------------------------------------------------------- /test-integration/postgres.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Tweak PATH for Travis 4 | export PATH=$PATH:$HOME/gopath/bin 5 | 6 | OPTIONS="-config=test-integration/dbconfig.yml -env postgres" 7 | 8 | set -ex 9 | 10 | sql-migrate status $OPTIONS 11 | sql-migrate up $OPTIONS 12 | sql-migrate down $OPTIONS 13 | sql-migrate redo $OPTIONS 14 | sql-migrate status $OPTIONS 15 | -------------------------------------------------------------------------------- /test-integration/sqlite.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Tweak PATH for Travis 4 | export PATH=$PATH:$HOME/gopath/bin 5 | 6 | OPTIONS="-config=test-integration/dbconfig.yml -env sqlite" 7 | 8 | set -ex 9 | 10 | sql-migrate status $OPTIONS 11 | sql-migrate up $OPTIONS 12 | sql-migrate down $OPTIONS 13 | sql-migrate redo $OPTIONS 14 | sql-migrate status $OPTIONS 15 | 16 | # Should have used the custom migrations table 17 | sqlite3 test.db "SELECT COUNT(*) FROM migrations" 18 | -------------------------------------------------------------------------------- /test-migrations/1_initial.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | -- SQL in section 'Up' is executed when this migration is applied 3 | CREATE TABLE people (id int); 4 | 5 | 6 | -- +migrate Down 7 | -- SQL section 'Down' is executed when this migration is rolled back 8 | DROP TABLE people; 9 | -------------------------------------------------------------------------------- /test-migrations/2_record.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | INSERT INTO people (id) VALUES (1); 3 | 4 | -- +migrate Down 5 | DELETE FROM people WHERE id=1; 6 | -------------------------------------------------------------------------------- /toapply_test.go: -------------------------------------------------------------------------------- 1 | package migrate 2 | 3 | import ( 4 | "sort" 5 | 6 | //revive:disable-next-line:dot-imports 7 | . "gopkg.in/check.v1" 8 | ) 9 | 10 | var toapplyMigrations = []*Migration{ 11 | {Id: "abc", Up: nil, Down: nil}, 12 | {Id: "cde", Up: nil, Down: nil}, 13 | {Id: "efg", Up: nil, Down: nil}, 14 | } 15 | 16 | type ToApplyMigrateSuite struct{} 17 | 18 | var _ = Suite(&ToApplyMigrateSuite{}) 19 | 20 | func (*ToApplyMigrateSuite) TestGetAll(c *C) { 21 | toApply := ToApply(toapplyMigrations, "", Up) 22 | c.Assert(toApply, HasLen, 3) 23 | c.Assert(toApply[0], Equals, toapplyMigrations[0]) 24 | c.Assert(toApply[1], Equals, toapplyMigrations[1]) 25 | c.Assert(toApply[2], Equals, toapplyMigrations[2]) 26 | } 27 | 28 | func (*ToApplyMigrateSuite) TestGetAbc(c *C) { 29 | toApply := ToApply(toapplyMigrations, "abc", Up) 30 | c.Assert(toApply, HasLen, 2) 31 | c.Assert(toApply[0], Equals, toapplyMigrations[1]) 32 | c.Assert(toApply[1], Equals, toapplyMigrations[2]) 33 | } 34 | 35 | func (*ToApplyMigrateSuite) TestGetCde(c *C) { 36 | toApply := ToApply(toapplyMigrations, "cde", Up) 37 | c.Assert(toApply, HasLen, 1) 38 | c.Assert(toApply[0], Equals, toapplyMigrations[2]) 39 | } 40 | 41 | func (*ToApplyMigrateSuite) TestGetDone(c *C) { 42 | toApply := ToApply(toapplyMigrations, "efg", Up) 43 | c.Assert(toApply, HasLen, 0) 44 | 45 | toApply = ToApply(toapplyMigrations, "zzz", Up) 46 | c.Assert(toApply, HasLen, 0) 47 | } 48 | 49 | func (*ToApplyMigrateSuite) TestDownDone(c *C) { 50 | toApply := ToApply(toapplyMigrations, "", Down) 51 | c.Assert(toApply, HasLen, 0) 52 | } 53 | 54 | func (*ToApplyMigrateSuite) TestDownCde(c *C) { 55 | toApply := ToApply(toapplyMigrations, "cde", Down) 56 | c.Assert(toApply, HasLen, 2) 57 | c.Assert(toApply[0], Equals, toapplyMigrations[1]) 58 | c.Assert(toApply[1], Equals, toapplyMigrations[0]) 59 | } 60 | 61 | func (*ToApplyMigrateSuite) TestDownAbc(c *C) { 62 | toApply := ToApply(toapplyMigrations, "abc", Down) 63 | c.Assert(toApply, HasLen, 1) 64 | c.Assert(toApply[0], Equals, toapplyMigrations[0]) 65 | } 66 | 67 | func (*ToApplyMigrateSuite) TestDownAll(c *C) { 68 | toApply := ToApply(toapplyMigrations, "efg", Down) 69 | c.Assert(toApply, HasLen, 3) 70 | c.Assert(toApply[0], Equals, toapplyMigrations[2]) 71 | c.Assert(toApply[1], Equals, toapplyMigrations[1]) 72 | c.Assert(toApply[2], Equals, toapplyMigrations[0]) 73 | 74 | toApply = ToApply(toapplyMigrations, "zzz", Down) 75 | c.Assert(toApply, HasLen, 3) 76 | c.Assert(toApply[0], Equals, toapplyMigrations[2]) 77 | c.Assert(toApply[1], Equals, toapplyMigrations[1]) 78 | c.Assert(toApply[2], Equals, toapplyMigrations[0]) 79 | } 80 | 81 | func (*ToApplyMigrateSuite) TestAlphaNumericMigrations(c *C) { 82 | migrations := byId([]*Migration{ 83 | {Id: "10_abc", Up: nil, Down: nil}, 84 | {Id: "1_abc", Up: nil, Down: nil}, 85 | {Id: "efg", Up: nil, Down: nil}, 86 | {Id: "2_cde", Up: nil, Down: nil}, 87 | {Id: "35_cde", Up: nil, Down: nil}, 88 | }) 89 | 90 | sort.Sort(migrations) 91 | 92 | toApplyUp := ToApply(migrations, "2_cde", Up) 93 | c.Assert(toApplyUp, HasLen, 3) 94 | c.Assert(toApplyUp[0].Id, Equals, "10_abc") 95 | c.Assert(toApplyUp[1].Id, Equals, "35_cde") 96 | c.Assert(toApplyUp[2].Id, Equals, "efg") 97 | 98 | toApplyDown := ToApply(migrations, "2_cde", Down) 99 | c.Assert(toApplyDown, HasLen, 2) 100 | c.Assert(toApplyDown[0].Id, Equals, "2_cde") 101 | c.Assert(toApplyDown[1].Id, Equals, "1_abc") 102 | } 103 | --------------------------------------------------------------------------------