feat(exécution): dernière performance et meilleur score par exercice (ticket #81)

Ajoute la migration Drift v17 (colonne sourceExerciseIdSnapshot sur
workout_history_set_results et workout_history_step_results, index
associés), le port ExercisePerformanceReferenceRepository et son
implémentation Drift, et le use case
ExercisePerformanceReferenceUseCase (dernière performance + meilleur
score par métrique active pour un exercice donné).

Validé GO par Main : dart analyze propre (1 lint mineur de style),
178 tests (4 échecs préexistants sans rapport avec ce lot).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 07:54:49 +02:00
parent e05405157a
commit ac51c16b51
10 changed files with 1073 additions and 3 deletions

View File

@ -48,7 +48,7 @@ final class AppDatabase extends _$AppDatabase {
}
@override
int get schemaVersion => 16;
int get schemaVersion => 17;
@override
MigrationStrategy get migration {
@ -113,6 +113,9 @@ final class AppDatabase extends _$AppDatabase {
if (from < 16) {
await _migrateToSchema16(migrator);
}
if (from < 17) {
await _migrateToSchema17();
}
await _createIndexes();
},
beforeOpen: (details) async {
@ -229,10 +232,24 @@ final class AppDatabase extends _$AppDatabase {
'CREATE INDEX IF NOT EXISTS idx_workout_history_set_results_history_id '
'ON workout_history_set_results (workout_history_id)',
);
await customStatement(
'CREATE INDEX IF NOT EXISTS '
'idx_workout_history_set_results_source_exercise '
'ON workout_history_set_results (source_exercise_id_snapshot, '
'set_index) WHERE deleted_at IS NULL AND '
'source_exercise_id_snapshot IS NOT NULL',
);
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_workout_history_step_results_history_id '
'ON workout_history_step_results (workout_history_id)',
);
await customStatement(
'CREATE INDEX IF NOT EXISTS '
'idx_workout_history_step_results_source_exercise '
'ON workout_history_step_results (source_exercise_id_snapshot, '
'set_index, step_index) WHERE deleted_at IS NULL AND '
'source_exercise_id_snapshot IS NOT NULL',
);
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_change_log_entity '
'ON change_log (entity_type, entity_id)',
@ -697,6 +714,79 @@ CREATE TABLE IF NOT EXISTS active_set_timer_states (
);
}
Future<void> _migrateToSchema17() async {
await _addColumnIfMissing(
tableName: 'workout_history_set_results',
columnName: 'source_exercise_id_snapshot',
definition: 'source_exercise_id_snapshot TEXT',
);
await _addColumnIfMissing(
tableName: 'workout_history_step_results',
columnName: 'source_exercise_id_snapshot',
definition: 'source_exercise_id_snapshot TEXT',
);
await _backfillWorkoutHistorySetSourceExerciseIds();
await _backfillWorkoutHistoryStepSourceExerciseIds();
}
Future<void> _backfillWorkoutHistorySetSourceExerciseIds() async {
await customStatement(r'''
UPDATE workout_history_set_results AS result
SET source_exercise_id_snapshot = (
SELECT json_extract(exercise.value, '$.sourceExerciseId')
FROM workout_history AS history,
json_each(
COALESCE(
json_extract(
history.history_snapshot_json,
'$.resolvedTemplateSnapshotJson'
),
history.history_snapshot_json
),
'$.programs'
) AS program,
json_each(
json_extract(program.value, '$.programSnapshotJson'),
'$.exercises'
) AS exercise
WHERE history.id = result.workout_history_id
AND json_extract(program.value, '$.id') = result.program_snapshot_id
AND json_extract(exercise.value, '$.id') = result.exercise_snapshot_id
LIMIT 1
)
WHERE result.source_exercise_id_snapshot IS NULL
''');
}
Future<void> _backfillWorkoutHistoryStepSourceExerciseIds() async {
await customStatement(r'''
UPDATE workout_history_step_results AS result
SET source_exercise_id_snapshot = (
SELECT json_extract(exercise.value, '$.sourceExerciseId')
FROM workout_history AS history,
json_each(
COALESCE(
json_extract(
history.history_snapshot_json,
'$.resolvedTemplateSnapshotJson'
),
history.history_snapshot_json
),
'$.programs'
) AS program,
json_each(
json_extract(program.value, '$.programSnapshotJson'),
'$.exercises'
) AS exercise
WHERE history.id = result.workout_history_id
AND json_extract(program.value, '$.id') = result.program_snapshot_id
AND json_extract(exercise.value, '$.id') = result.exercise_snapshot_id
LIMIT 1
)
WHERE result.source_exercise_id_snapshot IS NULL
''');
}
Future<void> _addColumnIfMissing({
required String tableName,
required String columnName,