Help us shape the future of Go! Take the 2026 Go Developer Survey

migratekit

package
v1.0.14 Latest Latest
Warning

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

Go to latest
Published: Nov 2, 2026 License: Apache-2.0 Imports: 26 Imported by: 0

Documentation

Overview

Package migratekit is the invariant half of a forge project's `db migrate` command tree: opening a migrator over an embedded migration set, closing it, and reporting what the schema is doing.

WHAT IS DELIBERATELY NOT HERE: policy. Whether a dirty schema aborts the run or is force-cleared, whether "nothing to apply" exits 0 or non-zero — those differ per deployment, so they stay in the project's own cmd/<bin>/cmd/db.go where an operator can read and change them.

WHAT IS DELIBERATELY NOT HERE EITHER: moving DOWN. Forge rolls forward only, so the Migrator can apply pending migrations and nothing else — there is no Steps(-n), Down() or Migrate(<lower version>). A down script claims to undo a release and cannot account for what the release already wrote; the recovery is a new forward migration. This package only reports FACTS (Up.Changed, State.Applied, State.Dirty) and never decides what a fact means. That split is the whole design: a helper that returned an error for a dirty schema would have made "auto-force in staging" unexpressible without dropping the library.

WHY THIS IS NOT IN serverkit. serverkit is imported by every forge server, and it is deliberately free of any compile-time golang-migrate dependency — serverkit.AutoMigrate takes a `func(*sql.DB, *slog.Logger) error` for exactly that reason. `db migrate` is a CLI concern, not a serving concern, so putting the migrator here keeps golang-migrate out of the link graph of every server that never runs a migration.

THE MIGRATION SOURCE IS AN fs.FS, NEVER A DIRECTORY PATH. A production image carries the binary and nothing else — the runtime stage copies /app/<project> and no db/ tree — so a migrator reading `file://db/migrations` can only ever fail there, and a deploy-time migration step that is guaranteed to fail is worse than no step at all. Taking an fs.FS means the caller hands over its `//go:embed migrations/*.sql` FS and the same command works in a checkout and inside the image.

Index

Constants

View Source
const (
	// DefaultLockTimeout bounds how long one attempt at a migration waits for
	// a lock. Long enough to ride out an ordinary short transaction, short
	// enough that the pile-up it causes behind the waiting DDL is not an
	// outage.
	DefaultLockTimeout = 5 * time.Second
	// DefaultStatementTimeout bounds one statement. It is a backstop against
	// a runaway statement, not a budget for the work: generous so a real
	// backfill is not cut off, and overridable per file
	// (`-- forge:statement-timeout`) when one needs hours.
	DefaultStatementTimeout = 30 * time.Minute
	// DefaultLockAttempts is how many times a migration is tried when its
	// lock times out, the first try included.
	DefaultLockAttempts = 5
	// DefaultRetryBackoff is the wait before the second attempt; each later
	// wait doubles, up to retryBackoffMax.
	DefaultRetryBackoff = time.Second
)

Defaults applied when Limits leaves a field zero.

View Source
const (
	// SchemePostgres is the standard postgres:// URL scheme.
	SchemePostgres = "postgres://"
	// SchemePostgresql is the equally standard postgresql:// spelling.
	SchemePostgresql = "postgresql://"
	// SchemePGX5 is what golang-migrate's pgx v5 database driver
	// registers itself as, and therefore the only scheme it will answer
	// to. A DATABASE_URL that a human or a cloud provider wrote uses one
	// of the two above, never this one.
	SchemePGX5 = "pgx5://"
)

Schemes the DSN normalizer understands. Exported so a caller (or a test) can assert over the same set the rewrite is driven by, rather than restating the string literals.

View Source
const DefaultAwaitTimeout = 5 * time.Minute

DefaultAwaitTimeout is how long AwaitSchema waits for another process to bring the schema current when AwaitOptions.Timeout is zero.

