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:
2026-07-27 18:58:21 +02:00
parent c6e2c5c00e
commit 7d371219b7
4 changed files with 118 additions and 6 deletions

View File

@@ -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)
}