migrationx

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 14, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

README

migrationx

Your schema's history as ordered SQL files — applied in transactions, verified by checksum, locked against concurrent deploys.

Status: v0, released — ships inside the cli module (tagged cli/vX.Y.Z). Database drivers never appear here — the engine runs on your *sql.DB (sqlite and mysql dialects).

TL;DR

go get github.com/Wigata-Intech/w-tools/cli
  • Migrations are <unix-timestamp>_<name>.up.sql / .down.sql pairs, minted by Create (timestamps, not sequence numbers — two engineers can't collide on merge), shipped via embed.FS or a directory
  • Fail-closed on every run: checksum tampering, orphaned history, and late-merged out-of-order migrations all abort; out-of-order applies only with explicit permission
  • Concurrent deploys are serialized by a database-side lock, and an already-applied migration is skipped, never re-executed (-- migrationx:no-transaction migrations excepted — deploy those serially)
  • Rollbacks are audited: warn-level log lines with version, host, and operator — the database keeps no trace of a down by design
  • Trigger/procedure bodies keep their semicolons inside -- migrationx:statement begin / end
  • Zero third-party dependencies

How to use with cli

One call wires the whole migrate tree into your command root:

root.Commands = append(root.Commands, migrationx.Command(
	func(ctx context.Context, allowOutOfOrder bool) (*migrationx.Migrator, error) {
		db, err := sql.Open("your-driver", cfg.DSN) // your driver, your DSN
		if err != nil {
			return nil, err
		}
		sub, _ := fs.Sub(migrations, "migrations") // //go:embed migrations/*.sql
		return migrationx.New(db, sub, migrationx.Config{
			Dialect:         migrationx.DialectSQLite,
			AllowOutOfOrder: allowOutOfOrder,
		})
	},
))
app migrate create add_users        # scaffold <unix>_add_users.{up,down}.sql
app migrate up                      # apply all pending
app migrate up -one | -to <version>
app migrate up -allow-out-of-order  # accept a late-merged branch
app migrate down                    # roll back the newest; -all | -to <version>
app migrate status                  # ✓ applied, pending, out-of-order

How to use standalone

The engine is plain database/sql — no cli required:

m, err := migrationx.New(db, migrationsFS, migrationx.Config{Dialect: migrationx.DialectMySQL})
if err != nil {
	return err // a stray file, bad name, or unparseable SQL aborts here, never at apply time
}
if err := m.Up(ctx); err != nil {
	return err
}

Up, UpByOne, UpTo, Down, DownTo, Status, Version — every mutating call verifies first (checksums, orphans, ordering) and each migration runs in its own transaction with the history write, commit or rollback as one unit. On mysql, DDL auto-commits mid-migration — single-DDL-statement migrations are the recommended practice there, and Status makes any partial state visible.

What it costs

Fake-driver numbers — the engine's own work with the database costing nothing. Measured on a MacBook Pro — Apple M2 Pro (10 cores), 16 GB RAM, macOS 26.5.2, go1.26.6.

cd cli/migrationx && go test -run='^$' -bench=. -benchmem .
Raw output
goos: darwin
goarch: arm64
pkg: github.com/Wigata-Intech/w-tools/cli/migrationx
cpu: Apple M2 Pro
BenchmarkParseScript-10    	   29318	     39679 ns/op	   42320 B/op	     509 allocs/op
BenchmarkLoad-10           	    6358	    185756 ns/op	  130201 B/op	    3443 allocs/op
BenchmarkUpTen-10          	   25497	     47126 ns/op	   38358 B/op	     557 allocs/op
ok  	github.com/Wigata-Intech/w-tools/cli/migrationx	4.721s
Measure Result
Scanning a 100-statement file (BenchmarkParseScript) ~40 µs
Loading + checksumming 100 migration pairs (BenchmarkLoad) ~186 µs
Applying ten migrations — transactions, probes, history (BenchmarkUpTen) ~47 µs of engine overhead

Migrations run once per deploy; real cost is your SQL, not the engine. Fuzzing covers the two own-parsers — the statement scanner (mirror-oracle invariants) and the filename parser (round-trip invariants):

Fuzzing — commands and raw output
$ go test -run='^$' -fuzz=FuzzParseScript -fuzztime=10s .
$ go test -run='^$' -fuzz=FuzzParseFilename -fuzztime=10s .

The full story — module overview, precedence, entrypoints: the cli README.

Documentation

Overview

Package migrationx is a SQL migration engine on the standard library: ordered SQL files, a history table with checksums, transactions where the database supports them, and a migration lock — with database drivers staying entirely on the consumer's side.

Migrations are pairs of files named <unix-timestamp>_<name>.up.sql and .down.sql, minted by Create and shipped via embed.FS or a directory. The engine runs on the consumer's *sql.DB against sqlite or mysql.

Every mutating run verifies before it acts, and ambiguity aborts: an applied migration whose file changed (checksum mismatch) or disappeared (orphan) stops the run; pending migrations older than the newest applied one — the late-merged branch — are refused unless Config.AllowOutOfOrder acknowledges them.

Concurrent runners are serialized by a database-side lock, and a migration another runner already applied is skipped, never re-executed. The one exception is a no-transaction migration: its statements run outside any transaction, so the concurrent-runner guarantee does not cover it — deploy those serially.

A migration engine bug is a data incident, not a bug report: when in doubt, this package stops.

Index

Constants

View Source
const (
	// DefaultTable is the history table name when Config.Table is empty.
	DefaultTable = "migration_histories"

	// DefaultLockTimeout bounds the wait for the migration lock when
	// Config.LockTimeout is zero.
	DefaultLockTimeout = 30 * time.Second
)

Variables

This section is empty.

Functions

func Command

func Command(open func(ctx context.Context, allowOutOfOrder bool) (*Migrator, error)) *cli.Command

Command builds the migrate command tree — create, up, down, status — for registration in a cli root. open constructs the Migrator after flags resolve: the DSN, dialect, and filesystem live in the caller's own configuration; allowOutOfOrder carries the --allow-out-of-order flag into Config.

The grammar: the bare verb does the safe default (up applies everything, down rolls back one), --one/--all flip the quantity, --to targets a version, and combining a quantity flag with --to is an error.

func Create

func Create(dir, name string) (string, string, error)

Create scaffolds a timestamped migration pair in dir — <unix>_<name>.up.sql and .down.sql — and returns both paths. The version is the current unix second: tool-minted timestamps are what keep two engineers' migrations from colliding on merge. The name must be lowercase [a-z0-9_].

Types

type Config

type Config struct {
	// Dialect selects the database: DialectSQLite or DialectMySQL.
	Dialect Dialect

	// Table is the history table name. Default DefaultTable.
	Table string

	// Log receives progress and the down-audit lines. Nil means
	// slog.Default().
	Log *slog.Logger

	// AllowOutOfOrder applies pending migrations older than the newest
	// applied one — the late-merged branch — instead of refusing. A
	// deploy-time human decision: never enable it unattended.
	AllowOutOfOrder bool

	// LockTimeout bounds the wait for the migration lock. Default
	// DefaultLockTimeout.
	LockTimeout time.Duration
}

Config configures New. Dialect is required; every other field has a safe default.

type Dialect

type Dialect int

Dialect selects the database-specific behavior: history-table DDL, the migration lock, and transaction control. Config.Dialect is required.

const (
	DialectSQLite Dialect = iota + 1
	DialectMySQL
)

The supported dialects. The zero value is invalid — choosing a database is an explicit decision.

type Migration

type Migration struct {
	Version    int64
	Name       string
	Applied    bool
	AppliedAt  time.Time // zero when not applied, or when the stored value was unreadable
	OutOfOrder bool      // pending and older than the newest applied
}

Migration is one row of Status: a version known to the filesystem, the history table, or both.

type Migrator

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

Migrator applies, rolls back, and reports migrations. Create one with New; it is safe for a single caller — migrations are a deploy step, not a concurrent workload.

func New

func New(db *sql.DB, fsys fs.FS, cfg Config) (*Migrator, error)

New validates the configuration, loads and validates every migration file, and creates the history table if it does not exist — the first deploy needs no manual bootstrap.

func (*Migrator) Down

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

Down rolls back the applied migration with the highest version.

func (*Migrator) DownTo

func (m *Migrator) DownTo(ctx context.Context, version int64) error

DownTo rolls back every applied migration with a version greater than version, highest first; DownTo(ctx, 0) rolls back everything. Negative versions are treated as 0.

func (*Migrator) Status

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

Status reports every migration the filesystem or the history table knows, in version order. It takes no lock.

func (*Migrator) Up

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

Up applies every pending migration in version order.

func (*Migrator) UpByOne

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

UpByOne applies exactly the oldest pending migration.

func (*Migrator) UpTo

func (m *Migrator) UpTo(ctx context.Context, version int64) error

UpTo applies pending migrations up to and including version.

func (*Migrator) Version

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

Version reports the newest applied version, 0 when none.

Jump to

Keyboard shortcuts

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