package store import ( "context" "fmt" "log" "sort" "strconv" "strings" "time" "git.azzamo.net/Michilis/Spanglish/photo-api/migrations" ) // advisoryLockKey is an arbitrary fixed key for pg_advisory_lock so two // instances can't run migrations concurrently. SQLite relies on its file lock. const advisoryLockKey = 792346801 // Migrate applies embedded migrations for the active dialect in version // order, recording applied versions in photos_schema_migrations. func (db *DB) Migrate(ctx context.Context) error { conn, err := db.Conn(ctx) if err != nil { return err } defer conn.Close() if db.Type == Postgres { if _, err := conn.ExecContext(ctx, "SELECT pg_advisory_lock($1)", advisoryLockKey); err != nil { return fmt.Errorf("advisory lock: %w", err) } defer conn.ExecContext(ctx, "SELECT pg_advisory_unlock($1)", advisoryLockKey) } createVersions := "CREATE TABLE IF NOT EXISTS photos_schema_migrations (version integer PRIMARY KEY, applied_at text NOT NULL)" if db.Type == Postgres { createVersions = "CREATE TABLE IF NOT EXISTS photos_schema_migrations (version integer PRIMARY KEY, applied_at timestamptz NOT NULL)" } if _, err := conn.ExecContext(ctx, createVersions); err != nil { return fmt.Errorf("create photos_schema_migrations: %w", err) } applied := map[int]bool{} rows, err := conn.QueryContext(ctx, "SELECT version FROM photos_schema_migrations") if err != nil { return err } for rows.Next() { var v int if err := rows.Scan(&v); err != nil { rows.Close() return err } applied[v] = true } rows.Close() if err := rows.Err(); err != nil { return err } suffix := ".sqlite.sql" if db.Type == Postgres { suffix = ".pg.sql" } entries, err := migrations.FS.ReadDir(".") if err != nil { return err } var names []string for _, e := range entries { if strings.HasSuffix(e.Name(), suffix) { names = append(names, e.Name()) } } sort.Strings(names) for _, name := range names { version, err := strconv.Atoi(strings.SplitN(name, "_", 2)[0]) if err != nil { return fmt.Errorf("migration %s: name must start with a numeric version", name) } if applied[version] { continue } body, err := migrations.FS.ReadFile(name) if err != nil { return err } tx, err := conn.BeginTx(ctx, nil) if err != nil { return err } for _, stmt := range strings.Split(string(body), ";") { stmt = strings.TrimSpace(stmt) if stmt == "" { continue } if _, err := tx.ExecContext(ctx, stmt); err != nil { tx.Rollback() return fmt.Errorf("migration %s: %w", name, err) } } if _, err := tx.ExecContext(ctx, db.Rebind("INSERT INTO photos_schema_migrations (version, applied_at) VALUES (?, ?)"), version, db.TimeArg(time.Now())); err != nil { tx.Rollback() return fmt.Errorf("record migration %s: %w", name, err) } if err := tx.Commit(); err != nil { return fmt.Errorf("commit migration %s: %w", name, err) } log.Printf("applied migration %s", name) } return nil }