package store import ( "context" "database/sql" "fmt" "time" _ "modernc.org/sqlite" "go.uber.org/zap" ) const schema = ` CREATE TABLE IF NOT EXISTS runs ( run_id TEXT PRIMARY KEY, adapter TEXT NOT NULL, model TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'running', created_at DATETIME NOT NULL, completed_at DATETIME, error TEXT ); ` // RunRecord holds the persisted state of an execution. type RunRecord struct { RunID string Adapter string Model string Status string CreatedAt time.Time CompletedAt *time.Time Error string } // Store persists run history to SQLite. type Store struct { db *sql.DB logger *zap.Logger } // New opens (or creates) the SQLite database and runs the schema migration. func New(dsn string, logger *zap.Logger) (*Store, error) { db, err := sql.Open("sqlite", dsn) if err != nil { return nil, fmt.Errorf("store: open %s: %w", dsn, err) } db.SetMaxOpenConns(1) // SQLite is single-writer if _, err := db.Exec(schema); err != nil { return nil, fmt.Errorf("store: migrate: %w", err) } logger.Info("store ready", zap.String("dsn", dsn)) return &Store{db: db, logger: logger}, nil } // Close closes the underlying database connection. func (s *Store) Close() error { return s.db.Close() } // InsertRun records a new run. func (s *Store) InsertRun(ctx context.Context, r RunRecord) error { _, err := s.db.ExecContext(ctx, `INSERT INTO runs (run_id, adapter, model, status, created_at) VALUES (?,?,?,?,?)`, r.RunID, r.Adapter, r.Model, r.Status, r.CreatedAt.UTC(), ) return err } // CompleteRun marks a run as completed or failed. func (s *Store) CompleteRun(ctx context.Context, runID, status, errMsg string) error { _, err := s.db.ExecContext(ctx, `UPDATE runs SET status=?, completed_at=?, error=? WHERE run_id=?`, status, time.Now().UTC(), errMsg, runID, ) return err } // GetRun retrieves a run record by ID. func (s *Store) GetRun(ctx context.Context, runID string) (*RunRecord, error) { row := s.db.QueryRowContext(ctx, `SELECT run_id, adapter, model, status, created_at, completed_at, error FROM runs WHERE run_id=?`, runID, ) var r RunRecord var completedAt sql.NullTime var errMsg sql.NullString if err := row.Scan(&r.RunID, &r.Adapter, &r.Model, &r.Status, &r.CreatedAt, &completedAt, &errMsg); err != nil { if err == sql.ErrNoRows { return nil, nil } return nil, err } if completedAt.Valid { r.CompletedAt = &completedAt.Time } r.Error = errMsg.String return &r, nil }