fix(exercice): sélection d'un score par défaut à 0 impossible (ticket #71)
Corrige la contrainte CHECK sur la table exercises (tables.dart) qui empêchait à tort une valeur de score par défaut à 0, et ajoute la migration Drift schemaVersion 12→13 reconstruisant la table avec la contrainte corrigée (app_database.dart). build_runner OK, test de régression ajouté. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -46,7 +46,7 @@ final class AppDatabase extends _$AppDatabase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 12;
|
int get schemaVersion => 13;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration {
|
MigrationStrategy get migration {
|
||||||
@ -97,6 +97,9 @@ final class AppDatabase extends _$AppDatabase {
|
|||||||
if (from < 12) {
|
if (from < 12) {
|
||||||
await _migrateToSchema12(migrator);
|
await _migrateToSchema12(migrator);
|
||||||
}
|
}
|
||||||
|
if (from < 13) {
|
||||||
|
await _migrateToSchema13();
|
||||||
|
}
|
||||||
await _createIndexes();
|
await _createIndexes();
|
||||||
},
|
},
|
||||||
beforeOpen: (details) async {
|
beforeOpen: (details) async {
|
||||||
@ -359,4 +362,111 @@ extension on AppDatabase {
|
|||||||
await migrator.createTable(shareInboxItems);
|
await migrator.createTable(shareInboxItems);
|
||||||
await migrator.createTable(pendingShareActions);
|
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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -204,7 +204,7 @@ class Exercises extends SyncableTable {
|
|||||||
'CHECK (default_target_time_seconds IS NULL OR '
|
'CHECK (default_target_time_seconds IS NULL OR '
|
||||||
'default_target_time_seconds > 0)',
|
'default_target_time_seconds > 0)',
|
||||||
'CHECK (default_target_reps IS NULL OR default_target_reps > 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 IS NULL OR default_target_score >= 0)',
|
||||||
'CHECK (default_target_score_time_ms IS NULL OR '
|
'CHECK (default_target_score_time_ms IS NULL OR '
|
||||||
'default_target_score_time_ms > 0)',
|
'default_target_score_time_ms > 0)',
|
||||||
];
|
];
|
||||||
|
|||||||
@ -79,6 +79,25 @@ void main() {
|
|||||||
expect(changes.every((change) => change.entityType == 'Exercise'), isTrue);
|
expect(changes.every((change) => change.entityType == 'Exercise'), isTrue);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('exercise repository accepts zero manual default score', () async {
|
||||||
|
final exercise = Exercise(
|
||||||
|
metadata: _metadata('exercise-score-zero', DateTime.utc(2026, 7, 17, 12)),
|
||||||
|
name: 'Score nul',
|
||||||
|
hasTimeMeasure: false,
|
||||||
|
hasRepsMeasure: false,
|
||||||
|
hasScoreMeasure: true,
|
||||||
|
scoreLabel: 'Score',
|
||||||
|
scoreUnit: 'pts',
|
||||||
|
defaultTargetScore: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
await exerciseRepository.save(exercise);
|
||||||
|
|
||||||
|
final restored = await exerciseRepository.findById(exercise.metadata.id);
|
||||||
|
expect(restored, isNotNull);
|
||||||
|
expect(restored!.defaultTargetScore, 0);
|
||||||
|
});
|
||||||
|
|
||||||
test('exercise repository round-trips configured steps', () async {
|
test('exercise repository round-trips configured steps', () async {
|
||||||
final now = DateTime.utc(2026, 7, 17, 12);
|
final now = DateTime.utc(2026, 7, 17, 12);
|
||||||
final exercise = Exercise(
|
final exercise = Exercise(
|
||||||
|
|||||||
Reference in New Issue
Block a user