queen

package module
v0.7.0 Latest Latest
Warning

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

Go to latest
Published: May 24, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

README

Queen

Queen

Lightweight database migration library for Go.
Define migrations in Go, not SQL files.

Go Reference Tests Integration Tests Go Report Card Release License

Features

  • Migrations as Go code — type-safe structs validated at compile time, no separate .sql files to ship.
  • SQL or Go functions — pure SQL for schema, Go functions for data transformations. SQL and Go parts can be combined in one migration.
  • PostgreSQL-first production path — PostgreSQL is the reference driver for advisory locking, transactional migrations, and atomic migration records.
  • 6 databases — PostgreSQL, MySQL, SQLite, ClickHouse, CockroachDB, MS SQL Server, with driver-specific guarantees.
  • Embeddable CLI + TUI — wire the CLI into your own binary alongside your migrations (see CLI).
  • Migration locking — database-backed locking with configurable timeout; see per-driver guarantees below.
  • Checksum validation — SHA-256 over SQL detects modifications to already-applied migrations and blocks up, down, and reset before they run.
  • Gap detection — finds missing, skipped, or unregistered migrations.
  • Dry run / plan / explain — preview what will run before touching the database.
  • Naming patterns — optional enforcement for sequential, padded sequential, or semver migration versions.
  • Configurable isolation — set transaction isolation level globally or per migration.
  • Rich metadata — records who applied each migration, when, on which host, in which environment, and how long it took.
  • Migration toolkitsquash, baseline, and import commands to consolidate history or onboard an existing database, including goose SQL migrations.
  • Migration tap — stream every migration's SQL, duration, rows, and errors to a pluggable sink. Live TUI view via queen up --tap.
  • Rollback testingqueen check --rollback-test runs an opt-in up -> reset -> up cycle against a clean test database.

Installation

go get github.com/yaop-labs/queen

Requires Go 1.26.3+.

Release Status And API Stability

Queen is being hardened for a PostgreSQL-first production release. The public API is small and intended to stay simple, but pre-1.0 releases may still make breaking changes when they improve safety, correctness, or performance.

Current stability policy:

  • PostgreSQL behavior is the reference contract.
  • Compile-time compatibility is preferred, but unsafe behavior may be changed even if some callers relied on it.
  • Returned migration/config data should be treated as snapshots. Mutating it must not be used to reconfigure a running Queen.
  • Non-PostgreSQL drivers are supported with the caveats documented in Locking And Transaction Guarantees.

Current known limitations:

  • SQLite locking is for local/single-process use. Do not rely on it to coordinate multiple migrator processes.
  • ClickHouse locking is best-effort and should be operationally serialized by your deployment system.
  • CockroachDB can return retryable 40001 serialization errors; explicit driver retry handling is still planned.
  • Non-PostgreSQL drivers do not currently record migration metadata in the same transaction as the migration body.

Quick Start

package main

import (
    "context"
    "database/sql"
    "log"

    "github.com/yaop-labs/queen"
    "github.com/yaop-labs/queen/drivers/postgres"
    _ "github.com/jackc/pgx/v5/stdlib"
)

func main() {
    db, err := sql.Open("pgx", "postgres://localhost/myapp?sslmode=disable")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    q := queen.New(postgres.New(db))
    defer q.Close()

    q.MustAdd(queen.M{
        Version: "001",
        Name:    "create_users_table",
        UpSQL: `
            CREATE TABLE users (
                id SERIAL PRIMARY KEY,
                email VARCHAR(255) NOT NULL UNIQUE,
                name VARCHAR(255),
                created_at TIMESTAMP DEFAULT NOW()
            )
        `,
        DownSQL: `DROP TABLE users`,
    })

    if err := q.Up(context.Background()); err != nil {
        log.Fatal(err)
    }
}

Go Function Migrations

When a change is more than schema, register a Go function instead of (or alongside) SQL. For transactional drivers such as PostgreSQL, SQL and Go-function migrations run inside the same transaction:

q.MustAdd(queen.M{
    Version:        "002",
    Name:           "normalize_emails",
    ManualChecksum: "v1", // bump when the function logic changes
    UpFunc: func(ctx context.Context, tx *sql.Tx) error {
        _, err := tx.ExecContext(ctx,
            `UPDATE users SET email = LOWER(TRIM(email))`)
        return err
    },
})

UpSQL and UpFunc can be combined in a single migration. Queen runs UpSQL before UpFunc. For rollback, DownFunc runs before DownSQL, so custom cleanup can happen before schema is dropped.

CLI

Because migrations are Go code, Queen's CLI is shipped as a library you embed in your own main.go together with your migrations. This way the CLI always knows about the exact set of migrations your application ships with — no separate registry, no file scanning.

The CLI exists for operational workflows around the same migration registry you use in code: CI checks, release plans, production confirmation prompts, gap detection, baselining, squashing, goose import, and human inspection through the TUI. If your application only needs "run all pending migrations on startup", the library API is enough.

// cmd/migrate/main.go
package main

import (
    "github.com/yaop-labs/queen"
    "github.com/yaop-labs/queen/cli"
)

func main() {
    cli.Run(func(q *queen.Queen) {
        q.MustAdd(queen.M{Version: "001", Name: "create_users", UpSQL: `...`, DownSQL: `...`})
        q.MustAdd(queen.M{Version: "002", Name: "add_index",    UpSQL: `...`, DownSQL: `...`})
    })
}

Then run any command against your project:

go run ./cmd/migrate up --driver postgres --dsn "postgres://localhost/myapp?sslmode=disable"
go run ./cmd/migrate status
go run ./cmd/migrate plan

