diff --git a/internal/database/migrations.go b/internal/database/migrations.go index 4636340..4ab4996 100644 --- a/internal/database/migrations.go +++ b/internal/database/migrations.go @@ -83,8 +83,9 @@ func collectMigrations() ([]string, error) { } // bootstrapMigrationsTable ensures the schema_migrations table exists by -// applying 000.sql if the table is missing. For databases with a legacy -// TEXT-based schema_migrations table, it converts to the new INTEGER format. +// applying 000_migration.sql if the table is missing. For databases with a +// legacy TEXT-based schema_migrations table, it converts to the new INTEGER +// format. func bootstrapMigrationsTable(ctx context.Context, db *sql.DB, log *slog.Logger) error { var tableExists int @@ -103,12 +104,12 @@ func bootstrapMigrationsTable(ctx context.Context, db *sql.DB, log *slog.Logger) return convertLegacyMigrations(ctx, db, log) } -// applyBootstrapMigration reads and executes 000.sql to create the +// applyBootstrapMigration reads and executes 000_migration.sql to create the // schema_migrations table on a fresh database. func applyBootstrapMigration(ctx context.Context, db *sql.DB, log *slog.Logger) error { - content, err := migrationsFS.ReadFile("migrations/000.sql") + content, err := migrationsFS.ReadFile("migrations/000_migration.sql") if err != nil { - return fmt.Errorf("failed to read bootstrap migration 000.sql: %w", err) + return fmt.Errorf("failed to read bootstrap migration 000_migration.sql: %w", err) } if log != nil { @@ -211,31 +212,48 @@ func readLegacyVersions(ctx context.Context, db *sql.DB) ([]int, error) { } // rebuildMigrationsTable drops the old schema_migrations table, recreates it -// via 000.sql, and re-inserts the given integer versions. +// via 000_migration.sql, and re-inserts the given integer versions. The entire +// operation runs in a single transaction so a crash cannot lose migration data. func rebuildMigrationsTable(ctx context.Context, db *sql.DB, versions []int) error { - _, err := db.ExecContext(ctx, "DROP TABLE schema_migrations") + content, err := migrationsFS.ReadFile("migrations/000_migration.sql") + if err != nil { + return fmt.Errorf("failed to read bootstrap migration 000_migration.sql: %w", err) + } + + tx, err := db.BeginTx(ctx, nil) + if err != nil { + return fmt.Errorf("failed to begin transaction for legacy conversion: %w", err) + } + + defer func() { + if err != nil { + _ = tx.Rollback() + } + }() + + _, err = tx.ExecContext(ctx, "DROP TABLE schema_migrations") if err != nil { return fmt.Errorf("failed to drop legacy migrations table: %w", err) } - content, err := migrationsFS.ReadFile("migrations/000.sql") - if err != nil { - return fmt.Errorf("failed to read bootstrap migration 000.sql: %w", err) - } - - _, err = db.ExecContext(ctx, string(content)) + _, err = tx.ExecContext(ctx, string(content)) if err != nil { return fmt.Errorf("failed to create new migrations table: %w", err) } for _, v := range versions { - _, insErr := db.ExecContext(ctx, + _, err = tx.ExecContext(ctx, "INSERT OR IGNORE INTO schema_migrations (version) VALUES (?)", v) - if insErr != nil { - return fmt.Errorf("failed to insert converted version %d: %w", v, insErr) + if err != nil { + return fmt.Errorf("failed to insert converted version %d: %w", v, err) } } + err = tx.Commit() + if err != nil { + return fmt.Errorf("failed to commit legacy conversion: %w", err) + } + return nil } diff --git a/internal/database/migrations/000.sql b/internal/database/migrations/000_migration.sql similarity index 100% rename from internal/database/migrations/000.sql rename to internal/database/migrations/000_migration.sql diff --git a/internal/database/migrations_test.go b/internal/database/migrations_test.go index 12f0f64..fe6bc47 100644 --- a/internal/database/migrations_test.go +++ b/internal/database/migrations_test.go @@ -20,7 +20,7 @@ func TestParseMigrationVersion(t *testing.T) { wantVersion int wantErr bool }{ - {filename: "000.sql", wantVersion: 0}, + {filename: "000_migration.sql", wantVersion: 0}, {filename: "001_initial.sql", wantVersion: 1}, {filename: "002_remove_container_id.sql", wantVersion: 2}, {filename: "007_add_resource_limits.sql", wantVersion: 7},