// Package db opens the SQLite database and applies embedded SQL // migrations in filename order at startup. Uses modernc.org/sqlite // (pure Go, no cgo). package db import ( "database/sql" "embed" "fmt" "io/fs" "path/filepath" "sort" "strings" _ "modernc.org/sqlite" ) //go:embed migrations/*.sql var migrationsFS embed.FS // Open opens the SQLite DB at path, enabling foreign keys and WAL, // and applies every embedded migration in filename order. func Open(path string) (*sql.DB, error) { dsn := fmt.Sprintf("file:%s?_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)", filepath.ToSlash(path)) db, err := sql.Open("sqlite", dsn) if err != nil { return nil, fmt.Errorf("open sqlite: %w", err) } if err := db.Ping(); err != nil { _ = db.Close() return nil, fmt.Errorf("ping sqlite: %w", err) } if err := migrate(db); err != nil { _ = db.Close() return nil, err } return db, nil } func migrate(db *sql.DB) error { entries, err := fs.ReadDir(migrationsFS, "migrations") if err != nil { return fmt.Errorf("read migrations: %w", err) } names := make([]string, 0, len(entries)) for _, e := range entries { if !e.IsDir() && strings.HasSuffix(e.Name(), ".sql") { names = append(names, e.Name()) } } sort.Strings(names) if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS schema_migrations (name TEXT PRIMARY KEY, applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP)`); err != nil { return fmt.Errorf("ensure schema_migrations: %w", err) } for _, name := range names { var applied int if err := db.QueryRow(`SELECT COUNT(1) FROM schema_migrations WHERE name = ?`, name).Scan(&applied); err != nil { return fmt.Errorf("check migration %s: %w", name, err) } if applied > 0 { continue } content, err := migrationsFS.ReadFile("migrations/" + name) if err != nil { return fmt.Errorf("read migration %s: %w", name, err) } tx, err := db.Begin() if err != nil { return fmt.Errorf("begin migration %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 }