package failures import ( "context" "database/sql" "reanimator/internal/domain" "reanimator/internal/idgen" ) type FailureRepository struct { db *sql.DB idgen *idgen.Generator } func NewFailureRepository(db *sql.DB) *FailureRepository { return &FailureRepository{ db: db, idgen: idgen.NewGenerator(db), } } func (r *FailureRepository) BeginTx(ctx context.Context) (*sql.Tx, error) { return r.db.BeginTx(ctx, nil) } type execer interface { ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row } func execerFor(db *sql.DB, tx *sql.Tx) execer { if tx != nil { return tx } return db } func (r *FailureRepository) Upsert(ctx context.Context, tx *sql.Tx, event domain.FailureEvent) (string, error) { execer := execerFor(r.db, tx) var existingID string err := execer.QueryRowContext(ctx, ` SELECT id FROM failure_events WHERE source = ? AND part_id = ? AND ((machine_id IS NULL AND ? IS NULL) OR machine_id = ?) AND failure_type = ? AND failure_time = ? AND COALESCE(details, '') = COALESCE(?, '') LIMIT 1`, event.Source, event.PartID, event.MachineID, event.MachineID, event.FailureType, event.FailureTime.UTC(), event.Details, ).Scan(&existingID) if err == nil { return existingID, nil } if err != sql.ErrNoRows { return "", err } // Generate ID for new failure events id, err := r.idgen.Generate(ctx, idgen.FailureEvent) if err != nil { return "", err } _, err = execer.ExecContext(ctx, `INSERT INTO failure_events (id, source, external_id, part_id, machine_id, failure_type, failure_time, details, confidence) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE part_id = VALUES(part_id), machine_id = VALUES(machine_id), failure_type = VALUES(failure_type), failure_time = VALUES(failure_time), details = VALUES(details), confidence = VALUES(confidence)`, id, event.Source, event.ExternalID, event.PartID, event.MachineID, event.FailureType, event.FailureTime, event.Details, event.Confidence, ) if err != nil { return "", err } var resultID string row := execer.QueryRowContext(ctx, `SELECT id FROM failure_events WHERE source = ? AND external_id = ?`, event.Source, event.ExternalID, ) if err := row.Scan(&resultID); err != nil { return "", err } return resultID, nil } func (r *FailureRepository) ListAll(ctx context.Context, limit int) ([]domain.FailureEvent, error) { if limit <= 0 { limit = 200 } rows, err := r.db.QueryContext(ctx, `SELECT id, source, external_id, part_id, machine_id, failure_type, failure_time, details, confidence, created_at FROM failure_events ORDER BY failure_time DESC, created_at DESC LIMIT ?`, limit, ) if err != nil { return nil, err } defer rows.Close() items := make([]domain.FailureEvent, 0) for rows.Next() { var event domain.FailureEvent var machineID sql.NullString var details sql.NullString var confidence sql.NullFloat64 if err := rows.Scan(&event.ID, &event.Source, &event.ExternalID, &event.PartID, &machineID, &event.FailureType, &event.FailureTime, &details, &confidence, &event.CreatedAt); err != nil { return nil, err } if machineID.Valid { value := machineID.String event.MachineID = &value } if details.Valid { value := details.String event.Details = &value } if confidence.Valid { value := confidence.Float64 event.Confidence = &value } items = append(items, event) } if err := rows.Err(); err != nil { return nil, err } return items, nil } func (r *FailureRepository) Get(ctx context.Context, id string) (domain.FailureEvent, error) { row := r.db.QueryRowContext(ctx, `SELECT id, source, external_id, part_id, machine_id, failure_type, failure_time, details, confidence, created_at FROM failure_events WHERE id = ?`, id, ) var event domain.FailureEvent var machineID sql.NullString var details sql.NullString var confidence sql.NullFloat64 if err := row.Scan(&event.ID, &event.Source, &event.ExternalID, &event.PartID, &machineID, &event.FailureType, &event.FailureTime, &details, &confidence, &event.CreatedAt); err != nil { return domain.FailureEvent{}, err } if machineID.Valid { value := machineID.String event.MachineID = &value } if details.Valid { value := details.String event.Details = &value } if confidence.Valid { value := confidence.Float64 event.Confidence = &value } return event, nil }