Merge backfill into Sync now, replace Full backfill with a destructive Reset all
Sync now now runs Backfill (resumes from the watermark, so a widened history horizon is picked up automatically) then IncrementalSync then detail-fill, in one click. "Full backfill" is gone -- in its place, "Reset all" (styled as a destructive action, gated by a confirm dialog) wipes every synced activity and its laps/kind assignments and rewinds the backfill watermark, so the next sync performs a genuinely fresh pull instead of trying to patch up existing rows with newer schema fields.
This commit is contained in:
27
backend/internal/store/reset.go
Normal file
27
backend/internal/store/reset.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ResetAllSyncedData deletes every synced activity (cascading to its laps,
|
||||
// activity_samples, and kind_assignments via ON DELETE CASCADE) and rewinds
|
||||
// the backfill watermark to its initial state, so a subsequent Backfill
|
||||
// starts a genuinely fresh pull instead of thinking history is already
|
||||
// covered. Workout kinds (the user's taxonomy) are left untouched.
|
||||
func (db *DB) ResetAllSyncedData(ctx context.Context) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin reset tx: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
if _, err := tx.ExecContext(ctx, `DELETE FROM activities`); err != nil {
|
||||
return fmt.Errorf("delete activities: %w", err)
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `UPDATE sync_state SET earliest_synced_date = NULL, backfill_complete = 0 WHERE id = 1`); err != nil {
|
||||
return fmt.Errorf("reset sync state: %w", err)
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
71
backend/internal/store/reset_test.go
Normal file
71
backend/internal/store/reset_test.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResetAllSyncedData_DeletesActivitiesCascadesAndRewindsWatermark(t *testing.T) {
|
||||
db := openTestDB(t)
|
||||
ctx := context.Background()
|
||||
|
||||
activityID, err := db.UpsertActivity(ctx, Activity{GarminActivityID: 1, ActivityType: "running", StartTimeUTC: "2026-07-11 06:00:00", RawJSON: "{}"})
|
||||
if err != nil {
|
||||
t.Fatalf("UpsertActivity: %v", err)
|
||||
}
|
||||
kindID, err := db.CreateWorkoutKind(ctx, WorkoutKind{Name: "Test Reset Kind", RuleJSON: `{"match":"all","conditions":[]}`, IsActive: true})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateWorkoutKind: %v", err)
|
||||
}
|
||||
if err := db.ReplaceLaps(ctx, activityID, []Lap{{LapIndex: 1}}); err != nil {
|
||||
t.Fatalf("ReplaceLaps: %v", err)
|
||||
}
|
||||
if _, err := db.InsertKindAssignment(ctx, KindAssignment{
|
||||
ActivityID: activityID, WorkoutKindID: &kindID, AssignmentSource: AssignmentSourceRuleEngine,
|
||||
Status: AssignmentStatusAssigned, CandidateKindsJSON: "[]",
|
||||
}); err != nil {
|
||||
t.Fatalf("InsertKindAssignment: %v", err)
|
||||
}
|
||||
if err := db.UpdateSyncState(ctx, "2020-01-01", true); err != nil {
|
||||
t.Fatalf("UpdateSyncState: %v", err)
|
||||
}
|
||||
|
||||
if err := db.ResetAllSyncedData(ctx); err != nil {
|
||||
t.Fatalf("ResetAllSyncedData: %v", err)
|
||||
}
|
||||
|
||||
activities, err := db.ListActivities(ctx, ActivityFilter{})
|
||||
if err != nil {
|
||||
t.Fatalf("ListActivities: %v", err)
|
||||
}
|
||||
if len(activities) != 0 {
|
||||
t.Errorf("expected 0 activities after reset, got %d", len(activities))
|
||||
}
|
||||
laps, err := db.LapsForActivity(ctx, activityID)
|
||||
if err != nil {
|
||||
t.Fatalf("LapsForActivity: %v", err)
|
||||
}
|
||||
if len(laps) != 0 {
|
||||
t.Errorf("expected laps to cascade-delete, got %d", len(laps))
|
||||
}
|
||||
if _, ok, err := db.CurrentAssignment(ctx, activityID); err != nil || ok {
|
||||
t.Errorf("expected kind assignment to cascade-delete, ok=%v err=%v", ok, err)
|
||||
}
|
||||
|
||||
state, err := db.GetSyncState(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GetSyncState: %v", err)
|
||||
}
|
||||
if state.EarliestSyncedDate != nil || state.BackfillComplete {
|
||||
t.Errorf("expected watermark rewound to fresh state, got %+v", state)
|
||||
}
|
||||
|
||||
// Workout kinds (the user's taxonomy) must survive a reset.
|
||||
kind, ok, err := db.GetWorkoutKind(ctx, kindID)
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("GetWorkoutKind after reset: ok=%v err=%v", ok, err)
|
||||
}
|
||||
if kind.Name != "Test Reset Kind" {
|
||||
t.Errorf("workout kind was unexpectedly affected by reset: %+v", kind)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user