Available commands: up, down, reset, goto, status, log, plan, explain, validate, check, gap, diff, doctor, create, init, squash, baseline, import, tui.

Configuration can also come from a .queen.yaml file (--use-config), with per-environment settings (--env production).

Example config files are included in the repository:

  • .queen.yaml.example — environment config for the embedded CLI.
  • .queenignore.example — ignored migration gap format.

Keep migrations in a normal Go package and keep the CLI entrypoint thin:

myapp/
  cmd/
    migrate/
      main.go
  migrations/
    migrations.go
    001_create_users.go
    002_add_user_slug.go
  internal/
  go.mod

migrations/migrations.go owns the registry:

package migrations

import "github.com/yaop-labs/queen"

func Register(q *queen.Queen) {
    Register001CreateUsers(q)
    Register002AddUserSlug(q)
}

Each migration file registers one focused change:

package migrations

import "github.com/yaop-labs/queen"

func Register001CreateUsers(q *queen.Queen) {
    q.MustAdd(queen.M{
        Version: "001",
        Name:    "create_users",
        UpSQL:   `CREATE TABLE users (id BIGSERIAL PRIMARY KEY, email TEXT NOT NULL UNIQUE);`,
        DownSQL: `DROP TABLE users;`,
    })
}

Your app can import the same migrations package if you run migrations from application startup, and cmd/migrate can import it for release tooling.

CLI toolkit

Onboard an existing database by marking registered migrations as already applied without executing their SQL:

queen baseline --at 010 --driver sqlite --dsn ./app.db
queen baseline --version 010 --dry-run

baseline writes migration-table records with action=baseline. Use it only when the database schema already matches the migrations you are marking.

Consolidate SQL migration history into a new generated migration file:

queen squash 001,002,003 --into initial_schema --dry-run
queen squash --from 001 --to 010 --into initial_schema

squash currently supports registered SQL migrations with SQL rollbacks. It creates the new squashed file and leaves old migration files in place for review.

Import goose SQL migrations into Queen's Go migration format:

queen import ./db/migrations --from goose --output migrations --dry-run
queen import ./db/migrations --from goose

Replace queen with your embedded migrator binary or go run ./cmd/migrate if you do not install a binary named queen.

Queen preserves the version prefix from each goose filename, including timestamp versions such as 20240524054622_create_users.sql. The importer currently supports goose .sql files with -- +goose Up and -- +goose Down sections; goose Go migrations are not converted automatically. Import writes files with exclusive create semantics and fails if a generated file already exists.

Dry Run

Inspect what Up or Down will do without applying anything:

plans, _ := q.DryRun(ctx, queen.DirectionUp, 0)
for _, p := range plans {
    fmt.Printf("%s %s [%s] destructive=%v warnings=%v\n",
        p.Version, p.Name, p.Type, p.IsDestructive, p.Warnings)
}

Checksums

SQL migrations are checksummed from their UpSQL and DownSQL. If an already-applied SQL migration is edited, Queen reports it as modified in Status() and Explain(), Validate() returns ErrChecksumMismatch, and Up, Down, and Reset fail before executing any migration work.

Go-function migrations cannot be hashed from source code at runtime. For those migrations, set ManualChecksum and bump it whenever the function logic changes:

q.MustAdd(queen.M{
    Version:        "003",
    Name:           "backfill_user_slugs",
    ManualChecksum: "backfill-user-slugs-v2",
    UpFunc: func(ctx context.Context, tx *sql.Tx) error {
        _, err := tx.ExecContext(ctx, `UPDATE users SET slug = LOWER(name)`)
        return err
    },
})

Migration Tap

Observe each migration as it runs — SQL text, duration, rows affected, and errors — without bolting on a separate proxy. Install a tap.Sink:

import "github.com/yaop-labs/queen/tap"

sink := tap.NewJSONSink(os.Stdout) // one JSON line per event
q := queen.New(postgres.New(db), queen.WithTap(sink))

For each migration the sink receives a start event, one exec event per captured SQL statement, and an end event with the total duration and any error.

SQL migrations emit a single exec event with the full SQL. Inside a Go-function migration, wrap the transaction to capture each statement:

q.MustAdd(queen.M{
    Version:        "002",
    Name:           "backfill",
    ManualChecksum: "v1",
    UpFunc: func(ctx context.Context, tx *sql.Tx) error {
        t := tap.ObserveTx(ctx, tx) // no-op when tap is disabled
        if _, err := t.ExecContext(ctx, `UPDATE users SET email = LOWER(email)`); err != nil {
            return err
        }
        _, err := t.ExecContext(ctx, `UPDATE users SET name = TRIM(name)`)
        return err
    },
})

Built-in sinks: NopSink, FuncSink, MultiSink, ChannelSink (non-blocking, drops on overflow), JSONSink, RecorderSink (in-memory, for tests). Implement tap.Sink for custom destinations.

tap.ObserveTx does not create a transaction; Queen already passes your Go migration the active *sql.Tx. It only wraps that transaction so tap can observe ExecContext, QueryContext, QueryRowContext, and prepared statements. The older tap.Tx helper is still available as a deprecated compatibility alias.

Wrap any sink with tap.NewAnalyzerSink to add sql-tap-style diagnostics:

sink := tap.NewAnalyzerSink(
    tap.NewJSONSink(os.Stdout),
    tap.DefaultAnalyzerConfig(), // slow >=100ms, N+1: 5 repeated SELECTs in 1s
)
q := queen.New(postgres.New(db), queen.WithTap(sink))

Analyzed exec events include:

  • operation — first SQL keyword, such as select, insert, create
  • sql_template — normalized SQL for grouping and N+1 detection
  • bound_sql — SQL with positional args rendered for inspection
  • slow, n_plus_1, n_plus_1_count, n_plus_1_alert
  • index — statement number within the migration

