apps/node 중심 구현 — TCP+JSON transport, Hexagonal Architecture, mock/cli adapter, fx DI, SQLite 실행 이력 저장. edge/control-plane/worker는 cobra placeholder. 유닛 테스트 및 통합 테스트 클라이언트 계획서 추가.
100 lines
2.5 KiB
Go
100 lines
2.5 KiB
Go
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
|
|
}
|