2026-08-25 — Migration consolidation + boot-time fix
Brief mark, not a full changelog — see code comments for detail.
Branch: feature/remove-runmigrations (CoreAPI), pushed, not yet merged.
Removed
- RunMigrations() (pkg/db/migrate.go) — a second, separate migration
path with its own hardcoded ~61-model list, running before
RunMigrationMain(). Both ran identical self-heal + AutoMigrate
logic against the same connection, so the split was pure duplication.
All models now migrate through RegisterModels/RunMigrationMain only.
Verified via full pg_dump schema comparison (101 tables, byte-identical
before/after) and a background-agent audit of every call site between
db.Init() and RunMigrationMain() for ordering hazards — none found.
Fixed — migration boot time
- Self-heal's per-column existence check (Migrator().HasColumn(), one
round trip per field) replaced with one batched query per table
(existingColumnSet). Query count for self-heal: ~1,500 → ~171.
- DisableForeignKeyConstraintWhenMigrating: true — no model in pkg/db
declares a foreignKey/references tag, so GORM's per-model
FK-constraint checks during AutoMigrate never had anything to do.
- New schema_fingerprint table: a checksum of every registered model's
field shape (name, type, every raw GORM tag — sorted by key, so
check:/index/uniqueIndex/etc. all count, not just a hand-picked
subset). If it matches what's stored, RunMigrationMain() skips the
entire self-heal + AutoMigrate pass outright. Fails open on any error
(never silently skips on uncertainty); FORCE_SCHEMA_CHECK=true
bypasses it on demand.
Measured against the actual shared remote dev DB that motivated this
(139.59.253.119, the case where round-trip latency turns O(models)
queries into real wall-clock time):
| Before | After | |
|---|---|---|
| Cold boot (schema actually changed, or first boot ever) | ~111s | ~85s |
| Warm restart (nothing changed) | ~111s (ran every time, unconditionally) | ~1.5s |
Verified the fingerprint is actually sensitive to real changes, not a
rubber-stamp: edited a check: constraint's allowed values and separately
added a new field to a live model — both correctly failed the fingerprint
match and ran the full migration (confirmed via a real ALTER TABLE ADD
COLUMN executing for the added-field case), then reverted both test edits.
Not done, deliberately deferred
- Moving off AutoMigrate to versioned migrations (golang-migrate/
goose/atlas) — the actual structural fix; this session's changes
are the stopgap to make the current approach tolerable while that's
scoped separately.
- The remaining ~76s (of the original ~111s) that's GORM's own
AutoMigrate diffing (per-column pg_description lookups) is untouched
by any of this — it only shows up now on the first/cold boot case,
which is accepted as tolerable (~2 min) per explicit product decision.
See: Migrations guide