package store import ( "context" "database/sql" "errors" "time" ) type IncidentView struct { IncidentUUID [16]byte DetectedAt time.Time ResolvedAt *time.Time State IncidentState Kind IncidentKind Scope IncidentScope ClientID string IssueUUID *[16]byte Summary string DataLoss bool AutomaticallyRepairable bool } type IncidentPage struct { IncludeResolved bool Limit uint32 SnapshotBoundary int64 AfterDetectedAt int64 AfterUUID [16]byte HasAfter bool } func (store *Store) ListIncidentViews(ctx context.Context, page IncidentPage) ([]IncidentView, bool, error) { if page.Limit == 0 || page.Limit > 1000 || page.SnapshotBoundary <= 0 { return nil, false, errors.New("invalid incident page") } database, err := store.openDatabase() if err != nil { return nil, false, err } query := `SELECT incident_uuid, detected_at, resolved_at, state, kind, scope, client_id, issue_uuid, summary, data_loss, automatically_repairable FROM storage_incidents WHERE detected_at <= ?` args := []any{page.SnapshotBoundary} if !page.IncludeResolved { query += ` AND state = 1` } if page.HasAfter { query += ` AND (detected_at < ? OR (detected_at = ? AND incident_uuid < ?))` args = append(args, page.AfterDetectedAt, page.AfterDetectedAt, page.AfterUUID[:]) } query += ` ORDER BY detected_at DESC, incident_uuid DESC LIMIT ?` args = append(args, page.Limit+1) rows, err := database.QueryContext(ctx, query, args...) if err != nil { return nil, false, err } defer rows.Close() result := make([]IncidentView, 0, page.Limit) for rows.Next() { view, scanErr := scanIncidentView(rows) if scanErr != nil { return nil, false, scanErr } if uint32(len(result)) < page.Limit { result = append(result, view) } else { return result, true, rows.Err() } } if err := rows.Err(); err != nil { return nil, false, err } return result, false, nil } func (store *Store) GetIncidentView(ctx context.Context, incidentUUID [16]byte) (IncidentView, error) { if incidentUUID == [16]byte{} { return IncidentView{}, ErrIncidentNotFound } database, err := store.openDatabase() if err != nil { return IncidentView{}, err } view, err := scanIncidentView(database.QueryRowContext(ctx, `SELECT incident_uuid, detected_at, resolved_at, state, kind, scope, client_id, issue_uuid, summary, data_loss, automatically_repairable FROM storage_incidents WHERE incident_uuid = ?`, incidentUUID[:])) if errors.Is(err, sql.ErrNoRows) { return IncidentView{}, ErrIncidentNotFound } return view, err } func scanIncidentView(scanner interface{ Scan(...any) error }) (IncidentView, error) { var view IncidentView var encoded, issue []byte var detected int64 var resolved sql.NullInt64 var state, kind uint32 var client sql.NullString var dataLoss, repairable int if err := scanner.Scan(&encoded, &detected, &resolved, &state, &kind, &view.Scope, &client, &issue, &view.Summary, &dataLoss, &repairable); err != nil { return view, err } if len(encoded) != 16 || (issue != nil && len(issue) != 16) || state < uint32(IncidentOpen) || state > uint32(IncidentAcknowledged) || kind < uint32(IncidentUncommittedTail) || kind > uint32(IncidentDiskExhaustion) { return view, ErrInvalidSegmentRecord } copy(view.IncidentUUID[:], encoded) view.DetectedAt = time.Unix(0, detected).UTC() view.ResolvedAt = nullableTime(resolved) view.State, view.Kind = IncidentState(state), IncidentKind(kind) if client.Valid { view.ClientID = client.String } view.DataLoss, view.AutomaticallyRepairable = dataLoss == 1, repairable == 1 if issue != nil { value := [16]byte{} copy(value[:], issue) view.IssueUUID = &value } return view, nil }