View Source
const ForceCommand = "forge db migrate force"

ForceCommand is the forge command that clears a dirty schema, named by AutoMigrate's dirty refusal. It is Migrator.Force over a checkout's migrations. Spelled out rather than imported for the reason RebaseCommand is, and a test in internal/cli asserts it resolves to a real command.

The refusal names it beside the binary's own `db migrate force`, which a scaffolded binary carries: AutoMigrate runs inside a binary whose name and command tree it cannot know, so that one can only be described.

View Source
const GrantsFile = "grants/readonly_gen.sql"

GrantsFile is the grants script's path relative to the directory that holds the migrations' directory — "grants/readonly_gen.sql" beside "migrations/", which is where a project's embed puts it.

View Source
const GrantsRowsDir = "grants/readonly"

GrantsRowsDir holds the script's per-table rows, relative to the same directory as GrantsFile: one file per table (pkg/dbgrants.Files), spliced into the script in file-name order.

View Source
const NewMigrationCommand = "forge db migration new"

NewMigrationCommand is the forge command that creates a migration, named by the refusal's fix for a file that has already merged. Spelled out here for the same reason as RebaseCommand, and held to a real command by the same internal/cli test.

View Source
const Off time.Duration = -1

Off, as Limits.LockTimeout or StatementTimeout, disables that timeout. (Zero means "the default", so it cannot also mean "none".)

View Source
const RebaseCommand = "forge db migration rebase"

RebaseCommand is the forge command that re-versions a migration, named by this package's refusal message.

It is spelled out here rather than imported because pkg/ may not depend on forge's internal packages — this error travels into user projects, which link only pkg. The internal copy lives in internal/linter/migrationlint.RebaseCommand, and a test in internal/cli asserts that both spellings resolve to a real CLI command. That test is the reason for this comment: this string was advice for a subcommand that did not exist, and a user following it hit "unknown command".

Variables

This section is empty.

Functions

func AutoMigrate

func AutoMigrate(fsys fs.FS, dir string) func(*sql.DB, *slog.Logger) error

AutoMigrate is the BOOT-TIME half of the migration story, the counterpart to the Migrator the `db migrate` CLI drives.

WHY IT TAKES AN OPEN *sql.DB WHILE Open TAKES A DSN. The two callers have genuinely different handles, and converting between them is the failure mode this signature avoids. The server has already opened its pool by the time it migrates — serverkit.AutoMigrate hands that exact *sql.DB over — so asking for a DSN here would open a SECOND connection to the same database during boot, one that bypasses the pool tuning already applied. The CLI, by contrast, has no pool and only a URL. So the migrator that runs beside a live pool binds to the pool, and the one that runs alone opens its own.

It returns a CLOSURE rather than running immediately because serverkit.AutoMigrate takes a `func(*sql.DB, *slog.Logger) error` — that indirection is what keeps golang-migrate out of serverkit's link graph, so a server that never migrates does not pay for the driver. Binding the FS here and the database there is what lets the project's call site be a single expression naming its embedded migrations.

The dir argument is the path INSIDE fsys holding the .sql files ("migrations" for the conventional `//go:embed migrations/*.sql` layout).

A nil fsys SKIPS, and this is the one place migratekit is quieter than Open, which refuses it. The two are answering different questions. `db migrate up` is an explicit instruction, so a binary that embeds nothing has failed to do what was asked. Boot is not: AutoMigrate runs on every startup of a project that may simply not have written its first migration yet, and failing there would make a brand-new project unable to start. Both are the behaviour the binary's user is entitled to expect from the command they ran.

Every OTHER failure mode is LOUD. An unreadable embedded FS, an unreadable schema_migrations table, or a schema left dirty by a half-applied migration each abort startup rather than serve traffic against a database in an unknown state.

It is Apply, logged: one line per fact the run reports (an adoption and a schema ahead at WARN). A caller that wants the facts as values — a `db migrate up` command printing what it did — calls Apply.

