Add vendor directory and update vendorHash to null
This commit is contained in:
parent
523831cb8d
commit
1e5424c844
778 changed files with 407919 additions and 1 deletions
22
vendor/github.com/pressly/goose/v3/.gitignore
generated
vendored
Normal file
22
vendor/github.com/pressly/goose/v3/.gitignore
generated
vendored
Normal 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
31
vendor/github.com/pressly/goose/v3/.golangci.yaml
generated
vendored
Normal 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
37
vendor/github.com/pressly/goose/v3/.goreleaser.yaml
generated
vendored
Normal 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
316
vendor/github.com/pressly/goose/v3/CHANGELOG.md
generated
vendored
Normal 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
22
vendor/github.com/pressly/goose/v3/LICENSE
generated
vendored
Normal 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
147
vendor/github.com/pressly/goose/v3/Makefile
generated
vendored
Normal 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
497
vendor/github.com/pressly/goose/v3/README.md
generated
vendored
Normal file
|
|
@ -0,0 +1,497 @@
|
|||
# goose
|
||||
|
||||
<img align="right" width="125" src="assets/goose_logo.png">
|
||||
|
||||
[](https://github.com/pressly/goose/actions/workflows/ci.yaml)
|
||||
[](https://pkg.go.dev/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> • </span> [up](#up)<span> • </span> [up-to](#up-to)<span> • </span> [down](#down)<span> • </span> [down-to](#down-to)<span> • </span> [status](#status)<span> • </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
120
vendor/github.com/pressly/goose/v3/create.go
generated
vendored
Normal 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
|
||||
}
|
||||
`))
|
||||
25
vendor/github.com/pressly/goose/v3/database/dialect/querier.go
generated
vendored
Normal file
25
vendor/github.com/pressly/goose/v3/database/dialect/querier.go
generated
vendored
Normal 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
|
||||
}
|
||||
22
vendor/github.com/pressly/goose/v3/database/dialect/querier_extended.go
generated
vendored
Normal file
22
vendor/github.com/pressly/goose/v3/database/dialect/querier_extended.go
generated
vendored
Normal 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
212
vendor/github.com/pressly/goose/v3/database/dialects.go
generated
vendored
Normal 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
14
vendor/github.com/pressly/goose/v3/database/doc.go
generated
vendored
Normal 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
|
||||
23
vendor/github.com/pressly/goose/v3/database/sql_extended.go
generated
vendored
Normal file
23
vendor/github.com/pressly/goose/v3/database/sql_extended.go
generated
vendored
Normal 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
66
vendor/github.com/pressly/goose/v3/database/store.go
generated
vendored
Normal 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
|
||||
}
|
||||
33
vendor/github.com/pressly/goose/v3/database/store_extended.go
generated
vendored
Normal file
33
vendor/github.com/pressly/goose/v3/database/store_extended.go
generated
vendored
Normal 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
46
vendor/github.com/pressly/goose/v3/db.go
generated
vendored
Normal 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
71
vendor/github.com/pressly/goose/v3/dialect.go
generated
vendored
Normal 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
107
vendor/github.com/pressly/goose/v3/down.go
generated
vendored
Normal 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
54
vendor/github.com/pressly/goose/v3/fix.go
generated
vendored
Normal 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
104
vendor/github.com/pressly/goose/v3/globals.go
generated
vendored
Normal 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
141
vendor/github.com/pressly/goose/v3/goose.go
generated
vendored
Normal 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
84
vendor/github.com/pressly/goose/v3/helpers.go
generated
vendored
Normal 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
37
vendor/github.com/pressly/goose/v3/install.sh
generated
vendored
Normal 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
|
||||
37
vendor/github.com/pressly/goose/v3/internal/controller/store.go
generated
vendored
Normal file
37
vendor/github.com/pressly/goose/v3/internal/controller/store.go
generated
vendored
Normal 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
|
||||
}
|
||||
53
vendor/github.com/pressly/goose/v3/internal/dialects/clickhouse.go
generated
vendored
Normal file
53
vendor/github.com/pressly/goose/v3/internal/dialects/clickhouse.go
generated
vendored
Normal 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)
|
||||
}
|
||||
66
vendor/github.com/pressly/goose/v3/internal/dialects/dsql.go
generated
vendored
Normal file
66
vendor/github.com/pressly/goose/v3/internal/dialects/dsql.go
generated
vendored
Normal 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)
|
||||
}
|
||||
62
vendor/github.com/pressly/goose/v3/internal/dialects/mysql.go
generated
vendored
Normal file
62
vendor/github.com/pressly/goose/v3/internal/dialects/mysql.go
generated
vendored
Normal 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)
|
||||
}
|
||||
70
vendor/github.com/pressly/goose/v3/internal/dialects/postgres.go
generated
vendored
Normal file
70
vendor/github.com/pressly/goose/v3/internal/dialects/postgres.go
generated
vendored
Normal 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
|
||||
}
|
||||
52
vendor/github.com/pressly/goose/v3/internal/dialects/redshift.go
generated
vendored
Normal file
52
vendor/github.com/pressly/goose/v3/internal/dialects/redshift.go
generated
vendored
Normal 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)
|
||||
}
|
||||
51
vendor/github.com/pressly/goose/v3/internal/dialects/sqlite3.go
generated
vendored
Normal file
51
vendor/github.com/pressly/goose/v3/internal/dialects/sqlite3.go
generated
vendored
Normal 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)
|
||||
}
|
||||
51
vendor/github.com/pressly/goose/v3/internal/dialects/sqlserver.go
generated
vendored
Normal file
51
vendor/github.com/pressly/goose/v3/internal/dialects/sqlserver.go
generated
vendored
Normal 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)
|
||||
}
|
||||
54
vendor/github.com/pressly/goose/v3/internal/dialects/starrocks.go
generated
vendored
Normal file
54
vendor/github.com/pressly/goose/v3/internal/dialects/starrocks.go
generated
vendored
Normal 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)
|
||||
}
|
||||
52
vendor/github.com/pressly/goose/v3/internal/dialects/tidb.go
generated
vendored
Normal file
52
vendor/github.com/pressly/goose/v3/internal/dialects/tidb.go
generated
vendored
Normal 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)
|
||||
}
|
||||
14
vendor/github.com/pressly/goose/v3/internal/dialects/turso.go
generated
vendored
Normal file
14
vendor/github.com/pressly/goose/v3/internal/dialects/turso.go
generated
vendored
Normal 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)
|
||||
54
vendor/github.com/pressly/goose/v3/internal/dialects/vertica.go
generated
vendored
Normal file
54
vendor/github.com/pressly/goose/v3/internal/dialects/vertica.go
generated
vendored
Normal 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)
|
||||
}
|
||||
62
vendor/github.com/pressly/goose/v3/internal/dialects/ydb.go
generated
vendored
Normal file
62
vendor/github.com/pressly/goose/v3/internal/dialects/ydb.go
generated
vendored
Normal 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)
|
||||
}
|
||||
124
vendor/github.com/pressly/goose/v3/internal/gooseutil/resolve.go
generated
vendored
Normal file
124
vendor/github.com/pressly/goose/v3/internal/gooseutil/resolve.go
generated
vendored
Normal 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]
|
||||
})
|
||||
}
|
||||
171
vendor/github.com/pressly/goose/v3/internal/legacystore/legacystore.go
generated
vendored
Normal file
171
vendor/github.com/pressly/goose/v3/internal/legacystore/legacystore.go
generated
vendored
Normal 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(×tamp, &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
|
||||
}
|
||||
59
vendor/github.com/pressly/goose/v3/internal/sqlparser/parse.go
generated
vendored
Normal file
59
vendor/github.com/pressly/goose/v3/internal/sqlparser/parse.go
generated
vendored
Normal 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
|
||||
}
|
||||
394
vendor/github.com/pressly/goose/v3/internal/sqlparser/parser.go
generated
vendored
Normal file
394
vendor/github.com/pressly/goose/v3/internal/sqlparser/parser.go
generated
vendored
Normal 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, ";")
|
||||
}
|
||||
282
vendor/github.com/pressly/goose/v3/lock/internal/store/postgres.go
generated
vendored
Normal file
282
vendor/github.com/pressly/goose/v3/lock/internal/store/postgres.go
generated
vendored
Normal 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
|
||||
}
|
||||
51
vendor/github.com/pressly/goose/v3/lock/internal/store/store.go
generated
vendored
Normal file
51
vendor/github.com/pressly/goose/v3/lock/internal/store/store.go
generated
vendored
Normal 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
|
||||
}
|
||||
28
vendor/github.com/pressly/goose/v3/lock/internal/table/config.go
generated
vendored
Normal file
28
vendor/github.com/pressly/goose/v3/lock/internal/table/config.go
generated
vendored
Normal 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
|
||||
}
|
||||
237
vendor/github.com/pressly/goose/v3/lock/internal/table/locker.go
generated
vendored
Normal file
237
vendor/github.com/pressly/goose/v3/lock/internal/table/locker.go
generated
vendored
Normal 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
32
vendor/github.com/pressly/goose/v3/lock/locker.go
generated
vendored
Normal 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
165
vendor/github.com/pressly/goose/v3/lock/postgres.go
generated
vendored
Normal 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"))
|
||||
})
|
||||
}
|
||||
98
vendor/github.com/pressly/goose/v3/lock/session_locker_options.go
generated
vendored
Normal file
98
vendor/github.com/pressly/goose/v3/lock/session_locker_options.go
generated
vendored
Normal 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)
|
||||
}
|
||||
136
vendor/github.com/pressly/goose/v3/lock/table_locker_options.go
generated
vendored
Normal file
136
vendor/github.com/pressly/goose/v3/lock/table_locker_options.go
generated
vendored
Normal 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
36
vendor/github.com/pressly/goose/v3/log.go
generated
vendored
Normal 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
379
vendor/github.com/pressly/goose/v3/migrate.go
generated
vendored
Normal 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
398
vendor/github.com/pressly/goose/v3/migration.go
generated
vendored
Normal 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
115
vendor/github.com/pressly/goose/v3/migration_sql.go
generated
vendored
Normal 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
28
vendor/github.com/pressly/goose/v3/osfs.go
generated
vendored
Normal 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
670
vendor/github.com/pressly/goose/v3/provider.go
generated
vendored
Normal 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
196
vendor/github.com/pressly/goose/v3/provider_collect.go
generated
vendored
Normal 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
48
vendor/github.com/pressly/goose/v3/provider_errors.go
generated
vendored
Normal 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
280
vendor/github.com/pressly/goose/v3/provider_options.go
generated
vendored
Normal 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
535
vendor/github.com/pressly/goose/v3/provider_run.go
generated
vendored
Normal 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
91
vendor/github.com/pressly/goose/v3/provider_types.go
generated
vendored
Normal 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
51
vendor/github.com/pressly/goose/v3/redo.go
generated
vendored
Normal 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
133
vendor/github.com/pressly/goose/v3/register.go
generated
vendored
Normal 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
64
vendor/github.com/pressly/goose/v3/reset.go
generated
vendored
Normal 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
64
vendor/github.com/pressly/goose/v3/status.go
generated
vendored
Normal 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
221
vendor/github.com/pressly/goose/v3/up.go
generated
vendored
Normal 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
52
vendor/github.com/pressly/goose/v3/version.go
generated
vendored
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue