From 9c4a302bbf9fb977ed4e870921230fd3569aa7bc Mon Sep 17 00:00:00 2001 From: Philip Zeyliger Date: Fri, 23 Jan 2026 23:17:33 +0000 Subject: [PATCH] shelley: run schema migrations in a transaction Prompt: exed now runs its schema migrations in a transaction; we should do the same for shelley. Each migration now runs within a single transaction that: 1. Executes the migration SQL 2. Records it in the migrations table If either step fails, the entire migration is rolled back. This matches the pattern used by exed's migration system. Co-authored-by: Shelley --- db/db.go | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/db/db.go b/db/db.go index 05571114f221fe1f7aaca85478bece701183204f..c922094d2dae3563c97ef358a6e018182c82fd43 100644 --- a/db/db.go +++ b/db/db.go @@ -178,13 +178,8 @@ func (db *DB) Migrate(ctx context.Context) error { if !executedMigrations[migrationNumber] { slog.Info("running migration", "file", migration, "number", migrationNumber) - if err := db.executeMigration(ctx, migration); err != nil { - return fmt.Errorf("failed to execute migration %s: %w", migration, err) - } - - err = db.pool.Exec(ctx, "INSERT INTO migrations (migration_number, migration_name) VALUES (?, ?)", migrationNumber, migration) - if err != nil { - return fmt.Errorf("failed to record migration %s in migrations table: %w", migration, err) + if err := db.runMigration(ctx, migration, migrationNumber); err != nil { + return err } } } @@ -192,18 +187,25 @@ func (db *DB) Migrate(ctx context.Context) error { return nil } -// executeMigration executes a single migration file -func (db *DB) executeMigration(ctx context.Context, filename string) error { +// runMigration executes a single migration file within a transaction, +// including recording it in the migrations table. +func (db *DB) runMigration(ctx context.Context, filename string, migrationNumber int) error { content, err := schemaFS.ReadFile("schema/" + filename) if err != nil { return fmt.Errorf("failed to read migration file %s: %w", filename, err) } - if err := db.pool.Exec(ctx, string(content)); err != nil { - return fmt.Errorf("failed to execute migration %s: %w", filename, err) - } + return db.pool.Tx(ctx, func(ctx context.Context, tx *Tx) error { + if _, err := tx.Exec(string(content)); err != nil { + return fmt.Errorf("failed to execute migration %s: %w", filename, err) + } - return nil + if _, err := tx.Exec("INSERT INTO migrations (migration_number, migration_name) VALUES (?, ?)", migrationNumber, filename); err != nil { + return fmt.Errorf("failed to record migration %s in migrations table: %w", filename, err) + } + + return nil + }) } // Pool returns the underlying connection pool for advanced operations