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

@ -13,6 +13,7 @@ abstract interface class AppDependencies {
ActiveExerciseStepUseCases get activeExerciseStepUseCases; ActiveExerciseStepUseCases get activeExerciseStepUseCases;
CloseWorkoutSessionUseCase get closeWorkoutSessionUseCase; CloseWorkoutSessionUseCase get closeWorkoutSessionUseCase;
WorkoutHistoryUseCases get workoutHistoryUseCases; WorkoutHistoryUseCases get workoutHistoryUseCases;
ExercisePerformanceReferenceUseCase get exercisePerformanceReferenceUseCase;
SyncUseCases get syncUseCases; SyncUseCases get syncUseCases;
ShareUseCases get shareUseCases; ShareUseCases get shareUseCases;
} }
@ -29,6 +30,7 @@ final class AppBootstrap implements AppDependencies {
required this.activeExerciseStepUseCases, required this.activeExerciseStepUseCases,
required this.closeWorkoutSessionUseCase, required this.closeWorkoutSessionUseCase,
required this.workoutHistoryUseCases, required this.workoutHistoryUseCases,
required this.exercisePerformanceReferenceUseCase,
required this.syncUseCases, required this.syncUseCases,
required this.shareUseCases, required this.shareUseCases,
required this.syncGateway, required this.syncGateway,
@ -54,6 +56,8 @@ final class AppBootstrap implements AppDependencies {
@override @override
final WorkoutHistoryUseCases workoutHistoryUseCases; final WorkoutHistoryUseCases workoutHistoryUseCases;
@override @override
final ExercisePerformanceReferenceUseCase exercisePerformanceReferenceUseCase;
@override
final SyncUseCases syncUseCases; final SyncUseCases syncUseCases;
@override @override
final ShareUseCases shareUseCases; final ShareUseCases shareUseCases;
@ -77,6 +81,8 @@ final class AppBootstrap implements AppDependencies {
final templateRepository = DriftWorkoutTemplateRepository(database); final templateRepository = DriftWorkoutTemplateRepository(database);
final activeSessionRepository = DriftActiveSessionRepository(database); final activeSessionRepository = DriftActiveSessionRepository(database);
final historyRepository = DriftWorkoutHistoryRepository(database); final historyRepository = DriftWorkoutHistoryRepository(database);
final performanceReferenceRepository =
DriftExercisePerformanceReferenceRepository(database);
final ids = LocalIdGenerator(); final ids = LocalIdGenerator();
const clock = SystemClock(); const clock = SystemClock();
const originDeviceId = 'local-device'; const originDeviceId = 'local-device';
@ -159,6 +165,9 @@ final class AppBootstrap implements AppDependencies {
repository: historyRepository, repository: historyRepository,
clock: clock, clock: clock,
), ),
exercisePerformanceReferenceUseCase: ExercisePerformanceReferenceUseCase(
repository: performanceReferenceRepository,
),
syncUseCases: SyncUseCases( syncUseCases: SyncUseCases(
tokenStore: const SecureStorageAuthTokenStore(), tokenStore: const SecureStorageAuthTokenStore(),
remoteSyncApi: remoteSyncApi, remoteSyncApi: remoteSyncApi,

View File

@ -466,6 +466,106 @@ abstract interface class WorkoutHistoryRepository {
Future<void> delete(String id, DateTime deletedAt); Future<void> delete(String id, DateTime deletedAt);
} }
final class ActivePerformanceMeasures {
const ActivePerformanceMeasures({
required this.timeEnabled,
required this.repsEnabled,
required this.scoreEnabled,
this.scoreInputMode = ScoreInputMode.manual,
});
final bool timeEnabled;
final bool repsEnabled;
final bool scoreEnabled;
final ScoreInputMode scoreInputMode;
bool get hasAny => timeEnabled || repsEnabled || scoreEnabled;
}
enum PerformanceMetric { score, reps, time }
final class WorkoutHistorySetPerformance {
const WorkoutHistorySetPerformance({
required this.workoutHistoryId,
required this.startedAt,
required this.setIndex,
required this.exerciseNameSnapshot,
required this.scoreInputModeSnapshot,
this.actualTimeMs,
this.actualReps,
this.actualScore,
this.actualScoreTimeMs,
this.completedAt,
});
final String workoutHistoryId;
final DateTime startedAt;
final int setIndex;
final String exerciseNameSnapshot;
final ScoreInputMode scoreInputModeSnapshot;
final int? actualTimeMs;
final int? actualReps;
final double? actualScore;
final int? actualScoreTimeMs;
final DateTime? completedAt;
}
final class WorkoutHistoryMetricPerformance {
const WorkoutHistoryMetricPerformance({
required this.workoutHistoryId,
required this.startedAt,
required this.setIndex,
required this.exerciseNameSnapshot,
required this.metric,
required this.scoreInputModeSnapshot,
this.actualTimeMs,
this.actualReps,
this.actualScore,
this.actualScoreTimeMs,
this.completedAt,
});
final String workoutHistoryId;
final DateTime startedAt;
final int setIndex;
final String exerciseNameSnapshot;
final PerformanceMetric metric;
final ScoreInputMode scoreInputModeSnapshot;
final int? actualTimeMs;
final int? actualReps;
final double? actualScore;
final int? actualScoreTimeMs;
final DateTime? completedAt;
}
final class ExercisePerformanceReference {
const ExercisePerformanceReference({
required this.hasAnyHistoryForExercise,
this.last,
this.record,
});
final bool hasAnyHistoryForExercise;
final WorkoutHistorySetPerformance? last;
final WorkoutHistoryMetricPerformance? record;
}
abstract interface class ExercisePerformanceReferenceRepository {
Future<bool> hasAnyCompletedHistoryForExercise(String exerciseId);
Future<WorkoutHistorySetPerformance?> findLatestSetPerformance({
required String exerciseId,
required ActivePerformanceMeasures activeMeasures,
required int currentSetIndex,
});
Future<WorkoutHistoryMetricPerformance?> findBestMetricPerformance({
required String exerciseId,
required PerformanceMetric metric,
required ScoreInputMode scoreInputMode,
});
}
final class SyncRunSummary { final class SyncRunSummary {
const SyncRunSummary({ const SyncRunSummary({
required this.pushedChanges, required this.pushedChanges,

View File

@ -3329,6 +3329,9 @@ final class CloseWorkoutSessionUseCase {
final stepResults = await sessionRepository.listExerciseStepResults( final stepResults = await sessionRepository.listExerciseStepResults(
sessionId, sessionId,
); );
final snapshots = _exerciseSnapshotsById(
session.resolvedTemplateSnapshotJson,
);
final historyId = ids.newId(); final historyId = ids.newId();
final historyResults = _historyResultsFromActiveResults( final historyResults = _historyResultsFromActiveResults(
historyId: historyId, historyId: historyId,
@ -3341,6 +3344,7 @@ final class CloseWorkoutSessionUseCase {
final historyStepResults = _historyStepResultsFromActiveResults( final historyStepResults = _historyStepResultsFromActiveResults(
historyId: historyId, historyId: historyId,
results: stepResults, results: stepResults,
resolvedTemplateSnapshotJson: session.resolvedTemplateSnapshotJson,
now: now, now: now,
ids: ids, ids: ids,
originDeviceId: originDeviceId, originDeviceId: originDeviceId,
@ -3377,6 +3381,8 @@ final class CloseWorkoutSessionUseCase {
'scoreInputModeSnapshot': result.scoreInputModeSnapshot.name, 'scoreInputModeSnapshot': result.scoreInputModeSnapshot.name,
'scoreLabelSnapshot': result.scoreLabelSnapshot, 'scoreLabelSnapshot': result.scoreLabelSnapshot,
'scoreUnitSnapshot': result.scoreUnitSnapshot, 'scoreUnitSnapshot': result.scoreUnitSnapshot,
'sourceExerciseIdSnapshot':
snapshots[result.exerciseSnapshotId]?.sourceExerciseId,
'status': result.status.name, 'status': result.status.name,
}, },
) )
@ -3407,6 +3413,8 @@ final class CloseWorkoutSessionUseCase {
'actualScore': result.actualScore, 'actualScore': result.actualScore,
'actualScoreTimeMs': result.actualScoreTimeMs, 'actualScoreTimeMs': result.actualScoreTimeMs,
'note': result.note, 'note': result.note,
'sourceExerciseIdSnapshot':
snapshots[result.exerciseSnapshotId]?.sourceExerciseId,
}, },
) )
.toList(), .toList(),
@ -3571,6 +3579,59 @@ void _validateExerciseSteps(List<ExerciseStep> steps) {
} }
} }
final class ExercisePerformanceReferenceUseCase {
const ExercisePerformanceReferenceUseCase({required this.repository});
final ExercisePerformanceReferenceRepository repository;
Future<ExercisePerformanceReference> getExercisePerformanceReference({
required String exerciseId,
required ActivePerformanceMeasures activeMeasures,
required int currentSetIndex,
}) async {
final hasAnyHistory = await repository.hasAnyCompletedHistoryForExercise(
exerciseId,
);
if (!activeMeasures.hasAny) {
return ExercisePerformanceReference(
hasAnyHistoryForExercise: hasAnyHistory,
);
}
final last = await repository.findLatestSetPerformance(
exerciseId: exerciseId,
activeMeasures: activeMeasures,
currentSetIndex: currentSetIndex,
);
final recordMetric = _recordMetricFor(activeMeasures);
final record = recordMetric == null
? null
: await repository.findBestMetricPerformance(
exerciseId: exerciseId,
metric: recordMetric,
scoreInputMode: activeMeasures.scoreInputMode,
);
return ExercisePerformanceReference(
hasAnyHistoryForExercise: hasAnyHistory,
last: last,
record: record,
);
}
}
PerformanceMetric? _recordMetricFor(ActivePerformanceMeasures measures) {
if (measures.scoreEnabled) {
return PerformanceMetric.score;
}
if (measures.repsEnabled) {
return PerformanceMetric.reps;
}
if (measures.timeEnabled) {
return PerformanceMetric.time;
}
return null;
}
void _validateAuthInput({required String email, required String password}) { void _validateAuthInput({required String email, required String password}) {
final normalizedEmail = email.trim(); final normalizedEmail = email.trim();
final hasBasicEmailShape = RegExp( final hasBasicEmailShape = RegExp(
@ -4055,6 +4116,7 @@ List<WorkoutHistorySetResult> _historyResultsFromActiveResults({
result.scoreLabelSnapshot ?? snapshot?.scoreLabelSnapshot, result.scoreLabelSnapshot ?? snapshot?.scoreLabelSnapshot,
scoreUnitSnapshot: scoreUnitSnapshot:
result.scoreUnitSnapshot ?? snapshot?.scoreUnitSnapshot, result.scoreUnitSnapshot ?? snapshot?.scoreUnitSnapshot,
sourceExerciseIdSnapshot: snapshot?.sourceExerciseId,
startedAt: result.startedAt, startedAt: result.startedAt,
completedAt: result.completedAt, completedAt: result.completedAt,
status: result.status, status: result.status,
@ -4089,6 +4151,7 @@ Map<String, _ResolvedExerciseSnapshot> _exerciseSnapshotsById(
exerciseSnapshotId: id, exerciseSnapshotId: id,
programNameSnapshot: programName, programNameSnapshot: programName,
exerciseNameSnapshot: exercise['exerciseNameSnapshot'] as String? ?? id, exerciseNameSnapshot: exercise['exerciseNameSnapshot'] as String? ?? id,
sourceExerciseId: exercise['sourceExerciseId'] as String?,
timeEnabled: exercise['timeEnabled'] == true, timeEnabled: exercise['timeEnabled'] == true,
repsEnabled: exercise['repsEnabled'] == true, repsEnabled: exercise['repsEnabled'] == true,
scoreEnabled: exercise['scoreEnabled'] == true, scoreEnabled: exercise['scoreEnabled'] == true,
@ -4118,6 +4181,7 @@ final class _ResolvedExerciseSnapshot {
required this.exerciseSnapshotId, required this.exerciseSnapshotId,
required this.programNameSnapshot, required this.programNameSnapshot,
required this.exerciseNameSnapshot, required this.exerciseNameSnapshot,
this.sourceExerciseId,
required this.timeEnabled, required this.timeEnabled,
required this.repsEnabled, required this.repsEnabled,
required this.scoreEnabled, required this.scoreEnabled,
@ -4136,6 +4200,7 @@ final class _ResolvedExerciseSnapshot {
final String exerciseSnapshotId; final String exerciseSnapshotId;
final String programNameSnapshot; final String programNameSnapshot;
final String exerciseNameSnapshot; final String exerciseNameSnapshot;
final String? sourceExerciseId;
final bool timeEnabled; final bool timeEnabled;
final bool repsEnabled; final bool repsEnabled;
final bool scoreEnabled; final bool scoreEnabled;
@ -4181,11 +4246,14 @@ List<ExerciseStep> _exerciseStepsFromSnapshot(Object? value) {
List<WorkoutHistoryStepResult> _historyStepResultsFromActiveResults({ List<WorkoutHistoryStepResult> _historyStepResultsFromActiveResults({
required String historyId, required String historyId,
required List<ActiveExerciseStepResult> results, required List<ActiveExerciseStepResult> results,
required String resolvedTemplateSnapshotJson,
required DateTime now, required DateTime now,
required IdGenerator ids, required IdGenerator ids,
required String originDeviceId, required String originDeviceId,
}) { }) {
final snapshots = _exerciseSnapshotsById(resolvedTemplateSnapshotJson);
return results.map((result) { return results.map((result) {
final snapshot = snapshots[result.exerciseSnapshotId];
return WorkoutHistoryStepResult( return WorkoutHistoryStepResult(
metadata: _newMetadata(ids, originDeviceId, now), metadata: _newMetadata(ids, originDeviceId, now),
workoutHistoryId: historyId, workoutHistoryId: historyId,
@ -4214,6 +4282,7 @@ List<WorkoutHistoryStepResult> _historyStepResultsFromActiveResults({
actualScore: result.actualScore, actualScore: result.actualScore,
actualScoreTimeMs: result.actualScoreTimeMs, actualScoreTimeMs: result.actualScoreTimeMs,
note: result.note, note: result.note,
sourceExerciseIdSnapshot: snapshot?.sourceExerciseId,
); );
}).toList(); }).toList();
} }

View File

@ -1444,6 +1444,7 @@ final class WorkoutHistorySetResult {
this.scoreInputModeSnapshot = ScoreInputMode.manual, this.scoreInputModeSnapshot = ScoreInputMode.manual,
this.scoreLabelSnapshot, this.scoreLabelSnapshot,
this.scoreUnitSnapshot, this.scoreUnitSnapshot,
this.sourceExerciseIdSnapshot,
this.startedAt, this.startedAt,
this.completedAt, this.completedAt,
this.status = SetResultStatus.completed, this.status = SetResultStatus.completed,
@ -1493,6 +1494,7 @@ final class WorkoutHistorySetResult {
final ScoreInputMode scoreInputModeSnapshot; final ScoreInputMode scoreInputModeSnapshot;
final String? scoreLabelSnapshot; final String? scoreLabelSnapshot;
final String? scoreUnitSnapshot; final String? scoreUnitSnapshot;
final String? sourceExerciseIdSnapshot;
final DateTime? startedAt; final DateTime? startedAt;
final DateTime? completedAt; final DateTime? completedAt;
final SetResultStatus status; final SetResultStatus status;
@ -1527,6 +1529,7 @@ final class WorkoutHistoryStepResult {
this.actualScore, this.actualScore,
this.actualScoreTimeMs, this.actualScoreTimeMs,
this.note, this.note,
this.sourceExerciseIdSnapshot,
}) { }) {
_validateExerciseStepResult( _validateExerciseStepResult(
programIndex: programIndex, programIndex: programIndex,
@ -1577,6 +1580,7 @@ final class WorkoutHistoryStepResult {
final double? actualScore; final double? actualScore;
final int? actualScoreTimeMs; final int? actualScoreTimeMs;
final String? note; final String? note;
final String? sourceExerciseIdSnapshot;
} }
String _nonBlank(String? value, String label) { String _nonBlank(String? value, String label) {

View File

@ -48,7 +48,7 @@ final class AppDatabase extends _$AppDatabase {
} }
@override @override
int get schemaVersion => 16; int get schemaVersion => 17;
@override @override
MigrationStrategy get migration { MigrationStrategy get migration {
@ -113,6 +113,9 @@ final class AppDatabase extends _$AppDatabase {
if (from < 16) { if (from < 16) {
await _migrateToSchema16(migrator); await _migrateToSchema16(migrator);
} }
if (from < 17) {
await _migrateToSchema17();
}
await _createIndexes(); await _createIndexes();
}, },
beforeOpen: (details) async { beforeOpen: (details) async {
@ -229,10 +232,24 @@ final class AppDatabase extends _$AppDatabase {
'CREATE INDEX IF NOT EXISTS idx_workout_history_set_results_history_id ' 'CREATE INDEX IF NOT EXISTS idx_workout_history_set_results_history_id '
'ON workout_history_set_results (workout_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( await customStatement(
'CREATE INDEX IF NOT EXISTS idx_workout_history_step_results_history_id ' 'CREATE INDEX IF NOT EXISTS idx_workout_history_step_results_history_id '
'ON workout_history_step_results (workout_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( await customStatement(
'CREATE INDEX IF NOT EXISTS idx_change_log_entity ' 'CREATE INDEX IF NOT EXISTS idx_change_log_entity '
'ON change_log (entity_type, entity_id)', '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({ Future<void> _addColumnIfMissing({
required String tableName, required String tableName,
required String columnName, required String columnName,

View File

@ -24020,6 +24020,17 @@ class $WorkoutHistorySetResultsTable extends WorkoutHistorySetResults
type: DriftSqlType.string, type: DriftSqlType.string,
requiredDuringInsert: false, requiredDuringInsert: false,
); );
static const VerificationMeta _sourceExerciseIdSnapshotMeta =
const VerificationMeta('sourceExerciseIdSnapshot');
@override
late final GeneratedColumn<String> sourceExerciseIdSnapshot =
GeneratedColumn<String>(
'source_exercise_id_snapshot',
aliasedName,
true,
type: DriftSqlType.string,
requiredDuringInsert: false,
);
static const VerificationMeta _startedAtMeta = const VerificationMeta( static const VerificationMeta _startedAtMeta = const VerificationMeta(
'startedAt', 'startedAt',
); );
@ -24087,6 +24098,7 @@ class $WorkoutHistorySetResultsTable extends WorkoutHistorySetResults
actualScoreTimeMs, actualScoreTimeMs,
scoreLabelSnapshot, scoreLabelSnapshot,
scoreUnitSnapshot, scoreUnitSnapshot,
sourceExerciseIdSnapshot,
startedAt, startedAt,
completedAt, completedAt,
status, status,
@ -24410,6 +24422,15 @@ class $WorkoutHistorySetResultsTable extends WorkoutHistorySetResults
), ),
); );
} }
if (data.containsKey('source_exercise_id_snapshot')) {
context.handle(
_sourceExerciseIdSnapshotMeta,
sourceExerciseIdSnapshot.isAcceptableOrUnknown(
data['source_exercise_id_snapshot']!,
_sourceExerciseIdSnapshotMeta,
),
);
}
if (data.containsKey('started_at')) { if (data.containsKey('started_at')) {
context.handle( context.handle(
_startedAtMeta, _startedAtMeta,
@ -24575,6 +24596,10 @@ class $WorkoutHistorySetResultsTable extends WorkoutHistorySetResults
DriftSqlType.string, DriftSqlType.string,
data['${effectivePrefix}score_unit_snapshot'], data['${effectivePrefix}score_unit_snapshot'],
), ),
sourceExerciseIdSnapshot: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}source_exercise_id_snapshot'],
),
startedAt: attachedDatabase.typeMapping.read( startedAt: attachedDatabase.typeMapping.read(
DriftSqlType.dateTime, DriftSqlType.dateTime,
data['${effectivePrefix}started_at'], data['${effectivePrefix}started_at'],
@ -24631,6 +24656,7 @@ class WorkoutHistorySetResult extends DataClass
final int? actualScoreTimeMs; final int? actualScoreTimeMs;
final String? scoreLabelSnapshot; final String? scoreLabelSnapshot;
final String? scoreUnitSnapshot; final String? scoreUnitSnapshot;
final String? sourceExerciseIdSnapshot;
final DateTime? startedAt; final DateTime? startedAt;
final DateTime? completedAt; final DateTime? completedAt;
final String status; final String status;
@ -24668,6 +24694,7 @@ class WorkoutHistorySetResult extends DataClass
this.actualScoreTimeMs, this.actualScoreTimeMs,
this.scoreLabelSnapshot, this.scoreLabelSnapshot,
this.scoreUnitSnapshot, this.scoreUnitSnapshot,
this.sourceExerciseIdSnapshot,
this.startedAt, this.startedAt,
this.completedAt, this.completedAt,
required this.status, required this.status,
@ -24740,6 +24767,11 @@ class WorkoutHistorySetResult extends DataClass
if (!nullToAbsent || scoreUnitSnapshot != null) { if (!nullToAbsent || scoreUnitSnapshot != null) {
map['score_unit_snapshot'] = Variable<String>(scoreUnitSnapshot); map['score_unit_snapshot'] = Variable<String>(scoreUnitSnapshot);
} }
if (!nullToAbsent || sourceExerciseIdSnapshot != null) {
map['source_exercise_id_snapshot'] = Variable<String>(
sourceExerciseIdSnapshot,
);
}
if (!nullToAbsent || startedAt != null) { if (!nullToAbsent || startedAt != null) {
map['started_at'] = Variable<DateTime>(startedAt); map['started_at'] = Variable<DateTime>(startedAt);
} }
@ -24815,6 +24847,9 @@ class WorkoutHistorySetResult extends DataClass
scoreUnitSnapshot: scoreUnitSnapshot == null && nullToAbsent scoreUnitSnapshot: scoreUnitSnapshot == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(scoreUnitSnapshot), : Value(scoreUnitSnapshot),
sourceExerciseIdSnapshot: sourceExerciseIdSnapshot == null && nullToAbsent
? const Value.absent()
: Value(sourceExerciseIdSnapshot),
startedAt: startedAt == null && nullToAbsent startedAt: startedAt == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(startedAt), : Value(startedAt),
@ -24890,6 +24925,9 @@ class WorkoutHistorySetResult extends DataClass
scoreUnitSnapshot: serializer.fromJson<String?>( scoreUnitSnapshot: serializer.fromJson<String?>(
json['scoreUnitSnapshot'], json['scoreUnitSnapshot'],
), ),
sourceExerciseIdSnapshot: serializer.fromJson<String?>(
json['sourceExerciseIdSnapshot'],
),
startedAt: serializer.fromJson<DateTime?>(json['startedAt']), startedAt: serializer.fromJson<DateTime?>(json['startedAt']),
completedAt: serializer.fromJson<DateTime?>(json['completedAt']), completedAt: serializer.fromJson<DateTime?>(json['completedAt']),
status: serializer.fromJson<String>(json['status']), status: serializer.fromJson<String>(json['status']),
@ -24938,6 +24976,9 @@ class WorkoutHistorySetResult extends DataClass
'actualScoreTimeMs': serializer.toJson<int?>(actualScoreTimeMs), 'actualScoreTimeMs': serializer.toJson<int?>(actualScoreTimeMs),
'scoreLabelSnapshot': serializer.toJson<String?>(scoreLabelSnapshot), 'scoreLabelSnapshot': serializer.toJson<String?>(scoreLabelSnapshot),
'scoreUnitSnapshot': serializer.toJson<String?>(scoreUnitSnapshot), 'scoreUnitSnapshot': serializer.toJson<String?>(scoreUnitSnapshot),
'sourceExerciseIdSnapshot': serializer.toJson<String?>(
sourceExerciseIdSnapshot,
),
'startedAt': serializer.toJson<DateTime?>(startedAt), 'startedAt': serializer.toJson<DateTime?>(startedAt),
'completedAt': serializer.toJson<DateTime?>(completedAt), 'completedAt': serializer.toJson<DateTime?>(completedAt),
'status': serializer.toJson<String>(status), 'status': serializer.toJson<String>(status),
@ -24978,6 +25019,7 @@ class WorkoutHistorySetResult extends DataClass
Value<int?> actualScoreTimeMs = const Value.absent(), Value<int?> actualScoreTimeMs = const Value.absent(),
Value<String?> scoreLabelSnapshot = const Value.absent(), Value<String?> scoreLabelSnapshot = const Value.absent(),
Value<String?> scoreUnitSnapshot = const Value.absent(), Value<String?> scoreUnitSnapshot = const Value.absent(),
Value<String?> sourceExerciseIdSnapshot = const Value.absent(),
Value<DateTime?> startedAt = const Value.absent(), Value<DateTime?> startedAt = const Value.absent(),
Value<DateTime?> completedAt = const Value.absent(), Value<DateTime?> completedAt = const Value.absent(),
String? status, String? status,
@ -25034,6 +25076,9 @@ class WorkoutHistorySetResult extends DataClass
scoreUnitSnapshot: scoreUnitSnapshot.present scoreUnitSnapshot: scoreUnitSnapshot.present
? scoreUnitSnapshot.value ? scoreUnitSnapshot.value
: this.scoreUnitSnapshot, : this.scoreUnitSnapshot,
sourceExerciseIdSnapshot: sourceExerciseIdSnapshot.present
? sourceExerciseIdSnapshot.value
: this.sourceExerciseIdSnapshot,
startedAt: startedAt.present ? startedAt.value : this.startedAt, startedAt: startedAt.present ? startedAt.value : this.startedAt,
completedAt: completedAt.present ? completedAt.value : this.completedAt, completedAt: completedAt.present ? completedAt.value : this.completedAt,
status: status ?? this.status, status: status ?? this.status,
@ -25129,6 +25174,9 @@ class WorkoutHistorySetResult extends DataClass
scoreUnitSnapshot: data.scoreUnitSnapshot.present scoreUnitSnapshot: data.scoreUnitSnapshot.present
? data.scoreUnitSnapshot.value ? data.scoreUnitSnapshot.value
: this.scoreUnitSnapshot, : this.scoreUnitSnapshot,
sourceExerciseIdSnapshot: data.sourceExerciseIdSnapshot.present
? data.sourceExerciseIdSnapshot.value
: this.sourceExerciseIdSnapshot,
startedAt: data.startedAt.present ? data.startedAt.value : this.startedAt, startedAt: data.startedAt.present ? data.startedAt.value : this.startedAt,
completedAt: data.completedAt.present completedAt: data.completedAt.present
? data.completedAt.value ? data.completedAt.value
@ -25173,6 +25221,7 @@ class WorkoutHistorySetResult extends DataClass
..write('actualScoreTimeMs: $actualScoreTimeMs, ') ..write('actualScoreTimeMs: $actualScoreTimeMs, ')
..write('scoreLabelSnapshot: $scoreLabelSnapshot, ') ..write('scoreLabelSnapshot: $scoreLabelSnapshot, ')
..write('scoreUnitSnapshot: $scoreUnitSnapshot, ') ..write('scoreUnitSnapshot: $scoreUnitSnapshot, ')
..write('sourceExerciseIdSnapshot: $sourceExerciseIdSnapshot, ')
..write('startedAt: $startedAt, ') ..write('startedAt: $startedAt, ')
..write('completedAt: $completedAt, ') ..write('completedAt: $completedAt, ')
..write('status: $status') ..write('status: $status')
@ -25215,6 +25264,7 @@ class WorkoutHistorySetResult extends DataClass
actualScoreTimeMs, actualScoreTimeMs,
scoreLabelSnapshot, scoreLabelSnapshot,
scoreUnitSnapshot, scoreUnitSnapshot,
sourceExerciseIdSnapshot,
startedAt, startedAt,
completedAt, completedAt,
status, status,
@ -25256,6 +25306,7 @@ class WorkoutHistorySetResult extends DataClass
other.actualScoreTimeMs == this.actualScoreTimeMs && other.actualScoreTimeMs == this.actualScoreTimeMs &&
other.scoreLabelSnapshot == this.scoreLabelSnapshot && other.scoreLabelSnapshot == this.scoreLabelSnapshot &&
other.scoreUnitSnapshot == this.scoreUnitSnapshot && other.scoreUnitSnapshot == this.scoreUnitSnapshot &&
other.sourceExerciseIdSnapshot == this.sourceExerciseIdSnapshot &&
other.startedAt == this.startedAt && other.startedAt == this.startedAt &&
other.completedAt == this.completedAt && other.completedAt == this.completedAt &&
other.status == this.status); other.status == this.status);
@ -25296,6 +25347,7 @@ class WorkoutHistorySetResultsCompanion
final Value<int?> actualScoreTimeMs; final Value<int?> actualScoreTimeMs;
final Value<String?> scoreLabelSnapshot; final Value<String?> scoreLabelSnapshot;
final Value<String?> scoreUnitSnapshot; final Value<String?> scoreUnitSnapshot;
final Value<String?> sourceExerciseIdSnapshot;
final Value<DateTime?> startedAt; final Value<DateTime?> startedAt;
final Value<DateTime?> completedAt; final Value<DateTime?> completedAt;
final Value<String> status; final Value<String> status;
@ -25334,6 +25386,7 @@ class WorkoutHistorySetResultsCompanion
this.actualScoreTimeMs = const Value.absent(), this.actualScoreTimeMs = const Value.absent(),
this.scoreLabelSnapshot = const Value.absent(), this.scoreLabelSnapshot = const Value.absent(),
this.scoreUnitSnapshot = const Value.absent(), this.scoreUnitSnapshot = const Value.absent(),
this.sourceExerciseIdSnapshot = const Value.absent(),
this.startedAt = const Value.absent(), this.startedAt = const Value.absent(),
this.completedAt = const Value.absent(), this.completedAt = const Value.absent(),
this.status = const Value.absent(), this.status = const Value.absent(),
@ -25373,6 +25426,7 @@ class WorkoutHistorySetResultsCompanion
this.actualScoreTimeMs = const Value.absent(), this.actualScoreTimeMs = const Value.absent(),
this.scoreLabelSnapshot = const Value.absent(), this.scoreLabelSnapshot = const Value.absent(),
this.scoreUnitSnapshot = const Value.absent(), this.scoreUnitSnapshot = const Value.absent(),
this.sourceExerciseIdSnapshot = const Value.absent(),
this.startedAt = const Value.absent(), this.startedAt = const Value.absent(),
this.completedAt = const Value.absent(), this.completedAt = const Value.absent(),
this.status = const Value.absent(), this.status = const Value.absent(),
@ -25428,6 +25482,7 @@ class WorkoutHistorySetResultsCompanion
Expression<int>? actualScoreTimeMs, Expression<int>? actualScoreTimeMs,
Expression<String>? scoreLabelSnapshot, Expression<String>? scoreLabelSnapshot,
Expression<String>? scoreUnitSnapshot, Expression<String>? scoreUnitSnapshot,
Expression<String>? sourceExerciseIdSnapshot,
Expression<DateTime>? startedAt, Expression<DateTime>? startedAt,
Expression<DateTime>? completedAt, Expression<DateTime>? completedAt,
Expression<String>? status, Expression<String>? status,
@ -25480,6 +25535,8 @@ class WorkoutHistorySetResultsCompanion
if (scoreLabelSnapshot != null) if (scoreLabelSnapshot != null)
'score_label_snapshot': scoreLabelSnapshot, 'score_label_snapshot': scoreLabelSnapshot,
if (scoreUnitSnapshot != null) 'score_unit_snapshot': scoreUnitSnapshot, if (scoreUnitSnapshot != null) 'score_unit_snapshot': scoreUnitSnapshot,
if (sourceExerciseIdSnapshot != null)
'source_exercise_id_snapshot': sourceExerciseIdSnapshot,
if (startedAt != null) 'started_at': startedAt, if (startedAt != null) 'started_at': startedAt,
if (completedAt != null) 'completed_at': completedAt, if (completedAt != null) 'completed_at': completedAt,
if (status != null) 'status': status, if (status != null) 'status': status,
@ -25521,6 +25578,7 @@ class WorkoutHistorySetResultsCompanion
Value<int?>? actualScoreTimeMs, Value<int?>? actualScoreTimeMs,
Value<String?>? scoreLabelSnapshot, Value<String?>? scoreLabelSnapshot,
Value<String?>? scoreUnitSnapshot, Value<String?>? scoreUnitSnapshot,
Value<String?>? sourceExerciseIdSnapshot,
Value<DateTime?>? startedAt, Value<DateTime?>? startedAt,
Value<DateTime?>? completedAt, Value<DateTime?>? completedAt,
Value<String>? status, Value<String>? status,
@ -25563,6 +25621,8 @@ class WorkoutHistorySetResultsCompanion
actualScoreTimeMs: actualScoreTimeMs ?? this.actualScoreTimeMs, actualScoreTimeMs: actualScoreTimeMs ?? this.actualScoreTimeMs,
scoreLabelSnapshot: scoreLabelSnapshot ?? this.scoreLabelSnapshot, scoreLabelSnapshot: scoreLabelSnapshot ?? this.scoreLabelSnapshot,
scoreUnitSnapshot: scoreUnitSnapshot ?? this.scoreUnitSnapshot, scoreUnitSnapshot: scoreUnitSnapshot ?? this.scoreUnitSnapshot,
sourceExerciseIdSnapshot:
sourceExerciseIdSnapshot ?? this.sourceExerciseIdSnapshot,
startedAt: startedAt ?? this.startedAt, startedAt: startedAt ?? this.startedAt,
completedAt: completedAt ?? this.completedAt, completedAt: completedAt ?? this.completedAt,
status: status ?? this.status, status: status ?? this.status,
@ -25688,6 +25748,11 @@ class WorkoutHistorySetResultsCompanion
if (scoreUnitSnapshot.present) { if (scoreUnitSnapshot.present) {
map['score_unit_snapshot'] = Variable<String>(scoreUnitSnapshot.value); map['score_unit_snapshot'] = Variable<String>(scoreUnitSnapshot.value);
} }
if (sourceExerciseIdSnapshot.present) {
map['source_exercise_id_snapshot'] = Variable<String>(
sourceExerciseIdSnapshot.value,
);
}
if (startedAt.present) { if (startedAt.present) {
map['started_at'] = Variable<DateTime>(startedAt.value); map['started_at'] = Variable<DateTime>(startedAt.value);
} }
@ -25739,6 +25804,7 @@ class WorkoutHistorySetResultsCompanion
..write('actualScoreTimeMs: $actualScoreTimeMs, ') ..write('actualScoreTimeMs: $actualScoreTimeMs, ')
..write('scoreLabelSnapshot: $scoreLabelSnapshot, ') ..write('scoreLabelSnapshot: $scoreLabelSnapshot, ')
..write('scoreUnitSnapshot: $scoreUnitSnapshot, ') ..write('scoreUnitSnapshot: $scoreUnitSnapshot, ')
..write('sourceExerciseIdSnapshot: $sourceExerciseIdSnapshot, ')
..write('startedAt: $startedAt, ') ..write('startedAt: $startedAt, ')
..write('completedAt: $completedAt, ') ..write('completedAt: $completedAt, ')
..write('status: $status, ') ..write('status: $status, ')
@ -26171,6 +26237,17 @@ class $WorkoutHistoryStepResultsTable extends WorkoutHistoryStepResults
type: DriftSqlType.string, type: DriftSqlType.string,
requiredDuringInsert: false, requiredDuringInsert: false,
); );
static const VerificationMeta _sourceExerciseIdSnapshotMeta =
const VerificationMeta('sourceExerciseIdSnapshot');
@override
late final GeneratedColumn<String> sourceExerciseIdSnapshot =
GeneratedColumn<String>(
'source_exercise_id_snapshot',
aliasedName,
true,
type: DriftSqlType.string,
requiredDuringInsert: false,
);
@override @override
List<GeneratedColumn> get $columns => [ List<GeneratedColumn> get $columns => [
id, id,
@ -26210,6 +26287,7 @@ class $WorkoutHistoryStepResultsTable extends WorkoutHistoryStepResults
actualScore, actualScore,
actualScoreTimeMs, actualScoreTimeMs,
note, note,
sourceExerciseIdSnapshot,
]; ];
@override @override
String get aliasedName => _alias ?? actualTableName; String get aliasedName => _alias ?? actualTableName;
@ -26560,6 +26638,15 @@ class $WorkoutHistoryStepResultsTable extends WorkoutHistoryStepResults
note.isAcceptableOrUnknown(data['note']!, _noteMeta), note.isAcceptableOrUnknown(data['note']!, _noteMeta),
); );
} }
if (data.containsKey('source_exercise_id_snapshot')) {
context.handle(
_sourceExerciseIdSnapshotMeta,
sourceExerciseIdSnapshot.isAcceptableOrUnknown(
data['source_exercise_id_snapshot']!,
_sourceExerciseIdSnapshotMeta,
),
);
}
return context; return context;
} }
@ -26720,6 +26807,10 @@ class $WorkoutHistoryStepResultsTable extends WorkoutHistoryStepResults
DriftSqlType.string, DriftSqlType.string,
data['${effectivePrefix}note'], data['${effectivePrefix}note'],
), ),
sourceExerciseIdSnapshot: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}source_exercise_id_snapshot'],
),
); );
} }
@ -26768,6 +26859,7 @@ class WorkoutHistoryStepResult extends DataClass
final double? actualScore; final double? actualScore;
final int? actualScoreTimeMs; final int? actualScoreTimeMs;
final String? note; final String? note;
final String? sourceExerciseIdSnapshot;
const WorkoutHistoryStepResult({ const WorkoutHistoryStepResult({
required this.id, required this.id,
required this.createdAt, required this.createdAt,
@ -26806,6 +26898,7 @@ class WorkoutHistoryStepResult extends DataClass
this.actualScore, this.actualScore,
this.actualScoreTimeMs, this.actualScoreTimeMs,
this.note, this.note,
this.sourceExerciseIdSnapshot,
}); });
@override @override
Map<String, Expression> toColumns(bool nullToAbsent) { Map<String, Expression> toColumns(bool nullToAbsent) {
@ -26883,6 +26976,11 @@ class WorkoutHistoryStepResult extends DataClass
if (!nullToAbsent || note != null) { if (!nullToAbsent || note != null) {
map['note'] = Variable<String>(note); map['note'] = Variable<String>(note);
} }
if (!nullToAbsent || sourceExerciseIdSnapshot != null) {
map['source_exercise_id_snapshot'] = Variable<String>(
sourceExerciseIdSnapshot,
);
}
return map; return map;
} }
@ -26956,6 +27054,9 @@ class WorkoutHistoryStepResult extends DataClass
? const Value.absent() ? const Value.absent()
: Value(actualScoreTimeMs), : Value(actualScoreTimeMs),
note: note == null && nullToAbsent ? const Value.absent() : Value(note), note: note == null && nullToAbsent ? const Value.absent() : Value(note),
sourceExerciseIdSnapshot: sourceExerciseIdSnapshot == null && nullToAbsent
? const Value.absent()
: Value(sourceExerciseIdSnapshot),
); );
} }
@ -27018,6 +27119,9 @@ class WorkoutHistoryStepResult extends DataClass
actualScore: serializer.fromJson<double?>(json['actualScore']), actualScore: serializer.fromJson<double?>(json['actualScore']),
actualScoreTimeMs: serializer.fromJson<int?>(json['actualScoreTimeMs']), actualScoreTimeMs: serializer.fromJson<int?>(json['actualScoreTimeMs']),
note: serializer.fromJson<String?>(json['note']), note: serializer.fromJson<String?>(json['note']),
sourceExerciseIdSnapshot: serializer.fromJson<String?>(
json['sourceExerciseIdSnapshot'],
),
); );
} }
@override @override
@ -27065,6 +27169,9 @@ class WorkoutHistoryStepResult extends DataClass
'actualScore': serializer.toJson<double?>(actualScore), 'actualScore': serializer.toJson<double?>(actualScore),
'actualScoreTimeMs': serializer.toJson<int?>(actualScoreTimeMs), 'actualScoreTimeMs': serializer.toJson<int?>(actualScoreTimeMs),
'note': serializer.toJson<String?>(note), 'note': serializer.toJson<String?>(note),
'sourceExerciseIdSnapshot': serializer.toJson<String?>(
sourceExerciseIdSnapshot,
),
}; };
} }
@ -27106,6 +27213,7 @@ class WorkoutHistoryStepResult extends DataClass
Value<double?> actualScore = const Value.absent(), Value<double?> actualScore = const Value.absent(),
Value<int?> actualScoreTimeMs = const Value.absent(), Value<int?> actualScoreTimeMs = const Value.absent(),
Value<String?> note = const Value.absent(), Value<String?> note = const Value.absent(),
Value<String?> sourceExerciseIdSnapshot = const Value.absent(),
}) => WorkoutHistoryStepResult( }) => WorkoutHistoryStepResult(
id: id ?? this.id, id: id ?? this.id,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
@ -27160,6 +27268,9 @@ class WorkoutHistoryStepResult extends DataClass
? actualScoreTimeMs.value ? actualScoreTimeMs.value
: this.actualScoreTimeMs, : this.actualScoreTimeMs,
note: note.present ? note.value : this.note, note: note.present ? note.value : this.note,
sourceExerciseIdSnapshot: sourceExerciseIdSnapshot.present
? sourceExerciseIdSnapshot.value
: this.sourceExerciseIdSnapshot,
); );
WorkoutHistoryStepResult copyWithCompanion( WorkoutHistoryStepResult copyWithCompanion(
WorkoutHistoryStepResultsCompanion data, WorkoutHistoryStepResultsCompanion data,
@ -27256,6 +27367,9 @@ class WorkoutHistoryStepResult extends DataClass
? data.actualScoreTimeMs.value ? data.actualScoreTimeMs.value
: this.actualScoreTimeMs, : this.actualScoreTimeMs,
note: data.note.present ? data.note.value : this.note, note: data.note.present ? data.note.value : this.note,
sourceExerciseIdSnapshot: data.sourceExerciseIdSnapshot.present
? data.sourceExerciseIdSnapshot.value
: this.sourceExerciseIdSnapshot,
); );
} }
@ -27298,7 +27412,8 @@ class WorkoutHistoryStepResult extends DataClass
..write('actualReps: $actualReps, ') ..write('actualReps: $actualReps, ')
..write('actualScore: $actualScore, ') ..write('actualScore: $actualScore, ')
..write('actualScoreTimeMs: $actualScoreTimeMs, ') ..write('actualScoreTimeMs: $actualScoreTimeMs, ')
..write('note: $note') ..write('note: $note, ')
..write('sourceExerciseIdSnapshot: $sourceExerciseIdSnapshot')
..write(')')) ..write(')'))
.toString(); .toString();
} }
@ -27342,6 +27457,7 @@ class WorkoutHistoryStepResult extends DataClass
actualScore, actualScore,
actualScoreTimeMs, actualScoreTimeMs,
note, note,
sourceExerciseIdSnapshot,
]); ]);
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
@ -27383,7 +27499,8 @@ class WorkoutHistoryStepResult extends DataClass
other.actualReps == this.actualReps && other.actualReps == this.actualReps &&
other.actualScore == this.actualScore && other.actualScore == this.actualScore &&
other.actualScoreTimeMs == this.actualScoreTimeMs && other.actualScoreTimeMs == this.actualScoreTimeMs &&
other.note == this.note); other.note == this.note &&
other.sourceExerciseIdSnapshot == this.sourceExerciseIdSnapshot);
} }
class WorkoutHistoryStepResultsCompanion class WorkoutHistoryStepResultsCompanion
@ -27425,6 +27542,7 @@ class WorkoutHistoryStepResultsCompanion
final Value<double?> actualScore; final Value<double?> actualScore;
final Value<int?> actualScoreTimeMs; final Value<int?> actualScoreTimeMs;
final Value<String?> note; final Value<String?> note;
final Value<String?> sourceExerciseIdSnapshot;
final Value<int> rowid; final Value<int> rowid;
const WorkoutHistoryStepResultsCompanion({ const WorkoutHistoryStepResultsCompanion({
this.id = const Value.absent(), this.id = const Value.absent(),
@ -27464,6 +27582,7 @@ class WorkoutHistoryStepResultsCompanion
this.actualScore = const Value.absent(), this.actualScore = const Value.absent(),
this.actualScoreTimeMs = const Value.absent(), this.actualScoreTimeMs = const Value.absent(),
this.note = const Value.absent(), this.note = const Value.absent(),
this.sourceExerciseIdSnapshot = const Value.absent(),
this.rowid = const Value.absent(), this.rowid = const Value.absent(),
}); });
WorkoutHistoryStepResultsCompanion.insert({ WorkoutHistoryStepResultsCompanion.insert({
@ -27504,6 +27623,7 @@ class WorkoutHistoryStepResultsCompanion
this.actualScore = const Value.absent(), this.actualScore = const Value.absent(),
this.actualScoreTimeMs = const Value.absent(), this.actualScoreTimeMs = const Value.absent(),
this.note = const Value.absent(), this.note = const Value.absent(),
this.sourceExerciseIdSnapshot = const Value.absent(),
this.rowid = const Value.absent(), this.rowid = const Value.absent(),
}) : id = Value(id), }) : id = Value(id),
createdAt = Value(createdAt), createdAt = Value(createdAt),
@ -27563,6 +27683,7 @@ class WorkoutHistoryStepResultsCompanion
Expression<double>? actualScore, Expression<double>? actualScore,
Expression<int>? actualScoreTimeMs, Expression<int>? actualScoreTimeMs,
Expression<String>? note, Expression<String>? note,
Expression<String>? sourceExerciseIdSnapshot,
Expression<int>? rowid, Expression<int>? rowid,
}) { }) {
return RawValuesInsertable({ return RawValuesInsertable({
@ -27610,6 +27731,8 @@ class WorkoutHistoryStepResultsCompanion
if (actualScore != null) 'actual_score': actualScore, if (actualScore != null) 'actual_score': actualScore,
if (actualScoreTimeMs != null) 'actual_score_time_ms': actualScoreTimeMs, if (actualScoreTimeMs != null) 'actual_score_time_ms': actualScoreTimeMs,
if (note != null) 'note': note, if (note != null) 'note': note,
if (sourceExerciseIdSnapshot != null)
'source_exercise_id_snapshot': sourceExerciseIdSnapshot,
if (rowid != null) 'rowid': rowid, if (rowid != null) 'rowid': rowid,
}); });
} }
@ -27652,6 +27775,7 @@ class WorkoutHistoryStepResultsCompanion
Value<double?>? actualScore, Value<double?>? actualScore,
Value<int?>? actualScoreTimeMs, Value<int?>? actualScoreTimeMs,
Value<String?>? note, Value<String?>? note,
Value<String?>? sourceExerciseIdSnapshot,
Value<int>? rowid, Value<int>? rowid,
}) { }) {
return WorkoutHistoryStepResultsCompanion( return WorkoutHistoryStepResultsCompanion(
@ -27694,6 +27818,8 @@ class WorkoutHistoryStepResultsCompanion
actualScore: actualScore ?? this.actualScore, actualScore: actualScore ?? this.actualScore,
actualScoreTimeMs: actualScoreTimeMs ?? this.actualScoreTimeMs, actualScoreTimeMs: actualScoreTimeMs ?? this.actualScoreTimeMs,
note: note ?? this.note, note: note ?? this.note,
sourceExerciseIdSnapshot:
sourceExerciseIdSnapshot ?? this.sourceExerciseIdSnapshot,
rowid: rowid ?? this.rowid, rowid: rowid ?? this.rowid,
); );
} }
@ -27820,6 +27946,11 @@ class WorkoutHistoryStepResultsCompanion
if (note.present) { if (note.present) {
map['note'] = Variable<String>(note.value); map['note'] = Variable<String>(note.value);
} }
if (sourceExerciseIdSnapshot.present) {
map['source_exercise_id_snapshot'] = Variable<String>(
sourceExerciseIdSnapshot.value,
);
}
if (rowid.present) { if (rowid.present) {
map['rowid'] = Variable<int>(rowid.value); map['rowid'] = Variable<int>(rowid.value);
} }
@ -27866,6 +27997,7 @@ class WorkoutHistoryStepResultsCompanion
..write('actualScore: $actualScore, ') ..write('actualScore: $actualScore, ')
..write('actualScoreTimeMs: $actualScoreTimeMs, ') ..write('actualScoreTimeMs: $actualScoreTimeMs, ')
..write('note: $note, ') ..write('note: $note, ')
..write('sourceExerciseIdSnapshot: $sourceExerciseIdSnapshot, ')
..write('rowid: $rowid') ..write('rowid: $rowid')
..write(')')) ..write(')'))
.toString(); .toString();
@ -45147,6 +45279,7 @@ typedef $$WorkoutHistorySetResultsTableCreateCompanionBuilder =
Value<int?> actualScoreTimeMs, Value<int?> actualScoreTimeMs,
Value<String?> scoreLabelSnapshot, Value<String?> scoreLabelSnapshot,
Value<String?> scoreUnitSnapshot, Value<String?> scoreUnitSnapshot,
Value<String?> sourceExerciseIdSnapshot,
Value<DateTime?> startedAt, Value<DateTime?> startedAt,
Value<DateTime?> completedAt, Value<DateTime?> completedAt,
Value<String> status, Value<String> status,
@ -45187,6 +45320,7 @@ typedef $$WorkoutHistorySetResultsTableUpdateCompanionBuilder =
Value<int?> actualScoreTimeMs, Value<int?> actualScoreTimeMs,
Value<String?> scoreLabelSnapshot, Value<String?> scoreLabelSnapshot,
Value<String?> scoreUnitSnapshot, Value<String?> scoreUnitSnapshot,
Value<String?> sourceExerciseIdSnapshot,
Value<DateTime?> startedAt, Value<DateTime?> startedAt,
Value<DateTime?> completedAt, Value<DateTime?> completedAt,
Value<String> status, Value<String> status,
@ -45395,6 +45529,11 @@ class $$WorkoutHistorySetResultsTableFilterComposer
builder: (column) => ColumnFilters(column), builder: (column) => ColumnFilters(column),
); );
ColumnFilters<String> get sourceExerciseIdSnapshot => $composableBuilder(
column: $table.sourceExerciseIdSnapshot,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<DateTime> get startedAt => $composableBuilder( ColumnFilters<DateTime> get startedAt => $composableBuilder(
column: $table.startedAt, column: $table.startedAt,
builder: (column) => ColumnFilters(column), builder: (column) => ColumnFilters(column),
@ -45603,6 +45742,11 @@ class $$WorkoutHistorySetResultsTableOrderingComposer
builder: (column) => ColumnOrderings(column), builder: (column) => ColumnOrderings(column),
); );
ColumnOrderings<String> get sourceExerciseIdSnapshot => $composableBuilder(
column: $table.sourceExerciseIdSnapshot,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<DateTime> get startedAt => $composableBuilder( ColumnOrderings<DateTime> get startedAt => $composableBuilder(
column: $table.startedAt, column: $table.startedAt,
builder: (column) => ColumnOrderings(column), builder: (column) => ColumnOrderings(column),
@ -45799,6 +45943,11 @@ class $$WorkoutHistorySetResultsTableAnnotationComposer
builder: (column) => column, builder: (column) => column,
); );
GeneratedColumn<String> get sourceExerciseIdSnapshot => $composableBuilder(
column: $table.sourceExerciseIdSnapshot,
builder: (column) => column,
);
GeneratedColumn<DateTime> get startedAt => GeneratedColumn<DateTime> get startedAt =>
$composableBuilder(column: $table.startedAt, builder: (column) => column); $composableBuilder(column: $table.startedAt, builder: (column) => column);
@ -45906,6 +46055,7 @@ class $$WorkoutHistorySetResultsTableTableManager
Value<int?> actualScoreTimeMs = const Value.absent(), Value<int?> actualScoreTimeMs = const Value.absent(),
Value<String?> scoreLabelSnapshot = const Value.absent(), Value<String?> scoreLabelSnapshot = const Value.absent(),
Value<String?> scoreUnitSnapshot = const Value.absent(), Value<String?> scoreUnitSnapshot = const Value.absent(),
Value<String?> sourceExerciseIdSnapshot = const Value.absent(),
Value<DateTime?> startedAt = const Value.absent(), Value<DateTime?> startedAt = const Value.absent(),
Value<DateTime?> completedAt = const Value.absent(), Value<DateTime?> completedAt = const Value.absent(),
Value<String> status = const Value.absent(), Value<String> status = const Value.absent(),
@ -45944,6 +46094,7 @@ class $$WorkoutHistorySetResultsTableTableManager
actualScoreTimeMs: actualScoreTimeMs, actualScoreTimeMs: actualScoreTimeMs,
scoreLabelSnapshot: scoreLabelSnapshot, scoreLabelSnapshot: scoreLabelSnapshot,
scoreUnitSnapshot: scoreUnitSnapshot, scoreUnitSnapshot: scoreUnitSnapshot,
sourceExerciseIdSnapshot: sourceExerciseIdSnapshot,
startedAt: startedAt, startedAt: startedAt,
completedAt: completedAt, completedAt: completedAt,
status: status, status: status,
@ -45984,6 +46135,7 @@ class $$WorkoutHistorySetResultsTableTableManager
Value<int?> actualScoreTimeMs = const Value.absent(), Value<int?> actualScoreTimeMs = const Value.absent(),
Value<String?> scoreLabelSnapshot = const Value.absent(), Value<String?> scoreLabelSnapshot = const Value.absent(),
Value<String?> scoreUnitSnapshot = const Value.absent(), Value<String?> scoreUnitSnapshot = const Value.absent(),
Value<String?> sourceExerciseIdSnapshot = const Value.absent(),
Value<DateTime?> startedAt = const Value.absent(), Value<DateTime?> startedAt = const Value.absent(),
Value<DateTime?> completedAt = const Value.absent(), Value<DateTime?> completedAt = const Value.absent(),
Value<String> status = const Value.absent(), Value<String> status = const Value.absent(),
@ -46022,6 +46174,7 @@ class $$WorkoutHistorySetResultsTableTableManager
actualScoreTimeMs: actualScoreTimeMs, actualScoreTimeMs: actualScoreTimeMs,
scoreLabelSnapshot: scoreLabelSnapshot, scoreLabelSnapshot: scoreLabelSnapshot,
scoreUnitSnapshot: scoreUnitSnapshot, scoreUnitSnapshot: scoreUnitSnapshot,
sourceExerciseIdSnapshot: sourceExerciseIdSnapshot,
startedAt: startedAt, startedAt: startedAt,
completedAt: completedAt, completedAt: completedAt,
status: status, status: status,
@ -46135,6 +46288,7 @@ typedef $$WorkoutHistoryStepResultsTableCreateCompanionBuilder =
Value<double?> actualScore, Value<double?> actualScore,
Value<int?> actualScoreTimeMs, Value<int?> actualScoreTimeMs,
Value<String?> note, Value<String?> note,
Value<String?> sourceExerciseIdSnapshot,
Value<int> rowid, Value<int> rowid,
}); });
typedef $$WorkoutHistoryStepResultsTableUpdateCompanionBuilder = typedef $$WorkoutHistoryStepResultsTableUpdateCompanionBuilder =
@ -46176,6 +46330,7 @@ typedef $$WorkoutHistoryStepResultsTableUpdateCompanionBuilder =
Value<double?> actualScore, Value<double?> actualScore,
Value<int?> actualScoreTimeMs, Value<int?> actualScoreTimeMs,
Value<String?> note, Value<String?> note,
Value<String?> sourceExerciseIdSnapshot,
Value<int> rowid, Value<int> rowid,
}); });
@ -46401,6 +46556,11 @@ class $$WorkoutHistoryStepResultsTableFilterComposer
builder: (column) => ColumnFilters(column), builder: (column) => ColumnFilters(column),
); );
ColumnFilters<String> get sourceExerciseIdSnapshot => $composableBuilder(
column: $table.sourceExerciseIdSnapshot,
builder: (column) => ColumnFilters(column),
);
$$WorkoutHistoriesTableFilterComposer get workoutHistoryId { $$WorkoutHistoriesTableFilterComposer get workoutHistoryId {
final $$WorkoutHistoriesTableFilterComposer composer = $composerBuilder( final $$WorkoutHistoriesTableFilterComposer composer = $composerBuilder(
composer: this, composer: this,
@ -46614,6 +46774,11 @@ class $$WorkoutHistoryStepResultsTableOrderingComposer
builder: (column) => ColumnOrderings(column), builder: (column) => ColumnOrderings(column),
); );
ColumnOrderings<String> get sourceExerciseIdSnapshot => $composableBuilder(
column: $table.sourceExerciseIdSnapshot,
builder: (column) => ColumnOrderings(column),
);
$$WorkoutHistoriesTableOrderingComposer get workoutHistoryId { $$WorkoutHistoriesTableOrderingComposer get workoutHistoryId {
final $$WorkoutHistoriesTableOrderingComposer composer = $composerBuilder( final $$WorkoutHistoriesTableOrderingComposer composer = $composerBuilder(
composer: this, composer: this,
@ -46807,6 +46972,11 @@ class $$WorkoutHistoryStepResultsTableAnnotationComposer
GeneratedColumn<String> get note => GeneratedColumn<String> get note =>
$composableBuilder(column: $table.note, builder: (column) => column); $composableBuilder(column: $table.note, builder: (column) => column);
GeneratedColumn<String> get sourceExerciseIdSnapshot => $composableBuilder(
column: $table.sourceExerciseIdSnapshot,
builder: (column) => column,
);
$$WorkoutHistoriesTableAnnotationComposer get workoutHistoryId { $$WorkoutHistoriesTableAnnotationComposer get workoutHistoryId {
final $$WorkoutHistoriesTableAnnotationComposer composer = $composerBuilder( final $$WorkoutHistoriesTableAnnotationComposer composer = $composerBuilder(
composer: this, composer: this,
@ -46910,6 +47080,7 @@ class $$WorkoutHistoryStepResultsTableTableManager
Value<double?> actualScore = const Value.absent(), Value<double?> actualScore = const Value.absent(),
Value<int?> actualScoreTimeMs = const Value.absent(), Value<int?> actualScoreTimeMs = const Value.absent(),
Value<String?> note = const Value.absent(), Value<String?> note = const Value.absent(),
Value<String?> sourceExerciseIdSnapshot = const Value.absent(),
Value<int> rowid = const Value.absent(), Value<int> rowid = const Value.absent(),
}) => WorkoutHistoryStepResultsCompanion( }) => WorkoutHistoryStepResultsCompanion(
id: id, id: id,
@ -46949,6 +47120,7 @@ class $$WorkoutHistoryStepResultsTableTableManager
actualScore: actualScore, actualScore: actualScore,
actualScoreTimeMs: actualScoreTimeMs, actualScoreTimeMs: actualScoreTimeMs,
note: note, note: note,
sourceExerciseIdSnapshot: sourceExerciseIdSnapshot,
rowid: rowid, rowid: rowid,
), ),
createCompanionCallback: createCompanionCallback:
@ -46990,6 +47162,7 @@ class $$WorkoutHistoryStepResultsTableTableManager
Value<double?> actualScore = const Value.absent(), Value<double?> actualScore = const Value.absent(),
Value<int?> actualScoreTimeMs = const Value.absent(), Value<int?> actualScoreTimeMs = const Value.absent(),
Value<String?> note = const Value.absent(), Value<String?> note = const Value.absent(),
Value<String?> sourceExerciseIdSnapshot = const Value.absent(),
Value<int> rowid = const Value.absent(), Value<int> rowid = const Value.absent(),
}) => WorkoutHistoryStepResultsCompanion.insert( }) => WorkoutHistoryStepResultsCompanion.insert(
id: id, id: id,
@ -47029,6 +47202,7 @@ class $$WorkoutHistoryStepResultsTableTableManager
actualScore: actualScore, actualScore: actualScore,
actualScoreTimeMs: actualScoreTimeMs, actualScoreTimeMs: actualScoreTimeMs,
note: note, note: note,
sourceExerciseIdSnapshot: sourceExerciseIdSnapshot,
rowid: rowid, rowid: rowid,
), ),
withReferenceMapper: (p0) => p0 withReferenceMapper: (p0) => p0

View File

@ -1602,6 +1602,138 @@ final class DriftWorkoutHistoryRepository implements WorkoutHistoryRepository {
} }
} }
final class DriftExercisePerformanceReferenceRepository
implements ExercisePerformanceReferenceRepository {
const DriftExercisePerformanceReferenceRepository(this.database);
final db.AppDatabase database;
@override
Future<bool> hasAnyCompletedHistoryForExercise(String exerciseId) async {
final row = await database
.customSelect(
'SELECT 1 FROM workout_history_set_results AS result '
'INNER JOIN workout_history AS history '
'ON history.id = result.workout_history_id '
'WHERE result.deleted_at IS NULL '
'AND history.deleted_at IS NULL '
'AND history.completed = 1 '
'AND result.status = ? '
'AND result.source_exercise_id_snapshot = ? '
'LIMIT 1',
variables: [
const Variable<String>('completed'),
Variable<String>(exerciseId),
],
)
.getSingleOrNull();
return row != null;
}
@override
Future<WorkoutHistorySetPerformance?> findLatestSetPerformance({
required String exerciseId,
required ActivePerformanceMeasures activeMeasures,
required int currentSetIndex,
}) async {
final valuePredicate = _activeMeasuresPredicate(activeMeasures);
if (valuePredicate == null) {
return null;
}
final row = await database
.customSelect(
'''
WITH candidates AS (
SELECT
result.workout_history_id,
history.started_at AS history_started_at,
result.set_index,
result.exercise_name_snapshot,
result.score_input_mode_snapshot,
result.actual_time_ms,
result.actual_reps,
result.actual_score,
result.actual_score_time_ms,
result.completed_at
FROM workout_history_set_results AS result
INNER JOIN workout_history AS history
ON history.id = result.workout_history_id
WHERE result.deleted_at IS NULL
AND history.deleted_at IS NULL
AND history.completed = 1
AND result.status = ?
AND result.source_exercise_id_snapshot = ?
AND ($valuePredicate)
),
latest_history AS (
SELECT workout_history_id
FROM candidates
ORDER BY history_started_at DESC
LIMIT 1
)
SELECT *
FROM candidates
WHERE workout_history_id = (SELECT workout_history_id FROM latest_history)
ORDER BY
CASE WHEN set_index = ? THEN 0 ELSE 1 END,
set_index DESC
LIMIT 1
''',
variables: [
const Variable<String>('completed'),
Variable<String>(exerciseId),
Variable<int>(currentSetIndex),
],
)
.getSingleOrNull();
return row == null ? null : _setPerformanceFromRow(row);
}
@override
Future<WorkoutHistoryMetricPerformance?> findBestMetricPerformance({
required String exerciseId,
required PerformanceMetric metric,
required domain.ScoreInputMode scoreInputMode,
}) async {
final metricSql = _metricSql(metric, scoreInputMode);
final row = await database
.customSelect(
'''
SELECT
result.workout_history_id,
history.started_at AS history_started_at,
result.set_index,
result.exercise_name_snapshot,
result.score_input_mode_snapshot,
result.actual_time_ms,
result.actual_reps,
result.actual_score,
result.actual_score_time_ms,
result.completed_at
FROM workout_history_set_results AS result
INNER JOIN workout_history AS history
ON history.id = result.workout_history_id
WHERE result.deleted_at IS NULL
AND history.deleted_at IS NULL
AND history.completed = 1
AND result.status = ?
AND result.source_exercise_id_snapshot = ?
AND ${metricSql.predicate}
ORDER BY ${metricSql.ordering},
history.started_at DESC,
result.completed_at DESC
LIMIT 1
''',
variables: [
const Variable<String>('completed'),
Variable<String>(exerciseId),
],
)
.getSingleOrNull();
return row == null ? null : _metricPerformanceFromRow(row, metric: metric);
}
}
Future<void> _upsertWithChangeLog({ Future<void> _upsertWithChangeLog({
required db.AppDatabase database, required db.AppDatabase database,
required String tableName, required String tableName,
@ -2863,6 +2995,94 @@ domain.ActiveRestState _activeRestStateFromCustomRow(QueryRow row) {
); );
} }
WorkoutHistorySetPerformance _setPerformanceFromRow(QueryRow row) {
final data = row.data;
return WorkoutHistorySetPerformance(
workoutHistoryId: data['workout_history_id'] as String,
startedAt: _dateTimeFromData(data, 'history_started_at'),
setIndex: data['set_index'] as int,
exerciseNameSnapshot: data['exercise_name_snapshot'] as String,
scoreInputModeSnapshot: _scoreInputModeFromDb(
data['score_input_mode_snapshot'] as String,
),
actualTimeMs: data['actual_time_ms'] as int?,
actualReps: data['actual_reps'] as int?,
actualScore: (data['actual_score'] as num?)?.toDouble(),
actualScoreTimeMs: data['actual_score_time_ms'] as int?,
completedAt: _dateTimeOrNullFromData(data, 'completed_at'),
);
}
WorkoutHistoryMetricPerformance _metricPerformanceFromRow(
QueryRow row, {
required PerformanceMetric metric,
}) {
final set = _setPerformanceFromRow(row);
return WorkoutHistoryMetricPerformance(
workoutHistoryId: set.workoutHistoryId,
startedAt: set.startedAt,
setIndex: set.setIndex,
exerciseNameSnapshot: set.exerciseNameSnapshot,
metric: metric,
scoreInputModeSnapshot: set.scoreInputModeSnapshot,
actualTimeMs: set.actualTimeMs,
actualReps: set.actualReps,
actualScore: set.actualScore,
actualScoreTimeMs: set.actualScoreTimeMs,
completedAt: set.completedAt,
);
}
String? _activeMeasuresPredicate(ActivePerformanceMeasures measures) {
final predicates = <String>[];
if (measures.scoreEnabled) {
predicates.add(
measures.scoreInputMode == domain.ScoreInputMode.stopwatch
? "(result.score_input_mode_snapshot = 'stopwatch' "
'AND result.actual_score_time_ms IS NOT NULL)'
: "(result.score_input_mode_snapshot = 'manual' "
'AND result.actual_score IS NOT NULL)',
);
}
if (measures.repsEnabled) {
predicates.add('result.actual_reps IS NOT NULL');
}
if (measures.timeEnabled) {
predicates.add('result.actual_time_ms IS NOT NULL');
}
return predicates.isEmpty ? null : predicates.join(' OR ');
}
({String predicate, String ordering}) _metricSql(
PerformanceMetric metric,
domain.ScoreInputMode scoreInputMode,
) {
return switch (metric) {
PerformanceMetric.score =>
scoreInputMode == domain.ScoreInputMode.stopwatch
? (
predicate:
"result.score_input_mode_snapshot = 'stopwatch' "
'AND result.actual_score_time_ms IS NOT NULL',
ordering: 'result.actual_score_time_ms ASC',
)
: (
predicate:
"result.score_input_mode_snapshot = 'manual' "
'AND result.actual_score IS NOT NULL',
ordering: 'result.actual_score DESC',
),
PerformanceMetric.reps => (
predicate: 'result.actual_reps IS NOT NULL',
ordering: 'result.actual_reps DESC',
),
PerformanceMetric.time => (
predicate: 'result.actual_time_ms IS NOT NULL',
ordering: 'result.actual_time_ms DESC',
),
};
}
db.ActiveScoreStopwatchStatesCompanion _activeScoreStopwatchStateCompanion( db.ActiveScoreStopwatchStatesCompanion _activeScoreStopwatchStateCompanion(
domain.ActiveScoreStopwatchState state, domain.ActiveScoreStopwatchState state,
) { ) {
@ -3108,6 +3328,7 @@ db.WorkoutHistorySetResultsCompanion _workoutHistorySetResultCompanion(
actualScoreTimeMs: Value(result.actualScoreTimeMs), actualScoreTimeMs: Value(result.actualScoreTimeMs),
scoreLabelSnapshot: Value(result.scoreLabelSnapshot), scoreLabelSnapshot: Value(result.scoreLabelSnapshot),
scoreUnitSnapshot: Value(result.scoreUnitSnapshot), scoreUnitSnapshot: Value(result.scoreUnitSnapshot),
sourceExerciseIdSnapshot: Value(result.sourceExerciseIdSnapshot),
startedAt: Value(_utcOrNull(result.startedAt)), startedAt: Value(_utcOrNull(result.startedAt)),
completedAt: Value(_utcOrNull(result.completedAt)), completedAt: Value(_utcOrNull(result.completedAt)),
status: Value(_setResultStatusToDb(result.status)), status: Value(_setResultStatusToDb(result.status)),
@ -3160,6 +3381,7 @@ db.WorkoutHistoryStepResultsCompanion _workoutHistoryStepResultCompanion(
actualScore: Value<double?>(result.actualScore), actualScore: Value<double?>(result.actualScore),
actualScoreTimeMs: Value<int?>(result.actualScoreTimeMs), actualScoreTimeMs: Value<int?>(result.actualScoreTimeMs),
note: Value<String?>(result.note), note: Value<String?>(result.note),
sourceExerciseIdSnapshot: Value<String?>(result.sourceExerciseIdSnapshot),
); );
} }
@ -3210,6 +3432,7 @@ domain.WorkoutHistorySetResult _workoutHistorySetResultFromRow(
actualScoreTimeMs: row.actualScoreTimeMs, actualScoreTimeMs: row.actualScoreTimeMs,
scoreLabelSnapshot: row.scoreLabelSnapshot, scoreLabelSnapshot: row.scoreLabelSnapshot,
scoreUnitSnapshot: row.scoreUnitSnapshot, scoreUnitSnapshot: row.scoreUnitSnapshot,
sourceExerciseIdSnapshot: row.sourceExerciseIdSnapshot,
startedAt: _utcOrNull(row.startedAt), startedAt: _utcOrNull(row.startedAt),
completedAt: _utcOrNull(row.completedAt), completedAt: _utcOrNull(row.completedAt),
status: _setResultStatusFromDb(row.status), status: _setResultStatusFromDb(row.status),
@ -3249,6 +3472,7 @@ domain.WorkoutHistoryStepResult _workoutHistoryStepResultFromRow(
actualScore: row.actualScore, actualScore: row.actualScore,
actualScoreTimeMs: row.actualScoreTimeMs, actualScoreTimeMs: row.actualScoreTimeMs,
note: row.note, note: row.note,
sourceExerciseIdSnapshot: row.sourceExerciseIdSnapshot,
); );
} }