The tap package also includes helpers for programmatic inspection:

events := recorder.Events()

filter, _ := tap.ParseFilter("op:select d>100ms slow")
for _, e := range events {
    if filter.Match(e) {
        fmt.Println(e.BoundSQL)
    }
}

summary := tap.Summarize(events)
top := tap.TopQueries(events, "total", 10)
_ = summary
_ = top

_ = tap.WriteMarkdown(os.Stdout, events)
_ = tap.WriteJSONL(os.Stdout, events)

A live TUI view is wired into the CLI:

queen up --tap

The live view enables the analyzer by default. Tune it with --tap-slow-threshold and --tap-nplus1-threshold.

The full-screen TUI is available as a separate command:

queen tui --driver postgres --dsn "$DATABASE_URL"

It shows migration status, gaps, details, SQL preview for SQL migrations, and tap/explain panels for inspected operations. Go-function migrations do not have static SQL preview; their executed SQL appears in tap when the function uses tap.ObserveTx.

Migration Metadata

For each applied migration record, Queen can persist execution metadata alongside version, name, checksum, and timestamp. The built-in drivers support these fields:

  • applied_by — current OS user when available
  • duration_ms — migration execution time in milliseconds
  • hostname — current machine hostname when available
  • environment — value of QUEEN_ENV
  • action — operation type such as apply or mark-applied
  • status — operation result such as success
  • error_message — optional error details when recorded by the driver flow

This table represents the current applied state of migrations. It is not a full append-only audit log of every migration event.

Use Status() when you need the current state in code, and Driver().GetApplied() when you want the persisted applied records including metadata.

Driver Interface

Custom database drivers implement the queen.Driver interface:

type Driver interface {
    Init(ctx context.Context) error
    GetApplied(ctx context.Context) ([]Applied, error)
    Record(ctx context.Context, m *Migration, meta *MigrationMetadata) error
    Remove(ctx context.Context, version string) error
    Lock(ctx context.Context, timeout time.Duration) error
    Unlock(ctx context.Context) error
    Exec(ctx context.Context, isolationLevel sql.IsolationLevel, fn func(*sql.Tx) error) error
    Close() error
}

In practice:

  • Init prepares the migration tracking tables or any driver-specific state
  • GetApplied returns the persisted applied migration records
  • Record persists a successful applied migration together with optional metadata
  • Remove removes an applied migration record after rollback
  • Lock and Unlock provide the driver's migration lock semantics
  • Exec runs migration code using the driver's transactional semantics and requested isolation level where supported
  • Close releases driver resources

If you are implementing a custom driver, use the PostgreSQL driver as the reference behavior for locking, metadata persistence, and transaction execution. Drivers that can write migration records inside the same transaction as the migration body can implement TransactionalRecorder.

Locking And Transaction Guarantees

When multiple instances of your app or CI run migrations concurrently, Queen asks the active driver to acquire a database-backed lock:

q := queen.NewWithConfig(driver, &queen.Config{
    TableName:   "queen_migrations",
    LockTimeout: 10 * time.Minute,
})

Set SkipLock: true for single-instance setups or local development.

Current release guarantees are intentionally Postgres-first:

Database Locking guarantee Migration transaction Migration record atomic with body
PostgreSQL Production-ready advisory lock pinned to one connection Yes Yes
MySQL Uses GET_LOCK; supported, less heavily exercised than PostgreSQL Depends on MySQL DDL semantics No
SQLite Local/single-process use recommended; do not rely on it as a distributed lock Yes for transactional statements No
ClickHouse Best-effort table lock; not a strong concurrent migrator guarantee No true transaction support No
CockroachDB Table lock path; SERIALIZABLE retry handling is still planned Yes, but retryable 40001 errors need follow-up handling No
MSSQL Uses application locks; supported, pending extra connection-state hardening Yes for transactional statements No

Use PostgreSQL for production environments that require concurrent migrator safety and atomic migration bookkeeping.

For PostgreSQL production use:

  • Use the pgx stdlib driver (github.com/jackc/pgx/v5/stdlib).
  • Keep locking enabled. SkipLock is only for controlled single-runner situations.
  • Prefer SQL migrations or Go functions that use the provided *sql.Tx.
  • Keep ManualChecksum stable for Go-function migrations and bump it when the function logic changes.
  • Let Queen record migrations through the PostgreSQL driver so the migration body and migration record commit atomically.

If your application already uses native pgxpool.Pool, use the pool adapter:

pool, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL"))
if err != nil {
    log.Fatal(err)
}
defer pool.Close()

q := queen.New(postgres.NewFromPool(pool))

Queen uses pgx's database/sql adapter under the hood for this path. Closing Queen does not close the caller-owned pgxpool.Pool.

Down(ctx, n <= 0) rolls back exactly one latest applied migration. This is intentional for compatibility; use Reset(ctx) when you want to roll back everything.

Queen also validates applied checksums before Down and Reset, not just before Up. If the code for an applied migration has drifted, fix the drift or intentionally update the recorded state before rolling back.

CI/CD

Queen migrations are Go code, so the release artifact for migrations is a small Go binary, not a directory of SQL files consumed by a global CLI.

Recommended pipeline shape:

go test ./...
go run ./cmd/migrate check --driver postgres --dsn "$DATABASE_URL" --ci --no-gaps
go run ./cmd/migrate plan --driver postgres --dsn "$DATABASE_URL"
go run ./cmd/migrate up --driver postgres --dsn "$DATABASE_URL" --yes
go run ./cmd/migrate status --driver postgres --dsn "$DATABASE_URL"

