migratekit

package module
v1.10.0 Latest Latest
Warning

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

Go to latest
Published: Sep 27, 2026 License: MIT Imports: 24 Imported by: 0

README

migratekit

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

Release baseline

v1.10.0 is the recommended v1 release. It adds verified conversions from retired chains and opt-in strict integrity (see Conversions) to v1.0.5, which includes the fresh v1.0.4 API baseline and corrects ClickHouse target-database scoping and migration identity validation. The baseline replaced the retired pre-launch public API and database contracts; there are no compatibility aliases or legacy-ledger conversions. Reset pre-launch application databases before adoption. ClickHouse requires a fresh, target-scoped ledger; old unscoped rows are rejected without conversion.

Both database drivers initialize tracking automatically. Neither exposes a Setup method; use normal operations such as ApplyMigrations. Startup validation remains read-only. New breaking public API changes require a new major version.

Old public Go downloads cannot be erased or overwritten. Retraction metadata excludes the retired versions from normal version queries; see release policy.

Install

go get github.com/open-rails/migratekit@v1.10.0

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 (2 lines; ApplyMigrations ensures the tracking table)
    m := migratekit.NewPostgres(db, "doujins")
    m.ApplyMigrations(ctx, migrations)

    // Optional: target a configured schema. Unqualified migration SQL runs
    // under SET LOCAL search_path = "<schema>", public.
    m = migratekit.NewPostgres(db, "doujins").WithSchema(cfg.DB.Schema)

    // Optional: if migrations are authored with hard-qualified canonical DDL,
    // rewrite that app-owned schema to the configured schema while applying.
    m = migratekit.NewPostgres(db, "openrails").WithSchema(cfg.DB.Schema, "openrails")
}
ClickHouse (Complete Example)

ClickHouse support lives in its own subpackage, migratekit/chmigrate, which imports github.com/ClickHouse/clickhouse-go/v2. The root migratekit package has zero ClickHouse imports: if you only need the Postgres migrator, go mod tidy never pulls in the ClickHouse client (Go's module graph pruning drops it once nothing in your module imports migratekit/chmigrate).

package main

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

    "github.com/open-rails/migratekit"
    "github.com/open-rails/migratekit/chmigrate"
    _ "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 := chmigrate.New(&chmigrate.Config{
        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.ApplyMigrations(ctx, migrations)
}
Startup validation (read-only)
// Fail app startup if any migration is pending. Never creates tables.
err := migratekit.ValidatePostgresMigrations(ctx, db,
    migratekit.MigrationSource{App: "authkit", FS: authkitFS},
    migratekit.MigrationSource{App: "billing", FS: billingFS},
)

// ClickHouse equivalent, in the chmigrate subpackage:
err = chmigrate.ValidateMigrations(ctx, &chmigrate.Config{App: "doujins", Database: "analytics", PostgresDB: pg}, clickhouseFS)

When boot refuses: operator runbook

The identity checks stop a migration from silently never running. That is worth a boot refusal — but only if there is a way out of it that is not psql and a hand-written UPDATE public.migrations. Use the CLI:

go run github.com/open-rails/migratekit/cmd/migratekit status \
  -app tensorhub -dir migrations/postgres -dsn "$DATABASE_URL"

Run the command within the application module to use its pinned migratekit version.

status prints, for every discrepancy, WHAT is wrong (file, number, both digests), the LIKELY CAUSES, and the command that resolves each one. Every boot refusal ends by pointing at it.

What you see What it means Resolution
number "N" was already applied by a DIFFERENT file (error) Two lanes claimed N; the other one merged first. Your file would be recorded as already applied and its DDL would never run. Renumber your file to the next free number. Nothing in the database changes.
migration X was EDITED after it was applied (warning — boot proceeds) X's SQL tokens changed since it ran here. This database has the old schema, a fresh one gets the new. Inspect the diff. Acknowledged intentional change: repair accept-content N --reason "…"; otherwise revert and add a new migration. Whitespace/comments are ignored automatically.
migration X sorts BEFORE Y, which is already applied (error, WithStrictOrdering only) A lane branched below the high-water mark and merged late. Renumber X above Y. Genuine backport: apply --allow-below-applied N --reason "…", which applies it once and records the deviation.
migration X is failed, not applied (error) A -- migratekit:no-transaction migration failed or was interrupted, so it may be HALF applied. The ledger holds the state rather than guessing. Undo the partial work (a failed CREATE INDEX CONCURRENTLY leaves an INVALID index — DROP INDEX it), then repair resolve N --rerun --reason "…". If the DDL actually landed, repair resolve N --applied --reason "…".
Row 1's error, but your files are right and the ledger is the wrong side A restored backup, an adopted database, or a hand-fixed row: the ledger remembers a tree that no longer exists. repair adopt N --reason "…" — or repair adopt --all-unmatched --reason "…" when every row mismatches, which is what a restore actually looks like.

Rows 1 and 4 are the same error with opposite fixes, which is why status prints both causes and you pick. The question to ask is which side is stale, the files or the ledger? Renumber when a colliding file really exists; adopt when it does not.

The repair verbs
migratekit repair adopt 42 --reason "restored the 2026-08-10 backup"
migratekit repair adopt --all-unmatched --reason "adopted the doujins cluster"
migratekit repair accept-content 7 --reason "comment typo; DDL byte-identical"
migratekit apply --allow-below-applied 91 --reason "backport of the th#1712 index"
migratekit history          # everything anyone has ever repaired here

Every one of them:

  • requires --reason, recorded verbatim — a ledger repair with no recorded reason is indistinguishable from tampering;
  • writes an audit row to public.migration_repairs (verb, reason, --operator, the OS user, the host, and the old and new identity) in the same transaction as the change. A repaired ledger is visible history, not an erased one;
  • touches the ledger's identity columns only — never schema, never DDL, and never marks an unapplied migration applied;
  • supports --dry-run, which prints the exact before/after and writes nothing;
  • refuses to run in CI. A repair rewrites one database's ledger after a human has read the diff. If every database trips over the same thing, the chain is wrong and the fix belongs in the repository.

The same verbs are available to Go callers as (*Postgres).RepairAdopt, RepairAdoptAllUnmatched, RepairAcceptContent, ApplyWithOrderingException and RepairHistory, all taking a RepairRequest{Reason, Operator, DryRun}.

