migratekit

package module
v1.0.1 Latest Latest
Warning

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

Go to latest
Published: Sep 19, 2026 License: MIT Imports: 23 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 (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", 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. Since v1.6.0 there is:

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

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)

Everything in this section is the v1 compatibility boundary. Within v1.x it will only grow — no removals, no signature changes, no breaking behavior changes to the documented contracts below. Anything NOT listed here (unexported helpers, exact error message text, internal locking mechanics) is an implementation detail and may change in any release.

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 Normalized numeric prefix of a migration filename ("001_x.up.sql" → "1"). This is the tracking key stored in public.migrations.name.
CheckChain(names []string) error (v1.5.0) 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 (v1.5.0) The sha256 the ledger records for a migration. Since v1.7.0 it hashes the CANONICAL BODY — the file with its own -- parent: header removed — so adding a parent line to an applied migration changes no ledger digest. A headerless file hashes exactly as it did in v1.5.0.
SemanticContentDigest(content string) string (v1.8.0) 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 ledger row. SemanticDigest is empty for rows written before v1.8.0 and is backfilled when the legacy raw digest still matches.
Load(fsys fs.FS, dir string, opts ...LoadOption) ([]Migration, error) (v1.7.0) LoadFromFS plus options. RequireParentLinks() makes a headerless migration an error; WithChainWarnFunc(fn) redirects the tolerance warnings.
VerifyChain(migrations []Migration, opts ...LoadOption) error (v1.7.0) The parent-link check on an already-loaded chain.
CheckChainFS(fsys fs.FS, dir string, requireLinks bool) error (v1.7.0) 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 (v1.7.0) Refuses a constraint over pre-existing data that carries no repair. Pure file analysis.
Relink(dir string, RelinkOptions) ([]RelinkChange, error) (v1.7.0) 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/upgrades 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 migration names (normalized prefixes) for this app, database='postgres'.
(*Postgres) ValidateAllApplied(ctx, []Migration) error Read-only startup gate: error naming pending migrations, never creates tables.
(*Postgres) WithStrictOrdering() *Postgres (v1.5.0) Refuse a pending migration that sorts below one already applied. Opt-in.
(*Postgres) AppliedRecords(ctx) (map[string]AppliedRecord, error) (v1.5.0) Ledger keyed by tracking key, carrying the recorded filename and content digest.
(*Postgres) WithWarnFunc(func(Discrepancy)) *Postgres (v1.6.0) 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) (v1.6.0) Applied set, pending set, every discrepancy with cause and resolution, and the repair history. Read-only apart from Setup.
(*Postgres) RepairAdopt(ctx, Migration, RepairRequest) (RepairResult, error) (v1.6.0) 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) (v1.6.0) The same for every mismatched row at once — the restored-backup shape.
(*Postgres) RepairAcceptContent(ctx, Migration, RepairRequest) (RepairResult, error) (v1.6.0) 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 (v1.6.0) Apply with a one-shot exemption from the ordering rule. Identity checks are not relaxed.
(*Postgres) RepairHistory(ctx) ([]RepairRecord, error) (v1.6.0) The audit trail, newest first.
type RepairRequest struct { Reason, Operator string; DryRun bool } (v1.6.0) Reason is required. Every repair refuses under CI (DetectCI).
type Status, type Discrepancy, type RepairResult, type RepairRecord, Severity, DiscrepancyKind (v1.6.0) Reporting types. Discrepancy.String() is the full explanation; OneLine() is the log-line form.
DetectCI() (string, bool) (v1.6.0) Names the CI environment variable that is set, if any.
ClickHouse (migratekit/chmigrate, since v1.4.0)

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 } PostgresDB is 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) Recorded names for this app, database='clickhouse'.
(*ClickHouse) Setup(ctx) error Ensures the Postgres tracker is ready.
(*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 are part of the API and will not change within v1.x:

  1. Tracking table: public.migrations (id, app, database, schema, name, filename, content_sha256, semantic_sha256, status, error, migrated_at, UNIQUE(app, database, schema, name)) with database ∈ {postgres, clickhouse}. Since v1.6.0 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.
Added in v1.5.0 (migration identity)

The applied-migrations ledger was keyed by the migration NUMBER alone, which is not an identity: two different files that each claim number N normalize to the same key, so once one is applied the other is reported "already applied" and its DDL never runs — no error, clean boot, green suite. LoadFromFS rejects two colliding files in one tree, but the damaging case never has both in one tree: lane A's N is applied to a live database, lane B renumbers or reverts, and B's N is skipped forever.

v1.5.0 records filename and content_sha256 next to the key and checks them on every apply:

  • Identity — number N applied by a different file is a hard error naming both files and demanding a renumber.
  • Integrity — a migration edited after it ran is a hard error.

Both are always on and cannot fire spuriously: rows written by ≤v1.4.0 carry no identity, and unknown reads as unknown, never as a mismatch. The two new columns are added by Setup(); no consumer action is required.

  • Ordering — WithStrictOrdering() additionally refuses a pending migration that sorts below one already applied. Opt-in, because existing chains legitimately carry gaps and late arrivals that predate the rule.
  • CheckChain(names) validates a chain as a plain file listing (duplicates, gaps, monotonicity) with no database, for a CI gate on the merge boundary.

Execution is unchanged: appliers are serialized by the advisory lock and migrations run one at a time, in order.

Changed in v1.6.0 (the resolution path)

v1.5.0 made three failures visible. It did not make any of them resolvable: the boot refused, named the problem, and left the operator with psql. v1.6.0 is the other half — status, four audited repair verbs, and one relaxation.

Content drift is now a WARNING, not a boot error. An operator who edited a migration that already ran often cannot restore the old bytes, and a database held down over a comment change is a worse outcome than the divergence the check guards against. The boot proceeds and emits a warning naming the file, both digests, the risk, and repair accept-content. Content drift has no boot-refusal mode: stopping the application cannot repair either the migration file or the already-applied schema.

The other two are unchanged. A number applied by a different file is still a hard error — it is the silent-never-runs killer, and its fix (renumber, or adopt) is always available. WithStrictOrdering() behaves exactly as before.

Everything else is additive: Status, WithWarnFunc, the repair verbs, the public.migration_repairs audit table (created by Setup), and the cmd/migratekit CLI. No consumer action is required, and no call site changes.

Parent-hash links. Every migration carries its parent as its first non-blank line:

-- parent: 5 sha256:9f2c…e1

and the first migration of the chain carries -- parent: root. The chain is verified at LOAD — pure file reading, no database — so boot and CI both inherit it, and there is no second verification point to keep in sync.

Two things follow. An ordering conflict becomes structurally impossible: the lane that merges second has a parent line pointing at a file that is no longer its predecessor, which is a deterministic refusal in its own PR rather than a boot refusal in production. And the directory becomes a hash chain: tampering with history breaks every later link, which is atlas.sum's property with no sum file to maintain.

Refused, each naming both files: a missing parent, a hash mismatch, two files claiming the same parent, two roots, a root that is not the lowest-numbered file, a link that points forward, and a link that skips the immediate predecessor (the stale-after-renumber shape).

The digest excludes the parent line. ContentDigest hashes the canonical body — the file with its own header removed — and that is also what content_sha256 stores. Adding parent lines to already-applied migrations therefore changes no ledger digest: adoption is silent, every digest v1.5.0 wrote is still correct, and there is nothing to backfill.

Changed in v1.8.0 (warning-only semantic drift)

Content drift is now informational in every API and CLI path; the strict-content escape hatch is removed. semantic_sha256 records a PostgreSQL token digest alongside the historical byte digest. Existing rows are upgraded automatically when their old digest still matches, after which comments, whitespace, and unquoted-identifier case do not produce warnings. Real token changes still warn with both raw digests and the audited repair accept-content path. Identity collisions, ordering violations, and unfinished no-transaction migrations remain hard errors.

Renumbering is now git mv PLUS updating your parent line. The error message says so, and migratekit relink does it in one command:

migratekit relink -dir migrations/postgres            # fix the parent lines
migratekit relink -dir migrations/postgres --check    # CI: exit 1 if any are stale

relink is an AUTHORING verb and deliberately does NOT carry the repair verbs' guardrails — no --reason, no audit row, no CI refusal. The repair verbs mutate a production ledger: state that is invisible, shared, and has no history of its own. relink mutates files, and files already have an audit log — git. A --reason would duplicate the commit message, an audit table cannot record a change that may never be committed, and refusing to run in CI would be wrong because relink --check is the CI gate.

A squash resets the chain root, with no special case in the code: a squash deletes the files it replaces, so the squash file is the lowest-numbered file present and legitimately carries -- parent: root. One rule — exactly one root, and it must be the lowest-numbered file — covers squashes and ordinary chains alike.

Adoption. Headerless migrations are tolerated with a warning for one minor version. Mixed chains work: a headed file verifies against a headerless parent (hashing a parent does not require the parent to have a header), so a repo adopts by heading its newest file and letting the rest follow. Load(fsys, dir, RequireParentLinks()) makes a missing header an error. The default flips no earlier than v1.8.0.

-- migratekit:no-transaction. Migrations are transactional by default and the ledger row commits with the DDL. CREATE INDEX CONCURRENTLY — the only index build that does not take an ACCESS EXCLUSIVE lock on a live table — cannot live inside that, so a migration whose leading comment block carries the directive runs outside a transaction, one statement at a time, still under the advisory lock.

Such a migration may be PARTIALLY applied, and no design makes it atomic. So the ledger holds the state instead of hiding it: running before it executes, applied on success, failed with the Postgres error text on failure, and still running if the process dies. Boot refuses on any row that is not applied — not absent, which would silently re-run half-applied DDL, and not applied, which would silently skip the other half. The operator clears it with the audited migratekit repair resolve N --applied|--rerun --reason "…".

Repair totality. A constraint added over a table that already has rows is a bet that the rows comply, and when the bet loses it fails on a live database mid-boot. So:

A constraint over a PRE-EXISTING table must be preceded, in the same file, by either the repair DML that makes the data satisfy it, or -- Repair: none-needed <reason>.

Position is the rule, not a detail: a repair below the constraint runs after the constraint has already refused the rows. A table CREATE TABLEd in the same file is exempt — no pre-existing row can exist. Detection is a conservative lexical scan of the closed set of DDL that can refuse stored rows (ADD CONSTRAINT … CHECK / FOREIGN KEY / UNIQUE, VALIDATE CONSTRAINT, CREATE UNIQUE INDEX, SET NOT NULL, ALTER COLUMN … TYPE, ADD COLUMN … NOT NULL with no default); when it cannot resolve which table a statement targets it reports UNKNOWN and requires the waiver rather than guessing.

Measuring the real data is how you choose the repair — delete versus backfill is a semantic call you cannot make without looking at the rows — but nothing requires or parses a measurement. CheckRepairTotality enforces the repair.

migratekit check -dir migrations/postgres --require-links   # numbering + links + repair rule
Removed in v1.0 (was public in v0.x)
  • Postgres.Apply, Postgres.Lock, Postgres.Unlock, ClickHouse.Apply, ClickHouse.Lock, ClickHouse.Unlock — the manual lock/apply path bypassed the applied-set check and made stateful locking part of the surface; ApplyMigrations is the supported path. (No known consumer used these.)
  • Postgres.Close — closed the caller's *sql.DB, which the migrator never owned.
  • MigrationSource.FS and the ValidateClickHouseMigrations filesystem parameter are now fs.FS instead of embed.FS (source-compatible: embed.FS satisfies fs.FS).
Changed in v1.4.0 (ClickHouse moved to its own subpackage)

Everything ClickHouse-related moved from the root package into migratekit/chmigrate, to get github.com/ClickHouse/clickhouse-go/v2 (and its ch-go dependency) out of every Postgres-only consumer's build:

  • migratekit.ClickHouseConfig → chmigrate.Config (same fields).
  • migratekit.NewClickHouse → chmigrate.New (same signature, returns *chmigrate.ClickHouse).
  • migratekit.ClickHouse → chmigrate.ClickHouse (same methods).
  • migratekit.ValidateClickHouseMigrations → chmigrate.ValidateMigrations.

Migration for existing ClickHouse consumers: change the import to add "github.com/open-rails/migratekit/chmigrate", and replace migratekit.NewClickHouse(&migratekit.ClickHouseConfig{...}) with chmigrate.New(&chmigrate.Config{...}). migratekit.Migration and migratekit.Prefix are unchanged and still used for migration content.

Postgres-only consumers need no code changes; running go mod tidy is enough to drop clickhouse-go/ch-go from go.mod/go.sum.

Compatibility policy
  • v1.x releases may add symbols, struct fields with useful zero values, and optional behavior — never remove or change what is documented above.
  • Exact error message text is not part of the API; only the documented error conditions are. (Typed sentinel errors may be added additively later.)
  • The module follows Go module semver: any future break means v2 with a new import path. The bar for v2 is intentionally very high.

Schema

migratekit creates two tables in the public schema on first Setup():

CREATE TABLE public.migrations (
    id BIGSERIAL PRIMARY KEY,
    app TEXT NOT NULL,
    database TEXT NOT NULL,
    schema TEXT NOT NULL DEFAULT '',
    name TEXT NOT NULL,             -- the ledger KEY: Prefix(filename)
    filename TEXT,                  -- v1.5.0 identity; NULL on older rows
    content_sha256 TEXT,            -- v1.5.0 integrity; NULL on older rows
    semantic_sha256 TEXT,           -- v1.8.0 token digest; NULL until upgraded
    status TEXT NOT NULL DEFAULT 'applied',  -- v1.7.0: applied | running | failed
    "error" TEXT,                   -- v1.7.0: why a no-transaction apply failed
    migrated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE(app, database, schema, name)
);

-- v1.6.0: every `migratekit repair` lands here, in the same transaction as the
-- identity change it made. The checks are only worth having if there is a way
-- past them; this is what keeps that way honest.
CREATE TABLE public.migration_repairs (
    id BIGSERIAL PRIMARY KEY,
    app TEXT NOT NULL,
    database TEXT NOT NULL,
    schema TEXT NOT NULL DEFAULT '',
    name TEXT NOT NULL,             -- the 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: 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"

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 (v1.7.0): 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. Headerless files are tolerated with a warning for one minor version.

Repair rule (v1.7.0): 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.

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 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.

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 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 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 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 ledger key — Prefix(filename).
	Key string
	// Filename is the full migration filename that claimed Key. Empty for
	// rows written before v1.5.0.
	Filename string
	// Digest is the sha256 of the applied migration's content. Empty for
	// rows written before v1.5.0.
	Digest string
	// SemanticDigest hashes SQL tokens while ignoring comments and formatting.
	// Empty for rows written before v1.8.0.
	SemanticDigest string
	// Status is the apply state: applied, running or failed. Empty for rows
	// written before v1.7.0, which are applied by construction — only a
	// no-transaction migration can be anything else.
	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 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 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). Setup() always 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) 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) 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) 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 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