migrate

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 30, 2026 License: AGPL-3.0 Imports: 13 Imported by: 0

Documentation

Overview

Package migrate runs database schema migrations with goose over the pgx stdlib driver (a database/sql handle, distinct from an application's pgxpool). The migration set is supplied by the caller — an fs.FS and a directory within it, via New — rather than embedded in this package, so each application embeds and owns its own migrations and this package only supplies the up/down/status/reset/up-to/down-to plumbing plus the pooler-safe connection handling.

It is built on goose's Provider API (goose.NewProvider), not the legacy package-level dispatcher (goose.RunContext). The legacy API configures goose through process-global state — one base fs.FS and one dialect, both set once via init() — so a library built on it could only ever serve one migration set per process. Provider takes its filesystem per instance and holds no global state, so a Runner has none either: two Runners over different migration sets coexist safely in one process.

goose records applied migrations in its goose_db_version table (created automatically on first run); the up/down/status/reset/up-to/down-to operations consult it to compute the delta to apply or revert.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func SchemaExists added in v0.2.0

func SchemaExists(ctx context.Context, dsn, schema string, opts ...Option) (bool, error)

SchemaExists reports whether schema exists as a Postgres schema at dsn, via a direct catalog query. Unlike every other operation in this package, it neither creates the schema nor touches goose's version-table machinery — callers use it to check what is there BEFORE deciding whether the schema-creating path (Up, or anything built with WithEnsureSchema) is appropriate. Accepts the same Option set as every other operation here — notably PoolerSafe, for a caller whose only DSN is a transaction pooler.

func WriteStatus

func WriteStatus(w io.Writer, statuses []MigrationStatus) error

WriteStatus renders statuses to w in the same table format the legacy goose dispatcher printed — through its own logger, which, despite this package's former doc comment claiming stdout, actually wrote to stderr. Callers now choose the destination explicitly; Nestova passes os.Stdout, which finally makes that documented behaviour true.

Types

type MigrationStatus

type MigrationStatus struct {
	Version   int64
	Source    string // filepath.Base of the migration file
	Applied   bool
	AppliedAt time.Time // zero value if not applied
}

MigrationStatus is one migration's applied/pending state, independent of goose's own type so goose stays out of this package's public API surface.

type NewOption added in v0.2.0

type NewOption func(*newOptions)

NewOption customizes New.

func WithEnsureSchema added in v0.2.0

func WithEnsureSchema(schema string) NewOption

WithEnsureSchema creates schema, via CREATE SCHEMA IF NOT EXISTS, on connect — before goose's Provider first touches its version table. goose itself never creates a schema, so a caller whose version table (see WithVersionTable) lives outside the database's default schema needs one guaranteed to exist first, or Provider construction fails trying to create that table against a schema that is not there yet.

func WithSessionLock added in v0.2.0

func WithSessionLock() NewOption

WithSessionLock enables a Postgres session-level advisory lock (goose's WithSessionLocker over lock.NewPostgresSessionLocker), held for the duration of each operation, so two processes racing an Up serialize instead of both applying migrations at once.

The lock is goose's lock.DefaultLockID, and Postgres advisory locks are scoped to the DATABASE rather than to a migration set — so this serializes against EVERY goose session-locked migration set in the same database, not only against another Runner over this one. That is safe but coarser than it sounds; if a set ever needs its own lock, this option has to grow a lock ID (goose's lock.WithLockID).

func WithVersionTable added in v0.2.0

func WithVersionTable(name string) NewOption

WithVersionTable sets a non-default goose version table name — optionally schema-qualified as "schema.table" — for a migration set that shares a database with other independently versioned migration sets and so cannot use goose's default goose_db_version.

type Option

type Option func(*options)

Option customizes a migration run.

func PoolerSafe

func PoolerSafe() Option

PoolerSafe configures the migration connection to use the simple query protocol so goose's version-bookkeeping queries do not rely on named server-side prepared statements, which a transaction pooler (PgBouncer / Supabase Supavisor) cannot keep across multiplexed transactions. Prefer pointing the DSN at a direct/session connection over enabling this.

type Runner

type Runner struct {
	// contains filtered or unexported fields
}

Runner applies migrations from one caller-supplied filesystem. Construct one with New; it holds no process-global state, so multiple Runners over different migration sets are safe to use concurrently in one process.

func New

func New(fsys fs.FS, dir string, opts ...NewOption) (*Runner, error)

New returns a Runner over the .sql migrations rooted at dir within fsys. dir may be "" or "." for a filesystem that is already rooted at its migrations (e.g. a //go:embed of just the migration files); any other value names a subdirectory of fsys.

New fails fast when dir holds no .sql migrations, rather than deferring that discovery to the first Up/Down/etc. call: goose's Provider globs the ROOT of the fs.FS it is given for "*.sql" — it does not walk into subdirectories — so this check mirrors exactly what every Runner method requires of the filesystem it was built from. fs.Sub does not itself fail on a missing directory, so without this check a bad dir would surface only much later, as an opaque "no migrations found" from inside goose.

func (*Runner) AppliedVersion added in v0.2.0

func (r *Runner) AppliedVersion(ctx context.Context, dsn string, opts ...Option) (int64, error)

AppliedVersion returns the highest migration version currently recorded as applied in the database — via goose's own Provider.GetDBVersion, so it reflects what the DATABASE itself has recorded rather than r's filesystem. Unlike Status (which reports one entry per migration r's filesystem knows about), a version applied by a newer binary sharing this migration set, with no corresponding source file here, still surfaces correctly: this is what a caller compares against its own highest known version to detect that case.

AppliedVersion is NOT side-effect-free, despite reading like a probe. goose's GetDBVersion goes through Provider.initialize(ctx, true), so it acquires r's session lock when r was built WithSessionLock — blocking up to goose's five-minute lock retry against a concurrent migration, then failing with an opaque lock error — and it ensures the version table exists (CREATE TABLE plus the zero-version INSERT). r's own WithEnsureSchema runs first and creates the schema. A caller probing a shared schema it must neither create nor contend for should call SchemaExists first and build its Runner WITHOUT WithSessionLock, exactly as identity/migrate.RequireVersion does and for the same reason.

func (*Runner) Down

func (r *Runner) Down(ctx context.Context, dsn string, opts ...Option) error

Down rolls back the most recently applied migration. If nothing is applied, it returns a wrapped goose.ErrNoNextVersion — unlike Reset and DownTo, Down does not treat an empty database as a no-op.

func (*Runner) DownTo

func (r *Runner) DownTo(ctx context.Context, dsn string, version int64, opts ...Option) error

DownTo rolls back migrations until only those up to and including the given goose version remain applied — the mirror of UpTo. DownTo(ctx, dsn, 24) leaves 00024 applied and rolls back everything above it.

This is the pinned-version-boundary pattern for migration tests: a test that means "roll back exactly migration N" must use DownTo(N-1), never Down. Down rolls back whatever is LATEST, so a test written against it silently starts exercising a different migration the moment another one lands on top. Pinning both boundaries keeps such a test meaningful at any future highest version.

func (*Runner) Reset

func (r *Runner) Reset(ctx context.Context, dsn string, opts ...Option) error

Reset rolls back every applied migration, via DownTo(0). Intended for tests and local resets.

Unlike the legacy dispatcher this package used to sit on, Reset needs no special case for a pristine database: goose's Provider ensures the version table (and its zero-version row) exists before reading applied versions, so DownTo(0) against an already-empty schema is a clean no-op rather than an error.

Reset is also STRICTER than the legacy behaviour about orphan versions: a database row recorded for a version with no corresponding migration file — for example, one migrated from a different branch — now fails loudly instead of being silently skipped. That is deliberate; do not read it as a bug.

func (*Runner) Status

func (r *Runner) Status(ctx context.Context, dsn string, opts ...Option) ([]MigrationStatus, error)

Status returns the applied/pending state of every migration in the Runner's filesystem, ordered by version ascending. It performs no output itself; pass the result to WriteStatus to render it.

func (*Runner) Up

func (r *Runner) Up(ctx context.Context, dsn string, opts ...Option) error

Up applies all pending migrations.

func (*Runner) UpTo

func (r *Runner) UpTo(ctx context.Context, dsn string, version int64, opts ...Option) error

UpTo applies migrations up to and including the given goose version — the migration file's numeric filename prefix, e.g. 24 for 00024_reward_catalog_admin.sql.

This exists for gated tests that need to seed data against an intermediate schema (i.e. stop applying migrations partway through) and then apply one specific migration on top of it, to prove that migration handles pre-existing rows correctly — coverage a plain Reset+Up cannot provide, since that always starts from an empty database where a backfill UPDATE trivially matches zero rows.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL