gomigr

package module
v1.0.1 Latest Latest
Warning

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

Go to latest
Published: May 15, 2026 License: MIT Imports: 12 Imported by: 0

README

gomigr

Russian / Русский

gomigr is a PostgreSQL DSL migrator written in Go. It applies and reverts migrations in topological order based on depends-on declarations rather than plain filename sorting. It ships both as a CLI binary (cmd/gomigr) and as a Go library (root package github.com/therox/gomigr).

Installation

go install github.com/therox/gomigr/cmd/gomigr@latest

The binary needs Go 1.21+. The only runtime dependencies are github.com/jackc/pgx/v5/stdlib and gopkg.in/yaml.v3.

Configuration

Sources are merged in this order (later sources override earlier ones):

  1. Built-in defaults: migrations_dir = "./migrations". There is no default DSN — it must be provided.
  2. YAML file at --config <path> (default ./configs/config.yaml). If the file is missing, this step is silently skipped.
  3. Environment variables: GOMIGR_DSN, GOMIGR_MIGRATIONS_DIR.
  4. CLI flags: --dsn, --migrations-dir.

GOMIGR_USER (read by the CLI as one of the applied_by sources) and GOMIGR_LOG_LEVEL (logger level) live outside this merge — they are not overridden by YAML and have no CLI-flag equivalents in the merge chain. --to, --steps, --all, --yes are per-command arguments for up/down, not persistent settings.

The state table name (schema_migrations) is not configurable — it is hard-coded.

Example YAML:

dsn: "postgres://user:pass@localhost:5432/app?sslmode=disable"
migrations_dir: "./migrations"

Migration file format

File name: <version>_<snake_case_description>.sql, where <version> is a sortable identifier (integer or date such as 20260601). <version> is the migration's unique identifier (UNIQUE constraint in schema_migrations); the primary key of the table is the surrogate id column described below.

-- depends-on: 20260701
-- depends-on: 20260702, 20260703

-- +migrate Up
CREATE TABLE users (...);

-- +migrate Down
DROP TABLE users;

Parsing rules:

  • -- depends-on: comments may appear 0..N times anywhere before -- +migrate Up. A single line may list several versions separated by commas.
  • Markers -- +migrate Up and -- +migrate Down are mandatory.
  • Everything between them is the Up section; everything after -- +migrate Down is the Down section. An empty Down section is allowed (logged as a warning).
  • Sections are passed to pgx as-is. Semicolons inside string literals and $$ ... $$ blocks are not split — multi-statement bodies execute within one transaction.
  • Checksum is sha256 of the normalized file content.

CLI commands

gomigr up      [--config path] [--dsn ...] [--migrations-dir path] [--user name] [--to V | --steps N]
gomigr down    [--config path] [--dsn ...] [--migrations-dir path] [--user name] (--to V | --steps N | --all [--yes])
gomigr status  [--config path] [--dsn ...] [--migrations-dir path]
gomigr create  [--config path] [--migrations-dir path] <name>
  • up without flags applies all pending migrations in topological order. --to V restricts the batch to versions <= V plus their transitive pending dependencies; transitive dependencies with version greater than V are still applied and logged as warnings. --to and --steps are mutually exclusive.

  • up --steps N applies only the first N pending migrations in topological order. Topo order guarantees dependencies come before dependents, so the first-N prefix is self-contained — no migration in the prefix can have a dependency that was cut off. N must be a positive integer; the CLI rejects an explicit --steps 0 and negative values (the library-level UpOptions{Steps: 0} still means "no limit", for symmetry with empty To). If N >= len(pending), all pending are applied.

  • down requires an explicit scope — running gomigr down without any of --to, --steps, --all exits with code 1 and the message specify --to <version>, --steps <N> or --all. The three flags are mutually exclusive: combining any two prints --to, --steps, --all are mutually exclusive and exits with code 1.

  • down --to V reverts all applied migrations whose version is > V in reverse topological order (a dependent migration is reverted before the one it depends on).

  • down --steps N reverts the N most recently applied migrations, ordered by id in schema_migrations (chronological apply order, not lexicographic version order). N must be a positive integer; the CLI rejects an explicit --steps 0 and negative values with --steps must be a positive integer. Cascade is followed: if any of the starting N has applied dependents that fall outside the starting set, those dependents are pulled in and a WARN is logged for each extra migration. The final batch is executed in reverse topological order.

  • down --all reverts every applied migration. In an interactive terminal it prints This will revert ALL applied migrations. Type 'yes' to continue: to stderr and reads a line from stdin; the operation proceeds only when the trimmed, lowercased input equals yes. Any other input aborts with exit code 1 and aborted by user. In a non-interactive context (pipe, CI), --all without --yes exits with code 1 and --all requires --yes in non-interactive mode. Passing --yes skips the prompt entirely. --yes only makes sense alongside --all; with --to or --steps it is silently ignored.

  • status prints a table of currently applied migrations in chronological apply order (sorted by id) with columns ID, VERSION, NAME, STATUS, APPLIED_BY, APPLIED_AT. ID is the sequential number of the row in schema_migrations (BIGSERIAL, hands out values in the order migrations were applied) — it does not coincide with VERSION when depends-on reordered the natural lexicographic sequence. APPLIED_AT is rendered as RFC 3339 in UTC. Example output:

    ID  VERSION   NAME                STATUS             APPLIED_BY  APPLIED_AT
    1   20260101  create_users        applied            sergey      2026-05-15T18:42:01Z
    2   20260201  create_posts        applied            sergey      2026-05-15T18:42:01Z
    3   20260301  add_audit_trigger   applied            sergey      2026-05-15T18:42:02Z
    4   20260401  seed_demo_data      checksum_mismatch  sergey      2026-05-15T18:42:02Z
    
  • create <name> writes <UTC timestamp>_<name>.sql into the migrations directory with an empty Up/Down template. <name> must match ^[a-z0-9][a-z0-9_]*$. This command does not touch the database.

