Add vendor directory and update vendorHash to null

This commit is contained in:
soup 2026-01-17 22:29:54 -05:00
parent 523831cb8d
commit 1e5424c844
Signed by: soup
SSH key fingerprint: SHA256:GYxje8eQkJ6HZKzVWDdyOUF1TyDiprruGhE0Ym8qYDY
778 changed files with 407919 additions and 1 deletions

22
vendor/github.com/pressly/goose/v3/.gitignore generated vendored Normal file
View file

@ -0,0 +1,22 @@
.idea
.vscode
.DS_Store
*.swp
*.test
# Files output by tests
/bin
# Coverage files
coverage.out
coverage.html
# Local testing
.envrc
*.FAIL
dist/
release_notes.txt
go.work
go.work.sum

31
vendor/github.com/pressly/goose/v3/.golangci.yaml generated vendored Normal file
View file

@ -0,0 +1,31 @@
version: "2"
linters:
default: none
enable:
- errcheck
- govet
- ineffassign
- misspell
- staticcheck
- testifylint
- unused
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofmt
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$

37
vendor/github.com/pressly/goose/v3/.goreleaser.yaml generated vendored Normal file
View file

@ -0,0 +1,37 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
#
# See https://goreleaser.com/customization/ for more information.
version: 2
project_name: goose
before:
hooks:
- go mod tidy
builds:
- env:
- CGO_ENABLED=0
binary: goose
main: ./cmd/goose
goos:
- linux
- windows
- darwin
goarch:
- amd64
- arm64
ldflags:
# The v prefix is stripped by goreleaser, so we need to add it back.
# https://goreleaser.com/customization/templates/#fnref:version-prefix
- "-s -w -X main.version=v{{ .Version }}"
archives:
- formats:
- binary
name_template: >-
{{ .ProjectName }}_{{- tolower .Os }}_{{- if eq .Arch "amd64" }}x86_64{{- else }}{{ .Arch }}{{ end }}
checksum:
name_template: "checksums.txt"
snapshot:
version_template: "{{ incpatch .Version }}-next"
changelog:
use: github-native

316
vendor/github.com/pressly/goose/v3/CHANGELOG.md generated vendored Normal file
View file

