[
  {
    "path": ".github/dependabot.yaml",
    "content": "version: 2\n\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n      day: \"saturday\"\n    assignees:\n      - \"mfridman\"\n  - package-ecosystem: \"gomod\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n      day: \"saturday\"\n    groups:\n      gomod:\n        patterns:\n          - \"*\"\n    assignees:\n      - \"mfridman\"\n    ignore:\n      - dependency-name: \"*\"\n        update-types: [\"version-update:semver-patch\"]\n"
  },
  {
    "path": ".github/workflows/ci.yaml",
    "content": "name: Goose CI\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n  workflow_dispatch:\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  test:\n    name: Run unit tests\n    timeout-minutes: 10\n\n    strategy:\n      matrix:\n        go-version: [oldstable, stable]\n        os: [ubuntu-latest]\n\n    runs-on: ${{ matrix.os }}\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v6\n      - name: Install Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: ${{ matrix.go-version }}\n      - name: Check Go code formatting\n        run: |\n          if [ \"$(gofmt -s -l . | wc -l)\" -gt 0 ]; then\n            gofmt -s -l .\n            echo \"Please format Go code by running: go fmt ./...\"\n            exit 1\n          fi\n      - name: Install tparse\n        run: |\n          mkdir -p $HOME/.local/bin\n          curl -L -o $HOME/.local/bin/tparse https://github.com/mfridman/tparse/releases/latest/download/tparse_linux_x86_64\n          chmod +x $HOME/.local/bin/tparse\n          echo \"$HOME/.local/bin\" >> \"$GITHUB_PATH\"\n      - name: Run tests\n        run: |\n          make add-gowork\n          mkdir -p bin\n          go vet ./...\n          go build ./...\n          make test-packages\n      - name: Install GoReleaser\n        if: github.event_name == 'push' && github.ref == 'refs/heads/main' && matrix.go-version == 'stable'\n        uses: goreleaser/goreleaser-action@v7\n        with:\n          install-only: true\n          distribution: goreleaser\n          version: \"~> v2\"\n      - name: Gorelease dry-run\n        if: github.event_name == 'push' && github.ref == 'refs/heads/main' && matrix.go-version == 'stable'\n        run: |\n          goreleaser release --skip=publish --snapshot --fail-fast --clean\n"
  },
  {
    "path": ".github/workflows/integration.yaml",
    "content": "name: Goose integration tests\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n  workflow_dispatch:\n\nconcurrency: \n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  test:\n    name: Run integration tests\n    timeout-minutes: 10\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v6\n      - name: Install Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: \"stable\"\n      - name: Install tparse\n        run: |\n          mkdir -p $HOME/.local/bin\n          curl -L -o $HOME/.local/bin/tparse https://github.com/mfridman/tparse/releases/latest/download/tparse_linux_x86_64\n          chmod +x $HOME/.local/bin/tparse\n          echo \"$HOME/.local/bin\" >> \"$GITHUB_PATH\"\n      - name: Run full integration tests\n        run: |\n          make test-integration\n"
  },
  {
    "path": ".github/workflows/lint.yaml",
    "content": "name: golangci\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n  workflow_dispatch:\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  golangci:\n    name: lint\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v6\n      - uses: actions/setup-go@v6\n        with:\n          go-version: \"stable\"\n      - run: make add-gowork\n      - name: golangci-lint\n        uses: golangci/golangci-lint-action@v9\n        with:\n          # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version\n          version: latest\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          args: --timeout=2m --verbose\n\n          # Optional: working directory, useful for monorepos\n          # working-directory: somedir\n\n          # Optional: golangci-lint command line arguments.\n          # args: --issues-exit-code=0\n\n          # Optional: show only new issues if it's a pull request. The default value is `false`.\n          # only-new-issues: true\n\n          # Optional: if set to true then the all caching functionality will be complete disabled,\n          #           takes precedence over all other caching options.\n          # skip-cache: true\n\n          # Optional: if set to true then the action don't cache or restore ~/go/pkg.\n          # skip-pkg-cache: true\n\n          # Optional: if set to true then the action don't cache or restore ~/.cache/go-build.\n          # skip-build-cache: true\n"
  },
  {
    "path": ".github/workflows/release.yaml",
    "content": "name: goreleaser\non:\n  push:\n    tags:\n      - '*'\npermissions:\n  contents: write\njobs:\n  goreleaser:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n      - run: git fetch --force --tags\n      - uses: actions/setup-go@v6\n        with:\n          go-version: stable\n      - name: Generate release notes\n        continue-on-error: true\n        run: ./scripts/release-notes.sh ${{github.ref_name}} > ${{runner.temp}}/release_notes.txt\n      - run: make add-gowork\n      - name: Run GoReleaser\n        uses: goreleaser/goreleaser-action@v7\n        with:\n          distribution: goreleaser\n          version: \"~> v2\"\n          args: release --clean --release-notes=${{runner.temp}}/release_notes.txt\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": ".idea\n.vscode\n.DS_Store\n*.swp\n*.test\n\n# Files output by tests\n/bin\n\n# Coverage files\ncoverage.out\ncoverage.html\n\n# Local testing\n.envrc\n*.FAIL\n\ndist/\nrelease_notes.txt\n\ngo.work\ngo.work.sum\n"
  },
  {
    "path": ".golangci.yaml",
    "content": "version: \"2\"\nlinters:\n  default: none\n  enable:\n    - errcheck\n    - govet\n    - ineffassign\n    - misspell\n    - staticcheck\n    - testifylint\n    - unused\n  exclusions:\n    generated: lax\n    presets:\n      - comments\n      - common-false-positives\n      - legacy\n      - std-error-handling\n    paths:\n      - third_party$\n      - builtin$\n      - examples$\nformatters:\n  enable:\n    - gofmt\n  exclusions:\n    generated: lax\n    paths:\n      - third_party$\n      - builtin$\n      - examples$\n"
  },
  {
    "path": ".goreleaser.yaml",
    "content": "# yaml-language-server: $schema=https://goreleaser.com/static/schema.json\n#\n# See https://goreleaser.com/customization/ for more information.\nversion: 2\nproject_name: goose\n\nbefore:\n  hooks:\n    - go mod tidy\nbuilds:\n  - env:\n      - CGO_ENABLED=0\n    binary: goose\n    main: ./cmd/goose\n    goos:\n      - linux\n      - windows\n      - darwin\n    goarch:\n      - amd64\n      - arm64\n    ldflags:\n      # The v prefix is stripped by goreleaser, so we need to add it back.\n      # https://goreleaser.com/customization/templates/#fnref:version-prefix\n      - \"-s -w -X main.version=v{{ .Version }}\"\n\narchives:\n  - formats:\n      - binary\n    name_template: >-\n      {{ .ProjectName }}_{{- tolower .Os }}_{{- if eq .Arch \"amd64\" }}x86_64{{- else }}{{ .Arch }}{{ end }}\nchecksum:\n  name_template: \"checksums.txt\"\nsnapshot:\n  version_template: \"{{ incpatch .Version }}-next\"\nchangelog:\n  use: github-native\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project\nadheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [Unreleased]\n\n## [v3.27.0] - 2026-02-22\n\n### Added\n\n- Preliminary Spanner dialect support (#966)\n\n### Changed\n\n- **Minimum Go version is now 1.25**\n- SQL migration templates no longer include `StatementBegin` and `StatementEnd` annotations. These\n  are only needed for complex statements containing semicolons (e.g., stored procedures). See\n  [docs](https://pressly.github.io/goose/documentation/annotations/#complex-statements) for details.\n- Various dependency upgrades\n\n## [v3.26.0] - 2025-10-03\n\n- Add `*slog.Logger` support to goose provider via option `WithSlog` (#989)\n- Add convenience `WithTableName` provider option (#985)\n- Minor bug fixes and dependency upgrades\n- Add general purpose `Locker` interface to support DB locking with a table-based Postgres\n  implementation via `lock.NewPostgresTableLocker` (#993 for more details)\n  - Unlike `SessionLocker`, this uses the `*sql.DB` connection pool\n  - Add `WithLocker` option to goose provider\n\n## [v3.25.0] - 2025-08-24\n\n- Upgrade go deps (#976)\n- Remove references/tests for vertica and add deprecation warnings (#978)\n- Add Aurora DSQL as a new database dialect to goose `Provider` (#971)\n- Add DDL isolation support for Aurora DSQL compatibility (#970)\n- Update Apply to respect no versioning option (#950)\n- Expose dialect `Querier` (#939)\n\n## [v3.24.3]\n\n- Add `GOOSE_TABLE` environment variable -- lower priority than `-table` flag, but higher than the\n  default table name. (#932)\n- Dependency updates\n\n## [v3.24.2]\n\n- Add `TableExists` table existence check for the mysql dialect (#895)\n- Upgrade **minimum Go version to 1.23**\n- Various dependency updates\n\n## [v3.24.1]\n\n- Fix regression (`v3.23.1` and `v3.24.0`) in postgres migration table existence check for\n  non-default schema. (#882, #883, #884).\n\n## [v3.24.0]\n\n- Add support for loading environment variables from `.env` files, enabled by default.\n  - The default file name is `.env`, but can be changed with the `-env=<filename>` flag.\n  - To disable this feature, set `-env=none`.\n\n## [v3.23.1]\n\n- Store implementations can **optionally** implement the `TableExists` method to provide optimized\n  table existence checks (#860)\n  - Default postgres Store implementation updated to use `pg_tables` system catalog, more to follow\n  - Backward compatible change - existing implementations will continue to work without modification\n\n```go\nTableExists(ctx context.Context, db database.DBTxConn) (bool, error)\n```\n\n## [v3.23.0]\n\n- Add `WithLogger` to `NewProvider` to allow custom loggers (#833)\n- Update Provider `WithVerbose` behavior to log all SQL statements (#851)\n- Upgrade dependencies and rebuild binaries with latest Go version (`go1.23.3`)\n\n## [v3.22.1]\n\n- Upgrade dependencies and rebuild binaries with latest Go version (`go1.23.1`)\n\n## [v3.22.0]\n\n- Minimum Go version is now 1.21\n- Add Unwrap to PartialError (#815)\n- Allow flags anywhere on the CLI (#814)\n\n`goose` uses the default Go `flag` parsing library, which means flags **must** be defined before the\nfirst positional argument. We've updated this behavior to allow flags to be defined anywhere. For\nmore details, see [blog post](https://mfridman.com/blog/2024/allowing-flags-anywhere-on-the-cli/).\n\n- Update `WithDisableGlobalRegistry` behavior (#783). When set, this will ignore globally-registered\n  migrationse entirely instead of the previous behavior of raising an error. Specifically, the\n  following check is removed:\n\n```go\nif len(global) > 0 {\n\treturn nil, errors.New(\"global registry disabled, but provider has registered go migrations\")\n}\n```\n\nThis enables creating isolated goose provider(s) in legacy environments where global migrations may\nbe registered. Without updating this behavior, it would be impossible to use\n`WithDisableGlobalRegistry` in combination with provider-scoped `WithGoMigrations`.\n\n- Postgres, updated schema to use identity instead of serial and make `tstamp` not nullable (#556)\n\n```diff\n- id serial NOT NULL,\n+ id integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,\n\n- tstamp timestamp NULL default now(),\n+ tstamp timestamp NOT NULL DEFAULT now()\n```\n\n- MySQL, updated schema to not use SERIAL alias (#816)\n\n```diff\n- id serial NOT NULL,\n+ id bigint(20) unsigned NOT NULL AUTO_INCREMENT,\n```\n\n## [v3.21.1]\n\n- Add `GetVersions` method to `goose.Provider`, returns the current (max db) version and the latest\n  (max filesystem) version. (#756)\n- Clarify `GetLatestVersion` method MUST return `ErrVersionNotFound` if no latest migration is\n  found. Previously it was returning a -1 and nil error, which was inconsistent with the rest of the\n  API surface.\n\n- Add `GetLatestVersion` implementations to all existing dialects. This is an optimization to avoid\n  loading all migrations when only the latest version is needed. This uses the `max` function in SQL\n  to get the latest version_id irrespective of the order of applied migrations.\n  - Refactor existing portions of the code to use the new `GetLatestVersion` method.\n\n## [v3.21.0]\n\n- Retracted. Broken release, please use v3.21.1 instead.\n\n## [v3.20.0]\n\n- Expand the `Store` interface by adding a `GetLatestVersion` method and make the interface public.\n- Add a (non-blocking) method to check if there are pending migrations to the `goose.Provider`\n  (#751):\n\n```go\nfunc (p *Provider) HasPending(context.Context) (bool, error) {}\n```\n\nThe underlying implementation **does not respect the `SessionLocker`** (if one is enabled) and can\nbe used to check for pending migrations without blocking or being blocked by other operations.\n\n- The methods `.Up`, `.UpByOne`, and `.UpTo` from `goose.Provider` will invoke `.HasPending` before\n  acquiring a lock with `SessionLocker` (if enabled). This addresses an edge case in\n  Kubernetes-style deployments where newer pods with long-running migrations prevent older pods -\n  which have all known migrations applied - from starting up due to an advisory lock. For more\n  details, refer to https://github.com/pressly/goose/pull/507#discussion_r1266498077 and #751.\n- Move integration tests to `./internal/testing` and make it a separate Go module. This will allow\n  us to have a cleaner top-level go.mod file and avoid imports unrelated to the goose project. See\n  [integration/README.md](https://github.com/pressly/goose/blob/d0641b5bfb3bd5d38d95fe7a63d7ddf2d282234d/internal/testing/integration/README.md)\n  for more details. This shouldn't affect users of the goose library.\n\n## [v3.19.2] - 2024-03-13\n\n- Remove duckdb support. The driver uses Cgo and we've decided to remove it until we can find a\n  better solution. If you were using duckdb with goose, please let us know by opening an issue.\n\n## [v3.19.1] - 2024-03-11\n\n- Fix selecting dialect for `redshift`\n- Add `GOOSE_MIGRATION_DIR` documentation\n- Bump github.com/opencontainers/runc to `v1.1.12` (security fix)\n- Update CI tests for go1.22\n- Make goose annotations case-insensitive\n  - All `-- +goose` annotations are now case-insensitive. This means that `-- +goose Up` and `--\n+goose up` are now equivalent. This change was made to improve the user experience and to make the\n    annotations more consistent.\n\n## [v3.19.0] - 2024-03-11\n\n- Use [v3.19.1] instead. This was tagged but not released and does not contain release binaries.\n\n## [v3.18.0] - 2024-01-31\n\n- Add environment variable substitution for SQL migrations. (#604)\n\n  - This feature is **disabled by default**, and can be enabled by adding an annotation to the\n    migration file:\n\n    ```sql\n    -- +goose ENVSUB ON\n    ```\n\n  - When enabled, goose will attempt to substitute environment variables in the SQL migration\n    queries until the end of the file, or until the annotation `-- +goose ENVSUB OFF` is found. For\n    example, if the environment variable `REGION` is set to `us_east_1`, the following SQL migration\n    will be substituted to `SELECT * FROM regions WHERE name = 'us_east_1';`\n\n    ```sql\n    -- +goose ENVSUB ON\n    -- +goose Up\n    SELECT * FROM regions WHERE name = '${REGION}';\n    ```\n\n- Add native [Turso](https://turso.tech/) support with libsql driver. (#658)\n\n- Fixed query for list migrations in YDB (#684)\n\n## [v3.17.0] - 2023-12-15\n\n- Standardised the MIT license (#647)\n- Improve provider `Apply()` errors, add `ErrNotApplied` when attempting to rollback a migration\n  that has not been previously applied. (#660)\n- Add `WithDisableGlobalRegistry` option to `NewProvider` to disable the global registry. (#645)\n- Add `-timeout` flag to CLI to set the maximum allowed duration for queries to run. Default remains\n  no timeout. (#627)\n- Add optional logging in `Provider` when `WithVerbose` option is supplied. (#668)\n\n⚠️ Potential Breaking Change ⚠️\n\n- Update `goose create` to use UTC time instead of local time. (#242)\n\n## [v3.16.0] - 2023-11-12\n\n- Added YDB support. (#592)\n- Fix sqlserver query to ensure DB version. (#601)\n- Allow setting / resetting the global Go migration registry. (#602)\n  - `SetGlobalMigrations` and `ResetGlobalMigrations` functions have been added.\n  - Introduce `NewGoMigration` for constructing Go migrations.\n- Add initial implementation of `goose.NewProvider`.\n\n🎉 Read more about this new feature here:\n\nhttps://pressly.github.io/goose/blog/2023/goose-provider/\n\nThe motivation behind the Provider was simple - to reduce global state and make goose easier to\nconsume as an imported package.\n\nHere's a quick summary:\n\n- Avoid global state\n- Make Provider safe to use concurrently\n- Unlock (no pun intended) new features, such as database locking\n- Make logging configurable\n- Better error handling with proper return values\n- Double down on Go migrations\n- ... and more!\n\n## [v3.15.1] - 2023-10-10\n\n- Fix regression that prevented registering Go migrations that didn't have the corresponding files\n  available in the filesystem. (#588)\n  - If Go migrations have been registered globally, but there are no .go files in the filesystem,\n    **always include** them.\n  - If Go migrations have been registered, and there are .go files in the filesystem, **only\n    include** those migrations. This was the original motivation behind #553.\n  - If there are .go files in the filesystem but not registered, **raise an error**. This is to\n    prevent accidentally adding valid looking Go migration files without explicitly registering\n    them.\n\n## [v3.15.0] - 2023-08-12\n\n- Fix `sqlparser` to avoid skipping the last statement when it's not terminated with a semicolon\n  within a StatementBegin/End block. (#580)\n- Add `**go1.21**` to the CI matrix.\n- Bump minimum version of module in go.mod to `go1.19`.\n- Fix version output when installing pre-built binaries (#585).\n\n## [v3.14.0] - 2023-07-26\n\n- Filter registered Go migrations from the global map with corresponding .go files from the\n  filesystem.\n  - The code previously assumed all .go migrations would be in the same folder, so this should not\n    be a breaking change.\n  - See #553 for more details\n- Improve output log message for applied up migrations. #562\n- Fix an issue where `AddMigrationNoTxContext` was registering the wrong source because it skipped\n  too many frames. #572\n- Improve binary version output when using go install.\n\n## [v3.13.4] - 2023-07-07\n\n- Fix pre-built binary versioning and make small improvements to GoReleaser config.\n- Fix an edge case in the `sqlparser` where the last up statement may be ignored if it's\n  unterminated with a semicolon and followed by a `-- +goose Down` annotation.\n- Trim `Logger` interface to `Printf` and `Fatalf` methods only. Projects that have previously\n  implemented the `Logger` interface should not be affected, and can remove unused methods.\n\n## [v3.13.1] - 2023-07-03\n\n- Add pre-built binaries with GoReleaser and update the build process.\n\n## [v3.13.0] - 2023-06-29\n\n- Add a changelog to the project, based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).\n- Update go.mod and retract all `v3.12.X` tags. They were accidentally pushed and contain a\n  reference to the wrong Go module.\n- Fix `up` and `up -allowing-missing` behavior.\n- Fix empty version in log output.\n- Add new `context.Context`-aware functions and methods, for both sql and go migrations.\n- Return error when no migration files found or dir is not a directory.\n\n[Unreleased]: https://github.com/pressly/goose/compare/v3.27.0...HEAD\n[v3.27.0]: https://github.com/pressly/goose/compare/v3.26.0...v3.27.0\n[v3.26.0]: https://github.com/pressly/goose/compare/v3.25.0...v3.26.0\n[v3.25.0]: https://github.com/pressly/goose/compare/v3.24.3...v3.25.0\n[v3.24.3]: https://github.com/pressly/goose/compare/v3.24.2...v3.24.3\n[v3.24.2]: https://github.com/pressly/goose/compare/v3.24.1...v3.24.2\n[v3.24.1]: https://github.com/pressly/goose/compare/v3.24.0...v3.24.1\n[v3.24.0]: https://github.com/pressly/goose/compare/v3.23.1...v3.24.0\n[v3.23.1]: https://github.com/pressly/goose/compare/v3.23.0...v3.23.1\n[v3.23.0]: https://github.com/pressly/goose/compare/v3.22.1...v3.23.0\n[v3.22.1]: https://github.com/pressly/goose/compare/v3.22.0...v3.22.1\n[v3.22.0]: https://github.com/pressly/goose/compare/v3.21.1...v3.22.0\n[v3.21.1]: https://github.com/pressly/goose/compare/v3.20.0...v3.21.1\n[v3.21.0]: https://github.com/pressly/goose/compare/v3.20.0...v3.21.0\n[v3.20.0]: https://github.com/pressly/goose/compare/v3.19.2...v3.20.0\n[v3.19.2]: https://github.com/pressly/goose/compare/v3.19.1...v3.19.2\n[v3.19.1]: https://github.com/pressly/goose/compare/v3.19.0...v3.19.1\n[v3.19.0]: https://github.com/pressly/goose/compare/v3.18.0...v3.19.0\n[v3.18.0]: https://github.com/pressly/goose/compare/v3.17.0...v3.18.0\n[v3.17.0]: https://github.com/pressly/goose/compare/v3.16.0...v3.17.0\n[v3.16.0]: https://github.com/pressly/goose/compare/v3.15.1...v3.16.0\n[v3.15.1]: https://github.com/pressly/goose/compare/v3.15.0...v3.15.1\n[v3.15.0]: https://github.com/pressly/goose/compare/v3.14.0...v3.15.0\n[v3.14.0]: https://github.com/pressly/goose/compare/v3.13.4...v3.14.0\n[v3.13.4]: https://github.com/pressly/goose/compare/v3.13.1...v3.13.4\n[v3.13.1]: https://github.com/pressly/goose/compare/v3.13.0...v3.13.1\n[v3.13.0]: https://github.com/pressly/goose/releases/tag/v3.13.0\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nOriginal work Copyright (c) 2012 Liam Staskawicz\nModified work Copyright (c) 2016 Vojtech Vitek\nModified work Copyright (c) 2021 Michael Fridman, Vojtech Vitek\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "GO_TEST_FLAGS ?= -race -count=1 -v -timeout=5m -json\n\n# These are the default values for the test database. They can be overridden\nDB_USER ?= dbuser\nDB_PASSWORD ?= password1\nDB_NAME ?= testdb\nDB_POSTGRES_PORT ?= 5433\nDB_MYSQL_PORT ?= 3307\nDB_CLICKHOUSE_PORT ?= 9001\nDB_YDB_PORT ?= 2136\nDB_TURSO_PORT ?= 8080\nDB_STARROCKS_PORT ?= 9030\n\nlist-build-tags:\n\t@echo \"Available build tags:\"\n\t@echo \"$$(rg -o --trim 'no_[a-zA-Z0-9_]+' ./cmd/goose \\\n\t\t--no-line-number --no-filename | sort | uniq | \\\n\t\txargs -n 4 | column -t | sed 's/^/  /')\"\n\n.PHONY: dist\ndist:\n\t@mkdir -p ./bin\n\t@rm -f ./bin/*\n\tGOOS=darwin  GOARCH=amd64 go build -o ./bin/goose-darwin64       ./cmd/goose\n\tGOOS=linux   GOARCH=amd64 go build -o ./bin/goose-linux64        ./cmd/goose\n\tGOOS=linux   GOARCH=386   go build -o ./bin/goose-linux386       ./cmd/goose\n\tGOOS=windows GOARCH=amd64 go build -o ./bin/goose-windows64.exe  ./cmd/goose\n\tGOOS=windows GOARCH=386   go build -o ./bin/goose-windows386.exe ./cmd/goose\n\n.PHONY: clean\nclean:\n\t@find . -type f -name '*.FAIL' -delete\n\n.PHONY: lint\nlint: tools\n\t@golangci-lint run ./... --fix\n\n.PHONY: tools\ntools:\n\t@go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest\n\t@go install github.com/mfridman/tparse@main\n\ntest-packages:\n\tgo test $(GO_TEST_FLAGS) $$(go list ./... | grep -v -e /bin -e /cmd -e /examples) |\\\n\t\ttparse --follow -sort=elapsed -trimpath=auto -all\n\ntest-packages-short:\n\tgo test -test.short $(GO_TEST_FLAGS) $$(go list ./... | grep -v -e /bin -e /cmd -e /examples) |\\\n\t\ttparse --follow -sort=elapsed\n\ncoverage-short:\n\tgo test ./ -test.short $(GO_TEST_FLAGS) -cover -coverprofile=coverage.out | tparse --follow -sort=elapsed\n\tgo tool cover -html=coverage.out\n\ncoverage:\n\tgo test ./ $(GO_TEST_FLAGS) -cover -coverprofile=coverage.out | tparse --follow -sort=elapsed\n\tgo tool cover -html=coverage.out\n\ntest-lock-coverage:\n\tgo test ./internal/testing/integration/locking ./lock/internal/... -cover -coverpkg=./lock/internal/... -coverprofile=coverage.out\n\tgo tool cover -html=coverage.out -o coverage.html\n\t@echo \"Lock package coverage: $$(go tool cover -func=coverage.out | tail -1 | awk '{print $$3}')\"\n\topen coverage.html\n\n#\n# Integration-related targets\n#\nadd-gowork:\n\t@[ -f go.work ] || go work init\n\t@[ -f go.work.sum ] || go work use -r .\n\nremove-gowork:\n\trm -rf go.work go.work.sum\n\nupgrade-integration-deps:\n\tcd ./internal/testing && go get -u ./... && go mod tidy\n\ntest-postgres-long: add-gowork test-postgres\n\tgo test $(GO_TEST_FLAGS) ./internal/testing/integration -run='(TestPostgresProviderLocking|TestPostgresSessionLocker)' |\\\n\t\ttparse --follow -sort=elapsed\n\ntest-postgres: add-gowork\n\tgo test $(GO_TEST_FLAGS) ./internal/testing/integration -run=\"^TestPostgres$$\" | tparse --follow -sort=elapsed\n\ntest-spanner: add-gowork\n\tgo test $(GO_TEST_FLAGS) ./internal/testing/integration -run='TestSpanner' | tparse --follow -sort=elapsed\n\ntest-clickhouse: add-gowork\n\tgo test $(GO_TEST_FLAGS) ./internal/testing/integration -run='(TestClickhouse|TestClickhouseRemote)' |\\\n\t\ttparse --follow -sort=elapsed\n\ntest-mysql: add-gowork\n\tgo test $(GO_TEST_FLAGS) ./internal/testing/integration -run='TestMySQL' | tparse --follow -sort=elapsed\n\ntest-turso: add-gowork\n\tgo test $(GO_TEST_FLAGS) ./internal/testing/integration -run='TestTurso' | tparse --follow -sort=elapsed\n\ntest-ydb: add-gowork\n\tgo test $(GO_TEST_FLAGS) ./internal/testing/integration -run='TestYDB' | tparse --follow -sort=elapsed\n\ntest-starrocks: add-gowork\n\tgo test $(GO_TEST_FLAGS) ./internal/testing/integration -run='TestStarrocks' | tparse --follow -sort=elapsed\n\ntest-integration: add-gowork\n\tgo test $(GO_TEST_FLAGS) ./internal/testing/integration/... | tparse --follow -sort=elapsed -trimpath=auto -all\n\n#\n# Docker-related targets\n#\n\ndocker-cleanup:\n\tdocker stop -t=0 $$(docker ps --filter=\"label=goose_test\" -aq)\n\ndocker-postgres:\n\tdocker run --rm -d \\\n\t\t-e POSTGRES_USER=$(DB_USER) \\\n\t\t-e POSTGRES_PASSWORD=$(DB_PASSWORD) \\\n\t\t-e POSTGRES_DB=$(DB_NAME) \\\n\t\t-p $(DB_POSTGRES_PORT):5432 \\\n\t\t-l goose_test \\\n\t\tpostgres:14-alpine -c log_statement=all\n\techo \"postgres://$(DB_USER):$(DB_PASSWORD)@localhost:$(DB_POSTGRES_PORT)/$(DB_NAME)?sslmode=disable\"\n\ndocker-mysql:\n\tdocker run --rm -d \\\n\t\t-e MYSQL_ROOT_PASSWORD=rootpassword1 \\\n\t\t-e MYSQL_DATABASE=$(DB_NAME) \\\n\t\t-e MYSQL_USER=$(DB_USER) \\\n\t\t-e MYSQL_PASSWORD=$(DB_PASSWORD) \\\n\t\t-p $(DB_MYSQL_PORT):3306 \\\n\t\t-l goose_test \\\n\t\tmysql:8.0.31\n\techo \"mysql://$(DB_USER):$(DB_PASSWORD)@localhost:$(DB_MYSQL_PORT)/$(DB_NAME)?parseTime=true\"\n\ndocker-clickhouse:\n\tdocker run --rm -d \\\n\t\t-e CLICKHOUSE_DB=$(DB_NAME) \\\n\t\t-e CLICKHOUSE_USER=$(DB_USER) \\\n\t\t-e CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1 \\\n\t\t-e CLICKHOUSE_PASSWORD=$(DB_PASSWORD) \\\n\t\t-p $(DB_CLICKHOUSE_PORT):9000/tcp \\\n\t\t-l goose_test \\\n\t\tclickhouse/clickhouse-server:23-alpine\n\techo \"clickhouse://$(DB_USER):$(DB_PASSWORD)@localhost:$(DB_CLICKHOUSE_PORT)/$(DB_NAME)\"\n\ndocker-turso:\n\tdocker run --rm -d \\\n\t\t-p $(DB_TURSO_PORT):8080 \\\n\t\t-l goose_test \\\n\t\tghcr.io/tursodatabase/libsql-server:v0.22.10\n"
  },
  {
    "path": "README.md",
    "content": "# goose\n\n<img align=\"right\" width=\"125\" src=\"assets/goose_logo.png\">\n\n[![Goose\nCI](https://github.com/pressly/goose/actions/workflows/ci.yaml/badge.svg)](https://github.com/pressly/goose/actions/workflows/ci.yaml)\n[![Go\nReference](https://pkg.go.dev/badge/github.com/pressly/goose/v3.svg)](https://pkg.go.dev/github.com/pressly/goose/v3)\n[![Go Report\nCard](https://goreportcard.com/badge/github.com/pressly/goose/v3)](https://goreportcard.com/report/github.com/pressly/goose/v3)\n\nGoose is a database migration tool. Both a CLI and a library.\n\nManage your **database schema** by creating incremental SQL changes or Go functions.\n\n#### Features\n\n- Works against multiple databases:\n  - Postgres, MySQL, Spanner, SQLite, YDB, ClickHouse, MSSQL, Vertica, and\n    more.\n- Supports Go migrations written as plain functions.\n- Supports [embedded](https://pkg.go.dev/embed/) migrations.\n- Out-of-order migrations.\n- Seeding data.\n- Environment variable substitution in SQL migrations.\n- ... and more.\n\n# Install\n\n```shell\ngo install github.com/pressly/goose/v3/cmd/goose@latest\n```\n\nThis will install the `goose` binary to your `$GOPATH/bin` directory.\n\nBinary too big? Build a lite version by excluding the drivers you don't need:\n\n```shell\ngo build -tags='no_postgres no_mysql no_sqlite3 no_ydb' -o goose ./cmd/goose\n\n# Available build tags:\n#   no_clickhouse  no_libsql   no_mssql    no_mysql\n#   no_postgres    no_sqlite3  no_vertica  no_ydb\n```\n\nFor macOS users `goose` is available as a [Homebrew\nFormulae](https://formulae.brew.sh/formula/goose#default):\n\n```shell\nbrew install goose\n```\n\nSee [installation documentation](https://pressly.github.io/goose/installation/) for more details.\n\n# Usage\n\n<details>\n<summary>Click to show <code>goose help</code> output</summary>\n\n```\nUsage: goose DRIVER DBSTRING [OPTIONS] COMMAND\n\nor\n\nSet environment key\nGOOSE_DRIVER=DRIVER\nGOOSE_DBSTRING=DBSTRING\nGOOSE_MIGRATION_DIR=MIGRATION_DIR\n\nUsage: goose [OPTIONS] COMMAND\n\nDrivers:\n    postgres\n    mysql\n    sqlite3\n    spanner\n    mssql\n    redshift\n    tidb\n    clickhouse\n    ydb\n    starrocks\n    turso\n\nExamples:\n    goose sqlite3 ./foo.db status\n    goose sqlite3 ./foo.db create init sql\n    goose sqlite3 ./foo.db create add_some_column sql\n    goose sqlite3 ./foo.db create fetch_user_data go\n    goose sqlite3 ./foo.db up\n\n    goose postgres \"user=postgres dbname=postgres sslmode=disable\" status\n    goose mysql \"user:password@/dbname?parseTime=true\" status\n    goose spanner \"projects/project/instances/instance/databases/database\" status\n    goose redshift \"postgres://user:password@qwerty.us-east-1.redshift.amazonaws.com:5439/db\" status\n    goose tidb \"user:password@/dbname?parseTime=true\" status\n    goose mssql \"sqlserver://user:password@hostname:1433?database=master\" status\n    goose clickhouse \"tcp://127.0.0.1:9000\" status\n    goose ydb \"grpcs://localhost:2135/local?go_query_mode=scripting&go_fake_tx=scripting&go_query_bind=declare,numeric\" status\n    goose starrocks \"user:password@/dbname?parseTime=true&interpolateParams=true\" status\n\n    GOOSE_DRIVER=sqlite3 GOOSE_DBSTRING=./foo.db goose status\n    GOOSE_DRIVER=sqlite3 GOOSE_DBSTRING=./foo.db goose create init sql\n    GOOSE_DRIVER=postgres GOOSE_DBSTRING=\"user=postgres dbname=postgres sslmode=disable\" goose status\n    GOOSE_DRIVER=mysql GOOSE_DBSTRING=\"user:password@/dbname\" goose status\n    GOOSE_DRIVER=redshift GOOSE_DBSTRING=\"postgres://user:password@qwerty.us-east-1.redshift.amazonaws.com:5439/db\" goose status\n    GOOSE_DRIVER=clickhouse GOOSE_DBSTRING=\"clickhouse://user:password@qwerty.clickhouse.cloud:9440/dbname?secure=true&skip_verify=false\" goose status\n\nOptions:\n\n  -allow-missing\n        applies missing (out-of-order) migrations\n  -certfile string\n        file path to root CA's certificates in pem format (only support on mysql)\n  -dir string\n        directory with migration files (default \".\", can be set via the GOOSE_MIGRATION_DIR env variable).\n  -h    print help\n  -no-color\n        disable color output (NO_COLOR env variable supported)\n  -no-versioning\n        apply migration commands with no versioning, in file order, from directory pointed to\n  -s    use sequential numbering for new migrations\n  -ssl-cert string\n        file path to SSL certificates in pem format (only support on mysql)\n  -ssl-key string\n        file path to SSL key in pem format (only support on mysql)\n  -table string\n        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.\n  -timeout duration\n        maximum allowed duration for queries to run; e.g., 1h13m\n  -v    enable verbose mode\n  -version\n        print version\n\nCommands:\n    up                   Migrate the DB to the most recent version available\n    up-by-one            Migrate the DB up by 1\n    up-to VERSION        Migrate the DB to a specific VERSION\n    down                 Roll back the version by 1\n    down-to VERSION      Roll back to a specific VERSION\n    redo                 Re-run the latest migration\n    reset                Roll back all migrations\n    status               Dump the migration status for the current DB\n    version              Print the current version of the database\n    create NAME [sql|go] Creates new migration file with the current timestamp\n    fix                  Apply sequential ordering to migrations\n    validate             Check migration files without running them\n```\n\n</details>\n\nCommonly used commands:\n\n[create](#create)<span>&nbsp;•&nbsp;</span> [up](#up)<span>&nbsp;•&nbsp;</span> [up-to](#up-to)<span>&nbsp;•&nbsp;</span> [down](#down)<span>&nbsp;•&nbsp;</span> [down-to](#down-to)<span>&nbsp;•&nbsp;</span> [status](#status)<span>&nbsp;•&nbsp;</span> [version](#version)\n\n## create\n\nCreate a new SQL migration.\n\n    $ goose create add_some_column sql\n    $ Created new file: 20170506082420_add_some_column.sql\n\n    $ goose -s create add_some_column sql\n    $ Created new file: 00001_add_some_column.sql\n\nEdit the newly created file to define the behavior of your migration.\n\nYou can also create a Go migration, if you then invoke it with [your own goose\nbinary](#go-migrations):\n\n    $ goose create fetch_user_data go\n    $ Created new file: 20170506082421_fetch_user_data.go\n\n## up\n\nApply all available migrations.\n\n    $ goose up\n    $ OK    001_basics.sql\n    $ OK    002_next.sql\n    $ OK    003_and_again.go\n\n## up-to\n\nMigrate up to a specific version.\n\n    $ goose up-to 20170506082420\n    $ OK    20170506082420_create_table.sql\n\n## up-by-one\n\nMigrate up a single migration from the current version\n\n    $ goose up-by-one\n    $ OK    20170614145246_change_type.sql\n\n## down\n\nRoll back a single migration from the current version.\n\n    $ goose down\n    $ OK    003_and_again.go\n\n## down-to\n\nRoll back migrations to a specific version.\n\n    $ goose down-to 20170506082527\n    $ OK    20170506082527_alter_column.sql\n\nOr, roll back all migrations (careful!):\n\n    $ goose down-to 0\n\n## status\n\nPrint the status of all migrations:\n\n    $ goose status\n    $   Applied At                  Migration\n    $   =======================================\n    $   Sun Jan  6 11:25:03 2013 -- 001_basics.sql\n    $   Sun Jan  6 11:25:03 2013 -- 002_next.sql\n    $   Pending                  -- 003_and_again.go\n\nNote: for MySQL [parseTime flag](https://github.com/go-sql-driver/mysql#parsetime) must be enabled.\n\nNote: for MySQL\n[`multiStatements`](https://github.com/go-sql-driver/mysql?tab=readme-ov-file#multistatements) must\nbe enabled. This is required when writing multiple queries separated by ';' characters in a single\nsql file.\n\n## version\n\nPrint the current version of the database:\n\n    $ goose version\n    $ goose: version 002\n\n# Environment Variables\n\nIf you prefer to use environment variables, instead of passing the driver and database string as\narguments, you can set the following environment variables:\n\n**1. Via environment variables:**\n\n```shell\nexport GOOSE_DRIVER=DRIVER\nexport GOOSE_DBSTRING=DBSTRING\nexport GOOSE_MIGRATION_DIR=MIGRATION_DIR\nexport GOOSE_TABLE=TABLENAME\n```\n\n**2. Via `.env` files with corresponding variables. `.env` file example**:\n\n```env\nGOOSE_DRIVER=postgres\nGOOSE_DBSTRING=postgres://admin:admin@localhost:5432/admin_db\nGOOSE_MIGRATION_DIR=./migrations\nGOOSE_TABLE=custom.goose_migrations\n```\n\nLoading from `.env` files is enabled by default. To disable this feature, set the `-env=none` flag.\nIf you want to load from a specific file, set the `-env` flag to the file path.\n\nFor more details about environment variables, see the [official documentation on environment\nvariables](https://pressly.github.io/goose/documentation/environment-variables/).\n\n# Migrations\n\ngoose supports migrations written in SQL or in Go.\n\n## SQL Migrations\n\nA sample SQL migration looks like:\n\n```sql\n-- +goose Up\nCREATE TABLE post (\n    id int NOT NULL,\n    title text,\n    body text,\n    PRIMARY KEY(id)\n);\n\n-- +goose Down\nDROP TABLE post;\n```\n\nEach migration file must have exactly one `-- +goose Up` annotation. The `-- +goose Down` annotation\nis optional. If the file has both annotations, then the `-- +goose Up` annotation **must** come\nfirst.\n\nNotice the annotations in the comments. Any statements following `-- +goose Up` will be executed as\npart of a forward migration, and any statements following `-- +goose Down` will be executed as part\nof a rollback.\n\nBy default, all migrations are run within a transaction. Some statements like `CREATE DATABASE`,\nhowever, cannot be run within a transaction. You may optionally add `-- +goose NO TRANSACTION` to\nthe top of your migration file in order to skip transactions within that specific migration file.\nBoth Up and Down migrations within this file will be run without transactions.\n\nBy default, SQL statements are delimited by semicolons - in fact, query statements must end with a\nsemicolon to be properly recognized by goose.\n\nBy default, all migrations are run on the public schema. If you want to use a different schema,\nspecify the schema name using the table option like `-table='schemaname.goose_db_version`.\n\nMore complex statements (PL/pgSQL) that have semicolons within them must be annotated with `--\n+goose StatementBegin` and `-- +goose StatementEnd` to be properly recognized. For example:\n\n```sql\n-- +goose Up\n-- +goose StatementBegin\nCREATE OR REPLACE FUNCTION histories_partition_creation( DATE, DATE )\nreturns void AS $$\nDECLARE\n  create_query text;\nBEGIN\n  FOR create_query IN SELECT\n      'CREATE TABLE IF NOT EXISTS histories_'\n      || TO_CHAR( d, 'YYYY_MM' )\n      || ' ( CHECK( created_at >= timestamp '''\n      || TO_CHAR( d, 'YYYY-MM-DD 00:00:00' )\n      || ''' AND created_at < timestamp '''\n      || TO_CHAR( d + INTERVAL '1 month', 'YYYY-MM-DD 00:00:00' )\n      || ''' ) ) inherits ( histories );'\n    FROM generate_series( $1, $2, '1 month' ) AS d\n  LOOP\n    EXECUTE create_query;\n  END LOOP;  -- LOOP END\nEND;         -- FUNCTION END\n$$\nlanguage plpgsql;\n-- +goose StatementEnd\n```\n\nGoose supports environment variable substitution in SQL migrations through annotations. To enable\nthis feature, use the `-- +goose ENVSUB ON` annotation before the queries where you want\nsubstitution applied. It stays active until the `-- +goose ENVSUB OFF` annotation is encountered.\nYou can use these annotations multiple times within a file.\n\nThis feature is disabled by default for backward compatibility with existing scripts.\n\nFor `PL/pgSQL` functions or other statements where substitution is not desired, wrap the annotations\nexplicitly around the relevant parts. For example, to exclude escaping the `$$` characters:\n\n```sql\n-- +goose StatementBegin\nCREATE OR REPLACE FUNCTION test_func()\nRETURNS void AS $$\n-- +goose ENVSUB ON\nBEGIN\n\tRAISE NOTICE '${SOME_ENV_VAR}';\nEND;\n-- +goose ENVSUB OFF\n$$ LANGUAGE plpgsql;\n-- +goose StatementEnd\n```\n\n<details>\n<summary>Supported expansions (click here to expand):</summary>\n\n- `${VAR}` or $VAR - expands to the value of the environment variable `VAR`\n- `${VAR:-default}` - expands to the value of the environment variable `VAR`, or `default` if `VAR`\n  is unset or null\n- `${VAR-default}` - expands to the value of the environment variable `VAR`, or `default` if `VAR`\n  is unset\n- `${VAR?err_msg}` - expands to the value of the environment variable `VAR`, or prints `err_msg` and\n  error if `VAR` unset\n- ~~`${VAR:?err_msg}` - expands to the value of the environment variable `VAR`, or prints `err_msg`\n  and error if `VAR` unset or null.~~ **THIS IS NOT SUPPORTED**\n\nSee\n[mfridman/interpolate](https://github.com/mfridman/interpolate?tab=readme-ov-file#supported-expansions)\nfor more details on supported expansions.\n\n</details>\n\n## Embedded sql migrations\n\nGo 1.16 introduced new feature: [compile-time embedding](https://pkg.go.dev/embed/) files into\nbinary and corresponding [filesystem abstraction](https://pkg.go.dev/io/fs/).\n\nThis feature can be used only for applying existing migrations. Modifying operations such as `fix`\nand `create` will continue to operate on OS filesystem even if using embedded files. This is\nexpected behaviour because `io/fs` interfaces allows read-only access.\n\nMake sure to configure the correct SQL dialect, see [dialect.go](./dialect.go) for supported SQL\ndialects.\n\nExample usage, assuming that SQL migrations are placed in the `migrations` directory:\n\n```go\npackage main\n\nimport (\n    \"database/sql\"\n    \"embed\"\n\n    \"github.com/pressly/goose/v3\"\n)\n\n//go:embed migrations/*.sql\nvar embedMigrations embed.FS\n\nfunc main() {\n    var db *sql.DB\n    // setup database\n\n    goose.SetBaseFS(embedMigrations)\n\n    if err := goose.SetDialect(\"postgres\"); err != nil {\n        panic(err)\n    }\n\n    if err := goose.Up(db, \"migrations\"); err != nil {\n        panic(err)\n    }\n\n    // run app\n}\n```\n\nNote that we pass `\"migrations\"` as directory argument in `Up` because embedding saves directory\nstructure.\n\n## Go Migrations\n\n1. Create your own goose binary, see [example](./examples/go-migrations)\n2. Import `github.com/pressly/goose`\n3. Register your migration functions\n4. Include your `migrations` package into Go build: in `main.go`, `import _ \"github.com/me/myapp/migrations\"`\n5. Run goose command, ie. `goose.Up(db *sql.DB, dir string)`\n\nA [sample Go migration 00002_users_add_email.go file](./examples/go-migrations/00002_rename_root.go)\nlooks like:\n\n```go\npackage migrations\n\nimport (\n\t\"database/sql\"\n\n\t\"github.com/pressly/goose/v3\"\n)\n\nfunc init() {\n\tgoose.AddMigration(Up, Down)\n}\n\nfunc Up(tx *sql.Tx) error {\n\t_, err := tx.Exec(\"UPDATE users SET username='admin' WHERE username='root';\")\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc Down(tx *sql.Tx) error {\n\t_, err := tx.Exec(\"UPDATE users SET username='root' WHERE username='admin';\")\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n```\n\nNote that Go migration files must begin with a numeric value, followed by an underscore, and must\nnot end with `*_test.go`.\n\n# Hybrid Versioning\n\nPlease, read the [versioning\nproblem](https://github.com/pressly/goose/issues/63#issuecomment-428681694) first.\n\nBy default, if you attempt to apply missing (out-of-order) migrations `goose` will raise an error.\nHowever, If you want to apply these missing migrations pass goose the `-allow-missing` flag, or if\nusing as a library supply the functional option `goose.WithAllowMissing()` to Up, UpTo or UpByOne.\n\nHowever, we strongly recommend adopting a hybrid versioning approach, using both timestamps and\nsequential numbers. Migrations created during the development process are timestamped and sequential\nversions are ran on production. We believe this method will prevent the problem of conflicting\nversions when writing software in a team environment.\n\nTo help you adopt this approach, `create` will use the current timestamp as the migration version.\nWhen you're ready to deploy your migrations in a production environment, we also provide a helpful\n`fix` command to convert your migrations into sequential order, while preserving the timestamp\nordering. We recommend running `fix` in the CI pipeline, and only when the migrations are ready for\nproduction.\n\n## Credit\n\nThe gopher mascot was designed by [Renée French](https://reneefrench.blogspot.com/) / [CC\n3.0.](https://creativecommons.org/licenses/by/3.0/) For more info check out the [Go\nBlog](https://go.dev/blog/gopher). Adapted by Ellen.\n\n## License\n\nLicensed under [MIT License](./LICENSE)\n"
  },
  {
    "path": "cmd/goose/driver_clickhouse.go",
    "content": "//go:build !no_clickhouse\n\npackage main\n\nimport (\n\t_ \"github.com/ClickHouse/clickhouse-go/v2\"\n)\n"
  },
  {
    "path": "cmd/goose/driver_mssql.go",
    "content": "//go:build !no_mssql\n\npackage main\n\nimport (\n\t_ \"github.com/microsoft/go-mssqldb\"\n)\n"
  },
  {
    "path": "cmd/goose/driver_mysql.go",
    "content": "//go:build !no_mysql\n\npackage main\n\nimport (\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\n\t\"github.com/go-sql-driver/mysql\"\n\t_ \"github.com/ziutek/mymysql/godrv\"\n)\n\n// normalizeMySQLDSN parses the dsn used with the mysql driver to always have\n// the parameter `parseTime` set to true. This allows internal goose logic\n// to assume that DATETIME/DATE/TIMESTAMP can be scanned into the time.Time\n// type.\nfunc normalizeDBString(driver string, str string, certfile string, sslcert string, sslkey string) string {\n\tif driver == \"mysql\" {\n\t\tisTLS := certfile != \"\"\n\t\tif isTLS {\n\t\t\tif err := registerTLSConfig(certfile, sslcert, sslkey); err != nil {\n\t\t\t\tlog.Fatalf(\"goose run: %v\", err)\n\t\t\t}\n\t\t}\n\t\tvar err error\n\t\tstr, err = normalizeMySQLDSN(str, isTLS)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"failed to normalize MySQL connection string: %v\", err)\n\t\t}\n\t}\n\treturn str\n}\n\nconst tlsConfigKey = \"custom\"\n\nfunc normalizeMySQLDSN(dsn string, tls bool) (string, error) {\n\tconfig, err := mysql.ParseDSN(dsn)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tconfig.ParseTime = true\n\tif tls {\n\t\tconfig.TLSConfig = tlsConfigKey\n\t}\n\treturn config.FormatDSN(), nil\n}\n\nfunc registerTLSConfig(pemfile string, sslcert string, sslkey string) error {\n\trootCertPool := x509.NewCertPool()\n\tpem, err := os.ReadFile(pemfile)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif ok := rootCertPool.AppendCertsFromPEM(pem); !ok {\n\t\treturn fmt.Errorf(\"failed to append PEM: %q\", pemfile)\n\t}\n\n\ttlsConfig := &tls.Config{\n\t\tRootCAs: rootCertPool,\n\t}\n\tif sslcert != \"\" && sslkey != \"\" {\n\t\tcert, err := tls.LoadX509KeyPair(sslcert, sslkey)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to load x509 keypair: %w\", err)\n\t\t}\n\t\ttlsConfig.Certificates = append(tlsConfig.Certificates, cert)\n\t}\n\treturn mysql.RegisterTLSConfig(tlsConfigKey, tlsConfig)\n}\n"
  },
  {
    "path": "cmd/goose/driver_no_mysql.go",
    "content": "//go:build no_mysql\n\npackage main\n\nfunc normalizeDBString(driver string, str string, certfile string, sslcert string, sslkey string) string {\n\treturn str\n}\n"
  },
  {
    "path": "cmd/goose/driver_postgres.go",
    "content": "//go:build !no_postgres\n\npackage main\n\nimport (\n\t_ \"github.com/jackc/pgx/v5/stdlib\"\n)\n"
  },
  {
    "path": "cmd/goose/driver_sqlite3.go",
    "content": "//go:build !no_sqlite3 && !(windows && arm64)\n\npackage main\n\nimport (\n\t_ \"modernc.org/sqlite\"\n)\n"
  },
  {
    "path": "cmd/goose/driver_turso.go",
    "content": "//go:build !no_libsql\n\npackage main\n\nimport (\n\t_ \"github.com/tursodatabase/libsql-client-go/libsql\"\n)\n"
  },
  {
    "path": "cmd/goose/driver_vertica.go",
    "content": "//go:build !no_vertica\n\npackage main\n\nimport (\n\t_ \"github.com/vertica/vertica-sql-go\"\n)\n"
  },
  {
    "path": "cmd/goose/driver_ydb.go",
    "content": "//go:build !no_ydb\n\npackage main\n\nimport (\n\t_ \"github.com/ydb-platform/ydb-go-sdk/v3\"\n)\n"
  },
  {
    "path": "cmd/goose/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime/debug\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"text/tabwriter\"\n\t\"text/template\"\n\n\t\"github.com/joho/godotenv\"\n\t\"github.com/mfridman/xflag\"\n\t\"github.com/pressly/goose/v3\"\n\t\"github.com/pressly/goose/v3/internal/migrationstats\"\n)\n\nvar (\n\tDefaultMigrationDir = \".\"\n\n\tflags        = flag.NewFlagSet(\"goose\", flag.ExitOnError)\n\tdir          = flags.String(\"dir\", DefaultMigrationDir, \"directory with migration files, (GOOSE_MIGRATION_DIR env variable supported)\")\n\ttable        = flags.String(\"table\", \"\", \"migrations table name\")\n\tverbose      = flags.Bool(\"v\", false, \"enable verbose mode\")\n\thelp         = flags.Bool(\"h\", false, \"print help\")\n\tversionFlag  = flags.Bool(\"version\", false, \"print version\")\n\tcertfile     = flags.String(\"certfile\", \"\", \"file path to root CA's certificates in pem format (only support on mysql)\")\n\tsequential   = flags.Bool(\"s\", false, \"use sequential numbering for new migrations\")\n\tallowMissing = flags.Bool(\"allow-missing\", false, \"applies missing (out-of-order) migrations\")\n\tsslcert      = flags.String(\"ssl-cert\", \"\", \"file path to SSL certificates in pem format (only support on mysql)\")\n\tsslkey       = flags.String(\"ssl-key\", \"\", \"file path to SSL key in pem format (only support on mysql)\")\n\tnoVersioning = flags.Bool(\"no-versioning\", false, \"apply migration commands with no versioning, in file order, from directory pointed to\")\n\tnoColor      = flags.Bool(\"no-color\", false, \"disable color output (NO_COLOR env variable supported)\")\n\ttimeout      = flags.Duration(\"timeout\", 0, \"maximum allowed duration for queries to run; e.g., 1h13m\")\n\tenvFile      = flags.String(\"env\", \"\", \"load environment variables from file (default .env)\")\n)\n\nvar version string\n\nfunc main() {\n\tctx := context.Background()\n\n\tflags.Usage = usage\n\n\tif err := xflag.ParseToEnd(flags, os.Args[1:]); err != nil {\n\t\tlog.Fatalf(\"failed to parse args: %v\", err)\n\t\treturn\n\t}\n\n\tif *versionFlag {\n\t\tbuildInfo, ok := debug.ReadBuildInfo()\n\t\tif version == \"\" && ok && buildInfo != nil && buildInfo.Main.Version != \"\" {\n\t\t\tversion = buildInfo.Main.Version\n\t\t}\n\t\tfmt.Printf(\"goose version: %s\\n\", strings.TrimSpace(version))\n\t\treturn\n\t}\n\n\tswitch *envFile {\n\tcase \"\":\n\t\t// Best effort to load default .env file\n\t\t_ = godotenv.Load()\n\tcase \"none\":\n\t\t// Do not load any env file\n\tdefault:\n\t\tif err := godotenv.Load(*envFile); err != nil {\n\t\t\tlog.Fatalf(\"failed to load env file: %v\", err)\n\t\t}\n\t}\n\tenvConfig := loadEnvConfig()\n\n\tif *verbose {\n\t\tgoose.SetVerbose(true)\n\t}\n\tif *sequential {\n\t\tgoose.SetSequential(true)\n\t}\n\n\t// The order of precedence should be: flag > env variable > default value.\n\tgoose.SetTableName(firstNonEmpty(*table, envConfig.table, goose.DefaultTablename))\n\n\targs := flags.Args()\n\n\tif *help {\n\t\tflags.Usage()\n\t\treturn\n\t}\n\n\tif len(args) == 0 {\n\t\tflags.Usage()\n\t\tos.Exit(1)\n\t}\n\n\t// The -dir option has not been set, check whether the env variable is set\n\t// before defaulting to \".\".\n\tif *dir == DefaultMigrationDir && envConfig.dir != \"\" {\n\t\t*dir = envConfig.dir\n\t}\n\n\tswitch args[0] {\n\tcase \"init\":\n\t\tif err := gooseInit(*dir); err != nil {\n\t\t\tlog.Fatalf(\"goose run: %v\", err)\n\t\t}\n\t\treturn\n\tcase \"create\":\n\t\tif err := goose.RunContext(ctx, \"create\", nil, *dir, args[1:]...); err != nil {\n\t\t\tlog.Fatalf(\"goose run: %v\", err)\n\t\t}\n\t\treturn\n\tcase \"fix\":\n\t\tif err := goose.RunContext(ctx, \"fix\", nil, *dir); err != nil {\n\t\t\tlog.Fatalf(\"goose run: %v\", err)\n\t\t}\n\t\treturn\n\tcase \"env\":\n\t\tfor _, env := range envConfig.listEnvs() {\n\t\t\tfmt.Printf(\"%s=%q\\n\", env.Name, env.Value)\n\t\t}\n\t\treturn\n\tcase \"validate\":\n\t\tif err := printValidate(*dir, *verbose); err != nil {\n\t\t\tlog.Fatalf(\"goose validate: %v\", err)\n\t\t}\n\t\treturn\n\tcase \"beta\":\n\t\tremain := args[1:]\n\t\tif len(remain) == 0 {\n\t\t\tlog.Println(\"goose beta: missing subcommand\")\n\t\t\tos.Exit(1)\n\t\t}\n\t\tswitch remain[0] {\n\t\tcase \"drivers\":\n\t\t\tprintDrivers()\n\t\t}\n\t\treturn\n\t}\n\n\targs = mergeArgs(envConfig, args)\n\tif len(args) < 3 {\n\t\tflags.Usage()\n\t\tos.Exit(1)\n\t}\n\n\tdriver, dbstring, command := args[0], args[1], args[2]\n\tdb, err := goose.OpenDBWithDriver(driver, normalizeDBString(driver, dbstring, *certfile, *sslcert, *sslkey))\n\tif err != nil {\n\t\tlog.Fatalf(\"-dbstring=%q: %v\", dbstring, err)\n\t}\n\tdefer func() {\n\t\tif err := db.Close(); err != nil {\n\t\t\tlog.Fatalf(\"goose: failed to close DB: %v\", err)\n\t\t}\n\t}()\n\n\targuments := []string{}\n\tif len(args) > 3 {\n\t\targuments = append(arguments, args[3:]...)\n\t}\n\toptions := []goose.OptionsFunc{}\n\tif *noColor || envConfig.noColor {\n\t\toptions = append(options, goose.WithNoColor(true))\n\t}\n\tif *allowMissing {\n\t\toptions = append(options, goose.WithAllowMissing())\n\t}\n\tif *noVersioning {\n\t\toptions = append(options, goose.WithNoVersioning())\n\t}\n\tif timeout != nil && *timeout != 0 {\n\t\tvar cancel context.CancelFunc\n\t\tctx, cancel = context.WithTimeout(ctx, *timeout)\n\t\tdefer cancel()\n\t}\n\tif err := goose.RunWithOptionsContext(\n\t\tctx,\n\t\tcommand,\n\t\tdb,\n\t\t*dir,\n\t\targuments,\n\t\toptions...,\n\t); err != nil {\n\t\tlog.Fatalf(\"goose run: %v\", err)\n\t}\n}\n\nfunc printDrivers() {\n\tdrivers := mergeDrivers(sql.Drivers())\n\tif len(drivers) == 0 {\n\t\tfmt.Println(\"No drivers found\")\n\t\treturn\n\t}\n\tfmt.Println(\"Available drivers:\")\n\tfor _, driver := range drivers {\n\t\tfmt.Printf(\"  %s\\n\", driver)\n\t}\n}\n\n// mergeDrivers merges drivers with a common prefix into a single line.\nfunc mergeDrivers(drivers []string) []string {\n\tdriverMap := make(map[string][]string)\n\n\tfor _, driver := range drivers {\n\t\tparts := strings.Split(driver, \"/\")\n\t\tif len(parts) > 1 {\n\t\t\t// Merge drivers with a common prefix \"/\"\n\t\t\tprefix := parts[0]\n\t\t\tdriverMap[prefix] = append(driverMap[prefix], driver)\n\t\t} else {\n\t\t\t// Add drivers without a prefix directly\n\t\t\tdriverMap[driver] = append(driverMap[driver], driver)\n\t\t}\n\t}\n\tvar merged []string\n\tfor _, versions := range driverMap {\n\t\tsort.Strings(versions)\n\t\tmerged = append(merged, strings.Join(versions, \", \"))\n\t}\n\tsort.Strings(merged)\n\treturn merged\n}\n\nfunc mergeArgs(config *envConfig, args []string) []string {\n\tif len(args) < 1 {\n\t\treturn args\n\t}\n\tif s := config.driver; s != \"\" {\n\t\targs = append([]string{s}, args...)\n\t}\n\tif s := config.dbstring; s != \"\" {\n\t\targs = append([]string{args[0], s}, args[1:]...)\n\t}\n\treturn args\n}\n\nfunc usage() {\n\tfmt.Println(usagePrefix)\n\tflags.PrintDefaults()\n\tfmt.Println(usageCommands)\n}\n\nvar (\n\tusagePrefix = `Usage: goose DRIVER DBSTRING [OPTIONS] COMMAND\n\nor\n\nSet environment key\nGOOSE_DRIVER=DRIVER\nGOOSE_DBSTRING=DBSTRING\nGOOSE_MIGRATION_DIR=MIGRATION_DIR\n\nUsage: goose [OPTIONS] COMMAND\n\nDrivers:\n    postgres\n    mysql\n    sqlite3\n    spanner\n    mssql\n    redshift\n    tidb\n    clickhouse\n    ydb\n    starrocks\n    turso\n\nExamples:\n    goose sqlite3 ./foo.db status\n    goose sqlite3 ./foo.db create init sql\n    goose sqlite3 ./foo.db create add_some_column sql\n    goose sqlite3 ./foo.db create fetch_user_data go\n    goose sqlite3 ./foo.db up\n\n    goose postgres \"user=postgres dbname=postgres sslmode=disable\" status\n    goose mysql \"user:password@/dbname?parseTime=true\" status\n    goose redshift \"postgres://user:password@qwerty.us-east-1.redshift.amazonaws.com:5439/db\" status\n    goose tidb \"user:password@/dbname?parseTime=true\" status\n    goose mssql \"sqlserver://user:password@dbname:1433?database=master\" status\n    goose clickhouse \"tcp://127.0.0.1:9000\" status\n    goose ydb \"grpcs://localhost:2135/local?go_query_mode=scripting&go_fake_tx=scripting&go_query_bind=declare,numeric\" status\n    goose turso \"libsql://dbname.turso.io?authToken=token\" status\n\n    GOOSE_DRIVER=sqlite3 GOOSE_DBSTRING=./foo.db goose status\n    GOOSE_DRIVER=sqlite3 GOOSE_DBSTRING=./foo.db goose create init sql\n    GOOSE_DRIVER=postgres GOOSE_DBSTRING=\"user=postgres dbname=postgres sslmode=disable\" goose status\n    GOOSE_DRIVER=mysql GOOSE_DBSTRING=\"user:password@/dbname\" goose status\n    GOOSE_DRIVER=redshift GOOSE_DBSTRING=\"postgres://user:password@qwerty.us-east-1.redshift.amazonaws.com:5439/db\" goose status\n    GOOSE_DRIVER=turso GOOSE_DBSTRING=\"libsql://dbname.turso.io?authToken=token\" goose status\n    GOOSE_DRIVER=clickhouse GOOSE_DBSTRING=\"clickhouse://user:password@qwerty.clickhouse.cloud:9440/dbname?secure=true&skip_verify=false\" goose status\n\nOptions:\n`\n\n\tusageCommands = `\nCommands:\n    up                   Migrate the DB to the most recent version available\n    up-by-one            Migrate the DB up by 1\n    up-to VERSION        Migrate the DB to a specific VERSION\n    down                 Roll back the version by 1\n    down-to VERSION      Roll back to a specific VERSION\n    redo                 Re-run the latest migration\n    reset                Roll back all migrations\n    status               Dump the migration status for the current DB\n    version              Print the current version of the database\n    create NAME [sql|go] Creates new migration file with the current timestamp\n    fix                  Apply sequential ordering to migrations\n    validate             Check migration files without running them\n`\n)\n\nvar sqlMigrationTemplate = template.Must(template.New(\"goose.sql-migration\").Parse(`-- Thank you for giving goose a try!\n-- \n-- This file was automatically created running goose init. If you're familiar with goose\n-- feel free to remove/rename this file, write some SQL and goose up. Briefly,\n-- \n-- Documentation can be found here: https://pressly.github.io/goose\n--\n-- A single goose .sql file holds both Up and Down migrations.\n-- \n-- All goose .sql files are expected to have a -- +goose Up annotation.\n-- The -- +goose Down annotation is optional, but recommended, and must come after the Up annotation.\n-- \n-- The -- +goose NO TRANSACTION annotation may be added to the top of the file to run statements \n-- outside a transaction. Both Up and Down migrations within this file will be run without a transaction.\n-- \n-- More complex statements that have semicolons within them must be annotated with \n-- the -- +goose StatementBegin and -- +goose StatementEnd annotations to be properly recognized.\n-- \n-- Use GitHub issues for reporting bugs and requesting features, enjoy!\n\n-- +goose Up\nSELECT 'up SQL query';\n\n-- +goose Down\nSELECT 'down SQL query';\n`))\n\n// initDir will create a directory with an empty SQL migration file.\nfunc gooseInit(dir string) error {\n\tif dir == \"\" || dir == DefaultMigrationDir {\n\t\tdir = \"migrations\"\n\t}\n\t_, err := os.Stat(dir)\n\tswitch {\n\tcase errors.Is(err, fs.ErrNotExist):\n\tcase err == nil, errors.Is(err, fs.ErrExist):\n\t\treturn fmt.Errorf(\"directory already exists: %s\", dir)\n\tdefault:\n\t\treturn err\n\t}\n\tif err := os.MkdirAll(dir, 0755); err != nil {\n\t\treturn err\n\t}\n\treturn goose.CreateWithTemplate(nil, dir, sqlMigrationTemplate, \"initial\", \"sql\")\n}\n\nfunc gatherFilenames(filename string) ([]string, error) {\n\tstat, err := os.Stat(filename)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar filenames []string\n\tif stat.IsDir() {\n\t\tfor _, pattern := range []string{\"*.sql\", \"*.go\"} {\n\t\t\tfile, err := filepath.Glob(filepath.Join(filename, pattern))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tfilenames = append(filenames, file...)\n\t\t}\n\t} else {\n\t\tfilenames = append(filenames, filename)\n\t}\n\tsort.Strings(filenames)\n\treturn filenames, nil\n}\n\nfunc printValidate(filename string, verbose bool) error {\n\tfilenames, err := gatherFilenames(filename)\n\tif err != nil {\n\t\treturn err\n\t}\n\tstats, err := migrationstats.GatherStats(\n\t\tmigrationstats.NewFileWalker(filenames...),\n\t\tfalse,\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\t// TODO(mf): we should introduce a --debug flag, which allows printing\n\t// more internal debug information and leave verbose for additional information.\n\tif !verbose {\n\t\treturn nil\n\t}\n\tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', tabwriter.TabIndent)\n\tfmtPattern := \"%v\\t%v\\t%v\\t%v\\t%v\\t\\n\"\n\tfmt.Fprintf(w, fmtPattern, \"Type\", \"Txn\", \"Up\", \"Down\", \"Name\")\n\tfmt.Fprintf(w, fmtPattern, \"────\", \"───\", \"──\", \"────\", \"────\")\n\tfor _, m := range stats {\n\t\ttxnStr := \"✔\"\n\t\tif !m.Tx {\n\t\t\ttxnStr = \"✘\"\n\t\t}\n\t\tfmt.Fprintf(w, fmtPattern,\n\t\t\tstrings.TrimPrefix(filepath.Ext(m.FileName), \".\"),\n\t\t\ttxnStr,\n\t\t\tm.UpCount,\n\t\t\tm.DownCount,\n\t\t\tfilepath.Base(m.FileName),\n\t\t)\n\t}\n\treturn w.Flush()\n}\n\ntype envConfig struct {\n\tdriver   string\n\tdbstring string\n\tdir      string\n\ttable    string\n\tnoColor  bool\n}\n\nfunc loadEnvConfig() *envConfig {\n\tnoColorBool, _ := strconv.ParseBool(envOr(\"NO_COLOR\", \"false\"))\n\treturn &envConfig{\n\t\tdriver:   envOr(\"GOOSE_DRIVER\", \"\"),\n\t\tdbstring: envOr(\"GOOSE_DBSTRING\", \"\"),\n\t\ttable:    envOr(\"GOOSE_TABLE\", \"\"),\n\t\tdir:      envOr(\"GOOSE_MIGRATION_DIR\", DefaultMigrationDir),\n\t\t// https://no-color.org/\n\t\tnoColor: noColorBool,\n\t}\n}\n\nfunc (c *envConfig) listEnvs() []envVar {\n\treturn []envVar{\n\t\t{Name: \"GOOSE_DRIVER\", Value: c.driver},\n\t\t{Name: \"GOOSE_DBSTRING\", Value: c.dbstring},\n\t\t{Name: \"GOOSE_MIGRATION_DIR\", Value: c.dir},\n\t\t{Name: \"GOOSE_TABLE\", Value: c.table},\n\t\t{Name: \"NO_COLOR\", Value: strconv.FormatBool(c.noColor)},\n\t}\n}\n\ntype envVar struct {\n\tName  string\n\tValue string\n}\n\n// envOr returns os.Getenv(key) if set, or else default.\nfunc envOr(key, def string) string {\n\tval := os.Getenv(key)\n\tif val == \"\" {\n\t\tval = def\n\t}\n\treturn val\n}\n\n// firstNonEmpty returns the first non-empty string from the provided input or an empty string if all are empty.\nfunc firstNonEmpty(values ...string) string {\n\tfor _, v := range values {\n\t\tif v != \"\" {\n\t\t\treturn v\n\t\t}\n\t}\n\treturn \"\"\n}\n"
  },
  {
    "path": "cmd/goose/main_test.go",
    "content": "package main\n\nimport (\n\t\"testing\"\n)\n\nfunc TestFirstNonEmpty(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"no values\",\n\t\t\tinput:    []string{},\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"all empty values\",\n\t\t\tinput:    []string{\"\", \"\", \"\"},\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"single non-empty value at start\",\n\t\t\tinput:    []string{\"value\", \"\", \"\"},\n\t\t\texpected: \"value\",\n\t\t},\n\t\t{\n\t\t\tname:     \"single non-empty value in middle\",\n\t\t\tinput:    []string{\"\", \"value\", \"\"},\n\t\t\texpected: \"value\",\n\t\t},\n\t\t{\n\t\t\tname:     \"single non-empty value at end\",\n\t\t\tinput:    []string{\"\", \"\", \"value\"},\n\t\t\texpected: \"value\",\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple non-empty values\",\n\t\t\tinput:    []string{\"first\", \"second\", \"third\"},\n\t\t\texpected: \"first\",\n\t\t},\n\t\t{\n\t\t\tname:     \"mixed empty and non-empty values\",\n\t\t\tinput:    []string{\"\", \"value1\", \"\", \"value2\"},\n\t\t\texpected: \"value1\",\n\t\t},\n\t\t{\n\t\t\tname:     \"only one value, empty\",\n\t\t\tinput:    []string{\"\"},\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"only one value, non-empty\",\n\t\t\tinput:    []string{\"value\"},\n\t\t\texpected: \"value\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := firstNonEmpty(tt.input...)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"expected %q, got %q\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "create.go",
    "content": "package goose\n\nimport (\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"text/template\"\n\t\"time\"\n)\n\ntype tmplVars struct {\n\tVersion   string\n\tCamelName string\n}\n\nvar (\n\tsequential = false\n)\n\n// SetSequential set whether to use sequential versioning instead of timestamp based versioning\nfunc SetSequential(s bool) {\n\tsequential = s\n}\n\n// Create writes a new blank migration file.\nfunc CreateWithTemplate(db *sql.DB, dir string, tmpl *template.Template, name, migrationType string) error {\n\tversion := time.Now().UTC().Format(timestampFormat)\n\n\tif sequential {\n\t\t// always use DirFS here because it's modifying operation\n\t\tmigrations, err := collectMigrationsFS(osFS{}, dir, minVersion, maxVersion, registeredGoMigrations)\n\t\tif err != nil && !errors.Is(err, ErrNoMigrationFiles) {\n\t\t\treturn err\n\t\t}\n\n\t\tvMigrations, err := migrations.versioned()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif last, err := vMigrations.Last(); err == nil {\n\t\t\tversion = fmt.Sprintf(seqVersionTemplate, last.Version+1)\n\t\t} else {\n\t\t\tversion = fmt.Sprintf(seqVersionTemplate, int64(1))\n\t\t}\n\t}\n\n\tfilename := fmt.Sprintf(\"%v_%v.%v\", version, snakeCase(name), migrationType)\n\n\tif tmpl == nil {\n\t\tif migrationType == \"go\" {\n\t\t\ttmpl = goSQLMigrationTemplate\n\t\t} else {\n\t\t\ttmpl = sqlMigrationTemplate\n\t\t}\n\t}\n\n\tpath := filepath.Join(dir, filename)\n\tif _, err := os.Stat(path); !os.IsNotExist(err) {\n\t\treturn fmt.Errorf(\"failed to create migration file: %w\", err)\n\t}\n\n\tf, err := os.Create(path)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create migration file: %w\", err)\n\t}\n\tdefer f.Close()\n\n\tvars := tmplVars{\n\t\tVersion:   version,\n\t\tCamelName: camelCase(name),\n\t}\n\tif err := tmpl.Execute(f, vars); err != nil {\n\t\treturn fmt.Errorf(\"failed to execute tmpl: %w\", err)\n\t}\n\n\tlog.Printf(\"Created new file: %s\", f.Name())\n\treturn nil\n}\n\n// Create writes a new blank migration file.\nfunc Create(db *sql.DB, dir, name, migrationType string) error {\n\treturn CreateWithTemplate(db, dir, nil, name, migrationType)\n}\n\nvar sqlMigrationTemplate = template.Must(template.New(\"goose.sql-migration\").Parse(`-- +goose Up\nSELECT 'up SQL query';\n\n-- +goose Down\nSELECT 'down SQL query';\n`))\n\nvar goSQLMigrationTemplate = template.Must(template.New(\"goose.go-migration\").Parse(`package migrations\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"github.com/pressly/goose/v3\"\n)\n\nfunc init() {\n\tgoose.AddMigrationContext(up{{.CamelName}}, down{{.CamelName}})\n}\n\nfunc up{{.CamelName}}(ctx context.Context, tx *sql.Tx) error {\n\t// This code is executed when the migration is applied.\n\treturn nil\n}\n\nfunc down{{.CamelName}}(ctx context.Context, tx *sql.Tx) error {\n\t// This code is executed when the migration is rolled back.\n\treturn nil\n}\n`))\n"
  },
  {
    "path": "create_test.go",
    "content": "package goose\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestSequential(t *testing.T) {\n\tt.Parallel()\n\tif testing.Short() {\n\t\tt.Skip(\"skip long running test\")\n\t}\n\n\tdir := t.TempDir()\n\tdefer os.Remove(\"./bin/create-goose\") // clean up\n\n\tcommands := []string{\n\t\t\"go build -o ./bin/create-goose ./cmd/goose\",\n\t\tfmt.Sprintf(\"./bin/create-goose -s -dir=%s create create_table\", dir),\n\t\tfmt.Sprintf(\"./bin/create-goose -s -dir=%s create add_users\", dir),\n\t\tfmt.Sprintf(\"./bin/create-goose -s -dir=%s create add_indices\", dir),\n\t\tfmt.Sprintf(\"./bin/create-goose -s -dir=%s create update_users\", dir),\n\t}\n\n\tfor _, cmd := range commands {\n\t\targs := strings.Split(cmd, \" \")\n\t\ttime.Sleep(1 * time.Second)\n\t\tcmd := exec.Command(args[0], args[1:]...)\n\t\tcmd.Env = os.Environ()\n\t\tout, err := cmd.CombinedOutput()\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"%s:\\n%v\\n\\n%s\", err, cmd, out)\n\t\t}\n\t}\n\n\tfiles, err := os.ReadDir(dir)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// check that the files are in order\n\tfor i, f := range files {\n\t\texpected := fmt.Sprintf(\"%05v\", i+1)\n\t\tif !strings.HasPrefix(f.Name(), expected) {\n\t\t\tt.Errorf(\"failed to find %s prefix in %s\", expected, f.Name())\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "database/dialect/querier.go",
    "content": "package dialect\n\n// Querier is the interface that wraps the basic methods to create a dialect specific query.\n//\n// It is intended tio be using with [database.NewStoreFromQuerier] to create a new [database.Store]\n// implementation based on a custom querier.\ntype Querier interface {\n\t// CreateTable returns the SQL query string to create the db version table.\n\tCreateTable(tableName string) string\n\t// InsertVersion returns the SQL query string to insert a new version into the db version table.\n\tInsertVersion(tableName string) string\n\t// DeleteVersion returns the SQL query string to delete a version from the db version table.\n\tDeleteVersion(tableName string) string\n\t// GetMigrationByVersion returns the SQL query string to get a single migration by version.\n\t//\n\t// The query should return the timestamp and is_applied columns.\n\tGetMigrationByVersion(tableName string) string\n\t// ListMigrations returns the SQL query string to list all migrations in descending order by id.\n\t//\n\t// The query should return the version_id and is_applied columns.\n\tListMigrations(tableName string) string\n\t// GetLatestVersion returns the SQL query string to get the last version_id from the db version\n\t// table. Returns a nullable int64 value.\n\tGetLatestVersion(tableName string) string\n}\n"
  },
  {
    "path": "database/dialect/querier_extended.go",
    "content": "package dialect\n\n// QuerierExtender extends the [Querier] interface with optional database-specific optimizations.\n// While not required, implementing these methods can improve performance.\n//\n// IMPORTANT: This interface may be expanded in future versions. Implementors must be prepared to\n// update their implementations when new methods are added.\n//\n// Example compile-time check:\n//\n//\tvar _ QuerierExtender = (*CustomQuerierExtended)(nil)\n//\n// In short, it's exported to allow implementors to have a compile-time check that they are\n// implementing the interface correctly.\ntype QuerierExtender interface {\n\tQuerier\n\n\t// TableExists returns a database-specific SQL query to check if a table exists. For example,\n\t// implementations might query system catalogs like pg_tables or sqlite_master. Return empty\n\t// string if not supported.\n\tTableExists(tableName string) string\n}\n"
  },
  {
    "path": "database/dialects.go",
    "content": "package database\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/pressly/goose/v3/database/dialect\"\n\t\"github.com/pressly/goose/v3/internal/dialects\"\n)\n\n// Dialect is the type of database dialect.\ntype Dialect string\n\nconst (\n\tDialectCustom     Dialect = \"\"\n\tDialectClickHouse Dialect = \"clickhouse\"\n\tDialectAuroraDSQL Dialect = \"dsql\"\n\tDialectMSSQL      Dialect = \"mssql\"\n\tDialectMySQL      Dialect = \"mysql\"\n\tDialectPostgres   Dialect = \"postgres\"\n\tDialectRedshift   Dialect = \"redshift\"\n\tDialectSQLite3    Dialect = \"sqlite3\"\n\tDialectSpanner    Dialect = \"spanner\"\n\tDialectStarrocks  Dialect = \"starrocks\"\n\tDialectTiDB       Dialect = \"tidb\"\n\tDialectTurso      Dialect = \"turso\"\n\tDialectYdB        Dialect = \"ydb\"\n\n\t// DEPRECATED: Vertica support is deprecated and will be removed in a future release.\n\tDialectVertica Dialect = \"vertica\"\n)\n\n// NewStore returns a new [Store] implementation for the given dialect.\nfunc NewStore(d Dialect, tableName string) (Store, error) {\n\tif d == DialectCustom {\n\t\treturn nil, errors.New(\"custom dialect is not supported\")\n\t}\n\tlookup := map[Dialect]dialect.Querier{\n\t\tDialectClickHouse: dialects.NewClickhouse(),\n\t\tDialectAuroraDSQL: dialects.NewAuroraDSQL(),\n\t\tDialectMSSQL:      dialects.NewSqlserver(),\n\t\tDialectMySQL:      dialects.NewMysql(),\n\t\tDialectPostgres:   dialects.NewPostgres(),\n\t\tDialectRedshift:   dialects.NewRedshift(),\n\t\tDialectSQLite3:    dialects.NewSqlite3(),\n\t\tDialectSpanner:    dialects.NewSpanner(),\n\t\tDialectStarrocks:  dialects.NewStarrocks(),\n\t\tDialectTiDB:       dialects.NewTidb(),\n\t\tDialectTurso:      dialects.NewTurso(),\n\t\tDialectVertica:    dialects.NewVertica(),\n\t\tDialectYdB:        dialects.NewYDB(),\n\t}\n\tquerier, ok := lookup[d]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"unknown dialect: %q\", d)\n\t}\n\treturn NewStoreFromQuerier(tableName, querier)\n}\n\n// NewStoreFromQuerier returns a new [Store] implementation for the given querier.\n//\n// Most of the time you should use [NewStore] instead of this function, as it will automatically\n// create a dialect-specific querier for you. This function is useful if you want to use a custom\n// querier that is not part of the standard dialects.\nfunc NewStoreFromQuerier(tableName string, querier dialect.Querier) (Store, error) {\n\tif tableName == \"\" {\n\t\treturn nil, errors.New(\"table name must not be empty\")\n\t}\n\tif querier == nil {\n\t\treturn nil, errors.New(\"querier must not be nil\")\n\t}\n\treturn &store{\n\t\ttableName: tableName,\n\t\tquerier:   newQueryController(querier),\n\t}, nil\n}\n\ntype store struct {\n\ttableName string\n\tquerier   *queryController\n}\n\nvar _ Store = (*store)(nil)\n\nfunc (s *store) Tablename() string {\n\treturn s.tableName\n}\n\nfunc (s *store) CreateVersionTable(ctx context.Context, db DBTxConn) error {\n\tq := s.querier.CreateTable(s.tableName)\n\tif _, err := db.ExecContext(ctx, q); err != nil {\n\t\treturn fmt.Errorf(\"failed to create version table %q: %w\", s.tableName, err)\n\t}\n\treturn nil\n}\n\nfunc (s *store) Insert(ctx context.Context, db DBTxConn, req InsertRequest) error {\n\tq := s.querier.InsertVersion(s.tableName)\n\tif _, err := db.ExecContext(ctx, q, req.Version, true); err != nil {\n\t\treturn fmt.Errorf(\"failed to insert version %d: %w\", req.Version, err)\n\t}\n\treturn nil\n}\n\nfunc (s *store) Delete(ctx context.Context, db DBTxConn, version int64) error {\n\tq := s.querier.DeleteVersion(s.tableName)\n\tif _, err := db.ExecContext(ctx, q, version); err != nil {\n\t\treturn fmt.Errorf(\"failed to delete version %d: %w\", version, err)\n\t}\n\treturn nil\n}\n\nfunc (s *store) GetMigration(\n\tctx context.Context,\n\tdb DBTxConn,\n\tversion int64,\n) (*GetMigrationResult, error) {\n\tq := s.querier.GetMigrationByVersion(s.tableName)\n\tvar result GetMigrationResult\n\tif err := db.QueryRowContext(ctx, q, version).Scan(\n\t\t&result.Timestamp,\n\t\t&result.IsApplied,\n\t); err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn nil, fmt.Errorf(\"%w: %d\", ErrVersionNotFound, version)\n\t\t}\n\t\treturn nil, fmt.Errorf(\"failed to get migration %d: %w\", version, err)\n\t}\n\treturn &result, nil\n}\n\nfunc (s *store) GetLatestVersion(ctx context.Context, db DBTxConn) (int64, error) {\n\tq := s.querier.GetLatestVersion(s.tableName)\n\tvar version sql.NullInt64\n\tif err := db.QueryRowContext(ctx, q).Scan(&version); err != nil {\n\t\treturn -1, fmt.Errorf(\"failed to get latest version: %w\", err)\n\t}\n\tif !version.Valid {\n\t\treturn -1, fmt.Errorf(\"latest %w\", ErrVersionNotFound)\n\t}\n\treturn version.Int64, nil\n}\n\nfunc (s *store) ListMigrations(\n\tctx context.Context,\n\tdb DBTxConn,\n) ([]*ListMigrationsResult, error) {\n\tq := s.querier.ListMigrations(s.tableName)\n\trows, err := db.QueryContext(ctx, q)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to list migrations: %w\", err)\n\t}\n\tdefer rows.Close()\n\n\tvar migrations []*ListMigrationsResult\n\tfor rows.Next() {\n\t\tvar result ListMigrationsResult\n\t\tif err := rows.Scan(&result.Version, &result.IsApplied); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to scan list migrations result: %w\", err)\n\t\t}\n\t\tmigrations = append(migrations, &result)\n\t}\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn migrations, nil\n}\n\n//\n//\n//\n// Additional methods that are not part of the core Store interface, but are extended by the\n// [controller.StoreController] type.\n//\n//\n//\n\nfunc (s *store) TableExists(ctx context.Context, db DBTxConn) (bool, error) {\n\tq := s.querier.TableExists(s.tableName)\n\tif q == \"\" {\n\t\treturn false, errors.ErrUnsupported\n\t}\n\tvar exists bool\n\t// Note, we do not pass the table name as an argument to the query, as the query should be\n\t// pre-defined by the dialect.\n\tif err := db.QueryRowContext(ctx, q).Scan(&exists); err != nil {\n\t\treturn false, fmt.Errorf(\"failed to check if table exists: %w\", err)\n\t}\n\treturn exists, nil\n}\n\nvar _ dialect.Querier = (*queryController)(nil)\n\ntype queryController struct{ dialect.Querier }\n\n// newQueryController returns a new QueryController that wraps the given Querier.\nfunc newQueryController(querier dialect.Querier) *queryController {\n\treturn &queryController{Querier: querier}\n}\n\n// Optional methods\n\n// TableExists returns the SQL query string to check if the version table exists. If the Querier\n// does not implement this method, it will return an empty string.\n//\n// Returns a boolean value.\nfunc (c *queryController) TableExists(tableName string) string {\n\tif t, ok := c.Querier.(interface{ TableExists(string) string }); ok {\n\t\treturn t.TableExists(tableName)\n\t}\n\treturn \"\"\n}\n"
  },
  {
    "path": "database/doc.go",
    "content": "// Package database defines a generic [Store] interface for goose to use when interacting with the\n// database. It is meant to be generic and not tied to any specific database technology.\n//\n// At a high level, a [Store] is responsible for:\n//   - Creating a version table\n//   - Inserting and deleting a version\n//   - Getting a specific version\n//   - Listing all applied versions\n//\n// Use the [NewStore] function to create a [Store] for one of the supported dialects.\n//\n// For more advanced use cases, it's possible to implement a custom [Store] for a database that\n// goose does not support.\npackage database\n"
  },
  {
    "path": "database/sql_extended.go",
    "content": "package database\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n)\n\n// DBTxConn is a thin interface for common methods that is satisfied by *sql.DB, *sql.Tx and\n// *sql.Conn.\n//\n// There is a long outstanding issue to formalize a std lib interface, but alas. See:\n// https://github.com/golang/go/issues/14468\ntype DBTxConn interface {\n\tExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)\n\tQueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)\n\tQueryRowContext(ctx context.Context, query string, args ...any) *sql.Row\n}\n\nvar (\n\t_ DBTxConn = (*sql.DB)(nil)\n\t_ DBTxConn = (*sql.Tx)(nil)\n\t_ DBTxConn = (*sql.Conn)(nil)\n)\n"
  },
  {
    "path": "database/store.go",
    "content": "package database\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"time\"\n)\n\nvar (\n\t// ErrVersionNotFound must be returned by [GetMigration] or [GetLatestVersion] when a migration\n\t// does not exist.\n\tErrVersionNotFound = errors.New(\"version not found\")\n\n\t// ErrNotImplemented must be returned by methods that are not implemented.\n\tErrNotImplemented = errors.New(\"not implemented\")\n)\n\n// Store is an interface that defines methods for tracking and managing migrations. It is used by\n// the goose package to interact with a database. By defining a Store interface, multiple\n// implementations can be created to support different databases without reimplementing the\n// migration logic.\n//\n// This package provides several dialects that implement the Store interface. While most users won't\n// need to create their own Store, if you need to support a database that isn't currently supported,\n// you can implement your own!\ntype Store interface {\n\t// Tablename is the name of the version table. This table is used to record applied migrations\n\t// and must not be an empty string.\n\tTablename() string\n\t// CreateVersionTable creates the version table, which is used to track migrations.\n\tCreateVersionTable(ctx context.Context, db DBTxConn) error\n\t// Insert a version id into the version table.\n\tInsert(ctx context.Context, db DBTxConn, req InsertRequest) error\n\t// Delete a version id from the version table.\n\tDelete(ctx context.Context, db DBTxConn, version int64) error\n\t// GetMigration retrieves a single migration by version id. If the query succeeds, but the\n\t// version is not found, this method must return [ErrVersionNotFound].\n\tGetMigration(ctx context.Context, db DBTxConn, version int64) (*GetMigrationResult, error)\n\t// GetLatestVersion retrieves the last applied migration version. If no migrations exist, this\n\t// method must return [ErrVersionNotFound].\n\tGetLatestVersion(ctx context.Context, db DBTxConn) (int64, error)\n\t// ListMigrations retrieves all migrations sorted in descending order by id or timestamp. If\n\t// there are no migrations, return empty slice with no error. Typically this method will return\n\t// at least one migration, because the initial version (0) is always inserted into the version\n\t// table when it is created.\n\tListMigrations(ctx context.Context, db DBTxConn) ([]*ListMigrationsResult, error)\n}\n\ntype InsertRequest struct {\n\tVersion int64\n\n\t// TODO(mf): in the future, we maybe want to expand this struct so implementors can store\n\t// additional information. See the following issues for more information:\n\t//  - https://github.com/pressly/goose/issues/422\n\t//  - https://github.com/pressly/goose/issues/288\n}\n\ntype GetMigrationResult struct {\n\tTimestamp time.Time\n\tIsApplied bool\n}\n\ntype ListMigrationsResult struct {\n\tVersion   int64\n\tIsApplied bool\n}\n"
  },
  {
    "path": "database/store_extended.go",
    "content": "package database\n\nimport \"context\"\n\n// StoreExtender is an extension of the Store interface that provides optional optimizations and\n// database-specific features. While not required by the core goose package, implementing these\n// methods can improve performance and functionality for specific databases.\n//\n// IMPORTANT: This interface may be expanded in future versions. Implementors MUST be prepared to\n// update their implementations when new methods are added, either by implementing the new\n// functionality or returning [errors.ErrUnsupported].\n//\n// The goose package handles these extended capabilities through a [controller.StoreController],\n// which automatically uses optimized methods when available while falling back to default behavior\n// when they're not implemented.\n//\n// Example usage to verify implementation:\n//\n//\tvar _ StoreExtender = (*CustomStoreExtended)(nil)\n//\n// In short, it's exported to allows implementors to have a compile-time check that they are\n// implementing the interface correctly.\ntype StoreExtender interface {\n\tStore\n\n\t// TableExists checks if the migrations table exists in the database. Implementing this method\n\t// allows goose to optimize table existence checks by using database-specific system catalogs\n\t// (e.g., pg_tables for PostgreSQL, sqlite_master for SQLite) instead of generic SQL queries.\n\t//\n\t// Return [errors.ErrUnsupported] if the database does not provide an efficient way to check\n\t// table existence.\n\tTableExists(ctx context.Context, db DBTxConn) (bool, error)\n}\n"
  },
  {
    "path": "database/store_test.go",
    "content": "package database_test\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/pressly/goose/v3/database\"\n\t\"github.com/stretchr/testify/require\"\n\t\"go.uber.org/multierr\"\n\t\"modernc.org/sqlite\"\n)\n\n// The goal of this test is to verify the database store package works as expected. This test is not\n// meant to be exhaustive or test every possible database dialect. It is meant to verify the Store\n// interface works against a real database.\n\nfunc TestDialectStore(t *testing.T) {\n\tt.Parallel()\n\tt.Run(\"invalid\", func(t *testing.T) {\n\t\t// Test empty table name.\n\t\t_, err := database.NewStore(database.DialectSQLite3, \"\")\n\t\trequire.Error(t, err)\n\t\t// Test unknown dialect.\n\t\t_, err = database.NewStore(\"unknown-dialect\", \"foo\")\n\t\trequire.Error(t, err)\n\t\t// Test empty dialect.\n\t\t_, err = database.NewStore(\"\", \"foo\")\n\t\trequire.Error(t, err)\n\t})\n\t// Test generic behavior.\n\tt.Run(\"sqlite3\", func(t *testing.T) {\n\t\tdb, err := sql.Open(\"sqlite\", \":memory:\")\n\t\trequire.NoError(t, err)\n\t\ttestStore(context.Background(), t, database.DialectSQLite3, db, func(t *testing.T, err error) {\n\t\t\tt.Helper()\n\t\t\tvar sqliteErr *sqlite.Error\n\t\t\tok := errors.As(err, &sqliteErr)\n\t\t\trequire.True(t, ok)\n\t\t\trequire.Equal(t, 1, sqliteErr.Code()) // Generic error (SQLITE_ERROR)\n\t\t\trequire.Contains(t, sqliteErr.Error(), \"table test_goose_db_version already exists\")\n\t\t})\n\t})\n\tt.Run(\"ListMigrations\", func(t *testing.T) {\n\t\tdir := t.TempDir()\n\t\tdb, err := sql.Open(\"sqlite\", filepath.Join(dir, \"sql_embed.db\"))\n\t\trequire.NoError(t, err)\n\t\tstore, err := database.NewStore(database.DialectSQLite3, \"foo\")\n\t\trequire.NoError(t, err)\n\t\terr = store.CreateVersionTable(context.Background(), db)\n\t\trequire.NoError(t, err)\n\t\tinsert := func(db *sql.DB, version int64) error {\n\t\t\treturn store.Insert(context.Background(), db, database.InsertRequest{Version: version})\n\t\t}\n\t\trequire.NoError(t, insert(db, 1))\n\t\trequire.NoError(t, insert(db, 3))\n\t\trequire.NoError(t, insert(db, 2))\n\t\tres, err := store.ListMigrations(context.Background(), db)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, res, 3)\n\t\t// Check versions are in descending order: [2, 3, 1]\n\t\trequire.EqualValues(t, 2, res[0].Version)\n\t\trequire.EqualValues(t, 3, res[1].Version)\n\t\trequire.EqualValues(t, 1, res[2].Version)\n\t})\n}\n\n// testStore tests various store operations.\n//\n// If alreadyExists is not nil, it will be used to assert the error returned by CreateVersionTable\n// when the version table already exists.\nfunc testStore(\n\tctx context.Context,\n\tt *testing.T,\n\td database.Dialect,\n\tdb *sql.DB,\n\talreadyExists func(t *testing.T, err error),\n) {\n\tconst (\n\t\ttableName = \"test_goose_db_version\"\n\t)\n\tstore, err := database.NewStore(d, tableName)\n\trequire.NoError(t, err)\n\t// Create the version table.\n\terr = runTx(ctx, db, func(tx *sql.Tx) error {\n\t\treturn store.CreateVersionTable(ctx, tx)\n\t})\n\trequire.NoError(t, err)\n\t// Create the version table again. This should fail.\n\terr = runTx(ctx, db, func(tx *sql.Tx) error {\n\t\treturn store.CreateVersionTable(ctx, tx)\n\t})\n\trequire.Error(t, err)\n\tif alreadyExists != nil {\n\t\talreadyExists(t, err)\n\t}\n\t// Get the latest version. There should be none.\n\t_, err = store.GetLatestVersion(ctx, db)\n\trequire.ErrorIs(t, err, database.ErrVersionNotFound)\n\n\t// List migrations. There should be none.\n\terr = runConn(ctx, db, func(conn *sql.Conn) error {\n\t\tres, err := store.ListMigrations(ctx, conn)\n\t\trequire.NoError(t, err)\n\t\trequire.Empty(t, res, 0)\n\t\treturn nil\n\t})\n\trequire.NoError(t, err)\n\n\t// Insert 5 migrations in addition to the zero migration.\n\tfor i := range 6 {\n\t\terr = runConn(ctx, db, func(conn *sql.Conn) error {\n\t\t\terr := store.Insert(ctx, conn, database.InsertRequest{Version: int64(i)})\n\t\t\trequire.NoError(t, err)\n\t\t\tlatest, err := store.GetLatestVersion(ctx, conn)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, latest, int64(i))\n\t\t\treturn nil\n\t\t})\n\t\trequire.NoError(t, err)\n\t}\n\n\t// List migrations. There should be 6.\n\terr = runConn(ctx, db, func(conn *sql.Conn) error {\n\t\tres, err := store.ListMigrations(ctx, conn)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, res, 6)\n\t\t// Check versions are in descending order.\n\t\tfor i := range 6 {\n\t\t\trequire.EqualValues(t, res[i].Version, 5-i)\n\t\t}\n\t\treturn nil\n\t})\n\trequire.NoError(t, err)\n\n\t// Delete 3 migrations backwards\n\tfor i := 5; i >= 3; i-- {\n\t\terr = runConn(ctx, db, func(conn *sql.Conn) error {\n\t\t\terr := store.Delete(ctx, conn, int64(i))\n\t\t\trequire.NoError(t, err)\n\t\t\tlatest, err := store.GetLatestVersion(ctx, conn)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, latest, int64(i-1))\n\t\t\treturn nil\n\t\t})\n\t\trequire.NoError(t, err)\n\t}\n\n\t// List migrations. There should be 3.\n\terr = runConn(ctx, db, func(conn *sql.Conn) error {\n\t\tres, err := store.ListMigrations(ctx, conn)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, res, 3)\n\t\t// Check that the remaining versions are in descending order.\n\t\tfor i := range 3 {\n\t\t\trequire.EqualValues(t, res[i].Version, 2-i)\n\t\t}\n\t\treturn nil\n\t})\n\trequire.NoError(t, err)\n\n\t// Get remaining migrations one by one.\n\tfor i := range 3 {\n\t\terr = runConn(ctx, db, func(conn *sql.Conn) error {\n\t\t\tres, err := store.GetMigration(ctx, conn, int64(i))\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.True(t, res.IsApplied)\n\t\t\trequire.False(t, res.Timestamp.IsZero())\n\t\t\treturn nil\n\t\t})\n\t\trequire.NoError(t, err)\n\t}\n\n\t// Delete remaining migrations one by one and use all 3 connection types:\n\n\t// 1. *sql.Tx\n\terr = runTx(ctx, db, func(tx *sql.Tx) error {\n\t\terr := store.Delete(ctx, tx, 2)\n\t\trequire.NoError(t, err)\n\t\tlatest, err := store.GetLatestVersion(ctx, tx)\n\t\trequire.NoError(t, err)\n\t\trequire.EqualValues(t, 1, latest)\n\t\treturn nil\n\t})\n\trequire.NoError(t, err)\n\t// 2. *sql.Conn\n\terr = runConn(ctx, db, func(conn *sql.Conn) error {\n\t\terr := store.Delete(ctx, conn, 1)\n\t\trequire.NoError(t, err)\n\t\tlatest, err := store.GetLatestVersion(ctx, conn)\n\t\trequire.NoError(t, err)\n\t\trequire.EqualValues(t, 0, latest)\n\t\treturn nil\n\t})\n\trequire.NoError(t, err)\n\t// 3. *sql.DB\n\terr = store.Delete(ctx, db, 0)\n\trequire.NoError(t, err)\n\t_, err = store.GetLatestVersion(ctx, db)\n\trequire.ErrorIs(t, err, database.ErrVersionNotFound)\n\n\t// List migrations. There should be none.\n\terr = runConn(ctx, db, func(conn *sql.Conn) error {\n\t\tres, err := store.ListMigrations(ctx, conn)\n\t\trequire.NoError(t, err)\n\t\trequire.Empty(t, res)\n\t\treturn nil\n\t})\n\trequire.NoError(t, err)\n\n\t// Try to get a migration that does not exist.\n\terr = runConn(ctx, db, func(conn *sql.Conn) error {\n\t\t_, err := store.GetMigration(ctx, conn, 0)\n\t\trequire.Error(t, err)\n\t\trequire.ErrorIs(t, err, database.ErrVersionNotFound)\n\t\treturn nil\n\t})\n\trequire.NoError(t, err)\n}\n\nfunc runTx(ctx context.Context, db *sql.DB, fn func(*sql.Tx) error) (retErr error) {\n\ttx, err := db.BeginTx(ctx, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() {\n\t\tif retErr != nil {\n\t\t\tretErr = multierr.Append(retErr, tx.Rollback())\n\t\t}\n\t}()\n\tif err := fn(tx); err != nil {\n\t\treturn err\n\t}\n\treturn tx.Commit()\n}\n\nfunc runConn(ctx context.Context, db *sql.DB, fn func(*sql.Conn) error) (retErr error) {\n\tconn, err := db.Conn(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() {\n\t\tif retErr != nil {\n\t\t\tretErr = multierr.Append(retErr, conn.Close())\n\t\t}\n\t}()\n\tif err := fn(conn); err != nil {\n\t\treturn err\n\t}\n\treturn conn.Close()\n}\n"
  },
  {
    "path": "db.go",
    "content": "package goose\n\nimport (\n\t\"database/sql\"\n\t\"fmt\"\n)\n\n// OpenDBWithDriver creates a connection to a database, and modifies goose internals to be\n// compatible with the supplied driver by calling SetDialect.\nfunc OpenDBWithDriver(driver string, dbstring string) (*sql.DB, error) {\n\tif err := SetDialect(driver); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// The Go ecosystem has added more and more drivers over the years. As a result, there's no\n\t// longer a one-to-one match between the driver name and the dialect name. For instance, there's\n\t// no \"redshift\" driver, but that's the internal dialect name within goose. Hence, we need to\n\t// convert the dialect name to a supported driver name. This conversion is a best-effort\n\t// attempt, as we can't support both lib/pq and pgx, which some users might have.\n\t//\n\t// We recommend users to create a [NewProvider] with the desired dialect, open a connection\n\t// using their preferred driver, and provide the *sql.DB to goose. This approach removes the\n\t// need for mapping dialects to drivers, rendering this function unnecessary.\n\n\tswitch driver {\n\tcase \"mssql\":\n\t\tdriver = \"sqlserver\"\n\tcase \"tidb\":\n\t\tdriver = \"mysql\"\n\tcase \"spanner\":\n\t\tdriver = \"spanner\"\n\tcase \"turso\":\n\t\tdriver = \"libsql\"\n\tcase \"sqlite3\":\n\t\tdriver = \"sqlite\"\n\tcase \"postgres\", \"redshift\":\n\t\tdriver = \"pgx\"\n\tcase \"starrocks\":\n\t\tdriver = \"mysql\"\n\t}\n\n\tswitch driver {\n\tcase \"postgres\", \"pgx\", \"sqlite3\", \"sqlite\", \"spanner\", \"mysql\", \"sqlserver\", \"clickhouse\", \"vertica\", \"azuresql\", \"ydb\", \"libsql\", \"starrocks\":\n\t\treturn sql.Open(driver, dbstring)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported driver %s\", driver)\n\t}\n}\n"
  },
  {
    "path": "dialect.go",
    "content": "package goose\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/pressly/goose/v3/database\"\n\t\"github.com/pressly/goose/v3/internal/legacystore\"\n)\n\n// Dialect is the type of database dialect. It is an alias for [database.Dialect].\ntype Dialect = database.Dialect\n\nconst (\n\tDialectCustom     Dialect = database.DialectCustom\n\tDialectClickHouse Dialect = database.DialectClickHouse\n\tDialectMSSQL      Dialect = database.DialectMSSQL\n\tDialectMySQL      Dialect = database.DialectMySQL\n\tDialectPostgres   Dialect = database.DialectPostgres\n\tDialectRedshift   Dialect = database.DialectRedshift\n\tDialectSQLite3    Dialect = database.DialectSQLite3\n\tDialectSpanner    Dialect = database.DialectSpanner\n\tDialectStarrocks  Dialect = database.DialectStarrocks\n\tDialectTiDB       Dialect = database.DialectTiDB\n\tDialectTurso      Dialect = database.DialectTurso\n\tDialectYdB        Dialect = database.DialectYdB\n\n\t// Dialects only available to the [Provider].\n\tDialectAuroraDSQL Dialect = database.DialectAuroraDSQL\n\n\t// DEPRECATED: Vertica support is deprecated and will be removed in a future release.\n\tDialectVertica Dialect = database.DialectVertica\n)\n\nfunc init() {\n\tstore, _ = legacystore.NewStore(DialectPostgres)\n}\n\nvar store legacystore.Store\n\n// SetDialect sets the dialect to use for the goose package.\nfunc SetDialect(s string) error {\n\tvar d Dialect\n\tswitch s {\n\tcase \"postgres\", \"pgx\":\n\t\td = DialectPostgres\n\tcase \"mysql\":\n\t\td = DialectMySQL\n\tcase \"sqlite3\", \"sqlite\":\n\t\td = DialectSQLite3\n\tcase \"spanner\":\n\t\td = DialectSpanner\n\tcase \"mssql\", \"azuresql\", \"sqlserver\":\n\t\td = DialectMSSQL\n\tcase \"redshift\":\n\t\td = DialectRedshift\n\tcase \"tidb\":\n\t\td = DialectTiDB\n\tcase \"clickhouse\":\n\t\td = DialectClickHouse\n\tcase \"vertica\":\n\t\td = DialectVertica\n\tcase \"ydb\":\n\t\td = DialectYdB\n\tcase \"turso\":\n\t\td = DialectTurso\n\tcase \"starrocks\":\n\t\td = DialectStarrocks\n\tdefault:\n\t\treturn fmt.Errorf(\"%q: unknown dialect\", s)\n\t}\n\tvar err error\n\tstore, err = legacystore.NewStore(d)\n\treturn err\n}\n"
  },
  {
    "path": "down.go",
    "content": "package goose\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n)\n\n// Down rolls back a single migration from the current version.\nfunc Down(db *sql.DB, dir string, opts ...OptionsFunc) error {\n\tctx := context.Background()\n\treturn DownContext(ctx, db, dir, opts...)\n}\n\n// DownContext rolls back a single migration from the current version.\nfunc DownContext(ctx context.Context, db *sql.DB, dir string, opts ...OptionsFunc) error {\n\toption := &options{}\n\tfor _, f := range opts {\n\t\tf(option)\n\t}\n\tmigrations, err := CollectMigrations(dir, minVersion, maxVersion)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif option.noVersioning {\n\t\tif len(migrations) == 0 {\n\t\t\treturn nil\n\t\t}\n\t\tcurrentVersion := migrations[len(migrations)-1].Version\n\t\t// Migrate only the latest migration down.\n\t\treturn downToNoVersioning(ctx, db, migrations, currentVersion-1)\n\t}\n\tcurrentVersion, err := GetDBVersionContext(ctx, db)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcurrent, err := migrations.Current(currentVersion)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"migration %v: %w\", currentVersion, err)\n\t}\n\treturn current.DownContext(ctx, db)\n}\n\n// DownTo rolls back migrations to a specific version.\nfunc DownTo(db *sql.DB, dir string, version int64, opts ...OptionsFunc) error {\n\tctx := context.Background()\n\treturn DownToContext(ctx, db, dir, version, opts...)\n}\n\n// DownToContext rolls back migrations to a specific version.\nfunc DownToContext(ctx context.Context, db *sql.DB, dir string, version int64, opts ...OptionsFunc) error {\n\toption := &options{}\n\tfor _, f := range opts {\n\t\tf(option)\n\t}\n\tmigrations, err := CollectMigrations(dir, minVersion, maxVersion)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif option.noVersioning {\n\t\treturn downToNoVersioning(ctx, db, migrations, version)\n\t}\n\n\tfor {\n\t\tcurrentVersion, err := GetDBVersionContext(ctx, db)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif currentVersion == 0 {\n\t\t\tlog.Printf(\"goose: no migrations to run. current version: %d\", currentVersion)\n\t\t\treturn nil\n\t\t}\n\t\tcurrent, err := migrations.Current(currentVersion)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"goose: migration file not found for current version (%d), error: %s\", currentVersion, err)\n\t\t\treturn err\n\t\t}\n\n\t\tif current.Version <= version {\n\t\t\tlog.Printf(\"goose: no migrations to run. current version: %d\", currentVersion)\n\t\t\treturn nil\n\t\t}\n\n\t\tif err = current.DownContext(ctx, db); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n}\n\n// downToNoVersioning applies down migrations down to, but not including, the\n// target version.\nfunc downToNoVersioning(ctx context.Context, db *sql.DB, migrations Migrations, version int64) error {\n\tvar finalVersion int64\n\tfor i := len(migrations) - 1; i >= 0; i-- {\n\t\tif version >= migrations[i].Version {\n\t\t\tfinalVersion = migrations[i].Version\n\t\t\tbreak\n\t\t}\n\t\tmigrations[i].noVersioning = true\n\t\tif err := migrations[i].DownContext(ctx, db); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tlog.Printf(\"goose: down to current file version: %d\", finalVersion)\n\treturn nil\n}\n"
  },
  {
    "path": "examples/README.md",
    "content": "# 1. [SQL migrations](sql-migrations)\n# 2. [Go migrations](go-migrations)"
  },
  {
    "path": "examples/go-migrations/00001_create_users_table.sql",
    "content": "-- +goose Up\nCREATE TABLE users (\n    id INTEGER PRIMARY KEY AUTOINCREMENT,\n    username TEXT,\n    name TEXT,\n    surname TEXT\n);\n\nINSERT INTO users VALUES\n(0, 'root', '', ''),\n(1, 'vojtechvitek', 'Vojtech', 'Vitek');\n\n-- +goose Down\nDROP TABLE users;\n"
  },
  {
    "path": "examples/go-migrations/00002_rename_root.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\n\t\"github.com/pressly/goose/v3\"\n)\n\nfunc init() {\n\tgoose.AddMigrationContext(Up00002, Down00002)\n}\n\nfunc Up00002(ctx context.Context, tx *sql.Tx) error {\n\t_, err := tx.ExecContext(ctx, \"UPDATE users SET username='admin' WHERE username='root';\")\n\treturn err\n}\n\nfunc Down00002(ctx context.Context, tx *sql.Tx) error {\n\t_, err := tx.ExecContext(ctx, \"UPDATE users SET username='root' WHERE username='admin';\")\n\treturn err\n}\n"
  },
  {
    "path": "examples/go-migrations/00003_add_user_no_tx.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\n\t\"github.com/pressly/goose/v3\"\n)\n\nfunc init() {\n\tgoose.AddMigrationNoTxContext(Up00003, Down00003)\n}\n\nfunc Up00003(ctx context.Context, db *sql.DB) error {\n\tid, err := getUserID(db, \"jamesbond\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif id == 0 {\n\t\tquery := \"INSERT INTO users (username, name, surname) VALUES ($1, $2, $3)\"\n\t\tif _, err := db.ExecContext(ctx, query, \"jamesbond\", \"James\", \"Bond\"); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc getUserID(db *sql.DB, username string) (int, error) {\n\tvar id int\n\terr := db.QueryRow(\"SELECT id FROM users WHERE username = $1\", username).Scan(&id)\n\tif err != nil && !errors.Is(err, sql.ErrNoRows) {\n\t\treturn 0, err\n\t}\n\treturn id, nil\n}\n\nfunc Down00003(ctx context.Context, db *sql.DB) error {\n\tquery := \"DELETE FROM users WHERE username = $1\"\n\tif _, err := db.ExecContext(ctx, query, \"jamesbond\"); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "examples/go-migrations/README.md",
    "content": "# SQL + Go migrations\n\n## This example: Custom goose binary with built-in Go migrations\n\n```bash\n$ go build -o goose-custom *.go\n```\n\n```bash\n$ ./goose-custom sqlite3 ./foo.db status\n    Applied At                  Migration\n    =======================================\n    Pending                  -- 00001_create_users_table.sql\n    Pending                  -- 00002_rename_root.go\n    Pending                  -- 00003_add_user_no_tx.go\n\n$ ./goose-custom sqlite3 ./foo.db up\n    OK   00001_create_users_table.sql (711.58µs)\n    OK   00002_rename_root.go (302.08µs)\n    OK   00003_add_user_no_tx.go (648.71µs)\n    goose: no migrations to run. current version: 3\n\n$ ./goose-custom sqlite3 ./foo.db status\n    Applied At                  Migration\n    =======================================\n    00001_create_users_table.sql\n    00002_rename_root.go\n    00003_add_user_no_tx.go\n```\n\n## Best practice: Split migrations into a standalone package\n\n1. Move [main.go](main.go) into your `cmd/` directory\n\n2. Rename package name in all `*_.go` migration files from `main` to `migrations`.\n\n3. Import this `migrations` package from your custom [cmd/main.go](main.go) file:\n\n   ```go\n   import (\n       // Invoke init() functions within migrations pkg.\n       _ \"github.com/pressly/goose/example/migrations-go\"\n   )\n   ```\n"
  },
  {
    "path": "examples/go-migrations/main.go",
    "content": "// This is custom goose binary with sqlite3 support only.\n\npackage main\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"log\"\n\t\"os\"\n\n\t\"github.com/pressly/goose/v3\"\n\t_ \"modernc.org/sqlite\"\n)\n\nvar (\n\tflags = flag.NewFlagSet(\"goose\", flag.ExitOnError)\n\tdir   = flags.String(\"dir\", \".\", \"directory with migration files\")\n)\n\nfunc main() {\n\tif err := flags.Parse(os.Args[1:]); err != nil {\n\t\tlog.Fatalf(\"goose: failed to parse flags: %v\", err)\n\t}\n\targs := flags.Args()\n\n\tif len(args) < 3 {\n\t\tflags.Usage()\n\t\treturn\n\t}\n\n\tdbstring, command := args[1], args[2]\n\n\tdb, err := goose.OpenDBWithDriver(\"sqlite\", dbstring)\n\tif err != nil {\n\t\tlog.Fatalf(\"goose: failed to open DB: %v\", err)\n\t}\n\n\tdefer func() {\n\t\tif err := db.Close(); err != nil {\n\t\t\tlog.Fatalf(\"goose: failed to close DB: %v\", err)\n\t\t}\n\t}()\n\n\targuments := []string{}\n\tif len(args) > 3 {\n\t\targuments = append(arguments, args[3:]...)\n\t}\n\n\tctx := context.Background()\n\tif err := goose.RunContext(ctx, command, db, *dir, arguments...); err != nil {\n\t\tlog.Fatalf(\"goose %v: %v\", command, err)\n\t}\n}\n"
  },
  {
    "path": "examples/sql-migrations/00001_create_users_table.sql",
    "content": "-- +goose Up\nCREATE TABLE users (\n    id int NOT NULL PRIMARY KEY,\n    username text,\n    name text,\n    surname text\n);\n\nINSERT INTO users VALUES\n(0, 'root', '', ''),\n(1, 'vojtechvitek', 'Vojtech', 'Vitek');\n\n-- +goose Down\nDROP TABLE users;\n"
  },
  {
    "path": "examples/sql-migrations/00002_rename_root.sql",
    "content": "-- +goose Up\n-- +goose StatementBegin\nUPDATE users SET username='admin' WHERE username='root';\n-- +goose StatementEnd\n\n-- +goose Down\n-- +goose StatementBegin\nUPDATE users SET username='root' WHERE username='admin';\n-- +goose StatementEnd\n"
  },
  {
    "path": "examples/sql-migrations/00003_no_transaction.sql",
    "content": "-- +goose NO TRANSACTION\n-- +goose Up\nCREATE TABLE post (\n    id int NOT NULL,\n    title text,\n    body text,\n    PRIMARY KEY(id)\n);\n\n-- +goose Down\nDROP TABLE post;\n"
  },
  {
    "path": "examples/sql-migrations/README.md",
    "content": "# SQL migrations only\n\nSee [this example](../go-migrations) for Go migrations.\n\n```bash\n$ go install github.com/pressly/goose/v3/cmd/goose@latest\n```\n\n```bash\n$ goose sqlite3 ./foo.db status\n    Applied At                  Migration\n    =======================================\n    Pending                  -- 00001_create_users_table.sql\n    Pending                  -- 00002_rename_root.sql\n\n$ goose sqlite3 ./foo.db up\nOK    00001_create_users_table.sql\nOK    00002_rename_root.sql\ngoose: no migrations to run. current version: 2\n\n$ goose sqlite3 ./foo.db status\n    Applied At                  Migration\n    =======================================\n    Mon Jun 19 21:56:00 2017 -- 00001_create_users_table.sql\n    Mon Jun 19 21:56:00 2017 -- 00002_rename_root.sql\n```\n"
  },
  {
    "path": "fix.go",
    "content": "package goose\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n)\n\nconst seqVersionTemplate = \"%05v\"\n\nfunc Fix(dir string) error {\n\t// always use osFS here because it's modifying operation\n\tmigrations, err := collectMigrationsFS(osFS{}, dir, minVersion, maxVersion, registeredGoMigrations)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// split into timestamped and versioned migrations\n\ttsMigrations, err := migrations.timestamped()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvMigrations, err := migrations.versioned()\n\tif err != nil {\n\t\treturn err\n\t}\n\t// Initial version.\n\tversion := int64(1)\n\tif last, err := vMigrations.Last(); err == nil {\n\t\tversion = last.Version + 1\n\t}\n\n\t// fix filenames by replacing timestamps with sequential versions\n\tfor _, tsm := range tsMigrations {\n\t\toldPath := tsm.Source\n\t\tnewPath := strings.Replace(\n\t\t\toldPath,\n\t\t\tfmt.Sprintf(\"%d\", tsm.Version),\n\t\t\tfmt.Sprintf(seqVersionTemplate, version),\n\t\t\t1,\n\t\t)\n\n\t\tif err := os.Rename(oldPath, newPath); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tlog.Printf(\"RENAMED %s => %s\", filepath.Base(oldPath), filepath.Base(newPath))\n\t\tversion++\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "fix_test.go",
    "content": "package goose\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestFix(t *testing.T) {\n\tt.Parallel()\n\tif testing.Short() {\n\t\tt.Skip(\"skip long running test\")\n\t}\n\n\tdir := t.TempDir()\n\tdefer os.Remove(\"./bin/fix-goose\") // clean up\n\n\tcommands := []string{\n\t\t\"go build -o ./bin/fix-goose ./cmd/goose\",\n\t\tfmt.Sprintf(\"./bin/fix-goose -dir=%s create create_table\", dir),\n\t\tfmt.Sprintf(\"./bin/fix-goose -dir=%s create add_users\", dir),\n\t\tfmt.Sprintf(\"./bin/fix-goose -dir=%s create add_indices\", dir),\n\t\tfmt.Sprintf(\"./bin/fix-goose -dir=%s create update_users\", dir),\n\t\tfmt.Sprintf(\"./bin/fix-goose -dir=%s fix\", dir),\n\t}\n\n\tfor _, cmd := range commands {\n\t\targs := strings.Split(cmd, \" \")\n\t\ttime.Sleep(1 * time.Second)\n\t\tcmd := exec.Command(args[0], args[1:]...)\n\t\tcmd.Env = os.Environ()\n\t\tout, err := cmd.CombinedOutput()\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"%s:\\n%v\\n\\n%s\", err, cmd, out)\n\t\t}\n\t}\n\n\tfiles, err := os.ReadDir(dir)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// check that the files are in order\n\tfor i, f := range files {\n\t\texpected := fmt.Sprintf(\"%05v\", i+1)\n\t\tif !strings.HasPrefix(f.Name(), expected) {\n\t\t\tt.Errorf(\"failed to find %s prefix in %s\", expected, f.Name())\n\t\t}\n\t}\n\n\t// add more migrations and then fix it\n\tcommands = []string{\n\t\tfmt.Sprintf(\"./bin/fix-goose -dir=%s create remove_column\", dir),\n\t\tfmt.Sprintf(\"./bin/fix-goose -dir=%s create create_books_table\", dir),\n\t\tfmt.Sprintf(\"./bin/fix-goose -dir=%s fix\", dir),\n\t}\n\n\tfor _, cmd := range commands {\n\t\targs := strings.Split(cmd, \" \")\n\t\ttime.Sleep(1 * time.Second)\n\t\tout, err := exec.Command(args[0], args[1:]...).CombinedOutput()\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"%s:\\n%v\\n\\n%s\", err, cmd, out)\n\t\t}\n\t}\n\n\tfiles, err = os.ReadDir(dir)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// check that the files still in order\n\tfor i, f := range files {\n\t\texpected := fmt.Sprintf(\"%05v\", i+1)\n\t\tif !strings.HasPrefix(f.Name(), expected) {\n\t\t\tt.Errorf(\"failed to find %s prefix in %s\", expected, f.Name())\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "globals.go",
    "content": "package goose\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"path/filepath\"\n)\n\nvar (\n\tregisteredGoMigrations = make(map[int64]*Migration)\n)\n\n// ResetGlobalMigrations resets the global Go migrations registry.\n//\n// Not safe for concurrent use.\nfunc ResetGlobalMigrations() {\n\tregisteredGoMigrations = make(map[int64]*Migration)\n}\n\n// SetGlobalMigrations registers Go migrations globally. It returns an error if a migration with the\n// same version has already been registered. Go migrations must be constructed using the\n// [NewGoMigration] function.\n//\n// Not safe for concurrent use.\nfunc SetGlobalMigrations(migrations ...*Migration) error {\n\tfor _, m := range migrations {\n\t\tif _, ok := registeredGoMigrations[m.Version]; ok {\n\t\t\treturn fmt.Errorf(\"go migration with version %d already registered\", m.Version)\n\t\t}\n\t\tif err := checkGoMigration(m); err != nil {\n\t\t\treturn fmt.Errorf(\"invalid go migration: %w\", err)\n\t\t}\n\t\tregisteredGoMigrations[m.Version] = m\n\t}\n\treturn nil\n}\n\nfunc checkGoMigration(m *Migration) error {\n\tif !m.construct {\n\t\treturn errors.New(\"must use NewGoMigration to construct migrations\")\n\t}\n\tif !m.Registered {\n\t\treturn errors.New(\"must be registered\")\n\t}\n\tif m.Type != TypeGo {\n\t\treturn fmt.Errorf(\"type must be %q\", TypeGo)\n\t}\n\tif m.Version < 1 {\n\t\treturn errors.New(\"version must be greater than zero\")\n\t}\n\tif m.Source != \"\" {\n\t\tif filepath.Ext(m.Source) != \".go\" {\n\t\t\treturn fmt.Errorf(\"source must have .go extension: %q\", m.Source)\n\t\t}\n\t\t// If the source is set, expect it to be a path with a numeric component that matches the\n\t\t// version. This field is not intended to be used for descriptive purposes.\n\t\tversion, err := NumericComponent(m.Source)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"invalid source: %w\", err)\n\t\t}\n\t\tif version != m.Version {\n\t\t\treturn fmt.Errorf(\"version:%d does not match numeric component in source %q\", m.Version, m.Source)\n\t\t}\n\t}\n\tif err := checkGoFunc(m.goUp); err != nil {\n\t\treturn fmt.Errorf(\"up function: %w\", err)\n\t}\n\tif err := checkGoFunc(m.goDown); err != nil {\n\t\treturn fmt.Errorf(\"down function: %w\", err)\n\t}\n\tif m.UpFnContext != nil && m.UpFnNoTxContext != nil {\n\t\treturn errors.New(\"must specify exactly one of UpFnContext or UpFnNoTxContext\")\n\t}\n\tif m.UpFn != nil && m.UpFnNoTx != nil {\n\t\treturn errors.New(\"must specify exactly one of UpFn or UpFnNoTx\")\n\t}\n\tif m.DownFnContext != nil && m.DownFnNoTxContext != nil {\n\t\treturn errors.New(\"must specify exactly one of DownFnContext or DownFnNoTxContext\")\n\t}\n\tif m.DownFn != nil && m.DownFnNoTx != nil {\n\t\treturn errors.New(\"must specify exactly one of DownFn or DownFnNoTx\")\n\t}\n\treturn nil\n}\n\nfunc checkGoFunc(f *GoFunc) error {\n\tif f.RunTx != nil && f.RunDB != nil {\n\t\treturn errors.New(\"must specify exactly one of RunTx or RunDB\")\n\t}\n\tswitch f.Mode {\n\tcase TransactionEnabled, TransactionDisabled:\n\t\t// No functions, but mode is set. This is not an error. It means the user wants to\n\t\t// record a version with the given mode but not run any functions.\n\tdefault:\n\t\treturn fmt.Errorf(\"invalid mode: %d\", f.Mode)\n\t}\n\tif f.RunDB != nil && f.Mode != TransactionDisabled {\n\t\treturn fmt.Errorf(\"transaction mode must be disabled or unspecified when RunDB is set\")\n\t}\n\tif f.RunTx != nil && f.Mode != TransactionEnabled {\n\t\treturn fmt.Errorf(\"transaction mode must be enabled or unspecified when RunTx is set\")\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "globals_test.go",
    "content": "package goose\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNewGoMigration(t *testing.T) {\n\tt.Run(\"valid_both_nil\", func(t *testing.T) {\n\t\tm := NewGoMigration(1, nil, nil)\n\t\t// roundtrip\n\t\trequire.EqualValues(t, 1, m.Version)\n\t\trequire.Equal(t, TypeGo, m.Type)\n\t\trequire.True(t, m.Registered)\n\t\trequire.EqualValues(t, -1, m.Next)\n\t\trequire.EqualValues(t, -1, m.Previous)\n\t\trequire.Empty(t, m.Source)\n\t\trequire.Nil(t, m.UpFnNoTxContext)\n\t\trequire.Nil(t, m.DownFnNoTxContext)\n\t\trequire.Nil(t, m.UpFnContext)\n\t\trequire.Nil(t, m.DownFnContext)\n\t\trequire.Nil(t, m.UpFn)\n\t\trequire.Nil(t, m.DownFn)\n\t\trequire.Nil(t, m.UpFnNoTx)\n\t\trequire.Nil(t, m.DownFnNoTx)\n\t\trequire.NotNil(t, m.goUp)\n\t\trequire.NotNil(t, m.goDown)\n\t\trequire.Equal(t, TransactionEnabled, m.goUp.Mode)\n\t\trequire.Equal(t, TransactionEnabled, m.goDown.Mode)\n\t})\n\tt.Run(\"all_set\", func(t *testing.T) {\n\t\t// This will eventually be an error when registering migrations.\n\t\tm := NewGoMigration(\n\t\t\t1,\n\t\t\t&GoFunc{RunTx: func(context.Context, *sql.Tx) error { return nil }, RunDB: func(context.Context, *sql.DB) error { return nil }},\n\t\t\t&GoFunc{RunTx: func(context.Context, *sql.Tx) error { return nil }, RunDB: func(context.Context, *sql.DB) error { return nil }},\n\t\t)\n\t\t// check only functions\n\t\trequire.NotNil(t, m.UpFn)\n\t\trequire.NotNil(t, m.UpFnContext)\n\t\trequire.NotNil(t, m.UpFnNoTx)\n\t\trequire.NotNil(t, m.UpFnNoTxContext)\n\t\trequire.NotNil(t, m.DownFn)\n\t\trequire.NotNil(t, m.DownFnContext)\n\t\trequire.NotNil(t, m.DownFnNoTx)\n\t\trequire.NotNil(t, m.DownFnNoTxContext)\n\t})\n}\n\nfunc TestTransactionMode(t *testing.T) {\n\tt.Cleanup(ResetGlobalMigrations)\n\n\trunDB := func(context.Context, *sql.DB) error { return nil }\n\trunTx := func(context.Context, *sql.Tx) error { return nil }\n\n\terr := SetGlobalMigrations(\n\t\tNewGoMigration(1, &GoFunc{RunTx: runTx, RunDB: runDB}, nil), // cannot specify both\n\t)\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"up function: must specify exactly one of RunTx or RunDB\")\n\terr = SetGlobalMigrations(\n\t\tNewGoMigration(1, nil, &GoFunc{RunTx: runTx, RunDB: runDB}), // cannot specify both\n\t)\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"down function: must specify exactly one of RunTx or RunDB\")\n\terr = SetGlobalMigrations(\n\t\tNewGoMigration(1, &GoFunc{RunTx: runTx, Mode: TransactionDisabled}, nil), // invalid explicit mode tx\n\t)\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"up function: transaction mode must be enabled or unspecified when RunTx is set\")\n\terr = SetGlobalMigrations(\n\t\tNewGoMigration(1, nil, &GoFunc{RunTx: runTx, Mode: TransactionDisabled}), // invalid explicit mode tx\n\t)\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"down function: transaction mode must be enabled or unspecified when RunTx is set\")\n\terr = SetGlobalMigrations(\n\t\tNewGoMigration(1, &GoFunc{RunDB: runDB, Mode: TransactionEnabled}, nil), // invalid explicit mode no-tx\n\t)\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"up function: transaction mode must be disabled or unspecified when RunDB is set\")\n\terr = SetGlobalMigrations(\n\t\tNewGoMigration(1, nil, &GoFunc{RunDB: runDB, Mode: TransactionEnabled}), // invalid explicit mode no-tx\n\t)\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"down function: transaction mode must be disabled or unspecified when RunDB is set\")\n\n\tt.Run(\"default_mode\", func(t *testing.T) {\n\t\tt.Cleanup(ResetGlobalMigrations)\n\n\t\tm := NewGoMigration(1, nil, nil)\n\t\terr = SetGlobalMigrations(m)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, registeredGoMigrations, 1)\n\t\tregistered := registeredGoMigrations[1]\n\t\trequire.NotNil(t, registered.goUp)\n\t\trequire.NotNil(t, registered.goDown)\n\t\trequire.Equal(t, TransactionEnabled, registered.goUp.Mode)\n\t\trequire.Equal(t, TransactionEnabled, registered.goDown.Mode)\n\n\t\tmigration2 := NewGoMigration(2, nil, nil)\n\t\t// reset so we can check the default is set\n\t\tmigration2.goUp.Mode, migration2.goDown.Mode = 0, 0\n\t\terr = SetGlobalMigrations(migration2)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"invalid go migration: up function: invalid mode: 0\")\n\n\t\tmigration3 := NewGoMigration(3, nil, nil)\n\t\t// reset so we can check the default is set\n\t\tmigration3.goDown.Mode = 0\n\t\terr = SetGlobalMigrations(migration3)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"invalid go migration: down function: invalid mode: 0\")\n\t})\n\tt.Run(\"unknown_mode\", func(t *testing.T) {\n\t\tm := NewGoMigration(1, nil, nil)\n\t\tm.goUp.Mode, m.goDown.Mode = 3, 3 // reset to default\n\t\terr := SetGlobalMigrations(m)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"invalid mode: 3\")\n\t})\n}\n\nfunc TestLegacyFunctions(t *testing.T) {\n\tt.Cleanup(ResetGlobalMigrations)\n\n\trunDB := func(context.Context, *sql.DB) error { return nil }\n\trunTx := func(context.Context, *sql.Tx) error { return nil }\n\n\tassertMigration := func(t *testing.T, m *Migration, version int64) {\n\t\tt.Helper()\n\t\trequire.Equal(t, version, m.Version)\n\t\trequire.Equal(t, TypeGo, m.Type)\n\t\trequire.True(t, m.Registered)\n\t\trequire.EqualValues(t, -1, m.Next)\n\t\trequire.EqualValues(t, -1, m.Previous)\n\t\trequire.Empty(t, m.Source)\n\t}\n\n\tt.Run(\"all_tx\", func(t *testing.T) {\n\t\tt.Cleanup(ResetGlobalMigrations)\n\t\terr := SetGlobalMigrations(\n\t\t\tNewGoMigration(1, &GoFunc{RunTx: runTx}, &GoFunc{RunTx: runTx}),\n\t\t)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, registeredGoMigrations, 1)\n\t\tm := registeredGoMigrations[1]\n\t\tassertMigration(t, m, 1)\n\t\t// Legacy functions.\n\t\trequire.Nil(t, m.UpFnNoTxContext)\n\t\trequire.Nil(t, m.DownFnNoTxContext)\n\t\t// Context-aware functions.\n\t\trequire.NotNil(t, m.goUp)\n\t\trequire.NotNil(t, m.UpFnContext)\n\t\trequire.NotNil(t, m.goDown)\n\t\trequire.NotNil(t, m.DownFnContext)\n\t\t// Always nil\n\t\trequire.NotNil(t, m.UpFn)\n\t\trequire.NotNil(t, m.DownFn)\n\t\trequire.Nil(t, m.UpFnNoTx)\n\t\trequire.Nil(t, m.DownFnNoTx)\n\t})\n\tt.Run(\"all_db\", func(t *testing.T) {\n\t\tt.Cleanup(ResetGlobalMigrations)\n\t\terr := SetGlobalMigrations(\n\t\t\tNewGoMigration(2, &GoFunc{RunDB: runDB}, &GoFunc{RunDB: runDB}),\n\t\t)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, registeredGoMigrations, 1)\n\t\tm := registeredGoMigrations[2]\n\t\tassertMigration(t, m, 2)\n\t\t// Legacy functions.\n\t\trequire.NotNil(t, m.UpFnNoTxContext)\n\t\trequire.NotNil(t, m.goUp)\n\t\trequire.NotNil(t, m.DownFnNoTxContext)\n\t\trequire.NotNil(t, m.goDown)\n\t\t// Context-aware functions.\n\t\trequire.Nil(t, m.UpFnContext)\n\t\trequire.Nil(t, m.DownFnContext)\n\t\t// Always nil\n\t\trequire.Nil(t, m.UpFn)\n\t\trequire.Nil(t, m.DownFn)\n\t\trequire.NotNil(t, m.UpFnNoTx)\n\t\trequire.NotNil(t, m.DownFnNoTx)\n\t})\n}\n\nfunc TestGlobalRegister(t *testing.T) {\n\tt.Cleanup(ResetGlobalMigrations)\n\n\t// runDB := func(context.Context, *sql.DB) error { return nil }\n\trunTx := func(context.Context, *sql.Tx) error { return nil }\n\n\t// Success.\n\terr := SetGlobalMigrations([]*Migration{}...)\n\trequire.NoError(t, err)\n\terr = SetGlobalMigrations(\n\t\tNewGoMigration(1, &GoFunc{RunTx: runTx}, nil),\n\t)\n\trequire.NoError(t, err)\n\t// Try to register the same migration again.\n\terr = SetGlobalMigrations(\n\t\tNewGoMigration(1, &GoFunc{RunTx: runTx}, nil),\n\t)\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"go migration with version 1 already registered\")\n\terr = SetGlobalMigrations(&Migration{Registered: true, Version: 2, Type: TypeGo})\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"must use NewGoMigration to construct migrations\")\n}\n\nfunc TestCheckMigration(t *testing.T) {\n\t// Success.\n\terr := checkGoMigration(NewGoMigration(1, nil, nil))\n\trequire.NoError(t, err)\n\t// Failures.\n\terr = checkGoMigration(&Migration{})\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"must use NewGoMigration to construct migrations\")\n\terr = checkGoMigration(&Migration{construct: true})\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"must be registered\")\n\terr = checkGoMigration(&Migration{construct: true, Registered: true})\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), `type must be \"go\"`)\n\terr = checkGoMigration(&Migration{construct: true, Registered: true, Type: TypeGo})\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"version must be greater than zero\")\n\terr = checkGoMigration(&Migration{construct: true, Registered: true, Type: TypeGo, Version: 1, goUp: &GoFunc{}, goDown: &GoFunc{}})\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"up function: invalid mode: 0\")\n\terr = checkGoMigration(&Migration{construct: true, Registered: true, Type: TypeGo, Version: 1, goUp: &GoFunc{Mode: TransactionEnabled}, goDown: &GoFunc{}})\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"down function: invalid mode: 0\")\n\t// Success.\n\terr = checkGoMigration(&Migration{construct: true, Registered: true, Type: TypeGo, Version: 1, goUp: &GoFunc{Mode: TransactionEnabled}, goDown: &GoFunc{Mode: TransactionEnabled}})\n\trequire.NoError(t, err)\n\t// Failures.\n\terr = checkGoMigration(&Migration{construct: true, Registered: true, Type: TypeGo, Version: 1, Source: \"foo\"})\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), `source must have .go extension: \"foo\"`)\n\terr = checkGoMigration(&Migration{construct: true, Registered: true, Type: TypeGo, Version: 1, Source: \"foo.go\"})\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), `no filename separator '_' found`)\n\terr = checkGoMigration(&Migration{construct: true, Registered: true, Type: TypeGo, Version: 2, Source: \"00001_foo.sql\"})\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), `source must have .go extension: \"00001_foo.sql\"`)\n\terr = checkGoMigration(&Migration{construct: true, Registered: true, Type: TypeGo, Version: 2, Source: \"00001_foo.go\"})\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), `version:2 does not match numeric component in source \"00001_foo.go\"`)\n\terr = checkGoMigration(&Migration{construct: true, Registered: true, Type: TypeGo, Version: 1,\n\t\tUpFnContext:     func(context.Context, *sql.Tx) error { return nil },\n\t\tUpFnNoTxContext: func(context.Context, *sql.DB) error { return nil },\n\t\tgoUp:            &GoFunc{Mode: TransactionEnabled},\n\t\tgoDown:          &GoFunc{Mode: TransactionEnabled},\n\t})\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"must specify exactly one of UpFnContext or UpFnNoTxContext\")\n\terr = checkGoMigration(&Migration{construct: true, Registered: true, Type: TypeGo, Version: 1,\n\t\tDownFnContext:     func(context.Context, *sql.Tx) error { return nil },\n\t\tDownFnNoTxContext: func(context.Context, *sql.DB) error { return nil },\n\t\tgoUp:              &GoFunc{Mode: TransactionEnabled},\n\t\tgoDown:            &GoFunc{Mode: TransactionEnabled},\n\t})\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"must specify exactly one of DownFnContext or DownFnNoTxContext\")\n\terr = checkGoMigration(&Migration{construct: true, Registered: true, Type: TypeGo, Version: 1,\n\t\tUpFn:     func(*sql.Tx) error { return nil },\n\t\tUpFnNoTx: func(*sql.DB) error { return nil },\n\t\tgoUp:     &GoFunc{Mode: TransactionEnabled},\n\t\tgoDown:   &GoFunc{Mode: TransactionEnabled},\n\t})\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"must specify exactly one of UpFn or UpFnNoTx\")\n\terr = checkGoMigration(&Migration{construct: true, Registered: true, Type: TypeGo, Version: 1,\n\t\tDownFn:     func(*sql.Tx) error { return nil },\n\t\tDownFnNoTx: func(*sql.DB) error { return nil },\n\t\tgoUp:       &GoFunc{Mode: TransactionEnabled},\n\t\tgoDown:     &GoFunc{Mode: TransactionEnabled},\n\t})\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"must specify exactly one of DownFn or DownFnNoTx\")\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/pressly/goose/v3\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/ClickHouse/clickhouse-go/v2 v2.43.0\n\tgithub.com/containerd/errdefs v1.0.0\n\tgithub.com/go-sql-driver/mysql v1.9.3\n\tgithub.com/jackc/pgx/v5 v5.8.0\n\tgithub.com/joho/godotenv v1.5.1\n\tgithub.com/mfridman/interpolate v0.0.2\n\tgithub.com/mfridman/xflag v0.1.0\n\tgithub.com/microsoft/go-mssqldb v1.9.6\n\tgithub.com/moby/moby/api v1.54.0\n\tgithub.com/moby/moby/client v0.3.0\n\tgithub.com/sethvargo/go-retry v0.3.0\n\tgithub.com/stretchr/testify v1.11.1\n\tgithub.com/tursodatabase/libsql-client-go v0.0.0-20251219100830-236aa1ff8acc\n\tgithub.com/vertica/vertica-sql-go v1.3.5\n\tgithub.com/ydb-platform/ydb-go-sdk/v3 v3.127.0\n\tgithub.com/ziutek/mymysql v1.5.4\n\tgo.uber.org/multierr v1.11.0\n\tgolang.org/x/sync v0.20.0\n\tmodernc.org/sqlite v1.46.1\n)\n\nrequire (\n\tfilippo.io/edwards25519 v1.2.0 // indirect\n\tgithub.com/ClickHouse/ch-go v0.71.0 // indirect\n\tgithub.com/Microsoft/go-winio v0.6.2 // indirect\n\tgithub.com/andybalholm/brotli v1.2.0 // indirect\n\tgithub.com/antlr4-go/antlr/v4 v4.13.1 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/coder/websocket v1.8.14 // indirect\n\tgithub.com/containerd/errdefs/pkg v0.3.0 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/distribution/reference v0.6.0 // indirect\n\tgithub.com/docker/go-connections v0.6.0 // indirect\n\tgithub.com/docker/go-units v0.5.0 // indirect\n\tgithub.com/dustin/go-humanize v1.0.1 // indirect\n\tgithub.com/elastic/go-sysinfo v1.15.4 // indirect\n\tgithub.com/elastic/go-windows v1.0.2 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/go-faster/city v1.0.1 // indirect\n\tgithub.com/go-faster/errors v0.7.1 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/golang-jwt/jwt/v4 v4.5.2 // indirect\n\tgithub.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect\n\tgithub.com/golang-sql/sqlexp v0.1.0 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/jackc/pgpassfile v1.0.0 // indirect\n\tgithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect\n\tgithub.com/jackc/puddle/v2 v2.2.2 // indirect\n\tgithub.com/jonboulle/clockwork v0.5.0 // indirect\n\tgithub.com/klauspost/compress v1.18.4 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/moby/docker-image-spec v1.3.1 // indirect\n\tgithub.com/ncruces/go-strftime v1.0.0 // indirect\n\tgithub.com/opencontainers/go-digest v1.0.0 // indirect\n\tgithub.com/opencontainers/image-spec v1.1.1 // indirect\n\tgithub.com/paulmach/orb v0.12.0 // indirect\n\tgithub.com/pierrec/lz4/v4 v4.1.25 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgithub.com/prometheus/procfs v0.19.2 // indirect\n\tgithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect\n\tgithub.com/segmentio/asm v1.2.1 // indirect\n\tgithub.com/shopspring/decimal v1.4.0 // indirect\n\tgithub.com/ydb-platform/ydb-go-genproto v0.0.0-20260128080146-c4ed16b24b37 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.2.1 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect\n\tgo.opentelemetry.io/otel v1.40.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.40.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.40.0 // indirect\n\tgo.yaml.in/yaml/v3 v3.0.4 // indirect\n\tgolang.org/x/crypto v0.48.0 // indirect\n\tgolang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect\n\tgolang.org/x/net v0.50.0 // indirect\n\tgolang.org/x/sys v0.41.0 // indirect\n\tgolang.org/x/text v0.34.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect\n\tgoogle.golang.org/grpc v1.79.3 // indirect\n\tgoogle.golang.org/protobuf v1.36.11 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n\thowett.net/plist v1.0.1 // indirect\n\tmodernc.org/libc v1.68.0 // indirect\n\tmodernc.org/mathutil v1.7.1 // indirect\n\tmodernc.org/memory v1.11.0 // indirect\n)\n\nretract (\n\tv3.21.0 // Invalid replace directives\n\tv3.12.2 // Invalid module reference\n\tv3.12.1 // Invalid module reference\n\tv3.12.0 // Invalid module reference\n)\n"
  },
  {
    "path": "go.sum",
    "content": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\nfilippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=\nfilippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=\ngithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U=\ngithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=\ngithub.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=\ngithub.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA=\ngithub.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 h1:Wgf5rZba3YZqeTNJPtvqZoBu1sBN/L4sry+u2U3Y75w=\ngithub.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1/go.mod h1:xxCBG/f/4Vbmh2XQJBsOmNdxWUY5j/s27jujKPbQf14=\ngithub.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI=\ngithub.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww=\ngithub.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=\ngithub.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/ClickHouse/ch-go v0.71.0 h1:bUdZ/EZj/LcVHsMqaRUP2holqygrPWQKeMjc6nZoyRM=\ngithub.com/ClickHouse/ch-go v0.71.0/go.mod h1:NwbNc+7jaqfY58dmdDUbG4Jl22vThgx1cYjBw0vtgXw=\ngithub.com/ClickHouse/clickhouse-go/v2 v2.43.0 h1:fUR05TrF1GyvLDa/mAQjkx7KbgwdLRffs2n9O3WobtE=\ngithub.com/ClickHouse/clickhouse-go/v2 v2.43.0/go.mod h1:o6jf7JM/zveWC/PP277BLxjHy5KjnGX/jfljhM4s34g=\ngithub.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=\ngithub.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=\ngithub.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=\ngithub.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=\ngithub.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=\ngithub.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=\ngithub.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=\ngithub.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=\ngithub.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=\ngithub.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=\ngithub.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=\ngithub.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=\ngithub.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=\ngithub.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=\ngithub.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=\ngithub.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=\ngithub.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=\ngithub.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=\ngithub.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=\ngithub.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=\ngithub.com/elastic/go-sysinfo v1.8.1/go.mod h1:JfllUnzoQV/JRYymbH3dO1yggI3mV2oTKSXsDHM+uIM=\ngithub.com/elastic/go-sysinfo v1.15.4 h1:A3zQcunCxik14MgXu39cXFXcIw2sFXZ0zL886eyiv1Q=\ngithub.com/elastic/go-sysinfo v1.15.4/go.mod h1:ZBVXmqS368dOn/jvijV/zHLfakWTYHBZPk3G244lHrU=\ngithub.com/elastic/go-windows v1.0.0/go.mod h1:TsU0Nrp7/y3+VwE82FoZF8gC/XFg/Elz6CcloAxnPgU=\ngithub.com/elastic/go-windows v1.0.2 h1:yoLLsAsV5cfg9FLhZ9EXZ2n2sQFKeDYrHenkcivY4vI=\ngithub.com/elastic/go-windows v1.0.2/go.mod h1:bGcDpBzXgYSqM0Gx3DM4+UxFj300SZLixie9u9ixLM8=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=\ngithub.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=\ngithub.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=\ngithub.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=\ngithub.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=\ngithub.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=\ngithub.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=\ngithub.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=\ngithub.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=\ngithub.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=\ngithub.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=\ngithub.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=\ngithub.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=\ngithub.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=\ngithub.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=\ngithub.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=\ngithub.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=\ngithub.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=\ngithub.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=\ngithub.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=\ngithub.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=\ngithub.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=\ngithub.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=\ngithub.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak=\ngithub.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=\ngithub.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=\ngithub.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=\ngithub.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=\ngithub.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=\ngithub.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=\ngithub.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=\ngithub.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=\ngithub.com/mfridman/xflag v0.1.0 h1:TWZrZwG1QklFX5S4j1vxfF1sZbZeZSGofMwPMLAF29M=\ngithub.com/mfridman/xflag v0.1.0/go.mod h1:/483ywM5ZO5SuMVjrIGquYNE5CzLrj5Ux/LxWWnjRaE=\ngithub.com/microsoft/go-mssqldb v1.9.6 h1:1MNQg5UiSsokiPz3++K2KPx4moKrwIqly1wv+RyCKTw=\ngithub.com/microsoft/go-mssqldb v1.9.6/go.mod h1:yYMPDufyoF2vVuVCUGtZARr06DKFIhMrluTcgWlXpr4=\ngithub.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=\ngithub.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=\ngithub.com/moby/moby/api v1.54.0 h1:7kbUgyiKcoBhm0UrWbdrMs7RX8dnwzURKVbZGy2GnL0=\ngithub.com/moby/moby/api v1.54.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=\ngithub.com/moby/moby/client v0.3.0 h1:UUGL5okry+Aomj3WhGt9Aigl3ZOxZGqR7XPo+RLPlKs=\ngithub.com/moby/moby/client v0.3.0/go.mod h1:HJgFbJRvogDQjbM8fqc1MCEm4mIAGMLjXbgwoZp6jCQ=\ngithub.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=\ngithub.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=\ngithub.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=\ngithub.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=\ngithub.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=\ngithub.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=\ngithub.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=\ngithub.com/paulmach/orb v0.12.0 h1:z+zOwjmG3MyEEqzv92UN49Lg1JFYx0L9GpGKNVDKk1s=\ngithub.com/paulmach/orb v0.12.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=\ngithub.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=\ngithub.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0=\ngithub.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=\ngithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=\ngithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/procfs v0.0.0-20190425082905-87a4384529e0/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=\ngithub.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=\ngithub.com/rekby/fixenv v0.6.1 h1:jUFiSPpajT4WY2cYuc++7Y1zWrnCxnovGCIX72PZniM=\ngithub.com/rekby/fixenv v0.6.1/go.mod h1:/b5LRc06BYJtslRtHKxsPWFT/ySpHV+rWvzTg+XWk4c=\ngithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=\ngithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=\ngithub.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=\ngithub.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=\ngithub.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=\ngithub.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=\ngithub.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=\ngithub.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=\ngithub.com/tursodatabase/libsql-client-go v0.0.0-20251219100830-236aa1ff8acc h1:lzi/5fg2EfinRlh3v//YyIhnc4tY7BTqazQGwb1ar+0=\ngithub.com/tursodatabase/libsql-client-go v0.0.0-20251219100830-236aa1ff8acc/go.mod h1:08inkKyguB6CGGssc/JzhmQWwBgFQBgjlYFjxjRh7nU=\ngithub.com/vertica/vertica-sql-go v1.3.5 h1:IrfH2WIgzZ45yDHyjVFrXU2LuKNIjF5Nwi90a6cfgUI=\ngithub.com/vertica/vertica-sql-go v1.3.5/go.mod h1:jnn2GFuv+O2Jcjktb7zyc4Utlbu9YVqpHH/lx63+1M4=\ngithub.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=\ngithub.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=\ngithub.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=\ngithub.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=\ngithub.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=\ngithub.com/ydb-platform/ydb-go-genproto v0.0.0-20260128080146-c4ed16b24b37 h1:kUXMT/fM/DpDT66WQgRUf3I8VOAWjypkMf52W5PChwA=\ngithub.com/ydb-platform/ydb-go-genproto v0.0.0-20260128080146-c4ed16b24b37/go.mod h1:Er+FePu1dNUieD+XTMDduGpQuCPssK5Q4BjF+IIXJ3I=\ngithub.com/ydb-platform/ydb-go-sdk/v3 v3.127.0 h1:OfHS9ZkZgCy6y/CJ9N8123DXrgaY2BPxWsQiQ8e3wC8=\ngithub.com/ydb-platform/ydb-go-sdk/v3 v3.127.0/go.mod h1:stS1mQYjbJvwwYaYzKyFY9eMiuVXWWXQA6T+SpOLg9c=\ngithub.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=\ngithub.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=\ngo.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=\ngo.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=\ngo.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=\ngo.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=\ngo.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=\ngo.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=\ngo.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=\ngo.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=\ngo.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=\ngo.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=\ngo.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=\ngo.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=\ngo.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=\ngo.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=\ngo.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=\ngo.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=\ngolang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=\ngolang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=\ngolang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=\ngolang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=\ngolang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=\ngolang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=\ngolang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=\ngolang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=\ngolang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=\ngonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=\ngoogle.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=\ngoogle.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=\ngoogle.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=\ngoogle.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=\ngopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=\ngotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhowett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=\nhowett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=\nhowett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=\nmodernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=\nmodernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=\nmodernc.org/ccgo/v4 v4.30.2 h1:4yPaaq9dXYXZ2V8s1UgrC3KIj580l2N4ClrLwnbv2so=\nmodernc.org/ccgo/v4 v4.30.2/go.mod h1:yZMnhWEdW0qw3EtCndG1+ldRrVGS+bIwyWmAWzS0XEw=\nmodernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=\nmodernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=\nmodernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=\nmodernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=\nmodernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=\nmodernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=\nmodernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=\nmodernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=\nmodernc.org/libc v1.68.0 h1:PJ5ikFOV5pwpW+VqCK1hKJuEWsonkIJhhIXyuF/91pQ=\nmodernc.org/libc v1.68.0/go.mod h1:NnKCYeoYgsEqnY3PgvNgAeaJnso968ygU8Z0DxjoEc0=\nmodernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=\nmodernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=\nmodernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=\nmodernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=\nmodernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=\nmodernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=\nmodernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=\nmodernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=\nmodernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=\nmodernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=\nmodernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=\nmodernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=\nmodernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=\nmodernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=\npgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=\npgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=\n"
  },
  {
    "path": "goose.go",
    "content": "package goose\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"strconv\"\n)\n\n// Deprecated: VERSION will no longer be supported in the next major release.\nconst VERSION = \"v3.18.0\"\n\nvar (\n\tminVersion      = int64(0)\n\tmaxVersion      = int64((1 << 63) - 1)\n\ttimestampFormat = \"20060102150405\"\n\tverbose         = false\n\tnoColor         = false\n\n\t// base fs to lookup migrations\n\tbaseFS fs.FS = osFS{}\n)\n\n// SetVerbose set the goose verbosity mode\nfunc SetVerbose(v bool) {\n\tverbose = v\n}\n\n// SetBaseFS sets a base FS to discover migrations. It can be used with 'embed' package.\n// Calling with 'nil' argument leads to default behaviour: discovering migrations from os filesystem.\n// Note that modifying operations like Create will use os filesystem anyway.\nfunc SetBaseFS(fsys fs.FS) {\n\tif fsys == nil {\n\t\tfsys = osFS{}\n\t}\n\n\tbaseFS = fsys\n}\n\n// Run runs a goose command.\n//\n// Deprecated: Use RunContext.\nfunc Run(command string, db *sql.DB, dir string, args ...string) error {\n\tctx := context.Background()\n\treturn RunContext(ctx, command, db, dir, args...)\n}\n\n// RunContext runs a goose command.\nfunc RunContext(ctx context.Context, command string, db *sql.DB, dir string, args ...string) error {\n\treturn run(ctx, command, db, dir, args)\n}\n\n// RunWithOptions runs a goose command with options.\n//\n// Deprecated: Use RunWithOptionsContext.\nfunc RunWithOptions(command string, db *sql.DB, dir string, args []string, options ...OptionsFunc) error {\n\tctx := context.Background()\n\treturn RunWithOptionsContext(ctx, command, db, dir, args, options...)\n}\n\n// RunWithOptionsContext runs a goose command with options.\nfunc RunWithOptionsContext(ctx context.Context, command string, db *sql.DB, dir string, args []string, options ...OptionsFunc) error {\n\treturn run(ctx, command, db, dir, args, options...)\n}\n\nfunc run(ctx context.Context, command string, db *sql.DB, dir string, args []string, options ...OptionsFunc) error {\n\tswitch command {\n\tcase \"up\":\n\t\tif err := UpContext(ctx, db, dir, options...); err != nil {\n\t\t\treturn err\n\t\t}\n\tcase \"up-by-one\":\n\t\tif err := UpByOneContext(ctx, db, dir, options...); err != nil {\n\t\t\treturn err\n\t\t}\n\tcase \"up-to\":\n\t\tif len(args) == 0 {\n\t\t\treturn fmt.Errorf(\"up-to must be of form: goose DRIVER DBSTRING [OPTIONS] up-to VERSION\")\n\t\t}\n\n\t\tversion, err := strconv.ParseInt(args[0], 10, 64)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"version must be a number (got '%s')\", args[0])\n\t\t}\n\t\tif err := UpToContext(ctx, db, dir, version, options...); err != nil {\n\t\t\treturn err\n\t\t}\n\tcase \"create\":\n\t\tif len(args) == 0 {\n\t\t\treturn fmt.Errorf(\"create must be of form: goose DRIVER DBSTRING [OPTIONS] create NAME [go|sql]\")\n\t\t}\n\n\t\tmigrationType := \"go\"\n\t\tif len(args) == 2 {\n\t\t\tmigrationType = args[1]\n\t\t}\n\t\tif err := Create(db, dir, args[0], migrationType); err != nil {\n\t\t\treturn err\n\t\t}\n\tcase \"down\":\n\t\tif err := DownContext(ctx, db, dir, options...); err != nil {\n\t\t\treturn err\n\t\t}\n\tcase \"down-to\":\n\t\tif len(args) == 0 {\n\t\t\treturn fmt.Errorf(\"down-to must be of form: goose DRIVER DBSTRING [OPTIONS] down-to VERSION\")\n\t\t}\n\n\t\tversion, err := strconv.ParseInt(args[0], 10, 64)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"version must be a number (got '%s')\", args[0])\n\t\t}\n\t\tif err := DownToContext(ctx, db, dir, version, options...); err != nil {\n\t\t\treturn err\n\t\t}\n\tcase \"fix\":\n\t\tif err := Fix(dir); err != nil {\n\t\t\treturn err\n\t\t}\n\tcase \"redo\":\n\t\tif err := RedoContext(ctx, db, dir, options...); err != nil {\n\t\t\treturn err\n\t\t}\n\tcase \"reset\":\n\t\tif err := ResetContext(ctx, db, dir, options...); err != nil {\n\t\t\treturn err\n\t\t}\n\tcase \"status\":\n\t\tif err := StatusContext(ctx, db, dir, options...); err != nil {\n\t\t\treturn err\n\t\t}\n\tcase \"version\":\n\t\tif err := VersionContext(ctx, db, dir, options...); err != nil {\n\t\t\treturn err\n\t\t}\n\tdefault:\n\t\treturn fmt.Errorf(\"%q: no such command\", command)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "goose_cli_test.go",
    "content": "package goose_test\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\t_ \"modernc.org/sqlite\"\n)\n\nconst (\n\t// gooseTestBinaryVersion is utilized in conjunction with a linker variable to set the version\n\t// of a binary created solely for testing purposes. It is used to test the --version flag.\n\tgooseTestBinaryVersion = \"v0.0.0\"\n)\n\nfunc TestFullBinary(t *testing.T) {\n\tt.Parallel()\n\tcli := buildGooseCLI(t, false)\n\tout, err := cli.run(\"--version\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"goose version: \"+gooseTestBinaryVersion+\"\\n\", out)\n}\n\nfunc TestLiteBinary(t *testing.T) {\n\tt.Parallel()\n\tcli := buildGooseCLI(t, true)\n\n\tt.Run(\"binary_version\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tout, err := cli.run(\"--version\")\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"goose version: \"+gooseTestBinaryVersion+\"\\n\", out)\n\t})\n\tt.Run(\"default_binary\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tdir := t.TempDir()\n\t\ttotal := countSQLFiles(t, \"testdata/migrations\")\n\t\tcommands := []struct {\n\t\t\tcmd string\n\t\t\tout string\n\t\t}{\n\t\t\t{\"up\", \"goose: successfully migrated database to version: \" + strconv.Itoa(total)},\n\t\t\t{\"version\", \"goose: version \" + strconv.Itoa(total)},\n\t\t\t{\"down\", \"OK\"},\n\t\t\t{\"version\", \"goose: version \" + strconv.Itoa(total-1)},\n\t\t\t{\"status\", \"\"},\n\t\t\t{\"reset\", \"OK\"},\n\t\t\t{\"version\", \"goose: version 0\"},\n\t\t}\n\t\tfor _, c := range commands {\n\t\t\tout, err := cli.run(\"-dir=testdata/migrations\", \"sqlite3\", filepath.Join(dir, \"sql.db\"), c.cmd)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Contains(t, out, c.out)\n\t\t}\n\t})\n\tt.Run(\"gh_issue_532\", func(t *testing.T) {\n\t\t// https://github.com/pressly/goose/issues/532\n\t\tt.Parallel()\n\t\tdir := t.TempDir()\n\t\ttotal := countSQLFiles(t, \"testdata/migrations\")\n\t\t_, err := cli.run(\"-dir=testdata/migrations\", \"sqlite3\", filepath.Join(dir, \"sql.db\"), \"up\")\n\t\trequire.NoError(t, err)\n\t\tout, err := cli.run(\"-dir=testdata/migrations\", \"sqlite3\", filepath.Join(dir, \"sql.db\"), \"up\")\n\t\trequire.NoError(t, err)\n\t\trequire.Contains(t, out, \"goose: no migrations to run. current version: \"+strconv.Itoa(total))\n\t\tout, err = cli.run(\"-dir=testdata/migrations\", \"sqlite3\", filepath.Join(dir, \"sql.db\"), \"version\")\n\t\trequire.NoError(t, err)\n\t\trequire.Contains(t, out, \"goose: version \"+strconv.Itoa(total))\n\t})\n\tt.Run(\"gh_issue_293\", func(t *testing.T) {\n\t\t// https://github.com/pressly/goose/issues/293\n\t\tt.Parallel()\n\t\tdir := t.TempDir()\n\t\ttotal := countSQLFiles(t, \"testdata/migrations\")\n\t\tcommands := []struct {\n\t\t\tcmd string\n\t\t\tout string\n\t\t}{\n\t\t\t{\"up\", \"goose: successfully migrated database to version: \" + strconv.Itoa(total)},\n\t\t\t{\"version\", \"goose: version \" + strconv.Itoa(total)},\n\t\t\t{\"down\", \"OK\"},\n\t\t\t{\"down\", \"OK\"},\n\t\t\t{\"version\", \"goose: version \" + strconv.Itoa(total-2)},\n\t\t\t{\"up\", \"goose: successfully migrated database to version: \" + strconv.Itoa(total)},\n\t\t\t{\"status\", \"\"},\n\t\t}\n\t\tfor _, c := range commands {\n\t\t\tout, err := cli.run(\"-dir=testdata/migrations\", \"sqlite3\", filepath.Join(dir, \"sql.db\"), c.cmd)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Contains(t, out, c.out)\n\t\t}\n\t})\n\tt.Run(\"gh_issue_336\", func(t *testing.T) {\n\t\t// https://github.com/pressly/goose/issues/336\n\t\tt.Parallel()\n\t\tdir := t.TempDir()\n\t\t_, err := cli.run(\"-dir=\"+dir, \"sqlite3\", filepath.Join(dir, \"sql.db\"), \"up\")\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"goose run: no migration files found\")\n\t})\n\tt.Run(\"create_and_fix\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tdir := t.TempDir()\n\t\tcreateEmptyFile(t, dir, \"00001_alpha.sql\")\n\t\tcreateEmptyFile(t, dir, \"00003_bravo.sql\")\n\t\tcreateEmptyFile(t, dir, \"20230826163141_charlie.sql\")\n\t\tcreateEmptyFile(t, dir, \"20230826163151_delta.go\")\n\t\ttotal, err := os.ReadDir(dir)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, total, 4)\n\t\tmigrationFiles := []struct {\n\t\t\tname     string\n\t\t\tfileType string\n\t\t}{\n\t\t\t{\"echo\", \"sql\"},\n\t\t\t{\"foxtrot\", \"go\"},\n\t\t\t{\"golf\", \"\"},\n\t\t}\n\t\tfor i, f := range migrationFiles {\n\t\t\targs := []string{\"-dir=\" + dir, \"create\", f.name}\n\t\t\tif f.fileType != \"\" {\n\t\t\t\targs = append(args, f.fileType)\n\t\t\t}\n\t\t\tout, err := cli.run(args...)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Contains(t, out, \"Created new file\")\n\t\t\t// ensure different timestamps, granularity is 1 second\n\t\t\tif i < len(migrationFiles)-1 {\n\t\t\t\ttime.Sleep(1100 * time.Millisecond)\n\t\t\t}\n\t\t}\n\t\ttotal, err = os.ReadDir(dir)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, total, 7)\n\t\tout, err := cli.run(\"-dir=\"+dir, \"fix\")\n\t\trequire.NoError(t, err)\n\t\trequire.Contains(t, out, \"RENAMED\")\n\t\tfiles, err := os.ReadDir(dir)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, files, 7)\n\t\texpected := []string{\n\t\t\t\"00001_alpha.sql\",\n\t\t\t\"00003_bravo.sql\",\n\t\t\t\"00004_charlie.sql\",\n\t\t\t\"00005_delta.go\",\n\t\t\t\"00006_echo.sql\",\n\t\t\t\"00007_foxtrot.go\",\n\t\t\t\"00008_golf.go\",\n\t\t}\n\t\tfor i, f := range files {\n\t\t\trequire.Equal(t, f.Name(), expected[i])\n\t\t}\n\t})\n}\n\ntype gooseBinary struct {\n\tbinaryPath string\n}\n\nfunc (g gooseBinary) run(params ...string) (string, error) {\n\tcmd := exec.Command(g.binaryPath, params...)\n\tout, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to run goose command: %v\\nout: %v\", err, string(out))\n\t}\n\treturn string(out), nil\n}\n\n// buildGooseCLI builds goose test binary, which is used for testing goose CLI. It is built with all\n// drivers enabled, unless lite is true, in which case all drivers are disabled except sqlite3\nfunc buildGooseCLI(t *testing.T, lite bool) gooseBinary {\n\tt.Helper()\n\tbinName := \"goose-test\"\n\tdir := t.TempDir()\n\toutput := filepath.Join(dir, binName)\n\t// usage: go build [-o output] [build flags] [packages]\n\targs := []string{\n\t\t\"build\",\n\t\t\"-o\", output,\n\t\t\"-ldflags=-s -w -X main.version=\" + gooseTestBinaryVersion,\n\t}\n\tif lite {\n\t\targs = append(args, \"-tags=no_clickhouse no_mssql no_mysql no_vertica no_postgres\")\n\t}\n\targs = append(args, \"./cmd/goose\")\n\tbuild := exec.Command(\"go\", args...)\n\tout, err := build.CombinedOutput()\n\tif err != nil {\n\t\tt.Fatalf(\"failed to build %s binary: %v: %s\", binName, err, string(out))\n\t}\n\treturn gooseBinary{\n\t\tbinaryPath: output,\n\t}\n}\n\nfunc countSQLFiles(t *testing.T, dir string) int {\n\tt.Helper()\n\tfiles, err := filepath.Glob(filepath.Join(dir, \"*.sql\"))\n\trequire.NoError(t, err)\n\treturn len(files)\n}\n\nfunc createEmptyFile(t *testing.T, dir, name string) {\n\tt.Helper()\n\tpath := filepath.Join(dir, name)\n\tf, err := os.Create(path)\n\trequire.NoError(t, err)\n\tdefer f.Close()\n}\n"
  },
  {
    "path": "goose_embed_test.go",
    "content": "package goose_test\n\nimport (\n\t\"database/sql\"\n\t\"embed\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/pressly/goose/v3\"\n\t\"github.com/stretchr/testify/require\"\n\t_ \"modernc.org/sqlite\"\n)\n\n//go:embed testdata/migrations/*.sql\nvar embedMigrations embed.FS\n\nfunc TestEmbeddedMigrations(t *testing.T) {\n\tdir := t.TempDir()\n\t// not using t.Parallel here to avoid races\n\tdb, err := sql.Open(\"sqlite\", filepath.Join(dir, \"sql_embed.db\"))\n\trequire.NoError(t, err)\n\n\tdb.SetMaxOpenConns(1)\n\n\tmigrationFiles, err := fs.ReadDir(embedMigrations, \"testdata/migrations\")\n\trequire.NoError(t, err)\n\ttotal := len(migrationFiles)\n\n\t// decouple from existing structure\n\tfsys, err := fs.Sub(embedMigrations, \"testdata/migrations\")\n\trequire.NoError(t, err)\n\n\tgoose.SetBaseFS(fsys)\n\tt.Cleanup(func() { goose.SetBaseFS(nil) })\n\trequire.NoError(t, goose.SetDialect(\"sqlite3\"))\n\n\tt.Run(\"migration_cycle\", func(t *testing.T) {\n\t\terr := goose.Up(db, \".\")\n\t\trequire.NoError(t, err)\n\t\tver, err := goose.GetDBVersion(db)\n\t\trequire.NoError(t, err)\n\t\trequire.EqualValues(t, ver, total)\n\t\terr = goose.Reset(db, \".\")\n\t\trequire.NoError(t, err)\n\t\tver, err = goose.GetDBVersion(db)\n\t\trequire.NoError(t, err)\n\t\trequire.EqualValues(t, 0, ver)\n\t})\n\tt.Run(\"create_uses_os_fs\", func(t *testing.T) {\n\t\tdir := t.TempDir()\n\t\terr := goose.Create(db, dir, \"test\", \"sql\")\n\t\trequire.NoError(t, err)\n\t\tpaths, _ := filepath.Glob(filepath.Join(dir, \"*test.sql\"))\n\t\trequire.NotEmpty(t, paths)\n\t\terr = goose.Fix(dir)\n\t\trequire.NoError(t, err)\n\t\t_, err = os.Stat(filepath.Join(dir, \"00001_test.sql\"))\n\t\trequire.NoError(t, err)\n\t})\n}\n"
  },
  {
    "path": "helpers.go",
    "content": "package goose\n\nimport (\n\t\"bytes\"\n\t\"strings\"\n\t\"unicode\"\n\t\"unicode/utf8\"\n)\n\ntype camelSnakeStateMachine int\n\nconst ( //                                           _$$_This is some text, OK?!\n\tidle          camelSnakeStateMachine = iota // 0 ↑                     ↑   ↑\n\tfirstAlphaNum                               // 1     ↑    ↑  ↑    ↑     ↑\n\talphaNum                                    // 2      ↑↑↑  ↑  ↑↑↑  ↑↑↑   ↑\n\tdelimiter                                   // 3         ↑  ↑    ↑    ↑   ↑\n)\n\nfunc (s camelSnakeStateMachine) next(r rune) camelSnakeStateMachine {\n\tswitch s {\n\tcase idle:\n\t\tif isAlphaNum(r) {\n\t\t\treturn firstAlphaNum\n\t\t}\n\tcase firstAlphaNum:\n\t\tif isAlphaNum(r) {\n\t\t\treturn alphaNum\n\t\t}\n\t\treturn delimiter\n\tcase alphaNum:\n\t\tif !isAlphaNum(r) {\n\t\t\treturn delimiter\n\t\t}\n\tcase delimiter:\n\t\tif isAlphaNum(r) {\n\t\t\treturn firstAlphaNum\n\t\t}\n\t\treturn idle\n\t}\n\treturn s\n}\n\nfunc camelCase(str string) string {\n\tvar b strings.Builder\n\n\tstateMachine := idle\n\tfor i := 0; i < len(str); {\n\t\tr, size := utf8.DecodeRuneInString(str[i:])\n\t\ti += size\n\t\tstateMachine = stateMachine.next(r)\n\t\tswitch stateMachine {\n\t\tcase firstAlphaNum:\n\t\t\tb.WriteRune(unicode.ToUpper(r))\n\t\tcase alphaNum:\n\t\t\tb.WriteRune(unicode.ToLower(r))\n\t\t}\n\t}\n\treturn b.String()\n}\n\nfunc snakeCase(str string) string {\n\tvar b bytes.Buffer\n\n\tstateMachine := idle\n\tfor i := 0; i < len(str); {\n\t\tr, size := utf8.DecodeRuneInString(str[i:])\n\t\ti += size\n\t\tstateMachine = stateMachine.next(r)\n\t\tswitch stateMachine {\n\t\tcase firstAlphaNum, alphaNum:\n\t\t\tb.WriteRune(unicode.ToLower(r))\n\t\tcase delimiter:\n\t\t\tb.WriteByte('_')\n\t\t}\n\t}\n\tif stateMachine == idle {\n\t\treturn string(bytes.TrimSuffix(b.Bytes(), []byte{'_'}))\n\t}\n\treturn b.String()\n}\n\nfunc isAlphaNum(r rune) bool {\n\treturn unicode.IsLetter(r) || unicode.IsNumber(r)\n}\n"
  },
  {
    "path": "helpers_test.go",
    "content": "package goose\n\nimport (\n\t\"testing\"\n)\n\nfunc TestCamelSnake(t *testing.T) {\n\tt.Parallel()\n\n\ttt := []struct {\n\t\tin    string\n\t\tcamel string\n\t\tsnake string\n\t}{\n\t\t{in: \"Add updated_at to users table\", camel: \"AddUpdatedAtToUsersTable\", snake: \"add_updated_at_to_users_table\"},\n\t\t{in: \"$()&^%(_--crazy__--input$)\", camel: \"CrazyInput\", snake: \"crazy_input\"},\n\t}\n\n\tfor _, test := range tt {\n\t\tif got := camelCase(test.in); got != test.camel {\n\t\t\tt.Errorf(\"unexpected CamelCase for input(%q), got %q, want %q\", test.in, got, test.camel)\n\t\t}\n\t\tif got := snakeCase(test.in); got != test.snake {\n\t\t\tt.Errorf(\"unexpected snake_case for input(%q), got %q, want %q\", test.in, got, test.snake)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "install.sh",
    "content": "#!/bin/sh\n# Adapted from the Deno installer: Copyright 2019 the Deno authors. All rights reserved. MIT license.\n# Ref: https://github.com/denoland/deno_install\n# TODO(everyone): Keep this script simple and easily auditable.\n\n# TODO(mf): this should work on Linux and macOS. Not intended for Windows.\n\nset -e\n\nos=$(uname -s | tr '[:upper:]' '[:lower:]')\narch=$(uname -m)\n\nif [ \"$arch\" = \"aarch64\" ]; then\n\tarch=\"arm64\"\nfi\n\nif [ $# -eq 0 ]; then\n\tgoose_uri=\"https://github.com/pressly/goose/releases/latest/download/goose_${os}_${arch}\"\nelse\n\tgoose_uri=\"https://github.com/pressly/goose/releases/download/${1}/goose_${os}_${arch}\"\nfi\n\ngoose_install=\"${GOOSE_INSTALL:-/usr/local}\"\nbin_dir=\"${goose_install}/bin\"\nexe=\"${bin_dir}/goose\"\n\nif [ ! -d \"${bin_dir}\" ]; then\n\tmkdir -p \"${bin_dir}\"\nfi\n\ncurl --silent --show-error --location --fail --location --output \"${exe}\" \"$goose_uri\"\nchmod +x \"${exe}\"\n\necho \"Goose was installed successfully to ${exe}\"\nif command -v goose >/dev/null; then\n\techo \"Run 'goose --help' to get started\"\nfi\n"
  },
  {
    "path": "internal/controller/store.go",
    "content": "package controller\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/pressly/goose/v3/database\"\n)\n\n// A StoreController is used by the goose package to interact with a database. This type is a\n// wrapper around the Store interface, but can be extended to include additional (optional) methods\n// that are not part of the core Store interface.\ntype StoreController struct{ database.Store }\n\nvar _ database.StoreExtender = (*StoreController)(nil)\n\n// NewStoreController returns a new StoreController that wraps the given Store.\n//\n// If the Store implements the following optional methods, the StoreController will call them as\n// appropriate:\n//\n//   - TableExists(context.Context, DBTxConn) (bool, error)\n//\n// If the Store does not implement a method, it will either return a [errors.ErrUnsupported] error\n// or fall back to the default behavior.\nfunc NewStoreController(store database.Store) *StoreController {\n\treturn &StoreController{store}\n}\n\nfunc (c *StoreController) TableExists(ctx context.Context, db database.DBTxConn) (bool, error) {\n\tif t, ok := c.Store.(interface {\n\t\tTableExists(ctx context.Context, db database.DBTxConn) (bool, error)\n\t}); ok {\n\t\treturn t.TableExists(ctx, db)\n\t}\n\treturn false, errors.ErrUnsupported\n}\n"
  },
  {
    "path": "internal/dialects/clickhouse.go",
    "content": "package dialects\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/pressly/goose/v3/database/dialect\"\n)\n\n// NewClickhouse returns a new [dialect.Querier] for Clickhouse dialect.\nfunc NewClickhouse() dialect.Querier {\n\treturn &clickhouse{}\n}\n\ntype clickhouse struct{}\n\nvar _ dialect.Querier = (*clickhouse)(nil)\n\nfunc (c *clickhouse) CreateTable(tableName string) string {\n\tq := `CREATE TABLE IF NOT EXISTS %s (\n\t\tversion_id Int64,\n\t\tis_applied UInt8,\n\t\tdate Date default now(),\n\t\ttstamp DateTime default now()\n\t  )\n\t  ENGINE = MergeTree()\n\t\tORDER BY (date)`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (c *clickhouse) InsertVersion(tableName string) string {\n\tq := `INSERT INTO %s (version_id, is_applied) VALUES ($1, $2)`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (c *clickhouse) DeleteVersion(tableName string) string {\n\tq := `ALTER TABLE %s DELETE WHERE version_id = $1 SETTINGS mutations_sync = 2`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (c *clickhouse) GetMigrationByVersion(tableName string) string {\n\tq := `SELECT tstamp, is_applied FROM %s WHERE version_id = $1 ORDER BY tstamp DESC LIMIT 1`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (c *clickhouse) ListMigrations(tableName string) string {\n\tq := `SELECT version_id, is_applied FROM %s ORDER BY version_id DESC`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (c *clickhouse) GetLatestVersion(tableName string) string {\n\tq := `SELECT max(version_id) FROM %s`\n\treturn fmt.Sprintf(q, tableName)\n}\n"
  },
  {
    "path": "internal/dialects/dsql.go",
    "content": "package dialects\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/pressly/goose/v3/database/dialect\"\n)\n\n// NewAuroraDSQL returns a new [dialect.Querier] for Aurora DSQL dialect.\nfunc NewAuroraDSQL() dialect.QuerierExtender {\n\treturn &dsql{}\n}\n\ntype dsql struct{}\n\nvar _ dialect.QuerierExtender = (*dsql)(nil)\n\nfunc (d *dsql) CreateTable(tableName string) string {\n\tq := `CREATE TABLE %s (\n\t\tid integer PRIMARY KEY,\n\t\tversion_id bigint NOT NULL,\n\t\tis_applied boolean NOT NULL,\n\t\ttstamp timestamp NOT NULL DEFAULT now()\n\t)`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (d *dsql) InsertVersion(tableName string) string {\n\tq := `INSERT INTO %s (id, version_id, is_applied) \n\t      VALUES (\n\t          COALESCE((SELECT MAX(id) FROM %s), 0) + 1,\n\t          $1, \n\t          $2\n\t      )`\n\treturn fmt.Sprintf(q, tableName, tableName)\n}\n\nfunc (d *dsql) DeleteVersion(tableName string) string {\n\tq := `DELETE FROM %s WHERE version_id=$1`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (d *dsql) GetMigrationByVersion(tableName string) string {\n\tq := `SELECT tstamp, is_applied FROM %s WHERE version_id=$1 ORDER BY tstamp DESC LIMIT 1`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (d *dsql) ListMigrations(tableName string) string {\n\tq := `SELECT version_id, is_applied from %s ORDER BY id DESC`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (d *dsql) GetLatestVersion(tableName string) string {\n\tq := `SELECT max(version_id) FROM %s`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (d *dsql) TableExists(tableName string) string {\n\tschemaName, tableName := parseTableIdentifier(tableName)\n\tif schemaName != \"\" {\n\t\tq := `SELECT EXISTS ( SELECT 1 FROM pg_tables WHERE schemaname = '%s' AND tablename = '%s' )`\n\t\treturn fmt.Sprintf(q, schemaName, tableName)\n\t}\n\tq := `SELECT EXISTS ( SELECT 1 FROM pg_tables WHERE (current_schema() IS NULL OR schemaname = current_schema()) AND tablename = '%s' )`\n\treturn fmt.Sprintf(q, tableName)\n}\n"
  },
  {
    "path": "internal/dialects/mysql.go",
    "content": "package dialects\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/pressly/goose/v3/database/dialect\"\n)\n\n// NewMysql returns a new [dialect.Querier] for MySQL dialect.\nfunc NewMysql() dialect.QuerierExtender {\n\treturn &mysql{}\n}\n\ntype mysql struct{}\n\nvar _ dialect.QuerierExtender = (*mysql)(nil)\n\nfunc (m *mysql) CreateTable(tableName string) string {\n\tq := `CREATE TABLE %s (\n\t\tid bigint(20) unsigned NOT NULL AUTO_INCREMENT,\n\t\tversion_id bigint NOT NULL,\n\t\tis_applied boolean NOT NULL,\n\t\ttstamp timestamp NULL default now(),\n\t\tPRIMARY KEY(id)\n\t)`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (m *mysql) InsertVersion(tableName string) string {\n\tq := `INSERT INTO %s (version_id, is_applied) VALUES (?, ?)`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (m *mysql) DeleteVersion(tableName string) string {\n\tq := `DELETE FROM %s WHERE version_id=?`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (m *mysql) GetMigrationByVersion(tableName string) string {\n\tq := `SELECT tstamp, is_applied FROM %s WHERE version_id=? ORDER BY tstamp DESC LIMIT 1`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (m *mysql) ListMigrations(tableName string) string {\n\tq := `SELECT version_id, is_applied from %s ORDER BY id DESC`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (m *mysql) GetLatestVersion(tableName string) string {\n\tq := `SELECT MAX(version_id) FROM %s`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (m *mysql) TableExists(tableName string) string {\n\tschemaName, tableName := parseTableIdentifier(tableName)\n\tif schemaName != \"\" {\n\t\tq := `SELECT EXISTS ( SELECT 1 FROM information_schema.tables WHERE table_schema = '%s' AND table_name = '%s' )`\n\t\treturn fmt.Sprintf(q, schemaName, tableName)\n\t}\n\tq := `SELECT EXISTS ( SELECT 1 FROM information_schema.tables WHERE (database() IS NULL OR table_schema = database()) AND table_name = '%s' )`\n\treturn fmt.Sprintf(q, tableName)\n}\n"
  },
  {
    "path": "internal/dialects/postgres.go",
    "content": "package dialects\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/pressly/goose/v3/database/dialect\"\n)\n\n// NewPostgres returns a new [dialect.Querier] for PostgreSQL dialect.\nfunc NewPostgres() dialect.QuerierExtender {\n\treturn &postgres{}\n}\n\ntype postgres struct{}\n\nvar _ dialect.QuerierExtender = (*postgres)(nil)\n\nfunc (p *postgres) CreateTable(tableName string) string {\n\tq := `CREATE TABLE %s (\n\t\tid integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,\n\t\tversion_id bigint NOT NULL,\n\t\tis_applied boolean NOT NULL,\n\t\ttstamp timestamp NOT NULL DEFAULT now()\n\t)`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (p *postgres) InsertVersion(tableName string) string {\n\tq := `INSERT INTO %s (version_id, is_applied) VALUES ($1, $2)`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (p *postgres) DeleteVersion(tableName string) string {\n\tq := `DELETE FROM %s WHERE version_id=$1`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (p *postgres) GetMigrationByVersion(tableName string) string {\n\tq := `SELECT tstamp, is_applied FROM %s WHERE version_id=$1 ORDER BY tstamp DESC LIMIT 1`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (p *postgres) ListMigrations(tableName string) string {\n\tq := `SELECT version_id, is_applied from %s ORDER BY id DESC`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (p *postgres) GetLatestVersion(tableName string) string {\n\tq := `SELECT max(version_id) FROM %s`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (p *postgres) TableExists(tableName string) string {\n\tschemaName, tableName := parseTableIdentifier(tableName)\n\tif schemaName != \"\" {\n\t\tq := `SELECT EXISTS ( SELECT 1 FROM pg_tables WHERE schemaname = '%s' AND tablename = '%s' )`\n\t\treturn fmt.Sprintf(q, schemaName, tableName)\n\t}\n\tq := `SELECT EXISTS ( SELECT 1 FROM pg_tables WHERE (current_schema() IS NULL OR schemaname = current_schema()) AND tablename = '%s' )`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc parseTableIdentifier(name string) (schema, table string) {\n\tschema, table, found := strings.Cut(name, \".\")\n\tif !found {\n\t\treturn \"\", name\n\t}\n\treturn schema, table\n}\n"
  },
  {
    "path": "internal/dialects/redshift.go",
    "content": "package dialects\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/pressly/goose/v3/database/dialect\"\n)\n\n// Redshift returns a new [dialect.Querier] for Redshift dialect.\nfunc NewRedshift() dialect.Querier {\n\treturn &redshift{}\n}\n\ntype redshift struct{}\n\nvar _ dialect.Querier = (*redshift)(nil)\n\nfunc (r *redshift) CreateTable(tableName string) string {\n\tq := `CREATE TABLE %s (\n\t\tid integer NOT NULL identity(1, 1),\n\t\tversion_id bigint NOT NULL,\n\t\tis_applied boolean NOT NULL,\n\t\ttstamp timestamp NULL default sysdate,\n\t\tPRIMARY KEY(id)\n\t)`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (r *redshift) InsertVersion(tableName string) string {\n\tq := `INSERT INTO %s (version_id, is_applied) VALUES ($1, $2)`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (r *redshift) DeleteVersion(tableName string) string {\n\tq := `DELETE FROM %s WHERE version_id=$1`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (r *redshift) GetMigrationByVersion(tableName string) string {\n\tq := `SELECT tstamp, is_applied FROM %s WHERE version_id=$1 ORDER BY tstamp DESC LIMIT 1`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (r *redshift) ListMigrations(tableName string) string {\n\tq := `SELECT version_id, is_applied from %s ORDER BY id DESC`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (r *redshift) GetLatestVersion(tableName string) string {\n\tq := `SELECT max(version_id) FROM %s`\n\treturn fmt.Sprintf(q, tableName)\n}\n"
  },
  {
    "path": "internal/dialects/spanner.go",
    "content": "package dialects\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/pressly/goose/v3/database/dialect\"\n)\n\n// NewSpanner returns a [dialect.Querier] for Spanner dialect.\nfunc NewSpanner() dialect.Querier {\n\treturn &spanner{}\n}\n\ntype spanner struct{}\n\nvar _ dialect.Querier = (*spanner)(nil)\n\nfunc (s *spanner) CreateTable(tableName string) string {\n\tq := `CREATE TABLE %s (\n\t\tversion_id INT64 NOT NULL,\n\t\tis_applied BOOL NOT NULL,\n\t\ttstamp TIMESTAMP DEFAULT (CURRENT_TIMESTAMP()),\n\t) PRIMARY KEY(version_id)`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (s *spanner) InsertVersion(tableName string) string {\n\tq := `INSERT INTO %s (version_id, is_applied) VALUES (?, ?)`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (s *spanner) DeleteVersion(tableName string) string {\n\tq := `DELETE FROM %s WHERE version_id=?`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (s *spanner) GetMigrationByVersion(tableName string) string {\n\tq := `SELECT tstamp, is_applied FROM %s WHERE version_id=? ORDER BY tstamp DESC LIMIT 1`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (s *spanner) ListMigrations(tableName string) string {\n\tq := `SELECT version_id, is_applied from %s ORDER BY version_id DESC`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (s *spanner) GetLatestVersion(tableName string) string {\n\tq := `SELECT MAX(version_id) FROM %s`\n\treturn fmt.Sprintf(q, tableName)\n}\n"
  },
  {
    "path": "internal/dialects/sqlite3.go",
    "content": "package dialects\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/pressly/goose/v3/database/dialect\"\n)\n\n// NewSqlite3 returns a [dialect.Querier] for SQLite3 dialect.\nfunc NewSqlite3() dialect.Querier {\n\treturn &sqlite3{}\n}\n\ntype sqlite3 struct{}\n\nvar _ dialect.Querier = (*sqlite3)(nil)\n\nfunc (s *sqlite3) CreateTable(tableName string) string {\n\tq := `CREATE TABLE %s (\n\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\tversion_id INTEGER NOT NULL,\n\t\tis_applied INTEGER NOT NULL,\n\t\ttstamp TIMESTAMP DEFAULT (datetime('now'))\n\t)`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (s *sqlite3) InsertVersion(tableName string) string {\n\tq := `INSERT INTO %s (version_id, is_applied) VALUES (?, ?)`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (s *sqlite3) DeleteVersion(tableName string) string {\n\tq := `DELETE FROM %s WHERE version_id=?`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (s *sqlite3) GetMigrationByVersion(tableName string) string {\n\tq := `SELECT tstamp, is_applied FROM %s WHERE version_id=? ORDER BY tstamp DESC LIMIT 1`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (s *sqlite3) ListMigrations(tableName string) string {\n\tq := `SELECT version_id, is_applied from %s ORDER BY id DESC`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (s *sqlite3) GetLatestVersion(tableName string) string {\n\tq := `SELECT MAX(version_id) FROM %s`\n\treturn fmt.Sprintf(q, tableName)\n}\n"
  },
  {
    "path": "internal/dialects/sqlserver.go",
    "content": "package dialects\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/pressly/goose/v3/database/dialect\"\n)\n\n// NewSqlserver returns a [dialect.Querier] for SQL Server dialect.\nfunc NewSqlserver() dialect.Querier {\n\treturn &sqlserver{}\n}\n\ntype sqlserver struct{}\n\nvar _ dialect.Querier = (*sqlserver)(nil)\n\nfunc (s *sqlserver) CreateTable(tableName string) string {\n\tq := `CREATE TABLE %s (\n\t\tid INT NOT NULL IDENTITY(1,1) PRIMARY KEY,\n\t\tversion_id BIGINT NOT NULL,\n\t\tis_applied BIT NOT NULL,\n\t\ttstamp DATETIME NULL DEFAULT CURRENT_TIMESTAMP\n\t)`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (s *sqlserver) InsertVersion(tableName string) string {\n\tq := `INSERT INTO %s (version_id, is_applied) VALUES (@p1, @p2)`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (s *sqlserver) DeleteVersion(tableName string) string {\n\tq := `DELETE FROM %s WHERE version_id=@p1`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (s *sqlserver) GetMigrationByVersion(tableName string) string {\n\tq := `SELECT TOP 1 tstamp, is_applied FROM %s WHERE version_id=@p1 ORDER BY tstamp DESC`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (s *sqlserver) ListMigrations(tableName string) string {\n\tq := `SELECT version_id, is_applied FROM %s ORDER BY id DESC`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (s *sqlserver) GetLatestVersion(tableName string) string {\n\tq := `SELECT MAX(version_id) FROM %s`\n\treturn fmt.Sprintf(q, tableName)\n}\n"
  },
  {
    "path": "internal/dialects/starrocks.go",
    "content": "package dialects\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/pressly/goose/v3/database/dialect\"\n)\n\n// NewStarrocks returns a [dialect.Querier] for StarRocks dialect.\nfunc NewStarrocks() dialect.Querier {\n\treturn &starrocks{}\n}\n\ntype starrocks struct{}\n\nvar _ dialect.Querier = (*starrocks)(nil)\n\nfunc (m *starrocks) CreateTable(tableName string) string {\n\tq := `CREATE TABLE IF NOT EXISTS %s (\n\t\tid bigint NOT NULL AUTO_INCREMENT,\n\t\tversion_id bigint NOT NULL,\n\t\tis_applied boolean NOT NULL,\n\t\ttstamp datetime NULL default CURRENT_TIMESTAMP\n\t)\n\tPRIMARY KEY (id)\n\tDISTRIBUTED BY HASH (id)\n\tORDER BY (id,version_id)`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (m *starrocks) InsertVersion(tableName string) string {\n\tq := `INSERT INTO %s (version_id, is_applied) VALUES (?, ?)`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (m *starrocks) DeleteVersion(tableName string) string {\n\tq := `DELETE FROM %s WHERE version_id=?`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (m *starrocks) GetMigrationByVersion(tableName string) string {\n\tq := `SELECT tstamp, is_applied FROM %s WHERE version_id=? ORDER BY tstamp DESC LIMIT 1`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (m *starrocks) ListMigrations(tableName string) string {\n\tq := `SELECT version_id, is_applied from %s ORDER BY id DESC`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (m *starrocks) GetLatestVersion(tableName string) string {\n\tq := `SELECT MAX(version_id) FROM %s`\n\treturn fmt.Sprintf(q, tableName)\n}\n"
  },
  {
    "path": "internal/dialects/tidb.go",
    "content": "package dialects\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/pressly/goose/v3/database/dialect\"\n)\n\n// NewTidb returns a [dialect.Querier] for TiDB dialect.\nfunc NewTidb() dialect.Querier {\n\treturn &Tidb{}\n}\n\ntype Tidb struct{}\n\nvar _ dialect.Querier = (*Tidb)(nil)\n\nfunc (t *Tidb) CreateTable(tableName string) string {\n\tq := `CREATE TABLE %s (\n\t\tid BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE,\n\t\tversion_id bigint NOT NULL,\n\t\tis_applied boolean NOT NULL,\n\t\ttstamp timestamp NULL default now(),\n\t\tPRIMARY KEY(id)\n\t)`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (t *Tidb) InsertVersion(tableName string) string {\n\tq := `INSERT INTO %s (version_id, is_applied) VALUES (?, ?)`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (t *Tidb) DeleteVersion(tableName string) string {\n\tq := `DELETE FROM %s WHERE version_id=?`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (t *Tidb) GetMigrationByVersion(tableName string) string {\n\tq := `SELECT tstamp, is_applied FROM %s WHERE version_id=? ORDER BY tstamp DESC LIMIT 1`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (t *Tidb) ListMigrations(tableName string) string {\n\tq := `SELECT version_id, is_applied from %s ORDER BY id DESC`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (t *Tidb) GetLatestVersion(tableName string) string {\n\tq := `SELECT MAX(version_id) FROM %s`\n\treturn fmt.Sprintf(q, tableName)\n}\n"
  },
  {
    "path": "internal/dialects/turso.go",
    "content": "package dialects\n\nimport \"github.com/pressly/goose/v3/database/dialect\"\n\n// NewTurso returns a [dialect.Querier] for Turso dialect.\nfunc NewTurso() dialect.Querier {\n\treturn &turso{}\n}\n\ntype turso struct {\n\tsqlite3\n}\n\nvar _ dialect.Querier = (*turso)(nil)\n"
  },
  {
    "path": "internal/dialects/vertica.go",
    "content": "package dialects\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/pressly/goose/v3/database/dialect\"\n)\n\n// NewVertica returns a new [dialect.Querier] for Vertica dialect.\n//\n// DEPRECATED: Vertica support is deprecated and will be removed in a future release.\nfunc NewVertica() dialect.Querier {\n\treturn &vertica{}\n}\n\ntype vertica struct{}\n\nvar _ dialect.Querier = (*vertica)(nil)\n\nfunc (v *vertica) CreateTable(tableName string) string {\n\tq := `CREATE TABLE %s (\n\t\tid identity(1,1) NOT NULL,\n\t\tversion_id bigint NOT NULL,\n\t\tis_applied boolean NOT NULL,\n\t\ttstamp timestamp NULL default now(),\n\t\tPRIMARY KEY(id)\n\t)`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (v *vertica) InsertVersion(tableName string) string {\n\tq := `INSERT INTO %s (version_id, is_applied) VALUES (?, ?)`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (v *vertica) DeleteVersion(tableName string) string {\n\tq := `DELETE FROM %s WHERE version_id=?`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (v *vertica) GetMigrationByVersion(tableName string) string {\n\tq := `SELECT tstamp, is_applied FROM %s WHERE version_id=? ORDER BY tstamp DESC LIMIT 1`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (v *vertica) ListMigrations(tableName string) string {\n\tq := `SELECT version_id, is_applied from %s ORDER BY id DESC`\n\treturn fmt.Sprintf(q, tableName)\n}\n\nfunc (v *vertica) GetLatestVersion(tableName string) string {\n\tq := `SELECT MAX(version_id) FROM %s`\n\treturn fmt.Sprintf(q, tableName)\n}\n"
  },
  {
    "path": "internal/dialects/ydb.go",
    "content": "package dialects\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/pressly/goose/v3/database/dialect\"\n)\n\n// NewYDB returns a new [dialect.Querier] for Vertica dialect.\nfunc NewYDB() dialect.Querier {\n\treturn &ydb{}\n}\n\ntype ydb struct{}\n\nvar _ dialect.Querier = (*ydb)(nil)\n\nfunc formatYDBTableName(tableName string) string {\n\treturn fmt.Sprintf(\"`%s`\", tableName)\n}\n\nfunc (c *ydb) CreateTable(tableName string) string {\n\tformatedYDBTableName := formatYDBTableName(tableName)\n\tq := `CREATE TABLE %s (\n\t\tversion_id Uint64,\n\t\tis_applied Bool,\n\t\ttstamp Timestamp,\n\n\t\tPRIMARY KEY(version_id)\n\t)`\n\treturn fmt.Sprintf(q, formatedYDBTableName)\n}\n\nfunc (c *ydb) InsertVersion(tableName string) string {\n\tformatedYDBTableName := formatYDBTableName(tableName)\n\tq := `INSERT INTO %s (\n\t\tversion_id, \n\t\tis_applied, \n\t\ttstamp\n\t) VALUES (\n\t\tCAST($1 AS Uint64), \n\t\t$2, \n\t\tCurrentUtcTimestamp()\n\t)`\n\treturn fmt.Sprintf(q, formatedYDBTableName)\n}\n\nfunc (c *ydb) DeleteVersion(tableName string) string {\n\tformatedYDBTableName := formatYDBTableName(tableName)\n\tq := `DELETE FROM %s WHERE version_id = $1`\n\treturn fmt.Sprintf(q, formatedYDBTableName)\n}\n\nfunc (c *ydb) GetMigrationByVersion(tableName string) string {\n\tformatedYDBTableName := formatYDBTableName(tableName)\n\tq := `SELECT tstamp, is_applied FROM %s WHERE version_id = $1 ORDER BY tstamp DESC LIMIT 1`\n\treturn fmt.Sprintf(q, formatedYDBTableName)\n}\n\nfunc (c *ydb) ListMigrations(tableName string) string {\n\tformatedYDBTableName := formatYDBTableName(tableName)\n\tq := `\n\tSELECT version_id, is_applied, tstamp AS __discard_column_tstamp \n\tFROM %s ORDER BY __discard_column_tstamp DESC`\n\treturn fmt.Sprintf(q, formatedYDBTableName)\n}\n\nfunc (c *ydb) GetLatestVersion(tableName string) string {\n\tformatedYDBTableName := formatYDBTableName(tableName)\n\tq := `SELECT MAX(version_id) FROM %s`\n\treturn fmt.Sprintf(q, formatedYDBTableName)\n}\n"
  },
  {
    "path": "internal/gooseutil/resolve.go",
    "content": "// Package gooseutil provides utility functions we want to keep internal to the package. It's\n// intended to be a collection of well-tested helper functions.\npackage gooseutil\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n)\n\n// UpVersions returns a list of migrations to apply based on the versions in the filesystem and the\n// versions in the database. The target version can be used to specify a target version. In most\n// cases this will be math.MaxInt64.\n//\n// The allowMissing flag can be used to allow missing migrations as part of the list of migrations\n// to apply. Otherwise, an error will be returned if there are missing migrations in the database.\nfunc UpVersions(\n\tfsysVersions []int64,\n\tdbVersions []int64,\n\ttarget int64,\n\tallowMissing bool,\n) ([]int64, error) {\n\t// Sort the list of versions in the filesystem. This should already be sorted, but we do this\n\t// just in case.\n\tslices.Sort(fsysVersions)\n\n\t// dbAppliedVersions is a map of all applied migrations in the database.\n\tdbAppliedVersions := make(map[int64]bool, len(dbVersions))\n\tvar dbMaxVersion int64\n\tfor _, v := range dbVersions {\n\t\tdbAppliedVersions[v] = true\n\t\tif v > dbMaxVersion {\n\t\t\tdbMaxVersion = v\n\t\t}\n\t}\n\n\t// Get a list of migrations that are missing from the database. A missing migration is one that\n\t// has a version less than the max version in the database and has not been applied.\n\t//\n\t// In most cases the target version is math.MaxInt64, but it can be used to specify a target\n\t// version. In which case we respect the target version and only surface migrations up to and\n\t// including that target.\n\tvar missing []int64\n\tfor _, v := range fsysVersions {\n\t\tif dbAppliedVersions[v] {\n\t\t\tcontinue\n\t\t}\n\t\tif v < dbMaxVersion && v <= target {\n\t\t\tmissing = append(missing, v)\n\t\t}\n\t}\n\n\t// feat(mf): It is very possible someone may want to apply ONLY new migrations and skip missing\n\t// migrations entirely. At the moment this is not supported, but leaving this comment because\n\t// that's where that logic would be handled.\n\t//\n\t// For example, if database has 1,4 already applied and 2,3,5 are new, we would apply only 5 and\n\t// skip 2,3. Not sure if this is a common use case, but it's possible someone may want to do\n\t// this.\n\tif len(missing) > 0 && !allowMissing {\n\t\treturn nil, newMissingError(missing, dbMaxVersion, target)\n\t}\n\n\tvar out []int64\n\n\t// 1. Add missing migrations to the list of migrations to apply, if any.\n\tout = append(out, missing...)\n\n\t// 2. Add new migrations to the list of migrations to apply, if any.\n\tfor _, v := range fsysVersions {\n\t\tif dbAppliedVersions[v] {\n\t\t\tcontinue\n\t\t}\n\t\tif v > dbMaxVersion && v <= target {\n\t\t\tout = append(out, v)\n\t\t}\n\t}\n\t// 3. Sort the list of migrations to apply.\n\tslices.Sort(out)\n\n\treturn out, nil\n}\n\nfunc newMissingError(\n\tmissing []int64,\n\tdbMaxVersion int64,\n\ttarget int64,\n) error {\n\tslices.Sort(missing)\n\n\tcollected := make([]string, 0, len(missing))\n\tfor _, v := range missing {\n\t\tcollected = append(collected, strconv.FormatInt(v, 10))\n\t}\n\n\tmsg := \"migration\"\n\tif len(collected) > 1 {\n\t\tmsg += \"s\"\n\t}\n\n\tvar versionsMsg string\n\tif len(collected) > 1 {\n\t\tversionsMsg = \"versions \" + strings.Join(collected, \",\")\n\t} else {\n\t\tversionsMsg = \"version \" + collected[0]\n\t}\n\n\tdesiredMsg := fmt.Sprintf(\"database version (%d)\", dbMaxVersion)\n\tif target != math.MaxInt64 {\n\t\tdesiredMsg += fmt.Sprintf(\", with target version (%d)\", target)\n\t}\n\n\treturn fmt.Errorf(\"detected %d missing (out-of-order) %s lower than %s: %s\",\n\t\tlen(missing), msg, desiredMsg, versionsMsg,\n\t)\n}\n"
  },
  {
    "path": "internal/gooseutil/resolve_test.go",
    "content": "package gooseutil\n\nimport (\n\t\"math\"\n\t\"slices\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestResolveVersions(t *testing.T) {\n\tt.Parallel()\n\tt.Run(\"not_allow_missing\", func(t *testing.T) {\n\t\t// Nothing to apply nil\n\t\tgot, err := UpVersions(nil, nil, math.MaxInt64, false)\n\t\trequire.NoError(t, err)\n\t\trequire.Empty(t, got)\n\t\t// Nothing to apply empty\n\t\tgot, err = UpVersions([]int64{}, []int64{}, math.MaxInt64, false)\n\t\trequire.NoError(t, err)\n\t\trequire.Empty(t, got)\n\n\t\t// Nothing new\n\t\tgot, err = UpVersions([]int64{1, 2, 3}, []int64{1, 2, 3}, math.MaxInt64, false)\n\t\trequire.NoError(t, err)\n\t\trequire.Empty(t, got)\n\n\t\t// All new\n\t\tgot, err = UpVersions([]int64{1, 2, 3}, []int64{}, math.MaxInt64, false)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, got, 3)\n\t\trequire.Equal(t, int64(1), got[0])\n\t\trequire.Equal(t, int64(2), got[1])\n\t\trequire.Equal(t, int64(3), got[2])\n\n\t\t// Squashed, no new\n\t\tgot, err = UpVersions([]int64{3}, []int64{3}, math.MaxInt64, false)\n\t\trequire.NoError(t, err)\n\t\trequire.Empty(t, got)\n\t\t// Squashed, 1 new\n\t\tgot, err = UpVersions([]int64{3, 4}, []int64{3}, math.MaxInt64, false)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, got, 1)\n\t\trequire.Equal(t, int64(4), got[0])\n\n\t\t// Some new with target\n\t\tgot, err = UpVersions([]int64{1, 2, 3, 4, 5}, []int64{1, 2}, 4, false)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, got, 2)\n\t\trequire.Equal(t, int64(3), got[0])\n\t\trequire.Equal(t, int64(4), got[1]) // up to and including target\n\t\t// Some new with zero target\n\t\tgot, err = UpVersions([]int64{1, 2, 3, 4, 5}, []int64{1, 2}, 0, false)\n\t\trequire.NoError(t, err)\n\t\trequire.Empty(t, got)\n\n\t\t// Error: one missing migrations with max target\n\t\t_, err = UpVersions([]int64{1, 2, 3, 4}, []int64{1 /* 2*/, 3}, math.MaxInt64, false)\n\t\trequire.Error(t, err)\n\t\trequire.Equal(t,\n\t\t\t\"detected 1 missing (out-of-order) migration lower than database version (3): version 2\",\n\t\t\terr.Error(),\n\t\t)\n\n\t\t// Error: multiple missing migrations with max target\n\t\t_, err = UpVersions([]int64{1, 2, 3, 4, 5}, []int64{ /* 1 */ 2 /* 3 */, 4, 5}, math.MaxInt64, false)\n\t\trequire.Error(t, err)\n\t\trequire.Equal(t,\n\t\t\t\"detected 2 missing (out-of-order) migrations lower than database version (5): versions 1,3\",\n\t\t\terr.Error(),\n\t\t)\n\n\t\tt.Run(\"target_lower_than_max\", func(t *testing.T) {\n\n\t\t\t// These tests are a bit of an edge case but an important one worth documenting. There\n\t\t\t// can be missing migrations above and/or below the target version which itself can be\n\t\t\t// lower than the max db version. For example, migrations 1,2,3,4 in the filesystem, and\n\t\t\t// migrations 1,2,4 applied to the database and the user requested target 2. Technically\n\t\t\t// there are no missing migrations based on the target version since 1,2 have been\n\t\t\t// applied, but there is 1 missing migration (3) based on the max db version. Should\n\t\t\t// this return an error, or report no pending migrations?\n\t\t\t//\n\t\t\t// We've taken the stance that this SHOULD respect the target version and surface an\n\t\t\t// error if there are missing migrations below the target version. This is because the\n\t\t\t// user has explicitly requested a target version and we should respect that.\n\n\t\t\tgot, err = UpVersions([]int64{1, 2, 3}, []int64{1 /* 2 */, 3}, 1, false)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Empty(t, got)\n\t\t\tgot, err = UpVersions([]int64{1, 2, 3}, []int64{1 /* 2 */, 3}, 2, false)\n\t\t\trequire.Error(t, err)\n\t\t\trequire.Equal(t,\n\t\t\t\t\"detected 1 missing (out-of-order) migration lower than database version (3), with target version (2): version 2\",\n\t\t\t\terr.Error(),\n\t\t\t)\n\t\t\tgot, err = UpVersions([]int64{1, 2, 3}, []int64{1 /* 2 */, 3}, 3, false)\n\t\t\trequire.Error(t, err)\n\t\t\trequire.Equal(t,\n\t\t\t\t\"detected 1 missing (out-of-order) migration lower than database version (3), with target version (3): version 2\",\n\t\t\t\terr.Error(),\n\t\t\t)\n\n\t\t\t_, err = UpVersions([]int64{1, 2, 3, 4, 5, 6}, []int64{1 /* 2 */, 3, 4 /* 5*/, 6}, 4, false)\n\t\t\trequire.Error(t, err)\n\t\t\trequire.Equal(t,\n\t\t\t\t\"detected 1 missing (out-of-order) migration lower than database version (6), with target version (4): version 2\",\n\t\t\t\terr.Error(),\n\t\t\t)\n\t\t\t_, err = UpVersions([]int64{1, 2, 3, 4, 5, 6}, []int64{1 /* 2 */, 3, 4 /* 5*/, 6}, 6, false)\n\t\t\trequire.Error(t, err)\n\t\t\trequire.Equal(t,\n\t\t\t\t\"detected 2 missing (out-of-order) migrations lower than database version (6), with target version (6): versions 2,5\",\n\t\t\t\terr.Error(),\n\t\t\t)\n\t\t})\n\t})\n\n\tt.Run(\"allow_missing\", func(t *testing.T) {\n\t\t// Nothing to apply nil\n\t\tgot, err := UpVersions(nil, nil, math.MaxInt64, true)\n\t\trequire.NoError(t, err)\n\t\trequire.Empty(t, got)\n\t\t// Nothing to apply empty\n\t\tgot, err = UpVersions([]int64{}, []int64{}, math.MaxInt64, true)\n\t\trequire.NoError(t, err)\n\t\trequire.Empty(t, got)\n\n\t\t// Nothing new\n\t\tgot, err = UpVersions([]int64{1, 2, 3}, []int64{1, 2, 3}, math.MaxInt64, true)\n\t\trequire.NoError(t, err)\n\t\trequire.Empty(t, got)\n\n\t\t// All new\n\t\tgot, err = UpVersions([]int64{1, 2, 3}, []int64{}, math.MaxInt64, true)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, got, 3)\n\t\trequire.Equal(t, int64(1), got[0])\n\t\trequire.Equal(t, int64(2), got[1])\n\t\trequire.Equal(t, int64(3), got[2])\n\n\t\t// Squashed, no new\n\t\tgot, err = UpVersions([]int64{3}, []int64{3}, math.MaxInt64, true)\n\t\trequire.NoError(t, err)\n\t\trequire.Empty(t, got)\n\t\t// Squashed, 1 new\n\t\tgot, err = UpVersions([]int64{3, 4}, []int64{3}, math.MaxInt64, true)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, got, 1)\n\t\trequire.Equal(t, int64(4), got[0])\n\n\t\t// Some new with target\n\t\tgot, err = UpVersions([]int64{1, 2, 3, 4, 5}, []int64{1, 2}, 4, true)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, got, 2)\n\t\trequire.Equal(t, int64(3), got[0])\n\t\trequire.Equal(t, int64(4), got[1]) // up to and including target\n\t\t// Some new with zero target\n\t\tgot, err = UpVersions([]int64{1, 2, 3, 4, 5}, []int64{1, 2}, 0, true)\n\t\trequire.NoError(t, err)\n\t\trequire.Empty(t, got)\n\n\t\t// No error: one missing\n\t\tgot, err = UpVersions([]int64{1, 2, 3}, []int64{1 /* 2*/, 3}, math.MaxInt64, true)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, got, 1)\n\t\trequire.Equal(t, int64(2), got[0]) // missing\n\n\t\t// No error: multiple missing and new with max target\n\t\tgot, err = UpVersions([]int64{1, 2, 3, 4, 5}, []int64{ /* 1 */ 2 /* 3 */, 4}, math.MaxInt64, true)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, got, 3)\n\t\trequire.Equal(t, int64(1), got[0]) // missing\n\t\trequire.Equal(t, int64(3), got[1]) // missing\n\t\trequire.Equal(t, int64(5), got[2])\n\n\t\tt.Run(\"target_lower_than_max\", func(t *testing.T) {\n\t\t\tgot, err = UpVersions([]int64{1, 2, 3}, []int64{1 /* 2 */, 3}, 1, true)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Empty(t, got)\n\t\t\tgot, err = UpVersions([]int64{1, 2, 3}, []int64{1 /* 2 */, 3}, 2, true)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, got, 1)\n\t\t\trequire.Equal(t, int64(2), got[0]) // missing\n\t\t\tgot, err = UpVersions([]int64{1, 2, 3}, []int64{1 /* 2 */, 3}, 3, true)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, got, 1)\n\t\t\trequire.Equal(t, int64(2), got[0]) // missing\n\n\t\t\tgot, err = UpVersions([]int64{1, 2, 3, 4, 5, 6}, []int64{1 /* 2 */, 3, 4 /* 5*/, 6}, 4, true)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, got, 1)\n\t\t\trequire.Equal(t, int64(2), got[0]) // missing\n\t\t\tgot, err = UpVersions([]int64{1, 2, 3, 4, 5, 6}, []int64{1 /* 2 */, 3, 4 /* 5*/, 6}, 6, true)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, got, 2)\n\t\t\trequire.Equal(t, int64(2), got[0]) // missing\n\t\t\trequire.Equal(t, int64(5), got[1]) // missing\n\t\t})\n\t})\n\n\tt.Run(\"sort_ascending\", func(t *testing.T) {\n\t\tgot := []int64{5, 3, 4, 2, 1}\n\t\tslices.Sort(got)\n\t\trequire.Equal(t, []int64{1, 2, 3, 4, 5}, got)\n\t})\n}\n"
  },
  {
    "path": "internal/legacystore/legacystore.go",
    "content": "package legacystore\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/pressly/goose/v3/database\"\n\t\"github.com/pressly/goose/v3/database/dialect\"\n\t\"github.com/pressly/goose/v3/internal/dialects\"\n)\n\n// Store is the interface that wraps the basic methods for a database dialect.\n//\n// A dialect is a set of SQL statements that are specific to a database.\n//\n// By defining a store interface, we can support multiple databases\n// with a single codebase.\n//\n// The underlying implementation does not modify the error. It is the callers\n// responsibility to assert for the correct error, such as sql.ErrNoRows.\ntype Store interface {\n\t// CreateVersionTable creates the version table within a transaction.\n\t// This table is used to store goose migrations.\n\tCreateVersionTable(ctx context.Context, tx *sql.Tx, tableName string) error\n\n\t// InsertVersion inserts a version id into the version table within a transaction.\n\tInsertVersion(ctx context.Context, tx *sql.Tx, tableName string, version int64) error\n\t// InsertVersionNoTx inserts a version id into the version table without a transaction.\n\tInsertVersionNoTx(ctx context.Context, db *sql.DB, tableName string, version int64) error\n\n\t// DeleteVersion deletes a version id from the version table within a transaction.\n\tDeleteVersion(ctx context.Context, tx *sql.Tx, tableName string, version int64) error\n\t// DeleteVersionNoTx deletes a version id from the version table without a transaction.\n\tDeleteVersionNoTx(ctx context.Context, db *sql.DB, tableName string, version int64) error\n\n\t// GetMigrationRow retrieves a single migration by version id.\n\t//\n\t// Returns the raw sql error if the query fails. It is the callers responsibility\n\t// to assert for the correct error, such as sql.ErrNoRows.\n\tGetMigration(ctx context.Context, db *sql.DB, tableName string, version int64) (*GetMigrationResult, error)\n\n\t// ListMigrations retrieves all migrations sorted in descending order by id.\n\t//\n\t// If there are no migrations, an empty slice is returned with no error.\n\tListMigrations(ctx context.Context, db *sql.DB, tableName string) ([]*ListMigrationsResult, error)\n}\n\n// NewStore returns a new Store for the given dialect.\nfunc NewStore(d database.Dialect) (Store, error) {\n\tvar querier dialect.Querier\n\tswitch d {\n\tcase database.DialectPostgres:\n\t\tquerier = dialects.NewPostgres()\n\tcase database.DialectMySQL:\n\t\tquerier = dialects.NewMysql()\n\tcase database.DialectSQLite3:\n\t\tquerier = dialects.NewSqlite3()\n\tcase database.DialectSpanner:\n\t\tquerier = dialects.NewSpanner()\n\tcase database.DialectMSSQL:\n\t\tquerier = dialects.NewSqlserver()\n\tcase database.DialectRedshift:\n\t\tquerier = dialects.NewRedshift()\n\tcase database.DialectTiDB:\n\t\tquerier = dialects.NewTidb()\n\tcase database.DialectClickHouse:\n\t\tquerier = dialects.NewClickhouse()\n\tcase database.DialectVertica:\n\t\tquerier = dialects.NewVertica()\n\tcase database.DialectYdB:\n\t\tquerier = dialects.NewYDB()\n\tcase database.DialectTurso:\n\t\tquerier = dialects.NewTurso()\n\tcase database.DialectStarrocks:\n\t\tquerier = dialects.NewStarrocks()\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unknown querier dialect: %v\", d)\n\t}\n\treturn &store{querier: querier}, nil\n}\n\ntype GetMigrationResult struct {\n\tIsApplied bool\n\tTimestamp time.Time\n}\n\ntype ListMigrationsResult struct {\n\tVersionID int64\n\tIsApplied bool\n}\n\ntype store struct {\n\tquerier dialect.Querier\n}\n\nvar _ Store = (*store)(nil)\n\nfunc (s *store) CreateVersionTable(ctx context.Context, tx *sql.Tx, tableName string) error {\n\tq := s.querier.CreateTable(tableName)\n\t_, err := tx.ExecContext(ctx, q)\n\treturn err\n}\n\nfunc (s *store) InsertVersion(ctx context.Context, tx *sql.Tx, tableName string, version int64) error {\n\tq := s.querier.InsertVersion(tableName)\n\t_, err := tx.ExecContext(ctx, q, version, true)\n\treturn err\n}\n\nfunc (s *store) InsertVersionNoTx(ctx context.Context, db *sql.DB, tableName string, version int64) error {\n\tq := s.querier.InsertVersion(tableName)\n\t_, err := db.ExecContext(ctx, q, version, true)\n\treturn err\n}\n\nfunc (s *store) DeleteVersion(ctx context.Context, tx *sql.Tx, tableName string, version int64) error {\n\tq := s.querier.DeleteVersion(tableName)\n\t_, err := tx.ExecContext(ctx, q, version)\n\treturn err\n}\n\nfunc (s *store) DeleteVersionNoTx(ctx context.Context, db *sql.DB, tableName string, version int64) error {\n\tq := s.querier.DeleteVersion(tableName)\n\t_, err := db.ExecContext(ctx, q, version)\n\treturn err\n}\n\nfunc (s *store) GetMigration(\n\tctx context.Context,\n\tdb *sql.DB,\n\ttableName string,\n\tversion int64,\n) (*GetMigrationResult, error) {\n\tq := s.querier.GetMigrationByVersion(tableName)\n\tvar timestamp time.Time\n\tvar isApplied bool\n\terr := db.QueryRowContext(ctx, q, version).Scan(&timestamp, &isApplied)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &GetMigrationResult{\n\t\tIsApplied: isApplied,\n\t\tTimestamp: timestamp,\n\t}, nil\n}\n\nfunc (s *store) ListMigrations(ctx context.Context, db *sql.DB, tableName string) ([]*ListMigrationsResult, error) {\n\tq := s.querier.ListMigrations(tableName)\n\trows, err := db.QueryContext(ctx, q)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tvar migrations []*ListMigrationsResult\n\tfor rows.Next() {\n\t\tvar version int64\n\t\tvar isApplied bool\n\t\tif err := rows.Scan(&version, &isApplied); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tmigrations = append(migrations, &ListMigrationsResult{\n\t\t\tVersionID: version,\n\t\t\tIsApplied: isApplied,\n\t\t})\n\t}\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn migrations, nil\n}\n"
  },
  {
    "path": "internal/migrationstats/migration_go.go",
    "content": "package migrationstats\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/parser\"\n\t\"go/token\"\n\t\"io\"\n\t\"strings\"\n)\n\nconst (\n\tregisterGoFuncName            = \"AddMigration\"\n\tregisterGoFuncNameNoTx        = \"AddMigrationNoTx\"\n\tregisterGoFuncNameContext     = \"AddMigrationContext\"\n\tregisterGoFuncNameNoTxContext = \"AddMigrationNoTxContext\"\n)\n\ntype goMigration struct {\n\tname                     string\n\tuseTx                    *bool\n\tupFuncName, downFuncName string\n}\n\nfunc parseGoFile(r io.Reader) (*goMigration, error) {\n\tastFile, err := parser.ParseFile(\n\t\ttoken.NewFileSet(),\n\t\t\"\", // filename\n\t\tr,\n\t\t// We don't need to resolve imports, so we can skip it.\n\t\t// This speeds up the parsing process.\n\t\t// See https://github.com/golang/go/issues/46485\n\t\tparser.SkipObjectResolution,\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, decl := range astFile.Decls {\n\t\tfn, ok := decl.(*ast.FuncDecl)\n\t\tif !ok || fn == nil || fn.Name == nil {\n\t\t\tcontinue\n\t\t}\n\t\tif fn.Name.Name == \"init\" {\n\t\t\treturn parseInitFunc(fn)\n\t\t}\n\t}\n\treturn nil, errors.New(\"no init function\")\n}\n\nfunc parseInitFunc(fd *ast.FuncDecl) (*goMigration, error) {\n\tif fd == nil {\n\t\treturn nil, fmt.Errorf(\"function declaration must not be nil\")\n\t}\n\tif fd.Body == nil {\n\t\treturn nil, fmt.Errorf(\"no function body\")\n\t}\n\tif len(fd.Body.List) == 0 {\n\t\treturn nil, fmt.Errorf(\"no registered goose functions\")\n\t}\n\tgf := new(goMigration)\n\tfor _, statement := range fd.Body.List {\n\t\texpr, ok := statement.(*ast.ExprStmt)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tcall, ok := expr.X.(*ast.CallExpr)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tsel, ok := call.Fun.(*ast.SelectorExpr)\n\t\tif !ok || sel == nil {\n\t\t\tcontinue\n\t\t}\n\t\tfuncName := sel.Sel.Name\n\t\tb := false\n\t\tswitch funcName {\n\t\tcase registerGoFuncName, registerGoFuncNameContext:\n\t\t\tb = true\n\t\t\tgf.useTx = &b\n\t\tcase registerGoFuncNameNoTx, registerGoFuncNameNoTxContext:\n\t\t\tgf.useTx = &b\n\t\tdefault:\n\t\t\tcontinue\n\t\t}\n\t\tif gf.name != \"\" {\n\t\t\treturn nil, fmt.Errorf(\"found duplicate registered functions:\\nprevious: %v\\ncurrent: %v\", gf.name, funcName)\n\t\t}\n\t\tgf.name = funcName\n\n\t\tif len(call.Args) != 2 {\n\t\t\treturn nil, fmt.Errorf(\"registered goose functions have 2 arguments: got %d\", len(call.Args))\n\t\t}\n\t\tgetNameFromExpr := func(expr ast.Expr) (string, error) {\n\t\t\targ, ok := expr.(*ast.Ident)\n\t\t\tif !ok {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to assert argument identifier: got %T\", arg)\n\t\t\t}\n\t\t\treturn arg.Name, nil\n\t\t}\n\t\tvar err error\n\t\tgf.upFuncName, err = getNameFromExpr(call.Args[0])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tgf.downFuncName, err = getNameFromExpr(call.Args[1])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\t// validation\n\tswitch gf.name {\n\tcase registerGoFuncName, registerGoFuncNameNoTx, registerGoFuncNameContext, registerGoFuncNameNoTxContext:\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"goose register function must be one of: %s\",\n\t\t\tstrings.Join([]string{\n\t\t\t\tregisterGoFuncName,\n\t\t\t\tregisterGoFuncNameNoTx,\n\t\t\t\tregisterGoFuncNameContext,\n\t\t\t\tregisterGoFuncNameNoTxContext,\n\t\t\t}, \", \"),\n\t\t)\n\t}\n\tif gf.useTx == nil {\n\t\treturn nil, errors.New(\"validation error: failed to identify transaction: got nil bool\")\n\t}\n\t// The up and down functions can either be named Go functions or \"nil\", an\n\t// empty string means there is a flaw in our parsing logic of the Go source code.\n\tif gf.upFuncName == \"\" {\n\t\treturn nil, fmt.Errorf(\"validation error: up function is empty string\")\n\t}\n\tif gf.downFuncName == \"\" {\n\t\treturn nil, fmt.Errorf(\"validation error: down function is empty string\")\n\t}\n\treturn gf, nil\n}\n"
  },
  {
    "path": "internal/migrationstats/migration_sql.go",
    "content": "package migrationstats\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/pressly/goose/v3/internal/sqlparser\"\n)\n\ntype sqlMigration struct {\n\tuseTx              bool\n\tupCount, downCount int\n}\n\nfunc parseSQLFile(r io.Reader, debug bool) (*sqlMigration, error) {\n\tby, err := io.ReadAll(r)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tupStatements, txUp, err := sqlparser.ParseSQLMigration(\n\t\tbytes.NewReader(by),\n\t\tsqlparser.DirectionUp,\n\t\tdebug,\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdownStatements, txDown, err := sqlparser.ParseSQLMigration(\n\t\tbytes.NewReader(by),\n\t\tsqlparser.DirectionDown,\n\t\tdebug,\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// This is a sanity check to ensure that the parser is behaving as expected.\n\tif txUp != txDown {\n\t\treturn nil, fmt.Errorf(\"up and down statements must have the same transaction mode\")\n\t}\n\treturn &sqlMigration{\n\t\tuseTx:     txUp,\n\t\tupCount:   len(upStatements),\n\t\tdownCount: len(downStatements),\n\t}, nil\n}\n"
  },
  {
    "path": "internal/migrationstats/migrationstats.go",
    "content": "package migrationstats\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"path/filepath\"\n\n\t\"github.com/pressly/goose/v3\"\n)\n\n// FileWalker walks all files for GatherStats.\ntype FileWalker interface {\n\t// Walk invokes fn for each file.\n\tWalk(fn func(filename string, r io.Reader) error) error\n}\n\n// Stats contains the stats for a migration file.\ntype Stats struct {\n\t// FileName is the name of the file.\n\tFileName string\n\t// Version is the version of the migration.\n\tVersion int64\n\t// Tx is true if the .sql migration file has a +goose NO TRANSACTION annotation\n\t// or the .go migration file calls AddMigrationNoTx.\n\tTx bool\n\t// UpCount is the number of statements in the Up migration.\n\tUpCount int\n\t// DownCount is the number of statements in the Down migration.\n\tDownCount int\n}\n\n// GatherStats returns the migration file stats.\nfunc GatherStats(fw FileWalker, debug bool) ([]*Stats, error) {\n\tvar stats []*Stats\n\terr := fw.Walk(func(filename string, r io.Reader) error {\n\t\tversion, err := goose.NumericComponent(filename)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get version from file %q: %w\", filename, err)\n\t\t}\n\t\tvar up, down int\n\t\tvar tx bool\n\t\tswitch filepath.Ext(filename) {\n\t\tcase \".sql\":\n\t\t\tm, err := parseSQLFile(r, debug)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to parse file %q: %w\", filename, err)\n\t\t\t}\n\t\t\tup, down = m.upCount, m.downCount\n\t\t\ttx = m.useTx\n\t\tcase \".go\":\n\t\t\tm, err := parseGoFile(r)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to parse file %q: %w\", filename, err)\n\t\t\t}\n\t\t\tup, down = nilAsNumber(m.upFuncName), nilAsNumber(m.downFuncName)\n\t\t\ttx = *m.useTx\n\t\t}\n\t\tstats = append(stats, &Stats{\n\t\t\tFileName:  filename,\n\t\t\tVersion:   version,\n\t\t\tTx:        tx,\n\t\t\tUpCount:   up,\n\t\t\tDownCount: down,\n\t\t})\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn stats, nil\n}\n\nfunc nilAsNumber(s string) int {\n\tif s != \"nil\" {\n\t\treturn 1\n\t}\n\treturn 0\n}\n"
  },
  {
    "path": "internal/migrationstats/migrationstats_test.go",
    "content": "package migrationstats\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestParsingGoMigrations(t *testing.T) {\n\tt.Parallel()\n\ttests := []struct {\n\t\tname                     string\n\t\tinput                    string\n\t\twantUpName, wantDownName string\n\t\twantTx                   bool\n\t}{\n\t\t// AddMigration\n\t\t{\"upAndDown\", upAndDown, \"up001\", \"down001\", true},\n\t\t{\"downOnly\", downOnly, \"nil\", \"down002\", true},\n\t\t{\"upOnly\", upOnly, \"up003\", \"nil\", true},\n\t\t{\"upAndDownNil\", upAndDownNil, \"nil\", \"nil\", true},\n\t\t// AddMigrationNoTx\n\t\t{\"upAndDownNoTx\", upAndDownNoTx, \"up001\", \"down001\", false},\n\t\t{\"downOnlyNoTx\", downOnlyNoTx, \"nil\", \"down002\", false},\n\t\t{\"upOnlyNoTx\", upOnlyNoTx, \"up003\", \"nil\", false},\n\t\t{\"upAndDownNilNoTx\", upAndDownNilNoTx, \"nil\", \"nil\", false},\n\t}\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tg, err := parseGoFile(strings.NewReader(tc.input))\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.NotNil(t, g.useTx)\n\t\t\trequire.Equal(t, tc.wantTx, *g.useTx)\n\t\t\trequire.Equal(t, tc.wantDownName, g.downFuncName)\n\t\t\trequire.Equal(t, tc.wantUpName, g.upFuncName)\n\t\t})\n\t}\n}\n\nfunc TestGoMigrationStats(t *testing.T) {\n\tt.Parallel()\n\n\tbase := \"../../tests/gomigrations/success/testdata\"\n\tall, err := os.ReadDir(base)\n\trequire.NoError(t, err)\n\trequire.Len(t, all, 16)\n\tfiles := make([]string, 0, len(all))\n\tfor _, f := range all {\n\t\tfiles = append(files, filepath.Join(base, f.Name()))\n\t}\n\tstats, err := GatherStats(NewFileWalker(files...), false)\n\trequire.NoError(t, err)\n\trequire.Len(t, stats, 16)\n\tcheckGoStats(t, stats[0], \"001_up_down.go\", 1, 1, 1, true)\n\tcheckGoStats(t, stats[1], \"002_up_only.go\", 2, 1, 0, true)\n\tcheckGoStats(t, stats[2], \"003_down_only.go\", 3, 0, 1, true)\n\tcheckGoStats(t, stats[3], \"004_empty.go\", 4, 0, 0, true)\n\tcheckGoStats(t, stats[4], \"005_up_down_no_tx.go\", 5, 1, 1, false)\n\tcheckGoStats(t, stats[5], \"006_up_only_no_tx.go\", 6, 1, 0, false)\n\tcheckGoStats(t, stats[6], \"007_down_only_no_tx.go\", 7, 0, 1, false)\n\tcheckGoStats(t, stats[7], \"008_empty_no_tx.go\", 8, 0, 0, false)\n\tcheckGoStats(t, stats[8], \"009_up_down_ctx.go\", 9, 1, 1, true)\n\tcheckGoStats(t, stats[9], \"010_up_only_ctx.go\", 10, 1, 0, true)\n\tcheckGoStats(t, stats[10], \"011_down_only_ctx.go\", 11, 0, 1, true)\n\tcheckGoStats(t, stats[11], \"012_empty_ctx.go\", 12, 0, 0, true)\n\tcheckGoStats(t, stats[12], \"013_up_down_no_tx_ctx.go\", 13, 1, 1, false)\n\tcheckGoStats(t, stats[13], \"014_up_only_no_tx_ctx.go\", 14, 1, 0, false)\n\tcheckGoStats(t, stats[14], \"015_down_only_no_tx_ctx.go\", 15, 0, 1, false)\n\tcheckGoStats(t, stats[15], \"016_empty_no_tx_ctx.go\", 16, 0, 0, false)\n}\n\nfunc checkGoStats(t *testing.T, stats *Stats, filename string, version int64, upCount, downCount int, tx bool) {\n\tt.Helper()\n\trequire.Equal(t, filepath.Base(stats.FileName), filename)\n\trequire.Equal(t, stats.Version, version)\n\trequire.Equal(t, stats.UpCount, upCount)\n\trequire.Equal(t, stats.DownCount, downCount)\n\trequire.Equal(t, stats.Tx, tx)\n}\n\nfunc TestParsingGoMigrationsError(t *testing.T) {\n\tt.Parallel()\n\t_, err := parseGoFile(strings.NewReader(emptyInit))\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"no registered goose functions\")\n\n\t_, err = parseGoFile(strings.NewReader(wrongName))\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"AddMigration, AddMigrationNoTx, AddMigrationContext, AddMigrationNoTxContext\")\n}\n\nvar (\n\tupAndDown = `package foo\n\nimport (\n\t\"database/sql\"\n\n\t\"github.com/pressly/goose/v3\"\n)\n\nfunc init() {\n\tgoose.AddMigration(up001, down001)\n}\n\nfunc up001(tx *sql.Tx) error { return nil }\n\nfunc down001(tx *sql.Tx) error { return nil }`\n\n\tdownOnly = `package testgo\n\nimport (\n\t\"database/sql\"\n\n\t\"github.com/pressly/goose/v3\"\n)\n\nfunc init() {\n\tgoose.AddMigration(nil, down002)\n}\n\nfunc down002(tx *sql.Tx) error { return nil }`\n\n\tupOnly = `package testgo\n\nimport (\n\t\"database/sql\"\n\n\t\"github.com/pressly/goose/v3\"\n)\n\nfunc init() {\n\tgoose.AddMigration(up003, nil)\n}\n\nfunc up003(tx *sql.Tx) error { return nil }`\n\n\tupAndDownNil = `package testgo\n\nimport (\n\t\"database/sql\"\n\n\t\"github.com/pressly/goose/v3\"\n)\n\nfunc init() {\n\tgoose.AddMigration(nil, nil)\n}`\n)\nvar (\n\tupAndDownNoTx = `package foo\n\nimport (\n\t\"database/sql\"\n\n\t\"github.com/pressly/goose/v3\"\n)\n\nfunc init() {\n\tgoose.AddMigrationNoTx(up001, down001)\n}\n\nfunc up001(db *sql.DB) error { return nil }\n\nfunc down001(db *sql.DB) error { return nil }`\n\n\tdownOnlyNoTx = `package testgo\n\nimport (\n\t\"database/sql\"\n\n\t\"github.com/pressly/goose/v3\"\n)\n\nfunc init() {\n\tgoose.AddMigrationNoTx(nil, down002)\n}\n\nfunc down002(db *sql.DB) error { return nil }`\n\n\tupOnlyNoTx = `package testgo\n\nimport (\n\t\"database/sql\"\n\n\t\"github.com/pressly/goose/v3\"\n)\n\nfunc init() {\n\tgoose.AddMigrationNoTx(up003, nil)\n}\n\nfunc up003(db *sql.DB) error { return nil }`\n\n\tupAndDownNilNoTx = `package testgo\n\nimport (\n\t\"database/sql\"\n\n\t\"github.com/pressly/goose/v3\"\n)\n\nfunc init() {\n\tgoose.AddMigrationNoTx(nil, nil)\n}`\n)\n\nvar (\n\temptyInit = `package testgo\n\nfunc init() {}`\n\n\twrongName = `package testgo\n\nimport (\n\t\"database/sql\"\n\n\t\"github.com/pressly/goose/v3\"\n)\n\nfunc init() {\n\tgoose.AddMigrationWrongName(nil, nil)\n}`\n)\n"
  },
  {
    "path": "internal/migrationstats/migrationstats_walker.go",
    "content": "package migrationstats\n\nimport (\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n)\n\n// NewFileWalker returns a new FileWalker for the given filenames.\n//\n// Filenames without a .sql or .go extension are ignored.\nfunc NewFileWalker(filenames ...string) FileWalker {\n\treturn &fileWalker{\n\t\tfilenames: filenames,\n\t}\n}\n\ntype fileWalker struct {\n\tfilenames []string\n}\n\nvar _ FileWalker = (*fileWalker)(nil)\n\nfunc (f *fileWalker) Walk(fn func(filename string, r io.Reader) error) error {\n\tfor _, filename := range f.filenames {\n\t\text := filepath.Ext(filename)\n\t\tif ext != \".sql\" && ext != \".go\" {\n\t\t\tcontinue\n\t\t}\n\t\tif err := walk(filename, fn); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc walk(filename string, fn func(filename string, r io.Reader) error) error {\n\tfile, err := os.Open(filename)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer file.Close()\n\treturn fn(filename, file)\n}\n"
  },
  {
    "path": "internal/sqlparser/parse.go",
    "content": "package sqlparser\n\nimport (\n\t\"fmt\"\n\t\"io/fs\"\n\n\t\"go.uber.org/multierr\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\ntype ParsedSQL struct {\n\tUseTx    bool\n\tUp, Down []string\n}\n\nfunc ParseAllFromFS(fsys fs.FS, filename string, debug bool) (*ParsedSQL, error) {\n\tparsedSQL := new(ParsedSQL)\n\t// TODO(mf): parse is called twice, once for up and once for down. This is inefficient. It\n\t// should be possible to parse both directions in one pass. Also, UseTx is set once (but\n\t// returned twice), which is unnecessary and potentially error-prone if the two calls to\n\t// parseSQL disagree based on direction.\n\tvar g errgroup.Group\n\tg.Go(func() error {\n\t\tup, useTx, err := parse(fsys, filename, DirectionUp, debug)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tparsedSQL.Up = up\n\t\tparsedSQL.UseTx = useTx\n\t\treturn nil\n\t})\n\tg.Go(func() error {\n\t\tdown, _, err := parse(fsys, filename, DirectionDown, debug)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tparsedSQL.Down = down\n\t\treturn nil\n\t})\n\tif err := g.Wait(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn parsedSQL, nil\n}\n\nfunc parse(fsys fs.FS, filename string, direction Direction, debug bool) (_ []string, _ bool, retErr error) {\n\tr, err := fsys.Open(filename)\n\tif err != nil {\n\t\treturn nil, false, err\n\t}\n\tdefer func() {\n\t\tretErr = multierr.Append(retErr, r.Close())\n\t}()\n\tstmts, useTx, err := ParseSQLMigration(r, direction, debug)\n\tif err != nil {\n\t\treturn nil, false, fmt.Errorf(\"failed to parse %s: %w\", filename, err)\n\t}\n\treturn stmts, useTx, nil\n}\n"
  },
  {
    "path": "internal/sqlparser/parse_test.go",
    "content": "package sqlparser_test\n\nimport (\n\t\"os\"\n\t\"testing\"\n\t\"testing/fstest\"\n\n\t\"github.com/pressly/goose/v3/internal/sqlparser\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestParseAllFromFS(t *testing.T) {\n\tt.Parallel()\n\tt.Run(\"file_not_exist\", func(t *testing.T) {\n\t\tmapFS := fstest.MapFS{}\n\t\t_, err := sqlparser.ParseAllFromFS(mapFS, \"001_foo.sql\", false)\n\t\trequire.Error(t, err)\n\t\trequire.ErrorIs(t, err, os.ErrNotExist)\n\t})\n\tt.Run(\"empty_file\", func(t *testing.T) {\n\t\tmapFS := fstest.MapFS{\n\t\t\t\"001_foo.sql\": &fstest.MapFile{},\n\t\t}\n\t\t_, err := sqlparser.ParseAllFromFS(mapFS, \"001_foo.sql\", false)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"failed to parse migration\")\n\t\trequire.Contains(t, err.Error(), \"must start with '-- +goose Up' annotation\")\n\t})\n\tt.Run(\"all_statements\", func(t *testing.T) {\n\t\tmapFS := fstest.MapFS{\n\t\t\t\"001_foo.sql\": newFile(`\n-- +goose Up\n`),\n\t\t\t\"002_bar.sql\": newFile(`\n-- +goose Up\n-- +goose Down\n`),\n\t\t\t\"003_baz.sql\": newFile(`\n-- +goose Up\nCREATE TABLE foo (id int);\nCREATE TABLE bar (id int);\n\n-- +goose Down\nDROP TABLE bar;\n`),\n\t\t\t\"004_qux.sql\": newFile(`\n-- +goose NO TRANSACTION\n-- +goose Up\nCREATE TABLE foo (id int);\n-- +goose Down\nDROP TABLE foo;\n`),\n\t\t}\n\t\tparsedSQL, err := sqlparser.ParseAllFromFS(mapFS, \"001_foo.sql\", false)\n\t\trequire.NoError(t, err)\n\t\tassertParsedSQL(t, parsedSQL, true, 0, 0)\n\t\tparsedSQL, err = sqlparser.ParseAllFromFS(mapFS, \"002_bar.sql\", false)\n\t\trequire.NoError(t, err)\n\t\tassertParsedSQL(t, parsedSQL, true, 0, 0)\n\t\tparsedSQL, err = sqlparser.ParseAllFromFS(mapFS, \"003_baz.sql\", false)\n\t\trequire.NoError(t, err)\n\t\tassertParsedSQL(t, parsedSQL, true, 2, 1)\n\t\tparsedSQL, err = sqlparser.ParseAllFromFS(mapFS, \"004_qux.sql\", false)\n\t\trequire.NoError(t, err)\n\t\tassertParsedSQL(t, parsedSQL, false, 1, 1)\n\t})\n}\n\nfunc assertParsedSQL(t *testing.T, got *sqlparser.ParsedSQL, useTx bool, up, down int) {\n\tt.Helper()\n\trequire.NotNil(t, got)\n\trequire.Equal(t, len(got.Up), up)\n\trequire.Equal(t, len(got.Down), down)\n\trequire.Equal(t, got.UseTx, useTx)\n}\n\nfunc newFile(data string) *fstest.MapFile {\n\treturn &fstest.MapFile{\n\t\tData: []byte(data),\n\t}\n}\n"
  },
  {
    "path": "internal/sqlparser/parser.go",
    "content": "package sqlparser\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/mfridman/interpolate\"\n)\n\ntype Direction string\n\nconst (\n\tDirectionUp   Direction = \"up\"\n\tDirectionDown Direction = \"down\"\n)\n\nfunc FromBool(b bool) Direction {\n\tif b {\n\t\treturn DirectionUp\n\t}\n\treturn DirectionDown\n}\n\nfunc (d Direction) String() string {\n\treturn string(d)\n}\n\nfunc (d Direction) ToBool() bool {\n\treturn d == DirectionUp\n}\n\ntype parserState int\n\nconst (\n\tstart                   parserState = iota // 0\n\tgooseUp                                    // 1\n\tgooseStatementBeginUp                      // 2\n\tgooseStatementEndUp                        // 3\n\tgooseDown                                  // 4\n\tgooseStatementBeginDown                    // 5\n\tgooseStatementEndDown                      // 6\n)\n\ntype stateMachine struct {\n\tstate   parserState\n\tverbose bool\n}\n\nfunc newStateMachine(begin parserState, verbose bool) *stateMachine {\n\treturn &stateMachine{\n\t\tstate:   begin,\n\t\tverbose: verbose,\n\t}\n}\n\nfunc (s *stateMachine) get() parserState {\n\treturn s.state\n}\n\nfunc (s *stateMachine) set(new parserState) {\n\ts.print(\"set %d => %d\", s.state, new)\n\ts.state = new\n}\n\nconst (\n\tgrayColor  = \"\\033[90m\"\n\tresetColor = \"\\033[00m\"\n)\n\nfunc (s *stateMachine) print(msg string, args ...any) {\n\tmsg = \"StateMachine: \" + msg\n\tif s.verbose {\n\t\tlog.Printf(grayColor+msg+resetColor, args...)\n\t}\n}\n\nconst scanBufSize = 4 * 1024 * 1024\n\nvar bufferPool = sync.Pool{\n\tNew: func() any {\n\t\tbuf := make([]byte, scanBufSize)\n\t\treturn &buf\n\t},\n}\n\n// Split given SQL script into individual statements and return\n// SQL statements for given direction (up=true, down=false).\n//\n// The base case is to simply split on semicolons, as these\n// naturally terminate a statement.\n//\n// However, more complex cases like pl/pgsql can have semicolons\n// within a statement. For these cases, we provide the explicit annotations\n// 'StatementBegin' and 'StatementEnd' to allow the script to\n// tell us to ignore semicolons.\nfunc ParseSQLMigration(r io.Reader, direction Direction, debug bool) (stmts []string, useTx bool, err error) {\n\tscanBufPtr := bufferPool.Get().(*[]byte)\n\tscanBuf := *scanBufPtr\n\tdefer bufferPool.Put(scanBufPtr)\n\n\tscanner := bufio.NewScanner(r)\n\tscanner.Buffer(scanBuf, scanBufSize)\n\n\tstateMachine := newStateMachine(start, debug)\n\tuseTx = true\n\tuseEnvsub := false\n\n\tvar buf bytes.Buffer\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\t\tif debug {\n\t\t\tlog.Println(line)\n\t\t}\n\t\tif stateMachine.get() == start && strings.TrimSpace(line) == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Check for annotations.\n\t\t// All annotations must be in format: \"-- +goose [annotation]\"\n\t\tif strings.HasPrefix(strings.TrimSpace(line), \"--\") && strings.Contains(line, \"+goose\") {\n\t\t\tvar cmd annotation\n\n\t\t\tcmd, err = extractAnnotation(line)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, false, fmt.Errorf(\"failed to parse annotation line %q: %w\", line, err)\n\t\t\t}\n\n\t\t\tswitch cmd {\n\t\t\tcase annotationUp:\n\t\t\t\tswitch stateMachine.get() {\n\t\t\t\tcase start:\n\t\t\t\t\tstateMachine.set(gooseUp)\n\t\t\t\tdefault:\n\t\t\t\t\treturn nil, false, fmt.Errorf(\"duplicate '-- +goose Up' annotations; stateMachine=%d, see https://github.com/pressly/goose#sql-migrations\", stateMachine.state)\n\t\t\t\t}\n\t\t\t\tcontinue\n\n\t\t\tcase annotationDown:\n\t\t\t\tswitch stateMachine.get() {\n\t\t\t\tcase gooseUp, gooseStatementEndUp:\n\t\t\t\t\t// If we hit a down annotation, but the buffer is not empty, we have an unfinished SQL query from a\n\t\t\t\t\t// previous up annotation. This is an error, because we expect the SQL query to be terminated by a semicolon\n\t\t\t\t\t// and the buffer to have been reset.\n\t\t\t\t\tif bufferRemaining := strings.TrimSpace(buf.String()); len(bufferRemaining) > 0 {\n\t\t\t\t\t\treturn nil, false, missingSemicolonError(stateMachine.state, direction, bufferRemaining)\n\t\t\t\t\t}\n\t\t\t\t\tstateMachine.set(gooseDown)\n\t\t\t\tdefault:\n\t\t\t\t\treturn nil, false, fmt.Errorf(\"must start with '-- +goose Up' annotation, stateMachine=%d, see https://github.com/pressly/goose#sql-migrations\", stateMachine.state)\n\t\t\t\t}\n\t\t\t\tcontinue\n\n\t\t\tcase annotationStatementBegin:\n\t\t\t\tswitch stateMachine.get() {\n\t\t\t\tcase gooseUp, gooseStatementEndUp:\n\t\t\t\t\tstateMachine.set(gooseStatementBeginUp)\n\t\t\t\tcase gooseDown, gooseStatementEndDown:\n\t\t\t\t\tstateMachine.set(gooseStatementBeginDown)\n\t\t\t\tdefault:\n\t\t\t\t\treturn 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)\n\t\t\t\t}\n\t\t\t\tcontinue\n\n\t\t\tcase annotationStatementEnd:\n\t\t\t\tswitch stateMachine.get() {\n\t\t\t\tcase gooseStatementBeginUp:\n\t\t\t\t\tstateMachine.set(gooseStatementEndUp)\n\t\t\t\tcase gooseStatementBeginDown:\n\t\t\t\t\tstateMachine.set(gooseStatementEndDown)\n\t\t\t\tdefault:\n\t\t\t\t\treturn nil, false, errors.New(\"'-- +goose StatementEnd' must be defined after '-- +goose StatementBegin', see https://github.com/pressly/goose#sql-migrations\")\n\t\t\t\t}\n\n\t\t\tcase annotationNoTransaction:\n\t\t\t\tuseTx = false\n\t\t\t\tcontinue\n\n\t\t\tcase annotationEnvsubOn:\n\t\t\t\tuseEnvsub = true\n\t\t\t\tcontinue\n\n\t\t\tcase annotationEnvsubOff:\n\t\t\t\tuseEnvsub = false\n\t\t\t\tcontinue\n\n\t\t\tdefault:\n\t\t\t\treturn nil, false, fmt.Errorf(\"unknown annotation: %q\", cmd)\n\t\t\t}\n\t\t}\n\t\t// Once we've started parsing a statement the buffer is no longer empty,\n\t\t// we keep all comments up until the end of the statement (the buffer will be reset).\n\t\t// All other comments in the file are ignored.\n\t\tif buf.Len() == 0 {\n\t\t\t// This check ensures leading comments and empty lines prior to a statement are ignored.\n\t\t\tif strings.HasPrefix(strings.TrimSpace(line), \"--\") || line == \"\" {\n\t\t\t\tstateMachine.print(\"ignore comment\")\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tswitch stateMachine.get() {\n\t\tcase gooseStatementEndDown, gooseStatementEndUp:\n\t\t\t// Do not include the \"+goose StatementEnd\" annotation in the final statement.\n\t\tdefault:\n\t\t\tif useEnvsub {\n\t\t\t\texpanded, err := interpolate.Interpolate(&envWrapper{}, line)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, false, fmt.Errorf(\"variable substitution failed: %w:\\n%s\", err, line)\n\t\t\t\t}\n\t\t\t\tline = expanded\n\t\t\t}\n\t\t\t// Write SQL line to a buffer.\n\t\t\tif _, err := buf.WriteString(line + \"\\n\"); err != nil {\n\t\t\t\treturn nil, false, fmt.Errorf(\"failed to write to buf: %w\", err)\n\t\t\t}\n\t\t}\n\t\t// Read SQL body one by line, if we're in the right direction.\n\t\t//\n\t\t// 1) basic query with semicolon; 2) psql statement\n\t\t//\n\t\t// Export statement once we hit end of statement.\n\t\tswitch stateMachine.get() {\n\t\tcase gooseUp, gooseStatementBeginUp, gooseStatementEndUp:\n\t\t\tif direction == DirectionDown {\n\t\t\t\tbuf.Reset()\n\t\t\t\tstateMachine.print(\"ignore down\")\n\t\t\t\tcontinue\n\t\t\t}\n\t\tcase gooseDown, gooseStatementBeginDown, gooseStatementEndDown:\n\t\t\tif direction == DirectionUp {\n\t\t\t\tbuf.Reset()\n\t\t\t\tstateMachine.print(\"ignore up\")\n\t\t\t\tcontinue\n\t\t\t}\n\t\tdefault:\n\t\t\treturn 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)\n\t\t}\n\n\t\tswitch stateMachine.get() {\n\t\tcase gooseUp:\n\t\t\tif endsWithSemicolon(line) {\n\t\t\t\tstmts = append(stmts, cleanupStatement(buf.String()))\n\t\t\t\tbuf.Reset()\n\t\t\t\tstateMachine.print(\"store simple Up query\")\n\t\t\t}\n\t\tcase gooseDown:\n\t\t\tif endsWithSemicolon(line) {\n\t\t\t\tstmts = append(stmts, cleanupStatement(buf.String()))\n\t\t\t\tbuf.Reset()\n\t\t\t\tstateMachine.print(\"store simple Down query\")\n\t\t\t}\n\t\tcase gooseStatementEndUp:\n\t\t\tstmts = append(stmts, cleanupStatement(buf.String()))\n\t\t\tbuf.Reset()\n\t\t\tstateMachine.print(\"store Up statement\")\n\t\t\tstateMachine.set(gooseUp)\n\t\tcase gooseStatementEndDown:\n\t\t\tstmts = append(stmts, cleanupStatement(buf.String()))\n\t\t\tbuf.Reset()\n\t\t\tstateMachine.print(\"store Down statement\")\n\t\t\tstateMachine.set(gooseDown)\n\t\t}\n\t}\n\tif err := scanner.Err(); err != nil {\n\t\treturn nil, false, fmt.Errorf(\"failed to scan migration: %w\", err)\n\t}\n\t// EOF\n\n\tswitch stateMachine.get() {\n\tcase start:\n\t\treturn nil, false, errors.New(\"failed to parse migration: must start with '-- +goose Up' annotation, see https://github.com/pressly/goose#sql-migrations\")\n\tcase gooseStatementBeginUp, gooseStatementBeginDown:\n\t\treturn nil, false, errors.New(\"failed to parse migration: missing '-- +goose StatementEnd' annotation\")\n\t}\n\n\tif bufferRemaining := strings.TrimSpace(buf.String()); len(bufferRemaining) > 0 {\n\t\treturn nil, false, missingSemicolonError(stateMachine.state, direction, bufferRemaining)\n\t}\n\n\treturn stmts, useTx, nil\n}\n\ntype annotation string\n\nconst (\n\tannotationUp             annotation = \"Up\"\n\tannotationDown           annotation = \"Down\"\n\tannotationStatementBegin annotation = \"StatementBegin\"\n\tannotationStatementEnd   annotation = \"StatementEnd\"\n\tannotationNoTransaction  annotation = \"NO TRANSACTION\"\n\tannotationEnvsubOn       annotation = \"ENVSUB ON\"\n\tannotationEnvsubOff      annotation = \"ENVSUB OFF\"\n)\n\nvar supportedAnnotations = map[annotation]struct{}{\n\tannotationUp:             {},\n\tannotationDown:           {},\n\tannotationStatementBegin: {},\n\tannotationStatementEnd:   {},\n\tannotationNoTransaction:  {},\n\tannotationEnvsubOn:       {},\n\tannotationEnvsubOff:      {},\n}\n\nvar (\n\terrEmptyAnnotation   = errors.New(\"empty annotation\")\n\terrInvalidAnnotation = errors.New(\"invalid annotation\")\n)\n\n// extractAnnotation extracts the annotation from the line.\n// All annotations must be in format: \"-- +goose [annotation]\"\n// Allowed annotations: Up, Down, StatementBegin, StatementEnd, NO TRANSACTION, ENVSUB ON, ENVSUB OFF\nfunc extractAnnotation(line string) (annotation, error) {\n\t// If line contains leading whitespace - return error.\n\tif strings.HasPrefix(line, \" \") || strings.HasPrefix(line, \"\\t\") {\n\t\treturn \"\", fmt.Errorf(\"%q contains leading whitespace: %w\", line, errInvalidAnnotation)\n\t}\n\n\t// Extract the annotation from the line, by removing the leading \"--\"\n\tcmd := strings.ReplaceAll(line, \"--\", \"\")\n\n\t// Extract the annotation from the line, by removing the leading \"+goose\"\n\tcmd = strings.Replace(cmd, \"+goose\", \"\", 1)\n\n\tif strings.Contains(cmd, \"+goose\") {\n\t\treturn \"\", fmt.Errorf(\"%q contains multiple '+goose' annotations: %w\", cmd, errInvalidAnnotation)\n\t}\n\n\t// Remove leading and trailing whitespace from the annotation command.\n\tcmd = strings.TrimSpace(cmd)\n\n\tif cmd == \"\" {\n\t\treturn \"\", errEmptyAnnotation\n\t}\n\n\ta := annotation(cmd)\n\n\tfor s := range supportedAnnotations {\n\t\tif strings.EqualFold(string(s), string(a)) {\n\t\t\treturn s, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"%q not supported: %w\", cmd, errInvalidAnnotation)\n}\n\nfunc missingSemicolonError(state parserState, direction Direction, s string) error {\n\treturn fmt.Errorf(\"failed to parse migration: state %d, direction: %v: unexpected unfinished SQL query: %q: missing semicolon?\",\n\t\tstate,\n\t\tdirection,\n\t\ts,\n\t)\n}\n\ntype envWrapper struct{}\n\nvar _ interpolate.Env = (*envWrapper)(nil)\n\nfunc (e *envWrapper) Get(key string) (string, bool) {\n\treturn os.LookupEnv(key)\n}\n\nfunc cleanupStatement(input string) string {\n\treturn strings.TrimSpace(input)\n}\n\n// Checks the line to see if the line has a statement-ending semicolon\n// or if the line contains a double-dash comment.\nfunc endsWithSemicolon(line string) bool {\n\tscanBufPtr := bufferPool.Get().(*[]byte)\n\tscanBuf := *scanBufPtr\n\tdefer bufferPool.Put(scanBufPtr)\n\n\tprev := \"\"\n\tscanner := bufio.NewScanner(strings.NewReader(line))\n\tscanner.Buffer(scanBuf, scanBufSize)\n\tscanner.Split(bufio.ScanWords)\n\n\tfor scanner.Scan() {\n\t\tword := scanner.Text()\n\t\tif strings.HasPrefix(word, \"--\") {\n\t\t\tbreak\n\t\t}\n\t\tprev = word\n\t}\n\n\treturn strings.HasSuffix(prev, \";\")\n}\n"
  },
  {
    "path": "internal/sqlparser/parser_test.go",
    "content": "package sqlparser\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar (\n\tdebug = false\n)\n\nfunc TestMain(m *testing.M) {\n\tdebug, _ = strconv.ParseBool(os.Getenv(\"DEBUG_TEST\"))\n\tos.Exit(m.Run())\n}\n\nfunc TestSemicolons(t *testing.T) {\n\tt.Parallel()\n\n\ttype testData struct {\n\t\tline   string\n\t\tresult bool\n\t}\n\n\ttests := []testData{\n\t\t{line: \"END;\", result: true},\n\t\t{line: \"END; -- comment\", result: true},\n\t\t{line: \"END   ; -- comment\", result: true},\n\t\t{line: \"END -- comment\", result: false},\n\t\t{line: \"END -- comment ;\", result: false},\n\t\t{line: \"END \\\" ; \\\" -- comment\", result: false},\n\t}\n\n\tfor _, test := range tests {\n\t\tr := endsWithSemicolon(test.line)\n\t\tif r != test.result {\n\t\t\tt.Errorf(\"incorrect semicolon. got %v, want %v\", r, test.result)\n\t\t}\n\t}\n}\n\nfunc TestSplitStatements(t *testing.T) {\n\tt.Parallel()\n\n\ttype testData struct {\n\t\tsql  string\n\t\tup   int\n\t\tdown int\n\t}\n\n\ttt := []testData{\n\t\t{sql: multilineSQL, up: 4, down: 1},\n\t\t{sql: emptySQL, up: 0, down: 0},\n\t\t{sql: emptySQL2, up: 0, down: 0},\n\t\t{sql: functxt, up: 2, down: 2},\n\t\t{sql: mysqlChangeDelimiter, up: 4, down: 0},\n\t\t{sql: copyFromStdin, up: 1, down: 0},\n\t\t{sql: plpgsqlSyntax, up: 2, down: 2},\n\t\t{sql: plpgsqlSyntaxMixedStatements, up: 2, down: 2},\n\t}\n\n\tfor i, test := range tt {\n\t\t// up\n\t\tstmts, _, err := ParseSQLMigration(strings.NewReader(test.sql), DirectionUp, debug)\n\t\tif err != nil {\n\t\t\tt.Error(fmt.Errorf(\"tt[%v] unexpected error: %w\", i, err))\n\t\t}\n\t\tif len(stmts) != test.up {\n\t\t\tt.Errorf(\"tt[%v] incorrect number of up stmts. got %v (%+v), want %v\", i, len(stmts), stmts, test.up)\n\t\t}\n\n\t\t// down\n\t\tstmts, _, err = ParseSQLMigration(strings.NewReader(test.sql), DirectionDown, debug)\n\t\tif err != nil {\n\t\t\tt.Error(fmt.Errorf(\"tt[%v] unexpected error: %w\", i, err))\n\t\t}\n\t\tif len(stmts) != test.down {\n\t\t\tt.Errorf(\"tt[%v] incorrect number of down stmts. got %v (%+v), want %v\", i, len(stmts), stmts, test.down)\n\t\t}\n\t}\n}\n\nfunc TestInvalidUp(t *testing.T) {\n\tt.Parallel()\n\n\ttestdataDir := filepath.Join(\"testdata\", \"invalid\", \"up\")\n\tentries, err := os.ReadDir(testdataDir)\n\trequire.NoError(t, err)\n\trequire.NotEmpty(t, entries)\n\n\tfor _, entry := range entries {\n\t\tby, err := os.ReadFile(filepath.Join(testdataDir, entry.Name()))\n\t\trequire.NoError(t, err)\n\t\t_, _, err = ParseSQLMigration(strings.NewReader(string(by)), DirectionUp, false)\n\t\trequire.Error(t, err)\n\t}\n}\n\nfunc TestUseTransactions(t *testing.T) {\n\tt.Parallel()\n\n\ttype testData struct {\n\t\tfileName        string\n\t\tuseTransactions bool\n\t}\n\n\ttests := []testData{\n\t\t{fileName: \"testdata/valid-txn/00001_create_users_table.sql\", useTransactions: true},\n\t\t{fileName: \"testdata/valid-txn/00002_rename_root.sql\", useTransactions: true},\n\t\t{fileName: \"testdata/valid-txn/00003_no_transaction.sql\", useTransactions: false},\n\t}\n\n\tfor _, test := range tests {\n\t\tf, err := os.Open(test.fileName)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\t_, useTx, err := ParseSQLMigration(f, DirectionUp, debug)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\tif useTx != test.useTransactions {\n\t\t\tt.Errorf(\"Failed transaction check. got %v, want %v\", useTx, test.useTransactions)\n\t\t}\n\t\tf.Close()\n\t}\n}\n\nfunc TestParsingErrors(t *testing.T) {\n\ttt := []string{\n\t\tstatementBeginNoStatementEnd,\n\t\tunfinishedSQL,\n\t\tnoUpDownAnnotations,\n\t\tmultiUpDown,\n\t\tdownFirst,\n\t}\n\tfor i, sql := range tt {\n\t\t_, _, err := ParseSQLMigration(strings.NewReader(sql), DirectionUp, debug)\n\t\tif err == nil {\n\t\t\tt.Errorf(\"expected error on tt[%v] %q\", i, sql)\n\t\t}\n\t}\n}\n\nvar multilineSQL = `-- +goose Up\nCREATE TABLE post (\n\t\tid int NOT NULL,\n\t\ttitle text,\n\t\tbody text,\n\t\tPRIMARY KEY(id)\n);                  -- 1st stmt\n\n-- comment\nSELECT 2;           -- 2nd stmt\nSELECT 3; SELECT 3; -- 3rd stmt\nSELECT 4;           -- 4th stmt\n\n-- +goose Down\n-- comment\nDROP TABLE post;    -- 1st stmt\n`\n\nvar functxt = `-- +goose Up\nCREATE TABLE IF NOT EXISTS histories (\n\tid                BIGSERIAL  PRIMARY KEY,\n\tcurrent_value     varchar(2000) NOT NULL,\n\tcreated_at      timestamp with time zone  NOT NULL\n);\n\n-- +goose StatementBegin\nCREATE OR REPLACE FUNCTION histories_partition_creation( DATE, DATE )\nreturns void AS $$\nDECLARE\n\tcreate_query text;\nBEGIN\n\tFOR create_query IN SELECT\n\t\t\t'CREATE TABLE IF NOT EXISTS histories_'\n\t\t\t|| TO_CHAR( d, 'YYYY_MM' )\n\t\t\t|| ' ( CHECK( created_at >= timestamp '''\n\t\t\t|| TO_CHAR( d, 'YYYY-MM-DD 00:00:00' )\n\t\t\t|| ''' AND created_at < timestamp '''\n\t\t\t|| TO_CHAR( d + INTERVAL '1 month', 'YYYY-MM-DD 00:00:00' )\n\t\t\t|| ''' ) ) inherits ( histories );'\n\t\tFROM generate_series( $1, $2, '1 month' ) AS d\n\tLOOP\n\t\tEXECUTE create_query;\n\tEND LOOP;  -- LOOP END\nEND;         -- FUNCTION END\n$$\nlanguage plpgsql;\n-- +goose StatementEnd\n\n-- +goose Down\ndrop function histories_partition_creation(DATE, DATE);\ndrop TABLE histories;\n`\n\nvar multiUpDown = `-- +goose Up\nCREATE TABLE post (\n\t\tid int NOT NULL,\n\t\ttitle text,\n\t\tbody text,\n\t\tPRIMARY KEY(id)\n);\n\n-- +goose Down\nDROP TABLE post;\n\n-- +goose Up\nCREATE TABLE fancier_post (\n\t\tid int NOT NULL,\n\t\ttitle text,\n\t\tbody text,\n\t\tcreated_on timestamp without time zone,\n\t\tPRIMARY KEY(id)\n);\n`\n\nvar downFirst = `-- +goose Down\nDROP TABLE fancier_post;\n`\n\nvar statementBeginNoStatementEnd = `-- +goose Up\nCREATE TABLE IF NOT EXISTS histories (\n  id                BIGSERIAL  PRIMARY KEY,\n  current_value     varchar(2000) NOT NULL,\n  created_at      timestamp with time zone  NOT NULL\n);\n\n-- +goose StatementBegin\nCREATE OR REPLACE FUNCTION histories_partition_creation( DATE, DATE )\nreturns void AS $$\nDECLARE\n  create_query text;\nBEGIN\n  FOR create_query IN SELECT\n      'CREATE TABLE IF NOT EXISTS histories_'\n      || TO_CHAR( d, 'YYYY_MM' )\n      || ' ( CHECK( created_at >= timestamp '''\n      || TO_CHAR( d, 'YYYY-MM-DD 00:00:00' )\n      || ''' AND created_at < timestamp '''\n      || TO_CHAR( d + INTERVAL '1 month', 'YYYY-MM-DD 00:00:00' )\n      || ''' ) ) inherits ( histories );'\n    FROM generate_series( $1, $2, '1 month' ) AS d\n  LOOP\n    EXECUTE create_query;\n  END LOOP;  -- LOOP END\nEND;         -- FUNCTION END\n$$\nlanguage plpgsql;\n\n-- +goose Down\ndrop function histories_partition_creation(DATE, DATE);\ndrop TABLE histories;\n`\n\nvar unfinishedSQL = `\n-- +goose Up\nALTER TABLE post\n\n-- +goose Down\n`\n\nvar emptySQL = `-- +goose Up\n-- This is just a comment`\n\nvar emptySQL2 = `\n\n-- comment\n-- +goose Up\n\n-- comment\n-- +goose Down\n\n`\n\nvar noUpDownAnnotations = `\nCREATE TABLE post (\n    id int NOT NULL,\n    title text,\n    body text,\n    PRIMARY KEY(id)\n);\n`\n\nvar mysqlChangeDelimiter = `\n-- +goose Up\n-- +goose StatementBegin\nDELIMITER | \n-- +goose StatementEnd\n\n-- +goose StatementBegin\nCREATE FUNCTION my_func( str CHAR(255) ) RETURNS CHAR(255) DETERMINISTIC\nBEGIN \n  RETURN \"Dummy Body\"; \nEND | \n-- +goose StatementEnd\n\n-- +goose StatementBegin\nDELIMITER ; \n-- +goose StatementEnd\n\nselect my_func(\"123\") from dual;\n-- +goose Down\n`\n\nvar copyFromStdin = `\n-- +goose Up\n-- +goose StatementBegin\nCOPY public.django_content_type (id, app_label, model) FROM stdin;\n1\tadmin\tlogentry\n2\tauth\tpermission\n3\tauth\tgroup\n4\tauth\tuser\n5\tcontenttypes\tcontenttype\n6\tsessions\tsession\n\\.\n-- +goose StatementEnd\n`\n\nvar plpgsqlSyntax = `\n-- +goose Up\n-- +goose StatementBegin\nCREATE OR REPLACE FUNCTION update_updated_at_column()\nRETURNS TRIGGER AS $$\nBEGIN\n    NEW.updated_at = now();\n    RETURN NEW;\nEND;\n$$ language 'plpgsql';\n-- +goose StatementEnd\n-- +goose StatementBegin\nCREATE TRIGGER update_properties_updated_at BEFORE UPDATE ON properties FOR EACH ROW EXECUTE PROCEDURE  update_updated_at_column();\n-- +goose StatementEnd\n\n-- +goose Down\n-- +goose StatementBegin\nDROP TRIGGER update_properties_updated_at\n-- +goose StatementEnd\n-- +goose StatementBegin\nDROP FUNCTION update_updated_at_column()\n-- +goose StatementEnd\n`\n\nvar plpgsqlSyntaxMixedStatements = `\n-- +goose Up\n-- +goose StatementBegin\nCREATE OR REPLACE FUNCTION update_updated_at_column()\nRETURNS TRIGGER AS $$\nBEGIN\n    NEW.updated_at = now();\n    RETURN NEW;\nEND;\n$$ language 'plpgsql';\n-- +goose StatementEnd\n\nCREATE TRIGGER update_properties_updated_at\nBEFORE UPDATE\nON properties \nFOR EACH ROW EXECUTE PROCEDURE  update_updated_at_column();\n\n-- +goose Down\nDROP TRIGGER update_properties_updated_at;\nDROP FUNCTION update_updated_at_column();\n`\n\nfunc TestValidUp(t *testing.T) {\n\tt.Parallel()\n\t// Test valid \"up\" parser logic.\n\t//\n\t// This test expects each directory, such as: internal/sqlparser/testdata/valid-up/test01\n\t//\n\t// to contain exactly one migration file called \"input.sql\". We read this file and pass it\n\t// to the parser. Then we compare the statements against the golden files.\n\t// Each golden file is equivalent to one statement.\n\t//\n\t// ├── 01.up.golden.sql\n\t// ├── 02.up.golden.sql\n\t// ├── 03.up.golden.sql\n\t// └── input.sql\n\ttests := []struct {\n\t\tName            string\n\t\tStatementsCount int\n\t}{\n\t\t{Name: \"test01\", StatementsCount: 3},\n\t\t{Name: \"test02\", StatementsCount: 1},\n\t\t{Name: \"test03\", StatementsCount: 1},\n\t\t{Name: \"test04\", StatementsCount: 3},\n\t\t{Name: \"test05\", StatementsCount: 2},\n\t\t{Name: \"test06\", StatementsCount: 5},\n\t\t{Name: \"test07\", StatementsCount: 1},\n\t\t{Name: \"test08\", StatementsCount: 6},\n\t\t{Name: \"test09\", StatementsCount: 1},\n\t}\n\tfor _, tc := range tests {\n\t\tpath := filepath.Join(\"testdata\", \"valid-up\", tc.Name)\n\t\tt.Run(tc.Name, func(t *testing.T) {\n\t\t\ttestValid(t, path, tc.StatementsCount, DirectionUp)\n\t\t})\n\t}\n}\n\nfunc testValid(t *testing.T, dir string, count int, direction Direction) {\n\tt.Helper()\n\n\tf, err := os.Open(filepath.Join(dir, \"input.sql\"))\n\trequire.NoError(t, err)\n\tt.Cleanup(func() { f.Close() })\n\tstatements, _, err := ParseSQLMigration(f, direction, debug)\n\trequire.NoError(t, err)\n\trequire.Equal(t, len(statements), count)\n\tcompareStatements(t, dir, statements, direction)\n}\n\nfunc compareStatements(t *testing.T, dir string, statements []string, direction Direction) {\n\tt.Helper()\n\n\tfiles, err := filepath.Glob(filepath.Join(dir, fmt.Sprintf(\"*.%s.golden.sql\", direction)))\n\trequire.NoError(t, err)\n\tif len(statements) != len(files) {\n\t\tt.Fatalf(\"mismatch between parsed statements (%d) and golden files (%d), did you check in NN.{up|down}.golden.sql file in %q?\", len(statements), len(files), dir)\n\t}\n\tfor _, goldenFile := range files {\n\t\tgoldenFile = filepath.Base(goldenFile)\n\t\tbefore, _, ok := strings.Cut(goldenFile, \".\")\n\t\tif !ok {\n\t\t\tt.Fatal(`failed to cut on file delimiter \".\", must be of the format NN.{up|down}.golden.sql`)\n\t\t}\n\t\tindex, err := strconv.Atoi(before)\n\t\trequire.NoError(t, err)\n\t\tindex--\n\n\t\tgoldenFilePath := filepath.Join(dir, goldenFile)\n\t\tby, err := os.ReadFile(goldenFilePath)\n\t\trequire.NoError(t, err)\n\n\t\tgot, want := statements[index], string(by)\n\n\t\tif got != want {\n\t\t\tif isCIEnvironment() {\n\t\t\t\tt.Errorf(\"input does not match expected golden file:\\n\\ngot:\\n%s\\n\\nwant:\\n%s\\n\", got, want)\n\t\t\t} else {\n\t\t\t\tt.Error(\"input does not match expected output; diff files with .FAIL to debug\")\n\t\t\t\tt.Logf(\"\\ndiff %v %v\",\n\t\t\t\t\tfilepath.Join(\"internal\", \"sqlparser\", goldenFilePath+\".FAIL\"),\n\t\t\t\t\tfilepath.Join(\"internal\", \"sqlparser\", goldenFilePath),\n\t\t\t\t)\n\t\t\t\terr := os.WriteFile(goldenFilePath+\".FAIL\", []byte(got), 0644)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc isCIEnvironment() bool {\n\tok, _ := strconv.ParseBool(os.Getenv(\"CI\"))\n\treturn ok\n}\n\nfunc TestEnvsub(t *testing.T) {\n\t// Do not run in parallel, as this test sets environment variables.\n\n\t// Test valid migrations with ${var} like statements when on are substituted for the whole\n\t// migration.\n\tt.Setenv(\"GOOSE_ENV_REGION\", \"us_east_\")\n\tt.Setenv(\"GOOSE_ENV_SET_BUT_EMPTY_VALUE\", \"\")\n\tt.Setenv(\"GOOSE_ENV_NAME\", \"foo\")\n\n\ttests := []struct {\n\t\tName      string\n\t\tDownCount int\n\t\tUpCount   int\n\t}{\n\t\t{Name: \"test01\", UpCount: 4, DownCount: 1},\n\t\t{Name: \"test02\", UpCount: 3, DownCount: 0},\n\t\t{Name: \"test03\", UpCount: 1, DownCount: 0},\n\t}\n\tfor _, tc := range tests {\n\t\tt.Run(tc.Name, func(t *testing.T) {\n\t\t\tdir := filepath.Join(\"testdata\", \"envsub\", tc.Name)\n\t\t\ttestValid(t, dir, tc.UpCount, DirectionUp)\n\t\t\ttestValid(t, dir, tc.DownCount, DirectionDown)\n\t\t})\n\t}\n}\n\nfunc TestEnvsubError(t *testing.T) {\n\tt.Parallel()\n\n\ts := `\n-- +goose ENVSUB ON\n-- +goose Up\nCREATE TABLE post (\n\tid int NOT NULL,\n\ttitle text,\n\t${SOME_UNSET_VAR?required env var not set} text,\n\tPRIMARY KEY(id)\n);\n`\n\t_, _, err := ParseSQLMigration(strings.NewReader(s), DirectionUp, debug)\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"variable substitution failed: $SOME_UNSET_VAR: required env var not set:\")\n}\n\nfunc Test_extractAnnotation(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tinput   string\n\t\twant    annotation\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname:    \"Up\",\n\t\t\tinput:   \"-- +goose Up\",\n\t\t\twant:    annotationUp,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"Down\",\n\t\t\tinput:   \"-- +goose Down\",\n\t\t\twant:    annotationDown,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"StmtBegin\",\n\t\t\tinput:   \"-- +goose StatementBegin\",\n\t\t\twant:    annotationStatementBegin,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"NoTransact\",\n\t\t\tinput:   \"-- +goose NO TRANSACTION\",\n\t\t\twant:    annotationNoTransaction,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"Unsupported\",\n\t\t\tinput:   \"-- +goose unsupported\",\n\t\t\twant:    \"\",\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"Empty\",\n\t\t\tinput:   \"-- +goose\",\n\t\t\twant:    \"\",\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"statement with spaces and Uppercase\",\n\t\t\tinput:   \"-- +goose   UP \t\",\n\t\t\twant:    annotationUp,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"statement with leading whitespace - error\",\n\t\t\tinput:   \" -- +goose   UP \t\",\n\t\t\twant:    \"\",\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"statement with leading \\t - error\",\n\t\t\tinput:   \"\\t-- +goose   UP \t\",\n\t\t\twant:    \"\",\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"multiple +goose annotations - error\",\n\t\t\tinput:   \"-- +goose +goose Up\",\n\t\t\twant:    \"\",\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := extractAnnotation(tt.input)\n\t\t\tif tt.wantErr {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t\trequire.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/sqlparser/testdata/envsub/test01/01.down.golden.sql",
    "content": "DROP TABLE us_east_post;    -- 1st stmt"
  },
  {
    "path": "internal/sqlparser/testdata/envsub/test01/01.up.golden.sql",
    "content": "CREATE TABLE us_east_post (\n\t\tid int NOT NULL,\n\t\ttitle text,\n\t\tbody text,\n\t\tPRIMARY KEY(id)\n);                  -- 1st stmt"
  },
  {
    "path": "internal/sqlparser/testdata/envsub/test01/02.up.golden.sql",
    "content": "SELECT 2;           -- 2nd stmt"
  },
  {
    "path": "internal/sqlparser/testdata/envsub/test01/03.up.golden.sql",
    "content": "SELECT 3; SELECT 3; -- 3rd stmt"
  },
  {
    "path": "internal/sqlparser/testdata/envsub/test01/04.up.golden.sql",
    "content": "SELECT 4;           -- 4th stmt"
  },
  {
    "path": "internal/sqlparser/testdata/envsub/test01/input.sql",
    "content": "-- +goose ENVSUB ON\n-- +goose Up\nCREATE TABLE ${GOOSE_ENV_REGION}post (\n\t\tid int NOT NULL,\n\t\ttitle text,\n\t\tbody text,\n\t\tPRIMARY KEY(id)\n);                  -- 1st stmt\n\n-- comment\nSELECT 2;           -- 2nd stmt\nSELECT 3; SELECT 3; -- 3rd stmt\nSELECT 4;           -- 4th stmt\n\n-- +goose Down\n-- comment\nDROP TABLE ${GOOSE_ENV_REGION}post;    -- 1st stmt\n"
  },
  {
    "path": "internal/sqlparser/testdata/envsub/test02/01.up.golden.sql",
    "content": "CREATE TABLE post (\n\tid int NOT NULL,\n\ttitle text,\n\tfoo text,\n\tfootitle3 text,\n\tdefaulttitle4 text,\n\ttitle5 text,\n);"
  },
  {
    "path": "internal/sqlparser/testdata/envsub/test02/02.up.golden.sql",
    "content": "CREATE TABLE post (\n\tid int NOT NULL,\n\ttitle text,\n\t$GOOSE_ENV_NAME text,\n\t${GOOSE_ENV_NAME}title3 text,\n\t${ANOTHER_VAR:-default}title4 text,\n\t${GOOSE_ENV_SET_BUT_EMPTY_VALUE-default}title5 text,\n);"
  },
  {
    "path": "internal/sqlparser/testdata/envsub/test02/03.up.golden.sql",
    "content": "CREATE OR REPLACE FUNCTION test_func()\nRETURNS void AS $$\nBEGIN\n\tRAISE NOTICE 'foo $GOOSE_ENV_NAME $GOOSE_ENV_NAME';\nEND;\n$$ LANGUAGE plpgsql;"
  },
  {
    "path": "internal/sqlparser/testdata/envsub/test02/input.sql",
    "content": "-- +goose Up\n\n-- +goose ENVSUB ON\nCREATE TABLE post (\n\tid int NOT NULL,\n\ttitle text,\n\t$GOOSE_ENV_NAME text,\n\t${GOOSE_ENV_NAME}title3 text,\n\t${ANOTHER_VAR:-default}title4 text,\n\t${GOOSE_ENV_SET_BUT_EMPTY_VALUE-default}title5 text,\n);\n-- +goose ENVSUB OFF\n\nCREATE TABLE post (\n\tid int NOT NULL,\n\ttitle text,\n\t$GOOSE_ENV_NAME text,\n\t${GOOSE_ENV_NAME}title3 text,\n\t${ANOTHER_VAR:-default}title4 text,\n\t${GOOSE_ENV_SET_BUT_EMPTY_VALUE-default}title5 text,\n);\n\n-- +goose StatementBegin\nCREATE OR REPLACE FUNCTION test_func()\nRETURNS void AS $$\n-- +goose ENVSUB ON\nBEGIN\n\tRAISE NOTICE '${GOOSE_ENV_NAME} \\$GOOSE_ENV_NAME \\$GOOSE_ENV_NAME';\nEND;\n-- +goose ENVSUB OFF\n$$ LANGUAGE plpgsql;\n-- +goose StatementEnd\n"
  },
  {
    "path": "internal/sqlparser/testdata/envsub/test03/01.up.golden.sql",
    "content": "CREATE TABLE post (\n\tid int NOT NULL,\n\ttitle text,\n\t$NAME text,\n\t${NAME}title3 text,\n\t${ANOTHER_VAR:-default}title4 text,\n\t${SET_BUT_EMPTY_VALUE-default}title5 text,\n);"
  },
  {
    "path": "internal/sqlparser/testdata/envsub/test03/input.sql",
    "content": "-- +goose Up\nCREATE TABLE post (\n\tid int NOT NULL,\n\ttitle text,\n\t$NAME text,\n\t${NAME}title3 text,\n\t${ANOTHER_VAR:-default}title4 text,\n\t${SET_BUT_EMPTY_VALUE-default}title5 text,\n);\n"
  },
  {
    "path": "internal/sqlparser/testdata/invalid/up/a.sql",
    "content": "-- +goose Up\nSELECT * FROM foo;\nSELECT * FROM bar\n-- +goose Down\nSELECT * FROM baz;\n"
  },
  {
    "path": "internal/sqlparser/testdata/invalid/up/b.sql",
    "content": "-- +goose Up\nSELECT * FROM bar\n-- +goose Down\nSELECT * FROM baz;\n"
  },
  {
    "path": "internal/sqlparser/testdata/invalid/up/c.sql",
    "content": "-- +goose Up\nSELECT * FROM bar\n-- +goose Down\n"
  },
  {
    "path": "internal/sqlparser/testdata/invalid/up/d.sql",
    "content": "-- +goose Up\nSELECT * FROM bar\n"
  },
  {
    "path": "internal/sqlparser/testdata/valid-txn/00001_create_users_table.sql",
    "content": "-- +goose Up\nCREATE TABLE users (\n    id int NOT NULL PRIMARY KEY,\n    username text,\n    name text,\n    surname text\n);\n\nINSERT INTO users VALUES\n(0, 'root', '', ''),\n(1, 'vojtechvitek', 'Vojtech', 'Vitek');\n\n-- +goose Down\nDROP TABLE users;\n"
  },
  {
    "path": "internal/sqlparser/testdata/valid-txn/00002_rename_root.sql",
    "content": "-- +goose Up\n-- +goose StatementBegin\nUPDATE users SET username='admin' WHERE username='root';\n-- +goose StatementEnd\n\n-- +goose Down\n-- +goose StatementBegin\nUPDATE users SET username='root' WHERE username='admin';\n-- +goose StatementEnd\n"
  },
  {
    "path": "internal/sqlparser/testdata/valid-txn/00003_no_transaction.sql",
    "content": "-- +goose NO TRANSACTION\n-- +goose Up\nCREATE TABLE post (\n    id int NOT NULL,\n    title text,\n    body text,\n    PRIMARY KEY(id)\n);\n\n-- +goose Down\nDROP TABLE post;\n"
  },
  {
    "path": "internal/sqlparser/testdata/valid-up/test01/01.up.golden.sql",
    "content": "CREATE TABLE emp (\n    empname text,\n    salary integer,\n    last_date timestamp,\n    last_user text\n);"
  },
  {
    "path": "internal/sqlparser/testdata/valid-up/test01/02.up.golden.sql",
    "content": "CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$\n    BEGIN\n        -- Check that empname and salary are given\n        IF NEW.empname IS NULL THEN\n            RAISE EXCEPTION 'empname cannot be null';\n        END IF;\n        IF NEW.salary IS NULL THEN\n            RAISE EXCEPTION '% cannot have null salary', NEW.empname;\n        END IF;\n\n        -- Who works for us when they must pay for it?\n        IF NEW.salary < 0 THEN\n            RAISE EXCEPTION '% cannot have a negative salary', NEW.empname;\n        END IF;\n\n        -- Remember who changed the payroll when\n        NEW.last_date := current_timestamp;\n        NEW.last_user := current_user;\n        RETURN NEW;\n    END;\n$emp_stamp$ LANGUAGE plpgsql;"
  },
  {
    "path": "internal/sqlparser/testdata/valid-up/test01/03.up.golden.sql",
    "content": "CREATE TRIGGER emp_stamp BEFORE INSERT OR UPDATE ON emp\n    FOR EACH ROW EXECUTE FUNCTION emp_stamp();"
  },
  {
    "path": "internal/sqlparser/testdata/valid-up/test01/input.sql",
    "content": "-- +goose UP\nCREATE TABLE emp (\n    empname text,\n    salary integer,\n    last_date timestamp,\n    last_user text\n);\n\n-- +goose statementBegin\nCREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$\n    BEGIN\n        -- Check that empname and salary are given\n        IF NEW.empname IS NULL THEN\n            RAISE EXCEPTION 'empname cannot be null';\n        END IF;\n        IF NEW.salary IS NULL THEN\n            RAISE EXCEPTION '% cannot have null salary', NEW.empname;\n        END IF;\n\n        -- Who works for us when they must pay for it?\n        IF NEW.salary < 0 THEN\n            RAISE EXCEPTION '% cannot have a negative salary', NEW.empname;\n        END IF;\n\n        -- Remember who changed the payroll when\n        NEW.last_date := current_timestamp;\n        NEW.last_user := current_user;\n        RETURN NEW;\n    END;\n$emp_stamp$ LANGUAGE plpgsql;\n-- +goose StatementEnd\n\nCREATE TRIGGER emp_stamp BEFORE INSERT OR UPDATE ON emp\n    FOR EACH ROW EXECUTE FUNCTION emp_stamp();\n\n-- +goose Down\n"
  },
  {
    "path": "internal/sqlparser/testdata/valid-up/test02/01.up.golden.sql",
    "content": "CREATE TABLE emp (\n    empname           text NOT NULL,\n    salary            integer\n);\n\nCREATE TABLE emp_audit(\n    operation         char(1)   NOT NULL,\n    stamp             timestamp NOT NULL,\n    userid            text      NOT NULL,\n    empname           text      NOT NULL,\n    salary integer\n);\n\nCREATE OR REPLACE FUNCTION process_emp_audit() RETURNS TRIGGER AS $emp_audit$\n    BEGIN\n        --\n        -- Create a row in emp_audit to reflect the operation performed on emp,\n        -- making use of the special variable TG_OP to work out the operation.\n        --\n        IF (TG_OP = 'DELETE') THEN\n            INSERT INTO emp_audit SELECT 'D', now(), user, OLD.*;\n        ELSIF (TG_OP = 'UPDATE') THEN\n            INSERT INTO emp_audit SELECT 'U', now(), user, NEW.*;\n        ELSIF (TG_OP = 'INSERT') THEN\n            INSERT INTO emp_audit SELECT 'I', now(), user, NEW.*;\n        END IF;\n        RETURN NULL; -- result is ignored since this is an AFTER trigger\n    END;\n$emp_audit$ LANGUAGE plpgsql;\n\nCREATE TRIGGER emp_audit\nAFTER INSERT OR UPDATE OR DELETE ON emp\n    FOR EACH ROW EXECUTE FUNCTION process_emp_audit();"
  },
  {
    "path": "internal/sqlparser/testdata/valid-up/test02/input.sql",
    "content": "-- +goose Up\n-- +goose StatementBegin\nCREATE TABLE emp (\n    empname           text NOT NULL,\n    salary            integer\n);\n\nCREATE TABLE emp_audit(\n    operation         char(1)   NOT NULL,\n    stamp             timestamp NOT NULL,\n    userid            text      NOT NULL,\n    empname           text      NOT NULL,\n    salary integer\n);\n\nCREATE OR REPLACE FUNCTION process_emp_audit() RETURNS TRIGGER AS $emp_audit$\n    BEGIN\n        --\n        -- Create a row in emp_audit to reflect the operation performed on emp,\n        -- making use of the special variable TG_OP to work out the operation.\n        --\n        IF (TG_OP = 'DELETE') THEN\n            INSERT INTO emp_audit SELECT 'D', now(), user, OLD.*;\n        ELSIF (TG_OP = 'UPDATE') THEN\n            INSERT INTO emp_audit SELECT 'U', now(), user, NEW.*;\n        ELSIF (TG_OP = 'INSERT') THEN\n            INSERT INTO emp_audit SELECT 'I', now(), user, NEW.*;\n        END IF;\n        RETURN NULL; -- result is ignored since this is an AFTER trigger\n    END;\n$emp_audit$ LANGUAGE plpgsql;\n\nCREATE TRIGGER emp_audit\nAFTER INSERT OR UPDATE OR DELETE ON emp\n    FOR EACH ROW EXECUTE FUNCTION process_emp_audit();\n-- +goose StatementEnd\n\n-- +goose Down\n"
  },
  {
    "path": "internal/sqlparser/testdata/valid-up/test03/01.up.golden.sql",
    "content": "CREATE FUNCTION cs_update_referrer_type_proc() RETURNS INTEGER AS '\nDECLARE\n    referrer_keys RECORD;  -- Declare a generic record to be used in a FOR\n    a_output varchar(4000);\nBEGIN \n    a_output := ''CREATE FUNCTION cs_find_referrer_type(varchar,varchar,varchar) \n                  RETURNS VARCHAR AS '''' \n                     DECLARE \n                         v_host ALIAS FOR $1; \n                         v_domain ALIAS FOR $2; \n                         v_url ALIAS FOR $3;\n                     BEGIN ''; \n\n-- \n-- Notice how we scan through the results of a query in a FOR loop\n-- using the FOR <record> construct.\n--\n\n    FOR referrer_keys IN SELECT * FROM cs_referrer_keys ORDER BY try_order LOOP\n        a_output := a_output || '' IF v_'' || referrer_keys.kind || '' LIKE '''''''''' \n                 || referrer_keys.key_string || '''''''''' THEN RETURN '''''' \n                 || referrer_keys.referrer_type || ''''''; END IF;''; \n    END LOOP; \n  \n    a_output := a_output || '' RETURN NULL; END; '''' LANGUAGE ''''plpgsql'''';''; \n \n    -- This works because we are not substituting any variables\n    -- Otherwise it would fail. Look at PERFORM for another way to run functions\n    \n    EXECUTE a_output; \nEND; \n' LANGUAGE 'plpgsql'; -- This comment WILL BE preserved.\n-- And so will this one."
  },
  {
    "path": "internal/sqlparser/testdata/valid-up/test03/input.sql",
    "content": "-- +goose Up\n-- +goose StatementBegin\nCREATE FUNCTION cs_update_referrer_type_proc() RETURNS INTEGER AS '\nDECLARE\n    referrer_keys RECORD;  -- Declare a generic record to be used in a FOR\n    a_output varchar(4000);\nBEGIN \n    a_output := ''CREATE FUNCTION cs_find_referrer_type(varchar,varchar,varchar) \n                  RETURNS VARCHAR AS '''' \n                     DECLARE \n                         v_host ALIAS FOR $1; \n                         v_domain ALIAS FOR $2; \n                         v_url ALIAS FOR $3;\n                     BEGIN ''; \n\n-- \n-- Notice how we scan through the results of a query in a FOR loop\n-- using the FOR <record> construct.\n--\n\n    FOR referrer_keys IN SELECT * FROM cs_referrer_keys ORDER BY try_order LOOP\n        a_output := a_output || '' IF v_'' || referrer_keys.kind || '' LIKE '''''''''' \n                 || referrer_keys.key_string || '''''''''' THEN RETURN '''''' \n                 || referrer_keys.referrer_type || ''''''; END IF;''; \n    END LOOP; \n  \n    a_output := a_output || '' RETURN NULL; END; '''' LANGUAGE ''''plpgsql'''';''; \n \n    -- This works because we are not substituting any variables\n    -- Otherwise it would fail. Look at PERFORM for another way to run functions\n    \n    EXECUTE a_output; \nEND; \n' LANGUAGE 'plpgsql'; -- This comment WILL BE preserved.\n-- And so will this one.\n-- +goose StatementEnd\n\n-- +goose Down\n"
  },
  {
    "path": "internal/sqlparser/testdata/valid-up/test04/01.up.golden.sql",
    "content": "CREATE TABLE ssh_keys (\n    id integer NOT NULL,\n    \"publicKey\" text\n);"
  },
  {
    "path": "internal/sqlparser/testdata/valid-up/test04/02.up.golden.sql",
    "content": "INSERT INTO ssh_keys (id, \"publicKey\") VALUES (1, '-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAqAo9QORIXMPMa/qv8908Z2sH2+Xa/wITYTJQ2ojTZlgsQiQf85ifw3dgvVZK\nM7Zifl2NyVVCPb0hELr2JJla1u/1CgiuqDpcjP2cCu2YxB/JGyCvcon+3tETUz3Ri9NGzHCZ\nfkuWRZjkUvy7nfPLjzM+t6SEvY4lbn3ihLPumZjwgvuCY3vDZY8V1/NMoP8MKATGR+S7D7gv\nI6KD9jkiSsTJMiotb/dRkXE3bG0nmjchhhLzMG551G8IZEpWBHDqEisCIl8yCd9YZV69BZTu\nL48zPl/CFvA+KJJ6LklxfwWeVDQ+ve2OIW0B1uLhR/MsoYbDQztbgIayg6ieMO/KlQIDAQAB\n-----END RSA PUBLIC KEY-----\n');"
  },
  {
    "path": "internal/sqlparser/testdata/valid-up/test04/03.up.golden.sql",
    "content": "INSERT INTO ssh_keys (id, \"publicKey\") VALUES (2, '-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABsQAAAAdzc2gtZH\nNzAAAAgQDKYlGRv+Ul4WYKN3F83zchQC24ZV17EJCKwakASf//u2p1+K4iuTQbPgcUWpET\nw4HkCRlOVwYkM4ZL2QncUDyEX3o0UDEgrnhWhLJJR2yCMSsqTNffME3X7YdP2LcE0OM9ZP\n9vb5+TwLr6c4edwlu3QYc4VStWSstdR9DD7vun9QAAABUA+DRjW9u+5IEPHyx+FhEoKsRm\n8aEAAACAU2auArSDuTVbJBE/oP6x+4vrud2qFtALsuFaLPfqN5gVBofRJlIOpQobyi022R\n6SPtX/DEbWkVuHfiyxkjb0Gb0obBzGM+RNqjOF6OE9sh7OuBfLaWW44OZasg1VXzkRcoWv\n7jClfKYi2Q/LxHGhZoqy1uYlHPYP5CmCpiELrUMAAACAfp0KpTVyYQjO2nLPuBhnsepfxQ\nkT+FDqDBp1rZfB4uKx4q466Aq0jeev1OeQEYZpj3+q4b2XX54zXDwvJLuiD9WSmC7jvT0+\nEUmF55PHW4inloG9pMUzeQnx3k8WDcRJbcAMalpoCCsb0jEPIiyBGBtQu0gOoLL+N+G2Cl\nU+/FEAAAHgOKSlwjikpcIAAAAHc3NoLWRzcwAAAIEAymJRkb/lJeFmCjdxfN83IUAtuGVd\nexCQisGpAEn//7tqdfiuIrk0Gz4HFFqRE8OB5AkZTlcGJDOGS9kJ3FA8hF96NFAxIK54Vo\nSySUdsgjErKkzX3zBN1+2HT9i3BNDjPWT/b2+fk8C6+nOHncJbt0GHOFUrVkrLXUfQw+77\np/UAAAAVAPg0Y1vbvuSBDx8sfhYRKCrEZvGhAAAAgFNmrgK0g7k1WyQRP6D+sfuL67ndqh\nbQC7LhWiz36jeYFQaH0SZSDqUKG8otNtkekj7V/wxG1pFbh34ssZI29Bm9KGwcxjPkTaoz\nhejhPbIezrgXy2lluODmWrINVV85EXKFr+4wpXymItkPy8RxoWaKstbmJRz2D+QpgqYhC6\n1DAAAAgH6dCqU1cmEIztpyz7gYZ7HqX8UJE/hQ6gwada2XweLiseKuOugKtI3nr9TnkBGG\naY9/quG9l1+eM1w8LyS7og/Vkpgu4709PhFJheeTx1uIp5aBvaTFM3kJ8d5PFg3ESW3ADG\npaaAgrG9IxDyIsgRgbULtIDqCy/jfhtgpVPvxRAAAAFQCPXzpVtY5yJTN1zBo9pTGeg+f3\nEgAAAAZub25hbWUBAgME\n-----END OPENSSH PRIVATE KEY-----\n');"
  },
  {
    "path": "internal/sqlparser/testdata/valid-up/test04/input.sql",
    "content": "-- +goose Up\n\nCREATE TABLE ssh_keys (\n    id integer NOT NULL,\n    \"publicKey\" text\n);\n\n-- +goose StatementBegin\nINSERT INTO ssh_keys (id, \"publicKey\") VALUES (1, '-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAqAo9QORIXMPMa/qv8908Z2sH2+Xa/wITYTJQ2ojTZlgsQiQf85ifw3dgvVZK\nM7Zifl2NyVVCPb0hELr2JJla1u/1CgiuqDpcjP2cCu2YxB/JGyCvcon+3tETUz3Ri9NGzHCZ\nfkuWRZjkUvy7nfPLjzM+t6SEvY4lbn3ihLPumZjwgvuCY3vDZY8V1/NMoP8MKATGR+S7D7gv\nI6KD9jkiSsTJMiotb/dRkXE3bG0nmjchhhLzMG551G8IZEpWBHDqEisCIl8yCd9YZV69BZTu\nL48zPl/CFvA+KJJ6LklxfwWeVDQ+ve2OIW0B1uLhR/MsoYbDQztbgIayg6ieMO/KlQIDAQAB\n-----END RSA PUBLIC KEY-----\n');\n-- +goose StatementEnd\n\nINSERT INTO ssh_keys (id, \"publicKey\") VALUES (2, '-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABsQAAAAdzc2gtZH\nNzAAAAgQDKYlGRv+Ul4WYKN3F83zchQC24ZV17EJCKwakASf//u2p1+K4iuTQbPgcUWpET\nw4HkCRlOVwYkM4ZL2QncUDyEX3o0UDEgrnhWhLJJR2yCMSsqTNffME3X7YdP2LcE0OM9ZP\n9vb5+TwLr6c4edwlu3QYc4VStWSstdR9DD7vun9QAAABUA+DRjW9u+5IEPHyx+FhEoKsRm\n8aEAAACAU2auArSDuTVbJBE/oP6x+4vrud2qFtALsuFaLPfqN5gVBofRJlIOpQobyi022R\n6SPtX/DEbWkVuHfiyxkjb0Gb0obBzGM+RNqjOF6OE9sh7OuBfLaWW44OZasg1VXzkRcoWv\n7jClfKYi2Q/LxHGhZoqy1uYlHPYP5CmCpiELrUMAAACAfp0KpTVyYQjO2nLPuBhnsepfxQ\nkT+FDqDBp1rZfB4uKx4q466Aq0jeev1OeQEYZpj3+q4b2XX54zXDwvJLuiD9WSmC7jvT0+\nEUmF55PHW4inloG9pMUzeQnx3k8WDcRJbcAMalpoCCsb0jEPIiyBGBtQu0gOoLL+N+G2Cl\nU+/FEAAAHgOKSlwjikpcIAAAAHc3NoLWRzcwAAAIEAymJRkb/lJeFmCjdxfN83IUAtuGVd\nexCQisGpAEn//7tqdfiuIrk0Gz4HFFqRE8OB5AkZTlcGJDOGS9kJ3FA8hF96NFAxIK54Vo\nSySUdsgjErKkzX3zBN1+2HT9i3BNDjPWT/b2+fk8C6+nOHncJbt0GHOFUrVkrLXUfQw+77\np/UAAAAVAPg0Y1vbvuSBDx8sfhYRKCrEZvGhAAAAgFNmrgK0g7k1WyQRP6D+sfuL67ndqh\nbQC7LhWiz36jeYFQaH0SZSDqUKG8otNtkekj7V/wxG1pFbh34ssZI29Bm9KGwcxjPkTaoz\nhejhPbIezrgXy2lluODmWrINVV85EXKFr+4wpXymItkPy8RxoWaKstbmJRz2D+QpgqYhC6\n1DAAAAgH6dCqU1cmEIztpyz7gYZ7HqX8UJE/hQ6gwada2XweLiseKuOugKtI3nr9TnkBGG\naY9/quG9l1+eM1w8LyS7og/Vkpgu4709PhFJheeTx1uIp5aBvaTFM3kJ8d5PFg3ESW3ADG\npaaAgrG9IxDyIsgRgbULtIDqCy/jfhtgpVPvxRAAAAFQCPXzpVtY5yJTN1zBo9pTGeg+f3\nEgAAAAZub25hbWUBAgME\n-----END OPENSSH PRIVATE KEY-----\n');\n\n-- +goose Down\n"
  },
  {
    "path": "internal/sqlparser/testdata/valid-up/test05/01.up.golden.sql",
    "content": "CREATE TABLE ssh_keys (\n    id integer NOT NULL,\n    \"publicKey\" text\n-- insert comment there\n);"
  },
  {
    "path": "internal/sqlparser/testdata/valid-up/test05/02.up.golden.sql",
    "content": "CREATE TABLE ssh_keys_backup (\n    id integer NOT NULL,\n    -- insert comment here\n    \"publicKey\" text\n    -- insert comment there\n);"
  },
  {
    "path": "internal/sqlparser/testdata/valid-up/test05/input.sql",
    "content": "-- +goose Up\n\nCREATE TABLE ssh_keys (\n    id integer NOT NULL,\n    \"publicKey\" text\n-- insert comment there\n);\n-- insert comment there\n\n-- This is a dangling comment\n-- Another comment\n-- Foo comment\n\nCREATE TABLE ssh_keys_backup (\n    id integer NOT NULL,\n    -- insert comment here\n    \"publicKey\" text\n    -- insert comment there\n);\n\n\n-- +goose Down\n"
  },
  {
    "path": "internal/sqlparser/testdata/valid-up/test06/01.up.golden.sql",
    "content": "CREATE TABLE article (\n    id text,\n            content text);"
  },
  {
    "path": "internal/sqlparser/testdata/valid-up/test06/02.up.golden.sql",
    "content": "INSERT INTO article (id, content) VALUES ('id_0001',  E'# My markdown doc\n\nfirst paragraph\n\nsecond paragraph');"
  },
  {
    "path": "internal/sqlparser/testdata/valid-up/test06/03.up.golden.sql",
    "content": "INSERT INTO article (id, content) VALUES ('id_0002',  E'# My second \n\nmarkdown doc\n\nfirst paragraph\n\n-- with a comment\n    -- with an indent comment\n\nsecond paragraph');"
  },
  {
    "path": "internal/sqlparser/testdata/valid-up/test06/04.up.golden.sql",
    "content": "CREATE FUNCTION do_something(sql TEXT) RETURNS INTEGER AS $$\nBEGIN\n  -- initiate technology\n  PERFORM something_or_other(sql);\n\n  -- increase technology\n  PERFORM some_other_thing(sql);\n\n  -- technology was successful\n  RETURN 1;\nEND;\n$$ LANGUAGE plpgsql;\n\n-- 3 this comment WILL BE preserved\n  -- 4 this comment WILL BE preserved"
  },
  {
    "path": "internal/sqlparser/testdata/valid-up/test06/05.up.golden.sql",
    "content": "INSERT INTO post (id, title, body)\nVALUES ('id_01', 'my_title', '\nthis is an insert statement including empty lines.\n\nempty (blank) lines can be meaningful.\n\nleave the lines to keep the text syntax.\n');"
  },
  {
    "path": "internal/sqlparser/testdata/valid-up/test06/input.sql",
    "content": "-- +goose Up\n\nCREATE TABLE article (\n    id text,\n            content text);\n\nINSERT INTO article (id, content) VALUES ('id_0001',  E'# My markdown doc\n\nfirst paragraph\n\nsecond paragraph');\n\nINSERT INTO article (id, content) VALUES ('id_0002',  E'# My second \n\nmarkdown doc\n\nfirst paragraph\n\n-- with a comment\n    -- with an indent comment\n\nsecond paragraph');\n\n-- +goose StatementBegin\n\n\n\n\n-- 1 this comment will NOT be preserved\n  -- 2 this comment will NOT be preserved\n\n\nCREATE FUNCTION do_something(sql TEXT) RETURNS INTEGER AS $$\nBEGIN\n  -- initiate technology\n  PERFORM something_or_other(sql);\n\n  -- increase technology\n  PERFORM some_other_thing(sql);\n\n  -- technology was successful\n  RETURN 1;\nEND;\n$$ LANGUAGE plpgsql;\n\n-- 3 this comment WILL BE preserved\n  -- 4 this comment WILL BE preserved\n\n\n-- +goose StatementEnd\n\nINSERT INTO post (id, title, body)\nVALUES ('id_01', 'my_title', '\nthis is an insert statement including empty lines.\n\nempty (blank) lines can be meaningful.\n\nleave the lines to keep the text syntax.\n');\n\n-- +goose Down\nTRUNCATE TABLE post; \n"
  },
  {
    "path": "internal/sqlparser/testdata/valid-up/test07/01.up.golden.sql",
    "content": "CREATE INDEX ON public.users (user_id);"
  },
  {
    "path": "internal/sqlparser/testdata/valid-up/test07/input.sql",
    "content": "    \n-- +goose Up\n-- +goose StatementBegin\nCREATE INDEX ON public.users (user_id);\n-- +goose StatementEnd\n\n-- +goose Down\n-- +goose StatementBegin\nDROP INDEX IF EXISTS users_user_id_idx;\n-- +goose StatementEnd\n"
  },
  {
    "path": "internal/sqlparser/testdata/valid-up/test08/01.up.golden.sql",
    "content": "CREATE TABLE `table_a` (\n    `column_1` DATETIME DEFAULT NOW(),\n    `column_2` DATETIME DEFAULT NOW(),\n    `column_3` DATETIME DEFAULT NOW()\n) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;"
  },
  {
    "path": "internal/sqlparser/testdata/valid-up/test08/02.up.golden.sql",
    "content": "CREATE TABLE `table_b` (\n    `column_1` DATETIME DEFAULT NOW(),\n    `column_2` DATETIME DEFAULT NOW(),\n    `column_3` DATETIME DEFAULT NOW()\n) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;"
  },
  {
    "path": "internal/sqlparser/testdata/valid-up/test08/03.up.golden.sql",
    "content": "CREATE TABLE `table_c` (\n    `column_1` DATETIME DEFAULT NOW(),\n    `column_2` DATETIME DEFAULT NOW(),\n    `column_3` DATETIME DEFAULT NOW()\n) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;"
  },
  {
    "path": "internal/sqlparser/testdata/valid-up/test08/04.up.golden.sql",
    "content": "/*!80031 ALTER TABLE `table_a` MODIFY `column_1` TEXT NOT NULL */;"
  },
  {
    "path": "internal/sqlparser/testdata/valid-up/test08/05.up.golden.sql",
    "content": "/*!80031 ALTER TABLE `table_b` MODIFY `column_2` TEXT NOT NULL */;"
  },
  {
    "path": "internal/sqlparser/testdata/valid-up/test08/06.up.golden.sql",
    "content": "/*!80033 ALTER TABLE `table_c` MODIFY `column_3` TEXT NOT NULL */;"
  },
  {
    "path": "internal/sqlparser/testdata/valid-up/test08/input.sql",
    "content": "-- +goose Up\n\nCREATE TABLE `table_a` (\n    `column_1` DATETIME DEFAULT NOW(),\n    `column_2` DATETIME DEFAULT NOW(),\n    `column_3` DATETIME DEFAULT NOW()\n) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;\n\nCREATE TABLE `table_b` (\n    `column_1` DATETIME DEFAULT NOW(),\n    `column_2` DATETIME DEFAULT NOW(),\n    `column_3` DATETIME DEFAULT NOW()\n) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;\n\nCREATE TABLE `table_c` (\n    `column_1` DATETIME DEFAULT NOW(),\n    `column_2` DATETIME DEFAULT NOW(),\n    `column_3` DATETIME DEFAULT NOW()\n) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;\n\n/*!80031 ALTER TABLE `table_a` MODIFY `column_1` TEXT NOT NULL */;\n/*!80031 ALTER TABLE `table_b` MODIFY `column_2` TEXT NOT NULL */;\n/*!80033 ALTER TABLE `table_c` MODIFY `column_3` TEXT NOT NULL */;\n"
  },
  {
    "path": "internal/sqlparser/testdata/valid-up/test09/01.up.golden.sql",
    "content": "create table t ( id int );\nupdate rows set value = now() -- missing semicolon. valid statement because wrapped in goose annotation, but will fail when executed."
  },
  {
    "path": "internal/sqlparser/testdata/valid-up/test09/input.sql",
    "content": "-- +goose Up\n-- +goose StatementBegin\ncreate table t ( id int );\nupdate rows set value = now() -- missing semicolon. valid statement because wrapped in goose annotation, but will fail when executed.\n-- +goose StatementEnd\n\n-- +goose Down\nDROP TABLE IF EXISTS t;\n"
  },
  {
    "path": "internal/testing/go.mod",
    "content": "module github.com/pressly/goose/v3/internal/testing\n\ngo 1.25.0\n\nrequire (\n\tcloud.google.com/go/spanner v1.88.0\n\tgithub.com/ClickHouse/clickhouse-go/v2 v2.43.0\n\tgithub.com/go-sql-driver/mysql v1.9.3\n\tgithub.com/googleapis/go-sql-spanner v1.24.0\n\tgithub.com/jackc/pgx/v5 v5.8.0\n\tgithub.com/ory/dockertest/v3 v3.12.0\n\tgithub.com/pressly/goose/v3 v3.26.0\n\tgithub.com/stretchr/testify v1.11.1\n\tgithub.com/tursodatabase/libsql-client-go v0.0.0-20251219100830-236aa1ff8acc\n\tgithub.com/ydb-platform/ydb-go-sdk/v3 v3.127.0\n\tgolang.org/x/sync v0.19.0\n)\n\nrequire (\n\tcel.dev/expr v0.25.1 // indirect\n\tcloud.google.com/go v0.123.0 // indirect\n\tcloud.google.com/go/auth v0.18.2 // indirect\n\tcloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect\n\tcloud.google.com/go/compute/metadata v0.9.0 // indirect\n\tcloud.google.com/go/iam v1.5.3 // indirect\n\tcloud.google.com/go/longrunning v0.8.0 // indirect\n\tcloud.google.com/go/monitoring v1.24.3 // indirect\n\tdario.cat/mergo v1.0.2 // indirect\n\tfilippo.io/edwards25519 v1.2.0 // indirect\n\tgithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect\n\tgithub.com/ClickHouse/ch-go v0.71.0 // indirect\n\tgithub.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.6.0 // indirect\n\tgithub.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect\n\tgithub.com/Microsoft/go-winio v0.6.2 // indirect\n\tgithub.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect\n\tgithub.com/andybalholm/brotli v1.2.0 // indirect\n\tgithub.com/antlr4-go/antlr/v4 v4.13.1 // indirect\n\tgithub.com/cenkalti/backoff/v4 v4.3.0 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect\n\tgithub.com/coder/websocket v1.8.14 // indirect\n\tgithub.com/containerd/continuity v0.4.5 // indirect\n\tgithub.com/containerd/errdefs v1.0.0 // indirect\n\tgithub.com/containerd/errdefs/pkg v0.3.0 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/distribution/reference v0.6.0 // indirect\n\tgithub.com/docker/cli v29.2.1+incompatible // indirect\n\tgithub.com/docker/go-connections v0.6.0 // indirect\n\tgithub.com/docker/go-units v0.5.0 // indirect\n\tgithub.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect\n\tgithub.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/go-faster/city v1.0.1 // indirect\n\tgithub.com/go-faster/errors v0.7.1 // indirect\n\tgithub.com/go-jose/go-jose/v4 v4.1.3 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/go-viper/mapstructure/v2 v2.5.0 // indirect\n\tgithub.com/golang-jwt/jwt/v4 v4.5.2 // indirect\n\tgithub.com/google/s2a-go v0.1.9 // indirect\n\tgithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/googleapis/enterprise-certificate-proxy v0.3.12 // indirect\n\tgithub.com/googleapis/gax-go/v2 v2.17.0 // indirect\n\tgithub.com/hashicorp/golang-lru/v2 v2.0.7 // indirect\n\tgithub.com/jackc/pgpassfile v1.0.0 // indirect\n\tgithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect\n\tgithub.com/jackc/puddle/v2 v2.2.2 // indirect\n\tgithub.com/jonboulle/clockwork v0.5.0 // indirect\n\tgithub.com/klauspost/compress v1.18.4 // indirect\n\tgithub.com/mfridman/interpolate v0.0.2 // indirect\n\tgithub.com/moby/docker-image-spec v1.3.1 // indirect\n\tgithub.com/moby/moby/api v1.53.0 // indirect\n\tgithub.com/moby/moby/client v0.2.2 // indirect\n\tgithub.com/moby/sys/user v0.4.0 // indirect\n\tgithub.com/moby/term v0.5.2 // indirect\n\tgithub.com/opencontainers/go-digest v1.0.0 // indirect\n\tgithub.com/opencontainers/image-spec v1.1.1 // indirect\n\tgithub.com/opencontainers/runc v1.3.3 // indirect\n\tgithub.com/paulmach/orb v0.12.0 // indirect\n\tgithub.com/pierrec/lz4/v4 v4.1.25 // indirect\n\tgithub.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/segmentio/asm v1.2.1 // indirect\n\tgithub.com/sethvargo/go-retry v0.3.0 // indirect\n\tgithub.com/shopspring/decimal v1.4.0 // indirect\n\tgithub.com/sirupsen/logrus v1.9.4 // indirect\n\tgithub.com/spiffe/go-spiffe/v2 v2.6.0 // indirect\n\tgithub.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect\n\tgithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect\n\tgithub.com/xeipuuv/gojsonschema v1.2.0 // indirect\n\tgithub.com/ydb-platform/ydb-go-genproto v0.0.0-20260128080146-c4ed16b24b37 // indirect\n\tgo.opencensus.io v0.24.0 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.2.1 // indirect\n\tgo.opentelemetry.io/contrib/detectors/gcp v1.40.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect\n\tgo.opentelemetry.io/otel v1.40.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.40.0 // indirect\n\tgo.opentelemetry.io/otel/sdk v1.40.0 // indirect\n\tgo.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.40.0 // indirect\n\tgo.uber.org/multierr v1.11.0 // indirect\n\tgo.yaml.in/yaml/v3 v3.0.4 // indirect\n\tgolang.org/x/crypto v0.48.0 // indirect\n\tgolang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect\n\tgolang.org/x/net v0.50.0 // indirect\n\tgolang.org/x/oauth2 v0.35.0 // indirect\n\tgolang.org/x/sys v0.41.0 // indirect\n\tgolang.org/x/text v0.34.0 // indirect\n\tgolang.org/x/time v0.14.0 // indirect\n\tgoogle.golang.org/api v0.267.0 // indirect\n\tgoogle.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect\n\tgoogle.golang.org/grpc v1.79.3 // indirect\n\tgoogle.golang.org/protobuf v1.36.11 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "internal/testing/go.sum",
    "content": "cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=\ncel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=\ncloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=\ncloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=\ncloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=\ncloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=\ncloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=\ncloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=\ncloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=\ncloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=\ncloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=\ncloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=\ncloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=\ncloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=\ncloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=\ncloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=\ncloud.google.com/go/spanner v1.88.0 h1:HS+5TuEYZOVOXj9K+0EtrbTw7bKBLrMe3vgGsbnehmU=\ncloud.google.com/go/spanner v1.88.0/go.mod h1:MzulBwuuYwQUVdkZXBBFapmXee3N+sQrj2T/yup6uEE=\ndario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=\ndario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=\nfilippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=\nfilippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=\ngithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=\ngithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/ClickHouse/ch-go v0.71.0 h1:bUdZ/EZj/LcVHsMqaRUP2holqygrPWQKeMjc6nZoyRM=\ngithub.com/ClickHouse/ch-go v0.71.0/go.mod h1:NwbNc+7jaqfY58dmdDUbG4Jl22vThgx1cYjBw0vtgXw=\ngithub.com/ClickHouse/clickhouse-go/v2 v2.43.0 h1:fUR05TrF1GyvLDa/mAQjkx7KbgwdLRffs2n9O3WobtE=\ngithub.com/ClickHouse/clickhouse-go/v2 v2.43.0/go.mod h1:o6jf7JM/zveWC/PP277BLxjHy5KjnGX/jfljhM4s34g=\ngithub.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.6.0 h1:BzsL0qE7LvtTEtXG7Dt5NS1EP0CQwI21HZfj9aGghhw=\ngithub.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.6.0/go.mod h1:I7kE2kM3qCr9QPT4cU4cCFYkEpVyVr16YOGUHzy+nR0=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=\ngithub.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=\ngithub.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=\ngithub.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=\ngithub.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=\ngithub.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=\ngithub.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=\ngithub.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=\ngithub.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=\ngithub.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=\ngithub.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=\ngithub.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=\ngithub.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=\ngithub.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=\ngithub.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=\ngithub.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=\ngithub.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=\ngithub.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=\ngithub.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=\ngithub.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=\ngithub.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=\ngithub.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=\ngithub.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=\ngithub.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=\ngithub.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=\ngithub.com/docker/cli v29.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg=\ngithub.com/docker/cli v29.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=\ngithub.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=\ngithub.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=\ngithub.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=\ngithub.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=\ngithub.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=\ngithub.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=\ngithub.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=\ngithub.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=\ngithub.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=\ngithub.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=\ngithub.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=\ngithub.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=\ngithub.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=\ngithub.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=\ngithub.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=\ngithub.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=\ngithub.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=\ngithub.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=\ngithub.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=\ngithub.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=\ngithub.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=\ngithub.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=\ngithub.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=\ngithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=\ngithub.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=\ngithub.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=\ngithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=\ngithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.12 h1:Fg+zsqzYEs1ZnvmcztTYxhgCBsx3eEhEwQ1W/lHq/sQ=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.12/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=\ngithub.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=\ngithub.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=\ngithub.com/googleapis/go-sql-spanner v1.24.0 h1:K36siIx0KEka6Xttu2Rti9cBGK7mYQIoo6gUrW87uTU=\ngithub.com/googleapis/go-sql-spanner v1.24.0/go.mod h1:ltBracyoOyIYJjTQcDxuYmJDfPgknsQMs63liLSF4AA=\ngithub.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=\ngithub.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=\ngithub.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=\ngithub.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=\ngithub.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=\ngithub.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=\ngithub.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=\ngithub.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=\ngithub.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=\ngithub.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=\ngithub.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=\ngithub.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=\ngithub.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=\ngithub.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=\ngithub.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=\ngithub.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=\ngithub.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=\ngithub.com/moby/moby/api v1.53.0 h1:PihqG1ncw4W+8mZs69jlwGXdaYBeb5brF6BL7mPIS/w=\ngithub.com/moby/moby/api v1.53.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=\ngithub.com/moby/moby/client v0.2.2 h1:Pt4hRMCAIlyjL3cr8M5TrXCwKzguebPAc2do2ur7dEM=\ngithub.com/moby/moby/client v0.2.2/go.mod h1:2EkIPVNCqR05CMIzL1mfA07t0HvVUUOl85pasRz/GmQ=\ngithub.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=\ngithub.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=\ngithub.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=\ngithub.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=\ngithub.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=\ngithub.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=\ngithub.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=\ngithub.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=\ngithub.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=\ngithub.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=\ngithub.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=\ngithub.com/opencontainers/runc v1.3.3 h1:qlmBbbhu+yY0QM7jqfuat7M1H3/iXjju3VkP9lkFQr4=\ngithub.com/opencontainers/runc v1.3.3/go.mod h1:D7rL72gfWxVs9cJ2/AayxB0Hlvn9g0gaF1R7uunumSI=\ngithub.com/ory/dockertest/v3 v3.12.0 h1:3oV9d0sDzlSQfHtIaB5k6ghUCVMVLpAY8hwrqoCyRCw=\ngithub.com/ory/dockertest/v3 v3.12.0/go.mod h1:aKNDTva3cp8dwOWwb9cWuX84aH5akkxXRvO7KCwWVjE=\ngithub.com/paulmach/orb v0.12.0 h1:z+zOwjmG3MyEEqzv92UN49Lg1JFYx0L9GpGKNVDKk1s=\ngithub.com/paulmach/orb v0.12.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=\ngithub.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=\ngithub.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0=\ngithub.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=\ngithub.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pressly/goose/v3 v3.26.0 h1:KJakav68jdH0WDvoAcj8+n61WqOIaPGgH0bJWS6jpmM=\ngithub.com/pressly/goose/v3 v3.26.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/rekby/fixenv v0.6.1 h1:jUFiSPpajT4WY2cYuc++7Y1zWrnCxnovGCIX72PZniM=\ngithub.com/rekby/fixenv v0.6.1/go.mod h1:/b5LRc06BYJtslRtHKxsPWFT/ySpHV+rWvzTg+XWk4c=\ngithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=\ngithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=\ngithub.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=\ngithub.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=\ngithub.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=\ngithub.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=\ngithub.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=\ngithub.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=\ngithub.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=\ngithub.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=\ngithub.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=\ngithub.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=\ngithub.com/tursodatabase/libsql-client-go v0.0.0-20251219100830-236aa1ff8acc h1:lzi/5fg2EfinRlh3v//YyIhnc4tY7BTqazQGwb1ar+0=\ngithub.com/tursodatabase/libsql-client-go v0.0.0-20251219100830-236aa1ff8acc/go.mod h1:08inkKyguB6CGGssc/JzhmQWwBgFQBgjlYFjxjRh7nU=\ngithub.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=\ngithub.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=\ngithub.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=\ngithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=\ngithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=\ngithub.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=\ngithub.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=\ngithub.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=\ngithub.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=\ngithub.com/ydb-platform/ydb-go-genproto v0.0.0-20260128080146-c4ed16b24b37 h1:kUXMT/fM/DpDT66WQgRUf3I8VOAWjypkMf52W5PChwA=\ngithub.com/ydb-platform/ydb-go-genproto v0.0.0-20260128080146-c4ed16b24b37/go.mod h1:Er+FePu1dNUieD+XTMDduGpQuCPssK5Q4BjF+IIXJ3I=\ngithub.com/ydb-platform/ydb-go-sdk/v3 v3.127.0 h1:OfHS9ZkZgCy6y/CJ9N8123DXrgaY2BPxWsQiQ8e3wC8=\ngithub.com/ydb-platform/ydb-go-sdk/v3 v3.127.0/go.mod h1:stS1mQYjbJvwwYaYzKyFY9eMiuVXWWXQA6T+SpOLg9c=\ngithub.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngo.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=\ngo.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=\ngo.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/contrib/detectors/gcp v1.40.0 h1:Awaf8gmW99tZTOWqkLCOl6aw1/rxAWVlHsHIZ3fT2sA=\ngo.opentelemetry.io/contrib/detectors/gcp v1.40.0/go.mod h1:99OY9ZCqyLkzJLTh5XhECpLRSxcZl+ZDKBEO+jMBFR4=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 h1:XmiuHzgJt067+a6kwyAzkhXooYVv3/TOw9cM2VfJgUM=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0/go.mod h1:KDgtbWKTQs4bM+VPUr6WlL9m/WXcmkCcBlIzqxPGzmI=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=\ngo.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=\ngo.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=\ngo.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=\ngo.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=\ngo.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=\ngo.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=\ngo.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=\ngo.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=\ngo.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=\ngo.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=\ngo.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngo.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=\ngo.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=\ngo.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=\ngo.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=\ngolang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=\ngolang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=\ngolang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=\ngolang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=\ngolang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=\ngolang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=\ngolang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=\ngolang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=\ngolang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=\ngolang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=\ngonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=\ngoogle.golang.org/api v0.267.0 h1:w+vfWPMPYeRs8qH1aYYsFX68jMls5acWl/jocfLomwE=\ngoogle.golang.org/api v0.267.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d h1:vsOm753cOAMkt76efriTCDKjpCbK18XGHMJHo0JUKhc=\ngoogle.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:0oz9d7g9QLSdv9/lgbIjowW1JoxMbxmBVNe8i6tORJI=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d h1:EocjzKLywydp5uZ5tJ79iP6Q0UjDnyiHkGRWxuPBP8s=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:48U2I+QQUYhsFrg2SY6r+nJzeOtjey7j//WBESw+qyQ=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=\ngoogle.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=\ngoogle.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=\ngoogle.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=\ngoogle.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=\ngoogle.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=\ngotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nmodernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=\nmodernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=\nmodernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=\nmodernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=\nmodernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=\nmodernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=\nmodernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=\nmodernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=\npgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=\npgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=\n"
  },
  {
    "path": "internal/testing/integration/README.md",
    "content": "# Integration tests\n\nThis directory contains integration tests for the [pressly/goose/v3][goose_module] Go module. An\nintegration test is a test that runs against a real database (docker container) and exercises the\nsame driver used by the CLI.\n\n## Why is this a separate module?\n\nThere are separate `go.mod` and `go.sum` files in this directory to allow for the use of different\ndependencies. We leverage [multi-module workspaces](https://go.dev/doc/tutorial/workspaces) to glue\nthings together.\n\nNamely, we want to avoid dependencies on docker, sql drivers, and other dependencies **that are**\nnot necessary for the core functionality of the goose library.\n\n## Overview\n\nThere are separate migration files for each database that we support, see the [migrations\ndirectory][migrations_dir]. Databases typically have different SQL syntax and features, so the\nmigration files are different.\n\nA good set of migrations should be representative of the types of migrations users will write\ntypically write. This should include:\n\n- Creating and dropping tables\n- Adding and removing columns\n- Creating and dropping indexes\n- Inserting and deleting data\n- Complex SQL statements that require special handling with `StatementBegin` and `StatementEnd`\n  annotations\n- Statements that must run outside a transaction, annotated with `-- +goose NO TRANSACTION`\n\nThere is a common test function that applies migrations up, down and then up again.\n\nThe gold standard is the PostgreSQL migration files. We try to make other migration files as close\nto the PostgreSQL files as possible, but this is not always possible or desirable.\n\nLastly, some tests will assert for database state after migrations are applied.\n\nTo add a new `.sql` file, you can use the following command:\n\n```\ngoose -s -dir testdata/migrations/<database_name> create <filename> sql\n```\n\n- Update the database name (e.g. `postgres`)\n- Update the filename name (e.g. `b`) as needed\n\n## Limitation\n\nNote, the integration tests are not exhaustive.\n\nThey are meantto ensure that the goose library works with the various databases that we support and\nthe chosen drivers. We do not test every possible combination of operations, nor do we test every\npossible edge case. We rely on the unit tests in the goose package to cover library-specific logic.\n\n[goose_module]: https://pkg.go.dev/github.com/pressly/goose/v3\n[migrations_dir]: ./testdata/migrations\n"
  },
  {
    "path": "internal/testing/integration/database_test.go",
    "content": "package integration\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/pressly/goose/v3\"\n\t\"github.com/pressly/goose/v3/database\"\n\t\"github.com/pressly/goose/v3/internal/testing/testdb\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPostgres(t *testing.T) {\n\tt.Parallel()\n\n\tdb, cleanup, err := testdb.NewPostgres()\n\trequire.NoError(t, err)\n\tt.Cleanup(cleanup)\n\trequire.NoError(t, db.Ping())\n\n\ttestDatabase(t, database.DialectPostgres, db, \"testdata/migrations/postgres\")\n}\n\nfunc TestSpanner(t *testing.T) {\n\tt.Parallel()\n\n\tdb, cleanup, err := testdb.NewSpanner()\n\trequire.NoError(t, err)\n\tt.Cleanup(cleanup)\n\trequire.NoError(t, db.Ping())\n\n\ttestDatabase(t, database.DialectSpanner, db, \"testdata/migrations/spanner\", goose.WithIsolateDDL(true))\n}\n\nfunc TestClickhouse(t *testing.T) {\n\tt.Parallel()\n\n\tdb, cleanup, err := testdb.NewClickHouse()\n\trequire.NoError(t, err)\n\tt.Cleanup(cleanup)\n\trequire.NoError(t, db.Ping())\n\n\ttestDatabase(t, database.DialectClickHouse, db, \"testdata/migrations/clickhouse\")\n\n\ttype result struct {\n\t\tcustomerID     string    `db:\"customer_id\"`\n\t\ttimestamp      time.Time `db:\"time_stamp\"`\n\t\tclickEventType string    `db:\"click_event_type\"`\n\t\tcountryCode    string    `db:\"country_code\"`\n\t\tsourceID       int64     `db:\"source_id\"`\n\t}\n\trows, err := db.Query(`SELECT * FROM clickstream ORDER BY customer_id`)\n\trequire.NoError(t, err)\n\tvar results []result\n\tfor rows.Next() {\n\t\tvar r result\n\t\terr = rows.Scan(&r.customerID, &r.timestamp, &r.clickEventType, &r.countryCode, &r.sourceID)\n\t\trequire.NoError(t, err)\n\t\tresults = append(results, r)\n\t}\n\trequire.Equal(t, len(results), 3)\n\trequire.NoError(t, rows.Close())\n\trequire.NoError(t, rows.Err())\n\n\tparseTime := func(t *testing.T, s string) time.Time {\n\t\tt.Helper()\n\t\ttm, err := time.Parse(\"2006-01-02\", s)\n\t\trequire.NoError(t, err)\n\t\treturn tm\n\t}\n\twant := []result{\n\t\t{\"customer1\", parseTime(t, \"2021-10-02\"), \"add_to_cart\", \"US\", 568239},\n\t\t{\"customer2\", parseTime(t, \"2021-10-30\"), \"remove_from_cart\", \"\", 0},\n\t\t{\"customer3\", parseTime(t, \"2021-11-07\"), \"checkout\", \"\", 307493},\n\t}\n\tfor i, result := range results {\n\t\trequire.Equal(t, result.customerID, want[i].customerID)\n\t\trequire.Equal(t, result.timestamp, want[i].timestamp)\n\t\trequire.Equal(t, result.clickEventType, want[i].clickEventType)\n\t\tif result.countryCode != \"\" && want[i].countryCode != \"\" {\n\t\t\trequire.Equal(t, result.countryCode, want[i].countryCode)\n\t\t}\n\t\trequire.Equal(t, result.sourceID, want[i].sourceID)\n\t}\n}\n\nfunc TestClickhouseRemote(t *testing.T) {\n\tt.Parallel()\n\n\tdb, cleanup, err := testdb.NewClickHouse()\n\trequire.NoError(t, err)\n\tt.Cleanup(cleanup)\n\trequire.NoError(t, db.Ping())\n\ttestDatabase(t, database.DialectClickHouse, db, \"testdata/migrations/clickhouse-remote\")\n\n\t// assert that the taxi_zone_dictionary table has been created and populated\n\tvar count int\n\terr = db.QueryRow(`SELECT count(*) FROM taxi_zone_dictionary`).Scan(&count)\n\trequire.NoError(t, err)\n\trequire.Equal(t, 265, count)\n}\n\nfunc TestMySQL(t *testing.T) {\n\tt.Parallel()\n\n\tdb, cleanup, err := testdb.NewMariaDB()\n\trequire.NoError(t, err)\n\tt.Cleanup(cleanup)\n\trequire.NoError(t, db.Ping())\n\n\ttestDatabase(t, database.DialectMySQL, db, \"testdata/migrations/mysql\")\n}\n\nfunc TestTurso(t *testing.T) {\n\tt.Parallel()\n\n\tdb, cleanup, err := testdb.NewTurso()\n\trequire.NoError(t, err)\n\tt.Cleanup(cleanup)\n\trequire.NoError(t, db.Ping())\n\n\ttestDatabase(t, database.DialectTurso, db, \"testdata/migrations/turso\")\n}\n\nfunc TestYDB(t *testing.T) {\n\tt.Parallel()\n\n\tdb, cleanup, err := testdb.NewYdb()\n\trequire.NoError(t, err)\n\tt.Cleanup(cleanup)\n\trequire.NoError(t, db.Ping())\n\n\ttestDatabase(t, database.DialectYdB, db, \"testdata/migrations/ydb\")\n}\n\nfunc TestStarrocks(t *testing.T) {\n\tt.Parallel()\n\n\t// t.Skip(\"Starrocks is flaky on CI, see https://github.com/pressly/goose/issues/881\")\n\n\tdb, cleanup, err := testdb.NewStarrocks()\n\trequire.NoError(t, err)\n\tt.Cleanup(cleanup)\n\trequire.NoError(t, db.Ping())\n\n\ttestDatabase(t, database.DialectStarrocks, db, \"testdata/migrations/starrocks\", goose.WithIsolateDDL(true))\n}\n"
  },
  {
    "path": "internal/testing/integration/integration.go",
    "content": "package integration\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/pressly/goose/v3\"\n\t\"github.com/pressly/goose/v3/database\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype collected struct {\n\tfullpath string\n\tversion  int64\n}\n\nfunc collectMigrations(t *testing.T, dir string) []collected {\n\tt.Helper()\n\n\tfiles, err := os.ReadDir(dir)\n\trequire.NoError(t, err)\n\tall := make([]collected, 0, len(files))\n\tfor _, f := range files {\n\t\trequire.False(t, f.IsDir())\n\t\tv, err := goose.NumericComponent(f.Name())\n\t\trequire.NoError(t, err)\n\t\tall = append(all, collected{\n\t\t\tfullpath: filepath.Base(f.Name()),\n\t\t\tversion:  v,\n\t\t})\n\t}\n\treturn all\n}\n\nfunc testDatabase(t *testing.T, dialect database.Dialect, db *sql.DB, migrationsDir string, opts ...goose.ProviderOption) {\n\tt.Helper()\n\n\tctx := context.Background()\n\t// collect all migration files from the testdata directory\n\twantFiles := collectMigrations(t, migrationsDir)\n\t// initialize a new goose provider\n\tp, err := goose.NewProvider(dialect, db, os.DirFS(migrationsDir), opts...)\n\trequire.NoError(t, err)\n\trequire.Equal(t, len(wantFiles), len(p.ListSources()), \"number of migrations\")\n\t// run all up migrations\n\tresults, err := p.Up(ctx)\n\trequire.NoError(t, err)\n\trequire.Equal(t, len(wantFiles), len(results), \"number of migrations applied\")\n\tfor i, r := range results {\n\t\trequire.Equal(t, wantFiles[i].fullpath, r.Source.Path, \"migration file\")\n\t\trequire.Equal(t, wantFiles[i].version, r.Source.Version, \"migration version\")\n\t}\n\t// check the current version\n\tcurrentVersion, err := p.GetDBVersion(ctx)\n\trequire.NoError(t, err)\n\trequire.Equal(t, len(wantFiles), int(currentVersion), \"current version\")\n\t// run all down migrations\n\tresults, err = p.DownTo(ctx, 0)\n\trequire.NoError(t, err)\n\trequire.Equal(t, len(wantFiles), len(results), \"number of migrations rolled back\")\n\t// check the current version\n\tcurrentVersion, err = p.GetDBVersion(ctx)\n\trequire.NoError(t, err)\n\trequire.Equal(t, 0, int(currentVersion), \"current version\")\n\t// run all up migrations one by one\n\tfor i := range len(wantFiles) {\n\t\tresult, err := p.UpByOne(ctx)\n\t\trequire.NoError(t, err)\n\t\tif errors.Is(err, goose.ErrNoNextVersion) {\n\t\t\tbreak\n\t\t}\n\t\trequire.Equal(t, wantFiles[i].fullpath, result.Source.Path, \"migration file\")\n\t}\n\t// check the current version\n\tcurrentVersion, err = p.GetDBVersion(ctx)\n\trequire.NoError(t, err)\n\trequire.Equal(t, len(wantFiles), int(currentVersion), \"current version\")\n}\n"
  },
  {
    "path": "internal/testing/integration/locking/postgres_locking_test.go",
    "content": "package locking_test\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"hash/crc32\"\n\t\"math/rand/v2\"\n\t\"os\"\n\t\"sort\"\n\t\"sync\"\n\t\"testing\"\n\t\"testing/fstest\"\n\t\"time\"\n\n\t\"github.com/pressly/goose/v3\"\n\t\"github.com/pressly/goose/v3/internal/testing/testdb\"\n\t\"github.com/pressly/goose/v3/lock\"\n\t\"github.com/stretchr/testify/require\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\nfunc TestPostgresSessionLocker(t *testing.T) {\n\tt.Parallel()\n\tif testing.Short() {\n\t\tt.Skip(\"skipping test in short mode.\")\n\t}\n\n\tdb, cleanup, err := testdb.NewPostgres()\n\trequire.NoError(t, err)\n\tt.Cleanup(cleanup)\n\n\t// Do not run subtests in parallel, because they are using the same database.\n\n\tt.Run(\"lock_and_unlock\", func(t *testing.T) {\n\t\tconst (\n\t\t\tlockID int64 = 123456789\n\t\t)\n\t\tlocker, err := lock.NewPostgresSessionLocker(\n\t\t\tlock.WithLockID(lockID),\n\t\t\tlock.WithLockTimeout(1, 4),   // 4 second timeout\n\t\t\tlock.WithUnlockTimeout(1, 4), // 4 second timeout\n\t\t)\n\t\trequire.NoError(t, err)\n\t\tctx := context.Background()\n\t\tconn, err := db.Conn(ctx)\n\t\trequire.NoError(t, err)\n\t\tt.Cleanup(func() {\n\t\t\trequire.NoError(t, conn.Close())\n\t\t})\n\t\terr = locker.SessionLock(ctx, conn)\n\t\trequire.NoError(t, err)\n\t\t// Check that the lock was acquired.\n\t\texists, err := existsPgLock(ctx, db, lockID)\n\t\trequire.NoError(t, err)\n\t\trequire.True(t, exists)\n\t\t// Check that the lock is released.\n\t\terr = locker.SessionUnlock(ctx, conn)\n\t\trequire.NoError(t, err)\n\t\texists, err = existsPgLock(ctx, db, lockID)\n\t\trequire.NoError(t, err)\n\t\trequire.False(t, exists)\n\t})\n\tt.Run(\"lock_close_conn_unlock\", func(t *testing.T) {\n\t\tlocker, err := lock.NewPostgresSessionLocker(\n\t\t\tlock.WithLockTimeout(1, 4),   // 4 second timeout\n\t\t\tlock.WithUnlockTimeout(1, 4), // 4 second timeout\n\t\t)\n\t\trequire.NoError(t, err)\n\t\tctx := context.Background()\n\t\tconn, err := db.Conn(ctx)\n\t\trequire.NoError(t, err)\n\n\t\terr = locker.SessionLock(ctx, conn)\n\t\trequire.NoError(t, err)\n\t\texists, err := existsPgLock(ctx, db, lock.DefaultLockID)\n\t\trequire.NoError(t, err)\n\t\trequire.True(t, exists)\n\t\t// Simulate a connection close.\n\t\terr = conn.Close()\n\t\trequire.NoError(t, err)\n\t\t// Check an error is returned when unlocking, because the connection is already closed.\n\t\terr = locker.SessionUnlock(ctx, conn)\n\t\trequire.Error(t, err)\n\t\trequire.True(t, errors.Is(err, sql.ErrConnDone))\n\t})\n\tt.Run(\"multiple_connections\", func(t *testing.T) {\n\t\tconst (\n\t\t\tworkers = 5\n\t\t)\n\t\tch := make(chan error)\n\t\tvar wg sync.WaitGroup\n\t\tfor i := 0; i < workers; i++ {\n\t\t\twg.Add(1)\n\n\t\t\tgo func() {\n\t\t\t\tdefer wg.Done()\n\t\t\t\tctx := context.Background()\n\t\t\t\tconn, err := db.Conn(ctx)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tt.Cleanup(func() {\n\t\t\t\t\trequire.NoError(t, conn.Close())\n\t\t\t\t})\n\t\t\t\t// Exactly one connection should acquire the lock. While the other connections\n\t\t\t\t// should fail to acquire the lock and timeout.\n\t\t\t\tlocker, err := lock.NewPostgresSessionLocker(\n\t\t\t\t\tlock.WithLockTimeout(1, 4),   // 4 second timeout\n\t\t\t\t\tlock.WithUnlockTimeout(1, 4), // 4 second timeout\n\t\t\t\t)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\t// NOTE, we are not unlocking the lock, because we want to test that the lock is\n\t\t\t\t// released when the connection is closed.\n\t\t\t\tch <- locker.SessionLock(ctx, conn)\n\t\t\t}()\n\t\t}\n\t\tgo func() {\n\t\t\twg.Wait()\n\t\t\tclose(ch)\n\t\t}()\n\t\tvar errors []error\n\t\tfor err := range ch {\n\t\t\tif err != nil {\n\t\t\t\terrors = append(errors, err)\n\t\t\t}\n\t\t}\n\t\trequire.Equal(t, len(errors), workers-1) // One worker succeeds, the rest fail.\n\t\tfor _, err := range errors {\n\t\t\trequire.Error(t, err)\n\t\t\trequire.Equal(t, err.Error(), \"failed to acquire lock\")\n\t\t}\n\t\texists, err := existsPgLock(context.Background(), db, lock.DefaultLockID)\n\t\trequire.NoError(t, err)\n\t\trequire.True(t, exists)\n\t})\n\tt.Run(\"unlock_with_different_connection_error\", func(t *testing.T) {\n\t\trandomLockID := rand.Int64()\n\t\tctx := context.Background()\n\t\tlocker, err := lock.NewPostgresSessionLocker(\n\t\t\tlock.WithLockID(randomLockID),\n\t\t\tlock.WithLockTimeout(1, 4),   // 4 second timeout\n\t\t\tlock.WithUnlockTimeout(1, 4), // 4 second timeout\n\t\t)\n\t\trequire.NoError(t, err)\n\n\t\tconn1, err := db.Conn(ctx)\n\t\trequire.NoError(t, err)\n\t\terr = locker.SessionLock(ctx, conn1)\n\t\trequire.NoError(t, err)\n\t\tt.Cleanup(func() {\n\t\t\t// Defer the unlock with the same connection.\n\t\t\terr = locker.SessionUnlock(ctx, conn1)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.NoError(t, conn1.Close())\n\t\t})\n\t\texists, err := existsPgLock(ctx, db, randomLockID)\n\t\trequire.NoError(t, err)\n\t\trequire.True(t, exists)\n\t\t// Unlock with a different connection.\n\t\tconn2, err := db.Conn(ctx)\n\t\trequire.NoError(t, err)\n\t\tt.Cleanup(func() {\n\t\t\trequire.NoError(t, conn2.Close())\n\t\t})\n\t\t// Check an error is returned when unlocking with a different connection.\n\t\terr = locker.SessionUnlock(ctx, conn2)\n\t\trequire.Error(t, err)\n\t})\n}\n\nfunc TestPostgresProviderLocking(t *testing.T) {\n\tt.Parallel()\n\tif testing.Short() {\n\t\tt.Skip(\"skipping test in short mode.\")\n\t}\n\n\t// The migrations are written in such a way they cannot be applied in parallel, they will fail\n\t// 99.9999% of the time. This test ensures that the advisory session lock mode works as\n\t// expected.\n\n\t// TODO(mf): small improvement here is to use the SAME postgres instance but different databases\n\t// created from a template. This will speed up the test.\n\n\tdb, cleanup, err := testdb.NewPostgres()\n\trequire.NoError(t, err)\n\tt.Cleanup(cleanup)\n\n\tnewProvider := func() *goose.Provider {\n\n\t\tsessionLocker, err := lock.NewPostgresSessionLocker(\n\t\t\tlock.WithLockTimeout(5, 60), // Timeout 5min. Try every 5s up to 60 times.\n\t\t)\n\t\trequire.NoError(t, err)\n\t\tp, err := goose.NewProvider(\n\t\t\tgoose.DialectPostgres,\n\t\t\tdb,\n\t\t\tos.DirFS(\"../testdata/migrations/postgres\"),\n\t\t\tgoose.WithSessionLocker(sessionLocker), // Use advisory session lock mode.\n\t\t)\n\t\trequire.NoError(t, err)\n\n\t\treturn p\n\t}\n\n\tprovider1 := newProvider()\n\tprovider2 := newProvider()\n\n\tsources := provider1.ListSources()\n\tmaxVersion := sources[len(sources)-1].Version\n\n\t// Since the lock mode is advisory session, only one of these providers is expected to apply ALL\n\t// the migrations. The other provider should apply NO migrations. The test MUST fail if both\n\t// providers apply migrations.\n\n\tt.Run(\"up\", func(t *testing.T) {\n\t\tvar g errgroup.Group\n\t\tvar res1, res2 int\n\t\tg.Go(func() error {\n\t\t\tctx := context.Background()\n\t\t\tresults, err := provider1.Up(ctx)\n\t\t\trequire.NoError(t, err)\n\t\t\tres1 = len(results)\n\t\t\tcurrentVersion, err := provider1.GetDBVersion(ctx)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, currentVersion, maxVersion)\n\t\t\treturn nil\n\t\t})\n\t\tg.Go(func() error {\n\t\t\tctx := context.Background()\n\t\t\tresults, err := provider2.Up(ctx)\n\t\t\trequire.NoError(t, err)\n\t\t\tres2 = len(results)\n\t\t\tcurrentVersion, err := provider2.GetDBVersion(ctx)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, currentVersion, maxVersion)\n\t\t\treturn nil\n\t\t})\n\t\trequire.NoError(t, g.Wait())\n\t\t// One of the providers should have applied all migrations and the other should have applied\n\t\t// no migrations, but with no error.\n\t\tif res1 == 0 && res2 == 0 {\n\t\t\tt.Fatal(\"both providers applied no migrations\")\n\t\t}\n\t\tif res1 > 0 && res2 > 0 {\n\t\t\tt.Fatal(\"both providers applied migrations\")\n\t\t}\n\t})\n\n\t// Reset the database and run the same test with the advisory lock mode, but apply migrations\n\t// one-by-one.\n\t{\n\t\t_, err := provider1.DownTo(context.Background(), 0)\n\t\trequire.NoError(t, err)\n\t\tcurrentVersion, err := provider1.GetDBVersion(context.Background())\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, currentVersion, int64(0))\n\t}\n\tt.Run(\"up_by_one\", func(t *testing.T) {\n\t\tvar g errgroup.Group\n\t\tvar (\n\t\t\tmu      sync.Mutex\n\t\t\tapplied []int64\n\t\t)\n\t\tg.Go(func() error {\n\t\t\tfor {\n\t\t\t\tresult, err := provider1.UpByOne(context.Background())\n\t\t\t\tif err != nil {\n\t\t\t\t\tif errors.Is(err, goose.ErrNoNextVersion) {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, result)\n\t\t\t\tmu.Lock()\n\t\t\t\tapplied = append(applied, result.Source.Version)\n\t\t\t\tmu.Unlock()\n\t\t\t}\n\t\t})\n\t\tg.Go(func() error {\n\t\t\tfor {\n\t\t\t\tresult, err := provider2.UpByOne(context.Background())\n\t\t\t\tif err != nil {\n\t\t\t\t\tif errors.Is(err, goose.ErrNoNextVersion) {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, result)\n\t\t\t\tmu.Lock()\n\t\t\t\tapplied = append(applied, result.Source.Version)\n\t\t\t\tmu.Unlock()\n\t\t\t}\n\t\t})\n\t\trequire.NoError(t, g.Wait())\n\t\trequire.Equal(t, len(applied), len(sources))\n\t\tsort.Slice(applied, func(i, j int) bool {\n\t\t\treturn applied[i] < applied[j]\n\t\t})\n\t\t// Each migration should have been applied up exactly once.\n\t\tfor i := 0; i < len(sources); i++ {\n\t\t\trequire.Equal(t, applied[i], sources[i].Version)\n\t\t}\n\t})\n\n\t// Restore the database state by applying all migrations and run the same test with the advisory\n\t// lock mode, but apply down migrations in parallel.\n\t{\n\t\t_, err := provider1.Up(context.Background())\n\t\trequire.NoError(t, err)\n\t\tcurrentVersion, err := provider1.GetDBVersion(context.Background())\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, currentVersion, maxVersion)\n\t}\n\n\tt.Run(\"down_to\", func(t *testing.T) {\n\t\tvar g errgroup.Group\n\t\tvar res1, res2 int\n\t\tg.Go(func() error {\n\t\t\tctx := context.Background()\n\t\t\tresults, err := provider1.DownTo(ctx, 0)\n\t\t\trequire.NoError(t, err)\n\t\t\tres1 = len(results)\n\t\t\tcurrentVersion, err := provider1.GetDBVersion(ctx)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, int64(0), currentVersion)\n\t\t\treturn nil\n\t\t})\n\t\tg.Go(func() error {\n\t\t\tctx := context.Background()\n\t\t\tresults, err := provider2.DownTo(ctx, 0)\n\t\t\trequire.NoError(t, err)\n\t\t\tres2 = len(results)\n\t\t\tcurrentVersion, err := provider2.GetDBVersion(ctx)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, int64(0), currentVersion)\n\t\t\treturn nil\n\t\t})\n\t\trequire.NoError(t, g.Wait())\n\n\t\tif res1 == 0 && res2 == 0 {\n\t\t\tt.Fatal(\"both providers applied no migrations\")\n\t\t}\n\t\tif res1 > 0 && res2 > 0 {\n\t\t\tt.Fatal(\"both providers applied migrations\")\n\t\t}\n\t})\n\n\t// Restore the database state by applying all migrations and run the same test with the advisory\n\t// lock mode, but apply down migrations one-by-one.\n\t{\n\t\t_, err := provider1.Up(context.Background())\n\t\trequire.NoError(t, err)\n\t\tcurrentVersion, err := provider1.GetDBVersion(context.Background())\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, currentVersion, maxVersion)\n\t}\n\n\tt.Run(\"down_by_one\", func(t *testing.T) {\n\t\tvar g errgroup.Group\n\t\tvar (\n\t\t\tmu      sync.Mutex\n\t\t\tapplied []int64\n\t\t)\n\t\tg.Go(func() error {\n\t\t\tfor {\n\t\t\t\tresult, err := provider1.Down(context.Background())\n\t\t\t\tif err != nil {\n\t\t\t\t\tif errors.Is(err, goose.ErrNoNextVersion) {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, result)\n\t\t\t\tmu.Lock()\n\t\t\t\tapplied = append(applied, result.Source.Version)\n\t\t\t\tmu.Unlock()\n\t\t\t}\n\t\t})\n\t\tg.Go(func() error {\n\t\t\tfor {\n\t\t\t\tresult, err := provider2.Down(context.Background())\n\t\t\t\tif err != nil {\n\t\t\t\t\tif errors.Is(err, goose.ErrNoNextVersion) {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, result)\n\t\t\t\tmu.Lock()\n\t\t\t\tapplied = append(applied, result.Source.Version)\n\t\t\t\tmu.Unlock()\n\t\t\t}\n\t\t})\n\t\trequire.NoError(t, g.Wait())\n\t\trequire.Equal(t, len(applied), len(sources))\n\t\tsort.Slice(applied, func(i, j int) bool {\n\t\t\treturn applied[i] < applied[j]\n\t\t})\n\t\t// Each migration should have been applied down exactly once. Since this is sequential the\n\t\t// applied down migrations should be in reverse order.\n\t\tfor i := len(sources) - 1; i >= 0; i-- {\n\t\t\trequire.Equal(t, applied[i], sources[i].Version)\n\t\t}\n\t})\n}\n\nfunc TestPostgresPending(t *testing.T) {\n\tt.Parallel()\n\tif testing.Short() {\n\t\tt.Skip(\"skipping test in short mode.\")\n\t}\n\tconst testDir = \"../testdata/migrations/postgres\"\n\n\tdb, cleanup, err := testdb.NewPostgres()\n\trequire.NoError(t, err)\n\tt.Cleanup(cleanup)\n\n\tfiles, err := os.ReadDir(testDir)\n\trequire.NoError(t, err)\n\n\tworkers := 15\n\n\trun := func(t *testing.T, want bool, wantCurrent, wantTarget int) {\n\t\tt.Helper()\n\t\tvar g errgroup.Group\n\t\tboolCh := make(chan bool, workers)\n\t\tfor i := 0; i < workers; i++ {\n\t\t\tg.Go(func() error {\n\t\t\t\tp, err := goose.NewProvider(goose.DialectPostgres, db, os.DirFS(testDir))\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\thasPending, err := p.HasPending(context.Background())\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tboolCh <- hasPending\n\t\t\t\tcurrent, target, err := p.GetVersions(context.Background())\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.Equal(t, current, int64(wantCurrent))\n\t\t\t\trequire.Equal(t, target, int64(wantTarget))\n\t\t\t\treturn nil\n\n\t\t\t})\n\t\t}\n\t\trequire.NoError(t, g.Wait())\n\t\tclose(boolCh)\n\t\t// expect all values to be true\n\t\tfor hasPending := range boolCh {\n\t\t\trequire.Equal(t, hasPending, want)\n\t\t}\n\t}\n\tt.Run(\"concurrent_has_pending\", func(t *testing.T) {\n\t\trun(t, true, 0, len(files))\n\t})\n\n\t// apply all migrations\n\tp, err := goose.NewProvider(goose.DialectPostgres, db, os.DirFS(\"../testdata/migrations/postgres\"))\n\trequire.NoError(t, err)\n\t_, err = p.Up(context.Background())\n\trequire.NoError(t, err)\n\n\tt.Run(\"concurrent_no_pending\", func(t *testing.T) {\n\t\trun(t, false, len(files), len(files))\n\t})\n\n\t// Add a new migration file\n\tlastVersion := len(files)\n\tnewVersion := fmt.Sprintf(\"%d_new_migration.sql\", lastVersion+1)\n\tfsys := fstest.MapFS{\n\t\tnewVersion: &fstest.MapFile{Data: []byte(`\n-- +goose Up\nSELECT pg_sleep_for('4 seconds');\n`)},\n\t}\n\tlockID := int64(crc32.Checksum([]byte(t.Name()), crc32.MakeTable(crc32.IEEE)))\n\t// Create a new provider with the new migration file\n\tsessionLocker, err := lock.NewPostgresSessionLocker(lock.WithLockTimeout(1, 10), lock.WithLockID(lockID)) // Timeout 5min. Try every 1s up to 10 times.\n\trequire.NoError(t, err)\n\tnewProvider, err := goose.NewProvider(goose.DialectPostgres, db, fsys, goose.WithSessionLocker(sessionLocker))\n\trequire.NoError(t, err)\n\trequire.Equal(t, len(newProvider.ListSources()), 1)\n\toldProvider := p\n\trequire.Equal(t, len(oldProvider.ListSources()), len(files))\n\n\tvar g errgroup.Group\n\tg.Go(func() error {\n\t\thasPending, err := newProvider.HasPending(context.Background())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\trequire.True(t, hasPending)\n\t\tcurrent, target, err := newProvider.GetVersions(context.Background())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\trequire.EqualValues(t, current, lastVersion)\n\t\trequire.EqualValues(t, target, lastVersion+1)\n\t\treturn nil\n\t})\n\tg.Go(func() error {\n\t\thasPending, err := oldProvider.HasPending(context.Background())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\trequire.False(t, hasPending)\n\t\tcurrent, target, err := oldProvider.GetVersions(context.Background())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\trequire.EqualValues(t, current, lastVersion)\n\t\trequire.EqualValues(t, target, lastVersion)\n\t\treturn nil\n\t})\n\trequire.NoError(t, g.Wait())\n\n\t// A new provider is running in the background with a session lock to simulate a long running\n\t// migration. If older instances come up, they should not have any pending migrations and not be\n\t// affected by the long running migration. Test the following scenario:\n\t// https://github.com/pressly/goose/pull/507#discussion_r1266498077\n\tg.Go(func() error {\n\t\t_, err := newProvider.Up(context.Background())\n\t\treturn err\n\t})\n\ttime.Sleep(1 * time.Second)\n\tisLocked, err := existsPgLock(context.Background(), db, lockID)\n\trequire.NoError(t, err)\n\trequire.True(t, isLocked)\n\thasPending, err := oldProvider.HasPending(context.Background())\n\trequire.NoError(t, err)\n\trequire.False(t, hasPending)\n\tcurrent, target, err := oldProvider.GetVersions(context.Background())\n\trequire.NoError(t, err)\n\trequire.EqualValues(t, current, lastVersion)\n\trequire.EqualValues(t, target, lastVersion)\n\t// Wait for the long running migration to finish\n\trequire.NoError(t, g.Wait())\n\t// Check that the new migration was applied\n\thasPending, err = newProvider.HasPending(context.Background())\n\trequire.NoError(t, err)\n\trequire.False(t, hasPending)\n\tcurrent, target, err = newProvider.GetVersions(context.Background())\n\trequire.NoError(t, err)\n\trequire.EqualValues(t, current, lastVersion+1)\n\trequire.EqualValues(t, target, lastVersion+1)\n\t// The max version should be the new migration\n\tcurrentVersion, err := newProvider.GetDBVersion(context.Background())\n\trequire.NoError(t, err)\n\trequire.EqualValues(t, currentVersion, lastVersion+1)\n}\n\nfunc existsPgLock(ctx context.Context, db *sql.DB, lockID int64) (bool, error) {\n\tq := `SELECT EXISTS(SELECT 1 FROM pg_locks WHERE locktype='advisory' AND ((classid::bigint<<32)|objid::bigint)=$1)`\n\trow := db.QueryRowContext(ctx, q, lockID)\n\tvar exists bool\n\tif err := row.Scan(&exists); err != nil {\n\t\treturn false, err\n\t}\n\treturn exists, nil\n}\n"
  },
  {
    "path": "internal/testing/integration/locking/postgres_table_locking_test.go",
    "content": "package locking_test\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\t\"math/rand/v2\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/pressly/goose/v3\"\n\t\"github.com/pressly/goose/v3/internal/testing/testdb\"\n\t\"github.com/pressly/goose/v3/lock\"\n\t\"github.com/pressly/goose/v3/lock/locktesting\"\n\t\"github.com/pressly/goose/v3/testdata\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestConcurrentTableLocking(t *testing.T) {\n\tt.Parallel()\n\tif testing.Short() {\n\t\tt.Skip(\"skipping test in short mode\")\n\t}\n\n\tdb, cleanup, err := testdb.NewPostgres()\n\trequire.NoError(t, err)\n\tt.Cleanup(cleanup)\n\n\t// All lockers must compete for the SAME lock ID\n\tlockID := rand.Int64()\n\n\tnewLocker := func(t *testing.T) lock.Locker {\n\t\tlocker, err := lock.NewPostgresTableLocker(\n\t\t\tlock.WithTableLockID(lockID), // Same lock ID for all lockers!!\n\t\t\tlock.WithTableHeartbeatInterval(200*time.Millisecond),\n\n\t\t\t// This value is important - it controls how long a locker will keep retrying to acquire\n\t\t\t// the lock and must be shorter than the overall lock timeout below.\n\n\t\t\tlock.WithTableLockTimeout(50*time.Millisecond, 2), // 200ms total wait time\n\t\t)\n\t\trequire.NoError(t, err)\n\t\treturn locker\n\t}\n\n\tlocktesting.TestConcurrentLocking(t, db, newLocker, 1*time.Second)\n}\n\nfunc TestSequentialTableLocking(t *testing.T) {\n\tt.Parallel()\n\tif testing.Short() {\n\t\tt.Skip(\"skipping test in short mode\")\n\t}\n\tdb, cleanup, err := testdb.NewPostgres()\n\trequire.NoError(t, err)\n\tt.Cleanup(cleanup)\n\n\tlockID := rand.Int64()\n\n\t// Create two lockers - first has long lease, second has short retry timeout\n\tlocker1, err := lock.NewPostgresTableLocker(\n\t\tlock.WithTableLockID(lockID),\n\t\tlock.WithTableLeaseDuration(2*time.Second), // Long lease to ensure it doesn't expire\n\t\tlock.WithTableHeartbeatInterval(200*time.Millisecond),\n\t)\n\trequire.NoError(t, err)\n\n\tlocker2, err := lock.NewPostgresTableLocker(\n\t\tlock.WithTableLockID(lockID),\n\t\tlock.WithTableLeaseDuration(2*time.Second),\n\t\tlock.WithTableHeartbeatInterval(200*time.Millisecond),\n\t\tlock.WithTableLockTimeout(50*time.Millisecond, 4), // Only 200ms total timeout\n\t)\n\trequire.NoError(t, err)\n\n\tctx := context.Background()\n\n\t// First locker acquires the lock\n\terr = locker1.Lock(ctx, db)\n\trequire.NoError(t, err)\n\tt.Log(\"Locker 1 acquired lock\")\n\n\t// Second locker should fail to acquire the lock (will timeout after 200ms of retries)\n\tctx2, cancel := context.WithTimeout(ctx, 400*time.Millisecond)\n\tdefer cancel()\n\terr = locker2.Lock(ctx2, db)\n\trequire.Error(t, err)\n\tt.Log(\"Locker 2 correctly failed to acquire lock\")\n\n\t// First locker releases the lock\n\terr = locker1.Unlock(ctx, db)\n\trequire.NoError(t, err)\n\tt.Log(\"Locker 1 released lock\")\n\n\t// Now second locker should be able to acquire the lock\n\terr = locker2.Lock(ctx, db)\n\trequire.NoError(t, err)\n\tt.Log(\"Locker 2 acquired lock after locker 1 released\")\n\n\t// Clean up\n\terr = locker2.Unlock(ctx, db)\n\trequire.NoError(t, err)\n\tt.Log(\"Locker 2 released lock\")\n}\n\nfunc TestLockerImplementations(t *testing.T) {\n\tt.Parallel()\n\tif testing.Short() {\n\t\tt.Skip(\"skipping integration test in short mode\")\n\t}\n\n\tt.Run(\"postgres_table_locker_unique\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tdb, cleanup, err := testdb.NewPostgres()\n\t\trequire.NoError(t, err)\n\t\tt.Cleanup(cleanup)\n\n\t\t// Use the same lock ID for all providers so they compete for the same table row\n\t\tsharedLockID := rand.Int64()\n\n\t\tlocktesting.TestProviderLocking(t, func(t *testing.T) *goose.Provider {\n\t\t\tt.Helper()\n\n\t\t\t// Create a UNIQUE table-based locker instance per provider, but same lock ID\n\t\t\tlocker, err := lock.NewPostgresTableLocker(\n\t\t\t\tlock.WithTableLockID(sharedLockID),\n\t\t\t\tlock.WithTableLockTimeout(200*time.Millisecond, 25), // 25 retries, 5s total\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tp, err := goose.NewProvider(\n\t\t\t\tgoose.DialectPostgres,\n\t\t\t\tdb,\n\t\t\t\ttestdata.MustMigrationsFS(),\n\t\t\t\tgoose.WithLocker(locker),\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\t\t\treturn p\n\t\t})\n\t})\n\tt.Run(\"postgres_table_locker_shared\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tdb, cleanup, err := testdb.NewPostgres()\n\t\trequire.NoError(t, err)\n\t\tt.Cleanup(cleanup)\n\n\t\t// Create a SHARED table-based locker per provider\n\t\tlocker, err := lock.NewPostgresTableLocker(\n\t\t\tlock.WithTableLockID(rand.Int64()),\n\t\t\tlock.WithTableLockTimeout(200*time.Millisecond, 25), // 25 retries, 5s total\n\t\t)\n\t\trequire.NoError(t, err)\n\n\t\tlocktesting.TestProviderLocking(t, func(t *testing.T) *goose.Provider {\n\t\t\tt.Helper()\n\n\t\t\tp, err := goose.NewProvider(\n\t\t\t\tgoose.DialectPostgres,\n\t\t\t\tdb,\n\t\t\t\ttestdata.MustMigrationsFS(),\n\t\t\t\tgoose.WithLocker(locker),\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\t\t\treturn p\n\t\t})\n\t})\n\tt.Run(\"postgres_session_locker_unique\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tdb, cleanup, err := testdb.NewPostgres()\n\t\trequire.NoError(t, err)\n\t\tt.Cleanup(cleanup)\n\n\t\t// Use the same lock ID for all providers so they compete for the same advisory lock\n\t\tsharedLockID := rand.Int64()\n\n\t\tlocktesting.TestProviderLocking(t, func(t *testing.T) *goose.Provider {\n\t\t\tt.Helper()\n\n\t\t\t// Each provider gets a UNIQUE session locker instance, but same lock ID\n\t\t\t// This simulates multiple pods with separate locker instances competing for same advisory lock\n\t\t\tsessionLocker, err := lock.NewPostgresSessionLocker(\n\t\t\t\tlock.WithLockID(sharedLockID), // Same lock ID for all providers\n\t\t\t\tlock.WithLockTimeout(1, 10),   // 10 retries, 10s total\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tp, err := goose.NewProvider(\n\t\t\t\tgoose.DialectPostgres,\n\t\t\t\tdb,\n\t\t\t\ttestdata.MustMigrationsFS(),\n\t\t\t\tgoose.WithSessionLocker(sessionLocker),\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\t\t\treturn p\n\t\t})\n\t})\n\tt.Run(\"postgres_session_locker_shared\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tdb, cleanup, err := testdb.NewPostgres()\n\t\trequire.NoError(t, err)\n\t\tt.Cleanup(cleanup)\n\n\t\t// Create a shared session locker (advisory lock) for all providers\n\t\tsessionLocker, err := lock.NewPostgresSessionLocker(\n\t\t\tlock.WithLockID(rand.Int64()),\n\t\t\tlock.WithLockTimeout(1, 10), // 10 retries, 10s total\n\t\t)\n\t\trequire.NoError(t, err)\n\n\t\tlocktesting.TestProviderLocking(t, func(t *testing.T) *goose.Provider {\n\t\t\tt.Helper()\n\n\t\t\tp, err := goose.NewProvider(\n\t\t\t\tgoose.DialectPostgres,\n\t\t\t\tdb,\n\t\t\t\ttestdata.MustMigrationsFS(),\n\t\t\t\tgoose.WithSessionLocker(sessionLocker),\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\t\t\treturn p\n\t\t})\n\t})\n}\n\nfunc TestPostgresTableLockerIntegration(t *testing.T) {\n\tif testing.Short() {\n\t\tt.Skip(\"skipping integration test in short mode\")\n\t}\n\tdb, cleanup, err := testdb.NewPostgres()\n\trequire.NoError(t, err)\n\tt.Cleanup(cleanup)\n\n\tt.Run(\"basic_lock_unlock\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Create a table locker with very short timeouts for testing\n\t\tlocker, err := lock.NewPostgresTableLocker(\n\t\t\tlock.WithTableName(\"test_locks\"),\n\t\t\tlock.WithTableLockID(rand.Int64()),\n\t\t\tlock.WithTableLeaseDuration(5*time.Second),\n\t\t\tlock.WithTableHeartbeatInterval(1*time.Second),\n\t\t\tlock.WithTableLockTimeout(100*time.Millisecond, 2), // Very short timeout\n\t\t)\n\t\trequire.NoError(t, err)\n\n\t\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\t\tdefer cancel()\n\n\t\terr = locker.Lock(ctx, db)\n\t\trequire.NoError(t, err)\n\n\t\terr = locker.Unlock(ctx, db)\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"cleanup_stale_locks\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tlockID := rand.Int64()\n\n\t\t// Create a locker with very short lease to test cleanup functionality\n\t\tlocker, err := lock.NewPostgresTableLocker(\n\t\t\tlock.WithTableLockID(lockID),\n\t\t\tlock.WithTableLeaseDuration(100*time.Millisecond), // Very short lease\n\t\t\tlock.WithTableHeartbeatInterval(50*time.Millisecond),\n\t\t)\n\t\trequire.NoError(t, err)\n\n\t\tctx := context.Background()\n\n\t\t// Acquire the lock\n\t\terr = locker.Lock(ctx, db)\n\t\trequire.NoError(t, err)\n\n\t\t// Let the lease expire by waiting longer than lease duration\n\t\ttime.Sleep(200 * time.Millisecond)\n\n\t\t// Create a second locker that should be able to acquire the lock\n\t\t// because the first one's lease has expired\n\t\tlocker2, err := lock.NewPostgresTableLocker(\n\t\t\tlock.WithTableLockID(lockID), // Same lock ID\n\t\t\tlock.WithTableLeaseDuration(5*time.Second),\n\t\t\tlock.WithTableHeartbeatInterval(1*time.Second),\n\t\t)\n\t\trequire.NoError(t, err)\n\n\t\t// This should succeed because cleanup of stale locks allows it\n\t\tctx2, cancel := context.WithTimeout(ctx, 2*time.Second)\n\t\tdefer cancel()\n\t\terr = locker2.Lock(ctx2, db)\n\t\trequire.NoError(t, err)\n\n\t\t// Clean up\n\t\terr = locker2.Unlock(ctx, db)\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"with_logger\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar logOutput strings.Builder\n\t\tlogger := slog.New(slog.NewTextHandler(&logOutput, &slog.HandlerOptions{\n\t\t\tLevel: slog.LevelDebug,\n\t\t}))\n\n\t\t// Create a table locker with logging enabled\n\t\tlocker, err := lock.NewPostgresTableLocker(\n\t\t\tlock.WithTableName(\"test_locks_with_logger\"),\n\t\t\tlock.WithTableLockID(rand.Int64()),\n\t\t\tlock.WithTableLeaseDuration(2*time.Second),\n\t\t\tlock.WithTableHeartbeatInterval(500*time.Millisecond),\n\t\t\tlock.WithTableLogger(logger),\n\t\t)\n\t\trequire.NoError(t, err)\n\n\t\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\t\tdefer cancel()\n\n\t\t// Test that lock operations generate log messages\n\t\terr = locker.Lock(ctx, db)\n\t\trequire.NoError(t, err)\n\n\t\t// Wait a moment for heartbeat\n\t\ttime.Sleep(1 * time.Second)\n\n\t\terr = locker.Unlock(ctx, db)\n\t\trequire.NoError(t, err)\n\n\t\t// Check that we got some log output\n\t\tlogs := logOutput.String()\n\t\trequire.Contains(t, logs, \"successfully acquired lock\")\n\t\trequire.Contains(t, logs, \"successfully released lock\")\n\t\trequire.Contains(t, logs, \"heartbeat updated lease\")\n\t})\n}\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/clickhouse/00001_a.sql",
    "content": "-- +goose Up\nCREATE TABLE IF NOT EXISTS trips\n(\n    `trip_id` UInt32,\n    `vendor_id` Enum8('1' = 1, '2' = 2, '3' = 3, '4' = 4, 'CMT' = 5, 'VTS' = 6, 'DDS' = 7, 'B02512' = 10, 'B02598' = 11, 'B02617' = 12, 'B02682' = 13, 'B02764' = 14, '' = 15),\n    `pickup_date` Date,\n    `pickup_datetime` DateTime,\n    `dropoff_date` Date,\n    `dropoff_datetime` DateTime,\n    `store_and_fwd_flag` UInt8,\n    `rate_code_id` UInt8,\n    `pickup_longitude` Float64,\n    `pickup_latitude` Float64,\n    `dropoff_longitude` Float64,\n    `dropoff_latitude` Float64,\n    `passenger_count` UInt8,\n    `trip_distance` Float64,\n    `fare_amount` Float32,\n    `extra` Float32,\n    `mta_tax` Float32,\n    `tip_amount` Float32,\n    `tolls_amount` Float32,\n    `ehail_fee` Float32,\n    `improvement_surcharge` Float32,\n    `total_amount` Float32,\n    `payment_type` Enum8('UNK' = 0, 'CSH' = 1, 'CRE' = 2, 'NOC' = 3, 'DIS' = 4),\n    `trip_type` UInt8,\n    `pickup` FixedString(25),\n    `dropoff` FixedString(25),\n    `cab_type` Enum8('yellow' = 1, 'green' = 2, 'uber' = 3),\n    `pickup_nyct2010_gid` Int8,\n    `pickup_ctlabel` Float32,\n    `pickup_borocode` Int8,\n    `pickup_ct2010` String,\n    `pickup_boroct2010` FixedString(7),\n    `pickup_cdeligibil` String,\n    `pickup_ntacode` FixedString(4),\n    `pickup_ntaname` String,\n    `pickup_puma` UInt16,\n    `dropoff_nyct2010_gid` UInt8,\n    `dropoff_ctlabel` Float32,\n    `dropoff_borocode` UInt8,\n    `dropoff_ct2010` String,\n    `dropoff_boroct2010` FixedString(7),\n    `dropoff_cdeligibil` String,\n    `dropoff_ntacode` FixedString(4),\n    `dropoff_ntaname` String,\n    `dropoff_puma` UInt16\n)\nENGINE = MergeTree\nPARTITION BY toYYYYMM(pickup_date)\nORDER BY pickup_datetime;\n\n-- +goose Down\nDROP TABLE IF EXISTS trips;\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/clickhouse/00002_b.sql",
    "content": "-- +goose Up\nCREATE TABLE IF NOT EXISTS clickstream (\n    customer_id String, \n    time_stamp Date, \n    click_event_type String,\n    country_code FixedString(2), \t\n    source_id UInt64\n) \nENGINE = MergeTree()\nORDER BY (time_stamp);\n\n-- +goose Down\nDROP TABLE IF EXISTS clickstream;\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/clickhouse/00003_c.sql",
    "content": "-- +goose Up\nINSERT INTO clickstream VALUES ('customer1', '2021-10-02', 'add_to_cart', 'US', 568239 ); \n\nINSERT INTO clickstream (customer_id, time_stamp, click_event_type) VALUES ('customer2', '2021-10-30', 'remove_from_cart' );\n\nINSERT INTO clickstream (* EXCEPT(country_code)) VALUES ('customer3', '2021-11-07', 'checkout', 307493 );\n\n-- +goose Down\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/clickhouse-remote/00001_a.sql",
    "content": "-- +goose Up\nCREATE DICTIONARY taxi_zone_dictionary (\n    LocationID UInt16 DEFAULT 0,\n    Borough String,\n    Zone String,\n    service_zone String\n)\nPRIMARY KEY LocationID\nSOURCE(HTTP(\n    url 'https://datasets-documentation.s3.eu-west-3.amazonaws.com/nyc-taxi/taxi_zone_lookup.csv'\n    format 'CSVWithNames'\n))\nLIFETIME(0)\nLAYOUT(HASHED());\n\n-- +goose Down\nDROP DICTIONARY IF EXISTS taxi_zone_dictionary;\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/clickhouse-remote-backup/taxi_zone_lookup.csv",
    "content": "\"LocationID\",\"Borough\",\"Zone\",\"service_zone\"\r\n1,\"EWR\",\"Newark Airport\",\"EWR\"\r\n2,\"Queens\",\"Jamaica Bay\",\"Boro Zone\"\r\n3,\"Bronx\",\"Allerton/Pelham Gardens\",\"Boro Zone\"\r\n4,\"Manhattan\",\"Alphabet City\",\"Yellow Zone\"\r\n5,\"Staten Island\",\"Arden Heights\",\"Boro Zone\"\r\n6,\"Staten Island\",\"Arrochar/Fort Wadsworth\",\"Boro Zone\"\r\n7,\"Queens\",\"Astoria\",\"Boro Zone\"\r\n8,\"Queens\",\"Astoria Park\",\"Boro Zone\"\r\n9,\"Queens\",\"Auburndale\",\"Boro Zone\"\r\n10,\"Queens\",\"Baisley Park\",\"Boro Zone\"\r\n11,\"Brooklyn\",\"Bath Beach\",\"Boro Zone\"\r\n12,\"Manhattan\",\"Battery Park\",\"Yellow Zone\"\r\n13,\"Manhattan\",\"Battery Park City\",\"Yellow Zone\"\r\n14,\"Brooklyn\",\"Bay Ridge\",\"Boro Zone\"\r\n15,\"Queens\",\"Bay Terrace/Fort Totten\",\"Boro Zone\"\r\n16,\"Queens\",\"Bayside\",\"Boro Zone\"\r\n17,\"Brooklyn\",\"Bedford\",\"Boro Zone\"\r\n18,\"Bronx\",\"Bedford Park\",\"Boro Zone\"\r\n19,\"Queens\",\"Bellerose\",\"Boro Zone\"\r\n20,\"Bronx\",\"Belmont\",\"Boro Zone\"\r\n21,\"Brooklyn\",\"Bensonhurst East\",\"Boro Zone\"\r\n22,\"Brooklyn\",\"Bensonhurst West\",\"Boro Zone\"\r\n23,\"Staten Island\",\"Bloomfield/Emerson Hill\",\"Boro Zone\"\r\n24,\"Manhattan\",\"Bloomingdale\",\"Yellow Zone\"\r\n25,\"Brooklyn\",\"Boerum Hill\",\"Boro Zone\"\r\n26,\"Brooklyn\",\"Borough Park\",\"Boro Zone\"\r\n27,\"Queens\",\"Breezy Point/Fort Tilden/Riis Beach\",\"Boro Zone\"\r\n28,\"Queens\",\"Briarwood/Jamaica Hills\",\"Boro Zone\"\r\n29,\"Brooklyn\",\"Brighton Beach\",\"Boro Zone\"\r\n30,\"Queens\",\"Broad Channel\",\"Boro Zone\"\r\n31,\"Bronx\",\"Bronx Park\",\"Boro Zone\"\r\n32,\"Bronx\",\"Bronxdale\",\"Boro Zone\"\r\n33,\"Brooklyn\",\"Brooklyn Heights\",\"Boro Zone\"\r\n34,\"Brooklyn\",\"Brooklyn Navy Yard\",\"Boro Zone\"\r\n35,\"Brooklyn\",\"Brownsville\",\"Boro Zone\"\r\n36,\"Brooklyn\",\"Bushwick North\",\"Boro Zone\"\r\n37,\"Brooklyn\",\"Bushwick South\",\"Boro Zone\"\r\n38,\"Queens\",\"Cambria Heights\",\"Boro Zone\"\r\n39,\"Brooklyn\",\"Canarsie\",\"Boro Zone\"\r\n40,\"Brooklyn\",\"Carroll Gardens\",\"Boro Zone\"\r\n41,\"Manhattan\",\"Central Harlem\",\"Boro Zone\"\r\n42,\"Manhattan\",\"Central Harlem North\",\"Boro Zone\"\r\n43,\"Manhattan\",\"Central Park\",\"Yellow Zone\"\r\n44,\"Staten Island\",\"Charleston/Tottenville\",\"Boro Zone\"\r\n45,\"Manhattan\",\"Chinatown\",\"Yellow Zone\"\r\n46,\"Bronx\",\"City Island\",\"Boro Zone\"\r\n47,\"Bronx\",\"Claremont/Bathgate\",\"Boro Zone\"\r\n48,\"Manhattan\",\"Clinton East\",\"Yellow Zone\"\r\n49,\"Brooklyn\",\"Clinton Hill\",\"Boro Zone\"\r\n50,\"Manhattan\",\"Clinton West\",\"Yellow Zone\"\r\n51,\"Bronx\",\"Co-Op City\",\"Boro Zone\"\r\n52,\"Brooklyn\",\"Cobble Hill\",\"Boro Zone\"\r\n53,\"Queens\",\"College Point\",\"Boro Zone\"\r\n54,\"Brooklyn\",\"Columbia Street\",\"Boro Zone\"\r\n55,\"Brooklyn\",\"Coney Island\",\"Boro Zone\"\r\n56,\"Queens\",\"Corona\",\"Boro Zone\"\r\n57,\"Queens\",\"Corona\",\"Boro Zone\"\r\n58,\"Bronx\",\"Country Club\",\"Boro Zone\"\r\n59,\"Bronx\",\"Crotona Park\",\"Boro Zone\"\r\n60,\"Bronx\",\"Crotona Park East\",\"Boro Zone\"\r\n61,\"Brooklyn\",\"Crown Heights North\",\"Boro Zone\"\r\n62,\"Brooklyn\",\"Crown Heights South\",\"Boro Zone\"\r\n63,\"Brooklyn\",\"Cypress Hills\",\"Boro Zone\"\r\n64,\"Queens\",\"Douglaston\",\"Boro Zone\"\r\n65,\"Brooklyn\",\"Downtown Brooklyn/MetroTech\",\"Boro Zone\"\r\n66,\"Brooklyn\",\"DUMBO/Vinegar Hill\",\"Boro Zone\"\r\n67,\"Brooklyn\",\"Dyker Heights\",\"Boro Zone\"\r\n68,\"Manhattan\",\"East Chelsea\",\"Yellow Zone\"\r\n69,\"Bronx\",\"East Concourse/Concourse Village\",\"Boro Zone\"\r\n70,\"Queens\",\"East Elmhurst\",\"Boro Zone\"\r\n71,\"Brooklyn\",\"East Flatbush/Farragut\",\"Boro Zone\"\r\n72,\"Brooklyn\",\"East Flatbush/Remsen Village\",\"Boro Zone\"\r\n73,\"Queens\",\"East Flushing\",\"Boro Zone\"\r\n74,\"Manhattan\",\"East Harlem North\",\"Boro Zone\"\r\n75,\"Manhattan\",\"East Harlem South\",\"Boro Zone\"\r\n76,\"Brooklyn\",\"East New York\",\"Boro Zone\"\r\n77,\"Brooklyn\",\"East New York/Pennsylvania Avenue\",\"Boro Zone\"\r\n78,\"Bronx\",\"East Tremont\",\"Boro Zone\"\r\n79,\"Manhattan\",\"East Village\",\"Yellow Zone\"\r\n80,\"Brooklyn\",\"East Williamsburg\",\"Boro Zone\"\r\n81,\"Bronx\",\"Eastchester\",\"Boro Zone\"\r\n82,\"Queens\",\"Elmhurst\",\"Boro Zone\"\r\n83,\"Queens\",\"Elmhurst/Maspeth\",\"Boro Zone\"\r\n84,\"Staten Island\",\"Eltingville/Annadale/Prince's Bay\",\"Boro Zone\"\r\n85,\"Brooklyn\",\"Erasmus\",\"Boro Zone\"\r\n86,\"Queens\",\"Far Rockaway\",\"Boro Zone\"\r\n87,\"Manhattan\",\"Financial District North\",\"Yellow Zone\"\r\n88,\"Manhattan\",\"Financial District South\",\"Yellow Zone\"\r\n89,\"Brooklyn\",\"Flatbush/Ditmas Park\",\"Boro Zone\"\r\n90,\"Manhattan\",\"Flatiron\",\"Yellow Zone\"\r\n91,\"Brooklyn\",\"Flatlands\",\"Boro Zone\"\r\n92,\"Queens\",\"Flushing\",\"Boro Zone\"\r\n93,\"Queens\",\"Flushing Meadows-Corona Park\",\"Boro Zone\"\r\n94,\"Bronx\",\"Fordham South\",\"Boro Zone\"\r\n95,\"Queens\",\"Forest Hills\",\"Boro Zone\"\r\n96,\"Queens\",\"Forest Park/Highland Park\",\"Boro Zone\"\r\n97,\"Brooklyn\",\"Fort Greene\",\"Boro Zone\"\r\n98,\"Queens\",\"Fresh Meadows\",\"Boro Zone\"\r\n99,\"Staten Island\",\"Freshkills Park\",\"Boro Zone\"\r\n100,\"Manhattan\",\"Garment District\",\"Yellow Zone\"\r\n101,\"Queens\",\"Glen Oaks\",\"Boro Zone\"\r\n102,\"Queens\",\"Glendale\",\"Boro Zone\"\r\n103,\"Manhattan\",\"Governor's Island/Ellis Island/Liberty Island\",\"Yellow Zone\"\r\n104,\"Manhattan\",\"Governor's Island/Ellis Island/Liberty Island\",\"Yellow Zone\"\r\n105,\"Manhattan\",\"Governor's Island/Ellis Island/Liberty Island\",\"Yellow Zone\"\r\n106,\"Brooklyn\",\"Gowanus\",\"Boro Zone\"\r\n107,\"Manhattan\",\"Gramercy\",\"Yellow Zone\"\r\n108,\"Brooklyn\",\"Gravesend\",\"Boro Zone\"\r\n109,\"Staten Island\",\"Great Kills\",\"Boro Zone\"\r\n110,\"Staten Island\",\"Great Kills Park\",\"Boro Zone\"\r\n111,\"Brooklyn\",\"Green-Wood Cemetery\",\"Boro Zone\"\r\n112,\"Brooklyn\",\"Greenpoint\",\"Boro Zone\"\r\n113,\"Manhattan\",\"Greenwich Village North\",\"Yellow Zone\"\r\n114,\"Manhattan\",\"Greenwich Village South\",\"Yellow Zone\"\r\n115,\"Staten Island\",\"Grymes Hill/Clifton\",\"Boro Zone\"\r\n116,\"Manhattan\",\"Hamilton Heights\",\"Boro Zone\"\r\n117,\"Queens\",\"Hammels/Arverne\",\"Boro Zone\"\r\n118,\"Staten Island\",\"Heartland Village/Todt Hill\",\"Boro Zone\"\r\n119,\"Bronx\",\"Highbridge\",\"Boro Zone\"\r\n120,\"Manhattan\",\"Highbridge Park\",\"Boro Zone\"\r\n121,\"Queens\",\"Hillcrest/Pomonok\",\"Boro Zone\"\r\n122,\"Queens\",\"Hollis\",\"Boro Zone\"\r\n123,\"Brooklyn\",\"Homecrest\",\"Boro Zone\"\r\n124,\"Queens\",\"Howard Beach\",\"Boro Zone\"\r\n125,\"Manhattan\",\"Hudson Sq\",\"Yellow Zone\"\r\n126,\"Bronx\",\"Hunts Point\",\"Boro Zone\"\r\n127,\"Manhattan\",\"Inwood\",\"Boro Zone\"\r\n128,\"Manhattan\",\"Inwood Hill Park\",\"Boro Zone\"\r\n129,\"Queens\",\"Jackson Heights\",\"Boro Zone\"\r\n130,\"Queens\",\"Jamaica\",\"Boro Zone\"\r\n131,\"Queens\",\"Jamaica Estates\",\"Boro Zone\"\r\n132,\"Queens\",\"JFK Airport\",\"Airports\"\r\n133,\"Brooklyn\",\"Kensington\",\"Boro Zone\"\r\n134,\"Queens\",\"Kew Gardens\",\"Boro Zone\"\r\n135,\"Queens\",\"Kew Gardens Hills\",\"Boro Zone\"\r\n136,\"Bronx\",\"Kingsbridge Heights\",\"Boro Zone\"\r\n137,\"Manhattan\",\"Kips Bay\",\"Yellow Zone\"\r\n138,\"Queens\",\"LaGuardia Airport\",\"Airports\"\r\n139,\"Queens\",\"Laurelton\",\"Boro Zone\"\r\n140,\"Manhattan\",\"Lenox Hill East\",\"Yellow Zone\"\r\n141,\"Manhattan\",\"Lenox Hill West\",\"Yellow Zone\"\r\n142,\"Manhattan\",\"Lincoln Square East\",\"Yellow Zone\"\r\n143,\"Manhattan\",\"Lincoln Square West\",\"Yellow Zone\"\r\n144,\"Manhattan\",\"Little Italy/NoLiTa\",\"Yellow Zone\"\r\n145,\"Queens\",\"Long Island City/Hunters Point\",\"Boro Zone\"\r\n146,\"Queens\",\"Long Island City/Queens Plaza\",\"Boro Zone\"\r\n147,\"Bronx\",\"Longwood\",\"Boro Zone\"\r\n148,\"Manhattan\",\"Lower East Side\",\"Yellow Zone\"\r\n149,\"Brooklyn\",\"Madison\",\"Boro Zone\"\r\n150,\"Brooklyn\",\"Manhattan Beach\",\"Boro Zone\"\r\n151,\"Manhattan\",\"Manhattan Valley\",\"Yellow Zone\"\r\n152,\"Manhattan\",\"Manhattanville\",\"Boro Zone\"\r\n153,\"Manhattan\",\"Marble Hill\",\"Boro Zone\"\r\n154,\"Brooklyn\",\"Marine Park/Floyd Bennett Field\",\"Boro Zone\"\r\n155,\"Brooklyn\",\"Marine Park/Mill Basin\",\"Boro Zone\"\r\n156,\"Staten Island\",\"Mariners Harbor\",\"Boro Zone\"\r\n157,\"Queens\",\"Maspeth\",\"Boro Zone\"\r\n158,\"Manhattan\",\"Meatpacking/West Village West\",\"Yellow Zone\"\r\n159,\"Bronx\",\"Melrose South\",\"Boro Zone\"\r\n160,\"Queens\",\"Middle Village\",\"Boro Zone\"\r\n161,\"Manhattan\",\"Midtown Center\",\"Yellow Zone\"\r\n162,\"Manhattan\",\"Midtown East\",\"Yellow Zone\"\r\n163,\"Manhattan\",\"Midtown North\",\"Yellow Zone\"\r\n164,\"Manhattan\",\"Midtown South\",\"Yellow Zone\"\r\n165,\"Brooklyn\",\"Midwood\",\"Boro Zone\"\r\n166,\"Manhattan\",\"Morningside Heights\",\"Boro Zone\"\r\n167,\"Bronx\",\"Morrisania/Melrose\",\"Boro Zone\"\r\n168,\"Bronx\",\"Mott Haven/Port Morris\",\"Boro Zone\"\r\n169,\"Bronx\",\"Mount Hope\",\"Boro Zone\"\r\n170,\"Manhattan\",\"Murray Hill\",\"Yellow Zone\"\r\n171,\"Queens\",\"Murray Hill-Queens\",\"Boro Zone\"\r\n172,\"Staten Island\",\"New Dorp/Midland Beach\",\"Boro Zone\"\r\n173,\"Queens\",\"North Corona\",\"Boro Zone\"\r\n174,\"Bronx\",\"Norwood\",\"Boro Zone\"\r\n175,\"Queens\",\"Oakland Gardens\",\"Boro Zone\"\r\n176,\"Staten Island\",\"Oakwood\",\"Boro Zone\"\r\n177,\"Brooklyn\",\"Ocean Hill\",\"Boro Zone\"\r\n178,\"Brooklyn\",\"Ocean Parkway South\",\"Boro Zone\"\r\n179,\"Queens\",\"Old Astoria\",\"Boro Zone\"\r\n180,\"Queens\",\"Ozone Park\",\"Boro Zone\"\r\n181,\"Brooklyn\",\"Park Slope\",\"Boro Zone\"\r\n182,\"Bronx\",\"Parkchester\",\"Boro Zone\"\r\n183,\"Bronx\",\"Pelham Bay\",\"Boro Zone\"\r\n184,\"Bronx\",\"Pelham Bay Park\",\"Boro Zone\"\r\n185,\"Bronx\",\"Pelham Parkway\",\"Boro Zone\"\r\n186,\"Manhattan\",\"Penn Station/Madison Sq West\",\"Yellow Zone\"\r\n187,\"Staten Island\",\"Port Richmond\",\"Boro Zone\"\r\n188,\"Brooklyn\",\"Prospect-Lefferts Gardens\",\"Boro Zone\"\r\n189,\"Brooklyn\",\"Prospect Heights\",\"Boro Zone\"\r\n190,\"Brooklyn\",\"Prospect Park\",\"Boro Zone\"\r\n191,\"Queens\",\"Queens Village\",\"Boro Zone\"\r\n192,\"Queens\",\"Queensboro Hill\",\"Boro Zone\"\r\n193,\"Queens\",\"Queensbridge/Ravenswood\",\"Boro Zone\"\r\n194,\"Manhattan\",\"Randalls Island\",\"Yellow Zone\"\r\n195,\"Brooklyn\",\"Red Hook\",\"Boro Zone\"\r\n196,\"Queens\",\"Rego Park\",\"Boro Zone\"\r\n197,\"Queens\",\"Richmond Hill\",\"Boro Zone\"\r\n198,\"Queens\",\"Ridgewood\",\"Boro Zone\"\r\n199,\"Bronx\",\"Rikers Island\",\"Boro Zone\"\r\n200,\"Bronx\",\"Riverdale/North Riverdale/Fieldston\",\"Boro Zone\"\r\n201,\"Queens\",\"Rockaway Park\",\"Boro Zone\"\r\n202,\"Manhattan\",\"Roosevelt Island\",\"Boro Zone\"\r\n203,\"Queens\",\"Rosedale\",\"Boro Zone\"\r\n204,\"Staten Island\",\"Rossville/Woodrow\",\"Boro Zone\"\r\n205,\"Queens\",\"Saint Albans\",\"Boro Zone\"\r\n206,\"Staten Island\",\"Saint George/New Brighton\",\"Boro Zone\"\r\n207,\"Queens\",\"Saint Michaels Cemetery/Woodside\",\"Boro Zone\"\r\n208,\"Bronx\",\"Schuylerville/Edgewater Park\",\"Boro Zone\"\r\n209,\"Manhattan\",\"Seaport\",\"Yellow Zone\"\r\n210,\"Brooklyn\",\"Sheepshead Bay\",\"Boro Zone\"\r\n211,\"Manhattan\",\"SoHo\",\"Yellow Zone\"\r\n212,\"Bronx\",\"Soundview/Bruckner\",\"Boro Zone\"\r\n213,\"Bronx\",\"Soundview/Castle Hill\",\"Boro Zone\"\r\n214,\"Staten Island\",\"South Beach/Dongan Hills\",\"Boro Zone\"\r\n215,\"Queens\",\"South Jamaica\",\"Boro Zone\"\r\n216,\"Queens\",\"South Ozone Park\",\"Boro Zone\"\r\n217,\"Brooklyn\",\"South Williamsburg\",\"Boro Zone\"\r\n218,\"Queens\",\"Springfield Gardens North\",\"Boro Zone\"\r\n219,\"Queens\",\"Springfield Gardens South\",\"Boro Zone\"\r\n220,\"Bronx\",\"Spuyten Duyvil/Kingsbridge\",\"Boro Zone\"\r\n221,\"Staten Island\",\"Stapleton\",\"Boro Zone\"\r\n222,\"Brooklyn\",\"Starrett City\",\"Boro Zone\"\r\n223,\"Queens\",\"Steinway\",\"Boro Zone\"\r\n224,\"Manhattan\",\"Stuy Town/Peter Cooper Village\",\"Yellow Zone\"\r\n225,\"Brooklyn\",\"Stuyvesant Heights\",\"Boro Zone\"\r\n226,\"Queens\",\"Sunnyside\",\"Boro Zone\"\r\n227,\"Brooklyn\",\"Sunset Park East\",\"Boro Zone\"\r\n228,\"Brooklyn\",\"Sunset Park West\",\"Boro Zone\"\r\n229,\"Manhattan\",\"Sutton Place/Turtle Bay North\",\"Yellow Zone\"\r\n230,\"Manhattan\",\"Times Sq/Theatre District\",\"Yellow Zone\"\r\n231,\"Manhattan\",\"TriBeCa/Civic Center\",\"Yellow Zone\"\r\n232,\"Manhattan\",\"Two Bridges/Seward Park\",\"Yellow Zone\"\r\n233,\"Manhattan\",\"UN/Turtle Bay South\",\"Yellow Zone\"\r\n234,\"Manhattan\",\"Union Sq\",\"Yellow Zone\"\r\n235,\"Bronx\",\"University Heights/Morris Heights\",\"Boro Zone\"\r\n236,\"Manhattan\",\"Upper East Side North\",\"Yellow Zone\"\r\n237,\"Manhattan\",\"Upper East Side South\",\"Yellow Zone\"\r\n238,\"Manhattan\",\"Upper West Side North\",\"Yellow Zone\"\r\n239,\"Manhattan\",\"Upper West Side South\",\"Yellow Zone\"\r\n240,\"Bronx\",\"Van Cortlandt Park\",\"Boro Zone\"\r\n241,\"Bronx\",\"Van Cortlandt Village\",\"Boro Zone\"\r\n242,\"Bronx\",\"Van Nest/Morris Park\",\"Boro Zone\"\r\n243,\"Manhattan\",\"Washington Heights North\",\"Boro Zone\"\r\n244,\"Manhattan\",\"Washington Heights South\",\"Boro Zone\"\r\n245,\"Staten Island\",\"West Brighton\",\"Boro Zone\"\r\n246,\"Manhattan\",\"West Chelsea/Hudson Yards\",\"Yellow Zone\"\r\n247,\"Bronx\",\"West Concourse\",\"Boro Zone\"\r\n248,\"Bronx\",\"West Farms/Bronx River\",\"Boro Zone\"\r\n249,\"Manhattan\",\"West Village\",\"Yellow Zone\"\r\n250,\"Bronx\",\"Westchester Village/Unionport\",\"Boro Zone\"\r\n251,\"Staten Island\",\"Westerleigh\",\"Boro Zone\"\r\n252,\"Queens\",\"Whitestone\",\"Boro Zone\"\r\n253,\"Queens\",\"Willets Point\",\"Boro Zone\"\r\n254,\"Bronx\",\"Williamsbridge/Olinville\",\"Boro Zone\"\r\n255,\"Brooklyn\",\"Williamsburg (North Side)\",\"Boro Zone\"\r\n256,\"Brooklyn\",\"Williamsburg (South Side)\",\"Boro Zone\"\r\n257,\"Brooklyn\",\"Windsor Terrace\",\"Boro Zone\"\r\n258,\"Queens\",\"Woodhaven\",\"Boro Zone\"\r\n259,\"Bronx\",\"Woodlawn/Wakefield\",\"Boro Zone\"\r\n260,\"Queens\",\"Woodside\",\"Boro Zone\"\r\n261,\"Manhattan\",\"World Trade Center\",\"Yellow Zone\"\r\n262,\"Manhattan\",\"Yorkville East\",\"Yellow Zone\"\r\n263,\"Manhattan\",\"Yorkville West\",\"Yellow Zone\"\r\n264,\"Unknown\",\"NV\",\"N/A\"\r\n265,\"Unknown\",\"NA\",\"N/A\"\r\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/mysql/00001_table.sql",
    "content": "-- +goose Up\n-- +goose StatementBegin\nCREATE TABLE owners (\n    owner_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,\n    owner_name VARCHAR(255) NOT NULL,\n    owner_type ENUM('user', 'organization') NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS repos (\n    repo_id BIGINT UNSIGNED AUTO_INCREMENT NOT NULL,\n    repo_full_name VARCHAR(255) NOT NULL,\n    repo_owner_id BIGINT UNSIGNED NOT NULL,\n    \n    PRIMARY KEY (repo_id),\n    FOREIGN KEY (repo_owner_id) REFERENCES owners(owner_id) ON DELETE CASCADE\n);\n-- +goose StatementEnd\n\n-- +goose Down\n-- +goose StatementBegin\nDROP TABLE IF EXISTS repos;\nDROP TABLE IF EXISTS owners;\n-- +goose StatementEnd\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/mysql/00002_insert.sql",
    "content": "-- +goose Up\n-- +goose StatementBegin\nINSERT INTO owners (owner_name, owner_type)\nVALUES\n  ('lucas', 'user'),\n  ('space', 'organization');\n-- +goose StatementEnd\n\nINSERT INTO owners (owner_name, owner_type)\nVALUES\n  ('james', 'user'),\n  ('pressly', 'organization');\n\nINSERT INTO repos (repo_full_name, repo_owner_id)\nVALUES\n  ('james/rover', (SELECT owner_id FROM owners WHERE owner_name = 'james')),\n  ('pressly/goose', (SELECT owner_id FROM owners WHERE owner_name = 'pressly'));\n\n-- +goose Down\n-- +goose StatementBegin\nDELETE FROM owners;\n-- +goose StatementEnd\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/mysql/00003_alter.sql",
    "content": "-- +goose Up\n-- +goose StatementBegin\nALTER TABLE repos \n    ADD COLUMN IF NOT EXISTS homepage_url TEXT;\n\nALTER TABLE repos \n    ADD COLUMN is_private BOOLEAN NOT NULL DEFAULT FALSE;\n-- +goose StatementEnd\n\n-- +goose Down\n-- +goose StatementBegin\nALTER TABLE repos\n    DROP COLUMN IF EXISTS homepage_url;\n\nALTER TABLE repos\n    DROP COLUMN IF EXISTS is_private;\n-- +goose StatementEnd\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/mysql/00004_empty.sql",
    "content": "-- +goose Up\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/mysql/00005_no_tx.sql",
    "content": "-- +goose NO TRANSACTION\n\n-- +goose Up\nCREATE UNIQUE INDEX owners_owner_name_idx ON owners(owner_name);\n\n-- +goose Down\nDROP INDEX IF EXISTS owners_owner_name_idx ON owners;\n\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/mysql/00006_complex.sql",
    "content": "-- +goose up\n\n-- +goose statementbegin\nCREATE OR REPLACE PROCEDURE insert_repository(\n    IN p_repo_full_name VARCHAR(255),\n    IN p_owner_name VARCHAR(255),\n    IN p_owner_type VARCHAR(20)\n)\nBEGIN\n    DECLARE v_owner_id BIGINT;\n    DECLARE v_repo_id BIGINT;\n\n    -- Check if the owner already exists\n    SELECT owner_id INTO v_owner_id\n    FROM owners\n    WHERE owner_name = p_owner_name AND owner_type = p_owner_type;\n\n    -- If the owner does not exist, insert a new owner\n    IF v_owner_id IS NULL THEN\n        INSERT INTO owners (owner_name, owner_type)\n        VALUES (p_owner_name, p_owner_type);\n        \n        SET v_owner_id = LAST_INSERT_ID();\n    END IF;\n\n    -- Insert the repository using the obtained owner_id\n    INSERT INTO repos (repo_full_name, repo_owner_id)\n    VALUES (p_repo_full_name, v_owner_id);\n\n    -- No explicit return needed in procedures\n\nEND;\n-- +goose statementend\n\n-- +goose down\nDROP PROCEDURE IF EXISTS insert_repository;\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/postgres/00001_table.sql",
    "content": "-- +goose Up\n-- +goose StatementBegin\nCREATE TYPE owner_type as ENUM('user', 'organization');\n\nCREATE TABLE owners (\n    owner_id BIGSERIAL PRIMARY KEY,\n    owner_name text NOT NULL,\n    owner_type owner_type NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS repos (\n    repo_id BIGSERIAL NOT NULL,\n    repo_full_name text NOT NULL,\n    repo_owner_id bigint NOT NULL REFERENCES owners(owner_id) ON DELETE CASCADE,\n\n    PRIMARY KEY (repo_id)\n);\n-- +goose StatementEnd\n\n-- +goose Down\n-- +goose StatementBegin\nDROP TABLE IF EXISTS repos;\nDROP TABLE IF EXISTS owners;\nDROP TYPE owner_type;\n-- +goose StatementEnd\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/postgres/00002_insert.sql",
    "content": "-- +goose Up\n-- +goose StatementBegin\nINSERT INTO owners(owner_name, owner_type) \n    VALUES ('lucas', 'user'), ('space', 'organization');\n-- +goose StatementEnd\n\nINSERT INTO owners(owner_name, owner_type) \n    VALUES ('james', 'user'), ('pressly', 'organization');\n\nINSERT INTO repos(repo_full_name, repo_owner_id) \n    VALUES ('james/rover', 3), ('pressly/goose', 4);\n\n-- +goose Down\n-- +goose StatementBegin\nDELETE FROM owners;\n-- +goose StatementEnd\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/postgres/00003_alter.sql",
    "content": "-- +goose Up\n-- +goose StatementBegin\nALTER TABLE repos \n    ADD COLUMN IF NOT EXISTS homepage_url text,\n    ADD COLUMN is_private boolean NOT NULL DEFAULT false;\n-- +goose StatementEnd\n\n-- +goose Down\n-- +goose StatementBegin\nALTER TABLE repos\n    DROP COLUMN IF EXISTS homepage_url,\n    DROP COLUMN is_private;\n-- +goose StatementEnd\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/postgres/00004_empty.sql",
    "content": "-- +goose Up\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/postgres/00005_no_tx.sql",
    "content": "-- +goose NO TRANSACTION\n\n-- +goose Up\nCREATE UNIQUE INDEX CONCURRENTLY ON owners(owner_name);\n\n-- +goose Down\nDROP INDEX IF EXISTS owners_owner_name_idx;\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/postgres/00006_complex.sql",
    "content": "-- +goose up\n-- +goose statementbegin\nCREATE OR REPLACE FUNCTION insert_repository(\n    p_repo_full_name TEXT,\n    p_owner_name TEXT,\n    p_owner_type OWNER_TYPE\n) RETURNS VOID AS $$\nDECLARE\n    v_owner_id BIGINT;\n    v_repo_id BIGINT;\nBEGIN\n    -- Check if the owner already exists\n    SELECT owner_id INTO v_owner_id\n    FROM owners\n    WHERE owner_name = p_owner_name AND owner_type = p_owner_type;\n\n    -- If the owner does not exist, insert a new owner\n    IF v_owner_id IS NULL THEN\n        INSERT INTO owners (owner_name, owner_type)\n        VALUES (p_owner_name, p_owner_type)\n        RETURNING owner_id INTO v_owner_id;\n    END IF;\n\n    -- Insert the repository using the obtained owner_id\n    INSERT INTO repos (repo_full_name, repo_owner_id)\n    VALUES (p_repo_full_name, v_owner_id)\n    RETURNING repo_id INTO v_repo_id;\n\n    -- Commit the transaction\n    COMMIT;\nEND;\n$$ LANGUAGE plpgsql;\n-- +goose statementend\n\n-- +goose down\nDROP FUNCTION IF EXISTS insert_repository(TEXT, TEXT, OWNER_TYPE);\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/spanner/00001_table.sql",
    "content": "-- +goose NO TRANSACTION\n-- +goose Up\n-- +goose StatementBegin\nCREATE TABLE owners (\n    owner_id INT64 NOT NULL,\n    owner_name STRING(255) NOT NULL,\n    owner_type STRING(50) NOT NULL,\n) PRIMARY KEY(owner_id)\n-- +goose StatementEnd\n\n-- +goose Down\n-- +goose StatementBegin\nDROP TABLE owners\n-- +goose StatementEnd\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/spanner/00002_insert.sql",
    "content": "-- +goose Up\n-- +goose StatementBegin\nINSERT INTO owners (owner_id, owner_name, owner_type) VALUES\n  (1, 'lucas', 'user'),\n  (2, 'space', 'organization'),\n  (3, 'james', 'user'),\n  (4, 'pressly', 'organization');\n-- +goose StatementEnd\n\n-- +goose Down\n-- +goose StatementBegin\nDELETE FROM owners WHERE owner_id IS NOT NULL\n-- +goose StatementEnd\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/spanner/00003_alter.sql",
    "content": "-- +goose NO TRANSACTION\n-- +goose Up\n-- +goose StatementBegin\nALTER TABLE owners ADD COLUMN homepage_url STRING(255)\n-- +goose StatementEnd\n\n-- +goose Down\n-- +goose StatementBegin\nALTER TABLE owners DROP COLUMN homepage_url\n-- +goose StatementEnd\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/spanner/00004_empty.sql",
    "content": "-- +goose Up\n-- no-op\n\n-- +goose Down\n-- no-op\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/spanner/00005_no_tx.sql",
    "content": "-- +goose NO TRANSACTION\n\n-- +goose Up\n-- +goose StatementBegin\nCREATE UNIQUE NULL_FILTERED INDEX owners_owner_name_idx ON owners (owner_name)\n-- +goose StatementEnd\n\n-- +goose Down\n-- +goose StatementBegin\nDROP INDEX owners_owner_name_idx\n-- +goose StatementEnd\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/spanner/00006_view.sql",
    "content": "-- +goose NO TRANSACTION\n\n-- +goose Up\n-- +goose StatementBegin\nCREATE VIEW view_owners\nSQL SECURITY INVOKER AS\nSELECT\n  owners.owner_id,\n  owners.owner_name,\n  owners.owner_type\nFROM owners\n-- +goose StatementEnd\n\n-- +goose Down\n-- +goose StatementBegin\nDROP VIEW view_owners\n-- +goose StatementEnd\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/starrocks/00001_a.sql",
    "content": "-- +goose Up\n-- +goose StatementBegin\nCREATE SCHEMA IF NOT EXISTS testing;\n-- +goose StatementEnd\n\n-- +goose Down\n-- +goose StatementBegin\nDROP SCHEMA IF EXISTS testing;\n-- +goose StatementEnd\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/starrocks/00002_b.sql",
    "content": "-- +goose Up\n-- +goose StatementBegin\nCREATE TABLE testing.test_migrations_1 (\n\t\tversion_id bigint NOT NULL,\n\t\tid bigint NOT NULL AUTO_INCREMENT,\n\t\tis_applied boolean NOT NULL,\n\t\ttstamp datetime NULL default CURRENT_TIMESTAMP\n\t)\n\tPRIMARY KEY (version_id,id)\n\tDISTRIBUTED BY HASH (id)\n\tORDER BY (version_id);\n-- +goose StatementEnd\n-- +goose StatementBegin\nCREATE TABLE testing.test_migrations_2 (\n\t\tversion_id bigint NOT NULL,\n\t\tid bigint NOT NULL AUTO_INCREMENT,\n\t\tis_applied boolean NOT NULL,\n\t\ttstamp datetime NULL default CURRENT_TIMESTAMP\n\t)\n\tPRIMARY KEY (version_id,id)\n\tDISTRIBUTED BY HASH (id)\n\tORDER BY (version_id);\n-- +goose StatementEnd\n\n-- +goose Down\n-- +goose StatementBegin\nDROP TABLE IF EXISTS testing.test_migrations_1;\n-- +goose StatementEnd\n-- +goose StatementBegin\nDROP TABLE IF EXISTS testing.test_migrations_2;\n-- +goose StatementEnd\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/starrocks/00003_c.sql",
    "content": "-- +goose Up\n-- +goose StatementBegin\nINSERT INTO testing.test_migrations_1 (version_id, is_applied) VALUES (1, true);\n-- +goose StatementEnd\n-- +goose StatementBegin\nINSERT INTO testing.test_migrations_1 (version_id, is_applied) VALUES (2, true);\n-- +goose StatementEnd\n-- +goose StatementBegin\nINSERT INTO testing.test_migrations_1 (version_id, is_applied) VALUES (3, true);\n-- +goose StatementEnd\n\n-- +goose Down\n-- +goose StatementBegin\nDELETE FROM testing.test_migrations_1 WHERE version_id < 10;\n-- +goose StatementEnd\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/turso/00001_table.sql",
    "content": "-- +goose Up\n-- +goose StatementBegin\nCREATE TABLE owners (\n    owner_id INTEGER PRIMARY KEY AUTOINCREMENT,\n    owner_name TEXT NOT NULL,\n    owner_type TEXT CHECK(owner_type IN ('user', 'organization')) NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS repos (\n    repo_id INTEGER PRIMARY KEY AUTOINCREMENT,\n    repo_full_name TEXT NOT NULL,\n    repo_owner_id INTEGER NOT NULL REFERENCES owners(owner_id) ON DELETE CASCADE\n);\n-- +goose StatementEnd\n\n-- +goose Down\n-- +goose StatementBegin\nDROP TABLE IF EXISTS repos;\nDROP TABLE IF EXISTS owners;\n-- +goose StatementEnd\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/turso/00002_insert.sql",
    "content": "-- +goose Up\n-- +goose StatementBegin\nINSERT INTO owners(owner_name, owner_type) \nVALUES \n    ('lucas', 'user'),\n    ('space', 'organization'),\n    ('james', 'user'),\n    ('pressly', 'organization');\n-- +goose StatementEnd\n\nINSERT INTO repos(repo_full_name, repo_owner_id) \nVALUES \n    ('james/rover', (SELECT owner_id FROM owners WHERE owner_name = 'james')),\n    ('pressly/goose', (SELECT owner_id FROM owners WHERE owner_name = 'pressly'));\n\n-- +goose Down\n-- +goose StatementBegin\nDELETE FROM owners;\n-- +goose StatementEnd\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/turso/00003_alter.sql",
    "content": "-- +goose Up\n-- +goose StatementBegin\nALTER TABLE repos \nADD COLUMN homepage_url TEXT;\n\nALTER TABLE repos \nADD COLUMN is_private BOOLEAN DEFAULT 0;\n-- +goose StatementEnd\n\n-- +goose Down\n-- +goose StatementBegin\nALTER TABLE repos\nDROP COLUMN homepage_url;\n\nALTER TABLE repos\nDROP COLUMN is_private;\n-- +goose StatementEnd\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/turso/00004_empty.sql",
    "content": "-- +goose Up\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/turso/00005_no_tx.sql",
    "content": "-- +goose NO TRANSACTION\n\n-- +goose Up\nCREATE UNIQUE INDEX IF NOT EXISTS idx_owners_owner_name ON owners(owner_name);\n\n\n-- +goose Down\nDROP INDEX IF EXISTS idx_owners_owner_name;\n\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/ydb/00001_a.sql",
    "content": "-- +goose Up\n-- +goose StatementBegin\nCREATE TABLE owners (\n    owner_id Uint64,\n    owner_name Utf8,\n    owner_type Utf8,\n    PRIMARY KEY (owner_id)\n);\nCREATE TABLE repos (\n    repo_id Uint64,\n    repo_owner_id Uint64,\n    repo_full_name Utf8,\n    PRIMARY KEY (repo_id)\n);\n-- +goose StatementEnd\n\n-- +goose Down\n-- +goose StatementBegin\nDROP TABLE repos;\nDROP TABLE owners;\n-- +goose StatementEnd\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/ydb/00002_b.sql",
    "content": "-- +goose Up\n-- +goose StatementBegin\nINSERT INTO owners(owner_id, owner_name, owner_type)\nVALUES (1, 'lucas', 'user'), (2, 'space', 'organization');\n-- +goose StatementEnd\n\n-- +goose Down\n-- +goose StatementBegin\nDELETE FROM owners;\n-- +goose StatementEnd\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/ydb/00003_c.sql",
    "content": "\n-- +goose Up\n-- +goose StatementBegin\nINSERT INTO owners(owner_id, owner_name, owner_type)\nVALUES (3, 'james', 'user'), (4, 'pressly', 'organization');\nINSERT INTO repos(repo_id, repo_full_name, repo_owner_id)\nVALUES (1, 'james/rover', 3), (2, 'pressly/goose', 4);\n-- +goose StatementEnd\n\n-- +goose Down\n-- +goose StatementBegin\nDELETE FROM owners WHERE (owner_id = 3 OR owner_id = 4);\nDELETE FROM repos WHERE (repo_id = 1 OR repo_id = 2);\n-- +goose StatementEnd\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/ydb/00004_d.sql",
    "content": "-- +goose Up\n-- +goose StatementBegin\nALTER TABLE repos\n    ADD COLUMN homepage_url Utf8,\n    ADD COLUMN is_private Bool;\n-- +goose StatementEnd\n\n-- +goose Down\n-- +goose StatementBegin\nALTER TABLE repos\n    DROP COLUMN  homepage_url,\n    DROP COLUMN is_private;\n-- +goose StatementEnd\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/ydb/00005_e.sql",
    "content": "-- +goose Up\n-- +goose StatementBegin\n-- NOTE: intentionally left blank to verify migration logic.\nSELECT 'up SQL query';\n-- +goose StatementEnd\n\n-- +goose Down\n-- +goose StatementBegin\n-- NOTE: intentionally left blank to verify migration logic.\nSELECT 'down SQL query';\n-- +goose StatementEnd\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/ydb/00006_f.sql",
    "content": "-- +goose Up\n-- +goose StatementBegin\nCREATE TABLE stargazers (\n    stargazer_repo_id Uint64,\n    stargazer_owner_id UInt64,\n    stargazer_starred_at Timestamp,\n    stargazer_location Utf8,\n    PRIMARY KEY (stargazer_repo_id, stargazer_owner_id)\n);\n-- +goose StatementEnd\n\n-- +goose Down\n-- +goose StatementBegin\nDROP TABLE stargazers;\n-- +goose StatementEnd\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/ydb/00007_g.sql",
    "content": "-- +goose Up\n-- +goose StatementBegin\nCREATE TABLE issues (\n    issue_id Uint64,\n    issue_created_by Uint64,\n    issue_repo_id Uint64,\n    issue_created_at Timestamp,\n    issue_description Utf8,\n    PRIMARY KEY (issue_id)\n);\n-- +goose StatementEnd\n\n-- +goose Down\n-- +goose StatementBegin\nDROP TABLE issues;\n-- +goose StatementEnd\n"
  },
  {
    "path": "internal/testing/integration/testdata/migrations/ydb/00008_h.sql",
    "content": "-- +goose Up\n-- +goose StatementBegin\nALTER TABLE stargazers DROP COLUMN stargazer_location;\n-- +goose StatementEnd\n\n-- +goose Down\n-- +goose StatementBegin\nALTER TABLE stargazers ADD COLUMN stargazer_location Utf8;\n-- +goose StatementEnd\n"
  },
  {
    "path": "internal/testing/testdb/clickhouse.go",
    "content": "package testdb\n\nimport (\n\t\"crypto/tls\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"log\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/ClickHouse/clickhouse-go/v2\"\n\t\"github.com/ory/dockertest/v3\"\n\t\"github.com/ory/dockertest/v3/docker\"\n)\n\nconst (\n\t// https://hub.docker.com/r/clickhouse/clickhouse-server/\n\tCLICKHOUSE_IMAGE   = \"clickhouse/clickhouse-server\"\n\tCLICKHOUSE_VERSION = \"24-alpine\"\n\n\tCLICKHOUSE_DB                        = \"clickdb\"\n\tCLICKHOUSE_USER                      = \"clickuser\"\n\tCLICKHOUSE_PASSWORD                  = \"password1\"\n\tCLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT = \"1\"\n)\n\nfunc newClickHouse(opts ...OptionsFunc) (*sql.DB, func(), error) {\n\toption := &options{}\n\tfor _, f := range opts {\n\t\tf(option)\n\t}\n\t// Uses a sensible default on windows (tcp/http) and linux/osx (socket).\n\tpool, err := dockertest.NewPool(\"\")\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\trunOptions := &dockertest.RunOptions{\n\t\tRepository: CLICKHOUSE_IMAGE,\n\t\tTag:        CLICKHOUSE_VERSION,\n\t\tEnv: []string{\n\t\t\t\"CLICKHOUSE_DB=\" + CLICKHOUSE_DB,\n\t\t\t\"CLICKHOUSE_USER=\" + CLICKHOUSE_USER,\n\t\t\t\"CLICKHOUSE_PASSWORD=\" + CLICKHOUSE_PASSWORD,\n\t\t\t\"CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=\" + CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT,\n\t\t},\n\t\tLabels:       map[string]string{\"goose_test\": \"1\"},\n\t\tPortBindings: make(map[docker.Port][]docker.PortBinding),\n\t}\n\t// Port 8123 is used for HTTP, but we're using the TCP protocol endpoint (port 9000).\n\t// Ref: https://clickhouse.com/docs/en/interfaces/http/\n\t// Ref: https://clickhouse.com/docs/en/interfaces/tcp/\n\tif option.bindPort > 0 {\n\t\trunOptions.PortBindings[docker.Port(\"9000/tcp\")] = []docker.PortBinding{\n\t\t\t{HostPort: strconv.Itoa(option.bindPort)},\n\t\t}\n\t}\n\tcontainer, err := pool.RunWithOptions(\n\t\trunOptions,\n\t\tfunc(config *docker.HostConfig) {\n\t\t\t// Set AutoRemove to true so that stopped container goes away by itself.\n\t\t\tconfig.AutoRemove = true\n\t\t\tconfig.RestartPolicy = docker.RestartPolicy{Name: \"no\"}\n\t\t},\n\t)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tcleanup := func() {\n\t\tif option.debug {\n\t\t\t// User must manually delete the Docker container.\n\t\t\treturn\n\t\t}\n\t\tif err := pool.Purge(container); err != nil {\n\t\t\tlog.Printf(\"failed to purge resource: %v\", err)\n\t\t}\n\t}\n\t// Fetch port assigned to container\n\taddress := fmt.Sprintf(\"%s:%s\", \"localhost\", container.GetPort(\"9000/tcp\"))\n\n\tvar db *sql.DB\n\t// Exponential backoff-retry, because the application in the container\n\t// might not be ready to accept connections yet.\n\tif err := pool.Retry(func() error {\n\t\tdb = clickHouseOpenDB(address, nil, option.debug)\n\t\treturn db.Ping()\n\t}); err != nil {\n\t\treturn nil, cleanup, fmt.Errorf(\"could not connect to docker database: %w\", err)\n\t}\n\treturn db, cleanup, nil\n}\n\nfunc clickHouseOpenDB(address string, tlsConfig *tls.Config, debug bool) *sql.DB {\n\tdb := clickhouse.OpenDB(&clickhouse.Options{\n\t\tAddr: []string{address},\n\t\tAuth: clickhouse.Auth{\n\t\t\tDatabase: CLICKHOUSE_DB,\n\t\t\tUsername: CLICKHOUSE_USER,\n\t\t\tPassword: CLICKHOUSE_PASSWORD,\n\t\t},\n\t\tTLS: tlsConfig,\n\t\tSettings: clickhouse.Settings{\n\t\t\t\"max_execution_time\": 60,\n\t\t},\n\t\tDialTimeout: 5 * time.Second,\n\t\tCompression: &clickhouse.Compression{\n\t\t\tMethod: clickhouse.CompressionLZ4,\n\t\t},\n\t\tDebug: debug,\n\t})\n\tdb.SetMaxIdleConns(5)\n\tdb.SetMaxOpenConns(10)\n\tdb.SetConnMaxLifetime(time.Hour)\n\treturn db\n}\n"
  },
  {
    "path": "internal/testing/testdb/container_healthcheck.go",
    "content": "package testdb\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/ory/dockertest/v3\"\n\t\"github.com/ory/dockertest/v3/docker/types\"\n)\n\n// containerWaitHealthy waits until docker container with specified id is healthy\nfunc containerWaitHealthy(ctx context.Context, pool *dockertest.Pool, id string) error {\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\t\tdefault:\n\t\t\tattemptCtx, attemptCancel := context.WithTimeout(ctx, time.Second)\n\t\t\tstatus, err := containerHealthStatus(attemptCtx, pool, id)\n\t\t\tattemptCancel()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif status == types.Healthy {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc containerHealthStatus(ctx context.Context, pool *dockertest.Pool, id string) (string, error) {\n\tcurrentContainer, err := pool.Client.InspectContainerWithContext(id, ctx)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn currentContainer.State.Health.Status, nil\n\n}\n"
  },
  {
    "path": "internal/testing/testdb/mariadb.go",
    "content": "package testdb\n\nimport (\n\t\"database/sql\"\n\t\"fmt\"\n\t\"log\"\n\t\"strconv\"\n\t\"time\"\n\n\t_ \"github.com/go-sql-driver/mysql\"\n\t\"github.com/ory/dockertest/v3\"\n\t\"github.com/ory/dockertest/v3/docker\"\n)\n\nconst (\n\t// https://hub.docker.com/_/mariadb\n\tMARIADB_IMAGE   = \"mariadb\"\n\tMARIADB_VERSION = \"11\"\n\n\tMARIADB_DB       = \"testdb\"\n\tMARIADB_USER     = \"tester\"\n\tMARIADB_PASSWORD = \"password1\"\n)\n\nfunc newMariaDB(opts ...OptionsFunc) (*sql.DB, func(), error) {\n\toption := &options{}\n\tfor _, f := range opts {\n\t\tf(option)\n\t}\n\t// Uses a sensible default on windows (tcp/http) and linux/osx (socket).\n\tpool, err := dockertest.NewPool(\"\")\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to connect to docker: %v\", err)\n\t}\n\toptions := &dockertest.RunOptions{\n\t\tRepository: MARIADB_IMAGE,\n\t\tTag:        MARIADB_VERSION,\n\t\tEnv: []string{\n\t\t\t\"MARIADB_USER=\" + MARIADB_USER,\n\t\t\t\"MARIADB_PASSWORD=\" + MARIADB_PASSWORD,\n\t\t\t\"MARIADB_ROOT_PASSWORD=\" + MARIADB_PASSWORD,\n\t\t\t\"MARIADB_DATABASE=\" + MARIADB_DB,\n\t\t},\n\t\tLabels:       map[string]string{\"goose_test\": \"1\"},\n\t\tPortBindings: make(map[docker.Port][]docker.PortBinding),\n\t}\n\tif option.bindPort > 0 {\n\t\toptions.PortBindings[docker.Port(\"3306/tcp\")] = []docker.PortBinding{\n\t\t\t{HostPort: strconv.Itoa(option.bindPort)},\n\t\t}\n\t}\n\tcontainer, err := pool.RunWithOptions(\n\t\toptions,\n\t\tfunc(config *docker.HostConfig) {\n\t\t\t// Set AutoRemove to true so that stopped container goes away by itself.\n\t\t\tconfig.AutoRemove = true\n\t\t\t// config.PortBindings = options.PortBindings\n\t\t\tconfig.RestartPolicy = docker.RestartPolicy{Name: \"no\"}\n\t\t},\n\t)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to create docker container: %v\", err)\n\t}\n\tcleanup := func() {\n\t\tif option.debug {\n\t\t\t// User must manually delete the Docker container.\n\t\t\treturn\n\t\t}\n\t\tif err := pool.Purge(container); err != nil {\n\t\t\tlog.Printf(\"failed to purge resource: %v\", err)\n\t\t}\n\t}\n\t// MySQL DSN: username:password@protocol(address)/dbname?param=value\n\tdsn := fmt.Sprintf(\"%s:%s@(%s:%s)/%s?parseTime=true&multiStatements=true\",\n\t\tMARIADB_USER,\n\t\tMARIADB_PASSWORD,\n\t\t\"localhost\",\n\t\tcontainer.GetPort(\"3306/tcp\"), // Fetch port dynamically assigned to container\n\t\tMARIADB_DB,\n\t)\n\tvar db *sql.DB\n\t// Exponential backoff-retry, because the application in the container\n\t// might not be ready to accept connections yet. Add an extra sleep\n\t// because mariadb containers take much longer to startup.\n\ttime.Sleep(5 * time.Second)\n\tif err := pool.Retry(func() error {\n\t\tvar err error\n\t\tdb, err = sql.Open(\"mysql\", dsn)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn db.Ping()\n\t},\n\t); err != nil {\n\t\treturn nil, cleanup, fmt.Errorf(\"could not connect to docker database: %v\", err)\n\t}\n\treturn db, cleanup, nil\n}\n"
  },
  {
    "path": "internal/testing/testdb/options.go",
    "content": "package testdb\n\ntype options struct {\n\tbindPort int\n\tdebug    bool\n}\n\ntype OptionsFunc func(o *options)\n\nfunc WithBindPort(n int) OptionsFunc {\n\treturn func(o *options) { o.bindPort = n }\n}\n\nfunc WithDebug(b bool) OptionsFunc {\n\treturn func(o *options) { o.debug = b }\n}\n"
  },
  {
    "path": "internal/testing/testdb/postgres.go",
    "content": "package testdb\n\nimport (\n\t\"database/sql\"\n\t\"fmt\"\n\t\"log\"\n\t\"strconv\"\n\n\t_ \"github.com/jackc/pgx/v5/stdlib\"\n\t\"github.com/ory/dockertest/v3\"\n\t\"github.com/ory/dockertest/v3/docker\"\n)\n\nconst (\n\t// https://hub.docker.com/_/postgres\n\tPOSTGRES_IMAGE   = \"postgres\"\n\tPOSTGRES_VERSION = \"16-alpine\"\n\n\tPOSTGRES_DB       = \"testdb\"\n\tPOSTGRES_USER     = \"postgres\"\n\tPOSTGRES_PASSWORD = \"password1\"\n)\n\nfunc newPostgres(opts ...OptionsFunc) (*sql.DB, func(), error) {\n\toption := &options{}\n\tfor _, f := range opts {\n\t\tf(option)\n\t}\n\t// Uses a sensible default on windows (tcp/http) and linux/osx (socket).\n\tpool, err := dockertest.NewPool(\"\")\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to connect to docker: %v\", err)\n\t}\n\toptions := &dockertest.RunOptions{\n\t\tRepository: POSTGRES_IMAGE,\n\t\tTag:        POSTGRES_VERSION,\n\t\tEnv: []string{\n\t\t\t\"POSTGRES_USER=\" + POSTGRES_USER,\n\t\t\t\"POSTGRES_PASSWORD=\" + POSTGRES_PASSWORD,\n\t\t\t\"POSTGRES_DB=\" + POSTGRES_DB,\n\t\t\t\"listen_addresses = '*'\",\n\t\t},\n\t\tLabels:       map[string]string{\"goose_test\": \"1\"},\n\t\tPortBindings: make(map[docker.Port][]docker.PortBinding),\n\t}\n\tif option.bindPort > 0 {\n\t\toptions.PortBindings[docker.Port(\"5432/tcp\")] = []docker.PortBinding{\n\t\t\t{HostPort: strconv.Itoa(option.bindPort)},\n\t\t}\n\t}\n\tcontainer, err := pool.RunWithOptions(\n\t\toptions,\n\t\tfunc(config *docker.HostConfig) {\n\t\t\t// Set AutoRemove to true so that stopped container goes away by itself.\n\t\t\tconfig.AutoRemove = true\n\t\t\tconfig.RestartPolicy = docker.RestartPolicy{Name: \"no\"}\n\t\t},\n\t)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to create docker container: %v\", err)\n\t}\n\tcleanup := func() {\n\t\tif option.debug {\n\t\t\t// User must manually delete the Docker container.\n\t\t\treturn\n\t\t}\n\t\tif err := pool.Purge(container); err != nil {\n\t\t\tlog.Printf(\"failed to purge resource: %v\", err)\n\t\t}\n\t}\n\tpsqlInfo := fmt.Sprintf(\"host=%s port=%s user=%s password=%s dbname=%s sslmode=disable\",\n\t\t\"localhost\",\n\t\tcontainer.GetPort(\"5432/tcp\"), // Fetch port dynamically assigned to container\n\t\tPOSTGRES_USER,\n\t\tPOSTGRES_PASSWORD,\n\t\tPOSTGRES_DB,\n\t)\n\tvar db *sql.DB\n\t// Exponential backoff-retry, because the application in the container\n\t// might not be ready to accept connections yet.\n\tif err := pool.Retry(\n\t\tfunc() error {\n\t\t\tvar err error\n\t\t\tdb, err = sql.Open(\"pgx\", psqlInfo)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn db.Ping()\n\t\t},\n\t); err != nil {\n\t\treturn nil, cleanup, fmt.Errorf(\"could not connect to docker database: %v\", err)\n\t}\n\treturn db, cleanup, nil\n}\n"
  },
  {
    "path": "internal/testing/testdb/spanner.go",
    "content": "package testdb\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t_ \"github.com/googleapis/go-sql-spanner\" // Spanner driver\n\t\"github.com/ory/dockertest/v3\"\n\t\"github.com/ory/dockertest/v3/docker\"\n\n\tdatabase \"cloud.google.com/go/spanner/admin/database/apiv1\"\n\tdbpb \"cloud.google.com/go/spanner/admin/database/apiv1/databasepb\"\n\tinstance \"cloud.google.com/go/spanner/admin/instance/apiv1\"\n\tinspb \"cloud.google.com/go/spanner/admin/instance/apiv1/instancepb\"\n)\n\nconst (\n\tSPANNER_IMAGE   = \"gcr.io/cloud-spanner-emulator/emulator\"\n\tSPANNER_VERSION = \"latest\"\n\n\tSPANNER_PROJECT  = \"test-project\"\n\tSPANNER_INSTANCE = \"test-instance\"\n\tSPANNER_DATABASE = \"test-db\"\n)\n\nfunc newSpanner(opts ...OptionsFunc) (*sql.DB, func(), error) {\n\toption := &options{}\n\tfor _, f := range opts {\n\t\tf(option)\n\t}\n\n\tpool, err := dockertest.NewPool(\"\")\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to connect to docker: %v\", err)\n\t}\n\n\tresource, err := pool.RunWithOptions(\n\t\t&dockertest.RunOptions{\n\t\t\tRepository: SPANNER_IMAGE,\n\t\t\tTag:        SPANNER_VERSION,\n\t\t\tExposedPorts: []string{\n\t\t\t\t\"9010/tcp\", \"9020/tcp\",\n\t\t\t},\n\t\t\tLabels: map[string]string{\"goose_test\": \"1\"},\n\t\t},\n\t\tfunc(config *docker.HostConfig) {\n\t\t\tconfig.AutoRemove = true\n\t\t\tconfig.RestartPolicy = docker.RestartPolicy{Name: \"no\"}\n\t\t},\n\t)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to start spanner emulator container: %v\", err)\n\t}\n\n\thostPort := resource.GetPort(\"9010/tcp\")\n\temulatorHost := fmt.Sprintf(\"localhost:%s\", hostPort)\n\tos.Setenv(\"SPANNER_EMULATOR_HOST\", emulatorHost)\n\n\t// Provision instance + database inside emulator\n\terr = pool.Retry(func() error {\n\t\tctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)\n\t\tdefer cancel()\n\n\t\tif err := createSpannerResources(ctx); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tdsn := fmt.Sprintf(\"projects/%s/instances/%s/databases/%s\", SPANNER_PROJECT, SPANNER_INSTANCE, SPANNER_DATABASE)\n\t\tdb, err := sql.Open(\"spanner\", dsn)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer db.Close()\n\t\treturn db.PingContext(ctx)\n\t})\n\tif err != nil {\n\t\t_ = pool.Purge(resource)\n\t\treturn nil, nil, fmt.Errorf(\"could not initialize spanner emulator: %v\", err)\n\t}\n\n\tdsn := fmt.Sprintf(\"projects/%s/instances/%s/databases/%s\", SPANNER_PROJECT, SPANNER_INSTANCE, SPANNER_DATABASE)\n\tdb, err := sql.Open(\"spanner\", dsn)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to open spanner DB: %v\", err)\n\t}\n\n\tcleanup := func() {\n\t\tif err := db.Close(); err != nil {\n\t\t\tlog.Printf(\"failed to close spanner db: %v\", err)\n\t\t}\n\t\tif err := pool.Purge(resource); err != nil {\n\t\t\tlog.Printf(\"failed to purge spanner emulator container: %v\", err)\n\t\t}\n\t}\n\n\treturn db, cleanup, nil\n}\n\nfunc createSpannerResources(ctx context.Context) error {\n\tinstClient, err := instance.NewInstanceAdminClient(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"create instance client failed: %w\", err)\n\t}\n\tdefer instClient.Close()\n\n\tdbClient, err := database.NewDatabaseAdminClient(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"create database client failed: %w\", err)\n\t}\n\tdefer dbClient.Close()\n\n\tinstReq := &inspb.CreateInstanceRequest{\n\t\tParent:     \"projects/\" + SPANNER_PROJECT,\n\t\tInstanceId: SPANNER_INSTANCE,\n\t\tInstance: &inspb.Instance{\n\t\t\tConfig:      \"projects/\" + SPANNER_PROJECT + \"/instanceConfigs/emulator-config\",\n\t\t\tDisplayName: \"Test Instance\",\n\t\t\tNodeCount:   1,\n\t\t},\n\t}\n\tif _, err = instClient.CreateInstance(ctx, instReq); err != nil &&\n\t\t!strings.Contains(err.Error(), \"AlreadyExists\") {\n\t\treturn fmt.Errorf(\"create instance failed: %w\", err)\n\t}\n\n\tdbReq := &dbpb.CreateDatabaseRequest{\n\t\tParent:          fmt.Sprintf(\"projects/%s/instances/%s\", SPANNER_PROJECT, SPANNER_INSTANCE),\n\t\tCreateStatement: \"CREATE DATABASE `\" + SPANNER_DATABASE + \"`\",\n\t}\n\tif _, err = dbClient.CreateDatabase(ctx, dbReq); err != nil &&\n\t\t!strings.Contains(err.Error(), \"AlreadyExists\") {\n\t\treturn fmt.Errorf(\"create database failed: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/testing/testdb/starrocks.go",
    "content": "package testdb\n\nimport (\n\t\"database/sql\"\n\t\"fmt\"\n\t\"log\"\n\t\"strconv\"\n\t\"time\"\n\n\t_ \"github.com/go-sql-driver/mysql\"\n\t\"github.com/ory/dockertest/v3\"\n\t\"github.com/ory/dockertest/v3/docker\"\n)\n\nconst (\n\t// https://hub.docker.com/r/starrocks/allin1-ubuntu\n\tSTARROCKS_IMAGE   = \"starrocks/allin1-ubuntu\"\n\tSTARROCKS_VERSION = \"3.5.11\"\n\n\tSTARROCKS_USER    = \"root\"\n\tSTARROCKS_INIT_DB = \"migrations\"\n)\n\nfunc newStarrocks(opts ...OptionsFunc) (*sql.DB, func(), error) {\n\toption := &options{}\n\tfor _, f := range opts {\n\t\tf(option)\n\t}\n\t// Uses a sensible default on windows (tcp/http) and linux/osx (socket).\n\tpool, err := dockertest.NewPool(\"\")\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to connect to docker: %v\", err)\n\t}\n\n\toptions := &dockertest.RunOptions{\n\t\tRepository:   STARROCKS_IMAGE,\n\t\tTag:          STARROCKS_VERSION,\n\t\tLabels:       map[string]string{\"goose_test\": \"1\"},\n\t\tPortBindings: make(map[docker.Port][]docker.PortBinding),\n\t\tExposedPorts: []string{\"9030/tcp\"},\n\t}\n\tif option.bindPort > 0 {\n\t\toptions.PortBindings[docker.Port(\"9030/tcp\")] = []docker.PortBinding{\n\t\t\t{HostPort: strconv.Itoa(option.bindPort)},\n\t\t}\n\t}\n\tcontainer, err := pool.RunWithOptions(\n\t\toptions,\n\t\tfunc(config *docker.HostConfig) {\n\t\t\t// Set AutoRemove to true so that stopped container goes away by itself.\n\t\t\tconfig.AutoRemove = true\n\t\t\tconfig.RestartPolicy = docker.RestartPolicy{Name: \"no\"}\n\t\t},\n\t)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to create docker container: %v\", err)\n\t}\n\tcleanup := func() {\n\t\tif option.debug {\n\t\t\t// User must manually delete the Docker container.\n\t\t\treturn\n\t\t}\n\t\tif err := pool.Purge(container); err != nil {\n\t\t\tlog.Printf(\"failed to purge resource: %v\", err)\n\t\t}\n\t}\n\tdsn := fmt.Sprintf(\"%s:%s@(%s:%s)/%s?parseTime=true&interpolateParams=true\",\n\t\tSTARROCKS_USER,\n\t\t\"\",\n\t\t\"localhost\",\n\t\tcontainer.GetPort(\"9030/tcp\"), // Fetch port dynamically assigned to container,\n\t\t\"\",\n\t)\n\tvar db *sql.DB\n\n\t// Exponential backoff-retry, because the application in the container\n\t// might not be ready to accept connections yet. Add an extra sleep\n\t// because container take much longer to startup.\n\tpool.MaxWait = time.Minute * 2\n\tif err := pool.Retry(func() error {\n\t\tvar err error\n\t\tdb, err = sql.Open(\"mysql\", dsn)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t_, err = db.Exec(\"CREATE DATABASE IF NOT EXISTS \" + STARROCKS_INIT_DB)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"could not create initial database: %v\", err)\n\t\t}\n\t\t_, err = db.Exec(\"USE \" + STARROCKS_INIT_DB)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"could not set default initial database: %v\", err)\n\t\t}\n\n\t\treturn db.Ping()\n\t},\n\t); err != nil {\n\t\treturn nil, cleanup, fmt.Errorf(\"could not connect to docker database: %v\", err)\n\t}\n\n\treturn db, cleanup, nil\n}\n"
  },
  {
    "path": "internal/testing/testdb/testdb.go",
    "content": "package testdb\n\nimport \"database/sql\"\n\n// NewClickHouse starts a ClickHouse docker container. Returns db connection and a docker cleanup function.\nfunc NewClickHouse(options ...OptionsFunc) (db *sql.DB, cleanup func(), err error) {\n\treturn newClickHouse(options...)\n}\n\n// NewPostgres starts a PostgreSQL docker container. Returns db connection and a docker cleanup function.\nfunc NewPostgres(options ...OptionsFunc) (db *sql.DB, cleanup func(), err error) {\n\treturn newPostgres(options...)\n}\n\n// NewSpanner starts a Spanner docker container. Returns db connection and a docker cleanup function.\nfunc NewSpanner(options ...OptionsFunc) (db *sql.DB, cleanup func(), err error) {\n\treturn newSpanner(options...)\n}\n\n// NewMariaDB starts a MariaDB docker container. Returns a db connection and a docker cleanup function.\nfunc NewMariaDB(options ...OptionsFunc) (db *sql.DB, cleanup func(), err error) {\n\treturn newMariaDB(options...)\n}\n\n// NewYdb starts a YDB docker container. Returns db connection and a docker cleanup function.\nfunc NewYdb(options ...OptionsFunc) (db *sql.DB, cleanup func(), err error) {\n\treturn newYdb(options...)\n}\n\n// NewStarrocks starts a Starrocks docker container. Returns db connection and a docker cleanup function.\nfunc NewStarrocks(options ...OptionsFunc) (db *sql.DB, cleanup func(), err error) {\n\treturn newStarrocks(options...)\n}\n"
  },
  {
    "path": "internal/testing/testdb/turso.go",
    "content": "package testdb\n\nimport (\n\t\"database/sql\"\n\t\"fmt\"\n\t\"log\"\n\t\"strconv\"\n\n\t\"github.com/ory/dockertest/v3\"\n\t\"github.com/ory/dockertest/v3/docker\"\n\t_ \"github.com/tursodatabase/libsql-client-go/libsql\"\n)\n\nconst (\n\t// ghcr.io/tursodatabase/libsql-server:v0.23.7\n\tTURSO_IMAGE   = \"ghcr.io/tursodatabase/libsql-server\"\n\tTURSO_VERSION = \"v0.24.7\"\n\tTURSO_PORT    = \"8080\"\n)\n\n// NewTurso starts a Turso docker container. Returns db connection and a docker cleanup function.\nfunc NewTurso(options ...OptionsFunc) (db *sql.DB, cleanup func(), err error) {\n\treturn newTurso(options...)\n}\n\nfunc newTurso(opts ...OptionsFunc) (*sql.DB, func(), error) {\n\toption := &options{}\n\tfor _, f := range opts {\n\t\tf(option)\n\t}\n\t// Uses a sensible default on windows (tcp/http) and linux/osx (socket).\n\tpool, err := dockertest.NewPool(\"\")\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\trunOptions := &dockertest.RunOptions{\n\t\tRepository:   TURSO_IMAGE,\n\t\tTag:          TURSO_VERSION,\n\t\tLabels:       map[string]string{\"goose_test\": \"1\"},\n\t\tPortBindings: make(map[docker.Port][]docker.PortBinding),\n\t}\n\tif option.debug {\n\t\trunOptions.Env = append(runOptions.Env, \"RUST=trace\")\n\t} else {\n\t\trunOptions.Env = append(runOptions.Env, \"RUST=error\")\n\t}\n\tif option.bindPort > 0 {\n\t\trunOptions.PortBindings[TURSO_PORT+\"/tcp\"] = []docker.PortBinding{\n\t\t\t{HostPort: strconv.Itoa(option.bindPort)},\n\t\t}\n\t}\n\tcontainer, err := pool.RunWithOptions(\n\t\trunOptions,\n\t\tfunc(config *docker.HostConfig) {\n\t\t\t// Set AutoRemove to true so that stopped container goes away by itself.\n\t\t\tconfig.AutoRemove = true\n\t\t\tconfig.RestartPolicy = docker.RestartPolicy{Name: \"no\"}\n\t\t},\n\t)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tcleanup := func() {\n\t\tif option.debug {\n\t\t\t// User must manually delete the Docker container.\n\t\t\treturn\n\t\t}\n\t\tif err := pool.Purge(container); err != nil {\n\t\t\tlog.Printf(\"failed to purge resource: %v\", err)\n\t\t}\n\t}\n\t// Fetch port assigned to container\n\n\tvar db *sql.DB\n\t// Exponential backoff-retry, because the application in the container\n\t// might not be ready to accept connections yet.\n\tif err := pool.Retry(func() error {\n\t\tdb, err = tursoOpenDB(container)\n\t\treturn err\n\t}); err != nil {\n\t\treturn nil, cleanup, fmt.Errorf(\"could not connect to docker database: %w\", err)\n\t}\n\treturn db, cleanup, nil\n}\n\nfunc tursoOpenDB(container *dockertest.Resource) (*sql.DB, error) {\n\taddress := fmt.Sprintf(\"http://127.0.0.1:%s\", container.GetPort(TURSO_PORT+\"/tcp\"))\n\tdb, err := sql.Open(\"libsql\", address)\n\tif err != nil {\n\t\treturn db, err\n\t}\n\t// let's do a ping to be sure we are connected\n\tvar result int\n\terr = db.QueryRow(\"SELECT 1\").Scan(&result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn db, nil\n}\n"
  },
  {
    "path": "internal/testing/testdb/ydb.go",
    "content": "package testdb\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/ory/dockertest/v3\"\n\t\"github.com/ory/dockertest/v3/docker\"\n\t\"github.com/ydb-platform/ydb-go-sdk/v3\"\n\t\"github.com/ydb-platform/ydb-go-sdk/v3/balancers\"\n\tydblog \"github.com/ydb-platform/ydb-go-sdk/v3/log\"\n\t\"github.com/ydb-platform/ydb-go-sdk/v3/trace\"\n)\n\nconst (\n\tYDB_IMAGE    = \"ghcr.io/ydb-platform/local-ydb\"\n\tYDB_VERSION  = \"24.1\"\n\tYDB_PORT     = \"2136\"\n\tYDB_UI_PORT  = \"8765\"\n\tYDB_DATABASE = \"local\"\n)\n\nfunc newYdb(opts ...OptionsFunc) (*sql.DB, func(), error) {\n\toption := &options{}\n\tfor _, f := range opts {\n\t\tf(option)\n\t}\n\t// Uses a sensible default on windows (tcp/http) and linux/osx (socket).\n\tpool, err := dockertest.NewPool(\"\")\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\trunOptions := &dockertest.RunOptions{\n\t\tRepository: YDB_IMAGE,\n\t\tTag:        YDB_VERSION,\n\t\tEnv: []string{\n\t\t\t\"YDB_USE_IN_MEMORY_PDISKS=true\",\n\t\t\t\"YDB_LOCAL_SURVIVE_RESTART=true\",\n\t\t\t\"GRPC_PORT=\" + YDB_PORT,\n\t\t\t\"MON_PORT=\" + YDB_UI_PORT,\n\t\t},\n\t\tLabels:       map[string]string{\"goose_test\": \"1\"},\n\t\tPortBindings: map[docker.Port][]docker.PortBinding{},\n\t\tMounts:       []string{os.TempDir() + \":/ydb_certs\"},\n\t\tHostname:     \"localhost\",\n\t}\n\tif option.debug {\n\t\trunOptions.Env = append(runOptions.Env, \"YDB_DEFAULT_LOG_LEVEL=NOTICE\")\n\t} else {\n\t\trunOptions.Env = append(runOptions.Env, \"YDB_DEFAULT_LOG_LEVEL=ERROR\")\n\t}\n\tif option.bindPort > 0 {\n\t\trunOptions.PortBindings[YDB_PORT+\"/tcp\"] = []docker.PortBinding{\n\t\t\t{HostPort: strconv.Itoa(option.bindPort)},\n\t\t}\n\t}\n\tcontainer, err := pool.RunWithOptions(\n\t\trunOptions,\n\t\tfunc(config *docker.HostConfig) {\n\t\t\t// Set AutoRemove to true so that stopped container goes away by itself.\n\t\t\tconfig.AutoRemove = true\n\t\t\tconfig.RestartPolicy = docker.RestartPolicy{Name: \"no\"}\n\t\t\tconfig.Init = true\n\t\t},\n\t)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tcleanup := func() {\n\t\tif option.debug {\n\t\t\t// User must manually delete the Docker container.\n\t\t\treturn\n\t\t}\n\t\tif err := pool.Purge(container); err != nil {\n\t\t\tlog.Printf(\"failed to purge resource: %v\", err)\n\t\t}\n\t}\n\t// Fetch port assigned to container\n\tdsn := fmt.Sprintf(\"grpc://%s:%s/%s\",\n\t\t\"localhost\",\n\t\tcontainer.GetPort(YDB_PORT+\"/tcp\"),\n\t\tYDB_DATABASE,\n\t)\n\n\tvar db *sql.DB\n\t// Exponential backoff-retry, because the application in the container\n\t// might not be ready to accept connections yet.\n\tif err := pool.Retry(func() (err error) {\n\t\tctx, cancel := context.WithTimeout(context.Background(), time.Second*5)\n\t\tdefer cancel()\n\t\tif err := containerWaitHealthy(ctx, pool, container.Container.ID); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\topts := []ydb.Option{\n\t\t\tydb.WithBalancer(balancers.SingleConn()),\n\t\t}\n\n\t\tif option.debug {\n\t\t\topts = append(opts, ydb.WithLogger(ydblog.Default(os.Stdout), trace.DetailsAll, ydblog.WithLogQuery()))\n\t\t}\n\n\t\tnativeDriver, err := ydb.Open(ctx, dsn, opts...)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer func() {\n\t\t\tif err != nil {\n\t\t\t\t_ = nativeDriver.Close(context.Background())\n\t\t\t}\n\t\t}()\n\t\tconnector, err := ydb.Connector(nativeDriver,\n\t\t\tydb.WithDefaultQueryMode(ydb.ScriptingQueryMode),\n\t\t\tydb.WithFakeTx(ydb.ScriptingQueryMode),\n\t\t\tydb.WithAutoDeclare(),\n\t\t\tydb.WithNumericArgs(),\n\t\t)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer func() {\n\t\t\tif err != nil {\n\t\t\t\t_ = connector.Close()\n\t\t\t}\n\t\t}()\n\n\t\tdb = sql.OpenDB(connector)\n\t\tdb.SetMaxIdleConns(5)\n\t\tdb.SetMaxOpenConns(10)\n\t\tdb.SetConnMaxLifetime(time.Hour)\n\n\t\terr = db.Ping()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn nil\n\t}); err != nil {\n\t\tcleanup()\n\t\treturn nil, nil, fmt.Errorf(\"could not connect to docker database: %w\", err)\n\t}\n\treturn db, cleanup, nil\n}\n"
  },
  {
    "path": "lock/internal/store/postgres.go",
    "content": "package store\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"go.uber.org/multierr\"\n)\n\n// NewPostgres creates a new Postgres-based [LockStore].\nfunc NewPostgres(tableName string) (LockStore, error) {\n\tif tableName == \"\" {\n\t\treturn nil, errors.New(\"table name must not be empty\")\n\t}\n\treturn &postgresStore{\n\t\ttableName: tableName,\n\t}, nil\n}\n\nvar _ LockStore = (*postgresStore)(nil)\n\ntype postgresStore struct {\n\ttableName string\n}\n\nfunc (s *postgresStore) TableExists(\n\tctx context.Context,\n\tdb *sql.DB,\n) (bool, error) {\n\tvar query string\n\tschemaName, tableName := parseTableIdentifier(s.tableName)\n\tif schemaName != \"\" {\n\t\tq := `SELECT EXISTS ( SELECT 1 FROM pg_tables WHERE schemaname = '%s' AND tablename = '%s' )`\n\t\tquery = fmt.Sprintf(q, schemaName, tableName)\n\t} else {\n\t\tq := `SELECT EXISTS ( SELECT 1 FROM pg_tables WHERE (current_schema() IS NULL OR schemaname = current_schema()) AND tablename = '%s' )`\n\t\tquery = fmt.Sprintf(q, tableName)\n\t}\n\n\tvar exists bool\n\tif err := db.QueryRowContext(ctx, query).Scan(\n\t\t&exists,\n\t); err != nil {\n\t\treturn false, fmt.Errorf(\"table exists: %w\", err)\n\t}\n\treturn exists, nil\n}\n\nfunc (s *postgresStore) CreateLockTable(\n\tctx context.Context,\n\tdb *sql.DB,\n) error {\n\texists, err := s.TableExists(ctx, db)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"check lock table existence: %w\", err)\n\t}\n\tif exists {\n\t\treturn nil\n\t}\n\n\tquery := fmt.Sprintf(`CREATE TABLE %s (\n\t\tlock_id bigint NOT NULL PRIMARY KEY,\n\t\tlocked boolean NOT NULL DEFAULT false,\n\t\tlocked_at timestamptz NULL,\n\t\tlocked_by text NULL,\n\t\tlease_expires_at timestamptz NULL,\n\t\tupdated_at timestamptz NULL\n\t)`, s.tableName)\n\tif _, err := db.ExecContext(ctx, query); err != nil {\n\t\t// Double-check if another process created it concurrently\n\t\tif exists, checkErr := s.TableExists(ctx, db); checkErr == nil && exists {\n\t\t\t// Another process created it, that's fine!\n\t\t\treturn nil\n\t\t}\n\t\treturn fmt.Errorf(\"create lock table %q: %w\", s.tableName, err)\n\t}\n\treturn nil\n}\n\nfunc (s *postgresStore) AcquireLock(\n\tctx context.Context,\n\tdb *sql.DB,\n\tlockID int64,\n\tlockedBy string,\n\tleaseDuration time.Duration,\n) (*AcquireLockResult, error) {\n\tquery := fmt.Sprintf(`INSERT INTO %s (lock_id, locked, locked_at, locked_by, lease_expires_at, updated_at)\n\tVALUES ($1, true, now(), $2, now() + $3::interval, now())\n\tON CONFLICT (lock_id) DO UPDATE SET\n\t\tlocked = true,\n\t\tlocked_at = now(),\n\t\tlocked_by = $2,\n\t\tlease_expires_at = now() + $3::interval,\n\t\tupdated_at = now()\n\tWHERE %s.locked = false OR %s.lease_expires_at < now()\n\tRETURNING locked_by, lease_expires_at`, s.tableName, s.tableName, s.tableName)\n\n\t// Convert duration to PostgreSQL interval format\n\tleaseDurationStr := formatDurationAsInterval(leaseDuration)\n\n\tvar returnedLockedBy string\n\tvar leaseExpiresAt time.Time\n\terr := db.QueryRowContext(ctx, query,\n\t\tlockID,\n\t\tlockedBy,\n\t\tleaseDurationStr,\n\t).Scan(\n\t\t&returnedLockedBy,\n\t\t&leaseExpiresAt,\n\t)\n\tif err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\t// TODO(mf): should we return a special error type here?\n\t\t\treturn nil, fmt.Errorf(\"acquire lock %d: already held by another instance\", lockID)\n\t\t}\n\t\treturn nil, fmt.Errorf(\"acquire lock %d: %w\", lockID, err)\n\t}\n\n\t// Verify we got the lock by checking the returned locked_by matches our instance ID\n\tif returnedLockedBy != lockedBy {\n\t\treturn nil, fmt.Errorf(\"acquire lock %d: acquired by %s instead of %s\", lockID, returnedLockedBy, lockedBy)\n\t}\n\n\treturn &AcquireLockResult{\n\t\tLockedBy:       returnedLockedBy,\n\t\tLeaseExpiresAt: leaseExpiresAt,\n\t}, nil\n}\n\nfunc (s *postgresStore) ReleaseLock(\n\tctx context.Context,\n\tdb *sql.DB,\n\tlockID int64,\n\tlockedBy string,\n) (*ReleaseLockResult, error) {\n\t// Release lock only if it's held by the current instance\n\tquery := fmt.Sprintf(`UPDATE %s SET\n\t\tlocked = false,\n\t\tlocked_at = NULL,\n\t\tlocked_by = NULL,\n\t\tlease_expires_at = NULL,\n\t\tupdated_at = now()\n\tWHERE lock_id = $1 AND locked_by = $2\n\tRETURNING lock_id`, s.tableName)\n\n\tvar returnedLockID int64\n\terr := db.QueryRowContext(ctx, query,\n\t\tlockID,\n\t\tlockedBy,\n\t).Scan(\n\t\t&returnedLockID,\n\t)\n\tif err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\t// TODO(mf): should we return a special error type here?\n\t\t\treturn nil, fmt.Errorf(\"release lock %d: not held by this instance\", lockID)\n\t\t}\n\t\treturn nil, fmt.Errorf(\"release lock %d: %w\", lockID, err)\n\t}\n\n\t// Verify the correct lock was released\n\tif returnedLockID != lockID {\n\t\treturn nil, fmt.Errorf(\"release lock %d: returned lock ID %d does not match\", lockID, returnedLockID)\n\t}\n\n\treturn &ReleaseLockResult{\n\t\tLockID: returnedLockID,\n\t}, nil\n}\n\nfunc (s *postgresStore) UpdateLease(\n\tctx context.Context,\n\tdb *sql.DB,\n\tlockID int64,\n\tlockedBy string,\n\tleaseDuration time.Duration,\n) (*UpdateLeaseResult, error) {\n\t// Update lease expiration time for heartbeat, only if we own the lock\n\tquery := fmt.Sprintf(`UPDATE %s SET\n\t\tlease_expires_at = now() + $1::interval,\n\t\tupdated_at = now()\n\tWHERE lock_id = $2 AND locked_by = $3 AND locked = true\n\tRETURNING lease_expires_at`, s.tableName)\n\n\t// Convert duration to PostgreSQL interval format\n\tintervalStr := formatDurationAsInterval(leaseDuration)\n\n\tvar leaseExpiresAt time.Time\n\terr := db.QueryRowContext(ctx, query,\n\t\tintervalStr,\n\t\tlockID,\n\t\tlockedBy,\n\t).Scan(\n\t\t&leaseExpiresAt,\n\t)\n\tif err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn nil, fmt.Errorf(\"failed to update lease for lock %d: not held by this instance\", lockID)\n\t\t}\n\t\treturn nil, fmt.Errorf(\"failed to update lease for lock %d: %w\", lockID, err)\n\t}\n\n\treturn &UpdateLeaseResult{\n\t\tLeaseExpiresAt: leaseExpiresAt,\n\t}, nil\n}\n\nfunc (s *postgresStore) CheckLockStatus(\n\tctx context.Context,\n\tdb *sql.DB,\n\tlockID int64,\n) (*LockStatus, error) {\n\tquery := fmt.Sprintf(`SELECT locked, locked_by, lease_expires_at, updated_at FROM %s WHERE lock_id = $1`, s.tableName)\n\tvar status LockStatus\n\n\terr := db.QueryRowContext(ctx, query,\n\t\tlockID,\n\t).Scan(\n\t\t&status.Locked,\n\t\t&status.LockedBy,\n\t\t&status.LeaseExpiresAt,\n\t\t&status.UpdatedAt,\n\t)\n\tif err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn nil, fmt.Errorf(\"lock %d not found\", lockID)\n\t\t}\n\t\treturn nil, fmt.Errorf(\"check lock status for %d: %w\", lockID, err)\n\t}\n\n\treturn &status, nil\n}\n\nfunc (s *postgresStore) CleanupStaleLocks(ctx context.Context, db *sql.DB) (_ []int64, retErr error) {\n\tquery := fmt.Sprintf(`UPDATE %s SET\n\t\tlocked = false,\n\t\tlocked_at = NULL,\n\t\tlocked_by = NULL,\n\t\tlease_expires_at = NULL,\n\t\tupdated_at = now()\n\tWHERE locked = true AND lease_expires_at < now()\n\tRETURNING lock_id`, s.tableName)\n\n\trows, err := db.QueryContext(ctx, query)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"cleanup stale locks: %w\", err)\n\t}\n\tdefer func() {\n\t\tretErr = multierr.Append(retErr, rows.Close())\n\t}()\n\n\tvar cleanedLocks []int64\n\tfor rows.Next() {\n\t\tvar lockID int64\n\t\tif err := rows.Scan(&lockID); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"scan cleaned lock ID: %w\", err)\n\t\t}\n\t\tcleanedLocks = append(cleanedLocks, lockID)\n\t}\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, fmt.Errorf(\"iterate over cleaned locks: %w\", err)\n\t}\n\n\treturn cleanedLocks, nil\n}\n\n// formatDurationAsInterval converts a time.Duration to PostgreSQL interval format\nfunc formatDurationAsInterval(d time.Duration) string {\n\treturn fmt.Sprintf(\"%d seconds\", int(d.Seconds()))\n}\n\nfunc parseTableIdentifier(name string) (schema, table string) {\n\tschema, table, found := strings.Cut(name, \".\")\n\tif !found {\n\t\treturn \"\", name\n\t}\n\treturn schema, table\n}\n"
  },
  {
    "path": "lock/internal/store/store.go",
    "content": "package store\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"time\"\n)\n\n// LockStore defines the interface for storing and managing database locks.\ntype LockStore interface {\n\t// CreateLockTable creates the lock table if it doesn't exist. Implementations should ensure\n\t// that this operation is idempotent.\n\tCreateLockTable(ctx context.Context, db *sql.DB) error\n\t// TableExists checks if the lock table exists.\n\tTableExists(ctx context.Context, db *sql.DB) (bool, error)\n\t// AcquireLock attempts to acquire a lock for the given lockID.\n\tAcquireLock(ctx context.Context, db *sql.DB, lockID int64, lockedBy string, leaseDuration time.Duration) (*AcquireLockResult, error)\n\t// ReleaseLock releases a lock held by the current instance.\n\tReleaseLock(ctx context.Context, db *sql.DB, lockID int64, lockedBy string) (*ReleaseLockResult, error)\n\t// UpdateLease updates the lease expiration time for a lock (heartbeat).\n\tUpdateLease(ctx context.Context, db *sql.DB, lockID int64, lockedBy string, leaseDuration time.Duration) (*UpdateLeaseResult, error)\n\t// CheckLockStatus checks the current status of a lock.\n\tCheckLockStatus(ctx context.Context, db *sql.DB, lockID int64) (*LockStatus, error)\n\t// CleanupStaleLocks removes any locks that have expired using server time. Returns the list of\n\t// lock IDs that were cleaned up, if any.\n\tCleanupStaleLocks(ctx context.Context, db *sql.DB) ([]int64, error)\n}\n\n// LockStatus represents the current status of a lock.\ntype LockStatus struct {\n\tLocked         bool\n\tLockedBy       *string\n\tLeaseExpiresAt *time.Time\n\tUpdatedAt      *time.Time\n}\n\n// AcquireLockResult contains the result of a lock acquisition attempt.\ntype AcquireLockResult struct {\n\tLockedBy       string\n\tLeaseExpiresAt time.Time\n}\n\n// ReleaseLockResult contains the result of a lock release.\ntype ReleaseLockResult struct {\n\tLockID int64\n}\n\n// UpdateLeaseResult contains the result of a lease update.\ntype UpdateLeaseResult struct {\n\tLeaseExpiresAt time.Time\n}\n"
  },
  {
    "path": "lock/internal/table/config.go",
    "content": "package table\n\nimport (\n\t\"log/slog\"\n\t\"time\"\n)\n\n// Config holds configuration for table locker.\ntype Config struct {\n\tTableName         string\n\tLockID            int64\n\tLeaseDuration     time.Duration\n\tHeartbeatInterval time.Duration\n\tLockTimeout       ProbeConfig\n\tUnlockTimeout     ProbeConfig\n\n\t// Optional logger for lock operations\n\tLogger *slog.Logger\n\n\t// Optional custom retry policy for database errors\n\tRetryPolicy RetryPolicyFunc\n}\n\n// ProbeConfig holds retry configuration.\ntype ProbeConfig struct {\n\tIntervalDuration time.Duration\n\tFailureThreshold uint64\n}\n"
  },
  {
    "path": "lock/internal/table/locker.go",
    "content": "package table\n\nimport (\n\t\"cmp\"\n\t\"context\"\n\t\"crypto/rand\"\n\t\"database/sql\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/pressly/goose/v3/lock/internal/store\"\n\t\"github.com/sethvargo/go-retry\"\n)\n\n// RetryPolicyFunc inspects an error and returns whether the caller should retry the operation. This\n// allows for database-specific error handling without hardcoding driver-specific logic.\ntype RetryPolicyFunc func(err error) bool\n\n// Locker implements table-based locking for databases. This implementation is safe for concurrent\n// use by multiple goroutines.\ntype Locker struct {\n\tstore             store.LockStore\n\ttableName         string\n\tlockID            int64\n\tinstanceID        string\n\tleaseDuration     time.Duration\n\theartbeatInterval time.Duration\n\tretryLock         retry.Backoff\n\tretryUnlock       retry.Backoff\n\tlogger            *slog.Logger\n\tretryPolicy       RetryPolicyFunc\n\n\t// Application-level coordination\n\tmu sync.Mutex\n\n\t// Heartbeat management\n\theartbeatCancel context.CancelFunc\n\theartbeatDone   chan struct{}\n}\n\n// New creates a new table-based locker.\nfunc New(lockStore store.LockStore, cfg Config) *Locker {\n\t// Generate instance identifier\n\thostname, _ := os.Hostname()\n\thostname = cmp.Or(hostname, \"unknown-hostname\")\n\tinstanceID := fmt.Sprintf(\"%s-%d-%s\", hostname, os.Getpid(), randomHex(4))\n\n\tlogger := cfg.Logger\n\tif logger == nil {\n\t\tlogger = slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))\n\t}\n\n\treturn &Locker{\n\t\tstore:             lockStore,\n\t\ttableName:         cfg.TableName,\n\t\tlockID:            cfg.LockID,\n\t\tinstanceID:        instanceID,\n\t\tleaseDuration:     cfg.LeaseDuration,\n\t\theartbeatInterval: cfg.HeartbeatInterval,\n\t\tlogger:            logger,\n\t\tretryPolicy:       cfg.RetryPolicy,\n\t\tretryLock: retry.WithMaxRetries(\n\t\t\tcfg.LockTimeout.FailureThreshold,\n\t\t\t// Add +/- 25% jitter to reduce thundering herd\n\t\t\tretry.WithJitterPercent(25, retry.NewConstant(cfg.LockTimeout.IntervalDuration)),\n\t\t),\n\t\tretryUnlock: retry.WithMaxRetries(\n\t\t\tcfg.UnlockTimeout.FailureThreshold,\n\t\t\t// Add +/- 25% jitter to reduce thundering herd\n\t\t\tretry.WithJitterPercent(25, retry.NewConstant(cfg.UnlockTimeout.IntervalDuration)),\n\t\t),\n\t}\n}\n\n// Lock acquires the database lock. This method is safe for concurrent use - the mutex is held until\n// Unlock() is called. Only one goroutine can hold the lock at a time across the entire lifecycle.\nfunc (l *Locker) Lock(ctx context.Context, db *sql.DB) error {\n\tl.mu.Lock()\n\t// NOTE: mutex is NOT defer unlocked here, it remains held until Unlock() is called explicitly\n\t// or a specific error occurs below!\n\n\t// Ensure the lock table exists\n\tif err := l.store.CreateLockTable(ctx, db); err != nil {\n\t\tl.mu.Unlock()\n\t\treturn fmt.Errorf(\"ensure lock table exists: %w\", err)\n\t}\n\n\terr := retry.Do(ctx, l.retryLock, func(ctx context.Context) error {\n\t\t_, err := l.store.AcquireLock(ctx, db, l.lockID, l.instanceID, l.leaseDuration)\n\t\tif err != nil {\n\t\t\t// Clean up any stale locks before retrying\n\t\t\tif _, cleanupErr := l.store.CleanupStaleLocks(ctx, db); cleanupErr != nil {\n\t\t\t\tl.logger.WarnContext(ctx, \"failed to cleanup stale locks\",\n\t\t\t\t\tslog.Int64(\"lock_table\", l.lockID),\n\t\t\t\t\tslog.Any(\"error\", cleanupErr),\n\t\t\t\t)\n\t\t\t\t// Continue with retry, cleanup failure shouldn't block acquisition attempts\n\t\t\t}\n\t\t\tif l.shouldRetry(err) {\n\t\t\t\treturn retry.RetryableError(fmt.Errorf(\"acquire retryable lock: %w\", err))\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"acquire lock: %w\", err)\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tl.mu.Unlock()\n\t\tl.logger.WarnContext(ctx, \"failed to acquire lock after retries\",\n\t\t\tslog.Int64(\"lock_id\", l.lockID),\n\t\t\tslog.String(\"instance_id\", l.instanceID),\n\t\t\tslog.Any(\"error\", err),\n\t\t)\n\t\treturn fmt.Errorf(\"acquire lock %d after retries: %w\", l.lockID, err)\n\t}\n\n\tl.logger.DebugContext(ctx, \"successfully acquired lock\",\n\t\tslog.Int64(\"lock_id\", l.lockID),\n\t\tslog.String(\"instance_id\", l.instanceID),\n\t\tslog.Duration(\"lease_duration\", l.leaseDuration),\n\t)\n\t// Start heartbeat to maintain the lease\n\tl.startHeartbeat(ctx, db)\n\n\t// Mutex remains held - will be released in Unlock()\n\treturn nil\n}\n\n// Unlock releases the database lock. This method must be called exactly once after a successful\n// Lock() call.\nfunc (l *Locker) Unlock(ctx context.Context, db *sql.DB) error {\n\t// NOTE: The mutex was acquired in Lock() and is still held\n\tdefer l.mu.Unlock()\n\n\t// Use a context that can't be cancelled to ensure we always attempt to unlock even if the\n\t// caller's context is cancelled. The call can control the retry behavior via the configured\n\t// timeouts.\n\tctx = context.WithoutCancel(ctx)\n\n\t// Stop heartbeat first\n\tl.stopHeartbeat()\n\n\terr := retry.Do(ctx, l.retryUnlock, func(ctx context.Context) error {\n\t\t_, err := l.store.ReleaseLock(ctx, db, l.lockID, l.instanceID)\n\t\tif err != nil {\n\t\t\tif l.shouldRetry(err) {\n\t\t\t\treturn retry.RetryableError(fmt.Errorf(\"release retryable lock: %w\", err))\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"release lock: %w\", err)\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tl.logger.WarnContext(ctx, \"failed to release lock\",\n\t\t\tslog.Int64(\"lock_id\", l.lockID),\n\t\t\tslog.String(\"instance_id\", l.instanceID),\n\t\t\tslog.Any(\"error\", err),\n\t\t)\n\t\treturn err\n\t}\n\n\tl.logger.DebugContext(ctx, \"successfully released lock\",\n\t\tslog.Int64(\"lock_id\", l.lockID),\n\t\tslog.String(\"instance_id\", l.instanceID),\n\t)\n\treturn nil\n}\n\n// startHeartbeat starts the heartbeat goroutine (called from within Lock with mutex held)\nfunc (l *Locker) startHeartbeat(parentCtx context.Context, db *sql.DB) {\n\t// If there's already a heartbeat running, stop it first\n\tl.stopHeartbeat()\n\n\t// Create a new context for the heartbeat\n\tctx, cancel := context.WithCancel(parentCtx)\n\tl.heartbeatCancel = cancel\n\tl.heartbeatDone = make(chan struct{})\n\n\tgo func() {\n\t\tdefer close(l.heartbeatDone)\n\t\tticker := time.NewTicker(l.heartbeatInterval)\n\t\tdefer ticker.Stop()\n\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn\n\t\t\tcase <-ticker.C:\n\t\t\t\tresult, err := l.store.UpdateLease(ctx, db, l.lockID, l.instanceID, l.leaseDuration)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// TODO(mf): should we add a retry policy here?\n\t\t\t\t\tl.logger.WarnContext(ctx, \"heartbeat failed to update lease\",\n\t\t\t\t\t\tslog.Int64(\"lock_id\", l.lockID),\n\t\t\t\t\t\tslog.String(\"instance_id\", l.instanceID),\n\t\t\t\t\t\tslog.Any(\"error\", err),\n\t\t\t\t\t)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tl.logger.DebugContext(ctx, \"heartbeat updated lease\",\n\t\t\t\t\tslog.Int64(\"lock_id\", l.lockID),\n\t\t\t\t\tslog.String(\"instance_id\", l.instanceID),\n\t\t\t\t\tslog.Time(\"lease_expires_at\", result.LeaseExpiresAt),\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t}()\n}\n\n// stopHeartbeat stops the heartbeat goroutine (called from within Unlock with mutex held).\nfunc (l *Locker) stopHeartbeat() {\n\tif l.heartbeatCancel != nil {\n\t\tl.heartbeatCancel()\n\t\t<-l.heartbeatDone\n\t\tl.heartbeatCancel = nil\n\t\tl.heartbeatDone = nil\n\t}\n}\n\n// shouldRetry determines whether an error is retryable based on the configured retry policy. If no\n// retry policy is configured, it defaults to always retrying.\nfunc (l *Locker) shouldRetry(err error) bool {\n\tif l.retryPolicy != nil {\n\t\treturn l.retryPolicy(err)\n\t}\n\treturn true\n}\n\nfunc randomHex(n int) string {\n\tb := make([]byte, n)\n\tif _, err := rand.Read(b); err != nil {\n\t\treturn fmt.Sprintf(\"%0*x\", n*2, time.Now().UnixNano())\n\t}\n\treturn hex.EncodeToString(b)\n}\n"
  },
  {
    "path": "lock/locker.go",
    "content": "// Package lock defines the Locker interface and implements the locking logic.\npackage lock\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n)\n\nvar (\n\t// ErrLockNotImplemented is returned when the database does not support locking.\n\tErrLockNotImplemented = errors.New(\"lock not implemented\")\n\t// ErrUnlockNotImplemented is returned when the database does not support unlocking.\n\tErrUnlockNotImplemented = errors.New(\"unlock not implemented\")\n)\n\n// SessionLocker is the interface to lock and unlock the database for the duration of a session. The\n// session is defined as the duration of a single connection and both methods must be called on the\n// same connection.\ntype SessionLocker interface {\n\tSessionLock(ctx context.Context, conn *sql.Conn) error\n\tSessionUnlock(ctx context.Context, conn *sql.Conn) error\n}\n\n// Locker is the interface to lock and unlock the database.\n//\n// Unlike [SessionLocker], the Lock and Unlock methods are called on a [*sql.DB] and do not require\n// the same connection to be used for both methods.\ntype Locker interface {\n\tLock(ctx context.Context, db *sql.DB) error\n\tUnlock(ctx context.Context, db *sql.DB) error\n}\n"
  },
  {
    "path": "lock/locktesting/locktesting.go",
    "content": "package locktesting\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/pressly/goose/v3\"\n\t\"github.com/pressly/goose/v3/lock\"\n\t\"github.com/stretchr/testify/require\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\n// TestProviderLocking is a reusable test helper that verifies locking behavior. It creates the a\n// bunch of providers with the same locker configuration and verifies that only one provider can\n// apply migrations at a time.\n//\n// The test verifies the core locking contract:\n//\n//  1. Only one provider should apply migrations when running concurrently\n//  2. The other providers should apply zero migrations (blocked by lock)\n//  3. All providers should complete without errors\n//  4. Final database state should be consistent\nfunc TestProviderLocking(\n\tt *testing.T,\n\tnewProvider func(*testing.T) *goose.Provider,\n) {\n\tt.Helper()\n\n\t// Number of concurrent providers to test\n\tconst count = 5\n\n\t// Create providers\n\tproviders := make([]*goose.Provider, count)\n\tfor i := range count {\n\t\tproviders[i] = newProvider(t)\n\t}\n\t// Sanity check - ensure providers have migration sources\n\tsources := providers[0].ListSources()\n\trequire.NotEmpty(t, sources, \"no migration sources found - check provider fsys\")\n\tmaxVersion := sources[len(sources)-1].Version\n\t// Ensure all providers have the same sources\n\tfor _, p := range providers {\n\t\trequire.Equal(t, sources, p.ListSources(), \"providers have different migration sources\")\n\t}\n\n\t// Since locking is enabled, only one of these providers should apply ALL the migrations. The\n\t// other providers should apply NO migrations.\n\n\tvar g errgroup.Group\n\tresults := make([]int, count)\n\n\tfor i := range count {\n\t\tg.Go(func() error {\n\t\t\tctx := context.Background()\n\t\t\tmigrationResults, err := providers[i].Up(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tresults[i] = len(migrationResults)\n\t\t\t// Useful for debugging:\n\t\t\t//\n\t\t\t// t.Logf(\"Provider %d applied %d migrations\", i, len(migrationResults))\n\t\t\tcurrentVersion, err := providers[i].GetDBVersion(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif currentVersion != maxVersion {\n\t\t\t\treturn fmt.Errorf(\"provider %d: expected version %d, got %d\", i, maxVersion, currentVersion)\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\t}\n\trequire.NoError(t, g.Wait())\n\n\t// Verify locking behavior: exactly one provider should have done all the work\n\tvar (\n\t\tprovidersWithWork   = 0\n\t\tproviderWithAllWork = -1 // Index of provider that did all the work\n\t)\n\tfor i, res := range results {\n\t\tif res > 0 {\n\t\t\tprovidersWithWork++\n\t\t\tif res == len(sources) {\n\t\t\t\tproviderWithAllWork = i\n\t\t\t}\n\t\t}\n\t}\n\t// Verify exactly one provider did work\n\trequire.Equal(t, 1, providersWithWork, \"exactly one provider should apply migrations - locking is not working\")\n\t// Verify that provider did all the work\n\trequire.NotEqual(t, -1, providerWithAllWork, \"one provider should have applied all migrations - locking is not working\")\n\t// Verify all others did no work\n\tfor i, res := range results {\n\t\tif i != providerWithAllWork {\n\t\t\trequire.Equal(t, 0, res, \"provider%d should have applied 0 migrations\", i)\n\t\t}\n\t}\n}\n\n// TestConcurrentLocking is a reusable test helper that verifies concurrent locker behavior. It\n// creates the specified number of lockers using the factory function and verifies that only one\n// locker can acquire the lock at a time.\n//\n// IMPORTANT: The newLocker function MUST create lockers that compete for the SAME lock resource.\n// For table-based lockers, this means using the same lock ID. For advisory locks, the same lock ID.\n// If each locker targets a different resource, multiple lockers will succeed (which breaks the\n// test).\n//\n// The test verifies the core locking contract:\n//\n//  1. Only one locker should successfully acquire the lock when running concurrently\n//  2. The other lockers should fail to acquire the lock (blocked/timeout)\n//  3. All lockers should complete without hanging\nfunc TestConcurrentLocking(\n\tt *testing.T,\n\tdb *sql.DB,\n\tnewLocker func(*testing.T) lock.Locker,\n\tlockTimeout time.Duration,\n) {\n\tt.Helper()\n\tctx := context.Background()\n\n\t// TODO(mf): I wonder if there's a better way to do logging in tests that conditionally enables\n\t// it. Maybe using testing.T.Log? But that doesn't have levels. Maybe use a global flag to\n\t// enable debug logging in tests?\n\n\t// logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}))\n\t// logger := slog.New(slog.DiscardHandler)\n\n\t// Number of concurrent lockers to test\n\tconst count = 5\n\n\tlockers := make([]lock.Locker, count)\n\tfor i := range count {\n\t\tlockers[i] = newLocker(t)\n\t}\n\n\t// Use buffered channel to collect successful lock acquisitions\n\tsuccessCh := make(chan int, count)\n\tvar wg sync.WaitGroup\n\n\t// Start multiple goroutines trying to acquire the same lock\n\tfor i := range count {\n\t\twg.Go(func() {\n\n\t\t\tctx, cancel := context.WithTimeout(ctx, lockTimeout)\n\t\t\tdefer cancel()\n\n\t\t\t// Try to acquire the lock\n\t\t\tif err := lockers[i].Lock(ctx, db); err != nil {\n\t\t\t\t// logger.Debug(\"Locker failed to acquire lock\", slog.Int(\"locker\", i), slog.String(\"error\", err.Error()))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tsuccessCh <- i\n\t\t\t// logger.Debug(\"Locker acquired lock\", slog.Int(\"locker\", i))\n\n\t\t\t// Hold the lock long enough for all other goroutines to exhaust their retries. This\n\t\t\t// ensures only ONE locker succeeds in the concurrent test\n\t\t\ttime.Sleep(lockTimeout * 2)\n\n\t\t\t// Release the lock\n\t\t\tif err := lockers[i].Unlock(ctx, db); err != nil {\n\t\t\t\tt.Errorf(\"Locker %d failed to release lock: %v\", i, err)\n\t\t\t}\n\t\t\t// } else {\n\t\t\t// logger.Debug(\"Locker released lock\", slog.Int(\"locker\", i))\n\t\t\t// }\n\t\t})\n\t}\n\t// Wait for all goroutines with timeout\n\tdone := make(chan struct{})\n\tgo func() {\n\t\twg.Wait()\n\t\tclose(done)\n\t}()\n\n\tselect {\n\tcase <-done:\n\t\t// Test completed normally\n\tcase <-time.After(lockTimeout + 5*time.Second):\n\t\tt.Fatal(\"Test timed out - lockers took too long\")\n\t}\n\n\t// Collect results from channel\n\tclose(successCh)\n\tvar successful []int\n\tfor id := range successCh {\n\t\tsuccessful = append(successful, id)\n\t}\n\n\trequire.Len(t, successful, 1, \"Exactly one locker should acquire the lock\")\n\t// logger.Debug(\"Concurrent locking test passed\", slog.Int(\"winning_locker\", successful[0]))\n}\n"
  },
  {
    "path": "lock/postgres.go",
    "content": "package lock\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/pressly/goose/v3/lock/internal/store\"\n\t\"github.com/pressly/goose/v3/lock/internal/table\"\n\t\"github.com/sethvargo/go-retry\"\n)\n\n// NewPostgresTableLocker returns a Locker that uses PostgreSQL table-based locking. It manages a\n// single lock row and keeps the lock alive automatically.\n//\n// Default behavior:\n//\n//   - Lease (30s): How long the lock is valid if heartbeat stops\n//   - Heartbeat (5s): How often the lock gets refreshed to keep it alive\n//   - If the process dies, others can take the lock after lease expires\n//\n// Defaults:\n//\n//\tTable: \"goose_lock\"\n//\tLock ID: 4097083626 (crc64 of \"goose\")\n//\tLock retry: 5s intervals, 5min timeout\n//\tUnlock retry: 2s intervals, 1min timeout\n//\n// Lock and Unlock both retry on failure. Lock stays alive automatically until released. All\n// defaults can be overridden with options.\nfunc NewPostgresTableLocker(options ...TableLockerOption) (Locker, error) {\n\tconfig := table.Config{\n\t\tTableName:         DefaultLockTableName,\n\t\tLockID:            DefaultLockID,\n\t\tLeaseDuration:     30 * time.Second,\n\t\tHeartbeatInterval: 5 * time.Second,\n\t\tLockTimeout: table.ProbeConfig{\n\t\t\tIntervalDuration: 5 * time.Second,\n\t\t\tFailureThreshold: 60, // 5 minutes total\n\t\t},\n\t\tUnlockTimeout: table.ProbeConfig{\n\t\t\tIntervalDuration: 2 * time.Second,\n\t\t\tFailureThreshold: 30, // 1 minute total\n\t\t},\n\t}\n\tfor _, opt := range options {\n\t\tif err := opt.apply(&config); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tlockStore, err := store.NewPostgres(config.TableName)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"create lock store: %w\", err)\n\t}\n\treturn table.New(lockStore, config), nil\n}\n\n// NewPostgresSessionLocker returns a SessionLocker that utilizes PostgreSQL's exclusive\n// session-level advisory lock mechanism.\n//\n// This function creates a SessionLocker that can be used to acquire and release a lock for\n// synchronization purposes. The lock acquisition is retried until it is successfully acquired or\n// until the failure threshold is reached. The default lock duration is set to 5 minutes, and the\n// default unlock duration is set to 1 minute.\n//\n// If you have long running migrations, you may want to increase the lock duration.\n//\n// See [SessionLockerOption] for options that can be used to configure the SessionLocker.\nfunc NewPostgresSessionLocker(opts ...SessionLockerOption) (SessionLocker, error) {\n\tcfg := sessionLockerConfig{\n\t\tlockID: DefaultLockID,\n\t\tlockProbe: probe{\n\t\t\tintervalDuration: 5 * time.Second,\n\t\t\tfailureThreshold: 60,\n\t\t},\n\t\tunlockProbe: probe{\n\t\t\tintervalDuration: 2 * time.Second,\n\t\t\tfailureThreshold: 30,\n\t\t},\n\t}\n\tfor _, opt := range opts {\n\t\tif err := opt.apply(&cfg); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn &postgresSessionLocker{\n\t\tlockID: cfg.lockID,\n\t\tretryLock: retry.WithMaxRetries(\n\t\t\tcfg.lockProbe.failureThreshold,\n\t\t\tretry.NewConstant(cfg.lockProbe.intervalDuration),\n\t\t),\n\t\tretryUnlock: retry.WithMaxRetries(\n\t\t\tcfg.unlockProbe.failureThreshold,\n\t\t\tretry.NewConstant(cfg.unlockProbe.intervalDuration),\n\t\t),\n\t}, nil\n}\n\ntype postgresSessionLocker struct {\n\tlockID      int64\n\tretryLock   retry.Backoff\n\tretryUnlock retry.Backoff\n}\n\nvar _ SessionLocker = (*postgresSessionLocker)(nil)\n\nfunc (l *postgresSessionLocker) SessionLock(ctx context.Context, conn *sql.Conn) error {\n\treturn retry.Do(ctx, l.retryLock, func(ctx context.Context) error {\n\t\trow := conn.QueryRowContext(ctx, \"SELECT pg_try_advisory_lock($1)\", l.lockID)\n\t\tvar locked bool\n\t\tif err := row.Scan(&locked); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute pg_try_advisory_lock: %w\", err)\n\t\t}\n\t\tif locked {\n\t\t\t// A session-level advisory lock was acquired.\n\t\t\treturn nil\n\t\t}\n\t\t// A session-level advisory lock could not be acquired. This is likely because another\n\t\t// process has already acquired the lock. We will continue retrying until the lock is\n\t\t// acquired or the maximum number of retries is reached.\n\t\treturn retry.RetryableError(errors.New(\"failed to acquire lock\"))\n\t})\n}\n\nfunc (l *postgresSessionLocker) SessionUnlock(ctx context.Context, conn *sql.Conn) error {\n\treturn retry.Do(ctx, l.retryUnlock, func(ctx context.Context) error {\n\t\tvar unlocked bool\n\t\trow := conn.QueryRowContext(ctx, \"SELECT pg_advisory_unlock($1)\", l.lockID)\n\t\tif err := row.Scan(&unlocked); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute pg_advisory_unlock: %w\", err)\n\t\t}\n\t\tif unlocked {\n\t\t\t// A session-level advisory lock was released.\n\t\t\treturn nil\n\t\t}\n\t\t/*\n\t\t\tdocs(md): provide users with some documentation on how they can unlock the session\n\t\t\tmanually.\n\n\t\t\tThis is probably not an issue for 99.99% of users since pg_advisory_unlock_all() will\n\t\t\trelease all session level advisory locks held by the current session. It is implicitly\n\t\t\tinvoked at session end, even if the client disconnects ungracefully.\n\n\t\t\tHere is output from a session that has a lock held:\n\n\t\t\tSELECT pid, granted, ((classid::bigint << 32) | objid::bigint) AS goose_lock_id FROM\n\t\t\tpg_locks WHERE locktype = 'advisory';\n\n\t\t\t| pid | granted | goose_lock_id       |\n\t\t\t|-----|---------|---------------------|\n\t\t\t| 191 | t       | 4097083626          |\n\n\t\t\tA forceful way to unlock the session is to terminate the backend with SIGTERM:\n\n\t\t\tSELECT pg_terminate_backend(191);\n\n\t\t\tSubsequent commands on the same connection will fail with:\n\n\t\t\tQuery 1 ERROR: FATAL: terminating connection due to administrator command\n\t\t*/\n\t\treturn retry.RetryableError(errors.New(\"failed to unlock session\"))\n\t})\n}\n"
  },
  {
    "path": "lock/session_locker_options.go",
    "content": "package lock\n\nimport (\n\t\"errors\"\n\t\"time\"\n)\n\nconst (\n\t// DefaultLockID is the id used to lock the database for migrations. It is a crc64 hash of the\n\t// string \"goose\". This is used to ensure that the lock is unique to goose.\n\t//\n\t// crc32.Checksum([]byte(\"goose\"), crc32.MakeTable(crc32.IEEE))\n\tDefaultLockID int64 = 4097083626\n)\n\n// SessionLockerOption is used to configure a SessionLocker.\ntype SessionLockerOption interface {\n\tapply(*sessionLockerConfig) error\n}\n\n// WithLockID sets the lock ID to use when locking the database.\n//\n// If WithLockID is not called, the DefaultLockID is used.\nfunc WithLockID(lockID int64) SessionLockerOption {\n\treturn sessionLockerConfigFunc(func(c *sessionLockerConfig) error {\n\t\tc.lockID = lockID\n\t\treturn nil\n\t})\n}\n\n// WithLockTimeout sets the max duration to wait for the lock to be acquired. The total duration\n// will be the period times the failure threshold.\n//\n// By default, the lock timeout is 300s (5min), where the lock is retried every 5 seconds (period)\n// up to 60 times (failure threshold).\n//\n// The minimum period is 1 second, and the minimum failure threshold is 1.\nfunc WithLockTimeout(period, failureThreshold uint64) SessionLockerOption {\n\treturn sessionLockerConfigFunc(func(c *sessionLockerConfig) error {\n\t\tif period < 1 {\n\t\t\treturn errors.New(\"period must be greater than 0, minimum is 1\")\n\t\t}\n\t\tif failureThreshold < 1 {\n\t\t\treturn errors.New(\"failure threshold must be greater than 0, minimum is 1\")\n\t\t}\n\t\tc.lockProbe = probe{\n\t\t\tintervalDuration: time.Duration(period) * time.Second,\n\t\t\tfailureThreshold: failureThreshold,\n\t\t}\n\t\treturn nil\n\t})\n}\n\n// WithUnlockTimeout sets the max duration to wait for the lock to be released. The total duration\n// will be the period times the failure threshold.\n//\n// By default, the lock timeout is 60s, where the lock is retried every 2 seconds (period) up to 30\n// times (failure threshold).\n//\n// The minimum period is 1 second, and the minimum failure threshold is 1.\nfunc WithUnlockTimeout(period, failureThreshold uint64) SessionLockerOption {\n\treturn sessionLockerConfigFunc(func(c *sessionLockerConfig) error {\n\t\tif period < 1 {\n\t\t\treturn errors.New(\"period must be greater than 0, minimum is 1\")\n\t\t}\n\t\tif failureThreshold < 1 {\n\t\t\treturn errors.New(\"failure threshold must be greater than 0, minimum is 1\")\n\t\t}\n\t\tc.unlockProbe = probe{\n\t\t\tintervalDuration: time.Duration(period) * time.Second,\n\t\t\tfailureThreshold: failureThreshold,\n\t\t}\n\t\treturn nil\n\t})\n}\n\ntype sessionLockerConfig struct {\n\tlockID      int64\n\tlockProbe   probe\n\tunlockProbe probe\n}\n\n// probe is used to configure how often and how many times to retry a lock or unlock operation. The\n// total timeout will be the period times the failure threshold.\ntype probe struct {\n\t// How often (in seconds) to perform the probe.\n\tintervalDuration time.Duration\n\t// Number of times to retry the probe.\n\tfailureThreshold uint64\n}\n\nvar _ SessionLockerOption = (sessionLockerConfigFunc)(nil)\n\ntype sessionLockerConfigFunc func(*sessionLockerConfig) error\n\nfunc (f sessionLockerConfigFunc) apply(cfg *sessionLockerConfig) error {\n\treturn f(cfg)\n}\n"
  },
  {
    "path": "lock/table_locker_options.go",
    "content": "package lock\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"time\"\n\n\t\"github.com/pressly/goose/v3/lock/internal/table\"\n)\n\nconst (\n\t// DefaultLockTableName is the default name of the lock table.\n\tDefaultLockTableName = \"goose_lock\"\n)\n\n// TableLockerOption is used to configure a table-based locker.\ntype TableLockerOption interface {\n\tapply(*table.Config) error\n}\n\n// WithTableName sets the name of the lock table.\nfunc WithTableName(tableName string) TableLockerOption {\n\treturn tableLockerConfigFunc(func(c *table.Config) error {\n\t\tif tableName == \"\" {\n\t\t\treturn errors.New(\"lock table name must not be empty\")\n\t\t}\n\t\tc.TableName = tableName\n\t\treturn nil\n\t})\n}\n\n// WithTableLockID sets the lock ID to use for this locker instance. Different lock IDs allow for\n// multiple independent locks in the same table.\nfunc WithTableLockID(lockID int64) TableLockerOption {\n\treturn tableLockerConfigFunc(func(c *table.Config) error {\n\t\tif lockID <= 0 {\n\t\t\treturn fmt.Errorf(\"lock ID must be greater than zero: %d\", lockID)\n\t\t}\n\t\tc.LockID = lockID\n\t\treturn nil\n\t})\n}\n\n// WithTableLeaseDuration sets how long a lock lease lasts. The lock will expire after this duration\n// if not renewed by heartbeat.\nfunc WithTableLeaseDuration(duration time.Duration) TableLockerOption {\n\treturn tableLockerConfigFunc(func(c *table.Config) error {\n\t\tif duration <= 0 {\n\t\t\treturn errors.New(\"lease duration must be positive\")\n\t\t}\n\t\tc.LeaseDuration = duration\n\t\treturn nil\n\t})\n}\n\n// WithTableHeartbeatInterval sets how often to send heartbeat updates to renew the lease. This\n// should be significantly smaller than the lease duration.\nfunc WithTableHeartbeatInterval(interval time.Duration) TableLockerOption {\n\treturn tableLockerConfigFunc(func(c *table.Config) error {\n\t\tif interval <= 0 {\n\t\t\treturn errors.New(\"heartbeat interval must be positive\")\n\t\t}\n\t\tc.HeartbeatInterval = interval\n\t\treturn nil\n\t})\n}\n\n// WithTableLockTimeout configures how long to retry acquiring a lock and how often to retry.\nfunc WithTableLockTimeout(intervalDuration time.Duration, failureThreshold uint64) TableLockerOption {\n\treturn tableLockerConfigFunc(func(c *table.Config) error {\n\t\tif intervalDuration <= 0 {\n\t\t\treturn errors.New(\"lock timeout interval duration must be positive\")\n\t\t}\n\t\tif failureThreshold == 0 {\n\t\t\treturn errors.New(\"lock timeout failure threshold must be positive\")\n\t\t}\n\t\tc.LockTimeout = table.ProbeConfig{\n\t\t\tIntervalDuration: intervalDuration,\n\t\t\tFailureThreshold: failureThreshold,\n\t\t}\n\t\treturn nil\n\t})\n}\n\n// WithTableUnlockTimeout configures how long to retry releasing a lock and how often to retry.\nfunc WithTableUnlockTimeout(intervalDuration time.Duration, failureThreshold uint64) TableLockerOption {\n\treturn tableLockerConfigFunc(func(c *table.Config) error {\n\t\tif intervalDuration <= 0 {\n\t\t\treturn errors.New(\"unlock timeout interval duration must be positive\")\n\t\t}\n\t\tif failureThreshold == 0 {\n\t\t\treturn errors.New(\"unlock timeout failure threshold must be positive\")\n\t\t}\n\t\tc.UnlockTimeout = table.ProbeConfig{\n\t\t\tIntervalDuration: intervalDuration,\n\t\t\tFailureThreshold: failureThreshold,\n\t\t}\n\t\treturn nil\n\t})\n}\n\n// WithTableLogger sets an optional logger for lock operations. If not provided, lock operations\n// will use a default logger that only logs errors to stderr.\nfunc WithTableLogger(logger *slog.Logger) TableLockerOption {\n\treturn tableLockerConfigFunc(func(c *table.Config) error {\n\t\tc.Logger = logger\n\t\treturn nil\n\t})\n}\n\n// WithTableRetryPolicy sets an optional callback to classify database errors during table lock\n// operations.\n//\n// The provided function is invoked whenever a database operation fails. This includes Lock(),\n// Unlock(), and heartbeat/lease update operations.\n//\n// If the function returns true, the operation is retried according to the configured retry/backoff\n// policy.\n//\n// If it returns false, the operation fails immediately, bypassing any retries.\n//\n// This allows clients to implement custom logic for transient errors, driver-specific errors, or\n// application-specific failure handling.\nfunc WithTableRetryPolicy(retryPolicy func(error) bool) TableLockerOption {\n\treturn tableLockerConfigFunc(func(c *table.Config) error {\n\t\tc.RetryPolicy = retryPolicy\n\t\treturn nil\n\t})\n}\n\ntype tableLockerConfigFunc func(*table.Config) error\n\nfunc (f tableLockerConfigFunc) apply(cfg *table.Config) error {\n\treturn f(cfg)\n}\n"
  },
  {
    "path": "lock/table_locker_options_test.go",
    "content": "package lock\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestTableLockerOptions(t *testing.T) {\n\t// Test that options are applied correctly\n\tlocker, err := NewPostgresTableLocker(\n\t\tWithTableName(\"custom_locks\"),\n\t\tWithTableLockID(999),\n\t\tWithTableLeaseDuration(10*time.Second),\n\t\tWithTableHeartbeatInterval(3*time.Second),\n\t)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, locker)\n\t// Test invalid lease duration\n\t_, err = NewPostgresTableLocker(WithTableLeaseDuration(-1 * time.Second))\n\trequire.Error(t, err)\n\t// Test invalid heartbeat interval\n\t_, err = NewPostgresTableLocker(WithTableHeartbeatInterval(0))\n\trequire.Error(t, err)\n\t// Test empty table name\n\t_, err = NewPostgresTableLocker(WithTableName(\"\"))\n\trequire.Error(t, err)\n\t// Test invalid lock ID\n\t_, err = NewPostgresTableLocker(WithTableLockID(0))\n\trequire.Error(t, err)\n\t// Test invalid lock timeout interval duration\n\t_, err = NewPostgresTableLocker(WithTableLockTimeout(0, 10))\n\trequire.Error(t, err)\n\t// Test invalid lock timeout failure threshold\n\t_, err = NewPostgresTableLocker(WithTableLockTimeout(5*time.Second, 0))\n\trequire.Error(t, err)\n\t// Test invalid unlock timeout interval duration\n\t_, err = NewPostgresTableLocker(WithTableUnlockTimeout(0, 10))\n\trequire.Error(t, err)\n\t// Test invalid unlock timeout failure threshold\n\t_, err = NewPostgresTableLocker(WithTableUnlockTimeout(5*time.Second, 0))\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "log.go",
    "content": "package goose\n\nimport (\n\tstd \"log\"\n)\n\nvar log Logger = &stdLogger{}\n\n// Logger is standard logger interface\ntype Logger interface {\n\tFatalf(format string, v ...any)\n\tPrintf(format string, v ...any)\n}\n\n// SetLogger sets the logger for package output\nfunc SetLogger(l Logger) {\n\tlog = l\n}\n\n// stdLogger is a default logger that outputs to a stdlib's log.std logger.\ntype stdLogger struct{}\n\nfunc (*stdLogger) Fatalf(format string, v ...any) { std.Fatalf(format, v...) }\nfunc (*stdLogger) Printf(format string, v ...any) { std.Printf(format, v...) }\n\n// NopLogger returns a logger that discards all logged output.\nfunc NopLogger() Logger {\n\treturn &nopLogger{}\n}\n\ntype nopLogger struct{}\n\nvar _ Logger = (*nopLogger)(nil)\n\nfunc (*nopLogger) Fatalf(format string, v ...any) {}\nfunc (*nopLogger) Printf(format string, v ...any) {}\n"
  },
  {
    "path": "migrate.go",
    "content": "package goose\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"math\"\n\t\"path\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"go.uber.org/multierr\"\n)\n\nvar (\n\t// ErrNoMigrationFiles when no migration files have been found.\n\tErrNoMigrationFiles = errors.New(\"no migration files found\")\n\t// ErrNoCurrentVersion when a current migration version is not found.\n\tErrNoCurrentVersion = errors.New(\"no current version found\")\n\t// ErrNoNextVersion when the next migration version is not found.\n\tErrNoNextVersion = errors.New(\"no next version found\")\n\t// MaxVersion is the maximum allowed version.\n\tMaxVersion int64 = math.MaxInt64\n)\n\n// Migrations slice.\ntype Migrations []*Migration\n\n// helpers so we can use pkg sort\nfunc (ms Migrations) Len() int      { return len(ms) }\nfunc (ms Migrations) Swap(i, j int) { ms[i], ms[j] = ms[j], ms[i] }\nfunc (ms Migrations) Less(i, j int) bool {\n\tif ms[i].Version == ms[j].Version {\n\t\tpanic(fmt.Sprintf(\"goose: duplicate version %v detected:\\n%v\\n%v\", ms[i].Version, ms[i].Source, ms[j].Source))\n\t}\n\treturn ms[i].Version < ms[j].Version\n}\n\n// Current gets the current migration.\nfunc (ms Migrations) Current(current int64) (*Migration, error) {\n\tfor i, migration := range ms {\n\t\tif migration.Version == current {\n\t\t\treturn ms[i], nil\n\t\t}\n\t}\n\n\treturn nil, ErrNoCurrentVersion\n}\n\n// Next gets the next migration.\nfunc (ms Migrations) Next(current int64) (*Migration, error) {\n\tfor i, migration := range ms {\n\t\tif migration.Version > current {\n\t\t\treturn ms[i], nil\n\t\t}\n\t}\n\n\treturn nil, ErrNoNextVersion\n}\n\n// Previous : Get the previous migration.\nfunc (ms Migrations) Previous(current int64) (*Migration, error) {\n\tfor i := len(ms) - 1; i >= 0; i-- {\n\t\tif ms[i].Version < current {\n\t\t\treturn ms[i], nil\n\t\t}\n\t}\n\n\treturn nil, ErrNoNextVersion\n}\n\n// Last gets the last migration.\nfunc (ms Migrations) Last() (*Migration, error) {\n\tif len(ms) == 0 {\n\t\treturn nil, ErrNoNextVersion\n\t}\n\n\treturn ms[len(ms)-1], nil\n}\n\n// Versioned gets versioned migrations.\nfunc (ms Migrations) versioned() (Migrations, error) {\n\tvar migrations Migrations\n\n\t// assume that the user will never have more than 19700101000000 migrations\n\tfor _, m := range ms {\n\t\t// parse version as timestamp\n\t\tversionTime, err := time.Parse(timestampFormat, fmt.Sprintf(\"%d\", m.Version))\n\n\t\tif versionTime.Before(time.Unix(0, 0)) || err != nil {\n\t\t\tmigrations = append(migrations, m)\n\t\t}\n\t}\n\n\treturn migrations, nil\n}\n\n// Timestamped gets the timestamped migrations.\nfunc (ms Migrations) timestamped() (Migrations, error) {\n\tvar migrations Migrations\n\n\t// assume that the user will never have more than 19700101000000 migrations\n\tfor _, m := range ms {\n\t\t// parse version as timestamp\n\t\tversionTime, err := time.Parse(timestampFormat, fmt.Sprintf(\"%d\", m.Version))\n\t\tif err != nil {\n\t\t\t// probably not a timestamp\n\t\t\tcontinue\n\t\t}\n\n\t\tif versionTime.After(time.Unix(0, 0)) {\n\t\t\tmigrations = append(migrations, m)\n\t\t}\n\t}\n\treturn migrations, nil\n}\n\nfunc (ms Migrations) String() string {\n\tvar str strings.Builder\n\tfor _, m := range ms {\n\t\tfmt.Fprintln(&str, m)\n\t}\n\treturn str.String()\n}\n\nfunc collectMigrationsFS(\n\tfsys fs.FS,\n\tdirpath string,\n\tcurrent, target int64,\n\tregistered map[int64]*Migration,\n) (Migrations, error) {\n\tif _, err := fs.Stat(fsys, dirpath); err != nil {\n\t\tif errors.Is(err, fs.ErrNotExist) {\n\t\t\treturn nil, fmt.Errorf(\"%s directory does not exist\", dirpath)\n\t\t}\n\t\treturn nil, err\n\t}\n\tvar migrations Migrations\n\t// SQL migration files.\n\tsqlMigrationFiles, err := fs.Glob(fsys, path.Join(dirpath, \"*.sql\"))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, file := range sqlMigrationFiles {\n\t\tv, err := NumericComponent(file)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"could not parse SQL migration file %q: %w\", file, err)\n\t\t}\n\t\tif versionFilter(v, current, target) {\n\t\t\tmigrations = append(migrations, &Migration{\n\t\t\t\tVersion:  v,\n\t\t\t\tNext:     -1,\n\t\t\t\tPrevious: -1,\n\t\t\t\tSource:   file,\n\t\t\t})\n\t\t}\n\t}\n\t// Go migration files.\n\tgoMigrations, err := collectGoMigrations(fsys, dirpath, registered, current, target)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tmigrations = append(migrations, goMigrations...)\n\tif len(migrations) == 0 {\n\t\treturn nil, ErrNoMigrationFiles\n\t}\n\treturn sortAndConnectMigrations(migrations), nil\n}\n\n// CollectMigrations returns all the valid looking migration scripts in the\n// migrations folder and go func registry, and key them by version.\nfunc CollectMigrations(dirpath string, current, target int64) (Migrations, error) {\n\treturn collectMigrationsFS(baseFS, dirpath, current, target, registeredGoMigrations)\n}\n\nfunc sortAndConnectMigrations(migrations Migrations) Migrations {\n\tsort.Sort(migrations)\n\n\t// now that we're sorted in the appropriate direction,\n\t// populate next and previous for each migration\n\tfor i, m := range migrations {\n\t\tprev := int64(-1)\n\t\tif i > 0 {\n\t\t\tprev = migrations[i-1].Version\n\t\t\tmigrations[i-1].Next = m.Version\n\t\t}\n\t\tmigrations[i].Previous = prev\n\t}\n\n\treturn migrations\n}\n\nfunc versionFilter(v, current, target int64) bool {\n\tif target > current {\n\t\treturn v > current && v <= target\n\t}\n\tif target < current {\n\t\treturn v <= current && v > target\n\t}\n\treturn false\n}\n\n// EnsureDBVersion retrieves the current version for this DB.\n// Create and initialize the DB version table if it doesn't exist.\nfunc EnsureDBVersion(db *sql.DB) (int64, error) {\n\tctx := context.Background()\n\treturn EnsureDBVersionContext(ctx, db)\n}\n\n// EnsureDBVersionContext retrieves the current version for this DB.\n// Create and initialize the DB version table if it doesn't exist.\nfunc EnsureDBVersionContext(ctx context.Context, db *sql.DB) (int64, error) {\n\tdbMigrations, err := store.ListMigrations(ctx, db, TableName())\n\tif err != nil {\n\t\tcreateErr := createVersionTable(ctx, db)\n\t\tif createErr != nil {\n\t\t\treturn 0, multierr.Append(err, createErr)\n\t\t}\n\t\treturn 0, nil\n\t}\n\t// The most recent record for each migration specifies\n\t// whether it has been applied or rolled back.\n\t// The first version we find that has been applied is the current version.\n\t//\n\t// TODO(mf): for historic reasons, we continue to use the is_applied column,\n\t// but at some point we need to deprecate this logic and ideally remove\n\t// this column.\n\t//\n\t// For context, see:\n\t// https://github.com/pressly/goose/pull/131#pullrequestreview-178409168\n\t//\n\t// The dbMigrations list is expected to be ordered by descending ID. But\n\t// in the future we should be able to query the last record only.\n\tskipLookup := make(map[int64]struct{})\n\tfor _, m := range dbMigrations {\n\t\t// Have we already marked this version to be skipped?\n\t\tif _, ok := skipLookup[m.VersionID]; ok {\n\t\t\tcontinue\n\t\t}\n\t\t// If version has been applied we are done.\n\t\tif m.IsApplied {\n\t\t\treturn m.VersionID, nil\n\t\t}\n\t\t// Latest version of migration has not been applied.\n\t\tskipLookup[m.VersionID] = struct{}{}\n\t}\n\treturn 0, ErrNoNextVersion\n}\n\n// createVersionTable creates the db version table and inserts the\n// initial 0 value into it.\nfunc createVersionTable(ctx context.Context, db *sql.DB) error {\n\ttxn, err := db.BeginTx(ctx, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := store.CreateVersionTable(ctx, txn, TableName()); err != nil {\n\t\t_ = txn.Rollback()\n\t\treturn err\n\t}\n\tif err := store.InsertVersion(ctx, txn, TableName(), 0); err != nil {\n\t\t_ = txn.Rollback()\n\t\treturn err\n\t}\n\treturn txn.Commit()\n}\n\n// GetDBVersion is an alias for EnsureDBVersion, but returns -1 in error.\nfunc GetDBVersion(db *sql.DB) (int64, error) {\n\tctx := context.Background()\n\treturn GetDBVersionContext(ctx, db)\n}\n\n// GetDBVersionContext is an alias for EnsureDBVersion, but returns -1 in error.\nfunc GetDBVersionContext(ctx context.Context, db *sql.DB) (int64, error) {\n\tversion, err := EnsureDBVersionContext(ctx, db)\n\tif err != nil {\n\t\treturn -1, err\n\t}\n\n\treturn version, nil\n}\n\n// collectGoMigrations collects Go migrations from the filesystem and merges them with registered\n// migrations.\n//\n// If Go migrations have been registered globally, with [goose.AddNamedMigration...], but there are\n// no corresponding .go files in the filesystem, add them to the migrations slice.\n//\n// If Go migrations have been registered, and there are .go files in the filesystem dirpath, ONLY\n// include those in the migrations slices.\n//\n// Lastly, if there are .go files in the filesystem but they have not been registered, raise an\n// error. This is to prevent users from accidentally adding valid looking Go files to the migrations\n// folder without registering them.\nfunc collectGoMigrations(\n\tfsys fs.FS,\n\tdirpath string,\n\tregisteredGoMigrations map[int64]*Migration,\n\tcurrent, target int64,\n) (Migrations, error) {\n\t// Sanity check registered migrations have the correct version prefix.\n\tfor _, m := range registeredGoMigrations {\n\t\tif _, err := NumericComponent(m.Source); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"could not parse go migration file %s: %w\", m.Source, err)\n\t\t}\n\t}\n\tgoFiles, err := fs.Glob(fsys, path.Join(dirpath, \"*.go\"))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// If there are no Go files in the filesystem and no registered Go migrations, return early.\n\tif len(goFiles) == 0 && len(registeredGoMigrations) == 0 {\n\t\treturn nil, nil\n\t}\n\ttype source struct {\n\t\tfullpath string\n\t\tversion  int64\n\t}\n\t// Find all Go files that have a version prefix and are within the requested range.\n\tvar sources []source\n\tfor _, fullpath := range goFiles {\n\t\tv, err := NumericComponent(fullpath)\n\t\tif err != nil {\n\t\t\tcontinue // Skip any files that don't have version prefix.\n\t\t}\n\t\tif strings.HasSuffix(fullpath, \"_test.go\") {\n\t\t\tcontinue // Skip Go test files.\n\t\t}\n\t\tif versionFilter(v, current, target) {\n\t\t\tsources = append(sources, source{\n\t\t\t\tfullpath: fullpath,\n\t\t\t\tversion:  v,\n\t\t\t})\n\t\t}\n\t}\n\tvar (\n\t\tmigrations Migrations\n\t)\n\tif len(sources) > 0 {\n\t\tfor _, s := range sources {\n\t\t\tmigration, ok := registeredGoMigrations[s.version]\n\t\t\tif ok {\n\t\t\t\tmigrations = append(migrations, migration)\n\t\t\t} else {\n\t\t\t\t// TODO(mf): something that bothers me about this implementation is it will be\n\t\t\t\t// lazily evaluated and the error will only be raised if the user tries to run the\n\t\t\t\t// migration. It would be better to raise an error much earlier in the process.\n\t\t\t\tmigrations = append(migrations, &Migration{\n\t\t\t\t\tVersion:    s.version,\n\t\t\t\t\tNext:       -1,\n\t\t\t\t\tPrevious:   -1,\n\t\t\t\t\tSource:     s.fullpath,\n\t\t\t\t\tRegistered: false,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// Some users may register Go migrations manually via AddNamedMigration_ functions but not\n\t\t// provide the corresponding .go files in the filesystem. In this case, we include them\n\t\t// wholesale in the migrations slice.\n\t\t//\n\t\t// This is a valid use case because users may want to build a custom binary that only embeds\n\t\t// the SQL migration files and some other mechanism for registering Go migrations.\n\t\tfor _, migration := range registeredGoMigrations {\n\t\t\tv, err := NumericComponent(migration.Source)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"could not parse go migration file %s: %w\", migration.Source, err)\n\t\t\t}\n\t\t\tif versionFilter(v, current, target) {\n\t\t\t\tmigrations = append(migrations, migration)\n\t\t\t}\n\t\t}\n\t}\n\treturn migrations, nil\n}\n"
  },
  {
    "path": "migrate_test.go",
    "content": "package goose\n\nimport (\n\t\"io/fs\"\n\t\"math\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestMigrationSort(t *testing.T) {\n\tt.Parallel()\n\n\tms := Migrations{}\n\n\t// insert in any order\n\tms = append(ms, newMigration(20120000, \"test\"))\n\tms = append(ms, newMigration(20128000, \"test\"))\n\tms = append(ms, newMigration(20129000, \"test\"))\n\tms = append(ms, newMigration(20127000, \"test\"))\n\n\tms = sortAndConnectMigrations(ms)\n\n\tsorted := []int64{20120000, 20127000, 20128000, 20129000}\n\n\tvalidateMigrationSort(t, ms, sorted)\n}\n\nfunc newMigration(v int64, src string) *Migration {\n\treturn &Migration{Version: v, Previous: -1, Next: -1, Source: src}\n}\n\nfunc validateMigrationSort(t *testing.T, ms Migrations, sorted []int64) {\n\tt.Helper()\n\tfor i, m := range ms {\n\t\tif sorted[i] != m.Version {\n\t\t\tt.Error(\"incorrect sorted version\")\n\t\t}\n\n\t\tvar next, prev int64\n\n\t\tif i == 0 {\n\t\t\tprev = -1\n\t\t\tnext = ms[i+1].Version\n\t\t} else if i == len(ms)-1 {\n\t\t\tprev = ms[i-1].Version\n\t\t\tnext = -1\n\t\t} else {\n\t\t\tprev = ms[i-1].Version\n\t\t\tnext = ms[i+1].Version\n\t\t}\n\n\t\tif m.Next != next {\n\t\t\tt.Errorf(\"mismatched Next. v: %v, got %v, wanted %v\\n\", m, m.Next, next)\n\t\t}\n\n\t\tif m.Previous != prev {\n\t\t\tt.Errorf(\"mismatched Previous v: %v, got %v, wanted %v\\n\", m, m.Previous, prev)\n\t\t}\n\t}\n\n\tt.Log(ms)\n}\n\nfunc TestCollectMigrations(t *testing.T) {\n\t// Not safe to run in parallel\n\tt.Run(\"no_migration_files_found\", func(t *testing.T) {\n\t\ttmp := t.TempDir()\n\t\terr := os.MkdirAll(filepath.Join(tmp, \"migrations-test\"), 0755)\n\t\trequire.NoError(t, err)\n\t\t_, err = collectMigrationsFS(os.DirFS(tmp), \"migrations-test\", 0, math.MaxInt64, nil)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"no migration files found\")\n\t})\n\tt.Run(\"filesystem_registered_with_single_dirpath\", func(t *testing.T) {\n\t\tt.Cleanup(func() { clearMap(registeredGoMigrations) })\n\t\tfile1, file2 := \"09081_a.go\", \"09082_b.go\"\n\t\tfile3, file4 := \"19081_a.go\", \"19082_b.go\"\n\t\tAddNamedMigrationContext(file1, nil, nil)\n\t\tAddNamedMigrationContext(file2, nil, nil)\n\t\trequire.Len(t, registeredGoMigrations, 2)\n\t\ttmp := t.TempDir()\n\t\tdir := filepath.Join(tmp, \"migrations\", \"dir1\")\n\t\terr := os.MkdirAll(dir, 0755)\n\t\trequire.NoError(t, err)\n\t\tcreateEmptyFile(t, dir, file1)\n\t\tcreateEmptyFile(t, dir, file2)\n\t\tcreateEmptyFile(t, dir, file3)\n\t\tcreateEmptyFile(t, dir, file4)\n\t\tfsys := os.DirFS(tmp)\n\t\tfiles, err := fs.ReadDir(fsys, \"migrations/dir1\")\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, files, 4)\n\t\tall, err := collectMigrationsFS(fsys, \"migrations/dir1\", 0, math.MaxInt64, registeredGoMigrations)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, all, 4)\n\t\trequire.EqualValues(t, 9081, all[0].Version)\n\t\trequire.EqualValues(t, 9082, all[1].Version)\n\t\trequire.EqualValues(t, 19081, all[2].Version)\n\t\trequire.EqualValues(t, 19082, all[3].Version)\n\t})\n\tt.Run(\"filesystem_registered_with_multiple_dirpath\", func(t *testing.T) {\n\t\tt.Cleanup(func() { clearMap(registeredGoMigrations) })\n\t\tfile1, file2, file3 := \"00001_a.go\", \"00002_b.go\", \"01111_c.go\"\n\t\tAddNamedMigrationContext(file1, nil, nil)\n\t\tAddNamedMigrationContext(file2, nil, nil)\n\t\tAddNamedMigrationContext(file3, nil, nil)\n\t\trequire.Len(t, registeredGoMigrations, 3)\n\t\ttmp := t.TempDir()\n\t\tdir1 := filepath.Join(tmp, \"migrations\", \"dir1\")\n\t\tdir2 := filepath.Join(tmp, \"migrations\", \"dir2\")\n\t\terr := os.MkdirAll(dir1, 0755)\n\t\trequire.NoError(t, err)\n\t\terr = os.MkdirAll(dir2, 0755)\n\t\trequire.NoError(t, err)\n\t\tcreateEmptyFile(t, dir1, file1)\n\t\tcreateEmptyFile(t, dir1, file2)\n\t\tcreateEmptyFile(t, dir2, file3)\n\t\tfsys := os.DirFS(tmp)\n\t\t// Validate if dirpath 1 is specified we get the two Go migrations in migrations/dir1 folder\n\t\t// even though 3 Go migrations have been registered.\n\t\t{\n\t\t\tall, err := collectMigrationsFS(fsys, \"migrations/dir1\", 0, math.MaxInt64, registeredGoMigrations)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, all, 2)\n\t\t\trequire.EqualValues(t, 1, all[0].Version)\n\t\t\trequire.EqualValues(t, 2, all[1].Version)\n\t\t}\n\t\t// Validate if dirpath 2 is specified we only get the one Go migration in migrations/dir2 folder\n\t\t// even though 3 Go migrations have been registered.\n\t\t{\n\t\t\tall, err := collectMigrationsFS(fsys, \"migrations/dir2\", 0, math.MaxInt64, registeredGoMigrations)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, all, 1)\n\t\t\trequire.EqualValues(t, 1111, all[0].Version)\n\t\t}\n\t})\n\tt.Run(\"empty_filesystem_registered_manually\", func(t *testing.T) {\n\t\tt.Cleanup(func() { clearMap(registeredGoMigrations) })\n\t\tAddNamedMigrationContext(\"00101_a.go\", nil, nil)\n\t\tAddNamedMigrationContext(\"00102_b.go\", nil, nil)\n\t\trequire.Len(t, registeredGoMigrations, 2)\n\t\ttmp := t.TempDir()\n\t\terr := os.MkdirAll(filepath.Join(tmp, \"migrations\"), 0755)\n\t\trequire.NoError(t, err)\n\t\tall, err := collectMigrationsFS(os.DirFS(tmp), \"migrations\", 0, math.MaxInt64, registeredGoMigrations)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, all, 2)\n\t\trequire.EqualValues(t, 101, all[0].Version)\n\t\trequire.EqualValues(t, 102, all[1].Version)\n\t})\n\tt.Run(\"unregistered_go_migrations\", func(t *testing.T) {\n\t\tt.Cleanup(func() { clearMap(registeredGoMigrations) })\n\t\tfile1, file2, file3 := \"00001_a.go\", \"00998_b.go\", \"00999_c.go\"\n\t\t// Only register file1 and file3, somehow user forgot to init in the\n\t\t// valid looking file2 Go migration\n\t\tAddNamedMigrationContext(file1, nil, nil)\n\t\tAddNamedMigrationContext(file3, nil, nil)\n\t\trequire.Len(t, registeredGoMigrations, 2)\n\t\ttmp := t.TempDir()\n\t\tdir1 := filepath.Join(tmp, \"migrations\", \"dir1\")\n\t\terr := os.MkdirAll(dir1, 0755)\n\t\trequire.NoError(t, err)\n\t\t// Include the valid file2 with file1, file3. But remember, it has NOT been\n\t\t// registered.\n\t\tcreateEmptyFile(t, dir1, file1)\n\t\tcreateEmptyFile(t, dir1, file2)\n\t\tcreateEmptyFile(t, dir1, file3)\n\t\tall, err := collectMigrationsFS(os.DirFS(tmp), \"migrations/dir1\", 0, math.MaxInt64, registeredGoMigrations)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, all, 3)\n\t\trequire.EqualValues(t, 1, all[0].Version)\n\t\trequire.True(t, all[0].Registered)\n\t\trequire.EqualValues(t, 998, all[1].Version)\n\t\t// This migrations is marked unregistered and will lazily raise an error if/when this\n\t\t// migration is run\n\t\trequire.False(t, all[1].Registered)\n\t\trequire.EqualValues(t, 999, all[2].Version)\n\t\trequire.True(t, all[2].Registered)\n\t})\n\tt.Run(\"with_skipped_go_files\", func(t *testing.T) {\n\t\tt.Cleanup(func() { clearMap(registeredGoMigrations) })\n\t\tfile1, file2, file3, file4 := \"00001_a.go\", \"00002_b.sql\", \"00999_c_test.go\", \"embed.go\"\n\t\tAddNamedMigrationContext(file1, nil, nil)\n\t\trequire.Len(t, registeredGoMigrations, 1)\n\t\ttmp := t.TempDir()\n\t\tdir1 := filepath.Join(tmp, \"migrations\", \"dir1\")\n\t\terr := os.MkdirAll(dir1, 0755)\n\t\trequire.NoError(t, err)\n\t\tcreateEmptyFile(t, dir1, file1)\n\t\tcreateEmptyFile(t, dir1, file2)\n\t\tcreateEmptyFile(t, dir1, file3)\n\t\tcreateEmptyFile(t, dir1, file4)\n\t\tall, err := collectMigrationsFS(os.DirFS(tmp), \"migrations/dir1\", 0, math.MaxInt64, registeredGoMigrations)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, all, 2)\n\t\trequire.EqualValues(t, 1, all[0].Version)\n\t\trequire.True(t, all[0].Registered)\n\t\trequire.EqualValues(t, 2, all[1].Version)\n\t\trequire.False(t, all[1].Registered)\n\t})\n\tt.Run(\"current_and_target\", func(t *testing.T) {\n\t\tt.Cleanup(func() { clearMap(registeredGoMigrations) })\n\t\tfile1, file2, file3 := \"01001_a.go\", \"01002_b.sql\", \"01003_c.go\"\n\t\tAddNamedMigrationContext(file1, nil, nil)\n\t\tAddNamedMigrationContext(file3, nil, nil)\n\t\trequire.Len(t, registeredGoMigrations, 2)\n\t\ttmp := t.TempDir()\n\t\tdir1 := filepath.Join(tmp, \"migrations\", \"dir1\")\n\t\terr := os.MkdirAll(dir1, 0755)\n\t\trequire.NoError(t, err)\n\t\tcreateEmptyFile(t, dir1, file1)\n\t\tcreateEmptyFile(t, dir1, file2)\n\t\tcreateEmptyFile(t, dir1, file3)\n\t\tall, err := collectMigrationsFS(os.DirFS(tmp), \"migrations/dir1\", 1001, 1003, registeredGoMigrations)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, all, 2)\n\t\trequire.EqualValues(t, 1002, all[0].Version)\n\t\trequire.EqualValues(t, 1003, all[1].Version)\n\t})\n}\n\nfunc TestVersionFilter(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tv       int64\n\t\tcurrent int64\n\t\ttarget  int64\n\t\twant    bool\n\t}{\n\t\t{2, 1, 3, true},  // v is within the range\n\t\t{4, 1, 3, false}, // v is outside the range\n\t\t{2, 3, 1, true},  // v is within the reversed range\n\t\t{4, 3, 1, false}, // v is outside the reversed range\n\t\t{3, 1, 3, true},  // v is equal to target\n\t\t{1, 1, 3, false}, // v is equal to current, not within the range\n\t\t{1, 3, 1, false}, // v is equal to current, not within the reversed range\n\t\t// Always return false if current equal target\n\t\t{1, 2, 2, false},\n\t\t{2, 2, 2, false},\n\t\t{3, 2, 2, false},\n\t}\n\tfor _, tc := range tests {\n\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\tgot := versionFilter(tc.v, tc.current, tc.target)\n\t\t\tif got != tc.want {\n\t\t\t\tt.Errorf(\"versionFilter(%d, %d, %d) = %v, want %v\", tc.v, tc.current, tc.target, got, tc.want)\n\t\t\t}\n\t\t})\n\t}\n}\nfunc createEmptyFile(t *testing.T, dir, name string) {\n\tt.Helper()\n\tpath := filepath.Join(dir, name)\n\tf, err := os.Create(path)\n\trequire.NoError(t, err)\n\tdefer f.Close()\n}\n\nfunc clearMap(m map[int64]*Migration) {\n\tfor k := range m {\n\t\tdelete(m, k)\n\t}\n}\n"
  },
  {
    "path": "migration.go",
    "content": "package goose\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pressly/goose/v3/internal/sqlparser\"\n)\n\n// NewGoMigration creates a new Go migration.\n//\n// Both up and down functions may be nil, in which case the migration will be recorded in the\n// versions table but no functions will be run. This is useful for recording (up) or deleting (down)\n// a version without running any functions. See [GoFunc] for more details.\nfunc NewGoMigration(version int64, up, down *GoFunc) *Migration {\n\tm := &Migration{\n\t\tType:       TypeGo,\n\t\tRegistered: true,\n\t\tVersion:    version,\n\t\tNext:       -1, Previous: -1,\n\t\tgoUp:      &GoFunc{Mode: TransactionEnabled},\n\t\tgoDown:    &GoFunc{Mode: TransactionEnabled},\n\t\tconstruct: true,\n\t}\n\tupdateMode := func(f *GoFunc) *GoFunc {\n\t\t// infer mode from function\n\t\tif f.Mode == 0 {\n\t\t\tif f.RunTx != nil && f.RunDB == nil {\n\t\t\t\tf.Mode = TransactionEnabled\n\t\t\t}\n\t\t\tif f.RunTx == nil && f.RunDB != nil {\n\t\t\t\tf.Mode = TransactionDisabled\n\t\t\t}\n\t\t\t// Always default to TransactionEnabled if both functions are nil. This is the most\n\t\t\t// common use case.\n\t\t\tif f.RunDB == nil && f.RunTx == nil {\n\t\t\t\tf.Mode = TransactionEnabled\n\t\t\t}\n\t\t}\n\t\treturn f\n\t}\n\t// To maintain backwards compatibility, we set ALL legacy functions. In a future major version,\n\t// we will remove these fields in favor of [GoFunc].\n\t//\n\t// Note, this function does not do any validation. Validation is lazily done when the migration\n\t// is registered.\n\tif up != nil {\n\t\tm.goUp = updateMode(up)\n\n\t\tif up.RunDB != nil {\n\t\t\tm.UpFnNoTxContext = up.RunDB          // func(context.Context, *sql.DB) error\n\t\t\tm.UpFnNoTx = withoutContext(up.RunDB) // func(*sql.DB) error\n\t\t}\n\t\tif up.RunTx != nil {\n\t\t\tm.UseTx = true\n\t\t\tm.UpFnContext = up.RunTx          // func(context.Context, *sql.Tx) error\n\t\t\tm.UpFn = withoutContext(up.RunTx) // func(*sql.Tx) error\n\t\t}\n\t}\n\tif down != nil {\n\t\tm.goDown = updateMode(down)\n\n\t\tif down.RunDB != nil {\n\t\t\tm.DownFnNoTxContext = down.RunDB          // func(context.Context, *sql.DB) error\n\t\t\tm.DownFnNoTx = withoutContext(down.RunDB) // func(*sql.DB) error\n\t\t}\n\t\tif down.RunTx != nil {\n\t\t\tm.UseTx = true\n\t\t\tm.DownFnContext = down.RunTx          // func(context.Context, *sql.Tx) error\n\t\t\tm.DownFn = withoutContext(down.RunTx) // func(*sql.Tx) error\n\t\t}\n\t}\n\treturn m\n}\n\n// Migration struct represents either a SQL or Go migration.\n//\n// Avoid constructing migrations manually, use [NewGoMigration] function.\ntype Migration struct {\n\tType    MigrationType\n\tVersion int64\n\t// Source is the path to the .sql script or .go file. It may be empty for Go migrations that\n\t// have been registered globally and don't have a source file.\n\tSource string\n\n\tUpFnContext, DownFnContext         GoMigrationContext\n\tUpFnNoTxContext, DownFnNoTxContext GoMigrationNoTxContext\n\n\t// These fields will be removed in a future major version. They are here for backwards\n\t// compatibility and are an implementation detail.\n\tRegistered bool\n\tUseTx      bool\n\tNext       int64 // next version, or -1 if none\n\tPrevious   int64 // previous version, -1 if none\n\n\t// We still save the non-context versions in the struct in case someone is using them. Goose\n\t// does not use these internally anymore in favor of the context-aware versions. These fields\n\t// will be removed in a future major version.\n\n\tUpFn       GoMigration     // Deprecated: use UpFnContext instead.\n\tDownFn     GoMigration     // Deprecated: use DownFnContext instead.\n\tUpFnNoTx   GoMigrationNoTx // Deprecated: use UpFnNoTxContext instead.\n\tDownFnNoTx GoMigrationNoTx // Deprecated: use DownFnNoTxContext instead.\n\n\tnoVersioning bool\n\n\t// These fields are used internally by goose and users are not expected to set them. Instead,\n\t// use [NewGoMigration] to create a new go migration.\n\tconstruct    bool\n\tgoUp, goDown *GoFunc\n\n\tsql sqlMigration\n}\n\ntype sqlMigration struct {\n\t// The Parsed field is used to track whether the SQL migration has been parsed. It serves as an\n\t// optimization to avoid parsing migrations that may never be needed. Typically, migrations are\n\t// incremental, and users often run only the most recent ones, making parsing of prior\n\t// migrations unnecessary in most cases.\n\tParsed bool\n\n\t// Parsed must be set to true before the following fields are used.\n\tUseTx bool\n\tUp    []string\n\tDown  []string\n}\n\n// GoFunc represents a Go migration function.\ntype GoFunc struct {\n\t// Exactly one of these must be set, or both must be nil.\n\tRunTx func(ctx context.Context, tx *sql.Tx) error\n\t// -- OR --\n\tRunDB func(ctx context.Context, db *sql.DB) error\n\n\t// Mode is the transaction mode for the migration. When one of the run functions is set, the\n\t// mode will be inferred from the function and the field is ignored. Users do not need to set\n\t// this field when supplying a run function.\n\t//\n\t// If both run functions are nil, the mode defaults to TransactionEnabled. The use case for nil\n\t// functions is to record a version in the version table without invoking a Go migration\n\t// function.\n\t//\n\t// The only time this field is required is if BOTH run functions are nil AND you want to\n\t// override the default transaction mode.\n\tMode TransactionMode\n}\n\n// TransactionMode represents the possible transaction modes for a migration.\ntype TransactionMode int\n\nconst (\n\tTransactionEnabled TransactionMode = iota + 1\n\tTransactionDisabled\n)\n\nfunc (m TransactionMode) String() string {\n\tswitch m {\n\tcase TransactionEnabled:\n\t\treturn \"transaction_enabled\"\n\tcase TransactionDisabled:\n\t\treturn \"transaction_disabled\"\n\tdefault:\n\t\treturn fmt.Sprintf(\"unknown transaction mode (%d)\", m)\n\t}\n}\n\n// MigrationRecord struct.\n//\n// Deprecated: unused and will be removed in a future major version.\ntype MigrationRecord struct {\n\tVersionID int64\n\tTStamp    time.Time\n\tIsApplied bool // was this a result of up() or down()\n}\n\nfunc (m *Migration) String() string {\n\treturn fmt.Sprint(m.Source)\n}\n\n// Up runs an up migration.\nfunc (m *Migration) Up(db *sql.DB) error {\n\tctx := context.Background()\n\treturn m.UpContext(ctx, db)\n}\n\n// UpContext runs an up migration.\nfunc (m *Migration) UpContext(ctx context.Context, db *sql.DB) error {\n\tif err := m.run(ctx, db, true); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// Down runs a down migration.\nfunc (m *Migration) Down(db *sql.DB) error {\n\tctx := context.Background()\n\treturn m.DownContext(ctx, db)\n}\n\n// DownContext runs a down migration.\nfunc (m *Migration) DownContext(ctx context.Context, db *sql.DB) error {\n\tif err := m.run(ctx, db, false); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (m *Migration) run(ctx context.Context, db *sql.DB, direction bool) error {\n\tswitch filepath.Ext(m.Source) {\n\tcase \".sql\":\n\t\tf, err := baseFS.Open(m.Source)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"ERROR %v: failed to open SQL migration file: %w\", filepath.Base(m.Source), err)\n\t\t}\n\t\tdefer f.Close()\n\n\t\tstatements, useTx, err := sqlparser.ParseSQLMigration(f, sqlparser.FromBool(direction), verbose)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"ERROR %v: failed to parse SQL migration file: %w\", filepath.Base(m.Source), err)\n\t\t}\n\n\t\tstart := time.Now()\n\t\tif err := runSQLMigration(ctx, db, statements, useTx, m.Version, direction, m.noVersioning); err != nil {\n\t\t\treturn fmt.Errorf(\"ERROR %v: failed to run SQL migration: %w\", filepath.Base(m.Source), err)\n\t\t}\n\t\tfinish := truncateDuration(time.Since(start))\n\n\t\tif len(statements) > 0 {\n\t\t\tlog.Printf(\"OK   %s (%s)\", filepath.Base(m.Source), finish)\n\t\t} else {\n\t\t\tlog.Printf(\"EMPTY %s (%s)\", filepath.Base(m.Source), finish)\n\t\t}\n\n\tcase \".go\":\n\t\tif !m.Registered {\n\t\t\treturn 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)\n\t\t}\n\t\tstart := time.Now()\n\t\tvar empty bool\n\t\tif m.UseTx {\n\t\t\t// Run go-based migration inside a tx.\n\t\t\tfn := m.DownFnContext\n\t\t\tif direction {\n\t\t\t\tfn = m.UpFnContext\n\t\t\t}\n\t\t\tempty = (fn == nil)\n\t\t\tif err := runGoMigration(\n\t\t\t\tctx,\n\t\t\t\tdb,\n\t\t\t\tfn,\n\t\t\t\tm.Version,\n\t\t\t\tdirection,\n\t\t\t\t!m.noVersioning,\n\t\t\t); err != nil {\n\t\t\t\treturn fmt.Errorf(\"ERROR go migration: %q: %w\", filepath.Base(m.Source), err)\n\t\t\t}\n\t\t} else {\n\t\t\t// Run go-based migration outside a tx.\n\t\t\tfn := m.DownFnNoTxContext\n\t\t\tif direction {\n\t\t\t\tfn = m.UpFnNoTxContext\n\t\t\t}\n\t\t\tempty = (fn == nil)\n\t\t\tif err := runGoMigrationNoTx(\n\t\t\t\tctx,\n\t\t\t\tdb,\n\t\t\t\tfn,\n\t\t\t\tm.Version,\n\t\t\t\tdirection,\n\t\t\t\t!m.noVersioning,\n\t\t\t); err != nil {\n\t\t\t\treturn fmt.Errorf(\"ERROR go migration no tx: %q: %w\", filepath.Base(m.Source), err)\n\t\t\t}\n\t\t}\n\t\tfinish := truncateDuration(time.Since(start))\n\t\tif !empty {\n\t\t\tlog.Printf(\"OK   %s (%s)\", filepath.Base(m.Source), finish)\n\t\t} else {\n\t\t\tlog.Printf(\"EMPTY %s (%s)\", filepath.Base(m.Source), finish)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc runGoMigrationNoTx(\n\tctx context.Context,\n\tdb *sql.DB,\n\tfn GoMigrationNoTxContext,\n\tversion int64,\n\tdirection bool,\n\trecordVersion bool,\n) error {\n\tif fn != nil {\n\t\t// Run go migration function.\n\t\tif err := fn(ctx, db); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to run go migration: %w\", err)\n\t\t}\n\t}\n\tif recordVersion {\n\t\treturn insertOrDeleteVersionNoTx(ctx, db, version, direction)\n\t}\n\treturn nil\n}\n\nfunc runGoMigration(\n\tctx context.Context,\n\tdb *sql.DB,\n\tfn GoMigrationContext,\n\tversion int64,\n\tdirection bool,\n\trecordVersion bool,\n) error {\n\tif fn == nil && !recordVersion {\n\t\treturn nil\n\t}\n\ttx, err := db.BeginTx(ctx, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to begin transaction: %w\", err)\n\t}\n\tif fn != nil {\n\t\t// Run go migration function.\n\t\tif err := fn(ctx, tx); err != nil {\n\t\t\t_ = tx.Rollback()\n\t\t\treturn fmt.Errorf(\"failed to run go migration: %w\", err)\n\t\t}\n\t}\n\tif recordVersion {\n\t\tif err := insertOrDeleteVersion(ctx, tx, version, direction); err != nil {\n\t\t\t_ = tx.Rollback()\n\t\t\treturn fmt.Errorf(\"failed to update version: %w\", err)\n\t\t}\n\t}\n\tif err := tx.Commit(); err != nil {\n\t\treturn fmt.Errorf(\"failed to commit transaction: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc insertOrDeleteVersion(ctx context.Context, tx *sql.Tx, version int64, direction bool) error {\n\tif direction {\n\t\treturn store.InsertVersion(ctx, tx, TableName(), version)\n\t}\n\treturn store.DeleteVersion(ctx, tx, TableName(), version)\n}\n\nfunc insertOrDeleteVersionNoTx(ctx context.Context, db *sql.DB, version int64, direction bool) error {\n\tif direction {\n\t\treturn store.InsertVersionNoTx(ctx, db, TableName(), version)\n\t}\n\treturn store.DeleteVersionNoTx(ctx, db, TableName(), version)\n}\n\n// NumericComponent parses the version from the migration file name.\n//\n// XXX_descriptivename.ext where XXX specifies the version number and ext specifies the type of\n// migration, either .sql or .go.\nfunc NumericComponent(filename string) (int64, error) {\n\tbase := filepath.Base(filename)\n\tif ext := filepath.Ext(base); ext != \".go\" && ext != \".sql\" {\n\t\treturn 0, errors.New(\"migration file does not have .sql or .go file extension\")\n\t}\n\tbefore, _, ok := strings.Cut(base, \"_\")\n\tif !ok {\n\t\treturn 0, errors.New(\"no filename separator '_' found\")\n\t}\n\tn, err := strconv.ParseInt(before, 10, 64)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"failed to parse version from migration file: %s: %w\", base, err)\n\t}\n\tif n < 1 {\n\t\treturn 0, errors.New(\"migration version must be greater than zero\")\n\t}\n\treturn n, nil\n}\n\nfunc truncateDuration(d time.Duration) time.Duration {\n\tfor _, v := range []time.Duration{\n\t\ttime.Second,\n\t\ttime.Millisecond,\n\t\ttime.Microsecond,\n\t} {\n\t\tif d > v {\n\t\t\treturn d.Round(v / time.Duration(100))\n\t\t}\n\t}\n\treturn d\n}\n\n// ref returns a string that identifies the migration. This is used for logging and error messages.\nfunc (m *Migration) ref() string {\n\treturn fmt.Sprintf(\"(type:%s,version:%d)\", m.Type, m.Version)\n}\n"
  },
  {
    "path": "migration_sql.go",
    "content": "package goose\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"regexp\"\n)\n\n// Run a migration specified in raw SQL.\n//\n// Sections of the script can be annotated with a special comment,\n// starting with \"-- +goose\" to specify whether the section should\n// be applied during an Up or Down migration\n//\n// All statements following an Up or Down annotation are grouped together\n// until another direction annotation is found.\nfunc runSQLMigration(\n\tctx context.Context,\n\tdb *sql.DB,\n\tstatements []string,\n\tuseTx bool,\n\tv int64,\n\tdirection bool,\n\tnoVersioning bool,\n) error {\n\tif useTx {\n\t\t// TRANSACTION.\n\n\t\tverboseInfo(\"Begin transaction\")\n\n\t\ttx, err := db.BeginTx(ctx, nil)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to begin transaction: %w\", err)\n\t\t}\n\n\t\tfor _, query := range statements {\n\t\t\tverboseInfo(\"Executing statement: %s\\n\", clearStatement(query))\n\t\t\tif _, err := tx.ExecContext(ctx, query); err != nil {\n\t\t\t\tverboseInfo(\"Rollback transaction\")\n\t\t\t\t_ = tx.Rollback()\n\t\t\t\treturn fmt.Errorf(\"failed to execute SQL query %q: %w\", clearStatement(query), err)\n\t\t\t}\n\t\t}\n\n\t\tif !noVersioning {\n\t\t\tif direction {\n\t\t\t\tif err := store.InsertVersion(ctx, tx, TableName(), v); err != nil {\n\t\t\t\t\tverboseInfo(\"Rollback transaction\")\n\t\t\t\t\t_ = tx.Rollback()\n\t\t\t\t\treturn fmt.Errorf(\"failed to insert new goose version: %w\", err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif err := store.DeleteVersion(ctx, tx, TableName(), v); err != nil {\n\t\t\t\t\tverboseInfo(\"Rollback transaction\")\n\t\t\t\t\t_ = tx.Rollback()\n\t\t\t\t\treturn fmt.Errorf(\"failed to delete goose version: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tverboseInfo(\"Commit transaction\")\n\t\tif err := tx.Commit(); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to commit transaction: %w\", err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\t// NO TRANSACTION.\n\tfor _, query := range statements {\n\t\tverboseInfo(\"Executing statement: %s\", clearStatement(query))\n\t\tif _, err := db.ExecContext(ctx, query); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute SQL query %q: %w\", clearStatement(query), err)\n\t\t}\n\t}\n\tif !noVersioning {\n\t\tif direction {\n\t\t\tif err := store.InsertVersionNoTx(ctx, db, TableName(), v); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to insert new goose version: %w\", err)\n\t\t\t}\n\t\t} else {\n\t\t\tif err := store.DeleteVersionNoTx(ctx, db, TableName(), v); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to delete goose version: %w\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nconst (\n\tgrayColor  = \"\\033[90m\"\n\tresetColor = \"\\033[00m\"\n)\n\nfunc verboseInfo(s string, args ...any) {\n\tif verbose {\n\t\tif noColor {\n\t\t\tlog.Printf(s, args...)\n\t\t} else {\n\t\t\tlog.Printf(grayColor+s+resetColor, args...)\n\t\t}\n\t}\n}\n\nvar (\n\tmatchSQLComments = regexp.MustCompile(`(?m)^--.*$[\\r\\n]*`)\n\tmatchEmptyEOL    = regexp.MustCompile(`(?m)^$[\\r\\n]*`) // TODO: Duplicate\n)\n\nfunc clearStatement(s string) string {\n\ts = matchSQLComments.ReplaceAllString(s, ``)\n\treturn matchEmptyEOL.ReplaceAllString(s, ``)\n}\n"
  },
  {
    "path": "osfs.go",
    "content": "package goose\n\nimport (\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n)\n\n// osFS wraps functions working with os filesystem to implement fs.FS interfaces.\ntype osFS struct{}\n\nfunc (osFS) Open(name string) (fs.File, error) { return os.Open(filepath.FromSlash(name)) }\n\nfunc (osFS) ReadDir(name string) ([]fs.DirEntry, error) { return os.ReadDir(filepath.FromSlash(name)) }\n\nfunc (osFS) Stat(name string) (fs.FileInfo, error) { return os.Stat(filepath.FromSlash(name)) }\n\nfunc (osFS) ReadFile(name string) ([]byte, error) { return os.ReadFile(filepath.FromSlash(name)) }\n\nfunc (osFS) Glob(pattern string) ([]string, error) { return filepath.Glob(filepath.FromSlash(pattern)) }\n\ntype noopFS struct{}\n\nvar _ fs.FS = noopFS{}\n\nfunc (f noopFS) Open(name string) (fs.File, error) {\n\treturn nil, os.ErrNotExist\n}\n"
  },
  {
    "path": "pkg/dockermanage/doc.go",
    "content": "// Package dockermanage provides lightweight Docker container lifecycle helpers for integration\n// testing.\n//\n// A [Manager] wraps the native Docker client and exposes methods to start, stop, remove, and list\n// containers. Containers are configured through functional [Option] values such as [WithImage],\n// [WithContainerPortTCP], and [WithEnv].\n//\n// After starting a container, use [Manager.WaitReady] with a custom [ReadinessFunc] to block until\n// the service inside the container is accepting connections.\n//\n// Every container created through this package is tagged with the [ManagedLabelKey] label, which\n// allows bulk operations like [Manager.StopManaged] and [Manager.RemoveManaged] to clean up all\n// managed containers.\n//\n// Database-specific sub-packages (e.g., postgres) provide opinionated defaults for common\n// databases.\npackage dockermanage\n"
  },
  {
    "path": "pkg/dockermanage/dockerpostgres/postgres.go",
    "content": "package dockerpostgres\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"maps\"\n\t\"net\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pressly/goose/v3/pkg/dockermanage\"\n)\n\nconst (\n\t// DefaultImage is the default PostgreSQL image.\n\tDefaultImage = \"postgres:16-alpine\"\n\n\t// DefaultDatabase is the default PostgreSQL database name.\n\tDefaultDatabase = \"testdb\"\n\n\t// DefaultUser is the default PostgreSQL user.\n\tDefaultUser = \"postgres\"\n\n\t// DefaultPassword is the default PostgreSQL password.\n\tDefaultPassword = \"password1\"\n\n\tdefaultContainerPort = \"5432/tcp\"\n)\n\n// Option configures a PostgreSQL container instance.\ntype Option interface {\n\tapply(*config) error\n}\n\ntype optionFunc func(*config) error\n\nfunc (f optionFunc) apply(cfg *config) error {\n\treturn f(cfg)\n}\n\ntype config struct {\n\timage    string\n\tdatabase string\n\tuser     string\n\tpassword string\n\thostPort int\n\tlabels   map[string]string\n}\n\nfunc defaultConfig() *config {\n\treturn &config{\n\t\timage:    DefaultImage,\n\t\tdatabase: DefaultDatabase,\n\t\tuser:     DefaultUser,\n\t\tpassword: DefaultPassword,\n\t\tlabels: map[string]string{\n\t\t\tdockermanage.ManagedLabelKey: \"postgres\",\n\t\t},\n\t}\n}\n\n// WithImage sets the PostgreSQL image.\nfunc WithImage(image string) Option {\n\treturn optionFunc(func(cfg *config) error {\n\t\timage = strings.TrimSpace(image)\n\t\tif image == \"\" {\n\t\t\treturn errors.New(\"image must not be empty\")\n\t\t}\n\t\tcfg.image = image\n\t\treturn nil\n\t})\n}\n\n// WithDatabase sets the database name.\nfunc WithDatabase(database string) Option {\n\treturn optionFunc(func(cfg *config) error {\n\t\tdatabase = strings.TrimSpace(database)\n\t\tif database == \"\" {\n\t\t\treturn errors.New(\"database must not be empty\")\n\t\t}\n\t\tcfg.database = database\n\t\treturn nil\n\t})\n}\n\n// WithUser sets the database user.\nfunc WithUser(user string) Option {\n\treturn optionFunc(func(cfg *config) error {\n\t\tuser = strings.TrimSpace(user)\n\t\tif user == \"\" {\n\t\t\treturn errors.New(\"user must not be empty\")\n\t\t}\n\t\tcfg.user = user\n\t\treturn nil\n\t})\n}\n\n// WithPassword sets the database user password.\nfunc WithPassword(password string) Option {\n\treturn optionFunc(func(cfg *config) error {\n\t\tif password == \"\" {\n\t\t\treturn errors.New(\"password must not be empty\")\n\t\t}\n\t\tcfg.password = password\n\t\treturn nil\n\t})\n}\n\n// WithHostPort sets a fixed host port. If unset, Docker auto-assigns one.\nfunc WithHostPort(port int) Option {\n\treturn optionFunc(func(cfg *config) error {\n\t\tif port <= 0 || port > 65535 {\n\t\t\treturn fmt.Errorf(\"host port must be in range 1-65535: %d\", port)\n\t\t}\n\t\tcfg.hostPort = port\n\t\treturn nil\n\t})\n}\n\n// WithLabel appends a container label.\nfunc WithLabel(key, value string) Option {\n\treturn optionFunc(func(cfg *config) error {\n\t\tkey = strings.TrimSpace(key)\n\t\tif key == \"\" {\n\t\t\treturn errors.New(\"label key must not be empty\")\n\t\t}\n\t\tcfg.labels[key] = value\n\t\treturn nil\n\t})\n}\n\n// WithLabels merges labels into container labels.\nfunc WithLabels(labels map[string]string) Option {\n\treturn optionFunc(func(cfg *config) error {\n\t\tfor key, value := range maps.Clone(labels) {\n\t\t\tkey = strings.TrimSpace(key)\n\t\t\tif key == \"\" {\n\t\t\t\treturn errors.New(\"label key must not be empty\")\n\t\t\t}\n\t\t\tcfg.labels[key] = value\n\t\t}\n\t\treturn nil\n\t})\n}\n\n// Instance represents a running PostgreSQL container.\ntype Instance struct {\n\tContainer *dockermanage.Container\n\tDatabase  string\n\tUser      string\n\tPassword  string\n}\n\n// DSN returns a connection string suitable for PostgreSQL drivers.\nfunc (i *Instance) DSN() string {\n\tif i == nil || i.Container == nil {\n\t\treturn \"\"\n\t}\n\treturn fmt.Sprintf(\n\t\t\"host=%s port=%d user=%s password=%s dbname=%s sslmode=disable\",\n\t\ti.Container.Host,\n\t\ti.Container.Port,\n\t\ti.User,\n\t\ti.Password,\n\t\ti.Database,\n\t)\n}\n\n// Start starts a PostgreSQL container and waits for its TCP port to be reachable.\nfunc Start(ctx context.Context, manager *dockermanage.Manager, options ...Option) (_ *Instance, retErr error) {\n\tif manager == nil {\n\t\treturn nil, errors.New(\"manager must not be nil\")\n\t}\n\tcfg := defaultConfig()\n\tfor _, opt := range options {\n\t\tif opt == nil {\n\t\t\tcontinue\n\t\t}\n\t\tif err := opt.apply(cfg); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tstartOptions := []dockermanage.Option{\n\t\tdockermanage.WithImage(cfg.image),\n\t\tdockermanage.WithContainerPort(defaultContainerPort),\n\t\tdockermanage.WithEnvVars([]string{\n\t\t\t\"POSTGRES_DB=\" + cfg.database,\n\t\t\t\"POSTGRES_USER=\" + cfg.user,\n\t\t\t\"POSTGRES_PASSWORD=\" + cfg.password,\n\t\t}),\n\t\tdockermanage.WithLabels(cfg.labels),\n\t}\n\tif cfg.hostPort > 0 {\n\t\tstartOptions = append(startOptions, dockermanage.WithHostPort(cfg.hostPort))\n\t}\n\n\tcontainer, err := manager.Start(ctx, startOptions...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() {\n\t\tif retErr != nil {\n\t\t\tretErr = errors.Join(retErr, manager.Remove(ctx, container.ID))\n\t\t}\n\t}()\n\n\tif err := manager.WaitReady(ctx, container, TCPReady); err != nil {\n\t\treturn nil, fmt.Errorf(\"wait for postgres readiness: %w\", err)\n\t}\n\treturn &Instance{\n\t\tContainer: container,\n\t\tDatabase:  cfg.database,\n\t\tUser:      cfg.user,\n\t\tPassword:  cfg.password,\n\t}, nil\n}\n\n// TCPReady checks whether the Postgres TCP port is accepting connections.\nfunc TCPReady(ctx context.Context, c *dockermanage.Container) error {\n\tif c == nil {\n\t\treturn errors.New(\"container must not be nil\")\n\t}\n\tdialer := net.Dialer{Timeout: 500 * time.Millisecond}\n\tconn, err := dialer.DialContext(ctx, \"tcp\", net.JoinHostPort(c.Host, strconv.Itoa(c.Port)))\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn conn.Close()\n}\n"
  },
  {
    "path": "pkg/dockermanage/dockerpostgres/postgres_test.go",
    "content": "package dockerpostgres_test\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"log/slog\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/jackc/pgx/v5\"\n\t\"github.com/pressly/goose/v3/pkg/dockermanage\"\n\t\"github.com/pressly/goose/v3/pkg/dockermanage/dockerpostgres\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc newManager(t *testing.T) *dockermanage.Manager {\n\tt.Helper()\n\tm, err := dockermanage.NewManager(slog.New(slog.NewTextHandler(os.Stderr, nil)))\n\trequire.NoError(t, err)\n\tt.Cleanup(func() {\n\t\tassert.NoError(t, m.Close())\n\t})\n\treturn m\n}\n\nfunc TestStartAndConnect(t *testing.T) {\n\tt.Parallel()\n\n\tm := newManager(t)\n\tctx := t.Context()\n\n\tinstance, err := dockerpostgres.Start(ctx, m)\n\trequire.NoError(t, err)\n\tt.Cleanup(func() {\n\t\tassert.NoError(t, m.Remove(context.WithoutCancel(ctx), instance.Container.ID))\n\t})\n\n\trequire.Positive(t, instance.Container.Port)\n\trequire.NotEmpty(t, instance.Container.Host)\n\n\t// TCP readiness doesn't guarantee Postgres is accepting queries yet. Wait for a real ping.\n\terr = m.WaitReady(ctx, instance.Container, func(ctx context.Context, c *dockermanage.Container) error {\n\t\tconn, err := pgx.Connect(ctx, instance.DSN())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn errors.Join(conn.Ping(ctx), conn.Close(ctx))\n\t})\n\trequire.NoError(t, err)\n}\n\nfunc TestStartAndStop(t *testing.T) {\n\tt.Parallel()\n\n\tm := newManager(t)\n\tctx := t.Context()\n\n\tinstance, err := dockerpostgres.Start(ctx, m)\n\trequire.NoError(t, err)\n\n\trequire.NoError(t, m.Stop(ctx, instance.Container.ID))\n\trequire.NoError(t, m.Remove(ctx, instance.Container.ID))\n}\n\nfunc TestManagedLabel(t *testing.T) {\n\tt.Parallel()\n\n\tm := newManager(t)\n\tctx := t.Context()\n\n\tinstance, err := dockerpostgres.Start(ctx, m)\n\trequire.NoError(t, err)\n\tt.Cleanup(func() {\n\t\tassert.NoError(t, m.Remove(context.WithoutCancel(ctx), instance.Container.ID))\n\t})\n\n\tlabel, ok := instance.Container.Labels[dockermanage.ManagedLabelKey]\n\trequire.True(t, ok, \"expected managed label to be set\")\n\trequire.Equal(t, \"postgres\", label)\n\n\tids, err := m.ListManaged(ctx)\n\trequire.NoError(t, err)\n\trequire.Contains(t, ids, instance.Container.ID)\n}\n\nfunc TestExecPgDump(t *testing.T) {\n\tt.Parallel()\n\n\tm := newManager(t)\n\tctx := t.Context()\n\n\tinstance, err := dockerpostgres.Start(ctx, m)\n\trequire.NoError(t, err)\n\tt.Cleanup(func() {\n\t\tassert.NoError(t, m.Remove(context.WithoutCancel(ctx), instance.Container.ID))\n\t})\n\n\t// Wait until Postgres is actually accepting connections.\n\terr = m.WaitReady(ctx, instance.Container, func(ctx context.Context, c *dockermanage.Container) error {\n\t\tconn, err := pgx.Connect(ctx, instance.DSN())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn errors.Join(conn.Ping(ctx), conn.Close(ctx))\n\t})\n\trequire.NoError(t, err)\n\n\tvar stdout, stderr bytes.Buffer\n\tresult, err := m.Exec(ctx, instance.Container.ID, dockermanage.ExecOptions{\n\t\tCmd: []string{\n\t\t\t\"pg_dump\",\n\t\t\t\"-U\", instance.User,\n\t\t\t\"-d\", instance.Database,\n\t\t\t\"--schema-only\",\n\t\t},\n\t\tStdout: &stdout,\n\t\tStderr: &stderr,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, 0, result.ExitCode, \"stderr: %s\", stderr.String())\n\trequire.Contains(t, stdout.String(), \"PostgreSQL database dump\")\n\trequire.Contains(t, stdout.String(), \"PostgreSQL database dump complete\")\n}\n"
  },
  {
    "path": "pkg/dockermanage/manager.go",
    "content": "package dockermanage\n\nimport (\n\t\"context\"\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"maps\"\n\t\"net/netip\"\n\t\"os\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/containerd/errdefs\"\n\t\"github.com/moby/moby/api/types/container\"\n\t\"github.com/moby/moby/api/types/network\"\n\t\"github.com/moby/moby/client\"\n\t\"github.com/sethvargo/go-retry\"\n)\n\nconst (\n\tdefaultReadinessTimeout = 30 * time.Second\n\tdefaultReadinessDelay   = 500 * time.Millisecond\n)\n\n// Container is a running Docker container managed by this package.\ntype Container struct {\n\tID     string\n\tImage  string\n\tHost   string\n\tPort   int\n\tLabels map[string]string\n}\n\n// ReadinessFunc reports whether a container is ready.\ntype ReadinessFunc func(ctx context.Context, container *Container) error\n\n// Manager manages Docker containers using the native Docker client.\ntype Manager struct {\n\tclient *client.Client\n\tlogger *slog.Logger\n}\n\n// NewManager creates a new manager backed by the Docker client configured from environment.\nfunc NewManager(logger *slog.Logger) (*Manager, error) {\n\tdockerClient, err := client.New(\n\t\tclient.FromEnv,\n\t)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"create Docker client: %w\", err)\n\t}\n\treturn newManagerWithClient(dockerClient, logger), nil\n}\n\nfunc newManagerWithClient(dockerClient *client.Client, logger *slog.Logger) *Manager {\n\tif logger == nil {\n\t\tlogger = slog.New(slog.NewTextHandler(io.Discard, nil))\n\t}\n\treturn &Manager{\n\t\tclient: dockerClient,\n\t\tlogger: logger.With(slog.String(\"logger\", \"dockermanage\")),\n\t}\n}\n\n// Start starts a container with the provided options.\nfunc (m *Manager) Start(ctx context.Context, options ...Option) (_ *Container, retErr error) {\n\tcfg := defaultConfig()\n\tcfg.pullProgress = os.Stderr\n\tfor _, opt := range options {\n\t\tif opt == nil {\n\t\t\tcontinue\n\t\t}\n\t\tif err := opt.apply(cfg); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tif cfg.image == \"\" {\n\t\treturn nil, errors.New(\"image is required\")\n\t}\n\tif cfg.containerPort.IsZero() {\n\t\treturn nil, errors.New(\"container port is required\")\n\t}\n\tif err := m.pullImageIfNotExists(ctx, cfg.image, cfg.pullProgress); err != nil {\n\t\treturn nil, fmt.Errorf(\"pull image %s: %w\", cfg.image, err)\n\t}\n\n\tportBinding := network.PortBinding{HostIP: netip.MustParseAddr(cfg.hostIP)}\n\tif cfg.hostPort > 0 {\n\t\tportBinding.HostPort = strconv.Itoa(cfg.hostPort)\n\t}\n\n\tresp, err := m.client.ContainerCreate(ctx, client.ContainerCreateOptions{\n\t\tName: cfg.name,\n\t\tConfig: &container.Config{\n\t\t\tImage: cfg.image,\n\t\t\tEnv:   cfg.envVars,\n\t\t\tExposedPorts: network.PortSet{\n\t\t\t\tcfg.containerPort: struct{}{},\n\t\t\t},\n\t\t\tLabels: maps.Clone(cfg.labels),\n\t\t},\n\t\tHostConfig: &container.HostConfig{\n\t\t\tPortBindings: network.PortMap{cfg.containerPort: []network.PortBinding{portBinding}},\n\t\t\tAutoRemove:   cfg.autoRemove,\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"create container: %w\", err)\n\t}\n\tdefer func() {\n\t\tif retErr != nil {\n\t\t\tcleanupCtx := context.WithoutCancel(ctx)\n\t\t\t_, err := m.client.ContainerRemove(cleanupCtx, resp.ID, client.ContainerRemoveOptions{Force: true})\n\t\t\tif err != nil {\n\t\t\t\tm.logger.Error(\n\t\t\t\t\t\"remove container after start failure\",\n\t\t\t\t\tslog.String(\"container_id\", resp.ID),\n\t\t\t\t\tslog.Any(\"error\", err),\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t}()\n\n\tif _, err := m.client.ContainerStart(ctx, resp.ID, client.ContainerStartOptions{}); err != nil {\n\t\treturn nil, fmt.Errorf(\"start container: %w\", err)\n\t}\n\n\thostPort := cfg.hostPort\n\tif hostPort == 0 {\n\t\tinspectResult, err := m.client.ContainerInspect(ctx, resp.ID, client.ContainerInspectOptions{})\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"inspect container for port: %w\", err)\n\t\t}\n\t\thostPort, err = resolveBoundPort(inspectResult.Container, cfg.containerPort)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"resolve host port: %w\", err)\n\t\t}\n\t}\n\n\tm.logger.Info(\n\t\t\"docker container started\",\n\t\tslog.String(\"container_id\", resp.ID),\n\t\tslog.String(\"image\", cfg.image),\n\t\tslog.Int(\"port\", hostPort),\n\t)\n\treturn &Container{\n\t\tID:     resp.ID,\n\t\tImage:  cfg.image,\n\t\tHost:   cfg.hostIP,\n\t\tPort:   hostPort,\n\t\tLabels: maps.Clone(cfg.labels),\n\t}, nil\n}\n\nfunc resolveBoundPort(containerJSON container.InspectResponse, containerPort network.Port) (int, error) {\n\tif containerJSON.NetworkSettings == nil {\n\t\treturn 0, errors.New(\"container network settings are missing\")\n\t}\n\tportBindings, ok := containerJSON.NetworkSettings.Ports[containerPort]\n\tif !ok || len(portBindings) == 0 {\n\t\treturn 0, fmt.Errorf(\"no port bindings found for %s\", containerPort)\n\t}\n\tfor _, binding := range portBindings {\n\t\tif binding.HostPort == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tport, err := strconv.Atoi(binding.HostPort)\n\t\tif err != nil {\n\t\t\treturn 0, fmt.Errorf(\"parse host port %q: %w\", binding.HostPort, err)\n\t\t}\n\t\treturn port, nil\n\t}\n\treturn 0, fmt.Errorf(\"no host port found for %s\", containerPort)\n}\n\n// Stop stops a running container.\nfunc (m *Manager) Stop(ctx context.Context, containerID string) error {\n\tif _, err := m.client.ContainerStop(ctx, containerID, client.ContainerStopOptions{}); err != nil {\n\t\treturn fmt.Errorf(\"stop container %s: %w\", containerID, err)\n\t}\n\tm.logger.Info(\"docker container stopped\", slog.String(\"container_id\", containerID))\n\treturn nil\n}\n\n// Remove removes a container. If running, it is force removed.\nfunc (m *Manager) Remove(ctx context.Context, containerID string) error {\n\tif _, err := m.client.ContainerRemove(ctx, containerID, client.ContainerRemoveOptions{Force: true}); err != nil {\n\t\treturn fmt.Errorf(\"remove container %s: %w\", containerID, err)\n\t}\n\tm.logger.Info(\"docker container removed\", slog.String(\"container_id\", containerID))\n\treturn nil\n}\n\n// WaitOption configures WaitReady behavior.\ntype WaitOption func(*waitConfig)\n\ntype waitConfig struct {\n\ttimeout time.Duration\n\tdelay   time.Duration\n}\n\n// WithTimeout sets the maximum time to wait for readiness. Defaults to 30s.\nfunc WithTimeout(d time.Duration) WaitOption {\n\treturn func(cfg *waitConfig) { cfg.timeout = d }\n}\n\n// WithDelay sets the interval between readiness checks. Defaults to 500ms.\nfunc WithDelay(d time.Duration) WaitOption {\n\treturn func(cfg *waitConfig) { cfg.delay = d }\n}\n\n// WaitReady waits until a custom readiness checker succeeds.\nfunc (m *Manager) WaitReady(ctx context.Context, container *Container, readiness ReadinessFunc, opts ...WaitOption) error {\n\tif container == nil {\n\t\treturn errors.New(\"container must not be nil\")\n\t}\n\tif readiness == nil {\n\t\treturn errors.New(\"readiness function must not be nil\")\n\t}\n\n\tcfg := &waitConfig{\n\t\ttimeout: defaultReadinessTimeout,\n\t\tdelay:   defaultReadinessDelay,\n\t}\n\tfor _, opt := range opts {\n\t\topt(cfg)\n\t}\n\tif cfg.timeout <= 0 {\n\t\treturn fmt.Errorf(\"timeout must be positive: %v\", cfg.timeout)\n\t}\n\n\tretryCtx, cancel := context.WithTimeout(ctx, cfg.timeout)\n\tdefer cancel()\n\n\tbackoff := retry.NewConstant(cfg.delay)\n\terr := retry.Do(retryCtx, backoff, func(ctx context.Context) error {\n\t\tif err := readiness(ctx, container); err != nil {\n\t\t\treturn retry.RetryableError(err)\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"container %s did not become ready within %s: %w\", container.ID, cfg.timeout, err)\n\t}\n\tm.logger.Info(\"docker container ready\", slog.String(\"container_id\", container.ID))\n\treturn nil\n}\n\n// ListManaged returns all container IDs started by this package.\nfunc (m *Manager) ListManaged(ctx context.Context) ([]string, error) {\n\tresult, err := m.client.ContainerList(ctx, client.ContainerListOptions{\n\t\tAll:     true,\n\t\tFilters: client.Filters{}.Add(\"label\", ManagedLabelKey),\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"list managed containers: %w\", err)\n\t}\n\tids := make([]string, 0, len(result.Items))\n\tfor _, c := range result.Items {\n\t\tids = append(ids, c.ID)\n\t}\n\treturn ids, nil\n}\n\n// StopManaged stops all containers started by this package.\nfunc (m *Manager) StopManaged(ctx context.Context) error {\n\tids, err := m.ListManaged(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"list containers for stop: %w\", err)\n\t}\n\tvar errs []error\n\tfor _, id := range ids {\n\t\tif err := m.Stop(ctx, id); err != nil {\n\t\t\terrs = append(errs, err)\n\t\t}\n\t}\n\tif len(errs) > 0 {\n\t\treturn errors.Join(errs...)\n\t}\n\tm.logger.Info(\"stopped all managed containers\", slog.Int(\"count\", len(ids)))\n\treturn nil\n}\n\n// RemoveManaged removes all containers started by this package.\nfunc (m *Manager) RemoveManaged(ctx context.Context) error {\n\tids, err := m.ListManaged(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"list containers for remove: %w\", err)\n\t}\n\tvar errs []error\n\tfor _, id := range ids {\n\t\tif err := m.Remove(ctx, id); err != nil {\n\t\t\terrs = append(errs, err)\n\t\t}\n\t}\n\tif len(errs) > 0 {\n\t\treturn errors.Join(errs...)\n\t}\n\tm.logger.Info(\"removed all managed containers\", slog.Int(\"count\", len(ids)))\n\treturn nil\n}\n\n// ExecOptions configures a command to run inside a container.\ntype ExecOptions struct {\n\tCmd    []string  // Command and arguments\n\tEnv    []string  // Optional environment variables\n\tStdout io.Writer // Where to write stdout (nil → discard)\n\tStderr io.Writer // Where to write stderr (nil → discard)\n}\n\n// ExecResult holds the outcome of an exec invocation.\ntype ExecResult struct {\n\tExitCode int\n}\n\n// Exec runs a command inside a running container and streams its output.\nfunc (m *Manager) Exec(ctx context.Context, containerID string, opts ExecOptions) (*ExecResult, error) {\n\tif len(opts.Cmd) == 0 {\n\t\treturn nil, errors.New(\"cmd must not be empty\")\n\t}\n\tstdout := opts.Stdout\n\tif stdout == nil {\n\t\tstdout = io.Discard\n\t}\n\tstderr := opts.Stderr\n\tif stderr == nil {\n\t\tstderr = io.Discard\n\t}\n\n\tcreateResp, err := m.client.ExecCreate(ctx, containerID, client.ExecCreateOptions{\n\t\tCmd:          opts.Cmd,\n\t\tEnv:          opts.Env,\n\t\tAttachStdout: true,\n\t\tAttachStderr: true,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"exec create: %w\", err)\n\t}\n\n\tattachResp, err := m.client.ExecAttach(ctx, createResp.ID, client.ExecAttachOptions{})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"exec attach: %w\", err)\n\t}\n\tdefer attachResp.Close()\n\n\tif err := demuxDockerStream(attachResp.Reader, stdout, stderr); err != nil {\n\t\treturn nil, fmt.Errorf(\"exec stream: %w\", err)\n\t}\n\n\tinspectResp, err := m.client.ExecInspect(ctx, createResp.ID, client.ExecInspectOptions{})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"exec inspect: %w\", err)\n\t}\n\n\tm.logger.Info(\n\t\t\"docker exec completed\",\n\t\tslog.String(\"container_id\", containerID),\n\t\tslog.Int(\"exit_code\", inspectResp.ExitCode),\n\t)\n\treturn &ExecResult{ExitCode: inspectResp.ExitCode}, nil\n}\n\n// Close closes the underlying Docker client.\nfunc (m *Manager) Close() error {\n\treturn m.client.Close()\n}\n\n// Docker multiplexed stream constants. When TTY is disabled, Docker prefixes each output frame with\n// an 8-byte header: [streamType(1), padding(3), payloadSize(4 big-endian)].\nconst (\n\tstreamStdout     byte = 1\n\tstreamStderr     byte = 2\n\tstreamHeaderSize      = 8\n)\n\n// demuxDockerStream reads a Docker multiplexed stream and routes each frame to the appropriate\n// writer based on its stream type.\nfunc demuxDockerStream(r io.Reader, stdout, stderr io.Writer) error {\n\tvar header [streamHeaderSize]byte\n\tfor {\n\t\tif _, err := io.ReadFull(r, header[:]); err != nil {\n\t\t\tif err == io.EOF {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t\tpayloadSize := int64(binary.BigEndian.Uint32(header[4:]))\n\t\tvar dst io.Writer\n\t\tswitch header[0] {\n\t\tcase streamStdout:\n\t\t\tdst = stdout\n\t\tcase streamStderr:\n\t\t\tdst = stderr\n\t\tdefault:\n\t\t\tdst = io.Discard\n\t\t}\n\t\tif _, err := io.CopyN(dst, r, payloadSize); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n}\n\nfunc (m *Manager) pullImageIfNotExists(ctx context.Context, imageName string, progressWriter io.Writer) (retErr error) {\n\tif _, err := m.client.ImageInspect(ctx, imageName); err == nil {\n\t\treturn nil\n\t} else if !errdefs.IsNotFound(err) {\n\t\treturn fmt.Errorf(\"inspect image: %w\", err)\n\t}\n\n\treader, err := m.client.ImagePull(ctx, imageName, client.ImagePullOptions{})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"pull image: %w\", err)\n\t}\n\tdefer func() {\n\t\tretErr = errors.Join(retErr, reader.Close())\n\t}()\n\n\tif progressWriter == nil {\n\t\tprogressWriter = io.Discard\n\t}\n\tif _, err := io.Copy(progressWriter, reader); err != nil {\n\t\treturn fmt.Errorf(\"stream pull output: %w\", err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/dockermanage/options.go",
    "content": "package dockermanage\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"maps\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/moby/moby/api/types/network\"\n)\n\nconst (\n\t// DefaultHostIP is the default host IP used for port bindings.\n\tDefaultHostIP = \"127.0.0.1\"\n\n\t// ManagedLabelKey marks containers created by this package. The value indicates the container\n\t// type (e.g., \"postgres\"). Presence of the key means the container is managed.\n\tManagedLabelKey = \"pressly.goose\"\n)\n\n// Option configures container start behavior.\ntype Option interface {\n\tapply(*config) error\n}\n\ntype optionFunc func(*config) error\n\nfunc (f optionFunc) apply(cfg *config) error {\n\treturn f(cfg)\n}\n\ntype config struct {\n\tname          string\n\timage         string\n\tcontainerPort network.Port\n\thostIP        string\n\thostPort      int\n\tenvVars       []string\n\tautoRemove    bool\n\tpullProgress  io.Writer\n\tlabels        map[string]string\n}\n\nfunc defaultConfig() *config {\n\treturn &config{\n\t\thostIP:  DefaultHostIP,\n\t\tenvVars: []string{},\n\t\tlabels: map[string]string{\n\t\t\tManagedLabelKey: \"\",\n\t\t},\n\t}\n}\n\n// WithName sets the container name.\nfunc WithName(name string) Option {\n\treturn optionFunc(func(cfg *config) error {\n\t\tname = strings.TrimSpace(name)\n\t\tif name == \"\" {\n\t\t\treturn errors.New(\"container name must not be empty\")\n\t\t}\n\t\tcfg.name = name\n\t\treturn nil\n\t})\n}\n\n// WithImage sets the container image (for example: postgres:16-alpine).\nfunc WithImage(image string) Option {\n\treturn optionFunc(func(cfg *config) error {\n\t\timage = strings.TrimSpace(image)\n\t\tif image == \"\" {\n\t\t\treturn errors.New(\"image must not be empty\")\n\t\t}\n\t\tcfg.image = image\n\t\treturn nil\n\t})\n}\n\n// WithContainerPort sets the container port to expose, for example: \"5432/tcp\".\nfunc WithContainerPort(port string) Option {\n\treturn optionFunc(func(cfg *config) error {\n\t\tp, err := network.ParsePort(port)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"invalid container port: %w\", err)\n\t\t}\n\t\tcfg.containerPort = p\n\t\treturn nil\n\t})\n}\n\n// WithContainerPortTCP is a convenience helper for TCP ports.\nfunc WithContainerPortTCP(port int) Option {\n\treturn optionFunc(func(cfg *config) error {\n\t\tif port <= 0 || port > 65535 {\n\t\t\treturn fmt.Errorf(\"container port must be in range 1-65535: %d\", port)\n\t\t}\n\t\tp, ok := network.PortFrom(uint16(port), network.TCP)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"invalid container port: %d\", port)\n\t\t}\n\t\tcfg.containerPort = p\n\t\treturn nil\n\t})\n}\n\n// WithHostIP sets the host IP to bind the container port to.\nfunc WithHostIP(hostIP string) Option {\n\treturn optionFunc(func(cfg *config) error {\n\t\thostIP = strings.TrimSpace(hostIP)\n\t\tif hostIP == \"\" {\n\t\t\treturn errors.New(\"host IP must not be empty\")\n\t\t}\n\t\tcfg.hostIP = hostIP\n\t\treturn nil\n\t})\n}\n\n// WithHostPort sets a fixed host port. Leave unset to auto-assign.\nfunc WithHostPort(port int) Option {\n\treturn optionFunc(func(cfg *config) error {\n\t\tif port <= 0 || port > 65535 {\n\t\t\treturn fmt.Errorf(\"host port must be in range 1-65535: %d\", port)\n\t\t}\n\t\tcfg.hostPort = port\n\t\treturn nil\n\t})\n}\n\n// WithEnv appends a single environment variable.\nfunc WithEnv(key, value string) Option {\n\treturn optionFunc(func(cfg *config) error {\n\t\tkey = strings.TrimSpace(key)\n\t\tif key == \"\" {\n\t\t\treturn errors.New(\"env key must not be empty\")\n\t\t}\n\t\tif strings.Contains(key, \"=\") {\n\t\t\treturn fmt.Errorf(\"env key must not contain '=': %s\", key)\n\t\t}\n\t\tcfg.envVars = append(cfg.envVars, key+\"=\"+value)\n\t\treturn nil\n\t})\n}\n\n// WithEnvVars appends environment variables in KEY=VALUE format.\nfunc WithEnvVars(envVars []string) Option {\n\treturn optionFunc(func(cfg *config) error {\n\t\tcfg.envVars = append(cfg.envVars, slices.Clone(envVars)...)\n\t\treturn nil\n\t})\n}\n\n// WithAutoRemove configures Docker AutoRemove behavior.\nfunc WithAutoRemove(autoRemove bool) Option {\n\treturn optionFunc(func(cfg *config) error {\n\t\tcfg.autoRemove = autoRemove\n\t\treturn nil\n\t})\n}\n\n// WithPullProgress sets where image pull output is streamed.\nfunc WithPullProgress(w io.Writer) Option {\n\treturn optionFunc(func(cfg *config) error {\n\t\tcfg.pullProgress = w\n\t\treturn nil\n\t})\n}\n\n// WithLabel sets a single container label.\nfunc WithLabel(key, value string) Option {\n\treturn optionFunc(func(cfg *config) error {\n\t\tkey = strings.TrimSpace(key)\n\t\tif key == \"\" {\n\t\t\treturn errors.New(\"label key must not be empty\")\n\t\t}\n\t\tcfg.labels[key] = value\n\t\treturn nil\n\t})\n}\n\n// WithLabels merges labels into container labels.\nfunc WithLabels(labels map[string]string) Option {\n\treturn optionFunc(func(cfg *config) error {\n\t\tfor key, value := range maps.Clone(labels) {\n\t\t\tkey = strings.TrimSpace(key)\n\t\t\tif key == \"\" {\n\t\t\t\treturn errors.New(\"label key must not be empty\")\n\t\t\t}\n\t\t\tcfg.labels[key] = value\n\t\t}\n\t\treturn nil\n\t})\n}\n"
  },
  {
    "path": "provider.go",
    "content": "package goose\n\nimport (\n\t\"cmp\"\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"maps\"\n\t\"math\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/pressly/goose/v3/database\"\n\t\"github.com/pressly/goose/v3/internal/controller\"\n\t\"github.com/pressly/goose/v3/internal/gooseutil\"\n\t\"github.com/pressly/goose/v3/internal/sqlparser\"\n\t\"go.uber.org/multierr\"\n)\n\n// Provider is a goose migration provider.\ntype Provider struct {\n\t// mu protects all accesses to the provider and must be held when calling operations on the\n\t// database.\n\tmu sync.Mutex\n\n\tdb               *sql.DB\n\tstore            *controller.StoreController\n\tversionTableOnce sync.Once\n\n\tfsys fs.FS\n\tcfg  config\n\n\t// migrations are ordered by version in ascending order. This list will never be empty and\n\t// contains all migrations known to the provider.\n\tmigrations []*Migration\n}\n\n// NewProvider returns a new goose provider.\n//\n// The caller is responsible for matching the database dialect with the database/sql driver. For\n// example, if the database dialect is \"postgres\", the database/sql driver could be\n// github.com/lib/pq or github.com/jackc/pgx. Each dialect has a corresponding [database.Dialect]\n// constant backed by a default [database.Store] implementation. For more advanced use cases, such\n// as using a custom table name or supplying a custom store implementation, see [WithStore].\n//\n// fsys is the filesystem used to read migration files, but may be nil. Most users will want to use\n// [os.DirFS], os.DirFS(\"path/to/migrations\"), to read migrations from the local filesystem.\n// However, it is possible to use a different \"filesystem\", such as [embed.FS] or filter out\n// migrations using [fs.Sub].\n//\n// See [ProviderOption] for more information on configuring the provider.\n//\n// Unless otherwise specified, all methods on Provider are safe for concurrent use.\nfunc NewProvider(dialect Dialect, db *sql.DB, fsys fs.FS, opts ...ProviderOption) (*Provider, error) {\n\tif db == nil {\n\t\treturn nil, errors.New(\"db must not be nil\")\n\t}\n\tif fsys == nil {\n\t\tfsys = noopFS{}\n\t}\n\tcfg := config{\n\t\tregistered:      make(map[int64]*Migration),\n\t\texcludePaths:    make(map[string]bool),\n\t\texcludeVersions: make(map[int64]bool),\n\t}\n\tfor _, opt := range opts {\n\t\tif err := opt.apply(&cfg); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\t// Allow users to specify a custom store implementation, but only if they don't specify a\n\t// dialect. If they specify a dialect, we'll use the default store implementation.\n\tif dialect == DialectCustom && cfg.store == nil {\n\t\treturn nil, errors.New(\"custom store must be supplied when using a custom dialect, make sure to pass WithStore option\")\n\t}\n\tif dialect != DialectCustom && cfg.store != nil {\n\t\treturn nil, errors.New(\"custom store must not be specified when using one of the default dialects, use DialectCustom instead\")\n\t}\n\t// Allow table name to be set only if store is not set.\n\tif cfg.tableName != \"\" && cfg.store != nil {\n\t\treturn nil, errors.New(\"WithTableName cannot be used with WithStore; set the table name directly on your custom store\")\n\t}\n\n\t// Set default logger if neither was provided\n\tif cfg.slogger == nil && cfg.logger == nil {\n\t\tcfg.logger = &stdLogger{}\n\t}\n\tvar store database.Store\n\tif dialect != \"\" {\n\t\tvar err error\n\t\tstore, err = database.NewStore(dialect, cmp.Or(cfg.tableName, DefaultTablename))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t} else {\n\t\tstore = cfg.store\n\t}\n\tif store.Tablename() == \"\" {\n\t\treturn nil, errors.New(\"invalid store implementation: table name must not be empty\")\n\t}\n\treturn newProvider(db, store, fsys, cfg, registeredGoMigrations /* global */)\n}\n\nfunc newProvider(\n\tdb *sql.DB,\n\tstore database.Store,\n\tfsys fs.FS,\n\tcfg config,\n\tglobal map[int64]*Migration,\n) (*Provider, error) {\n\t// Collect migrations from the filesystem and merge with registered migrations.\n\t//\n\t// Note, we don't parse SQL migrations here. They are parsed lazily when required.\n\n\t// feat(mf): we could add a flag to parse SQL migrations eagerly. This would allow us to return\n\t// an error if there are any SQL parsing errors. This adds a bit overhead to startup though, so\n\t// we should make it optional.\n\tfilesystemSources, err := collectFilesystemSources(fsys, false, cfg.excludePaths, cfg.excludeVersions)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tversionToGoMigration := make(map[int64]*Migration)\n\t// Add user-registered Go migrations from the provider.\n\tmaps.Copy(versionToGoMigration, cfg.registered)\n\t// Skip adding global Go migrations if explicitly disabled.\n\tif cfg.disableGlobalRegistry {\n\t\t// TODO(mf): let's add a warn-level log here to inform users if len(global) > 0. Would like\n\t\t// to add this once we're on go1.21 and leverage the new slog package.\n\t} else {\n\t\tfor version, m := range global {\n\t\t\tif _, ok := versionToGoMigration[version]; ok {\n\t\t\t\treturn nil, fmt.Errorf(\"global go migration conflicts with provider-registered go migration with version %d\", version)\n\t\t\t}\n\t\t\tversionToGoMigration[version] = m\n\t\t}\n\t}\n\t// At this point we have all registered unique Go migrations (if any). We need to merge them\n\t// with SQL migrations from the filesystem.\n\tmigrations, err := merge(filesystemSources, versionToGoMigration)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(migrations) == 0 {\n\t\treturn nil, ErrNoMigrations\n\t}\n\treturn &Provider{\n\t\tdb:         db,\n\t\tfsys:       fsys,\n\t\tcfg:        cfg,\n\t\tstore:      controller.NewStoreController(store),\n\t\tmigrations: migrations,\n\t}, nil\n}\n\n// Status returns the status of all migrations, merging the list of migrations from the database and\n// filesystem. The returned items are ordered by version, in ascending order.\nfunc (p *Provider) Status(ctx context.Context) ([]*MigrationStatus, error) {\n\treturn p.status(ctx)\n}\n\n// HasPending returns true if there are pending migrations to apply, otherwise, it returns false. If\n// out-of-order migrations are disabled, yet some are detected, this method returns an error.\n//\n// Note, this method will not use a SessionLocker or Locker if one is configured. This allows\n// callers to check for pending migrations without blocking or being blocked by other operations.\nfunc (p *Provider) HasPending(ctx context.Context) (bool, error) {\n\treturn p.hasPending(ctx)\n}\n\n// GetVersions returns the max database version and the target version to migrate to.\n//\n// Note, this method will not use a SessionLocker or Locker if one is configured. This allows\n// callers to check for versions without blocking or being blocked by other operations.\nfunc (p *Provider) GetVersions(ctx context.Context) (current, target int64, err error) {\n\treturn p.getVersions(ctx)\n}\n\n// GetDBVersion returns the highest version recorded in the database, regardless of the order in\n// which migrations were applied. For example, if migrations were applied out of order (1,4,2,3),\n// this method returns 4. If no migrations have been applied, it returns 0.\nfunc (p *Provider) GetDBVersion(ctx context.Context) (int64, error) {\n\tif p.cfg.disableVersioning {\n\t\treturn -1, errors.New(\"getting database version not supported when versioning is disabled\")\n\t}\n\treturn p.getDBMaxVersion(ctx, nil)\n}\n\n// ListSources returns a list of all migration sources known to the provider, sorted in ascending\n// order by version. The path field may be empty for manually registered migrations, such as Go\n// migrations registered using the [WithGoMigrations] option.\nfunc (p *Provider) ListSources() []*Source {\n\tsources := make([]*Source, 0, len(p.migrations))\n\tfor _, m := range p.migrations {\n\t\tsources = append(sources, &Source{\n\t\t\tType:    m.Type,\n\t\t\tPath:    m.Source,\n\t\t\tVersion: m.Version,\n\t\t})\n\t}\n\treturn sources\n}\n\n// Ping attempts to ping the database to verify a connection is available.\nfunc (p *Provider) Ping(ctx context.Context) error {\n\treturn p.db.PingContext(ctx)\n}\n\n// Close closes the database connection initially supplied to the provider.\nfunc (p *Provider) Close() error {\n\treturn p.db.Close()\n}\n\n// ApplyVersion applies exactly one migration for the specified version. If there is no migration\n// available for the specified version, this method returns [ErrVersionNotFound]. If the migration\n// has already been applied, this method returns [ErrAlreadyApplied].\n//\n// The direction parameter determines the migration direction: true for up migration and false for\n// down migration.\nfunc (p *Provider) ApplyVersion(ctx context.Context, version int64, direction bool) (*MigrationResult, error) {\n\tres, err := p.apply(ctx, version, direction)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// This should never happen, we must return exactly one result.\n\tif len(res) != 1 {\n\t\tversions := make([]string, 0, len(res))\n\t\tfor _, r := range res {\n\t\t\tversions = append(versions, strconv.FormatInt(r.Source.Version, 10))\n\t\t}\n\t\treturn nil, fmt.Errorf(\n\t\t\t\"unexpected number of migrations applied running apply, expecting exactly one result: %v\",\n\t\t\tstrings.Join(versions, \",\"),\n\t\t)\n\t}\n\treturn res[0], nil\n}\n\n// Up applies all pending migrations. If there are no new migrations to apply, this method returns\n// empty list and nil error.\nfunc (p *Provider) Up(ctx context.Context) ([]*MigrationResult, error) {\n\thasPending, err := p.HasPending(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !hasPending {\n\t\treturn nil, nil\n\t}\n\treturn p.up(ctx, false, math.MaxInt64)\n}\n\n// UpByOne applies the next pending migration. If there is no next migration to apply, this method\n// returns [ErrNoNextVersion].\nfunc (p *Provider) UpByOne(ctx context.Context) (*MigrationResult, error) {\n\thasPending, err := p.HasPending(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !hasPending {\n\t\treturn nil, ErrNoNextVersion\n\t}\n\tres, err := p.up(ctx, true, math.MaxInt64)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(res) == 0 {\n\t\treturn nil, ErrNoNextVersion\n\t}\n\t// This should never happen, we must return exactly one result.\n\tif len(res) != 1 {\n\t\tversions := make([]string, 0, len(res))\n\t\tfor _, r := range res {\n\t\t\tversions = append(versions, strconv.FormatInt(r.Source.Version, 10))\n\t\t}\n\t\treturn nil, fmt.Errorf(\n\t\t\t\"unexpected number of migrations applied running up-by-one, expecting exactly one result: %v\",\n\t\t\tstrings.Join(versions, \",\"),\n\t\t)\n\t}\n\treturn res[0], nil\n}\n\n// UpTo applies all pending migrations up to, and including, the specified version. If there are no\n// migrations to apply, this method returns empty list and nil error.\n//\n// For example, if there are three new migrations (9,10,11) and the current database version is 8\n// with a requested version of 10, only versions 9,10 will be applied.\nfunc (p *Provider) UpTo(ctx context.Context, version int64) ([]*MigrationResult, error) {\n\thasPending, err := p.HasPending(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !hasPending {\n\t\treturn nil, nil\n\t}\n\treturn p.up(ctx, false, version)\n}\n\n// Down rolls back the most recently applied migration. If there are no migrations to rollback, this\n// method returns [ErrNoNextVersion].\n//\n// Note, migrations are rolled back in the order they were applied. And not in the reverse order of\n// the migration version. This only applies in scenarios where migrations are allowed to be applied\n// out of order.\nfunc (p *Provider) Down(ctx context.Context) (*MigrationResult, error) {\n\tres, err := p.down(ctx, true, 0)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(res) == 0 {\n\t\treturn nil, ErrNoNextVersion\n\t}\n\t// This should never happen, we must return exactly one result.\n\tif len(res) != 1 {\n\t\tversions := make([]string, 0, len(res))\n\t\tfor _, r := range res {\n\t\t\tversions = append(versions, strconv.FormatInt(r.Source.Version, 10))\n\t\t}\n\t\treturn nil, fmt.Errorf(\n\t\t\t\"unexpected number of migrations applied running down, expecting exactly one result: %v\",\n\t\t\tstrings.Join(versions, \",\"),\n\t\t)\n\t}\n\treturn res[0], nil\n}\n\n// DownTo rolls back all migrations down to, but not including, the specified version.\n//\n// For example, if the current database version is 11,10,9... and the requested version is 9, only\n// migrations 11, 10 will be rolled back.\n//\n// Note, migrations are rolled back in the order they were applied. And not in the reverse order of\n// the migration version. This only applies in scenarios where migrations are allowed to be applied\n// out of order.\nfunc (p *Provider) DownTo(ctx context.Context, version int64) ([]*MigrationResult, error) {\n\tif version < 0 {\n\t\treturn nil, fmt.Errorf(\"invalid version: must be a valid number or zero: %d\", version)\n\t}\n\treturn p.down(ctx, false, version)\n}\n\n// *** Internal methods ***\n\nfunc (p *Provider) up(\n\tctx context.Context,\n\tbyOne bool,\n\tversion int64,\n) (_ []*MigrationResult, retErr error) {\n\tif version < 1 {\n\t\treturn nil, errInvalidVersion\n\t}\n\tconn, cleanup, err := p.initialize(ctx, true)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to initialize: %w\", err)\n\t}\n\tdefer func() {\n\t\tretErr = multierr.Append(retErr, cleanup())\n\t}()\n\n\tif len(p.migrations) == 0 {\n\t\treturn nil, nil\n\t}\n\tvar apply []*Migration\n\tif p.cfg.disableVersioning {\n\t\tif byOne {\n\t\t\treturn nil, errors.New(\"up-by-one not supported when versioning is disabled\")\n\t\t}\n\t\tapply = p.migrations\n\t} else {\n\t\t// optimize(mf): Listing all migrations from the database isn't great.\n\t\t//\n\t\t// The ideal implementation would be to query for the current max version and then apply\n\t\t// migrations greater than that version. However, a nice property of the current\n\t\t// implementation is that we can make stronger guarantees about unapplied migrations.\n\t\t//\n\t\t// In cases where users do not use out-of-order migrations, we want to surface an error if\n\t\t// there are older unapplied migrations. See https://github.com/pressly/goose/issues/761 for\n\t\t// more details.\n\t\t//\n\t\t// And in cases where users do use out-of-order migrations, we need to build a list of older\n\t\t// migrations that need to be applied, so we need to query for all migrations anyways.\n\t\tdbMigrations, err := p.store.ListMigrations(ctx, conn)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif len(dbMigrations) == 0 {\n\t\t\treturn nil, errMissingZeroVersion\n\t\t}\n\t\tversions, err := gooseutil.UpVersions(\n\t\t\tgetVersionsFromMigrations(p.migrations),     // fsys versions\n\t\t\tgetVersionsFromListMigrations(dbMigrations), // db versions\n\t\t\tversion,\n\t\t\tp.cfg.allowMissing,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor _, v := range versions {\n\t\t\tm, err := p.getMigration(v)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tapply = append(apply, m)\n\t\t}\n\t}\n\treturn p.runMigrations(ctx, conn, apply, sqlparser.DirectionUp, byOne)\n}\n\nfunc (p *Provider) down(\n\tctx context.Context,\n\tbyOne bool,\n\tversion int64,\n) (_ []*MigrationResult, retErr error) {\n\tconn, cleanup, err := p.initialize(ctx, true)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to initialize: %w\", err)\n\t}\n\tdefer func() {\n\t\tretErr = multierr.Append(retErr, cleanup())\n\t}()\n\n\tif len(p.migrations) == 0 {\n\t\treturn nil, nil\n\t}\n\tif p.cfg.disableVersioning {\n\t\tvar downMigrations []*Migration\n\t\tif byOne {\n\t\t\tlast := p.migrations[len(p.migrations)-1]\n\t\t\tdownMigrations = []*Migration{last}\n\t\t} else {\n\t\t\tdownMigrations = p.migrations\n\t\t}\n\t\treturn p.runMigrations(ctx, conn, downMigrations, sqlparser.DirectionDown, byOne)\n\t}\n\tdbMigrations, err := p.store.ListMigrations(ctx, conn)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(dbMigrations) == 0 {\n\t\treturn nil, errMissingZeroVersion\n\t}\n\t// We never migrate the zero version down.\n\tif dbMigrations[0].Version == 0 {\n\t\tp.logf(ctx,\n\t\t\t\"no migrations to run, current version: 0\",\n\t\t\t\"no migrations to run\",\n\t\t\tslog.Int64(\"version\", 0),\n\t\t)\n\t\treturn nil, nil\n\t}\n\tvar apply []*Migration\n\tfor _, dbMigration := range dbMigrations {\n\t\tif dbMigration.Version <= version {\n\t\t\tbreak\n\t\t}\n\t\tm, err := p.getMigration(dbMigration.Version)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tapply = append(apply, m)\n\t}\n\treturn p.runMigrations(ctx, conn, apply, sqlparser.DirectionDown, byOne)\n}\n\nfunc (p *Provider) apply(\n\tctx context.Context,\n\tversion int64,\n\tdirection bool,\n) (_ []*MigrationResult, retErr error) {\n\tif version < 1 {\n\t\treturn nil, errInvalidVersion\n\t}\n\tm, err := p.getMigration(version)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tconn, cleanup, err := p.initialize(ctx, true)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to initialize: %w\", err)\n\t}\n\tdefer func() {\n\t\tretErr = multierr.Append(retErr, cleanup())\n\t}()\n\n\td := sqlparser.DirectionDown\n\tif direction {\n\t\td = sqlparser.DirectionUp\n\t}\n\n\tif p.cfg.disableVersioning {\n\t\t// If versioning is disabled, we simply run the migration.\n\t\treturn p.runMigrations(ctx, conn, []*Migration{m}, d, true)\n\t}\n\n\tresult, err := p.store.GetMigration(ctx, conn, version)\n\tif err != nil && !errors.Is(err, database.ErrVersionNotFound) {\n\t\treturn nil, err\n\t}\n\t// There are a few states here:\n\t//  1. direction is up\n\t//    a. migration is applied, this is an error (ErrAlreadyApplied)\n\t//    b. migration is not applied, apply it\n\tif direction && result != nil {\n\t\treturn nil, fmt.Errorf(\"version %d: %w\", version, ErrAlreadyApplied)\n\t}\n\t//  2. direction is down\n\t//    a. migration is applied, rollback\n\t//    b. migration is not applied, this is an error (ErrNotApplied)\n\tif !direction && result == nil {\n\t\treturn nil, fmt.Errorf(\"version %d: %w\", version, ErrNotApplied)\n\t}\n\treturn p.runMigrations(ctx, conn, []*Migration{m}, d, true)\n}\n\nfunc (p *Provider) getVersions(ctx context.Context) (current, target int64, retErr error) {\n\tconn, cleanup, err := p.initialize(ctx, false)\n\tif err != nil {\n\t\treturn -1, -1, fmt.Errorf(\"failed to initialize: %w\", err)\n\t}\n\tdefer func() {\n\t\tretErr = multierr.Append(retErr, cleanup())\n\t}()\n\n\ttarget = p.migrations[len(p.migrations)-1].Version\n\n\t// If versioning is disabled, we always have pending migrations and the target version is the\n\t// last migration.\n\tif p.cfg.disableVersioning {\n\t\treturn -1, target, nil\n\t}\n\n\tcurrent, err = p.store.GetLatestVersion(ctx, conn)\n\tif err != nil {\n\t\tif errors.Is(err, database.ErrVersionNotFound) {\n\t\t\treturn -1, target, errMissingZeroVersion\n\t\t}\n\t\treturn -1, target, err\n\t}\n\treturn current, target, nil\n}\n\nfunc (p *Provider) hasPending(ctx context.Context) (_ bool, retErr error) {\n\tconn, cleanup, err := p.initialize(ctx, false)\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"failed to initialize: %w\", err)\n\t}\n\tdefer func() {\n\t\tretErr = multierr.Append(retErr, cleanup())\n\t}()\n\n\t// If versioning is disabled, we always have pending migrations.\n\tif p.cfg.disableVersioning {\n\t\treturn true, nil\n\t}\n\n\t// List all migrations from the database. Careful, optimizations here can lead to subtle bugs.\n\t// We have 2 important cases to consider:\n\t//\n\t//  1.  Users have enabled out-of-order migrations, in which case we need to check if any\n\t//      migrations are missing and report that there are pending migrations. Do not surface an\n\t//      error because this is a valid state.\n\t//\n\t//  2.  Users have disabled out-of-order migrations (default), in which case we need to check if all\n\t//      migrations have been applied. We cannot check for the highest applied version because we lose the\n\t//      ability to surface an error if an out-of-order migration was introduced. It would be silently\n\t//      ignored and the user would not know that they have unapplied migrations.\n\t//\n\t//      Maybe we could consider adding a flag to the provider such as IgnoreMissing, which would\n\t//      allow silently ignoring missing migrations. This would be useful for users that have built\n\t//      checks that prevent missing migrations from being introduced.\n\n\tdbMigrations, err := p.store.ListMigrations(ctx, conn)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tapply, err := gooseutil.UpVersions(\n\t\tgetVersionsFromMigrations(p.migrations),     // fsys versions\n\t\tgetVersionsFromListMigrations(dbMigrations), // db versions\n\t\tmath.MaxInt64,\n\t\tp.cfg.allowMissing,\n\t)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn len(apply) > 0, nil\n}\n\nfunc getVersionsFromMigrations(in []*Migration) []int64 {\n\tout := make([]int64, 0, len(in))\n\tfor _, m := range in {\n\t\tout = append(out, m.Version)\n\t}\n\treturn out\n\n}\n\nfunc getVersionsFromListMigrations(in []*database.ListMigrationsResult) []int64 {\n\tout := make([]int64, 0, len(in))\n\tfor _, m := range in {\n\t\tout = append(out, m.Version)\n\t}\n\treturn out\n\n}\n\nfunc (p *Provider) status(ctx context.Context) (_ []*MigrationStatus, retErr error) {\n\tconn, cleanup, err := p.initialize(ctx, true)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to initialize: %w\", err)\n\t}\n\tdefer func() {\n\t\tretErr = multierr.Append(retErr, cleanup())\n\t}()\n\n\tstatus := make([]*MigrationStatus, 0, len(p.migrations))\n\tfor _, m := range p.migrations {\n\t\tmigrationStatus := &MigrationStatus{\n\t\t\tSource: &Source{\n\t\t\t\tType:    m.Type,\n\t\t\t\tPath:    m.Source,\n\t\t\t\tVersion: m.Version,\n\t\t\t},\n\t\t\tState: StatePending,\n\t\t}\n\t\t// If versioning is disabled, we can't check the database for applied migrations, so we\n\t\t// assume all migrations are pending.\n\t\tif !p.cfg.disableVersioning {\n\t\t\tdbResult, err := p.store.GetMigration(ctx, conn, m.Version)\n\t\t\tif err != nil && !errors.Is(err, database.ErrVersionNotFound) {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif dbResult != nil {\n\t\t\t\tmigrationStatus.State = StateApplied\n\t\t\t\tmigrationStatus.AppliedAt = dbResult.Timestamp\n\t\t\t}\n\t\t}\n\t\tstatus = append(status, migrationStatus)\n\t}\n\n\treturn status, nil\n}\n\n// getDBMaxVersion returns the highest version recorded in the database, regardless of the order in\n// which migrations were applied. conn may be nil, in which case a connection is initialized.\nfunc (p *Provider) getDBMaxVersion(ctx context.Context, conn *sql.Conn) (_ int64, retErr error) {\n\tif conn == nil {\n\t\tvar cleanup func() error\n\t\tvar err error\n\t\tconn, cleanup, err = p.initialize(ctx, true)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\tdefer func() {\n\t\t\tretErr = multierr.Append(retErr, cleanup())\n\t\t}()\n\t}\n\n\tlatest, err := p.store.GetLatestVersion(ctx, conn)\n\tif err != nil {\n\t\tif errors.Is(err, database.ErrVersionNotFound) {\n\t\t\treturn 0, errMissingZeroVersion\n\t\t}\n\t\treturn -1, err\n\t}\n\treturn latest, nil\n}\n"
  },
  {
    "path": "provider_collect.go",
    "content": "package goose\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"strings\"\n)\n\n// fileSources represents a collection of migration files on the filesystem.\ntype fileSources struct {\n\tsqlSources []Source\n\tgoSources  []Source\n}\n\n// collectFilesystemSources scans the file system for migration files that have a numeric prefix\n// (greater than one) followed by an underscore and a file extension of either .go or .sql. fsys may\n// be nil, in which case an empty fileSources is returned.\n//\n// If strict is true, then any error parsing the numeric component of the filename will result in an\n// error. The file is skipped otherwise.\n//\n// This function DOES NOT parse SQL migrations or merge registered Go migrations. It only collects\n// migration sources from the filesystem.\nfunc collectFilesystemSources(\n\tfsys fs.FS,\n\tstrict bool,\n\texcludePaths map[string]bool,\n\texcludeVersions map[int64]bool,\n) (*fileSources, error) {\n\tif fsys == nil {\n\t\treturn new(fileSources), nil\n\t}\n\tsources := new(fileSources)\n\tversionToBaseLookup := make(map[int64]string) // map[version]filepath.Base(fullpath)\n\tfor _, pattern := range []string{\n\t\t\"*.sql\",\n\t\t\"*.go\",\n\t} {\n\t\tfiles, err := fs.Glob(fsys, pattern)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to glob pattern %q: %w\", pattern, err)\n\t\t}\n\t\tfor _, fullpath := range files {\n\t\t\tbase := filepath.Base(fullpath)\n\t\t\tif strings.HasSuffix(base, \"_test.go\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif excludePaths[base] {\n\t\t\t\t// TODO(mf): log this?\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// If the filename has a valid looking version of the form: NUMBER_.{sql,go}, then use\n\t\t\t// that as the version. Otherwise, ignore it. This allows users to have arbitrary\n\t\t\t// filenames, but still have versioned migrations within the same directory. For\n\t\t\t// example, a user could have a helpers.go file which contains unexported helper\n\t\t\t// functions for migrations.\n\t\t\tversion, err := NumericComponent(base)\n\t\t\tif err != nil {\n\t\t\t\tif strict {\n\t\t\t\t\treturn nil, fmt.Errorf(\"failed to parse numeric component from %q: %w\", base, err)\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif excludeVersions[version] {\n\t\t\t\t// TODO: log this?\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// Ensure there are no duplicate versions.\n\t\t\tif existing, ok := versionToBaseLookup[version]; ok {\n\t\t\t\treturn nil, fmt.Errorf(\"found duplicate migration version %d:\\n\\texisting:%v\\n\\tcurrent:%v\",\n\t\t\t\t\tversion,\n\t\t\t\t\texisting,\n\t\t\t\t\tbase,\n\t\t\t\t)\n\t\t\t}\n\t\t\tswitch filepath.Ext(base) {\n\t\t\tcase \".sql\":\n\t\t\t\tsources.sqlSources = append(sources.sqlSources, Source{\n\t\t\t\t\tType:    TypeSQL,\n\t\t\t\t\tPath:    fullpath,\n\t\t\t\t\tVersion: version,\n\t\t\t\t})\n\t\t\tcase \".go\":\n\t\t\t\tsources.goSources = append(sources.goSources, Source{\n\t\t\t\t\tType:    TypeGo,\n\t\t\t\t\tPath:    fullpath,\n\t\t\t\t\tVersion: version,\n\t\t\t\t})\n\t\t\tdefault:\n\t\t\t\t// Should never happen since we already filtered out all other file types.\n\t\t\t\treturn nil, fmt.Errorf(\"invalid file extension: %q\", base)\n\t\t\t}\n\t\t\t// Add the version to the lookup map.\n\t\t\tversionToBaseLookup[version] = base\n\t\t}\n\t}\n\treturn sources, nil\n}\n\nfunc newSQLMigration(source Source) *Migration {\n\treturn &Migration{\n\t\tType:      source.Type,\n\t\tVersion:   source.Version,\n\t\tSource:    source.Path,\n\t\tconstruct: true,\n\t\tNext:      -1, Previous: -1,\n\t\tsql: sqlMigration{\n\t\t\tParsed: false, // SQL migrations are parsed lazily.\n\t\t},\n\t}\n}\n\nfunc merge(sources *fileSources, registered map[int64]*Migration) ([]*Migration, error) {\n\tvar migrations []*Migration\n\tmigrationLookup := make(map[int64]*Migration)\n\t// Add all SQL migrations to the list of migrations.\n\tfor _, source := range sources.sqlSources {\n\t\tm := newSQLMigration(source)\n\t\tmigrations = append(migrations, m)\n\t\tmigrationLookup[source.Version] = m\n\t}\n\t// If there are no Go files in the filesystem and no registered Go migrations, return early.\n\tif len(sources.goSources) == 0 && len(registered) == 0 {\n\t\treturn migrations, nil\n\t}\n\t// Return an error if the given sources contain a versioned Go migration that has not been\n\t// registered. This is a sanity check to ensure users didn't accidentally create a valid looking\n\t// Go migration file on disk and forget to register it.\n\t//\n\t// This is almost always a user error.\n\tvar unregistered []string\n\tfor _, s := range sources.goSources {\n\t\tm, ok := registered[s.Version]\n\t\tif !ok {\n\t\t\tunregistered = append(unregistered, s.Path)\n\t\t} else {\n\t\t\t// Populate the source path for registered Go migrations that have a corresponding file\n\t\t\t// on disk.\n\t\t\tm.Source = s.Path\n\t\t}\n\t}\n\tif len(unregistered) > 0 {\n\t\treturn nil, unregisteredError(unregistered)\n\t}\n\t// Add all registered Go migrations to the list of migrations, checking for duplicate versions.\n\t//\n\t// Important, users can register Go migrations manually via goose.Add_ functions. These\n\t// migrations may not have a corresponding file on disk. Which is fine! We include them\n\t// wholesale as part of migrations. This allows users to build a custom binary that only embeds\n\t// the SQL migration files.\n\tfor version, r := range registered {\n\t\t// Ensure there are no duplicate versions.\n\t\tif existing, ok := migrationLookup[version]; ok {\n\t\t\tfullpath := r.Source\n\t\t\tif fullpath == \"\" {\n\t\t\t\tfullpath = \"no source path\"\n\t\t\t}\n\t\t\treturn nil, fmt.Errorf(\"found duplicate migration version %d:\\n\\texisting:%v\\n\\tcurrent:%v\",\n\t\t\t\tversion,\n\t\t\t\texisting.Source,\n\t\t\t\tfullpath,\n\t\t\t)\n\t\t}\n\t\tmigrations = append(migrations, r)\n\t\tmigrationLookup[version] = r\n\t}\n\t// Sort migrations by version in ascending order.\n\tsort.Slice(migrations, func(i, j int) bool {\n\t\treturn migrations[i].Version < migrations[j].Version\n\t})\n\treturn migrations, nil\n}\n\nfunc unregisteredError(unregistered []string) error {\n\tconst (\n\t\thintURL = \"https://github.com/pressly/goose/tree/master/examples/go-migrations\"\n\t)\n\tf := \"file\"\n\tif len(unregistered) > 1 {\n\t\tf += \"s\"\n\t}\n\tvar b strings.Builder\n\n\tfmt.Fprintf(&b, \"error: detected %d unregistered Go %s:\\n\", len(unregistered), f)\n\tfor _, name := range unregistered {\n\t\tb.WriteString(\"\\t\" + name + \"\\n\")\n\t}\n\thint := fmt.Sprintf(\"hint: go functions must be registered and built into a custom binary see:\\n%s\", hintURL)\n\tb.WriteString(hint)\n\tb.WriteString(\"\\n\")\n\n\treturn errors.New(b.String())\n}\n"
  },
  {
    "path": "provider_collect_test.go",
    "content": "package goose\n\nimport (\n\t\"io/fs\"\n\t\"testing\"\n\t\"testing/fstest\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCollectFileSources(t *testing.T) {\n\tt.Parallel()\n\tt.Run(\"nil_fsys\", func(t *testing.T) {\n\t\tsources, err := collectFilesystemSources(nil, false, nil, nil)\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, sources)\n\t\trequire.Empty(t, sources.goSources)\n\t\trequire.Empty(t, sources.sqlSources)\n\t})\n\tt.Run(\"noop_fsys\", func(t *testing.T) {\n\t\tsources, err := collectFilesystemSources(noopFS{}, false, nil, nil)\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, sources)\n\t\trequire.Empty(t, sources.goSources)\n\t\trequire.Empty(t, sources.sqlSources)\n\t})\n\tt.Run(\"empty_fsys\", func(t *testing.T) {\n\t\tsources, err := collectFilesystemSources(fstest.MapFS{}, false, nil, nil)\n\t\trequire.NoError(t, err)\n\t\trequire.Empty(t, sources.goSources)\n\t\trequire.Empty(t, sources.sqlSources)\n\t\trequire.NotNil(t, sources)\n\t})\n\tt.Run(\"incorrect_fsys\", func(t *testing.T) {\n\t\tmapFS := fstest.MapFS{\n\t\t\t\"00000_foo.sql\": sqlMapFile,\n\t\t}\n\t\t// strict disable - should not error\n\t\tsources, err := collectFilesystemSources(mapFS, false, nil, nil)\n\t\trequire.NoError(t, err)\n\t\trequire.Empty(t, sources.goSources)\n\t\trequire.Empty(t, sources.sqlSources)\n\t\t// strict enabled - should error\n\t\t_, err = collectFilesystemSources(mapFS, true, nil, nil)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"migration version must be greater than zero\")\n\t})\n\tt.Run(\"collect\", func(t *testing.T) {\n\t\tfsys, err := fs.Sub(newSQLOnlyFS(), \"migrations\")\n\t\trequire.NoError(t, err)\n\t\tsources, err := collectFilesystemSources(fsys, false, nil, nil)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, sources.sqlSources, 4)\n\t\trequire.Empty(t, sources.goSources)\n\t\texpected := fileSources{\n\t\t\tsqlSources: []Source{\n\t\t\t\tnewSource(TypeSQL, \"00001_foo.sql\", 1),\n\t\t\t\tnewSource(TypeSQL, \"00002_bar.sql\", 2),\n\t\t\t\tnewSource(TypeSQL, \"00003_baz.sql\", 3),\n\t\t\t\tnewSource(TypeSQL, \"00110_qux.sql\", 110),\n\t\t\t},\n\t\t}\n\t\tfor i := 0; i < len(sources.sqlSources); i++ {\n\t\t\trequire.Equal(t, sources.sqlSources[i], expected.sqlSources[i])\n\t\t}\n\t})\n\tt.Run(\"excludes\", func(t *testing.T) {\n\t\tfsys, err := fs.Sub(newSQLOnlyFS(), \"migrations\")\n\t\trequire.NoError(t, err)\n\t\tsources, err := collectFilesystemSources(\n\t\t\tfsys,\n\t\t\tfalse,\n\t\t\t// exclude 2 files explicitly\n\t\t\tmap[string]bool{\n\t\t\t\t\"00002_bar.sql\": true,\n\t\t\t\t\"00110_qux.sql\": true,\n\t\t\t},\n\t\t\tnil,\n\t\t)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, sources.sqlSources, 2)\n\t\trequire.Empty(t, sources.goSources)\n\t\texpected := fileSources{\n\t\t\tsqlSources: []Source{\n\t\t\t\tnewSource(TypeSQL, \"00001_foo.sql\", 1),\n\t\t\t\tnewSource(TypeSQL, \"00003_baz.sql\", 3),\n\t\t\t},\n\t\t}\n\t\tfor i := 0; i < len(sources.sqlSources); i++ {\n\t\t\trequire.Equal(t, sources.sqlSources[i], expected.sqlSources[i])\n\t\t}\n\t})\n\tt.Run(\"strict\", func(t *testing.T) {\n\t\tmapFS := newSQLOnlyFS()\n\t\t// Add a file with no version number\n\t\tmapFS[\"migrations/not_valid.sql\"] = &fstest.MapFile{Data: []byte(\"invalid\")}\n\t\tfsys, err := fs.Sub(mapFS, \"migrations\")\n\t\trequire.NoError(t, err)\n\t\t_, err = collectFilesystemSources(fsys, true, nil, nil)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), `failed to parse numeric component from \"not_valid.sql\"`)\n\t})\n\tt.Run(\"skip_go_test_files\", func(t *testing.T) {\n\t\tmapFS := fstest.MapFS{\n\t\t\t\"1_foo.sql\":     sqlMapFile,\n\t\t\t\"2_bar.sql\":     sqlMapFile,\n\t\t\t\"3_baz.sql\":     sqlMapFile,\n\t\t\t\"4_qux.sql\":     sqlMapFile,\n\t\t\t\"5_foo_test.go\": {Data: []byte(`package goose_test`)},\n\t\t}\n\t\tsources, err := collectFilesystemSources(mapFS, false, nil, nil)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, sources.sqlSources, 4)\n\t\trequire.Empty(t, sources.goSources)\n\t})\n\tt.Run(\"skip_random_files\", func(t *testing.T) {\n\t\tmapFS := fstest.MapFS{\n\t\t\t\"1_foo.sql\":                sqlMapFile,\n\t\t\t\"4_something.go\":           {Data: []byte(`package goose`)},\n\t\t\t\"5_qux.sql\":                sqlMapFile,\n\t\t\t\"README.md\":                {Data: []byte(`# README`)},\n\t\t\t\"LICENSE\":                  {Data: []byte(`MIT`)},\n\t\t\t\"no_a_real_migration.sql\":  {Data: []byte(`SELECT 1;`)},\n\t\t\t\"some/other/dir/2_foo.sql\": {Data: []byte(`SELECT 1;`)},\n\t\t}\n\t\tsources, err := collectFilesystemSources(mapFS, false, nil, nil)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, sources.sqlSources, 2)\n\t\trequire.Len(t, sources.goSources, 1)\n\t\t// 1\n\t\trequire.Equal(t, \"1_foo.sql\", sources.sqlSources[0].Path)\n\t\trequire.EqualValues(t, 1, sources.sqlSources[0].Version)\n\t\t// 2\n\t\trequire.Equal(t, \"5_qux.sql\", sources.sqlSources[1].Path)\n\t\trequire.EqualValues(t, 5, sources.sqlSources[1].Version)\n\t\t// 3\n\t\trequire.Equal(t, \"4_something.go\", sources.goSources[0].Path)\n\t\trequire.EqualValues(t, 4, sources.goSources[0].Version)\n\t})\n\tt.Run(\"duplicate_versions\", func(t *testing.T) {\n\t\tmapFS := fstest.MapFS{\n\t\t\t\"001_foo.sql\": sqlMapFile,\n\t\t\t\"01_bar.sql\":  sqlMapFile,\n\t\t}\n\t\t_, err := collectFilesystemSources(mapFS, false, nil, nil)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"found duplicate migration version 1\")\n\t})\n\tt.Run(\"dirpath\", func(t *testing.T) {\n\t\tmapFS := fstest.MapFS{\n\t\t\t\"dir1/101_a.sql\": sqlMapFile,\n\t\t\t\"dir1/102_b.sql\": sqlMapFile,\n\t\t\t\"dir1/103_c.sql\": sqlMapFile,\n\t\t\t\"dir2/201_a.sql\": sqlMapFile,\n\t\t\t\"876_a.sql\":      sqlMapFile,\n\t\t}\n\t\tassertDirpath := func(dirpath string, sqlSources []Source) {\n\t\t\tt.Helper()\n\t\t\tf, err := fs.Sub(mapFS, dirpath)\n\t\t\trequire.NoError(t, err)\n\t\t\tgot, err := collectFilesystemSources(f, false, nil, nil)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, sqlSources, len(got.sqlSources))\n\t\t\trequire.Empty(t, got.goSources)\n\t\t\tfor i := 0; i < len(got.sqlSources); i++ {\n\t\t\t\trequire.Equal(t, got.sqlSources[i], sqlSources[i])\n\t\t\t}\n\t\t}\n\t\tassertDirpath(\".\", []Source{\n\t\t\tnewSource(TypeSQL, \"876_a.sql\", 876),\n\t\t})\n\t\tassertDirpath(\"dir1\", []Source{\n\t\t\tnewSource(TypeSQL, \"101_a.sql\", 101),\n\t\t\tnewSource(TypeSQL, \"102_b.sql\", 102),\n\t\t\tnewSource(TypeSQL, \"103_c.sql\", 103),\n\t\t})\n\t\tassertDirpath(\"dir2\", []Source{\n\t\t\tnewSource(TypeSQL, \"201_a.sql\", 201),\n\t\t})\n\t\tassertDirpath(\"dir3\", nil)\n\t})\n}\n\nfunc TestMerge(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"with_go_files_on_disk\", func(t *testing.T) {\n\t\tmapFS := fstest.MapFS{\n\t\t\t// SQL\n\t\t\t\"migrations/00001_foo.sql\": sqlMapFile,\n\t\t\t// Go\n\t\t\t\"migrations/00002_bar.go\": {Data: []byte(`package migrations`)},\n\t\t\t\"migrations/00003_baz.go\": {Data: []byte(`package migrations`)},\n\t\t}\n\t\tfsys, err := fs.Sub(mapFS, \"migrations\")\n\t\trequire.NoError(t, err)\n\t\tsources, err := collectFilesystemSources(fsys, false, nil, nil)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, sources.sqlSources, 1)\n\t\trequire.Len(t, sources.goSources, 2)\n\t\tt.Run(\"valid\", func(t *testing.T) {\n\t\t\tregistered := map[int64]*Migration{\n\t\t\t\t2: NewGoMigration(2, nil, nil),\n\t\t\t\t3: NewGoMigration(3, nil, nil),\n\t\t\t}\n\t\t\tmigrations, err := merge(sources, registered)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, migrations, 3)\n\t\t\tassertMigration(t, migrations[0], newSource(TypeSQL, \"00001_foo.sql\", 1))\n\t\t\tassertMigration(t, migrations[1], newSource(TypeGo, \"00002_bar.go\", 2))\n\t\t\tassertMigration(t, migrations[2], newSource(TypeGo, \"00003_baz.go\", 3))\n\t\t})\n\t\tt.Run(\"unregistered_all\", func(t *testing.T) {\n\t\t\t_, err := merge(sources, nil)\n\t\t\trequire.Error(t, err)\n\t\t\trequire.Contains(t, err.Error(), \"error: detected 2 unregistered Go files:\")\n\t\t\trequire.Contains(t, err.Error(), \"00002_bar.go\")\n\t\t\trequire.Contains(t, err.Error(), \"00003_baz.go\")\n\t\t})\n\t\tt.Run(\"unregistered_some\", func(t *testing.T) {\n\t\t\t_, err := merge(sources, map[int64]*Migration{2: NewGoMigration(2, nil, nil)})\n\t\t\trequire.Error(t, err)\n\t\t\trequire.Contains(t, err.Error(), \"error: detected 1 unregistered Go file\")\n\t\t\trequire.Contains(t, err.Error(), \"00003_baz.go\")\n\t\t})\n\t\tt.Run(\"duplicate_sql\", func(t *testing.T) {\n\t\t\t_, err := merge(sources, map[int64]*Migration{\n\t\t\t\t1: NewGoMigration(1, nil, nil), // duplicate. SQL already exists.\n\t\t\t\t2: NewGoMigration(2, nil, nil),\n\t\t\t\t3: NewGoMigration(3, nil, nil),\n\t\t\t})\n\t\t\trequire.Error(t, err)\n\t\t\trequire.Contains(t, err.Error(), \"found duplicate migration version 1\")\n\t\t})\n\t})\n\tt.Run(\"no_go_files_on_disk\", func(t *testing.T) {\n\t\tmapFS := fstest.MapFS{\n\t\t\t// SQL\n\t\t\t\"migrations/00001_foo.sql\": sqlMapFile,\n\t\t\t\"migrations/00002_bar.sql\": sqlMapFile,\n\t\t\t\"migrations/00005_baz.sql\": sqlMapFile,\n\t\t}\n\t\tfsys, err := fs.Sub(mapFS, \"migrations\")\n\t\trequire.NoError(t, err)\n\t\tsources, err := collectFilesystemSources(fsys, false, nil, nil)\n\t\trequire.NoError(t, err)\n\t\tt.Run(\"unregistered_all\", func(t *testing.T) {\n\t\t\tmigrations, err := merge(sources, map[int64]*Migration{\n\t\t\t\t3: NewGoMigration(3, nil, nil),\n\t\t\t\t// 4 is missing\n\t\t\t\t6: NewGoMigration(6, nil, nil),\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, migrations, 5)\n\t\t\tassertMigration(t, migrations[0], newSource(TypeSQL, \"00001_foo.sql\", 1))\n\t\t\tassertMigration(t, migrations[1], newSource(TypeSQL, \"00002_bar.sql\", 2))\n\t\t\tassertMigration(t, migrations[2], newSource(TypeGo, \"\", 3))\n\t\t\tassertMigration(t, migrations[3], newSource(TypeSQL, \"00005_baz.sql\", 5))\n\t\t\tassertMigration(t, migrations[4], newSource(TypeGo, \"\", 6))\n\t\t})\n\t})\n\tt.Run(\"partial_go_files_on_disk\", func(t *testing.T) {\n\t\tmapFS := fstest.MapFS{\n\t\t\t\"migrations/00001_foo.sql\": sqlMapFile,\n\t\t\t\"migrations/00002_bar.go\":  &fstest.MapFile{Data: []byte(`package migrations`)},\n\t\t}\n\t\tfsys, err := fs.Sub(mapFS, \"migrations\")\n\t\trequire.NoError(t, err)\n\t\tsources, err := collectFilesystemSources(fsys, false, nil, nil)\n\t\trequire.NoError(t, err)\n\t\tt.Run(\"unregistered_all\", func(t *testing.T) {\n\t\t\tmigrations, err := merge(sources, map[int64]*Migration{\n\t\t\t\t// This is the only Go file on disk.\n\t\t\t\t2: NewGoMigration(2, nil, nil),\n\t\t\t\t// These are not on disk. Explicitly registered.\n\t\t\t\t3: NewGoMigration(3, nil, nil),\n\t\t\t\t6: NewGoMigration(6, nil, nil),\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, migrations, 4)\n\t\t\tassertMigration(t, migrations[0], newSource(TypeSQL, \"00001_foo.sql\", 1))\n\t\t\tassertMigration(t, migrations[1], newSource(TypeGo, \"00002_bar.go\", 2))\n\t\t\tassertMigration(t, migrations[2], newSource(TypeGo, \"\", 3))\n\t\t\tassertMigration(t, migrations[3], newSource(TypeGo, \"\", 6))\n\t\t})\n\t})\n}\n\nfunc assertMigration(t *testing.T, got *Migration, want Source) {\n\tt.Helper()\n\trequire.Equal(t, want.Type, got.Type)\n\trequire.Equal(t, want.Version, got.Version)\n\trequire.Equal(t, want.Path, got.Source)\n\tswitch got.Type {\n\tcase TypeGo:\n\t\trequire.NotNil(t, got.goUp)\n\t\trequire.NotNil(t, got.goDown)\n\tcase TypeSQL:\n\t\trequire.False(t, got.sql.Parsed)\n\tdefault:\n\t\tt.Fatalf(\"unknown migration type: %s\", got.Type)\n\t}\n}\n\nfunc newSQLOnlyFS() fstest.MapFS {\n\treturn fstest.MapFS{\n\t\t\"migrations/00001_foo.sql\": sqlMapFile,\n\t\t\"migrations/00002_bar.sql\": sqlMapFile,\n\t\t\"migrations/00003_baz.sql\": sqlMapFile,\n\t\t\"migrations/00110_qux.sql\": sqlMapFile,\n\t}\n}\n\nfunc newSource(t MigrationType, fullpath string, version int64) Source {\n\treturn Source{\n\t\tType:    t,\n\t\tPath:    fullpath,\n\t\tVersion: version,\n\t}\n}\n\nvar (\n\tsqlMapFile = &fstest.MapFile{Data: []byte(`-- +goose Up`)}\n)\n"
  },
  {
    "path": "provider_errors.go",
    "content": "package goose\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n)\n\nvar (\n\t// ErrVersionNotFound is returned when a specific migration version is not located. This can\n\t// occur if a .sql file or a Go migration function for the specified version is missing.\n\tErrVersionNotFound = errors.New(\"version not found\")\n\n\t// ErrNoMigrations is returned by [NewProvider] when no migrations are found.\n\tErrNoMigrations = errors.New(\"no migrations found\")\n\n\t// ErrAlreadyApplied indicates that the migration cannot be applied because it has already been\n\t// executed. This error is returned by [Provider.Apply].\n\tErrAlreadyApplied = errors.New(\"migration already applied\")\n\n\t// ErrNotApplied indicates that the rollback cannot be performed because the migration has not\n\t// yet been applied. This error is returned by [Provider.Apply].\n\tErrNotApplied = errors.New(\"migration not applied\")\n\n\t// errInvalidVersion is returned when a migration version is invalid.\n\terrInvalidVersion = errors.New(\"version must be greater than 0\")\n)\n\n// PartialError is returned when a migration fails, but some migrations already got applied.\ntype PartialError struct {\n\t// Applied are migrations that were applied successfully before the error occurred. May be\n\t// empty.\n\tApplied []*MigrationResult\n\t// Failed contains the result of the migration that failed. Cannot be nil.\n\tFailed *MigrationResult\n\t// Err is the error that occurred while running the migration and caused the failure.\n\tErr error\n}\n\nfunc (e *PartialError) Error() string {\n\treturn fmt.Sprintf(\n\t\t\"partial migration error (type:%s,version:%d): %v\",\n\t\te.Failed.Source.Type, e.Failed.Source.Version, e.Err,\n\t)\n}\n\nfunc (e *PartialError) Unwrap() error {\n\treturn e.Err\n}\n"
  },
  {
    "path": "provider_options.go",
    "content": "package goose\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/pressly/goose/v3/database\"\n\t\"github.com/pressly/goose/v3/lock\"\n)\n\nconst (\n\t// DefaultTablename is the default name of the database table used to track history of applied\n\t// migrations.\n\tDefaultTablename = \"goose_db_version\"\n)\n\n// ProviderOption is a configuration option for a goose goose.\ntype ProviderOption interface {\n\tapply(*config) error\n}\n\n// WithStore configures the provider with a custom [database.Store], allowing users to bring their\n// own implementation of the store interface. When this option is used, the dialect parameter of\n// [NewProvider] must be set to [DialectCustom].\n//\n// This option cannot be used together with [WithTableName], since the table name is set on the\n// store.\n//\n// By default, the provider uses the [database.NewStore] function to create a store backed by one of\n// the officially supported dialects.\nfunc WithStore(store database.Store) ProviderOption {\n\treturn configFunc(func(c *config) error {\n\t\tif c.store != nil {\n\t\t\treturn fmt.Errorf(\"store already set: %T\", c.store)\n\t\t}\n\t\tif store == nil {\n\t\t\treturn errors.New(\"store must not be nil\")\n\t\t}\n\t\tif store.Tablename() == \"\" {\n\t\t\treturn errors.New(\"store implementation must set the table name\")\n\t\t}\n\t\tc.store = store\n\t\treturn nil\n\t})\n}\n\n// WithTableName sets the name of the database table used to track history of applied migrations.\n// This option cannot be used together with [WithStore], since the table name is set on the store.\n//\n// Default is \"goose_db_version\".\nfunc WithTableName(name string) ProviderOption {\n\treturn configFunc(func(c *config) error {\n\t\tif name == \"\" {\n\t\t\treturn errors.New(\"table name must not be empty\")\n\t\t}\n\t\tc.tableName = name\n\t\treturn nil\n\t})\n}\n\n// WithVerbose enables verbose logging.\nfunc WithVerbose(b bool) ProviderOption {\n\treturn configFunc(func(c *config) error {\n\t\tc.verbose = b\n\t\treturn nil\n\t})\n}\n\n// WithSessionLocker enables locking using the provided SessionLocker.\n//\n// If WithSessionLocker is not called, locking is disabled. Must not be used together with\n// [WithLocker].\nfunc WithSessionLocker(locker lock.SessionLocker) ProviderOption {\n\treturn configFunc(func(c *config) error {\n\t\tif c.lockEnabled {\n\t\t\treturn errors.New(\"lock already enabled\")\n\t\t}\n\t\tif c.sessionLocker != nil {\n\t\t\treturn errors.New(\"session locker already set\")\n\t\t}\n\t\tif c.locker != nil {\n\t\t\treturn errors.New(\"locker already set; cannot use both SessionLocker and Locker\")\n\t\t}\n\t\tif locker == nil {\n\t\t\treturn errors.New(\"session locker must not be nil\")\n\t\t}\n\t\tc.lockEnabled = true\n\t\tc.sessionLocker = locker\n\t\treturn nil\n\t})\n}\n\n// WithLocker enables locking using the provided Locker.\n//\n// If WithLocker is not called, locking is disabled. Must not be used together with\n// [WithSessionLocker].\nfunc WithLocker(locker lock.Locker) ProviderOption {\n\treturn configFunc(func(c *config) error {\n\t\tif c.lockEnabled {\n\t\t\treturn errors.New(\"lock already enabled\")\n\t\t}\n\t\tif c.locker != nil {\n\t\t\treturn errors.New(\"locker already set\")\n\t\t}\n\t\tif c.sessionLocker != nil {\n\t\t\treturn errors.New(\"session locker already set; cannot use both SessionLocker and Locker\")\n\t\t}\n\t\tif locker == nil {\n\t\t\treturn errors.New(\"locker must not be nil\")\n\t\t}\n\t\tc.lockEnabled = true\n\t\tc.locker = locker\n\t\treturn nil\n\t})\n}\n\n// WithExcludeNames excludes the given file name from the list of migrations. If called multiple\n// times, the list of excludes is merged.\nfunc WithExcludeNames(excludes []string) ProviderOption {\n\treturn configFunc(func(c *config) error {\n\t\tfor _, name := range excludes {\n\t\t\tif _, ok := c.excludePaths[name]; ok {\n\t\t\t\treturn fmt.Errorf(\"duplicate exclude file name: %s\", name)\n\t\t\t}\n\t\t\tc.excludePaths[name] = true\n\t\t}\n\t\treturn nil\n\t})\n}\n\n// WithExcludeVersions excludes the given versions from the list of migrations. If called multiple\n// times, the list of excludes is merged.\nfunc WithExcludeVersions(versions []int64) ProviderOption {\n\treturn configFunc(func(c *config) error {\n\t\tfor _, version := range versions {\n\t\t\tif version < 1 {\n\t\t\t\treturn errInvalidVersion\n\t\t\t}\n\t\t\tif _, ok := c.excludeVersions[version]; ok {\n\t\t\t\treturn fmt.Errorf(\"duplicate excludes version: %d\", version)\n\t\t\t}\n\t\t\tc.excludeVersions[version] = true\n\t\t}\n\t\treturn nil\n\t})\n}\n\n// WithGoMigrations registers Go migrations with the provider. If a Go migration with the same\n// version has already been registered, an error will be returned.\n//\n// Go migrations must be constructed using the [NewGoMigration] function.\nfunc WithGoMigrations(migrations ...*Migration) ProviderOption {\n\treturn configFunc(func(c *config) error {\n\t\tfor _, m := range migrations {\n\t\t\tif _, ok := c.registered[m.Version]; ok {\n\t\t\t\treturn fmt.Errorf(\"go migration with version %d already registered\", m.Version)\n\t\t\t}\n\t\t\tif err := checkGoMigration(m); err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid go migration: %w\", err)\n\t\t\t}\n\t\t\tc.registered[m.Version] = m\n\t\t}\n\t\treturn nil\n\t})\n}\n\n// WithDisableGlobalRegistry prevents the provider from registering Go migrations from the global\n// registry. By default, goose will register all Go migrations including those registered globally.\nfunc WithDisableGlobalRegistry(b bool) ProviderOption {\n\treturn configFunc(func(c *config) error {\n\t\tc.disableGlobalRegistry = b\n\t\treturn nil\n\t})\n}\n\n// WithAllowOutofOrder allows the provider to apply missing (out-of-order) migrations. By default,\n// goose will raise an error if it encounters a missing migration.\n//\n// For example: migrations 1,3 are applied and then version 2,6 are introduced. If this option is\n// true, then goose will apply 2 (missing) and 6 (new) instead of raising an error. The final order\n// of applied migrations will be: 1,3,2,6. Out-of-order migrations are always applied first,\n// followed by new migrations.\nfunc WithAllowOutofOrder(b bool) ProviderOption {\n\treturn configFunc(func(c *config) error {\n\t\tc.allowMissing = b\n\t\treturn nil\n\t})\n}\n\n// WithDisableVersioning disables versioning. Disabling versioning allows applying migrations\n// without tracking the versions in the database schema table. Useful for tests, seeding a database\n// or running ad-hoc queries. By default, goose will track all versions in the database schema\n// table.\nfunc WithDisableVersioning(b bool) ProviderOption {\n\treturn configFunc(func(c *config) error {\n\t\tc.disableVersioning = b\n\t\treturn nil\n\t})\n}\n\n// WithLogger will set a custom Logger, which will override the default logger. Cannot be used\n// together with [WithSlog].\nfunc WithLogger(l Logger) ProviderOption {\n\treturn configFunc(func(c *config) error {\n\t\tif l == nil {\n\t\t\treturn errors.New(\"logger must not be nil\")\n\t\t}\n\t\tif c.slogger != nil {\n\t\t\treturn errors.New(\"cannot use both WithLogger and WithSlog\")\n\t\t}\n\t\tc.logger = l\n\t\treturn nil\n\t})\n}\n\n// WithSlog will set a custom [*slog.Logger] for structured logging. This enables rich structured\n// logging with attributes like source, direction, duration, etc. Cannot be used together with\n// [WithLogger].\n//\n// Example:\n//\n//\tlogger := slog.New(slog.NewTextHandler(os.Stdout, nil))\n//\tp, err := goose.NewProvider(\"postgres\", db, fs, goose.WithSlog(logger))\nfunc WithSlog(logger *slog.Logger) ProviderOption {\n\treturn configFunc(func(c *config) error {\n\t\tif logger == nil {\n\t\t\treturn errors.New(\"slog logger must not be nil\")\n\t\t}\n\t\tif c.logger != nil {\n\t\t\treturn errors.New(\"cannot use both WithLogger and WithSlog\")\n\t\t}\n\t\tc.slogger = logger\n\t\treturn nil\n\t})\n}\n\n// WithIsolateDDL executes DDL operations separately from DML operations. This is useful for\n// databases like AWS Aurora DSQL that don't support mixing DDL and DML within the same transaction.\nfunc WithIsolateDDL(b bool) ProviderOption {\n\treturn configFunc(func(c *config) error {\n\t\tc.isolateDDL = b\n\t\treturn nil\n\t})\n}\n\ntype config struct {\n\ttableName string\n\tstore     database.Store\n\n\tverbose         bool\n\texcludePaths    map[string]bool\n\texcludeVersions map[int64]bool\n\n\t// Go migrations registered by the user. These will be merged/resolved against the globally\n\t// registered migrations.\n\tregistered map[int64]*Migration\n\n\t// Locking options\n\tlockEnabled   bool\n\tsessionLocker lock.SessionLocker\n\tlocker        lock.Locker\n\n\t// Feature\n\tdisableVersioning     bool\n\tallowMissing          bool\n\tdisableGlobalRegistry bool\n\tisolateDDL            bool\n\n\t// Only a single logger can be set, they are mutually exclusive. If neither is set, a default\n\t// [Logger] will be set to maintain backward compatibility in /v3.\n\tlogger  Logger\n\tslogger *slog.Logger\n}\n\ntype configFunc func(*config) error\n\nfunc (f configFunc) apply(cfg *config) error {\n\treturn f(cfg)\n}\n"
  },
  {
    "path": "provider_options_test.go",
    "content": "package goose_test\n\nimport (\n\t\"database/sql\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"testing/fstest\"\n\n\t\"github.com/pressly/goose/v3\"\n\t\"github.com/pressly/goose/v3/database\"\n\t\"github.com/stretchr/testify/require\"\n\t_ \"modernc.org/sqlite\"\n)\n\nfunc TestNewProvider(t *testing.T) {\n\tdir := t.TempDir()\n\tdb, err := sql.Open(\"sqlite\", filepath.Join(dir, \"sql_embed.db\"))\n\trequire.NoError(t, err)\n\tfsys := fstest.MapFS{\n\t\t\"1_foo.sql\": {Data: []byte(migration1)},\n\t\t\"2_bar.sql\": {Data: []byte(migration2)},\n\t\t\"3_baz.sql\": {Data: []byte(migration3)},\n\t\t\"4_qux.sql\": {Data: []byte(migration4)},\n\t}\n\tt.Run(\"invalid\", func(t *testing.T) {\n\t\t// Empty dialect not allowed\n\t\t_, err = goose.NewProvider(goose.DialectCustom, db, fsys)\n\t\trequire.Error(t, err)\n\t\t// Invalid dialect not allowed\n\t\t_, err = goose.NewProvider(\"unknown-dialect\", db, fsys)\n\t\trequire.Error(t, err)\n\t\t// Nil db not allowed\n\t\t_, err = goose.NewProvider(goose.DialectSQLite3, nil, fsys)\n\t\trequire.Error(t, err)\n\t\t// Nil store not allowed\n\t\t_, err = goose.NewProvider(goose.DialectSQLite3, db, nil, goose.WithStore(nil))\n\t\trequire.Error(t, err)\n\t\t// Cannot set both dialect and store\n\t\tstore, err := database.NewStore(goose.DialectSQLite3, \"custom_table\")\n\t\trequire.NoError(t, err)\n\t\t_, err = goose.NewProvider(goose.DialectSQLite3, db, nil, goose.WithStore(store))\n\t\trequire.Error(t, err)\n\t\t// Multiple stores not allowed\n\t\t_, err = goose.NewProvider(goose.DialectSQLite3, db, nil,\n\t\t\tgoose.WithStore(store),\n\t\t\tgoose.WithStore(store),\n\t\t)\n\t\trequire.Error(t, err)\n\t\t// Cannot set empty table name\n\t\t_, err = goose.NewProvider(goose.DialectSQLite3, db, nil, goose.WithTableName(\"\"))\n\t\trequire.Error(t, err)\n\t\t// Cannot set table name when custom store is set\n\t\t_, err = goose.NewProvider(goose.DialectCustom, db, nil,\n\t\t\tgoose.WithStore(store),\n\t\t\tgoose.WithTableName(\"custom_table\"),\n\t\t)\n\t\trequire.Error(t, err)\n\t})\n\tt.Run(\"valid\", func(t *testing.T) {\n\t\t// Valid dialect, db, and fsys allowed\n\t\t_, err = goose.NewProvider(goose.DialectSQLite3, db, fsys)\n\t\trequire.NoError(t, err)\n\t\t// Valid dialect, db, fsys, and verbose allowed\n\t\t_, err = goose.NewProvider(goose.DialectSQLite3, db, fsys,\n\t\t\tgoose.WithVerbose(testing.Verbose()),\n\t\t)\n\t\trequire.NoError(t, err)\n\t\t// Custom store allowed\n\t\tconst tableName = \"custom_table\"\n\t\tstore, err := database.NewStore(goose.DialectSQLite3, tableName)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, tableName, store.Tablename())\n\t\t_, err = goose.NewProvider(goose.DialectCustom, db, fsys, goose.WithStore(store))\n\t\trequire.NoError(t, err)\n\t\t// Custom table name allowed on dialect-based store\n\t\t_, err = goose.NewProvider(goose.DialectSQLite3, db, fsys, goose.WithTableName(\"some_table\"))\n\t\trequire.NoError(t, err)\n\t})\n}\n"
  },
  {
    "path": "provider_run.go",
    "content": "package goose\n\nimport (\n\t\"cmp\"\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"path/filepath\"\n\t\"runtime/debug\"\n\t\"slices\"\n\t\"time\"\n\n\t\"github.com/pressly/goose/v3/database\"\n\t\"github.com/pressly/goose/v3/internal/sqlparser\"\n\t\"github.com/sethvargo/go-retry\"\n\t\"go.uber.org/multierr\"\n)\n\nvar (\n\terrMissingZeroVersion = errors.New(\"missing zero version migration\")\n)\n\nfunc (p *Provider) prepareMigration(fsys fs.FS, m *Migration, direction bool) error {\n\tswitch m.Type {\n\tcase TypeGo:\n\t\tif m.goUp.Mode == 0 {\n\t\t\treturn errors.New(\"go up migration mode is not set\")\n\t\t}\n\t\tif m.goDown.Mode == 0 {\n\t\t\treturn errors.New(\"go down migration mode is not set\")\n\t\t}\n\t\tvar useTx bool\n\t\tif direction {\n\t\t\tuseTx = m.goUp.Mode == TransactionEnabled\n\t\t} else {\n\t\t\tuseTx = m.goDown.Mode == TransactionEnabled\n\t\t}\n\t\t// bug(mf): this is a potential deadlock scenario. We're running Go migrations with *sql.DB,\n\t\t// but are locking the database with *sql.Conn. If the caller sets max open connections to\n\t\t// 1, then this will deadlock because the Go migration will try to acquire a connection from\n\t\t// the pool, but the pool is exhausted because the lock is held.\n\t\t//\n\t\t// A potential solution is to expose a third Go register function *sql.Conn. Or continue to\n\t\t// use *sql.DB and document that the user SHOULD NOT SET max open connections to 1. This is\n\t\t// a bit of an edge case. For now, we guard against this scenario by checking the max open\n\t\t// connections and returning an error.\n\t\tif p.cfg.lockEnabled && p.cfg.sessionLocker != nil && p.db.Stats().MaxOpenConnections == 1 {\n\t\t\tif !useTx {\n\t\t\t\treturn errors.New(\"potential deadlock detected: cannot run Go migration without a transaction when max open connections set to 1\")\n\t\t\t}\n\t\t}\n\t\treturn nil\n\tcase TypeSQL:\n\t\tif m.sql.Parsed {\n\t\t\treturn nil\n\t\t}\n\t\tparsed, err := sqlparser.ParseAllFromFS(fsys, m.Source, false)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tm.sql.Parsed = true\n\t\tm.sql.UseTx = parsed.UseTx\n\t\tm.sql.Up, m.sql.Down = parsed.Up, parsed.Down\n\t\treturn nil\n\t}\n\treturn fmt.Errorf(\"invalid migration type: %+v\", m)\n}\n\nfunc (p *Provider) logf(ctx context.Context, legacyMsg string, slogMsg string, attrs ...slog.Attr) {\n\tif !p.cfg.verbose {\n\t\treturn\n\t}\n\tif p.cfg.slogger != nil {\n\t\t// Sort attributes by key for consistent ordering\n\t\tslices.SortFunc(attrs, func(a, b slog.Attr) int {\n\t\t\treturn cmp.Compare(a.Key, b.Key)\n\t\t})\n\t\t// Use slog with structured attributes\n\t\targs := make([]any, 0, len(attrs)+1)\n\t\t// Add the logger=goose identifier\n\t\targs = append(args, slog.String(\"logger\", \"goose\"))\n\t\tfor _, attr := range attrs {\n\t\t\targs = append(args, attr)\n\t\t}\n\t\tp.cfg.slogger.InfoContext(ctx, slogMsg, args...)\n\t} else if p.cfg.logger != nil {\n\t\tp.cfg.logger.Printf(\"goose: %s\", legacyMsg)\n\t}\n}\n\n// runMigrations runs migrations sequentially in the given direction. If the migrations list is\n// empty, return nil without error.\nfunc (p *Provider) runMigrations(\n\tctx context.Context,\n\tconn *sql.Conn,\n\tmigrations []*Migration,\n\tdirection sqlparser.Direction,\n\tbyOne bool,\n) ([]*MigrationResult, error) {\n\tif len(migrations) == 0 {\n\t\tif !p.cfg.disableVersioning {\n\t\t\t// No need to print this message if versioning is disabled because there are no\n\t\t\t// migrations being tracked in the goose version table.\n\t\t\tmaxVersion, err := p.getDBMaxVersion(ctx, conn)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tp.logf(ctx,\n\t\t\t\tfmt.Sprintf(\"no migrations to run, current version: %d\", maxVersion),\n\t\t\t\t\"no migrations to run\",\n\t\t\t\tslog.Int64(\"current_version\", maxVersion),\n\t\t\t)\n\t\t}\n\t\treturn nil, nil\n\t}\n\tapply := migrations\n\tif byOne {\n\t\tapply = migrations[:1]\n\t}\n\n\t// SQL migrations are lazily parsed in both directions. This is done before attempting to run\n\t// any migrations to catch errors early and prevent leaving the database in an incomplete state.\n\n\tfor _, m := range apply {\n\t\tif err := p.prepareMigration(p.fsys, m, direction.ToBool()); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to prepare migration %s: %w\", m.ref(), err)\n\t\t}\n\t}\n\n\t// feat(mf): If we decide to add support for advisory locks at the transaction level, this may\n\t// be a good place to acquire the lock. However, we need to be sure that ALL migrations are safe\n\t// to run in a transaction.\n\n\t// feat(mf): this is where we can (optionally) group multiple migrations to be run in a single\n\t// transaction. The default is to apply each migration sequentially on its own. See the\n\t// following issues for more details:\n\t//  - https://github.com/pressly/goose/issues/485\n\t//  - https://github.com/pressly/goose/issues/222\n\t//\n\t// Be careful, we can't use a single transaction for all migrations because some may be marked\n\t// as not using a transaction.\n\n\tvar results []*MigrationResult\n\tfor _, m := range apply {\n\t\tresult := &MigrationResult{\n\t\t\tSource: &Source{\n\t\t\t\tType:    m.Type,\n\t\t\t\tPath:    m.Source,\n\t\t\t\tVersion: m.Version,\n\t\t\t},\n\t\t\tDirection: direction.String(),\n\t\t\tEmpty:     isEmpty(m, direction.ToBool()),\n\t\t}\n\t\tstart := time.Now()\n\t\tif err := p.runIndividually(ctx, conn, m, direction.ToBool()); err != nil {\n\t\t\t// TODO(mf): we should also return the pending migrations here, the remaining items in\n\t\t\t// the apply slice.\n\t\t\tresult.Error = err\n\t\t\tresult.Duration = time.Since(start)\n\t\t\treturn nil, &PartialError{\n\t\t\t\tApplied: results,\n\t\t\t\tFailed:  result,\n\t\t\t\tErr:     err,\n\t\t\t}\n\t\t}\n\t\tresult.Duration = time.Since(start)\n\t\tresults = append(results, result)\n\t\t// Log the result of the migration.\n\t\tvar state string\n\t\tif result.Empty {\n\t\t\tstate = \"empty\"\n\t\t} else {\n\t\t\tstate = \"applied\"\n\t\t}\n\t\tp.logf(ctx,\n\t\t\tresult.String(),\n\t\t\t\"migration completed\",\n\t\t\tslog.String(\"source\", filepath.Base(result.Source.Path)),\n\t\t\tslog.String(\"direction\", result.Direction),\n\t\t\tslog.Float64(\"duration_seconds\", result.Duration.Seconds()),\n\t\t\tslog.String(\"state\", state),\n\t\t\tslog.Int64(\"version\", result.Source.Version),\n\t\t\tslog.String(\"type\", string(result.Source.Type)),\n\t\t)\n\t}\n\tif !p.cfg.disableVersioning && !byOne {\n\t\tmaxVersion, err := p.getDBMaxVersion(ctx, conn)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tp.logf(ctx,\n\t\t\tfmt.Sprintf(\"successfully migrated database, current version: %d\", maxVersion),\n\t\t\t\"successfully migrated database\",\n\t\t\tslog.Int64(\"current_version\", maxVersion),\n\t\t)\n\t}\n\treturn results, nil\n}\n\nfunc (p *Provider) runIndividually(\n\tctx context.Context,\n\tconn *sql.Conn,\n\tm *Migration,\n\tdirection bool,\n) error {\n\tuseTx, err := useTx(m, direction)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif useTx && !p.cfg.isolateDDL {\n\t\treturn beginTx(ctx, conn, func(tx *sql.Tx) error {\n\t\t\tif err := p.runMigration(ctx, tx, m, direction); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn p.maybeInsertOrDelete(ctx, tx, m.Version, direction)\n\t\t})\n\t}\n\tswitch m.Type {\n\tcase TypeGo:\n\t\t// Note, we are using *sql.DB instead of *sql.Conn because it's the Go migration contract.\n\t\t// This may be a deadlock scenario if max open connections is set to 1 AND a lock is\n\t\t// acquired on the database. In this case, the migration will block forever unable to\n\t\t// acquire a connection from the pool.\n\t\t//\n\t\t// For now, we guard against this scenario by checking the max open connections and\n\t\t// returning an error in the prepareMigration function.\n\t\tif err := p.runMigration(ctx, p.db, m, direction); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn p.maybeInsertOrDelete(ctx, p.db, m.Version, direction)\n\tcase TypeSQL:\n\t\tif err := p.runMigration(ctx, conn, m, direction); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn p.maybeInsertOrDelete(ctx, conn, m.Version, direction)\n\t}\n\treturn fmt.Errorf(\"failed to run individual migration: neither sql or go: %v\", m)\n}\n\nfunc (p *Provider) maybeInsertOrDelete(\n\tctx context.Context,\n\tdb database.DBTxConn,\n\tversion int64,\n\tdirection bool,\n) error {\n\t// If versioning is disabled, we don't need to insert or delete the migration version.\n\tif p.cfg.disableVersioning {\n\t\treturn nil\n\t}\n\tif direction {\n\t\treturn p.store.Insert(ctx, db, database.InsertRequest{Version: version})\n\t}\n\treturn p.store.Delete(ctx, db, version)\n}\n\n// beginTx begins a transaction and runs the given function. If the function returns an error, the\n// transaction is rolled back. Otherwise, the transaction is committed.\nfunc beginTx(ctx context.Context, conn *sql.Conn, fn func(tx *sql.Tx) error) (retErr error) {\n\ttx, err := conn.BeginTx(ctx, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() {\n\t\tif retErr != nil {\n\t\t\tretErr = multierr.Append(retErr, tx.Rollback())\n\t\t}\n\t}()\n\tif err := fn(tx); err != nil {\n\t\treturn err\n\t}\n\treturn tx.Commit()\n}\n\nfunc (p *Provider) initialize(ctx context.Context, useLocker bool) (*sql.Conn, func() error, error) {\n\tp.mu.Lock()\n\tconn, err := p.db.Conn(ctx)\n\tif err != nil {\n\t\tp.mu.Unlock()\n\t\treturn nil, nil, err\n\t}\n\t// cleanup is a function that cleans up the connection, and optionally, the lock.\n\tcleanup := func() error {\n\t\tp.mu.Unlock()\n\t\treturn conn.Close()\n\t}\n\n\t// Handle locking if enabled and requested\n\tif useLocker && p.cfg.lockEnabled {\n\t\t// Session locker (connection-based locking)\n\t\tif p.cfg.sessionLocker != nil {\n\t\t\tl := p.cfg.sessionLocker\n\t\t\tif err := l.SessionLock(ctx, conn); err != nil {\n\t\t\t\treturn nil, nil, multierr.Append(err, cleanup())\n\t\t\t}\n\t\t\t// A lock was acquired, so we need to unlock the session when we're done. This is done\n\t\t\t// by returning a cleanup function that unlocks the session and closes the connection.\n\t\t\tcleanup = func() error {\n\t\t\t\tp.mu.Unlock()\n\t\t\t\t// Use a detached context to unlock the session. This is because the context passed\n\t\t\t\t// to SessionLock may have been canceled, and we don't want to cancel the unlock.\n\t\t\t\treturn multierr.Append(\n\t\t\t\t\tl.SessionUnlock(context.WithoutCancel(ctx), conn),\n\t\t\t\t\tconn.Close(),\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t\t// General locker (db-based locking)\n\t\tif p.cfg.locker != nil {\n\t\t\tl := p.cfg.locker\n\t\t\tif err := l.Lock(ctx, p.db); err != nil {\n\t\t\t\treturn nil, nil, multierr.Append(err, cleanup())\n\t\t\t}\n\t\t\t// A lock was acquired, so we need to unlock when we're done.\n\t\t\tcleanup = func() error {\n\t\t\t\tp.mu.Unlock()\n\t\t\t\t// Use a detached context to unlock. This is because the context passed to Lock may\n\t\t\t\t// have been canceled, and we don't want to cancel the unlock.\n\t\t\t\treturn multierr.Append(\n\t\t\t\t\tl.Unlock(context.WithoutCancel(ctx), p.db),\n\t\t\t\t\tconn.Close(),\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t}\n\t// If versioning is enabled, ensure the version table exists. For ad-hoc migrations, we don't\n\t// need the version table because no versions are being tracked.\n\tif !p.cfg.disableVersioning {\n\t\tif err := p.ensureVersionTable(ctx, conn); err != nil {\n\t\t\treturn nil, nil, multierr.Append(err, cleanup())\n\t\t}\n\t}\n\treturn conn, cleanup, nil\n}\n\nfunc (p *Provider) ensureVersionTable(\n\tctx context.Context,\n\tconn *sql.Conn,\n) (retErr error) {\n\t// There are 2 optimizations here:\n\t//  - 1. We create the version table once per Provider instance.\n\t//  - 2. We retry the operation a few times in case the table is being created concurrently.\n\t//\n\t// Regarding item 2, certain goose operations, like HasPending, don't respect a SessionLocker.\n\t// So, when goose is run for the first time in a multi-instance environment, it's possible that\n\t// multiple instances will try to create the version table at the same time. This is why we\n\t// retry this operation a few times. Best case, the table is created by one instance and all the\n\t// other instances see that change immediately. Worst case, all instances try to create the\n\t// table at the same time, but only one will succeed and the others will retry.\n\tp.versionTableOnce.Do(func() {\n\t\tretErr = p.tryEnsureVersionTable(ctx, conn)\n\t})\n\treturn retErr\n}\n\nfunc (p *Provider) tryEnsureVersionTable(ctx context.Context, conn *sql.Conn) error {\n\tb := retry.NewConstant(1 * time.Second)\n\tb = retry.WithMaxRetries(3, b)\n\treturn retry.Do(ctx, b, func(ctx context.Context) error {\n\t\texists, err := p.store.TableExists(ctx, conn)\n\t\tif err == nil && exists {\n\t\t\treturn nil\n\t\t} else if err != nil && errors.Is(err, errors.ErrUnsupported) {\n\t\t\t// Fallback strategy for checking table existence:\n\t\t\t//\n\t\t\t// When direct table existence checks aren't supported, we attempt to query the initial\n\t\t\t// migration (version 0). This approach has two implications:\n\t\t\t//\n\t\t\t//  1. If the table exists, the query succeeds and confirms existence\n\t\t\t//  2. If the table doesn't exist, the query fails and generates an error log\n\t\t\t//\n\t\t\t// Note: This check must occur outside any transaction, as a failed query would\n\t\t\t// otherwise cause the entire transaction to roll back. The error logs generated by this\n\t\t\t// approach are expected and can be safely ignored.\n\t\t\tif res, err := p.store.GetMigration(ctx, conn, 0); err == nil && res != nil {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\t// Fallthrough to create the table.\n\t\t} else if err != nil {\n\t\t\treturn fmt.Errorf(\"check if version table exists: %w\", err)\n\t\t}\n\n\t\tif p.cfg.isolateDDL {\n\t\t\t// If isolation is enabled, we create the version table separately to ensure subsequent\n\t\t\t// DML operations are not mixed with DDL.\n\t\t\tif err := p.store.CreateVersionTable(ctx, conn); err != nil {\n\t\t\t\treturn retry.RetryableError(fmt.Errorf(\"create version table: %w\", err))\n\t\t\t}\n\t\t\tif err := p.store.Insert(ctx, conn, database.InsertRequest{Version: 0}); err != nil {\n\t\t\t\treturn retry.RetryableError(fmt.Errorf(\"insert zero version: %w\", err))\n\t\t\t}\n\t\t} else {\n\t\t\t// If DDL isolation is not enabled, we can create the version table and insert the zero\n\t\t\t// version in a single transaction.\n\t\t\tif err := beginTx(ctx, conn, func(tx *sql.Tx) error {\n\t\t\t\tif err := p.store.CreateVersionTable(ctx, tx); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn p.store.Insert(ctx, tx, database.InsertRequest{Version: 0})\n\t\t\t}); err != nil {\n\t\t\t\t// Mark the error as retryable so we can try again. It's possible that another\n\t\t\t\t// instance is creating the table at the same time and the checks above will succeed\n\t\t\t\t// on the next iteration.\n\t\t\t\treturn retry.RetryableError(fmt.Errorf(\"create version table: %w\", err))\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t})\n}\n\n// getMigration returns the migration for the given version. If no migration is found, then\n// ErrVersionNotFound is returned.\nfunc (p *Provider) getMigration(version int64) (*Migration, error) {\n\tfor _, m := range p.migrations {\n\t\tif m.Version == version {\n\t\t\treturn m, nil\n\t\t}\n\t}\n\treturn nil, ErrVersionNotFound\n}\n\n// useTx is a helper function that returns true if the migration should be run in a transaction. It\n// must only be called after the migration has been parsed and initialized.\nfunc useTx(m *Migration, direction bool) (bool, error) {\n\tswitch m.Type {\n\tcase TypeGo:\n\t\tif m.goUp.Mode == 0 || m.goDown.Mode == 0 {\n\t\t\treturn false, fmt.Errorf(\"go migrations must have a mode set\")\n\t\t}\n\t\tif direction {\n\t\t\treturn m.goUp.Mode == TransactionEnabled, nil\n\t\t}\n\t\treturn m.goDown.Mode == TransactionEnabled, nil\n\tcase TypeSQL:\n\t\tif !m.sql.Parsed {\n\t\t\treturn false, fmt.Errorf(\"sql migrations must be parsed\")\n\t\t}\n\t\treturn m.sql.UseTx, nil\n\t}\n\treturn false, fmt.Errorf(\"use tx: invalid migration type: %q\", m.Type)\n}\n\n// isEmpty is a helper function that returns true if the migration has no functions or no statements\n// to execute. It must only be called after the migration has been parsed and initialized.\nfunc isEmpty(m *Migration, direction bool) bool {\n\tswitch m.Type {\n\tcase TypeGo:\n\t\tif direction {\n\t\t\treturn m.goUp.RunTx == nil && m.goUp.RunDB == nil\n\t\t}\n\t\treturn m.goDown.RunTx == nil && m.goDown.RunDB == nil\n\tcase TypeSQL:\n\t\tif direction {\n\t\t\treturn len(m.sql.Up) == 0\n\t\t}\n\t\treturn len(m.sql.Down) == 0\n\t}\n\treturn true\n}\n\n// runMigration is a helper function that runs the migration in the given direction. It must only be\n// called after the migration has been parsed and initialized.\nfunc (p *Provider) runMigration(ctx context.Context, db database.DBTxConn, m *Migration, direction bool) error {\n\tswitch m.Type {\n\tcase TypeGo:\n\t\treturn p.runGo(ctx, db, m, direction)\n\tcase TypeSQL:\n\t\treturn p.runSQL(ctx, db, m, direction)\n\t}\n\treturn fmt.Errorf(\"invalid migration type: %q\", m.Type)\n}\n\n// runGo is a helper function that runs the given Go functions in the given direction. It must only\n// be called after the migration has been initialized.\nfunc (p *Provider) runGo(ctx context.Context, db database.DBTxConn, m *Migration, direction bool) (retErr error) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tretErr = fmt.Errorf(\"panic: %v\\n%s\", r, debug.Stack())\n\t\t}\n\t}()\n\n\tswitch db := db.(type) {\n\tcase *sql.Conn:\n\t\treturn fmt.Errorf(\"go migrations are not supported with *sql.Conn\")\n\tcase *sql.DB:\n\t\tif direction && m.goUp.RunDB != nil {\n\t\t\treturn m.goUp.RunDB(ctx, db)\n\t\t}\n\t\tif !direction && m.goDown.RunDB != nil {\n\t\t\treturn m.goDown.RunDB(ctx, db)\n\t\t}\n\t\treturn nil\n\tcase *sql.Tx:\n\t\tif direction && m.goUp.RunTx != nil {\n\t\t\treturn m.goUp.RunTx(ctx, db)\n\t\t}\n\t\tif !direction && m.goDown.RunTx != nil {\n\t\t\treturn m.goDown.RunTx(ctx, db)\n\t\t}\n\t\treturn nil\n\t}\n\treturn fmt.Errorf(\"invalid database connection type: %T\", db)\n}\n\n// runSQL is a helper function that runs the given SQL statements in the given direction. It must\n// only be called after the migration has been parsed.\nfunc (p *Provider) runSQL(ctx context.Context, db database.DBTxConn, m *Migration, direction bool) error {\n\tif !m.sql.Parsed {\n\t\treturn fmt.Errorf(\"sql migrations must be parsed\")\n\t}\n\tvar statements []string\n\tif direction {\n\t\tstatements = m.sql.Up\n\t} else {\n\t\tstatements = m.sql.Down\n\t}\n\tfor _, stmt := range statements {\n\t\tp.logf(ctx,\n\t\t\tfmt.Sprintf(\"Executing statement: %s\", stmt),\n\t\t\t\"executing statement\",\n\t\t\tslog.String(\"statement\", stmt),\n\t\t\tslog.String(\"source\", filepath.Base(m.Source)),\n\t\t\tslog.Int64(\"version\", m.Version),\n\t\t\tslog.String(\"type\", string(m.Type)),\n\t\t\tslog.String(\"direction\", string(sqlparser.FromBool(direction))),\n\t\t)\n\t\tif _, err := db.ExecContext(ctx, stmt); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "provider_run_test.go",
    "content": "package goose_test\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"math/rand\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"sync\"\n\t\"testing\"\n\t\"testing/fstest\"\n\n\t\"github.com/pressly/goose/v3\"\n\t\"github.com/pressly/goose/v3/database\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestProviderRun(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"closed_db\", func(t *testing.T) {\n\t\tp, db := newProviderWithDB(t)\n\t\trequire.NoError(t, db.Close())\n\t\t_, err := p.Up(context.Background())\n\t\trequire.Error(t, err)\n\t\trequire.Equal(t, \"failed to initialize: sql: database is closed\", err.Error())\n\t})\n\tt.Run(\"ping_and_close\", func(t *testing.T) {\n\t\tp, _ := newProviderWithDB(t)\n\t\tt.Cleanup(func() {\n\t\t\trequire.NoError(t, p.Close())\n\t\t})\n\t\trequire.NoError(t, p.Ping(context.Background()))\n\t})\n\tt.Run(\"apply_unknown_version\", func(t *testing.T) {\n\t\tp, _ := newProviderWithDB(t)\n\t\t_, err := p.ApplyVersion(context.Background(), 999, true)\n\t\trequire.Error(t, err)\n\t\trequire.ErrorIs(t, err, goose.ErrVersionNotFound)\n\t\t_, err = p.ApplyVersion(context.Background(), 999, false)\n\t\trequire.Error(t, err)\n\t\trequire.ErrorIs(t, err, goose.ErrVersionNotFound)\n\t})\n\tt.Run(\"run_zero\", func(t *testing.T) {\n\t\tp, _ := newProviderWithDB(t)\n\t\t_, err := p.UpTo(context.Background(), 0)\n\t\trequire.Error(t, err)\n\t\trequire.Equal(t, \"version must be greater than 0\", err.Error())\n\t\t_, err = p.DownTo(context.Background(), -1)\n\t\trequire.Error(t, err)\n\t\trequire.Equal(t, \"invalid version: must be a valid number or zero: -1\", err.Error())\n\t\t_, err = p.ApplyVersion(context.Background(), 0, true)\n\t\trequire.Error(t, err)\n\t\trequire.Equal(t, \"version must be greater than 0\", err.Error())\n\t})\n\tt.Run(\"up_and_down_all\", func(t *testing.T) {\n\t\tctx := context.Background()\n\t\tp, _ := newProviderWithDB(t)\n\t\tconst (\n\t\t\tnumCount = 7\n\t\t)\n\t\tsources := p.ListSources()\n\t\trequire.Len(t, sources, numCount)\n\t\t// Ensure only SQL migrations are returned\n\t\tfor _, s := range sources {\n\t\t\trequire.Equal(t, goose.TypeSQL, s.Type)\n\t\t}\n\t\t// Test Up\n\t\tres, err := p.Up(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, res, numCount)\n\t\tassertResult(t, res[0], newSource(goose.TypeSQL, \"00001_users_table.sql\", 1), \"up\", false)\n\t\tassertResult(t, res[1], newSource(goose.TypeSQL, \"00002_posts_table.sql\", 2), \"up\", false)\n\t\tassertResult(t, res[2], newSource(goose.TypeSQL, \"00003_comments_table.sql\", 3), \"up\", false)\n\t\tassertResult(t, res[3], newSource(goose.TypeSQL, \"00004_insert_data.sql\", 4), \"up\", false)\n\t\tassertResult(t, res[4], newSource(goose.TypeSQL, \"00005_posts_view.sql\", 5), \"up\", false)\n\t\tassertResult(t, res[5], newSource(goose.TypeSQL, \"00006_empty_up.sql\", 6), \"up\", true)\n\t\tassertResult(t, res[6], newSource(goose.TypeSQL, \"00007_empty_up_down.sql\", 7), \"up\", true)\n\t\t// Test Down\n\t\tres, err = p.DownTo(ctx, 0)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, res, numCount)\n\t\tassertResult(t, res[0], newSource(goose.TypeSQL, \"00007_empty_up_down.sql\", 7), \"down\", true)\n\t\tassertResult(t, res[1], newSource(goose.TypeSQL, \"00006_empty_up.sql\", 6), \"down\", true)\n\t\tassertResult(t, res[2], newSource(goose.TypeSQL, \"00005_posts_view.sql\", 5), \"down\", false)\n\t\tassertResult(t, res[3], newSource(goose.TypeSQL, \"00004_insert_data.sql\", 4), \"down\", false)\n\t\tassertResult(t, res[4], newSource(goose.TypeSQL, \"00003_comments_table.sql\", 3), \"down\", false)\n\t\tassertResult(t, res[5], newSource(goose.TypeSQL, \"00002_posts_table.sql\", 2), \"down\", false)\n\t\tassertResult(t, res[6], newSource(goose.TypeSQL, \"00001_users_table.sql\", 1), \"down\", false)\n\t})\n\tt.Run(\"up_and_down_by_one\", func(t *testing.T) {\n\t\tctx := context.Background()\n\t\tp, _ := newProviderWithDB(t)\n\t\tmaxVersion := len(p.ListSources())\n\t\t// Apply all migrations one-by-one.\n\t\tvar counter int\n\t\tfor {\n\t\t\tres, err := p.UpByOne(ctx)\n\t\t\tcounter++\n\t\t\tif counter > maxVersion {\n\t\t\t\tif !errors.Is(err, goose.ErrNoNextVersion) {\n\t\t\t\t\tt.Fatalf(\"incorrect error: got:%v want:%v\", err, goose.ErrNoNextVersion)\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.NotNil(t, res)\n\t\t\trequire.Equal(t, res.Source.Version, int64(counter))\n\t\t}\n\t\tcurrentVersion, err := p.GetDBVersion(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, currentVersion, int64(maxVersion))\n\t\t// Reset counter\n\t\tcounter = 0\n\t\t// Rollback all migrations one-by-one.\n\t\tfor {\n\t\t\tres, err := p.Down(ctx)\n\t\t\tcounter++\n\t\t\tif counter > maxVersion {\n\t\t\t\tif !errors.Is(err, goose.ErrNoNextVersion) {\n\t\t\t\t\tt.Fatalf(\"incorrect error: got:%v want:%v\", err, goose.ErrNoNextVersion)\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.NotNil(t, res)\n\t\t\trequire.Equal(t, res.Source.Version, int64(maxVersion-counter+1))\n\t\t}\n\t\t// Once everything is tested the version should match the highest testdata version\n\t\tcurrentVersion, err = p.GetDBVersion(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.EqualValues(t, 0, currentVersion)\n\t})\n\tt.Run(\"up_to\", func(t *testing.T) {\n\t\tctx := context.Background()\n\t\tp, db := newProviderWithDB(t)\n\t\tconst (\n\t\t\tupToVersion int64 = 2\n\t\t)\n\t\tresults, err := p.UpTo(ctx, upToVersion)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, results, int(upToVersion))\n\t\tassertResult(t, results[0], newSource(goose.TypeSQL, \"00001_users_table.sql\", 1), \"up\", false)\n\t\tassertResult(t, results[1], newSource(goose.TypeSQL, \"00002_posts_table.sql\", 2), \"up\", false)\n\t\t// Fetch the goose version from DB\n\t\tcurrentVersion, err := p.GetDBVersion(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, upToVersion, currentVersion)\n\t\t// Validate the version actually matches what goose claims it is\n\t\tgotVersion, err := getMaxVersionID(db, goose.DefaultTablename)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, upToVersion, gotVersion)\n\t})\n\tt.Run(\"sql_connections\", func(t *testing.T) {\n\t\ttt := []struct {\n\t\t\tname         string\n\t\t\tmaxOpenConns int\n\t\t\tmaxIdleConns int\n\t\t\tuseDefaults  bool\n\t\t}{\n\t\t\t// Single connection ensures goose is able to function correctly when multiple\n\t\t\t// connections are not available.\n\t\t\t{name: \"single_conn\", maxOpenConns: 1, maxIdleConns: 1},\n\t\t\t{name: \"defaults\", useDefaults: true},\n\t\t}\n\t\tfor _, tc := range tt {\n\t\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t\tctx := context.Background()\n\t\t\t\t// Start a new database for each test case.\n\t\t\t\tp, db := newProviderWithDB(t)\n\t\t\t\tif !tc.useDefaults {\n\t\t\t\t\tdb.SetMaxOpenConns(tc.maxOpenConns)\n\t\t\t\t\tdb.SetMaxIdleConns(tc.maxIdleConns)\n\t\t\t\t}\n\t\t\t\tsources := p.ListSources()\n\t\t\t\trequire.NotEmpty(t, sources)\n\n\t\t\t\tcurrentVersion, err := p.GetDBVersion(ctx)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.EqualValues(t, 0, currentVersion)\n\n\t\t\t\t{\n\t\t\t\t\t// Apply all up migrations\n\t\t\t\t\tupResult, err := p.Up(ctx)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.Len(t, sources, len(upResult))\n\t\t\t\t\tcurrentVersion, err := p.GetDBVersion(ctx)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.Equal(t, currentVersion, p.ListSources()[len(sources)-1].Version)\n\t\t\t\t\t// Validate the db migration version actually matches what goose claims it is\n\t\t\t\t\tgotVersion, err := getMaxVersionID(db, goose.DefaultTablename)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.Equal(t, gotVersion, currentVersion)\n\t\t\t\t\ttables, err := getTableNames(db)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\tif !reflect.DeepEqual(tables, knownTables) {\n\t\t\t\t\t\tt.Logf(\"got tables: %v\", tables)\n\t\t\t\t\t\tt.Logf(\"known tables: %v\", knownTables)\n\t\t\t\t\t\tt.Fatal(\"failed to match tables\")\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t{\n\t\t\t\t\t// Apply all down migrations\n\t\t\t\t\tdownResult, err := p.DownTo(ctx, 0)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.Len(t, sources, len(downResult))\n\t\t\t\t\tgotVersion, err := getMaxVersionID(db, goose.DefaultTablename)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.EqualValues(t, 0, gotVersion)\n\t\t\t\t\t// Should only be left with a single table, the default goose table\n\t\t\t\t\ttables, err := getTableNames(db)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\tknownTables := []string{goose.DefaultTablename, \"sqlite_sequence\"}\n\t\t\t\t\tif !reflect.DeepEqual(tables, knownTables) {\n\t\t\t\t\t\tt.Logf(\"got tables: %v\", tables)\n\t\t\t\t\t\tt.Logf(\"known tables: %v\", knownTables)\n\t\t\t\t\t\tt.Fatal(\"failed to match tables\")\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t})\n\tt.Run(\"apply\", func(t *testing.T) {\n\t\tctx := context.Background()\n\t\tp, _ := newProviderWithDB(t)\n\t\tsources := p.ListSources()\n\t\t// Apply all migrations in the up direction.\n\t\tfor _, s := range sources {\n\t\t\tres, err := p.ApplyVersion(ctx, s.Version, true)\n\t\t\trequire.NoError(t, err)\n\t\t\t// Round-trip the migration result through the database to ensure it's valid.\n\t\t\tvar empty bool\n\t\t\tif s.Version == 6 || s.Version == 7 {\n\t\t\t\tempty = true\n\t\t\t}\n\t\t\tassertResult(t, res, s, \"up\", empty)\n\t\t}\n\t\t// Apply all migrations in the down direction.\n\t\tfor i := len(sources) - 1; i >= 0; i-- {\n\t\t\ts := sources[i]\n\t\t\tres, err := p.ApplyVersion(ctx, s.Version, false)\n\t\t\trequire.NoError(t, err)\n\t\t\t// Round-trip the migration result through the database to ensure it's valid.\n\t\t\tvar empty bool\n\t\t\tif s.Version == 6 || s.Version == 7 {\n\t\t\t\tempty = true\n\t\t\t}\n\t\t\tassertResult(t, res, s, \"down\", empty)\n\t\t}\n\t\t// Try apply version 1 multiple times\n\t\t_, err := p.ApplyVersion(ctx, 1, true)\n\t\trequire.NoError(t, err)\n\t\t_, err = p.ApplyVersion(ctx, 1, true)\n\t\trequire.Error(t, err)\n\t\trequire.ErrorIs(t, err, goose.ErrAlreadyApplied)\n\t\trequire.Contains(t, err.Error(), \"version 1: migration already applied\")\n\t\tt.Run(\"no_versioning\", func(t *testing.T) {\n\t\t\tp, db := newProviderWithDB(t, goose.WithDisableVersioning(true))\n\t\t\t_, err := p.ApplyVersion(ctx, 1, true)\n\t\t\trequire.NoError(t, err)\n\t\t\ttables, err := getTableNames(db)\n\t\t\trequire.NoError(t, err)\n\t\t\t// When versioning is disabled and a single migration is applied, the only table\n\t\t\t// expected is whatever the migration creates. No goose table is created.\n\t\t\tknownTables := []string{\"users\"}\n\t\t\trequire.Equal(t, knownTables, tables)\n\t\t})\n\t})\n\tt.Run(\"status\", func(t *testing.T) {\n\t\tctx := context.Background()\n\t\tp, _ := newProviderWithDB(t)\n\t\tnumCount := len(p.ListSources())\n\t\t// Before any migrations are applied, the status should be empty.\n\t\tstatus, err := p.Status(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, len(status), numCount)\n\t\tassertStatus(t, status[0], goose.StatePending, newSource(goose.TypeSQL, \"00001_users_table.sql\", 1), true)\n\t\tassertStatus(t, status[1], goose.StatePending, newSource(goose.TypeSQL, \"00002_posts_table.sql\", 2), true)\n\t\tassertStatus(t, status[2], goose.StatePending, newSource(goose.TypeSQL, \"00003_comments_table.sql\", 3), true)\n\t\tassertStatus(t, status[3], goose.StatePending, newSource(goose.TypeSQL, \"00004_insert_data.sql\", 4), true)\n\t\tassertStatus(t, status[4], goose.StatePending, newSource(goose.TypeSQL, \"00005_posts_view.sql\", 5), true)\n\t\tassertStatus(t, status[5], goose.StatePending, newSource(goose.TypeSQL, \"00006_empty_up.sql\", 6), true)\n\t\tassertStatus(t, status[6], goose.StatePending, newSource(goose.TypeSQL, \"00007_empty_up_down.sql\", 7), true)\n\t\t// Apply all migrations\n\t\t_, err = p.Up(ctx)\n\t\trequire.NoError(t, err)\n\t\tstatus, err = p.Status(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, len(status), numCount)\n\t\tassertStatus(t, status[0], goose.StateApplied, newSource(goose.TypeSQL, \"00001_users_table.sql\", 1), false)\n\t\tassertStatus(t, status[1], goose.StateApplied, newSource(goose.TypeSQL, \"00002_posts_table.sql\", 2), false)\n\t\tassertStatus(t, status[2], goose.StateApplied, newSource(goose.TypeSQL, \"00003_comments_table.sql\", 3), false)\n\t\tassertStatus(t, status[3], goose.StateApplied, newSource(goose.TypeSQL, \"00004_insert_data.sql\", 4), false)\n\t\tassertStatus(t, status[4], goose.StateApplied, newSource(goose.TypeSQL, \"00005_posts_view.sql\", 5), false)\n\t\tassertStatus(t, status[5], goose.StateApplied, newSource(goose.TypeSQL, \"00006_empty_up.sql\", 6), false)\n\t\tassertStatus(t, status[6], goose.StateApplied, newSource(goose.TypeSQL, \"00007_empty_up_down.sql\", 7), false)\n\t})\n\tt.Run(\"tx_partial_errors\", func(t *testing.T) {\n\t\tcountOwners := func(db *sql.DB) (int, error) {\n\t\t\tq := `SELECT count(*)FROM owners`\n\t\t\tvar count int\n\t\t\tif err := db.QueryRow(q).Scan(&count); err != nil {\n\t\t\t\treturn 0, err\n\t\t\t}\n\t\t\treturn count, nil\n\t\t}\n\n\t\tctx := context.Background()\n\t\tdb := newDB(t)\n\t\tmapFS := fstest.MapFS{\n\t\t\t\"00001_users_table.sql\": newMapFile(`\n-- +goose Up\nCREATE TABLE owners ( owner_name TEXT NOT NULL );\n`),\n\t\t\t\"00002_partial_error.sql\": newMapFile(`\n-- +goose Up\nINSERT INTO invalid_table (invalid_table) VALUES ('invalid_value');\n`),\n\t\t\t\"00003_insert_data.sql\": newMapFile(`\n-- +goose Up\nINSERT INTO owners (owner_name) VALUES ('seed-user-1');\nINSERT INTO owners (owner_name) VALUES ('seed-user-2');\nINSERT INTO owners (owner_name) VALUES ('seed-user-3');\n`),\n\t\t}\n\t\tp, err := goose.NewProvider(goose.DialectSQLite3, db, mapFS)\n\t\trequire.NoError(t, err)\n\t\t_, err = p.Up(ctx)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"partial migration error (type:sql,version:2)\")\n\t\texpected := new(goose.PartialError)\n\t\trequire.ErrorAs(t, err, &expected)\n\t\t// Check Err field\n\t\trequire.Contains(t, expected.Err.Error(), \"SQL logic error: no such table: invalid_table (1)\")\n\t\t// Check Results field\n\t\trequire.Len(t, expected.Applied, 1)\n\t\tassertResult(t, expected.Applied[0], newSource(goose.TypeSQL, \"00001_users_table.sql\", 1), \"up\", false)\n\t\t// Check Failed field\n\t\trequire.NotNil(t, expected.Failed)\n\t\tassertSource(t, expected.Failed.Source, goose.TypeSQL, \"00002_partial_error.sql\", 2)\n\t\trequire.False(t, expected.Failed.Empty)\n\t\trequire.Error(t, expected.Failed.Error)\n\t\trequire.Contains(t, expected.Failed.Error.Error(), \"SQL logic error: no such table: invalid_table (1)\")\n\t\trequire.Equal(t, \"up\", expected.Failed.Direction)\n\t\trequire.Positive(t, expected.Failed.Duration)\n\n\t\t// Ensure the partial error did not affect the database.\n\t\tcount, err := countOwners(db)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, 0, count)\n\n\t\tstatus, err := p.Status(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, status, 3)\n\t\tassertStatus(t, status[0], goose.StateApplied, newSource(goose.TypeSQL, \"00001_users_table.sql\", 1), false)\n\t\tassertStatus(t, status[1], goose.StatePending, newSource(goose.TypeSQL, \"00002_partial_error.sql\", 2), true)\n\t\tassertStatus(t, status[2], goose.StatePending, newSource(goose.TypeSQL, \"00003_insert_data.sql\", 3), true)\n\t})\n\tt.Run(\"isolate_ddl\", func(t *testing.T) {\n\t\tctx := context.Background()\n\t\tp, _ := newProviderWithDB(t, goose.WithIsolateDDL(true))\n\t\t// Apply all migrations\n\t\tres, err := p.Up(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, res, 7)\n\t\tcurrentVersion, err := p.GetDBVersion(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.EqualValues(t, 7, currentVersion)\n\t})\n}\n\nfunc TestConcurrentProvider(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"up\", func(t *testing.T) {\n\t\tctx := context.Background()\n\t\tp, _ := newProviderWithDB(t)\n\t\tmaxVersion := len(p.ListSources())\n\n\t\tch := make(chan int64)\n\t\tvar wg sync.WaitGroup\n\t\tfor range maxVersion {\n\n\t\t\twg.Go(func() {\n\t\t\t\tres, err := p.UpByOne(ctx)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif res == nil {\n\t\t\t\t\tt.Errorf(\"expected non-nil result, got nil\")\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tch <- res.Source.Version\n\t\t\t})\n\t\t}\n\t\tgo func() {\n\t\t\twg.Wait()\n\t\t\tclose(ch)\n\t\t}()\n\t\tvar versions []int64\n\t\tfor version := range ch {\n\t\t\tversions = append(versions, version)\n\t\t}\n\t\t// Fail early if any of the goroutines failed.\n\t\tif t.Failed() {\n\t\t\treturn\n\t\t}\n\t\trequire.Equal(t, len(versions), maxVersion)\n\t\tfor i := range maxVersion {\n\t\t\trequire.Equal(t, versions[i], int64(i+1))\n\t\t}\n\t\tcurrentVersion, err := p.GetDBVersion(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.EqualValues(t, currentVersion, maxVersion)\n\t})\n\tt.Run(\"down\", func(t *testing.T) {\n\t\tctx := context.Background()\n\t\tp, _ := newProviderWithDB(t)\n\t\tmaxVersion := len(p.ListSources())\n\t\t// Apply all migrations\n\t\t_, err := p.Up(ctx)\n\t\trequire.NoError(t, err)\n\t\tcurrentVersion, err := p.GetDBVersion(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.EqualValues(t, currentVersion, maxVersion)\n\n\t\tch := make(chan []*goose.MigrationResult)\n\t\tvar wg sync.WaitGroup\n\t\tfor range maxVersion {\n\n\t\t\twg.Go(func() {\n\t\t\t\tres, err := p.DownTo(ctx, 0)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tch <- res\n\t\t\t})\n\t\t}\n\t\tgo func() {\n\t\t\twg.Wait()\n\t\t\tclose(ch)\n\t\t}()\n\t\tvar (\n\t\t\tvalid [][]*goose.MigrationResult\n\t\t\tempty [][]*goose.MigrationResult\n\t\t)\n\t\tfor results := range ch {\n\t\t\tif len(results) == 0 {\n\t\t\t\tempty = append(empty, results)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tvalid = append(valid, results)\n\t\t}\n\t\t// Fail early if any of the goroutines failed.\n\t\tif t.Failed() {\n\t\t\treturn\n\t\t}\n\t\trequire.Len(t, valid, 1)\n\t\trequire.Equal(t, len(empty), maxVersion-1)\n\t\t// Ensure the valid result is correct.\n\t\trequire.Equal(t, len(valid[0]), maxVersion)\n\t})\n}\n\nfunc TestNoVersioning(t *testing.T) {\n\tt.Parallel()\n\n\tcountSeedOwners := func(db *sql.DB) (int, error) {\n\t\tq := `SELECT count(*)FROM owners WHERE owner_name LIKE'seed-user-%'`\n\t\tvar count int\n\t\tif err := db.QueryRow(q).Scan(&count); err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\treturn count, nil\n\t}\n\tcountOwners := func(db *sql.DB) (int, error) {\n\t\tq := `SELECT count(*)FROM owners`\n\t\tvar count int\n\t\tif err := db.QueryRow(q).Scan(&count); err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\treturn count, nil\n\t}\n\tctx := context.Background()\n\tdbName := fmt.Sprintf(\"test_%s.db\", randomAlphaNumeric(8))\n\tdb, err := sql.Open(\"sqlite\", filepath.Join(t.TempDir(), dbName))\n\trequire.NoError(t, err)\n\tfsys := os.DirFS(filepath.Join(\"testdata\", \"no-versioning\", \"migrations\"))\n\tconst (\n\t\t// Total owners created by the seed files.\n\t\twantSeedOwnerCount = 250\n\t\t// These are owners created by migration files.\n\t\twantOwnerCount = 4\n\t)\n\tp, err := goose.NewProvider(goose.DialectSQLite3, db, fsys,\n\t\tgoose.WithVerbose(testing.Verbose()),\n\t\tgoose.WithDisableVersioning(false), // This is the default.\n\t)\n\trequire.Len(t, p.ListSources(), 3)\n\trequire.NoError(t, err)\n\t_, err = p.Up(ctx)\n\trequire.NoError(t, err)\n\tbaseVersion, err := p.GetDBVersion(ctx)\n\trequire.NoError(t, err)\n\trequire.EqualValues(t, 3, baseVersion)\n\tt.Run(\"seed-up-down-to-zero\", func(t *testing.T) {\n\t\tfsys := os.DirFS(filepath.Join(\"testdata\", \"no-versioning\", \"seed\"))\n\t\tp, err := goose.NewProvider(goose.DialectSQLite3, db, fsys,\n\t\t\tgoose.WithVerbose(testing.Verbose()),\n\t\t\tgoose.WithDisableVersioning(true), // Provider with no versioning.\n\t\t)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, p.ListSources(), 2)\n\n\t\t// Run (all) up migrations from the seed dir\n\t\t{\n\t\t\tupResult, err := p.Up(ctx)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, upResult, 2)\n\t\t\t// When versioning is disabled, we cannot track the version of the seed files.\n\t\t\t_, err = p.GetDBVersion(ctx)\n\t\t\trequire.Error(t, err)\n\t\t\tseedOwnerCount, err := countSeedOwners(db)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, wantSeedOwnerCount, seedOwnerCount)\n\t\t}\n\t\t// Run (all) down migrations from the seed dir\n\t\t{\n\t\t\tdownResult, err := p.DownTo(ctx, 0)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, downResult, 2)\n\t\t\t// When versioning is disabled, we cannot track the version of the seed files.\n\t\t\t_, err = p.GetDBVersion(ctx)\n\t\t\trequire.Error(t, err)\n\t\t\tseedOwnerCount, err := countSeedOwners(db)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, 0, seedOwnerCount)\n\t\t}\n\t\t// The migrations added 4 non-seed owners, they must remain in the database afterwards\n\t\townerCount, err := countOwners(db)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, wantOwnerCount, ownerCount)\n\t})\n}\n\nfunc TestAllowMissing(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\n\t// Developer A and B check out the \"main\" branch which is currently on version 3. Developer A\n\t// mistakenly creates migration 5 and commits. Developer B did not pull the latest changes and\n\t// commits migration 4. Oops -- now the migrations are out of order.\n\t//\n\t// When goose is set to allow missing migrations, then 5 is applied after 4 with no error.\n\t// Otherwise it's expected to be an error.\n\n\tt.Run(\"missing_now_allowed\", func(t *testing.T) {\n\t\tdb := newDB(t)\n\t\tp, err := goose.NewProvider(goose.DialectSQLite3, db, newFsys(),\n\t\t\tgoose.WithAllowOutofOrder(false),\n\t\t)\n\t\trequire.NoError(t, err)\n\n\t\t// Create and apply first 3 migrations.\n\t\t_, err = p.UpTo(ctx, 3)\n\t\trequire.NoError(t, err)\n\t\tcurrentVersion, err := p.GetDBVersion(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.EqualValues(t, 3, currentVersion)\n\n\t\t// Developer A - migration 5 (mistakenly applied)\n\t\tresult, err := p.ApplyVersion(ctx, 5, true)\n\t\trequire.NoError(t, err)\n\t\trequire.EqualValues(t, 5, result.Source.Version)\n\t\tcurrent, err := p.GetDBVersion(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.EqualValues(t, 5, current)\n\n\t\t// The database has migrations 1,2,3,5 applied.\n\n\t\t// Developer B is on version 3 (e.g., never pulled the latest changes). Adds migration 4. By\n\t\t// default goose does not allow missing (out-of-order) migrations, which means halt if a\n\t\t// missing migration is detected.\n\t\t_, err = p.Up(ctx)\n\t\trequire.Error(t, err)\n\t\t// found 1 missing (out-of-order) migration: [00004_insert_data.sql]\n\t\trequire.Contains(t, err.Error(), \"missing (out-of-order) migration\")\n\t\t// Confirm db version is unchanged.\n\t\tcurrent, err = p.GetDBVersion(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.EqualValues(t, 5, current)\n\n\t\t_, err = p.UpByOne(ctx)\n\t\trequire.Error(t, err)\n\t\t// found 1 missing (out-of-order) migration: [00004_insert_data.sql]\n\t\trequire.Contains(t, err.Error(), \"missing (out-of-order) migration\")\n\t\t// Confirm db version is unchanged.\n\t\tcurrent, err = p.GetDBVersion(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.EqualValues(t, 5, current)\n\n\t\t_, err = p.UpTo(ctx, math.MaxInt64)\n\t\trequire.Error(t, err)\n\t\t// found 1 missing (out-of-order) migration: [00004_insert_data.sql]\n\t\trequire.Contains(t, err.Error(), \"missing (out-of-order) migration\")\n\t\t// Confirm db version is unchanged.\n\t\tcurrent, err = p.GetDBVersion(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.EqualValues(t, 5, current)\n\t})\n\n\tt.Run(\"missing_allowed\", func(t *testing.T) {\n\t\tdb := newDB(t)\n\t\tp, err := goose.NewProvider(goose.DialectSQLite3, db, newFsys(),\n\t\t\tgoose.WithAllowOutofOrder(true),\n\t\t)\n\t\trequire.NoError(t, err)\n\n\t\t// Create and apply first 3 migrations.\n\t\t_, err = p.UpTo(ctx, 3)\n\t\trequire.NoError(t, err)\n\t\tcurrentVersion, err := p.GetDBVersion(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.EqualValues(t, 3, currentVersion)\n\n\t\t// Developer A - migration 5 (mistakenly applied)\n\t\t{\n\t\t\t_, err = p.ApplyVersion(ctx, 5, true)\n\t\t\trequire.NoError(t, err)\n\t\t\tcurrent, err := p.GetDBVersion(ctx)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.EqualValues(t, 5, current)\n\t\t}\n\t\t// Developer B - migration 4 (missing) and 6 (new)\n\t\t{\n\t\t\t// 4\n\t\t\tupResult, err := p.UpByOne(ctx)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.NotNil(t, upResult)\n\t\t\trequire.EqualValues(t, 4, upResult.Source.Version)\n\t\t\t// 6\n\t\t\tupResult, err = p.UpByOne(ctx)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.NotNil(t, upResult)\n\t\t\trequire.EqualValues(t, 6, upResult.Source.Version)\n\n\t\t\tcount, err := getGooseVersionCount(db, goose.DefaultTablename)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.EqualValues(t, 6, count)\n\t\t\tcurrent, err := p.GetDBVersion(ctx)\n\t\t\trequire.NoError(t, err)\n\t\t\t// Expecting max(version_id) to be 8\n\t\t\trequire.EqualValues(t, 6, current)\n\t\t}\n\n\t\t// The applied order in the database is expected to be:\n\t\t//      1,2,3,5,4,6\n\t\t// So migrating down should be the reverse of the applied order:\n\t\t//      6,4,5,3,2,1\n\n\t\ttestDownAndVersion := func(wantDBVersion, wantResultVersion int64) {\n\t\t\tcurrentVersion, err := p.GetDBVersion(ctx)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, wantDBVersion, currentVersion)\n\t\t\tdownRes, err := p.Down(ctx)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.NotNil(t, downRes)\n\t\t\trequire.Equal(t, wantResultVersion, downRes.Source.Version)\n\t\t}\n\n\t\t// This behaviour may need to change, see the following issues for more details:\n\t\t//  - https://github.com/pressly/goose/issues/523\n\t\t//  - https://github.com/pressly/goose/issues/402\n\n\t\ttestDownAndVersion(6, 6)\n\t\ttestDownAndVersion(5, 4) // Ensure the max db version is 5 before down.\n\t\ttestDownAndVersion(5, 5)\n\t\ttestDownAndVersion(3, 3)\n\t\ttestDownAndVersion(2, 2)\n\t\ttestDownAndVersion(1, 1)\n\t\t_, err = p.Down(ctx)\n\t\trequire.Error(t, err)\n\t\trequire.ErrorIs(t, err, goose.ErrNoNextVersion)\n\t})\n}\n\nfunc TestSQLiteSharedCache(t *testing.T) {\n\tt.Parallel()\n\t// goose uses *sql.Conn for most operations (incl. creating the initial table), but for Go\n\t// migrations when running outside a transaction we use *sql.DB. This is a problem for SQLite\n\t// because it does not support shared cache mode by default and it does not see the table that\n\t// was created through the initial connection. This test ensures goose works with SQLite shared\n\t// cache mode.\n\t//\n\t// Ref: https://www.sqlite.org/inmemorydb.html\n\t//\n\t// \"In-memory databases are allowed to use shared cache if they are opened using a URI filename.\n\t// If the unadorned \":memory:\" name is used to specify the in-memory database, then that\n\t// database always has a private cache and is only visible to the database connection that\n\t// originally opened it. However, the same in-memory database can be opened by two or more\n\t// database connections as follows: file::memory:?cache=shared\"\n\tt.Run(\"shared_cache\", func(t *testing.T) {\n\t\tdb, err := sql.Open(\"sqlite\", \"file::memory:?cache=shared\")\n\t\trequire.NoError(t, err)\n\t\tfsys := fstest.MapFS{\"00001_a.sql\": newMapFile(`-- +goose Up`)}\n\t\tp, err := goose.NewProvider(goose.DialectSQLite3, db, fsys,\n\t\t\tgoose.WithGoMigrations(\n\t\t\t\tgoose.NewGoMigration(2, &goose.GoFunc{Mode: goose.TransactionDisabled}, nil),\n\t\t\t),\n\t\t)\n\t\trequire.NoError(t, err)\n\t\t_, err = p.Up(context.Background())\n\t\trequire.NoError(t, err)\n\t})\n\tt.Run(\"no_shared_cache\", func(t *testing.T) {\n\t\tdb, err := sql.Open(\"sqlite\", \"file::memory:\")\n\t\trequire.NoError(t, err)\n\t\tfsys := fstest.MapFS{\"00001_a.sql\": newMapFile(`-- +goose Up`)}\n\t\tp, err := goose.NewProvider(goose.DialectSQLite3, db, fsys,\n\t\t\tgoose.WithGoMigrations(\n\t\t\t\tgoose.NewGoMigration(2, &goose.GoFunc{Mode: goose.TransactionDisabled}, nil),\n\t\t\t),\n\t\t)\n\t\trequire.NoError(t, err)\n\t\t_, err = p.Up(context.Background())\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"SQL logic error: no such table: goose_db_version\")\n\t})\n}\n\nfunc TestGoMigrationPanic(t *testing.T) {\n\tt.Parallel()\n\n\tctx := context.Background()\n\tconst (\n\t\twantErrString = \"panic: runtime error: index out of range [7] with length 0\"\n\t)\n\tmigration := goose.NewGoMigration(\n\t\t1,\n\t\t&goose.GoFunc{RunTx: func(ctx context.Context, tx *sql.Tx) error {\n\t\t\tvar ss []int\n\t\t\t_ = ss[7]\n\t\t\treturn nil\n\t\t}},\n\t\tnil,\n\t)\n\tp, err := goose.NewProvider(goose.DialectSQLite3, newDB(t), nil,\n\t\tgoose.WithGoMigrations(migration), // Add a Go migration that panics.\n\t)\n\trequire.NoError(t, err)\n\t_, err = p.Up(ctx)\n\trequire.Error(t, err)\n\texpected := new(goose.PartialError)\n\trequire.ErrorAs(t, err, &expected)\n\trequire.Contains(t, expected.Err.Error(), wantErrString)\n}\n\nfunc TestCustomStoreTableExists(t *testing.T) {\n\tt.Parallel()\n\tdb := newDB(t)\n\tstore, err := database.NewStore(database.DialectSQLite3, goose.DefaultTablename)\n\trequire.NoError(t, err)\n\tfor range 2 {\n\t\tp, err := goose.NewProvider(goose.DialectCustom, db, newFsys(),\n\t\t\tgoose.WithStore(&customStoreSQLite3{store}),\n\t\t)\n\t\trequire.NoError(t, err)\n\t\t_, err = p.Up(context.Background())\n\t\trequire.NoError(t, err)\n\t}\n}\n\nfunc TestProviderApply(t *testing.T) {\n\tt.Parallel()\n\n\tctx := context.Background()\n\tp, err := goose.NewProvider(goose.DialectSQLite3, newDB(t), newFsys())\n\trequire.NoError(t, err)\n\t_, err = p.ApplyVersion(ctx, 1, true)\n\trequire.NoError(t, err)\n\t// This version has a corresponding down migration, but has never been applied.\n\t_, err = p.ApplyVersion(ctx, 2, false)\n\trequire.Error(t, err)\n\trequire.ErrorIs(t, err, goose.ErrNotApplied)\n}\n\nfunc TestPending(t *testing.T) {\n\tt.Parallel()\n\tt.Run(\"allow_out_of_order\", func(t *testing.T) {\n\t\tctx := context.Background()\n\t\tfsys := newFsys()\n\t\tp, err := goose.NewProvider(goose.DialectSQLite3, newDB(t), fsys,\n\t\t\tgoose.WithAllowOutofOrder(true),\n\t\t)\n\t\trequire.NoError(t, err)\n\t\t// Some migrations have been applied out of order.\n\t\t_, err = p.ApplyVersion(ctx, 1, true)\n\t\trequire.NoError(t, err)\n\t\t_, err = p.ApplyVersion(ctx, 3, true)\n\t\trequire.NoError(t, err)\n\t\t// Even though the latest migration HAS been applied, there are still pending out-of-order\n\t\t// migrations.\n\t\tcurrent, target, err := p.GetVersions(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.EqualValues(t, 3, current)\n\t\trequire.Len(t, fsys, int(target))\n\t\thasPending, err := p.HasPending(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.True(t, hasPending)\n\t\t// Apply the missing migrations.\n\t\t_, err = p.Up(ctx)\n\t\trequire.NoError(t, err)\n\t\t// All migrations have been applied.\n\t\thasPending, err = p.HasPending(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.False(t, hasPending)\n\t\tcurrent, target, err = p.GetVersions(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, current, target)\n\t})\n\tt.Run(\"disallow_out_of_order\", func(t *testing.T) {\n\t\tctx := context.Background()\n\t\tfsys := newFsys()\n\n\t\trun := func(t *testing.T, versionToApply int64) {\n\t\t\tt.Helper()\n\t\t\tp, err := goose.NewProvider(goose.DialectSQLite3, newDB(t), fsys,\n\t\t\t\tgoose.WithAllowOutofOrder(false),\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\t\t\t// Some migrations have been applied.\n\t\t\t_, err = p.ApplyVersion(ctx, 1, true)\n\t\t\trequire.NoError(t, err)\n\t\t\t_, err = p.ApplyVersion(ctx, versionToApply, true)\n\t\t\trequire.NoError(t, err)\n\t\t\t// TODO(mf): revisit the pending check behavior in addition to the HasPending\n\t\t\t// method.\n\t\t\tcurrent, target, err := p.GetVersions(ctx)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, current, versionToApply)\n\t\t\trequire.Len(t, fsys, int(target))\n\t\t\t_, err = p.HasPending(ctx)\n\t\t\trequire.Error(t, err)\n\t\t\trequire.Contains(t, err.Error(), \"missing (out-of-order) migration\")\n\t\t\t_, err = p.Up(ctx)\n\t\t\trequire.Error(t, err)\n\t\t\trequire.Contains(t, err.Error(), \"missing (out-of-order) migration\")\n\t\t}\n\n\t\tt.Run(\"latest_version\", func(t *testing.T) {\n\t\t\trun(t, int64(len(fsys)))\n\t\t})\n\t\tt.Run(\"latest_version_minus_one\", func(t *testing.T) {\n\t\t\trun(t, int64(len(fsys)-1))\n\t\t})\n\t})\n}\n\nvar _ database.StoreExtender = (*customStoreSQLite3)(nil)\n\ntype customStoreSQLite3 struct{ database.Store }\n\nfunc (s *customStoreSQLite3) TableExists(ctx context.Context, db database.DBTxConn) (bool, error) {\n\tq := `SELECT EXISTS (SELECT 1 FROM sqlite_master WHERE type='table' AND name=?) AS table_exists`\n\tvar exists bool\n\tif err := db.QueryRowContext(ctx, q, s.Tablename()).Scan(&exists); err != nil {\n\t\treturn false, err\n\t}\n\treturn exists, nil\n}\n\nfunc getGooseVersionCount(db *sql.DB, gooseTable string) (int64, error) {\n\tvar gotVersion int64\n\tif err := db.QueryRow(\n\t\tfmt.Sprintf(\"SELECT count(*) FROM %s WHERE version_id > 0\", gooseTable),\n\t).Scan(&gotVersion); err != nil {\n\t\treturn 0, err\n\t}\n\treturn gotVersion, nil\n}\n\nfunc TestGoOnly(t *testing.T) {\n\tt.Cleanup(goose.ResetGlobalMigrations)\n\t// Not parallel because each subtest modifies global state.\n\n\tcountUser := func(db *sql.DB) int {\n\t\tq := `SELECT count(*)FROM users`\n\t\tvar count int\n\t\terr := db.QueryRow(q).Scan(&count)\n\t\trequire.NoError(t, err)\n\t\treturn count\n\t}\n\n\tt.Run(\"with_tx\", func(t *testing.T) {\n\t\tctx := context.Background()\n\t\tregister := []*goose.Migration{\n\t\t\tgoose.NewGoMigration(\n\t\t\t\t1,\n\t\t\t\t&goose.GoFunc{RunTx: newTxFn(\"CREATE TABLE users (id INTEGER PRIMARY KEY)\")},\n\t\t\t\t&goose.GoFunc{RunTx: newTxFn(\"DROP TABLE users\")},\n\t\t\t),\n\t\t}\n\t\terr := goose.SetGlobalMigrations(register...)\n\t\trequire.NoError(t, err)\n\t\tt.Cleanup(goose.ResetGlobalMigrations)\n\n\t\tdb := newDB(t)\n\t\tregister = []*goose.Migration{\n\t\t\tgoose.NewGoMigration(\n\t\t\t\t2,\n\t\t\t\t&goose.GoFunc{RunTx: newTxFn(\"INSERT INTO users (id) VALUES (1), (2), (3)\")},\n\t\t\t\t&goose.GoFunc{RunTx: newTxFn(\"DELETE FROM users\")},\n\t\t\t),\n\t\t}\n\t\tp, err := goose.NewProvider(goose.DialectSQLite3, db, nil,\n\t\t\tgoose.WithGoMigrations(register...),\n\t\t)\n\t\trequire.NoError(t, err)\n\t\tsources := p.ListSources()\n\t\trequire.Len(t, p.ListSources(), 2)\n\t\tassertSource(t, sources[0], goose.TypeGo, \"\", 1)\n\t\tassertSource(t, sources[1], goose.TypeGo, \"\", 2)\n\t\t// Apply migration 1\n\t\tres, err := p.UpByOne(ctx)\n\t\trequire.NoError(t, err)\n\t\tassertResult(t, res, newSource(goose.TypeGo, \"\", 1), \"up\", false)\n\t\trequire.Equal(t, 0, countUser(db))\n\t\trequire.True(t, tableExists(t, db, \"users\"))\n\t\t// Apply migration 2\n\t\tres, err = p.UpByOne(ctx)\n\t\trequire.NoError(t, err)\n\t\tassertResult(t, res, newSource(goose.TypeGo, \"\", 2), \"up\", false)\n\t\trequire.Equal(t, 3, countUser(db))\n\t\t// Rollback migration 2\n\t\tres, err = p.Down(ctx)\n\t\trequire.NoError(t, err)\n\t\tassertResult(t, res, newSource(goose.TypeGo, \"\", 2), \"down\", false)\n\t\trequire.Equal(t, 0, countUser(db))\n\t\t// Rollback migration 1\n\t\tres, err = p.Down(ctx)\n\t\trequire.NoError(t, err)\n\t\tassertResult(t, res, newSource(goose.TypeGo, \"\", 1), \"down\", false)\n\t\t// Check table does not exist\n\t\trequire.False(t, tableExists(t, db, \"users\"))\n\t})\n\tt.Run(\"with_db\", func(t *testing.T) {\n\t\tctx := context.Background()\n\t\tregister := []*goose.Migration{\n\t\t\tgoose.NewGoMigration(\n\t\t\t\t1,\n\t\t\t\t&goose.GoFunc{\n\t\t\t\t\tRunDB: newDBFn(\"CREATE TABLE users (id INTEGER PRIMARY KEY)\"),\n\t\t\t\t},\n\t\t\t\t&goose.GoFunc{\n\t\t\t\t\tRunDB: newDBFn(\"DROP TABLE users\"),\n\t\t\t\t},\n\t\t\t),\n\t\t}\n\t\terr := goose.SetGlobalMigrations(register...)\n\t\trequire.NoError(t, err)\n\t\tt.Cleanup(goose.ResetGlobalMigrations)\n\n\t\tdb := newDB(t)\n\t\tregister = []*goose.Migration{\n\t\t\tgoose.NewGoMigration(\n\t\t\t\t2,\n\t\t\t\t&goose.GoFunc{RunDB: newDBFn(\"INSERT INTO users (id) VALUES (1), (2), (3)\")},\n\t\t\t\t&goose.GoFunc{RunDB: newDBFn(\"DELETE FROM users\")},\n\t\t\t),\n\t\t}\n\t\tp, err := goose.NewProvider(goose.DialectSQLite3, db, nil,\n\t\t\tgoose.WithGoMigrations(register...),\n\t\t)\n\t\trequire.NoError(t, err)\n\t\tsources := p.ListSources()\n\t\trequire.Len(t, p.ListSources(), 2)\n\t\tassertSource(t, sources[0], goose.TypeGo, \"\", 1)\n\t\tassertSource(t, sources[1], goose.TypeGo, \"\", 2)\n\t\t// Apply migration 1\n\t\tres, err := p.UpByOne(ctx)\n\t\trequire.NoError(t, err)\n\t\tassertResult(t, res, newSource(goose.TypeGo, \"\", 1), \"up\", false)\n\t\trequire.Equal(t, 0, countUser(db))\n\t\trequire.True(t, tableExists(t, db, \"users\"))\n\t\t// Apply migration 2\n\t\tres, err = p.UpByOne(ctx)\n\t\trequire.NoError(t, err)\n\t\tassertResult(t, res, newSource(goose.TypeGo, \"\", 2), \"up\", false)\n\t\trequire.Equal(t, 3, countUser(db))\n\t\t// Rollback migration 2\n\t\tres, err = p.Down(ctx)\n\t\trequire.NoError(t, err)\n\t\tassertResult(t, res, newSource(goose.TypeGo, \"\", 2), \"down\", false)\n\t\trequire.Equal(t, 0, countUser(db))\n\t\t// Rollback migration 1\n\t\tres, err = p.Down(ctx)\n\t\trequire.NoError(t, err)\n\t\tassertResult(t, res, newSource(goose.TypeGo, \"\", 1), \"down\", false)\n\t\t// Check table does not exist\n\t\trequire.False(t, tableExists(t, db, \"users\"))\n\t})\n}\n\nfunc newDBFn(query string) func(context.Context, *sql.DB) error {\n\treturn func(ctx context.Context, db *sql.DB) error {\n\t\t_, err := db.ExecContext(ctx, query)\n\t\treturn err\n\t}\n}\n\nfunc newTxFn(query string) func(context.Context, *sql.Tx) error {\n\treturn func(ctx context.Context, tx *sql.Tx) error {\n\t\t_, err := tx.ExecContext(ctx, query)\n\t\treturn err\n\t}\n}\n\nfunc tableExists(t *testing.T, db *sql.DB, table string) bool {\n\tq := fmt.Sprintf(`SELECT CASE WHEN COUNT(*) > 0 THEN 1 ELSE 0 END AS table_exists FROM sqlite_master WHERE type = 'table' AND name = '%s'`, table)\n\tvar b string\n\terr := db.QueryRow(q).Scan(&b)\n\trequire.NoError(t, err)\n\treturn b == \"1\"\n}\n\nconst (\n\tcharset = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\"\n)\n\nfunc randomAlphaNumeric(length int) string {\n\tb := make([]byte, length)\n\tfor i := range b {\n\t\tb[i] = charset[rand.Intn(len(charset))]\n\t}\n\treturn string(b)\n}\n\nfunc newProviderWithDB(t *testing.T, opts ...goose.ProviderOption) (*goose.Provider, *sql.DB) {\n\tt.Helper()\n\tdb := newDB(t)\n\topts = append(\n\t\topts,\n\t\tgoose.WithVerbose(testing.Verbose()),\n\t)\n\tp, err := goose.NewProvider(goose.DialectSQLite3, db, newFsys(), opts...)\n\trequire.NoError(t, err)\n\treturn p, db\n}\n\nfunc newDB(t *testing.T) *sql.DB {\n\tt.Helper()\n\tdbName := fmt.Sprintf(\"test_%s.db\", randomAlphaNumeric(8))\n\tdb, err := sql.Open(\"sqlite\", filepath.Join(t.TempDir(), dbName))\n\trequire.NoError(t, err)\n\treturn db\n}\n\nfunc getMaxVersionID(db *sql.DB, gooseTable string) (int64, error) {\n\tvar gotVersion int64\n\tif err := db.QueryRow(\n\t\tfmt.Sprintf(\"select max(version_id) from %s\", gooseTable),\n\t).Scan(&gotVersion); err != nil {\n\t\treturn 0, err\n\t}\n\treturn gotVersion, nil\n}\n\nfunc getTableNames(db *sql.DB) ([]string, error) {\n\trows, err := db.Query(`SELECT name FROM sqlite_master WHERE type='table' ORDER BY name`)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar tables []string\n\tfor rows.Next() {\n\t\tvar name string\n\t\tif err := rows.Scan(&name); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\ttables = append(tables, name)\n\t}\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn tables, nil\n}\n\nfunc assertStatus(\n\tt *testing.T,\n\tgot *goose.MigrationStatus,\n\tstate goose.State,\n\tsource *goose.Source,\n\tappliedIsZero bool,\n) {\n\tt.Helper()\n\trequire.Equal(t, got.State, state)\n\trequire.Equal(t, got.Source, source)\n\trequire.Equal(t, got.AppliedAt.IsZero(), appliedIsZero)\n}\n\nfunc assertResult(\n\tt *testing.T,\n\tgot *goose.MigrationResult,\n\tsource *goose.Source,\n\tdirection string,\n\tisEmpty bool,\n) {\n\tt.Helper()\n\trequire.NotNil(t, got)\n\trequire.Equal(t, got.Source, source)\n\trequire.Equal(t, got.Direction, direction)\n\trequire.Equal(t, got.Empty, isEmpty)\n\trequire.NoError(t, got.Error)\n\trequire.Positive(t, got.Duration)\n}\n\nfunc assertSource(t *testing.T, got *goose.Source, typ goose.MigrationType, name string, version int64) {\n\tt.Helper()\n\trequire.Equal(t, got.Type, typ)\n\trequire.Equal(t, got.Path, name)\n\trequire.Equal(t, got.Version, version)\n}\n\nfunc newSource(t goose.MigrationType, fullpath string, version int64) *goose.Source {\n\treturn &goose.Source{\n\t\tType:    t,\n\t\tPath:    fullpath,\n\t\tVersion: version,\n\t}\n}\n\nfunc newMapFile(data string) *fstest.MapFile {\n\treturn &fstest.MapFile{\n\t\tData: []byte(data),\n\t}\n}\n\nfunc newFsys() fstest.MapFS {\n\treturn fstest.MapFS{\n\t\t\"00001_users_table.sql\":    newMapFile(runMigration1),\n\t\t\"00002_posts_table.sql\":    newMapFile(runMigration2),\n\t\t\"00003_comments_table.sql\": newMapFile(runMigration3),\n\t\t\"00004_insert_data.sql\":    newMapFile(runMigration4),\n\t\t\"00005_posts_view.sql\":     newMapFile(runMigration5),\n\t\t\"00006_empty_up.sql\":       newMapFile(runMigration6),\n\t\t\"00007_empty_up_down.sql\":  newMapFile(runMigration7),\n\t}\n}\n\nvar (\n\n\t// known tables are the tables (including goose table) created by running all migration files.\n\t// If you add a table, make sure to add to this list and keep it in order.\n\tknownTables = []string{\n\t\t\"comments\",\n\t\t\"goose_db_version\",\n\t\t\"posts\",\n\t\t\"sqlite_sequence\",\n\t\t\"users\",\n\t}\n\n\trunMigration1 = `\n-- +goose Up\nCREATE TABLE users (\n    id INTEGER PRIMARY KEY,\n    username TEXT NOT NULL,\n    email TEXT NOT NULL,\n    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n);\n\n-- +goose Down\nDROP TABLE users;\n`\n\n\trunMigration2 = `\n-- +goose Up\n-- +goose StatementBegin\nCREATE TABLE posts (\n    id INTEGER PRIMARY KEY,\n    title TEXT NOT NULL,\n    content TEXT NOT NULL,\n    author_id INTEGER NOT NULL,\n    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n    FOREIGN KEY (author_id) REFERENCES users(id)\n);\n-- +goose StatementEnd\nSELECT 1;\nSELECT 2;\n\n-- +goose Down\nDROP TABLE posts;\n`\n\n\trunMigration3 = `\n-- +goose Up\nCREATE TABLE comments (\n    id INTEGER PRIMARY KEY,\n    post_id INTEGER NOT NULL,\n    user_id INTEGER NOT NULL,\n    content TEXT NOT NULL,\n    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n    FOREIGN KEY (post_id) REFERENCES posts(id),\n    FOREIGN KEY (user_id) REFERENCES users(id)\n);\n\n-- +goose Down\nDROP TABLE comments;\nSELECT 1;\nSELECT 2;\nSELECT 3;\n`\n\n\trunMigration4 = `\n-- +goose Up\nINSERT INTO users (id, username, email)\nVALUES\n    (1, 'john_doe', 'john@example.com'),\n    (2, 'jane_smith', 'jane@example.com'),\n    (3, 'alice_wonderland', 'alice@example.com');\n\nINSERT INTO posts (id, title, content, author_id)\nVALUES\n    (1, 'Introduction to SQL', 'SQL is a powerful language for managing databases...', 1),\n    (2, 'Data Modeling Techniques', 'Choosing the right data model is crucial...', 2),\n    (3, 'Advanced Query Optimization', 'Optimizing queries can greatly improve...', 1);\n\nINSERT INTO comments (id, post_id, user_id, content)\nVALUES\n    (1, 1, 3, 'Great introduction! Looking forward to more.'),\n    (2, 1, 2, 'SQL can be a bit tricky at first, but practice helps.'),\n    (3, 2, 1, 'You covered normalization really well in this post.');\n\n-- +goose Down\nDELETE FROM comments;\nDELETE FROM posts;\nDELETE FROM users;\n`\n\n\trunMigration5 = `\n-- +goose NO TRANSACTION\n\n-- +goose Up\nCREATE VIEW posts_view AS\n    SELECT\n        p.id,\n        p.title,\n        p.content,\n        p.created_at,\n        u.username AS author\n    FROM posts p\n    JOIN users u ON p.author_id = u.id;\n\n-- +goose Down\nDROP VIEW posts_view;\n`\n\n\trunMigration6 = `\n-- +goose Up\n`\n\n\trunMigration7 = `\n-- +goose Up\n-- +goose Down\n`\n)\n"
  },
  {
    "path": "provider_test.go",
    "content": "package goose_test\n\nimport (\n\t\"database/sql\"\n\t\"io/fs\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"testing/fstest\"\n\n\t\"github.com/pressly/goose/v3\"\n\t\"github.com/stretchr/testify/require\"\n\t_ \"modernc.org/sqlite\"\n)\n\nfunc TestProvider(t *testing.T) {\n\tdir := t.TempDir()\n\tdb, err := sql.Open(\"sqlite\", filepath.Join(dir, \"sql_embed.db\"))\n\trequire.NoError(t, err)\n\tt.Run(\"empty\", func(t *testing.T) {\n\t\t_, err := goose.NewProvider(goose.DialectSQLite3, db, fstest.MapFS{})\n\t\trequire.Error(t, err)\n\t\trequire.ErrorIs(t, err, goose.ErrNoMigrations)\n\t})\n\n\tmapFS := fstest.MapFS{\n\t\t\"migrations/001_foo.sql\": {Data: []byte(`-- +goose Up`)},\n\t\t\"migrations/002_bar.sql\": {Data: []byte(`-- +goose Up`)},\n\t}\n\tfsys, err := fs.Sub(mapFS, \"migrations\")\n\trequire.NoError(t, err)\n\tp, err := goose.NewProvider(goose.DialectSQLite3, db, fsys)\n\trequire.NoError(t, err)\n\tsources := p.ListSources()\n\trequire.Len(t, sources, 2)\n\trequire.Equal(t, sources[0], newSource(goose.TypeSQL, \"001_foo.sql\", 1))\n\trequire.Equal(t, sources[1], newSource(goose.TypeSQL, \"002_bar.sql\", 2))\n}\n\nvar (\n\tmigration1 = `\n-- +goose Up\nCREATE TABLE foo (id INTEGER PRIMARY KEY);\n-- +goose Down\nDROP TABLE foo;\n`\n\tmigration2 = `\n-- +goose Up\nALTER TABLE foo ADD COLUMN name TEXT;\n-- +goose Down\nALTER TABLE foo DROP COLUMN name;\n`\n\tmigration3 = `\n-- +goose Up\nCREATE TABLE bar (\n    id INTEGER PRIMARY KEY,\n    description TEXT\n);\n-- +goose Down\nDROP TABLE bar;\n`\n\tmigration4 = `\n-- +goose Up\n-- Rename the 'foo' table to 'my_foo'\nALTER TABLE foo RENAME TO my_foo;\n\n-- Add a new column 'timestamp' to 'my_foo'\nALTER TABLE my_foo ADD COLUMN timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP;\n\n-- +goose Down\n-- Remove the 'timestamp' column from 'my_foo'\nALTER TABLE my_foo DROP COLUMN timestamp;\n\n-- Rename the 'my_foo' table back to 'foo'\nALTER TABLE my_foo RENAME TO foo;\n`\n)\n\nfunc TestPartialErrorUnwrap(t *testing.T) {\n\terr := &goose.PartialError{Err: goose.ErrNoCurrentVersion}\n\trequire.ErrorIs(t, err, goose.ErrNoCurrentVersion)\n}\n"
  },
  {
    "path": "provider_types.go",
    "content": "package goose\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"time\"\n)\n\n// MigrationType is the type of migration.\ntype MigrationType string\n\nconst (\n\tTypeGo  MigrationType = \"go\"\n\tTypeSQL MigrationType = \"sql\"\n)\n\n// Source represents a single migration source.\n//\n// The Path field may be empty if the migration was registered manually. This is typically the case\n// for Go migrations registered using the [WithGoMigration] option.\ntype Source struct {\n\tType    MigrationType\n\tPath    string\n\tVersion int64\n}\n\n// MigrationResult is the result of a single migration operation.\ntype MigrationResult struct {\n\tSource    *Source\n\tDuration  time.Duration\n\tDirection string\n\t// Empty indicates no action was taken during the migration, but it was still versioned. For\n\t// SQL, it means no statements; for Go, it's a nil function.\n\tEmpty bool\n\t// Error is only set if the migration failed.\n\tError error\n}\n\n// String returns a string representation of the migration result.\n//\n// Example down:\n//\n//\tEMPTY down 00006_posts_view-copy.sql (607.83µs)\n//\tOK    down 00005_posts_view.sql (646.25µs)\n//\n// Example up:\n//\n//\tOK    up 00005_posts_view.sql (727.5µs)\n//\tEMPTY up 00006_posts_view-copy.sql (378.33µs)\nfunc (m *MigrationResult) String() string {\n\tvar format string\n\tif m.Direction == \"up\" {\n\t\tformat = \"%-5s %-2s %s (%s)\"\n\t} else {\n\t\tformat = \"%-5s %-4s %s (%s)\"\n\t}\n\tvar state string\n\tif m.Empty {\n\t\tstate = \"EMPTY\"\n\t} else {\n\t\tstate = \"OK\"\n\t}\n\treturn fmt.Sprintf(format,\n\t\tstate,\n\t\tm.Direction,\n\t\tfilepath.Base(m.Source.Path),\n\t\ttruncateDuration(m.Duration),\n\t)\n}\n\n// State represents the state of a migration.\ntype State string\n\nconst (\n\t// StatePending is a migration that exists on the filesystem, but not in the database.\n\tStatePending State = \"pending\"\n\t// StateApplied is a migration that has been applied to the database and exists on the\n\t// filesystem.\n\tStateApplied State = \"applied\"\n\n\t// TODO(mf): we could also add a third state for untracked migrations. This would be useful for\n\t// migrations that were manually applied to the database, but not versioned. Or the Source was\n\t// deleted, but the migration still exists in the database. StateUntracked State = \"untracked\"\n)\n\n// MigrationStatus represents the status of a single migration.\ntype MigrationStatus struct {\n\tSource    *Source\n\tState     State\n\tAppliedAt time.Time\n}\n"
  },
  {
    "path": "redo.go",
    "content": "package goose\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n)\n\n// Redo rolls back the most recently applied migration, then runs it again.\nfunc Redo(db *sql.DB, dir string, opts ...OptionsFunc) error {\n\tctx := context.Background()\n\treturn RedoContext(ctx, db, dir, opts...)\n}\n\n// RedoContext rolls back the most recently applied migration, then runs it again.\nfunc RedoContext(ctx context.Context, db *sql.DB, dir string, opts ...OptionsFunc) error {\n\toption := &options{}\n\tfor _, f := range opts {\n\t\tf(option)\n\t}\n\tmigrations, err := CollectMigrations(dir, minVersion, maxVersion)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar (\n\t\tcurrentVersion int64\n\t)\n\tif option.noVersioning {\n\t\tif len(migrations) == 0 {\n\t\t\treturn nil\n\t\t}\n\t\tcurrentVersion = migrations[len(migrations)-1].Version\n\t} else {\n\t\tif currentVersion, err = GetDBVersionContext(ctx, db); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tcurrent, err := migrations.Current(currentVersion)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcurrent.noVersioning = option.noVersioning\n\n\tif err := current.DownContext(ctx, db); err != nil {\n\t\treturn err\n\t}\n\tif err := current.UpContext(ctx, db); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "register.go",
    "content": "package goose\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"runtime\"\n)\n\n// GoMigrationContext is a Go migration func that is run within a transaction and receives a\n// context.\ntype GoMigrationContext func(ctx context.Context, tx *sql.Tx) error\n\n// AddMigrationContext adds Go migrations.\nfunc AddMigrationContext(up, down GoMigrationContext) {\n\t_, filename, _, _ := runtime.Caller(1)\n\tAddNamedMigrationContext(filename, up, down)\n}\n\n// AddNamedMigrationContext adds named Go migrations.\nfunc AddNamedMigrationContext(filename string, up, down GoMigrationContext) {\n\tif err := register(\n\t\tfilename,\n\t\ttrue,\n\t\t&GoFunc{RunTx: up, Mode: TransactionEnabled},\n\t\t&GoFunc{RunTx: down, Mode: TransactionEnabled},\n\t); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// GoMigrationNoTxContext is a Go migration func that is run outside a transaction and receives a\n// context.\ntype GoMigrationNoTxContext func(ctx context.Context, db *sql.DB) error\n\n// AddMigrationNoTxContext adds Go migrations that will be run outside transaction.\nfunc AddMigrationNoTxContext(up, down GoMigrationNoTxContext) {\n\t_, filename, _, _ := runtime.Caller(1)\n\tAddNamedMigrationNoTxContext(filename, up, down)\n}\n\n// AddNamedMigrationNoTxContext adds named Go migrations that will be run outside transaction.\nfunc AddNamedMigrationNoTxContext(filename string, up, down GoMigrationNoTxContext) {\n\tif err := register(\n\t\tfilename,\n\t\tfalse,\n\t\t&GoFunc{RunDB: up, Mode: TransactionDisabled},\n\t\t&GoFunc{RunDB: down, Mode: TransactionDisabled},\n\t); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc register(filename string, useTx bool, up, down *GoFunc) error {\n\tv, _ := NumericComponent(filename)\n\tif existing, ok := registeredGoMigrations[v]; ok {\n\t\treturn fmt.Errorf(\"failed to add migration %q: version %d conflicts with %q\",\n\t\t\tfilename,\n\t\t\tv,\n\t\t\texisting.Source,\n\t\t)\n\t}\n\t// Add to global as a registered migration.\n\tm := NewGoMigration(v, up, down)\n\tm.Source = filename\n\t// We explicitly set transaction to maintain existing behavior. Both up and down may be nil, but\n\t// we know based on the register function what the user is requesting.\n\tm.UseTx = useTx\n\tregisteredGoMigrations[v] = m\n\treturn nil\n}\n\n// withContext changes the signature of a function that receives one argument to receive a context\n// and the argument.\nfunc withContext[T any](fn func(T) error) func(context.Context, T) error {\n\tif fn == nil {\n\t\treturn nil\n\t}\n\treturn func(ctx context.Context, t T) error {\n\t\treturn fn(t)\n\t}\n}\n\n// withoutContext changes the signature of a function that receives a context and one argument to\n// receive only the argument. When called the passed context is always context.Background().\nfunc withoutContext[T any](fn func(context.Context, T) error) func(T) error {\n\tif fn == nil {\n\t\treturn nil\n\t}\n\treturn func(t T) error {\n\t\treturn fn(context.Background(), t)\n\t}\n}\n\n// GoMigration is a Go migration func that is run within a transaction.\n//\n// Deprecated: Use GoMigrationContext.\ntype GoMigration func(tx *sql.Tx) error\n\n// GoMigrationNoTx is a Go migration func that is run outside a transaction.\n//\n// Deprecated: Use GoMigrationNoTxContext.\ntype GoMigrationNoTx func(db *sql.DB) error\n\n// AddMigration adds Go migrations.\n//\n// Deprecated: Use AddMigrationContext.\nfunc AddMigration(up, down GoMigration) {\n\t_, filename, _, _ := runtime.Caller(1)\n\tAddNamedMigrationContext(filename, withContext(up), withContext(down))\n}\n\n// AddNamedMigration adds named Go migrations.\n//\n// Deprecated: Use AddNamedMigrationContext.\nfunc AddNamedMigration(filename string, up, down GoMigration) {\n\tAddNamedMigrationContext(filename, withContext(up), withContext(down))\n}\n\n// AddMigrationNoTx adds Go migrations that will be run outside transaction.\n//\n// Deprecated: Use AddMigrationNoTxContext.\nfunc AddMigrationNoTx(up, down GoMigrationNoTx) {\n\t_, filename, _, _ := runtime.Caller(1)\n\tAddNamedMigrationNoTxContext(filename, withContext(up), withContext(down))\n}\n\n// AddNamedMigrationNoTx adds named Go migrations that will be run outside transaction.\n//\n// Deprecated: Use AddNamedMigrationNoTxContext.\nfunc AddNamedMigrationNoTx(filename string, up, down GoMigrationNoTx) {\n\tAddNamedMigrationNoTxContext(filename, withContext(up), withContext(down))\n}\n"
  },
  {
    "path": "reset.go",
    "content": "package goose\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"sort\"\n)\n\n// Reset rolls back all migrations\nfunc Reset(db *sql.DB, dir string, opts ...OptionsFunc) error {\n\tctx := context.Background()\n\treturn ResetContext(ctx, db, dir, opts...)\n}\n\n// ResetContext rolls back all migrations\nfunc ResetContext(ctx context.Context, db *sql.DB, dir string, opts ...OptionsFunc) error {\n\toption := &options{}\n\tfor _, f := range opts {\n\t\tf(option)\n\t}\n\tmigrations, err := CollectMigrations(dir, minVersion, maxVersion)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to collect migrations: %w\", err)\n\t}\n\tif option.noVersioning {\n\t\treturn DownToContext(ctx, db, dir, minVersion, opts...)\n\t}\n\n\tstatuses, err := dbMigrationsStatus(ctx, db)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get status of migrations: %w\", err)\n\t}\n\tsort.Sort(sort.Reverse(migrations))\n\n\tfor _, migration := range migrations {\n\t\tif !statuses[migration.Version] {\n\t\t\tcontinue\n\t\t}\n\t\tif err = migration.DownContext(ctx, db); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to db-down: %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc dbMigrationsStatus(ctx context.Context, db *sql.DB) (map[int64]bool, error) {\n\tdbMigrations, err := store.ListMigrations(ctx, db, TableName())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// The most recent record for each migration specifies\n\t// whether it has been applied or rolled back.\n\tresults := make(map[int64]bool)\n\n\tfor _, m := range dbMigrations {\n\t\tif _, ok := results[m.VersionID]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tresults[m.VersionID] = m.IsApplied\n\t}\n\treturn results, nil\n}\n"
  },
  {
    "path": "scripts/release-notes.sh",
    "content": "#!/bin/bash\n\nset -euo pipefail\n\n# Check if the required argument is provided\nif [ $# -lt 1 ]; then\n  echo \"Usage: $0 <semver version> [<changelog file>]\"\n  exit 1\nfi\n\nversion=\"$1\"\nchangelog_file=\"${2:-CHANGELOG.md}\"\n\n# Check if the changelog file exists\nif [ ! -f \"$changelog_file\" ]; then\n  echo \"Error: $changelog_file does not exist\"\n  exit 1\nfi\n\nCAPTURE=0\nitems=\"\"\n# Read the changelog file line by line\nwhile IFS= read -r LINE; do\n  # Stop capturing when we reach the next version sections\n  if [[ \"${LINE}\" == \"##\"* ]] && [[ \"${CAPTURE}\" -eq 1 ]]; then\n    break\n  fi\n  # Stop capturing when we reach the Unreleased section\n  if [[ \"${LINE}\" == \"[Unreleased]\"* ]]; then\n    break\n  fi\n  # Start capturing when we reach the specified version section\n  if [[ \"${LINE}\" == \"## [${version}]\"* ]] && [[ \"${CAPTURE}\" -eq 0 ]]; then\n    CAPTURE=1\n    continue\n  fi\n  # Capture the lines between the specified version and the next version\n  if [[ \"${CAPTURE}\" -eq 1 ]]; then\n    # Ignore empty lines\n    if [[ -z \"${LINE}\" ]]; then\n      continue\n    fi\n    items+=\"$(echo \"${LINE}\" | xargs -0)\"\n    # Add a newline between each item\n    if [[ -n \"$items\" ]]; then\n      items+=$'\\n'\n    fi\n  fi\ndone <\"${changelog_file}\"\n\nif [[ -n \"$items\" ]]; then\n  echo \"${items%$'\\n'}\"\nelse\n  echo \"No changelog items found for version $version\"\nfi\n"
  },
  {
    "path": "status.go",
    "content": "package goose\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"time\"\n)\n\n// Status prints the status of all migrations.\nfunc Status(db *sql.DB, dir string, opts ...OptionsFunc) error {\n\tctx := context.Background()\n\treturn StatusContext(ctx, db, dir, opts...)\n}\n\n// StatusContext prints the status of all migrations.\nfunc StatusContext(ctx context.Context, db *sql.DB, dir string, opts ...OptionsFunc) error {\n\toption := &options{}\n\tfor _, f := range opts {\n\t\tf(option)\n\t}\n\tmigrations, err := CollectMigrations(dir, minVersion, maxVersion)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to collect migrations: %w\", err)\n\t}\n\tif option.noVersioning {\n\t\tlog.Printf(\"    Applied At                  Migration\")\n\t\tlog.Printf(\"    =======================================\")\n\t\tfor _, current := range migrations {\n\t\t\tlog.Printf(\"    %-24s -- %v\", \"no versioning\", filepath.Base(current.Source))\n\t\t}\n\t\treturn nil\n\t}\n\n\t// must ensure that the version table exists if we're running on a pristine DB\n\tif _, err := EnsureDBVersionContext(ctx, db); err != nil {\n\t\treturn fmt.Errorf(\"failed to ensure DB version: %w\", err)\n\t}\n\n\tlog.Printf(\"    Applied At                  Migration\")\n\tlog.Printf(\"    =======================================\")\n\tfor _, migration := range migrations {\n\t\tif err := printMigrationStatus(ctx, db, migration.Version, filepath.Base(migration.Source)); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to print status: %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc printMigrationStatus(ctx context.Context, db *sql.DB, version int64, script string) error {\n\tm, err := store.GetMigration(ctx, db, TableName(), version)\n\tif err != nil && !errors.Is(err, sql.ErrNoRows) {\n\t\treturn fmt.Errorf(\"failed to query the latest migration: %w\", err)\n\t}\n\tappliedAt := \"Pending\"\n\tif m != nil && m.IsApplied {\n\t\tappliedAt = m.Timestamp.Format(time.ANSIC)\n\t}\n\tlog.Printf(\"    %-24s -- %v\", appliedAt, script)\n\treturn nil\n}\n"
  },
  {
    "path": "testdata/migrations/00001_users_table.sql",
    "content": "-- +goose Up\nCREATE TABLE users (\n    id INTEGER PRIMARY KEY,\n    username TEXT NOT NULL,\n    email TEXT NOT NULL,\n    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n);\n\n-- +goose Down\nDROP TABLE users;\n"
  },
  {
    "path": "testdata/migrations/00002_posts_table.sql",
    "content": "-- +goose Up\n-- +goose StatementBegin\nCREATE TABLE posts (\n    id INTEGER PRIMARY KEY,\n    title TEXT NOT NULL,\n    content TEXT NOT NULL,\n    author_id INTEGER NOT NULL,\n    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n    FOREIGN KEY (author_id) REFERENCES users(id)\n);\n-- +goose StatementEnd\n\n-- +goose Down\nDROP TABLE posts;\n"
  },
  {
    "path": "testdata/migrations/00003_comments_table.sql",
    "content": "-- +goose Up\nCREATE TABLE comments (\n    id INTEGER PRIMARY KEY,\n    post_id INTEGER NOT NULL,\n    user_id INTEGER NOT NULL,\n    content TEXT NOT NULL,\n    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n    FOREIGN KEY (post_id) REFERENCES posts(id),\n    FOREIGN KEY (user_id) REFERENCES users(id)\n);\n\n-- +goose Down\nDROP TABLE comments;\n"
  },
  {
    "path": "testdata/migrations/00004_insert_data.sql",
    "content": "-- +goose Up\nINSERT INTO users (id, username, email)\nVALUES\n    (1, 'john_doe', 'john@example.com'),\n    (2, 'jane_smith', 'jane@example.com'),\n    (3, 'alice_wonderland', 'alice@example.com');\n\nINSERT INTO posts (id, title, content, author_id)\nVALUES\n    (1, 'Introduction to SQL', 'SQL is a powerful language for managing databases...', 1),\n    (2, 'Data Modeling Techniques', 'Choosing the right data model is crucial...', 2),\n    (3, 'Advanced Query Optimization', 'Optimizing queries can greatly improve...', 1);\n\nINSERT INTO comments (id, post_id, user_id, content)\nVALUES\n    (1, 1, 3, 'Great introduction! Looking forward to more.'),\n    (2, 1, 2, 'SQL can be a bit tricky at first, but practice helps.'),\n    (3, 2, 1, 'You covered normalization really well in this post.');\n\n-- +goose Down\nDELETE FROM comments;\nDELETE FROM posts;\nDELETE FROM users;\n"
  },
  {
    "path": "testdata/migrations/00005_posts_view.sql",
    "content": "-- +goose NO TRANSACTION\n\n-- +goose Up\nCREATE VIEW posts_view AS\n    SELECT\n        p.id,\n        p.title,\n        p.content,\n        p.created_at,\n        u.username AS author\n    FROM posts p\n    JOIN users u ON p.author_id = u.id;\n\n-- +goose Down\nDROP VIEW posts_view;\n"
  },
  {
    "path": "testdata/no-versioning/migrations/00001_a.sql",
    "content": "-- +goose Up\nCREATE TABLE owners (\n    owner_id INTEGER PRIMARY KEY AUTOINCREMENT,\n    owner_name TEXT NOT NULL\n);\n\n-- +goose Down\nDROP TABLE IF EXISTS owners;\n"
  },
  {
    "path": "testdata/no-versioning/migrations/00002_b.sql",
    "content": "-- +goose Up\n-- +goose StatementBegin\nINSERT INTO owners(owner_name) VALUES ('lucas'), ('ocean');\n-- +goose StatementEnd\n\n-- +goose Down\n-- +goose StatementBegin\nDELETE FROM owners;\n-- +goose StatementEnd\n"
  },
  {
    "path": "testdata/no-versioning/migrations/00003_c.sql",
    "content": "-- +goose Up\n-- +goose StatementBegin\nINSERT INTO owners(owner_name) VALUES ('james'), ('space');\n-- +goose StatementEnd\n\n-- +goose Down\n-- +goose StatementBegin\nDELETE FROM owners WHERE owner_name IN ('james', 'space');\n-- +goose StatementEnd\n"
  },
  {
    "path": "testdata/no-versioning/seed/00001_a.sql",
    "content": "-- +goose Up\n-- +goose StatementBegin\n-- Insert 100 owners.\nINSERT INTO owners (owner_name)\nWITH numbers AS (\n    SELECT 1 AS n\n    UNION ALL\n    SELECT n + 1 FROM numbers WHERE n < 100\n)\nSELECT 'seed-user-' || n FROM numbers;\n-- +goose StatementEnd\n\n-- +goose Down\n-- +goose StatementBegin\n-- Delete the previously inserted data.\nDELETE FROM owners WHERE owner_name LIKE 'seed-user-%';\n-- +goose StatementEnd\n"
  },
  {
    "path": "testdata/no-versioning/seed/00002_b.sql",
    "content": "-- +goose Up\n\n-- Insert 150 more owners.\nINSERT INTO owners (owner_name)\nWITH numbers AS (\n    SELECT 101 AS n\n    UNION ALL\n    SELECT n + 1 FROM numbers WHERE n < 250\n)\nSELECT 'seed-user-' || n FROM numbers;\n\n-- +goose Down\n\n-- NOTE: there are 4 migration owners and 100 seed owners, that's why owner_id starts at 105\nDELETE FROM owners WHERE owner_name LIKE 'seed-user-%' AND owner_id BETWEEN 105 AND 254;\n"
  },
  {
    "path": "testdata/testdata.go",
    "content": "package testdata\n\nimport (\n\t\"embed\"\n\t\"io/fs\"\n)\n\n//go:embed migrations/*.sql\nvar migrationsFS embed.FS\n\n// MustMigrationsFS returns the embedded migrations filesystem.\nfunc MustMigrationsFS() fs.FS {\n\tfsys, err := fs.Sub(migrationsFS, \"migrations\")\n\tif err != nil {\n\t\t// This should never happen, since the subdirectory is hardcoded. If the layout of the\n\t\t// embedded files changes, this will panic to alert the developer to update the code\n\t\t// accordingly.\n\t\tpanic(err)\n\t}\n\treturn fsys\n}\n"
  },
  {
    "path": "tests/gomigrations/error/gomigrations_error_test.go",
    "content": "package gomigrations\n\nimport (\n\t\"database/sql\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/pressly/goose/v3\"\n\t_ \"github.com/pressly/goose/v3/tests/gomigrations/error/testdata\"\n\t\"github.com/stretchr/testify/require\"\n\t_ \"modernc.org/sqlite\"\n)\n\nfunc TestGoMigrationByOne(t *testing.T) {\n\ttempDir := t.TempDir()\n\tdb, err := sql.Open(\"sqlite\", filepath.Join(tempDir, \"test.db\"))\n\trequire.NoError(t, err)\n\terr = goose.SetDialect(string(goose.DialectSQLite3))\n\trequire.NoError(t, err)\n\t// Create goose table.\n\tcurrent, err := goose.EnsureDBVersion(db)\n\trequire.NoError(t, err)\n\trequire.EqualValues(t, 0, current)\n\t// Collect migrations.\n\tdir := \"testdata\"\n\tmigrations, err := goose.CollectMigrations(dir, 0, goose.MaxVersion)\n\trequire.NoError(t, err)\n\trequire.Len(t, migrations, 4)\n\n\t// Setup table.\n\terr = migrations[0].Up(db)\n\trequire.NoError(t, err)\n\tversion, err := goose.GetDBVersion(db)\n\trequire.NoError(t, err)\n\trequire.EqualValues(t, 1, version)\n\n\t// Registered Go migration run outside a goose tx using *sql.DB.\n\terr = migrations[1].Up(db)\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"failed to run go migration\")\n\tversion, err = goose.GetDBVersion(db)\n\trequire.NoError(t, err)\n\trequire.EqualValues(t, 1, version)\n\n\t// This migration was inserting 100 rows, but fails at 50, and\n\t// because it's run outside a goose tx then we expect 50 rows.\n\tvar count int\n\terr = db.QueryRow(\"SELECT COUNT(*) FROM foo\").Scan(&count)\n\trequire.NoError(t, err)\n\trequire.Equal(t, 50, count)\n\n\t// Truncate table so we have 0 rows.\n\terr = migrations[2].Up(db)\n\trequire.NoError(t, err)\n\tversion, err = goose.GetDBVersion(db)\n\trequire.NoError(t, err)\n\t// We're at version 3, but keep in mind 2 was never applied because it failed.\n\trequire.EqualValues(t, 3, version)\n\n\t// Registered Go migration run within a tx.\n\terr = migrations[3].Up(db)\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"failed to run go migration\")\n\tversion, err = goose.GetDBVersion(db)\n\trequire.NoError(t, err)\n\trequire.EqualValues(t, 3, version) // This migration failed, so we're still at 3.\n\t// This migration was inserting 100 rows, but fails at 50. However, since it's\n\t// running within a tx we expect none of the inserts to persist.\n\terr = db.QueryRow(\"SELECT COUNT(*) FROM foo\").Scan(&count)\n\trequire.NoError(t, err)\n\trequire.Equal(t, 0, count)\n\n}\n"
  },
  {
    "path": "tests/gomigrations/error/testdata/001_up_no_tx.go",
    "content": "package gomigrations\n\nimport (\n\t\"database/sql\"\n\n\t\"github.com/pressly/goose/v3\"\n)\n\nfunc init() {\n\tgoose.AddMigrationNoTx(up001, nil)\n}\n\nfunc up001(db *sql.DB) error {\n\tq := \"CREATE TABLE foo (id INTEGER)\"\n\t_, err := db.Exec(q)\n\treturn err\n}\n"
  },
  {
    "path": "tests/gomigrations/error/testdata/002_ERROR_insert_no_tx.go",
    "content": "package gomigrations\n\nimport (\n\t\"database/sql\"\n\t\"fmt\"\n\n\t\"github.com/pressly/goose/v3\"\n)\n\nfunc init() {\n\tgoose.AddMigrationNoTx(up002, nil)\n}\n\nfunc up002(db *sql.DB) error {\n\tfor i := 1; i <= 100; i++ {\n\t\tq := \"INSERT INTO foo VALUES ($1)\"\n\t\tif _, err := db.Exec(q, i); err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// Simulate an error when no tx. We should have 50 rows\n\t\t// inserted in the DB.\n\t\tif i == 50 {\n\t\t\treturn fmt.Errorf(\"simulate error: too many inserts\")\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "tests/gomigrations/error/testdata/003_truncate.go",
    "content": "package gomigrations\n\nimport (\n\t\"database/sql\"\n\n\t\"github.com/pressly/goose/v3\"\n)\n\nfunc init() {\n\tgoose.AddMigration(up003, nil)\n}\n\nfunc up003(tx *sql.Tx) error {\n\tq := \"DELETE FROM foo\"\n\t_, err := tx.Exec(q)\n\treturn err\n}\n"
  },
  {
    "path": "tests/gomigrations/error/testdata/004_ERROR_insert.go",
    "content": "package gomigrations\n\nimport (\n\t\"database/sql\"\n\t\"fmt\"\n\n\t\"github.com/pressly/goose/v3\"\n)\n\nfunc init() {\n\tgoose.AddMigration(up004, nil)\n}\n\nfunc up004(tx *sql.Tx) error {\n\tfor i := 1; i <= 100; i++ {\n\t\t// Simulate an error when no tx. We should have 50 rows\n\t\t// inserted in the DB.\n\t\tif i == 50 {\n\t\t\treturn fmt.Errorf(\"simulate error: too many inserts\")\n\t\t}\n\t\tq := \"INSERT INTO foo VALUES ($1)\"\n\t\tif _, err := tx.Exec(q); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "tests/gomigrations/register/register_test.go",
    "content": "package register_test\n\nimport (\n\t\"math\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/pressly/goose/v3\"\n\t_ \"github.com/pressly/goose/v3/tests/gomigrations/register/testdata\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestAddFunctions(t *testing.T) {\n\tgoMigrations, err := goose.CollectMigrations(\"testdata\", 0, math.MaxInt64)\n\trequire.NoError(t, err)\n\trequire.Len(t, goMigrations, 4)\n\n\tcheckMigration(t, goMigrations[0], &goose.Migration{\n\t\tVersion:    1,\n\t\tNext:       2,\n\t\tPrevious:   -1,\n\t\tSource:     \"001_addmigration.go\",\n\t\tRegistered: true,\n\t\tUseTx:      true,\n\t})\n\tcheckMigration(t, goMigrations[1], &goose.Migration{\n\t\tVersion:    2,\n\t\tNext:       3,\n\t\tPrevious:   1,\n\t\tSource:     \"002_addmigrationnotx.go\",\n\t\tRegistered: true,\n\t\tUseTx:      false,\n\t})\n\tcheckMigration(t, goMigrations[2], &goose.Migration{\n\t\tVersion:    3,\n\t\tNext:       4,\n\t\tPrevious:   2,\n\t\tSource:     \"003_addmigrationcontext.go\",\n\t\tRegistered: true,\n\t\tUseTx:      true,\n\t})\n\tcheckMigration(t, goMigrations[3], &goose.Migration{\n\t\tVersion:    4,\n\t\tNext:       -1,\n\t\tPrevious:   3,\n\t\tSource:     \"004_addmigrationnotxcontext.go\",\n\t\tRegistered: true,\n\t\tUseTx:      false,\n\t})\n}\n\nfunc checkMigration(t *testing.T, got *goose.Migration, want *goose.Migration) {\n\tt.Helper()\n\trequire.Equal(t, want.Version, got.Version)\n\trequire.Equal(t, want.Next, got.Next)\n\trequire.Equal(t, want.Previous, got.Previous)\n\trequire.Equal(t, want.Source, filepath.Base(got.Source))\n\trequire.Equal(t, want.Registered, got.Registered)\n\trequire.Equal(t, want.UseTx, got.UseTx)\n\tcheckFunctions(t, got)\n}\n\nfunc checkFunctions(t *testing.T, m *goose.Migration) {\n\tt.Helper()\n\tswitch filepath.Base(m.Source) {\n\tcase \"001_addmigration.go\":\n\t\t// With transaction\n\t\trequire.NotNil(t, m.UpFn)\n\t\trequire.NotNil(t, m.DownFn)\n\t\trequire.NotNil(t, m.UpFnContext)\n\t\trequire.NotNil(t, m.DownFnContext)\n\t\t// No transaction\n\t\trequire.Nil(t, m.UpFnNoTx)\n\t\trequire.Nil(t, m.DownFnNoTx)\n\t\trequire.Nil(t, m.UpFnNoTxContext)\n\t\trequire.Nil(t, m.DownFnNoTxContext)\n\tcase \"002_addmigrationnotx.go\":\n\t\t// With transaction\n\t\trequire.Nil(t, m.UpFn)\n\t\trequire.Nil(t, m.DownFn)\n\t\trequire.Nil(t, m.UpFnContext)\n\t\trequire.Nil(t, m.DownFnContext)\n\t\t// No transaction\n\t\trequire.NotNil(t, m.UpFnNoTx)\n\t\trequire.NotNil(t, m.DownFnNoTx)\n\t\trequire.NotNil(t, m.UpFnNoTxContext)\n\t\trequire.NotNil(t, m.DownFnNoTxContext)\n\tcase \"003_addmigrationcontext.go\":\n\t\t// With transaction\n\t\trequire.NotNil(t, m.UpFn)\n\t\trequire.NotNil(t, m.DownFn)\n\t\trequire.NotNil(t, m.UpFnContext)\n\t\trequire.NotNil(t, m.DownFnContext)\n\t\t// No transaction\n\t\trequire.Nil(t, m.UpFnNoTx)\n\t\trequire.Nil(t, m.DownFnNoTx)\n\t\trequire.Nil(t, m.UpFnNoTxContext)\n\t\trequire.Nil(t, m.DownFnNoTxContext)\n\tcase \"004_addmigrationnotxcontext.go\":\n\t\t// With transaction\n\t\trequire.Nil(t, m.UpFn)\n\t\trequire.Nil(t, m.DownFn)\n\t\trequire.Nil(t, m.UpFnContext)\n\t\trequire.Nil(t, m.DownFnContext)\n\t\t// No transaction\n\t\trequire.NotNil(t, m.UpFnNoTx)\n\t\trequire.NotNil(t, m.DownFnNoTx)\n\t\trequire.NotNil(t, m.UpFnNoTxContext)\n\t\trequire.NotNil(t, m.DownFnNoTxContext)\n\tdefault:\n\t\tt.Fatalf(\"unexpected migration: %s\", filepath.Base(m.Source))\n\t}\n}\n"
  },
  {
    "path": "tests/gomigrations/register/testdata/001_addmigration.go",
    "content": "package register\n\nimport (\n\t\"database/sql\"\n\n\t\"github.com/pressly/goose/v3\"\n)\n\nfunc init() {\n\tgoose.AddMigration(\n\t\tfunc(_ *sql.Tx) error { return nil },\n\t\tfunc(_ *sql.Tx) error { return nil },\n\t)\n}\n"
  },
  {
    "path": "tests/gomigrations/register/testdata/002_addmigrationnotx.go",
    "content": "package register\n\nimport (\n\t\"database/sql\"\n\n\t\"github.com/pressly/goose/v3\"\n)\n\nfunc init() {\n\tgoose.AddMigrationNoTx(\n\t\tfunc(_ *sql.DB) error { return nil },\n\t\tfunc(_ *sql.DB) error { return nil },\n\t)\n}\n"
  },
  {
    "path": "tests/gomigrations/register/testdata/003_addmigrationcontext.go",
    "content": "package register\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\n\t\"github.com/pressly/goose/v3\"\n)\n\nfunc init() {\n\tgoose.AddMigrationContext(\n\t\tfunc(_ context.Context, _ *sql.Tx) error { return nil },\n\t\tfunc(_ context.Context, _ *sql.Tx) error { return nil },\n\t)\n}\n"
  },
  {
    "path": "tests/gomigrations/register/testdata/004_addmigrationnotxcontext.go",
    "content": "package register\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\n\t\"github.com/pressly/goose/v3\"\n)\n\nfunc init() {\n\tgoose.AddMigrationNoTxContext(\n\t\tfunc(_ context.Context, _ *sql.DB) error { return nil },\n\t\tfunc(_ context.Context, _ *sql.DB) error { return nil },\n\t)\n}\n"
  },
  {
    "path": "tests/gomigrations/success/gomigrations_success_test.go",
    "content": "package gomigrations_test\n\nimport (\n\t\"database/sql\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/pressly/goose/v3\"\n\t\"github.com/stretchr/testify/require\"\n\n\t_ \"github.com/pressly/goose/v3/tests/gomigrations/success/testdata\"\n\t_ \"modernc.org/sqlite\"\n)\n\nfunc TestGoMigrationByOne(t *testing.T) {\n\tt.Parallel()\n\n\trequire.NoError(t, goose.SetDialect(\"sqlite3\"))\n\tdb, err := sql.Open(\"sqlite\", \":memory:\")\n\trequire.NoError(t, err)\n\tdir := \"testdata\"\n\tfiles, err := filepath.Glob(dir + \"/*.go\")\n\trequire.NoError(t, err)\n\n\tupByOne := func(t *testing.T) int64 {\n\t\tt.Helper()\n\t\terr = goose.UpByOne(db, dir)\n\t\tt.Logf(\"err: %v %s\", err, dir)\n\t\trequire.NoError(t, err)\n\t\tversion, err := goose.GetDBVersion(db)\n\t\trequire.NoError(t, err)\n\t\treturn version\n\t}\n\tdownByOne := func(t *testing.T) int64 {\n\t\tt.Helper()\n\t\terr = goose.Down(db, dir)\n\t\trequire.NoError(t, err)\n\t\tversion, err := goose.GetDBVersion(db)\n\t\trequire.NoError(t, err)\n\t\treturn version\n\t}\n\t// Migrate all files up-by-one.\n\tfor i := 1; i <= len(files); i++ {\n\t\trequire.EqualValues(t, upByOne(t), i)\n\t}\n\tversion, err := goose.GetDBVersion(db)\n\trequire.NoError(t, err)\n\trequire.Len(t, files, int(version))\n\n\ttables, err := ListTables(db)\n\trequire.NoError(t, err)\n\trequire.Equal(t,\n\t\t[]string{\n\t\t\t\"alpha\",\n\t\t\t\"bravo\",\n\t\t\t\"charlie\",\n\t\t\t\"delta\",\n\t\t\t\"echo\",\n\t\t\t\"foxtrot\",\n\t\t\t\"golf\",\n\t\t\t\"goose_db_version\",\n\t\t\t\"hotel\",\n\t\t\t\"sqlite_sequence\",\n\t\t},\n\t\ttables,\n\t)\n\n\t// Migrate all files down-by-one.\n\tfor i := len(files) - 1; i >= 0; i-- {\n\t\trequire.EqualValues(t, downByOne(t), i)\n\t}\n\tversion, err = goose.GetDBVersion(db)\n\trequire.NoError(t, err)\n\trequire.EqualValues(t, 0, version)\n\n\ttables, err = ListTables(db)\n\trequire.NoError(t, err)\n\trequire.Equal(t,\n\t\t[]string{\n\t\t\t\"goose_db_version\",\n\t\t\t\"sqlite_sequence\",\n\t\t},\n\t\ttables,\n\t)\n}\n\nfunc ListTables(db *sql.DB) ([]string, error) {\n\trows, err := db.Query(`SELECT name FROM sqlite_master WHERE type='table' ORDER BY name`)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tvar tables []string\n\tfor rows.Next() {\n\t\tvar name string\n\t\tif err := rows.Scan(&name); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\ttables = append(tables, name)\n\t}\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn tables, nil\n}\n"
  },
  {
    "path": "tests/gomigrations/success/testdata/001_up_down.go",
    "content": "package gomigrations\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\n\t\"github.com/pressly/goose/v3\"\n\t\"github.com/pressly/goose/v3/database\"\n)\n\nfunc init() {\n\tgoose.AddMigration(up001, down001)\n}\n\nfunc up001(tx *sql.Tx) error {\n\treturn createTable(tx, \"alpha\")\n}\n\nfunc down001(tx *sql.Tx) error {\n\treturn dropTable(tx, \"alpha\")\n}\n\nfunc createTable(db database.DBTxConn, name string) error {\n\t_, err := db.ExecContext(context.Background(), fmt.Sprintf(\"CREATE TABLE %s (id INTEGER)\", name))\n\treturn err\n}\n\nfunc dropTable(db database.DBTxConn, name string) error {\n\t_, err := db.ExecContext(context.Background(), fmt.Sprintf(\"DROP TABLE %s\", name))\n\treturn err\n}\n"
  },
  {
    "path": "tests/gomigrations/success/testdata/002_up_only.go",
    "content": "package gomigrations\n\nimport (\n\t\"database/sql\"\n\n\t\"github.com/pressly/goose/v3\"\n)\n\nfunc init() {\n\tgoose.AddMigration(up002, nil)\n}\n\nfunc up002(tx *sql.Tx) error {\n\treturn createTable(tx, \"bravo\")\n}\n"
  },
  {
    "path": "tests/gomigrations/success/testdata/003_down_only.go",
    "content": "package gomigrations\n\nimport (\n\t\"database/sql\"\n\n\t\"github.com/pressly/goose/v3\"\n)\n\nfunc init() {\n\tgoose.AddMigration(nil, down003)\n}\n\nfunc down003(tx *sql.Tx) error {\n\treturn dropTable(tx, \"bravo\")\n}\n"
  },
  {
    "path": "tests/gomigrations/success/testdata/004_empty.go",
    "content": "package gomigrations\n\nimport (\n\t\"github.com/pressly/goose/v3\"\n)\n\nfunc init() {\n\tgoose.AddMigration(nil, nil)\n}\n"
  },
  {
    "path": "tests/gomigrations/success/testdata/005_up_down_no_tx.go",
    "content": "package gomigrations\n\nimport (\n\t\"database/sql\"\n\n\t\"github.com/pressly/goose/v3\"\n)\n\nfunc init() {\n\tgoose.AddMigrationNoTx(up005, down005)\n}\n\nfunc up005(db *sql.DB) error {\n\treturn createTable(db, \"charlie\")\n}\n\nfunc down005(db *sql.DB) error {\n\treturn dropTable(db, \"charlie\")\n}\n"
  },
  {
    "path": "tests/gomigrations/success/testdata/006_up_only_no_tx.go",
    "content": "package gomigrations\n\nimport (\n\t\"database/sql\"\n\n\t\"github.com/pressly/goose/v3\"\n)\n\nfunc init() {\n\tgoose.AddMigrationNoTx(up006, nil)\n}\n\nfunc up006(db *sql.DB) error {\n\treturn createTable(db, \"delta\")\n}\n"
  },
  {
    "path": "tests/gomigrations/success/testdata/007_down_only_no_tx.go",
    "content": "package gomigrations\n\nimport (\n\t\"database/sql\"\n\n\t\"github.com/pressly/goose/v3\"\n)\n\nfunc init() {\n\tgoose.AddMigrationNoTx(nil, down007)\n}\n\nfunc down007(db *sql.DB) error {\n\treturn dropTable(db, \"delta\")\n}\n"
  },
  {
    "path": "tests/gomigrations/success/testdata/008_empty_no_tx.go",
    "content": "package gomigrations\n\nimport (\n\t\"github.com/pressly/goose/v3\"\n)\n\nfunc init() {\n\tgoose.AddMigrationNoTx(nil, nil)\n}\n"
  },
  {
    "path": "tests/gomigrations/success/testdata/009_up_down_ctx.go",
    "content": "package gomigrations\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\n\t\"github.com/pressly/goose/v3\"\n)\n\nfunc init() {\n\tgoose.AddMigrationContext(up009, down009)\n}\n\nfunc up009(ctx context.Context, tx *sql.Tx) error {\n\treturn createTable(tx, \"echo\")\n}\n\nfunc down009(ctx context.Context, tx *sql.Tx) error {\n\treturn dropTable(tx, \"echo\")\n}\n"
  },
  {
    "path": "tests/gomigrations/success/testdata/010_up_only_ctx.go",
    "content": "package gomigrations\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\n\t\"github.com/pressly/goose/v3\"\n)\n\nfunc init() {\n\tgoose.AddMigrationContext(up010, nil)\n}\n\nfunc up010(ctx context.Context, tx *sql.Tx) error {\n\treturn createTable(tx, \"foxtrot\")\n}\n"
  },
  {
    "path": "tests/gomigrations/success/testdata/011_down_only_ctx.go",
    "content": "package gomigrations\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\n\t\"github.com/pressly/goose/v3\"\n)\n\nfunc init() {\n\tgoose.AddMigrationContext(nil, down011)\n}\n\nfunc down011(ctx context.Context, tx *sql.Tx) error {\n\treturn dropTable(tx, \"foxtrot\")\n}\n"
  },
  {
    "path": "tests/gomigrations/success/testdata/012_empty_ctx.go",
    "content": "package gomigrations\n\nimport (\n\t\"github.com/pressly/goose/v3\"\n)\n\nfunc init() {\n\tgoose.AddMigrationContext(nil, nil)\n}\n"
  },
  {
    "path": "tests/gomigrations/success/testdata/013_up_down_no_tx_ctx.go",
    "content": "package gomigrations\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\n\t\"github.com/pressly/goose/v3\"\n)\n\nfunc init() {\n\tgoose.AddMigrationNoTxContext(up013, down013)\n}\n\nfunc up013(ctx context.Context, db *sql.DB) error {\n\treturn createTable(db, \"golf\")\n}\n\nfunc down013(ctx context.Context, db *sql.DB) error {\n\treturn dropTable(db, \"golf\")\n}\n"
  },
  {
    "path": "tests/gomigrations/success/testdata/014_up_only_no_tx_ctx.go",
    "content": "package gomigrations\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\n\t\"github.com/pressly/goose/v3\"\n)\n\nfunc init() {\n\tgoose.AddMigrationNoTxContext(up014, nil)\n}\n\nfunc up014(ctx context.Context, db *sql.DB) error {\n\treturn createTable(db, \"hotel\")\n}\n"
  },
  {
    "path": "tests/gomigrations/success/testdata/015_down_only_no_tx_ctx.go",
    "content": "package gomigrations\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\n\t\"github.com/pressly/goose/v3\"\n)\n\nfunc init() {\n\tgoose.AddMigrationNoTxContext(nil, down015)\n}\n\nfunc down015(ctx context.Context, db *sql.DB) error {\n\treturn dropTable(db, \"hotel\")\n}\n"
  },
  {
    "path": "tests/gomigrations/success/testdata/016_empty_no_tx_ctx.go",
    "content": "package gomigrations\n\nimport (\n\t\"github.com/pressly/goose/v3\"\n)\n\nfunc init() {\n\tgoose.AddMigrationNoTxContext(nil, nil)\n}\n"
  },
  {
    "path": "up.go",
    "content": "package goose\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n)\n\ntype options struct {\n\tallowMissing bool\n\tapplyUpByOne bool\n\tnoVersioning bool\n}\n\ntype OptionsFunc func(o *options)\n\nfunc WithAllowMissing() OptionsFunc {\n\treturn func(o *options) { o.allowMissing = true }\n}\n\nfunc WithNoVersioning() OptionsFunc {\n\treturn func(o *options) { o.noVersioning = true }\n}\n\nfunc WithNoColor(b bool) OptionsFunc {\n\treturn func(o *options) { noColor = b }\n}\n\nfunc withApplyUpByOne() OptionsFunc {\n\treturn func(o *options) { o.applyUpByOne = true }\n}\n\n// UpTo migrates up to a specific version.\nfunc UpTo(db *sql.DB, dir string, version int64, opts ...OptionsFunc) error {\n\tctx := context.Background()\n\treturn UpToContext(ctx, db, dir, version, opts...)\n}\n\nfunc UpToContext(ctx context.Context, db *sql.DB, dir string, version int64, opts ...OptionsFunc) error {\n\toption := &options{}\n\tfor _, f := range opts {\n\t\tf(option)\n\t}\n\tfoundMigrations, err := CollectMigrations(dir, minVersion, version)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif option.noVersioning {\n\t\tif len(foundMigrations) == 0 {\n\t\t\treturn nil\n\t\t}\n\t\tif option.applyUpByOne {\n\t\t\t// For up-by-one this means keep re-applying the first\n\t\t\t// migration over and over.\n\t\t\tversion = foundMigrations[0].Version\n\t\t}\n\t\treturn upToNoVersioning(ctx, db, foundMigrations, version)\n\t}\n\n\tif _, err := EnsureDBVersionContext(ctx, db); err != nil {\n\t\treturn err\n\t}\n\tdbMigrations, err := listAllDBVersions(ctx, db)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdbMaxVersion := dbMigrations[len(dbMigrations)-1].Version\n\t// lookupAppliedInDB is a map of all applied migrations in the database.\n\tlookupAppliedInDB := make(map[int64]bool)\n\tfor _, m := range dbMigrations {\n\t\tlookupAppliedInDB[m.Version] = true\n\t}\n\n\tmissingMigrations := findMissingMigrations(dbMigrations, foundMigrations, dbMaxVersion)\n\n\t// feature(mf): It is very possible someone may want to apply ONLY new migrations\n\t// and skip missing migrations altogether. At the moment this is not supported,\n\t// but leaving this comment because that's where that logic will be handled.\n\tif len(missingMigrations) > 0 && !option.allowMissing {\n\t\tvar collected []string\n\t\tfor _, m := range missingMigrations {\n\t\t\toutput := fmt.Sprintf(\"version %d: %s\", m.Version, m.Source)\n\t\t\tcollected = append(collected, output)\n\t\t}\n\t\treturn fmt.Errorf(\"error: found %d missing migrations before current version %d:\\n\\t%s\",\n\t\t\tlen(missingMigrations), dbMaxVersion, strings.Join(collected, \"\\n\\t\"))\n\t}\n\tvar migrationsToApply Migrations\n\tif option.allowMissing {\n\t\tmigrationsToApply = missingMigrations\n\t}\n\t// filter all migrations with a version greater than the supplied version (min) and less than or\n\t// equal to the requested version (max). Note, we do not need to filter out missing migrations\n\t// because we are only appending \"new\" migrations that have a higher version than the current\n\t// database max version, which inevitably means they are not \"missing\".\n\tfor _, m := range foundMigrations {\n\t\tif lookupAppliedInDB[m.Version] {\n\t\t\tcontinue\n\t\t}\n\t\tif m.Version > dbMaxVersion && m.Version <= version {\n\t\t\tmigrationsToApply = append(migrationsToApply, m)\n\t\t}\n\t}\n\n\tvar current int64\n\tfor _, m := range migrationsToApply {\n\t\tif err := m.UpContext(ctx, db); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif option.applyUpByOne {\n\t\t\treturn nil\n\t\t}\n\t\tcurrent = m.Version\n\t}\n\n\tif len(migrationsToApply) == 0 {\n\t\tcurrent, err = GetDBVersionContext(ctx, db)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tlog.Printf(\"goose: no migrations to run. current version: %d\", current)\n\t} else {\n\t\tlog.Printf(\"goose: successfully migrated database to version: %d\", current)\n\t}\n\n\t// At this point there are no more migrations to apply. But we need to maintain\n\t// the following behaviour:\n\t// UpByOne returns an error to signifying there are no more migrations.\n\t// Up and UpTo return nil\n\n\tif option.applyUpByOne {\n\t\treturn ErrNoNextVersion\n\t}\n\n\treturn nil\n}\n\n// upToNoVersioning applies up migrations up to, and including, the\n// target version.\nfunc upToNoVersioning(ctx context.Context, db *sql.DB, migrations Migrations, version int64) error {\n\tvar finalVersion int64\n\tfor _, current := range migrations {\n\t\tif current.Version > version {\n\t\t\tbreak\n\t\t}\n\t\tcurrent.noVersioning = true\n\t\tif err := current.UpContext(ctx, db); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfinalVersion = current.Version\n\t}\n\tlog.Printf(\"goose: up to current file version: %d\", finalVersion)\n\treturn nil\n}\n\n// Up applies all available migrations.\nfunc Up(db *sql.DB, dir string, opts ...OptionsFunc) error {\n\tctx := context.Background()\n\treturn UpContext(ctx, db, dir, opts...)\n}\n\n// UpContext applies all available migrations.\nfunc UpContext(ctx context.Context, db *sql.DB, dir string, opts ...OptionsFunc) error {\n\treturn UpToContext(ctx, db, dir, maxVersion, opts...)\n}\n\n// UpByOne migrates up by a single version.\nfunc UpByOne(db *sql.DB, dir string, opts ...OptionsFunc) error {\n\tctx := context.Background()\n\treturn UpByOneContext(ctx, db, dir, opts...)\n}\n\n// UpByOneContext migrates up by a single version.\nfunc UpByOneContext(ctx context.Context, db *sql.DB, dir string, opts ...OptionsFunc) error {\n\topts = append(opts, withApplyUpByOne())\n\treturn UpToContext(ctx, db, dir, maxVersion, opts...)\n}\n\n// listAllDBVersions returns a list of all migrations, ordered ascending.\nfunc listAllDBVersions(ctx context.Context, db *sql.DB) (Migrations, error) {\n\tdbMigrations, err := store.ListMigrations(ctx, db, TableName())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tall := make(Migrations, 0, len(dbMigrations))\n\tfor _, m := range dbMigrations {\n\t\tall = append(all, &Migration{\n\t\t\tVersion: m.VersionID,\n\t\t})\n\t}\n\t// ListMigrations returns migrations in descending order by id.\n\t// But we want to return them in ascending order by version_id, so we re-sort.\n\tsort.SliceStable(all, func(i, j int) bool {\n\t\treturn all[i].Version < all[j].Version\n\t})\n\treturn all, nil\n}\n\n// findMissingMigrations migrations returns all missing migrations.\n// A migrations is considered missing if it has a version less than the\n// current known max version.\nfunc findMissingMigrations(knownMigrations, newMigrations Migrations, dbMaxVersion int64) Migrations {\n\texisting := make(map[int64]bool)\n\tfor _, known := range knownMigrations {\n\t\texisting[known.Version] = true\n\t}\n\tvar missing Migrations\n\tfor _, new := range newMigrations {\n\t\tif !existing[new.Version] && new.Version < dbMaxVersion {\n\t\t\tmissing = append(missing, new)\n\t\t}\n\t}\n\tsort.SliceStable(missing, func(i, j int) bool {\n\t\treturn missing[i].Version < missing[j].Version\n\t})\n\treturn missing\n}\n"
  },
  {
    "path": "up_test.go",
    "content": "package goose\n\nimport (\n\t\"testing\"\n)\n\nfunc TestFindMissingMigrations(t *testing.T) {\n\tknown := Migrations{\n\t\t{Version: 1},\n\t\t{Version: 3},\n\t\t{Version: 4},\n\t\t{Version: 5},\n\t\t{Version: 7}, // <-- database max version_id\n\t}\n\tnew := Migrations{\n\t\t{Version: 1},\n\t\t{Version: 2}, // missing migration\n\t\t{Version: 3},\n\t\t{Version: 4},\n\t\t{Version: 5},\n\t\t{Version: 6}, // missing migration\n\t\t{Version: 7}, // <-- database max version_id\n\t\t{Version: 8}, // new migration\n\t}\n\tgot := findMissingMigrations(known, new, 7)\n\tif len(got) != 2 {\n\t\tt.Fatalf(\"invalid migration count: got:%d want:%d\", len(got), 2)\n\t}\n\tif got[0].Version != 2 {\n\t\tt.Errorf(\"expecting first migration: got:%d want:%d\", got[0].Version, 2)\n\t}\n\tif got[1].Version != 6 {\n\t\tt.Errorf(\"expecting second migration: got:%d want:%d\", got[0].Version, 6)\n\t}\n}\n"
  },
  {
    "path": "version.go",
    "content": "package goose\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n)\n\n// Version prints the current version of the database.\nfunc Version(db *sql.DB, dir string, opts ...OptionsFunc) error {\n\tctx := context.Background()\n\treturn VersionContext(ctx, db, dir, opts...)\n}\n\n// VersionContext prints the current version of the database.\nfunc VersionContext(ctx context.Context, db *sql.DB, dir string, opts ...OptionsFunc) error {\n\toption := &options{}\n\tfor _, f := range opts {\n\t\tf(option)\n\t}\n\tif option.noVersioning {\n\t\tvar current int64\n\t\tmigrations, err := CollectMigrations(dir, minVersion, maxVersion)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to collect migrations: %w\", err)\n\t\t}\n\t\tif len(migrations) > 0 {\n\t\t\tcurrent = migrations[len(migrations)-1].Version\n\t\t}\n\t\tlog.Printf(\"goose: file version %v\", current)\n\t\treturn nil\n\t}\n\n\tcurrent, err := GetDBVersionContext(ctx, db)\n\tif err != nil {\n\t\treturn err\n\t}\n\tlog.Printf(\"goose: version %v\", current)\n\treturn nil\n}\n\nvar tableName = \"goose_db_version\"\n\n// TableName returns goose db version table name\nfunc TableName() string {\n\treturn tableName\n}\n\n// SetTableName set goose db version table name\nfunc SetTableName(n string) {\n\ttableName = n\n}\n"
  }
]