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, ActiveScoreStopwatchStates, ActiveSetResults, ActiveWorkoutSessions, ChangeLogEntries, Exercises, ExerciseImages, ExerciseSteps, MediaAssets, OnlineAccountSessions, PendingShareActions, ProgramExercises, Programs, RemoteResourceMappings, ShareInboxItems, SyncMetadataEntries, 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 => 12; @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); } if (from < 9) { await _migrateToSchema9(migrator); } if (from < 10) { await _migrateToSchema10(migrator); } if (from < 11) { await _migrateToSchema11(migrator); } if (from < 12) { await _migrateToSchema12(migrator); } await _createIndexes(); }, beforeOpen: (details) async { await customStatement('PRAGMA foreign_keys = ON'); }, ); } Future _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_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_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 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_set_results_history_id ' 'ON workout_history_set_results (workout_history_id)', ); 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_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_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_history_step_results', 'workout_template_exercise_overrides', 'workout_template_programs', 'workout_templates', ]; extension on AppDatabase { Future _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 _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 _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 _migrateToSchema6() async { await customStatement( 'ALTER TABLE program_exercises ADD COLUMN ' 'exercise_image_media_ids_snapshot_json TEXT', ); } Future _migrateToSchema7() async { await customStatement( 'ALTER TABLE exercises ADD COLUMN icon_media_id TEXT ' 'REFERENCES media_assets(id)', ); } Future _migrateToSchema8(Migrator migrator) async { await migrator.createTable(exerciseSteps); await customStatement( 'ALTER TABLE program_exercises ADD COLUMN ' 'exercise_steps_snapshot_json TEXT', ); } Future _migrateToSchema9(Migrator migrator) async { await migrator.createTable(activeExerciseStepProgressStates); await migrator.createTable(activeExerciseStepResults); await migrator.createTable(workoutHistoryStepResults); } Future _migrateToSchema10(Migrator migrator) async { await migrator.createTable(onlineAccountSessions); } Future _migrateToSchema11(Migrator migrator) async { await migrator.createTable(syncMetadataEntries); await migrator.createTable(remoteResourceMappings); } Future _migrateToSchema12(Migrator migrator) async { await migrator.createTable(shareInboxItems); await migrator.createTable(pendingShareActions); } }