Stable API (v1)

This section describes the supported API starting at v1.0.4. It does not promise compatibility with the retired releases. Future changes follow Go semantic versioning; breaking this public contract requires a new major version. Unexported helpers and exact error-message wording are implementation details.

Loading
Symbol Contract
type Migration struct { Name, Content string } One SQL migration: filename + raw file content.
LoadFromFS(fsys fs.FS, dir ...string) ([]Migration, error) Loads every *.up.sql in dir (default "."), ordered by numeric prefix. Errors on duplicate normalized prefixes. Only the first dir element is used.
Prefix(name string) string Decimal display form of a migration prefix ("001_x.up.sql" → "1").
Sequence(name string) (int64, error) Validates and parses the prefix stored as BIGINT; accepts zero through 9223372036854775807.
ValidateSequences([]Migration) error Validates numeric, unique, increasing sequence numbers before database operations. Gaps are allowed.
CheckChain(names []string) error Validates a chain as a file listing — duplicate numbers, gaps, monotonicity — with no database. For a CI gate on the merge boundary.
ContentDigest(content string) string The SHA-256 of the migration body, excluding its own -- parent: header.
SemanticContentDigest(content string) string Token-level PostgreSQL digest: ignores comments, whitespace, and unquoted-identifier case while preserving quoted/dollar-quoted content exactly.
type AppliedRecord struct { Key, Filename, Digest, SemanticDigest, Status, Error string } One current ledger row. Key is the decimal representation of the BIGINT sequence column.
Load(fsys fs.FS, dir string, opts ...LoadOption) ([]Migration, error) LoadFromFS plus options. RequireParentLinks() makes a headerless migration an error; WithChainWarnFunc(fn) redirects the tolerance warnings.
VerifyChain(migrations []Migration, opts ...LoadOption) error The parent-link check on an already-loaded chain.
CheckChainFS(fsys fs.FS, dir string, requireLinks bool) error The CI gate: CheckChain's numbering rules plus parent-link validation, which needs the bytes and not just the names.
CheckRepairTotality(fsys fs.FS, dir string) error Refuses a constraint over pre-existing data that carries no repair. Pure file analysis.
Relink(dir string, RelinkOptions) ([]RelinkChange, error) Rewrites parent lines to match the current order. Files only — no database, no audit, CI-safe.
Postgres
Symbol Contract
NewPostgres(db *sql.DB, app string) *Postgres Migrator for one app's migrations. Never closes db.
NewPostgresFromPGXPool(pool *pgxpool.Pool, app string) (*Postgres, error) Creates an isolated, two-connection migration handle from a host pgx pool. The returned migrator owns that handle; call Close when done. The host pool is never used or mutated.
(*Postgres) Close() error Closes the isolated database handle created by NewPostgresFromPGXPool; no-op for NewPostgres values.
(*Postgres) WithSchema(schema string, rewriteFrom ...string) *Postgres Migrations run under SET LOCAL search_path = "<schema>", public. Optional rewriteFrom canonical schema names are rewritten to schema in migration SQL before execution, for portable hard-qualified app DDL such as openrails.foo. Tracking stays in public.migrations.
(*Postgres) ApplyMigrations(ctx, []Migration) error The one-call path: atomically initializes the tracking tables under the global bootstrap lock, then applies every unapplied migration in order under the migration advisory lock (lock taken only when there is work), records each by Prefix. Each migration runs in its own transaction.
(*Postgres) Applied(ctx) ([]string, error) Recorded sequences in decimal form, numerically ordered for this app, database='postgres'.
(*Postgres) ValidateAllApplied(ctx, []Migration) error Read-only startup gate: error naming pending migrations, never creates tables.
(*Postgres) WithStrictOrdering() *Postgres Refuse a pending migration that sorts below one already applied. Opt-in.
(*Postgres) WithStrictIntegrity() *Postgres An edited applied migration no longer just warns: the live schema is compared with a fresh build of the applied migrations. Equal: digests re-stamped (audit verb verify-content). Different: refuse with the migration, the schema diff and the resolution. Needs WithSchema. Opt-in.
(*Postgres) WithConversions(...Conversion) *Postgres Declares retired chains this chain converts from. See Conversions.
SchemaDiff(ctx, *sql.DB, schema, reference string) ([]string, error) Read-only comparison of two schemas by behaviour, with the rules conversions use.
(*Postgres) WithRender(Render) *Postgres How the current chain renders for another schema. Needed only for hard-qualified migrations.
type Conversion struct { Name string; Retired Render; Replaces int; SQL func(schema string) (string, error); Fallback string }, type Render func(schema string) ([]Migration, error), ErrSchemaMismatch Conversion declaration and its refusal error.
(*Postgres) AppliedRecords(ctx) (map[string]AppliedRecord, error) Ledger keyed by tracking key, carrying the recorded filename and content digest.
(*Postgres) WithWarnFunc(func(Discrepancy)) *Postgres Replace the warning sink. Default logs through slog.Default() at warn level; never silent unless you make it so.
(*Postgres) Status(ctx, []Migration) (Status, error) Applied set, pending set, every discrepancy with cause and resolution, and the repair history. Tracker tables are initialized automatically.
(*Postgres) RepairAdopt(ctx, Migration, RepairRequest) (RepairResult, error) Bind the file in the tree as the applied identity for its number. For a ledger that is the stale side.
(*Postgres) RepairAdoptAllUnmatched(ctx, []Migration, RepairRequest) ([]RepairResult, error) The same for every mismatched row at once — the restored-backup shape.
(*Postgres) RepairAcceptContent(ctx, Migration, RepairRequest) (RepairResult, error) Re-stamp the digest after a verified edit; clears the drift warning. Refuses on an identity mismatch.
(*Postgres) ApplyWithOrderingException(ctx, []Migration, allowBelow []string, RepairRequest) error Apply with a one-shot exemption from the ordering rule. Identity checks are not relaxed.
(*Postgres) RepairHistory(ctx) ([]RepairRecord, error) The audit trail, newest first.
type RepairRequest struct { Reason, Operator string; DryRun bool } Reason is required. Every repair refuses under CI (DetectCI).
type Status, type Discrepancy, type RepairResult, type RepairRecord, Severity, DiscrepancyKind Reporting types. Discrepancy.String() is the full explanation; OneLine() is the log-line form.
DetectCI() (string, bool) Names the CI environment variable that is set, if any.
ClickHouse (migratekit/chmigrate)

