// Package db handles SQLite database setup, pragma configuration, and // goose migrations. package db import ( "database/sql" "embed" "errors" "fmt" "io/fs" "log/slog" "os" "path/filepath" "github.com/pressly/goose/v3" _ "modernc.org/sqlite" ) //go:embed migrations/*.sql var MigrationFS embed.FS var DB *sql.DB func Init(dbPath string) (func(), error) { if err := os.MkdirAll(filepath.Dir(dbPath), 0o755); err != nil { return nil, fmt.Errorf("creating data dir: %w", err) } // busy_timeout must be first because the connection needs to block on // busy before WAL mode is set in case it hasn't been set already. pragmas := "?_pragma=busy_timeout(10000)&_pragma=journal_mode(WAL)&_pragma=journal_size_limit(200000000)&_pragma=synchronous(NORMAL)&_pragma=foreign_keys(ON)&_pragma=temp_store(MEMORY)&_pragma=cache_size(-32000)" var err error DB, err = goose.OpenDBWithDriver("sqlite", dbPath+pragmas) if err != nil { return nil, fmt.Errorf("opening database: %w", err) } if err := DB.Ping(); err != nil { return nil, errors.Join(fmt.Errorf("pinging database: %w", err), DB.Close()) } slog.Info("db connected", "db", dbPath) sub, err := fs.Sub(MigrationFS, "migrations") if err != nil { return nil, errors.Join(fmt.Errorf("migrations sub fs: %w", err), DB.Close()) } goose.SetBaseFS(sub) if err := goose.SetDialect("sqlite3"); err != nil { return nil, errors.Join(fmt.Errorf("setting goose dialect: %w", err), DB.Close()) } if err := goose.Up(DB, "."); err != nil { return nil, errors.Join(fmt.Errorf("running migrations: %w", err), DB.Close()) } if _, err := DB.Exec("PRAGMA optimize"); err != nil { return nil, errors.Join(fmt.Errorf("pragma optimize: %w", err), DB.Close()) } cleanup := func() { if _, err := DB.Exec("PRAGMA optimize(0x10002)"); err != nil { slog.Error("pragma optimize at shutdown", "error", err) } if err := DB.Close(); err != nil { slog.Error("closing database", "error", err) } } return cleanup, nil }