feat(migrate): configure embedded Goose providers
Use provider-local SQL and Go migrations, injected slog logging, and PostgreSQL session locking. Exercise timestamp migration flows and failures with SQLite and PostgreSQL, including concurrent startup, and require PostgreSQL integration coverage in CI. Refs #7
This commit is contained in:
1 parent
46c4416ed4
commit
badeefb9a9
13 files changed
+686
No files matched your search
@@ -0,0 +1,110 @@
|
||||
# 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 in filenames such as
|
||||
`20261027114500_create_users.sql`; sequential versions such as `00007_users.sql`
|
||||
also work. 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.
|
||||
Reference in new issue
Block a user