// Package db handles the SQLite connection and migrations. package db import ( "database/sql" "embed" "fmt" "sort" "strings" _ "modernc.org/sqlite" ) //go:embed migrations/*.sql var migrationsFS embed.FS // Open opens (and creates if needed) the SQLite database at the given path, // enables performance PRAGMAs, and applies embedded migrations. func Open(path string) (*sql.DB, error) { dsn := fmt.Sprintf("file:%s?_pragma=journal_mode(WAL)&_pragma=foreign_keys(on)&_pragma=synchronous(normal)&_pragma=busy_timeout(5000)", path) conn, err := sql.Open("sqlite", dsn) if err != nil { return nil, fmt.Errorf("open sqlite: %w", err) } if err := conn.Ping(); err != nil { _ = conn.Close() return nil, fmt.Errorf("ping sqlite: %w", err) } if err := migrate(conn); err != nil { _ = conn.Close() return nil, err } return conn, nil } // migrate applies all embedded *.sql migrations in alphabetical order, // skipping those already applied. Intentionally minimal in-house system // (no external dependency). func migrate(conn *sql.DB) error { if _, err := conn.Exec(`CREATE TABLE IF NOT EXISTS schema_migrations ( name TEXT PRIMARY KEY, applied_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) )`); err != nil { return fmt.Errorf("create schema_migrations: %w", err) } entries, err := migrationsFS.ReadDir("migrations") if err != nil { return fmt.Errorf("read migrations dir: %w", err) } names := make([]string, 0, len(entries)) for _, e := range entries { if e.IsDir() || !strings.HasSuffix(e.Name(), ".sql") { continue } names = append(names, e.Name()) } sort.Strings(names) for _, name := range names { var dummy string err := conn.QueryRow(`SELECT name FROM schema_migrations WHERE name = ?`, name).Scan(&dummy) if err == nil { continue // already applied } if err != sql.ErrNoRows { return fmt.Errorf("check migration %s: %w", name, err) } content, err := migrationsFS.ReadFile("migrations/" + name) if err != nil { return fmt.Errorf("read migration %s: %w", name, err) } tx, err := conn.Begin() if err != nil { return fmt.Errorf("begin tx for %s: %w", name, err) } if _, err := tx.Exec(string(content)); err != nil { _ = tx.Rollback() return fmt.Errorf("apply migration %s: %w", name, err) } if _, err := tx.Exec(`INSERT INTO schema_migrations(name) VALUES(?)`, name); 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) } } return nil }