package store import ( "context" "database/sql" "fmt" ) // WorkoutTypePace is a workout kind's user-declared target pace range and HR // range (percent of heart rate reserve). Informational only -- never read by // the classification rule engine. No history: fields are overwritten in // place. Has no user_id column of its own -- ownership is checked via a // join to workout_kinds.user_id, since it's always accessed 1:1 through a // specific workout kind. type WorkoutTypePace struct { WorkoutKindID int64 PaceMinSecPerKm *float64 PaceMaxSecPerKm *float64 HRMinPctHRR *float64 HRMaxPctHRR *float64 } func scanWorkoutTypePace(row interface{ Scan(...any) error }) (WorkoutTypePace, error) { var p WorkoutTypePace err := row.Scan(&p.WorkoutKindID, &p.PaceMinSecPerKm, &p.PaceMaxSecPerKm, &p.HRMinPctHRR, &p.HRMaxPctHRR) return p, err } const workoutTypePaceColumns = `wtp.workout_kind_id, wtp.pace_min_sec_per_km, wtp.pace_max_sec_per_km, wtp.hr_min_pct_hrr, wtp.hr_max_pct_hrr` // GetWorkoutTypePace fetches the pace/zone row for one workout kind, scoped // to userID via a join to workout_kinds. func (db *DB) GetWorkoutTypePace(ctx context.Context, userID, workoutKindID int64) (WorkoutTypePace, error) { row := db.QueryRowContext(ctx, ` SELECT `+workoutTypePaceColumns+` FROM workout_type_paces wtp JOIN workout_kinds wk ON wk.id = wtp.workout_kind_id WHERE wtp.workout_kind_id = ? AND wk.user_id = ?`, workoutKindID, userID) p, err := scanWorkoutTypePace(row) if err == sql.ErrNoRows { return WorkoutTypePace{WorkoutKindID: workoutKindID}, nil } if err != nil { return WorkoutTypePace{}, fmt.Errorf("get workout type pace for kind %d (user %d): %w", workoutKindID, userID, err) } return p, nil } // UpdateWorkoutTypePace overwrites the pace/zone row for one workout kind, // scoped so it can only ever affect a kind owned by userID. func (db *DB) UpdateWorkoutTypePace(ctx context.Context, userID int64, p WorkoutTypePace) error { _, err := db.ExecContext(ctx, ` UPDATE workout_type_paces SET pace_min_sec_per_km=?, pace_max_sec_per_km=?, hr_min_pct_hrr=?, hr_max_pct_hrr=? WHERE workout_kind_id=? AND workout_kind_id IN (SELECT id FROM workout_kinds WHERE user_id=?)`, p.PaceMinSecPerKm, p.PaceMaxSecPerKm, p.HRMinPctHRR, p.HRMaxPctHRR, p.WorkoutKindID, userID) if err != nil { return fmt.Errorf("update workout type pace for kind %d (user %d): %w", p.WorkoutKindID, userID, err) } return nil } // ListWorkoutTypePaces returns every one of userID's workout kinds' pace/zone rows. func (db *DB) ListWorkoutTypePaces(ctx context.Context, userID int64) ([]WorkoutTypePace, error) { rows, err := db.QueryContext(ctx, ` SELECT `+workoutTypePaceColumns+` FROM workout_type_paces wtp JOIN workout_kinds wk ON wk.id = wtp.workout_kind_id WHERE wk.user_id = ? ORDER BY wtp.workout_kind_id`, userID) if err != nil { return nil, fmt.Errorf("list workout type paces for user %d: %w", userID, err) } defer rows.Close() paces := []WorkoutTypePace{} for rows.Next() { p, err := scanWorkoutTypePace(rows) if err != nil { return nil, fmt.Errorf("scan workout type pace row: %w", err) } paces = append(paces, p) } return paces, rows.Err() }