Files
Spanglish/photo-api/internal/store/migrate.go
T
MichilisandCursor c9a600b6d6 Add photo galleries API and frontend for public and admin viewing.
Introduces the Go photo-api service, nginx/systemd deploy wiring, and Next.js gallery/lightbox pages so event photos can be managed and browsed.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-25 17:32:23 +00:00

116 lines
2.9 KiB
Go

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
}