feat(migrate): configure embedded Goose providers (#14)
Add embedded SQL and provider-local Go migrations with slog logging and Postgres session locking, returning Goose's Provider directly. Cover timestamp and sequential versions, failure recovery, and concurrent startup with SQLite and Postgres tests. Reuse page's Postgres CI service. Document authoring conventions and the Go template compatibility gate. Closes #7 Refs #9
This commit was merged in pull request #14.
This commit is contained in:
1 parent
c45e8abe08
commit
f0439000cb
15 files changed
+805
-56
No files matched your search
@@ -0,0 +1,97 @@
|
||||
# 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.
|
||||
|
||||
Create SQL migration files with Goose's
|
||||
[`create` command](https://pressly.github.io/goose/documentation/cli-commands/#create).
|
||||
For timestamp-to-sequential reconciliation before deployment, use its
|
||||
[`fix` command](https://pressly.github.io/goose/documentation/cli-commands/#fix)
|
||||
as described in [hybrid versioning](https://github.com/pressly/goose#hybrid-versioning).
|
||||
These commands edit source files before embedding; `fix` does not update database
|
||||
history. Never renumber migrations already applied to shared databases.
|
||||
|
||||
**Goose's default Go template is not compatible unchanged with this package.**
|
||||
It registers migrations globally from `init`, while this package uses explicit
|
||||
provider-local registration. Until the compatible authoring CLI planned in
|
||||
[#9](https://gitea.auvem.com/corestack/go/issues/9) is available, Go migrations
|
||||
require the adaptation described below; do not treat stock `goose create ... go`
|
||||
output as ready to use. That follow-up must supply and document the supported
|
||||
creation/reconciliation workflow before it is complete.
|
||||
|
||||
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.
|
||||
|
||||
Preserve Goose's creator/fix filenames and ship the complete migration history.
|
||||
The pinned Goose SQL-only provider relies on lexical filename order matching
|
||||
numeric version order. Timestamp versions and gaps are supported; never derive
|
||||
versions from file counts. For operations, errors, and SQL annotations, use the
|
||||
[Provider API](https://pkg.go.dev/github.com/pressly/goose/v3#Provider) and
|
||||
[SQL migration docs](https://pressly.github.io/goose/documentation/annotations/).
|
||||
|
||||
For Go functions, use Goose's
|
||||
[provider-local registration](https://pressly.github.io/goose/documentation/provider/#withgomigrations):
|
||||
pass fresh `goose.NewGoMigration` values as the trailing arguments to `migrate.New`.
|
||||
The creator's default Go template instead calls `goose.AddMigrationContext` from
|
||||
`init`; replace that registration with provider-local construction. Global
|
||||
registration is disabled to keep each provider's migration set isolated and avoid
|
||||
import side effects. If `fix` renames Go migrations, update their explicit
|
||||
registration versions to match. Versions must be unique across SQL and Go files.
|
||||
|
||||
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. 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.
|
||||
Reference in new issue
Block a user