From 22cc1a7fb1a00bd5cb32b0945da21f4090f9b309 Mon Sep 17 00:00:00 2001 From: clawbot Date: Thu, 26 Mar 2026 06:55:05 -0700 Subject: [PATCH] fix: rename 000.sql to 000_migration.sql and wrap legacy conversion in transaction - Rename bootstrap migration from 000.sql to 000_migration.sql per REPO_POLICIES.md naming convention (NNN_description.sql pattern) - Update all hardcoded references in migrations.go - Wrap rebuildMigrationsTable() DROP/CREATE/INSERT sequence in a single transaction for crash safety - Update test case filename to match renamed file --- internal/database/migrations.go | 50 +++++++++++++------ .../migrations/{000.sql => 000_migration.sql} | 0 internal/database/migrations_test.go | 2 +- 3 files changed, 35 insertions(+), 17 deletions(-) rename internal/database/migrations/{000.sql => 000_migration.sql} (100%) 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},