ClickHouse is a separate subpackage so the root package never imports github.com/ClickHouse/clickhouse-go/v2. Its stable surface:

Symbol Contract
type Config struct { ClientAddr, Database, Username, Password, App, Cluster string; PostgresDB *sql.DB } Database and PostgresDB are required: tracking rows live in Postgres public.migrations (database='clickhouse') and locking uses Postgres advisory locks. Cluster enables {{ON_CLUSTER}} expansion.
New(*Config) *ClickHouse Migrator; connects to ClickHouse lazily via native protocol.
(*ClickHouse) ApplyMigrations(ctx, []migratekit.Migration) error Same shape as Postgres. Statements run individually (no transactions) with up-to-30s retry on transient distributed-DDL errors.
(*ClickHouse) Applied(ctx) ([]string, error) Applied sequences for this app and explicit target database; initializes the Postgres tracker automatically.
(*ClickHouse) ValidateAllApplied(ctx, []migratekit.Migration) error Read-only startup gate.
(*ClickHouse) Close() error Closes only the native ClickHouse connection the migrator itself opened (never PostgresDB).
ValidateMigrations(ctx, *Config, fs.FS) error LoadFromFS + ValidateAllApplied in one call, mirroring ValidatePostgresMigrations for a single ClickHouse app.
Multi-source startup validation
Symbol Contract
type MigrationSource struct { App string; FS fs.FS } One app's migration filesystem.
ValidatePostgresMigrations(ctx, db, ...MigrationSource) error ValidateAllApplied across several apps in one call. MigrationSource.Schema and RewriteFrom mirror WithSchema for schema-aware callers.
Frozen behavioral contracts

These behaviors define the current API:

  1. Tracking table: public.migrations (id, app, database, schema, sequence, filename, content_sha256, semantic_sha256, status, error, migrated_at, UNIQUE(app, database, schema, sequence)) with database ∈ {postgres, clickhouse}. public.migration_repairs records every repair.
  2. Tracking key: the normalized numeric prefix (Prefix), not the filename. A different file claiming an applied number is a hard error. Edited SQL is always an operator warning; comment/format-only edits compare cleanly through semantic_sha256.
  3. Discovery: only *.up.sql files; *.down.sql is reserved; numeric-prefix ordering; duplicate prefixes are a load error.
  4. Locking: appliers are serialized by Postgres advisory locks held on a dedicated pinned connection for the duration of the apply; the lock is taken only when unapplied migrations exist; process death releases the lock with the connection.
  5. Templates: {{VAR}} / ${VAR} substitute from the environment at apply time; an unset variable is an error; an explicitly-empty variable substitutes as-is; {{ON_CLUSTER}}/${ON_CLUSTER} expand from chmigrate.Config.Cluster (empty → removed).
  6. Postgres atomicity: one migration = one transaction — the DDL and its ledger row commit together, so a failed migration applies nothing and records nothing. The one exception is explicit: a migration whose leading comment block carries -- migratekit:no-transaction runs outside a transaction and can fail half-applied; the ledger then records it failed (or running after a crash) and boot refuses until an operator resolves it.
  7. Postgres schema targeting: WithSchema(schema) sets a per-migration transaction search path for unqualified SQL. WithSchema(schema, "canonical") also rewrites app-owned canonical schema references to schema before execution; use this for portable hard-qualified DDL, not for shared schemas like public.
  8. ClickHouse non-atomicity: statements are split (quote-aware) and run individually; a partial failure leaves earlier statements applied and the migration unrecorded — every statement must be individually idempotent.
  9. Ownership: migrators never close a *sql.DB you pass in.
Identity, content drift, and repairs

Each migration is identified by its numeric sequence within its application, database driver, and configured schema. A different filename claiming an applied sequence is an error. Edited SQL emits a warning; comments, whitespace, and unquoted-identifier case are ignored by the semantic digest. Content drift does not block startup unless WithStrictIntegrity is set. WithStrictOrdering optionally rejects pending migrations below an already-applied sequence.

Status explains discrepancies. The CLI repair commands and their corresponding Go methods record the operator's reason and ledger change together in public.migration_repairs. They do not reconstruct application tables or convert an older ledger schema.

Conversions

A chain that rebaselines leaves databases built by the old chain with a ledger the new tree cannot use. A Conversion declares the retired chain and the SQL that turns it into the leading Replaces migrations of the current chain. During ApplyMigrations, under the migration lock and in one transaction, migratekit converts when the ledger records exactly the retired chain (or is empty while the schema has its shape):

  1. the live schema must equal a fresh build of the retired chain;
  2. the conversion SQL runs (it may RAISE to refuse, e.g. on data it cannot carry);
  3. the result must equal a fresh build of the replaced migrations;
  4. the retired ledger rows are replaced, each change with an audit row (verb convert).

Any failure rolls everything back and names the diff and the Fallback. Reference builds run the migrations in a throwaway schema inside the same transaction, so nothing is hard-coded and nothing persists. Schemas compare by behaviour: tables, columns, types, constraints (with names), indexes (not their names), triggers, functions, views, sequences and policies; not column order, comments, ownership or grants.

A migration may declare its immediate predecessor on the first non-blank line:

-- parent: 5 sha256:<digest-of-the-parent-body>

The first migration declares -- parent: root. Load verifies declared links while reading files, before any database access. RequireParentLinks() also rejects files without a header. Parent links must match the preceding migration's number and canonical content digest. The digest excludes the migration's own parent header.

After renumbering or editing migrations, update their parent links with:

migratekit relink -dir migrations/postgres
migratekit relink -dir migrations/postgres --check

relink edits source files only. It does not write to the database or need a repair reason. A squashed chain starts with a new root file.

CheckRepairTotality and migratekit check inspect constraints on pre-existing tables. Repair DML must precede the constraint, or the file must explain why no repair is needed with -- Repair: none-needed <reason>. Tables created in the same migration are exempt. Unresolved statement targets require an explicit waiver.