View File

@ -745,6 +745,7 @@ class WorkoutHistorySetResults extends SyncableTable {
IntColumn get actualScoreTimeMs => integer().nullable()(); IntColumn get actualScoreTimeMs => integer().nullable()();
TextColumn get scoreLabelSnapshot => text().nullable()(); TextColumn get scoreLabelSnapshot => text().nullable()();
TextColumn get scoreUnitSnapshot => text().nullable()(); TextColumn get scoreUnitSnapshot => text().nullable()();
TextColumn get sourceExerciseIdSnapshot => text().nullable()();
DateTimeColumn get startedAt => dateTime().nullable()(); DateTimeColumn get startedAt => dateTime().nullable()();
DateTimeColumn get completedAt => dateTime().nullable()(); DateTimeColumn get completedAt => dateTime().nullable()();
TextColumn get status => text().withDefault(const Constant('completed'))(); TextColumn get status => text().withDefault(const Constant('completed'))();
@ -821,6 +822,7 @@ class WorkoutHistoryStepResults extends SyncableTable {
RealColumn get actualScore => real().nullable()(); RealColumn get actualScore => real().nullable()();
IntColumn get actualScoreTimeMs => integer().nullable()(); IntColumn get actualScoreTimeMs => integer().nullable()();
TextColumn get note => text().nullable()(); TextColumn get note => text().nullable()();
TextColumn get sourceExerciseIdSnapshot => text().nullable()();
@override @override
List<String> get customConstraints => [ List<String> get customConstraints => [

View File

@ -14,6 +14,8 @@ void main() {
late local.DriftActiveSessionRepository activeRepository; late local.DriftActiveSessionRepository activeRepository;
late local.DriftWorkoutTemplateRepository templateRepository; late local.DriftWorkoutTemplateRepository templateRepository;
late local.DriftWorkoutHistoryRepository historyRepository; late local.DriftWorkoutHistoryRepository historyRepository;
late local.DriftExercisePerformanceReferenceRepository
performanceReferenceRepository;
setUp(() { setUp(() {
database = local.AppDatabase(NativeDatabase.memory()); database = local.AppDatabase(NativeDatabase.memory());
@ -22,6 +24,8 @@ void main() {
activeRepository = local.DriftActiveSessionRepository(database); activeRepository = local.DriftActiveSessionRepository(database);
templateRepository = local.DriftWorkoutTemplateRepository(database); templateRepository = local.DriftWorkoutTemplateRepository(database);
historyRepository = local.DriftWorkoutHistoryRepository(database); historyRepository = local.DriftWorkoutHistoryRepository(database);
performanceReferenceRepository =
local.DriftExercisePerformanceReferenceRepository(database);
}); });
tearDown(() async { tearDown(() async {
@ -843,6 +847,265 @@ void main() {
expect(restored.results.single.actualReps, 10); expect(restored.results.single.actualReps, 10);
expect(restored.results.single.actualScore, 80); expect(restored.results.single.actualScore, 80);
expect(restored.results.single.scoreUnitSnapshot, 'kg'); expect(restored.results.single.scoreUnitSnapshot, 'kg');
expect(restored.results.single.sourceExerciseIdSnapshot, 'exercise-1');
},
);
test(
'performance reference ignores skipped and null values for latest set',
() async {
final now = DateTime.utc(2026, 7, 22, 10);
await historyRepository.save(
_history(
id: 'history-old',
startedAt: now.subtract(const Duration(days: 2)),
result: _historySetResult(
id: 'result-old',
historyId: 'history-old',
sourceExerciseId: 'exercise-1',
setIndex: 0,
startedAt: now.subtract(const Duration(days: 2)),
actualReps: 8,
),
),
);
await historyRepository.save(
_history(
id: 'history-new',
startedAt: now,
results: [
_historySetResult(
id: 'result-skipped',
historyId: 'history-new',
sourceExerciseId: 'exercise-1',
setIndex: 0,
startedAt: now,
status: SetResultStatus.skipped,
),
_historySetResult(
id: 'result-null',
historyId: 'history-new',
sourceExerciseId: 'exercise-1',
setIndex: 1,
startedAt: now,
),
_historySetResult(
id: 'result-value',
historyId: 'history-new',
sourceExerciseId: 'exercise-1',
setIndex: 2,
startedAt: now,
actualReps: 11,
),
],
),
);
final latest = await performanceReferenceRepository
.findLatestSetPerformance(
exerciseId: 'exercise-1',
activeMeasures: const ActivePerformanceMeasures(
timeEnabled: false,
repsEnabled: true,
scoreEnabled: false,
),
currentSetIndex: 0,
);
expect(latest, isNotNull);
expect(latest!.workoutHistoryId, 'history-new');
expect(latest.setIndex, 2);
expect(latest.actualReps, 11);
},
);
test('performance reference uses same set when available', () async {
final now = DateTime.utc(2026, 7, 22, 11);
await historyRepository.save(
_history(
id: 'history-sets',
startedAt: now,
results: [
_historySetResult(
id: 'result-set-0',
historyId: 'history-sets',
sourceExerciseId: 'exercise-1',
setIndex: 0,
startedAt: now,
actualReps: 7,
),
_historySetResult(
id: 'result-set-1',
historyId: 'history-sets',
sourceExerciseId: 'exercise-1',
setIndex: 1,
startedAt: now,
actualReps: 9,
),
],
),
);
final latest = await performanceReferenceRepository
.findLatestSetPerformance(
exerciseId: 'exercise-1',
activeMeasures: const ActivePerformanceMeasures(
timeEnabled: false,
repsEnabled: true,
scoreEnabled: false,
),
currentSetIndex: 0,
);
expect(latest, isNotNull);
expect(latest!.setIndex, 0);
expect(latest.actualReps, 7);
});
test('performance reference finds records by metric rules', () async {
final now = DateTime.utc(2026, 7, 22, 12);
await historyRepository.save(
_history(
id: 'history-records',
startedAt: now,
results: [
_historySetResult(
id: 'result-reps-low',
historyId: 'history-records',
sourceExerciseId: 'exercise-1',
setIndex: 0,
startedAt: now,
actualTimeMs: 30000,
actualReps: 6,
actualScore: 15,
),
_historySetResult(
id: 'result-reps-high',
historyId: 'history-records',
sourceExerciseId: 'exercise-1',
setIndex: 1,
startedAt: now,
actualTimeMs: 45000,
actualReps: 12,
actualScore: 20,
),
_historySetResult(
id: 'result-stopwatch-slow',
historyId: 'history-records',
sourceExerciseId: 'exercise-1',
setIndex: 2,
startedAt: now,
scoreInputMode: ScoreInputMode.stopwatch,
actualScoreTimeMs: 11000,
),
_historySetResult(
id: 'result-stopwatch-fast',
historyId: 'history-records',
sourceExerciseId: 'exercise-1',
setIndex: 3,
startedAt: now,
scoreInputMode: ScoreInputMode.stopwatch,
actualScoreTimeMs: 9000,
),
],
),
);
final reps = await performanceReferenceRepository.findBestMetricPerformance(
exerciseId: 'exercise-1',
metric: PerformanceMetric.reps,
scoreInputMode: ScoreInputMode.manual,
);
final time = await performanceReferenceRepository.findBestMetricPerformance(
exerciseId: 'exercise-1',
metric: PerformanceMetric.time,
scoreInputMode: ScoreInputMode.manual,
);
final manualScore = await performanceReferenceRepository
.findBestMetricPerformance(
exerciseId: 'exercise-1',
metric: PerformanceMetric.score,
scoreInputMode: ScoreInputMode.manual,
);
final stopwatchScore = await performanceReferenceRepository
.findBestMetricPerformance(
exerciseId: 'exercise-1',
metric: PerformanceMetric.score,
scoreInputMode: ScoreInputMode.stopwatch,
);
expect(reps!.actualReps, 12);
expect(time!.actualTimeMs, 45000);
expect(manualScore!.actualScore, 20);
expect(stopwatchScore!.actualScoreTimeMs, 9000);
});
test('performance reference matches archived source exercise id', () async {
final now = DateTime.utc(2026, 7, 22, 13);
await exerciseRepository.save(
Exercise(
metadata: _metadata('exercise-archived', now),
name: 'Archived drill',
hasTimeMeasure: false,
hasRepsMeasure: true,
hasScoreMeasure: false,
archivedAt: now,
),
);
await historyRepository.save(
_history(
id: 'history-archived',
startedAt: now,
result: _historySetResult(
id: 'result-archived',
historyId: 'history-archived',
sourceExerciseId: 'exercise-archived',
setIndex: 0,
startedAt: now,
actualReps: 13,
),
),
);
expect(
await performanceReferenceRepository.hasAnyCompletedHistoryForExercise(
'exercise-archived',
),
isTrue,
);
final latest = await performanceReferenceRepository
.findLatestSetPerformance(
exerciseId: 'exercise-archived',
activeMeasures: const ActivePerformanceMeasures(
timeEnabled: false,
repsEnabled: true,
scoreEnabled: false,
),
currentSetIndex: 0,
);
expect(latest!.actualReps, 13);
});
test(
'performance reference use case prioritizes record score metric',
() async {
final repository = _FakePerformanceReferenceRepository();
final useCase = ExercisePerformanceReferenceUseCase(
repository: repository,
);
final reference = await useCase.getExercisePerformanceReference(
exerciseId: 'exercise-1',
activeMeasures: const ActivePerformanceMeasures(
timeEnabled: true,
repsEnabled: true,
scoreEnabled: true,
),
currentSetIndex: 0,
);
expect(reference.hasAnyHistoryForExercise, isTrue);
expect(repository.requestedMetric, PerformanceMetric.score);
}, },
); );
} }
@ -858,6 +1121,7 @@ String _resolvedSnapshot() {
'exercises': [ 'exercises': [
{ {
'id': 'exercise-snapshot-1', 'id': 'exercise-snapshot-1',
'sourceExerciseId': 'exercise-1',
'exerciseNameSnapshot': 'Squat', 'exerciseNameSnapshot': 'Squat',
'setsCount': 1, 'setsCount': 1,
'timeEnabled': true, 'timeEnabled': true,
@ -888,6 +1152,71 @@ EntityMetadata _metadata(String id, DateTime now, [int localRevision = 0]) {
); );
} }
WorkoutHistory _history({
required String id,
required DateTime startedAt,
WorkoutHistorySetResult? result,
List<WorkoutHistorySetResult>? results,
}) {
return WorkoutHistory(
metadata: _metadata(id, startedAt),
nameSnapshot: id,
startedAt: startedAt,
endedAt: startedAt.add(const Duration(minutes: 5)),
totalActiveMs: 300000,
completed: true,
historySnapshotJson: '{"name":"$id"}',
results: results ?? [result!],
);
}
WorkoutHistorySetResult _historySetResult({
required String id,
required String historyId,
required String sourceExerciseId,
required int setIndex,
required DateTime startedAt,
int? actualTimeMs,
int? actualReps,
double? actualScore,
int? actualScoreTimeMs,
ScoreInputMode scoreInputMode = ScoreInputMode.manual,
SetResultStatus status = SetResultStatus.completed,
}) {
final scoreEnabled = actualScore != null || actualScoreTimeMs != null;
return WorkoutHistorySetResult(
metadata: _metadata(id, startedAt),
workoutHistoryId: historyId,
programSnapshotId: 'program-snapshot',
exerciseSnapshotId: 'exercise-snapshot-$sourceExerciseId',
programIndex: 0,
exerciseIndex: 0,
setIndex: setIndex,
programNameSnapshot: 'Program',
exerciseNameSnapshot: 'Exercise',
timeEnabledSnapshot: actualTimeMs != null,
repsEnabledSnapshot:
actualReps != null || (actualTimeMs == null && !scoreEnabled),
scoreEnabledSnapshot: scoreEnabled,
actualTimeMs: actualTimeMs,
actualReps: actualReps,
actualScore: actualScore,
actualScoreTimeMs: actualScoreTimeMs,
scoreInputModeSnapshot: scoreInputMode,
scoreLabelSnapshot: scoreEnabled && scoreInputMode == ScoreInputMode.manual
? 'Score'
: null,
scoreUnitSnapshot: scoreEnabled && scoreInputMode == ScoreInputMode.manual
? 'pts'
: null,
sourceExerciseIdSnapshot: sourceExerciseId,
completedAt: status == SetResultStatus.completed
? startedAt.add(const Duration(minutes: 1))
: null,
status: status,
);
}
ProgramExercise _programExercise( ProgramExercise _programExercise(
String id, String id,
DateTime now, { DateTime now, {
@ -1019,3 +1348,40 @@ final class _FakeIds implements IdGenerator {
return 'id-$_next'; return 'id-$_next';
} }
} }
final class _FakePerformanceReferenceRepository
implements ExercisePerformanceReferenceRepository {
PerformanceMetric? requestedMetric;
@override
Future<bool> hasAnyCompletedHistoryForExercise(String exerciseId) async {
return true;
}
@override
Future<WorkoutHistorySetPerformance?> findLatestSetPerformance({
required String exerciseId,
required ActivePerformanceMeasures activeMeasures,
required int currentSetIndex,
}) async {
return null;
}
@override
Future<WorkoutHistoryMetricPerformance?> findBestMetricPerformance({
required String exerciseId,
required PerformanceMetric metric,
required ScoreInputMode scoreInputMode,
}) async {
requestedMetric = metric;
return WorkoutHistoryMetricPerformance(
workoutHistoryId: 'history-1',
startedAt: DateTime.utc(2026, 7, 22),
setIndex: 0,
exerciseNameSnapshot: 'Exercise',
metric: metric,
scoreInputModeSnapshot: scoreInputMode,
actualScore: 10,
);
}
}

View File

@ -211,6 +211,9 @@ final class _FakeBootstrap implements AppDependencies {
workoutHistoryUseCases = WorkoutHistoryUseCases( workoutHistoryUseCases = WorkoutHistoryUseCases(
repository: _FakeWorkoutHistoryRepository(), repository: _FakeWorkoutHistoryRepository(),
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)), clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
),
exercisePerformanceReferenceUseCase = ExercisePerformanceReferenceUseCase(
repository: _FakeExercisePerformanceReferenceRepository(),
); );
@override @override
@ -245,6 +248,9 @@ final class _FakeBootstrap implements AppDependencies {
@override @override
final WorkoutHistoryUseCases workoutHistoryUseCases; final WorkoutHistoryUseCases workoutHistoryUseCases;
@override
final ExercisePerformanceReferenceUseCase exercisePerformanceReferenceUseCase;
} }
String _sessionSnapshot() { String _sessionSnapshot() {
@ -704,3 +710,29 @@ final class _FakeWorkoutHistoryRepository implements WorkoutHistoryRepository {
@override @override
Future<void> saveStepResult(WorkoutHistoryStepResult result) async {} Future<void> saveStepResult(WorkoutHistoryStepResult result) async {}
} }
final class _FakeExercisePerformanceReferenceRepository
implements ExercisePerformanceReferenceRepository {
@override
Future<bool> hasAnyCompletedHistoryForExercise(String exerciseId) async {
return false;
}
@override
Future<WorkoutHistorySetPerformance?> findLatestSetPerformance({
required String exerciseId,
required ActivePerformanceMeasures activeMeasures,
required int currentSetIndex,
}) async {
return null;
}
@override
Future<WorkoutHistoryMetricPerformance?> findBestMetricPerformance({
required String exerciseId,
required PerformanceMetric metric,
required ScoreInputMode scoreInputMode,
}) async {
return null;
}
}