applied_by resolution order:

  1. --user <name> flag
  2. GOMIGR_USER environment variable
  3. Current OS user via os/user.Current()
  4. fallback "unknown"

depends-on semantics

  • Natural order is lexicographic by version.
  • A depends-on: B declaration in migration A forces B to be applied before A, even if version(B) > version(A).
  • Cycles are reported as an error listing the cycle vertices. gomigr does not attempt to break them.
  • A dangling dependency (version listed in depends-on: but not present anywhere on disk or in schema_migrations):
    • Detected before the batch starts: gomigr prints missing dependency: <V> required by <A> to stdout, logs an error and exits with code 2 without applying anything depending on the missing one.
    • Detected mid-batch: the current migration is not applied, the same line is printed to stdout, and previously committed migrations remain in the database (no global rollback). Exit code 2.

State storage

CREATE TABLE IF NOT EXISTS schema_migrations (
  id         BIGSERIAL PRIMARY KEY,
  version    TEXT NOT NULL UNIQUE,
  name       TEXT NOT NULL,
  checksum   TEXT NOT NULL,
  status     TEXT NOT NULL,
  applied_by TEXT NOT NULL,
  applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
  • id is a BIGSERIAL surrogate key whose values reflect the chronological order in which migrations were applied.
  • version is the migration identifier from the file name. It is kept UNIQUE, which preserves the "one row per version" invariant that used to be enforced via PRIMARY KEY.
  • The status column stores applied or checksum_mismatch.
  • A pg_try_advisory_lock is held for the whole up/down run, preventing concurrent invocations against the same database (ErrLockBusy is reported if the lock is busy).
  • Each migration applies/reverts inside its own transaction together with the matching INSERT/DELETE on schema_migrations. There is no global rollback: a failure on the N-th migration leaves the first N-1 applied.
  • When the checksum of a file differs from the value stored in the table, gomigr does not stop. It logs an error and updates the row's status to checksum_mismatch, then continues with the rest of the batch.

Logging and exit codes

  • log/slog, text format. Level is taken from GOMIGR_LOG_LEVEL (debug|info|warn|error, default info).
  • Logged events: current DB version on start, applying/applied, reverting/reverted. If there is nothing to do, a single No migration line is emitted and the process exits with code 0.
  • Exit codes: 0 success, 1 user or configuration error, 2 migration execution error.

Library usage

The root package exposes a small facade. The library opens its own short-lived *sql.DB against pgx/v5/stdlib, performs the work and closes it on Close() — the host application's pool is never touched.

package main

import (
    "context"
    "log/slog"
    "os"

    "github.com/therox/gomigr"
)

func main() {
    ctx := context.Background()
    m, err := gomigr.New(ctx, gomigr.Options{
        DSN:           os.Getenv("DATABASE_URL"),
        MigrationsDir: "./migrations",
        Logger:        slog.New(slog.NewTextHandler(os.Stderr, nil)),
    })
    if err != nil {
        panic(err)
    }
    defer m.Close()
    if err := m.Up(ctx, gomigr.UpOptions{}); err != nil {
        panic(err)
    }
}

Migrator.Up and Migrator.Down take UpOptions / DownOptions structs rather than a single toVersion string. The library deliberately refuses to guess scope on Down — an empty DownOptions{} returns ErrDownScopeRequired instead of reverting everything.

type UpOptions struct {
    To    string // upper version bound (inclusive); "" means no bound
    Steps int    // limit pending applies; 0 means no limit; <0 is an error
}

type DownOptions struct {
    To    string // revert applied with version > To
    Steps int    // revert N most recently applied migrations (by id); >0
    All   bool   // revert every applied migration (explicit opt-in)
}

Exactly one of DownOptions.To, DownOptions.Steps, DownOptions.All must be set (To != "", Steps > 0, or All == true). Otherwise:

  • empty DownOptions{}ErrDownScopeRequired
  • two or three fields set → errors.New("gomigr: DownOptions fields To, Steps, All are mutually exclusive")
  • Steps < 0 (Up or Down) → validation error before any DB call

Three Down examples:

// Revert everything (matches the old behaviour of Down(ctx, "")).
err := m.Down(ctx, gomigr.DownOptions{All: true})

// Revert applied migrations with version > "20260301".
err := m.Down(ctx, gomigr.DownOptions{To: "20260301"})

// Revert the 3 most recently applied migrations (plus any applied
// dependents that get pulled in by cascade — those are logged at WARN).
err := m.Down(ctx, gomigr.DownOptions{Steps: 3})

UpOptions{Steps: 0} is intentionally equivalent to no limit (symmetric with To == ""); CLI-side validation rejects --steps 0 separately, because at the CLI layer the only reason to pass the flag is to set a positive cap.

Error sentinels exported from the package (match with errors.Is):

  • ErrLockBusy — another gomigr instance is holding the advisory lock for this database.
  • ErrDownScopeRequiredMigrator.Down was called with an empty DownOptions{}; the library refuses to default to "revert everything".

The library never reads environment variables, never parses YAML and never calls os.Exit. All input is passed through Options; all output goes to the supplied *slog.Logger (a nil logger turns into a discard handler).

Integration tests

Unit tests run with the default Go toolchain: go test ./.... They do not require Docker.

Integration tests live under the integration build tag and spin up a real PostgreSQL via testcontainers-go. Docker must be available locally:

go test -tags=integration ./...

The default go test ./... invocation stays green even when Docker is not installed.

Limitations

  • PostgreSQL only — no other databases.
  • No dry-run, no Go-coded migrations, no seed data, no golang-migrate compatibility layer. Use this tool only if you accept the format above.
  • Each migration is its own atomic unit; failing migration N leaves N-1 migrations applied. Manual cleanup is your responsibility.
  • down refuses to revert anything while there is an applied migration whose file is missing from migrations_dir: its depends-on is lost with the file, so cascade integrity cannot be verified. Restore the file or clean schema_migrations manually before retrying.

Documentation

Overview

Package gomigr — DSL-мигратор PostgreSQL с топологической сортировкой по зависимостям. Используется как CLI (cmd/gomigr) и как Go-библиотека.

Публичный API библиотеки (тип Migrator, Options) реализуется в gomigr.go.

Реализация публичного фасада библиотеки gomigr: тип Migrator с методами Up, Down, Status, Close и конструктором New. CLI cmd/gomigr вызывает эти методы напрямую, не дублируя логику применения миграций.

Index

Constants

This section is empty.

Variables

View Source
var ErrDownScopeRequired = errors.New("gomigr: DownOptions requires To, Steps or All")

ErrDownScopeRequired возвращается из Migrator.Down, если DownOptions не задаёт ни одно из полей To, Steps, All — библиотека отказывается гадать, что хотел вызывающий. Раньше "пустой toVersion" трактовался как "откатить всё"; теперь это явный opt-in через DownOptions.All.

View Source
var ErrLockBusy = store.ErrLockBusy

ErrLockBusy — другой инстанс gomigr уже держит advisory lock и работает с этой БД. Re-export из internal/store, чтобы потребители библиотеки могли сматчить ошибку через errors.Is, не зная про internal-пакет.

Functions

This section is empty.

Types

type DownOptions added in v1.0.1

type DownOptions struct {
	// To — версия, до которой откатывать (откатываются applied с version > To).
	To string
	// Steps — откатить N последних applied-миграций по id; >0.
	Steps int
	// All — явно запросить откат всех applied. Эквивалент старого
	// Down(ctx, ""), но требует явного opt-in. CLI поверх этого добавляет
	// интерактивное подтверждение.
	All bool
}

DownOptions — параметры Migrator.Down. Хотя бы одно из полей должно быть задано, иначе вернётся ErrDownScopeRequired.

type MigrationStatus

type MigrationStatus struct {
	ID        int64
	Version   string
	Name      string
	Status    string
	AppliedBy string
	AppliedAt time.Time
}

MigrationStatus — одна строка вывода Migrator.Status().

type Migrator

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

Migrator — публичный фасад библиотеки. Создаётся через New, всегда должен закрываться через Close.

func New

func New(ctx context.Context, opts Options) (*Migrator, error)

New валидирует Options, открывает внутренний *sql.DB через pgx/stdlib в режиме simple protocol (для поддержки multi-statement в Up/Down SQL), пингует БД и создаёт schema_migrations при отсутствии.

Pool намеренно мал: SetMaxOpenConns(2), SetMaxIdleConns(1) — миграциям параллелизм не нужен, а второй слот нужен advisory-lock'у, занимающему одно соединение на всё время выполнения пачки.

func (*Migrator) Close

func (m *Migrator) Close() error

Close закрывает внутренний *sql.DB. После Close использовать Migrator нельзя. Повторный вызов безопасен.

func (*Migrator) Down

func (m *Migrator) Down(ctx context.Context, opts DownOptions) error

Down откатывает applied-миграции согласно DownOptions. Ровно одно из полей To, Steps, All должно быть задано (заданным считается непустой To, Steps > 0 или All == true); иначе возвращается ErrDownScopeRequired. Указание двух или более — ошибка взаимоисключения. Захватывает advisory lock на время выполнения.

func (*Migrator) Status

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

Status возвращает все применённые миграции (включая помеченные checksum_mismatch), отсортированные по id — это хронологический порядок применения, проставляемый BIGSERIAL'ом при INSERT'е в schema_migrations.

func (*Migrator) Up

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

Up применяет pending-миграции согласно UpOptions. Пустой UpOptions эквивалентен «применить всё pending без верхней границы». Захватывает advisory lock на время выполнения. Возвращает ErrLockBusy, если lock занят.

Валидация: Steps < 0 → ошибка до подключения к БД. Steps == 0 трактуется как «без ограничения» — симметрично пустому To.

type Options

type Options struct {
	// DSN — обязательная строка подключения PostgreSQL. Поддерживается
	// как URL (postgres://...), так и keyword-формат pgx (host=... user=...).
	DSN string
	// MigrationsDir — обязательный путь к директории с *.sql миграциями.
	MigrationsDir string
	// User — значение для колонки applied_by. Пустая строка резолвится
	// внутри библиотеки в os/user.Current() → "unknown".
	User string
	// Logger — *slog.Logger для всех сообщений. nil → дискардный handler.
	Logger *slog.Logger
}

Options — параметры конструктора New. Поля заполняются вызывающим кодом (CLI делает это после загрузки YAML/ENV/флагов; внешние Go-приложения — программно). Библиотека сама ENV не читает.

type UpOptions added in v1.0.1

type UpOptions struct {
	// To — верхняя граница version (включительно). Пустая строка — без границы.
	To string
	// Steps — ограничение количества применяемых pending-миграций (без учёта
	// докачанных транзитивных зависимостей). 0 — без ограничения.
	// Отрицательное значение — ошибка валидации.
	Steps int
}

UpOptions — параметры Migrator.Up.

Directories

Path Synopsis
cmd
gomigr command
Бинарь gomigr — тонкая CLI-обёртка над библиотекой github.com/therox/gomigr.
Бинарь gomigr — тонкая CLI-обёртка над библиотекой github.com/therox/gomigr.
Package config загружает настройки CLI gomigr из YAML и переменных окружения.
Package config загружает настройки CLI gomigr из YAML и переменных окружения.
Пакет graph строит граф зависимостей миграций и выполняет топологическую сортировку.
Пакет graph строит граф зависимостей миграций и выполняет топологическую сортировку.
internal
runner
Пакет runner — внутренний пакет, выполняющий пачку миграций.
Пакет runner — внутренний пакет, выполняющий пачку миграций.
store
Пакет store — внутренняя обёртка над *sql.DB для работы с таблицей состояния schema_migrations и advisory lock'ом.
Пакет store — внутренняя обёртка над *sql.DB для работы с таблицей состояния schema_migrations и advisory lock'ом.
Пакет parser реализует разбор файлов миграций в формате DSL gomigr.
Пакет parser реализует разбор файлов миграций в формате DSL gomigr.

Jump to

Keyboard shortcuts

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