// Package db owns the SQLite connection, schema migrations, and all queries // used by the Control API. It is the only package in the system that talks // to the database directly — agents and probers never connect to it. package db import ( "context" "database/sql" _ "embed" "fmt" "time" _ "modernc.org/sqlite" ) //go:embed migrations/0001_init.sql var initSchema string type DB struct { *sql.DB } // Open opens (creating if necessary) the SQLite database at path, applies // pragmas suited to a single-writer WAL workload, and runs any pending // schema migrations. func Open(ctx context.Context, path string) (*DB, error) { sqlDB, err := sql.Open("sqlite", path+"?_pragma=busy_timeout(5000)") if err != nil { return nil, fmt.Errorf("open sqlite: %w", err) } // Control API is the sole writer; one connection avoids SQLITE_BUSY // entirely for writes while still allowing concurrent reads via WAL. sqlDB.SetMaxOpenConns(1) for _, pragma := range []string{ "PRAGMA journal_mode=WAL", "PRAGMA synchronous=NORMAL", "PRAGMA foreign_keys=ON", "PRAGMA busy_timeout=5000", } { if _, err := sqlDB.ExecContext(ctx, pragma); err != nil { sqlDB.Close() return nil, fmt.Errorf("apply pragma %q: %w", pragma, err) } } d := &DB{DB: sqlDB} if err := d.migrate(ctx); err != nil { sqlDB.Close() return nil, fmt.Errorf("migrate: %w", err) } return d, nil } // migrate applies the embedded schema exactly once, tracked via // PRAGMA user_version so repeated startups are no-ops. func (d *DB) migrate(ctx context.Context) error { var version int if err := d.QueryRowContext(ctx, "PRAGMA user_version").Scan(&version); err != nil { return fmt.Errorf("read user_version: %w", err) } if version >= 1 { return nil } tx, err := d.BeginTx(ctx, nil) if err != nil { return err } defer tx.Rollback() if _, err := tx.ExecContext(ctx, initSchema); err != nil { return fmt.Errorf("apply 0001_init.sql: %w", err) } if _, err := tx.ExecContext(ctx, "PRAGMA user_version=1"); err != nil { return fmt.Errorf("set user_version: %w", err) } return tx.Commit() } // Now returns the current time truncated to millisecond precision, the // granularity used consistently for all timestamp columns. func Now() time.Time { return time.Now().UTC().Truncate(time.Millisecond) }