func AutoMigrateWith added in v1.0.7

func AutoMigrateWith(fsys fs.FS, dir string, limits Limits) func(*sql.DB, *slog.Logger) error

AutoMigrateWith is AutoMigrate with the run's lock and statement timeouts and retry set (locktimeout.go). Its retries are logged on the logger it is handed.

func AwaitMigrations added in v1.0.0

func AwaitMigrations(fsys fs.FS, dir string, opts AwaitOptions) func(context.Context, *sql.DB, *slog.Logger) error

AwaitMigrations is AwaitSchema bound to a binary's embedded migrations and logged, in the closure shape serverkit.AwaitMigrationsContext takes — the waiter's counterpart to AutoMigrate:

serverkit.AwaitMigrationsContext(ctx, skCfg, logger,
	migratekit.AwaitMigrations(forgedb.Source(), forgedb.SourceDir, migratekit.AwaitOptions{}))

func AwaitSchema added in v1.0.0

func AwaitSchema(ctx context.Context, db *sql.DB, fsys fs.FS, dir string, opts AwaitOptions) error

AwaitSchema blocks until db's schema is current for the migrations in fsys (dir inside it): clean, and at or past the newest version embedded. Past it is the previous release meeting the next one's schema mid deploy, which a waiter may always serve — the same rule an applier's schema-ahead check enforces for incompatible migrations is the applier's to enforce.

golang-migrate applies in order, and forge's migrator refuses to pass a version it never applied, so the recorded version is the whole answer. A dirty version is not a failure here: the migrator marks the version dirty while it runs and clears it on commit, so a waiter sees it on every healthy migration.

It returns nil at once when the binary embeds no migrations, ctx's error when ctx ends first, and a *SchemaPendingError when opts.Timeout passes.

func NormalizeDSN

func NormalizeDSN(dsn string) string

NormalizeDSN rewrites a standard postgres:// or postgresql:// URL to the pgx5:// scheme golang-migrate's pgx v5 driver registers under.

It exists because the SAME DSN has to work for two different consumers: the application's own pool (database/sql + the pgx stdlib driver, which wants postgres://) and golang-migrate's URL constructor (migrate.New, which resolves its database driver by URL scheme). Rather than ask an operator to set a second DATABASE_URL in a scheme they have never seen, the one they wrote is translated here. Open itself no longer goes through that constructor — it would open a second session (session.go) — but accepts either spelling.

A DSN in any other scheme is returned unchanged: this is a translation for a known pair, not a validator. An unusable DSN produces a loud driver error at Open, which is a better report than a silent rewrite.

Types

type Adopted

type Adopted struct {
	// Tool is the migration tool whose history was adopted (compat.ToolGoose,
	// or compat.ToolForge for a squashed forge history).
	Tool string
	// Version is the baseline's version, now the schema's.
	Version uint
	// Name is the baseline's filename.
	Name string
	// Versions is how many of the tool's versions were verified applied.
	Versions int
}

Adopted reports a database this call adopted: the baseline was recorded as applied and none of its SQL ran.

func (*Adopted) String

func (a *Adopted) String() string

String is the line a command prints for an adoption.

type AdoptionError

type AdoptionError struct {
	// Tool is the tool that migrated the database (compat.ToolGoose).
	Tool string
	// Table is the tool's version table that was read.
	Table string
	// Baseline is the migration that declares the adoption.
	Baseline string
	// Declared is how many versions the baseline lists.
	Declared int
	// Missing is every listed version the database never applied: it is
	// BEHIND the baseline.
	Missing []int64
	// Unexpected is every version the database applied that the baseline
	// does not list.
	Unexpected []int64
}

AdoptionError is a database another tool migrated whose recorded history is not the history the baseline replaces. Nothing was applied.

It is a type so a caller (and a test) can tell "this database is not the schema the baseline describes" from every other migration failure. The two lists call for different fixes, and the message names both.

