// Package store provides pansy's durable persistence layer: a pure-Go SQLite // database (modernc.org/sqlite — no cgo, so the binary stays static) with a // tiny embedded migration runner. Entity query methods are added per feature // issue; this file only handles opening and the connection pragmas. package store import ( "database/sql" "fmt" "net/url" "strings" _ "modernc.org/sqlite" // pure-Go driver, registered as "sqlite" ) // DB is a handle to the SQLite database backing pansy. Safe for concurrent use: // WAL mode allows many readers with one writer, and busy_timeout makes a // contended writer wait rather than fail with SQLITE_BUSY. type DB struct { sql *sql.DB } // Open opens (creating if absent) the SQLite database at path and returns a DB. // The pragmas — busy_timeout, WAL journal, and foreign-key enforcement — are // applied per connection via the DSN so every pooled connection inherits them. // A path of ":memory:" yields an ephemeral in-memory database (useful in tests); // the pool is pinned to a single connection so all queries see the same DB. // The caller owns the returned DB and must Close it. func Open(path string) (*DB, error) { dsn, memory := buildDSN(path) sqldb, err := sql.Open("sqlite", dsn) if err != nil { return nil, fmt.Errorf("store: open %q: %w", path, err) } if memory { // Each new connection to a bare ":memory:" DSN gets its OWN empty database, // so a pooled second connection wouldn't see the migrated schema. Pin the // pool to one connection to keep an in-memory DB coherent. sqldb.SetMaxOpenConns(1) } if err := sqldb.Ping(); err != nil { sqldb.Close() return nil, fmt.Errorf("store: ping %q: %w", path, err) } return &DB{sql: sqldb}, nil } // buildDSN turns a filesystem path (or ":memory:") into a modernc DSN carrying // the pragmas we want on every connection, and reports whether it is in-memory. // A path that already looks like a DSN (has a scheme or query) is passed through // untouched. func buildDSN(path string) (dsn string, memory bool) { memory = path == ":memory:" || strings.Contains(path, ":memory:") || strings.Contains(path, "mode=memory") if strings.HasPrefix(path, "file:") || strings.Contains(path, "?") { return path, memory } pragmas := url.Values{} pragmas.Add("_pragma", "busy_timeout(5000)") pragmas.Add("_pragma", "journal_mode(WAL)") pragmas.Add("_pragma", "foreign_keys(1)") return "file:" + path + "?" + pragmas.Encode(), memory } // Close closes the underlying database. func (d *DB) Close() error { return d.sql.Close() } // SQL exposes the underlying *sql.DB for the per-entity query files. func (d *DB) SQL() *sql.DB { return d.sql }