diff --git a/CHANGELOG.md b/CHANGELOG.md index de9937f7..f8b363d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.2.25 + +- Improves: Smaller UI changes +- Fix: Migration issue that resulted in a corrupted backup mechanism +- Fix: Database issues causing messages to be lost or the database to be corrupted +- Fix: Permission view did not disappear after they were granted + ## 0.2.23 - Improves: Smaller UI changes diff --git a/lib/src/database/twonly.db.dart b/lib/src/database/twonly.db.dart index b86d40b2..812b03e9 100644 --- a/lib/src/database/twonly.db.dart +++ b/lib/src/database/twonly.db.dart @@ -92,14 +92,15 @@ class TwonlyDB extends _$TwonlyDB { shareAcrossIsolates: true, setup: (rawDb) { rawDb - ..execute('PRAGMA journal_mode=WAL;') + ..execute('PRAGMA journal_mode=DELETE;') ..execute('PRAGMA synchronous=FULL;') ..execute('PRAGMA busy_timeout=5000;'); }, ), ); try { - if (userService.isUserCreated && userService.currentUser.enableDatabaseLogging) { + if (userService.isUserCreated && + userService.currentUser.enableDatabaseLogging) { return connection.interceptWith(DriftLoggingInterceptor()); } } catch (_) {} diff --git a/lib/src/services/user.service.dart b/lib/src/services/user.service.dart index e2b8e299..6d3c8018 100644 --- a/lib/src/services/user.service.dart +++ b/lib/src/services/user.service.dart @@ -32,6 +32,13 @@ class UserService { if (userDataMap != null) { final userData = UserData.fromJson(userDataMap); await RustKeyManager.setUserId(userId: userData.userId); + try { + // Ensure that the old userData is removed as it breaks the backup mechanism. + // This code can be removed when all users have updated to the latest version... + await SecureStorage.instance.delete(key: 'userData'); + } catch (e) { + Log.error('Could not delete user data from SecureStorage: $e'); + } return userData; } @@ -58,15 +65,20 @@ class UserService { } static Future _migrateFromSecureStorage(UserData userData) async { - // Currently empty migration logic as requested, but we MUST store the data await KeyValueStore.put('user', userData.toJson()); + try { await RustKeyManager.setUserId(userId: userData.userId); } catch (e) { Log.error('Could not set userId in RustKeyManager during migration: $e'); } - // Optional: Log migration + try { + await SecureStorage.instance.delete(key: 'userData'); + } catch (e) { + Log.error('Could not delete user data from SecureStorage: $e'); + } + Log.info('Migrated user data from SecureStorage to KeyValueStore'); } diff --git a/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart b/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart index 6f638aa6..253e62c8 100644 --- a/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart +++ b/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart @@ -63,23 +63,25 @@ class CameraPreviewControllerView extends StatefulWidget { final bool hideControllers; @override - State createState() => _CameraPreviewControllerViewState(); + State createState() => + _CameraPreviewControllerViewState(); } -class _CameraPreviewControllerViewState extends State { +class _CameraPreviewControllerViewState + extends State { Future? _permissionsFuture; @override void initState() { super.initState(); - if (!AppState.hasCameraPermissions) { - _permissionsFuture = checkPermissions().then((hasPermission) { - if (hasPermission) { + _permissionsFuture = checkPermissions().then((hasPermission) { + if (hasPermission && mounted) { + setState(() { AppState.hasCameraPermissions = true; - } - return hasPermission; - }); - } + }); + } + return hasPermission; + }); } @override @@ -107,6 +109,10 @@ class _CameraPreviewControllerViewState extends State { Future initAsync() async { _hasAudioPermission = await Permission.microphone.isGranted; - if (!_hasAudioPermission && !userService.currentUser.requestedAudioPermission) { + if (!_hasAudioPermission && + !userService.currentUser.requestedAudioPermission) { await UserService.update((u) => u.requestedAudioPermission = true); await requestMicrophonePermission(); } @@ -262,7 +269,8 @@ class _CameraPreviewViewState extends State { } Future updateScaleFactor(double newScale) async { - if (mc.selectedCameraDetails.scaleFactor == newScale || mc.cameraController == null) { + if (mc.selectedCameraDetails.scaleFactor == newScale || + mc.cameraController == null) { return; } await mc.cameraController?.setZoomLevel( @@ -345,7 +353,9 @@ class _CameraPreviewViewState extends State { bool sharedFromGallery = false, MediaType? mediaType, }) async { - final type = mediaType ?? ((videoFilePath != null) ? MediaType.video : MediaType.image); + final type = + mediaType ?? + ((videoFilePath != null) ? MediaType.video : MediaType.image); final mediaFileService = await initializeMediaUpload( type, userService.currentUser.defaultShowTime, @@ -386,9 +396,10 @@ class _CameraPreviewViewState extends State { mainCameraController: mc, previewLink: mc.sharedLinkForPreview, ), - transitionsBuilder: (context, animation, secondaryAnimation, child) { - return child; - }, + transitionsBuilder: + (context, animation, secondaryAnimation, child) { + return child; + }, transitionDuration: Duration.zero, reverseTransitionDuration: Duration.zero, ), @@ -418,13 +429,16 @@ class _CameraPreviewViewState extends State { return false; } - bool get isFront => mc.cameraController?.description.lensDirection == CameraLensDirection.front; + bool get isFront => + mc.cameraController?.description.lensDirection == + CameraLensDirection.front; Future onPanUpdate(dynamic details) async { if (details == null) { return; } - if (mc.cameraController == null || !mc.cameraController!.value.isInitialized) { + if (mc.cameraController == null || + !mc.cameraController!.value.isInitialized) { return; } @@ -553,7 +567,8 @@ class _CameraPreviewViewState extends State { } Future startVideoRecording() async { - if (mc.cameraController != null && mc.cameraController!.value.isRecordingVideo) { + if (mc.cameraController != null && + mc.cameraController!.value.isRecordingVideo) { return; } setState(() { @@ -573,7 +588,8 @@ class _CameraPreviewViewState extends State { _currentTime = clock.now(); }); if (_videoRecordingStarted != null && - _currentTime.difference(_videoRecordingStarted!).inSeconds >= maxVideoRecordingTime) { + _currentTime.difference(_videoRecordingStarted!).inSeconds >= + maxVideoRecordingTime) { timer.cancel(); _videoRecordingTimer = null; stopVideoRecording(); @@ -610,7 +626,8 @@ class _CameraPreviewViewState extends State { _videoRecordingLocked = false; }); - if (mc.cameraController == null || !mc.cameraController!.value.isRecordingVideo) { + if (mc.cameraController == null || + !mc.cameraController!.value.isRecordingVideo) { return; } @@ -638,7 +655,8 @@ class _CameraPreviewViewState extends State { @override Widget build(BuildContext context) { - if (mc.selectedCameraDetails.cameraId >= AppEnvironment.cameras.length || mc.cameraController == null) { + if (mc.selectedCameraDetails.cameraId >= AppEnvironment.cameras.length || + mc.cameraController == null) { return Container(); } return StreamBuilder( @@ -662,7 +680,9 @@ class _CameraPreviewViewState extends State { _baseScaleFactor = mc.selectedCameraDetails.scaleFactor; }); // Get the position of the pointer - final renderBox = keyTriggerButton.currentContext!.findRenderObject()! as RenderBox; + final renderBox = + keyTriggerButton.currentContext!.findRenderObject()! + as RenderBox; final localPosition = renderBox.globalToLocal( details.globalPosition, ); @@ -698,18 +718,24 @@ class _CameraPreviewViewState extends State { ), ), ), - if (!mc.isSharePreviewIsShown && widget.sendToGroup != null && !mc.isVideoRecording) + if (!mc.isSharePreviewIsShown && + widget.sendToGroup != null && + !mc.isVideoRecording) ShowTitleText( title: widget.sendToGroup!.groupName, desc: context.lang.cameraPreviewSendTo, ), - if (!mc.isSharePreviewIsShown && mc.sharedLinkForPreview != null && !mc.isVideoRecording) + if (!mc.isSharePreviewIsShown && + mc.sharedLinkForPreview != null && + !mc.isVideoRecording) ShowTitleText( title: mc.sharedLinkForPreview?.host ?? '', desc: 'Link', isLink: true, ), - if (!mc.isSharePreviewIsShown && !mc.isVideoRecording && !widget.hideControllers) + if (!mc.isSharePreviewIsShown && + !mc.isVideoRecording && + !widget.hideControllers) CameraTopActions( selectedCameraDetails: mc.selectedCameraDetails, hasAudioPermission: _hasAudioPermission, @@ -753,7 +779,8 @@ class _CameraPreviewViewState extends State { videoRecordingStarted: _videoRecordingStarted, maxVideoRecordingTime: maxVideoRecordingTime, ), - if (!mc.isSharePreviewIsShown && widget.sendToGroup != null || widget.hideControllers) + if (!mc.isSharePreviewIsShown && widget.sendToGroup != null || + widget.hideControllers) Positioned( left: 5, top: 10, diff --git a/lib/src/visual/views/camera/camera_preview_components/permissions_view.dart b/lib/src/visual/views/camera/camera_preview_components/permissions_view.dart index 411fb547..fa14088e 100644 --- a/lib/src/visual/views/camera/camera_preview_components/permissions_view.dart +++ b/lib/src/visual/views/camera/camera_preview_components/permissions_view.dart @@ -1,4 +1,4 @@ -// ignore_for_file: avoid_dynamic_calls +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -24,31 +24,56 @@ Future checkPermissions() async { return true; } -class PermissionHandlerViewState extends State { +class PermissionHandlerViewState extends State + with WidgetsBindingObserver { + Timer? _timer; + bool _isSuccessTriggered = false; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + _timer = Timer.periodic(const Duration(milliseconds: 500), (timer) async { + await _checkAndTriggerSuccess(); + }); + } + + @override + void dispose() { + _timer?.cancel(); + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + _checkAndTriggerSuccess(); + } + } + + Future _checkAndTriggerSuccess() async { + if (_isSuccessTriggered) return; + try { + if (await checkPermissions()) { + _isSuccessTriggered = true; + _timer?.cancel(); + // ignore: avoid_dynamic_calls + widget.onSuccess(); + } + } catch (e) { + Log.error(e); + } + } + Future> permissionServices() async { - // try { final statuses = await [ Permission.camera, - // Permission.microphone, Permission.notification, ].request(); - // } catch (e) {} - // You can request multiple permissions at once. - - // if (statuses[Permission.microphone]!.isPermanentlyDenied) { - // openAppSettings(); - // // setState(() {}); - // } else { - // // if (statuses[Permission.microphone]!.isDenied) { - // // } - // } if (statuses[Permission.camera]!.isPermanentlyDenied) { await openAppSettings(); - // setState(() {}); - } else { - // if (statuses[Permission.camera]!.isDenied) { - // } } return statuses; @@ -75,6 +100,7 @@ class PermissionHandlerViewState extends State { try { await permissionServices(); if (await checkPermissions()) { + // ignore: avoid_dynamic_calls widget.onSuccess(); } } catch (e) { diff --git a/lib/src/visual/views/chats/media_viewer.view.dart b/lib/src/visual/views/chats/media_viewer.view.dart index eda8e12e..d8cc4503 100644 --- a/lib/src/visual/views/chats/media_viewer.view.dart +++ b/lib/src/visual/views/chats/media_viewer.view.dart @@ -573,7 +573,7 @@ class _MediaViewerViewState extends State { ), ), ), - onPressed: () async { + onPressed: () { if (!showShortReactions) { displayShortReactions(); } else { diff --git a/lib/src/visual/views/chats/media_viewer_components/emoji_reactions_row.comp.dart b/lib/src/visual/views/chats/media_viewer_components/emoji_reactions_row.comp.dart index 98ded465..4b5884a8 100644 --- a/lib/src/visual/views/chats/media_viewer_components/emoji_reactions_row.comp.dart +++ b/lib/src/visual/views/chats/media_viewer_components/emoji_reactions_row.comp.dart @@ -67,26 +67,24 @@ class _EmojiReactionWidgetState extends State { @override Widget build(BuildContext context) { - return AnimatedSize( + return GestureDetector( key: _targetKey, - duration: const Duration(milliseconds: 200), - curve: Curves.linearToEaseOut, - child: GestureDetector( - onTap: () async { - await sendReaction(widget.groupId, widget.messageId, widget.emoji); - widget.emojiKey.currentState?.spawn( - getGlobalOffset(_targetKey), - widget.emoji, - ); - widget.hide(); - }, - child: SizedBox( - width: widget.show ? 40 : 10, - child: Center( - child: EmojiAnimationComp( - emoji: widget.emoji, - ), - ), + onTap: () async { + await sendReaction(widget.groupId, widget.messageId, widget.emoji); + widget.emojiKey.currentState?.spawn( + getGlobalOffset(_targetKey), + widget.emoji, + ); + widget.hide(); + }, + child: SizedBox( + width: 40, + child: Center( + child: widget.show + ? EmojiAnimationComp( + emoji: widget.emoji, + ) + : const SizedBox.shrink(), ), ), ); diff --git a/lib/src/visual/views/chats/media_viewer_components/reaction_buttons.comp.dart b/lib/src/visual/views/chats/media_viewer_components/reaction_buttons.comp.dart index 549f2af2..1bdffc4b 100644 --- a/lib/src/visual/views/chats/media_viewer_components/reaction_buttons.comp.dart +++ b/lib/src/visual/views/chats/media_viewer_components/reaction_buttons.comp.dart @@ -39,6 +39,7 @@ class ReactionButtons extends StatefulWidget { class _ReactionButtonsState extends State { int selectedShortReaction = -1; final GlobalKey _keyEmojiPicker = GlobalKey(); + bool _renderAnimations = false; List selectedEmojis = EmojiAnimationComp.animatedIcons.keys .toList() @@ -47,9 +48,28 @@ class _ReactionButtonsState extends State { @override void initState() { super.initState(); + _renderAnimations = widget.show; initAsync(); } + @override + void didUpdateWidget(ReactionButtons oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.show != oldWidget.show) { + if (widget.show) { + _renderAnimations = true; + } else { + Future.delayed(const Duration(milliseconds: 150), () { + if (mounted && !widget.show) { + setState(() { + _renderAnimations = false; + }); + } + }); + } + } + } + Future initAsync() async { if (userService.currentUser.preSelectedEmojies != null) { selectedEmojis = userService.currentUser.preSelectedEmojies!; @@ -71,91 +91,92 @@ class _ReactionButtonsState extends State { ? 50 : widget.mediaViewerDistanceFromBottom) : widget.mediaViewerDistanceFromBottom - 20, - left: widget.show ? 0 : MediaQuery.sizeOf(context).width / 2, - right: widget.show ? 0 : MediaQuery.sizeOf(context).width / 2, + left: 0, + right: 0, curve: Curves.linearToEaseOut, - child: AnimatedOpacity( - opacity: widget.show ? 1.0 : 0.0, // Fade in/out - duration: const Duration(milliseconds: 150), - child: Container( - color: widget.show ? Colors.black.withAlpha(0) : Colors.transparent, - padding: widget.show - ? const EdgeInsets.symmetric(vertical: 32) - : null, - child: Column( - children: [ - if (secondRowEmojis.isNotEmpty) + child: IgnorePointer( + ignoring: !widget.show, + child: AnimatedOpacity( + opacity: widget.show ? 1.0 : 0.0, // Fade in/out + duration: const Duration(milliseconds: 150), + child: Container( + color: widget.show ? Colors.black.withAlpha(0) : Colors.transparent, + padding: const EdgeInsets.symmetric(vertical: 32), + child: Column( + children: [ + if (secondRowEmojis.isNotEmpty) + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.end, + children: secondRowEmojis + .map( + (emoji) => EmojiReactionWidget( + messageId: widget.messageId, + groupId: widget.groupId, + hide: widget.hide, + show: _renderAnimations, + emoji: emoji as String, + emojiKey: widget.emojiKey, + ), + ) + .toList(), + ), + if (secondRowEmojis.isNotEmpty) const SizedBox(height: 15), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.end, - children: secondRowEmojis - .map( - (emoji) => EmojiReactionWidget( - messageId: widget.messageId, - groupId: widget.groupId, - hide: widget.hide, - show: widget.show, - emoji: emoji as String, - emojiKey: widget.emojiKey, + children: [ + ...firstRowEmojis.map( + (emoji) => EmojiReactionWidget( + messageId: widget.messageId, + groupId: widget.groupId, + hide: widget.hide, + show: _renderAnimations, + emoji: emoji, + emojiKey: widget.emojiKey, + ), + ), + GestureDetector( + key: _keyEmojiPicker, + onTap: () async { + final layer = + // ignore: inference_failure_on_function_invocation + await showModalBottomSheet( + context: context, + backgroundColor: context.color.surface, + builder: (context) { + return const EmojiPickerBottom(); + }, + ) + as EmojiLayerData?; + if (layer == null) return; + await sendReaction( + widget.groupId, + widget.messageId, + layer.text, + ); + widget.emojiKey.currentState?.spawn( + getGlobalOffset(_keyEmojiPicker), + layer.text, + ); + widget.hide(); + }, + child: Container( + decoration: BoxDecoration( + color: context.color.surfaceContainer.withAlpha(100), + borderRadius: BorderRadius.circular(12), ), - ) - .toList(), + padding: const EdgeInsets.all(8), + child: const FaIcon( + FontAwesomeIcons.ellipsisVertical, + size: 24, + ), + ), + ), + ], ), - if (secondRowEmojis.isNotEmpty) const SizedBox(height: 15), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - ...firstRowEmojis.map( - (emoji) => EmojiReactionWidget( - messageId: widget.messageId, - groupId: widget.groupId, - hide: widget.hide, - show: widget.show, - emoji: emoji, - emojiKey: widget.emojiKey, - ), - ), - GestureDetector( - key: _keyEmojiPicker, - onTap: () async { - final layer = - // ignore: inference_failure_on_function_invocation - await showModalBottomSheet( - context: context, - backgroundColor: context.color.surface, - builder: (context) { - return const EmojiPickerBottom(); - }, - ) - as EmojiLayerData?; - if (layer == null) return; - await sendReaction( - widget.groupId, - widget.messageId, - layer.text, - ); - widget.emojiKey.currentState?.spawn( - getGlobalOffset(_keyEmojiPicker), - layer.text, - ); - widget.hide(); - }, - child: Container( - decoration: BoxDecoration( - color: context.color.surfaceContainer.withAlpha(100), - borderRadius: BorderRadius.circular(12), - ), - padding: const EdgeInsets.all(8), - child: const FaIcon( - FontAwesomeIcons.ellipsisVertical, - size: 24, - ), - ), - ), - ], - ), - ], + ], + ), ), ), ), diff --git a/rust/src/backup/backup_archive.rs b/rust/src/backup/backup_archive.rs index bcbd73a4..d1dac2e7 100644 --- a/rust/src/backup/backup_archive.rs +++ b/rust/src/backup/backup_archive.rs @@ -55,15 +55,29 @@ impl BackupArchive { } if is_db { + // To avoid write-lock conflicts with Dart (which has the live database open in write mode), + // we copy the database file first, then open the copy in write mode to perform the backup. + let temp_copy_path = backup_data_dir.join(format!("{}.temp_copy", file_name)); + std::fs::copy(&file_path, &temp_copy_path)?; + let db = Database::new( - &file_path.display().to_string(), + &temp_copy_path.display().to_string(), encryption_key.as_deref(), - false, + false, // Open the copy in write mode required for encrypted backups ) .await?; let backup_database_file = backup_data_dir.join(file_name).display().to_string(); db.create_backup(backup_database_file.as_str(), encryption_key.as_deref()) .await?; + + // Close database connection to release file lock before removing it + drop(db); + remove_file(&temp_copy_path)?; + + // Perform integrity check of the new database file + let backup_db = + Database::new(&backup_database_file, encryption_key.as_deref(), false).await?; + backup_db.check_integrity().await?; } else { let file_backup = backup_data_dir.join(file_name); std::fs::copy(file_path, file_backup)?; diff --git a/rust/src/context.rs b/rust/src/context.rs index 53361e38..d409b523 100644 --- a/rust/src/context.rs +++ b/rust/src/context.rs @@ -63,14 +63,14 @@ impl Context { key_manager.store_to_keychain(&secure_storage)?; let rust_db_path = database_dir.join("rust_db.sqlite"); - let rust_db = Arc::new( - Database::new( - &rust_db_path.display().to_string(), - Some(&key_manager.main_key.get_database_key(DatabaseKey::RustDb)), - false, - ) - .await?, - ); + let rust_db = Database::new( + &rust_db_path.display().to_string(), + Some(&key_manager.main_key.get_database_key(DatabaseKey::RustDb)), + false, + ) + .await?; + rust_db.run_migrations().await?; + let rust_db = Arc::new(rust_db); Ok(Context::from_standalone(TwonlyStandalone { config, @@ -120,14 +120,14 @@ impl Context { let mut rust_db_key = key_manager.main_key.get_database_key(DatabaseKey::RustDb); - let rust_db = Arc::new( - Database::new( - &rust_db_path.display().to_string(), - Some(rust_db_key.as_str()), - false, - ) - .await?, - ); + let rust_db = Database::new( + &rust_db_path.display().to_string(), + Some(rust_db_key.as_str()), + false, + ) + .await?; + rust_db.run_migrations().await?; + let rust_db = Arc::new(rust_db); rust_db_key.zeroize(); diff --git a/rust/src/database/mod.rs b/rust/src/database/mod.rs index 46721db9..665459dc 100644 --- a/rust/src/database/mod.rs +++ b/rust/src/database/mod.rs @@ -26,10 +26,11 @@ impl Database { let mut connect_options = format!("{db_url}?mode=rwc") .parse::()? .log_statements(log_statements_level) - .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) + .journal_mode(sqlx::sqlite::SqliteJournalMode::Delete) .foreign_keys(true) .read_only(read_only) .busy_timeout(Duration::from_millis(5000)) + .pragma("synchronous", "FULL") .pragma("recursive_triggers", "ON") .log_slow_statements(tracing::log::LevelFilter::Warn, Duration::from_millis(500)); @@ -43,15 +44,18 @@ impl Database { .connect_with(connect_options) .await?; + Ok(Self { pool }) + } + + pub(crate) async fn run_migrations(&self) -> Result<()> { sqlx::migrate!("./src/database/migrations") - .run(&pool) + .run(&self.pool) .await .map_err(|e| { tracing::error!("migration error: {:?}", e); TwonlyError::Generic(format!("Migration error: {}", e)) })?; - - Ok(Self { pool }) + Ok(()) } pub(crate) async fn create_backup( @@ -91,6 +95,22 @@ impl Database { } Ok(()) } + + pub(crate) async fn check_integrity(&self) -> Result<()> { + let row: (String,) = sqlx::query_as("PRAGMA integrity_check") + .fetch_one(&self.pool) + .await + .map_err(|e| TwonlyError::Generic(format!("Integrity check query failed: {}", e)))?; + + if row.0.to_lowercase() == "ok" { + Ok(()) + } else { + Err(TwonlyError::Generic(format!( + "Database integrity check failed: {}", + row.0 + ))) + } + } } #[cfg(test)] @@ -109,6 +129,7 @@ mod tests { // 1. Create and initialize database with key let db = Database::new(&db_path, Some(key), false).await.unwrap(); + db.run_migrations().await.unwrap(); ReceivedMessage::insert(&db.pool, 1, b"hello world") .await .unwrap(); @@ -137,6 +158,7 @@ mod tests { let key = "secure_password"; let db = Database::new(&db_path, Some(key), false).await.unwrap(); + db.run_migrations().await.unwrap(); ReceivedMessage::insert(&db.pool, 1, b"hello world") .await .unwrap(); @@ -165,6 +187,7 @@ mod tests { let backup_path = dir.path().join("backup_plain.sqlite").display().to_string(); let db = Database::new(&db_path, None, false).await.unwrap(); + db.run_migrations().await.unwrap(); ReceivedMessage::insert(&db.pool, 1, b"hello world") .await .unwrap(); diff --git a/rust/src/database/tables/mod.rs b/rust/src/database/tables/mod.rs index c8a04d57..7d27b9ea 100644 --- a/rust/src/database/tables/mod.rs +++ b/rust/src/database/tables/mod.rs @@ -71,6 +71,7 @@ macro_rules! generate_table_tests { let dir = tempdir().unwrap(); let db_path = dir.path().join("test.sqlite").display().to_string(); let db = Database::new(&db_path, None, false).await.unwrap(); + db.run_migrations().await.unwrap(); let _id = $struct::$insert_fn(&db.pool, $($arg),+).await.unwrap(); let all = $struct::$select_all_fn(&db.pool).await.unwrap(); @@ -92,6 +93,7 @@ macro_rules! generate_test_select { let dir = tempdir().unwrap(); let db_path = dir.path().join("test.sqlite").display().to_string(); let db = Database::new(&db_path, None, false).await.unwrap(); + db.run_migrations().await.unwrap(); $struct::$insert_fn(&db.pool, $($arg),+).await.unwrap(); let results = $struct::$select_fn(&db.pool, $($sel_arg),+).await.unwrap();