For migration test databases that start empty, add rollback verification:

go run ./cmd/migrate check --driver postgres --dsn "$TEST_DATABASE_URL" --rollback-test

--rollback-test applies all migrations, rolls them back with Reset, then applies them again. It refuses to run if the target database already has applied migrations, so keep it pointed at a disposable test database.

For repeatable deployments, build the migrator once and run that exact binary:

go build -o queen-migrate ./cmd/migrate
./queen-migrate check --driver postgres --dsn "$DATABASE_URL" --ci --no-gaps
./queen-migrate up --driver postgres --dsn "$DATABASE_URL" --yes

This works well as:

  • a dedicated Kubernetes Job before rolling out the app;
  • a CI/CD deploy step with database credentials scoped only to the migration job;
  • a release image command such as /app/migrate up --yes;
  • a local developer command through go run ./cmd/migrate ....

Keep exactly one migrator job active per environment. PostgreSQL advisory locking protects against accidental concurrency, but the deployment system should still model migrations as a single explicit step.

Supported Databases

Database Driver SQL Driver Integration target
PostgreSQL queen/drivers/postgres github.com/jackc/pgx/v5/stdlib PostgreSQL 15
MySQL queen/drivers/mysql github.com/go-sql-driver/mysql MySQL 8.0
SQLite queen/drivers/sqlite github.com/mattn/go-sqlite3 Driver bundled SQLite
ClickHouse queen/drivers/clickhouse github.com/ClickHouse/clickhouse-go/v2 Latest container image
CockroachDB queen/drivers/cockroachdb github.com/jackc/pgx/v5/stdlib Latest container image
MSSQL queen/drivers/mssql github.com/microsoft/go-mssqldb SQL Server 2022

The table lists what Queen's integration tests exercise today; it is not a formal minimum-version matrix for every database. PostgreSQL 15 is the primary release target.

Documentation

Full documentation: yaop-labs.github.io/queen-docs.

License

Apache License 2.0

Documentation

Overview

Package queen provides a lightweight database migration library for Go.

Example

Example demonstrates basic usage of Queen migrations.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/yaop-labs/queen"
	"github.com/yaop-labs/queen/drivers/mock"
)

func main() {
	driver := mock.New()
	q := queen.New(driver)
	defer func() { _ = q.Close() }()

	// Register migrations
	q.MustAdd(queen.M{
		Version: "001",
		Name:    "create_users",
		UpSQL:   `CREATE TABLE users (id SERIAL PRIMARY KEY, email VARCHAR(255))`,
		DownSQL: `DROP TABLE users`,
	})

	q.MustAdd(queen.M{
		Version: "002",
		Name:    "add_users_name",
		UpSQL:   `ALTER TABLE users ADD COLUMN name VARCHAR(255)`,
		DownSQL: `ALTER TABLE users DROP COLUMN name`,
	})

	// Apply all pending migrations
	ctx := context.Background()
	if err := q.Up(ctx); err != nil {
		log.Fatal(err)
	}

	fmt.Println("Migrations applied successfully!")
}
Example (Configuration)

Example_configuration demonstrates custom configuration.

package main

import (
	"context"
	"time"

	"github.com/yaop-labs/queen"
	"github.com/yaop-labs/queen/drivers/mock"
)

func main() {
	driver := mock.New()

	config := &queen.Config{
		TableName:   "custom_migrations", // Custom table name
		LockTimeout: 30 * time.Minute,
		SkipLock:    false, // Enable lock protection
	}

	q := queen.NewWithConfig(driver, config)
	defer func() { _ = q.Close() }()

	_ = q.Up(context.Background())
}
Example (GoFunctionMigration)

Example_goFunctionMigration demonstrates using Go functions for complex migrations.

package main

import (
	"context"
	"database/sql"
	"strings"

	"github.com/yaop-labs/queen"
	"github.com/yaop-labs/queen/drivers/mock"
)

func main() {
	driver := mock.New()
	q := queen.New(driver)
	defer func() { _ = q.Close() }()

	// SQL migration to create table
	q.MustAdd(queen.M{
		Version: "001",
		Name:    "create_users",
		UpSQL:   `CREATE TABLE users (id SERIAL PRIMARY KEY, email VARCHAR(255))`,
		DownSQL: `DROP TABLE users`,
	})

	// Go function migration for complex data transformation
	q.MustAdd(queen.M{
		Version:        "002",
		Name:           "normalize_emails",
		ManualChecksum: "v1", // Track function changes
		UpFunc: func(ctx context.Context, tx *sql.Tx) error {
			rows, err := tx.QueryContext(ctx, "SELECT id, email FROM users")
			if err != nil {
				return err
			}
			defer func() { _ = rows.Close() }()

			for rows.Next() {
				var id int
				var email string
				if err := rows.Scan(&id, &email); err != nil {
					return err
				}

				normalized := strings.ToLower(strings.TrimSpace(email))

				_, err = tx.ExecContext(ctx,
					"UPDATE users SET email = $1 WHERE id = $2",
					normalized, id)
				if err != nil {
					return err
				}
			}

			return rows.Err()
		},
	})

	_ = q.Up(context.Background())
}
Example (ModularMigrations)

Example_modularMigrations demonstrates organizing migrations by domain.

package main

import (
	"context"

	"github.com/yaop-labs/queen"
	"github.com/yaop-labs/queen/drivers/mock"
)

func main() {
	driver := mock.New()
	q := queen.New(driver)
	defer func() { _ = q.Close() }()

	// Register migrations from different modules
	registerUserMigrations(q)
	registerPostMigrations(q)

	_ = q.Up(context.Background())
}

