Author SHA1 Message Date
end 864749c61c ci: always validate docs and safely ignore documentation-only changes
ci / check (pull_request) Successful in 54s
Documentation / docs (pull_request) Successful in 15s
2026-10-25 14:33:44 -07:00
end e9dcaaa22c ci: filter documentation and implementation workflows by changed paths
Documentation / docs (pull_request) Successful in 19s
ci / check (pull_request) Successful in 56s
2026-10-25 14:29:30 -07:00
end c349790f6c ci: consolidate documentation checks and avoid duplicate branch runs
ci / check (pull_request) Successful in 1m7s
2026-10-25 14:24:57 -07:00
end 6fee9584e5 chore: adopt Corestack project standards
Documentation / docs (push) Successful in 17s
Documentation / docs (pull_request) Successful in 25s
ci / check (push) Successful in 1m26s
ci / check (pull_request) Successful in 1m22s
2026-10-25 14:21:29 -07:00
17 changed files with 2 additions and 856 deletions

No files matched your search

-14
View File
@@ -15,17 +15,6 @@ on:
jobs: jobs:
check: check:
runs-on: ubuntu-latest runs-on: ubuntu-latest
services:
postgres:
image: postgres:18
env:
POSTGRES_PASSWORD: migrate-test
POSTGRES_DB: migrate_test
options: >-
--health-cmd "pg_isready -U postgres -d migrate_test"
--health-interval 5s
--health-timeout 5s
--health-retries 10
steps: steps:
- uses: actions/checkout@v7.0.1 - uses: actions/checkout@v7.0.1
with: with:
@@ -37,9 +26,6 @@ jobs:
run: test -z "$(gofmt -l .)" run: test -z "$(gofmt -l .)"
- run: go vet ./... - run: go vet ./...
- run: go test -race ./... - run: go test -race ./...
env:
CI: "true"
MIGRATE_POSTGRES_DSN: postgres://postgres:migrate-test@postgres:5432/migrate_test?sslmode=disable
- name: Require issue URLs in TODO comments - name: Require issue URLs in TODO comments
run: | run: |
find . -type d \( -name .git -o -name node_modules -o -name vendor -o -name generated -o -name gen -o -name target -o -name dist -o -name build -o -name .svelte-kit -o -name .next \) -prune -o -type f \( -name '*.go' -o -name '*.ts' -o -name '*.tsx' -o -name '*.js' -o -name '*.svelte' -o -name '*.rs' -o -name '*.sql' -o -name '*.sh' -o -name '*.proto' -o -name '*.graphql' -o -name '*.yml' -o -name '*.yaml' -o -name '*.toml' \) -exec awk 'match($0, "((^|[^:])[/][/]|[/][*]|(^|[[:space:]])(#|--)|^[[:space:]]*([*]|<!--))") { comment=substr($0, RSTART + RLENGTH); todo=index(comment, "TO" "DO"); if (todo && substr(comment, todo + 4) !~ "https?:[/][/]") { print FILENAME ":" FNR ":" $0; bad=1 } } END { exit bad }' {} + # shortcut: line-based scan can flag comment-like strings and misses multiline issue links, use a parser when these need accurate coverage find . -type d \( -name .git -o -name node_modules -o -name vendor -o -name generated -o -name gen -o -name target -o -name dist -o -name build -o -name .svelte-kit -o -name .next \) -prune -o -type f \( -name '*.go' -o -name '*.ts' -o -name '*.tsx' -o -name '*.js' -o -name '*.svelte' -o -name '*.rs' -o -name '*.sql' -o -name '*.sh' -o -name '*.proto' -o -name '*.graphql' -o -name '*.yml' -o -name '*.yaml' -o -name '*.toml' \) -exec awk 'match($0, "((^|[^:])[/][/]|[/][*]|(^|[[:space:]])(#|--)|^[[:space:]]*([*]|<!--))") { comment=substr($0, RSTART + RLENGTH); todo=index(comment, "TO" "DO"); if (todo && substr(comment, todo + 4) !~ "https?:[/][/]") { print FILENAME ":" FNR ":" $0; bad=1 } } END { exit bad }' {} + # shortcut: line-based scan can flag comment-like strings and misses multiline issue links, use a parser when these need accurate coverage
+2 -2
View File
@@ -6,7 +6,7 @@
- Documentation: `go run ./cmd/docs check`; create records with `go run ./cmd/docs new adr|plan "Title"`. This checker repository uses its local implementation as the source of truth. - Documentation: `go run ./cmd/docs check`; create records with `go run ./cmd/docs new adr|plan "Title"`. This checker repository uses its local implementation as the source of truth.
## Scope and decisions ## Scope and decisions
- Before editing under a directory with its own AGENTS.md, read it. Scoped files: none currently; this root governs the repository, including nested Go modules unless nearer instructions override it. - Before editing under a directory with its own AGENTS.md, read it. Scoped files: none; this root governs the single Go module.
- Explicit user instructions win; nearer scoped rules override broader rules. - Explicit user instructions win; nearer scoped rules override broader rules.
- Distinguish confirmed requirements, proposed defaults and unresolved decisions; resolve material ambiguity before dependent work. - Distinguish confirmed requirements, proposed defaults and unresolved decisions; resolve material ambiguity before dependent work.
- Proposed ADRs are context, not authority; read relevant accepted decisions before changing their scope. - Proposed ADRs are context, not authority; read relevant accepted decisions before changing their scope.
@@ -30,7 +30,7 @@ Go module `go.auvem.com/corestack`.
- Docs format (the spec `docs check` implements): `docs/docs-format.md`. - Docs format (the spec `docs check` implements): `docs/docs-format.md`.
- Decisions live in `docs/adr/`, plans in `docs/plans/`. Records are permanent; never rename them. - Decisions live in `docs/adr/`, plans in `docs/plans/`. Records are permanent; never rename them.
- Add dependencies only to the module that needs them; heavy dependencies go in nested modules (see the [module-boundary ADR](/docs/adr/20261025T214616275Z-version-light-core-packages-in-the-root-module-and-nest-heavy-dependency-modules.md)). - Dependencies are limited to `go.yaml.in/yaml/v3` and `github.com/yuin/goldmark`.
## Go ## Go
- Default backend language is Go; follow existing package boundaries. - Default backend language is Go; follow existing package boundaries.
-5
View File
@@ -2,11 +2,6 @@
Shared Go building blocks. The first one is `docs`, a checker for architecture decision records (ADRs) and implementation plans. Shared Go building blocks. The first one is `docs`, a checker for architecture decision records (ADRs) and implementation plans.
## migrate
[`migrate`](migrate/README.md) configures Goose providers for embedded SQL and Go
migrations, with structured logging and Postgres startup locking.
## docs ## docs
Install: Install:
@@ -1,51 +0,0 @@
---
title: "Version light core packages in the root module and nest heavy-dependency modules"
date: 2026-10-25
status: accepted
implementation: implemented
summary: Keep light core packages on root-module versions and isolate heavy or independently versioned packages in nested modules from their first commit.
tags:
- modules
- versioning
supersedes: []
superseded_by: []
related: []
tracking:
- https://gitea.auvem.com/corestack/go/issues/2
implemented_by:
- https://gitea.auvem.com/corestack/go/pulls/4
---
# Version light core packages in the root module and nest heavy-dependency modules
## Context and Problem Statement
The repository currently has one Go module, `go.auvem.com/corestack`. A plain `vX.Y.Z` tag versions every package in that module together and triggers a release of the `docs` binary. Light core packages can share this version stream and one changelog per tag; packages with heavy dependencies or independent release needs benefit from their own module boundaries.
Carving a package out after it has shipped in the root module is the expensive case: older root versions still contain it, while the new nested module supplies the same import path. Consumers can encounter ambiguous imports until coordinated releases and dependency upgrades remove the overlap. Starting a package as a nested module from its first commit avoids that migration.
The fixed dependency list in AGENTS.md described only the docs checker. It would block the planned Bob and Goose dependencies for `page` and `migrate`; dependency placement needs a module-boundary rule instead of a repository-wide allowlist.
## Considered Options
* Keep every package in the root module: simplest releases, but couples heavy and independently versioned packages to core releases and makes later extraction costly.
* Give every package a nested module immediately: avoids later extraction, but adds module and release bookkeeping to light core packages that can share a version.
* Keep light core packages in the root module and start heavy or independently versioned packages as nested modules: retain simple core releases while avoiding the costly extraction case.
## Decision Outcome
Choose the mixed boundary: light core packages share the root module; heavy-dependency or independently versioned packages begin as nested modules.
* Root module, plain `v*` tags: `lifecycle`, `lifecycle/cli`, `migrate`, `page`, and `cmd/docs`. They share one version stream and one changelog per root tag. Before 1.0, breaking changes use minor version bumps.
* Nested modules from their first commit: `gql`, `connect`, and any future package that brings heavy dependencies, such as gqlgen or protobuf, or is likely to need independent versioning. Each has its own `go.mod` and directory-prefixed tags, for example `gql/v0.1.0` and `connect/v0.1.0`.
* Add dependencies only to the module that needs them. The named core packages remain in the root module; heavy dependencies belong in the relevant nested module.
* Keep `cmd/docs` in the root module. The [release workflow](/.gitea/workflows/release.yml) listens only for plain `v*` tags, and [GoReleaser](/.goreleaser.yaml) builds `./cmd/docs`, so only root-module tags publish binaries. Prefixed library tags never publish binaries.
This decision establishes the boundary before the planned packages are added. The rule is implemented in [AGENTS.md](/AGENTS.md), and the existing release configuration already follows it. Future packages apply the rule when introduced; no empty modules are needed.
### Consequences
* Light core packages retain coordinated versions and releases. Consumers do not compile dependencies of packages they do not import; module graph pruning helps limit the dependency graph they load.
* Heavy-dependency and independently versioned packages avoid overlapping ownership of an import path with older root-module versions.
* Each nested module needs its own dependency maintenance, checks, and prefixed release tags. That bookkeeping is reserved for packages that need the boundary.
* Root-module consumers share version bumps even when only another core package changes. Moving a shipped root package into a nested module later still requires coordinated releases and consumer upgrades.
-23
View File
@@ -3,29 +3,6 @@ module go.auvem.com/corestack
go 1.27.1 go 1.27.1
require ( require (
github.com/jackc/pgx/v5 v5.11.0
github.com/pressly/goose/v3 v3.29.0
github.com/yuin/goldmark v1.8.6 github.com/yuin/goldmark v1.8.6
go.yaml.in/yaml/v3 v3.0.5 go.yaml.in/yaml/v3 v3.0.5
modernc.org/sqlite v1.60.1
)
require (
github.com/dustin/go-humanize v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.3 // indirect
github.com/mattn/go-isatty v0.0.24 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect
github.com/ncruces/go-strftime v1.1.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/sethvargo/go-retry v0.5.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/sync v0.24.0 // indirect
golang.org/x/sys v0.49.0 // indirect
golang.org/x/text v0.43.0 // indirect
modernc.org/libc v1.78.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.12.1 // indirect
) )
-77
View File
@@ -1,81 +1,4 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.1.0 h1:dbKTrvD0klcbBV/h4AWJdMuZogJACoMlvWIWZ5b2xWg=
github.com/dustin/go-humanize v1.1.0/go.mod h1:hc1CvRkJMsgxqjmjMQF3QNRAZBwY8AXBAzKYoSX9sFI=
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo=
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.11.0 h1:IzBBtyK9AHqf98cctWFifYSci2hgQR/cd56wB4p+ogg=
github.com/jackc/pgx/v5 v5.11.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.3 h1:f8HoREISb3l5eO9coPgK7U3hKLhWGokOFVcThl5jrao=
github.com/jackc/puddle/v2 v2.2.3/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
github.com/ncruces/go-strftime v1.1.0 h1:vtPmPWMkb539RGV9WpWT9EwJGvrOCKvMNI4obWL47Hk=
github.com/ncruces/go-strftime v1.1.0/go.mod h1:yVaq7iwCN09vHRrKBrrflXT0M9tTMG2KmZ9Fj7xeG/g=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pressly/goose/v3 v3.29.0 h1:dtEnBCT7CqkHIUCdgrXsrmeisaN8gFkRU4Y5OEwif4o=
github.com/pressly/goose/v3 v3.29.0/go.mod h1:plA4aP4+u+Q3FMTgbFbhF8TuAWpAdfWpoFGkJsIFWh0=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/sethvargo/go-retry v0.5.0 h1:JYfFKmVJ8ER06GReN1efzc5cm52BrVsFVzmcHiKJwfY=
github.com/sethvargo/go-retry v0.5.0/go.mod h1:tvsjdKG6xfiCx4LSiUZ06kcv38xvdVQwv8R6/VnnVWg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/yuin/goldmark v1.8.6 h1:d0VcaP1sx9GkFVkoW+KtggpGi2KZ965i14b0+bDQST4= github.com/yuin/goldmark v1.8.6 h1:d0VcaP1sx9GkFVkoW+KtggpGi2KZ965i14b0+bDQST4=
github.com/yuin/goldmark v1.8.6/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/goldmark v1.8.6/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI=
golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.30.1 h1:ZXInYH2CD24GPWSwOFPwB6JrnFt4/6d7ceW1Ukd30Bk=
modernc.org/cc/v4 v4.30.1/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.36.2 h1:j6d5HuXtBxF00vEcI5RE0O6EIrG6RyRpgMiGV2IK9ys=
modernc.org/ccgo/v4 v4.36.2/go.mod h1:Wh8QlPZq/US2DrsCc31FxJVBOEEcN2BBfla6OQDHydA=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.2.0 h1:/Y3LGDMQaoYf55PebP1qsZ5FDGgAZyG1RYKEHeQzh1o=
modernc.org/gc/v3 v3.2.0/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.78.0 h1:Lpm+e9K6ruVYEGwWfuBO1xXtYEaXDouFOG1SuWjFDP4=
modernc.org/libc v1.78.0/go.mod h1:wvxR55nOF2/66NkJoNV7/coXBTCvT3HR22rcqW6lOuA=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.12.1 h1:nFMiWrpStgZczNl6XI9GnIk/rWhYIyHGUaR04pGbp9g=
modernc.org/memory v1.12.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.60.1 h1:/blz53O951KWFOso4QQvEs/Fq6cDBKLtMVrYNSeJVKw=
modernc.org/sqlite v1.60.1/go.mod h1:1dIoEagfDE72QytD5scH1lxARtaUgKgHC/NuApA27r0=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
-118
View File
@@ -1,118 +0,0 @@
# migrate
`go.auvem.com/corestack/migrate` sets the house configuration for
[Goose's Provider API](https://pressly.github.io/goose/documentation/provider/).
It returns `*goose.Provider` directly: Goose owns execution, version tracking,
status, and errors. There is no lifecycle dependency or background work.
Embed migrations in the application's binary and run them before serving traffic:
```go
package app
import (
"context"
"database/sql"
"embed"
"log/slog"
"github.com/pressly/goose/v3"
"go.auvem.com/corestack/migrate"
)
//go:embed migrations/*.sql
var migrations embed.FS
func migrateDatabase(ctx context.Context, db *sql.DB, logger *slog.Logger) error {
provider, err := migrate.New(db, goose.DialectPostgres, migrations, "migrations", logger)
if err != nil {
return err
}
_, err = provider.Up(ctx)
return err
}
```
The caller opens and closes `db`, selects a matching database driver and dialect,
supplies a nonnil logger, and controls the operation context/deadline. Construction
is offline; SQL is parsed lazily by Goose. `Provider.Close` also closes `db`, so do
not defer it when sharing the pool with the application. Use `"."` for SQL files
at the embedded root. Files in the selected directory are discovered directly,
without recursively searching subdirectories.
Use unique positive versions and Goose's migration creator naming convention:
zero-padded sequential names such as `00007_users.sql` and `00010_events.sql`, or
fixed-width timestamps such as `20261027114500_create_users.sql`. Filenames must
sort lexically in numeric version order, including when mixing naming schemes;
choose padding wide enough for the complete history. Goose v3.29.0 preserves
lexical source order for SQL-only providers, so arbitrary unpadded names such as
`7_users.sql` and `10_events.sql` are unsupported by this house convention: they
can produce incorrect latest versions and source/status ordering. This package
does not rename or normalize migration files.
Never derive a version from a file count or add one to the current version.
Keep applied migrations immutable and ship the complete history. Goose rejects
newly introduced migrations below the current version by default.
```sql
-- +goose Up
CREATE TABLE users (id BIGINT PRIMARY KEY);
-- +goose Down
DROP TABLE users;
```
Goose runs each migration in a transaction by default. Its annotations support
statement blocks and explicit `NO TRANSACTION` where required; such migrations
and nontransactional database DDL can leave partial effects on failure. Down
migrations execute application-authored SQL and may delete data.
| Operation | Provider method |
| --- | --- |
| Apply all pending migrations | `Up(ctx)` |
| Apply pending versions through a target, inclusive | `UpTo(ctx, version)` |
| Roll back versions above a target; zero removes all | `DownTo(ctx, version)` |
| Inspect each migration's state | `Status(ctx)` |
| Current database version | `GetDBVersion(ctx)` |
| Current and latest available versions | `GetVersions(ctx)` |
| Detect pending migrations | `HasPending(ctx)` |
Targets are numeric version bounds, so gaps and timestamps work without special
handling. Up on a fully migrated database succeeds with no results. Operations
may create Goose's `goose_db_version` table even when only inspecting state.
Execution errors retain Goose's details; use `errors.As` with
`*goose.PartialError` to inspect applied and failed migrations. Transactional
failure leaves the database at the last committed migration version. Operation
logs use the injected slog logger; callers handle and log returned errors.
For Go functions, pass migrations created with `goose.NewGoMigration(version,
up, down)` as the trailing arguments to `migrate.New`. Use `goose.GoFunc.RunTx`
for transactional work or `RunDB` for work requiring its own connections. Each
provider should receive its own migration instances. Versions must be unique
across SQL and Go migrations. Global registration is disabled; no `init` or
`goose.AddMigration` calls are needed.
Postgres providers automatically use Goose's default session advisory locker.
Concurrent startup calls from separate providers/processes serialize migration
execution and recheck applied versions under the lock. The default lock ID
serializes Goose migrations throughout the same database, including different
schemas; lock acquisition retries for about five minutes and unlock retries for
about one minute, subject to the operation context. Use a direct connection or
a session-pooling proxy: transaction pooling cannot preserve session locks.
`RunDB` Go migrations need spare pool connections while the locked session is
held; avoid a one-connection pool for them.
Other dialects have no cross-provider/process lock configured by this package.
Serialize migration runs externally, for example with a single deployment job.
SQLite tests validate the SQL convention, not concurrent daemon startup safety.
Run the integration suite against an existing Postgres database with schema
creation privileges:
```sh
MIGRATE_POSTGRES_DSN='postgres://user:password@localhost/testdb?sslmode=disable' go test -race ./migrate
```
Tests create and clean only random schemas they own. Without a DSN, Postgres
tests skip locally; in CI a missing DSN fails, and the CI job supplies a Postgres
service. SQLite tests always run in process using temporary databases.
-65
View File
@@ -1,65 +0,0 @@
// Package migrate configures Goose providers for embedded application migrations.
// Applications own their database, migration files, and startup ordering.
package migrate
import (
"database/sql"
"embed"
"fmt"
"io/fs"
"log/slog"
"github.com/pressly/goose/v3"
"github.com/pressly/goose/v3/lock"
)
// New returns a provider for SQL files in dir and optional provider-local Go
// migrations. Pass "." for files at the embedded root and a nonnil logger.
// The selected directory must exist, including when using only Go migrations.
// Construction does not connect to db; the caller owns and closes db.
//
// PostgreSQL uses Goose's session advisory locker with its default lock ID and
// retry limits. Other dialects require the caller to serialize migration runs
// across providers/processes. PostgreSQL connections must preserve session
// affinity (transaction-pooling proxies cannot safely hold session locks).
//
// Use the returned Provider's Up, UpTo, DownTo, Status, GetDBVersion, GetVersions,
// and HasPending methods directly. Filenames must sort lexically in numeric
// version order: use zero-padded sequential or fixed-width timestamp prefixes,
// as produced by Goose's migration creator. Global Go registration is ignored.
func New(db *sql.DB, dialect goose.Dialect, files embed.FS, dir string, logger *slog.Logger, migrations ...*goose.Migration) (*goose.Provider, error) {
for i, migration := range migrations {
if migration == nil {
return nil, fmt.Errorf("migrate: Go migration %d must not be nil", i)
}
}
root, err := fs.Sub(files, dir)
if err != nil {
return nil, fmt.Errorf("migrate: directory %q: %w", dir, err)
}
info, err := fs.Stat(root, ".")
if err != nil {
return nil, fmt.Errorf("migrate: directory %q: %w", dir, err)
}
if !info.IsDir() {
return nil, fmt.Errorf("migrate: directory %q: not a directory", dir)
}
opts := []goose.ProviderOption{
goose.WithDisableGlobalRegistry(true),
goose.WithGoMigrations(migrations...),
goose.WithSlog(logger),
goose.WithVerbose(true),
}
if dialect == goose.DialectPostgres {
locker, err := lock.NewPostgresSessionLocker()
if err != nil {
return nil, fmt.Errorf("migrate: postgres locker: %w", err)
}
opts = append(opts, goose.WithSessionLocker(locker))
}
provider, err := goose.NewProvider(dialect, db, root, opts...)
if err != nil {
return nil, fmt.Errorf("migrate: new provider: %w", err)
}
return provider, nil
}
-457
View File
@@ -1,457 +0,0 @@
package migrate_test
import (
"bytes"
"context"
"crypto/rand"
"database/sql"
"embed"
"errors"
"io"
"io/fs"
"log/slog"
"os"
"path/filepath"
"slices"
"strings"
"testing"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/stdlib"
"github.com/pressly/goose/v3"
_ "modernc.org/sqlite"
"go.auvem.com/corestack/migrate"
)
// invariant: embedded fixtures are read-only; no test mutates shared migration state.
//
//go:embed testdata
var fixtures embed.FS
func TestSQLite(t *testing.T) {
for _, test := range []struct {
name string
run func(*testing.T, *sql.DB, goose.Dialect)
}{
{"flow", testFlow}, {"sql-only", testSQLOnly}, {"failure", testFailure},
} {
t.Run(test.name, func(t *testing.T) {
db, err := sql.Open("sqlite", filepath.Join(t.TempDir(), "migrate.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Error(err)
}
})
test.run(t, db, goose.DialectSQLite3)
})
}
}
func TestPostgres(t *testing.T) {
for _, test := range []struct {
name string
run func(*testing.T, *sql.DB, goose.Dialect)
}{
{"flow", testFlow}, {"sql-only", testSQLOnly}, {"failure", testFailure},
} {
t.Run(test.name, func(t *testing.T) { test.run(t, postgresDatabase(t)(), goose.DialectPostgres) })
}
}
func testFlow(t *testing.T, db *sql.DB, dialect goose.Dialect) {
t.Helper()
ctx, cancel := context.WithTimeout(t.Context(), time.Minute)
defer cancel()
var logs bytes.Buffer
goMigration := goose.NewGoMigration(20261027120000,
&goose.GoFunc{RunTx: func(ctx context.Context, tx *sql.Tx) error {
_, err := tx.ExecContext(ctx, "INSERT INTO migration_events (version) VALUES (20261027120000)")
return err
}},
&goose.GoFunc{RunTx: func(ctx context.Context, tx *sql.Tx) error {
_, err := tx.ExecContext(ctx, "DELETE FROM migration_events WHERE version = 20261027120000")
return err
}})
p, err := migrate.New(db, dialect, fixtures, "testdata/migrations", slog.New(slog.NewJSONHandler(&logs, nil)), goMigration)
if err != nil {
t.Fatal(err)
}
versions := []int64{7, 20261026093000, 20261027114500, 20261027120000}
// Registering on one provider must not leak into another provider in the same process.
other := newProvider(t, db, dialect, "testdata/migrations")
if len(other.ListSources()) != 3 {
t.Fatal("Go migration leaked to another provider")
}
assertState(t, ctx, p, 0, versions, 0)
results, err := p.UpTo(ctx, versions[1])
if err != nil {
t.Fatal(err)
}
assertResults(t, results, versions[:2])
assertState(t, ctx, p, versions[1], versions, 2)
results, err = p.Up(ctx)
if err != nil {
t.Fatal(err)
}
assertResults(t, results, versions[2:])
assertState(t, ctx, p, versions[3], versions, 4)
assertEvents(t, ctx, db, versions)
results, err = p.Up(ctx)
if err != nil || len(results) != 0 {
t.Fatalf("idempotent Up = %v, %v", results, err)
}
results, err = p.DownTo(ctx, versions[1])
if err != nil {
t.Fatal(err)
}
assertResults(t, results, []int64{versions[3], versions[2]})
assertState(t, ctx, p, versions[1], versions, 2)
assertEvents(t, ctx, db, versions[:2])
results, err = p.DownTo(ctx, versions[0])
if err != nil {
t.Fatal(err)
}
assertResults(t, results, []int64{versions[1]})
assertState(t, ctx, p, versions[0], versions, 1)
assertEvents(t, ctx, db, versions[:1])
if _, err := p.DownTo(ctx, 0); err != nil {
t.Fatal(err)
}
assertState(t, ctx, p, 0, versions, 0)
results, err = p.Up(ctx)
if err != nil {
t.Fatal(err)
}
assertResults(t, results, versions)
assertEvents(t, ctx, db, versions)
if !strings.Contains(logs.String(), `"logger":"goose"`) || !strings.Contains(logs.String(), `"version":20261027114500`) {
t.Fatalf("missing structured migration logs: %s", logs.String())
}
}
func testSQLOnly(t *testing.T, db *sql.DB, dialect goose.Dialect) {
t.Helper()
ctx, cancel := context.WithTimeout(t.Context(), time.Minute)
defer cancel()
for _, test := range []struct {
dir string
versions []int64
}{
{"testdata/numeric", []int64{7, 10}},
{"testdata/migrations", []int64{7, 20261026093000, 20261027114500}},
} {
p := newProvider(t, db, dialect, test.dir)
assertState(t, ctx, p, 0, test.versions, 0)
results, err := p.UpTo(ctx, test.versions[1]-1)
if err != nil {
t.Fatal(err)
}
assertResults(t, results, test.versions[:1])
assertState(t, ctx, p, test.versions[0], test.versions, 1)
results, err = p.Up(ctx)
if err != nil {
t.Fatal(err)
}
assertResults(t, results, test.versions[1:])
assertState(t, ctx, p, test.versions[len(test.versions)-1], test.versions, len(test.versions))
assertEvents(t, ctx, db, test.versions)
results, err = p.DownTo(ctx, test.versions[1]-1)
if err != nil {
t.Fatal(err)
}
wantDown := slices.Clone(test.versions[1:])
slices.Reverse(wantDown)
assertResults(t, results, wantDown)
assertState(t, ctx, p, test.versions[0], test.versions, 1)
if _, err := p.DownTo(ctx, 0); err != nil {
t.Fatal(err)
}
assertState(t, ctx, p, 0, test.versions, 0)
}
}
func testFailure(t *testing.T, db *sql.DB, dialect goose.Dialect) {
t.Helper()
ctx, cancel := context.WithTimeout(t.Context(), time.Minute)
defer cancel()
p := newProvider(t, db, dialect, "testdata/failure")
_, err := p.Up(ctx)
var partial *goose.PartialError
if !errors.As(err, &partial) || partial.Failed.Source.Version != 20261027114500 ||
!strings.Contains(err.Error(), "missing_migration_table") || !strings.Contains(err.Error(), "20261027114500") {
t.Fatalf("expected clear failed timestamp migration error, got %v", err)
}
assertResults(t, partial.Applied, []int64{7, 20261026093000})
assertState(t, ctx, p, 20261026093000, []int64{7, 20261026093000, 20261027114500}, 2)
assertEvents(t, ctx, db, []int64{7, 20261026093000})
// A second attempt must fail at the same version without repeating committed work.
if _, err := p.Up(ctx); err == nil {
t.Fatal("retry unexpectedly succeeded")
}
assertEvents(t, ctx, db, []int64{7, 20261026093000})
}
func newProvider(t *testing.T, db *sql.DB, dialect goose.Dialect, dir string, migrations ...*goose.Migration) *goose.Provider {
t.Helper()
p, err := migrate.New(db, dialect, fixtures, dir, slog.New(slog.NewTextHandler(io.Discard, nil)), migrations...)
if err != nil {
t.Fatal(err)
}
return p
}
func assertState(t *testing.T, ctx context.Context, p *goose.Provider, current int64, versions []int64, applied int) {
t.Helper()
var sources []int64
for _, source := range p.ListSources() {
sources = append(sources, source.Version)
}
if !slices.Equal(sources, versions) {
t.Fatalf("ListSources = %v; want %v", sources, versions)
}
got, err := p.GetDBVersion(ctx)
if err != nil || got != current {
t.Fatalf("GetDBVersion = %d, %v; want %d", got, err, current)
}
got, latest, err := p.GetVersions(ctx)
if err != nil || got != current || latest != versions[len(versions)-1] {
t.Fatalf("GetVersions = %d, %d, %v", got, latest, err)
}
pending, err := p.HasPending(ctx)
if err != nil || pending != (applied < len(versions)) {
t.Fatalf("HasPending = %v, %v", pending, err)
}
status, err := p.Status(ctx)
if err != nil {
t.Fatal(err)
}
if len(status) != len(versions) {
t.Fatalf("Status length = %d; want %d", len(status), len(versions))
}
for i, s := range status {
want := goose.StatePending
if i < applied {
want = goose.StateApplied
}
if s.Source.Version != versions[i] || s.State != want {
t.Fatalf("Status[%d] = %+v, version %d; want %s, %d", i, s, s.Source.Version, want, versions[i])
}
}
}
func assertResults(t *testing.T, results []*goose.MigrationResult, want []int64) {
t.Helper()
got := make([]int64, 0, len(results))
for _, r := range results {
got = append(got, r.Source.Version)
}
if !slices.Equal(got, want) {
t.Fatalf("applied versions = %v; want %v", got, want)
}
}
func assertEvents(t *testing.T, ctx context.Context, db *sql.DB, want []int64) {
t.Helper()
rows, err := db.QueryContext(ctx, "SELECT version FROM migration_events ORDER BY version")
if err != nil {
t.Fatal(err)
}
defer rows.Close()
var got []int64
for rows.Next() {
var v int64
if err := rows.Scan(&v); err != nil {
t.Fatal(err)
}
got = append(got, v)
}
if err := rows.Err(); err != nil {
t.Fatal(err)
}
if !slices.Equal(got, want) {
t.Fatalf("events = %v; want %v", got, want)
}
}
// postgresDatabase owns only its random schema, never the supplied database.
func postgresDatabase(t *testing.T) func() *sql.DB {
t.Helper()
dsn := os.Getenv("MIGRATE_POSTGRES_DSN")
if dsn == "" {
if os.Getenv("CI") != "" {
t.Fatal("MIGRATE_POSTGRES_DSN is required in CI")
}
t.Skip("set MIGRATE_POSTGRES_DSN to run Postgres integration tests")
}
config, err := pgx.ParseConfig(dsn)
if err != nil {
t.Fatal(err)
}
admin := stdlib.OpenDB(*config)
t.Cleanup(func() {
if err := admin.Close(); err != nil {
t.Error(err)
}
})
schema := "migrate_" + strings.ToLower(rand.Text())
ctx, cancel := context.WithTimeout(t.Context(), time.Minute)
defer cancel()
if _, err := admin.ExecContext(ctx, "CREATE SCHEMA "+schema); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
if _, err := admin.ExecContext(ctx, "DROP SCHEMA "+schema+" CASCADE"); err != nil {
t.Error(err)
}
})
config.RuntimeParams["search_path"] = schema
return func() *sql.DB {
db := stdlib.OpenDB(*config)
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Error(err)
}
})
return db
}
}
func TestPostgresConcurrentUp(t *testing.T) {
open := postgresDatabase(t)
ctx, cancel := context.WithTimeout(t.Context(), time.Minute)
defer cancel()
const workers = 4
providers := make([]*goose.Provider, workers)
for i := range providers {
// Each provider owns a distinct pool and Go migration instance, like separate daemons.
pause := goose.NewGoMigration(3, &goose.GoFunc{RunTx: func(ctx context.Context, tx *sql.Tx) error {
_, err := tx.ExecContext(ctx, "SELECT pg_sleep(0.2)")
return err
}}, nil)
providers[i] = newProvider(t, open(), goose.DialectPostgres, "testdata/migrations", pause)
}
type outcome struct {
results []*goose.MigrationResult
err error
}
start := make(chan struct{})
done := make(chan outcome, workers)
for _, p := range providers {
go func() {
select {
case <-start:
case <-ctx.Done():
done <- outcome{err: ctx.Err()}
return
}
results, err := p.Up(ctx)
done <- outcome{results, err}
}()
}
close(start)
var applied []int64
for range workers {
result := <-done
if result.err != nil {
t.Error(result.err)
}
for _, r := range result.results {
applied = append(applied, r.Source.Version)
}
}
slices.Sort(applied)
if want := []int64{3, 7, 20261026093000, 20261027114500}; !slices.Equal(applied, want) {
t.Fatalf("concurrent Up applied = %v; want exactly once each: %v", applied, want)
}
assertEvents(t, ctx, open(), []int64{7, 20261026093000, 20261027114500})
for _, p := range providers {
assertState(t, ctx, p, 20261027114500, applied, len(applied))
}
}
func TestConstructor(t *testing.T) {
db, err := sql.Open("sqlite", ":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
for _, test := range []struct {
name string
db *sql.DB
dialect goose.Dialect
dir string
logger *slog.Logger
migrations []*goose.Migration
contains string
}{
{name: "nil database", dialect: goose.DialectSQLite3, dir: "testdata/migrations", logger: logger, contains: "db must not be nil"},
{name: "nil logger", db: db, dialect: goose.DialectSQLite3, dir: "testdata/migrations", contains: "slog logger must not be nil"},
{name: "invalid directory", db: db, dialect: goose.DialectSQLite3, dir: "../migrations", logger: logger, contains: "directory"},
{name: "missing directory", db: db, dialect: goose.DialectSQLite3, dir: "missing", logger: logger, contains: "migrate:"},
{name: "invalid dialect", db: db, dialect: "unknown", dir: "testdata/migrations", logger: logger, contains: "dialect"},
{name: "nil migration", db: db, dialect: goose.DialectSQLite3, dir: "testdata/migrations", logger: logger, migrations: []*goose.Migration{nil}, contains: "must not be nil"},
{name: "duplicate version", db: db, dialect: goose.DialectSQLite3, dir: "testdata/migrations", logger: logger, migrations: []*goose.Migration{goose.NewGoMigration(7, nil, nil)}, contains: "7"},
} {
t.Run(test.name, func(t *testing.T) {
p, err := migrate.New(test.db, test.dialect, fixtures, test.dir, test.logger, test.migrations...)
if err == nil || p != nil || !strings.Contains(err.Error(), test.contains) {
t.Fatalf("New = %v, %v; want error containing %q", p, err, test.contains)
}
})
}
// Construction and validation are offline, even when the database is closed.
if err := db.Close(); err != nil {
t.Fatal(err)
}
if _, err := migrate.New(db, goose.DialectSQLite3, fixtures, "testdata/migrations", logger); err != nil {
t.Fatal(err)
}
}
func TestConstructorDirectory(t *testing.T) {
db, err := sql.Open("sqlite", ":memory:")
if err != nil {
t.Fatal(err)
}
// Directory validation and Go-only construction must remain offline.
if err := db.Close(); err != nil {
t.Fatal(err)
}
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
for _, test := range []struct {
dir string
wantErr string
}{
{dir: "missing", wantErr: "directory"},
{dir: "testdata/migrations/00007_events.sql", wantErr: "not a directory"},
{dir: "."},
{dir: "testdata"},
} {
t.Run(test.dir, func(t *testing.T) {
p, err := migrate.New(db, goose.DialectSQLite3, fixtures, test.dir, logger, goose.NewGoMigration(10, nil, nil))
if test.wantErr != "" {
if err == nil || p != nil || !strings.Contains(err.Error(), test.wantErr) || !strings.Contains(err.Error(), test.dir) {
t.Fatalf("New = %v, %v; want directory error containing %q and %q", p, err, test.wantErr, test.dir)
}
if test.dir == "missing" && !errors.Is(err, fs.ErrNotExist) {
t.Fatalf("New error = %v; want fs.ErrNotExist", err)
}
return
}
if err != nil {
t.Fatal(err)
}
if sources := p.ListSources(); len(sources) != 1 || sources[0].Version != 10 {
t.Fatalf("ListSources = %v; want Go migration 10 only", sources)
}
})
}
}
-6
View File
@@ -1,6 +0,0 @@
-- +goose Up
CREATE TABLE migration_events (version BIGINT PRIMARY KEY);
INSERT INTO migration_events (version) VALUES (7);
-- +goose Down
DROP TABLE migration_events;
-5
View File
@@ -1,5 +0,0 @@
-- +goose Up
INSERT INTO migration_events (version) VALUES (20261026093000);
-- +goose Down
DELETE FROM migration_events WHERE version = 20261026093000;
-6
View File
@@ -1,6 +0,0 @@
-- +goose Up
INSERT INTO migration_events (version) VALUES (20261027114500);
INSERT INTO missing_migration_table (version) VALUES (20261027114500);
-- +goose Down
DELETE FROM migration_events WHERE version = 20261027114500;
-6
View File
@@ -1,6 +0,0 @@
-- +goose Up
CREATE TABLE migration_events (version BIGINT PRIMARY KEY);
INSERT INTO migration_events (version) VALUES (7);
-- +goose Down
DROP TABLE migration_events;
-5
View File
@@ -1,5 +0,0 @@
-- +goose Up
INSERT INTO migration_events (version) VALUES (20261026093000);
-- +goose Down
DELETE FROM migration_events WHERE version = 20261026093000;
-5
View File
@@ -1,5 +0,0 @@
-- +goose Up
INSERT INTO migration_events (version) VALUES (20261027114500);
-- +goose Down
DELETE FROM migration_events WHERE version = 20261027114500;
-6
View File
@@ -1,6 +0,0 @@
-- +goose Up
CREATE TABLE migration_events (version BIGINT PRIMARY KEY);
INSERT INTO migration_events (version) VALUES (7);
-- +goose Down
DROP TABLE migration_events;
-5
View File
@@ -1,5 +0,0 @@
-- +goose Up
INSERT INTO migration_events (version) VALUES (10);
-- +goose Down
DELETE FROM migration_events WHERE version = 10;