func (*AdoptionError) Error

func (e *AdoptionError) Error() string

type Ahead

type Ahead struct {
	// Version is the schema version recorded in the database.
	Version uint
	// Latest is the newest migration version this binary embeds.
	Latest uint
	// Versions is every version the database has that this binary does not
	// embed, in order. Each was declared backward-compatible.
	Versions []uint
}

Ahead describes a schema one or more versions past the newest migration this binary embeds, where every such version was declared backward-compatible. It is a SUCCESS state — the binary's code can run — and it is reported, not swallowed, because "the database is ahead of the code" is the single most important thing an operator mid-deploy needs to see confirmed.

type AwaitOptions added in v1.0.0

type AwaitOptions struct {
	// Timeout bounds the wait; zero is DefaultAwaitTimeout. A waiter that
	// never sees its schema fails loudly instead of hanging in boot.
	Timeout time.Duration
	// Poll is how often the version is re-read; zero is one second.
	Poll time.Duration
}

AwaitOptions tunes AwaitSchema. The zero value is the default.

type DashboardPrivilegeError added in v1.0.14

type DashboardPrivilegeError struct {
	Role       string
	Violations []dbgrants.Violation
}

DashboardPrivilegeError fails a run after which the dashboard role holds more than read privileges.

func (*DashboardPrivilegeError) Error added in v1.0.14

func (e *DashboardPrivilegeError) Error() string

type DashboardRole added in v1.0.14

type DashboardRole struct {
	// Created is true when the role did not exist and this run created it,
	// NOLOGIN.
	Created bool
	// Missing is true when the role does not exist and the migrating role may
	// not create it. No migration this run applied names it.
	Missing bool
	// CanLogin is true when dashboards are on: the deploy made the role
	// login-capable. Off, its grants stay in place and nobody can use them.
	CanLogin bool
	// SecurityDefiner lists the SECURITY DEFINER functions the role can
	// execute (dbgrants.SecurityDefinerExposure), whether or not it can log in
	// now: each runs as its owner, so it can read past the column grants and
	// write. A warning for the caller to log, never a failure.
	SecurityDefiner []string
}

DashboardRole reports what a run found of the dashboard role. Nil on a Result means the run did not get that far (the schema was ahead).

type DashboardRoleMissingError added in v1.0.14

type DashboardRoleMissingError struct {
	Role string
	// Migrations are the pending files that name the role.
	Migrations []string
}

DashboardRoleMissingError refuses a run whose pending migrations name the dashboard role while the database has none and the migrating role may not create it. Nothing was applied.

func (*DashboardRoleMissingError) Error added in v1.0.14

func (e *DashboardRoleMissingError) Error() string

type ForeignHistoryError

type ForeignHistoryError struct {
	// Version is the schema version recorded in the database.
	Version uint
	// First and Latest name the migrations this binary embeds.
	First  string
	Latest uint
	// Embedded is how many migrations this binary embeds.
	Embedded int
}

ForeignHistoryError is a database built by migrations this binary does not carry: its schema is at a version this binary does not embed, and it recorded none of this binary's migrations as applied. Nothing was applied.

It is what a database the pre-squash history built meets when the squashed baseline does not declare that history. It used to surface as whichever check ran first, and both misled: "AHEAD of the newest migration this binary embeds" for a baseline numbered above the history, and "MISSING" (re-version the baseline) for one numbered below it.

func (*ForeignHistoryError) Error

func (e *ForeignHistoryError) Error() string

type Grants added in v1.0.0

type Grants struct {
	// Applied is true when the script ran.
	Applied bool
	// RoleMissing is true when the database has no [dbgrants.RoleName], so
	// the script was not run. That is the normal state of a database nobody
	// provisioned the role for, not a fault.
	RoleMissing bool
	// SecurityDefiner lists the SECURITY DEFINER functions the role can
	// execute (dbgrants.SecurityDefinerExposure): a way around its column
	// grants. A warning for the caller to log, never a failure.
	SecurityDefiner []string
}