migratekit check -dir migrations/postgres --require-links
Non-transactional PostgreSQL migrations

Every PostgreSQL migration runs in a transaction unless its leading comment block explicitly contains:

-- migratekit:no-transaction

Use this for commands such as CREATE INDEX CONCURRENTLY that cannot run inside a transaction. Migratekit does not infer transaction support by scanning SQL. Statements execute individually under the migration lock. The ledger records running, then applied on success or failed with the error on failure. A crash may leave running.

An unfinished non-transactional migration requires operator resolution before another apply. Inspect and repair the database, then use migratekit repair resolve N --applied|--rerun --reason "...". Ordinary transactional failures roll back both the migration and its ledger row.

Compatibility policy

The numeric-ledger release is an intentional pre-launch database hard cut. All controlled applications must reset their databases before adopting it. It retains the Go module path and public string keys used by reporting APIs; the database columns and bound sequence values are BIGINT/int64. There is no legacy ledger conversion path.

Schema

migratekit creates two tables in the public schema automatically on the first operation that needs tracking. This is a fresh-database hard cut: reset the application databases before adopting it. No table renames, column upgrades, or digest backfills run. Initialization never drops or converts existing tables.

CREATE TABLE public.migrations (
    id BIGSERIAL PRIMARY KEY,
    app TEXT NOT NULL,
    database TEXT NOT NULL,
    schema TEXT NOT NULL DEFAULT '',
    sequence BIGINT NOT NULL CHECK (sequence >= 0),       -- the numeric ledger key: Prefix(filename)
    filename TEXT,
    content_sha256 TEXT,
    semantic_sha256 TEXT,
    status TEXT NOT NULL DEFAULT 'applied',  -- applied | running | failed
    "error" TEXT,                   -- why a no-transaction apply failed
    migrated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE(app, database, schema, sequence)
);

