Monorepo for Tangled
tangled.org
1package db
2
3import (
4 "context"
5 "database/sql"
6 "fmt"
7 "log/slog"
8)
9
10type MigrationFn = func(context.Context, *sql.Tx) error
11
12type Migration struct {
13 Name string
14 Fn MigrationFn
15}
16
17func ensureMigrationsTable(ctx context.Context, conn *sql.Conn) error {
18 _, err := conn.ExecContext(ctx, `
19 create table if not exists migrations (
20 name text primary key,
21 applied_at timestamptz not null default now()
22 );
23 `)
24 return err
25}
26
27func RunMigration(ctx context.Context, conn *sql.Conn, logger *slog.Logger, m Migration) error {
28 logger = logger.With("migration", m.Name)
29
30 tx, err := conn.BeginTx(ctx, nil)
31 if err != nil {
32 return fmt.Errorf("begin migration tx: %w", err)
33 }
34 defer tx.Rollback()
35
36 var exists bool
37 if err := tx.QueryRowContext(ctx, `select exists (select 1 from migrations where name = $1)`, m.Name).Scan(&exists); err != nil {
38 return fmt.Errorf("checking migration state: %w", err)
39 }
40 if exists {
41 logger.Debug("migration already applied")
42 return nil
43 }
44
45 if err := m.Fn(ctx, tx); err != nil {
46 logger.Error("migration failed", "err", err)
47 return fmt.Errorf("running migration %s: %w", m.Name, err)
48 }
49
50 if _, err := tx.ExecContext(ctx, `insert into migrations (name) values ($1)`, m.Name); err != nil {
51 return fmt.Errorf("recording migration: %w", err)
52 }
53
54 if err := tx.Commit(); err != nil {
55 return fmt.Errorf("commit migration: %w", err)
56 }
57
58 logger.Info("migration applied")
59 return nil
60}
61
62func RunMigrations(ctx context.Context, conn *sql.Conn, logger *slog.Logger, ms []Migration) error {
63 if err := ensureMigrationsTable(ctx, conn); err != nil {
64 return fmt.Errorf("ensuring migrations table: %w", err)
65 }
66 for _, m := range ms {
67 if err := RunMigration(ctx, conn, logger, m); err != nil {
68 return err
69 }
70 }
71 return nil
72}