Grants reports what a run did with the read-only grants. Nil on a Result means the project embeds none.

type Limits added in v1.0.7

type Limits struct {
	// LockTimeout is lock_timeout for each attempt at a migration. Zero is
	// DefaultLockTimeout; Off disables it.
	LockTimeout time.Duration
	// StatementTimeout is statement_timeout. Zero is
	// DefaultStatementTimeout; Off disables it.
	StatementTimeout time.Duration
	// LockAttempts is how many times a migration whose lock timed out is
	// tried, the first included. Zero is DefaultLockAttempts; 1 never
	// retries.
	LockAttempts int
	// RetryBackoff is the wait before the second attempt, doubling after
	// each (with jitter, so replicas do not retry in step). Zero is
	// DefaultRetryBackoff.
	RetryBackoff time.Duration
}

Limits is how a run guards the locks and the time its migrations take. The zero value is the defaults above. A migration file overrides the two timeouts for itself with `-- forge:lock-timeout` and `-- forge:statement-timeout` (compat.ParseTimeouts).

func ParseLimits added in v1.0.7

func ParseLimits(lockTimeout, statementTimeout string, attempts int) (Limits, error)

ParseLimits builds Limits from the spellings a command line or an environment variable carries: durations as compat.ParseTimeout reads them (`30s`, `5m`, `off`), "" meaning the default, and attempts 0 meaning the default.

type LockTimeoutError added in v1.0.7

type LockTimeoutError struct {
	Version uint
	Name    string
	// Grants is true when the statement that timed out was a grants script
	// (grants.go) and not a migration. The run's migrations are committed by
	// then, and the next run applies the grants again.
	Grants bool
	// PartlyApplied is true when the migration could not be proven
	// all-or-nothing (it controls its own transaction, runs statement by
	// statement, or builds something concurrently the migrator cannot clean
	// up). It was not retried, and part of it may be committed: the schema is
	// left dirty at Version for a human to inspect.
	PartlyApplied bool
	Attempts      int
	Timeout       time.Duration
	// Leftover names the INVALID indexes a CONCURRENTLY attempt left behind
	// and that could not be dropped while the blocker still ran (dropping
	// one waits for the same sessions the build did). They do no harm beyond
	// their write cost, and the next run drops them before it builds again.
	Leftover []string
	// Err is the last timeout, as postgres reported it. It is the bare
	// postgres error, never a golang-migrate database.Error: the driver
	// wrapper above this one would rewrap that and lose this type.
	Err error
}

LockTimeoutError is a migration that could not get its lock in any of its attempts. Nothing of the migration was applied, and the schema is back at the version before it, clean, so running again simply tries the migration again.

func (*LockTimeoutError) Error added in v1.0.7

func (e *LockTimeoutError) Error() string

func (*LockTimeoutError) Unwrap added in v1.0.7

func (e *LockTimeoutError) Unwrap() error

type MigrationMismatch

type MigrationMismatch struct {
	Version uint
	// Applied is the descriptor recorded when the version was applied.
	Applied string
	// Embedded is the descriptor this binary carries for the same version.
	Embedded string
}

MigrationMismatch is one version whose applied file is not the file this binary embeds under that number.

type MigrationMismatchError

type MigrationMismatchError struct {
	Mismatches []MigrationMismatch
}

MigrationMismatchError reports versions the database applied from a different migration file than the one this binary embeds under the same number. Nothing was applied.

It is a type so a caller (and a test) can tell "the schema is not the one these version numbers describe" from every other migration failure: the fix is a human reconciling the schema, never a retry.

func (*MigrationMismatchError) Error

func (e *MigrationMismatchError) Error() string

type Migrator

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

Migrator is an open migration session — ONE Postgres session, from Open to Close, whatever it does in between (runlock.go, ONE SESSION). Always Close it.

