// Package db provides the SQLite database access layer for MCIAS. // // Security design: // - All queries use parameterized statements; no string concatenation. // - Foreign keys are enforced (PRAGMA foreign_keys = ON). // - WAL mode is enabled for safe concurrent reads during writes. // - The audit log is append-only: no update or delete operations are provided. package db import ( "context" "database/sql" "errors" "fmt" "time" _ "modernc.org/sqlite" // register the sqlite3 driver ) // DB wraps a *sql.DB with MCIAS-specific helpers. type DB struct { sql *sql.DB } // Open opens (or creates) the SQLite database at path and configures it for // MCIAS use (WAL mode, foreign keys, busy timeout). func Open(path string) (*DB, error) { // The modernc.org/sqlite driver is registered as "sqlite". sqlDB, err := sql.Open("sqlite", path) if err != nil { return nil, fmt.Errorf("db: open sqlite: %w", err) } // Use a single connection for writes; reads can use the pool. sqlDB.SetMaxOpenConns(1) db := &DB{sql: sqlDB} if err := db.configure(); err != nil { _ = sqlDB.Close() return nil, err } return db, nil } // configure applies PRAGMAs that must be set on every connection. func (db *DB) configure() error { pragmas := []string{ "PRAGMA journal_mode=WAL", "PRAGMA foreign_keys=ON", "PRAGMA busy_timeout=5000", "PRAGMA synchronous=NORMAL", } for _, p := range pragmas { if _, err := db.sql.Exec(p); err != nil { return fmt.Errorf("db: configure pragma %q: %w", p, err) } } return nil } // Close closes the underlying database connection. func (db *DB) Close() error { return db.sql.Close() } // Ping verifies the database connection is alive. func (db *DB) Ping(ctx context.Context) error { return db.sql.PingContext(ctx) } // SQL returns the underlying *sql.DB for use in tests or advanced queries. // Prefer the typed methods on DB for all production code. func (db *DB) SQL() *sql.DB { return db.sql } // now returns the current UTC time formatted as ISO-8601. func now() string { return time.Now().UTC().Format(time.RFC3339) } // parseTime parses an ISO-8601 UTC time string returned by SQLite. func parseTime(s string) (time.Time, error) { t, err := time.Parse(time.RFC3339, s) if err != nil { // Try without timezone suffix (some SQLite defaults). t, err = time.Parse("2006-01-02T15:04:05", s) if err != nil { return time.Time{}, fmt.Errorf("db: parse time %q: %w", s, err) } return t.UTC(), nil } return t.UTC(), nil } // ErrNotFound is returned when a requested record does not exist. var ErrNotFound = errors.New("db: record not found") // nullableTime converts a *string from SQLite into a *time.Time. func nullableTime(s *string) (*time.Time, error) { if s == nil { return nil, nil } t, err := parseTime(*s) if err != nil { return nil, err } return &t, nil }