feat(server): implemente synchronisation et serveur avec fixtures (#187)
- Implémente la couche de synchronisation avec le serveur - Ajoute les fixtures versionnées pour les tests - Met à jour Drift database et repositories pour le support sync - Améliore les tests de synchronisation - Corrige et améliore le watch companion pour la collecte de métriques Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -9,6 +9,7 @@ part 'app_database.g.dart';
|
||||
tables: [
|
||||
ActiveExerciseStepProgressStates,
|
||||
ActiveExerciseStepResults,
|
||||
ActiveWorkoutTelemetryWindowStates,
|
||||
ActiveRestStates,
|
||||
ActiveManualScoreStates,
|
||||
ActiveScoreStopwatchStates,
|
||||
@ -51,7 +52,7 @@ final class AppDatabase extends _$AppDatabase {
|
||||
}
|
||||
|
||||
@override
|
||||
int get schemaVersion => 25;
|
||||
int get schemaVersion => 26;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration {
|
||||
@ -141,6 +142,9 @@ final class AppDatabase extends _$AppDatabase {
|
||||
if (from < 25) {
|
||||
await _migrateToSchema25();
|
||||
}
|
||||
if (from < 26) {
|
||||
await _migrateToSchema26(migrator);
|
||||
}
|
||||
await _createIndexes();
|
||||
},
|
||||
beforeOpen: (details) async {
|
||||
@ -269,6 +273,11 @@ final class AppDatabase extends _$AppDatabase {
|
||||
'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_active_workout_telemetry_window_states_session '
|
||||
'ON active_workout_telemetry_window_states (session_id)',
|
||||
);
|
||||
await customStatement(
|
||||
'CREATE INDEX IF NOT EXISTS idx_workout_telemetry_aggregates_session '
|
||||
'ON workout_telemetry_aggregates (session_id, scope)',
|
||||
@ -472,6 +481,10 @@ extension on AppDatabase {
|
||||
await migrator.createTable(pendingShareActions);
|
||||
}
|
||||
|
||||
Future<void> _migrateToSchema26(Migrator migrator) async {
|
||||
await migrator.createTable(activeWorkoutTelemetryWindowStates);
|
||||
}
|
||||
|
||||
Future<void> _migrateToSchema13() async {
|
||||
await customStatement('PRAGMA foreign_keys = OFF');
|
||||
await customStatement('''
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -671,6 +671,7 @@ final class DriftLocalSyncChangeRepository
|
||||
.into(database.workoutHistoryStepResults)
|
||||
.insertOnConflictUpdate(_workoutHistoryStepResultCompanion(result));
|
||||
}
|
||||
await _replaceRemoteWorkoutTelemetry(history);
|
||||
|
||||
final activeResultIds = history.results
|
||||
.map((result) => result.metadata.id)
|
||||
@ -693,6 +694,44 @@ final class DriftLocalSyncChangeRepository
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _replaceRemoteWorkoutTelemetry(
|
||||
domain.WorkoutHistory history,
|
||||
) async {
|
||||
final samples = _workoutTelemetrySamplesFromHistoryPayload(history);
|
||||
if (samples.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final sessionIds = samples.map((sample) => sample.sessionId).toSet();
|
||||
for (final sessionId in sessionIds) {
|
||||
await (database.delete(
|
||||
database.workoutTelemetrySamples,
|
||||
)..where((table) => table.sessionId.equals(sessionId))).go();
|
||||
await (database.delete(
|
||||
database.workoutTelemetryAggregates,
|
||||
)..where((table) => table.sessionId.equals(sessionId))).go();
|
||||
}
|
||||
await database.batch((batch) {
|
||||
batch.insertAll(
|
||||
database.workoutTelemetrySamples,
|
||||
samples.map(_workoutTelemetrySampleCompanion).toList(),
|
||||
);
|
||||
});
|
||||
for (final sessionId in sessionIds) {
|
||||
final sessionSamples = samples
|
||||
.where((sample) => sample.sessionId == sessionId)
|
||||
.toList(growable: false);
|
||||
final aggregates = _workoutTelemetryAggregatesFromSamples(sessionSamples);
|
||||
if (aggregates.isNotEmpty) {
|
||||
await database.batch((batch) {
|
||||
batch.insertAll(
|
||||
database.workoutTelemetryAggregates,
|
||||
aggregates.map(_workoutTelemetryAggregateCompanion).toList(),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _softDeleteRemoteWorkoutHistoryChildren({
|
||||
required String tableName,
|
||||
required String historyId,
|
||||
@ -2150,6 +2189,34 @@ final class DriftWorkoutTelemetryRepository
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<domain.ActiveWorkoutTelemetryWindowState?> findWindowState(
|
||||
String sessionId,
|
||||
) async {
|
||||
final row = await (database.select(
|
||||
database.activeWorkoutTelemetryWindowStates,
|
||||
)..where((table) => table.sessionId.equals(sessionId))).getSingleOrNull();
|
||||
return row == null ? null : _activeWorkoutTelemetryWindowStateFromRow(row);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> saveWindowState(
|
||||
domain.ActiveWorkoutTelemetryWindowState state,
|
||||
) async {
|
||||
await database
|
||||
.into(database.activeWorkoutTelemetryWindowStates)
|
||||
.insertOnConflictUpdate(
|
||||
_activeWorkoutTelemetryWindowStateCompanion(state),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteWindowState(String sessionId) async {
|
||||
await (database.delete(
|
||||
database.activeWorkoutTelemetryWindowStates,
|
||||
)..where((table) => table.sessionId.equals(sessionId))).go();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<domain.WorkoutTelemetrySample>> listSamples(
|
||||
String sessionId,
|
||||
@ -5323,6 +5390,28 @@ db.WorkoutTelemetrySamplesCompanion _workoutTelemetrySampleCompanion(
|
||||
);
|
||||
}
|
||||
|
||||
db.ActiveWorkoutTelemetryWindowStatesCompanion
|
||||
_activeWorkoutTelemetryWindowStateCompanion(
|
||||
domain.ActiveWorkoutTelemetryWindowState state,
|
||||
) {
|
||||
return db.ActiveWorkoutTelemetryWindowStatesCompanion.insert(
|
||||
sessionId: state.sessionId,
|
||||
windowStartedActiveMs: state.windowStartedActiveMs,
|
||||
latestCapturedAt: state.latestCapturedAt.toUtc(),
|
||||
programIndex: Value(state.programIndex),
|
||||
exerciseIndex: Value(state.exerciseIndex),
|
||||
setIndex: Value(state.setIndex),
|
||||
passageIndex: Value(state.passageIndex),
|
||||
stepIndex: Value(state.stepIndex),
|
||||
programSnapshotId: Value(state.programSnapshotId),
|
||||
exerciseSnapshotId: Value(state.exerciseSnapshotId),
|
||||
stepSnapshotId: Value(state.stepSnapshotId),
|
||||
heartRateBpm: Value(state.heartRateBpm),
|
||||
distanceMeters: Value(state.distanceMeters),
|
||||
caloriesKcal: Value(state.caloriesKcal),
|
||||
);
|
||||
}
|
||||
|
||||
db.WorkoutTelemetryAggregatesCompanion _workoutTelemetryAggregateCompanion(
|
||||
domain.WorkoutTelemetryAggregate aggregate,
|
||||
) {
|
||||
@ -5486,6 +5575,28 @@ domain.WorkoutTelemetrySample _workoutTelemetrySampleFromRow(
|
||||
);
|
||||
}
|
||||
|
||||
domain.ActiveWorkoutTelemetryWindowState
|
||||
_activeWorkoutTelemetryWindowStateFromRow(
|
||||
db.ActiveWorkoutTelemetryWindowState row,
|
||||
) {
|
||||
return domain.ActiveWorkoutTelemetryWindowState(
|
||||
sessionId: row.sessionId,
|
||||
windowStartedActiveMs: row.windowStartedActiveMs,
|
||||
latestCapturedAt: _utc(row.latestCapturedAt),
|
||||
programIndex: row.programIndex,
|
||||
exerciseIndex: row.exerciseIndex,
|
||||
setIndex: row.setIndex,
|
||||
passageIndex: row.passageIndex,
|
||||
stepIndex: row.stepIndex,
|
||||
programSnapshotId: row.programSnapshotId,
|
||||
exerciseSnapshotId: row.exerciseSnapshotId,
|
||||
stepSnapshotId: row.stepSnapshotId,
|
||||
heartRateBpm: row.heartRateBpm,
|
||||
distanceMeters: row.distanceMeters,
|
||||
caloriesKcal: row.caloriesKcal,
|
||||
);
|
||||
}
|
||||
|
||||
bool _telemetrySampleMatchesScope(
|
||||
domain.WorkoutTelemetrySample sample, {
|
||||
required domain.WorkoutTelemetryAggregateScope scope,
|
||||
@ -5806,6 +5917,9 @@ Map<String, Object?> _localWorkoutHistoryPayload(
|
||||
'stepResults': history.stepResults
|
||||
.map(_workoutHistoryStepResultPayload)
|
||||
.toList(),
|
||||
'telemetrySamples': _workoutTelemetrySamplesFromHistoryPayload(
|
||||
history,
|
||||
).map(_workoutTelemetrySamplePayload).toList(),
|
||||
};
|
||||
|
||||
Map<String, Object?> _workoutHistorySetResultPayload(
|
||||
@ -5875,6 +5989,248 @@ Map<String, Object?> _workoutHistoryStepResultPayload(
|
||||
'sourceExerciseIdSnapshot': result.sourceExerciseIdSnapshot,
|
||||
};
|
||||
|
||||
Map<String, Object?> _workoutTelemetrySamplePayload(
|
||||
domain.WorkoutTelemetrySample sample,
|
||||
) => {
|
||||
'id': sample.id,
|
||||
'sessionId': sample.sessionId,
|
||||
'capturedAt': sample.capturedAt.toUtc().toIso8601String(),
|
||||
'programIndex': sample.programIndex,
|
||||
'exerciseIndex': sample.exerciseIndex,
|
||||
'setIndex': sample.setIndex,
|
||||
'passageIndex': sample.passageIndex,
|
||||
'stepIndex': sample.stepIndex,
|
||||
'programSnapshotId': sample.programSnapshotId,
|
||||
'exerciseSnapshotId': sample.exerciseSnapshotId,
|
||||
'stepSnapshotId': sample.stepSnapshotId,
|
||||
'heartRateBpm': sample.heartRateBpm,
|
||||
'distanceMeters': sample.distanceMeters,
|
||||
'caloriesKcal': sample.caloriesKcal,
|
||||
};
|
||||
|
||||
List<domain.WorkoutTelemetrySample> _workoutTelemetrySamplesFromHistoryPayload(
|
||||
domain.WorkoutHistory history,
|
||||
) {
|
||||
final decoded = jsonDecode(history.historySnapshotJson);
|
||||
final snapshot = decoded is Map
|
||||
? Map<String, Object?>.from(decoded)
|
||||
: const <String, Object?>{};
|
||||
final rawSamples = snapshot['telemetrySamples'];
|
||||
if (rawSamples is! List) {
|
||||
return const [];
|
||||
}
|
||||
return _workoutTelemetrySamplesFromPayload(rawSamples);
|
||||
}
|
||||
|
||||
List<domain.WorkoutTelemetrySample> _workoutTelemetrySamplesFromPayload(
|
||||
Object? value,
|
||||
) {
|
||||
if (value is! List) {
|
||||
return const [];
|
||||
}
|
||||
final output = <domain.WorkoutTelemetrySample>[];
|
||||
for (final rawSample in value) {
|
||||
if (rawSample is! Map) {
|
||||
continue;
|
||||
}
|
||||
final map = Map<String, Object?>.from(rawSample);
|
||||
final id = map['id'] as String?;
|
||||
final sessionId = map['sessionId'] as String?;
|
||||
final capturedAt = _dateTimeFromPayload(map['capturedAt']);
|
||||
if (id == null || sessionId == null || capturedAt == null) {
|
||||
continue;
|
||||
}
|
||||
output.add(
|
||||
domain.WorkoutTelemetrySample(
|
||||
id: id,
|
||||
sessionId: sessionId,
|
||||
capturedAt: capturedAt,
|
||||
programIndex: map['programIndex'] as int?,
|
||||
exerciseIndex: map['exerciseIndex'] as int?,
|
||||
setIndex: map['setIndex'] as int?,
|
||||
passageIndex: map['passageIndex'] as int?,
|
||||
stepIndex: map['stepIndex'] as int?,
|
||||
programSnapshotId: map['programSnapshotId'] as String?,
|
||||
exerciseSnapshotId: map['exerciseSnapshotId'] as String?,
|
||||
stepSnapshotId: map['stepSnapshotId'] as String?,
|
||||
heartRateBpm: map['heartRateBpm'] as int?,
|
||||
distanceMeters: (map['distanceMeters'] as num?)?.toDouble(),
|
||||
caloriesKcal: (map['caloriesKcal'] as num?)?.toDouble(),
|
||||
),
|
||||
);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
List<domain.WorkoutTelemetryAggregate> _workoutTelemetryAggregatesFromSamples(
|
||||
List<domain.WorkoutTelemetrySample> samples,
|
||||
) {
|
||||
final builders = <_TelemetryAggregateKey, _TelemetryAggregateBuilder>{};
|
||||
for (final sample in samples) {
|
||||
for (final key in _telemetryAggregateKeys(sample)) {
|
||||
builders
|
||||
.putIfAbsent(key, () => _TelemetryAggregateBuilder(key))
|
||||
.add(sample);
|
||||
}
|
||||
}
|
||||
return [for (final builder in builders.values) builder.build()];
|
||||
}
|
||||
|
||||
List<_TelemetryAggregateKey> _telemetryAggregateKeys(
|
||||
domain.WorkoutTelemetrySample sample,
|
||||
) {
|
||||
final keys = [
|
||||
_TelemetryAggregateKey(
|
||||
sessionId: sample.sessionId,
|
||||
scope: domain.WorkoutTelemetryAggregateScope.session,
|
||||
),
|
||||
];
|
||||
if (sample.programIndex != null && sample.exerciseIndex != null) {
|
||||
keys.add(
|
||||
_TelemetryAggregateKey(
|
||||
sessionId: sample.sessionId,
|
||||
scope: domain.WorkoutTelemetryAggregateScope.exercise,
|
||||
programIndex: sample.programIndex,
|
||||
exerciseIndex: sample.exerciseIndex,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (sample.programIndex != null &&
|
||||
sample.exerciseIndex != null &&
|
||||
sample.setIndex != null) {
|
||||
keys.add(
|
||||
_TelemetryAggregateKey(
|
||||
sessionId: sample.sessionId,
|
||||
scope: domain.WorkoutTelemetryAggregateScope.set,
|
||||
programIndex: sample.programIndex,
|
||||
exerciseIndex: sample.exerciseIndex,
|
||||
setIndex: sample.setIndex,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (sample.programIndex != null &&
|
||||
sample.exerciseIndex != null &&
|
||||
sample.setIndex != null &&
|
||||
sample.stepIndex != null) {
|
||||
keys.add(
|
||||
_TelemetryAggregateKey(
|
||||
sessionId: sample.sessionId,
|
||||
scope: domain.WorkoutTelemetryAggregateScope.step,
|
||||
programIndex: sample.programIndex,
|
||||
exerciseIndex: sample.exerciseIndex,
|
||||
setIndex: sample.setIndex,
|
||||
passageIndex: sample.passageIndex,
|
||||
stepIndex: sample.stepIndex,
|
||||
),
|
||||
);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
final class _TelemetryAggregateKey {
|
||||
const _TelemetryAggregateKey({
|
||||
required this.sessionId,
|
||||
required this.scope,
|
||||
this.programIndex,
|
||||
this.exerciseIndex,
|
||||
this.setIndex,
|
||||
this.passageIndex,
|
||||
this.stepIndex,
|
||||
});
|
||||
|
||||
final String sessionId;
|
||||
final domain.WorkoutTelemetryAggregateScope scope;
|
||||
final int? programIndex;
|
||||
final int? exerciseIndex;
|
||||
final int? setIndex;
|
||||
final int? passageIndex;
|
||||
final int? stepIndex;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
other is _TelemetryAggregateKey &&
|
||||
sessionId == other.sessionId &&
|
||||
scope == other.scope &&
|
||||
programIndex == other.programIndex &&
|
||||
exerciseIndex == other.exerciseIndex &&
|
||||
setIndex == other.setIndex &&
|
||||
passageIndex == other.passageIndex &&
|
||||
stepIndex == other.stepIndex;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
sessionId,
|
||||
scope,
|
||||
programIndex,
|
||||
exerciseIndex,
|
||||
setIndex,
|
||||
passageIndex,
|
||||
stepIndex,
|
||||
);
|
||||
}
|
||||
|
||||
final class _TelemetryAggregateBuilder {
|
||||
_TelemetryAggregateBuilder(this.key);
|
||||
|
||||
final _TelemetryAggregateKey key;
|
||||
var sampleCount = 0;
|
||||
var heartRateCount = 0;
|
||||
var heartRateSum = 0.0;
|
||||
int? minHeartRateBpm;
|
||||
int? maxHeartRateBpm;
|
||||
double? maxDistanceMeters;
|
||||
double? maxCaloriesKcal;
|
||||
|
||||
void add(domain.WorkoutTelemetrySample sample) {
|
||||
sampleCount += 1;
|
||||
final heartRate = sample.heartRateBpm;
|
||||
if (heartRate != null) {
|
||||
heartRateCount += 1;
|
||||
heartRateSum += heartRate;
|
||||
minHeartRateBpm = minHeartRateBpm == null
|
||||
? heartRate
|
||||
: (heartRate < minHeartRateBpm! ? heartRate : minHeartRateBpm);
|
||||
maxHeartRateBpm = maxHeartRateBpm == null
|
||||
? heartRate
|
||||
: (heartRate > maxHeartRateBpm! ? heartRate : maxHeartRateBpm);
|
||||
}
|
||||
final distance = sample.distanceMeters;
|
||||
if (distance != null) {
|
||||
maxDistanceMeters = maxDistanceMeters == null
|
||||
? distance
|
||||
: (distance > maxDistanceMeters! ? distance : maxDistanceMeters);
|
||||
}
|
||||
final calories = sample.caloriesKcal;
|
||||
if (calories != null) {
|
||||
maxCaloriesKcal = maxCaloriesKcal == null
|
||||
? calories
|
||||
: (calories > maxCaloriesKcal! ? calories : maxCaloriesKcal);
|
||||
}
|
||||
}
|
||||
|
||||
domain.WorkoutTelemetryAggregate build() {
|
||||
return domain.WorkoutTelemetryAggregate(
|
||||
sessionId: key.sessionId,
|
||||
scope: key.scope,
|
||||
programIndex: key.programIndex,
|
||||
exerciseIndex: key.exerciseIndex,
|
||||
setIndex: key.setIndex,
|
||||
passageIndex: key.passageIndex,
|
||||
stepIndex: key.stepIndex,
|
||||
sampleCount: sampleCount,
|
||||
minHeartRateBpm: minHeartRateBpm,
|
||||
averageHeartRateBpm: heartRateCount == 0
|
||||
? null
|
||||
: heartRateSum / heartRateCount,
|
||||
maxHeartRateBpm: maxHeartRateBpm,
|
||||
totalDistanceMeters: maxDistanceMeters,
|
||||
totalCaloriesKcal: maxCaloriesKcal,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LocalBackupResource _backupResource(
|
||||
domain.EntityMetadata metadata,
|
||||
Map<String, Object?> payload,
|
||||
@ -5937,6 +6293,7 @@ domain.WorkoutHistory _workoutHistoryFromLocalBackupPayload(
|
||||
) {
|
||||
final payload = item.payload;
|
||||
final metadata = _metadataFromPayload(item);
|
||||
final historySnapshotJson = _historySnapshotJsonWithTelemetryPayload(payload);
|
||||
return domain.WorkoutHistory(
|
||||
metadata: metadata,
|
||||
sourceWorkoutTemplateId: payload['sourceWorkoutTemplateId'] as String?,
|
||||
@ -5948,8 +6305,7 @@ domain.WorkoutHistory _workoutHistoryFromLocalBackupPayload(
|
||||
endedAt: _dateTimeFromPayload(payload['endedAt']) ?? item.clientUpdatedAt,
|
||||
totalActiveMs: payload['totalActiveMs'] as int? ?? 0,
|
||||
completed: payload['completed'] as bool? ?? false,
|
||||
historySnapshotJson:
|
||||
payload['historySnapshotJson'] as String? ?? '{"programs":[]}',
|
||||
historySnapshotJson: historySnapshotJson,
|
||||
minHeartRateBpm: payload['minHeartRateBpm'] as int?,
|
||||
averageHeartRateBpm: (payload['averageHeartRateBpm'] as num?)?.toDouble(),
|
||||
maxHeartRateBpm: payload['maxHeartRateBpm'] as int?,
|
||||
@ -5963,6 +6319,20 @@ domain.WorkoutHistory _workoutHistoryFromLocalBackupPayload(
|
||||
);
|
||||
}
|
||||
|
||||
String _historySnapshotJsonWithTelemetryPayload(Map<String, Object?> payload) {
|
||||
final rawSnapshot = payload['historySnapshotJson'] as String?;
|
||||
final rawSamples = payload['telemetrySamples'];
|
||||
if (rawSamples is! List) {
|
||||
return rawSnapshot ?? '{"programs":[]}';
|
||||
}
|
||||
final decoded = rawSnapshot == null ? null : jsonDecode(rawSnapshot);
|
||||
final snapshot = decoded is Map
|
||||
? Map<String, Object?>.from(decoded)
|
||||
: <String, Object?>{'programs': const []};
|
||||
snapshot['telemetrySamples'] = rawSamples;
|
||||
return jsonEncode(snapshot);
|
||||
}
|
||||
|
||||
domain.Exercise _exerciseFromPayload(RemoteSyncedItem item) {
|
||||
final payload = item.payload;
|
||||
return domain.Exercise(
|
||||
|
||||
@ -804,6 +804,48 @@ class WorkoutTelemetrySamples extends Table {
|
||||
];
|
||||
}
|
||||
|
||||
class ActiveWorkoutTelemetryWindowStates extends Table {
|
||||
@override
|
||||
String get tableName => 'active_workout_telemetry_window_states';
|
||||
|
||||
TextColumn get sessionId => text().references(
|
||||
ActiveWorkoutSessions,
|
||||
#id,
|
||||
onDelete: KeyAction.cascade,
|
||||
)();
|
||||
IntColumn get windowStartedActiveMs => integer()();
|
||||
DateTimeColumn get latestCapturedAt => dateTime()();
|
||||
IntColumn get programIndex => integer().nullable()();
|
||||
IntColumn get exerciseIndex => integer().nullable()();
|
||||
IntColumn get setIndex => integer().nullable()();
|
||||
IntColumn get passageIndex => integer().nullable()();
|
||||
IntColumn get stepIndex => integer().nullable()();
|
||||
TextColumn get programSnapshotId => text().nullable()();
|
||||
TextColumn get exerciseSnapshotId => text().nullable()();
|
||||
TextColumn get stepSnapshotId => text().nullable()();
|
||||
IntColumn get heartRateBpm => integer().nullable()();
|
||||
RealColumn get distanceMeters => real().nullable()();
|
||||
RealColumn get caloriesKcal => real().nullable()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {sessionId};
|
||||
|
||||
@override
|
||||
List<String> get customConstraints => [
|
||||
'CHECK (window_started_active_ms >= 0)',
|
||||
'CHECK (program_index IS NULL OR program_index >= 0)',
|
||||
'CHECK (exercise_index IS NULL OR exercise_index >= 0)',
|
||||
'CHECK (set_index IS NULL OR set_index >= 0)',
|
||||
'CHECK (passage_index IS NULL OR passage_index >= 0)',
|
||||
'CHECK (step_index IS NULL OR step_index >= 0)',
|
||||
'CHECK (heart_rate_bpm IS NULL OR heart_rate_bpm > 0)',
|
||||
'CHECK (distance_meters IS NULL OR distance_meters >= 0)',
|
||||
'CHECK (calories_kcal IS NULL OR calories_kcal >= 0)',
|
||||
'CHECK (heart_rate_bpm IS NOT NULL OR distance_meters IS NOT NULL OR '
|
||||
'calories_kcal IS NOT NULL)',
|
||||
];
|
||||
}
|
||||
|
||||
class WorkoutTelemetryAggregates extends Table {
|
||||
@override
|
||||
String get tableName => 'workout_telemetry_aggregates';
|
||||
|
||||
Reference in New Issue
Block a user