func Open

func Open(opts Options) (*Migrator, error)

Open builds a Migrator over the embedded migration set in opts.FS, on one session to opts.DSN that it holds until Close.

Both failure modes are LOUD and distinguishable, because they call for different fixes: a nil FS means the binary shipped without migrations (regenerate), while an unusable DSN means the database is unreachable or misaddressed (fix the environment).

Open takes the run lock while it builds golang-migrate's driver, so behind a running migration it waits — on its one session, which is all it holds.

func (*Migrator) Close

func (mg *Migrator) Close() error

Close releases the migration source and the session, and closes the pool the session came from when Open made it.

golang-migrate reports the source and the session independently and either can fail; every error is joined into one so a caller cannot report one and silently drop the other. Safe on a zero Migrator.

func (*Migrator) Force

func (mg *Migrator) Force(version int) error

Force sets the recorded version and CLEARS the dirty flag without running any SQL.

It is here so that "auto-force and retry" is expressible as a deployment policy in the caller's own db.go — the only alternative being that the project imports golang-migrate directly and this package stops being the boundary. It does not make forcing safe: the schema is whatever the failed migration left behind, and asserting a version does not change that.

It keeps the applied record (applied.go) saying what the version now says: the versions the force newly asserts are recorded under this binary's files, and rows above the forced version are dropped (forceApplied). Without that, the recovery Force exists for — repair a migration that failed part-way, force its version, run Up — was refused, because Up found the forced version passed and never recorded, and reported it MISSING.

It runs under the run lock, so it never interleaves with a replica's Up or a server's AutoMigrate. The record and the version are both written while the session holds appliedLockKey, as adopt does, so no reader sees the two disagree.

The record is committed FIRST, then the version. A crash between the two leaves the record ahead of a version that is still the old one, and running the same Force again finishes the job: forceApplied is idempotent. The reverse order left the forced version with no record, which Up refuses as MISSING, and re-running the Force did not repair it: the version it starts from already claims the forced one, so it records nothing new.

func (*Migrator) State

func (mg *Migrator) State() (State, error)

State reads the current schema state, folding golang-migrate's ErrNilVersion sentinel into State.Applied=false. A returned error therefore always means the version table is genuinely unreadable.

func (*Migrator) Up

func (mg *Migrator) Up() (res Result, err error)

Up applies every pending migration.

golang-migrate's ErrNoChange is folded into Result.Changed=false rather than surfaced as an error: on a concurrent rollout it is the EXPECTED outcome for every replica that lost the advisory-lock race, so treating it as failure would fail most of a healthy deploy. A caller that genuinely wants "fail if nothing was pending" has Changed to test.

A schema AHEAD of this binary (the database is at a version it does not embed — what rolling the app back produces) is classified before anything runs: a Result with Ahead set when every unknown version was declared backward-compatible, a *SchemaAheadError when not. golang-migrate alone fails both with "no migration found for version N".

Before either, every version the database recorded as applied is checked against the file this binary embeds under that number; a different file is a *MigrationMismatchError and nothing runs. After applying, Up records which file each new version was. See applied.go.

Up also records this binary's compatibility declarations, which is what lets an OLDER binary (at boot via AutoMigrate, or here) reach that verdict. See ahead.go.

First of all, a database another tool migrated — or a forge history that `forge db squash` replaced — is ADOPTED when the embedded baseline declares that history (`-- forge:adopts <tool> <versions>`): the baseline is recorded as applied without running, and everything after it applies as usual. A database whose history is not the declared one is an *AdoptionError and nothing runs. See adopt.go. A database a forge history this binary does not carry built, with no declaration to adopt it by, is a *ForeignHistoryError (adopt_forge.go).

The whole run holds migratekit's run lock, shared with AutoMigrate, so replicas racing a release through either entry point take turns, on the Migrator's one session: a run waiting its turn holds that session and nothing else. See runlock.go.