func registerUserMigrations(q *queen.Queen) {
	q.MustAdd(queen.M{
		Version: "users_001",
		Name:    "create_users",
		UpSQL:   `CREATE TABLE users (id SERIAL PRIMARY KEY)`,
		DownSQL: `DROP TABLE users`,
	})
}

func registerPostMigrations(q *queen.Queen) {
	q.MustAdd(queen.M{
		Version: "posts_001",
		Name:    "create_posts",
		UpSQL:   `CREATE TABLE posts (id SERIAL PRIMARY KEY)`,
		DownSQL: `DROP TABLE posts`,
	})
}
Example (Status)

Example_status demonstrates checking migration status.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/yaop-labs/queen"
	"github.com/yaop-labs/queen/drivers/mock"
)

func main() {
	driver := mock.New()
	q := queen.New(driver)
	defer func() { _ = q.Close() }()

	q.MustAdd(queen.M{
		Version: "001",
		Name:    "create_users",
		UpSQL:   `CREATE TABLE users (id INT)`,
		DownSQL: `DROP TABLE users`,
	})

	ctx := context.Background()

	// Check status of all migrations
	statuses, err := q.Status(ctx)
	if err != nil {
		log.Fatal(err)
	}

	for _, s := range statuses {
		fmt.Printf("Version: %s, Name: %s, Status: %s\n",
			s.Version, s.Name, s.Status)
	}
}
Example (Testing)

Example_testing demonstrates testing migrations.

package main

import (
	"testing"

	"github.com/yaop-labs/queen"
	"github.com/yaop-labs/queen/drivers/mock"
)

func main() {
	// In your test
	testFunc := func(t *testing.T) {
		driver := setupTestDB(t) // Your test DB setup

		q := queen.NewTest(t, driver) // Auto-cleanup on test end

		q.MustAdd(queen.M{
			Version: "001",
			Name:    "create_users",
			UpSQL:   `CREATE TABLE users (id INT)`,
			DownSQL: `DROP TABLE users`,
		})

		// Test both up and down migrations
		q.TestUpDown()
	}

	// Run the test (in real code, use go test)
	t := &testing.T{}
	testFunc(t)
}

func setupTestDB(_ *testing.T) queen.Driver {

	return mock.New()
}

Index

Examples

Constants

View Source
const (
	DirectionUp   = "up"
	DirectionDown = "down"
	DriverUnknown = "unknown"
)

Variables

View Source
var (
	ErrNoMigrations         = errors.New("no migrations registered")
	ErrVersionConflict      = errors.New("version conflict")
	ErrMigrationNotFound    = errors.New("migration not found")
	ErrChecksumMismatch     = errors.New("checksum mismatch")
	ErrLockTimeout          = errors.New("lock timeout")
	ErrNoDriver             = errors.New("driver not initialized")
	ErrInvalidMigration     = errors.New("invalid migration")
	ErrNameTooLong          = errors.New("migration name exceeds 63 characters")
	ErrInvalidMigrationName = errors.New("invalid migration name")
	ErrAlreadyApplied       = errors.New("migration already applied")
)

Functions

func IsValidMigrationName

func IsValidMigrationName(name string) bool

IsValidMigrationName reports whether name is a valid migration name. Valid names contain only lowercase letters, digits, and underscores.

func IsValidMigrationVersion

func IsValidMigrationVersion(version string) bool

IsValidMigrationVersion reports whether version is a valid migration version. Valid versions contain only letters, digits, dots, dashes, and underscores.

Types

type Applied

type Applied struct {
	Version      string
	Name         string
	AppliedAt    time.Time
	Checksum     string
	AppliedBy    string
	DurationMS   int64
	Hostname     string
	Environment  string
	Action       string
	Status       string
	ErrorMessage string
}

Applied represents a migration that has been applied.

type Config

type Config struct {
	TableName      string
	LockTimeout    time.Duration
	SkipLock       bool
	Naming         *NamingConfig
	IsolationLevel sql.IsolationLevel
}

Config configures Queen behavior.

func DefaultConfig

func DefaultConfig() *Config

DefaultConfig returns default configuration.

type Driver

type Driver interface {
	Init(ctx context.Context) error
	GetApplied(ctx context.Context) ([]Applied, error)
	Record(ctx context.Context, m *Migration, meta *MigrationMetadata) error
	Remove(ctx context.Context, version string) error
	Lock(ctx context.Context, timeout time.Duration) error
	Unlock(ctx context.Context) error
	Exec(ctx context.Context, isolationLevel sql.IsolationLevel, fn func(*sql.Tx) error) error
	Close() error
}

Driver is the interface that database-specific migration drivers must implement.

type Gap

type Gap struct {
	Type        GapType  `json:"type"`
	Version     string   `json:"version"`
	Name        string   `json:"name,omitempty"`
	Description string   `json:"description"`
	Severity    string   `json:"severity"`
	AppliedAt   *string  `json:"applied_at,omitempty"`
	BlockedBy   []string `json:"blocked_by,omitempty"`
}

type GapType

type GapType string
const (
	GapTypeNumbering    GapType = "numbering"
	GapTypeApplication  GapType = "application"
	GapTypeUnregistered GapType = "unregistered"
)

type IgnoredGap

type IgnoredGap struct {
	Version   string
	Reason    string
	IgnoredAt time.Time
	IgnoredBy string
}

type Logger

type Logger interface {
	InfoContext(ctx context.Context, msg string, args ...any)
	WarnContext(ctx context.Context, msg string, args ...any)
	ErrorContext(ctx context.Context, msg string, args ...any)
}

Logger is a structured logging interface compatible with slog.Logger.

type M

type M = Migration

type Migration