@ -0,0 +1,316 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
- Add `*slog.Logger` support to goose provider via option `WithSlog` (#989)
- Add convenience `WithTableName` provider option (#985)
- Minor bug fixes and dependency upgrades
- Add general purpose `Locker` interface to support DB locking with a table-based Postgres
implementation via `lock.NewPostgresTableLocker` (#993 for more details)
- Unlike `SessionLocker`, this uses the `*sql.DB` connection pool
- Add `WithLocker` option to goose provider
## [v3.25.0] - 2025-08-24
- Upgrade go deps (#976)
- Remove references/tests for vertica and add deprecation warnings (#978)
- Add Aurora DSQL as a new database dialect to goose `Provider` (#971)
- Add DDL isolation support for Aurora DSQL compatibility (#970)
- Update Apply to respect no versioning option (#950)
- Expose dialect `Querier` (#939)
## [v3.24.3]
- Add `GOOSE_TABLE` environment variable -- lower priority than `-table` flag, but higher than the
default table name. (#932)
- Dependency updates
## [v3.24.2]
- Add `TableExists` table existence check for the mysql dialect (#895)
- Upgrade **minimum Go version to 1.23**
- Various dependency updates
## [v3.24.1]
- Fix regression (`v3.23.1` and `v3.24.0`) in postgres migration table existence check for
non-default schema. (#882, #883, #884).
## [v3.24.0]
- Add support for loading environment variables from `.env` files, enabled by default.
- The default file name is `.env`, but can be changed with the `-env=<filename>` flag.
- To disable this feature, set `-env=none`.
## [v3.23.1]
- Store implementations can **optionally** implement the `TableExists` method to provide optimized
table existence checks (#860)
- Default postgres Store implementation updated to use `pg_tables` system catalog, more to follow
- Backward compatible change - existing implementations will continue to work without modification
```go
TableExists(ctx context.Context, db database.DBTxConn) (bool, error)
```
## [v3.23.0]
- Add `WithLogger` to `NewProvider` to allow custom loggers (#833)
- Update Provider `WithVerbose` behavior to log all SQL statements (#851)
- Upgrade dependencies and rebuild binaries with latest Go version (`go1.23.3`)
## [v3.22.1]
- Upgrade dependencies and rebuild binaries with latest Go version (`go1.23.1`)
## [v3.22.0]
- Minimum Go version is now 1.21
- Add Unwrap to PartialError (#815)
- Allow flags anywhere on the CLI (#814)
`goose` uses the default Go `flag` parsing library, which means flags **must** be defined before the
first positional argument. We've updated this behavior to allow flags to be defined anywhere. For
more details, see [blog post](https://mfridman.com/blog/2024/allowing-flags-anywhere-on-the-cli/).
- Update `WithDisableGlobalRegistry` behavior (#783). When set, this will ignore globally-registered
migrationse entirely instead of the previous behavior of raising an error. Specifically, the
following check is removed:
```go
if len(global) > 0 {
return nil, errors.New("global registry disabled, but provider has registered go migrations")
}
```
This enables creating isolated goose provider(s) in legacy environments where global migrations may
be registered. Without updating this behavior, it would be impossible to use
`WithDisableGlobalRegistry` in combination with provider-scoped `WithGoMigrations`.
- Postgres, updated schema to use identity instead of serial and make `tstamp` not nullable (#556)
```diff
- id serial NOT NULL,
+ id integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
- tstamp timestamp NULL default now(),
+ tstamp timestamp NOT NULL DEFAULT now()
```
- MySQL, updated schema to not use SERIAL alias (#816)
```diff
- id serial NOT NULL,
+ id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
```
## [v3.21.1]
- Add `GetVersions` method to `goose.Provider`, returns the current (max db) version and the latest
(max filesystem) version. (#756)
- Clarify `GetLatestVersion` method MUST return `ErrVersionNotFound` if no latest migration is
found. Previously it was returning a -1 and nil error, which was inconsistent with the rest of the
API surface.
- Add `GetLatestVersion` implementations to all existing dialects. This is an optimization to avoid
loading all migrations when only the latest version is needed. This uses the `max` function in SQL
to get the latest version_id irrespective of the order of applied migrations.
- Refactor existing portions of the code to use the new `GetLatestVersion` method.
## [v3.21.0]
- Retracted. Broken release, please use v3.21.1 instead.
## [v3.20.0]
- Expand the `Store` interface by adding a `GetLatestVersion` method and make the interface public.
- Add a (non-blocking) method to check if there are pending migrations to the `goose.Provider`
(#751):
```go
func (p *Provider) HasPending(context.Context) (bool, error) {}
```
The underlying implementation **does not respect the `SessionLocker`** (if one is enabled) and can
be used to check for pending migrations without blocking or being blocked by other operations.
- The methods `.Up`, `.UpByOne`, and `.UpTo` from `goose.Provider` will invoke `.HasPending` before
acquiring a lock with `SessionLocker` (if enabled). This addresses an edge case in
Kubernetes-style deployments where newer pods with long-running migrations prevent older pods -
which have all known migrations applied - from starting up due to an advisory lock. For more
details, refer to https://github.com/pressly/goose/pull/507#discussion_r1266498077 and #751.
- Move integration tests to `./internal/testing` and make it a separate Go module. This will allow
us to have a cleaner top-level go.mod file and avoid imports unrelated to the goose project. See
[integration/README.md](https://github.com/pressly/goose/blob/d0641b5bfb3bd5d38d95fe7a63d7ddf2d282234d/internal/testing/integration/README.md)
for more details. This shouldn't affect users of the goose library.
## [v3.19.2] - 2024-03-13
- Remove duckdb support. The driver uses Cgo and we've decided to remove it until we can find a
better solution. If you were using duckdb with goose, please let us know by opening an issue.
## [v3.19.1] - 2024-03-11
- Fix selecting dialect for `redshift`
- Add `GOOSE_MIGRATION_DIR` documentation
- Bump github.com/opencontainers/runc to `v1.1.12` (security fix)
- Update CI tests for go1.22
- Make goose annotations case-insensitive
- All `-- +goose` annotations are now case-insensitive. This means that `-- +goose Up` and `--
+goose up` are now equivalent. This change was made to improve the user experience and to make the
annotations more consistent.
## [v3.19.0] - 2024-03-11
- Use [v3.19.1] instead. This was tagged but not released and does not contain release binaries.
## [v3.18.0] - 2024-01-31
- Add environment variable substitution for SQL migrations. (#604)
- This feature is **disabled by default**, and can be enabled by adding an annotation to the
migration file:
```sql
-- +goose ENVSUB ON
```
- When enabled, goose will attempt to substitute environment variables in the SQL migration
queries until the end of the file, or until the annotation `-- +goose ENVSUB OFF` is found. For
example, if the environment variable `REGION` is set to `us_east_1`, the following SQL migration
will be substituted to `SELECT * FROM regions WHERE name = 'us_east_1';`
```sql
-- +goose ENVSUB ON
-- +goose Up
SELECT * FROM regions WHERE name = '${REGION}';
```
- Add native [Turso](https://turso.tech/) support with libsql driver. (#658)
- Fixed query for list migrations in YDB (#684)
## [v3.17.0] - 2023-12-15
- Standardised the MIT license (#647)
- Improve provider `Apply()` errors, add `ErrNotApplied` when attempting to rollback a migration
that has not been previously applied. (#660)
- Add `WithDisableGlobalRegistry` option to `NewProvider` to disable the global registry. (#645)
- Add `-timeout` flag to CLI to set the maximum allowed duration for queries to run. Default remains
no timeout. (#627)
- Add optional logging in `Provider` when `WithVerbose` option is supplied. (#668)
⚠️ Potential Breaking Change ⚠️
- Update `goose create` to use UTC time instead of local time. (#242)
## [v3.16.0] - 2023-11-12
- Added YDB support. (#592)
- Fix sqlserver query to ensure DB version. (#601)
- Allow setting / resetting the global Go migration registry. (#602)
- `SetGlobalMigrations` and `ResetGlobalMigrations` functions have been added.
- Introduce `NewGoMigration` for constructing Go migrations.
- Add initial implementation of `goose.NewProvider`.
🎉 Read more about this new feature here:
https://pressly.github.io/goose/blog/2023/goose-provider/
The motivation behind the Provider was simple - to reduce global state and make goose easier to
consume as an imported package.
Here's a quick summary:
- Avoid global state
- Make Provider safe to use concurrently
- Unlock (no pun intended) new features, such as database locking
- Make logging configurable
- Better error handling with proper return values
- Double down on Go migrations
- ... and more!
## [v3.15.1] - 2023-10-10
- Fix regression that prevented registering Go migrations that didn't have the corresponding files
available in the filesystem. (#588)
- If Go migrations have been registered globally, but there are no .go files in the filesystem,
**always include** them.
- If Go migrations have been registered, and there are .go files in the filesystem, **only
include** those migrations. This was the original motivation behind #553.
- If there are .go files in the filesystem but not registered, **raise an error**. This is to
prevent accidentally adding valid looking Go migration files without explicitly registering
them.
## [v3.15.0] - 2023-08-12
- Fix `sqlparser` to avoid skipping the last statement when it's not terminated with a semicolon
within a StatementBegin/End block. (#580)
- Add `**go1.21**` to the CI matrix.
- Bump minimum version of module in go.mod to `go1.19`.
- Fix version output when installing pre-built binaries (#585).
## [v3.14.0] - 2023-07-26
- Filter registered Go migrations from the global map with corresponding .go files from the
filesystem.
- The code previously assumed all .go migrations would be in the same folder, so this should not
be a breaking change.
- See #553 for more details
- Improve output log message for applied up migrations. #562
- Fix an issue where `AddMigrationNoTxContext` was registering the wrong source because it skipped
too many frames. #572
- Improve binary version output when using go install.
## [v3.13.4] - 2023-07-07
- Fix pre-built binary versioning and make small improvements to GoReleaser config.
- Fix an edge case in the `sqlparser` where the last up statement may be ignored if it's
unterminated with a semicolon and followed by a `-- +goose Down` annotation.
- Trim `Logger` interface to `Printf` and `Fatalf` methods only. Projects that have previously
implemented the `Logger` interface should not be affected, and can remove unused methods.
## [v3.13.1] - 2023-07-03
- Add pre-built binaries with GoReleaser and update the build process.
## [v3.13.0] - 2023-06-29
- Add a changelog to the project, based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Update go.mod and retract all `v3.12.X` tags. They were accidentally pushed and contain a
reference to the wrong Go module.
- Fix `up` and `up -allowing-missing` behavior.
- Fix empty version in log output.
- Add new `context.Context`-aware functions and methods, for both sql and go migrations.
- Return error when no migration files found or dir is not a directory.
[Unreleased]: https://github.com/pressly/goose/compare/v3.25.0...HEAD
[v3.25.0]: https://github.com/pressly/goose/compare/v3.24.3...v3.25.0
[v3.24.3]: https://github.com/pressly/goose/compare/v3.24.2...v3.24.3
[v3.24.2]: https://github.com/pressly/goose/compare/v3.24.1...v3.24.2
[v3.24.1]: https://github.com/pressly/goose/compare/v3.24.0...v3.24.1
[v3.24.0]: https://github.com/pressly/goose/compare/v3.23.1...v3.24.0
[v3.23.1]: https://github.com/pressly/goose/compare/v3.23.0...v3.23.1
[v3.23.0]: https://github.com/pressly/goose/compare/v3.22.1...v3.23.0
[v3.22.1]: https://github.com/pressly/goose/compare/v3.22.0...v3.22.1
[v3.22.0]: https://github.com/pressly/goose/compare/v3.21.1...v3.22.0
[v3.21.1]: https://github.com/pressly/goose/compare/v3.20.0...v3.21.1
[v3.21.0]: https://github.com/pressly/goose/compare/v3.20.0...v3.21.0
[v3.20.0]: https://github.com/pressly/goose/compare/v3.19.2...v3.20.0
[v3.19.2]: https://github.com/pressly/goose/compare/v3.19.1...v3.19.2
[v3.19.1]: https://github.com/pressly/goose/compare/v3.19.0...v3.19.1
[v3.19.0]: https://github.com/pressly/goose/compare/v3.18.0...v3.19.0
[v3.18.0]: https://github.com/pressly/goose/compare/v3.17.0...v3.18.0
[v3.17.0]: https://github.com/pressly/goose/compare/v3.16.0...v3.17.0
[v3.16.0]: https://github.com/pressly/goose/compare/v3.15.1...v3.16.0
[v3.15.1]: https://github.com/pressly/goose/compare/v3.15.0...v3.15.1
[v3.15.0]: https://github.com/pressly/goose/compare/v3.14.0...v3.15.0
[v3.14.0]: https://github.com/pressly/goose/compare/v3.13.4...v3.14.0
[v3.13.4]: https://github.com/pressly/goose/compare/v3.13.1...v3.13.4
[v3.13.1]: https://github.com/pressly/goose/compare/v3.13.0...v3.13.1
[v3.13.0]: https://github.com/pressly/goose/releases/tag/v3.13.0

22
vendor/github.com/pressly/goose/v3/LICENSE generated vendored Normal file
View file

@ -0,0 +1,22 @@
MIT License
Original work Copyright (c) 2012 Liam Staskawicz
Modified work Copyright (c) 2016 Vojtech Vitek
Modified work Copyright (c) 2021 Michael Fridman, Vojtech Vitek
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

147
vendor/github.com/pressly/goose/v3/Makefile generated vendored Normal file
View file

@ -0,0 +1,147 @@
GO_TEST_FLAGS ?= -race -count=1 -v -timeout=5m -json
# These are the default values for the test database. They can be overridden
DB_USER ?= dbuser
DB_PASSWORD ?= password1
DB_NAME ?= testdb
DB_POSTGRES_PORT ?= 5433
DB_MYSQL_PORT ?= 3307
DB_CLICKHOUSE_PORT ?= 9001
DB_YDB_PORT ?= 2136
DB_TURSO_PORT ?= 8080
DB_STARROCKS_PORT ?= 9030
list-build-tags:
@echo "Available build tags:"
@echo "$$(rg -o --trim 'no_[a-zA-Z0-9_]+' ./cmd/goose \
--no-line-number --no-filename | sort | uniq | \
xargs -n 4 | column -t | sed 's/^/ /')"
.PHONY: dist
dist:
@mkdir -p ./bin
@rm -f ./bin/*
GOOS=darwin GOARCH=amd64 go build -o ./bin/goose-darwin64 ./cmd/goose
GOOS=linux GOARCH=amd64 go build -o ./bin/goose-linux64 ./cmd/goose
GOOS=linux GOARCH=386 go build -o ./bin/goose-linux386 ./cmd/goose
GOOS=windows GOARCH=amd64 go build -o ./bin/goose-windows64.exe ./cmd/goose
GOOS=windows GOARCH=386 go build -o ./bin/goose-windows386.exe ./cmd/goose
.PHONY: clean
clean:
@find . -type f -name '*.FAIL' -delete
.PHONY: lint
lint: tools
@golangci-lint run ./... --fix
.PHONY: tools
tools:
@go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
@go install github.com/mfridman/tparse@main
test-packages:
go test $(GO_TEST_FLAGS) $$(go list ./... | grep -v -e /bin -e /cmd -e /examples) |\
tparse --follow -sort=elapsed -trimpath=auto -all
test-packages-short:
go test -test.short $(GO_TEST_FLAGS) $$(go list ./... | grep -v -e /bin -e /cmd -e /examples) |\
tparse --follow -sort=elapsed
coverage-short:
go test ./ -test.short $(GO_TEST_FLAGS) -cover -coverprofile=coverage.out | tparse --follow -sort=elapsed
go tool cover -html=coverage.out
coverage:
go test ./ $(GO_TEST_FLAGS) -cover -coverprofile=coverage.out | tparse --follow -sort=elapsed
go tool cover -html=coverage.out
test-lock-coverage:
go test ./internal/testing/integration/locking ./lock/internal/... -cover -coverpkg=./lock/internal/... -coverprofile=coverage.out
go tool cover -html=coverage.out -o coverage.html
@echo "Lock package coverage: $$(go tool cover -func=coverage.out | tail -1 | awk '{print $$3}')"
open coverage.html
#
# Integration-related targets
#
add-gowork:
@[ -f go.work ] || go work init
@[ -f go.work.sum ] || go work use -r .
remove-gowork:
rm -rf go.work go.work.sum
upgrade-integration-deps:
cd ./internal/testing && go get -u ./... && go mod tidy
test-postgres-long: add-gowork test-postgres
go test $(GO_TEST_FLAGS) ./internal/testing/integration -run='(TestPostgresProviderLocking|TestPostgresSessionLocker)' |\
tparse --follow -sort=elapsed
test-postgres: add-gowork
go test $(GO_TEST_FLAGS) ./internal/testing/integration -run="^TestPostgres$$" | tparse --follow -sort=elapsed
test-clickhouse: add-gowork
go test $(GO_TEST_FLAGS) ./internal/testing/integration -run='(TestClickhouse|TestClickhouseRemote)' |\
tparse --follow -sort=elapsed
test-mysql: add-gowork
go test $(GO_TEST_FLAGS) ./internal/testing/integration -run='TestMySQL' | tparse --follow -sort=elapsed
test-turso: add-gowork
go test $(GO_TEST_FLAGS) ./internal/testing/integration -run='TestTurso' | tparse --follow -sort=elapsed
test-ydb: add-gowork
go test $(GO_TEST_FLAGS) ./internal/testing/integration -run='TestYDB' | tparse --follow -sort=elapsed
test-starrocks: add-gowork
go test $(GO_TEST_FLAGS) ./internal/testing/integration -run='TestStarrocks' | tparse --follow -sort=elapsed
test-integration: add-gowork
go test $(GO_TEST_FLAGS) ./internal/testing/integration/... | tparse --follow -sort=elapsed -trimpath=auto -all
#
# Docker-related targets
#
docker-cleanup:
docker stop -t=0 $$(docker ps --filter="label=goose_test" -aq)
docker-postgres:
docker run --rm -d \
-e POSTGRES_USER=$(DB_USER) \
-e POSTGRES_PASSWORD=$(DB_PASSWORD) \
-e POSTGRES_DB=$(DB_NAME) \
-p $(DB_POSTGRES_PORT):5432 \
-l goose_test \
postgres:14-alpine -c log_statement=all
echo "postgres://$(DB_USER):$(DB_PASSWORD)@localhost:$(DB_POSTGRES_PORT)/$(DB_NAME)?sslmode=disable"
docker-mysql:
docker run --rm -d \
-e MYSQL_ROOT_PASSWORD=rootpassword1 \
-e MYSQL_DATABASE=$(DB_NAME) \
-e MYSQL_USER=$(DB_USER) \
-e MYSQL_PASSWORD=$(DB_PASSWORD) \
-p $(DB_MYSQL_PORT):3306 \
-l goose_test \
mysql:8.0.31
echo "mysql://$(DB_USER):$(DB_PASSWORD)@localhost:$(DB_MYSQL_PORT)/$(DB_NAME)?parseTime=true"
docker-clickhouse:
docker run --rm -d \
-e CLICKHOUSE_DB=$(DB_NAME) \
-e CLICKHOUSE_USER=$(DB_USER) \
-e CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1 \
-e CLICKHOUSE_PASSWORD=$(DB_PASSWORD) \
-p $(DB_CLICKHOUSE_PORT):9000/tcp \
-l goose_test \
clickhouse/clickhouse-server:23-alpine
echo "clickhouse://$(DB_USER):$(DB_PASSWORD)@localhost:$(DB_CLICKHOUSE_PORT)/$(DB_NAME)"
docker-turso:
docker run --rm -d \
-p $(DB_TURSO_PORT):8080 \
-l goose_test \
ghcr.io/tursodatabase/libsql-server:v0.22.10

497
vendor/github.com/pressly/goose/v3/README.md generated vendored Normal file
View file

@ -0,0 +1,497 @@
# goose
<img align="right" width="125" src="assets/goose_logo.png">
[![Goose
CI](https://github.com/pressly/goose/actions/workflows/ci.yaml/badge.svg)](https://github.com/pressly/goose/actions/workflows/ci.yaml)
[![Go
Reference](https://pkg.go.dev/badge/github.com/pressly/goose/v3.svg)](https://pkg.go.dev/github.com/pressly/goose/v3)
[![Go Report
Card](https://goreportcard.com/badge/github.com/pressly/goose/v3)](https://goreportcard.com/report/github.com/pressly/goose/v3)
Goose is a database migration tool. Both a CLI and a library.
Manage your **database schema** by creating incremental SQL changes or Go functions.
#### Features
- Works against multiple databases:
- Postgres, MySQL, SQLite, YDB, ClickHouse, MSSQL, and
more.
- Supports Go migrations written as plain functions.
- Supports [embedded](https://pkg.go.dev/embed/) migrations.
- Out-of-order migrations.
- Seeding data.
- Environment variable substitution in SQL migrations.
- ... and more.
# Install
```shell
go install github.com/pressly/goose/v3/cmd/goose@latest
```
This will install the `goose` binary to your `$GOPATH/bin` directory.
Binary too big? Build a lite version by excluding the drivers you don't need:
```shell
go build -tags='no_postgres no_mysql no_sqlite3 no_ydb' -o goose ./cmd/goose
# Available build tags:
# no_clickhouse no_libsql no_mssql no_mysql
# no_postgres no_sqlite3 no_vertica no_ydb
```
For macOS users `goose` is available as a [Homebrew
Formulae](https://formulae.brew.sh/formula/goose#default):
```shell
brew install goose
```
See [installation documentation](https://pressly.github.io/goose/installation/) for more details.
# Usage
<details>
<summary>Click to show <code>goose help</code> output</summary>
```
Usage: goose [OPTIONS] DRIVER DBSTRING COMMAND
or
Set environment key
GOOSE_DRIVER=DRIVER
GOOSE_DBSTRING=DBSTRING
GOOSE_MIGRATION_DIR=MIGRATION_DIR
Usage: goose [OPTIONS] COMMAND
Drivers:
postgres
mysql
sqlite3
mssql
redshift
tidb
clickhouse
ydb
starrocks
Examples:
goose sqlite3 ./foo.db status
goose sqlite3 ./foo.db create init sql
goose sqlite3 ./foo.db create add_some_column sql
goose sqlite3 ./foo.db create fetch_user_data go
goose sqlite3 ./foo.db up
goose postgres "user=postgres dbname=postgres sslmode=disable" status
goose mysql "user:password@/dbname?parseTime=true" status
goose redshift "postgres://user:password@qwerty.us-east-1.redshift.amazonaws.com:5439/db" status
goose tidb "user:password@/dbname?parseTime=true" status
goose mssql "sqlserver://user:password@hostname:1433?database=master" status
goose clickhouse "tcp://127.0.0.1:9000" status
goose ydb "grpcs://localhost:2135/local?go_query_mode=scripting&go_fake_tx=scripting&go_query_bind=declare,numeric" status
goose starrocks "user:password@/dbname?parseTime=true&interpolateParams=true" status
GOOSE_DRIVER=sqlite3 GOOSE_DBSTRING=./foo.db goose status
GOOSE_DRIVER=sqlite3 GOOSE_DBSTRING=./foo.db goose create init sql
GOOSE_DRIVER=postgres GOOSE_DBSTRING="user=postgres dbname=postgres sslmode=disable" goose status
GOOSE_DRIVER=mysql GOOSE_DBSTRING="user:password@/dbname" goose status
GOOSE_DRIVER=redshift GOOSE_DBSTRING="postgres://user:password@qwerty.us-east-1.redshift.amazonaws.com:5439/db" goose status
GOOSE_DRIVER=clickhouse GOOSE_DBSTRING="clickhouse://user:password@qwerty.clickhouse.cloud:9440/dbname?secure=true&skip_verify=false" goose status
Options:
-allow-missing
applies missing (out-of-order) migrations
-certfile string
file path to root CA's certificates in pem format (only support on mysql)
-dir string
directory with migration files (default ".", can be set via the GOOSE_MIGRATION_DIR env variable).
-h print help
-no-color
disable color output (NO_COLOR env variable supported)
-no-versioning
apply migration commands with no versioning, in file order, from directory pointed to
-s use sequential numbering for new migrations
-ssl-cert string
file path to SSL certificates in pem format (only support on mysql)
-ssl-key string
file path to SSL key in pem format (only support on mysql)
-table string
migrations table name (default "goose_db_version"). If you use a schema that is not `public`, you should set `schemaname.goose_db_version` when running commands.
-timeout duration
maximum allowed duration for queries to run; e.g., 1h13m
-v enable verbose mode
-version
print version
Commands:
up Migrate the DB to the most recent version available
up-by-one Migrate the DB up by 1
up-to VERSION Migrate the DB to a specific VERSION
down Roll back the version by 1
down-to VERSION Roll back to a specific VERSION
redo Re-run the latest migration
reset Roll back all migrations
status Dump the migration status for the current DB
version Print the current version of the database
create NAME [sql|go] Creates new migration file with the current timestamp
fix Apply sequential ordering to migrations
validate Check migration files without running them
```
</details>
Commonly used commands:
[create](#create)<span>&nbsp;&nbsp;</span> [up](#up)<span>&nbsp;&nbsp;</span> [up-to](#up-to)<span>&nbsp;&nbsp;</span> [down](#down)<span>&nbsp;&nbsp;</span> [down-to](#down-to)<span>&nbsp;&nbsp;</span> [status](#status)<span>&nbsp;&nbsp;</span> [version](#version)
## create
Create a new SQL migration.
$ goose create add_some_column sql
$ Created new file: 20170506082420_add_some_column.sql
$ goose -s create add_some_column sql
$ Created new file: 00001_add_some_column.sql
Edit the newly created file to define the behavior of your migration.
You can also create a Go migration, if you then invoke it with [your own goose
binary](#go-migrations):
$ goose create fetch_user_data go
$ Created new file: 20170506082421_fetch_user_data.go
## up
Apply all available migrations.
$ goose up
$ OK 001_basics.sql
$ OK 002_next.sql
$ OK 003_and_again.go
## up-to
Migrate up to a specific version.
$ goose up-to 20170506082420
$ OK 20170506082420_create_table.sql
## up-by-one
Migrate up a single migration from the current version
$ goose up-by-one
$ OK 20170614145246_change_type.sql
## down
Roll back a single migration from the current version.
$ goose down
$ OK 003_and_again.go
## down-to
Roll back migrations to a specific version.
$ goose down-to 20170506082527
$ OK 20170506082527_alter_column.sql
Or, roll back all migrations (careful!):
$ goose down-to 0
## status
Print the status of all migrations:
$ goose status
$ Applied At Migration
$ =======================================
$ Sun Jan 6 11:25:03 2013 -- 001_basics.sql
$ Sun Jan 6 11:25:03 2013 -- 002_next.sql
$ Pending -- 003_and_again.go
Note: for MySQL [parseTime flag](https://github.com/go-sql-driver/mysql#parsetime) must be enabled.
Note: for MySQL
[`multiStatements`](https://github.com/go-sql-driver/mysql?tab=readme-ov-file#multistatements) must
be enabled. This is required when writing multiple queries separated by ';' characters in a single
sql file.
## version
Print the current version of the database:
$ goose version
$ goose: version 002
# Environment Variables
If you prefer to use environment variables, instead of passing the driver and database string as
arguments, you can set the following environment variables:
**1. Via environment variables:**
```shell
export GOOSE_DRIVER=DRIVER
export GOOSE_DBSTRING=DBSTRING
export GOOSE_MIGRATION_DIR=MIGRATION_DIR
export GOOSE_TABLE=TABLENAME
```
**2. Via `.env` files with corresponding variables. `.env` file example**:
```env
GOOSE_DRIVER=postgres
GOOSE_DBSTRING=postgres://admin:admin@localhost:5432/admin_db
GOOSE_MIGRATION_DIR=./migrations
GOOSE_TABLE=custom.goose_migrations
```
Loading from `.env` files is enabled by default. To disable this feature, set the `-env=none` flag.
If you want to load from a specific file, set the `-env` flag to the file path.
For more details about environment variables, see the [official documentation on environment
variables](https://pressly.github.io/goose/documentation/environment-variables/).
# Migrations
goose supports migrations written in SQL or in Go.
## SQL Migrations
A sample SQL migration looks like:
```sql
-- +goose Up
CREATE TABLE post (
id int NOT NULL,
title text,
body text,
PRIMARY KEY(id)
);
-- +goose Down
DROP TABLE post;
```
Each migration file must have exactly one `-- +goose Up` annotation. The `-- +goose Down` annotation
is optional. If the file has both annotations, then the `-- +goose Up` annotation **must** come
first.
Notice the annotations in the comments. Any statements following `-- +goose Up` will be executed as
part of a forward migration, and any statements following `-- +goose Down` will be executed as part
of a rollback.
By default, all migrations are run within a transaction. Some statements like `CREATE DATABASE`,
however, cannot be run within a transaction. You may optionally add `-- +goose NO TRANSACTION` to
the top of your migration file in order to skip transactions within that specific migration file.
Both Up and Down migrations within this file will be run without transactions.
By default, SQL statements are delimited by semicolons - in fact, query statements must end with a
semicolon to be properly recognized by goose.
By default, all migrations are run on the public schema. If you want to use a different schema,
specify the schema name using the table option like `-table='schemaname.goose_db_version`.
More complex statements (PL/pgSQL) that have semicolons within them must be annotated with `--
+goose StatementBegin` and `-- +goose StatementEnd` to be properly recognized. For example:
```sql
-- +goose Up
-- +goose StatementBegin
CREATE OR REPLACE FUNCTION histories_partition_creation( DATE, DATE )
returns void AS $$
DECLARE
create_query text;
BEGIN
FOR create_query IN SELECT
'CREATE TABLE IF NOT EXISTS histories_'
|| TO_CHAR( d, 'YYYY_MM' )
|| ' ( CHECK( created_at >= timestamp '''
|| TO_CHAR( d, 'YYYY-MM-DD 00:00:00' )
|| ''' AND created_at < timestamp '''
|| TO_CHAR( d + INTERVAL '1 month', 'YYYY-MM-DD 00:00:00' )
|| ''' ) ) inherits ( histories );'
FROM generate_series( $1, $2, '1 month' ) AS d
LOOP
EXECUTE create_query;
END LOOP; -- LOOP END
END; -- FUNCTION END
$$
language plpgsql;
-- +goose StatementEnd
```
Goose supports environment variable substitution in SQL migrations through annotations. To enable
this feature, use the `-- +goose ENVSUB ON` annotation before the queries where you want
substitution applied. It stays active until the `-- +goose ENVSUB OFF` annotation is encountered.
You can use these annotations multiple times within a file.
This feature is disabled by default for backward compatibility with existing scripts.
For `PL/pgSQL` functions or other statements where substitution is not desired, wrap the annotations
explicitly around the relevant parts. For example, to exclude escaping the `**` characters:
```sql
-- +goose StatementBegin
CREATE OR REPLACE FUNCTION test_func()
RETURNS void AS $$
-- +goose ENVSUB ON
BEGIN
RAISE NOTICE '${SOME_ENV_VAR}';
END;
-- +goose ENVSUB OFF
$$ LANGUAGE plpgsql;
-- +goose StatementEnd
```
<details>
<summary>Supported expansions (click here to expand):</summary>
- `${VAR}` or $VAR - expands to the value of the environment variable `VAR`
- `${VAR:-default}` - expands to the value of the environment variable `VAR`, or `default` if `VAR`
is unset or null
- `${VAR-default}` - expands to the value of the environment variable `VAR`, or `default` if `VAR`
is unset
- `${VAR?err_msg}` - expands to the value of the environment variable `VAR`, or prints `err_msg` and
error if `VAR` unset
- ~~`${VAR:?err_msg}` - expands to the value of the environment variable `VAR`, or prints `err_msg`
and error if `VAR` unset or null.~~ **THIS IS NOT SUPPORTED**
See
[mfridman/interpolate](https://github.com/mfridman/interpolate?tab=readme-ov-file#supported-expansions)
for more details on supported expansions.
</details>
## Embedded sql migrations
Go 1.16 introduced new feature: [compile-time embedding](https://pkg.go.dev/embed/) files into
binary and corresponding [filesystem abstraction](https://pkg.go.dev/io/fs/).
This feature can be used only for applying existing migrations. Modifying operations such as `fix`
and `create` will continue to operate on OS filesystem even if using embedded files. This is
expected behaviour because `io/fs` interfaces allows read-only access.
Make sure to configure the correct SQL dialect, see [dialect.go](./dialect.go) for supported SQL
dialects.
Example usage, assuming that SQL migrations are placed in the `migrations` directory:
```go
package main
import (
"database/sql"
"embed"
"github.com/pressly/goose/v3"
)
//go:embed migrations/*.sql
var embedMigrations embed.FS
func main() {
var db *sql.DB
// setup database
goose.SetBaseFS(embedMigrations)
if err := goose.SetDialect("postgres"); err != nil {
panic(err)
}
if err := goose.Up(db, "migrations"); err != nil {
panic(err)
}
// run app
}
```
Note that we pass `"migrations"` as directory argument in `Up` because embedding saves directory
structure.
## Go Migrations
1. Create your own goose binary, see [example](./examples/go-migrations)
2. Import `github.com/pressly/goose`
3. Register your migration functions
4. Include your `migrations` package into Go build: in `main.go`, `import _ "github.com/me/myapp/migrations"`
5. Run goose command, ie. `goose.Up(db *sql.DB, dir string)`
A [sample Go migration 00002_users_add_email.go file](./examples/go-migrations/00002_rename_root.go)
looks like:
```go
package migrations
import (
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(Up, Down)
}
func Up(tx *sql.Tx) error {
_, err := tx.Exec("UPDATE users SET username='admin' WHERE username='root';")
if err != nil {
return err
}
return nil
}
func Down(tx *sql.Tx) error {
_, err := tx.Exec("UPDATE users SET username='root' WHERE username='admin';")
if err != nil {
return err
}
return nil
}
```
Note that Go migration files must begin with a numeric value, followed by an underscore, and must
not end with `*_test.go`.
# Hybrid Versioning
Please, read the [versioning
problem](https://github.com/pressly/goose/issues/63#issuecomment-428681694) first.
By default, if you attempt to apply missing (out-of-order) migrations `goose` will raise an error.
However, If you want to apply these missing migrations pass goose the `-allow-missing` flag, or if
using as a library supply the functional option `goose.WithAllowMissing()` to Up, UpTo or UpByOne.
However, we strongly recommend adopting a hybrid versioning approach, using both timestamps and
sequential numbers. Migrations created during the development process are timestamped and sequential
versions are ran on production. We believe this method will prevent the problem of conflicting
versions when writing software in a team environment.
To help you adopt this approach, `create` will use the current timestamp as the migration version.
When you're ready to deploy your migrations in a production environment, we also provide a helpful
`fix` command to convert your migrations into sequential order, while preserving the timestamp
ordering. We recommend running `fix` in the CI pipeline, and only when the migrations are ready for
production.
## Credit
The gopher mascot was designed by [Renée French](https://reneefrench.blogspot.com/) / [CC
3.0.](https://creativecommons.org/licenses/by/3.0/) For more info check out the [Go
Blog](https://go.dev/blog/gopher). Adapted by Ellen.
## License
Licensed under [MIT License](./LICENSE)

120
vendor/github.com/pressly/goose/v3/create.go generated vendored Normal file
View file

@ -0,0 +1,120 @@
package goose
import (
"database/sql"
"errors"
"fmt"
"os"
"path/filepath"
"text/template"
"time"
)
type tmplVars struct {
Version string
CamelName string
}
var (
sequential = false
)
// SetSequential set whether to use sequential versioning instead of timestamp based versioning
func SetSequential(s bool) {
sequential = s
}
// Create writes a new blank migration file.
func CreateWithTemplate(db *sql.DB, dir string, tmpl *template.Template, name, migrationType string) error {
version := time.Now().UTC().Format(timestampFormat)
if sequential {
// always use DirFS here because it's modifying operation
migrations, err := collectMigrationsFS(osFS{}, dir, minVersion, maxVersion, registeredGoMigrations)
if err != nil && !errors.Is(err, ErrNoMigrationFiles) {
return err
}
vMigrations, err := migrations.versioned()
if err != nil {
return err
}
if last, err := vMigrations.Last(); err == nil {
version = fmt.Sprintf(seqVersionTemplate, last.Version+1)
} else {
version = fmt.Sprintf(seqVersionTemplate, int64(1))
}
}
filename := fmt.Sprintf("%v_%v.%v", version, snakeCase(name), migrationType)
if tmpl == nil {
if migrationType == "go" {
tmpl = goSQLMigrationTemplate
} else {
tmpl = sqlMigrationTemplate
}
}
path := filepath.Join(dir, filename)
if _, err := os.Stat(path); !os.IsNotExist(err) {
return fmt.Errorf("failed to create migration file: %w", err)
}
f, err := os.Create(path)
if err != nil {
return fmt.Errorf("failed to create migration file: %w", err)
}
defer f.Close()
vars := tmplVars{
Version: version,
CamelName: camelCase(name),
}
if err := tmpl.Execute(f, vars); err != nil {
return fmt.Errorf("failed to execute tmpl: %w", err)
}
log.Printf("Created new file: %s", f.Name())
return nil
}
// Create writes a new blank migration file.
func Create(db *sql.DB, dir, name, migrationType string) error {
return CreateWithTemplate(db, dir, nil, name, migrationType)
}
var sqlMigrationTemplate = template.Must(template.New("goose.sql-migration").Parse(`-- +goose Up
-- +goose StatementBegin
SELECT 'up SQL query';
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
SELECT 'down SQL query';
-- +goose StatementEnd
`))
var goSQLMigrationTemplate = template.Must(template.New("goose.go-migration").Parse(`package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigrationContext(up{{.CamelName}}, down{{.CamelName}})
}
func up{{.CamelName}}(ctx context.Context, tx *sql.Tx) error {
// This code is executed when the migration is applied.
return nil
}
func down{{.CamelName}}(ctx context.Context, tx *sql.Tx) error {
// This code is executed when the migration is rolled back.
return nil
}
`))

View file

@ -0,0 +1,25 @@
package dialect
// Querier is the interface that wraps the basic methods to create a dialect specific query.
//
// It is intended tio be using with [database.NewStoreFromQuerier] to create a new [database.Store]
// implementation based on a custom querier.
type Querier interface {
// CreateTable returns the SQL query string to create the db version table.
CreateTable(tableName string) string
// InsertVersion returns the SQL query string to insert a new version into the db version table.
InsertVersion(tableName string) string
// DeleteVersion returns the SQL query string to delete a version from the db version table.
DeleteVersion(tableName string) string
// GetMigrationByVersion returns the SQL query string to get a single migration by version.
//
// The query should return the timestamp and is_applied columns.
GetMigrationByVersion(tableName string) string
// ListMigrations returns the SQL query string to list all migrations in descending order by id.
//
// The query should return the version_id and is_applied columns.
ListMigrations(tableName string) string
// GetLatestVersion returns the SQL query string to get the last version_id from the db version
// table. Returns a nullable int64 value.
GetLatestVersion(tableName string) string
}

View file

@ -0,0 +1,22 @@
package dialect
// QuerierExtender extends the [Querier] interface with optional database-specific optimizations.
// While not required, implementing these methods can improve performance.
//
// IMPORTANT: This interface may be expanded in future versions. Implementors must be prepared to
// update their implementations when new methods are added.
//
// Example compile-time check:
//
// var _ QuerierExtender = (*CustomQuerierExtended)(nil)
//
// In short, it's exported to allow implementors to have a compile-time check that they are
// implementing the interface correctly.
type QuerierExtender interface {
Querier
// TableExists returns a database-specific SQL query to check if a table exists. For example,
// implementations might query system catalogs like pg_tables or sqlite_master. Return empty
// string if not supported.
TableExists(tableName string) string
}

212
vendor/github.com/pressly/goose/v3/database/dialects.go generated vendored Normal file
View file

@ -0,0 +1,212 @@
package database
import (
"context"
"database/sql"
"errors"
"fmt"
"github.com/pressly/goose/v3/database/dialect"
"github.com/pressly/goose/v3/internal/dialects"
)
// Dialect is the type of database dialect.
type Dialect string
const (
DialectCustom Dialect = ""
DialectClickHouse Dialect = "clickhouse"
DialectAuroraDSQL Dialect = "dsql"
DialectMSSQL Dialect = "mssql"
DialectMySQL Dialect = "mysql"
DialectPostgres Dialect = "postgres"
DialectRedshift Dialect = "redshift"
DialectSQLite3 Dialect = "sqlite3"
DialectStarrocks Dialect = "starrocks"
DialectTiDB Dialect = "tidb"
DialectTurso Dialect = "turso"
DialectYdB Dialect = "ydb"
// DEPRECATED: Vertica support is deprecated and will be removed in a future release.
DialectVertica Dialect = "vertica"
)
// NewStore returns a new [Store] implementation for the given dialect.
func NewStore(d Dialect, tableName string) (Store, error) {
if d == DialectCustom {
return nil, errors.New("custom dialect is not supported")
}
lookup := map[Dialect]dialect.Querier{
DialectClickHouse: dialects.NewClickhouse(),
DialectAuroraDSQL: dialects.NewAuroraDSQL(),
DialectMSSQL: dialects.NewSqlserver(),
DialectMySQL: dialects.NewMysql(),
DialectPostgres: dialects.NewPostgres(),
DialectRedshift: dialects.NewRedshift(),
DialectSQLite3: dialects.NewSqlite3(),
DialectStarrocks: dialects.NewStarrocks(),
DialectTiDB: dialects.NewTidb(),
DialectTurso: dialects.NewTurso(),
DialectVertica: dialects.NewVertica(),
DialectYdB: dialects.NewYDB(),
}
querier, ok := lookup[d]
if !ok {
return nil, fmt.Errorf("unknown dialect: %q", d)
}
return NewStoreFromQuerier(tableName, querier)
}
// NewStoreFromQuerier returns a new [Store] implementation for the given querier.
//
// Most of the time you should use [NewStore] instead of this function, as it will automatically
// create a dialect-specific querier for you. This function is useful if you want to use a custom
// querier that is not part of the standard dialects.
func NewStoreFromQuerier(tableName string, querier dialect.Querier) (Store, error) {
if tableName == "" {
return nil, errors.New("table name must not be empty")
}
if querier == nil {
return nil, errors.New("querier must not be nil")
}
return &store{
tableName: tableName,
querier: newQueryController(querier),
}, nil
}
type store struct {
tableName string
querier *queryController
}
var _ Store = (*store)(nil)
func (s *store) Tablename() string {
return s.tableName
}
func (s *store) CreateVersionTable(ctx context.Context, db DBTxConn) error {
q := s.querier.CreateTable(s.tableName)
if _, err := db.ExecContext(ctx, q); err != nil {
return fmt.Errorf("failed to create version table %q: %w", s.tableName, err)
}
return nil
}
func (s *store) Insert(ctx context.Context, db DBTxConn, req InsertRequest) error {
q := s.querier.InsertVersion(s.tableName)
if _, err := db.ExecContext(ctx, q, req.Version, true); err != nil {
return fmt.Errorf("failed to insert version %d: %w", req.Version, err)
}
return nil
}
func (s *store) Delete(ctx context.Context, db DBTxConn, version int64) error {
q := s.querier.DeleteVersion(s.tableName)
if _, err := db.ExecContext(ctx, q, version); err != nil {
return fmt.Errorf("failed to delete version %d: %w", version, err)
}
return nil
}
func (s *store) GetMigration(
ctx context.Context,
db DBTxConn,
version int64,
) (*GetMigrationResult, error) {
q := s.querier.GetMigrationByVersion(s.tableName)
var result GetMigrationResult
if err := db.QueryRowContext(ctx, q, version).Scan(
&result.Timestamp,
&result.IsApplied,
); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("%w: %d", ErrVersionNotFound, version)
}
return nil, fmt.Errorf("failed to get migration %d: %w", version, err)
}
return &result, nil
}
func (s *store) GetLatestVersion(ctx context.Context, db DBTxConn) (int64, error) {
q := s.querier.GetLatestVersion(s.tableName)
var version sql.NullInt64
if err := db.QueryRowContext(ctx, q).Scan(&version); err != nil {
return -1, fmt.Errorf("failed to get latest version: %w", err)
}
if !version.Valid {
return -1, fmt.Errorf("latest %w", ErrVersionNotFound)
}
return version.Int64, nil
}
func (s *store) ListMigrations(
ctx context.Context,
db DBTxConn,
) ([]*ListMigrationsResult, error) {
q := s.querier.ListMigrations(s.tableName)
rows, err := db.QueryContext(ctx, q)
if err != nil {
return nil, fmt.Errorf("failed to list migrations: %w", err)
}
defer rows.Close()
var migrations []*ListMigrationsResult
for rows.Next() {
var result ListMigrationsResult
if err := rows.Scan(&result.Version, &result.IsApplied); err != nil {
return nil, fmt.Errorf("failed to scan list migrations result: %w", err)
}
migrations = append(migrations, &result)
}
if err := rows.Err(); err != nil {
return nil, err
}
return migrations, nil
}
//
//
//
// Additional methods that are not part of the core Store interface, but are extended by the
// [controller.StoreController] type.
//
//
//
func (s *store) TableExists(ctx context.Context, db DBTxConn) (bool, error) {
q := s.querier.TableExists(s.tableName)
if q == "" {
return false, errors.ErrUnsupported
}
var exists bool
// Note, we do not pass the table name as an argument to the query, as the query should be
// pre-defined by the dialect.
if err := db.QueryRowContext(ctx, q).Scan(&exists); err != nil {
return false, fmt.Errorf("failed to check if table exists: %w", err)
}
return exists, nil
}
var _ dialect.Querier = (*queryController)(nil)
type queryController struct{ dialect.Querier }
// newQueryController returns a new QueryController that wraps the given Querier.
func newQueryController(querier dialect.Querier) *queryController {
return &queryController{Querier: querier}
}
// Optional methods
// TableExists returns the SQL query string to check if the version table exists. If the Querier
// does not implement this method, it will return an empty string.
//
// Returns a boolean value.
func (c *queryController) TableExists(tableName string) string {
if t, ok := c.Querier.(interface{ TableExists(string) string }); ok {
return t.TableExists(tableName)
}
return ""
}

14
vendor/github.com/pressly/goose/v3/database/doc.go generated vendored Normal file
View file

@ -0,0 +1,14 @@
// Package database defines a generic [Store] interface for goose to use when interacting with the
// database. It is meant to be generic and not tied to any specific database technology.
//
// At a high level, a [Store] is responsible for:
// - Creating a version table
// - Inserting and deleting a version
// - Getting a specific version
// - Listing all applied versions
//
// Use the [NewStore] function to create a [Store] for one of the supported dialects.
//
// For more advanced use cases, it's possible to implement a custom [Store] for a database that
// goose does not support.
package database

View file

@ -0,0 +1,23 @@
package database
import (
"context"
"database/sql"
)
// DBTxConn is a thin interface for common methods that is satisfied by *sql.DB, *sql.Tx and
// *sql.Conn.
//
// There is a long outstanding issue to formalize a std lib interface, but alas. See:
// https://github.com/golang/go/issues/14468
type DBTxConn interface {
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
}
var (
_ DBTxConn = (*sql.DB)(nil)
_ DBTxConn = (*sql.Tx)(nil)
_ DBTxConn = (*sql.Conn)(nil)
)

66
vendor/github.com/pressly/goose/v3/database/store.go generated vendored Normal file
View file

@ -0,0 +1,66 @@
package database
import (
"context"
"errors"
"time"
)
var (
// ErrVersionNotFound must be returned by [GetMigration] or [GetLatestVersion] when a migration
// does not exist.
ErrVersionNotFound = errors.New("version not found")
// ErrNotImplemented must be returned by methods that are not implemented.
ErrNotImplemented = errors.New("not implemented")
)
// Store is an interface that defines methods for tracking and managing migrations. It is used by
// the goose package to interact with a database. By defining a Store interface, multiple
// implementations can be created to support different databases without reimplementing the
// migration logic.
//
// This package provides several dialects that implement the Store interface. While most users won't
// need to create their own Store, if you need to support a database that isn't currently supported,
// you can implement your own!
type Store interface {
// Tablename is the name of the version table. This table is used to record applied migrations
// and must not be an empty string.
Tablename() string
// CreateVersionTable creates the version table, which is used to track migrations.
CreateVersionTable(ctx context.Context, db DBTxConn) error
// Insert a version id into the version table.
Insert(ctx context.Context, db DBTxConn, req InsertRequest) error
// Delete a version id from the version table.
Delete(ctx context.Context, db DBTxConn, version int64) error
// GetMigration retrieves a single migration by version id. If the query succeeds, but the
// version is not found, this method must return [ErrVersionNotFound].
GetMigration(ctx context.Context, db DBTxConn, version int64) (*GetMigrationResult, error)
// GetLatestVersion retrieves the last applied migration version. If no migrations exist, this
// method must return [ErrVersionNotFound].
GetLatestVersion(ctx context.Context, db DBTxConn) (int64, error)
// ListMigrations retrieves all migrations sorted in descending order by id or timestamp. If
// there are no migrations, return empty slice with no error. Typically this method will return
// at least one migration, because the initial version (0) is always inserted into the version
// table when it is created.
ListMigrations(ctx context.Context, db DBTxConn) ([]*ListMigrationsResult, error)
}
type InsertRequest struct {
Version int64
// TODO(mf): in the future, we maybe want to expand this struct so implementors can store
// additional information. See the following issues for more information:
// - https://github.com/pressly/goose/issues/422
// - https://github.com/pressly/goose/issues/288
}
type GetMigrationResult struct {
Timestamp time.Time
IsApplied bool
}
type ListMigrationsResult struct {
Version int64
IsApplied bool
}

View file

@ -0,0 +1,33 @@
package database
import "context"
// StoreExtender is an extension of the Store interface that provides optional optimizations and
// database-specific features. While not required by the core goose package, implementing these
// methods can improve performance and functionality for specific databases.
//
// IMPORTANT: This interface may be expanded in future versions. Implementors MUST be prepared to
// update their implementations when new methods are added, either by implementing the new
// functionality or returning [errors.ErrUnsupported].
//
// The goose package handles these extended capabilities through a [controller.StoreController],
// which automatically uses optimized methods when available while falling back to default behavior
// when they're not implemented.
//
// Example usage to verify implementation:
//
// var _ StoreExtender = (*CustomStoreExtended)(nil)
//
// In short, it's exported to allows implementors to have a compile-time check that they are
// implementing the interface correctly.
type StoreExtender interface {
Store
// TableExists checks if the migrations table exists in the database. Implementing this method
// allows goose to optimize table existence checks by using database-specific system catalogs
// (e.g., pg_tables for PostgreSQL, sqlite_master for SQLite) instead of generic SQL queries.
//
// Return [errors.ErrUnsupported] if the database does not provide an efficient way to check
// table existence.
TableExists(ctx context.Context, db DBTxConn) (bool, error)
}

46
vendor/github.com/pressly/goose/v3/db.go generated vendored Normal file
View file

@ -0,0 +1,46 @@
package goose
import (
"database/sql"
"fmt"
)
// OpenDBWithDriver creates a connection to a database, and modifies goose internals to be
// compatible with the supplied driver by calling SetDialect.
func OpenDBWithDriver(driver string, dbstring string) (*sql.DB, error) {
if err := SetDialect(driver); err != nil {
return nil, err
}
// The Go ecosystem has added more and more drivers over the years. As a result, there's no
// longer a one-to-one match between the driver name and the dialect name. For instance, there's
// no "redshift" driver, but that's the internal dialect name within goose. Hence, we need to
// convert the dialect name to a supported driver name. This conversion is a best-effort
// attempt, as we can't support both lib/pq and pgx, which some users might have.
//
// We recommend users to create a [NewProvider] with the desired dialect, open a connection
// using their preferred driver, and provide the *sql.DB to goose. This approach removes the
// need for mapping dialects to drivers, rendering this function unnecessary.
switch driver {
case "mssql":
driver = "sqlserver"
case "tidb":
driver = "mysql"
case "turso":
driver = "libsql"
case "sqlite3":
driver = "sqlite"
case "postgres", "redshift":
driver = "pgx"
case "starrocks":
driver = "mysql"
}
switch driver {
case "postgres", "pgx", "sqlite3", "sqlite", "mysql", "sqlserver", "clickhouse", "vertica", "azuresql", "ydb", "libsql", "starrocks":
return sql.Open(driver, dbstring)
default:
return nil, fmt.Errorf("unsupported driver %s", driver)
}
}

71
vendor/github.com/pressly/goose/v3/dialect.go generated vendored Normal file
View file

@ -0,0 +1,71 @@
package goose
import (
"fmt"
"github.com/pressly/goose/v3/database"
"github.com/pressly/goose/v3/internal/legacystore"
)
// Dialect is the type of database dialect. It is an alias for [database.Dialect].
type Dialect = database.Dialect
const (
DialectCustom Dialect = database.DialectCustom
DialectClickHouse Dialect = database.DialectClickHouse
DialectMSSQL Dialect = database.DialectMSSQL
DialectMySQL Dialect = database.DialectMySQL
DialectPostgres Dialect = database.DialectPostgres
DialectRedshift Dialect = database.DialectRedshift
DialectSQLite3 Dialect = database.DialectSQLite3
DialectStarrocks Dialect = database.DialectStarrocks
DialectTiDB Dialect = database.DialectTiDB
DialectTurso Dialect = database.DialectTurso
DialectYdB Dialect = database.DialectYdB
// Dialects only available to the [Provider].
DialectAuroraDSQL Dialect = database.DialectAuroraDSQL
// DEPRECATED: Vertica support is deprecated and will be removed in a future release.
DialectVertica Dialect = database.DialectVertica
)
func init() {
store, _ = legacystore.NewStore(DialectPostgres)
}
var store legacystore.Store
// SetDialect sets the dialect to use for the goose package.
func SetDialect(s string) error {
var d Dialect
switch s {
case "postgres", "pgx":
d = DialectPostgres
case "mysql":
d = DialectMySQL
case "sqlite3", "sqlite":
d = DialectSQLite3
case "mssql", "azuresql", "sqlserver":
d = DialectMSSQL
case "redshift":
d = DialectRedshift
case "tidb":
d = DialectTiDB
case "clickhouse":
d = DialectClickHouse
case "vertica":
d = DialectVertica
case "ydb":
d = DialectYdB
case "turso":
d = DialectTurso
case "starrocks":
d = DialectStarrocks
default:
return fmt.Errorf("%q: unknown dialect", s)
}
var err error
store, err = legacystore.NewStore(d)
return err
}

107
vendor/github.com/pressly/goose/v3/down.go generated vendored Normal file
View file

@ -0,0 +1,107 @@
package goose
import (
"context"
"database/sql"
"fmt"
)
// Down rolls back a single migration from the current version.
func Down(db *sql.DB, dir string, opts ...OptionsFunc) error {
ctx := context.Background()
return DownContext(ctx, db, dir, opts...)
}
// DownContext rolls back a single migration from the current version.
func DownContext(ctx context.Context, db *sql.DB, dir string, opts ...OptionsFunc) error {
option := &options{}
for _, f := range opts {
f(option)
}
migrations, err := CollectMigrations(dir, minVersion, maxVersion)
if err != nil {
return err
}
if option.noVersioning {
if len(migrations) == 0 {
return nil
}
currentVersion := migrations[len(migrations)-1].Version
// Migrate only the latest migration down.
return downToNoVersioning(ctx, db, migrations, currentVersion-1)
}
currentVersion, err := GetDBVersionContext(ctx, db)
if err != nil {
return err
}
current, err := migrations.Current(currentVersion)
if err != nil {
return fmt.Errorf("migration %v: %w", currentVersion, err)
}
return current.DownContext(ctx, db)
}
// DownTo rolls back migrations to a specific version.
func DownTo(db *sql.DB, dir string, version int64, opts ...OptionsFunc) error {
ctx := context.Background()
return DownToContext(ctx, db, dir, version, opts...)
}
// DownToContext rolls back migrations to a specific version.
func DownToContext(ctx context.Context, db *sql.DB, dir string, version int64, opts ...OptionsFunc) error {
option := &options{}
for _, f := range opts {
f(option)
}
migrations, err := CollectMigrations(dir, minVersion, maxVersion)
if err != nil {
return err
}
if option.noVersioning {
return downToNoVersioning(ctx, db, migrations, version)
}
for {
currentVersion, err := GetDBVersionContext(ctx, db)
if err != nil {
return err
}
if currentVersion == 0 {
log.Printf("goose: no migrations to run. current version: %d", currentVersion)
return nil
}
current, err := migrations.Current(currentVersion)
if err != nil {
log.Printf("goose: migration file not found for current version (%d), error: %s", currentVersion, err)
return err
}
if current.Version <= version {
log.Printf("goose: no migrations to run. current version: %d", currentVersion)
return nil
}
if err = current.DownContext(ctx, db); err != nil {
return err
}
}
}
// downToNoVersioning applies down migrations down to, but not including, the
// target version.
func downToNoVersioning(ctx context.Context, db *sql.DB, migrations Migrations, version int64) error {
var finalVersion int64
for i := len(migrations) - 1; i >= 0; i-- {
if version >= migrations[i].Version {
finalVersion = migrations[i].Version
break
}
migrations[i].noVersioning = true
if err := migrations[i].DownContext(ctx, db); err != nil {
return err
}
}
log.Printf("goose: down to current file version: %d", finalVersion)
return nil
}

54
vendor/github.com/pressly/goose/v3/fix.go generated vendored Normal file
View file

@ -0,0 +1,54 @@
package goose
import (
"fmt"
"os"
"path/filepath"
"strings"
)
const seqVersionTemplate = "%05v"
func Fix(dir string) error {
// always use osFS here because it's modifying operation
migrations, err := collectMigrationsFS(osFS{}, dir, minVersion, maxVersion, registeredGoMigrations)
if err != nil {
return err
}
// split into timestamped and versioned migrations
tsMigrations, err := migrations.timestamped()
if err != nil {
return err
}
vMigrations, err := migrations.versioned()
if err != nil {
return err
}
// Initial version.
version := int64(1)
if last, err := vMigrations.Last(); err == nil {
version = last.Version + 1
}
// fix filenames by replacing timestamps with sequential versions
for _, tsm := range tsMigrations {
oldPath := tsm.Source
newPath := strings.Replace(
oldPath,
fmt.Sprintf("%d", tsm.Version),
fmt.Sprintf(seqVersionTemplate, version),
1,
)
if err := os.Rename(oldPath, newPath); err != nil {
return err
}
log.Printf("RENAMED %s => %s", filepath.Base(oldPath), filepath.Base(newPath))
version++
}
return nil
}

104
vendor/github.com/pressly/goose/v3/globals.go generated vendored Normal file
View file

@ -0,0 +1,104 @@
package goose
import (
"errors"
"fmt"
"path/filepath"
)
var (
registeredGoMigrations = make(map[int64]*Migration)
)
// ResetGlobalMigrations resets the global Go migrations registry.
//
// Not safe for concurrent use.
func ResetGlobalMigrations() {
registeredGoMigrations = make(map[int64]*Migration)
}
// SetGlobalMigrations registers Go migrations globally. It returns an error if a migration with the
// same version has already been registered. Go migrations must be constructed using the
// [NewGoMigration] function.
//
// Not safe for concurrent use.
func SetGlobalMigrations(migrations ...*Migration) error {
for _, m := range migrations {
if _, ok := registeredGoMigrations[m.Version]; ok {
return fmt.Errorf("go migration with version %d already registered", m.Version)
}
if err := checkGoMigration(m); err != nil {
return fmt.Errorf("invalid go migration: %w", err)
}
registeredGoMigrations[m.Version] = m
}
return nil
}
func checkGoMigration(m *Migration) error {
if !m.construct {
return errors.New("must use NewGoMigration to construct migrations")
}
if !m.Registered {
return errors.New("must be registered")
}
if m.Type != TypeGo {
return fmt.Errorf("type must be %q", TypeGo)
}
if m.Version < 1 {
return errors.New("version must be greater than zero")
}
if m.Source != "" {
if filepath.Ext(m.Source) != ".go" {
return fmt.Errorf("source must have .go extension: %q", m.Source)
}
// If the source is set, expect it to be a path with a numeric component that matches the
// version. This field is not intended to be used for descriptive purposes.
version, err := NumericComponent(m.Source)
if err != nil {
return fmt.Errorf("invalid source: %w", err)
}
if version != m.Version {
return fmt.Errorf("version:%d does not match numeric component in source %q", m.Version, m.Source)
}
}
if err := checkGoFunc(m.goUp); err != nil {
return fmt.Errorf("up function: %w", err)
}
if err := checkGoFunc(m.goDown); err != nil {
return fmt.Errorf("down function: %w", err)
}
if m.UpFnContext != nil && m.UpFnNoTxContext != nil {
return errors.New("must specify exactly one of UpFnContext or UpFnNoTxContext")
}
if m.UpFn != nil && m.UpFnNoTx != nil {
return errors.New("must specify exactly one of UpFn or UpFnNoTx")
}
if m.DownFnContext != nil && m.DownFnNoTxContext != nil {
return errors.New("must specify exactly one of DownFnContext or DownFnNoTxContext")
}
if m.DownFn != nil && m.DownFnNoTx != nil {
return errors.New("must specify exactly one of DownFn or DownFnNoTx")
}
return nil
}
func checkGoFunc(f *GoFunc) error {
if f.RunTx != nil && f.RunDB != nil {
return errors.New("must specify exactly one of RunTx or RunDB")
}
switch f.Mode {
case TransactionEnabled, TransactionDisabled:
// No functions, but mode is set. This is not an error. It means the user wants to
// record a version with the given mode but not run any functions.
default:
return fmt.Errorf("invalid mode: %d", f.Mode)
}
if f.RunDB != nil && f.Mode != TransactionDisabled {
return fmt.Errorf("transaction mode must be disabled or unspecified when RunDB is set")
}
if f.RunTx != nil && f.Mode != TransactionEnabled {
return fmt.Errorf("transaction mode must be enabled or unspecified when RunTx is set")
}
return nil
}

141
vendor/github.com/pressly/goose/v3/goose.go generated vendored Normal file
View file

@ -0,0 +1,141 @@
package goose
import (
"context"
"database/sql"
"fmt"
"io/fs"
"strconv"
)
// Deprecated: VERSION will no longer be supported in the next major release.
const VERSION = "v3.18.0"
var (
minVersion = int64(0)
maxVersion = int64((1 << 63) - 1)
timestampFormat = "20060102150405"
verbose = false
noColor = false
// base fs to lookup migrations
baseFS fs.FS = osFS{}
)
// SetVerbose set the goose verbosity mode
func SetVerbose(v bool) {
verbose = v
}
// SetBaseFS sets a base FS to discover migrations. It can be used with 'embed' package.
// Calling with 'nil' argument leads to default behaviour: discovering migrations from os filesystem.
// Note that modifying operations like Create will use os filesystem anyway.
func SetBaseFS(fsys fs.FS) {
if fsys == nil {
fsys = osFS{}
}
baseFS = fsys
}
// Run runs a goose command.
//
// Deprecated: Use RunContext.
func Run(command string, db *sql.DB, dir string, args ...string) error {
ctx := context.Background()
return RunContext(ctx, command, db, dir, args...)
}
// RunContext runs a goose command.
func RunContext(ctx context.Context, command string, db *sql.DB, dir string, args ...string) error {
return run(ctx, command, db, dir, args)
}
// RunWithOptions runs a goose command with options.
//
// Deprecated: Use RunWithOptionsContext.
func RunWithOptions(command string, db *sql.DB, dir string, args []string, options ...OptionsFunc) error {
ctx := context.Background()
return RunWithOptionsContext(ctx, command, db, dir, args, options...)
}
// RunWithOptionsContext runs a goose command with options.
func RunWithOptionsContext(ctx context.Context, command string, db *sql.DB, dir string, args []string, options ...OptionsFunc) error {
return run(ctx, command, db, dir, args, options...)
}
func run(ctx context.Context, command string, db *sql.DB, dir string, args []string, options ...OptionsFunc) error {
switch command {
case "up":
if err := UpContext(ctx, db, dir, options...); err != nil {
return err
}
case "up-by-one":
if err := UpByOneContext(ctx, db, dir, options...); err != nil {
return err
}
case "up-to":
if len(args) == 0 {
return fmt.Errorf("up-to must be of form: goose [OPTIONS] DRIVER DBSTRING up-to VERSION")
}
version, err := strconv.ParseInt(args[0], 10, 64)
if err != nil {
return fmt.Errorf("version must be a number (got '%s')", args[0])
}
if err := UpToContext(ctx, db, dir, version, options...); err != nil {
return err
}
case "create":
if len(args) == 0 {
return fmt.Errorf("create must be of form: goose [OPTIONS] DRIVER DBSTRING create NAME [go|sql]")
}
migrationType := "go"
if len(args) == 2 {
migrationType = args[1]
}
if err := Create(db, dir, args[0], migrationType); err != nil {
return err
}
case "down":
if err := DownContext(ctx, db, dir, options...); err != nil {
return err
}
case "down-to":
if len(args) == 0 {
return fmt.Errorf("down-to must be of form: goose [OPTIONS] DRIVER DBSTRING down-to VERSION")
}
version, err := strconv.ParseInt(args[0], 10, 64)
if err != nil {
return fmt.Errorf("version must be a number (got '%s')", args[0])
}
if err := DownToContext(ctx, db, dir, version, options...); err != nil {
return err
}
case "fix":
if err := Fix(dir); err != nil {
return err
}
case "redo":
if err := RedoContext(ctx, db, dir, options...); err != nil {
return err
}
case "reset":
if err := ResetContext(ctx, db, dir, options...); err != nil {
return err
}
case "status":
if err := StatusContext(ctx, db, dir, options...); err != nil {
return err
}
case "version":
if err := VersionContext(ctx, db, dir, options...); err != nil {
return err
}
default:
return fmt.Errorf("%q: no such command", command)
}
return nil
}

84
vendor/github.com/pressly/goose/v3/helpers.go generated vendored Normal file
View file

@ -0,0 +1,84 @@
package goose
import (
"bytes"
"strings"
"unicode"
"unicode/utf8"
)
type camelSnakeStateMachine int
const ( // _$$_This is some text, OK?!
idle camelSnakeStateMachine = iota // 0 ↑ ↑ ↑
firstAlphaNum // 1 ↑ ↑ ↑ ↑ ↑
alphaNum // 2 ↑↑↑ ↑ ↑↑↑ ↑↑↑ ↑
delimiter // 3 ↑ ↑ ↑ ↑ ↑
)
func (s camelSnakeStateMachine) next(r rune) camelSnakeStateMachine {
switch s {
case idle:
if isAlphaNum(r) {
return firstAlphaNum
}
case firstAlphaNum:
if isAlphaNum(r) {
return alphaNum
}
return delimiter
case alphaNum:
if !isAlphaNum(r) {
return delimiter
}
case delimiter:
if isAlphaNum(r) {
return firstAlphaNum
}
return idle
}
return s
}
func camelCase(str string) string {
var b strings.Builder
stateMachine := idle
for i := 0; i < len(str); {
r, size := utf8.DecodeRuneInString(str[i:])
i += size
stateMachine = stateMachine.next(r)
switch stateMachine {
case firstAlphaNum:
b.WriteRune(unicode.ToUpper(r))
case alphaNum:
b.WriteRune(unicode.ToLower(r))
}
}
return b.String()
}
func snakeCase(str string) string {
var b bytes.Buffer
stateMachine := idle
for i := 0; i < len(str); {
r, size := utf8.DecodeRuneInString(str[i:])
i += size
stateMachine = stateMachine.next(r)
switch stateMachine {
case firstAlphaNum, alphaNum:
b.WriteRune(unicode.ToLower(r))
case delimiter:
b.WriteByte('_')
}
}
if stateMachine == idle {
return string(bytes.TrimSuffix(b.Bytes(), []byte{'_'}))
}
return b.String()
}
func isAlphaNum(r rune) bool {
return unicode.IsLetter(r) || unicode.IsNumber(r)
}

37
vendor/github.com/pressly/goose/v3/install.sh generated vendored Normal file
View file

@ -0,0 +1,37 @@
#!/bin/sh
# Adapted from the Deno installer: Copyright 2019 the Deno authors. All rights reserved. MIT license.
# Ref: https://github.com/denoland/deno_install
# TODO(everyone): Keep this script simple and easily auditable.
# TODO(mf): this should work on Linux and macOS. Not intended for Windows.
set -e
os=$(uname -s | tr '[:upper:]' '[:lower:]')
arch=$(uname -m)
if [ "$arch" = "aarch64" ]; then
arch="arm64"
fi
if [ $# -eq 0 ]; then
goose_uri="https://github.com/pressly/goose/releases/latest/download/goose_${os}_${arch}"
else
goose_uri="https://github.com/pressly/goose/releases/download/${1}/goose_${os}_${arch}"
fi
goose_install="${GOOSE_INSTALL:-/usr/local}"
bin_dir="${goose_install}/bin"
exe="${bin_dir}/goose"
if [ ! -d "${bin_dir}" ]; then
mkdir -p "${bin_dir}"
fi
curl --silent --show-error --location --fail --location --output "${exe}" "$goose_uri"
chmod +x "${exe}"
echo "Goose was installed successfully to ${exe}"
if command -v goose >/dev/null; then
echo "Run 'goose --help' to get started"
fi

View file

@ -0,0 +1,37 @@
package controller
import (
"context"
"errors"
"github.com/pressly/goose/v3/database"
)
// A StoreController is used by the goose package to interact with a database. This type is a
// wrapper around the Store interface, but can be extended to include additional (optional) methods
// that are not part of the core Store interface.
type StoreController struct{ database.Store }
var _ database.StoreExtender = (*StoreController)(nil)
// NewStoreController returns a new StoreController that wraps the given Store.
//
// If the Store implements the following optional methods, the StoreController will call them as
// appropriate:
//
// - TableExists(context.Context, DBTxConn) (bool, error)
//
// If the Store does not implement a method, it will either return a [errors.ErrUnsupported] error
// or fall back to the default behavior.
func NewStoreController(store database.Store) *StoreController {
return &StoreController{store}
}
func (c *StoreController) TableExists(ctx context.Context, db database.DBTxConn) (bool, error) {
if t, ok := c.Store.(interface {
TableExists(ctx context.Context, db database.DBTxConn) (bool, error)
}); ok {
return t.TableExists(ctx, db)
}
return false, errors.ErrUnsupported
}

View file

@ -0,0 +1,53 @@
package dialects
import (
"fmt"
"github.com/pressly/goose/v3/database/dialect"
)
// NewClickhouse returns a new [dialect.Querier] for Clickhouse dialect.
func NewClickhouse() dialect.Querier {
return &clickhouse{}
}
type clickhouse struct{}
var _ dialect.Querier = (*clickhouse)(nil)
func (c *clickhouse) CreateTable(tableName string) string {
q := `CREATE TABLE IF NOT EXISTS %s (
version_id Int64,
is_applied UInt8,
date Date default now(),
tstamp DateTime default now()
)
ENGINE = MergeTree()
ORDER BY (date)`
return fmt.Sprintf(q, tableName)
}
func (c *clickhouse) InsertVersion(tableName string) string {
q := `INSERT INTO %s (version_id, is_applied) VALUES ($1, $2)`
return fmt.Sprintf(q, tableName)
}
func (c *clickhouse) DeleteVersion(tableName string) string {
q := `ALTER TABLE %s DELETE WHERE version_id = $1 SETTINGS mutations_sync = 2`
return fmt.Sprintf(q, tableName)
}
func (c *clickhouse) GetMigrationByVersion(tableName string) string {
q := `SELECT tstamp, is_applied FROM %s WHERE version_id = $1 ORDER BY tstamp DESC LIMIT 1`
return fmt.Sprintf(q, tableName)
}
func (c *clickhouse) ListMigrations(tableName string) string {
q := `SELECT version_id, is_applied FROM %s ORDER BY version_id DESC`
return fmt.Sprintf(q, tableName)
}
func (c *clickhouse) GetLatestVersion(tableName string) string {
q := `SELECT max(version_id) FROM %s`
return fmt.Sprintf(q, tableName)
}

View file

@ -0,0 +1,66 @@
package dialects
import (
"fmt"
"github.com/pressly/goose/v3/database/dialect"
)
// NewAuroraDSQL returns a new [dialect.Querier] for Aurora DSQL dialect.
func NewAuroraDSQL() dialect.QuerierExtender {
return &dsql{}
}
type dsql struct{}
var _ dialect.QuerierExtender = (*dsql)(nil)
func (d *dsql) CreateTable(tableName string) string {
q := `CREATE TABLE %s (
id integer PRIMARY KEY,
version_id bigint NOT NULL,
is_applied boolean NOT NULL,
tstamp timestamp NOT NULL DEFAULT now()
)`
return fmt.Sprintf(q, tableName)
}
func (d *dsql) InsertVersion(tableName string) string {
q := `INSERT INTO %s (id, version_id, is_applied)
VALUES (
COALESCE((SELECT MAX(id) FROM %s), 0) + 1,
$1,
$2
)`
return fmt.Sprintf(q, tableName, tableName)
}
func (d *dsql) DeleteVersion(tableName string) string {
q := `DELETE FROM %s WHERE version_id=$1`
return fmt.Sprintf(q, tableName)
}
func (d *dsql) GetMigrationByVersion(tableName string) string {
q := `SELECT tstamp, is_applied FROM %s WHERE version_id=$1 ORDER BY tstamp DESC LIMIT 1`
return fmt.Sprintf(q, tableName)
}
func (d *dsql) ListMigrations(tableName string) string {
q := `SELECT version_id, is_applied from %s ORDER BY id DESC`
return fmt.Sprintf(q, tableName)
}
func (d *dsql) GetLatestVersion(tableName string) string {
q := `SELECT max(version_id) FROM %s`
return fmt.Sprintf(q, tableName)
}
func (d *dsql) TableExists(tableName string) string {
schemaName, tableName := parseTableIdentifier(tableName)
if schemaName != "" {
q := `SELECT EXISTS ( SELECT 1 FROM pg_tables WHERE schemaname = '%s' AND tablename = '%s' )`
return fmt.Sprintf(q, schemaName, tableName)
}
q := `SELECT EXISTS ( SELECT 1 FROM pg_tables WHERE (current_schema() IS NULL OR schemaname = current_schema()) AND tablename = '%s' )`
return fmt.Sprintf(q, tableName)
}

View file

@ -0,0 +1,62 @@
package dialects
import (
"fmt"
"github.com/pressly/goose/v3/database/dialect"
)
// NewMysql returns a new [dialect.Querier] for MySQL dialect.
func NewMysql() dialect.QuerierExtender {
return &mysql{}
}
type mysql struct{}
var _ dialect.QuerierExtender = (*mysql)(nil)
func (m *mysql) CreateTable(tableName string) string {
q := `CREATE TABLE %s (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
version_id bigint NOT NULL,
is_applied boolean NOT NULL,
tstamp timestamp NULL default now(),
PRIMARY KEY(id)
)`
return fmt.Sprintf(q, tableName)
}
func (m *mysql) InsertVersion(tableName string) string {
q := `INSERT INTO %s (version_id, is_applied) VALUES (?, ?)`
return fmt.Sprintf(q, tableName)
}
func (m *mysql) DeleteVersion(tableName string) string {
q := `DELETE FROM %s WHERE version_id=?`
return fmt.Sprintf(q, tableName)
}
func (m *mysql) GetMigrationByVersion(tableName string) string {
q := `SELECT tstamp, is_applied FROM %s WHERE version_id=? ORDER BY tstamp DESC LIMIT 1`
return fmt.Sprintf(q, tableName)
}
func (m *mysql) ListMigrations(tableName string) string {
q := `SELECT version_id, is_applied from %s ORDER BY id DESC`
return fmt.Sprintf(q, tableName)
}
func (m *mysql) GetLatestVersion(tableName string) string {
q := `SELECT MAX(version_id) FROM %s`
return fmt.Sprintf(q, tableName)
}
func (m *mysql) TableExists(tableName string) string {
schemaName, tableName := parseTableIdentifier(tableName)
if schemaName != "" {
q := `SELECT EXISTS ( SELECT 1 FROM information_schema.tables WHERE table_schema = '%s' AND table_name = '%s' )`
return fmt.Sprintf(q, schemaName, tableName)
}
q := `SELECT EXISTS ( SELECT 1 FROM information_schema.tables WHERE (database() IS NULL OR table_schema = database()) AND table_name = '%s' )`
return fmt.Sprintf(q, tableName)
}

View file

@ -0,0 +1,70 @@
package dialects
import (
"fmt"
"strings"
"github.com/pressly/goose/v3/database/dialect"
)
// NewPostgres returns a new [dialect.Querier] for PostgreSQL dialect.
func NewPostgres() dialect.QuerierExtender {
return &postgres{}
}
type postgres struct{}
var _ dialect.QuerierExtender = (*postgres)(nil)
func (p *postgres) CreateTable(tableName string) string {
q := `CREATE TABLE %s (
id integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
version_id bigint NOT NULL,
is_applied boolean NOT NULL,
tstamp timestamp NOT NULL DEFAULT now()
)`
return fmt.Sprintf(q, tableName)
}
func (p *postgres) InsertVersion(tableName string) string {
q := `INSERT INTO %s (version_id, is_applied) VALUES ($1, $2)`
return fmt.Sprintf(q, tableName)
}
func (p *postgres) DeleteVersion(tableName string) string {
q := `DELETE FROM %s WHERE version_id=$1`
return fmt.Sprintf(q, tableName)
}
func (p *postgres) GetMigrationByVersion(tableName string) string {
q := `SELECT tstamp, is_applied FROM %s WHERE version_id=$1 ORDER BY tstamp DESC LIMIT 1`
return fmt.Sprintf(q, tableName)
}
func (p *postgres) ListMigrations(tableName string) string {
q := `SELECT version_id, is_applied from %s ORDER BY id DESC`
return fmt.Sprintf(q, tableName)
}
func (p *postgres) GetLatestVersion(tableName string) string {
q := `SELECT max(version_id) FROM %s`
return fmt.Sprintf(q, tableName)
}
func (p *postgres) TableExists(tableName string) string {
schemaName, tableName := parseTableIdentifier(tableName)
if schemaName != "" {
q := `SELECT EXISTS ( SELECT 1 FROM pg_tables WHERE schemaname = '%s' AND tablename = '%s' )`
return fmt.Sprintf(q, schemaName, tableName)
}
q := `SELECT EXISTS ( SELECT 1 FROM pg_tables WHERE (current_schema() IS NULL OR schemaname = current_schema()) AND tablename = '%s' )`
return fmt.Sprintf(q, tableName)
}
func parseTableIdentifier(name string) (schema, table string) {
schema, table, found := strings.Cut(name, ".")
if !found {
return "", name
}
return schema, table
}

View file

@ -0,0 +1,52 @@
package dialects
import (
"fmt"
"github.com/pressly/goose/v3/database/dialect"
)
// Redshift returns a new [dialect.Querier] for Redshift dialect.
func NewRedshift() dialect.Querier {
return &redshift{}
}
type redshift struct{}
var _ dialect.Querier = (*redshift)(nil)
func (r *redshift) CreateTable(tableName string) string {
q := `CREATE TABLE %s (
id integer NOT NULL identity(1, 1),
version_id bigint NOT NULL,
is_applied boolean NOT NULL,
tstamp timestamp NULL default sysdate,
PRIMARY KEY(id)
)`
return fmt.Sprintf(q, tableName)
}
func (r *redshift) InsertVersion(tableName string) string {
q := `INSERT INTO %s (version_id, is_applied) VALUES ($1, $2)`
return fmt.Sprintf(q, tableName)
}
func (r *redshift) DeleteVersion(tableName string) string {
q := `DELETE FROM %s WHERE version_id=$1`
return fmt.Sprintf(q, tableName)
}
func (r *redshift) GetMigrationByVersion(tableName string) string {
q := `SELECT tstamp, is_applied FROM %s WHERE version_id=$1 ORDER BY tstamp DESC LIMIT 1`
return fmt.Sprintf(q, tableName)
}
func (r *redshift) ListMigrations(tableName string) string {
q := `SELECT version_id, is_applied from %s ORDER BY id DESC`
return fmt.Sprintf(q, tableName)
}
func (r *redshift) GetLatestVersion(tableName string) string {
q := `SELECT max(version_id) FROM %s`
return fmt.Sprintf(q, tableName)
}

View file

@ -0,0 +1,51 @@
package dialects
import (
"fmt"
"github.com/pressly/goose/v3/database/dialect"
)
// NewSqlite3 returns a [dialect.Querier] for SQLite3 dialect.
func NewSqlite3() dialect.Querier {
return &sqlite3{}
}
type sqlite3 struct{}
var _ dialect.Querier = (*sqlite3)(nil)
func (s *sqlite3) CreateTable(tableName string) string {
q := `CREATE TABLE %s (
id INTEGER PRIMARY KEY AUTOINCREMENT,
version_id INTEGER NOT NULL,
is_applied INTEGER NOT NULL,
tstamp TIMESTAMP DEFAULT (datetime('now'))
)`
return fmt.Sprintf(q, tableName)
}
func (s *sqlite3) InsertVersion(tableName string) string {
q := `INSERT INTO %s (version_id, is_applied) VALUES (?, ?)`
return fmt.Sprintf(q, tableName)
}
func (s *sqlite3) DeleteVersion(tableName string) string {
q := `DELETE FROM %s WHERE version_id=?`
return fmt.Sprintf(q, tableName)
}
func (s *sqlite3) GetMigrationByVersion(tableName string) string {
q := `SELECT tstamp, is_applied FROM %s WHERE version_id=? ORDER BY tstamp DESC LIMIT 1`
return fmt.Sprintf(q, tableName)
}
func (s *sqlite3) ListMigrations(tableName string) string {
q := `SELECT version_id, is_applied from %s ORDER BY id DESC`
return fmt.Sprintf(q, tableName)
}
func (s *sqlite3) GetLatestVersion(tableName string) string {
q := `SELECT MAX(version_id) FROM %s`
return fmt.Sprintf(q, tableName)
}

View file

@ -0,0 +1,51 @@
package dialects
import (
"fmt"
"github.com/pressly/goose/v3/database/dialect"
)
// NewSqlserver returns a [dialect.Querier] for SQL Server dialect.
func NewSqlserver() dialect.Querier {
return &sqlserver{}
}
type sqlserver struct{}
var _ dialect.Querier = (*sqlserver)(nil)
func (s *sqlserver) CreateTable(tableName string) string {
q := `CREATE TABLE %s (
id INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
version_id BIGINT NOT NULL,
is_applied BIT NOT NULL,
tstamp DATETIME NULL DEFAULT CURRENT_TIMESTAMP
)`
return fmt.Sprintf(q, tableName)
}
func (s *sqlserver) InsertVersion(tableName string) string {
q := `INSERT INTO %s (version_id, is_applied) VALUES (@p1, @p2)`
return fmt.Sprintf(q, tableName)
}
func (s *sqlserver) DeleteVersion(tableName string) string {
q := `DELETE FROM %s WHERE version_id=@p1`
return fmt.Sprintf(q, tableName)
}
func (s *sqlserver) GetMigrationByVersion(tableName string) string {
q := `SELECT TOP 1 tstamp, is_applied FROM %s WHERE version_id=@p1 ORDER BY tstamp DESC`
return fmt.Sprintf(q, tableName)
}
func (s *sqlserver) ListMigrations(tableName string) string {
q := `SELECT version_id, is_applied FROM %s ORDER BY id DESC`
return fmt.Sprintf(q, tableName)
}
func (s *sqlserver) GetLatestVersion(tableName string) string {
q := `SELECT MAX(version_id) FROM %s`
return fmt.Sprintf(q, tableName)
}

View file

@ -0,0 +1,54 @@
package dialects
import (
"fmt"
"github.com/pressly/goose/v3/database/dialect"
)
// NewStarrocks returns a [dialect.Querier] for StarRocks dialect.
func NewStarrocks() dialect.Querier {
return &starrocks{}
}
type starrocks struct{}
var _ dialect.Querier = (*starrocks)(nil)
func (m *starrocks) CreateTable(tableName string) string {
q := `CREATE TABLE IF NOT EXISTS %s (
id bigint NOT NULL AUTO_INCREMENT,
version_id bigint NOT NULL,
is_applied boolean NOT NULL,
tstamp datetime NULL default CURRENT_TIMESTAMP
)
PRIMARY KEY (id)
DISTRIBUTED BY HASH (id)
ORDER BY (id,version_id)`
return fmt.Sprintf(q, tableName)
}
func (m *starrocks) InsertVersion(tableName string) string {
q := `INSERT INTO %s (version_id, is_applied) VALUES (?, ?)`
return fmt.Sprintf(q, tableName)
}
func (m *starrocks) DeleteVersion(tableName string) string {
q := `DELETE FROM %s WHERE version_id=?`
return fmt.Sprintf(q, tableName)
}
func (m *starrocks) GetMigrationByVersion(tableName string) string {
q := `SELECT tstamp, is_applied FROM %s WHERE version_id=? ORDER BY tstamp DESC LIMIT 1`
return fmt.Sprintf(q, tableName)
}
func (m *starrocks) ListMigrations(tableName string) string {
q := `SELECT version_id, is_applied from %s ORDER BY id DESC`
return fmt.Sprintf(q, tableName)
}
func (m *starrocks) GetLatestVersion(tableName string) string {
q := `SELECT MAX(version_id) FROM %s`
return fmt.Sprintf(q, tableName)
}

View file

@ -0,0 +1,52 @@
package dialects
import (
"fmt"
"github.com/pressly/goose/v3/database/dialect"
)
// NewTidb returns a [dialect.Querier] for TiDB dialect.
func NewTidb() dialect.Querier {
return &Tidb{}
}
type Tidb struct{}
var _ dialect.Querier = (*Tidb)(nil)
func (t *Tidb) CreateTable(tableName string) string {
q := `CREATE TABLE %s (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE,
version_id bigint NOT NULL,
is_applied boolean NOT NULL,
tstamp timestamp NULL default now(),
PRIMARY KEY(id)
)`
return fmt.Sprintf(q, tableName)
}
func (t *Tidb) InsertVersion(tableName string) string {
q := `INSERT INTO %s (version_id, is_applied) VALUES (?, ?)`
return fmt.Sprintf(q, tableName)
}
func (t *Tidb) DeleteVersion(tableName string) string {
q := `DELETE FROM %s WHERE version_id=?`
return fmt.Sprintf(q, tableName)
}
func (t *Tidb) GetMigrationByVersion(tableName string) string {
q := `SELECT tstamp, is_applied FROM %s WHERE version_id=? ORDER BY tstamp DESC LIMIT 1`
return fmt.Sprintf(q, tableName)
}
func (t *Tidb) ListMigrations(tableName string) string {
q := `SELECT version_id, is_applied from %s ORDER BY id DESC`
return fmt.Sprintf(q, tableName)
}
func (t *Tidb) GetLatestVersion(tableName string) string {
q := `SELECT MAX(version_id) FROM %s`
return fmt.Sprintf(q, tableName)
}

View file

@ -0,0 +1,14 @@
package dialects
import "github.com/pressly/goose/v3/database/dialect"
// NewTurso returns a [dialect.Querier] for Turso dialect.
func NewTurso() dialect.Querier {
return &turso{}
}
type turso struct {
sqlite3
}
var _ dialect.Querier = (*turso)(nil)

View file

@ -0,0 +1,54 @@
package dialects
import (
"fmt"
"github.com/pressly/goose/v3/database/dialect"
)
// NewVertica returns a new [dialect.Querier] for Vertica dialect.
//
// DEPRECATED: Vertica support is deprecated and will be removed in a future release.
func NewVertica() dialect.Querier {
return &vertica{}
}
type vertica struct{}
var _ dialect.Querier = (*vertica)(nil)
func (v *vertica) CreateTable(tableName string) string {
q := `CREATE TABLE %s (
id identity(1,1) NOT NULL,
version_id bigint NOT NULL,
is_applied boolean NOT NULL,
tstamp timestamp NULL default now(),
PRIMARY KEY(id)
)`
return fmt.Sprintf(q, tableName)
}
func (v *vertica) InsertVersion(tableName string) string {
q := `INSERT INTO %s (version_id, is_applied) VALUES (?, ?)`
return fmt.Sprintf(q, tableName)
}
func (v *vertica) DeleteVersion(tableName string) string {
q := `DELETE FROM %s WHERE version_id=?`
return fmt.Sprintf(q, tableName)
}
func (v *vertica) GetMigrationByVersion(tableName string) string {
q := `SELECT tstamp, is_applied FROM %s WHERE version_id=? ORDER BY tstamp DESC LIMIT 1`
return fmt.Sprintf(q, tableName)
}
func (v *vertica) ListMigrations(tableName string) string {
q := `SELECT version_id, is_applied from %s ORDER BY id DESC`
return fmt.Sprintf(q, tableName)
}
func (v *vertica) GetLatestVersion(tableName string) string {
q := `SELECT MAX(version_id) FROM %s`
return fmt.Sprintf(q, tableName)
}

View file

@ -0,0 +1,62 @@
package dialects
import (
"fmt"
"github.com/pressly/goose/v3/database/dialect"
)
// NewYDB returns a new [dialect.Querier] for Vertica dialect.
func NewYDB() dialect.Querier {
return &ydb{}
}
type ydb struct{}
var _ dialect.Querier = (*ydb)(nil)
func (c *ydb) CreateTable(tableName string) string {
q := `CREATE TABLE %s (
version_id Uint64,
is_applied Bool,
tstamp Timestamp,
PRIMARY KEY(version_id)
)`
return fmt.Sprintf(q, tableName)
}
func (c *ydb) InsertVersion(tableName string) string {
q := `INSERT INTO %s (
version_id,
is_applied,
tstamp
) VALUES (
CAST($1 AS Uint64),
$2,
CurrentUtcTimestamp()
)`
return fmt.Sprintf(q, tableName)
}
func (c *ydb) DeleteVersion(tableName string) string {
q := `DELETE FROM %s WHERE version_id = $1`
return fmt.Sprintf(q, tableName)
}
func (c *ydb) GetMigrationByVersion(tableName string) string {
q := `SELECT tstamp, is_applied FROM %s WHERE version_id = $1 ORDER BY tstamp DESC LIMIT 1`
return fmt.Sprintf(q, tableName)
}
func (c *ydb) ListMigrations(tableName string) string {
q := `
SELECT version_id, is_applied, tstamp AS __discard_column_tstamp
FROM %s ORDER BY __discard_column_tstamp DESC`
return fmt.Sprintf(q, tableName)
}
func (c *ydb) GetLatestVersion(tableName string) string {
q := `SELECT MAX(version_id) FROM %s`
return fmt.Sprintf(q, tableName)
}

View file

@ -0,0 +1,124 @@
// Package gooseutil provides utility functions we want to keep internal to the package. It's
// intended to be a collection of well-tested helper functions.
package gooseutil
import (
"fmt"
"math"
"sort"
"strconv"
"strings"
)
// UpVersions returns a list of migrations to apply based on the versions in the filesystem and the
// versions in the database. The target version can be used to specify a target version. In most
// cases this will be math.MaxInt64.
//
// The allowMissing flag can be used to allow missing migrations as part of the list of migrations
// to apply. Otherwise, an error will be returned if there are missing migrations in the database.
func UpVersions(
fsysVersions []int64,
dbVersions []int64,
target int64,
allowMissing bool,
) ([]int64, error) {
// Sort the list of versions in the filesystem. This should already be sorted, but we do this
// just in case.
sortAscending(fsysVersions)
// dbAppliedVersions is a map of all applied migrations in the database.
dbAppliedVersions := make(map[int64]bool, len(dbVersions))
var dbMaxVersion int64
for _, v := range dbVersions {
dbAppliedVersions[v] = true
if v > dbMaxVersion {
dbMaxVersion = v
}
}
// Get a list of migrations that are missing from the database. A missing migration is one that
// has a version less than the max version in the database and has not been applied.
//
// In most cases the target version is math.MaxInt64, but it can be used to specify a target
// version. In which case we respect the target version and only surface migrations up to and
// including that target.
var missing []int64
for _, v := range fsysVersions {
if dbAppliedVersions[v] {
continue
}
if v < dbMaxVersion && v <= target {
missing = append(missing, v)
}
}
// feat(mf): It is very possible someone may want to apply ONLY new migrations and skip missing
// migrations entirely. At the moment this is not supported, but leaving this comment because
// that's where that logic would be handled.
//
// For example, if database has 1,4 already applied and 2,3,5 are new, we would apply only 5 and
// skip 2,3. Not sure if this is a common use case, but it's possible someone may want to do
// this.
if len(missing) > 0 && !allowMissing {
return nil, newMissingError(missing, dbMaxVersion, target)
}
var out []int64
// 1. Add missing migrations to the list of migrations to apply, if any.
out = append(out, missing...)
// 2. Add new migrations to the list of migrations to apply, if any.
for _, v := range fsysVersions {
if dbAppliedVersions[v] {
continue
}
if v > dbMaxVersion && v <= target {
out = append(out, v)
}
}
// 3. Sort the list of migrations to apply.
sortAscending(out)
return out, nil
}
func newMissingError(
missing []int64,
dbMaxVersion int64,
target int64,
) error {
sortAscending(missing)
collected := make([]string, 0, len(missing))
for _, v := range missing {
collected = append(collected, strconv.FormatInt(v, 10))
}
msg := "migration"
if len(collected) > 1 {
msg += "s"
}
var versionsMsg string
if len(collected) > 1 {
versionsMsg = "versions " + strings.Join(collected, ",")
} else {
versionsMsg = "version " + collected[0]
}
desiredMsg := fmt.Sprintf("database version (%d)", dbMaxVersion)
if target != math.MaxInt64 {
desiredMsg += fmt.Sprintf(", with target version (%d)", target)
}
return fmt.Errorf("detected %d missing (out-of-order) %s lower than %s: %s",
len(missing), msg, desiredMsg, versionsMsg,
)
}
func sortAscending(versions []int64) {
sort.Slice(versions, func(i, j int) bool {
return versions[i] < versions[j]
})
}

View file

@ -0,0 +1,171 @@
package legacystore
import (
"context"
"database/sql"
"fmt"
"time"
"github.com/pressly/goose/v3/database"
"github.com/pressly/goose/v3/database/dialect"
"github.com/pressly/goose/v3/internal/dialects"
)
// Store is the interface that wraps the basic methods for a database dialect.
//
// A dialect is a set of SQL statements that are specific to a database.
//
// By defining a store interface, we can support multiple databases
// with a single codebase.
//
// The underlying implementation does not modify the error. It is the callers
// responsibility to assert for the correct error, such as sql.ErrNoRows.
type Store interface {
// CreateVersionTable creates the version table within a transaction.
// This table is used to store goose migrations.
CreateVersionTable(ctx context.Context, tx *sql.Tx, tableName string) error
// InsertVersion inserts a version id into the version table within a transaction.
InsertVersion(ctx context.Context, tx *sql.Tx, tableName string, version int64) error
// InsertVersionNoTx inserts a version id into the version table without a transaction.
InsertVersionNoTx(ctx context.Context, db *sql.DB, tableName string, version int64) error
// DeleteVersion deletes a version id from the version table within a transaction.
DeleteVersion(ctx context.Context, tx *sql.Tx, tableName string, version int64) error
// DeleteVersionNoTx deletes a version id from the version table without a transaction.
DeleteVersionNoTx(ctx context.Context, db *sql.DB, tableName string, version int64) error
// GetMigrationRow retrieves a single migration by version id.
//
// Returns the raw sql error if the query fails. It is the callers responsibility
// to assert for the correct error, such as sql.ErrNoRows.
GetMigration(ctx context.Context, db *sql.DB, tableName string, version int64) (*GetMigrationResult, error)
// ListMigrations retrieves all migrations sorted in descending order by id.
//
// If there are no migrations, an empty slice is returned with no error.
ListMigrations(ctx context.Context, db *sql.DB, tableName string) ([]*ListMigrationsResult, error)
}
// NewStore returns a new Store for the given dialect.
func NewStore(d database.Dialect) (Store, error) {
var querier dialect.Querier
switch d {
case database.DialectPostgres:
querier = dialects.NewPostgres()
case database.DialectMySQL:
querier = dialects.NewMysql()
case database.DialectSQLite3:
querier = dialects.NewSqlite3()
case database.DialectMSSQL:
querier = dialects.NewSqlserver()
case database.DialectRedshift:
querier = dialects.NewRedshift()
case database.DialectTiDB:
querier = dialects.NewTidb()
case database.DialectClickHouse:
querier = dialects.NewClickhouse()
case database.DialectVertica:
querier = dialects.NewVertica()
case database.DialectYdB:
querier = dialects.NewYDB()
case database.DialectTurso:
querier = dialects.NewTurso()
case database.DialectStarrocks:
querier = dialects.NewStarrocks()
default:
return nil, fmt.Errorf("unknown querier dialect: %v", d)
}
return &store{querier: querier}, nil
}
type GetMigrationResult struct {
IsApplied bool
Timestamp time.Time
}
type ListMigrationsResult struct {
VersionID int64
IsApplied bool
}
type store struct {
querier dialect.Querier
}
var _ Store = (*store)(nil)
func (s *store) CreateVersionTable(ctx context.Context, tx *sql.Tx, tableName string) error {
q := s.querier.CreateTable(tableName)
_, err := tx.ExecContext(ctx, q)
return err
}
func (s *store) InsertVersion(ctx context.Context, tx *sql.Tx, tableName string, version int64) error {
q := s.querier.InsertVersion(tableName)
_, err := tx.ExecContext(ctx, q, version, true)
return err
}
func (s *store) InsertVersionNoTx(ctx context.Context, db *sql.DB, tableName string, version int64) error {
q := s.querier.InsertVersion(tableName)
_, err := db.ExecContext(ctx, q, version, true)
return err
}
func (s *store) DeleteVersion(ctx context.Context, tx *sql.Tx, tableName string, version int64) error {
q := s.querier.DeleteVersion(tableName)
_, err := tx.ExecContext(ctx, q, version)
return err
}
func (s *store) DeleteVersionNoTx(ctx context.Context, db *sql.DB, tableName string, version int64) error {
q := s.querier.DeleteVersion(tableName)
_, err := db.ExecContext(ctx, q, version)
return err
}
func (s *store) GetMigration(
ctx context.Context,
db *sql.DB,
tableName string,
version int64,
) (*GetMigrationResult, error) {
q := s.querier.GetMigrationByVersion(tableName)
var timestamp time.Time
var isApplied bool
err := db.QueryRowContext(ctx, q, version).Scan(&timestamp, &isApplied)
if err != nil {
return nil, err
}
return &GetMigrationResult{
IsApplied: isApplied,
Timestamp: timestamp,
}, nil
}
func (s *store) ListMigrations(ctx context.Context, db *sql.DB, tableName string) ([]*ListMigrationsResult, error) {
q := s.querier.ListMigrations(tableName)
rows, err := db.QueryContext(ctx, q)
if err != nil {
return nil, err
}
defer rows.Close()
var migrations []*ListMigrationsResult
for rows.Next() {
var version int64
var isApplied bool
if err := rows.Scan(&version, &isApplied); err != nil {
return nil, err
}
migrations = append(migrations, &ListMigrationsResult{
VersionID: version,
IsApplied: isApplied,
})
}
if err := rows.Err(); err != nil {
return nil, err
}
return migrations, nil
}

View file

@ -0,0 +1,59 @@
package sqlparser
import (
"fmt"
"io/fs"
"go.uber.org/multierr"
"golang.org/x/sync/errgroup"
)
type ParsedSQL struct {
UseTx bool
Up, Down []string
}
func ParseAllFromFS(fsys fs.FS, filename string, debug bool) (*ParsedSQL, error) {
parsedSQL := new(ParsedSQL)
// TODO(mf): parse is called twice, once for up and once for down. This is inefficient. It
// should be possible to parse both directions in one pass. Also, UseTx is set once (but
// returned twice), which is unnecessary and potentially error-prone if the two calls to
// parseSQL disagree based on direction.
var g errgroup.Group
g.Go(func() error {
up, useTx, err := parse(fsys, filename, DirectionUp, debug)
if err != nil {
return err
}
parsedSQL.Up = up
parsedSQL.UseTx = useTx
return nil
})
g.Go(func() error {
down, _, err := parse(fsys, filename, DirectionDown, debug)
if err != nil {
return err
}
parsedSQL.Down = down
return nil
})
if err := g.Wait(); err != nil {
return nil, err
}
return parsedSQL, nil
}
func parse(fsys fs.FS, filename string, direction Direction, debug bool) (_ []string, _ bool, retErr error) {
r, err := fsys.Open(filename)
if err != nil {
return nil, false, err
}
defer func() {
retErr = multierr.Append(retErr, r.Close())
}()
stmts, useTx, err := ParseSQLMigration(r, direction, debug)
if err != nil {
return nil, false, fmt.Errorf("failed to parse %s: %w", filename, err)
}
return stmts, useTx, nil
}

View file

@ -0,0 +1,394 @@
package sqlparser
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"log"
"os"
"strings"
"sync"
"github.com/mfridman/interpolate"
)
type Direction string
const (
DirectionUp Direction = "up"
DirectionDown Direction = "down"
)
func FromBool(b bool) Direction {
if b {
return DirectionUp
}
return DirectionDown
}
func (d Direction) String() string {
return string(d)
}
func (d Direction) ToBool() bool {
return d == DirectionUp
}
type parserState int
const (
start parserState = iota // 0
gooseUp // 1
gooseStatementBeginUp // 2
gooseStatementEndUp // 3
gooseDown // 4
gooseStatementBeginDown // 5
gooseStatementEndDown // 6
)
type stateMachine struct {
state parserState
verbose bool
}
func newStateMachine(begin parserState, verbose bool) *stateMachine {
return &stateMachine{
state: begin,
verbose: verbose,
}
}
func (s *stateMachine) get() parserState {
return s.state
}
func (s *stateMachine) set(new parserState) {
s.print("set %d => %d", s.state, new)
s.state = new
}
const (
grayColor = "\033[90m"
resetColor = "\033[00m"
)
func (s *stateMachine) print(msg string, args ...interface{}) {
msg = "StateMachine: " + msg
if s.verbose {
log.Printf(grayColor+msg+resetColor, args...)
}
}
const scanBufSize = 4 * 1024 * 1024
var bufferPool = sync.Pool{
New: func() interface{} {
buf := make([]byte, scanBufSize)
return &buf
},
}
// Split given SQL script into individual statements and return
// SQL statements for given direction (up=true, down=false).
//
// The base case is to simply split on semicolons, as these
// naturally terminate a statement.
//
// However, more complex cases like pl/pgsql can have semicolons
// within a statement. For these cases, we provide the explicit annotations
// 'StatementBegin' and 'StatementEnd' to allow the script to
// tell us to ignore semicolons.
func ParseSQLMigration(r io.Reader, direction Direction, debug bool) (stmts []string, useTx bool, err error) {
scanBufPtr := bufferPool.Get().(*[]byte)
scanBuf := *scanBufPtr
defer bufferPool.Put(scanBufPtr)
scanner := bufio.NewScanner(r)
scanner.Buffer(scanBuf, scanBufSize)
stateMachine := newStateMachine(start, debug)
useTx = true
useEnvsub := false
var buf bytes.Buffer
for scanner.Scan() {
line := scanner.Text()
if debug {
log.Println(line)
}
if stateMachine.get() == start && strings.TrimSpace(line) == "" {
continue
}
// Check for annotations.
// All annotations must be in format: "-- +goose [annotation]"
if strings.HasPrefix(strings.TrimSpace(line), "--") && strings.Contains(line, "+goose") {
var cmd annotation
cmd, err = extractAnnotation(line)
if err != nil {
return nil, false, fmt.Errorf("failed to parse annotation line %q: %w", line, err)
}
switch cmd {
case annotationUp:
switch stateMachine.get() {
case start:
stateMachine.set(gooseUp)
default:
return nil, false, fmt.Errorf("duplicate '-- +goose Up' annotations; stateMachine=%d, see https://github.com/pressly/goose#sql-migrations", stateMachine.state)
}
continue
case annotationDown:
switch stateMachine.get() {
case gooseUp, gooseStatementEndUp:
// If we hit a down annotation, but the buffer is not empty, we have an unfinished SQL query from a
// previous up annotation. This is an error, because we expect the SQL query to be terminated by a semicolon
// and the buffer to have been reset.
if bufferRemaining := strings.TrimSpace(buf.String()); len(bufferRemaining) > 0 {
return nil, false, missingSemicolonError(stateMachine.state, direction, bufferRemaining)
}
stateMachine.set(gooseDown)
default:
return nil, false, fmt.Errorf("must start with '-- +goose Up' annotation, stateMachine=%d, see https://github.com/pressly/goose#sql-migrations", stateMachine.state)
}
continue
case annotationStatementBegin:
switch stateMachine.get() {
case gooseUp, gooseStatementEndUp:
stateMachine.set(gooseStatementBeginUp)
case gooseDown, gooseStatementEndDown:
stateMachine.set(gooseStatementBeginDown)
default:
return nil, false, fmt.Errorf("'-- +goose StatementBegin' must be defined after '-- +goose Up' or '-- +goose Down' annotation, stateMachine=%d, see https://github.com/pressly/goose#sql-migrations", stateMachine.state)
}
continue
case annotationStatementEnd:
switch stateMachine.get() {
case gooseStatementBeginUp:
stateMachine.set(gooseStatementEndUp)
case gooseStatementBeginDown:
stateMachine.set(gooseStatementEndDown)
default:
return nil, false, errors.New("'-- +goose StatementEnd' must be defined after '-- +goose StatementBegin', see https://github.com/pressly/goose#sql-migrations")
}
case annotationNoTransaction:
useTx = false
continue
case annotationEnvsubOn:
useEnvsub = true
continue
case annotationEnvsubOff:
useEnvsub = false
continue
default:
return nil, false, fmt.Errorf("unknown annotation: %q", cmd)
}
}
// Once we've started parsing a statement the buffer is no longer empty,
// we keep all comments up until the end of the statement (the buffer will be reset).
// All other comments in the file are ignored.
if buf.Len() == 0 {
// This check ensures leading comments and empty lines prior to a statement are ignored.
if strings.HasPrefix(strings.TrimSpace(line), "--") || line == "" {
stateMachine.print("ignore comment")
continue
}
}
switch stateMachine.get() {
case gooseStatementEndDown, gooseStatementEndUp:
// Do not include the "+goose StatementEnd" annotation in the final statement.
default:
if useEnvsub {
expanded, err := interpolate.Interpolate(&envWrapper{}, line)
if err != nil {
return nil, false, fmt.Errorf("variable substitution failed: %w:\n%s", err, line)
}
line = expanded
}
// Write SQL line to a buffer.
if _, err := buf.WriteString(line + "\n"); err != nil {
return nil, false, fmt.Errorf("failed to write to buf: %w", err)
}
}
// Read SQL body one by line, if we're in the right direction.
//
// 1) basic query with semicolon; 2) psql statement
//
// Export statement once we hit end of statement.
switch stateMachine.get() {
case gooseUp, gooseStatementBeginUp, gooseStatementEndUp:
if direction == DirectionDown {
buf.Reset()
stateMachine.print("ignore down")
continue
}
case gooseDown, gooseStatementBeginDown, gooseStatementEndDown:
if direction == DirectionUp {
buf.Reset()
stateMachine.print("ignore up")
continue
}
default:
return nil, false, fmt.Errorf("failed to parse migration: unexpected state %d on line %q, see https://github.com/pressly/goose#sql-migrations", stateMachine.state, line)
}
switch stateMachine.get() {
case gooseUp:
if endsWithSemicolon(line) {
stmts = append(stmts, cleanupStatement(buf.String()))
buf.Reset()
stateMachine.print("store simple Up query")
}
case gooseDown:
if endsWithSemicolon(line) {
stmts = append(stmts, cleanupStatement(buf.String()))
buf.Reset()
stateMachine.print("store simple Down query")
}
case gooseStatementEndUp:
stmts = append(stmts, cleanupStatement(buf.String()))
buf.Reset()
stateMachine.print("store Up statement")
stateMachine.set(gooseUp)
case gooseStatementEndDown:
stmts = append(stmts, cleanupStatement(buf.String()))
buf.Reset()
stateMachine.print("store Down statement")
stateMachine.set(gooseDown)
}
}
if err := scanner.Err(); err != nil {
return nil, false, fmt.Errorf("failed to scan migration: %w", err)
}
// EOF
switch stateMachine.get() {
case start:
return nil, false, errors.New("failed to parse migration: must start with '-- +goose Up' annotation, see https://github.com/pressly/goose#sql-migrations")
case gooseStatementBeginUp, gooseStatementBeginDown:
return nil, false, errors.New("failed to parse migration: missing '-- +goose StatementEnd' annotation")
}
if bufferRemaining := strings.TrimSpace(buf.String()); len(bufferRemaining) > 0 {
return nil, false, missingSemicolonError(stateMachine.state, direction, bufferRemaining)
}
return stmts, useTx, nil
}
type annotation string
const (
annotationUp annotation = "Up"
annotationDown annotation = "Down"
annotationStatementBegin annotation = "StatementBegin"
annotationStatementEnd annotation = "StatementEnd"
annotationNoTransaction annotation = "NO TRANSACTION"
annotationEnvsubOn annotation = "ENVSUB ON"
annotationEnvsubOff annotation = "ENVSUB OFF"
)
var supportedAnnotations = map[annotation]struct{}{
annotationUp: {},
annotationDown: {},
annotationStatementBegin: {},
annotationStatementEnd: {},
annotationNoTransaction: {},
annotationEnvsubOn: {},
annotationEnvsubOff: {},
}
var (
errEmptyAnnotation = errors.New("empty annotation")
errInvalidAnnotation = errors.New("invalid annotation")
)
// extractAnnotation extracts the annotation from the line.
// All annotations must be in format: "-- +goose [annotation]"
// Allowed annotations: Up, Down, StatementBegin, StatementEnd, NO TRANSACTION, ENVSUB ON, ENVSUB OFF
func extractAnnotation(line string) (annotation, error) {
// If line contains leading whitespace - return error.
if strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") {
return "", fmt.Errorf("%q contains leading whitespace: %w", line, errInvalidAnnotation)
}
// Extract the annotation from the line, by removing the leading "--"
cmd := strings.ReplaceAll(line, "--", "")
// Extract the annotation from the line, by removing the leading "+goose"
cmd = strings.Replace(cmd, "+goose", "", 1)
if strings.Contains(cmd, "+goose") {
return "", fmt.Errorf("%q contains multiple '+goose' annotations: %w", cmd, errInvalidAnnotation)
}
// Remove leading and trailing whitespace from the annotation command.
cmd = strings.TrimSpace(cmd)
if cmd == "" {
return "", errEmptyAnnotation
}
a := annotation(cmd)
for s := range supportedAnnotations {
if strings.EqualFold(string(s), string(a)) {
return s, nil
}
}
return "", fmt.Errorf("%q not supported: %w", cmd, errInvalidAnnotation)
}
func missingSemicolonError(state parserState, direction Direction, s string) error {
return fmt.Errorf("failed to parse migration: state %d, direction: %v: unexpected unfinished SQL query: %q: missing semicolon?",
state,
direction,
s,
)
}
type envWrapper struct{}
var _ interpolate.Env = (*envWrapper)(nil)
func (e *envWrapper) Get(key string) (string, bool) {
return os.LookupEnv(key)
}
func cleanupStatement(input string) string {
return strings.TrimSpace(input)
}
// Checks the line to see if the line has a statement-ending semicolon
// or if the line contains a double-dash comment.
func endsWithSemicolon(line string) bool {
scanBufPtr := bufferPool.Get().(*[]byte)
scanBuf := *scanBufPtr
defer bufferPool.Put(scanBufPtr)
prev := ""
scanner := bufio.NewScanner(strings.NewReader(line))
scanner.Buffer(scanBuf, scanBufSize)
scanner.Split(bufio.ScanWords)
for scanner.Scan() {
word := scanner.Text()
if strings.HasPrefix(word, "--") {
break
}
prev = word
}
return strings.HasSuffix(prev, ";")
}

View file

@ -0,0 +1,282 @@
package store
import (
"context"
"database/sql"
"errors"
"fmt"
"strings"
"time"
"go.uber.org/multierr"
)
// NewPostgres creates a new Postgres-based [LockStore].
func NewPostgres(tableName string) (LockStore, error) {
if tableName == "" {
return nil, errors.New("table name must not be empty")
}
return &postgresStore{
tableName: tableName,
}, nil
}
var _ LockStore = (*postgresStore)(nil)
type postgresStore struct {
tableName string
}
func (s *postgresStore) TableExists(
ctx context.Context,
db *sql.DB,
) (bool, error) {
var query string
schemaName, tableName := parseTableIdentifier(s.tableName)
if schemaName != "" {
q := `SELECT EXISTS ( SELECT 1 FROM pg_tables WHERE schemaname = '%s' AND tablename = '%s' )`
query = fmt.Sprintf(q, schemaName, tableName)
} else {
q := `SELECT EXISTS ( SELECT 1 FROM pg_tables WHERE (current_schema() IS NULL OR schemaname = current_schema()) AND tablename = '%s' )`
query = fmt.Sprintf(q, tableName)
}
var exists bool
if err := db.QueryRowContext(ctx, query).Scan(
&exists,
); err != nil {
return false, fmt.Errorf("table exists: %w", err)
}
return exists, nil
}
func (s *postgresStore) CreateLockTable(
ctx context.Context,
db *sql.DB,
) error {
exists, err := s.TableExists(ctx, db)
if err != nil {
return fmt.Errorf("check lock table existence: %w", err)
}
if exists {
return nil
}
query := fmt.Sprintf(`CREATE TABLE %s (
lock_id bigint NOT NULL PRIMARY KEY,
locked boolean NOT NULL DEFAULT false,
locked_at timestamptz NULL,
locked_by text NULL,
lease_expires_at timestamptz NULL,
updated_at timestamptz NULL
)`, s.tableName)
if _, err := db.ExecContext(ctx, query); err != nil {
// Double-check if another process created it concurrently
if exists, checkErr := s.TableExists(ctx, db); checkErr == nil && exists {
// Another process created it, that's fine!
return nil
}
return fmt.Errorf("create lock table %q: %w", s.tableName, err)
}
return nil
}
func (s *postgresStore) AcquireLock(
ctx context.Context,
db *sql.DB,
lockID int64,
lockedBy string,
leaseDuration time.Duration,
) (*AcquireLockResult, error) {
query := fmt.Sprintf(`INSERT INTO %s (lock_id, locked, locked_at, locked_by, lease_expires_at, updated_at)
VALUES ($1, true, now(), $2, now() + $3::interval, now())
ON CONFLICT (lock_id) DO UPDATE SET
locked = true,
locked_at = now(),
locked_by = $2,
lease_expires_at = now() + $3::interval,
updated_at = now()
WHERE %s.locked = false OR %s.lease_expires_at < now()
RETURNING locked_by, lease_expires_at`, s.tableName, s.tableName, s.tableName)
// Convert duration to PostgreSQL interval format
leaseDurationStr := formatDurationAsInterval(leaseDuration)
var returnedLockedBy string
var leaseExpiresAt time.Time
err := db.QueryRowContext(ctx, query,
lockID,
lockedBy,
leaseDurationStr,
).Scan(
&returnedLockedBy,
&leaseExpiresAt,
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
// TODO(mf): should we return a special error type here?
return nil, fmt.Errorf("acquire lock %d: already held by another instance", lockID)
}
return nil, fmt.Errorf("acquire lock %d: %w", lockID, err)
}
// Verify we got the lock by checking the returned locked_by matches our instance ID
if returnedLockedBy != lockedBy {
return nil, fmt.Errorf("acquire lock %d: acquired by %s instead of %s", lockID, returnedLockedBy, lockedBy)
}
return &AcquireLockResult{
LockedBy: returnedLockedBy,
LeaseExpiresAt: leaseExpiresAt,
}, nil
}
func (s *postgresStore) ReleaseLock(
ctx context.Context,
db *sql.DB,
lockID int64,
lockedBy string,
) (*ReleaseLockResult, error) {
// Release lock only if it's held by the current instance
query := fmt.Sprintf(`UPDATE %s SET
locked = false,
locked_at = NULL,
locked_by = NULL,
lease_expires_at = NULL,
updated_at = now()
WHERE lock_id = $1 AND locked_by = $2
RETURNING lock_id`, s.tableName)
var returnedLockID int64
err := db.QueryRowContext(ctx, query,
lockID,
lockedBy,
).Scan(
&returnedLockID,
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
// TODO(mf): should we return a special error type here?
return nil, fmt.Errorf("release lock %d: not held by this instance", lockID)
}
return nil, fmt.Errorf("release lock %d: %w", lockID, err)
}
// Verify the correct lock was released
if returnedLockID != lockID {
return nil, fmt.Errorf("release lock %d: returned lock ID %d does not match", lockID, returnedLockID)
}
return &ReleaseLockResult{
LockID: returnedLockID,
}, nil
}
func (s *postgresStore) UpdateLease(
ctx context.Context,
db *sql.DB,
lockID int64,
lockedBy string,
leaseDuration time.Duration,
) (*UpdateLeaseResult, error) {
// Update lease expiration time for heartbeat, only if we own the lock
query := fmt.Sprintf(`UPDATE %s SET
lease_expires_at = now() + $1::interval,
updated_at = now()
WHERE lock_id = $2 AND locked_by = $3 AND locked = true
RETURNING lease_expires_at`, s.tableName)
// Convert duration to PostgreSQL interval format
intervalStr := formatDurationAsInterval(leaseDuration)
var leaseExpiresAt time.Time
err := db.QueryRowContext(ctx, query,
intervalStr,
lockID,
lockedBy,
).Scan(
&leaseExpiresAt,
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("failed to update lease for lock %d: not held by this instance", lockID)
}
return nil, fmt.Errorf("failed to update lease for lock %d: %w", lockID, err)
}
return &UpdateLeaseResult{
LeaseExpiresAt: leaseExpiresAt,
}, nil
}
func (s *postgresStore) CheckLockStatus(
ctx context.Context,
db *sql.DB,
lockID int64,
) (*LockStatus, error) {
query := fmt.Sprintf(`SELECT locked, locked_by, lease_expires_at, updated_at FROM %s WHERE lock_id = $1`, s.tableName)
var status LockStatus
err := db.QueryRowContext(ctx, query,
lockID,
).Scan(
&status.Locked,
&status.LockedBy,
&status.LeaseExpiresAt,
&status.UpdatedAt,
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("lock %d not found", lockID)
}
return nil, fmt.Errorf("check lock status for %d: %w", lockID, err)
}
return &status, nil
}
func (s *postgresStore) CleanupStaleLocks(ctx context.Context, db *sql.DB) (_ []int64, retErr error) {
query := fmt.Sprintf(`UPDATE %s SET
locked = false,
locked_at = NULL,
locked_by = NULL,
lease_expires_at = NULL,
updated_at = now()
WHERE locked = true AND lease_expires_at < now()
RETURNING lock_id`, s.tableName)
rows, err := db.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("cleanup stale locks: %w", err)
}
defer func() {
retErr = multierr.Append(retErr, rows.Close())
}()
var cleanedLocks []int64
for rows.Next() {
var lockID int64
if err := rows.Scan(&lockID); err != nil {
return nil, fmt.Errorf("scan cleaned lock ID: %w", err)
}
cleanedLocks = append(cleanedLocks, lockID)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate over cleaned locks: %w", err)
}
return cleanedLocks, nil
}
// formatDurationAsInterval converts a time.Duration to PostgreSQL interval format
func formatDurationAsInterval(d time.Duration) string {
return fmt.Sprintf("%d seconds", int(d.Seconds()))
}
func parseTableIdentifier(name string) (schema, table string) {
schema, table, found := strings.Cut(name, ".")
if !found {
return "", name
}
return schema, table
}

View file

@ -0,0 +1,51 @@
package store
import (
"context"
"database/sql"
"time"
)
// LockStore defines the interface for storing and managing database locks.
type LockStore interface {
// CreateLockTable creates the lock table if it doesn't exist. Implementations should ensure
// that this operation is idempotent.
CreateLockTable(ctx context.Context, db *sql.DB) error
// TableExists checks if the lock table exists.
TableExists(ctx context.Context, db *sql.DB) (bool, error)
// AcquireLock attempts to acquire a lock for the given lockID.
AcquireLock(ctx context.Context, db *sql.DB, lockID int64, lockedBy string, leaseDuration time.Duration) (*AcquireLockResult, error)
// ReleaseLock releases a lock held by the current instance.
ReleaseLock(ctx context.Context, db *sql.DB, lockID int64, lockedBy string) (*ReleaseLockResult, error)
// UpdateLease updates the lease expiration time for a lock (heartbeat).
UpdateLease(ctx context.Context, db *sql.DB, lockID int64, lockedBy string, leaseDuration time.Duration) (*UpdateLeaseResult, error)
// CheckLockStatus checks the current status of a lock.
CheckLockStatus(ctx context.Context, db *sql.DB, lockID int64) (*LockStatus, error)
// CleanupStaleLocks removes any locks that have expired using server time. Returns the list of
// lock IDs that were cleaned up, if any.
CleanupStaleLocks(ctx context.Context, db *sql.DB) ([]int64, error)
}
// LockStatus represents the current status of a lock.
type LockStatus struct {
Locked bool
LockedBy *string
LeaseExpiresAt *time.Time
UpdatedAt *time.Time
}
// AcquireLockResult contains the result of a lock acquisition attempt.
type AcquireLockResult struct {
LockedBy string
LeaseExpiresAt time.Time
}
// ReleaseLockResult contains the result of a lock release.
type ReleaseLockResult struct {
LockID int64
}
// UpdateLeaseResult contains the result of a lease update.
type UpdateLeaseResult struct {
LeaseExpiresAt time.Time
}

View file

@ -0,0 +1,28 @@
package table
import (
"log/slog"
"time"
)
// Config holds configuration for table locker.
type Config struct {
TableName string
LockID int64
LeaseDuration time.Duration
HeartbeatInterval time.Duration
LockTimeout ProbeConfig
UnlockTimeout ProbeConfig
// Optional logger for lock operations
Logger *slog.Logger
// Optional custom retry policy for database errors
RetryPolicy RetryPolicyFunc
}
// ProbeConfig holds retry configuration.
type ProbeConfig struct {
IntervalDuration time.Duration
FailureThreshold uint64
}

View file

@ -0,0 +1,237 @@
package table
import (
"cmp"
"context"
"crypto/rand"
"database/sql"
"encoding/hex"
"fmt"
"log/slog"
"os"
"sync"
"time"
"github.com/pressly/goose/v3/lock/internal/store"
"github.com/sethvargo/go-retry"
)
// RetryPolicyFunc inspects an error and returns whether the caller should retry the operation. This
// allows for database-specific error handling without hardcoding driver-specific logic.
type RetryPolicyFunc func(err error) bool
// Locker implements table-based locking for databases. This implementation is safe for concurrent
// use by multiple goroutines.
type Locker struct {
store store.LockStore
tableName string
lockID int64
instanceID string
leaseDuration time.Duration
heartbeatInterval time.Duration
retryLock retry.Backoff
retryUnlock retry.Backoff
logger *slog.Logger
retryPolicy RetryPolicyFunc
// Application-level coordination
mu sync.Mutex
// Heartbeat management
heartbeatCancel context.CancelFunc
heartbeatDone chan struct{}
}
// New creates a new table-based locker.
func New(lockStore store.LockStore, cfg Config) *Locker {
// Generate instance identifier
hostname, _ := os.Hostname()
hostname = cmp.Or(hostname, "unknown-hostname")
instanceID := fmt.Sprintf("%s-%d-%s", hostname, os.Getpid(), randomHex(4))
logger := cfg.Logger
if logger == nil {
logger = slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
}
return &Locker{
store: lockStore,
tableName: cfg.TableName,
lockID: cfg.LockID,
instanceID: instanceID,
leaseDuration: cfg.LeaseDuration,
heartbeatInterval: cfg.HeartbeatInterval,
logger: logger,
retryPolicy: cfg.RetryPolicy,
retryLock: retry.WithMaxRetries(
cfg.LockTimeout.FailureThreshold,
// Add +/- 25% jitter to reduce thundering herd
retry.WithJitterPercent(25, retry.NewConstant(cfg.LockTimeout.IntervalDuration)),
),
retryUnlock: retry.WithMaxRetries(
cfg.UnlockTimeout.FailureThreshold,
// Add +/- 25% jitter to reduce thundering herd
retry.WithJitterPercent(25, retry.NewConstant(cfg.UnlockTimeout.IntervalDuration)),
),
}
}
// Lock acquires the database lock. This method is safe for concurrent use - the mutex is held until
// Unlock() is called. Only one goroutine can hold the lock at a time across the entire lifecycle.
func (l *Locker) Lock(ctx context.Context, db *sql.DB) error {
l.mu.Lock()
// NOTE: mutex is NOT defer unlocked here, it remains held until Unlock() is called explicitly
// or a specific error occurs below!
// Ensure the lock table exists
if err := l.store.CreateLockTable(ctx, db); err != nil {
l.mu.Unlock()
return fmt.Errorf("ensure lock table exists: %w", err)
}
err := retry.Do(ctx, l.retryLock, func(ctx context.Context) error {
_, err := l.store.AcquireLock(ctx, db, l.lockID, l.instanceID, l.leaseDuration)
if err != nil {
// Clean up any stale locks before retrying
if _, cleanupErr := l.store.CleanupStaleLocks(ctx, db); cleanupErr != nil {
l.logger.WarnContext(ctx, "failed to cleanup stale locks",
slog.Int64("lock_table", l.lockID),
slog.Any("error", cleanupErr),
)
// Continue with retry, cleanup failure shouldn't block acquisition attempts
}
if l.shouldRetry(err) {
return retry.RetryableError(fmt.Errorf("acquire retryable lock: %w", err))
}
return fmt.Errorf("acquire lock: %w", err)
}
return nil
})
if err != nil {
l.mu.Unlock()
l.logger.WarnContext(ctx, "failed to acquire lock after retries",
slog.Int64("lock_id", l.lockID),
slog.String("instance_id", l.instanceID),
slog.Any("error", err),
)
return fmt.Errorf("acquire lock %d after retries: %w", l.lockID, err)
}
l.logger.DebugContext(ctx, "successfully acquired lock",
slog.Int64("lock_id", l.lockID),
slog.String("instance_id", l.instanceID),
slog.Duration("lease_duration", l.leaseDuration),
)
// Start heartbeat to maintain the lease
l.startHeartbeat(ctx, db)
// Mutex remains held - will be released in Unlock()
return nil
}
// Unlock releases the database lock. This method must be called exactly once after a successful
// Lock() call.
func (l *Locker) Unlock(ctx context.Context, db *sql.DB) error {
// NOTE: The mutex was acquired in Lock() and is still held
defer l.mu.Unlock()
// Use a context that can't be cancelled to ensure we always attempt to unlock even if the
// caller's context is cancelled. The call can control the retry behavior via the configured
// timeouts.
ctx = context.WithoutCancel(ctx)
// Stop heartbeat first
l.stopHeartbeat()
err := retry.Do(ctx, l.retryUnlock, func(ctx context.Context) error {
_, err := l.store.ReleaseLock(ctx, db, l.lockID, l.instanceID)
if err != nil {
if l.shouldRetry(err) {
return retry.RetryableError(fmt.Errorf("release retryable lock: %w", err))
}
return fmt.Errorf("release lock: %w", err)
}
return nil
})
if err != nil {
l.logger.WarnContext(ctx, "failed to release lock",
slog.Int64("lock_id", l.lockID),
slog.String("instance_id", l.instanceID),
slog.Any("error", err),
)
return err
}
l.logger.DebugContext(ctx, "successfully released lock",
slog.Int64("lock_id", l.lockID),
slog.String("instance_id", l.instanceID),
)
return nil
}
// startHeartbeat starts the heartbeat goroutine (called from within Lock with mutex held)
func (l *Locker) startHeartbeat(parentCtx context.Context, db *sql.DB) {
// If there's already a heartbeat running, stop it first
l.stopHeartbeat()
// Create a new context for the heartbeat
ctx, cancel := context.WithCancel(parentCtx)
l.heartbeatCancel = cancel
l.heartbeatDone = make(chan struct{})
go func() {
defer close(l.heartbeatDone)
ticker := time.NewTicker(l.heartbeatInterval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
result, err := l.store.UpdateLease(ctx, db, l.lockID, l.instanceID, l.leaseDuration)
if err != nil {
// TODO(mf): should we add a retry policy here?
l.logger.WarnContext(ctx, "heartbeat failed to update lease",
slog.Int64("lock_id", l.lockID),
slog.String("instance_id", l.instanceID),
slog.Any("error", err),
)
continue
}
l.logger.DebugContext(ctx, "heartbeat updated lease",
slog.Int64("lock_id", l.lockID),
slog.String("instance_id", l.instanceID),
slog.Time("lease_expires_at", result.LeaseExpiresAt),
)
}
}
}()
}
// stopHeartbeat stops the heartbeat goroutine (called from within Unlock with mutex held).
func (l *Locker) stopHeartbeat() {
if l.heartbeatCancel != nil {
l.heartbeatCancel()
<-l.heartbeatDone
l.heartbeatCancel = nil
l.heartbeatDone = nil
}
}
// shouldRetry determines whether an error is retryable based on the configured retry policy. If no
// retry policy is configured, it defaults to always retrying.
func (l *Locker) shouldRetry(err error) bool {
if l.retryPolicy != nil {
return l.retryPolicy(err)
}
return true
}
func randomHex(n int) string {
b := make([]byte, n)
if _, err := rand.Read(b); err != nil {
return fmt.Sprintf("%0*x", n*2, time.Now().UnixNano())
}
return hex.EncodeToString(b)
}

32
vendor/github.com/pressly/goose/v3/lock/locker.go generated vendored Normal file
View file

@ -0,0 +1,32 @@
// Package lock defines the Locker interface and implements the locking logic.
package lock
import (
"context"
"database/sql"
"errors"
)
var (
// ErrLockNotImplemented is returned when the database does not support locking.
ErrLockNotImplemented = errors.New("lock not implemented")
// ErrUnlockNotImplemented is returned when the database does not support unlocking.
ErrUnlockNotImplemented = errors.New("unlock not implemented")
)
// SessionLocker is the interface to lock and unlock the database for the duration of a session. The
// session is defined as the duration of a single connection and both methods must be called on the
// same connection.
type SessionLocker interface {
SessionLock(ctx context.Context, conn *sql.Conn) error
SessionUnlock(ctx context.Context, conn *sql.Conn) error
}
// Locker is the interface to lock and unlock the database.
//
// Unlike [SessionLocker], the Lock and Unlock methods are called on a [*sql.DB] and do not require
// the same connection to be used for both methods.
type Locker interface {
Lock(ctx context.Context, db *sql.DB) error
Unlock(ctx context.Context, db *sql.DB) error
}

165
vendor/github.com/pressly/goose/v3/lock/postgres.go generated vendored Normal file
View file

@ -0,0 +1,165 @@
package lock
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
"github.com/pressly/goose/v3/lock/internal/store"
"github.com/pressly/goose/v3/lock/internal/table"
"github.com/sethvargo/go-retry"
)
// NewPostgresTableLocker returns a Locker that uses PostgreSQL table-based locking. It manages a
// single lock row and keeps the lock alive automatically.
//
// Default behavior:
//
// - Lease (30s): How long the lock is valid if heartbeat stops
// - Heartbeat (5s): How often the lock gets refreshed to keep it alive
// - If the process dies, others can take the lock after lease expires
//
// Defaults:
//
// Table: "goose_lock"
// Lock ID: 4097083626 (crc64 of "goose")
// Lock retry: 5s intervals, 5min timeout
// Unlock retry: 2s intervals, 1min timeout
//
// Lock and Unlock both retry on failure. Lock stays alive automatically until released. All
// defaults can be overridden with options.
func NewPostgresTableLocker(options ...TableLockerOption) (Locker, error) {
config := table.Config{
TableName: DefaultLockTableName,
LockID: DefaultLockID,
LeaseDuration: 30 * time.Second,
HeartbeatInterval: 5 * time.Second,
LockTimeout: table.ProbeConfig{
IntervalDuration: 5 * time.Second,
FailureThreshold: 60, // 5 minutes total
},
UnlockTimeout: table.ProbeConfig{
IntervalDuration: 2 * time.Second,
FailureThreshold: 30, // 1 minute total
},
}
for _, opt := range options {
if err := opt.apply(&config); err != nil {
return nil, err
}
}
lockStore, err := store.NewPostgres(config.TableName)
if err != nil {
return nil, fmt.Errorf("create lock store: %w", err)
}
return table.New(lockStore, config), nil
}
// NewPostgresSessionLocker returns a SessionLocker that utilizes PostgreSQL's exclusive
// session-level advisory lock mechanism.
//
// This function creates a SessionLocker that can be used to acquire and release a lock for
// synchronization purposes. The lock acquisition is retried until it is successfully acquired or
// until the failure threshold is reached. The default lock duration is set to 5 minutes, and the
// default unlock duration is set to 1 minute.
//
// If you have long running migrations, you may want to increase the lock duration.
//
// See [SessionLockerOption] for options that can be used to configure the SessionLocker.
func NewPostgresSessionLocker(opts ...SessionLockerOption) (SessionLocker, error) {
cfg := sessionLockerConfig{
lockID: DefaultLockID,
lockProbe: probe{
intervalDuration: 5 * time.Second,
failureThreshold: 60,
},
unlockProbe: probe{
intervalDuration: 2 * time.Second,
failureThreshold: 30,
},
}
for _, opt := range opts {
if err := opt.apply(&cfg); err != nil {
return nil, err
}
}
return &postgresSessionLocker{
lockID: cfg.lockID,
retryLock: retry.WithMaxRetries(
cfg.lockProbe.failureThreshold,
retry.NewConstant(cfg.lockProbe.intervalDuration),
),
retryUnlock: retry.WithMaxRetries(
cfg.unlockProbe.failureThreshold,
retry.NewConstant(cfg.unlockProbe.intervalDuration),
),
}, nil
}
type postgresSessionLocker struct {
lockID int64
retryLock retry.Backoff
retryUnlock retry.Backoff
}
var _ SessionLocker = (*postgresSessionLocker)(nil)
func (l *postgresSessionLocker) SessionLock(ctx context.Context, conn *sql.Conn) error {
return retry.Do(ctx, l.retryLock, func(ctx context.Context) error {
row := conn.QueryRowContext(ctx, "SELECT pg_try_advisory_lock($1)", l.lockID)
var locked bool
if err := row.Scan(&locked); err != nil {
return fmt.Errorf("failed to execute pg_try_advisory_lock: %w", err)
}
if locked {
// A session-level advisory lock was acquired.
return nil
}
// A session-level advisory lock could not be acquired. This is likely because another
// process has already acquired the lock. We will continue retrying until the lock is
// acquired or the maximum number of retries is reached.
return retry.RetryableError(errors.New("failed to acquire lock"))
})
}
func (l *postgresSessionLocker) SessionUnlock(ctx context.Context, conn *sql.Conn) error {
return retry.Do(ctx, l.retryUnlock, func(ctx context.Context) error {
var unlocked bool
row := conn.QueryRowContext(ctx, "SELECT pg_advisory_unlock($1)", l.lockID)
if err := row.Scan(&unlocked); err != nil {
return fmt.Errorf("failed to execute pg_advisory_unlock: %w", err)
}
if unlocked {
// A session-level advisory lock was released.
return nil
}
/*
docs(md): provide users with some documentation on how they can unlock the session
manually.
This is probably not an issue for 99.99% of users since pg_advisory_unlock_all() will
release all session level advisory locks held by the current session. It is implicitly
invoked at session end, even if the client disconnects ungracefully.
Here is output from a session that has a lock held:
SELECT pid, granted, ((classid::bigint << 32) | objid::bigint) AS goose_lock_id FROM
pg_locks WHERE locktype = 'advisory';
| pid | granted | goose_lock_id |
|-----|---------|---------------------|
| 191 | t | 4097083626 |
A forceful way to unlock the session is to terminate the backend with SIGTERM:
SELECT pg_terminate_backend(191);
Subsequent commands on the same connection will fail with:
Query 1 ERROR: FATAL: terminating connection due to administrator command
*/
return retry.RetryableError(errors.New("failed to unlock session"))
})
}

View file

@ -0,0 +1,98 @@
package lock
import (
"errors"
"time"
)
const (
// DefaultLockID is the id used to lock the database for migrations. It is a crc64 hash of the
// string "goose". This is used to ensure that the lock is unique to goose.
//
// crc32.Checksum([]byte("goose"), crc32.MakeTable(crc32.IEEE))
DefaultLockID int64 = 4097083626
)
// SessionLockerOption is used to configure a SessionLocker.
type SessionLockerOption interface {
apply(*sessionLockerConfig) error
}
// WithLockID sets the lock ID to use when locking the database.
//
// If WithLockID is not called, the DefaultLockID is used.
func WithLockID(lockID int64) SessionLockerOption {
return sessionLockerConfigFunc(func(c *sessionLockerConfig) error {
c.lockID = lockID
return nil
})
}
// WithLockTimeout sets the max duration to wait for the lock to be acquired. The total duration
// will be the period times the failure threshold.
//
// By default, the lock timeout is 300s (5min), where the lock is retried every 5 seconds (period)
// up to 60 times (failure threshold).
//
// The minimum period is 1 second, and the minimum failure threshold is 1.
func WithLockTimeout(period, failureThreshold uint64) SessionLockerOption {
return sessionLockerConfigFunc(func(c *sessionLockerConfig) error {
if period < 1 {
return errors.New("period must be greater than 0, minimum is 1")
}
if failureThreshold < 1 {
return errors.New("failure threshold must be greater than 0, minimum is 1")
}
c.lockProbe = probe{
intervalDuration: time.Duration(period) * time.Second,
failureThreshold: failureThreshold,
}
return nil
})
}
// WithUnlockTimeout sets the max duration to wait for the lock to be released. The total duration
// will be the period times the failure threshold.
//
// By default, the lock timeout is 60s, where the lock is retried every 2 seconds (period) up to 30
// times (failure threshold).
//
// The minimum period is 1 second, and the minimum failure threshold is 1.
func WithUnlockTimeout(period, failureThreshold uint64) SessionLockerOption {
return sessionLockerConfigFunc(func(c *sessionLockerConfig) error {
if period < 1 {
return errors.New("period must be greater than 0, minimum is 1")
}
if failureThreshold < 1 {
return errors.New("failure threshold must be greater than 0, minimum is 1")
}
c.unlockProbe = probe{
intervalDuration: time.Duration(period) * time.Second,
failureThreshold: failureThreshold,
}
return nil
})
}
type sessionLockerConfig struct {
lockID int64
lockProbe probe
unlockProbe probe
}
// probe is used to configure how often and how many times to retry a lock or unlock operation. The
// total timeout will be the period times the failure threshold.
type probe struct {
// How often (in seconds) to perform the probe.
intervalDuration time.Duration
// Number of times to retry the probe.
failureThreshold uint64
}
var _ SessionLockerOption = (sessionLockerConfigFunc)(nil)
type sessionLockerConfigFunc func(*sessionLockerConfig) error
func (f sessionLockerConfigFunc) apply(cfg *sessionLockerConfig) error {
return f(cfg)
}

View file

@ -0,0 +1,136 @@
package lock
import (
"errors"
"fmt"
"log/slog"
"time"
"github.com/pressly/goose/v3/lock/internal/table"
)
const (
// DefaultLockTableName is the default name of the lock table.
DefaultLockTableName = "goose_lock"
)
// TableLockerOption is used to configure a table-based locker.
type TableLockerOption interface {
apply(*table.Config) error
}
// WithTableName sets the name of the lock table.
func WithTableName(tableName string) TableLockerOption {
return tableLockerConfigFunc(func(c *table.Config) error {
if tableName == "" {
return errors.New("lock table name must not be empty")
}
c.TableName = tableName
return nil
})
}
// WithTableLockID sets the lock ID to use for this locker instance. Different lock IDs allow for
// multiple independent locks in the same table.
func WithTableLockID(lockID int64) TableLockerOption {
return tableLockerConfigFunc(func(c *table.Config) error {
if lockID <= 0 {
return fmt.Errorf("lock ID must be greater than zero: %d", lockID)
}
c.LockID = lockID
return nil
})
}
// WithTableLeaseDuration sets how long a lock lease lasts. The lock will expire after this duration
// if not renewed by heartbeat.
func WithTableLeaseDuration(duration time.Duration) TableLockerOption {
return tableLockerConfigFunc(func(c *table.Config) error {
if duration <= 0 {
return errors.New("lease duration must be positive")
}
c.LeaseDuration = duration
return nil
})
}
// WithTableHeartbeatInterval sets how often to send heartbeat updates to renew the lease. This
// should be significantly smaller than the lease duration.
func WithTableHeartbeatInterval(interval time.Duration) TableLockerOption {
return tableLockerConfigFunc(func(c *table.Config) error {
if interval <= 0 {
return errors.New("heartbeat interval must be positive")
}
c.HeartbeatInterval = interval
return nil
})
}
// WithTableLockTimeout configures how long to retry acquiring a lock and how often to retry.
func WithTableLockTimeout(intervalDuration time.Duration, failureThreshold uint64) TableLockerOption {
return tableLockerConfigFunc(func(c *table.Config) error {
if intervalDuration <= 0 {
return errors.New("lock timeout interval duration must be positive")
}
if failureThreshold == 0 {
return errors.New("lock timeout failure threshold must be positive")
}
c.LockTimeout = table.ProbeConfig{
IntervalDuration: intervalDuration,
FailureThreshold: failureThreshold,
}
return nil
})
}
// WithTableUnlockTimeout configures how long to retry releasing a lock and how often to retry.
func WithTableUnlockTimeout(intervalDuration time.Duration, failureThreshold uint64) TableLockerOption {
return tableLockerConfigFunc(func(c *table.Config) error {
if intervalDuration <= 0 {
return errors.New("unlock timeout interval duration must be positive")
}
if failureThreshold == 0 {
return errors.New("unlock timeout failure threshold must be positive")
}
c.UnlockTimeout = table.ProbeConfig{
IntervalDuration: intervalDuration,
FailureThreshold: failureThreshold,
}
return nil
})
}
// WithTableLogger sets an optional logger for lock operations. If not provided, lock operations
// will use a default logger that only logs errors to stderr.
func WithTableLogger(logger *slog.Logger) TableLockerOption {
return tableLockerConfigFunc(func(c *table.Config) error {
c.Logger = logger
return nil
})
}
// WithTableRetryPolicy sets an optional callback to classify database errors during table lock
// operations.
//
// The provided function is invoked whenever a database operation fails. This includes Lock(),
// Unlock(), and heartbeat/lease update operations.
//
// If the function returns true, the operation is retried according to the configured retry/backoff
// policy.
//
// If it returns false, the operation fails immediately, bypassing any retries.
//
// This allows clients to implement custom logic for transient errors, driver-specific errors, or
// application-specific failure handling.
func WithTableRetryPolicy(retryPolicy func(error) bool) TableLockerOption {
return tableLockerConfigFunc(func(c *table.Config) error {
c.RetryPolicy = retryPolicy
return nil
})
}
type tableLockerConfigFunc func(*table.Config) error
func (f tableLockerConfigFunc) apply(cfg *table.Config) error {
return f(cfg)
}

36
vendor/github.com/pressly/goose/v3/log.go generated vendored Normal file
View file

@ -0,0 +1,36 @@
package goose
import (
std "log"
)
var log Logger = &stdLogger{}
// Logger is standard logger interface
type Logger interface {
Fatalf(format string, v ...interface{})
Printf(format string, v ...interface{})
}
// SetLogger sets the logger for package output
func SetLogger(l Logger) {
log = l
}
// stdLogger is a default logger that outputs to a stdlib's log.std logger.
type stdLogger struct{}
func (*stdLogger) Fatalf(format string, v ...interface{}) { std.Fatalf(format, v...) }
func (*stdLogger) Printf(format string, v ...interface{}) { std.Printf(format, v...) }
// NopLogger returns a logger that discards all logged output.
func NopLogger() Logger {
return &nopLogger{}
}
type nopLogger struct{}
var _ Logger = (*nopLogger)(nil)
func (*nopLogger) Fatalf(format string, v ...interface{}) {}
func (*nopLogger) Printf(format string, v ...interface{}) {}

379
vendor/github.com/pressly/goose/v3/migrate.go generated vendored Normal file
View file

@ -0,0 +1,379 @@
package goose
import (
"context"
"database/sql"
"errors"
"fmt"
"io/fs"
"math"
"path"
"sort"
"strings"
"time"
"go.uber.org/multierr"
)
var (
// ErrNoMigrationFiles when no migration files have been found.
ErrNoMigrationFiles = errors.New("no migration files found")
// ErrNoCurrentVersion when a current migration version is not found.
ErrNoCurrentVersion = errors.New("no current version found")
// ErrNoNextVersion when the next migration version is not found.
ErrNoNextVersion = errors.New("no next version found")
// MaxVersion is the maximum allowed version.
MaxVersion int64 = math.MaxInt64
)
// Migrations slice.
type Migrations []*Migration
// helpers so we can use pkg sort
func (ms Migrations) Len() int { return len(ms) }
func (ms Migrations) Swap(i, j int) { ms[i], ms[j] = ms[j], ms[i] }
func (ms Migrations) Less(i, j int) bool {
if ms[i].Version == ms[j].Version {
panic(fmt.Sprintf("goose: duplicate version %v detected:\n%v\n%v", ms[i].Version, ms[i].Source, ms[j].Source))
}
return ms[i].Version < ms[j].Version
}
// Current gets the current migration.
func (ms Migrations) Current(current int64) (*Migration, error) {
for i, migration := range ms {
if migration.Version == current {
return ms[i], nil
}
}
return nil, ErrNoCurrentVersion
}
// Next gets the next migration.
func (ms Migrations) Next(current int64) (*Migration, error) {
for i, migration := range ms {
if migration.Version > current {
return ms[i], nil
}
}
return nil, ErrNoNextVersion
}
// Previous : Get the previous migration.
func (ms Migrations) Previous(current int64) (*Migration, error) {
for i := len(ms) - 1; i >= 0; i-- {
if ms[i].Version < current {
return ms[i], nil
}
}
return nil, ErrNoNextVersion
}
// Last gets the last migration.
func (ms Migrations) Last() (*Migration, error) {
if len(ms) == 0 {
return nil, ErrNoNextVersion
}
return ms[len(ms)-1], nil
}
// Versioned gets versioned migrations.
func (ms Migrations) versioned() (Migrations, error) {
var migrations Migrations
// assume that the user will never have more than 19700101000000 migrations
for _, m := range ms {
// parse version as timestamp
versionTime, err := time.Parse(timestampFormat, fmt.Sprintf("%d", m.Version))
if versionTime.Before(time.Unix(0, 0)) || err != nil {
migrations = append(migrations, m)
}
}
return migrations, nil
}
// Timestamped gets the timestamped migrations.
func (ms Migrations) timestamped() (Migrations, error) {
var migrations Migrations
// assume that the user will never have more than 19700101000000 migrations
for _, m := range ms {
// parse version as timestamp
versionTime, err := time.Parse(timestampFormat, fmt.Sprintf("%d", m.Version))
if err != nil {
// probably not a timestamp
continue
}
if versionTime.After(time.Unix(0, 0)) {
migrations = append(migrations, m)
}
}
return migrations, nil
}
func (ms Migrations) String() string {
str := ""
for _, m := range ms {
str += fmt.Sprintln(m)
}
return str
}
func collectMigrationsFS(
fsys fs.FS,
dirpath string,
current, target int64,
registered map[int64]*Migration,
) (Migrations, error) {
if _, err := fs.Stat(fsys, dirpath); err != nil {
if errors.Is(err, fs.ErrNotExist) {
return nil, fmt.Errorf("%s directory does not exist", dirpath)
}
return nil, err
}
var migrations Migrations
// SQL migration files.
sqlMigrationFiles, err := fs.Glob(fsys, path.Join(dirpath, "*.sql"))
if err != nil {
return nil, err
}
for _, file := range sqlMigrationFiles {
v, err := NumericComponent(file)
if err != nil {
return nil, fmt.Errorf("could not parse SQL migration file %q: %w", file, err)
}
if versionFilter(v, current, target) {
migrations = append(migrations, &Migration{
Version: v,
Next: -1,
Previous: -1,
Source: file,
})
}
}
// Go migration files.
goMigrations, err := collectGoMigrations(fsys, dirpath, registered, current, target)
if err != nil {
return nil, err
}
migrations = append(migrations, goMigrations...)
if len(migrations) == 0 {
return nil, ErrNoMigrationFiles
}
return sortAndConnectMigrations(migrations), nil
}
// CollectMigrations returns all the valid looking migration scripts in the
// migrations folder and go func registry, and key them by version.
func CollectMigrations(dirpath string, current, target int64) (Migrations, error) {
return collectMigrationsFS(baseFS, dirpath, current, target, registeredGoMigrations)
}
func sortAndConnectMigrations(migrations Migrations) Migrations {
sort.Sort(migrations)
// now that we're sorted in the appropriate direction,
// populate next and previous for each migration
for i, m := range migrations {
prev := int64(-1)
if i > 0 {
prev = migrations[i-1].Version
migrations[i-1].Next = m.Version
}
migrations[i].Previous = prev
}
return migrations
}
func versionFilter(v, current, target int64) bool {
if target > current {
return v > current && v <= target
}
if target < current {
return v <= current && v > target
}
return false
}
// EnsureDBVersion retrieves the current version for this DB.
// Create and initialize the DB version table if it doesn't exist.
func EnsureDBVersion(db *sql.DB) (int64, error) {
ctx := context.Background()
return EnsureDBVersionContext(ctx, db)
}
// EnsureDBVersionContext retrieves the current version for this DB.
// Create and initialize the DB version table if it doesn't exist.
func EnsureDBVersionContext(ctx context.Context, db *sql.DB) (int64, error) {
dbMigrations, err := store.ListMigrations(ctx, db, TableName())
if err != nil {
createErr := createVersionTable(ctx, db)
if createErr != nil {
return 0, multierr.Append(err, createErr)
}
return 0, nil
}
// The most recent record for each migration specifies
// whether it has been applied or rolled back.
// The first version we find that has been applied is the current version.
//
// TODO(mf): for historic reasons, we continue to use the is_applied column,
// but at some point we need to deprecate this logic and ideally remove
// this column.
//
// For context, see:
// https://github.com/pressly/goose/pull/131#pullrequestreview-178409168
//
// The dbMigrations list is expected to be ordered by descending ID. But
// in the future we should be able to query the last record only.
skipLookup := make(map[int64]struct{})
for _, m := range dbMigrations {
// Have we already marked this version to be skipped?
if _, ok := skipLookup[m.VersionID]; ok {
continue
}
// If version has been applied we are done.
if m.IsApplied {
return m.VersionID, nil
}
// Latest version of migration has not been applied.
skipLookup[m.VersionID] = struct{}{}
}
return 0, ErrNoNextVersion
}
// createVersionTable creates the db version table and inserts the
// initial 0 value into it.
func createVersionTable(ctx context.Context, db *sql.DB) error {
txn, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
if err := store.CreateVersionTable(ctx, txn, TableName()); err != nil {
_ = txn.Rollback()
return err
}
if err := store.InsertVersion(ctx, txn, TableName(), 0); err != nil {
_ = txn.Rollback()
return err
}
return txn.Commit()
}
// GetDBVersion is an alias for EnsureDBVersion, but returns -1 in error.
func GetDBVersion(db *sql.DB) (int64, error) {
ctx := context.Background()
return GetDBVersionContext(ctx, db)
}
// GetDBVersionContext is an alias for EnsureDBVersion, but returns -1 in error.
func GetDBVersionContext(ctx context.Context, db *sql.DB) (int64, error) {
version, err := EnsureDBVersionContext(ctx, db)
if err != nil {
return -1, err
}
return version, nil
}
// collectGoMigrations collects Go migrations from the filesystem and merges them with registered
// migrations.
//
// If Go migrations have been registered globally, with [goose.AddNamedMigration...], but there are
// no corresponding .go files in the filesystem, add them to the migrations slice.
//
// If Go migrations have been registered, and there are .go files in the filesystem dirpath, ONLY
// include those in the migrations slices.
//
// Lastly, if there are .go files in the filesystem but they have not been registered, raise an
// error. This is to prevent users from accidentally adding valid looking Go files to the migrations
// folder without registering them.
func collectGoMigrations(
fsys fs.FS,
dirpath string,
registeredGoMigrations map[int64]*Migration,
current, target int64,
) (Migrations, error) {
// Sanity check registered migrations have the correct version prefix.
for _, m := range registeredGoMigrations {
if _, err := NumericComponent(m.Source); err != nil {
return nil, fmt.Errorf("could not parse go migration file %s: %w", m.Source, err)
}
}
goFiles, err := fs.Glob(fsys, path.Join(dirpath, "*.go"))
if err != nil {
return nil, err
}
// If there are no Go files in the filesystem and no registered Go migrations, return early.
if len(goFiles) == 0 && len(registeredGoMigrations) == 0 {
return nil, nil
}
type source struct {
fullpath string
version int64
}
// Find all Go files that have a version prefix and are within the requested range.
var sources []source
for _, fullpath := range goFiles {
v, err := NumericComponent(fullpath)
if err != nil {
continue // Skip any files that don't have version prefix.
}
if strings.HasSuffix(fullpath, "_test.go") {
continue // Skip Go test files.
}
if versionFilter(v, current, target) {
sources = append(sources, source{
fullpath: fullpath,
version: v,
})
}
}
var (
migrations Migrations
)
if len(sources) > 0 {
for _, s := range sources {
migration, ok := registeredGoMigrations[s.version]
if ok {
migrations = append(migrations, migration)
} else {
// TODO(mf): something that bothers me about this implementation is it will be
// lazily evaluated and the error will only be raised if the user tries to run the
// migration. It would be better to raise an error much earlier in the process.
migrations = append(migrations, &Migration{
Version: s.version,
Next: -1,
Previous: -1,
Source: s.fullpath,
Registered: false,
})
}
}
} else {
// Some users may register Go migrations manually via AddNamedMigration_ functions but not
// provide the corresponding .go files in the filesystem. In this case, we include them
// wholesale in the migrations slice.
//
// This is a valid use case because users may want to build a custom binary that only embeds
// the SQL migration files and some other mechanism for registering Go migrations.
for _, migration := range registeredGoMigrations {
v, err := NumericComponent(migration.Source)
if err != nil {
return nil, fmt.Errorf("could not parse go migration file %s: %w", migration.Source, err)
}
if versionFilter(v, current, target) {
migrations = append(migrations, migration)
}
}
}
return migrations, nil
}

398
vendor/github.com/pressly/goose/v3/migration.go generated vendored Normal file
View file

@ -0,0 +1,398 @@
package goose
import (
"context"
"database/sql"
"errors"
"fmt"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/pressly/goose/v3/internal/sqlparser"
)
// NewGoMigration creates a new Go migration.
//
// Both up and down functions may be nil, in which case the migration will be recorded in the
// versions table but no functions will be run. This is useful for recording (up) or deleting (down)
// a version without running any functions. See [GoFunc] for more details.
func NewGoMigration(version int64, up, down *GoFunc) *Migration {
m := &Migration{
Type: TypeGo,
Registered: true,
Version: version,
Next: -1, Previous: -1,
goUp: &GoFunc{Mode: TransactionEnabled},
goDown: &GoFunc{Mode: TransactionEnabled},
construct: true,
}
updateMode := func(f *GoFunc) *GoFunc {
// infer mode from function
if f.Mode == 0 {
if f.RunTx != nil && f.RunDB == nil {
f.Mode = TransactionEnabled
}
if f.RunTx == nil && f.RunDB != nil {
f.Mode = TransactionDisabled
}
// Always default to TransactionEnabled if both functions are nil. This is the most
// common use case.
if f.RunDB == nil && f.RunTx == nil {
f.Mode = TransactionEnabled
}
}
return f
}
// To maintain backwards compatibility, we set ALL legacy functions. In a future major version,
// we will remove these fields in favor of [GoFunc].
//
// Note, this function does not do any validation. Validation is lazily done when the migration
// is registered.
if up != nil {
m.goUp = updateMode(up)
if up.RunDB != nil {
m.UpFnNoTxContext = up.RunDB // func(context.Context, *sql.DB) error
m.UpFnNoTx = withoutContext(up.RunDB) // func(*sql.DB) error
}
if up.RunTx != nil {
m.UseTx = true
m.UpFnContext = up.RunTx // func(context.Context, *sql.Tx) error
m.UpFn = withoutContext(up.RunTx) // func(*sql.Tx) error
}
}
if down != nil {
m.goDown = updateMode(down)
if down.RunDB != nil {
m.DownFnNoTxContext = down.RunDB // func(context.Context, *sql.DB) error
m.DownFnNoTx = withoutContext(down.RunDB) // func(*sql.DB) error
}
if down.RunTx != nil {
m.UseTx = true
m.DownFnContext = down.RunTx // func(context.Context, *sql.Tx) error
m.DownFn = withoutContext(down.RunTx) // func(*sql.Tx) error
}
}
return m
}
// Migration struct represents either a SQL or Go migration.
//
// Avoid constructing migrations manually, use [NewGoMigration] function.
type Migration struct {
Type MigrationType
Version int64
// Source is the path to the .sql script or .go file. It may be empty for Go migrations that
// have been registered globally and don't have a source file.
Source string
UpFnContext, DownFnContext GoMigrationContext
UpFnNoTxContext, DownFnNoTxContext GoMigrationNoTxContext
// These fields will be removed in a future major version. They are here for backwards
// compatibility and are an implementation detail.
Registered bool
UseTx bool
Next int64 // next version, or -1 if none
Previous int64 // previous version, -1 if none
// We still save the non-context versions in the struct in case someone is using them. Goose
// does not use these internally anymore in favor of the context-aware versions. These fields
// will be removed in a future major version.
UpFn GoMigration // Deprecated: use UpFnContext instead.
DownFn GoMigration // Deprecated: use DownFnContext instead.
UpFnNoTx GoMigrationNoTx // Deprecated: use UpFnNoTxContext instead.
DownFnNoTx GoMigrationNoTx // Deprecated: use DownFnNoTxContext instead.
noVersioning bool
// These fields are used internally by goose and users are not expected to set them. Instead,
// use [NewGoMigration] to create a new go migration.
construct bool
goUp, goDown *GoFunc
sql sqlMigration
}
type sqlMigration struct {
// The Parsed field is used to track whether the SQL migration has been parsed. It serves as an
// optimization to avoid parsing migrations that may never be needed. Typically, migrations are
// incremental, and users often run only the most recent ones, making parsing of prior
// migrations unnecessary in most cases.
Parsed bool
// Parsed must be set to true before the following fields are used.
UseTx bool
Up []string
Down []string
}
// GoFunc represents a Go migration function.
type GoFunc struct {
// Exactly one of these must be set, or both must be nil.
RunTx func(ctx context.Context, tx *sql.Tx) error
// -- OR --
RunDB func(ctx context.Context, db *sql.DB) error
// Mode is the transaction mode for the migration. When one of the run functions is set, the
// mode will be inferred from the function and the field is ignored. Users do not need to set
// this field when supplying a run function.
//
// If both run functions are nil, the mode defaults to TransactionEnabled. The use case for nil
// functions is to record a version in the version table without invoking a Go migration
// function.
//
// The only time this field is required is if BOTH run functions are nil AND you want to
// override the default transaction mode.
Mode TransactionMode
}
// TransactionMode represents the possible transaction modes for a migration.
type TransactionMode int
const (
TransactionEnabled TransactionMode = iota + 1
TransactionDisabled
)
func (m TransactionMode) String() string {
switch m {
case TransactionEnabled:
return "transaction_enabled"
case TransactionDisabled:
return "transaction_disabled"
default:
return fmt.Sprintf("unknown transaction mode (%d)", m)
}
}
// MigrationRecord struct.
//
// Deprecated: unused and will be removed in a future major version.
type MigrationRecord struct {
VersionID int64
TStamp time.Time
IsApplied bool // was this a result of up() or down()
}
func (m *Migration) String() string {
return fmt.Sprint(m.Source)
}
// Up runs an up migration.
func (m *Migration) Up(db *sql.DB) error {
ctx := context.Background()
return m.UpContext(ctx, db)
}
// UpContext runs an up migration.
func (m *Migration) UpContext(ctx context.Context, db *sql.DB) error {
if err := m.run(ctx, db, true); err != nil {
return err
}
return nil
}
// Down runs a down migration.
func (m *Migration) Down(db *sql.DB) error {
ctx := context.Background()
return m.DownContext(ctx, db)
}
// DownContext runs a down migration.
func (m *Migration) DownContext(ctx context.Context, db *sql.DB) error {
if err := m.run(ctx, db, false); err != nil {
return err
}
return nil
}
func (m *Migration) run(ctx context.Context, db *sql.DB, direction bool) error {
switch filepath.Ext(m.Source) {
case ".sql":
f, err := baseFS.Open(m.Source)
if err != nil {
return fmt.Errorf("ERROR %v: failed to open SQL migration file: %w", filepath.Base(m.Source), err)
}
defer f.Close()
statements, useTx, err := sqlparser.ParseSQLMigration(f, sqlparser.FromBool(direction), verbose)
if err != nil {
return fmt.Errorf("ERROR %v: failed to parse SQL migration file: %w", filepath.Base(m.Source), err)
}
start := time.Now()
if err := runSQLMigration(ctx, db, statements, useTx, m.Version, direction, m.noVersioning); err != nil {
return fmt.Errorf("ERROR %v: failed to run SQL migration: %w", filepath.Base(m.Source), err)
}
finish := truncateDuration(time.Since(start))
if len(statements) > 0 {
log.Printf("OK %s (%s)", filepath.Base(m.Source), finish)
} else {
log.Printf("EMPTY %s (%s)", filepath.Base(m.Source), finish)
}
case ".go":
if !m.Registered {
return fmt.Errorf("ERROR %v: failed to run Go migration: Go functions must be registered and built into a custom binary (see https://github.com/pressly/goose/tree/master/examples/go-migrations)", m.Source)
}
start := time.Now()
var empty bool
if m.UseTx {
// Run go-based migration inside a tx.
fn := m.DownFnContext
if direction {
fn = m.UpFnContext
}
empty = (fn == nil)
if err := runGoMigration(
ctx,
db,
fn,
m.Version,
direction,
!m.noVersioning,
); err != nil {
return fmt.Errorf("ERROR go migration: %q: %w", filepath.Base(m.Source), err)
}
} else {
// Run go-based migration outside a tx.
fn := m.DownFnNoTxContext
if direction {
fn = m.UpFnNoTxContext
}
empty = (fn == nil)
if err := runGoMigrationNoTx(
ctx,
db,
fn,
m.Version,
direction,
!m.noVersioning,
); err != nil {
return fmt.Errorf("ERROR go migration no tx: %q: %w", filepath.Base(m.Source), err)
}
}
finish := truncateDuration(time.Since(start))
if !empty {
log.Printf("OK %s (%s)", filepath.Base(m.Source), finish)
} else {
log.Printf("EMPTY %s (%s)", filepath.Base(m.Source), finish)
}
}
return nil
}
func runGoMigrationNoTx(
ctx context.Context,
db *sql.DB,
fn GoMigrationNoTxContext,
version int64,
direction bool,
recordVersion bool,
) error {
if fn != nil {
// Run go migration function.
if err := fn(ctx, db); err != nil {
return fmt.Errorf("failed to run go migration: %w", err)
}
}
if recordVersion {
return insertOrDeleteVersionNoTx(ctx, db, version, direction)
}
return nil
}
func runGoMigration(
ctx context.Context,
db *sql.DB,
fn GoMigrationContext,
version int64,
direction bool,
recordVersion bool,
) error {
if fn == nil && !recordVersion {
return nil
}
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
if fn != nil {
// Run go migration function.
if err := fn(ctx, tx); err != nil {
_ = tx.Rollback()
return fmt.Errorf("failed to run go migration: %w", err)
}
}
if recordVersion {
if err := insertOrDeleteVersion(ctx, tx, version, direction); err != nil {
_ = tx.Rollback()
return fmt.Errorf("failed to update version: %w", err)
}
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("failed to commit transaction: %w", err)
}
return nil
}
func insertOrDeleteVersion(ctx context.Context, tx *sql.Tx, version int64, direction bool) error {
if direction {
return store.InsertVersion(ctx, tx, TableName(), version)
}
return store.DeleteVersion(ctx, tx, TableName(), version)
}
func insertOrDeleteVersionNoTx(ctx context.Context, db *sql.DB, version int64, direction bool) error {
if direction {
return store.InsertVersionNoTx(ctx, db, TableName(), version)
}
return store.DeleteVersionNoTx(ctx, db, TableName(), version)
}
// NumericComponent parses the version from the migration file name.
//
// XXX_descriptivename.ext where XXX specifies the version number and ext specifies the type of
// migration, either .sql or .go.
func NumericComponent(filename string) (int64, error) {
base := filepath.Base(filename)
if ext := filepath.Ext(base); ext != ".go" && ext != ".sql" {
return 0, errors.New("migration file does not have .sql or .go file extension")
}
idx := strings.Index(base, "_")
if idx < 0 {
return 0, errors.New("no filename separator '_' found")
}
n, err := strconv.ParseInt(base[:idx], 10, 64)
if err != nil {
return 0, fmt.Errorf("failed to parse version from migration file: %s: %w", base, err)
}
if n < 1 {
return 0, errors.New("migration version must be greater than zero")
}
return n, nil
}
func truncateDuration(d time.Duration) time.Duration {
for _, v := range []time.Duration{
time.Second,
time.Millisecond,
time.Microsecond,
} {
if d > v {
return d.Round(v / time.Duration(100))
}
}
return d
}
// ref returns a string that identifies the migration. This is used for logging and error messages.
func (m *Migration) ref() string {
return fmt.Sprintf("(type:%s,version:%d)", m.Type, m.Version)
}

115
vendor/github.com/pressly/goose/v3/migration_sql.go generated vendored Normal file
View file

@ -0,0 +1,115 @@
package goose
import (
"context"
"database/sql"
"fmt"
"regexp"
)
// Run a migration specified in raw SQL.
//
// Sections of the script can be annotated with a special comment,
// starting with "-- +goose" to specify whether the section should
// be applied during an Up or Down migration
//
// All statements following an Up or Down annotation are grouped together
// until another direction annotation is found.
func runSQLMigration(
ctx context.Context,
db *sql.DB,
statements []string,
useTx bool,
v int64,
direction bool,
noVersioning bool,
) error {
if useTx {
// TRANSACTION.
verboseInfo("Begin transaction")
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
for _, query := range statements {
verboseInfo("Executing statement: %s\n", clearStatement(query))
if _, err := tx.ExecContext(ctx, query); err != nil {
verboseInfo("Rollback transaction")
_ = tx.Rollback()
return fmt.Errorf("failed to execute SQL query %q: %w", clearStatement(query), err)
}
}
if !noVersioning {
if direction {
if err := store.InsertVersion(ctx, tx, TableName(), v); err != nil {
verboseInfo("Rollback transaction")
_ = tx.Rollback()
return fmt.Errorf("failed to insert new goose version: %w", err)
}
} else {
if err := store.DeleteVersion(ctx, tx, TableName(), v); err != nil {
verboseInfo("Rollback transaction")
_ = tx.Rollback()
return fmt.Errorf("failed to delete goose version: %w", err)
}
}
}
verboseInfo("Commit transaction")
if err := tx.Commit(); err != nil {
return fmt.Errorf("failed to commit transaction: %w", err)
}
return nil
}
// NO TRANSACTION.
for _, query := range statements {
verboseInfo("Executing statement: %s", clearStatement(query))
if _, err := db.ExecContext(ctx, query); err != nil {
return fmt.Errorf("failed to execute SQL query %q: %w", clearStatement(query), err)
}
}
if !noVersioning {
if direction {
if err := store.InsertVersionNoTx(ctx, db, TableName(), v); err != nil {
return fmt.Errorf("failed to insert new goose version: %w", err)
}
} else {
if err := store.DeleteVersionNoTx(ctx, db, TableName(), v); err != nil {
return fmt.Errorf("failed to delete goose version: %w", err)
}
}
}
return nil
}
const (
grayColor = "\033[90m"
resetColor = "\033[00m"
)
func verboseInfo(s string, args ...interface{}) {
if verbose {
if noColor {
log.Printf(s, args...)
} else {
log.Printf(grayColor+s+resetColor, args...)
}
}
}
var (
matchSQLComments = regexp.MustCompile(`(?m)^--.*$[\r\n]*`)
matchEmptyEOL = regexp.MustCompile(`(?m)^$[\r\n]*`) // TODO: Duplicate
)
func clearStatement(s string) string {
s = matchSQLComments.ReplaceAllString(s, ``)
return matchEmptyEOL.ReplaceAllString(s, ``)
}

28
vendor/github.com/pressly/goose/v3/osfs.go generated vendored Normal file
View file

@ -0,0 +1,28 @@
package goose
import (
"io/fs"
"os"
"path/filepath"
)
// osFS wraps functions working with os filesystem to implement fs.FS interfaces.
type osFS struct{}
func (osFS) Open(name string) (fs.File, error) { return os.Open(filepath.FromSlash(name)) }
func (osFS) ReadDir(name string) ([]fs.DirEntry, error) { return os.ReadDir(filepath.FromSlash(name)) }
func (osFS) Stat(name string) (fs.FileInfo, error) { return os.Stat(filepath.FromSlash(name)) }
func (osFS) ReadFile(name string) ([]byte, error) { return os.ReadFile(filepath.FromSlash(name)) }
func (osFS) Glob(pattern string) ([]string, error) { return filepath.Glob(filepath.FromSlash(pattern)) }
type noopFS struct{}
var _ fs.FS = noopFS{}
func (f noopFS) Open(name string) (fs.File, error) {
return nil, os.ErrNotExist
}

670
vendor/github.com/pressly/goose/v3/provider.go generated vendored Normal file
View file

@ -0,0 +1,670 @@
package goose
import (
"cmp"
"context"
"database/sql"
"errors"
"fmt"
"io/fs"
"log/slog"
"math"
"strconv"
"strings"
"sync"
"github.com/pressly/goose/v3/database"
"github.com/pressly/goose/v3/internal/controller"
"github.com/pressly/goose/v3/internal/gooseutil"
"github.com/pressly/goose/v3/internal/sqlparser"
"go.uber.org/multierr"
)
// Provider is a goose migration provider.
type Provider struct {
// mu protects all accesses to the provider and must be held when calling operations on the
// database.
mu sync.Mutex
db *sql.DB
store *controller.StoreController
versionTableOnce sync.Once
fsys fs.FS
cfg config
// migrations are ordered by version in ascending order. This list will never be empty and
// contains all migrations known to the provider.
migrations []*Migration
}
// NewProvider returns a new goose provider.
//
// The caller is responsible for matching the database dialect with the database/sql driver. For
// example, if the database dialect is "postgres", the database/sql driver could be
// github.com/lib/pq or github.com/jackc/pgx. Each dialect has a corresponding [database.Dialect]
// constant backed by a default [database.Store] implementation. For more advanced use cases, such
// as using a custom table name or supplying a custom store implementation, see [WithStore].
//
// fsys is the filesystem used to read migration files, but may be nil. Most users will want to use
// [os.DirFS], os.DirFS("path/to/migrations"), to read migrations from the local filesystem.
// However, it is possible to use a different "filesystem", such as [embed.FS] or filter out
// migrations using [fs.Sub].
//
// See [ProviderOption] for more information on configuring the provider.
//
// Unless otherwise specified, all methods on Provider are safe for concurrent use.
func NewProvider(dialect Dialect, db *sql.DB, fsys fs.FS, opts ...ProviderOption) (*Provider, error) {
if db == nil {
return nil, errors.New("db must not be nil")
}
if fsys == nil {
fsys = noopFS{}
}
cfg := config{
registered: make(map[int64]*Migration),
excludePaths: make(map[string]bool),
excludeVersions: make(map[int64]bool),
}
for _, opt := range opts {
if err := opt.apply(&cfg); err != nil {
return nil, err
}
}
// Allow users to specify a custom store implementation, but only if they don't specify a
// dialect. If they specify a dialect, we'll use the default store implementation.
if dialect == DialectCustom && cfg.store == nil {
return nil, errors.New("custom store must be supplied when using a custom dialect, make sure to pass WithStore option")
}
if dialect != DialectCustom && cfg.store != nil {
return nil, errors.New("custom store must not be specified when using one of the default dialects, use DialectCustom instead")
}
// Allow table name to be set only if store is not set.
if cfg.tableName != "" && cfg.store != nil {
return nil, errors.New("WithTableName cannot be used with WithStore; set the table name directly on your custom store")
}
// Set default logger if neither was provided
if cfg.slogger == nil && cfg.logger == nil {
cfg.logger = &stdLogger{}
}
var store database.Store
if dialect != "" {
var err error
store, err = database.NewStore(dialect, cmp.Or(cfg.tableName, DefaultTablename))
if err != nil {
return nil, err
}
} else {
store = cfg.store
}
if store.Tablename() == "" {
return nil, errors.New("invalid store implementation: table name must not be empty")
}
return newProvider(db, store, fsys, cfg, registeredGoMigrations /* global */)
}
func newProvider(
db *sql.DB,
store database.Store,
fsys fs.FS,
cfg config,
global map[int64]*Migration,
) (*Provider, error) {
// Collect migrations from the filesystem and merge with registered migrations.
//
// Note, we don't parse SQL migrations here. They are parsed lazily when required.
// feat(mf): we could add a flag to parse SQL migrations eagerly. This would allow us to return
// an error if there are any SQL parsing errors. This adds a bit overhead to startup though, so
// we should make it optional.
filesystemSources, err := collectFilesystemSources(fsys, false, cfg.excludePaths, cfg.excludeVersions)
if err != nil {
return nil, err
}
versionToGoMigration := make(map[int64]*Migration)
// Add user-registered Go migrations from the provider.
for version, m := range cfg.registered {
versionToGoMigration[version] = m
}
// Skip adding global Go migrations if explicitly disabled.
if cfg.disableGlobalRegistry {
// TODO(mf): let's add a warn-level log here to inform users if len(global) > 0. Would like
// to add this once we're on go1.21 and leverage the new slog package.
} else {
for version, m := range global {
if _, ok := versionToGoMigration[version]; ok {
return nil, fmt.Errorf("global go migration conflicts with provider-registered go migration with version %d", version)
}
versionToGoMigration[version] = m
}
}
// At this point we have all registered unique Go migrations (if any). We need to merge them
// with SQL migrations from the filesystem.
migrations, err := merge(filesystemSources, versionToGoMigration)
if err != nil {
return nil, err
}
if len(migrations) == 0 {
return nil, ErrNoMigrations
}
return &Provider{
db: db,
fsys: fsys,
cfg: cfg,
store: controller.NewStoreController(store),
migrations: migrations,
}, nil
}
// Status returns the status of all migrations, merging the list of migrations from the database and
// filesystem. The returned items are ordered by version, in ascending order.
func (p *Provider) Status(ctx context.Context) ([]*MigrationStatus, error) {
return p.status(ctx)
}
// HasPending returns true if there are pending migrations to apply, otherwise, it returns false. If
// out-of-order migrations are disabled, yet some are detected, this method returns an error.
//
// Note, this method will not use a SessionLocker or Locker if one is configured. This allows
// callers to check for pending migrations without blocking or being blocked by other operations.
func (p *Provider) HasPending(ctx context.Context) (bool, error) {
return p.hasPending(ctx)
}
// GetVersions returns the max database version and the target version to migrate to.
//
// Note, this method will not use a SessionLocker or Locker if one is configured. This allows
// callers to check for versions without blocking or being blocked by other operations.
func (p *Provider) GetVersions(ctx context.Context) (current, target int64, err error) {
return p.getVersions(ctx)
}
// GetDBVersion returns the highest version recorded in the database, regardless of the order in
// which migrations were applied. For example, if migrations were applied out of order (1,4,2,3),
// this method returns 4. If no migrations have been applied, it returns 0.
func (p *Provider) GetDBVersion(ctx context.Context) (int64, error) {
if p.cfg.disableVersioning {
return -1, errors.New("getting database version not supported when versioning is disabled")
}
return p.getDBMaxVersion(ctx, nil)
}
// ListSources returns a list of all migration sources known to the provider, sorted in ascending
// order by version. The path field may be empty for manually registered migrations, such as Go
// migrations registered using the [WithGoMigrations] option.
func (p *Provider) ListSources() []*Source {
sources := make([]*Source, 0, len(p.migrations))
for _, m := range p.migrations {
sources = append(sources, &Source{
Type: m.Type,
Path: m.Source,
Version: m.Version,
})
}
return sources
}
// Ping attempts to ping the database to verify a connection is available.
func (p *Provider) Ping(ctx context.Context) error {
return p.db.PingContext(ctx)
}
// Close closes the database connection initially supplied to the provider.
func (p *Provider) Close() error {
return p.db.Close()
}
// ApplyVersion applies exactly one migration for the specified version. If there is no migration
// available for the specified version, this method returns [ErrVersionNotFound]. If the migration
// has already been applied, this method returns [ErrAlreadyApplied].
//
// The direction parameter determines the migration direction: true for up migration and false for
// down migration.
func (p *Provider) ApplyVersion(ctx context.Context, version int64, direction bool) (*MigrationResult, error) {
res, err := p.apply(ctx, version, direction)
if err != nil {
return nil, err
}
// This should never happen, we must return exactly one result.
if len(res) != 1 {
versions := make([]string, 0, len(res))
for _, r := range res {
versions = append(versions, strconv.FormatInt(r.Source.Version, 10))
}
return nil, fmt.Errorf(
"unexpected number of migrations applied running apply, expecting exactly one result: %v",
strings.Join(versions, ","),
)
}
return res[0], nil
}
// Up applies all pending migrations. If there are no new migrations to apply, this method returns
// empty list and nil error.
func (p *Provider) Up(ctx context.Context) ([]*MigrationResult, error) {
hasPending, err := p.HasPending(ctx)
if err != nil {
return nil, err
}
if !hasPending {
return nil, nil
}
return p.up(ctx, false, math.MaxInt64)
}
// UpByOne applies the next pending migration. If there is no next migration to apply, this method
// returns [ErrNoNextVersion].
func (p *Provider) UpByOne(ctx context.Context) (*MigrationResult, error) {
hasPending, err := p.HasPending(ctx)
if err != nil {
return nil, err
}
if !hasPending {
return nil, ErrNoNextVersion
}
res, err := p.up(ctx, true, math.MaxInt64)
if err != nil {
return nil, err
}
if len(res) == 0 {
return nil, ErrNoNextVersion
}
// This should never happen, we must return exactly one result.
if len(res) != 1 {
versions := make([]string, 0, len(res))
for _, r := range res {
versions = append(versions, strconv.FormatInt(r.Source.Version, 10))
}
return nil, fmt.Errorf(
"unexpected number of migrations applied running up-by-one, expecting exactly one result: %v",
strings.Join(versions, ","),
)
}
return res[0], nil
}
// UpTo applies all pending migrations up to, and including, the specified version. If there are no
// migrations to apply, this method returns empty list and nil error.
//
// For example, if there are three new migrations (9,10,11) and the current database version is 8
// with a requested version of 10, only versions 9,10 will be applied.
func (p *Provider) UpTo(ctx context.Context, version int64) ([]*MigrationResult, error) {
hasPending, err := p.HasPending(ctx)
if err != nil {
return nil, err
}
if !hasPending {
return nil, nil
}
return p.up(ctx, false, version)
}
// Down rolls back the most recently applied migration. If there are no migrations to rollback, this
// method returns [ErrNoNextVersion].
//
// Note, migrations are rolled back in the order they were applied. And not in the reverse order of
// the migration version. This only applies in scenarios where migrations are allowed to be applied
// out of order.
func (p *Provider) Down(ctx context.Context) (*MigrationResult, error) {
res, err := p.down(ctx, true, 0)
if err != nil {
return nil, err
}
if len(res) == 0 {
return nil, ErrNoNextVersion
}
// This should never happen, we must return exactly one result.
if len(res) != 1 {
versions := make([]string, 0, len(res))
for _, r := range res {
versions = append(versions, strconv.FormatInt(r.Source.Version, 10))
}
return nil, fmt.Errorf(
"unexpected number of migrations applied running down, expecting exactly one result: %v",
strings.Join(versions, ","),
)
}
return res[0], nil
}
// DownTo rolls back all migrations down to, but not including, the specified version.
//
// For example, if the current database version is 11,10,9... and the requested version is 9, only
// migrations 11, 10 will be rolled back.
//
// Note, migrations are rolled back in the order they were applied. And not in the reverse order of
// the migration version. This only applies in scenarios where migrations are allowed to be applied
// out of order.
func (p *Provider) DownTo(ctx context.Context, version int64) ([]*MigrationResult, error) {
if version < 0 {
return nil, fmt.Errorf("invalid version: must be a valid number or zero: %d", version)
}
return p.down(ctx, false, version)
}
// *** Internal methods ***
func (p *Provider) up(
ctx context.Context,
byOne bool,
version int64,
) (_ []*MigrationResult, retErr error) {
if version < 1 {
return nil, errInvalidVersion
}
conn, cleanup, err := p.initialize(ctx, true)
if err != nil {
return nil, fmt.Errorf("failed to initialize: %w", err)
}
defer func() {
retErr = multierr.Append(retErr, cleanup())
}()
if len(p.migrations) == 0 {
return nil, nil
}
var apply []*Migration
if p.cfg.disableVersioning {
if byOne {
return nil, errors.New("up-by-one not supported when versioning is disabled")
}
apply = p.migrations
} else {
// optimize(mf): Listing all migrations from the database isn't great.
//
// The ideal implementation would be to query for the current max version and then apply
// migrations greater than that version. However, a nice property of the current
// implementation is that we can make stronger guarantees about unapplied migrations.
//
// In cases where users do not use out-of-order migrations, we want to surface an error if
// there are older unapplied migrations. See https://github.com/pressly/goose/issues/761 for
// more details.
//
// And in cases where users do use out-of-order migrations, we need to build a list of older
// migrations that need to be applied, so we need to query for all migrations anyways.
dbMigrations, err := p.store.ListMigrations(ctx, conn)
if err != nil {
return nil, err
}
if len(dbMigrations) == 0 {
return nil, errMissingZeroVersion
}
versions, err := gooseutil.UpVersions(
getVersionsFromMigrations(p.migrations), // fsys versions
getVersionsFromListMigrations(dbMigrations), // db versions
version,
p.cfg.allowMissing,
)
if err != nil {
return nil, err
}
for _, v := range versions {
m, err := p.getMigration(v)
if err != nil {
return nil, err
}
apply = append(apply, m)
}
}
return p.runMigrations(ctx, conn, apply, sqlparser.DirectionUp, byOne)
}
func (p *Provider) down(
ctx context.Context,
byOne bool,
version int64,
) (_ []*MigrationResult, retErr error) {
conn, cleanup, err := p.initialize(ctx, true)
if err != nil {
return nil, fmt.Errorf("failed to initialize: %w", err)
}
defer func() {
retErr = multierr.Append(retErr, cleanup())
}()
if len(p.migrations) == 0 {
return nil, nil
}
if p.cfg.disableVersioning {
var downMigrations []*Migration
if byOne {
last := p.migrations[len(p.migrations)-1]
downMigrations = []*Migration{last}
} else {
downMigrations = p.migrations
}
return p.runMigrations(ctx, conn, downMigrations, sqlparser.DirectionDown, byOne)
}
dbMigrations, err := p.store.ListMigrations(ctx, conn)
if err != nil {
return nil, err
}
if len(dbMigrations) == 0 {
return nil, errMissingZeroVersion
}
// We never migrate the zero version down.
if dbMigrations[0].Version == 0 {
p.logf(ctx,
"no migrations to run, current version: 0",
"no migrations to run",
slog.Int64("version", 0),
)
return nil, nil
}
var apply []*Migration
for _, dbMigration := range dbMigrations {
if dbMigration.Version <= version {
break
}
m, err := p.getMigration(dbMigration.Version)
if err != nil {
return nil, err
}
apply = append(apply, m)
}
return p.runMigrations(ctx, conn, apply, sqlparser.DirectionDown, byOne)
}
func (p *Provider) apply(
ctx context.Context,
version int64,
direction bool,
) (_ []*MigrationResult, retErr error) {
if version < 1 {
return nil, errInvalidVersion
}
m, err := p.getMigration(version)
if err != nil {
return nil, err
}
conn, cleanup, err := p.initialize(ctx, true)
if err != nil {
return nil, fmt.Errorf("failed to initialize: %w", err)
}
defer func() {
retErr = multierr.Append(retErr, cleanup())
}()
d := sqlparser.DirectionDown
if direction {
d = sqlparser.DirectionUp
}
if p.cfg.disableVersioning {
// If versioning is disabled, we simply run the migration.
return p.runMigrations(ctx, conn, []*Migration{m}, d, true)
}
result, err := p.store.GetMigration(ctx, conn, version)
if err != nil && !errors.Is(err, database.ErrVersionNotFound) {
return nil, err
}
// There are a few states here:
// 1. direction is up
// a. migration is applied, this is an error (ErrAlreadyApplied)
// b. migration is not applied, apply it
if direction && result != nil {
return nil, fmt.Errorf("version %d: %w", version, ErrAlreadyApplied)
}
// 2. direction is down
// a. migration is applied, rollback
// b. migration is not applied, this is an error (ErrNotApplied)
if !direction && result == nil {
return nil, fmt.Errorf("version %d: %w", version, ErrNotApplied)
}
return p.runMigrations(ctx, conn, []*Migration{m}, d, true)
}
func (p *Provider) getVersions(ctx context.Context) (current, target int64, retErr error) {
conn, cleanup, err := p.initialize(ctx, false)
if err != nil {
return -1, -1, fmt.Errorf("failed to initialize: %w", err)
}
defer func() {
retErr = multierr.Append(retErr, cleanup())
}()
target = p.migrations[len(p.migrations)-1].Version
// If versioning is disabled, we always have pending migrations and the target version is the
// last migration.
if p.cfg.disableVersioning {
return -1, target, nil
}
current, err = p.store.GetLatestVersion(ctx, conn)
if err != nil {
if errors.Is(err, database.ErrVersionNotFound) {
return -1, target, errMissingZeroVersion
}
return -1, target, err
}
return current, target, nil
}
func (p *Provider) hasPending(ctx context.Context) (_ bool, retErr error) {
conn, cleanup, err := p.initialize(ctx, false)
if err != nil {
return false, fmt.Errorf("failed to initialize: %w", err)
}
defer func() {
retErr = multierr.Append(retErr, cleanup())
}()
// If versioning is disabled, we always have pending migrations.
if p.cfg.disableVersioning {
return true, nil
}
// List all migrations from the database. Careful, optimizations here can lead to subtle bugs.
// We have 2 important cases to consider:
//
// 1. Users have enabled out-of-order migrations, in which case we need to check if any
// migrations are missing and report that there are pending migrations. Do not surface an
// error because this is a valid state.
//
// 2. Users have disabled out-of-order migrations (default), in which case we need to check if all
// migrations have been applied. We cannot check for the highest applied version because we lose the
// ability to surface an error if an out-of-order migration was introduced. It would be silently
// ignored and the user would not know that they have unapplied migrations.
//
// Maybe we could consider adding a flag to the provider such as IgnoreMissing, which would
// allow silently ignoring missing migrations. This would be useful for users that have built
// checks that prevent missing migrations from being introduced.
dbMigrations, err := p.store.ListMigrations(ctx, conn)
if err != nil {
return false, err
}
apply, err := gooseutil.UpVersions(
getVersionsFromMigrations(p.migrations), // fsys versions
getVersionsFromListMigrations(dbMigrations), // db versions
math.MaxInt64,
p.cfg.allowMissing,
)
if err != nil {
return false, err
}
return len(apply) > 0, nil
}
func getVersionsFromMigrations(in []*Migration) []int64 {
out := make([]int64, 0, len(in))
for _, m := range in {
out = append(out, m.Version)
}
return out
}
func getVersionsFromListMigrations(in []*database.ListMigrationsResult) []int64 {
out := make([]int64, 0, len(in))
for _, m := range in {
out = append(out, m.Version)
}
return out
}
func (p *Provider) status(ctx context.Context) (_ []*MigrationStatus, retErr error) {
conn, cleanup, err := p.initialize(ctx, true)
if err != nil {
return nil, fmt.Errorf("failed to initialize: %w", err)
}
defer func() {
retErr = multierr.Append(retErr, cleanup())
}()
status := make([]*MigrationStatus, 0, len(p.migrations))
for _, m := range p.migrations {
migrationStatus := &MigrationStatus{
Source: &Source{
Type: m.Type,
Path: m.Source,
Version: m.Version,
},
State: StatePending,
}
// If versioning is disabled, we can't check the database for applied migrations, so we
// assume all migrations are pending.
if !p.cfg.disableVersioning {
dbResult, err := p.store.GetMigration(ctx, conn, m.Version)
if err != nil && !errors.Is(err, database.ErrVersionNotFound) {
return nil, err
}
if dbResult != nil {
migrationStatus.State = StateApplied
migrationStatus.AppliedAt = dbResult.Timestamp
}
}
status = append(status, migrationStatus)
}
return status, nil
}
// getDBMaxVersion returns the highest version recorded in the database, regardless of the order in
// which migrations were applied. conn may be nil, in which case a connection is initialized.
func (p *Provider) getDBMaxVersion(ctx context.Context, conn *sql.Conn) (_ int64, retErr error) {
if conn == nil {
var cleanup func() error
var err error
conn, cleanup, err = p.initialize(ctx, true)
if err != nil {
return 0, err
}
defer func() {
retErr = multierr.Append(retErr, cleanup())
}()
}
latest, err := p.store.GetLatestVersion(ctx, conn)
if err != nil {
if errors.Is(err, database.ErrVersionNotFound) {
return 0, errMissingZeroVersion
}
return -1, err
}
return latest, nil
}

196
vendor/github.com/pressly/goose/v3/provider_collect.go generated vendored Normal file
View file

@ -0,0 +1,196 @@
package goose
import (
"errors"
"fmt"
"io/fs"
"path/filepath"
"sort"
"strings"
)
// fileSources represents a collection of migration files on the filesystem.
type fileSources struct {
sqlSources []Source
goSources []Source
}
// collectFilesystemSources scans the file system for migration files that have a numeric prefix
// (greater than one) followed by an underscore and a file extension of either .go or .sql. fsys may
// be nil, in which case an empty fileSources is returned.
//
// If strict is true, then any error parsing the numeric component of the filename will result in an
// error. The file is skipped otherwise.
//
// This function DOES NOT parse SQL migrations or merge registered Go migrations. It only collects
// migration sources from the filesystem.
func collectFilesystemSources(
fsys fs.FS,
strict bool,
excludePaths map[string]bool,
excludeVersions map[int64]bool,
) (*fileSources, error) {
if fsys == nil {
return new(fileSources), nil
}
sources := new(fileSources)
versionToBaseLookup := make(map[int64]string) // map[version]filepath.Base(fullpath)
for _, pattern := range []string{
"*.sql",
"*.go",
} {
files, err := fs.Glob(fsys, pattern)
if err != nil {
return nil, fmt.Errorf("failed to glob pattern %q: %w", pattern, err)
}
for _, fullpath := range files {
base := filepath.Base(fullpath)
if strings.HasSuffix(base, "_test.go") {
continue
}
if excludePaths[base] {
// TODO(mf): log this?
continue
}
// If the filename has a valid looking version of the form: NUMBER_.{sql,go}, then use
// that as the version. Otherwise, ignore it. This allows users to have arbitrary
// filenames, but still have versioned migrations within the same directory. For
// example, a user could have a helpers.go file which contains unexported helper
// functions for migrations.
version, err := NumericComponent(base)
if err != nil {
if strict {
return nil, fmt.Errorf("failed to parse numeric component from %q: %w", base, err)
}
continue
}
if excludeVersions[version] {
// TODO: log this?
continue
}
// Ensure there are no duplicate versions.
if existing, ok := versionToBaseLookup[version]; ok {
return nil, fmt.Errorf("found duplicate migration version %d:\n\texisting:%v\n\tcurrent:%v",
version,
existing,
base,
)
}
switch filepath.Ext(base) {
case ".sql":
sources.sqlSources = append(sources.sqlSources, Source{
Type: TypeSQL,
Path: fullpath,
Version: version,
})
case ".go":
sources.goSources = append(sources.goSources, Source{
Type: TypeGo,
Path: fullpath,
Version: version,
})
default:
// Should never happen since we already filtered out all other file types.
return nil, fmt.Errorf("invalid file extension: %q", base)
}
// Add the version to the lookup map.
versionToBaseLookup[version] = base
}
}
return sources, nil
}
func newSQLMigration(source Source) *Migration {
return &Migration{
Type: source.Type,
Version: source.Version,
Source: source.Path,
construct: true,
Next: -1, Previous: -1,
sql: sqlMigration{
Parsed: false, // SQL migrations are parsed lazily.
},
}
}
func merge(sources *fileSources, registered map[int64]*Migration) ([]*Migration, error) {
var migrations []*Migration
migrationLookup := make(map[int64]*Migration)
// Add all SQL migrations to the list of migrations.
for _, source := range sources.sqlSources {
m := newSQLMigration(source)
migrations = append(migrations, m)
migrationLookup[source.Version] = m
}
// If there are no Go files in the filesystem and no registered Go migrations, return early.
if len(sources.goSources) == 0 && len(registered) == 0 {
return migrations, nil
}
// Return an error if the given sources contain a versioned Go migration that has not been
// registered. This is a sanity check to ensure users didn't accidentally create a valid looking
// Go migration file on disk and forget to register it.
//
// This is almost always a user error.
var unregistered []string
for _, s := range sources.goSources {
m, ok := registered[s.Version]
if !ok {
unregistered = append(unregistered, s.Path)
} else {
// Populate the source path for registered Go migrations that have a corresponding file
// on disk.
m.Source = s.Path
}
}
if len(unregistered) > 0 {
return nil, unregisteredError(unregistered)
}
// Add all registered Go migrations to the list of migrations, checking for duplicate versions.
//
// Important, users can register Go migrations manually via goose.Add_ functions. These
// migrations may not have a corresponding file on disk. Which is fine! We include them
// wholesale as part of migrations. This allows users to build a custom binary that only embeds
// the SQL migration files.
for version, r := range registered {
// Ensure there are no duplicate versions.
if existing, ok := migrationLookup[version]; ok {
fullpath := r.Source
if fullpath == "" {
fullpath = "no source path"
}
return nil, fmt.Errorf("found duplicate migration version %d:\n\texisting:%v\n\tcurrent:%v",
version,
existing.Source,
fullpath,
)
}
migrations = append(migrations, r)
migrationLookup[version] = r
}
// Sort migrations by version in ascending order.
sort.Slice(migrations, func(i, j int) bool {
return migrations[i].Version < migrations[j].Version
})
return migrations, nil
}
func unregisteredError(unregistered []string) error {
const (
hintURL = "https://github.com/pressly/goose/tree/master/examples/go-migrations"
)
f := "file"
if len(unregistered) > 1 {
f += "s"
}
var b strings.Builder
b.WriteString(fmt.Sprintf("error: detected %d unregistered Go %s:\n", len(unregistered), f))
for _, name := range unregistered {
b.WriteString("\t" + name + "\n")
}
hint := fmt.Sprintf("hint: go functions must be registered and built into a custom binary see:\n%s", hintURL)
b.WriteString(hint)
b.WriteString("\n")
return errors.New(b.String())
}

48
vendor/github.com/pressly/goose/v3/provider_errors.go generated vendored Normal file
View file

@ -0,0 +1,48 @@
package goose
import (
"errors"
"fmt"
)
var (
// ErrVersionNotFound is returned when a specific migration version is not located. This can
// occur if a .sql file or a Go migration function for the specified version is missing.
ErrVersionNotFound = errors.New("version not found")
// ErrNoMigrations is returned by [NewProvider] when no migrations are found.
ErrNoMigrations = errors.New("no migrations found")
// ErrAlreadyApplied indicates that the migration cannot be applied because it has already been
// executed. This error is returned by [Provider.Apply].
ErrAlreadyApplied = errors.New("migration already applied")
// ErrNotApplied indicates that the rollback cannot be performed because the migration has not
// yet been applied. This error is returned by [Provider.Apply].
ErrNotApplied = errors.New("migration not applied")
// errInvalidVersion is returned when a migration version is invalid.
errInvalidVersion = errors.New("version must be greater than 0")
)
// PartialError is returned when a migration fails, but some migrations already got applied.
type PartialError struct {
// Applied are migrations that were applied successfully before the error occurred. May be
// empty.
Applied []*MigrationResult
// Failed contains the result of the migration that failed. Cannot be nil.
Failed *MigrationResult
// Err is the error that occurred while running the migration and caused the failure.
Err error
}
func (e *PartialError) Error() string {
return fmt.Sprintf(
"partial migration error (type:%s,version:%d): %v",
e.Failed.Source.Type, e.Failed.Source.Version, e.Err,
)
}
func (e *PartialError) Unwrap() error {
return e.Err
}

280
vendor/github.com/pressly/goose/v3/provider_options.go generated vendored Normal file
View file

@ -0,0 +1,280 @@
package goose
import (
"errors"
"fmt"
"log/slog"
"github.com/pressly/goose/v3/database"
"github.com/pressly/goose/v3/lock"
)
const (
// DefaultTablename is the default name of the database table used to track history of applied
// migrations.
DefaultTablename = "goose_db_version"
)
// ProviderOption is a configuration option for a goose goose.
type ProviderOption interface {
apply(*config) error
}
// WithStore configures the provider with a custom [database.Store], allowing users to bring their
// own implementation of the store interface. When this option is used, the dialect parameter of
// [NewProvider] must be set to [DialectCustom].
//
// This option cannot be used together with [WithTableName], since the table name is set on the
// store.
//
// By default, the provider uses the [database.NewStore] function to create a store backed by one of
// the officially supported dialects.
func WithStore(store database.Store) ProviderOption {
return configFunc(func(c *config) error {
if c.store != nil {
return fmt.Errorf("store already set: %T", c.store)
}
if store == nil {
return errors.New("store must not be nil")
}
if store.Tablename() == "" {
return errors.New("store implementation must set the table name")
}
c.store = store
return nil
})
}
// WithTableName sets the name of the database table used to track history of applied migrations.
// This option cannot be used together with [WithStore], since the table name is set on the store.
//
// Default is "goose_db_version".
func WithTableName(name string) ProviderOption {
return configFunc(func(c *config) error {
if name == "" {
return errors.New("table name must not be empty")
}
c.tableName = name
return nil
})
}
// WithVerbose enables verbose logging.
func WithVerbose(b bool) ProviderOption {
return configFunc(func(c *config) error {
c.verbose = b
return nil
})
}
// WithSessionLocker enables locking using the provided SessionLocker.
//
// If WithSessionLocker is not called, locking is disabled. Must not be used together with
// [WithLocker].
func WithSessionLocker(locker lock.SessionLocker) ProviderOption {
return configFunc(func(c *config) error {
if c.lockEnabled {
return errors.New("lock already enabled")
}
if c.sessionLocker != nil {
return errors.New("session locker already set")
}
if c.locker != nil {
return errors.New("locker already set; cannot use both SessionLocker and Locker")
}
if locker == nil {
return errors.New("session locker must not be nil")
}
c.lockEnabled = true
c.sessionLocker = locker
return nil
})
}
// WithLocker enables locking using the provided Locker.
//
// If WithLocker is not called, locking is disabled. Must not be used together with
// [WithSessionLocker].
func WithLocker(locker lock.Locker) ProviderOption {
return configFunc(func(c *config) error {
if c.lockEnabled {
return errors.New("lock already enabled")
}
if c.locker != nil {
return errors.New("locker already set")
}
if c.sessionLocker != nil {
return errors.New("session locker already set; cannot use both SessionLocker and Locker")
}
if locker == nil {
return errors.New("locker must not be nil")
}
c.lockEnabled = true
c.locker = locker
return nil
})
}
// WithExcludeNames excludes the given file name from the list of migrations. If called multiple
// times, the list of excludes is merged.
func WithExcludeNames(excludes []string) ProviderOption {
return configFunc(func(c *config) error {
for _, name := range excludes {
if _, ok := c.excludePaths[name]; ok {
return fmt.Errorf("duplicate exclude file name: %s", name)
}
c.excludePaths[name] = true
}
return nil
})
}
// WithExcludeVersions excludes the given versions from the list of migrations. If called multiple
// times, the list of excludes is merged.
func WithExcludeVersions(versions []int64) ProviderOption {
return configFunc(func(c *config) error {
for _, version := range versions {
if version < 1 {
return errInvalidVersion
}
if _, ok := c.excludeVersions[version]; ok {
return fmt.Errorf("duplicate excludes version: %d", version)
}
c.excludeVersions[version] = true
}
return nil
})
}
// WithGoMigrations registers Go migrations with the provider. If a Go migration with the same
// version has already been registered, an error will be returned.
//
// Go migrations must be constructed using the [NewGoMigration] function.
func WithGoMigrations(migrations ...*Migration) ProviderOption {
return configFunc(func(c *config) error {
for _, m := range migrations {
if _, ok := c.registered[m.Version]; ok {
return fmt.Errorf("go migration with version %d already registered", m.Version)
}
if err := checkGoMigration(m); err != nil {
return fmt.Errorf("invalid go migration: %w", err)
}
c.registered[m.Version] = m
}
return nil
})
}
// WithDisableGlobalRegistry prevents the provider from registering Go migrations from the global
// registry. By default, goose will register all Go migrations including those registered globally.
func WithDisableGlobalRegistry(b bool) ProviderOption {
return configFunc(func(c *config) error {
c.disableGlobalRegistry = b
return nil
})
}
// WithAllowOutofOrder allows the provider to apply missing (out-of-order) migrations. By default,
// goose will raise an error if it encounters a missing migration.
//
// For example: migrations 1,3 are applied and then version 2,6 are introduced. If this option is
// true, then goose will apply 2 (missing) and 6 (new) instead of raising an error. The final order
// of applied migrations will be: 1,3,2,6. Out-of-order migrations are always applied first,
// followed by new migrations.
func WithAllowOutofOrder(b bool) ProviderOption {
return configFunc(func(c *config) error {
c.allowMissing = b
return nil
})
}
// WithDisableVersioning disables versioning. Disabling versioning allows applying migrations
// without tracking the versions in the database schema table. Useful for tests, seeding a database
// or running ad-hoc queries. By default, goose will track all versions in the database schema
// table.
func WithDisableVersioning(b bool) ProviderOption {
return configFunc(func(c *config) error {
c.disableVersioning = b
return nil
})
}
// WithLogger will set a custom Logger, which will override the default logger. Cannot be used
// together with [WithSlog].
func WithLogger(l Logger) ProviderOption {
return configFunc(func(c *config) error {
if l == nil {
return errors.New("logger must not be nil")
}
if c.slogger != nil {
return errors.New("cannot use both WithLogger and WithSlog")
}
c.logger = l
return nil
})
}
// WithSlog will set a custom [*slog.Logger] for structured logging. This enables rich structured
// logging with attributes like source, direction, duration, etc. Cannot be used together with
// [WithLogger].
//
// Example:
//
// logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
// p, err := goose.NewProvider("postgres", db, fs, goose.WithSlog(logger))
func WithSlog(logger *slog.Logger) ProviderOption {
return configFunc(func(c *config) error {
if logger == nil {
return errors.New("slog logger must not be nil")
}
if c.logger != nil {
return errors.New("cannot use both WithLogger and WithSlog")
}
c.slogger = logger
return nil
})
}
// WithIsolateDDL executes DDL operations separately from DML operations. This is useful for
// databases like AWS Aurora DSQL that don't support mixing DDL and DML within the same transaction.
func WithIsolateDDL(b bool) ProviderOption {
return configFunc(func(c *config) error {
c.isolateDDL = b
return nil
})
}
type config struct {
tableName string
store database.Store
verbose bool
excludePaths map[string]bool
excludeVersions map[int64]bool
// Go migrations registered by the user. These will be merged/resolved against the globally
// registered migrations.
registered map[int64]*Migration
// Locking options
lockEnabled bool
sessionLocker lock.SessionLocker
locker lock.Locker
// Feature
disableVersioning bool
allowMissing bool
disableGlobalRegistry bool
isolateDDL bool
// Only a single logger can be set, they are mutually exclusive. If neither is set, a default
// [Logger] will be set to maintain backward compatibility in /v3.
logger Logger
slogger *slog.Logger
}
type configFunc func(*config) error
func (f configFunc) apply(cfg *config) error {
return f(cfg)
}

535
vendor/github.com/pressly/goose/v3/provider_run.go generated vendored Normal file
View file

@ -0,0 +1,535 @@
package goose
import (
"cmp"
"context"
"database/sql"
"errors"
"fmt"
"io/fs"
"log/slog"
"path/filepath"
"runtime/debug"
"slices"
"time"
"github.com/pressly/goose/v3/database"
"github.com/pressly/goose/v3/internal/sqlparser"
"github.com/sethvargo/go-retry"
"go.uber.org/multierr"
)
var (
errMissingZeroVersion = errors.New("missing zero version migration")
)
func (p *Provider) prepareMigration(fsys fs.FS, m *Migration, direction bool) error {
switch m.Type {
case TypeGo:
if m.goUp.Mode == 0 {
return errors.New("go up migration mode is not set")
}
if m.goDown.Mode == 0 {
return errors.New("go down migration mode is not set")
}
var useTx bool
if direction {
useTx = m.goUp.Mode == TransactionEnabled
} else {
useTx = m.goDown.Mode == TransactionEnabled
}
// bug(mf): this is a potential deadlock scenario. We're running Go migrations with *sql.DB,
// but are locking the database with *sql.Conn. If the caller sets max open connections to
// 1, then this will deadlock because the Go migration will try to acquire a connection from
// the pool, but the pool is exhausted because the lock is held.
//
// A potential solution is to expose a third Go register function *sql.Conn. Or continue to
// use *sql.DB and document that the user SHOULD NOT SET max open connections to 1. This is
// a bit of an edge case. For now, we guard against this scenario by checking the max open
// connections and returning an error.
if p.cfg.lockEnabled && p.cfg.sessionLocker != nil && p.db.Stats().MaxOpenConnections == 1 {
if !useTx {
return errors.New("potential deadlock detected: cannot run Go migration without a transaction when max open connections set to 1")
}
}
return nil
case TypeSQL:
if m.sql.Parsed {
return nil
}
parsed, err := sqlparser.ParseAllFromFS(fsys, m.Source, false)
if err != nil {
return err
}
m.sql.Parsed = true
m.sql.UseTx = parsed.UseTx
m.sql.Up, m.sql.Down = parsed.Up, parsed.Down
return nil
}
return fmt.Errorf("invalid migration type: %+v", m)
}
func (p *Provider) logf(ctx context.Context, legacyMsg string, slogMsg string, attrs ...slog.Attr) {
if !p.cfg.verbose {
return
}
if p.cfg.slogger != nil {
// Sort attributes by key for consistent ordering
slices.SortFunc(attrs, func(a, b slog.Attr) int {
return cmp.Compare(a.Key, b.Key)
})
// Use slog with structured attributes
args := make([]any, 0, len(attrs)+1)
// Add the logger=goose identifier
args = append(args, slog.String("logger", "goose"))
for _, attr := range attrs {
args = append(args, attr)
}
p.cfg.slogger.InfoContext(ctx, slogMsg, args...)
} else if p.cfg.logger != nil {
p.cfg.logger.Printf("goose: %s", legacyMsg)
}
}
// runMigrations runs migrations sequentially in the given direction. If the migrations list is
// empty, return nil without error.
func (p *Provider) runMigrations(
ctx context.Context,
conn *sql.Conn,
migrations []*Migration,
direction sqlparser.Direction,
byOne bool,
) ([]*MigrationResult, error) {
if len(migrations) == 0 {
if !p.cfg.disableVersioning {
// No need to print this message if versioning is disabled because there are no
// migrations being tracked in the goose version table.
maxVersion, err := p.getDBMaxVersion(ctx, conn)
if err != nil {
return nil, err
}
p.logf(ctx,
fmt.Sprintf("no migrations to run, current version: %d", maxVersion),
"no migrations to run",
slog.Int64("current_version", maxVersion),
)
}
return nil, nil
}
apply := migrations
if byOne {
apply = migrations[:1]
}
// SQL migrations are lazily parsed in both directions. This is done before attempting to run
// any migrations to catch errors early and prevent leaving the database in an incomplete state.
for _, m := range apply {
if err := p.prepareMigration(p.fsys, m, direction.ToBool()); err != nil {
return nil, fmt.Errorf("failed to prepare migration %s: %w", m.ref(), err)
}
}
// feat(mf): If we decide to add support for advisory locks at the transaction level, this may
// be a good place to acquire the lock. However, we need to be sure that ALL migrations are safe
// to run in a transaction.
// feat(mf): this is where we can (optionally) group multiple migrations to be run in a single
// transaction. The default is to apply each migration sequentially on its own. See the
// following issues for more details:
// - https://github.com/pressly/goose/issues/485
// - https://github.com/pressly/goose/issues/222
//
// Be careful, we can't use a single transaction for all migrations because some may be marked
// as not using a transaction.
var results []*MigrationResult
for _, m := range apply {
result := &MigrationResult{
Source: &Source{
Type: m.Type,
Path: m.Source,
Version: m.Version,
},
Direction: direction.String(),
Empty: isEmpty(m, direction.ToBool()),
}
start := time.Now()
if err := p.runIndividually(ctx, conn, m, direction.ToBool()); err != nil {
// TODO(mf): we should also return the pending migrations here, the remaining items in
// the apply slice.
result.Error = err
result.Duration = time.Since(start)
return nil, &PartialError{
Applied: results,
Failed: result,
Err: err,
}
}
result.Duration = time.Since(start)
results = append(results, result)
// Log the result of the migration.
var state string
if result.Empty {
state = "empty"
} else {
state = "applied"
}
p.logf(ctx,
result.String(),
"migration completed",
slog.String("source", filepath.Base(result.Source.Path)),
slog.String("direction", result.Direction),
slog.Float64("duration_seconds", result.Duration.Seconds()),
slog.String("state", state),
slog.Int64("version", result.Source.Version),
slog.String("type", string(result.Source.Type)),
)
}
if !p.cfg.disableVersioning && !byOne {
maxVersion, err := p.getDBMaxVersion(ctx, conn)
if err != nil {
return nil, err
}
p.logf(ctx,
fmt.Sprintf("successfully migrated database, current version: %d", maxVersion),
"successfully migrated database",
slog.Int64("current_version", maxVersion),
)
}
return results, nil
}
func (p *Provider) runIndividually(
ctx context.Context,
conn *sql.Conn,
m *Migration,
direction bool,
) error {
useTx, err := useTx(m, direction)
if err != nil {
return err
}
if useTx && !p.cfg.isolateDDL {
return beginTx(ctx, conn, func(tx *sql.Tx) error {
if err := p.runMigration(ctx, tx, m, direction); err != nil {
return err
}
return p.maybeInsertOrDelete(ctx, tx, m.Version, direction)
})
}
switch m.Type {
case TypeGo:
// Note, we are using *sql.DB instead of *sql.Conn because it's the Go migration contract.
// This may be a deadlock scenario if max open connections is set to 1 AND a lock is
// acquired on the database. In this case, the migration will block forever unable to
// acquire a connection from the pool.
//
// For now, we guard against this scenario by checking the max open connections and
// returning an error in the prepareMigration function.
if err := p.runMigration(ctx, p.db, m, direction); err != nil {
return err
}
return p.maybeInsertOrDelete(ctx, p.db, m.Version, direction)
case TypeSQL:
if err := p.runMigration(ctx, conn, m, direction); err != nil {
return err
}
return p.maybeInsertOrDelete(ctx, conn, m.Version, direction)
}
return fmt.Errorf("failed to run individual migration: neither sql or go: %v", m)
}
func (p *Provider) maybeInsertOrDelete(
ctx context.Context,
db database.DBTxConn,
version int64,
direction bool,
) error {
// If versioning is disabled, we don't need to insert or delete the migration version.
if p.cfg.disableVersioning {
return nil
}
if direction {
return p.store.Insert(ctx, db, database.InsertRequest{Version: version})
}
return p.store.Delete(ctx, db, version)
}
// beginTx begins a transaction and runs the given function. If the function returns an error, the
// transaction is rolled back. Otherwise, the transaction is committed.
func beginTx(ctx context.Context, conn *sql.Conn, fn func(tx *sql.Tx) error) (retErr error) {
tx, err := conn.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
if retErr != nil {
retErr = multierr.Append(retErr, tx.Rollback())
}
}()
if err := fn(tx); err != nil {
return err
}
return tx.Commit()
}
func (p *Provider) initialize(ctx context.Context, useLocker bool) (*sql.Conn, func() error, error) {
p.mu.Lock()
conn, err := p.db.Conn(ctx)
if err != nil {
p.mu.Unlock()
return nil, nil, err
}
// cleanup is a function that cleans up the connection, and optionally, the lock.
cleanup := func() error {
p.mu.Unlock()
return conn.Close()
}
// Handle locking if enabled and requested
if useLocker && p.cfg.lockEnabled {
// Session locker (connection-based locking)
if p.cfg.sessionLocker != nil {
l := p.cfg.sessionLocker
if err := l.SessionLock(ctx, conn); err != nil {
return nil, nil, multierr.Append(err, cleanup())
}
// A lock was acquired, so we need to unlock the session when we're done. This is done
// by returning a cleanup function that unlocks the session and closes the connection.
cleanup = func() error {
p.mu.Unlock()
// Use a detached context to unlock the session. This is because the context passed
// to SessionLock may have been canceled, and we don't want to cancel the unlock.
return multierr.Append(
l.SessionUnlock(context.WithoutCancel(ctx), conn),
conn.Close(),
)
}
}
// General locker (db-based locking)
if p.cfg.locker != nil {
l := p.cfg.locker
if err := l.Lock(ctx, p.db); err != nil {
return nil, nil, multierr.Append(err, cleanup())
}
// A lock was acquired, so we need to unlock when we're done.
cleanup = func() error {
p.mu.Unlock()
// Use a detached context to unlock. This is because the context passed to Lock may
// have been canceled, and we don't want to cancel the unlock.
return multierr.Append(
l.Unlock(context.WithoutCancel(ctx), p.db),
conn.Close(),
)
}
}
}
// If versioning is enabled, ensure the version table exists. For ad-hoc migrations, we don't
// need the version table because no versions are being tracked.
if !p.cfg.disableVersioning {
if err := p.ensureVersionTable(ctx, conn); err != nil {
return nil, nil, multierr.Append(err, cleanup())
}
}
return conn, cleanup, nil
}
func (p *Provider) ensureVersionTable(
ctx context.Context,
conn *sql.Conn,
) (retErr error) {
// There are 2 optimizations here:
// - 1. We create the version table once per Provider instance.
// - 2. We retry the operation a few times in case the table is being created concurrently.
//
// Regarding item 2, certain goose operations, like HasPending, don't respect a SessionLocker.
// So, when goose is run for the first time in a multi-instance environment, it's possible that
// multiple instances will try to create the version table at the same time. This is why we
// retry this operation a few times. Best case, the table is created by one instance and all the
// other instances see that change immediately. Worst case, all instances try to create the
// table at the same time, but only one will succeed and the others will retry.
p.versionTableOnce.Do(func() {
retErr = p.tryEnsureVersionTable(ctx, conn)
})
return retErr
}
func (p *Provider) tryEnsureVersionTable(ctx context.Context, conn *sql.Conn) error {
b := retry.NewConstant(1 * time.Second)
b = retry.WithMaxRetries(3, b)
return retry.Do(ctx, b, func(ctx context.Context) error {
exists, err := p.store.TableExists(ctx, conn)
if err == nil && exists {
return nil
} else if err != nil && errors.Is(err, errors.ErrUnsupported) {
// Fallback strategy for checking table existence:
//
// When direct table existence checks aren't supported, we attempt to query the initial
// migration (version 0). This approach has two implications:
//
// 1. If the table exists, the query succeeds and confirms existence
// 2. If the table doesn't exist, the query fails and generates an error log
//
// Note: This check must occur outside any transaction, as a failed query would
// otherwise cause the entire transaction to roll back. The error logs generated by this
// approach are expected and can be safely ignored.
if res, err := p.store.GetMigration(ctx, conn, 0); err == nil && res != nil {
return nil
}
// Fallthrough to create the table.
} else if err != nil {
return fmt.Errorf("check if version table exists: %w", err)
}
if p.cfg.isolateDDL {
// If isolation is enabled, we create the version table separately to ensure subsequent
// DML operations are not mixed with DDL.
if err := p.store.CreateVersionTable(ctx, conn); err != nil {
return retry.RetryableError(fmt.Errorf("create version table: %w", err))
}
if err := p.store.Insert(ctx, conn, database.InsertRequest{Version: 0}); err != nil {
return retry.RetryableError(fmt.Errorf("insert zero version: %w", err))
}
} else {
// If DDL isolation is not enabled, we can create the version table and insert the zero
// version in a single transaction.
if err := beginTx(ctx, conn, func(tx *sql.Tx) error {
if err := p.store.CreateVersionTable(ctx, tx); err != nil {
return err
}
return p.store.Insert(ctx, tx, database.InsertRequest{Version: 0})
}); err != nil {
// Mark the error as retryable so we can try again. It's possible that another
// instance is creating the table at the same time and the checks above will succeed
// on the next iteration.
return retry.RetryableError(fmt.Errorf("create version table: %w", err))
}
}
return nil
})
}
// getMigration returns the migration for the given version. If no migration is found, then
// ErrVersionNotFound is returned.
func (p *Provider) getMigration(version int64) (*Migration, error) {
for _, m := range p.migrations {
if m.Version == version {
return m, nil
}
}
return nil, ErrVersionNotFound
}
// useTx is a helper function that returns true if the migration should be run in a transaction. It
// must only be called after the migration has been parsed and initialized.
func useTx(m *Migration, direction bool) (bool, error) {
switch m.Type {
case TypeGo:
if m.goUp.Mode == 0 || m.goDown.Mode == 0 {
return false, fmt.Errorf("go migrations must have a mode set")
}
if direction {
return m.goUp.Mode == TransactionEnabled, nil
}
return m.goDown.Mode == TransactionEnabled, nil
case TypeSQL:
if !m.sql.Parsed {
return false, fmt.Errorf("sql migrations must be parsed")
}
return m.sql.UseTx, nil
}
return false, fmt.Errorf("use tx: invalid migration type: %q", m.Type)
}
// isEmpty is a helper function that returns true if the migration has no functions or no statements
// to execute. It must only be called after the migration has been parsed and initialized.
func isEmpty(m *Migration, direction bool) bool {
switch m.Type {
case TypeGo:
if direction {
return m.goUp.RunTx == nil && m.goUp.RunDB == nil
}
return m.goDown.RunTx == nil && m.goDown.RunDB == nil
case TypeSQL:
if direction {
return len(m.sql.Up) == 0
}
return len(m.sql.Down) == 0
}
return true
}
// runMigration is a helper function that runs the migration in the given direction. It must only be
// called after the migration has been parsed and initialized.
func (p *Provider) runMigration(ctx context.Context, db database.DBTxConn, m *Migration, direction bool) error {
switch m.Type {
case TypeGo:
return p.runGo(ctx, db, m, direction)
case TypeSQL:
return p.runSQL(ctx, db, m, direction)
}
return fmt.Errorf("invalid migration type: %q", m.Type)
}
// runGo is a helper function that runs the given Go functions in the given direction. It must only
// be called after the migration has been initialized.
func (p *Provider) runGo(ctx context.Context, db database.DBTxConn, m *Migration, direction bool) (retErr error) {
defer func() {
if r := recover(); r != nil {
retErr = fmt.Errorf("panic: %v\n%s", r, debug.Stack())
}
}()
switch db := db.(type) {
case *sql.Conn:
return fmt.Errorf("go migrations are not supported with *sql.Conn")
case *sql.DB:
if direction && m.goUp.RunDB != nil {
return m.goUp.RunDB(ctx, db)
}
if !direction && m.goDown.RunDB != nil {
return m.goDown.RunDB(ctx, db)
}
return nil
case *sql.Tx:
if direction && m.goUp.RunTx != nil {
return m.goUp.RunTx(ctx, db)
}
if !direction && m.goDown.RunTx != nil {
return m.goDown.RunTx(ctx, db)
}
return nil
}
return fmt.Errorf("invalid database connection type: %T", db)
}
// runSQL is a helper function that runs the given SQL statements in the given direction. It must
// only be called after the migration has been parsed.
func (p *Provider) runSQL(ctx context.Context, db database.DBTxConn, m *Migration, direction bool) error {
if !m.sql.Parsed {
return fmt.Errorf("sql migrations must be parsed")
}
var statements []string
if direction {
statements = m.sql.Up
} else {
statements = m.sql.Down
}
for _, stmt := range statements {
p.logf(ctx,
fmt.Sprintf("Executing statement: %s", stmt),
"executing statement",
slog.String("statement", stmt),
slog.String("source", filepath.Base(m.Source)),
slog.Int64("version", m.Version),
slog.String("type", string(m.Type)),
slog.String("direction", string(sqlparser.FromBool(direction))),
)
if _, err := db.ExecContext(ctx, stmt); err != nil {
return err
}
}
return nil
}

91
vendor/github.com/pressly/goose/v3/provider_types.go generated vendored Normal file
View file

@ -0,0 +1,91 @@
package goose
import (
"fmt"
"path/filepath"
"time"
)
// MigrationType is the type of migration.
type MigrationType string
const (
TypeGo MigrationType = "go"
TypeSQL MigrationType = "sql"
)
// Source represents a single migration source.
//
// The Path field may be empty if the migration was registered manually. This is typically the case
// for Go migrations registered using the [WithGoMigration] option.
type Source struct {
Type MigrationType
Path string
Version int64
}
// MigrationResult is the result of a single migration operation.
type MigrationResult struct {
Source *Source
Duration time.Duration
Direction string
// Empty indicates no action was taken during the migration, but it was still versioned. For
// SQL, it means no statements; for Go, it's a nil function.
Empty bool
// Error is only set if the migration failed.
Error error
}
// String returns a string representation of the migration result.
//
// Example down:
//
// EMPTY down 00006_posts_view-copy.sql (607.83µs)
// OK down 00005_posts_view.sql (646.25µs)
//
// Example up:
//
// OK up 00005_posts_view.sql (727.5µs)
// EMPTY up 00006_posts_view-copy.sql (378.33µs)
func (m *MigrationResult) String() string {
var format string
if m.Direction == "up" {
format = "%-5s %-2s %s (%s)"
} else {
format = "%-5s %-4s %s (%s)"
}
var state string
if m.Empty {
state = "EMPTY"
} else {
state = "OK"
}
return fmt.Sprintf(format,
state,
m.Direction,
filepath.Base(m.Source.Path),
truncateDuration(m.Duration),
)
}
// State represents the state of a migration.
type State string
const (
// StatePending is a migration that exists on the filesystem, but not in the database.
StatePending State = "pending"
// StateApplied is a migration that has been applied to the database and exists on the
// filesystem.
StateApplied State = "applied"
// TODO(mf): we could also add a third state for untracked migrations. This would be useful for
// migrations that were manually applied to the database, but not versioned. Or the Source was
// deleted, but the migration still exists in the database. StateUntracked State = "untracked"
)
// MigrationStatus represents the status of a single migration.
type MigrationStatus struct {
Source *Source
State State
AppliedAt time.Time
}

51
vendor/github.com/pressly/goose/v3/redo.go generated vendored Normal file
View file

@ -0,0 +1,51 @@
package goose
import (
"context"
"database/sql"
)
// Redo rolls back the most recently applied migration, then runs it again.
func Redo(db *sql.DB, dir string, opts ...OptionsFunc) error {
ctx := context.Background()
return RedoContext(ctx, db, dir, opts...)
}
// RedoContext rolls back the most recently applied migration, then runs it again.
func RedoContext(ctx context.Context, db *sql.DB, dir string, opts ...OptionsFunc) error {
option := &options{}
for _, f := range opts {
f(option)
}
migrations, err := CollectMigrations(dir, minVersion, maxVersion)
if err != nil {
return err
}
var (
currentVersion int64
)
if option.noVersioning {
if len(migrations) == 0 {
return nil
}
currentVersion = migrations[len(migrations)-1].Version
} else {
if currentVersion, err = GetDBVersionContext(ctx, db); err != nil {
return err
}
}
current, err := migrations.Current(currentVersion)
if err != nil {
return err
}
current.noVersioning = option.noVersioning
if err := current.DownContext(ctx, db); err != nil {
return err
}
if err := current.UpContext(ctx, db); err != nil {
return err
}
return nil
}

133
vendor/github.com/pressly/goose/v3/register.go generated vendored Normal file
View file

@ -0,0 +1,133 @@
package goose
import (
"context"
"database/sql"
"fmt"
"runtime"
)
// GoMigrationContext is a Go migration func that is run within a transaction and receives a
// context.
type GoMigrationContext func(ctx context.Context, tx *sql.Tx) error
// AddMigrationContext adds Go migrations.
func AddMigrationContext(up, down GoMigrationContext) {
_, filename, _, _ := runtime.Caller(1)
AddNamedMigrationContext(filename, up, down)
}
// AddNamedMigrationContext adds named Go migrations.
func AddNamedMigrationContext(filename string, up, down GoMigrationContext) {
if err := register(
filename,
true,
&GoFunc{RunTx: up, Mode: TransactionEnabled},
&GoFunc{RunTx: down, Mode: TransactionEnabled},
); err != nil {
panic(err)
}
}
// GoMigrationNoTxContext is a Go migration func that is run outside a transaction and receives a
// context.
type GoMigrationNoTxContext func(ctx context.Context, db *sql.DB) error
// AddMigrationNoTxContext adds Go migrations that will be run outside transaction.
func AddMigrationNoTxContext(up, down GoMigrationNoTxContext) {
_, filename, _, _ := runtime.Caller(1)
AddNamedMigrationNoTxContext(filename, up, down)
}
// AddNamedMigrationNoTxContext adds named Go migrations that will be run outside transaction.
func AddNamedMigrationNoTxContext(filename string, up, down GoMigrationNoTxContext) {
if err := register(
filename,
false,
&GoFunc{RunDB: up, Mode: TransactionDisabled},
&GoFunc{RunDB: down, Mode: TransactionDisabled},
); err != nil {
panic(err)
}
}
func register(filename string, useTx bool, up, down *GoFunc) error {
v, _ := NumericComponent(filename)
if existing, ok := registeredGoMigrations[v]; ok {
return fmt.Errorf("failed to add migration %q: version %d conflicts with %q",
filename,
v,
existing.Source,
)
}
// Add to global as a registered migration.
m := NewGoMigration(v, up, down)
m.Source = filename
// We explicitly set transaction to maintain existing behavior. Both up and down may be nil, but
// we know based on the register function what the user is requesting.
m.UseTx = useTx
registeredGoMigrations[v] = m
return nil
}
// withContext changes the signature of a function that receives one argument to receive a context
// and the argument.
func withContext[T any](fn func(T) error) func(context.Context, T) error {
if fn == nil {
return nil
}
return func(ctx context.Context, t T) error {
return fn(t)
}
}
// withoutContext changes the signature of a function that receives a context and one argument to
// receive only the argument. When called the passed context is always context.Background().
func withoutContext[T any](fn func(context.Context, T) error) func(T) error {
if fn == nil {
return nil
}
return func(t T) error {
return fn(context.Background(), t)
}
}
// GoMigration is a Go migration func that is run within a transaction.
//
// Deprecated: Use GoMigrationContext.
type GoMigration func(tx *sql.Tx) error
// GoMigrationNoTx is a Go migration func that is run outside a transaction.
//
// Deprecated: Use GoMigrationNoTxContext.
type GoMigrationNoTx func(db *sql.DB) error
// AddMigration adds Go migrations.
//
// Deprecated: Use AddMigrationContext.
func AddMigration(up, down GoMigration) {
_, filename, _, _ := runtime.Caller(1)
AddNamedMigrationContext(filename, withContext(up), withContext(down))
}
// AddNamedMigration adds named Go migrations.
//
// Deprecated: Use AddNamedMigrationContext.
func AddNamedMigration(filename string, up, down GoMigration) {
AddNamedMigrationContext(filename, withContext(up), withContext(down))
}
// AddMigrationNoTx adds Go migrations that will be run outside transaction.
//
// Deprecated: Use AddMigrationNoTxContext.
func AddMigrationNoTx(up, down GoMigrationNoTx) {
_, filename, _, _ := runtime.Caller(1)
AddNamedMigrationNoTxContext(filename, withContext(up), withContext(down))
}
// AddNamedMigrationNoTx adds named Go migrations that will be run outside transaction.
//
// Deprecated: Use AddNamedMigrationNoTxContext.
func AddNamedMigrationNoTx(filename string, up, down GoMigrationNoTx) {
AddNamedMigrationNoTxContext(filename, withContext(up), withContext(down))
}

64
vendor/github.com/pressly/goose/v3/reset.go generated vendored Normal file
View file

@ -0,0 +1,64 @@
package goose
import (
"context"
"database/sql"
"fmt"
"sort"
)
// Reset rolls back all migrations
func Reset(db *sql.DB, dir string, opts ...OptionsFunc) error {
ctx := context.Background()
return ResetContext(ctx, db, dir, opts...)
}
// ResetContext rolls back all migrations
func ResetContext(ctx context.Context, db *sql.DB, dir string, opts ...OptionsFunc) error {
option := &options{}
for _, f := range opts {
f(option)
}
migrations, err := CollectMigrations(dir, minVersion, maxVersion)
if err != nil {
return fmt.Errorf("failed to collect migrations: %w", err)
}
if option.noVersioning {
return DownToContext(ctx, db, dir, minVersion, opts...)
}
statuses, err := dbMigrationsStatus(ctx, db)
if err != nil {
return fmt.Errorf("failed to get status of migrations: %w", err)
}
sort.Sort(sort.Reverse(migrations))
for _, migration := range migrations {
if !statuses[migration.Version] {
continue
}
if err = migration.DownContext(ctx, db); err != nil {
return fmt.Errorf("failed to db-down: %w", err)
}
}
return nil
}
func dbMigrationsStatus(ctx context.Context, db *sql.DB) (map[int64]bool, error) {
dbMigrations, err := store.ListMigrations(ctx, db, TableName())
if err != nil {
return nil, err
}
// The most recent record for each migration specifies
// whether it has been applied or rolled back.
results := make(map[int64]bool)
for _, m := range dbMigrations {
if _, ok := results[m.VersionID]; ok {
continue
}
results[m.VersionID] = m.IsApplied
}
return results, nil
}

64
vendor/github.com/pressly/goose/v3/status.go generated vendored Normal file
View file

@ -0,0 +1,64 @@
package goose
import (
"context"
"database/sql"
"errors"
"fmt"
"path/filepath"
"time"
)
// Status prints the status of all migrations.
func Status(db *sql.DB, dir string, opts ...OptionsFunc) error {
ctx := context.Background()
return StatusContext(ctx, db, dir, opts...)
}
// StatusContext prints the status of all migrations.
func StatusContext(ctx context.Context, db *sql.DB, dir string, opts ...OptionsFunc) error {
option := &options{}
for _, f := range opts {
f(option)
}
migrations, err := CollectMigrations(dir, minVersion, maxVersion)
if err != nil {
return fmt.Errorf("failed to collect migrations: %w", err)
}
if option.noVersioning {
log.Printf(" Applied At Migration")
log.Printf(" =======================================")
for _, current := range migrations {
log.Printf(" %-24s -- %v", "no versioning", filepath.Base(current.Source))
}
return nil
}
// must ensure that the version table exists if we're running on a pristine DB
if _, err := EnsureDBVersionContext(ctx, db); err != nil {
return fmt.Errorf("failed to ensure DB version: %w", err)
}
log.Printf(" Applied At Migration")
log.Printf(" =======================================")
for _, migration := range migrations {
if err := printMigrationStatus(ctx, db, migration.Version, filepath.Base(migration.Source)); err != nil {
return fmt.Errorf("failed to print status: %w", err)
}
}
return nil
}
func printMigrationStatus(ctx context.Context, db *sql.DB, version int64, script string) error {
m, err := store.GetMigration(ctx, db, TableName(), version)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("failed to query the latest migration: %w", err)
}
appliedAt := "Pending"
if m != nil && m.IsApplied {
appliedAt = m.Timestamp.Format(time.ANSIC)
}
log.Printf(" %-24s -- %v", appliedAt, script)
return nil
}

221
vendor/github.com/pressly/goose/v3/up.go generated vendored Normal file
View file

@ -0,0 +1,221 @@
package goose
import (
"context"
"database/sql"
"fmt"
"sort"
"strings"
)
type options struct {
allowMissing bool
applyUpByOne bool
noVersioning bool
}
type OptionsFunc func(o *options)
func WithAllowMissing() OptionsFunc {
return func(o *options) { o.allowMissing = true }
}
func WithNoVersioning() OptionsFunc {
return func(o *options) { o.noVersioning = true }
}
func WithNoColor(b bool) OptionsFunc {
return func(o *options) { noColor = b }
}
func withApplyUpByOne() OptionsFunc {
return func(o *options) { o.applyUpByOne = true }
}
// UpTo migrates up to a specific version.
func UpTo(db *sql.DB, dir string, version int64, opts ...OptionsFunc) error {
ctx := context.Background()
return UpToContext(ctx, db, dir, version, opts...)
}
func UpToContext(ctx context.Context, db *sql.DB, dir string, version int64, opts ...OptionsFunc) error {
option := &options{}
for _, f := range opts {
f(option)
}
foundMigrations, err := CollectMigrations(dir, minVersion, version)
if err != nil {
return err
}
if option.noVersioning {
if len(foundMigrations) == 0 {
return nil
}
if option.applyUpByOne {
// For up-by-one this means keep re-applying the first
// migration over and over.
version = foundMigrations[0].Version
}
return upToNoVersioning(ctx, db, foundMigrations, version)
}
if _, err := EnsureDBVersionContext(ctx, db); err != nil {
return err
}
dbMigrations, err := listAllDBVersions(ctx, db)
if err != nil {
return err
}
dbMaxVersion := dbMigrations[len(dbMigrations)-1].Version
// lookupAppliedInDB is a map of all applied migrations in the database.
lookupAppliedInDB := make(map[int64]bool)
for _, m := range dbMigrations {
lookupAppliedInDB[m.Version] = true
}
missingMigrations := findMissingMigrations(dbMigrations, foundMigrations, dbMaxVersion)
// feature(mf): It is very possible someone may want to apply ONLY new migrations
// and skip missing migrations altogether. At the moment this is not supported,
// but leaving this comment because that's where that logic will be handled.
if len(missingMigrations) > 0 && !option.allowMissing {
var collected []string
for _, m := range missingMigrations {
output := fmt.Sprintf("version %d: %s", m.Version, m.Source)
collected = append(collected, output)
}
return fmt.Errorf("error: found %d missing migrations before current version %d:\n\t%s",
len(missingMigrations), dbMaxVersion, strings.Join(collected, "\n\t"))
}
var migrationsToApply Migrations
if option.allowMissing {
migrationsToApply = missingMigrations
}
// filter all migrations with a version greater than the supplied version (min) and less than or
// equal to the requested version (max). Note, we do not need to filter out missing migrations
// because we are only appending "new" migrations that have a higher version than the current
// database max version, which inevitably means they are not "missing".
for _, m := range foundMigrations {
if lookupAppliedInDB[m.Version] {
continue
}
if m.Version > dbMaxVersion && m.Version <= version {
migrationsToApply = append(migrationsToApply, m)
}
}
var current int64
for _, m := range migrationsToApply {
if err := m.UpContext(ctx, db); err != nil {
return err
}
if option.applyUpByOne {
return nil
}
current = m.Version
}
if len(migrationsToApply) == 0 {
current, err = GetDBVersionContext(ctx, db)
if err != nil {
return err
}
log.Printf("goose: no migrations to run. current version: %d", current)
} else {
log.Printf("goose: successfully migrated database to version: %d", current)
}
// At this point there are no more migrations to apply. But we need to maintain
// the following behaviour:
// UpByOne returns an error to signifying there are no more migrations.
// Up and UpTo return nil
if option.applyUpByOne {
return ErrNoNextVersion
}
return nil
}
// upToNoVersioning applies up migrations up to, and including, the
// target version.
func upToNoVersioning(ctx context.Context, db *sql.DB, migrations Migrations, version int64) error {
var finalVersion int64
for _, current := range migrations {
if current.Version > version {
break
}
current.noVersioning = true
if err := current.UpContext(ctx, db); err != nil {
return err
}
finalVersion = current.Version
}
log.Printf("goose: up to current file version: %d", finalVersion)
return nil
}
// Up applies all available migrations.
func Up(db *sql.DB, dir string, opts ...OptionsFunc) error {
ctx := context.Background()
return UpContext(ctx, db, dir, opts...)
}
// UpContext applies all available migrations.
func UpContext(ctx context.Context, db *sql.DB, dir string, opts ...OptionsFunc) error {
return UpToContext(ctx, db, dir, maxVersion, opts...)
}
// UpByOne migrates up by a single version.
func UpByOne(db *sql.DB, dir string, opts ...OptionsFunc) error {
ctx := context.Background()
return UpByOneContext(ctx, db, dir, opts...)
}
// UpByOneContext migrates up by a single version.
func UpByOneContext(ctx context.Context, db *sql.DB, dir string, opts ...OptionsFunc) error {
opts = append(opts, withApplyUpByOne())
return UpToContext(ctx, db, dir, maxVersion, opts...)
}
// listAllDBVersions returns a list of all migrations, ordered ascending.
func listAllDBVersions(ctx context.Context, db *sql.DB) (Migrations, error) {
dbMigrations, err := store.ListMigrations(ctx, db, TableName())
if err != nil {
return nil, err
}
all := make(Migrations, 0, len(dbMigrations))
for _, m := range dbMigrations {
all = append(all, &Migration{
Version: m.VersionID,
})
}
// ListMigrations returns migrations in descending order by id.
// But we want to return them in ascending order by version_id, so we re-sort.
sort.SliceStable(all, func(i, j int) bool {
return all[i].Version < all[j].Version
})
return all, nil
}
// findMissingMigrations migrations returns all missing migrations.
// A migrations is considered missing if it has a version less than the
// current known max version.
func findMissingMigrations(knownMigrations, newMigrations Migrations, dbMaxVersion int64) Migrations {
existing := make(map[int64]bool)
for _, known := range knownMigrations {
existing[known.Version] = true
}
var missing Migrations
for _, new := range newMigrations {
if !existing[new.Version] && new.Version < dbMaxVersion {
missing = append(missing, new)
}
}
sort.SliceStable(missing, func(i, j int) bool {
return missing[i].Version < missing[j].Version
})
return missing
}

52
vendor/github.com/pressly/goose/v3/version.go generated vendored Normal file
View file

@ -0,0 +1,52 @@
package goose
import (
"context"
"database/sql"
"fmt"
)
// Version prints the current version of the database.
func Version(db *sql.DB, dir string, opts ...OptionsFunc) error {
ctx := context.Background()
return VersionContext(ctx, db, dir, opts...)
}
// VersionContext prints the current version of the database.
func VersionContext(ctx context.Context, db *sql.DB, dir string, opts ...OptionsFunc) error {
option := &options{}
for _, f := range opts {
f(option)
}
if option.noVersioning {
var current int64
migrations, err := CollectMigrations(dir, minVersion, maxVersion)
if err != nil {
return fmt.Errorf("failed to collect migrations: %w", err)
}
if len(migrations) > 0 {
current = migrations[len(migrations)-1].Version
}
log.Printf("goose: file version %v", current)
return nil
}
current, err := GetDBVersionContext(ctx, db)
if err != nil {
return err
}
log.Printf("goose: version %v", current)
return nil
}
var tableName = "goose_db_version"
// TableName returns goose db version table name
func TableName() string {
return tableName
}
// SetTableName set goose db version table name
func SetTableName(n string) {
tableName = n
}