feat(store): add ActivitiesMissingWorkout/CountActivitiesMissingWorkout

Mirrors ActivitiesMissingDetails/CountActivitiesMissingDetails, but
queries workout_id IS NOT NULL AND workout_raw_json IS NULL --
independent of whether details/splits were ever fetched, since
workout_id is known at initial upsert time. This also fixes a latent
gap: an activity whose details were fetched successfully but whose
workout fetch failed in the same run previously had no way to ever be
retried, since ActivitiesMissingDetails stops returning it the moment
details_fetched_at/splits_fetched_at are set.
This commit is contained in:
2026-07-27 06:49:44 +02:00
parent 7204a48c1c
commit 32fdfc1c72
2 changed files with 124 additions and 0 deletions

View File

@@ -272,3 +272,47 @@ func (db *DB) CountActivitiesMissingDetails(ctx context.Context, userID int64) (
}
return n, nil
}
// ActivitiesMissingWorkout returns userID's activities that have a
// structured workout (WorkoutID set at initial upsert time, straight from
// Garmin's activity summary) but haven't had get_workout_by_id fetched yet.
// Independently queryable from ActivitiesMissingDetails: workout_id is
// known well before any detail fetch, and workout_raw_json is only ever
// set by SetActivityWorkout, so this also picks up an activity whose
// details were fetched successfully in some prior run but whose workout
// fetch failed back then -- ActivitiesMissingDetails would never surface
// that activity again (details_fetched_at/splits_fetched_at are already
// set), silently losing its target pace/HR bands forever without this.
func (db *DB) ActivitiesMissingWorkout(ctx context.Context, userID int64, limit int) ([]Activity, error) {
rows, err := db.QueryContext(ctx, `SELECT `+activityColumns+` FROM activities
WHERE user_id = ? AND workout_id IS NOT NULL AND workout_raw_json IS NULL
ORDER BY start_time_utc DESC LIMIT ?`, userID, limit)
if err != nil {
return nil, fmt.Errorf("list activities missing workout for user %d: %w", userID, err)
}
defer rows.Close()
activities := []Activity{}
for rows.Next() {
a, err := scanActivity(rows)
if err != nil {
return nil, fmt.Errorf("scan activity row: %w", err)
}
activities = append(activities, a)
}
return activities, rows.Err()
}
// CountActivitiesMissingWorkout returns how many of userID's activities
// still need get_workout_by_id fetched, regardless of any per-call batch
// limit -- used to report overall remaining work, mirroring
// CountActivitiesMissingDetails.
func (db *DB) CountActivitiesMissingWorkout(ctx context.Context, userID int64) (int, error) {
var n int
err := db.QueryRowContext(ctx, `SELECT COUNT(*) FROM activities
WHERE user_id = ? AND workout_id IS NOT NULL AND workout_raw_json IS NULL`, userID).Scan(&n)
if err != nil {
return 0, fmt.Errorf("count activities missing workout for user %d: %w", userID, err)
}
return n, nil
}