type Migration struct {
	Version        string
	Name           string
	UpSQL          string
	DownSQL        string
	UpFunc         MigrationFunc
	DownFunc       MigrationFunc
	ManualChecksum string
	IsolationLevel sql.IsolationLevel
}

Migration represents a database migration.

func (*Migration) Checksum

func (m *Migration) Checksum() string

func (*Migration) HasRollback

func (m *Migration) HasRollback() bool

func (*Migration) IsDestructive

func (m *Migration) IsDestructive() bool

func (*Migration) Validate

func (m *Migration) Validate() error

type MigrationError

type MigrationError struct {
	Version   string
	Name      string
	Operation string
	Driver    string
	Cause     error
}

MigrationError wraps an error with migration context.

func (*MigrationError) Error

func (e *MigrationError) Error() string

func (*MigrationError) Unwrap

func (e *MigrationError) Unwrap() error

type MigrationFunc

type MigrationFunc func(ctx context.Context, tx *sql.Tx) error

type MigrationMetadata

type MigrationMetadata struct {
	AppliedBy    string
	DurationMS   int64
	Hostname     string
	Environment  string
	Action       string
	Status       string
	ErrorMessage string
}

MigrationMetadata contains metadata collected during migration execution.

type MigrationPlan

type MigrationPlan struct {
	Version       string        `json:"version"`
	Name          string        `json:"name"`
	Direction     string        `json:"direction"`
	Status        string        `json:"status"`
	Type          MigrationType `json:"type"`
	SQL           string        `json:"sql,omitempty"`
	HasRollback   bool          `json:"has_rollback"`
	IsDestructive bool          `json:"is_destructive"`
	Checksum      string        `json:"checksum"`
	Warnings      []string      `json:"warnings,omitempty"`
}

MigrationPlan represents a migration execution plan.

type MigrationStatus

type MigrationStatus struct {
	Version     string
	Name        string
	Status      Status
	AppliedAt   *time.Time
	Checksum    string
	HasRollback bool
	Destructive bool
}

MigrationStatus represents the current state of a migration.

type MigrationType

type MigrationType string
const (
	MigrationTypeSQL    MigrationType = "sql"
	MigrationTypeGoFunc MigrationType = "go-func"
	MigrationTypeMixed  MigrationType = "mixed"
)

type NamingConfig

type NamingConfig struct {
	Pattern NamingPattern
	Padding int
	Enforce bool
}

func DefaultNamingConfig

func DefaultNamingConfig() *NamingConfig

func (*NamingConfig) FindNextVersion

func (nc *NamingConfig) FindNextVersion(existingVersions []string) (string, error)

func (*NamingConfig) Validate

func (nc *NamingConfig) Validate(version string) error

type NamingPattern

type NamingPattern string
const (
	NamingPatternNone             NamingPattern = ""
	NamingPatternSequential       NamingPattern = "sequential"
	NamingPatternSequentialPadded NamingPattern = "sequential-padded"
	NamingPatternSemver           NamingPattern = "semver"
)

type Option

type Option func(*Queen)

Option configures a Queen instance.

func WithLogger

func WithLogger(logger Logger) Option

WithLogger sets a custom logger. Compatible with *slog.Logger.

func WithTap

func WithTap(sink tap.Sink) Option

WithTap installs a tap.Sink that receives a start/exec/end event for every migration. SQL migrations emit a single exec event with the full SQL text; Go-function migrations can opt into per-statement tapping by calling tap.ObserveTx(ctx, tx) and using the wrapper's ExecContext/QueryContext methods.

type Queen

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

Queen manages database migrations.

func New

func New(driver Driver, opts ...Option) *Queen

New creates a Queen instance with default configuration.

func NewWithConfig

func NewWithConfig(driver Driver, config *Config, opts ...Option) *Queen

NewWithConfig creates a Queen instance with custom configuration.

func (*Queen) Add

func (q *Queen) Add(m M) error

Add registers a migration. Returns ErrVersionConflict if version already exists.

func (*Queen) Close

func (q *Queen) Close() error

Close releases database resources.

func (*Queen) DetectGaps

func (q *Queen) DetectGaps(ctx context.Context) ([]Gap, error)

DetectGaps analyzes migrations and returns any detected gaps.

func (*Queen) Down

func (q *Queen) Down(ctx context.Context, n int) error

Down rolls back the last n migrations. If n <= 0, rolls back only the last migration.

Example

ExampleQueen_Down demonstrates rolling back migrations.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/yaop-labs/queen"
	"github.com/yaop-labs/queen/drivers/mock"
)

func main() {
	driver := mock.New()
	q := queen.New(driver)
	defer func() { _ = q.Close() }()

	ctx := context.Background()

	// Rollback last migration
	if err := q.Down(ctx, 1); err != nil {
		log.Fatal(err)
	}

	fmt.Println("Rolled back 1 migration")
}

func (*Queen) Driver

func (q *Queen) Driver() Driver

Driver returns the underlying database driver.

func (*Queen) DriverName

func (q *Queen) DriverName() string

DriverName returns the normalized database driver name used in diagnostics.

func (*Queen) DryRun

func (q *Queen) DryRun(ctx context.Context, direction string, limit int) ([]MigrationPlan, error)

DryRun returns a migration execution plan without applying migrations. Direction can be DirectionUp (pending) or DirectionDown (applied).

func (*Queen) Explain

func (q *Queen) Explain(ctx context.Context, version string) (*MigrationPlan, error)

Explain returns a detailed migration plan for a specific version.

func (*Queen) FindMigration

func (q *Queen) FindMigration(version string) *Migration

FindMigration returns a registered migration by version, or nil if not found.

func (*Queen) MustAdd

