package db import ( "context" "fmt" "time" "github.com/jackc/pgx/v5" ) // SolverResult represents a row in the solver_results table. type SolverResult struct { ID string ItemID string RevisionNumber int JobID *string Operation string // solve, diagnose, kinematic SolverName string Status string // SolveStatus string (Success, Failed, etc.) DOF *int Diagnostics []byte // raw JSONB Placements []byte // raw JSONB NumFrames int SolveTimeMS *float64 CreatedAt time.Time } // SolverResultRepository provides solver_results database operations. type SolverResultRepository struct { db *DB } // NewSolverResultRepository creates a new solver result repository. func NewSolverResultRepository(db *DB) *SolverResultRepository { return &SolverResultRepository{db: db} } // Upsert inserts or updates a solver result. The UNIQUE(item_id, revision_number, operation) // constraint means each revision has at most one result per operation type. func (r *SolverResultRepository) Upsert(ctx context.Context, s *SolverResult) error { err := r.db.pool.QueryRow(ctx, ` INSERT INTO solver_results (item_id, revision_number, job_id, operation, solver_name, status, dof, diagnostics, placements, num_frames, solve_time_ms) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) ON CONFLICT (item_id, revision_number, operation) DO UPDATE SET job_id = EXCLUDED.job_id, solver_name = EXCLUDED.solver_name, status = EXCLUDED.status, dof = EXCLUDED.dof, diagnostics = EXCLUDED.diagnostics, placements = EXCLUDED.placements, num_frames = EXCLUDED.num_frames, solve_time_ms = EXCLUDED.solve_time_ms, created_at = now() RETURNING id, created_at `, s.ItemID, s.RevisionNumber, s.JobID, s.Operation, s.SolverName, s.Status, s.DOF, s.Diagnostics, s.Placements, s.NumFrames, s.SolveTimeMS, ).Scan(&s.ID, &s.CreatedAt) if err != nil { return fmt.Errorf("upserting solver result: %w", err) } return nil } // GetByItem returns all solver results for an item, ordered by revision descending. func (r *SolverResultRepository) GetByItem(ctx context.Context, itemID string) ([]*SolverResult, error) { rows, err := r.db.pool.Query(ctx, ` SELECT id, item_id, revision_number, job_id, operation, solver_name, status, dof, diagnostics, placements, num_frames, solve_time_ms, created_at FROM solver_results WHERE item_id = $1 ORDER BY revision_number DESC, operation `, itemID) if err != nil { return nil, fmt.Errorf("listing solver results: %w", err) } defer rows.Close() return scanSolverResults(rows) } // GetByItemRevision returns a single solver result for an item/revision/operation. func (r *SolverResultRepository) GetByItemRevision(ctx context.Context, itemID string, revision int, operation string) (*SolverResult, error) { s := &SolverResult{} err := r.db.pool.QueryRow(ctx, ` SELECT id, item_id, revision_number, job_id, operation, solver_name, status, dof, diagnostics, placements, num_frames, solve_time_ms, created_at FROM solver_results WHERE item_id = $1 AND revision_number = $2 AND operation = $3 `, itemID, revision, operation).Scan( &s.ID, &s.ItemID, &s.RevisionNumber, &s.JobID, &s.Operation, &s.SolverName, &s.Status, &s.DOF, &s.Diagnostics, &s.Placements, &s.NumFrames, &s.SolveTimeMS, &s.CreatedAt, ) if err == pgx.ErrNoRows { return nil, nil } if err != nil { return nil, fmt.Errorf("getting solver result: %w", err) } return s, nil } func scanSolverResults(rows pgx.Rows) ([]*SolverResult, error) { var results []*SolverResult for rows.Next() { s := &SolverResult{} if err := rows.Scan( &s.ID, &s.ItemID, &s.RevisionNumber, &s.JobID, &s.Operation, &s.SolverName, &s.Status, &s.DOF, &s.Diagnostics, &s.Placements, &s.NumFrames, &s.SolveTimeMS, &s.CreatedAt, ); err != nil { return nil, fmt.Errorf("scanning solver result: %w", err) } results = append(results, s) } return results, rows.Err() }