checkAndRecover is the core logic: 1. Ensure tracking table exists. 2. Read DB version. Refuse if dirty. 3. Detect untracked migrations. 4. Detect missing files (needs rollback). 5. Detect content changes (needs --db-continue). 6. Capture current disk state for next time.
(ctx context.Context, logger slog.Logger, db *sql.DB, migrDir string, cfg *devConfig)
| 100 | // 5. Detect content changes (needs --db-continue). |
| 101 | // 6. Capture current disk state for next time. |
| 102 | func checkAndRecover(ctx context.Context, logger slog.Logger, db *sql.DB, migrDir string, cfg *devConfig) error { |
| 103 | if _, err := db.ExecContext(ctx, trackingDDL); err != nil { |
| 104 | return xerrors.Errorf("create tracking table: %w", err) |
| 105 | } |
| 106 | |
| 107 | dbVersion, dirty, err := currentMigrationVersion(ctx, db) |
| 108 | if err != nil { |
| 109 | return xerrors.Errorf("get db version: %w", err) |
| 110 | } |
| 111 | if dbVersion < 0 { |
| 112 | return nil // Fresh DB. |
| 113 | } |
| 114 | if dirty { |
| 115 | return xerrors.Errorf( |
| 116 | "database is dirty at version %d (a migration failed halfway)\n\n"+ |
| 117 | " --db-reset destroy database and start fresh\n", dbVersion) |
| 118 | } |
| 119 | |
| 120 | maxTracked, err := maxTrackedVersion(ctx, db) |
| 121 | if err != nil { |
| 122 | return xerrors.Errorf("get max tracked version: %w", err) |
| 123 | } |
| 124 | if dbVersion > maxTracked && maxTracked >= 0 { |
| 125 | // Gap between tracking and DB version. This happens when |
| 126 | // the server applied migrations via Up() but develop.sh |
| 127 | // was interrupted before updateMigrationTracking ran. |
| 128 | // captureDownSQL at the end of this function backfills |
| 129 | // from disk. |
| 130 | logger.Warn(ctx, "migration tracking gap detected, will backfill", |
| 131 | slog.F("db_version", dbVersion), |
| 132 | slog.F("max_tracked", maxTracked)) |
| 133 | } |
| 134 | |
| 135 | // Check for missing files (rollback candidates). |
| 136 | rollbacks, err := findRollbacks(ctx, db, migrDir) |
| 137 | if err != nil { |
| 138 | return xerrors.Errorf("find rollbacks: %w", err) |
| 139 | } |
| 140 | |
| 141 | if len(rollbacks) > 0 { |
| 142 | if !cfg.dbRollback { |
| 143 | var details strings.Builder |
| 144 | for _, rb := range rollbacks { |
| 145 | _, _ = fmt.Fprintf(&details, " version %d: %s (missing from disk)\n", rb.version, rb.filename) |
| 146 | } |
| 147 | return xerrors.Errorf( |
| 148 | "database has migrations that no longer exist on disk:\n%s\n"+ |
| 149 | " --db-rollback roll back these migrations (preserves data)\n"+ |
| 150 | " --db-reset destroy database and start fresh\n", |
| 151 | details.String()) |
| 152 | } |
| 153 | |
| 154 | if !contiguousFromTop(rollbacks, dbVersion) { |
| 155 | return xerrors.Errorf( |
| 156 | "cannot roll back: versions are not contiguous (%s); use --db-reset", |
| 157 | formatVersions(rollbacks)) |
| 158 | } |
| 159 |
no test coverage detected