- 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
276 lines
6.4 KiB
Go
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
|
|
}
|