migrationguard

package
v0.0.0-...-81c9f84 Latest Latest
Warning

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

Go to latest
Published: Sep 9, 2026 License: AGPL-3.0 Imports: 14 Imported by: 0

Documentation

Overview

Package migrationguard detects migrations whose content changed after they were applied.

bun keys applied migrations on the numeric filename prefix alone. Editing an already-applied migration file — renumbering it, or rewriting it in place while a dev loop has already run the earlier draft — therefore does NOT re-run it: the new statements never execute, `bun_migrations` still claims the migration is applied, and the database silently diverges from the schema the code was written against. That has now bitten this repository twice (spec 2026-08-18-02 and the earlier migration-consolidation desync), each time surfacing hours later as per-query WARNs and half-working features rather than as a startup failure.

The guard closes that gap: it records a content checksum for every applied migration in a side table and compares it on every boot. What happens on a mismatch depends on the configured Mode:

  • ModeStrict (the default, and the only sane choice in production) fails startup with a message naming the migration and the repair options, and writes nothing to the checksum table while any mismatch exists — a database that is already diverged must not have its record quietly updated.
  • ModeWarn logs the mismatch and lets the boot continue. It exists for local development, where editing a comment in an already-applied migration is common and must not brick the dev database. Unlike strict mode, warn mode still backfills rows with no recorded checksum even while other rows mismatch — the mismatched row's own checksum is never touched by either mode, which is what makes the warning recur until an operator repairs deliberately (`solidping migrate repair`).

Checksums cover the `.up.sql` half only. A `.down.sql` file never runs during a forward boot, so editing one cannot desync an applied schema, and treating it as part of the identity would trip the guard on harmless teardown edits.

Index

Constants

View Source
const ChecksumTable = "migration_checksums"

ChecksumTable is the side table the guard owns. It is intentionally NOT a column on `bun_migrations`: that table is bun's, created from bun's own model, and adding a column to it would be silently dropped the next time bun changes the model.

View Source
const DefaultTable = "bun_migrations"

DefaultTable is bun's migrations table, which the guard reads to learn which migrations a database claims to have applied.

Variables

View Source
var ErrChecksumMismatch = errors.New("migration content changed after it was applied")

ErrChecksumMismatch is the sentinel every content-drift failure wraps, so callers can distinguish "this database diverged" from an I/O error.

Functions

func Checksums

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

Checksums walks dir inside fsys and returns one entry per `NNN_name.up.sql` file, keyed by the numeric prefix.

func LogMismatches

func LogMismatches(ctx context.Context, mismatches []*MismatchError)

LogMismatches warn-logs each mismatch once. Callers running the standard pre-migrate/post-migrate Reconcile pair must invoke this only after the FIRST call: the second call sees the same unrepaired rows (repair is never implicit — see Guard.Repair) and would otherwise double the warning.

Types

type Guard

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

Guard verifies applied migrations against the checksums of the migration sources this binary carries.

func New

func New(db *bun.DB, expected map[string]Migration, extra ...Migration) *Guard

New builds a guard over the given expected migrations. Additional entries (Go migrations, which have no file to hash) are merged in on top of the file-derived set.

func (*Guard) Reconcile

func (g *Guard) Reconcile(ctx context.Context, mode Mode) ([]*MismatchError, error)

Reconcile creates the checksum table if needed, then for every migration the database claims to have applied either records its checksum (first boot on a database that predates the guard) or compares it, returning any mismatches as data rather than an error — the error return is reserved for genuine I/O/SQL failures, so callers can classify. Applying the resulting mode policy (see Mode.Apply) is the caller's job, which keeps Reconcile itself policy-free.

It is safe — and intended — to call this both before and after running migrations: the call before verifies (and backfills) what was already applied, the call after records what was just applied.

Whether a mismatch blocks the write of newly-discovered (never-recorded) rows depends on mode: ModeStrict reports drift before writing anything, so a database that is already diverged must not have its record quietly updated — not even for unrelated rows. ModeWarn keeps backfilling those unrelated rows regardless, which is what lets a dev database recover the rest of its checksum history while one mismatched row keeps warning every boot. Either way, a mismatched row's own checksum is never part of the write set — only Guard.Repair may change it.

func (*Guard) Repair

func (g *Guard) Repair(ctx context.Context) ([]RepairResult, error)

Repair is the explicit, operator-invoked exception to the "never overwrite a mismatched row" rule that both Reconcile modes honor: for every migration this binary ships and the database claims to have applied, it inserts a missing checksum row or updates a drifted one to the current file checksum (refreshing Comment and RecordedAt too). Migrations this binary no longer ships are left alone, same as Reconcile. It never touches bun_migrations and never runs a migration — only migration_checksums changes.

type Migration

type Migration struct {
	Name     string
	Comment  string
	Checksum string
}

Migration is one migration's identity plus the checksum of its content.

type MismatchError

type MismatchError struct {
	// Name is the numeric prefix bun keys on (e.g. "013").
	Name string
	// Comment is the informational half of the filename (e.g. "v0_16_0"),
	// empty for a migration that no longer names itself.
	Comment string
	// Recorded is the checksum stored when the migration was applied.
	Recorded string
	// Current is the checksum of the file as it exists now.
	Current string
}

MismatchError reports one applied migration whose file content no longer matches what was recorded when it was applied.

func (*MismatchError) Error

func (e *MismatchError) Error() string

func (*MismatchError) Unwrap

func (e *MismatchError) Unwrap() error

type Mode

type Mode string

Mode controls what Reconcile does with a mismatch: fail the boot (Strict, the default) or log and continue (Warn). See the package doc comment.

const (
	// ModeStrict fails the boot on any checksum mismatch and is the default —
	// production must never silently tolerate schema drift.
	ModeStrict Mode = "strict"
	// ModeWarn logs mismatches and lets the boot continue. Intended for local
	// development only.
	ModeWarn Mode = "warn"
)

func (Mode) Apply

func (mode Mode) Apply(mismatches []*MismatchError) error

Apply turns a Reconcile mismatch list into the mode's boot policy: strict mode (the default for any value other than ModeWarn, including the zero value) returns the joined mismatches as a boot-failing error; warn mode never blocks.

type RepairResult

type RepairResult struct {
	Name    string
	Comment string
	Old     string
	New     string
}

RepairResult reports one migration whose checksum record Repair changed. Old is empty for a backfill (a migration with no prior recorded row); it is the previous (stale) checksum for a row that had drifted.

Jump to

Keyboard shortcuts

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