package store import ( "context" "database/sql" "errors" "time" ) type CloneJobStatus string const ( CloneJobPending CloneJobStatus = "pending" CloneJobRunning CloneJobStatus = "running" CloneJobCompleted CloneJobStatus = "completed" CloneJobFailed CloneJobStatus = "failed" ) type CloneJob struct { ID int64 RepoID int64 Status CloneJobStatus StartedAt *time.Time FinishedAt *time.Time Error *string CreatedAt time.Time } type CloneJobStore interface { CreateCloneJob(ctx context.Context, repoID int64) (int64, error) GetCloneJob(ctx context.Context, id int64) (*CloneJob, error) GetLatestCloneJob(ctx context.Context, repoID int64) (*CloneJob, error) HasRunningCloneJob(ctx context.Context, repoID int64) (bool, error) MarkCloneJobStarted(ctx context.Context, id int64) error MarkCloneJobFinished(ctx context.Context, id int64, status CloneJobStatus, errMsg *string) error } type SQLiteCloneJobStore struct { db *sql.DB } func NewCloneJobStore(db *sql.DB) *SQLiteCloneJobStore { return &SQLiteCloneJobStore{db: db} } func (s *SQLiteCloneJobStore) CreateCloneJob(ctx context.Context, repoID int64) (int64, error) { res, err := s.db.ExecContext(ctx, `INSERT INTO clone_jobs (repo_id) VALUES (?)`, repoID) if err != nil { return 0, err } return res.LastInsertId() } func (s *SQLiteCloneJobStore) GetCloneJob(ctx context.Context, id int64) (*CloneJob, error) { row := s.db.QueryRowContext(ctx, `SELECT id, repo_id, status, started_at, finished_at, error, created_at FROM clone_jobs WHERE id = ?`, id) return scanCloneJob(row) } func (s *SQLiteCloneJobStore) GetLatestCloneJob(ctx context.Context, repoID int64) (*CloneJob, error) { row := s.db.QueryRowContext(ctx, `SELECT id, repo_id, status, started_at, finished_at, error, created_at FROM clone_jobs WHERE repo_id = ? ORDER BY created_at DESC LIMIT 1`, repoID) return scanCloneJob(row) } func (s *SQLiteCloneJobStore) HasRunningCloneJob(ctx context.Context, repoID int64) (bool, error) { var count int err := s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM clone_jobs WHERE repo_id = ? AND status IN ('pending','running')`, repoID, ).Scan(&count) return count > 0, err } func (s *SQLiteCloneJobStore) MarkCloneJobStarted(ctx context.Context, id int64) error { _, err := s.db.ExecContext(ctx, `UPDATE clone_jobs SET status='running', started_at=CURRENT_TIMESTAMP WHERE id=?`, id) return err } func (s *SQLiteCloneJobStore) MarkCloneJobFinished(ctx context.Context, id int64, status CloneJobStatus, errMsg *string) error { _, err := s.db.ExecContext(ctx, `UPDATE clone_jobs SET status=?, finished_at=CURRENT_TIMESTAMP, error=? WHERE id=?`, status, errMsg, id) return err } func scanCloneJob(row *sql.Row) (*CloneJob, error) { var j CloneJob var startedAt, finishedAt sql.NullTime var errMsg sql.NullString err := row.Scan(&j.ID, &j.RepoID, &j.Status, &startedAt, &finishedAt, &errMsg, &j.CreatedAt) if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } if err != nil { return nil, err } if startedAt.Valid { j.StartedAt = &startedAt.Time } if finishedAt.Valid { j.FinishedAt = &finishedAt.Time } if errMsg.Valid { j.Error = &errMsg.String } return &j, nil }