On error the Result still says what the run did before it failed: Before, and Adopted when an adoption committed — a later refusal does not undo it.

type MissingMigration

type MissingMigration struct {
	Version uint
	// Name is the migration's filename, so the fix names a real file.
	Name string
}

MissingMigration is one version this binary embeds that the database skipped: at or below the schema's version, with no applied row.

type MissingMigrationError

type MissingMigrationError struct {
	Missing []MissingMigration
	// Version is the schema version the database has reached.
	Version uint
}

MissingMigrationError reports migrations that can never run: their version is at or below the schema's current version, so golang-migrate will not look at them again. Nothing was applied.

It is a type so a caller (and a test) can tell "this release is carrying SQL that will never execute" from every other migration failure. The fix is always to give the migration a version NEWER than the schema has reached, never a retry.

func (*MissingMigrationError) Error

func (e *MissingMigrationError) Error() string

type Options

type Options struct {
	// FS holds the migration files — typically the project's embedded
	// `forgedb.MigrationsFS`. A nil FS is a hard error: it means the
	// binary embeds no migrations, and a migrate command that exits 0
	// against an unmigrated database is precisely the silence this
	// package exists to remove.
	FS fs.FS
	// Dir is the directory INSIDE FS holding the .sql files — "migrations"
	// for the conventional `//go:embed migrations/*.sql` layout. Empty
	// means the FS root.
	Dir string
	// DSN is the database URL. Passed through NormalizeDSN, so callers
	// hand over the DATABASE_URL they already have.
	DSN string
	// Run says how the run guards the locks and the time its migrations take
	// (locktimeout.go). The zero value is the defaults.
	Run RunOptions
}

Options configures Open.

type Result

type Result struct {
	// Changed reports whether THIS call applied anything.
	//
	// It is taken from the return of the migrate call itself, not from a
	// before/after version comparison, because the advisory lock is taken
	// INSIDE that call: when several replicas race the same release, one
	// applies and the others are released to find nothing to do. Only the
	// call's own return distinguishes "I applied it" from "someone else
	// did"; a version diff would credit every replica.
	Changed bool
	// Before is the state as of entry, After the state on exit. Both are
	// reported so a caller can log a "N -> M" line without a second round
	// trip, and so "no change" still carries the version it is at.
	Before State
	After  State
	// Ahead is set when the schema was AHEAD of this binary — the previous
	// release meeting the next one's schema mid-deploy — and every version it
	// does not embed was declared
	// backward-compatible. Nothing was applied (Changed is false); the
	// binary's code can run. A caller should say so loudly: it is the fact
	// an operator mid-deploy most needs confirmed. A schema ahead that is
	// NOT proven compatible is a *SchemaAheadError instead, never this.
	Ahead *Ahead
	// Adopted is set when THIS call adopted a database another tool
	// migrated: the baseline that declares that history was recorded as
	// applied without running its SQL (see adopt.go). Before is then the
	// state after adoption, so Changed and Before → After describe only the
	// migrations that ran on top of it. A caller should say so: it is the
	// one run in a database's life where the version moved and no SQL did.
	Adopted *Adopted
	// NothingEmbedded is set by Apply when the binary embeds no migrations
	// (a nil FS, or a directory with no .sql in it): a project that has not
	// written its first one. Nothing was read or applied. Open refuses that
	// instead; see AutoMigrate.
	NothingEmbedded bool
	// Grants is what the run did with the read-only role's grants (grants.go):
	// nil when the binary embeds none, or when the schema was ahead of it.
	Grants *Grants
	// Dashboard is what the run found of the dashboard role (dashboard.go):
	// nil when the schema was ahead of the binary.
	Dashboard *DashboardRole
}

Result describes what a migration call actually did.

func Apply

func Apply(ctx context.Context, db *sql.DB, fsys fs.FS, dir string) (Result, error)

