package postgres import ( "context" "database/sql" "encoding/json" "time" "github.com/orchard9/rdev/internal/domain" "github.com/orchard9/rdev/internal/port" ) // BuildEventRepository implements port.BuildEventStore using PostgreSQL. type BuildEventRepository struct { db *sql.DB } // NewBuildEventRepository creates a new build event repository. func NewBuildEventRepository(db *sql.DB) *BuildEventRepository { return &BuildEventRepository{db: db} } // Ensure BuildEventRepository implements port.BuildEventStore at compile time. var _ port.BuildEventStore = (*BuildEventRepository)(nil) // buildEventRow is the database representation of a build event. type buildEventRow struct { ID string `db:"id"` TaskID string `db:"task_id"` ProjectID string `db:"project_id"` Type string `db:"type"` Sequence int64 `db:"sequence"` Timestamp time.Time `db:"timestamp"` Data []byte `db:"data"` CreatedAt time.Time `db:"created_at"` } // Record stores a build event for later replay. func (r *BuildEventRepository) Record(ctx context.Context, event *domain.BuildEvent) error { dataBytes, err := json.Marshal(event.Data) if err != nil { return err } _, err = r.db.ExecContext(ctx, ` INSERT INTO build_events (id, task_id, project_id, type, sequence, timestamp, data) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (id) DO NOTHING `, event.ID, event.TaskID, event.ProjectID, event.Type, event.Sequence, event.Timestamp, dataBytes) return err } // ListByTask retrieves events for a task, optionally after a sequence number. func (r *BuildEventRepository) ListByTask(ctx context.Context, taskID string, afterSequence int64) ([]*domain.BuildEvent, error) { rows, err := r.db.QueryContext(ctx, ` SELECT id, task_id, project_id, type, sequence, timestamp, data FROM build_events WHERE task_id = $1 AND sequence > $2 ORDER BY sequence ASC `, taskID, afterSequence) if err != nil { return nil, err } defer func() { _ = rows.Close() }() var events []*domain.BuildEvent for rows.Next() { var row buildEventRow if err := rows.Scan(&row.ID, &row.TaskID, &row.ProjectID, &row.Type, &row.Sequence, &row.Timestamp, &row.Data); err != nil { return nil, err } var data domain.BuildEventData if err := json.Unmarshal(row.Data, &data); err != nil { // If unmarshal fails, use empty data data = domain.BuildEventData{} } events = append(events, &domain.BuildEvent{ ID: row.ID, TaskID: row.TaskID, ProjectID: row.ProjectID, Type: domain.BuildEventType(row.Type), Sequence: row.Sequence, Timestamp: row.Timestamp, Data: data, }) } return events, rows.Err() } // Cleanup removes events older than the specified age. func (r *BuildEventRepository) Cleanup(ctx context.Context, olderThan time.Duration) (int64, error) { cutoff := time.Now().Add(-olderThan) result, err := r.db.ExecContext(ctx, ` DELETE FROM build_events WHERE created_at < $1 `, cutoff) if err != nil { return 0, err } return result.RowsAffected() }