-- Each repair is recorded in the same transaction as its ledger change.
CREATE TABLE public.migration_repairs (
    id BIGSERIAL PRIMARY KEY,
    app TEXT NOT NULL,
    database TEXT NOT NULL,
    schema TEXT NOT NULL DEFAULT '',
    sequence BIGINT NOT NULL CHECK (sequence >= 0),       -- the numeric ledger key repaired
    verb TEXT NOT NULL,
    reason TEXT NOT NULL,
    operator TEXT NOT NULL DEFAULT '',
    os_user TEXT NOT NULL DEFAULT '',
    host TEXT NOT NULL DEFAULT '',
    old_filename TEXT, old_digest TEXT,
    new_filename TEXT, new_digest TEXT,
    repaired_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

Locking:

  • Postgres uses advisory locks (no lock table). The lock is acquired and released on a single dedicated connection pinned for the lock's lifetime — session advisory locks belong to the connection that took them, so going through the pool would acquire on one connection and "release" on another. If the process dies mid-migration, Postgres releases the lock when the pinned connection drops.
  • ClickHouse uses Postgres advisory locks (same pinning) and requires chmigrate.Config.PostgresDB.

Migration Files

Naming Convention

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

  • Number: An integer from 0 through 9223372036854775807 (leading zeros are optional)
  • 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

Files without the .up.sql suffix are ignored. A .up.sql file with an invalid numeric prefix is rejected.

❌ Invalid:

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 integer 1
  • 042, 42 both become integer 42

Ordering: migrations apply in numeric-prefix order (2_x before 10_x), not lexical filename order, so unpadded prefixes are safe.

Duplicate prefixes are rejected: because tracking is keyed by the normalized prefix, two files sharing a prefix (002_users.up.sql + 002_roles.up.sql, or 0042_x vs 42_y) would mean the second silently never runs. LoadFromFS returns an error instead.

Parent line: the first non-blank line of a migration is -- parent: <number> sha256:<digest-of-the-parent's-canonical-body>, or -- parent: root for the lowest-numbered file. The whole chain is verified at load. Renumbering means git mv and updating that line — migratekit relink does both parts of the second half. Use RequireParentLinks() to require headers on every file.

Repair rule: a migration that adds a constraint over a table that already has rows must carry, ABOVE the constraint, the DML that repairs the offending rows — or -- Repair: none-needed <reason>. Tables created in the same file are exempt. Measuring the real data first is how you decide between deleting and backfilling; the gate enforces the repair, not the measurement.

Template Variables

Migration SQL may reference environment variables as {{VAR_NAME}} or ${VAR_NAME}; they are substituted at execution time. A referenced variable that is not set is an error — silent empty-string substitution previously meant a typo'd {{CLICKHOUSE_PASWORD}} shipped an empty password into DDL. A variable explicitly set to the empty string is substituted as-is. {{ON_CLUSTER}} is special-cased for ClickHouse (expanded from chmigrate.Config.Cluster). Avoid ${...}/{{...}} sequences in migration SQL that are not meant as templates.

ClickHouse Semantics

ClickHouse has no transactional DDL. A multi-statement migration that fails partway leaves the earlier statements applied and the migration unrecorded, so the rerun re-executes them. Every statement in a ClickHouse migration must therefore be individually idempotent (CREATE TABLE IF NOT EXISTS, DROP ... IF EXISTS, etc.). Statements are split on ; with full awareness of string literals and comments, so semicolons inside quoted strings are safe.

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
  • Resolvable: every refusal has a migratekit status explanation and an audited repair verb
  • 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.

ClickHouse ledger identity

Config.Database must explicitly select a nonblank target. One Postgres ledger owns one logical ClickHouse deployment. Rows are scoped by app, database='clickhouse', and schema=Config.Database; a second target database receives its own migrations. Connection addresses, replicas, credentials and cluster names do not change that identity. Independent deployments with the same database name must use separate Postgres ledgers. Locks serialize all apps for a target database, including connections through different aliases.

Before applying any DDL, the complete supplied migration set must match every recorded filename and exact source SHA256 in its scope. Missing files, changed bytes, and old rows without a target scope are errors; there is no legacy adoption. Digests cover source before template expansion, so deployment environment values are not migration identity.

ClickHouse DDL is not transactional. A running row records identity before execution and becomes applied only after every statement succeeds. Calling ApplyMigrations again replays an identical incomplete migration from its first statement; migrations must be idempotent for that recovery to be safe. Changed source is refused even after partial execution. The read-only startup validators never initialize the ledger and reject pending or incomplete migrations, drift, and a missing ledger. They do not inspect physical ClickHouse objects.

Documentation

Index

Constants

View Source
const (
	StatusApplied = "applied"
	StatusRunning = "running"
	StatusFailed  = "failed"
)

Ledger statuses. An EMPTY status is 'applied': every row written before this existed, and every transactional insert since, is complete by construction.

Variables

View Source
var ErrBaselineUnsafe = errors.New("migratekit: refusing to baseline")

ErrBaselineUnsafe is returned when the ledger holds something a baseline must not overwrite.

View Source
var ErrNothingToRepair = errors.New("migratekit: nothing to repair")

ErrNothingToRepair is returned when the ledger already matches the tree.

View Source
var ErrRepairInCI = errors.New("migratekit: repairs are operator-runtime verbs and refuse to run in CI")

ErrRepairInCI is returned when a repair verb is invoked in CI.

View Source
var ErrSchemaMismatch = errors.New("migratekit: schema does not match its ledger")

ErrSchemaMismatch is returned when a schema is not what its ledger claims, so continuing would apply migrations to a schema they were not written for.

Functions

func CheckChain added in v1.0.1

func CheckChain(names []string) error

CheckChain validates a migration chain as a FILE LISTING — no database. It reports duplicate numbers, gaps and non-monotonic numbering, which is what a CI gate on the merge boundary needs. names may be in any order.

func CheckChainFS added in v1.0.1

func CheckChainFS(fsys fs.FS, dir string, requireLinks bool) error

CheckChainFS is the CI gate on a migration directory: the numbering rules of CheckChain plus parent-link validation, which needs the bytes and not just the names. requireLinks refuses a headerless migration.

func CheckRepairTotality added in v1.0.1

func CheckRepairTotality(fsys fs.FS, dir string) error

CheckRepairTotality lints every *.up.sql under dir. Pure file analysis — no database, no configuration, no deployment names. It belongs on the merge boundary beside CheckChain: a database that has already applied a migration cannot be helped by discovering it lacked a repair. If dir is empty it defaults to "." (root of the filesystem).

func ContentDigest added in v1.0.1

func ContentDigest(content string) string

ContentDigest is the ledger's digest of a migration's CANONICAL BODY — the file with its own `-- parent:` header removed (see chain.go). A headerless file hashes to exactly what v1.5.0 recorded, so adding a parent line to an already-applied migration changes no ledger digest.

func DetectCI added in v1.0.1

func DetectCI() (string, bool)

DetectCI reports the name of the CI environment variable that is set, if any. An empty-string value does not count — some shells export CI="".

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 RepairTotalityFindings added in v1.0.1

func RepairTotalityFindings(filePath, body string) []string

RepairTotalityFindings returns the lint's complaints about one migration, empty when it is clean. Every message names the statement AND both ways to satisfy it, because a gate whose output does not say what to write is a gate people route around.

func SchemaDiff added in v1.10.0

func SchemaDiff(ctx context.Context, db *sql.DB, schema, reference string) ([]string, error)

SchemaDiff compares two schemas of one database by behaviour, with the same rules conversions and strict integrity use, and lists every difference: "- " lines exist only in schema, "+ " lines only in reference. It reads the catalog in a transaction it rolls back, so it changes nothing.

func SemanticContentDigest added in v1.0.1

func SemanticContentDigest(content string) string

SemanticContentDigest hashes SQL tokens instead of file formatting. Comments, whitespace, and unquoted-identifier case do not affect it; quoted and dollar-quoted bodies remain byte-exact because whitespace inside them can be executable data.

func Sequence added in v1.0.4

func Sequence(name string) (int64, error)

Sequence parses an unsigned decimal migration prefix in the BIGINT range. Both separators (0001_schema and 0001-schema) and leading zeros are allowed.

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.

func ValidateSequences added in v1.0.4

func ValidateSequences(migrations []Migration) error

ValidateSequences checks numeric identities and increasing order before any migrations run. Zero and gaps are allowed; duplicate, signed, nonnumeric, and overflowing sequences are rejected. LoadFromFS returns this order.

func VerifyChain added in v1.0.1

func VerifyChain(migrations []Migration, opts ...LoadOption) error

VerifyChain validates the parent links of migrations, which must be in apply order (as returned by loadFiles). It reports the FIRST violation, naming both files involved, because a chain error is always about a relationship.

Types

type AppliedRecord added in v1.0.1

type AppliedRecord struct {
	// Key is the decimal display form of the BIGINT sequence.
	Key string
	// Filename is the full migration filename that claimed Key.
	Filename string
	// Digest is the sha256 of the applied migration's content.
	Digest string
	// SemanticDigest hashes SQL tokens while ignoring comments and formatting.
	SemanticDigest string
	// Status is applied, running, or failed. Only a no-transaction migration
	// can remain running or failed.
	Status string
	// Error is the recorded cause of a failed no-transaction apply.
	Error string
}

AppliedRecord is one row of the applied-migrations ledger.

type BaselinePlan added in v1.0.2

type BaselinePlan struct {
	// Record are migrations in the tree with no ledger row; they become applied.
	Record []Migration
	// Retire are ledger rows whose key the tree no longer carries and which sort
	// below everything it does; they are removed.
	Retire []AppliedRecord
	// Untouched are rows that already agree with the tree.
	Untouched []AppliedRecord
}

BaselinePlan is what Baseline would write, in the order it would write it.

func PlanBaseline added in v1.0.2

func PlanBaseline(applied map[string]AppliedRecord, migrations []Migration) (BaselinePlan, error)

PlanBaseline classifies the ledger against the tree and refuses anything a baseline must not decide on its own. It reads nothing and writes nothing, so a caller can show the plan before asking for it.

func (BaselinePlan) Empty added in v1.0.2

func (p BaselinePlan) Empty() bool

Empty reports whether a baseline would change nothing.

type BaselineRequest added in v1.0.2

type BaselineRequest struct {
	RepairRequest
	// SchemaVerified is the caller's assertion that every object the tree's
	// migrations build is already present. Baseline refuses without it,
	// because a ledger that claims migrations ran over a schema they did not
	// build is worse than the unreconciled ledger it replaces.
	SchemaVerified bool
}

BaselineRequest authorizes a baseline.

type Constraint added in v1.0.1

type Constraint struct {
	Line  int
	Kind  ConstraintKind
	Table string
	Name  string
}

Constraint is one detected statement that must hold over rows the migration did not write. Table is empty when the lexical scan could not resolve it — that is the UNKNOWN case, and it is reported rather than dropped.

type ConstraintKind added in v1.0.1

type ConstraintKind string

ConstraintKind classifies WHY a statement can refuse rows the migration did not write.

const (
	ConstraintCheck       ConstraintKind = "add-check"           // ADD CONSTRAINT ... CHECK, validating
	ConstraintValidate    ConstraintKind = "validate-constraint" // VALIDATE CONSTRAINT <name>
	ConstraintForeignKey  ConstraintKind = "add-foreign-key"     // ADD CONSTRAINT ... FOREIGN KEY, validating
	ConstraintUnique      ConstraintKind = "add-unique"          // ADD CONSTRAINT ... UNIQUE / PRIMARY KEY
	ConstraintUniqueIndex ConstraintKind = "create-unique-index" // CREATE UNIQUE INDEX on a pre-existing table
	ConstraintNotNull     ConstraintKind = "set-not-null"        // ALTER COLUMN ... SET NOT NULL
	ConstraintColumnType  ConstraintKind = "alter-column-type"   // ALTER COLUMN ... TYPE — the cast can fail
	ConstraintNotNullCol  ConstraintKind = "add-not-null-column" // ADD COLUMN ... NOT NULL with no DEFAULT
)

type Conversion added in v1.10.0

type Conversion struct {
	// Name identifies the conversion in errors and in the audit trail.
	Name string
	// Retired renders the retired chain. A database whose ledger records
	// exactly these migrations (or whose ledger is empty but whose schema has
	// exactly their shape) is converted.
	Retired Render
	// Replaces is how many leading migrations of the current chain the
	// converted schema satisfies; they are recorded as applied.
	Replaces int
	// SQL renders the conversion for a schema. It runs under the same
	// search_path and schema relocation as a migration.
	SQL func(schema string) (string, error)
	// Fallback tells an operator what to run instead if the conversion
	// refuses, e.g. "authkit v0.124.0, the last release of that chain".
	Fallback string
}

Conversion upgrades a database built by a retired chain to the leading migrations of the current chain.

type Discrepancy added in v1.0.1

type Discrepancy struct {
	Kind     DiscrepancyKind
	Severity Severity

	// Key is the ledger key (the bare migration number).
	Key string
	// File is the migration file in the tree. Empty for KindLedgerOnly.
	File string
	// LedgerFilename is the filename the ledger recorded, if any.
	LedgerFilename string
	// LedgerDigest is the content digest the ledger recorded, if any.
	LedgerDigest string
	// FileDigest is the digest of the file in the tree, if there is one.
	FileDigest string
	// Blocker is the already-applied migration a KindOrderViolation sorts
	// below.
	Blocker string
	// LedgerStatus is the recorded apply state for a KindDirtyMigration.
	LedgerStatus string
	// LedgerError is the recorded failure cause for a KindDirtyMigration.
	LedgerError string
}

Discrepancy is one disagreement between the migration files and the ledger, with everything an operator needs to resolve it.

func (Discrepancy) Headline added in v1.0.1

func (d Discrepancy) Headline() string

Headline is the one-sentence WHAT.

func (Discrepancy) OneLine added in v1.0.1

func (d Discrepancy) OneLine() string

OneLine is the whole warning in one line: what, the risk, and the verb that resolves it. This is what a log line has room for.

func (Discrepancy) String added in v1.0.1

func (d Discrepancy) String() string

String renders the full operator-facing explanation: what, why, and what to do about it, ending with the pointer at `migratekit status`.

type DiscrepancyKind added in v1.0.1

type DiscrepancyKind string

DiscrepancyKind names a class of ledger/tree disagreement.

const (
	// KindNumberMismatch: the ledger says this number was applied by a
	// different file. Hard error — the file in the tree would never run.
	KindNumberMismatch DiscrepancyKind = "number_applied_by_different_file"
	// KindContentDrift: an applied migration's SQL tokens changed since it ran.
	// It is informational and never blocks applying later migrations.
	KindContentDrift DiscrepancyKind = "applied_migration_edited"
	// KindOrderViolation: a pending migration sorts below one already
	// applied. Error under WithStrictOrdering.
	KindOrderViolation DiscrepancyKind = "pending_sorts_below_applied"
	// KindLedgerOnly: a ledger row with no migration file behind it.
	KindLedgerOnly DiscrepancyKind = "ledger_row_without_file"
	// KindDirtyMigration: a no-transaction migration that failed or was
	// interrupted, so it may be partially applied. Always a hard error.
	KindDirtyMigration DiscrepancyKind = "no_transaction_migration_unfinished"
)

type LoadOption added in v1.0.1

type LoadOption func(*loadConfig)

LoadOption configures Load.

func RequireParentLinks() LoadOption

RequireParentLinks makes a headerless migration an error instead of a warning. Off by default for one minor version so existing consumers keep booting; adopt it once your chain carries headers.

func WithChainWarnFunc added in v1.0.1

func WithChainWarnFunc(fn func(string)) LoadOption

WithChainWarnFunc redirects legacy-tolerance warnings. Defaults to the standard logger.

type Migration

type Migration struct {
	Name    string
	Content string
}

Migration is a single SQL migration

func Load added in v1.0.1

func Load(fsys fs.FS, dir string, opts ...LoadOption) ([]Migration, error)

Load reads migrations and verifies the parent-link chain. LoadFromFS is this with default options.

func LoadFromFS

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

LoadFromFS loads migrations from an embedded filesystem and verifies the parent-link chain with default options (headerless migrations tolerated with a warning). It is Load(fsys, dir) — use Load directly for RequireParentLinks or a custom warning sink. If dir is empty, defaults to "." (root of the filesystem).

type MigrationSource

type MigrationSource struct {
	App string
	FS  fs.FS
	// Schema optionally mirrors Postgres.WithSchema for validation helpers.
	// RewriteFrom carries canonical schema names to rewrite when applying via
	// Postgres.WithSchema(schema, rewriteFrom...).
	Schema      string
	RewriteFrom []string
}

MigrationSource represents a migration source with an app name and a migration filesystem (any fs.FS; embed.FS satisfies it).

type ParentLink struct {
	// Present is false for a headerless (legacy) migration.
	Present bool
	// IsRoot is true for `-- parent: root`.
	IsRoot bool
	// Parent is the normalized ledger key of the parent migration.
	Parent string
	// Digest is the claimed sha256 of the parent's canonical body.
	Digest string
}

ParentLink is the parsed `-- parent:` header of a migration.

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 NewPostgresFromPGXPool added in v1.0.1

func NewPostgresFromPGXPool(pool *pgxpool.Pool, app string) (*Postgres, error)

NewPostgresFromPGXPool creates a migration handle from a host pgx pool without using or mutating that pool. Migrations use connection-local session state (search_path, lock_timeout, statement_timeout) and pin one connection for the advisory lock while applying on another, so sharing the host pool could leak migration state into ordinary application queries.

The returned Postgres owns a separate *sql.DB. Call Close when the migration or validation operation is complete. The pool's connection configuration is copied; BeforeConnect and AfterConnect are preserved, while the host pool's acquisition/release lifecycle is not involved.

func (*Postgres) Applied

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

Applied returns list of applied migration names

func (*Postgres) AppliedRecords added in v1.0.1

func (p *Postgres) AppliedRecords(ctx context.Context) (map[string]AppliedRecord, error)

AppliedRecords returns the applied-migrations ledger keyed by ledger key.

func (*Postgres) ApplyMigrations

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

ApplyMigrations applies all unapplied migrations (only locks if needed). Tracker initialization runs first under migratekit's bootstrap lock, so there is no missing-table special case or caller-owned setup retry loop.

func (*Postgres) ApplyWithOrderingException added in v1.0.1

func (p *Postgres) ApplyWithOrderingException(ctx context.Context, migrations []Migration, allowBelow []string, req RepairRequest) error

ApplyWithOrderingException applies the chain while exempting the named migrations from the strict-ordering rule — the one-shot escape for a genuine backport that must land below the high-water mark. allowBelow entries may be bare numbers or filenames. Every exemption that actually applies a migration records an audit row.

The identity checks are NOT relaxed: this exempts ordering only.

func (*Postgres) Baseline added in v1.0.2

func (p *Postgres) Baseline(ctx context.Context, migrations []Migration, req BaselineRequest) (results []RepairResult, err error)

Baseline records the tree's migrations as applied and removes ledger rows the tree has retired, in one transaction, without running any DDL. Like the repair verbs it requires a reason, refuses to run in CI, and writes an audit row per change. Unlike them it takes the migration lock before it reads, because it decides the same rows ApplyMigrations does.

func (*Postgres) Close

func (p *Postgres) Close() error

Close releases the dedicated database handle owned by NewPostgresFromPGXPool. It never closes a *sql.DB supplied to NewPostgres.

func (*Postgres) RepairAcceptContent added in v1.0.1

func (p *Postgres) RepairAcceptContent(ctx context.Context, m Migration, req RepairRequest) (RepairResult, error)

RepairAcceptContent re-stamps the recorded digest of an applied migration after a verified edit. It is the "I looked at the diff and it is cosmetic" verb: it acknowledges the content-drift warning and silences it, on the record. It refuses when the ledger names a DIFFERENT file, because that is an identity problem and `repair adopt` is the verb that says so.

func (*Postgres) RepairAdopt added in v1.0.1

func (p *Postgres) RepairAdopt(ctx context.Context, m Migration, req RepairRequest) (RepairResult, error)

RepairAdopt binds the file in the tree as the applied identity for its ledger key: filename and content digest both become this file's.

This is the verb for a ledger that is the WRONG SIDE — a restored backup, a database adopted from somewhere else, a row somebody fixed by hand. The files are right and the ledger remembers a tree that no longer exists. It is NOT the verb for a lane collision: if two files genuinely claim one number, adopting one of them silently drops the other's DDL, which is exactly the failure this package exists to prevent. Read the diff first.

func (*Postgres) RepairAdoptAllUnmatched added in v1.0.1

func (p *Postgres) RepairAdoptAllUnmatched(ctx context.Context, migrations []Migration, req RepairRequest) ([]RepairResult, error)

RepairAdoptAllUnmatched adopts EVERY ledger row whose identity disagrees with the file in the tree that carries its number. This is the restored- backup shape: one dump, one restore, and every row mismatches at once. Rows that already match, and rows with no file behind them, are left alone.

func (*Postgres) RepairHistory added in v1.0.1

func (p *Postgres) RepairHistory(ctx context.Context) ([]RepairRecord, error)

RepairHistory returns this app's repair audit trail, newest first.

func (*Postgres) RepairResolve added in v1.0.1

func (p *Postgres) RepairResolve(ctx context.Context, m Migration, mode ResolveMode, req RepairRequest) (RepairResult, error)

RepairResolve clears a dirty ledger row. Like every repair verb it requires a reason, refuses to run in CI, writes an audit row in the same transaction as the change, and never touches a schema — resolving is the operator's statement about work they did by hand, not a re-run of the DDL.

func (*Postgres) Status added in v1.0.1

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

Status reports the ledger, the pending set, every discrepancy and the repair history for this app. It is read-only apart from ensuring the tracking tables exist, and it never fails on a discrepancy — reporting one is the whole point.

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) WithConversions added in v1.10.0

