migrate

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Jun 8, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package migrate is part of the GoFastr framework. See https://github.com/DonaldMurillo/gofastr for documentation.

Index

Constants

View Source
const AdvisoryLockKey int64 = 6724469554113028193

AdvisoryLockKey is the fixed 64-bit key used for the migration advisory lock on Postgres. It MUST stay stable across releases: during a rolling deploy an old and a new instance both try to migrate, and they only mutually exclude if they agree on this key. Changing it would let two instances run DDL concurrently — the exact race the lock exists to prevent.

The value is arbitrary but fixed (derived once from "gofastr.migrate" and frozen). Hosts that run unrelated migration tooling against the same database can override it via WithAdvisoryLockKey to avoid cross-tool contention.

Variables

View Source
var ErrDirty = errors.New("migrate: database is in a dirty state from a failed migration — reconcile manually and call Force")

ErrDirty is returned by Up and Down when the tracking table records a migration left in a dirty state by a previously failed no-transaction migration. The database may be partially migrated; an operator must reconcile it by hand and then call Force to clear the state before migrations can proceed again.

Functions

func EnsureDatabase

func EnsureDatabase(driver, dsn string) (bool, error)

EnsureDatabase creates the target database if it does not already exist, returning whether it had to create it. This is the "first migration can create the DB" capability — call it before running migrations.

  • SQLite (and any file/embedded driver): a no-op. The database file is created automatically when the runner opens it, so there is nothing to do; returns (false, nil).
  • Postgres: connects to the maintenance `postgres` database (you cannot CREATE a database while connected to the one being created), checks pg_database, and issues CREATE DATABASE when absent. Requires the driver to be linked into the binary and the role to have CREATEDB.

dsn is the normal target DSN (URL form `postgres://…/dbname` or keyword form `host=… dbname=…`); EnsureDatabase derives the admin DSN from it.

func WithAdvisoryLock

func WithAdvisoryLock(ctx context.Context, db *sql.DB, dialect Dialect, fn func(conn *sql.Conn) error) error

WithAdvisoryLock runs fn while holding a database-level lock that serializes migration across every process pointed at the same database. fn receives the pinned *sql.Conn that holds the lock and MUST do all of its work on that connection — running the migration on the same session as the lock is what keeps the whole thing correct on a single-connection pool (MaxOpenConns(1)), which a separate lock connection would deadlock.

  • Postgres: a session-level advisory lock on the pinned connection, acquired via pg_try_advisory_lock in a ctx-aware poll loop. A second instance waits until the first releases, or returns promptly if its ctx is cancelled. (A poll loop is used rather than the blocking pg_advisory_lock because lib/pq does not interrupt a blocked pg_advisory_lock on context cancellation — a stuck holder would otherwise hang boot forever.) This is the guard that makes auto-migrate-on-boot safe across N replicas.
  • SQLite: no lock is taken (SQLite serializes writers at the file level and the DDL we emit is idempotent), but fn still gets a pinned connection so callers have one uniform code path.

db == nil runs fn(nil) — callers already treat a nil db as a no-op.

func WithAdvisoryLockKey

func WithAdvisoryLockKey(ctx context.Context, db *sql.DB, dialect Dialect, key int64, fn func(conn *sql.Conn) error) error

WithAdvisoryLockKey is WithAdvisoryLock with an explicit lock key, for hosts that need to namespace the lock away from other migration tooling sharing the database.

Types

type ChecksumMismatchError

type ChecksumMismatchError struct {
	Version  uint64
	Name     string
	Recorded string
	Current  string
}

ChecksumMismatchError is returned by Up when an already-applied migration's recorded checksum no longer matches the registered migration's Up SQL. That means the migration file was edited after it was applied — a drift the runner refuses to paper over, because the live schema no longer matches what the (edited) file says it should be.

func (*ChecksumMismatchError) Error

func (e *ChecksumMismatchError) Error() string

type Dialect

type Dialect string

