Files
go/migrate
end f0439000cb
Documentation / docs (push) Successful in 17s
ci / check (push) Successful in 27s
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
2026-10-27 23:07:22 -07:00
..

migrate

go.auvem.com/corestack/migrate sets the house configuration for Goose's Provider API. 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. For timestamp-to-sequential reconciliation before deployment, use its fix command as described in 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 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:

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 and SQL migration docs.

For Go functions, use Goose's provider-local registration: 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:

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.