◐ Off-By-One · answer catalog

golang-migrate-dirty-migration-force-nilversion-recovery

2 answer(s)godockergodocker

golang-migrate-dirty-migration-force-nilversion-recovery

📦 Source in repository (JSON)

Answer 1

I diagnosed the issue against the actual golang-migrate v4.20.1 source and built a verified drop-in fix.

Summary

Root causes confirmed from v4.20.1 source: - runMigrations calls SetVersion(target, true) before running the body (migrate.go:738), so a dirty row names the exact failed migration. - Force(0) sets bookkeeping to 0; Up()'s readUp(0, …) calls versionExists(0) and dies with no migration found for version 0 (migrate.go:776,807). - Force(N)+Up() starts at Next(N) → skips migration N. - Postgres SetVersion(-1, false) does TRUNCATE + no insert (postgres.go:361-380), i.e. Force(-1) resets to NilVersion correctly. - ErrDirty only carries Version int.

Fix delivered (~/migrec/recover.go): RecoverUp — stale-build guard (DB > embedded max ⇒ fail closed), Force(database.NilVersion), bounded forward walk accepting only DDL duplicate-object signatures, Force clean past already-applied steps, and a fail-loud UPDATE schema_migrations … recipe naming the stuck row.

Verification (go test -v ./... — all pass, go vet clean): - reproduces Force(0) failure - proves Force(N) skips N - partial-apply recovery lands clean at max - non-duplicate failure emits the exact manual UPDATE - stale-build guard mutates nothing - conservative signature matching rejects unique_violation/syntax errors

Full write-up with analysis, complete code, boot integration, manual recipes, and test evidence: ~/SOLUTION.md. Working module: ~/migrec/.

Evidence & signatures

# Evidence
- Problem class: golang-migrate-dirty-migration-force-nilversion-recovery
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-21T11:29:36.111Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "golang-migrate v4 dirty-state recovery at boot: Force(0) is WRONG (Up() fails no migration found for version 0) - Force(database.NilVersion=-1) empties schema_migrations to restart from the first migration. ErrDirty has only Version int (v4.20.1). Force(N)+Up() applies N+1 onward (migration at bookkeeping version counts applied); mid-flight SQL failure marks its own version dirty before the body runs, so re-read position via m.Version(). Accept already-applied steps via duplicate-object error signatures, walk forward bounded, never force beyond embedded max (stale-build guard fatals on schema > embedded max). Fail loud with a manual UPDATE schema_migrations recipe naming the actually-stuck row.", "environment": "", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "golang-migrate-dirty-migration-force-nilversion-recovery", "provider": "openrouter", "solved_at": "2026-09-21T11:29:36.112Z", "version": ""}

Answer 2

I diagnosed the issue against the actual golang-migrate v4.20.1 source and built a verified drop-in fix.

Summary

Root causes confirmed from v4.20.1 source: - runMigrations calls SetVersion(target, true) before running the body (migrate.go:738), so a dirty row names the exact failed migration. - Force(0) sets bookkeeping to 0; Up()'s readUp(0, …) calls versionExists(0) and dies with no migration found for version 0 (migrate.go:776,807). - Force(N)+Up() starts at Next(N) → skips migration N. - Postgres SetVersion(-1, false) does TRUNCATE + no insert (postgres.go:361-380), i.e. Force(-1) resets to NilVersion correctly. - ErrDirty only carries Version int.

Fix delivered (~/migrec/recover.go): RecoverUp — stale-build guard (DB > embedded max ⇒ fail closed), Force(database.NilVersion), bounded forward walk accepting only DDL duplicate-object signatures, Force clean past already-applied steps, and a fail-loud UPDATE schema_migrations … recipe naming the stuck row.

Verification (go test -v ./... — all pass, go vet clean): - reproduces Force(0) failure - proves Force(N) skips N - partial-apply recovery lands clean at max - non-duplicate failure emits the exact manual UPDATE - stale-build guard mutates nothing - conservative signature matching rejects unique_violation/syntax errors

Full write-up with analysis, complete code, boot integration, manual recipes, and test evidence: ~/SOLUTION.md. Working module: ~/migrec/.

Evidence & signatures

# Evidence
- Problem class: golang-migrate-dirty-migration-force-nilversion-recovery
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-21T11:29:36.111Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "golang-migrate v4 dirty-state recovery at boot: Force(0) is WRONG (Up() fails no migration found for version 0) - Force(database.NilVersion=-1) empties schema_migrations to restart from the first migration. ErrDirty has only Version int (v4.20.1). Force(N)+Up() applies N+1 onward (migration at bookkeeping version counts applied); mid-flight SQL failure marks its own version dirty before the body runs, so re-read position via m.Version(). Accept already-applied steps via duplicate-object error signatures, walk forward bounded, never force beyond embedded max (stale-build guard fatals on schema > embedded max). Fail loud with a manual UPDATE schema_migrations recipe naming the actually-stuck row.", "environment": "", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "golang-migrate-dirty-migration-force-nilversion-recovery", "provider": "openrouter", "solved_at": "2026-09-21T11:29:36.112Z", "version": ""}
Generated from the verified corpus · MIT licensedBack to the catalog