store: scope workout kinds and their pace/HR ranges to a user_id

workout_kinds gains a real user_id column (Task 1); workout_type_paces has
none of its own and is scoped via a join to workout_kinds instead, since
it's always accessed through a specific kind.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 13:44:06 +02:00
parent ef410863c6
commit e8bde134ae
4 changed files with 56 additions and 36 deletions

View File

@@ -9,7 +9,9 @@ import (
// 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.
// 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
@@ -24,38 +26,49 @@ func scanWorkoutTypePace(row interface{ Scan(...any) error }) (WorkoutTypePace,
return p, err
}
const workoutTypePaceColumns = `workout_kind_id, pace_min_sec_per_km, pace_max_sec_per_km, hr_min_pct_hrr, hr_max_pct_hrr`
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.
func (db *DB) GetWorkoutTypePace(ctx context.Context, workoutKindID int64) (WorkoutTypePace, error) {
row := db.QueryRowContext(ctx, `SELECT `+workoutTypePaceColumns+` FROM workout_type_paces WHERE workout_kind_id = ?`, workoutKindID)
// 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: %w", workoutKindID, err)
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.
func (db *DB) UpdateWorkoutTypePace(ctx context.Context, p WorkoutTypePace) error {
// 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=?`,
p.PaceMinSecPerKm, p.PaceMaxSecPerKm, p.HRMinPctHRR, p.HRMaxPctHRR, p.WorkoutKindID)
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: %w", p.WorkoutKindID, err)
return fmt.Errorf("update workout type pace for kind %d (user %d): %w", p.WorkoutKindID, userID, err)
}
return nil
}
// ListWorkoutTypePaces returns every workout kind's pace/zone row.
func (db *DB) ListWorkoutTypePaces(ctx context.Context) ([]WorkoutTypePace, error) {
rows, err := db.QueryContext(ctx, `SELECT `+workoutTypePaceColumns+` FROM workout_type_paces ORDER BY workout_kind_id`)
// 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: %w", err)
return nil, fmt.Errorf("list workout type paces for user %d: %w", userID, err)
}
defer rows.Close()