diff --git a/internal/store/runs.go b/internal/store/runs.go new file mode 100644 index 0000000..c02ca33 --- /dev/null +++ b/internal/store/runs.go @@ -0,0 +1,90 @@ +package store + +import ( + "context" + "database/sql" + "fmt" +) + +type RunRecord struct { + ID int64 `json:"id"` + RepoID int64 `json:"repo_id"` + Kind string `json:"kind"` + Scope string `json:"scope"` + Status string `json:"status"` + StartedAt string `json:"started_at"` + FinishedAt string `json:"finished_at,omitempty"` + StatsJSON string `json:"stats_json,omitempty"` + ErrorText string `json:"error_text,omitempty"` +} + +func (s *Store) RecordRun(ctx context.Context, run RunRecord) (int64, error) { + table, err := runTable(run.Kind) + if err != nil { + return 0, err + } + var id int64 + err = s.db.QueryRowContext(ctx, ` + insert into `+table+`(repo_id, scope, status, started_at, finished_at, stats_json, error_text) + values(?, ?, ?, ?, ?, ?, ?) + returning id + `, run.RepoID, run.Scope, run.Status, run.StartedAt, nullString(run.FinishedAt), nullString(run.StatsJSON), nullString(run.ErrorText)).Scan(&id) + if err != nil { + return 0, fmt.Errorf("record %s run: %w", run.Kind, err) + } + return id, nil +} + +func (s *Store) ListRuns(ctx context.Context, repoID int64, kind string, limit int) ([]RunRecord, error) { + table, err := runTable(kind) + if err != nil { + return nil, err + } + if limit <= 0 { + limit = 20 + } + rows, err := s.db.QueryContext(ctx, ` + select id, repo_id, scope, status, started_at, finished_at, stats_json, error_text + from `+table+` + where repo_id = ? + order by id desc + limit ? + `, repoID, limit) + if err != nil { + return nil, fmt.Errorf("list %s runs: %w", kind, err) + } + defer rows.Close() + + var out []RunRecord + for rows.Next() { + var run RunRecord + var finishedAt, statsJSON, errorText sql.NullString + if err := rows.Scan(&run.ID, &run.RepoID, &run.Scope, &run.Status, &run.StartedAt, &finishedAt, &statsJSON, &errorText); err != nil { + return nil, fmt.Errorf("scan %s run: %w", kind, err) + } + run.Kind = kind + run.FinishedAt = finishedAt.String + run.StatsJSON = statsJSON.String + run.ErrorText = errorText.String + out = append(out, run) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("iterate %s runs: %w", kind, err) + } + return out, nil +} + +func runTable(kind string) (string, error) { + switch kind { + case "sync": + return "sync_runs", nil + case "summary": + return "summary_runs", nil + case "embedding": + return "embedding_runs", nil + case "cluster": + return "cluster_runs", nil + default: + return "", fmt.Errorf("unsupported run kind %q", kind) + } +} diff --git a/internal/store/runs_test.go b/internal/store/runs_test.go new file mode 100644 index 0000000..b2cd1e0 --- /dev/null +++ b/internal/store/runs_test.go @@ -0,0 +1,46 @@ +package store + +import ( + "context" + "path/filepath" + "testing" +) + +func TestRecordAndListRuns(t *testing.T) { + ctx := context.Background() + st, err := Open(ctx, filepath.Join(t.TempDir(), "gitcrawl.db")) + if err != nil { + t.Fatalf("open store: %v", err) + } + defer st.Close() + + repoID, err := st.UpsertRepository(ctx, Repository{ + Owner: "openclaw", + Name: "gitcrawl", + FullName: "openclaw/gitcrawl", + RawJSON: "{}", + UpdatedAt: "2026-04-26T00:00:00Z", + }) + if err != nil { + t.Fatalf("repo: %v", err) + } + if _, err := st.RecordRun(ctx, RunRecord{ + RepoID: repoID, + Kind: "sync", + Scope: "open", + Status: "success", + StartedAt: "2026-04-26T00:00:00Z", + FinishedAt: "2026-04-26T00:00:01Z", + StatsJSON: `{"threads_synced":1}`, + }); err != nil { + t.Fatalf("record run: %v", err) + } + + runs, err := st.ListRuns(ctx, repoID, "sync", 10) + if err != nil { + t.Fatalf("list runs: %v", err) + } + if len(runs) != 1 || runs[0].Kind != "sync" || runs[0].Status != "success" { + t.Fatalf("unexpected runs: %#v", runs) + } +}