gomigr

package module
v1.0.0 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 is a per-command argument for up/down, not a persistent setting.

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 version]
gomigr down    [--config path] [--dsn ...] [--migrations-dir path] [--user name] [--to version]
gomigr status  [--config path] [--dsn ...] [--migrations-dir path]
gomigr create  [--config path] [--migrations-dir path] <name>
  • up [--to V] applies all pending migrations whose version is <= V plus their transitive pending dependencies in topological order. Empty --to means "apply everything". Transitive dependencies with version greater than V are still applied and logged as warnings.

  • 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). Empty --to means "revert everything".

  • 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, ""); err != nil {
        panic(err)
    }
}

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 ErrLockBusy = store.ErrLockBusy

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

Functions

This section is empty.

Types

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, toVersion string) error

Down откатывает applied-миграции с version > toVersion в обратном топологическом порядке. Пустой toVersion означает «откатить всё». Захватывает 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, toVersion string) error

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

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 не читает.

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