func (q *Queen) MustAdd(m M)

MustAdd is like Add but panics on error.

func (*Queen) Reset

func (q *Queen) Reset(ctx context.Context) error

Reset rolls back all applied migrations.

Example

ExampleQueen_Reset demonstrates rolling back all migrations.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/yaop-labs/queen"
	"github.com/yaop-labs/queen/drivers/mock"
)

func main() {
	driver := mock.New()
	q := queen.New(driver)
	defer func() { _ = q.Close() }()

	ctx := context.Background()
	if err := q.Reset(ctx); err != nil {
		log.Fatal(err)
	}

	fmt.Println("All migrations rolled back")
}

func (*Queen) SQLDB

func (q *Queen) SQLDB() *sql.DB

SQLDB returns the underlying database handle when the driver exposes one. It is primarily used by interactive diagnostics such as EXPLAIN.

func (*Queen) SetTap

func (q *Queen) SetTap(sink tap.Sink)

SetTap replaces the active tap sink. It is intended for interactive tools that need to enable tap only around a specific operation.

func (*Queen) Status

func (q *Queen) Status(ctx context.Context) ([]MigrationStatus, error)

Status returns the status of all registered migrations.

func (*Queen) Up

func (q *Queen) Up(ctx context.Context) error

Up applies all pending migrations. Equivalent to UpSteps(ctx, 0).

Example

ExampleQueen_Up demonstrates applying all pending migrations.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/yaop-labs/queen"
	"github.com/yaop-labs/queen/drivers/mock"
)

func main() {
	driver := mock.New()
	q := queen.New(driver)
	defer func() { _ = q.Close() }()

	q.MustAdd(queen.M{
		Version: "001",
		Name:    "create_users",
		UpSQL:   `CREATE TABLE users (id INT)`,
		DownSQL: `DROP TABLE users`,
	})

	ctx := context.Background()
	if err := q.Up(ctx); err != nil {
		log.Fatal(err)
	}

	fmt.Println("All migrations applied")
}

func (*Queen) UpSteps

func (q *Queen) UpSteps(ctx context.Context, n int) error

UpSteps applies up to n pending migrations. If n <= 0, applies all.

Example

ExampleQueen_UpSteps demonstrates applying a specific number of migrations.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/yaop-labs/queen"
	"github.com/yaop-labs/queen/drivers/mock"
)

func main() {
	driver := mock.New()
	q := queen.New(driver)
	defer func() { _ = q.Close() }()

	q.MustAdd(queen.M{Version: "001", Name: "migration_1", UpSQL: "..."})
	q.MustAdd(queen.M{Version: "002", Name: "migration_2", UpSQL: "..."})
	q.MustAdd(queen.M{Version: "003", Name: "migration_3", UpSQL: "..."})

	ctx := context.Background()

	// Apply only the next 2 migrations
	if err := q.UpSteps(ctx, 2); err != nil {
		log.Fatal(err)
	}

	fmt.Println("Applied 2 migrations")
}

func (*Queen) Validate

func (q *Queen) Validate(ctx context.Context) error

Validate checks for duplicate versions, invalid migrations, and checksum mismatches.

Example

ExampleQueen_Validate demonstrates validating migrations.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/yaop-labs/queen"
	"github.com/yaop-labs/queen/drivers/mock"
)

func main() {
	driver := mock.New()
	q := queen.New(driver)
	defer func() { _ = q.Close() }()

	q.MustAdd(queen.M{
		Version: "001",
		Name:    "create_users",
		UpSQL:   `CREATE TABLE users (id INT)`,
	})

	ctx := context.Background()
	if err := q.Validate(ctx); err != nil {
		log.Fatalf("Validation failed: %v", err)
	}

	fmt.Println("All migrations valid")
}

type QueenIgnore

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

func LoadQueenIgnore

func LoadQueenIgnore() (*QueenIgnore, error)

func LoadQueenIgnoreFrom

func LoadQueenIgnoreFrom(path string) (*QueenIgnore, error)

func (*QueenIgnore) AddIgnore

func (qi *QueenIgnore) AddIgnore(version, reason, ignoredBy string) error

func (*QueenIgnore) GetReason

func (qi *QueenIgnore) GetReason(version string) string

func (*QueenIgnore) IsIgnored

func (qi *QueenIgnore) IsIgnored(version string) bool

func (*QueenIgnore) ListIgnored

func (qi *QueenIgnore) ListIgnored() []*IgnoredGap

func (*QueenIgnore) RemoveIgnore

func (qi *QueenIgnore) RemoveIgnore(version string) error

func (*QueenIgnore) Save

func (qi *QueenIgnore) Save() error

type SQLDBProvider

type SQLDBProvider interface {
	SQLDB() *sql.DB
}

SQLDBProvider is implemented by drivers that can expose their underlying database handle for optional diagnostics such as EXPLAIN.

type Status

type Status int
const (
	StatusPending Status = iota
	StatusApplied
	StatusModified
)

func (Status) String

func (s Status) String() string

type TestHelper

type TestHelper struct {
	*Queen
	// contains filtered or unexported fields
}

TestHelper provides testing utilities for migrations.

func NewTest

func NewTest(t *testing.T, driver Driver) *TestHelper
Example

ExampleNewTest demonstrates using the testing helper.

package main

import (
	"fmt"
	"testing"

	"github.com/yaop-labs/queen"
	"github.com/yaop-labs/queen/drivers/mock"
)

