Files
go/migrate

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.

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.

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.

-- +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:

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.