Skip to content

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