Étend le modèle Drift (tables.dart, app_database.dart/.g.dart, migration schemaVersion 7→8) et la couche application (entités, use cases, repositories) pour supporter des exercices composés de plusieurs étapes. flutter pub get OK, build_runner OK, flutter analyze propre (mêmes 8 infos préexistantes), 87/87 tests verts, build APK debug validé. Premier ticket du chantier "Exercice à plusieurs étapes" (#54). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
291 lines
9.4 KiB
Dart
291 lines
9.4 KiB
Dart
import 'package:drift/drift.dart';
|
|
import 'package:drift_flutter/drift_flutter.dart';
|
|
|
|
import 'tables.dart';
|
|
|
|
part 'app_database.g.dart';
|
|
|
|
@DriftDatabase(
|
|
tables: [
|
|
ActiveRestStates,
|
|
ActiveScoreStopwatchStates,
|
|
ActiveSetResults,
|
|
ActiveWorkoutSessions,
|
|
ChangeLogEntries,
|
|
Exercises,
|
|
ExerciseImages,
|
|
ExerciseSteps,
|
|
MediaAssets,
|
|
ProgramExercises,
|
|
Programs,
|
|
WorkoutHistories,
|
|
WorkoutHistorySetResults,
|
|
WorkoutTemplateExerciseOverrides,
|
|
WorkoutTemplatePrograms,
|
|
WorkoutTemplates,
|
|
],
|
|
)
|
|
final class AppDatabase extends _$AppDatabase {
|
|
AppDatabase(super.executor);
|
|
|
|
factory AppDatabase.open() {
|
|
return AppDatabase(
|
|
driftDatabase(
|
|
name: 'gametime',
|
|
native: const DriftNativeOptions(shareAcrossIsolates: true),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
int get schemaVersion => 8;
|
|
|
|
@override
|
|
MigrationStrategy get migration {
|
|
return MigrationStrategy(
|
|
onCreate: (migrator) async {
|
|
await migrator.createAll();
|
|
await _createIndexes();
|
|
},
|
|
onUpgrade: (migrator, from, to) async {
|
|
if (from < 2) {
|
|
await customStatement(
|
|
'ALTER TABLE active_set_results ADD COLUMN status TEXT NOT NULL '
|
|
"DEFAULT 'completed' CHECK (status IN ('completed', 'skipped'))",
|
|
);
|
|
await customStatement(
|
|
'ALTER TABLE workout_history_set_results ADD COLUMN status TEXT '
|
|
"NOT NULL DEFAULT 'completed' CHECK (status IN ('completed', "
|
|
"'skipped'))",
|
|
);
|
|
}
|
|
if (from < 3) {
|
|
await _migrateToSchema3(migrator);
|
|
}
|
|
if (from < 4) {
|
|
await _migrateToSchema4();
|
|
}
|
|
if (from < 5) {
|
|
await _migrateToSchema5(migrator);
|
|
}
|
|
if (from < 6) {
|
|
await _migrateToSchema6();
|
|
}
|
|
if (from < 7) {
|
|
await _migrateToSchema7();
|
|
}
|
|
if (from < 8) {
|
|
await _migrateToSchema8(migrator);
|
|
}
|
|
await _createIndexes();
|
|
},
|
|
beforeOpen: (details) async {
|
|
await customStatement('PRAGMA foreign_keys = ON');
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<void> _createIndexes() async {
|
|
for (final tableName in _syncableTableNames) {
|
|
await customStatement(
|
|
'CREATE INDEX IF NOT EXISTS idx_${tableName}_deleted_at '
|
|
'ON $tableName (deleted_at)',
|
|
);
|
|
await customStatement(
|
|
'CREATE INDEX IF NOT EXISTS idx_${tableName}_updated_at '
|
|
'ON $tableName (updated_at)',
|
|
);
|
|
await customStatement(
|
|
'CREATE INDEX IF NOT EXISTS idx_${tableName}_sync_state '
|
|
'ON $tableName (sync_state)',
|
|
);
|
|
await customStatement(
|
|
'CREATE INDEX IF NOT EXISTS idx_${tableName}_local_revision '
|
|
'ON $tableName (local_revision)',
|
|
);
|
|
}
|
|
|
|
await customStatement(
|
|
'CREATE INDEX IF NOT EXISTS idx_program_exercises_program_id '
|
|
'ON program_exercises (program_id)',
|
|
);
|
|
await customStatement(
|
|
'CREATE INDEX IF NOT EXISTS idx_exercise_images_exercise_id '
|
|
'ON exercise_images (exercise_id)',
|
|
);
|
|
await customStatement(
|
|
'CREATE INDEX IF NOT EXISTS idx_exercise_steps_exercise_id_position '
|
|
'ON exercise_steps (exercise_id, position)',
|
|
);
|
|
await customStatement(
|
|
'CREATE INDEX IF NOT EXISTS idx_workout_template_programs_template_id '
|
|
'ON workout_template_programs (workout_template_id)',
|
|
);
|
|
await customStatement(
|
|
'CREATE INDEX IF NOT EXISTS idx_active_set_results_session_id '
|
|
'ON active_set_results (active_workout_session_id)',
|
|
);
|
|
await customStatement(
|
|
'CREATE INDEX IF NOT EXISTS idx_active_score_stopwatch_states_session_id '
|
|
'ON active_score_stopwatch_states (active_workout_session_id)',
|
|
);
|
|
await customStatement(
|
|
'CREATE INDEX IF NOT EXISTS idx_active_rest_states_session_id '
|
|
'ON active_rest_states (active_workout_session_id)',
|
|
);
|
|
await customStatement(
|
|
'CREATE UNIQUE INDEX IF NOT EXISTS '
|
|
'idx_active_workout_sessions_single_open '
|
|
'ON active_workout_sessions ((1)) '
|
|
'WHERE deleted_at IS NULL '
|
|
"AND status IN ('running', 'paused', 'savedExit')",
|
|
);
|
|
await customStatement(
|
|
'CREATE INDEX IF NOT EXISTS idx_workout_history_started_at '
|
|
'ON workout_history (started_at)',
|
|
);
|
|
await customStatement(
|
|
'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_change_log_entity '
|
|
'ON change_log (entity_type, entity_id)',
|
|
);
|
|
await customStatement(
|
|
'CREATE INDEX IF NOT EXISTS idx_change_log_local_revision '
|
|
'ON change_log (local_revision)',
|
|
);
|
|
await customStatement(
|
|
'CREATE INDEX IF NOT EXISTS idx_change_log_synced_at '
|
|
'ON change_log (synced_at)',
|
|
);
|
|
}
|
|
}
|
|
|
|
const _syncableTableNames = [
|
|
'active_rest_states',
|
|
'active_score_stopwatch_states',
|
|
'active_set_results',
|
|
'active_workout_sessions',
|
|
'exercises',
|
|
'exercise_images',
|
|
'exercise_steps',
|
|
'media_assets',
|
|
'program_exercises',
|
|
'programs',
|
|
'workout_history',
|
|
'workout_history_set_results',
|
|
'workout_template_exercise_overrides',
|
|
'workout_template_programs',
|
|
'workout_templates',
|
|
];
|
|
|
|
extension on AppDatabase {
|
|
Future<void> _migrateToSchema3(Migrator migrator) async {
|
|
await customStatement(
|
|
'ALTER TABLE exercises ADD COLUMN score_input_mode TEXT NOT NULL '
|
|
"DEFAULT 'manual' CHECK (score_input_mode IN ('manual', 'stopwatch'))",
|
|
);
|
|
await customStatement(
|
|
'ALTER TABLE program_exercises ADD COLUMN score_input_mode_snapshot TEXT '
|
|
"NOT NULL DEFAULT 'manual' CHECK (score_input_mode_snapshot IN "
|
|
"('manual', 'stopwatch'))",
|
|
);
|
|
await customStatement(
|
|
'ALTER TABLE program_exercises ADD COLUMN target_score_time_ms INTEGER '
|
|
'CHECK (target_score_time_ms IS NULL OR target_score_time_ms > 0)',
|
|
);
|
|
await customStatement(
|
|
'ALTER TABLE workout_template_exercise_overrides ADD COLUMN '
|
|
'target_score_time_ms_override INTEGER CHECK '
|
|
'(target_score_time_ms_override IS NULL OR '
|
|
'target_score_time_ms_override > 0)',
|
|
);
|
|
await customStatement(
|
|
'ALTER TABLE active_set_results ADD COLUMN actual_score_time_ms INTEGER '
|
|
'CHECK (actual_score_time_ms IS NULL OR actual_score_time_ms >= 0)',
|
|
);
|
|
await customStatement(
|
|
'ALTER TABLE active_set_results ADD COLUMN score_input_mode_snapshot '
|
|
"TEXT NOT NULL DEFAULT 'manual' CHECK (score_input_mode_snapshot IN "
|
|
"('manual', 'stopwatch'))",
|
|
);
|
|
await customStatement(
|
|
'ALTER TABLE workout_history_set_results ADD COLUMN '
|
|
'score_input_mode_snapshot TEXT NOT NULL DEFAULT '
|
|
"'manual' CHECK (score_input_mode_snapshot IN ('manual', 'stopwatch'))",
|
|
);
|
|
await customStatement(
|
|
'ALTER TABLE workout_history_set_results ADD COLUMN '
|
|
'target_score_time_ms_snapshot INTEGER CHECK '
|
|
'(target_score_time_ms_snapshot IS NULL OR '
|
|
'target_score_time_ms_snapshot > 0)',
|
|
);
|
|
await customStatement(
|
|
'ALTER TABLE workout_history_set_results ADD COLUMN '
|
|
'actual_score_time_ms INTEGER CHECK (actual_score_time_ms IS NULL OR '
|
|
'actual_score_time_ms >= 0)',
|
|
);
|
|
await migrator.createTable(activeScoreStopwatchStates);
|
|
}
|
|
|
|
Future<void> _migrateToSchema4() async {
|
|
await customStatement(
|
|
'ALTER TABLE exercises ADD COLUMN default_target_time_seconds INTEGER '
|
|
'CHECK (default_target_time_seconds IS NULL OR '
|
|
'default_target_time_seconds > 0)',
|
|
);
|
|
await customStatement(
|
|
'ALTER TABLE exercises ADD COLUMN default_target_reps INTEGER '
|
|
'CHECK (default_target_reps IS NULL OR default_target_reps > 0)',
|
|
);
|
|
await customStatement(
|
|
'ALTER TABLE exercises ADD COLUMN default_target_score REAL '
|
|
'CHECK (default_target_score IS NULL OR default_target_score > 0)',
|
|
);
|
|
await customStatement(
|
|
'ALTER TABLE exercises ADD COLUMN default_target_score_time_ms INTEGER '
|
|
'CHECK (default_target_score_time_ms IS NULL OR '
|
|
'default_target_score_time_ms > 0)',
|
|
);
|
|
}
|
|
|
|
Future<void> _migrateToSchema5(Migrator migrator) async {
|
|
await migrator.createTable(exerciseImages);
|
|
await customStatement(
|
|
'INSERT OR IGNORE INTO exercise_images (id, created_at, updated_at, '
|
|
'deleted_at, schema_version, sync_state, local_revision, '
|
|
'origin_device_id, future_owner_profile_id, last_synced_at, '
|
|
'remote_revision, exercise_id, media_asset_id, position) '
|
|
"SELECT 'exercise-image:' || id || ':0', created_at, updated_at, "
|
|
'deleted_at, schema_version, sync_state, local_revision, '
|
|
'origin_device_id, future_owner_profile_id, last_synced_at, '
|
|
'remote_revision, id, image_media_id, 0 FROM exercises '
|
|
'WHERE image_media_id IS NOT NULL',
|
|
);
|
|
}
|
|
|
|
Future<void> _migrateToSchema6() async {
|
|
await customStatement(
|
|
'ALTER TABLE program_exercises ADD COLUMN '
|
|
'exercise_image_media_ids_snapshot_json TEXT',
|
|
);
|
|
}
|
|
|
|
Future<void> _migrateToSchema7() async {
|
|
await customStatement(
|
|
'ALTER TABLE exercises ADD COLUMN icon_media_id TEXT '
|
|
'REFERENCES media_assets(id)',
|
|
);
|
|
}
|
|
|
|
Future<void> _migrateToSchema8(Migrator migrator) async {
|
|
await migrator.createTable(exerciseSteps);
|
|
await customStatement(
|
|
'ALTER TABLE program_exercises ADD COLUMN '
|
|
'exercise_steps_snapshot_json TEXT',
|
|
);
|
|
}
|
|
}
|