func (p *Postgres) WithConversions(conversions ...Conversion) *Postgres

WithConversions declares retired chains this migrator can convert from.

func (*Postgres) WithRender added in v1.10.0

func (p *Postgres) WithRender(render Render) *Postgres

WithRender declares how the current chain renders for another schema. It is needed only when migrations are hard-qualified to the migrator's schema; schema-relative migrations relocate by search_path alone.

func (*Postgres) WithSchema

func (p *Postgres) WithSchema(schema string, rewriteFrom ...string) *Postgres

WithSchema configures the schema to target for migrations.

Unqualified DDL/DML runs under:

SET LOCAL search_path = "<schema>", public

Passing one or more canonical schema names also rewrites those schema references in migration SQL before execution. This lets apps author portable hard-qualified DDL against a default schema and relocate it at runtime:

migratekit.NewPostgres(db, "app").WithSchema(cfg.Schema, "openrails")

Tracking always stays in public.migrations.

func (*Postgres) WithStrictIntegrity added in v1.10.0

func (p *Postgres) WithStrictIntegrity() *Postgres

WithStrictIntegrity refuses to apply migrations over an applied migration whose content changed, unless the live schema still equals a fresh build of the tree's applied migrations. An edit that changes nothing a database does (a comment, formatting, an index rename) is re-stamped on the record and the apply proceeds; an edit that changes the schema refuses with the migration, the schema diff and the way out. Without it, content drift is a warning.

