iop/apps/node/internal/store/store.go
toki 2d6fde0876 기능: IOP 모노레포 스캐폴드 초기 구현
apps/node 중심 구현 — TCP+JSON transport, Hexagonal Architecture,
mock/cli adapter, fx DI, SQLite 실행 이력 저장.
edge/control-plane/worker는 cobra placeholder.
유닛 테스트 및 통합 테스트 클라이언트 계획서 추가.
2026-05-02 13:20:35 +09:00

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
}