applyAllMigrations applies pending database migrations in order. MIGRATION FAILURE HANDLING: If a migration fails, any strings interned during the migration will remain in string_pool. These orphaned strings will be automatically cleaned up by the periodic cleanup loop (24hrs). This is acceptable b
(ctx context.Context, migrations []string)
| 1415 | // |
| 1416 | // db.CleanupUnusedStrings(context.Background()) |
| 1417 | func (db *DB) applyAllMigrations(ctx context.Context, migrations []string) error { |
| 1418 | // Migrations that need foreign keys disabled due to table recreation |
| 1419 | needsForeignKeysOff := map[string]bool{ |
| 1420 | "010_add_files_cache_and_string_interning.sql": true, |
| 1421 | } |
| 1422 | |
| 1423 | // Begin single transaction for all migrations using BeginTx for proper connection handling |
| 1424 | tx, err := db.writerConn.BeginTx(ctx, nil) |
| 1425 | if err != nil { |
| 1426 | return fmt.Errorf("failed to begin transaction: %w", err) |
| 1427 | } |
| 1428 | |
| 1429 | // CRITICAL: Track whether we have an active transaction to rollback |
| 1430 | // This prevents double-rollback issues when recreating transactions mid-migration |
| 1431 | rollbackActive := func() { |
| 1432 | if tx != nil { |
| 1433 | tx.Rollback() |
| 1434 | tx = nil |
| 1435 | } |
| 1436 | } |
| 1437 | defer rollbackActive() |
| 1438 | |
| 1439 | // Apply each migration within the transaction |
| 1440 | for _, filename := range migrations { |
| 1441 | // Check if this migration needs foreign keys disabled |
| 1442 | if needsForeignKeysOff[filename] { |
| 1443 | // Commit current transaction before disabling foreign keys |
| 1444 | if err := tx.Commit(); err != nil { |
| 1445 | return fmt.Errorf("failed to commit before %s: %w", filename, err) |
| 1446 | } |
| 1447 | tx = nil // Clear tx so rollbackActive() won't try to rollback committed tx |
| 1448 | |
| 1449 | // Ensure foreign keys are re-enabled even if migration fails |
| 1450 | defer func() { |
| 1451 | if _, err := db.writerConn.ExecContext(context.Background(), "PRAGMA foreign_keys = ON"); err != nil { |
| 1452 | log.Error().Err(err).Msg("CRITICAL: Failed to re-enable foreign keys after migration - manual intervention required") |
| 1453 | } |
| 1454 | }() |
| 1455 | |
| 1456 | // Disable foreign keys (must be done outside transaction) |
| 1457 | if _, err := db.writerConn.ExecContext(ctx, "PRAGMA foreign_keys = OFF"); err != nil { |
| 1458 | return fmt.Errorf("failed to disable foreign keys for %s: %w", filename, err) |
| 1459 | } |
| 1460 | |
| 1461 | // Run migration within explicit transaction while foreign keys are disabled |
| 1462 | if err := func() error { |
| 1463 | fkTx, err := db.writerConn.BeginTx(ctx, nil) |
| 1464 | if err != nil { |
| 1465 | return fmt.Errorf("failed to begin migration transaction for %s: %w", filename, err) |
| 1466 | } |
| 1467 | committed := false |
| 1468 | defer func() { |
| 1469 | if !committed { |
| 1470 | if rollErr := fkTx.Rollback(); rollErr != nil && !errors.Is(rollErr, sql.ErrTxDone) { |
| 1471 | log.Error().Err(rollErr).Str("migration", filename).Msg("rollback failed for migration transaction") |
| 1472 | } |
| 1473 | } |
| 1474 | }() |