func (*Postgres) WithStrictOrdering added in v1.0.1

func (p *Postgres) WithStrictOrdering() *Postgres

WithStrictOrdering refuses to apply a pending migration that sorts below a migration already applied. Off by default; see the ordering note above.

func (*Postgres) WithWarnFunc added in v1.0.1

func (p *Postgres) WithWarnFunc(fn func(Discrepancy)) *Postgres

WithWarnFunc replaces the sink for warning-severity discrepancies. The default logs them through slog.Default() at warn level. A warning nobody sees is the failure mode this whole package exists to prevent, so the sink is never silent by default — pass func(Discrepancy){} to silence it deliberately.

type RelinkChange added in v1.0.1

type RelinkChange struct {
	File string
	From string // the previous header line, empty when there was none
	To   string // the header line it now carries
}

RelinkChange is one parent line Relink rewrote (or would rewrite).

func Relink(dir string, opts RelinkOptions) ([]RelinkChange, error)

Relink rewrites each migration's parent line to the digest of the file that actually precedes it, and stamps root on the lowest-numbered one. It reads and writes files and nothing else — no database, no context, no DSN.

type RelinkOptions added in v1.0.1

type RelinkOptions struct {
	// DryRun computes the changes without writing them.
	DryRun bool
	// From limits rewriting to migrations numbered at or above this prefix,
	// so a lane can rebase only its own new files.
	From string
}

