- store: SQLite (WAL, foreign keys ON, CGO-free driver) + the embedded migration runner; each module's migrations are name-prefixed (D-012) - audit: the append-only audit log; the actor threads via context (R-1) - users: the user_id/GUID/email anchors (D-013) - config: the YAML settings file (D-035) + validation - httpx: cookies · loopback detection · trusted-proxy XFF · CSRF · security headers · redaction - middleware: correlation id · audit · rate limiting - mail: the invite/recovery mailer seam - server: the core DOOR — the resolution switch, the dispatch registry (RegisterModule), the role gates, /setup · enroll/login · recovery flows, the reserved `static` workflow, and the loopback-only bootstrap (D-025)
99 lines
3 KiB
Go
99 lines
3 KiB
Go
// 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()
|
||
}
|