Finalise #183 : modele/persistance des types metier d'exercice, mapping vers la strategie Health Services, propagation du payload dans le contrat watch bridge et consommation cote Kotlin montre. Perimetre complet QA vert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1066 lines
34 KiB
Dart
1066 lines
34 KiB
Dart
import 'package:drift/drift.dart';
|
|
import 'package:drift_flutter/drift_flutter.dart';
|
|
|
|
import 'tables.dart';
|
|
|
|
part 'app_database.g.dart';
|
|
|
|
@DriftDatabase(
|
|
tables: [
|
|
ActiveExerciseStepProgressStates,
|
|
ActiveExerciseStepResults,
|
|
ActiveRestStates,
|
|
ActiveManualScoreStates,
|
|
ActiveScoreStopwatchStates,
|
|
ActiveSetTimerStates,
|
|
ActiveSetResults,
|
|
ActiveWorkoutSessions,
|
|
ChangeLogEntries,
|
|
Exercises,
|
|
ExerciseImages,
|
|
ExerciseSteps,
|
|
LocalSeedMetadata,
|
|
MediaAssets,
|
|
OnlineAccountSessions,
|
|
PendingShareActions,
|
|
ProgramExercises,
|
|
Programs,
|
|
RemoteResourceMappings,
|
|
ShareInboxItems,
|
|
SyncMetadataEntries,
|
|
WorkoutTelemetryAggregates,
|
|
WorkoutTelemetrySamples,
|
|
WorkoutHistories,
|
|
WorkoutHistorySetResults,
|
|
WorkoutHistoryStepResults,
|
|
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 => 25;
|
|
|
|
@override
|
|
MigrationStrategy get migration {
|
|
return MigrationStrategy(
|
|
onCreate: (migrator) async {
|
|
await migrator.createAll();
|
|
await _migrateToSchema15();
|
|
await _migrateToSchema16(migrator);
|
|
await _migrateToSchema25();
|
|
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);
|
|
}
|
|
if (from < 9) {
|
|
await _migrateToSchema9(migrator);
|
|
}
|
|
if (from < 10) {
|
|
await _migrateToSchema10(migrator);
|
|
}
|
|
if (from < 11) {
|
|
await _migrateToSchema11(migrator);
|
|
}
|
|
if (from < 12) {
|
|
await _migrateToSchema12(migrator);
|
|
}
|
|
if (from < 13) {
|
|
await _migrateToSchema13();
|
|
}
|
|
if (from < 14) {
|
|
await _migrateToSchema14();
|
|
}
|
|
if (from < 15) {
|
|
await _migrateToSchema15();
|
|
}
|
|
if (from < 16) {
|
|
await _migrateToSchema16(migrator);
|
|
}
|
|
if (from < 17) {
|
|
await _migrateToSchema17();
|
|
}
|
|
if (from < 19) {
|
|
await _migrateToSchema19();
|
|
}
|
|
if (from < 20) {
|
|
await _migrateToSchema20(migrator);
|
|
}
|
|
if (from < 21) {
|
|
await _migrateToSchema21();
|
|
}
|
|
if (from < 22) {
|
|
await _migrateToSchema22();
|
|
}
|
|
if (from < 23) {
|
|
await _migrateToSchema23();
|
|
}
|
|
if (from < 24) {
|
|
await _migrateToSchema24(migrator);
|
|
}
|
|
if (from < 25) {
|
|
await _migrateToSchema25();
|
|
}
|
|
await _createIndexes();
|
|
},
|
|
beforeOpen: (details) async {
|
|
await customStatement('PRAGMA foreign_keys = ON');
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<void> _createIndexes() async {
|
|
if (!await _hasTable(_syncableTableNames.first)) {
|
|
return;
|
|
}
|
|
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_exercises_category '
|
|
'ON exercises (category)',
|
|
);
|
|
await customStatement(
|
|
'CREATE INDEX IF NOT EXISTS idx_exercises_is_example '
|
|
'ON exercises (is_example)',
|
|
);
|
|
await customStatement(
|
|
'CREATE INDEX IF NOT EXISTS idx_programs_is_example '
|
|
'ON programs (is_example)',
|
|
);
|
|
await customStatement(
|
|
'CREATE INDEX IF NOT EXISTS idx_workout_templates_is_example '
|
|
'ON workout_templates (is_example)',
|
|
);
|
|
await customStatement(
|
|
'CREATE INDEX IF NOT EXISTS idx_online_account_sessions_logged_in '
|
|
'ON online_account_sessions (is_logged_in, updated_at)',
|
|
);
|
|
await customStatement(
|
|
'CREATE INDEX IF NOT EXISTS idx_remote_resource_mappings_resource '
|
|
'ON remote_resource_mappings (resource_type, client_id)',
|
|
);
|
|
await customStatement(
|
|
'CREATE INDEX IF NOT EXISTS idx_share_inbox_items_created_at '
|
|
'ON share_inbox_items (created_at)',
|
|
);
|
|
await customStatement(
|
|
'CREATE INDEX IF NOT EXISTS idx_pending_share_actions_status '
|
|
'ON pending_share_actions (status, created_at)',
|
|
);
|
|
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_manual_score_states_session_id '
|
|
'ON active_manual_score_states (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_set_timer_states_session_id '
|
|
'ON active_set_timer_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 INDEX IF NOT EXISTS '
|
|
'idx_active_exercise_step_progress_states_session_id '
|
|
'ON active_exercise_step_progress_states (active_workout_session_id)',
|
|
);
|
|
await customStatement(
|
|
'CREATE INDEX IF NOT EXISTS idx_active_exercise_step_results_session_id '
|
|
'ON active_exercise_step_results (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_completed_started '
|
|
'ON workout_history (completed, started_at) '
|
|
'WHERE deleted_at IS NULL',
|
|
);
|
|
await customStatement(
|
|
'CREATE INDEX IF NOT EXISTS idx_workout_telemetry_samples_session '
|
|
'ON workout_telemetry_samples (session_id, captured_at)',
|
|
);
|
|
await customStatement(
|
|
'CREATE INDEX IF NOT EXISTS idx_workout_telemetry_aggregates_session '
|
|
'ON workout_telemetry_aggregates (session_id, scope)',
|
|
);
|
|
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_history_set_progression_exercise '
|
|
'ON workout_history_set_results (source_exercise_id_snapshot, '
|
|
'exercise_snapshot_id, workout_history_id, status) '
|
|
'WHERE deleted_at IS NULL',
|
|
);
|
|
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_history_step_progression_exercise '
|
|
'ON workout_history_step_results (source_exercise_id_snapshot, '
|
|
'exercise_snapshot_id, workout_history_id, status) '
|
|
'WHERE deleted_at IS NULL',
|
|
);
|
|
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)',
|
|
);
|
|
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_exercise_step_progress_states',
|
|
'active_exercise_step_results',
|
|
'active_manual_score_states',
|
|
'active_rest_states',
|
|
'active_score_stopwatch_states',
|
|
'active_set_timer_states',
|
|
'active_set_results',
|
|
'active_workout_sessions',
|
|
'exercises',
|
|
'exercise_images',
|
|
'exercise_steps',
|
|
'media_assets',
|
|
'program_exercises',
|
|
'programs',
|
|
'workout_history',
|
|
'workout_history_set_results',
|
|
'workout_history_step_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',
|
|
);
|
|
}
|
|
|
|
Future<void> _migrateToSchema9(Migrator migrator) async {
|
|
await migrator.createTable(activeExerciseStepProgressStates);
|
|
await migrator.createTable(activeExerciseStepResults);
|
|
await migrator.createTable(workoutHistoryStepResults);
|
|
}
|
|
|
|
Future<void> _migrateToSchema10(Migrator migrator) async {
|
|
await migrator.createTable(onlineAccountSessions);
|
|
}
|
|
|
|
Future<void> _migrateToSchema11(Migrator migrator) async {
|
|
await migrator.createTable(syncMetadataEntries);
|
|
await migrator.createTable(remoteResourceMappings);
|
|
}
|
|
|
|
Future<void> _migrateToSchema12(Migrator migrator) async {
|
|
await migrator.createTable(shareInboxItems);
|
|
await migrator.createTable(pendingShareActions);
|
|
}
|
|
|
|
Future<void> _migrateToSchema13() async {
|
|
await customStatement('PRAGMA foreign_keys = OFF');
|
|
await customStatement('''
|
|
CREATE TABLE exercises_new (
|
|
id TEXT NOT NULL PRIMARY KEY,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL,
|
|
deleted_at INTEGER,
|
|
schema_version INTEGER NOT NULL DEFAULT 1,
|
|
sync_state TEXT NOT NULL CHECK (sync_state IN ('localOnly', 'dirty', 'synced', 'deleted')),
|
|
local_revision INTEGER NOT NULL CHECK (local_revision >= 0),
|
|
origin_device_id TEXT NOT NULL,
|
|
future_owner_profile_id TEXT,
|
|
last_synced_at INTEGER,
|
|
remote_revision TEXT,
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
icon_media_id TEXT REFERENCES media_assets(id),
|
|
video_media_id TEXT REFERENCES media_assets(id),
|
|
has_time_measure INTEGER NOT NULL CHECK (has_time_measure IN (0, 1)),
|
|
has_reps_measure INTEGER NOT NULL CHECK (has_reps_measure IN (0, 1)),
|
|
has_score_measure INTEGER NOT NULL CHECK (has_score_measure IN (0, 1)),
|
|
score_input_mode TEXT NOT NULL DEFAULT 'manual',
|
|
score_label TEXT,
|
|
score_unit TEXT,
|
|
default_target_time_seconds INTEGER,
|
|
default_target_reps INTEGER,
|
|
default_target_score REAL,
|
|
default_target_score_time_ms INTEGER,
|
|
archived_at INTEGER,
|
|
CHECK (length(trim(origin_device_id)) > 0),
|
|
CHECK (length(trim(name)) > 0),
|
|
CHECK (has_time_measure OR has_reps_measure OR has_score_measure),
|
|
CHECK (score_input_mode IN ('manual', 'stopwatch')),
|
|
CHECK (has_score_measure OR score_input_mode = 'manual'),
|
|
CHECK (score_input_mode != 'manual' OR NOT has_score_measure OR (score_label IS NOT NULL AND length(trim(score_label)) > 0 AND score_unit IS NOT NULL AND length(trim(score_unit)) > 0)),
|
|
CHECK (default_target_time_seconds IS NULL OR default_target_time_seconds > 0),
|
|
CHECK (default_target_reps IS NULL OR default_target_reps > 0),
|
|
CHECK (default_target_score IS NULL OR default_target_score >= 0),
|
|
CHECK (default_target_score_time_ms IS NULL OR default_target_score_time_ms > 0),
|
|
CHECK (future_owner_profile_id IS NULL OR length(trim(future_owner_profile_id)) > 0),
|
|
CHECK (remote_revision IS NULL OR length(trim(remote_revision)) > 0)
|
|
)
|
|
''');
|
|
await customStatement('''
|
|
INSERT INTO exercises_new (
|
|
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,
|
|
name,
|
|
description,
|
|
icon_media_id,
|
|
video_media_id,
|
|
has_time_measure,
|
|
has_reps_measure,
|
|
has_score_measure,
|
|
score_input_mode,
|
|
score_label,
|
|
score_unit,
|
|
default_target_time_seconds,
|
|
default_target_reps,
|
|
default_target_score,
|
|
default_target_score_time_ms,
|
|
archived_at
|
|
)
|
|
SELECT
|
|
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,
|
|
name,
|
|
description,
|
|
icon_media_id,
|
|
video_media_id,
|
|
has_time_measure,
|
|
has_reps_measure,
|
|
has_score_measure,
|
|
score_input_mode,
|
|
score_label,
|
|
score_unit,
|
|
default_target_time_seconds,
|
|
default_target_reps,
|
|
default_target_score,
|
|
default_target_score_time_ms,
|
|
archived_at
|
|
FROM exercises
|
|
''');
|
|
await customStatement('DROP TABLE exercises');
|
|
await customStatement('ALTER TABLE exercises_new RENAME TO exercises');
|
|
await customStatement('PRAGMA foreign_keys = ON');
|
|
}
|
|
|
|
Future<void> _migrateToSchema14() async {
|
|
await customStatement(
|
|
'ALTER TABLE exercises ADD COLUMN auto_start_next_timed_step '
|
|
'INTEGER NOT NULL DEFAULT 1 CHECK (auto_start_next_timed_step IN (0, 1))',
|
|
);
|
|
await customStatement(
|
|
'ALTER TABLE program_exercises ADD COLUMN '
|
|
'auto_start_next_timed_step_snapshot INTEGER NOT NULL DEFAULT 1 '
|
|
'CHECK (auto_start_next_timed_step_snapshot IN (0, 1))',
|
|
);
|
|
await customStatement(
|
|
'ALTER TABLE program_exercises ADD COLUMN '
|
|
'auto_start_next_timed_step_override INTEGER '
|
|
'CHECK (auto_start_next_timed_step_override IN (0, 1))',
|
|
);
|
|
await customStatement(
|
|
'ALTER TABLE workout_template_exercise_overrides ADD COLUMN '
|
|
'auto_start_next_timed_step_override INTEGER '
|
|
'CHECK (auto_start_next_timed_step_override IN (0, 1))',
|
|
);
|
|
}
|
|
|
|
Future<void> _migrateToSchema15() async {
|
|
await customStatement('PRAGMA foreign_keys = OFF');
|
|
await customStatement('''
|
|
CREATE TABLE active_score_stopwatch_states_new (
|
|
id TEXT NOT NULL PRIMARY KEY,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL,
|
|
deleted_at INTEGER,
|
|
schema_version INTEGER NOT NULL DEFAULT 1,
|
|
sync_state TEXT NOT NULL CHECK (sync_state IN ('localOnly', 'dirty', 'synced', 'deleted')),
|
|
local_revision INTEGER NOT NULL CHECK (local_revision >= 0),
|
|
origin_device_id TEXT NOT NULL,
|
|
future_owner_profile_id TEXT,
|
|
last_synced_at INTEGER,
|
|
remote_revision TEXT,
|
|
active_workout_session_id TEXT NOT NULL REFERENCES active_workout_sessions(id),
|
|
program_index INTEGER NOT NULL,
|
|
exercise_index INTEGER NOT NULL,
|
|
set_index INTEGER NOT NULL,
|
|
status TEXT NOT NULL,
|
|
started_at INTEGER NOT NULL,
|
|
accumulated_ms INTEGER NOT NULL,
|
|
stopped_at INTEGER,
|
|
UNIQUE (active_workout_session_id, program_index, exercise_index, set_index),
|
|
CHECK (length(trim(origin_device_id)) > 0),
|
|
CHECK (future_owner_profile_id IS NULL OR length(trim(future_owner_profile_id)) > 0),
|
|
CHECK (remote_revision IS NULL OR length(trim(remote_revision)) > 0),
|
|
CHECK (program_index >= 0),
|
|
CHECK (exercise_index >= 0),
|
|
CHECK (set_index >= 0),
|
|
CHECK (status IN ('running', 'paused', 'stopped')),
|
|
CHECK (accumulated_ms >= 0)
|
|
)
|
|
''');
|
|
await customStatement('''
|
|
INSERT INTO active_score_stopwatch_states_new (
|
|
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,
|
|
active_workout_session_id,
|
|
program_index,
|
|
exercise_index,
|
|
set_index,
|
|
status,
|
|
started_at,
|
|
accumulated_ms,
|
|
stopped_at
|
|
)
|
|
SELECT
|
|
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,
|
|
active_workout_session_id,
|
|
program_index,
|
|
exercise_index,
|
|
set_index,
|
|
status,
|
|
started_at,
|
|
accumulated_ms,
|
|
stopped_at
|
|
FROM active_score_stopwatch_states
|
|
''');
|
|
await customStatement('DROP TABLE active_score_stopwatch_states');
|
|
await customStatement(
|
|
'ALTER TABLE active_score_stopwatch_states_new '
|
|
'RENAME TO active_score_stopwatch_states',
|
|
);
|
|
await customStatement('PRAGMA foreign_keys = ON');
|
|
await customStatement('''
|
|
CREATE TABLE IF NOT EXISTS active_set_timer_states (
|
|
id TEXT NOT NULL PRIMARY KEY,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL,
|
|
deleted_at INTEGER,
|
|
schema_version INTEGER NOT NULL DEFAULT 1,
|
|
sync_state TEXT NOT NULL CHECK (sync_state IN ('localOnly', 'dirty', 'synced', 'deleted')),
|
|
local_revision INTEGER NOT NULL CHECK (local_revision >= 0),
|
|
origin_device_id TEXT NOT NULL,
|
|
future_owner_profile_id TEXT,
|
|
last_synced_at INTEGER,
|
|
remote_revision TEXT,
|
|
active_workout_session_id TEXT NOT NULL REFERENCES active_workout_sessions(id),
|
|
program_index INTEGER NOT NULL,
|
|
exercise_index INTEGER NOT NULL,
|
|
set_index INTEGER NOT NULL,
|
|
status TEXT NOT NULL,
|
|
started_at INTEGER,
|
|
accumulated_ms INTEGER NOT NULL,
|
|
stopped_at INTEGER,
|
|
skipped_at INTEGER,
|
|
UNIQUE (active_workout_session_id, program_index, exercise_index, set_index),
|
|
CHECK (length(trim(origin_device_id)) > 0),
|
|
CHECK (future_owner_profile_id IS NULL OR length(trim(future_owner_profile_id)) > 0),
|
|
CHECK (remote_revision IS NULL OR length(trim(remote_revision)) > 0),
|
|
CHECK (program_index >= 0),
|
|
CHECK (exercise_index >= 0),
|
|
CHECK (set_index >= 0),
|
|
CHECK (status IN ('running', 'paused', 'stopped', 'skipped')),
|
|
CHECK (status != 'running' OR started_at IS NOT NULL),
|
|
CHECK (accumulated_ms >= 0),
|
|
CHECK (stopped_at IS NULL OR skipped_at IS NULL)
|
|
)
|
|
''');
|
|
if (!await _hasColumn('active_rest_states', 'paused_at')) {
|
|
await customStatement(
|
|
'ALTER TABLE active_rest_states ADD COLUMN paused_at INTEGER',
|
|
);
|
|
}
|
|
if (!await _hasColumn('active_rest_states', 'accumulated_paused_ms')) {
|
|
await customStatement(
|
|
'ALTER TABLE active_rest_states ADD COLUMN accumulated_paused_ms '
|
|
'INTEGER NOT NULL DEFAULT 0 CHECK (accumulated_paused_ms >= 0)',
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> _migrateToSchema16(Migrator migrator) async {
|
|
await _addColumnIfMissing(
|
|
tableName: 'exercises',
|
|
columnName: 'category',
|
|
definition:
|
|
"category TEXT NOT NULL DEFAULT 'uncategorized' CHECK "
|
|
"(category IN ('shoot', 'freeThrows', 'dribble', 'finishing', "
|
|
"'conditioning', 'defense', 'mobility', 'uncategorized'))",
|
|
);
|
|
await _addColumnIfMissing(
|
|
tableName: 'exercises',
|
|
columnName: 'is_example',
|
|
definition:
|
|
'is_example INTEGER NOT NULL DEFAULT 0 '
|
|
'CHECK (is_example IN (0, 1))',
|
|
);
|
|
await _addColumnIfMissing(
|
|
tableName: 'programs',
|
|
columnName: 'is_example',
|
|
definition:
|
|
'is_example INTEGER NOT NULL DEFAULT 0 '
|
|
'CHECK (is_example IN (0, 1))',
|
|
);
|
|
await _addColumnIfMissing(
|
|
tableName: 'workout_templates',
|
|
columnName: 'is_example',
|
|
definition:
|
|
'is_example INTEGER NOT NULL DEFAULT 0 '
|
|
'CHECK (is_example IN (0, 1))',
|
|
);
|
|
await customStatement(
|
|
'CREATE TABLE IF NOT EXISTS local_seed_metadata ('
|
|
'key TEXT NOT NULL PRIMARY KEY, '
|
|
'version INTEGER NOT NULL CHECK (version >= 0), '
|
|
'applied_at INTEGER NOT NULL'
|
|
')',
|
|
);
|
|
}
|
|
|
|
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> _migrateToSchema19() async {
|
|
await _addColumnIfMissing(
|
|
tableName: 'exercises',
|
|
columnName: 'tags_json',
|
|
definition:
|
|
"tags_json TEXT NOT NULL DEFAULT '[]' CHECK (json_valid(tags_json))",
|
|
);
|
|
await _addColumnIfMissing(
|
|
tableName: 'programs',
|
|
columnName: 'tags_json',
|
|
definition:
|
|
"tags_json TEXT NOT NULL DEFAULT '[]' CHECK (json_valid(tags_json))",
|
|
);
|
|
await _addColumnIfMissing(
|
|
tableName: 'workout_templates',
|
|
columnName: 'tags_json',
|
|
definition:
|
|
"tags_json TEXT NOT NULL DEFAULT '[]' CHECK (json_valid(tags_json))",
|
|
);
|
|
}
|
|
|
|
Future<void> _migrateToSchema20(Migrator migrator) async {
|
|
await migrator.createTable(activeManualScoreStates);
|
|
}
|
|
|
|
Future<void> _migrateToSchema21() async {
|
|
await _addColumnIfMissing(
|
|
tableName: 'workout_history',
|
|
columnName: 'average_heart_rate_bpm',
|
|
definition:
|
|
'average_heart_rate_bpm REAL CHECK '
|
|
'(average_heart_rate_bpm IS NULL OR average_heart_rate_bpm > 0)',
|
|
);
|
|
await _addColumnIfMissing(
|
|
tableName: 'workout_history',
|
|
columnName: 'max_heart_rate_bpm',
|
|
definition:
|
|
'max_heart_rate_bpm INTEGER CHECK '
|
|
'(max_heart_rate_bpm IS NULL OR max_heart_rate_bpm > 0)',
|
|
);
|
|
}
|
|
|
|
Future<void> _migrateToSchema22() async {
|
|
await _addColumnIfMissing(
|
|
tableName: 'exercise_steps',
|
|
columnName: 'linked_to_series_score',
|
|
definition:
|
|
'linked_to_series_score INTEGER NOT NULL DEFAULT 0 '
|
|
'CHECK (linked_to_series_score IN (0, 1))',
|
|
);
|
|
}
|
|
|
|
Future<void> _migrateToSchema23() async {
|
|
await customStatement('''
|
|
CREATE TABLE IF NOT EXISTS share_inbox_items_v23 (
|
|
share_id TEXT NOT NULL PRIMARY KEY,
|
|
sender_user_id TEXT NOT NULL,
|
|
resource_type TEXT NOT NULL CHECK (
|
|
resource_type IN ('program', 'workoutTemplate', 'pack')
|
|
),
|
|
payload_json TEXT NOT NULL,
|
|
status TEXT NOT NULL CHECK (
|
|
status IN ('pending', 'accepted', 'declined', 'revoked')
|
|
),
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL,
|
|
responded_at INTEGER
|
|
)
|
|
''');
|
|
await customStatement('''
|
|
INSERT INTO share_inbox_items_v23 (
|
|
share_id,
|
|
sender_user_id,
|
|
resource_type,
|
|
payload_json,
|
|
status,
|
|
created_at,
|
|
updated_at,
|
|
responded_at
|
|
)
|
|
SELECT
|
|
share_id,
|
|
sender_user_id,
|
|
resource_type,
|
|
payload_json,
|
|
status,
|
|
created_at,
|
|
updated_at,
|
|
responded_at
|
|
FROM share_inbox_items
|
|
''');
|
|
await customStatement('DROP TABLE share_inbox_items');
|
|
await customStatement(
|
|
'ALTER TABLE share_inbox_items_v23 RENAME TO share_inbox_items',
|
|
);
|
|
|
|
await customStatement('''
|
|
CREATE TABLE IF NOT EXISTS pending_share_actions_v23 (
|
|
id TEXT NOT NULL PRIMARY KEY,
|
|
action_type TEXT NOT NULL CHECK (
|
|
action_type IN ('send', 'accept', 'decline', 'revoke')
|
|
),
|
|
share_id TEXT,
|
|
resource_type TEXT CHECK (
|
|
resource_type IS NULL OR
|
|
resource_type IN ('program', 'workoutTemplate', 'pack')
|
|
),
|
|
payload_json TEXT,
|
|
recipient_emails_json TEXT,
|
|
created_at INTEGER NOT NULL,
|
|
last_attempt_at INTEGER,
|
|
attempt_count INTEGER NOT NULL CHECK (attempt_count >= 0),
|
|
status TEXT NOT NULL CHECK (status IN ('pending', 'succeeded', 'failed'))
|
|
)
|
|
''');
|
|
await customStatement('''
|
|
INSERT INTO pending_share_actions_v23 (
|
|
id,
|
|
action_type,
|
|
share_id,
|
|
resource_type,
|
|
payload_json,
|
|
recipient_emails_json,
|
|
created_at,
|
|
last_attempt_at,
|
|
attempt_count,
|
|
status
|
|
)
|
|
SELECT
|
|
id,
|
|
action_type,
|
|
share_id,
|
|
resource_type,
|
|
payload_json,
|
|
recipient_emails_json,
|
|
created_at,
|
|
last_attempt_at,
|
|
attempt_count,
|
|
status
|
|
FROM pending_share_actions
|
|
''');
|
|
await customStatement('DROP TABLE pending_share_actions');
|
|
await customStatement(
|
|
'ALTER TABLE pending_share_actions_v23 RENAME TO pending_share_actions',
|
|
);
|
|
}
|
|
|
|
Future<void> _migrateToSchema24(Migrator migrator) async {
|
|
await _addColumnIfMissing(
|
|
tableName: 'workout_history',
|
|
columnName: 'min_heart_rate_bpm',
|
|
definition:
|
|
'min_heart_rate_bpm INTEGER CHECK '
|
|
'(min_heart_rate_bpm IS NULL OR min_heart_rate_bpm > 0)',
|
|
);
|
|
await _addColumnIfMissing(
|
|
tableName: 'workout_history',
|
|
columnName: 'total_distance_meters',
|
|
definition:
|
|
'total_distance_meters REAL CHECK '
|
|
'(total_distance_meters IS NULL OR total_distance_meters >= 0)',
|
|
);
|
|
await _addColumnIfMissing(
|
|
tableName: 'workout_history',
|
|
columnName: 'total_calories_kcal',
|
|
definition:
|
|
'total_calories_kcal REAL CHECK '
|
|
'(total_calories_kcal IS NULL OR total_calories_kcal >= 0)',
|
|
);
|
|
await migrator.createTable(workoutTelemetrySamples);
|
|
await migrator.createTable(workoutTelemetryAggregates);
|
|
}
|
|
|
|
Future<void> _migrateToSchema25() async {
|
|
await _addColumnIfMissing(
|
|
tableName: 'exercises',
|
|
columnName: 'business_types_json',
|
|
definition:
|
|
"business_types_json TEXT NOT NULL DEFAULT '[]' "
|
|
'CHECK (json_valid(business_types_json))',
|
|
);
|
|
await _addColumnIfMissing(
|
|
tableName: 'program_exercises',
|
|
columnName: 'health_services_exercise_type_strategy_snapshot_json',
|
|
definition:
|
|
'health_services_exercise_type_strategy_snapshot_json TEXT NOT NULL '
|
|
"DEFAULT '[\"RUNNING\",\"WALKING\","
|
|
"\"HIGH_INTENSITY_INTERVAL_TRAINING\",\"WORKOUT\"]' "
|
|
'CHECK (json_valid('
|
|
'health_services_exercise_type_strategy_snapshot_json))',
|
|
);
|
|
}
|
|
|
|
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,
|
|
required String definition,
|
|
}) async {
|
|
final columns = await customSelect('PRAGMA table_info($tableName)').get();
|
|
final exists = columns.any((row) => row.data['name'] == columnName);
|
|
if (!exists) {
|
|
await customStatement('ALTER TABLE $tableName ADD COLUMN $definition');
|
|
}
|
|
}
|
|
|
|
Future<bool> _hasColumn(String tableName, String columnName) async {
|
|
final rows = await customSelect('PRAGMA table_info($tableName)').get();
|
|
return rows.any((row) => row.data['name'] == columnName);
|
|
}
|
|
|
|
Future<bool> _hasTable(String tableName) async {
|
|
final rows = await customSelect(
|
|
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?",
|
|
variables: [Variable<String>(tableName)],
|
|
).get();
|
|
return rows.isNotEmpty;
|
|
}
|
|
}
|