RelinkOptions configures Relink.

type Render added in v1.10.0

type Render func(schema string) ([]Migration, error)

Render renders a migration chain for a target schema. Migrations that are schema-relative (they run under search_path) can return their content unchanged; hard-qualified migrations relocate their qualifiers. Rendered for the migrator's own schema, a chain must reproduce the ledger's digests.

type RepairRecord added in v1.0.1

type RepairRecord struct {
	ID          int64
	App         string
	Schema      string
	Key         string
	Verb        string
	Reason      string
	Operator    string
	OSUser      string
	Host        string
	OldFilename string
	OldDigest   string
	NewFilename string
	NewDigest   string
	At          time.Time
}

RepairRecord is one row of the repair audit trail.

func (RepairRecord) Line added in v1.0.1

func (r RepairRecord) Line() string

Line renders one audit row for a terminal.

type RepairRequest added in v1.0.1

type RepairRequest struct {
	// Reason is required and recorded verbatim. "Why" is the only part of a
	// repair a future reader cannot reconstruct.
	Reason string
	// Operator optionally names the human, alongside the OS user the audit
	// row records anyway.
	Operator string
	// DryRun computes and returns the repair without writing anything.
	DryRun bool
}

RepairRequest is the operator's authorization for a repair.

type RepairResult added in v1.0.1

type RepairResult struct {
	Verb        string
	Key         string
	OldFilename string
	OldDigest   string
	NewFilename string
	NewDigest   string
	DryRun      bool
}

RepairResult describes one ledger identity row before and after a repair.

func (RepairResult) String added in v1.0.1

func (r RepairResult) String() string

type ResolveMode added in v1.0.1

type ResolveMode string

ResolveMode is how an operator disposes of a migration left dirty by a failed or interrupted no-transaction apply.

const (
	// ResolveApplied marks it done: the operator finished or verified the work.
	ResolveApplied ResolveMode = "applied"
	// ResolveRerun deletes the row so the next boot runs the migration again.
	// Only safe when the migration is idempotent.
	ResolveRerun ResolveMode = "rerun"
)

type Severity added in v1.0.1

type Severity string

Severity ranks a discrepancy.

const (
	// SeverityError blocks the apply.
	SeverityError Severity = "error"
	// SeverityWarning is reported and logged; the apply proceeds.
	SeverityWarning Severity = "warning"
	// SeverityInfo is worth an operator's attention but is not a defect.
	SeverityInfo Severity = "info"
)

type Status added in v1.0.1

type Status struct {
	App    string
	Schema string
	// Applied is the ledger, in apply order.
	Applied []AppliedRecord
	// Pending names the migrations that have not run.
	Pending []string
	// Discrepancies is every disagreement, most severe first.
	Discrepancies []Discrepancy
	// Repairs is the audit trail, newest first.
	Repairs []RepairRecord
}

Status is the whole picture for one app: what is applied, what is pending, what disagrees, and every repair anybody has ever run here.

func (Status) HasErrors added in v1.0.1

func (s Status) HasErrors() bool

HasErrors reports whether any discrepancy would block an apply.

func (Status) Report added in v1.0.1

func (s Status) Report() string

Report renders Status for a terminal.

Directories

Path Synopsis
Package chmigrate is migratekit's ClickHouse migration driver.
Package chmigrate is migratekit's ClickHouse migration driver.
cmd
migratekit command
Command migratekit is the operator's side of migration identity: it explains a boot refusal and resolves it.
Command migratekit is the operator's side of migration identity: it explains a boot refusal and resolves it.
internal
coremigrate
Package coremigrate holds implementation primitives shared by migratekit's Postgres migrator (the root package) and its ClickHouse migrator (migratekit/chmigrate): the migrations-tracking table DDL and environment template substitution.
Package coremigrate holds implementation primitives shared by migratekit's Postgres migrator (the root package) and its ClickHouse migrator (migratekit/chmigrate): the migrations-tracking table DDL and environment template substitution.

Jump to

Keyboard shortcuts

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