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