Dialect represents the SQL dialect to use for migration queries.

const (
	DialectPostgres Dialect = "postgres"
	DialectSQLite   Dialect = "sqlite3"
)

type Migration

type Migration struct {
	Version uint64
	Name    string
	Up      string // SQL to apply the migration
	Down    string // SQL to roll back the migration

	// NoTransaction runs Up/Down WITHOUT wrapping them in a transaction. The
	// escape hatch for statements that cannot run inside a transaction —
	// CREATE INDEX CONCURRENTLY, VACUUM, CREATE DATABASE on Postgres. The cost
	// is that a failure mid-statement leaves the database partially migrated:
	// the runner records the migration dirty before running it and only clears
	// that flag on success, so a later run refuses to proceed (ErrDirty) until
	// an operator reconciles and calls Force. Set via the `-- +migrate
	// NoTransaction` directive in a SQL migration file.
	NoTransaction bool
}

Migration represents a single versioned database migration.

type MigrationRecord

type MigrationRecord struct {
	Version   uint64
	Name      string
	AppliedAt time.Time
	Checksum  string // SHA-256 of the Up SQL recorded at apply time
	Dirty     bool   // true if a no-transaction migration failed mid-apply
}

MigrationRecord is a row in the _migrations tracking table.

type Migrator

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

Migrator manages database migrations.

func New

func New(db *sql.DB, opts ...Option) *Migrator

New creates a new Migrator with the given database connection and options. By default the tracking table is "_migrations".

func (*Migrator) CreateMigrationsTable

func (m *Migrator) CreateMigrationsTable(ctx context.Context) error

CreateMigrationsTable ensures the migrations tracking table exists. Public entry point (used by Status and tests); runs on the pool.

func (*Migrator) Down

func (m *Migrator) Down(ctx context.Context, n int) error

Down rolls back the last n applied migrations in reverse version order. Serialized by the same advisory lock as Up.

func (*Migrator) Force

func (m *Migrator) Force(ctx context.Context, version uint64, applied bool) error

Force reconciles the tracking table by hand, the recovery path out of a dirty state and the way to adopt an existing database (baseline).

  • applied == true: mark `version` as cleanly applied without running its Up SQL. Inserts the row if missing (baseline), and clears any dirty flag on it. If the version is registered, its name and checksum are recorded so future drift checks line up.
  • applied == false: remove `version` from the tracking table entirely, so it is treated as pending again (e.g. a no-transaction migration that did not actually take effect).

Either way the dirty state for that version is cleared, unblocking Up/Down.

func (*Migrator) Register

func (m *Migrator) Register(mig Migration)

Register adds a migration. Migrations are kept sorted by Version.

func (*Migrator) RegisterFromReader

func (m *Migrator) RegisterFromReader(r io.Reader) error

RegisterFromReader parses migration SQL from a reader and registers it. The expected format uses comment directives:

-- +migrate Version <n>
-- +migrate Name <description>
-- +migrate Up
<up SQL statements>
-- +migrate Down
<down SQL statements>

func (*Migrator) Status

func (m *Migrator) Status(ctx context.Context) (*Status, error)

Status returns the current migration state: which are applied and which are pending.

func (*Migrator) Up

func (m *Migrator) Up(ctx context.Context) error

Up runs all pending migrations in version order. Each migration executes in its own transaction. Already-applied migrations are skipped. The whole run is serialized by a database advisory lock so two instances (e.g. rolling- deploy replicas) cannot apply migrations concurrently.

type Option

type Option func(*Migrator)

Option configures a Migrator.

func WithDialect

func WithDialect(d Dialect) Option

WithDialect sets the SQL dialect for the migrator. Defaults to DialectPostgres if not specified.

func WithTableName

func WithTableName(name string) Option

WithTableName sets a custom name for the migrations tracking table.

type Status

type Status struct {
	Applied []MigrationRecord
	Pending []Migration
}

Status holds the result of querying migration state.

Jump to

Keyboard shortcuts

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