package store import ( "context" "database/sql" _ "embed" "fmt" "strings" _ "modernc.org/sqlite" ) //go:embed 001_initial.sql var schema string // Open opens (or creates) the SQLite database at path and runs migrations. func Open(path string) (*sql.DB, error) { dsn := fmt.Sprintf( "file:%s?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)&_pragma=foreign_keys(ON)&_pragma=synchronous(NORMAL)", path, ) db, err := sql.Open("sqlite", dsn) if err != nil { return nil, fmt.Errorf("open sqlite: %w", err) } db.SetMaxOpenConns(1) // SQLite WAL: single writer if err := migrate(db); err != nil { db.Close() return nil, fmt.Errorf("migrate: %w", err) } return db, nil } // migrate runs embedded SQL migrations. Simple single-file approach: we track // a user_version pragma and apply the schema once if version == 0. func migrate(db *sql.DB) error { var version int if err := db.QueryRow("PRAGMA user_version").Scan(&version); err != nil { return err } if version >= 1 { return nil } ctx := context.Background() // Strip migration comments and split into individual statements stmts := splitStatements(schema) for _, stmt := range stmts { stmt = strings.TrimSpace(stmt) if stmt == "" { continue } if _, err := db.ExecContext(ctx, stmt); err != nil { return fmt.Errorf("exec %q: %w", stmt[:min(len(stmt), 60)], err) } } // PRAGMA user_version cannot be set inside a regular transaction in all SQLite versions; // execute it as a standalone statement. if _, err := db.ExecContext(ctx, "PRAGMA user_version = 1"); err != nil { return err } return nil } func splitStatements(sql string) []string { // Only process statements up to the "-- +migrate Down" marker. var lines []string for _, line := range strings.Split(sql, "\n") { trimmed := strings.TrimSpace(line) if trimmed == "-- +migrate Down" { break } if strings.HasPrefix(trimmed, "-- +migrate") { continue } lines = append(lines, line) } joined := strings.Join(lines, "\n") // Split on semicolons parts := strings.Split(joined, ";") return parts } func min(a, b int) int { if a < b { return a } return b }