From 0df7c424a1315bec26c96de4ffae9cefce3ae116 Mon Sep 17 00:00:00 2001 From: dawn Date: Wed, 21 Oct 2026 22:06:44 +0300 Subject: [PATCH] orm: don't take the write lock for a migration that is already applied Signed-off-by: dawn --- orm/orm.go | 10 +++++++++ orm/run_migration_test.go | 47 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 orm/run_migration_test.go diff --git a/orm/orm.go b/orm/orm.go index 927183787..99f8e831b 100644 --- a/orm/orm.go +++ b/orm/orm.go @@ -26,6 +26,16 @@ type migrationFn = func(*sql.Tx) error func RunMigration(c *sql.Conn, logger *slog.Logger, name string, migrationFn migrationFn) error { logger = logger.With("migration", name) + // check first, or a busy db fails every boot on the write lock + var applied bool + if err := c.QueryRowContext(context.Background(), "select exists (select 1 from migrations where name = ?)", name).Scan(&applied); err != nil { + return err + } + if applied { + logger.Warn("skipped migration, already applied") + return nil + } + tx, err := c.BeginTx(context.Background(), nil) if err != nil { return err diff --git a/orm/run_migration_test.go b/orm/run_migration_test.go new file mode 100644 index 000000000..451fce086 --- /dev/null +++ b/orm/run_migration_test.go @@ -0,0 +1,47 @@ +package orm + +import ( + "context" + "database/sql" + "io" + "log/slog" + "path/filepath" + "testing" +) + +func TestAppliedMigrationDoesNotNeedTheWriteLock(t *testing.T) { + ctx := context.Background() + dsn := "file:" + filepath.Join(t.TempDir(), "orm.db") + "?_txlock=immediate&_busy_timeout=200" + db, err := sql.Open("sqlite3", dsn) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { db.Close() }) + if _, err := db.Exec(` + create table migrations (id integer primary key autoincrement, name text unique); + insert into migrations (name) values ('done'); + `); err != nil { + t.Fatal(err) + } + + holder, err := db.BeginTx(ctx, nil) + if err != nil { + t.Fatal(err) + } + defer holder.Rollback() + + conn, err := db.Conn(ctx) + if err != nil { + t.Fatal(err) + } + defer conn.Close() + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + ran := false + err = RunMigration(conn, logger, "done", func(*sql.Tx) error { + ran = true + return nil + }) + if err != nil || ran { + t.Fatalf("applied migration with the write lock held = %v, ran %v, want it skipped", err, ran) + } +} -- 2.54.0