migratekit

package module
v0.7.15 Latest Latest
Warning

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

Go to latest
Published: Jan 29, 2026 License: MIT Imports: 12 Imported by: 0

README

migratekit

Minimal database migration library with app-scoped migrations and automatic locking.

Install

go get github.com/open-rails/migratekit

Usage

PostgreSQL (Complete Example)
package main

import (
    "context"
    "database/sql"
    "embed"

    "github.com/open-rails/migratekit"
    _ "github.com/lib/pq"
)

//go:embed migrations/postgres/*.sql
var postgresFS embed.FS

func main() {
    ctx := context.Background()
    db, _ := sql.Open("postgres", "postgres://...")

    // Load migrations from embedded FS
    migrations, _ := migratekit.LoadFromFS(postgresFS, "migrations/postgres")

    // Run migrations (3 lines)
    m := migratekit.NewPostgres(db, "doujins")
    m.Setup(ctx)
    m.ApplyMigrations(ctx, migrations)
}
ClickHouse (Complete Example)
package main

import (
    "context"
    "database/sql"
    "embed"

    "github.com/open-rails/migratekit"
    _ "github.com/lib/pq"
)

//go:embed migrations/clickhouse/*.sql
var clickhouseFS embed.FS

func main() {
    ctx := context.Background()
    pg, _ := sql.Open("postgres", "postgres://...")

    // Load migrations from embedded FS
    migrations, _ := migratekit.LoadFromFS(clickhouseFS, "migrations/clickhouse")

    // Run migrations (3 lines)
    m := migratekit.NewClickHouse(&migratekit.ClickHouseConfig{
        ClientAddr: "clickhouse:9000",
        Database:   "analytics",
        Username:   "analytics_user",
        Password:   "analytics_password",
        App:        "doujins",

        // ClickHouse migrations are tracked in Postgres public.migrations
        // (database='clickhouse') and use Postgres advisory locks.
        PostgresDB: pg,
    })
    m.Setup(ctx)
    m.ApplyMigrations(ctx, migrations)
}
Advanced (Manual Control)
m := migratekit.NewPostgres(db, "doujins")
m.Setup(ctx)

applied, _ := m.Applied(ctx)  // Get list of applied migrations (no lock)

// Only lock if you have work to do
var toApply []Migration
for _, mig := range migrations {
    if !contains(applied, prefix(mig.Name)) {
        toApply = append(toApply, mig)
    }
}

if len(toApply) > 0 {
    m.Lock(ctx)
    defer m.Unlock(ctx)
    for _, mig := range toApply {
        m.Apply(ctx, mig)
    }
}

API

Primary Methods
  • LoadFromFS(fsys, dir) - Load migrations from embedded filesystem
  • NewPostgres(db, app) - Create Postgres migrator
  • NewClickHouse(config) - Create ClickHouse migrator
  • Setup(ctx) - Create migration tables (idempotent)
  • ApplyMigrations(ctx, []Migration) - Apply all pending migrations (recommended)
Advanced Methods
  • Lock(ctx) - Acquire lock (waits up to 200s)
  • Unlock(ctx) - Release lock
  • Applied(ctx) - List of applied migration names ([]string)
  • Apply(ctx, Migration) - Apply a single migration
  • Close() - Cleanup

Schema

migratekit creates one table in public schema on first Setup():

CREATE TABLE public.migrations (
    id BIGSERIAL PRIMARY KEY,
    app TEXT NOT NULL,
    database TEXT NOT NULL,
    name TEXT NOT NULL,
    migrated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE(app, database, name)
);

Locking:

  • Postgres uses advisory locks (no lock table).
  • ClickHouse uses Postgres advisory locks and requires ClickHouseConfig.PostgresDB.

Migration Files

Naming Convention

Files must follow this pattern: {number}{separator}{description}.up.sql

  • Number: Any positive integer (leading zeros optional: 1, 01, 001 all work)
  • Separator: Underscore _ or hyphen -
  • Suffix: Must end with .up.sql

✅ Valid:

001_create_users.up.sql
1_create_users.up.sql
01-add-indexes.up.sql
42-add-timestamps.up.sql
0003_migrations.up.sql

❌ Invalid (will be skipped):

001_create_users.sql        # Missing .up.sql
create_users.up.sql         # Missing numeric prefix
001.create.users.up.sql     # Invalid separator (use _ or -)

Why .up.sql is required:

  • Standard convention used by golang-migrate, bun, etc.
  • Reserves .down.sql for future rollback support
  • Prevents accidental execution of non-migration SQL files

What gets stored: Numeric prefixes are normalized (leading zeros removed) before storage:

  • 001, 01, 1 all become "1"
  • 042, 42 both become "42"

Features

  • Smart locking: Only locks when there's work to do
  • App-scoped: Each app has independent migration sequences
  • Correct Postgres locking: Uses Postgres advisory locks (no lock table)
  • ClickHouse compatibility: Runs ClickHouse DDL via native protocol; tracks applied migrations in Postgres
  • Self-contained: Creates own tables on first run
  • Minimal: small codebase + minimal dependencies

Design

Why lock only when needed?

Checking what's applied (SELECT) doesn't need a lock. Only write operations need locks. This allows multiple services to check migrations concurrently without blocking.

Why per-app scoping?

Different apps (doujins, hentai0, billing) have independent migration sequences and can migrate concurrently.

Why single table?

Easy to query "show all migrations" and simpler permissions.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Prefix

func Prefix(name string) string

Prefix extracts numeric prefix from migration filenames and normalizes it. Supports both underscore and hyphen separators. Examples:

"001_create_users.up.sql" -> "1"
"1-create-users.up.sql"   -> "1"
"0042_add_field.up.sql"   -> "42"

func ValidateClickHouseMigrations

func ValidateClickHouseMigrations(ctx context.Context, config *ClickHouseConfig, fs embed.FS) error

ValidateClickHouseMigrations validates ClickHouse migrations. Returns an error if any migrations are pending.

func ValidatePostgresMigrations

func ValidatePostgresMigrations(ctx context.Context, db *sql.DB, sources ...MigrationSource) error

ValidatePostgresMigrations validates multiple Postgres migration sources at once. Returns an error if any migrations are pending.

Types

type ClickHouse

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

ClickHouse handles ClickHouse migrations via native protocol

func NewClickHouse

func NewClickHouse(config *ClickHouseConfig) *ClickHouse

NewClickHouse creates a ClickHouse migrator from config. Uses native protocol for all connections.

func (*ClickHouse) Applied

func (c *ClickHouse) Applied(ctx context.Context) ([]string, error)

Applied returns list of applied migrations

func (*ClickHouse) Apply

func (c *ClickHouse) Apply(ctx context.Context, m Migration) error

Apply applies a migration with exponential backoff retry for transient errors

func (*ClickHouse) ApplyMigrations

func (c *ClickHouse) ApplyMigrations(ctx context.Context, migrations []Migration) error

ApplyMigrations applies all unapplied migrations (only locks if needed) Automatically calls Setup() to ensure migration tables exist before proceeding.

func (*ClickHouse) Close

func (c *ClickHouse) Close() error

Close closes the connection

func (*ClickHouse) Lock

func (c *ClickHouse) Lock(ctx context.Context) error

Lock acquires a global database-wide migration lock All apps share the same lock (lock_name='global') to prevent concurrent ClickHouse migrations This is necessary because ON CLUSTER operations modify distributed DDL queue across all nodes

func (*ClickHouse) Setup

func (c *ClickHouse) Setup(ctx context.Context) error

Setup ensures database and tables exist

func (*ClickHouse) Unlock

func (c *ClickHouse) Unlock(ctx context.Context) error

Unlock releases the global lock

func (*ClickHouse) ValidateAllApplied

func (c *ClickHouse) ValidateAllApplied(ctx context.Context, migrations []Migration) error

ValidateAllApplied checks if all provided migrations have been applied. Returns an error listing any pending migrations if validation fails. This is intended for use during application startup to ensure the database schema is up-to-date before the app starts serving requests.

type ClickHouseConfig

type ClickHouseConfig struct {
	ClientAddr string // Native protocol address (e.g., clickhouse:9000)
	Database   string
	Username   string
	Password   string
	App        string
	Cluster    string // Optional; if specified, uses ON CLUSTER for DDL statements

	// Required. ClickHouse migration tracking + locking is done in Postgres:
	// - tracking: Postgres public.migrations with database='clickhouse'
	// - locking: Postgres advisory locks
	//
	// This intentionally avoids ClickHouse-based migration tables (`migrations`, `migration_locks`)
	// which are awkward to restore/merge and are not a good fit for authoritative state.
	PostgresDB *sql.DB
}

ClickHouseConfig holds configuration for ClickHouse migrations

type Migration

type Migration struct {
	Name    string
	Content string
}

Migration is a single SQL migration

func LoadFromFS

func LoadFromFS(fsys fs.FS, dir ...string) ([]Migration, error)

LoadFromFS loads migrations from an embedded filesystem. Reads all .up.sql files, sorted by name. If dir is empty, defaults to "." (root of the filesystem).

type MigrationSource

type MigrationSource struct {
	App string
	FS  embed.FS
}

MigrationSource represents a migration source with an app name and embedded filesystem

type Postgres

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

Postgres handles PostgreSQL migrations

func NewPostgres

func NewPostgres(db *sql.DB, app string) *Postgres

NewPostgres creates a Postgres migrator

func (*Postgres) Applied

func (p *Postgres) Applied(ctx context.Context) ([]string, error)

Applied returns list of applied migration names

func (*Postgres) Apply

func (p *Postgres) Apply(ctx context.Context, m Migration) error

Apply applies a single migration

func (*Postgres) ApplyMigrations

func (p *Postgres) ApplyMigrations(ctx context.Context, migrations []Migration) error

ApplyMigrations applies all unapplied migrations (only locks if needed) Automatically calls Setup() to ensure migration tables exist before proceeding.

func (*Postgres) Close

func (p *Postgres) Close() error

Close closes the database connection

func (*Postgres) Lock

func (p *Postgres) Lock(ctx context.Context) error

Lock acquires a global advisory lock for migrations This blocks until the lock is available (no polling needed) The lock is automatically released when the connection closes

func (*Postgres) Setup

func (p *Postgres) Setup(ctx context.Context) error

Setup ensures migration tables exist (idempotent)

func (*Postgres) Unlock

func (p *Postgres) Unlock(ctx context.Context) error

Unlock releases the global advisory lock

func (*Postgres) ValidateAllApplied

func (p *Postgres) ValidateAllApplied(ctx context.Context, migrations []Migration) error

ValidateAllApplied checks if all provided migrations have been applied. Returns an error listing any pending migrations if validation fails. This is intended for use during application startup to ensure the database schema is up-to-date before the app starts serving requests.

func (*Postgres) WithSchema

func (p *Postgres) WithSchema(schema string) *Postgres

WithSchema configures the schema to target for unqualified DDL/DML in migrations (via SET LOCAL search_path). This allows embedded subsystems to create tables in the host application's schema (River-style).

Jump to

Keyboard shortcuts

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