feat(store): add workout_not_found_at, exclude it from missing-workout queries
A confirmed-404 workout must stop being retried forever, but workout_raw_json should never be fabricated -- it stays null exactly as it does for "not yet fetched." workout_not_found_at is the separate marker ActivitiesMissingWorkout/CountActivitiesMissingWorkout now check for, mirroring the existing pattern of the other _fetched_at columns. UpsertActivity's ON CONFLICT clause already never touches these columns, so the marker persists across every later re-sync.
This commit is contained in:
@@ -45,8 +45,13 @@ type Activity struct {
|
||||
// internal/sync/mapping.go's alignWorkoutTargets). Nil when the activity
|
||||
// has no WorkoutID, or was synced before this column existed.
|
||||
WorkoutRawJSON *string
|
||||
CreatedAt string
|
||||
UpdatedAt string
|
||||
// WorkoutNotFoundAt is set when get_workout_by_id returned a definitive
|
||||
// HTTP 404 for this activity's WorkoutID -- distinct from
|
||||
// WorkoutRawJSON staying nil for "not yet fetched" (see
|
||||
// SetActivityWorkoutNotFound).
|
||||
WorkoutNotFoundAt *string
|
||||
CreatedAt string
|
||||
UpdatedAt string
|
||||
}
|
||||
|
||||
// UpsertActivity inserts a new activity for userID or updates the existing
|
||||
@@ -102,7 +107,7 @@ func scanActivity(row interface{ Scan(...any) error }) (Activity, error) {
|
||||
&a.AvgSpeedMps, &a.ElevationGainM,
|
||||
&a.AerobicTrainingEffect, &a.AnaerobicTrainingEffect, &a.VO2MaxValue,
|
||||
&a.RawJSON, &a.DetailsFetchedAt, &a.DetailsRawJSON, &a.SplitsFetchedAt, &a.WorkoutRawJSON,
|
||||
&a.CreatedAt, &a.UpdatedAt,
|
||||
&a.WorkoutNotFoundAt, &a.CreatedAt, &a.UpdatedAt,
|
||||
)
|
||||
return a, err
|
||||
}
|
||||
@@ -113,7 +118,7 @@ const activityColumns = `
|
||||
avg_speed_mps, elevation_gain_m,
|
||||
aerobic_training_effect, anaerobic_training_effect, vo2max_value,
|
||||
raw_json, details_fetched_at, details_raw_json, splits_fetched_at, workout_raw_json,
|
||||
created_at, updated_at
|
||||
workout_not_found_at, created_at, updated_at
|
||||
`
|
||||
|
||||
// GetActivity fetches one activity by its internal id, scoped to userID.
|
||||
@@ -225,6 +230,21 @@ func (db *DB) SetActivityWorkout(ctx context.Context, userID, activityID int64,
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetActivityWorkoutNotFound records that get_workout_by_id returned a
|
||||
// definitive HTTP 404 for this activity's WorkoutID -- the workout was
|
||||
// deleted on Garmin's side after being linked to this activity.
|
||||
// WorkoutRawJSON is deliberately left nil (never fabricated); this is a
|
||||
// separate marker so ActivitiesMissingWorkout stops retrying it forever.
|
||||
func (db *DB) SetActivityWorkoutNotFound(ctx context.Context, userID, activityID int64) error {
|
||||
_, err := db.ExecContext(ctx, `
|
||||
UPDATE activities SET workout_not_found_at = datetime('now'), updated_at = datetime('now')
|
||||
WHERE id = ? AND user_id = ?`, activityID, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("set activity %d workout not found for user %d: %w", activityID, userID, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetActivitySplitsFetched records that get_activity_splits has been fetched
|
||||
// for this activity.
|
||||
func (db *DB) SetActivitySplitsFetched(ctx context.Context, userID, activityID int64) error {
|
||||
@@ -293,7 +313,8 @@ func (db *DB) CountActivitiesMissingDetails(ctx context.Context, userID int64) (
|
||||
// that simply haven't been processed by fillActivityDetails at all yet.
|
||||
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 AND details_fetched_at IS NOT NULL
|
||||
WHERE user_id = ? AND workout_id IS NOT NULL AND workout_raw_json IS NULL
|
||||
AND details_fetched_at IS NOT NULL AND workout_not_found_at 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)
|
||||
@@ -319,7 +340,8 @@ func (db *DB) ActivitiesMissingWorkout(ctx context.Context, userID int64, limit
|
||||
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 AND details_fetched_at IS NOT NULL`, userID).Scan(&n)
|
||||
WHERE user_id = ? AND workout_id IS NOT NULL AND workout_raw_json IS NULL
|
||||
AND details_fetched_at IS NOT NULL AND workout_not_found_at IS NULL`, userID).Scan(&n)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("count activities missing workout for user %d: %w", userID, err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user