func main() {
	testFunc := func(t *testing.T) {
		driver := setupTestDB(t)

		// NewTest automatically cleans up when test ends
		q := queen.NewTest(t, driver)

		q.MustAdd(queen.M{
			Version: "001",
			Name:    "create_users",
			UpSQL:   `CREATE TABLE users (id INT)`,
			DownSQL: `DROP TABLE users`,
		})

		// Test migrations
		q.MustUp()
		q.MustValidate()

		fmt.Println("Test passed")
	}

	t := &testing.T{}
	testFunc(t)
}

func setupTestDB(_ *testing.T) queen.Driver {

	return mock.New()
}

func (*TestHelper) MustDown

func (th *TestHelper) MustDown(n int)

func (*TestHelper) MustReset

func (th *TestHelper) MustReset()

func (*TestHelper) MustUp

func (th *TestHelper) MustUp()

func (*TestHelper) MustValidate

func (th *TestHelper) MustValidate()

func (*TestHelper) TestRollback

func (th *TestHelper) TestRollback()
Example

ExampleTestHelper_TestRollback demonstrates thorough testing of down migrations.

package main

import (
	"fmt"
	"testing"

	"github.com/yaop-labs/queen"
	"github.com/yaop-labs/queen/drivers/mock"
)

func main() {
	testFunc := func(t *testing.T) {
		driver := setupTestDB(t)
		q := queen.NewTest(t, driver)

		q.MustAdd(queen.M{
			Version: "001",
			Name:    "create_users",
			UpSQL:   `CREATE TABLE users (id INT)`,
			DownSQL: `DROP TABLE users`,
		})

		q.MustAdd(queen.M{
			Version: "002",
			Name:    "add_email",
			UpSQL:   `ALTER TABLE users ADD COLUMN email VARCHAR(255)`,
			DownSQL: `ALTER TABLE users DROP COLUMN email`,
		})

		// TestRollback performs a full cycle: Up -> Down (one by one) -> Up
		// This catches broken Down migrations that don't properly undo their Up
		q.TestRollback()

		fmt.Println("All down migrations work correctly")
	}

	t := &testing.T{}
	testFunc(t)
}

func setupTestDB(_ *testing.T) queen.Driver {

	return mock.New()
}

func (*TestHelper) TestUpDown

func (th *TestHelper) TestUpDown()
Example

ExampleTestHelper_TestUpDown demonstrates testing up and down migrations.

package main

import (
	"fmt"
	"testing"

	"github.com/yaop-labs/queen"
	"github.com/yaop-labs/queen/drivers/mock"
)

func main() {
	testFunc := func(t *testing.T) {
		driver := setupTestDB(t)
		q := queen.NewTest(t, driver)

		q.MustAdd(queen.M{
			Version: "001",
			Name:    "create_users",
			UpSQL:   `CREATE TABLE users (id INT)`,
			DownSQL: `DROP TABLE users`,
		})

		// TestUpDown applies all migrations, then rolls them back
		q.TestUpDown()

		fmt.Println("Up and down migrations work correctly")
	}

	t := &testing.T{}
	testFunc(t)
}

func setupTestDB(_ *testing.T) queen.Driver {

	return mock.New()
}

type TransactionalRecorder

type TransactionalRecorder interface {
	RecordTx(ctx context.Context, tx *sql.Tx, m *Migration, meta *MigrationMetadata) error
	RemoveTx(ctx context.Context, tx *sql.Tx, version string) error
}

TransactionalRecorder is an optional Driver capability. When implemented, Queen records or removes the migration row inside the same transaction as the migration body, so schema/data changes and migration metadata commit together.

Directories

Path Synopsis
cli
Package cli provides a command-line interface for Queen migrations.
Package cli provides a command-line interface for Queen migrations.
tui
Package tui provides a terminal UI for Queen migrations.
Package tui provides a terminal UI for Queen migrations.
tui/live
Package live renders a Bubble Tea live-view of queen migration tap events.
Package live renders a Bubble Tea live-view of queen migration tap events.
drivers
base
Package base provides common functionality for Queen database drivers.
Package base provides common functionality for Queen database drivers.
clickhouse
Package clickhouse provides a ClickHouse driver for Queen migrations.
Package clickhouse provides a ClickHouse driver for Queen migrations.
cockroachdb
Package cockroachdb provides a CockroachDB driver for Queen migrations.
Package cockroachdb provides a CockroachDB driver for Queen migrations.
mock
Package mock provides an in-memory mock driver for testing Queen without a real database.
Package mock provides an in-memory mock driver for testing Queen without a real database.
mssql
Package mssql provides a MS SQL Server driver for Queen migrations.
Package mssql provides a MS SQL Server driver for Queen migrations.
mysql
Package mysql provides a MySQL driver for Queen migrations.
Package mysql provides a MySQL driver for Queen migrations.
postgres
Package postgres provides a PostgreSQL driver for Queen migrations.
Package postgres provides a PostgreSQL driver for Queen migrations.
sqlite
Package sqlite provides a SQLite driver for Queen migrations.
Package sqlite provides a SQLite driver for Queen migrations.
internal
checksum
Package checksum provides checksum calculation for migrations.
Package checksum provides checksum calculation for migrations.
sort
Package sort compares migration versions in natural order.
Package sort compares migration versions in natural order.
Package tap provides real-time observability of SQL executed during queen migrations.
Package tap provides real-time observability of SQL executed during queen migrations.
tests
demodata
Package demodata registers one shared migration story for Queen demos.
Package demodata registers one shared migration story for Queen demos.
tapdemo command
tapdemo is a focused development preview for the low-level tap live renderer.
tapdemo is a focused development preview for the low-level tap live renderer.
tuidemo command
tuidemo launches the main Queen TUI against an in-memory SQLite database.
tuidemo launches the main Queen TUI against an in-memory SQLite database.

Jump to

Keyboard shortcuts

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