fetch_ml/cmd/tui/internal/store/store.go
Jeremie Fraeys a3b957dcc0
refactor(cli): Update build system and core infrastructure
- Makefile: Update build targets for native library integration
- build.zig: Add SQLite linking and native hash library support
- scripts/build_rsync.sh: Update rsync embedded binary build process
- scripts/build_sqlite.sh: Add SQLite constants generation script
- src/assets/README.md: Document embedded asset structure
- src/utils/rsync_embedded_binary.zig: Update for new build layout
2026-02-20 21:39:51 -05:00

276 lines
6.4 KiB
Go

// Package store provides SQLite storage for TUI local mode
package store
import (
"database/sql"
"fmt"
"os"
"path/filepath"
_ "modernc.org/sqlite" // SQLite driver
)
// Store handles SQLite operations for the TUI
type Store struct {
db *sql.DB
dbPath string
}
// RunInfo represents a local run from SQLite
type RunInfo struct {
RunID string
ExperimentID string
Name string
Status string
StartTime string
EndTime *string
PID *int64
Synced bool
}
// Metric represents a logged metric
type Metric struct {
Key string
Value float64
Step int64
}
// Param represents a logged parameter
type Param struct {
Key string
Value string
}
// Open opens the SQLite database at the given path
func Open(dbPath string) (*Store, error) {
// Ensure directory exists
dir := filepath.Dir(dbPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return nil, fmt.Errorf("failed to create directory: %w", err)
}
db, err := sql.Open("sqlite", dbPath)
if err != nil {
return nil, fmt.Errorf("failed to open database: %w", err)
}
// Enable WAL mode for better concurrency
if _, err := db.Exec("PRAGMA journal_mode=WAL;"); err != nil {
_ = db.Close()
return nil, fmt.Errorf("failed to enable WAL: %w", err)
}
// Set synchronous mode for performance
if _, err := db.Exec("PRAGMA synchronous=NORMAL;"); err != nil {
_ = db.Close()
return nil, fmt.Errorf("failed to set synchronous: %w", err)
}
store := &Store{
db: db,
dbPath: dbPath,
}
// Initialize schema if needed
if err := store.initSchema(); err != nil {
_ = db.Close()
return nil, fmt.Errorf("failed to init schema: %w", err)
}
return store, nil
}
// Close closes the database connection
func (s *Store) Close() error {
if s.db != nil {
// Checkpoint WAL before closing
_, _ = s.db.Exec("PRAGMA wal_checkpoint(TRUNCATE);")
return s.db.Close()
}
return nil
}
// initSchema creates the required tables if they don't exist
func (s *Store) initSchema() error {
schema := `
CREATE TABLE IF NOT EXISTS ml_experiments (
experiment_id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
status TEXT DEFAULT 'active'
);
CREATE TABLE IF NOT EXISTS ml_runs (
run_id TEXT PRIMARY KEY,
experiment_id TEXT NOT NULL,
name TEXT,
status TEXT NOT NULL,
start_time DATETIME DEFAULT CURRENT_TIMESTAMP,
end_time DATETIME,
pid INTEGER,
synced INTEGER DEFAULT 0,
FOREIGN KEY (experiment_id) REFERENCES ml_experiments(experiment_id)
);
CREATE TABLE IF NOT EXISTS ml_metrics (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id TEXT NOT NULL,
key TEXT NOT NULL,
value REAL NOT NULL,
step INTEGER DEFAULT 0,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (run_id) REFERENCES ml_runs(run_id)
);
CREATE TABLE IF NOT EXISTS ml_params (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
FOREIGN KEY (run_id) REFERENCES ml_runs(run_id)
);
CREATE TABLE IF NOT EXISTS ml_tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
FOREIGN KEY (run_id) REFERENCES ml_runs(run_id)
);
CREATE INDEX IF NOT EXISTS idx_runs_experiment ON ml_runs(experiment_id);
CREATE INDEX IF NOT EXISTS idx_runs_status ON ml_runs(status);
CREATE INDEX IF NOT EXISTS idx_metrics_run ON ml_metrics(run_id);
CREATE INDEX IF NOT EXISTS idx_params_run ON ml_params(run_id);
CREATE INDEX IF NOT EXISTS idx_tags_run ON ml_tags(run_id);
`
_, err := s.db.Exec(schema)
return err
}
// GetUnsyncedRuns returns all runs that haven't been synced to the server
func (s *Store) GetUnsyncedRuns() ([]RunInfo, error) {
rows, err := s.db.Query(`
SELECT run_id, experiment_id, name, status, start_time, end_time, pid, synced
FROM ml_runs
WHERE synced = 0
ORDER BY start_time DESC
`)
if err != nil {
return nil, err
}
defer rows.Close()
var runs []RunInfo
for rows.Next() {
var r RunInfo
var endTime sql.NullString
var pid sql.NullInt64
err := rows.Scan(&r.RunID, &r.ExperimentID, &r.Name, &r.Status, &r.StartTime, &endTime, &pid, &r.Synced)
if err != nil {
return nil, err
}
if endTime.Valid {
r.EndTime = &endTime.String
}
if pid.Valid {
r.PID = &pid.Int64
}
runs = append(runs, r)
}
return runs, rows.Err()
}
// GetRunsByExperiment returns all runs for a given experiment
func (s *Store) GetRunsByExperiment(expID string) ([]RunInfo, error) {
rows, err := s.db.Query(`
SELECT run_id, experiment_id, name, status, start_time, end_time, pid, synced
FROM ml_runs
WHERE experiment_id = ?
ORDER BY start_time DESC
`, expID)
if err != nil {
return nil, err
}
defer rows.Close()
var runs []RunInfo
for rows.Next() {
var r RunInfo
var endTime sql.NullString
var pid sql.NullInt64
err := rows.Scan(&r.RunID, &r.ExperimentID, &r.Name, &r.Status, &r.StartTime, &endTime, &pid, &r.Synced)
if err != nil {
return nil, err
}
if endTime.Valid {
r.EndTime = &endTime.String
}
if pid.Valid {
r.PID = &pid.Int64
}
runs = append(runs, r)
}
return runs, rows.Err()
}
// MarkRunSynced marks a run as synced to the server
func (s *Store) MarkRunSynced(runID string) error {
_, err := s.db.Exec("UPDATE ml_runs SET synced = 1 WHERE run_id = ?", runID)
return err
}
// GetRunMetrics returns all metrics for a run
func (s *Store) GetRunMetrics(runID string) ([]Metric, error) {
rows, err := s.db.Query(`
SELECT key, value, step
FROM ml_metrics
WHERE run_id = ?
ORDER BY step ASC, key ASC
`, runID)
if err != nil {
return nil, err
}
defer rows.Close()
var metrics []Metric
for rows.Next() {
var m Metric
if err := rows.Scan(&m.Key, &m.Value, &m.Step); err != nil {
return nil, err
}
metrics = append(metrics, m)
}
return metrics, rows.Err()
}
// GetRunParams returns all params for a run
func (s *Store) GetRunParams(runID string) ([]Param, error) {
rows, err := s.db.Query(`
SELECT key, value
FROM ml_params
WHERE run_id = ?
ORDER BY key ASC
`, runID)
if err != nil {
return nil, err
}
defer rows.Close()
var params []Param
for rows.Next() {
var p Param
if err := rows.Scan(&p.Key, &p.Value); err != nil {
return nil, err
}
params = append(params, p)
}
return params, rows.Err()
}
// DB returns the underlying database connection
func (s *Store) DB() *sql.DB {
return s.db
}