package store import ( "context" "database/sql" "fmt" "vetting/internal/model" ) type Artifact struct { ID int64 RunID int64 StageID *int64 Kind string // inventory|spec_diff|hold_key|report|log|fio|iperf|smart Path string SHA256 string SizeBytes int64 } type Artifacts struct { DB *sql.DB } func (a *Artifacts) Create(ctx context.Context, art Artifact) (int64, error) { res, err := a.DB.ExecContext(ctx, ` INSERT INTO artifacts(run_id, stage_id, kind, path, sha256, size_bytes) VALUES(?,?,?,?,?,?) `, art.RunID, nullInt64(art.StageID), art.Kind, art.Path, art.SHA256, art.SizeBytes) if err != nil { return 0, fmt.Errorf("insert artifact: %w", err) } return res.LastInsertId() } // DeleteForRun removes every artifact row for a run. Returns the rows // that were deleted so the caller can unlink the on-disk files. Used by // the janitor; ordinary flow treats artifacts as append-only. func (a *Artifacts) DeleteForRun(ctx context.Context, runID int64) ([]Artifact, error) { arts, err := a.ListForRun(ctx, runID) if err != nil { return nil, err } if _, err := a.DB.ExecContext(ctx, `DELETE FROM artifacts WHERE run_id = ?`, runID); err != nil { return nil, fmt.Errorf("delete artifacts for run %d: %w", runID, err) } return arts, nil } func (a *Artifacts) ListForRun(ctx context.Context, runID int64) ([]Artifact, error) { rows, err := a.DB.QueryContext(ctx, ` SELECT id, run_id, stage_id, kind, path, sha256, size_bytes FROM artifacts WHERE run_id = ? ORDER BY id `, runID) if err != nil { return nil, err } defer rows.Close() var out []Artifact for rows.Next() { var ar Artifact var stageID sql.NullInt64 if err := rows.Scan(&ar.ID, &ar.RunID, &stageID, &ar.Kind, &ar.Path, &ar.SHA256, &ar.SizeBytes); err != nil { return nil, err } if stageID.Valid { v := stageID.Int64 ar.StageID = &v } out = append(out, ar) } return out, rows.Err() } type SpecDiffs struct { DB *sql.DB } func (s *SpecDiffs) ReplaceForRun(ctx context.Context, runID int64, diffs []model.SpecDiff) error { tx, err := s.DB.BeginTx(ctx, nil) if err != nil { return err } defer func() { _ = tx.Rollback() }() if _, err := tx.ExecContext(ctx, `DELETE FROM spec_diffs WHERE run_id = ?`, runID); err != nil { return err } for _, d := range diffs { if _, err := tx.ExecContext(ctx, ` INSERT INTO spec_diffs(run_id, field, expected, actual, severity, ignored) VALUES(?,?,?,?,?,?) `, runID, d.Field, d.Expected, d.Actual, d.Severity, 0); err != nil { return err } } return tx.Commit() } func (s *SpecDiffs) ListForRun(ctx context.Context, runID int64) ([]model.SpecDiff, error) { rows, err := s.DB.QueryContext(ctx, ` SELECT id, run_id, field, COALESCE(expected,''), COALESCE(actual,''), severity, ignored FROM spec_diffs WHERE run_id = ? ORDER BY id `, runID) if err != nil { return nil, err } defer rows.Close() var out []model.SpecDiff for rows.Next() { var d model.SpecDiff var ignored int if err := rows.Scan(&d.ID, &d.RunID, &d.Field, &d.Expected, &d.Actual, &d.Severity, &ignored); err != nil { return nil, err } d.Ignored = ignored != 0 out = append(out, d) } return out, rows.Err() } func nullInt64(p *int64) any { if p == nil { return nil } return *p }