Apply is AutoMigrate's run, returning what it did: every pending migration in fsys applied over ONE connection borrowed from db and given back, with the run lock taken on that connection — a run waiting its turn holds it and nothing else (runlock.go, ONE SESSION).

Its policy is AutoMigrate's: a binary embedding no migrations is Result.NothingEmbedded, not an error, and a schema left dirty is refused before anything runs, naming the way out. Everything else is Migrator.Up's run: adoption, the applied record's refusals, the schema-ahead verdict (Result.Ahead, or a *SchemaAheadError), and Result.Changed folding a replica that lost the race into "nothing to do". As with Up, on error the Result still reports Before and any adoption that committed.

A caller holding only a DSN — a `db migrate up` command — opens a pool, caps it at one connection, and hands it here: one session, and the boot path's verdicts.

func ApplyWith added in v1.0.7

func ApplyWith(ctx context.Context, db *sql.DB, fsys fs.FS, dir string, run RunOptions) (Result, error)

ApplyWith is Apply with the run's lock and statement timeouts and retry set (locktimeout.go).

type RunOptions added in v1.0.7

type RunOptions struct {
	Limits Limits
	// Logger receives one WARN line per attempt whose lock timed out. Nil is
	// slog.Default().
	Logger *slog.Logger
}

RunOptions is what a run takes beyond its migrations and its database.

type SchemaAheadError

type SchemaAheadError struct {
	// Version is the schema version recorded in the database.
	Version uint
	// Latest is the newest migration version this binary embeds.
	Latest uint
	// Incompatible is every version this binary does not embed that is not
	// recorded as backward-compatible — either declared otherwise, or
	// applied by a migrator that recorded nothing.
	Incompatible []uint
	// Unrecorded is true when the database holds no compatibility record
	// for the schema's own version: it was migrated by a binary that
	// predates the record, so nothing about the extra versions is known.
	Unrecorded bool
}

SchemaAheadError is a schema ahead of the binary that the binary CANNOT prove it is compatible with. Nothing was applied.

It is a type, not a string, so a caller's policy (and a test) can tell "old code meeting a schema change nobody declared safe" apart from every other migration failure.

func (*SchemaAheadError) Error

func (e *SchemaAheadError) Error() string

type SchemaPendingError added in v1.0.0

type SchemaPendingError struct {
	// Want is the newest migration version the binary embeds.
	Want uint
	// State is what the schema reported last.
	State State
	// Waited is how long AwaitSchema waited.
	Waited time.Duration
}

SchemaPendingError is AwaitSchema giving up: the schema is not yet at the version this binary embeds.

func (*SchemaPendingError) Error added in v1.0.0

func (e *SchemaPendingError) Error() string

type State

type State struct {
	// Version is the migration version recorded in the schema. Only
	// meaningful when Applied is true.
	Version uint
	// Dirty means a previous migration failed PARTWAY: the version was
	// recorded but its SQL did not finish. migratekit reports it and
	// stops there — whether that aborts the run or is force-cleared is
	// the caller's policy.
	Dirty bool
	// Applied reports whether any migration has ever been applied. False
	// is a normal, healthy state for a fresh database, which is why it is
	// a field rather than an error: golang-migrate signals it as
	// ErrNilVersion, and a caller that forgot to special-case that
	// sentinel would treat an empty database as broken.
	Applied bool
}

State is what the schema currently reports.

Directories

Path Synopsis
Package compat reads the contracts a migration declares about itself: that it is BACKWARD-COMPATIBLE, or deliberately BREAKS ROLLBACK (this file), and which foreign migration history a baseline ADOPTS (adopt.go).
Package compat reads the contracts a migration declares about itself: that it is BACKWARD-COMPATIBLE, or deliberately BREAKS ROLLBACK (this file), and which foreign migration history a baseline ADOPTS (adopt.go).

Jump to

Keyboard shortcuts

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