// Package store owns MyDay's SQLite connection and migration runner // (PROJECT_PLAN.md §12.1: "the core owns the DB connection and the migration // runner; each module owns its own tables, queries, and migrations"). // // It uses the CGO-free pure-Go driver modernc.org/sqlite (D-012), so the // shipped binary stays static — no mattn/go-sqlite3, no libc dependency — and // runs with WAL journaling and foreign keys ON. // // Spec: docs/specs/spec-004-identity-sessions.md §3–§4. package store import ( "database/sql" "fmt" "os" "path/filepath" "strings" _ "modernc.org/sqlite" // registers the "sqlite" driver (D-012) ) // Driver is the SQLite driver name registered by modernc.org/sqlite (D-012). const Driver = "sqlite" // DB wraps the standard connection pool. The embedded *sql.DB is what the core // services (users, auth, audit) consume. type DB struct { *sql.DB path string } // Open opens (creating if needed) the SQLite database at path. // // The DSN carries the connection pragmas so they apply to every connection the // pool opens: WAL journaling, foreign keys ON, a busy timeout, and NORMAL // synchronous (safe with WAL, faster than FULL). // // SQLite is a single-writer store, so the pool is pinned to ONE connection: // this removes lock contention (D-012: "one instance only") and keeps an // in-memory database coherent across the whole process. // // A non-empty ":memory:" (or file:...mode=memory) path opens a private // in-memory database — used by the unit tests. func Open(path string) (*DB, error) { if path == "" { return nil, fmt.Errorf("store: database path is empty") } if !isMemory(path) { if dir := filepath.Dir(path); dir != "" && dir != "." { if err := os.MkdirAll(dir, 0o750); err != nil { return nil, fmt.Errorf("store: create data directory %q: %w", dir, err) } } } db, err := sql.Open(Driver, buildDSN(path)) if err != nil { return nil, fmt.Errorf("store: open %q: %w", path, err) } db.SetMaxOpenConns(1) db.SetMaxIdleConns(1) db.SetConnMaxLifetime(0) if err := db.Ping(); err != nil { _ = db.Close() return nil, fmt.Errorf("store: ping %q: %w", path, err) } return &DB{DB: db, path: path}, nil } // Path returns the database path this connection was opened with. func (db *DB) Path() string { return db.path } // isMemory reports whether path names an in-memory database. func isMemory(path string) bool { return path == ":memory:" || strings.Contains(path, "mode=memory") } // buildDSN assembles the modernc.org/sqlite DSN. The `_pragma` parameters are // applied on every connection open (D-012: WAL · foreign keys ON). func buildDSN(path string) string { pragmas := []string{ "journal_mode(WAL)", "foreign_keys(1)", "busy_timeout(5000)", "synchronous(NORMAL)", } var b strings.Builder if isMemory(path) { b.WriteString("file::memory:") } else { b.WriteString("file:") b.WriteString(path) } b.WriteString("?_pragma=") b.WriteString(strings.Join(pragmas, "&_pragma=")) return b.String() }