From fe57a1c142992022cf9a81974802b7fff68ce4c6 Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Wed, 18 Mar 2026 11:38:14 +0100 Subject: [PATCH 1/8] chore: migrate persistence to async SQLDelight on JS --- data/persistence/build.gradle.kts | 6 +- .../kalium/persistence/db/GlobalDatabase.kt | 3 +- .../kalium/persistence/db/UserDatabase.kt | 5 +- .../kalium/persistence/db/GlobalDatabase.kt | 5 +- .../kalium/persistence/db/UserDatabase.kt | 9 ++- .../persistence/backup/DatabaseExporter.kt | 4 +- .../persistence/dao/ConnectionDAOImpl.kt | 6 +- .../wire/kalium/persistence/dao/PrekeyDAO.kt | 6 +- .../wire/kalium/persistence/dao/SearchDAO.kt | 14 ++-- .../wire/kalium/persistence/dao/ServiceDAO.kt | 4 +- .../kalium/persistence/dao/UserDAOImpl.kt | 46 ++++++------ .../persistence/dao/asset/AssetDAOImpl.kt | 4 +- .../dao/backup/NomadMessagesDAO.kt | 56 +++++++------- .../backup/RemoteBackupChangeLogDAOImpl.kt | 7 +- .../persistence/dao/call/CallDAOImpl.kt | 11 ++- .../persistence/dao/client/ClientDAOImpl.kt | 28 ++++--- .../dao/conversation/ConversationDAOImpl.kt | 64 ++++++++-------- .../ConversationMetaDataDAOImpl.kt | 7 +- .../folder/ConversationFolderDAOImpl.kt | 7 +- .../persistence/dao/event/EventDAOImpl.kt | 4 +- .../persistence/dao/member/MemberDAO.kt | 28 ++++--- .../persistence/dao/message/MessageDAOImpl.kt | 56 +++++++------- .../dao/message/MessageInsertExtension.kt | 31 ++++---- .../dao/message/MessageMetadataDAO.kt | 4 +- .../attachment/MessageAttachmentsDao.kt | 12 ++- .../dao/message/draft/MessageDraftDAOImpl.kt | 13 ++-- .../MessageAttachmentDraftDaoImpl.kt | 7 +- .../dao/publiclink/PublicLinkDaoImpl.kt | 4 +- .../persistence/dao/reaction/ReactionDAO.kt | 14 ++-- .../persistence/daokaliumdb/AccountsDAO.kt | 33 +++++---- .../daokaliumdb/ServerConfigurationDAO.kt | 23 +++--- .../persistence/db/GlobalDatabaseBuilder.kt | 6 +- .../persistence/db/UserDatabaseBuilder.kt | 37 +++++++--- .../kalium/persistence/db/GlobalDatabase.kt | 12 ++- .../persistence/db/SqlDelightAsyncSmoke.kt | 48 ++++++++++++ .../kalium/persistence/db/UserDatabase.kt | 40 ++++++++-- .../persistence/db/WebWorkerDriverFactory.kt | 73 +++++++++++++++++++ .../kalium/persistence/BaseDatabaseTest.kt | 19 +++-- .../kalium/persistence/GlobalDBBaseTest.kt | 12 ++- .../persistence/SqlDelightAsyncSmokeTest.kt | 49 +++++++++++++ .../kalium/persistence/db/GlobalDatabase.kt | 5 +- .../kalium/persistence/db/UserDatabase.kt | 5 +- .../logic/data/session/SessionRepository.kt | 4 +- 43 files changed, 578 insertions(+), 253 deletions(-) create mode 100644 data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/SqlDelightAsyncSmoke.kt create mode 100644 data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/WebWorkerDriverFactory.kt create mode 100644 data/persistence/src/jsTest/kotlin/com/wire/kalium/persistence/SqlDelightAsyncSmokeTest.kt diff --git a/data/persistence/build.gradle.kts b/data/persistence/build.gradle.kts index 6fada469030e..f02e3644ee38 100644 --- a/data/persistence/build.gradle.kts +++ b/data/persistence/build.gradle.kts @@ -27,7 +27,7 @@ plugins { kaliumLibrary { multiplatform { - enableJsTests.set(false) + enableJsTests.set(true) } } @@ -36,6 +36,7 @@ sqldelight { create("UserDatabase") { dialect(libs.sqldelight.dialect.get().toString()) packageName.set("com.wire.kalium.persistence") + generateAsync.set(true) val sourceFolderName = "db_user" srcDirs.setFrom(listOf("src/commonMain/$sourceFolderName")) schemaOutputDirectory.set(file("src/commonMain/$sourceFolderName/schemas")) @@ -45,6 +46,7 @@ sqldelight { create("GlobalDatabase") { dialect(libs.sqldelight.dialect.get().toString()) packageName.set("com.wire.kalium.persistence") + generateAsync.set(true) val sourceFolderName = "db_global" srcDirs.setFrom(listOf("src/commonMain/$sourceFolderName")) schemaOutputDirectory.set(file("src/commonMain/$sourceFolderName/schemas")) @@ -63,6 +65,7 @@ kotlin { implementation(libs.sqldelight.runtime) implementation(libs.sqldelight.coroutinesExtension) + implementation(libs.sqldelight.async) implementation(libs.sqldelight.primitiveAdapters) implementation(libs.ktxSerialization) implementation(libs.settings.kmp) @@ -97,6 +100,7 @@ kotlin { val jsMain by getting { dependencies { implementation(libs.sqldelight.jsDriver) + implementation(npm("@cashapp/sqldelight-sqljs-worker", libs.versions.sqldelight.get())) implementation(npm("sql.js", "1.6.2")) implementation(devNpm("copy-webpack-plugin", "9.1.0")) } diff --git a/data/persistence/src/androidMain/kotlin/com/wire/kalium/persistence/db/GlobalDatabase.kt b/data/persistence/src/androidMain/kotlin/com/wire/kalium/persistence/db/GlobalDatabase.kt index d6f49ed9dc22..c04767b13a3a 100644 --- a/data/persistence/src/androidMain/kotlin/com/wire/kalium/persistence/db/GlobalDatabase.kt +++ b/data/persistence/src/androidMain/kotlin/com/wire/kalium/persistence/db/GlobalDatabase.kt @@ -18,6 +18,7 @@ package com.wire.kalium.persistence.db +import app.cash.sqldelight.async.coroutines.synchronous import com.wire.kalium.persistence.GlobalDatabase import com.wire.kalium.persistence.util.FileNameUtil import kotlinx.coroutines.CoroutineDispatcher @@ -28,7 +29,7 @@ actual fun globalDatabaseProvider( passphrase: GlobalDatabaseSecret?, enableWAL: Boolean ): GlobalDatabaseBuilder { - val schema = GlobalDatabase.Schema + val schema = GlobalDatabase.Schema.synchronous() val dbName = FileNameUtil.globalDBName() val driver = databaseDriver( platformDatabaseData.context, diff --git a/data/persistence/src/androidMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt b/data/persistence/src/androidMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt index f472bf2df71b..452f12be8c02 100644 --- a/data/persistence/src/androidMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt +++ b/data/persistence/src/androidMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt @@ -22,6 +22,7 @@ package com.wire.kalium.persistence.db import android.content.Context import androidx.sqlite.db.SupportSQLiteDatabase +import app.cash.sqldelight.async.coroutines.synchronous import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.driver.android.AndroidSqliteDriver import com.wire.kalium.persistence.UserDatabase @@ -49,7 +50,7 @@ actual fun userDatabaseBuilder( context = platformDatabaseData.context, dbName = dbName, passphrase = passphrase?.value, - schema = UserDatabase.Schema + schema = UserDatabase.Schema.synchronous() ) { isWALEnabled = enableWAL } @@ -99,7 +100,7 @@ fun inMemoryDatabase( val passphrase = "testPass".toByteArray() System.loadLibrary("sqlcipher") val rawDriver = AndroidSqliteDriver( - schema = UserDatabase.Schema, + schema = UserDatabase.Schema.synchronous(), context = context, name = null, factory = SupportOpenHelperFactory(passphrase) diff --git a/data/persistence/src/appleMain/kotlin/com/wire/kalium/persistence/db/GlobalDatabase.kt b/data/persistence/src/appleMain/kotlin/com/wire/kalium/persistence/db/GlobalDatabase.kt index 43c07b77b37d..3570469567f0 100644 --- a/data/persistence/src/appleMain/kotlin/com/wire/kalium/persistence/db/GlobalDatabase.kt +++ b/data/persistence/src/appleMain/kotlin/com/wire/kalium/persistence/db/GlobalDatabase.kt @@ -18,6 +18,7 @@ package com.wire.kalium.persistence.db +import app.cash.sqldelight.async.coroutines.synchronous import com.wire.kalium.persistence.GlobalDatabase import com.wire.kalium.persistence.util.FileNameUtil import kotlinx.coroutines.CoroutineDispatcher @@ -32,7 +33,7 @@ actual fun globalDatabaseProvider( val driver = when (val data = platformDatabaseData.storageData) { is StorageData.FileBacked -> { NSFileManager.defaultManager.createDirectoryAtPath(data.storePath, true, null, null) - val schema = GlobalDatabase.Schema + val schema = GlobalDatabase.Schema.synchronous() databaseDriver(data.storePath, FileNameUtil.globalDBName(), schema) { isWALEnabled = false useGradleSafeSqliterLogging = platformDatabaseData.useGradleSafeSqliterLogging @@ -40,7 +41,7 @@ actual fun globalDatabaseProvider( } StorageData.InMemory -> - databaseDriver(null, FileNameUtil.globalDBName(), GlobalDatabase.Schema) { + databaseDriver(null, FileNameUtil.globalDBName(), GlobalDatabase.Schema.synchronous()) { isWALEnabled = false useGradleSafeSqliterLogging = platformDatabaseData.useGradleSafeSqliterLogging } diff --git a/data/persistence/src/appleMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt b/data/persistence/src/appleMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt index b681943a6425..3569d163b40b 100644 --- a/data/persistence/src/appleMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt +++ b/data/persistence/src/appleMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt @@ -20,6 +20,7 @@ package com.wire.kalium.persistence.db +import app.cash.sqldelight.async.coroutines.synchronous import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.driver.native.NativeSqliteDriver import com.wire.kalium.persistence.UserDatabase @@ -46,14 +47,14 @@ actual fun userDatabaseBuilder( null, null ) - databaseDriver(platformDatabaseData.storageData.storePath, FileNameUtil.userDBName(userId), UserDatabase.Schema) { + databaseDriver(platformDatabaseData.storageData.storePath, FileNameUtil.userDBName(userId), UserDatabase.Schema.synchronous()) { isWALEnabled = enableWAL useGradleSafeSqliterLogging = platformDatabaseData.useGradleSafeSqliterLogging } } StorageData.InMemory -> - databaseDriver(null, FileNameUtil.userDBName(userId), UserDatabase.Schema) { + databaseDriver(null, FileNameUtil.userDBName(userId), UserDatabase.Schema.synchronous()) { isWALEnabled = false useGradleSafeSqliterLogging = platformDatabaseData.useGradleSafeSqliterLogging } @@ -86,7 +87,7 @@ actual fun userDatabaseDriverByPath( enableWAL: Boolean ): SqlDriver { return NativeSqliteDriver( - UserDatabase.Schema, + UserDatabase.Schema.synchronous(), path ) } @@ -103,7 +104,7 @@ fun inMemoryDatabase( userId: UserIDEntity, dispatcher: CoroutineDispatcher ): UserDatabaseBuilder = InMemoryDatabaseCache.getOrCreate(userId) { - val rawDriver = databaseDriver(null, FileNameUtil.userDBName(userId), UserDatabase.Schema) { + val rawDriver = databaseDriver(null, FileNameUtil.userDBName(userId), UserDatabase.Schema.synchronous()) { isWALEnabled = false } diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/backup/DatabaseExporter.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/backup/DatabaseExporter.kt index a217f897d470..58090088700c 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/backup/DatabaseExporter.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/backup/DatabaseExporter.kt @@ -35,7 +35,7 @@ interface DatabaseExporter { * Export the user DB to a plain DB * @return the path to the plain DB file, null if the file was not created */ - fun exportToPlainDB(localDBPassphrase: UserDBSecret?): String? + suspend fun exportToPlainDB(localDBPassphrase: UserDBSecret?): String? /** * Delete the backup file and any temp data was created during the backup process @@ -55,7 +55,7 @@ internal class DatabaseExporterImpl internal constructor( private val backupUserId = user.copy(value = "backup-${user.value}") @Suppress("TooGenericExceptionCaught", "ReturnCount") - override fun exportToPlainDB(localDBPassphrase: UserDBSecret?): String? { + override suspend fun exportToPlainDB(localDBPassphrase: UserDBSecret?): String? { // delete the backup DB file if it exists if (!deleteBackupDBFile()) { kaliumLogger.e("Failed to delete the backup DB file") diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/ConnectionDAOImpl.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/ConnectionDAOImpl.kt index 268046927ed4..3e9faffc8971 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/ConnectionDAOImpl.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/ConnectionDAOImpl.kt @@ -18,6 +18,8 @@ package com.wire.kalium.persistence.dao +import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull + import app.cash.sqldelight.coroutines.asFlow import com.wire.kalium.persistence.ConnectionsQueries import com.wire.kalium.persistence.ConversationsQueries @@ -123,7 +125,7 @@ class ConnectionDAOImpl( } override suspend fun getConnection(conversationId: QualifiedIDEntity): ConnectionEntity? = withContext(readDispatcher.value) { - connectionsQueries.selectConnection(conversationId).executeAsOneOrNull()?.let { connectionMapper.toModel(it) } + connectionsQueries.selectConnection(conversationId).awaitAsOneOrNull()?.let { connectionMapper.toModel(it) } } override suspend fun getConnectionRequests(): Flow> { @@ -200,7 +202,7 @@ class ConnectionDAOImpl( override suspend fun getConnectionByUser(userId: QualifiedIDEntity): ConnectionEntity? { return withContext(readDispatcher.value) { - connectionsQueries.selectConnectionRequestByUser(userId, connectionMapper::toModel).executeAsOneOrNull() + connectionsQueries.selectConnectionRequestByUser(userId, connectionMapper::toModel).awaitAsOneOrNull() } } } diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/PrekeyDAO.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/PrekeyDAO.kt index 33aac1fd10de..ec03901498f7 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/PrekeyDAO.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/PrekeyDAO.kt @@ -18,6 +18,8 @@ package com.wire.kalium.persistence.dao +import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull + import com.wire.kalium.persistence.MetadataQueries import com.wire.kalium.persistence.db.ReadDispatcher import com.wire.kalium.persistence.db.WriteDispatcher @@ -36,7 +38,7 @@ internal class PrekeyDAOImpl internal constructor( ) : PrekeyDAO { override suspend fun updateMostRecentPreKeyId(newKeyId: Int) = withContext(writeDispatcher.value) { metadataQueries.transaction { - val currentId = metadataQueries.selectValueByKey(MOST_RECENT_PREKEY_ID).executeAsOneOrNull()?.toInt() + val currentId = metadataQueries.selectValueByKey(MOST_RECENT_PREKEY_ID).awaitAsOneOrNull()?.toInt() if (currentId == null || newKeyId > currentId) { metadataQueries.insertValue(MOST_RECENT_PREKEY_ID, newKeyId.toString()) } @@ -50,7 +52,7 @@ internal class PrekeyDAOImpl internal constructor( } override suspend fun mostRecentPreKeyId(): Int? = withContext(readDispatcher.value) { - metadataQueries.selectValueByKey(MOST_RECENT_PREKEY_ID).executeAsOneOrNull()?.toInt() + metadataQueries.selectValueByKey(MOST_RECENT_PREKEY_ID).awaitAsOneOrNull()?.toInt() } private companion object { diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/SearchDAO.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/SearchDAO.kt index 4b1fdd36b7e5..674b11155a19 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/SearchDAO.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/SearchDAO.kt @@ -17,6 +17,8 @@ */ package com.wire.kalium.persistence.dao +import app.cash.sqldelight.async.coroutines.awaitAsList + import com.wire.kalium.persistence.SearchQueries import com.wire.kalium.persistence.db.ReadDispatcher import kotlinx.coroutines.withContext @@ -72,11 +74,11 @@ internal class SearchDAOImpl internal constructor( ) : SearchDAO { override suspend fun getKnownContacts(): List = withContext(readDispatcher.value) { - searchQueries.selectAllConnectedUsers(mapper = UserSearchEntityMapper::map).executeAsList() + searchQueries.selectAllConnectedUsers(mapper = UserSearchEntityMapper::map).awaitAsList() } override suspend fun searchList(query: String): List = withContext(readDispatcher.value) { - searchQueries.searchByName(query, mapper = UserSearchEntityMapper::map).executeAsList() + searchQueries.searchByName(query, mapper = UserSearchEntityMapper::map).awaitAsList() } override suspend fun getKnownContactsExcludingAConversation(conversationId: ConversationIDEntity): List = @@ -84,7 +86,7 @@ internal class SearchDAOImpl internal constructor( searchQueries.selectAllConnectedUsersNotInConversation( conversationId, mapper = UserSearchEntityMapper::map - ).executeAsList() + ).awaitAsList() } override suspend fun searchListExcludingAConversation( @@ -95,14 +97,14 @@ internal class SearchDAOImpl internal constructor( query, conversationId, mapper = UserSearchEntityMapper::map - ).executeAsList() + ).awaitAsList() } override suspend fun handleSearch(searchQuery: String): List = withContext(readDispatcher.value) { searchQueries.searchByHandle( searchQuery, mapper = UserSearchEntityMapper::map - ).executeAsList() + ).awaitAsList() } override suspend fun handleSearchExcludingAConversation( @@ -113,6 +115,6 @@ internal class SearchDAOImpl internal constructor( searchQuery, conversationId, mapper = UserSearchEntityMapper::map - ).executeAsList() + ).awaitAsList() } } diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/ServiceDAO.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/ServiceDAO.kt index cd8df733d8c5..5a43ce03b2c4 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/ServiceDAO.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/ServiceDAO.kt @@ -17,6 +17,8 @@ */ package com.wire.kalium.persistence.dao +import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull + import app.cash.sqldelight.coroutines.asFlow import app.cash.sqldelight.coroutines.mapToOneOrNull import com.wire.kalium.persistence.ServiceQueries @@ -80,7 +82,7 @@ internal class ServiceDAOImpl( private val writeDispatcher: WriteDispatcher, ) : ServiceDAO { override suspend fun byId(id: BotIdEntity): ServiceEntity? = withContext(readDispatcher.value) { - serviceQueries.byId(id, mapper = ::mapToServiceEntity).executeAsOneOrNull() + serviceQueries.byId(id, mapper = ::mapToServiceEntity).awaitAsOneOrNull() } override suspend fun observeIsServiceMember(id: BotIdEntity, conversationId: ConversationIDEntity): Flow = diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAOImpl.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAOImpl.kt index baf57020a3fe..72f450db02bc 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAOImpl.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAOImpl.kt @@ -18,6 +18,10 @@ package com.wire.kalium.persistence.dao +import app.cash.sqldelight.async.coroutines.awaitAsList +import app.cash.sqldelight.async.coroutines.awaitAsOne +import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull + import app.cash.sqldelight.coroutines.asFlow import com.wire.kalium.persistence.UsersQueries import com.wire.kalium.persistence.cache.FlowCache @@ -228,7 +232,7 @@ class UserDAOImpl internal constructor( } } - private fun insertUser(user: UserEntity): Boolean { + private suspend fun insertUser(user: UserEntity): Boolean { userQueries.insertUser( qualified_id = user.id, name = user.name, @@ -248,7 +252,7 @@ class UserDAOImpl internal constructor( supported_protocols = user.supportedProtocols, active_one_on_one_conversation_id = user.activeOneOnOneConversationId ) - return userQueries.selectChanges().executeAsOne() > 0 + return userQueries.selectChanges().awaitAsOne() > 0 } override suspend fun upsertUsers(users: List) = withContext(writeDispatcher.value) { @@ -301,27 +305,27 @@ class UserDAOImpl internal constructor( withContext(readDispatcher.value) { userQueries.selectMinimizedByQualifiedId(listOf(qualifiedID)) { qualifiedId, name, completeAssetId, userType, accentId -> mapper.toModelMinimized(qualifiedId, name, completeAssetId, userType, accentId) - }.executeAsOneOrNull() + }.awaitAsOneOrNull() } override suspend fun getUsersMinimizedByQualifiedIDs(qualifiedIDs: List): List = withContext(readDispatcher.value) { userQueries.selectMinimizedByQualifiedId(qualifiedIDs) { qualifiedId, name, completeAssetId, userType, accentId -> mapper.toModelMinimized(qualifiedId, name, completeAssetId, userType, accentId) - }.executeAsList() + }.awaitAsList() } override suspend fun getUserDetailsByQualifiedID(qualifiedID: QualifiedIDEntity): UserDetailsEntity? = withContext(readDispatcher.value) { userQueries.selectDetailsByQualifiedId(listOf(qualifiedID)) - .executeAsOneOrNull() + .awaitAsOneOrNull() ?.let { mapper.toDetailsModel(it) } } override suspend fun getUsersDetailsByQualifiedIDList(qualifiedIDList: List): List = withContext(readDispatcher.value) { userQueries.selectDetailsByQualifiedId(qualifiedIDList) - .executeAsList() + .awaitAsList() .map { mapper.toDetailsModel(it) } } @@ -344,7 +348,7 @@ class UserDAOImpl internal constructor( .flowOn(readDispatcher.value) override suspend fun getUsersWithOneOnOneConversation(): List = withContext(readDispatcher.value) { - userQueries.selectUsersWithOneOnOne().executeAsList().map(mapper::toModel) + userQueries.selectUsersWithOneOnOne().awaitAsList().map(mapper::toModel) } override suspend fun deleteUserByQualifiedID(qualifiedID: QualifiedIDEntity) { @@ -358,26 +362,26 @@ class UserDAOImpl internal constructor( ): List = withContext(writeDispatcher.value) { userQueries.transactionWithResult { - val conversationIds = userQueries.selectGroupConversationsUserIsMemberOf(qualifiedID).executeAsList() + val conversationIds = userQueries.selectGroupConversationsUserIsMemberOf(qualifiedID).awaitAsList() safeMarkAsDeletedAndRemoveFromGroupConversation(qualifiedID) conversationIds } } // returns true if any row has been inserted or modified, false if exactly the same data already exists - private fun markUserAsDeleted(qualifiedID: QualifiedIDEntity, userType: UserTypeEntity): Boolean { + private suspend fun markUserAsDeleted(qualifiedID: QualifiedIDEntity, userType: UserTypeEntity): Boolean { userQueries.markUserAsDeleted(qualifiedID, userType) - return userQueries.selectChanges().executeAsOne() > 0 + return userQueries.selectChanges().awaitAsOne() > 0 } // returns true if any row has been inserted or modified, false if exactly the same data already exists - private fun deleteUserFromGroupConversations(qualifiedID: QualifiedIDEntity): Boolean { + private suspend fun deleteUserFromGroupConversations(qualifiedID: QualifiedIDEntity): Boolean { userQueries.deleteUserFromGroupConversations(qualifiedID) - return userQueries.selectChanges().executeAsOne() > 0 + return userQueries.selectChanges().awaitAsOne() > 0 } // returns true if any row has been inserted or modified, false if exactly the same data already exists - private fun safeMarkAsDeletedAndRemoveFromGroupConversation(qualifiedID: QualifiedIDEntity): Boolean { + private suspend fun safeMarkAsDeletedAndRemoveFromGroupConversation(qualifiedID: QualifiedIDEntity): Boolean { return markUserAsDeleted(qualifiedID, UserTypeEntity.NONE) or deleteUserFromGroupConversations(qualifiedID) } @@ -456,7 +460,7 @@ class UserDAOImpl internal constructor( override suspend fun getAllUsersDetailsByTeam(teamId: String): List = withContext(readDispatcher.value) { userQueries.selectUsersByTeam(teamId) - .executeAsList() + .awaitAsList() .map(mapper::toDetailsModel) } @@ -476,12 +480,12 @@ class UserDAOImpl internal constructor( override suspend fun getUsersDetailsWithoutMetadata() = withContext(readDispatcher.value) { userQueries.selectUsersWithoutMetadata() - .executeAsList() + .awaitAsList() .map(mapper::toDetailsModel) } override suspend fun allOtherUsersId(): List = withContext(readDispatcher.value) { - userQueries.userIdsWithoutSelf().executeAsList() + userQueries.userIdsWithoutSelf().awaitAsList() } override suspend fun updateUserSupportedProtocols(selfUserId: QualifiedIDEntity, supportedProtocols: Set) { @@ -517,15 +521,15 @@ class UserDAOImpl internal constructor( override suspend fun isAtLeastOneUserATeamMember(userId: List, teamId: String): Boolean = withContext(readDispatcher.value) { - userQueries.isOneUserATeamMember(userId, teamId).executeAsOneOrNull() ?: false + userQueries.isOneUserATeamMember(userId, teamId).awaitAsOneOrNull() ?: false } override suspend fun getOneOnOnConversationId(userId: UserIDEntity): QualifiedIDEntity? = withContext(readDispatcher.value) { - userQueries.selectOneOnOnConversationId(userId).executeAsOneOrNull()?.active_one_on_one_conversation_id + userQueries.selectOneOnOnConversationId(userId).awaitAsOneOrNull()?.active_one_on_one_conversation_id } override suspend fun getNameAndHandle(userId: UserIDEntity): NameAndHandleEntity? = withContext(readDispatcher.value) { - userQueries.selectNamesAndHandle(userId, ::NameAndHandleEntity).executeAsOneOrNull() + userQueries.selectNamesAndHandle(userId, ::NameAndHandleEntity).awaitAsOneOrNull() } override suspend fun updateTeamId(userId: UserIDEntity, teamId: String) { @@ -533,11 +537,11 @@ class UserDAOImpl internal constructor( } override suspend fun countContactsAmount(selfUserId: QualifiedIDEntity): Int = withContext(readDispatcher.value) { - userQueries.countContacts(selfUserId).executeAsOneOrNull()?.toInt() ?: 0 + userQueries.countContacts(selfUserId).awaitAsOneOrNull()?.toInt() ?: 0 } override suspend fun countTeamMembersAmount(teamId: String): Int = withContext(readDispatcher.value) { - userQueries.countTeamMembersFromTeam(teamId).executeAsOneOrNull()?.toInt() ?: 0 + userQueries.countTeamMembersFromTeam(teamId).awaitAsOneOrNull()?.toInt() ?: 0 } } diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/asset/AssetDAOImpl.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/asset/AssetDAOImpl.kt index 9ecb01a82f3f..8069326e140e 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/asset/AssetDAOImpl.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/asset/AssetDAOImpl.kt @@ -18,6 +18,8 @@ package com.wire.kalium.persistence.dao.asset +import app.cash.sqldelight.async.coroutines.awaitAsList + import app.cash.sqldelight.coroutines.asFlow import com.wire.kalium.persistence.AssetsQueries import com.wire.kalium.persistence.db.ReadDispatcher @@ -105,6 +107,6 @@ class AssetDAOImpl internal constructor( } override suspend fun getAssets(): List = withContext(readDispatcher.value) { - queries.getAssets(mapper::fromAssets).executeAsList() + queries.getAssets(mapper::fromAssets).awaitAsList() } } diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/backup/NomadMessagesDAO.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/backup/NomadMessagesDAO.kt index 4b69fd4bddfa..8d0197f9253a 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/backup/NomadMessagesDAO.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/backup/NomadMessagesDAO.kt @@ -18,6 +18,9 @@ package com.wire.kalium.persistence.dao.backup +import app.cash.sqldelight.async.coroutines.awaitAsOne +import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull + import com.wire.kalium.persistence.ConversationsQueries import com.wire.kalium.persistence.MessageAttachmentsQueries import com.wire.kalium.persistence.MessagesQueries @@ -116,7 +119,7 @@ internal class NomadMessagesDAOImpl internal constructor( ) } - private fun insertPlaceholderUsers(messages: List) { + private suspend fun insertPlaceholderUsers(messages: List) { messages .asSequence() .map { it.payload.senderUserId } @@ -124,7 +127,7 @@ internal class NomadMessagesDAOImpl internal constructor( .forEach { usersQueries.insertOrIgnoreUserId(it) } } - private fun insertPlaceholderConversations(messages: List) { + private suspend fun insertPlaceholderConversations(messages: List) { messages .groupBy { it.conversationId } .forEach { (conversationId, conversationMessages) -> @@ -135,7 +138,7 @@ internal class NomadMessagesDAOImpl internal constructor( } } - private fun insertMessages( + private suspend fun insertMessages( messages: List, lastReadDatesByConversation: Map, ): Int = @@ -143,7 +146,7 @@ internal class NomadMessagesDAOImpl internal constructor( insertMessageWithContentOrThrow(message, lastReadDatesByConversation) } - private fun insertMessageWithContentOrThrow( + private suspend fun insertMessageWithContentOrThrow( message: NomadMessageToInsert, lastReadDatesByConversation: Map, ): Boolean { @@ -161,7 +164,7 @@ internal class NomadMessagesDAOImpl internal constructor( expire_after_millis = null, self_deletion_end_date = null, ) - val insertedMessage = messagesQueries.selectChanges().executeAsOne() > 0 + val insertedMessage = messagesQueries.selectChanges().awaitAsOne() > 0 if (!insertedMessage) { return false } @@ -208,16 +211,19 @@ internal class NomadMessagesDAOImpl internal constructor( } } -private fun List.lastReadDatesByConversation( +private suspend fun List.lastReadDatesByConversation( conversationsQueries: ConversationsQueries, -): Map = - asSequence() +): Map { + val conversations = asSequence() .map { it.conversationId } .distinct() - .associateWith { conversationId -> - conversationsQueries.getConversationLastReadDate(conversationId).executeAsOneOrNull() + .toList() + + return conversations.associateWith { conversationId -> + conversationsQueries.getConversationLastReadDate(conversationId).awaitAsOneOrNull() ?: Instant.DISTANT_PAST - } + } +} private class NomadUnreadEventWriter( private val messagesQueries: MessagesQueries, @@ -225,7 +231,7 @@ private class NomadUnreadEventWriter( private val selfUserId: UserIDEntity, ) { - fun insertUnreadEvent( + suspend fun insertUnreadEvent( message: NomadMessageToInsert, lastReadDatesByConversation: Map, ) { @@ -264,13 +270,13 @@ private class NomadUnreadEventWriter( } } - private fun insertUnreadTextLikeContent( + private suspend fun insertUnreadTextLikeContent( message: NomadMessageToInsert, quotedMessageId: String?, mentions: List, ) { val isQuotingSelfUser = quotedMessageId?.let { quotedId -> - messagesQueries.getMessageSenderId(quotedId, message.conversationId).executeAsOneOrNull() == selfUserId + messagesQueries.getMessageSenderId(quotedId, message.conversationId).awaitAsOneOrNull() == selfUserId } ?: false val unreadType = when { @@ -293,7 +299,7 @@ private class NomadMessageContentWriter( private val messageAttachmentsQueries: MessageAttachmentsQueries, ) { - fun insertRegularContent(message: NomadMessageToInsert): Boolean { + private suspend fun insertRegularContent(message: NomadMessageToInsert): Boolean { val content = message.payload return when (content) { is SyncableMessagePayloadEntity.Text -> insertTextContent(message, content) @@ -308,7 +314,7 @@ private class NomadMessageContentWriter( } } - private fun insertTextContent( + private suspend fun insertTextContent( message: NomadMessageToInsert, content: SyncableMessagePayloadEntity.Text, ): Boolean { @@ -319,7 +325,7 @@ private class NomadMessageContentWriter( quoted_message_id = content.quotedMessageId, is_quote_verified = true, ) - val insertedContent = messagesQueries.selectChanges().executeAsOne() > 0 + val insertedContent = messagesQueries.selectChanges().awaitAsOne() > 0 content.mentions.forEach { messagesQueries.insertMessageMention( message_id = message.id, @@ -333,7 +339,7 @@ private class NomadMessageContentWriter( } @Suppress("ComplexCondition") - private fun insertAssetContent( + private suspend fun insertAssetContent( message: NomadMessageToInsert, content: SyncableMessagePayloadEntity.Asset, ): Boolean { @@ -367,10 +373,10 @@ private class NomadMessageContentWriter( asset_duration_ms = content.durationMs, asset_normalized_loudness = content.normalizedLoudness, ) - return messagesQueries.selectChanges().executeAsOne() > 0 + return messagesQueries.selectChanges().awaitAsOne() > 0 } - private fun insertLocationContent( + private suspend fun insertLocationContent( message: NomadMessageToInsert, content: SyncableMessagePayloadEntity.Location, ): Boolean { @@ -389,10 +395,10 @@ private class NomadMessageContentWriter( name = content.name, zoom = content.zoom, ) - return messagesQueries.selectChanges().executeAsOne() > 0 + return messagesQueries.selectChanges().awaitAsOne() > 0 } - private fun insertMultipartContent( + private suspend fun insertMultipartContent( message: NomadMessageToInsert, content: SyncableMessagePayloadEntity.Multipart, ): Boolean { @@ -403,7 +409,7 @@ private class NomadMessageContentWriter( quoted_message_id = content.quotedMessageId, is_quote_verified = true, ) - val insertedContent = messagesQueries.selectChanges().executeAsOne() > 0 + val insertedContent = messagesQueries.selectChanges().awaitAsOne() > 0 content.mentions.forEach { messagesQueries.insertMessageMention( message_id = message.id, @@ -434,7 +440,7 @@ private class NomadMessageContentWriter( return insertedContent } - private fun insertUnknownContent( + private suspend fun insertUnknownContent( messageId: String, conversationId: QualifiedIDEntity, typeName: String?, @@ -445,6 +451,6 @@ private class NomadMessageContentWriter( unknown_encoded_data = null, unknown_type_name = typeName, ) - return messagesQueries.selectChanges().executeAsOne() > 0 + return messagesQueries.selectChanges().awaitAsOne() > 0 } } diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/backup/RemoteBackupChangeLogDAOImpl.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/backup/RemoteBackupChangeLogDAOImpl.kt index ab98cabb12a4..a15bcafc0e3e 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/backup/RemoteBackupChangeLogDAOImpl.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/backup/RemoteBackupChangeLogDAOImpl.kt @@ -18,6 +18,7 @@ package com.wire.kalium.persistence.dao.backup +import app.cash.sqldelight.async.coroutines.awaitAsList import app.cash.sqldelight.coroutines.asFlow import com.wire.kalium.persistence.RemotebackupChangeLogQueries import com.wire.kalium.persistence.dao.QualifiedIDEntity @@ -124,7 +125,7 @@ internal class RemoteBackupChangeLogDAOImpl( override suspend fun getPendingChanges(): List = withContext(readDispatcher.value) { - queries.getPendingChanges(mapper = mapper::toChangeLogEntry).executeAsList() + queries.getPendingChanges(mapper = mapper::toChangeLogEntry).awaitAsList() } override suspend fun getLastPendingChangesBatch(limit: Long): ChangeLogSyncBatch = @@ -133,11 +134,11 @@ internal class RemoteBackupChangeLogDAOImpl( val events = queries.getLastPendingChangesWithPayload( limit = limit, mapper = mapper::toChangeLogSyncEvent - ).executeAsList() + ).awaitAsList() val conversationMetadata = queries.getConversationMetadataForLastPendingChanges( limit = limit, mapper = mapper::toConversationMetadataSyncEntity - ).executeAsList() + ).awaitAsList() ChangeLogSyncBatch( events = events, conversationMetadata = conversationMetadata diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/call/CallDAOImpl.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/call/CallDAOImpl.kt index c044d586ff49..1d8b87e66b0a 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/call/CallDAOImpl.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/call/CallDAOImpl.kt @@ -18,6 +18,9 @@ package com.wire.kalium.persistence.dao.call +import app.cash.sqldelight.async.coroutines.awaitAsOne +import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull + import app.cash.sqldelight.coroutines.asFlow import com.wire.kalium.persistence.CallsQueries import com.wire.kalium.persistence.dao.QualifiedIDEntity @@ -110,7 +113,7 @@ internal class CallDAOImpl( .flowOn(readDispatcher.value) override suspend fun getEstablishedCall(): CallEntity = withContext(readDispatcher.value) { - callsQueries.selectEstablishedCalls(mapper = mapper::fromCalls).executeAsOne() + callsQueries.selectEstablishedCalls(mapper = mapper::fromCalls).awaitAsOne() } override suspend fun observeOngoingCalls(): Flow> = @@ -129,12 +132,12 @@ internal class CallDAOImpl( } override suspend fun getCallerIdByConversationId(conversationId: QualifiedIDEntity): String? = withContext(readDispatcher.value) { - callsQueries.lastCallCallerIdByConversationId(conversationId).executeAsOneOrNull() + callsQueries.lastCallCallerIdByConversationId(conversationId).awaitAsOneOrNull() } override suspend fun getCallStatusByConversationId(conversationId: QualifiedIDEntity): CallEntity.Status? = withContext(readDispatcher.value) { - callsQueries.lastCallStatusByConversationId(conversationId).executeAsOneOrNull() + callsQueries.lastCallStatusByConversationId(conversationId).awaitAsOneOrNull() } override suspend fun getLastClosedCallByConversationId(conversationId: QualifiedIDEntity): Flow = @@ -147,7 +150,7 @@ internal class CallDAOImpl( conversationId: QualifiedIDEntity ): ConversationEntity.Type? = withContext(readDispatcher.value) { callsQueries.selectLastCallConversionTypeByConversationId(conversationId) - .executeAsOneOrNull() + .awaitAsOneOrNull() } override suspend fun updateOpenCallsToClosedStatus() { diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/client/ClientDAOImpl.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/client/ClientDAOImpl.kt index 14db869c42a8..420aa406450d 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/client/ClientDAOImpl.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/client/ClientDAOImpl.kt @@ -18,6 +18,10 @@ package com.wire.kalium.persistence.dao.client +import app.cash.sqldelight.async.coroutines.awaitAsList +import app.cash.sqldelight.async.coroutines.awaitAsOne +import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull + import app.cash.sqldelight.coroutines.asFlow import com.wire.kalium.persistence.ClientsQueries import com.wire.kalium.persistence.dao.ConversationIDEntity @@ -81,7 +85,7 @@ internal class ClientDAOImpl internal constructor( override suspend fun insertClient(client: InsertClientParam): Unit = withContext(writeDispatcher.value) { clientsQueries.transaction { insert(client) - val changes = clientsQueries.selectChanges().executeAsOne() + val changes = clientsQueries.selectChanges().awaitAsOne() if (changes == 0L) { // rollback the transaction if no changes were made so that it doesn't notify other queries if not needed this.rollback() @@ -90,7 +94,7 @@ internal class ClientDAOImpl internal constructor( } // returns true if any row has been inserted or modified, false if exactly the same data already exists - private fun insert(client: InsertClientParam): Boolean = with(client) { + private suspend fun insert(client: InsertClientParam): Boolean = with(client) { clientsQueries.insertClient( user_id = userId, id = id, @@ -105,7 +109,7 @@ internal class ClientDAOImpl internal constructor( mls_public_keys = mlsPublicKeys, is_async_notifications_capable = isAsyncNotificationsCapable ) - clientsQueries.selectChanges().executeAsOne() > 0 + clientsQueries.selectChanges().awaitAsOne() > 0 } override suspend fun insertClients(clients: List) = withContext(writeDispatcher.value) { @@ -127,7 +131,7 @@ internal class ClientDAOImpl internal constructor( val clients = it.value clientsQueries.deeteCliuentsOfUser(userId, clients) } - clientsQueries.usersWithNotClients(redundantClientsOfUsers.keys).executeAsList() + clientsQueries.usersWithNotClients(redundantClientsOfUsers.keys).awaitAsList() } } @@ -136,7 +140,7 @@ internal class ClientDAOImpl internal constructor( clients.groupBy { it.userId }.forEach { (userId, clientsList) -> val anyInsertedOrModified = clientsList.map { client -> insert(client) }.any { it } clientsQueries.deleteClientsOfUserExcept(userId, clientsList.map { it.id }) - val anyDeleted = clientsQueries.selectChanges().executeAsOne() > 0 + val anyDeleted = clientsQueries.selectChanges().awaitAsOne() > 0 if (!anyInsertedOrModified && !anyDeleted) { // rollback the transaction if no changes were made so that it doesn't notify other queries if not needed this.rollback() @@ -172,20 +176,20 @@ internal class ClientDAOImpl internal constructor( userIds: Set ): Map> = withContext(readDispatcher.value) { clientsQueries.selectRecipientsByConversationAndUserId(conversationId, userIds, mapper::fromClient) - .executeAsList() + .awaitAsList() .groupBy { it.userId } } // TODO(MO): instead of selecting as list and then grouping, do the grouping in SQL directly override suspend fun selectAllClients(): Map> = withContext(readDispatcher.value) { clientsQueries.selectAllClients(mapper::fromClient) - .executeAsList() + .awaitAsList() .groupBy { it.userId } } override suspend fun isMLSCapable(userId: QualifiedIDEntity, clientId: String): Boolean? = withContext(readDispatcher.value) { clientsQueries.isClientMLSCapable(userId, clientId) - .executeAsOneOrNull() + .awaitAsOneOrNull() } override suspend fun getClientsOfUserByQualifiedIDFlow(qualifiedID: QualifiedIDEntity): Flow> = @@ -196,7 +200,7 @@ internal class ClientDAOImpl internal constructor( override suspend fun getClientsOfUserByQualifiedID(qualifiedID: QualifiedIDEntity): List = withContext(readDispatcher.value) { clientsQueries.selectAllClientsByUserId(qualifiedID, mapper = mapper::fromClient) - .executeAsList() + .awaitAsList() } override suspend fun observeClientsByUserId(qualifiedID: QualifiedIDEntity): Flow> = withContext(readDispatcher.value) { @@ -210,7 +214,7 @@ internal class ClientDAOImpl internal constructor( ids: List ): Map> = withContext(readDispatcher.value) { clientsQueries.selectAllClientsByUserIdList(ids, mapper = mapper::fromClient) - .executeAsList() + .awaitAsList() .groupBy { it.userId } } @@ -230,14 +234,14 @@ internal class ClientDAOImpl internal constructor( override suspend fun getClientsOfConversation(id: QualifiedIDEntity): Map> = withContext(readDispatcher.value) { clientsQueries.selectAllClientsByConversation(id, mapper = mapper::fromClient) - .executeAsList() + .awaitAsList() .groupBy { it.userId } } override suspend fun conversationRecipient(ids: QualifiedIDEntity): Map> = withContext(readDispatcher.value) { clientsQueries.conversationRecipets(ids, mapper = mapper::fromClient) - .executeAsList() + .awaitAsList() .groupBy { it.userId } } } diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt index 6b12acb7f009..203a275fe416 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt @@ -18,6 +18,10 @@ package com.wire.kalium.persistence.dao.conversation +import app.cash.sqldelight.async.coroutines.awaitAsList +import app.cash.sqldelight.async.coroutines.awaitAsOne +import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull + import app.cash.sqldelight.coroutines.asFlow import com.wire.kalium.persistence.ConversationDetailsQueries import com.wire.kalium.persistence.ConversationDetailsWithEventsQueries @@ -107,25 +111,25 @@ internal class ConversationDAOImpl internal constructor( // endregion override suspend fun getSelfConversationId(protocol: ConversationEntity.Protocol) = withContext(readDispatcher.value) { - conversationQueries.selfConversationId(protocol).executeAsOneOrNull() + conversationQueries.selfConversationId(protocol).awaitAsOneOrNull() } override suspend fun getE2EIConversationClientInfoByClientId(clientId: String): E2EIConversationClientInfoEntity? = withContext(readDispatcher.value) { conversationQueries.getMLSGroupIdAndUserIdByClientId(clientId, conversationMapper::toE2EIConversationClient) - .executeAsOneOrNull() + .awaitAsOneOrNull() } override suspend fun getMLSGroupIdByUserId(userId: UserIDEntity): String? = withContext(readDispatcher.value) { conversationQueries.getMLSGroupIdByUserId(userId) - .executeAsOneOrNull() + .awaitAsOneOrNull() } override suspend fun getMLSGroupIdByConversationId(conversationId: QualifiedIDEntity): String? = withContext(readDispatcher.value) { conversationQueries.getMLSGroupIdByConversationId(conversationId) - .executeAsOneOrNull() + .awaitAsOneOrNull() ?.mls_group_id } @@ -190,7 +194,7 @@ internal class ConversationDAOImpl internal constructor( } } - private fun nonSuspendingInsertConversation(conversationEntity: ConversationEntity) { + private suspend fun nonSuspendingInsertConversation(conversationEntity: ConversationEntity) { with(conversationEntity) { conversationQueries.insertConversation( qualified_id = id, @@ -366,7 +370,7 @@ internal class ConversationDAOImpl internal constructor( } override suspend fun getCellName(conversationId: QualifiedIDEntity): String? = withContext(readDispatcher.value) { - conversationQueries.getCellName(conversationId).executeAsOneOrNull()?.wire_cell + conversationQueries.getCellName(conversationId).awaitAsOneOrNull()?.wire_cell } override suspend fun getConversationIds( @@ -375,14 +379,14 @@ internal class ConversationDAOImpl internal constructor( teamId: String? ): List { return withContext(readDispatcher.value) { - conversationQueries.selectConversationIds(protocol, type, teamId).executeAsList() + conversationQueries.selectConversationIds(protocol, type, teamId).awaitAsList() } } override suspend fun getTeamConversationIdsReadyToCompleteMigration(teamId: String): List { return withContext(readDispatcher.value) { conversationQueries.selectAllTeamProteusConversationsReadyForMigration(teamId) - .executeAsList() + .awaitAsList() .map { it.qualified_id } } } @@ -392,7 +396,7 @@ internal class ConversationDAOImpl internal constructor( protocol: ConversationEntity.Protocol ): List = withContext(readDispatcher.value) { - conversationQueries.selectOneOnOneConversationIdsByProtocol(protocol, userId).executeAsList() + conversationQueries.selectOneOnOneConversationIdsByProtocol(protocol, userId).awaitAsList() } override suspend fun observeOneOnOneConversationWithOtherUser(userId: UserIDEntity): Flow { @@ -411,28 +415,28 @@ internal class ConversationDAOImpl internal constructor( override suspend fun getConversationProtocolInfo(qualifiedID: QualifiedIDEntity): ConversationEntity.ProtocolInfo? = withContext(readDispatcher.value) { - conversationQueries.selectProtocolInfoByQualifiedId(qualifiedID, conversationMapper::mapProtocolInfo).executeAsOneOrNull() + conversationQueries.selectProtocolInfoByQualifiedId(qualifiedID, conversationMapper::mapProtocolInfo).awaitAsOneOrNull() } override suspend fun getConversationByGroupID(groupID: String): ConversationEntity? = withContext(readDispatcher.value) { conversationQueries.selectByGroupId(groupID, mapper = conversationMapper::toConversationEntity) - .executeAsOneOrNull() + .awaitAsOneOrNull() } override suspend fun getConversationIdByGroupID(groupID: String) = withContext(readDispatcher.value) { - conversationQueries.getConversationIdByGroupId(groupID).executeAsOneOrNull() + conversationQueries.getConversationIdByGroupId(groupID).awaitAsOneOrNull() } override suspend fun getConversationsByGroupState(groupState: ConversationEntity.GroupState): List = withContext(readDispatcher.value) { conversationQueries.selectByGroupState(groupState, conversationMapper::fromViewToModel) - .executeAsList() + .awaitAsList() } override suspend fun deleteConversationByQualifiedID(qualifiedID: QualifiedIDEntity) = withContext(writeDispatcher.value) { conversationQueries.transactionWithResult { conversationQueries.deleteConversation(qualifiedID) - conversationQueries.selectChanges().executeAsOne() > 0 + conversationQueries.selectChanges().awaitAsOne() > 0 } } @@ -538,7 +542,7 @@ internal class ConversationDAOImpl internal constructor( conversationQueries.selectByKeyingMaterialUpdate( ConversationEntity.GroupState.ESTABLISHED, DateTimeUtil.currentInstant().minus(threshold) - ).executeAsList() + ).awaitAsList() } override suspend fun setProposalTimer(proposalTimer: ProposalTimerEntity) { @@ -563,7 +567,7 @@ internal class ConversationDAOImpl internal constructor( override suspend fun whoDeletedMeInConversation(conversationId: QualifiedIDEntity, selfUserIdString: String): UserIDEntity? = withContext(readDispatcher.value) { - conversationQueries.whoDeletedMeInConversation(conversationId, selfUserIdString).executeAsOneOrNull() + conversationQueries.whoDeletedMeInConversation(conversationId, selfUserIdString).awaitAsOneOrNull() } override suspend fun updateConversationName(conversationId: QualifiedIDEntity, conversationName: String, dateTime: Instant) { @@ -590,12 +594,12 @@ internal class ConversationDAOImpl internal constructor( protocol, cipherSuite, conversationId - ).executeAsOne() > 0 + ).awaitAsOne() > 0 } } override suspend fun getConversationsByUserId(userId: UserIDEntity): List = withContext(readDispatcher.value) { - memberQueries.selectConversationsByMember(userId, conversationMapper::fromViewToModel).executeAsList() + memberQueries.selectConversationsByMember(userId, conversationMapper::fromViewToModel).awaitAsList() } override suspend fun updateConversationReceiptMode(conversationID: QualifiedIDEntity, receiptMode: ConversationEntity.ReceiptMode) { @@ -641,7 +645,7 @@ internal class ConversationDAOImpl internal constructor( } override suspend fun getConversationsWithoutMetadata(): List = withContext(readDispatcher.value) { - conversationQueries.selectConversationIdsWithoutMetadata().executeAsList() + conversationQueries.selectConversationIdsWithoutMetadata().awaitAsList() } override suspend fun updateDegradedConversationNotifiedFlag(conversationId: QualifiedIDEntity, updateFlag: Boolean) { @@ -683,7 +687,7 @@ internal class ConversationDAOImpl internal constructor( ) = withContext(writeDispatcher.value) { conversationQueries.transactionWithResult { conversationQueries.updateLegalHoldStatus(legalHoldStatus, conversationId) - conversationQueries.selectChanges().executeAsOne() > 0 + conversationQueries.selectChanges().awaitAsOne() > 0 } } @@ -692,7 +696,7 @@ internal class ConversationDAOImpl internal constructor( withContext(writeDispatcher.value) { conversationQueries.transactionWithResult { conversationQueries.upsertLegalHoldStatusChangeNotified(conversationId, notified) - conversationQueries.selectChanges().executeAsOne() > 0 + conversationQueries.selectChanges().awaitAsOne() > 0 } } @@ -712,16 +716,16 @@ internal class ConversationDAOImpl internal constructor( withContext(readDispatcher.value) { conversationQueries .getEstablishedSelfMLSGroupId() - .executeAsOneOrNull() + .awaitAsOneOrNull() ?.mls_group_id } override suspend fun selectGroupStatusMembersNamesAndHandles(groupID: String): EpochChangesDataEntity? = withContext(readDispatcher.value) { conversationQueries.transactionWithResult { - val (conversationId, mlsVerificationStatus) = conversationQueries.conversationIDByGroupId(groupID).executeAsOneOrNull() + val (conversationId, mlsVerificationStatus) = conversationQueries.conversationIDByGroupId(groupID).awaitAsOneOrNull() ?: return@transactionWithResult null - memberQueries.selectMembersNamesAndHandle(conversationId).executeAsList() + memberQueries.selectMembersNamesAndHandle(conversationId).awaitAsList() .let { members -> val membersMap = members.associate { it.user to NameAndHandleEntity(it.name, it.handle) } EpochChangesDataEntity( @@ -734,7 +738,7 @@ internal class ConversationDAOImpl internal constructor( } override suspend fun isAChannel(conversationId: QualifiedIDEntity): Boolean = withContext(readDispatcher.value) { - conversationQueries.selectIsChannel(conversationId).executeAsOneOrNull() ?: false + conversationQueries.selectIsChannel(conversationId).awaitAsOneOrNull() ?: false } override suspend fun updateChannelAddPermission( @@ -747,7 +751,7 @@ internal class ConversationDAOImpl internal constructor( } override suspend fun hasConversationWithCell() = withContext(readDispatcher.value) { - conversationQueries.hasConversationWithCell().executeAsOne() + conversationQueries.hasConversationWithCell().awaitAsOne() } override suspend fun updateReadDateAndGetHasUnreadEvents( @@ -757,7 +761,7 @@ internal class ConversationDAOImpl internal constructor( conversationQueries.transactionWithResult { unreadEventsQueries.deleteUnreadEvents(date, conversationID) conversationQueries.updateConversationReadDate(date, conversationID) - unreadEventsQueries.getHasUnreadEventsForConversation(conversationID).executeAsOneOrNull() ?: false + unreadEventsQueries.getHasUnreadEventsForConversation(conversationID).awaitAsOneOrNull() ?: false } } @@ -772,7 +776,7 @@ internal class ConversationDAOImpl internal constructor( conversationDates.forEach { (conversationId, date) -> unreadEventsQueries.deleteUnreadEvents(date, conversationId) conversationQueries.updateConversationReadDate(date, conversationId) - put(conversationId, unreadEventsQueries.getHasUnreadEventsForConversation(conversationId).executeAsOneOrNull() ?: false) + put(conversationId, unreadEventsQueries.getHasUnreadEventsForConversation(conversationId).awaitAsOneOrNull() ?: false) } } } @@ -780,11 +784,11 @@ internal class ConversationDAOImpl internal constructor( override suspend fun getMLSConversationsByDomain(domain: String): List = withContext(readDispatcher.value) { - conversationQueries.selectAllMLSConversationsByDomain(domain, conversationMapper::toConversationEntity).executeAsList() + conversationQueries.selectAllMLSConversationsByDomain(domain, conversationMapper::toConversationEntity).awaitAsList() } override suspend fun getCellGroupConversations(): List = withContext(readDispatcher.value) { - conversationQueries.selectCellGroupConversations(conversationMapper::toConversationEntity).executeAsList() + conversationQueries.selectCellGroupConversations(conversationMapper::toConversationEntity).awaitAsList() } override suspend fun getCellGroupConversationsPaged(limit: Int, offset: Int, query: String): List = diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationMetaDataDAOImpl.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationMetaDataDAOImpl.kt index 7f562c2726dd..19f4b74565b8 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationMetaDataDAOImpl.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationMetaDataDAOImpl.kt @@ -17,6 +17,9 @@ */ package com.wire.kalium.persistence.dao.conversation +import app.cash.sqldelight.async.coroutines.awaitAsOne +import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull + import com.wire.kalium.persistence.ConversationMetadataQueries import com.wire.kalium.persistence.dao.QualifiedIDEntity import com.wire.kalium.persistence.db.ReadDispatcher @@ -32,7 +35,7 @@ class ConversationMetaDataDAOImpl internal constructor( override suspend fun isInformedAboutDegradedMLSVerification( conversationId: QualifiedIDEntity ): Boolean = withContext(readDispatcher.value) { - conversationMetadataQueries.isInformedAboutDegradedMLSVerification(conversationId).executeAsOne() + conversationMetadataQueries.isInformedAboutDegradedMLSVerification(conversationId).awaitAsOne() } override suspend fun setInformedAboutDegradedMLSVerificationFlag(conversationId: QualifiedIDEntity, isInformed: Boolean) { @@ -44,7 +47,7 @@ class ConversationMetaDataDAOImpl internal constructor( override suspend fun typeAndProtocolInfo( conversationId: QualifiedIDEntity ): ConversationTypeAndProtocolInfo? = withContext(readDispatcher.value) { - conversationMetadataQueries.typeAndProtocolInfo(conversationId).executeAsOneOrNull()?.let { + conversationMetadataQueries.typeAndProtocolInfo(conversationId).awaitAsOneOrNull()?.let { ConversationTypeAndProtocolInfo( type = it.type, isChannel = it.is_channel, diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOImpl.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOImpl.kt index 3ae225a125df..54bde04b4ecc 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOImpl.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOImpl.kt @@ -17,6 +17,9 @@ */ package com.wire.kalium.persistence.dao.conversation.folder +import app.cash.sqldelight.async.coroutines.awaitAsList +import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull + import app.cash.sqldelight.coroutines.asFlow import com.wire.kalium.persistence.ConversationFolder import com.wire.kalium.persistence.ConversationFoldersQueries @@ -61,7 +64,7 @@ class ConversationFolderDAOImpl internal constructor( } override suspend fun getFoldersWithConversations(): List = withContext(readDispatcher.value) { - val labeledConversationList = conversationFoldersQueries.getAllFoldersWithConversations().executeAsList().map(::toEntity) + val labeledConversationList = conversationFoldersQueries.getAllFoldersWithConversations().awaitAsList().map(::toEntity) val folderMap = labeledConversationList.groupBy { it.folderId }.mapValues { entry -> val folderId = entry.key @@ -109,7 +112,7 @@ class ConversationFolderDAOImpl internal constructor( return conversationFoldersQueries.getFavoriteFolder { id, name, folderType -> ConversationFolderEntity(id, name, folderType) } - .executeAsOneOrNull() + .awaitAsOneOrNull() } override suspend fun updateConversationFolders(folderWithConversationsList: List) = diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/event/EventDAOImpl.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/event/EventDAOImpl.kt index c82046d1c1ee..8ec91a8b299c 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/event/EventDAOImpl.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/event/EventDAOImpl.kt @@ -17,6 +17,8 @@ */ package com.wire.kalium.persistence.dao.event +import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull + import app.cash.sqldelight.coroutines.asFlow import com.wire.kalium.persistence.EventsQueries import com.wire.kalium.persistence.db.ReadDispatcher @@ -83,7 +85,7 @@ class EventDAOImpl( override suspend fun getEventById(id: String): EventEntity? { return withContext(readDispatcher.value) { eventsQueries.getById(id, ::mapEvent) - .executeAsOneOrNull() + .awaitAsOneOrNull() } } diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/member/MemberDAO.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/member/MemberDAO.kt index 92f8c58a9a59..134c8f4ce24b 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/member/MemberDAO.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/member/MemberDAO.kt @@ -17,6 +17,10 @@ */ package com.wire.kalium.persistence.dao.member +import app.cash.sqldelight.async.coroutines.awaitAsList +import app.cash.sqldelight.async.coroutines.awaitAsOne +import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull + import app.cash.sqldelight.coroutines.asFlow import com.wire.kalium.persistence.ConversationsQueries import com.wire.kalium.persistence.MembersQueries @@ -87,7 +91,7 @@ internal class MemberDAOImpl internal constructor( override suspend fun insertMember(member: MemberEntity, conversationID: QualifiedIDEntity) = withContext(writeDispatcher.value) { memberQueries.transaction { userQueries.insertOrIgnoreUserId(member.user) - val conversationExist = conversationsQueries.selectByQualifiedId(conversationID).executeAsList().firstOrNull() != null + val conversationExist = conversationsQueries.selectByQualifiedId(conversationID).awaitAsList().firstOrNull() != null if (conversationExist) { memberQueries.insertMember(member.user, conversationID, member.role) } else { @@ -110,9 +114,9 @@ internal class MemberDAOImpl internal constructor( nonSuspendInsertMembersWithQualifiedId(memberList, conversationID) } - private fun nonSuspendInsertMembersWithQualifiedId(memberList: List, conversationID: QualifiedIDEntity) = + private suspend fun nonSuspendInsertMembersWithQualifiedId(memberList: List, conversationID: QualifiedIDEntity) = memberQueries.transaction { - val conversationExist = conversationsQueries.selectByQualifiedId(conversationID).executeAsList().firstOrNull() != null + val conversationExist = conversationsQueries.selectByQualifiedId(conversationID).awaitAsList().firstOrNull() != null for (member: MemberEntity in memberList) { userQueries.insertOrIgnoreUserId(member.user) if (conversationExist) { @@ -128,7 +132,7 @@ internal class MemberDAOImpl internal constructor( override suspend fun insertMembers(memberList: List, groupId: String) { withContext(writeDispatcher.value) { - conversationsQueries.selectByGroupId(groupId).executeAsOneOrNull()?.let { + conversationsQueries.selectByGroupId(groupId).awaitAsOneOrNull()?.let { nonSuspendInsertMembersWithQualifiedId(memberList, it.qualified_id) } } @@ -147,7 +151,7 @@ internal class MemberDAOImpl internal constructor( withContext(writeDispatcher.value) { memberQueries.transactionWithResult { memberQueries.deleteMembers(conversationID, userIDList) - memberQueries.selectChanges().executeAsOne() + memberQueries.selectChanges().awaitAsOne() } } @@ -173,7 +177,7 @@ internal class MemberDAOImpl internal constructor( ) = withContext(writeDispatcher.value) { memberQueries.transaction { conversationsQueries.updateConversationType(ConversationEntity.Type.ONE_ON_ONE, conversationID) - val conversationRecordExist = conversationsQueries.selectChanges().executeAsOne() != 0L + val conversationRecordExist = conversationsQueries.selectChanges().awaitAsOne() != 0L if (conversationRecordExist) { memberQueries.insertMember(member.user, conversationID, member.role) } else { @@ -196,7 +200,7 @@ internal class MemberDAOImpl internal constructor( withContext(writeDispatcher.value) { memberQueries.transaction { val membersToDelete = memberQueries.selectAllMembersByConversation(conversationID) - .executeAsList() + .awaitAsList() .filter { member -> memberList.none { it.user == member.user } } memberList.forEach { member -> @@ -215,7 +219,7 @@ internal class MemberDAOImpl internal constructor( secondDomain: String ): Map> = withContext(readDispatcher.value) { memberQueries.selectFederatedMembersWithOneOfDomainsFromGroupConversation(firstDomain, secondDomain) - .executeAsList() + .awaitAsList() .groupBy { it.conversation } .filter { (_, members) -> members.any { it.user.domain == firstDomain } && @@ -228,25 +232,25 @@ internal class MemberDAOImpl internal constructor( domain: String, ): Map = withContext(readDispatcher.value) { memberQueries.selectFederatedMembersFromOneOnOneConversations(domain) - .executeAsList() + .awaitAsList() .associateBy({ it.conversation }, { it.user }) } override suspend fun selectMembersNameAndHandle(conversationId: QualifiedIDEntity) = withContext(readDispatcher.value) { - memberQueries.selectMembersNamesAndHandle(conversationId).executeAsList() + memberQueries.selectMembersNamesAndHandle(conversationId).awaitAsList() .let { members -> members.associate { it.user to NameAndHandleEntity(it.name, it.handle) } } } override suspend fun getAllMembers(): List = withContext(readDispatcher.value) { memberQueries.selectAllMembers() - .executeAsList() + .awaitAsList() .map { it.user } } override suspend fun getConversationMembers(conversationId: QualifiedIDEntity): List = withContext(readDispatcher.value) { memberQueries.selectAllMembersByConversation(conversationId) - .executeAsList() + .awaitAsList() .map { it.user } } } diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt index 6f7a080c678d..49a1f1e3a758 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt @@ -18,6 +18,10 @@ package com.wire.kalium.persistence.dao.message +import app.cash.sqldelight.async.coroutines.awaitAsList +import app.cash.sqldelight.async.coroutines.awaitAsOne +import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull + import app.cash.sqldelight.coroutines.asFlow import com.wire.kalium.persistence.ConversationsQueries import com.wire.kalium.persistence.MessageAssetTransferStatus @@ -132,8 +136,8 @@ internal class MessageDAOImpl internal constructor( nonSuspendNeedsToBeNotified(id, conversationId) } - private fun nonSuspendNeedsToBeNotified(id: String, conversationId: QualifiedIDEntity) = - queries.needsToBeNotified(id, conversationId).executeAsList().firstOrNull() == 1L + private suspend fun nonSuspendNeedsToBeNotified(id: String, conversationId: QualifiedIDEntity) = + queries.needsToBeNotified(id, conversationId).awaitAsList().firstOrNull() == 1L override suspend fun insertOrIgnoreMessages( messages: List, @@ -166,7 +170,7 @@ internal class MessageDAOImpl internal constructor( /** * Be careful and run this operation in ONE wrapping transaction. */ - private fun insertInDB(message: MessageEntity, withUnreadEvents: Boolean = true, checkAssetUpdate: Boolean = true) { + private suspend fun insertInDB(message: MessageEntity, withUnreadEvents: Boolean = true, checkAssetUpdate: Boolean = true) { // do not add withContext if (!updateIdIfAlreadyExists(message)) { if (checkAssetUpdate && isValidAssetMessageUpdate(message)) { @@ -190,7 +194,7 @@ internal class MessageDAOImpl internal constructor( - [MessageEntityContent.ConversationStartedUnverifiedWarning] */ @Suppress("ComplexMethod") - private fun updateIdIfAlreadyExists(message: MessageEntity): Boolean = + private suspend fun updateIdIfAlreadyExists(message: MessageEntity): Boolean = when (message.content) { is MessageEntityContent.MemberChange, is MessageEntityContent.ConversationRenamed, is MessageEntityContent.ConversationStartedUnverifiedWarning, @@ -219,7 +223,7 @@ internal class MessageDAOImpl internal constructor( } messagesQuery - .executeAsList() + .awaitAsList() .firstOrNull { LocalId.check(it.id) && when (messageContent) { is MessageEntityContent.MemberChange -> @@ -279,7 +283,7 @@ internal class MessageDAOImpl internal constructor( } override suspend fun getMessageById(id: String, conversationId: QualifiedIDEntity): MessageEntity? = withContext(readDispatcher.value) { - queries.selectById(id, conversationId, mapper::toEntityMessageFromView).executeAsOneOrNull() + queries.selectById(id, conversationId, mapper::toEntityMessageFromView).awaitAsOneOrNull() } override suspend fun observeMessageById(id: String, conversationId: QualifiedIDEntity): Flow = @@ -312,7 +316,7 @@ internal class MessageDAOImpl internal constructor( limit.toLong(), offset.toLong(), mapper::toEntityAssetMessageFromView - ).executeAsList() + ).awaitAsList() } override suspend fun updateMessagesStatusIfNotRead( @@ -345,14 +349,14 @@ internal class MessageDAOImpl internal constructor( override suspend fun getLastMessagesByConversations(conversationIds: List): Map = withContext(readDispatcher.value) { queries.selectLastMessagesByConversationIds(conversationIds, mapper::toEntityMessageFromView) - .executeAsList() + .awaitAsList() .associateBy { it.conversationId } } override suspend fun getNotificationMessage(maxNumberOfMessagesPerConversation: Int): List = withContext(readDispatcher.value) { notificationQueries.getNotificationsMessages(mapper::toNotificationEntity) - .executeAsList() + .awaitAsList() } override suspend fun observeMessagesByConversationAndVisibilityAfterDate( @@ -377,7 +381,7 @@ internal class MessageDAOImpl internal constructor( MessageEntity.Status.PENDING, mapper::toEntityMessageFromView ) - .executeAsList() + .awaitAsList() } override suspend fun updateTextMessageContent( @@ -401,7 +405,7 @@ internal class MessageDAOImpl internal constructor( /** * Be careful and run this operation in ONE wrapping transaction. */ - private fun updateTextMessageContentInDB( + private suspend fun updateTextMessageContentInDB( editInstant: Instant, conversationId: QualifiedIDEntity, currentMessageId: String, @@ -511,13 +515,13 @@ internal class MessageDAOImpl internal constructor( visibility = visibility, creation_date = afterDate, creation_date_ = untilDate - ).executeAsList() + ).awaitAsList() } override suspend fun getReceiptModeFromGroupConversationByQualifiedID(qualifiedID: QualifiedIDEntity): ConversationEntity.ReceiptMode? = withContext(readDispatcher.value) { conversationsQueries.selectReceiptModeFromGroupConversationByQualifiedId(qualifiedID) - .executeAsOneOrNull() + .awaitAsOneOrNull() } override suspend fun promoteMessageToSentUpdatingServerTime( @@ -538,13 +542,13 @@ internal class MessageDAOImpl internal constructor( override suspend fun getAllPendingEphemeralMessages(): List { return withContext(readDispatcher.value) { - queries.selectPendingEphemeralMessages(mapper::toEntityMessageFromView).executeAsList() + queries.selectPendingEphemeralMessages(mapper::toEntityMessageFromView).awaitAsList() } } override suspend fun getAllAlreadyEndedEphemeralMessages(): List { return withContext(readDispatcher.value) { - queries.selectAlreadyEndedEphemeralMessages(mapper::toEntityMessageFromView).executeAsList() + queries.selectAlreadyEndedEphemeralMessages(mapper::toEntityMessageFromView).awaitAsList() } } @@ -576,7 +580,7 @@ internal class MessageDAOImpl internal constructor( } override suspend fun getConversationUnreadEventsCount(conversationId: QualifiedIDEntity): Long = withContext(readDispatcher.value) { - unreadEventsQueries.getConversationUnreadEventsCount(conversationId).executeAsOne() + unreadEventsQueries.getConversationUnreadEventsCount(conversationId).awaitAsOne() } override suspend fun observeMessageVisibility( @@ -596,7 +600,7 @@ internal class MessageDAOImpl internal constructor( ): Int = withContext(readDispatcher.value) { queries .selectSearchedConversationMessagePosition(conversationId, messageId) - .executeAsOne() + .awaitAsOne() .toInt() } @@ -614,7 +618,7 @@ internal class MessageDAOImpl internal constructor( offset = offset.toLong(), mapper = mapper::toEntityMessageFromView ) - .executeAsList() + .awaitAsList() } override suspend fun searchMessagesByTextGlobally( @@ -629,7 +633,7 @@ internal class MessageDAOImpl internal constructor( offset = offset.toLong(), mapper = mapper::toEntityMessageFromView ) - .executeAsList() + .awaitAsList() } override suspend fun observeAssetStatuses(conversationId: QualifiedIDEntity): Flow> = @@ -641,7 +645,7 @@ internal class MessageDAOImpl internal constructor( override suspend fun getMessageAssetTransferStatus(messageId: String, conversationId: QualifiedIDEntity): AssetTransferStatusEntity = withContext(readDispatcher.value) { assetStatusQueries.selectMessageAssetStatus(conversationId, messageId) - .executeAsOne() + .awaitAsOne() } override suspend fun observeAssetStatuses(): Flow> = @@ -657,22 +661,22 @@ internal class MessageDAOImpl internal constructor( assetViewQueries.getAllAssetMessagesByConversationId( conversationId, listOf(MessageEntity.ContentType.ASSET) - ).executeAsList().mapNotNull { it.assetId } + ).awaitAsList().mapNotNull { it.assetId } } } override suspend fun getSenderNameById(id: String, conversationId: QualifiedIDEntity): String? = withContext(readDispatcher.value) { - userQueries.selectNameByMessageId(id, conversationId).executeAsOneOrNull()?.name + userQueries.selectNameByMessageId(id, conversationId).awaitAsOneOrNull()?.name } override suspend fun getNextAudioMessageInConversation(prevMessageId: String, conversationId: QualifiedIDEntity): String? = withContext(readDispatcher.value) { - queries.selectNextAudioMessage(conversationId, prevMessageId).executeAsOneOrNull() + queries.selectNextAudioMessage(conversationId, prevMessageId).awaitAsOneOrNull() } override suspend fun countMessagesForBackup(contentTypes: Collection): Long = withContext(readDispatcher.value) { - queries.countBackupMessages(contentTypes).executeAsOne() + queries.countBackupMessages(contentTypes).awaitAsOne() } override suspend fun getPagedMessagesFlow( @@ -691,7 +695,7 @@ internal class MessageDAOImpl internal constructor( }.buffer() .flowOn(readDispatcher.value) - private fun getMessagesPage( + private suspend fun getMessagesPage( contentTypes: Collection, afterId: String, pageSize: Long, @@ -700,7 +704,7 @@ internal class MessageDAOImpl internal constructor( afterId = afterId, limit = pageSize, mapper::toEntityMessageFromView - ).executeAsList() + ).awaitAsList() override suspend fun updateCompositeMessageContent( conversationId: QualifiedIDEntity, diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageInsertExtension.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageInsertExtension.kt index f23679289bac..3b20d5873708 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageInsertExtension.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageInsertExtension.kt @@ -17,6 +17,9 @@ */ package com.wire.kalium.persistence.dao.message +import app.cash.sqldelight.async.coroutines.awaitAsList +import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull + import com.wire.kalium.persistence.ConversationsQueries import com.wire.kalium.persistence.MessageAttachmentsQueries import com.wire.kalium.persistence.MessagesQueries @@ -43,10 +46,10 @@ internal interface MessageInsertExtension { * that this asset message that is in the DB was only a preview message with valid metadata but no valid keys (Web clients send 2 * separated messages). Therefore, it still needs to be updated with the valid keys in order to be displayed. */ - fun isValidAssetMessageUpdate(message: MessageEntity): Boolean - fun updateAssetMessage(message: MessageEntity) + suspend fun isValidAssetMessageUpdate(message: MessageEntity): Boolean + suspend fun updateAssetMessage(message: MessageEntity) fun contentTypeOf(content: MessageEntityContent): MessageEntity.ContentType - fun insertMessageOrIgnore(message: MessageEntity, withUnreadEvents: Boolean = true) + suspend fun insertMessageOrIgnore(message: MessageEntity, withUnreadEvents: Boolean = true) } internal class MessageInsertExtensionImpl( @@ -58,12 +61,12 @@ internal class MessageInsertExtensionImpl( private val selfUserIDEntity: UserIDEntity ) : MessageInsertExtension { - override fun isValidAssetMessageUpdate(message: MessageEntity): Boolean { + override suspend fun isValidAssetMessageUpdate(message: MessageEntity): Boolean { if (message !is MessageEntity.Regular) return false // If asset has no valid keys, no need to query the DB val hasValidKeys = message.content is MessageEntityContent.Asset && message.content.hasValidRemoteData() val currentMessageHasMissingAssetInformation = - hasValidKeys && messagesQueries.selectById(message.id, message.conversationId).executeAsList().firstOrNull()?.let { + hasValidKeys && messagesQueries.selectById(message.id, message.conversationId).awaitAsList().firstOrNull()?.let { val isFromSameSender = message.senderUserId == it.senderUserId && message.senderClientId == it.senderClientId (it.assetId.isNullOrEmpty() || it.assetOtrKey.isNullOrEmpty() || it.assetSha256.isNullOrEmpty()) && isFromSameSender @@ -71,7 +74,7 @@ internal class MessageInsertExtensionImpl( return currentMessageHasMissingAssetInformation } - override fun updateAssetMessage(message: MessageEntity) { + override suspend fun updateAssetMessage(message: MessageEntity) { if (message.content !is MessageEntityContent.Asset) { return } @@ -97,7 +100,7 @@ internal class MessageInsertExtensionImpl( } @Suppress("TooGenericExceptionCaught") - override fun insertMessageOrIgnore(message: MessageEntity, withUnreadEvents: Boolean) { + override suspend fun insertMessageOrIgnore(message: MessageEntity, withUnreadEvents: Boolean) { try { insertBaseMessageOrError(message) insertMessageContent(message) @@ -111,7 +114,7 @@ internal class MessageInsertExtensionImpl( } } - private fun insertBaseMessageOrError(message: MessageEntity) { + private suspend fun insertBaseMessageOrError(message: MessageEntity) { // do not add withContext messagesQueries.insertMessage( id = message.id, @@ -132,7 +135,7 @@ internal class MessageInsertExtensionImpl( } @Suppress("LongMethod", "ComplexMethod") - private fun insertMessageContent(message: MessageEntity) { + private suspend fun insertMessageContent(message: MessageEntity) { when (val content = message.content) { is MessageEntityContent.Text -> { messagesQueries.insertMessageTextContent( @@ -389,8 +392,8 @@ internal class MessageInsertExtensionImpl( } @Suppress("LongMethod") - private fun insertUnreadEvent(message: MessageEntity) { - val lastRead = conversationsQueries.getConversationLastReadDate(message.conversationId).executeAsOneOrNull() + private suspend fun insertUnreadEvent(message: MessageEntity) { + val lastRead = conversationsQueries.getConversationLastReadDate(message.conversationId).awaitAsOneOrNull() ?: Instant.DISTANT_PAST if (!message.isSelfMessage && message.date > lastRead) { @@ -457,14 +460,14 @@ internal class MessageInsertExtensionImpl( } } - private fun insertUnreadTextContent(message: MessageEntity, textContent: MessageEntityContent.Text) { + private suspend fun insertUnreadTextContent(message: MessageEntity, textContent: MessageEntityContent.Text) { var isQuotingSelfUser = false if (textContent.quotedMessageId != null) { val senderId = messagesQueries.getMessageSenderId( textContent.quotedMessageId, message.conversationId ) - .executeAsOneOrNull() + .awaitAsOneOrNull() isQuotingSelfUser = senderId == selfUserIDEntity } when { @@ -492,7 +495,7 @@ internal class MessageInsertExtensionImpl( } } - private fun insertCompositeMessage( + private suspend fun insertCompositeMessage( content: MessageEntityContent.Composite, message: MessageEntity ) { diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageMetadataDAO.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageMetadataDAO.kt index 1f2b58a60230..23ff653c7b77 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageMetadataDAO.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageMetadataDAO.kt @@ -17,6 +17,8 @@ */ package com.wire.kalium.persistence.dao.message +import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull + import com.wire.kalium.persistence.MessageMetadataQueries import com.wire.kalium.persistence.dao.ConversationIDEntity import com.wire.kalium.persistence.dao.UserIDEntity @@ -33,6 +35,6 @@ internal class MessageMetadataDAOImpl internal constructor( ) : MessageMetadataDAO { override suspend fun originalSenderId(conversationId: ConversationIDEntity, messageId: String): UserIDEntity? = withContext(readDispatcher.value) { - metaDataQueries.originalSenderId(conversationId, messageId).executeAsOneOrNull() + metaDataQueries.originalSenderId(conversationId, messageId).awaitAsOneOrNull() } } diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/attachment/MessageAttachmentsDao.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/attachment/MessageAttachmentsDao.kt index ec04475ed633..f42a08c605a9 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/attachment/MessageAttachmentsDao.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/attachment/MessageAttachmentsDao.kt @@ -17,6 +17,10 @@ */ package com.wire.kalium.persistence.dao.message.attachment +import app.cash.sqldelight.async.coroutines.awaitAsList +import app.cash.sqldelight.async.coroutines.awaitAsOne +import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull + import app.cash.sqldelight.coroutines.asFlow import com.wire.kalium.persistence.MessageAttachmentsQueries import com.wire.kalium.persistence.dao.QualifiedIDEntity @@ -58,11 +62,11 @@ internal class MessageAttachmentsDaoImpl( override suspend fun getAttachments(messageId: String, conversationId: QualifiedIDEntity): List = withContext(readDispatcher.value) { - queries.getAttachments(messageId, conversationId, ::toDao).executeAsList() + queries.getAttachments(messageId, conversationId, ::toDao).awaitAsList() } override suspend fun getAttachments(): List = withContext(readDispatcher.value) { - queries.getAllAttachments(::toDao).executeAsList() + queries.getAllAttachments(::toDao).awaitAsList() } override suspend fun observeAttachments(): Flow> = withContext(readDispatcher.value) { @@ -73,7 +77,7 @@ internal class MessageAttachmentsDaoImpl( } override suspend fun getAttachment(assetId: String): MessageAttachmentEntity = withContext(readDispatcher.value) { - queries.getAttachment(asset_id = assetId, ::toDao).executeAsOne() + queries.getAttachment(asset_id = assetId, ::toDao).awaitAsOne() } override suspend fun updateAttachment( @@ -90,7 +94,7 @@ internal class MessageAttachmentsDaoImpl( } override suspend fun getAssetPath(assetId: String): String? = withContext(readDispatcher.value) { - queries.getAssetPath(asset_id = assetId).executeAsOneOrNull()?.asset_path + queries.getAssetPath(asset_id = assetId).awaitAsOneOrNull()?.asset_path } override suspend fun setAssetPath(assetId: String, path: String) { diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/draft/MessageDraftDAOImpl.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/draft/MessageDraftDAOImpl.kt index f5fb5395ee03..886a5e4ce244 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/draft/MessageDraftDAOImpl.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/draft/MessageDraftDAOImpl.kt @@ -17,6 +17,9 @@ */ package com.wire.kalium.persistence.dao.message.draft +import app.cash.sqldelight.async.coroutines.awaitAsOne +import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull + import app.cash.sqldelight.coroutines.asFlow import com.wire.kalium.persistence.ConversationsQueries import com.wire.kalium.persistence.MessageDraftsQueries @@ -42,7 +45,7 @@ class MessageDraftDAOImpl internal constructor( override suspend fun upsertMessageDraft(messageDraft: MessageDraftEntity) = withContext(writeDispatcher.value) { val conversationExists = conversationsQueries.selectConversationByQualifiedId(messageDraft.conversationId) - .executeAsOneOrNull() != null + .awaitAsOneOrNull() != null if (!conversationExists) { return@withContext @@ -50,7 +53,7 @@ class MessageDraftDAOImpl internal constructor( if (messageDraft.editMessageId != null) { val messageExists = messagesQueries.getMessage(messageDraft.editMessageId, messageDraft.conversationId) - .executeAsOneOrNull() != null + .awaitAsOneOrNull() != null if (!messageExists) { return@withContext } @@ -58,7 +61,7 @@ class MessageDraftDAOImpl internal constructor( if (messageDraft.quotedMessageId != null) { val quotedMessageExists = messagesQueries.getMessage(messageDraft.quotedMessageId, messageDraft.conversationId) - .executeAsOneOrNull() != null + .awaitAsOneOrNull() != null if (!quotedMessageExists) { return@withContext } @@ -72,7 +75,7 @@ class MessageDraftDAOImpl internal constructor( quoted_message_id = messageDraft.quotedMessageId, mention_list = messageDraft.selectedMentionList ) - val changes = queries.selectChanges().executeAsOne() + val changes = queries.selectChanges().awaitAsOne() if (changes == 0L) { // rollback the transaction if no changes were made so that it doesn't notify other queries about changes if not needed this.rollback() @@ -82,7 +85,7 @@ class MessageDraftDAOImpl internal constructor( override suspend fun getMessageDraft(conversationIDEntity: ConversationIDEntity): MessageDraftEntity? = withContext(coroutineContext) { - queries.getDraft(conversationIDEntity, ::toDao).executeAsOneOrNull() + queries.getDraft(conversationIDEntity, ::toDao).awaitAsOneOrNull() } override suspend fun removeMessageDraft(conversationIDEntity: ConversationIDEntity) { diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/messageattachment/MessageAttachmentDraftDaoImpl.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/messageattachment/MessageAttachmentDraftDaoImpl.kt index fded194accc3..076eaa46adc0 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/messageattachment/MessageAttachmentDraftDaoImpl.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/messageattachment/MessageAttachmentDraftDaoImpl.kt @@ -17,6 +17,9 @@ */ package com.wire.kalium.persistence.dao.messageattachment +import app.cash.sqldelight.async.coroutines.awaitAsList +import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull + import app.cash.sqldelight.coroutines.asFlow import com.wire.kalium.persistence.MessageAttachmentDraftQueries import com.wire.kalium.persistence.dao.QualifiedIDEntity @@ -81,7 +84,7 @@ internal class MessageAttachmentDraftDaoImpl internal constructor( override suspend fun getAttachments(conversationId: QualifiedIDEntity): List = withContext(readDispatcher.value) { - queries.getDrafts(conversationId, ::toDao).executeAsList() + queries.getDrafts(conversationId, ::toDao).awaitAsList() } override suspend fun observeAttachments(conversationId: QualifiedIDEntity): Flow> = @@ -90,7 +93,7 @@ internal class MessageAttachmentDraftDaoImpl internal constructor( .flowOn(readDispatcher.value) override suspend fun getAttachment(uuid: String): MessageAttachmentDraftEntity? = withContext(readDispatcher.value) { - queries.getDraft(uuid, ::toDao).executeAsOneOrNull() + queries.getDraft(uuid, ::toDao).awaitAsOneOrNull() } override suspend fun deleteAttachment(uuid: String) { diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/publiclink/PublicLinkDaoImpl.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/publiclink/PublicLinkDaoImpl.kt index dbdddde0c91e..47d9519a5615 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/publiclink/PublicLinkDaoImpl.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/publiclink/PublicLinkDaoImpl.kt @@ -17,6 +17,8 @@ */ package com.wire.kalium.persistence.dao.publiclink +import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull + import com.wire.kalium.persistence.PublicLinksQueries import com.wire.kalium.persistence.dao.publiclink.PublicLinkMapper.toDao import com.wire.kalium.persistence.db.ReadDispatcher @@ -43,7 +45,7 @@ internal class PublicLinkDaoImpl( override suspend fun get(id: String): PublicLinkEntity? = withContext(readDispatcher.value) { queries.getLink(id, ::toDao) - .executeAsOneOrNull() + .awaitAsOneOrNull() } override suspend fun delete(id: String) { diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/reaction/ReactionDAO.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/reaction/ReactionDAO.kt index 52b1b4ce9960..6ff6e3835bb1 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/reaction/ReactionDAO.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/reaction/ReactionDAO.kt @@ -18,6 +18,10 @@ package com.wire.kalium.persistence.dao.reaction +import app.cash.sqldelight.async.coroutines.awaitAsList +import app.cash.sqldelight.async.coroutines.awaitAsOne +import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull + import app.cash.sqldelight.coroutines.asFlow import com.wire.kalium.persistence.ReactionsQueries import com.wire.kalium.persistence.dao.ConversationIDEntity @@ -90,7 +94,7 @@ class ReactionDAOImpl( reactions: UserReactionsEntity ) = withContext(writeDispatcher.value) { reactionsQueries.transaction { - reactionsQueries.doesMessageExist(originalMessageId, conversationId).executeAsOneOrNull()?.let { + reactionsQueries.doesMessageExist(originalMessageId, conversationId).awaitAsOneOrNull()?.let { reactionsQueries.deleteAllReactionsOnMessageFromUser(originalMessageId, conversationId, senderUserId) reactions.forEach { reactionsQueries.insertReaction(originalMessageId, conversationId, senderUserId, it, instant.toIsoDateTimeString()) @@ -137,7 +141,7 @@ class ReactionDAOImpl( .selectByMessageIdAndConversationIdAndSenderId(originalMessageId, conversationId, senderUserId) { _, _, _, emoji, _ -> emoji } - .executeAsList() + .awaitAsList() .toSet() } @@ -163,17 +167,17 @@ class ReactionDAOImpl( }.buffer() .flowOn(readDispatcher.value) - private fun getReactionsPage( + private suspend fun getReactionsPage( offset: Long, pageSize: Long, ) = reactionsQueries.selectReactionsByMessagePaged( offset = offset, limit = pageSize, mapper = MessageMapper::toReactionEntityFromView - ).executeAsList() + ).awaitAsList() override suspend fun countMessageReactionsBackup(): Long = withContext(readDispatcher.value) { - reactionsQueries.countReactionsForBackup().executeAsOne() + reactionsQueries.countReactionsForBackup().awaitAsOne() } } diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/daokaliumdb/AccountsDAO.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/daokaliumdb/AccountsDAO.kt index bf1d1873b67b..50e1f15e0d98 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/daokaliumdb/AccountsDAO.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/daokaliumdb/AccountsDAO.kt @@ -18,6 +18,10 @@ package com.wire.kalium.persistence.daokaliumdb +import app.cash.sqldelight.async.coroutines.awaitAsList +import app.cash.sqldelight.async.coroutines.awaitAsOne +import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull + import app.cash.sqldelight.coroutines.asFlow import com.wire.kalium.persistence.AccountsQueries import com.wire.kalium.persistence.CurrentAccountQueries @@ -179,7 +183,7 @@ interface AccountsDAO { suspend fun setAllAccountsPersistentWebSocketEnabled(enabled: Boolean) suspend fun persistentWebSocketStatus(userIDEntity: UserIDEntity): Boolean suspend fun accountInfo(userIDEntity: UserIDEntity): AccountInfoEntity? - fun fullAccountInfo(userIDEntity: UserIDEntity): FullAccountEntity? + suspend fun fullAccountInfo(userIDEntity: UserIDEntity): FullAccountEntity? suspend fun getAllValidAccountPersistentWebSocketStatus(): Flow> suspend fun getAccountManagedBy(userIDEntity: UserIDEntity): ManagedByEntity? suspend fun validAccountWithServerConfigId(): Map @@ -194,7 +198,7 @@ internal class AccountsDAOImpl internal constructor( private val mapper: AccountMapper = AccountMapper ) : AccountsDAO { override suspend fun ssoId(userIDEntity: UserIDEntity): SsoIdEntity? = withContext(queriesContext) { - queries.ssoId(userIDEntity).executeAsOneOrNull() + queries.ssoId(userIDEntity).awaitAsOneOrNull() ?.let { mapper.toSsoIdEntity( scim_external_id = it.scim_external_id, @@ -234,11 +238,11 @@ internal class AccountsDAOImpl internal constructor( .flowOn(queriesContext) override suspend fun allAccountList(): List = withContext(queriesContext) { - queries.allAccounts(mapper = mapper::fromAccount).executeAsList() + queries.allAccounts(mapper = mapper::fromAccount).awaitAsList() } override suspend fun allValidAccountList(): List = withContext(queriesContext) { - queries.allValidAccounts(mapper = mapper::fromAccount).executeAsList() + queries.allValidAccounts(mapper = mapper::fromAccount).awaitAsList() } override suspend fun observerValidAccountList(): Flow> = @@ -255,15 +259,15 @@ internal class AccountsDAOImpl internal constructor( override suspend fun isFederated(userIDEntity: UserIDEntity): Boolean? = withContext(queriesContext) { - queries.isFederationEnabled(userIDEntity).executeAsOneOrNull() + queries.isFederationEnabled(userIDEntity).awaitAsOneOrNull() } override suspend fun doesValidAccountExists(userIDEntity: UserIDEntity): Boolean = withContext(queriesContext) { - queries.doesValidAccountExist(userIDEntity).executeAsOne() + queries.doesValidAccountExist(userIDEntity).awaitAsOne() } override suspend fun currentAccount(): AccountInfoEntity? = withContext(queriesContext) { - currentAccountQueries.currentAccountInfo(mapper = mapper::fromAccount).executeAsOneOrNull() + currentAccountQueries.currentAccountInfo(mapper = mapper::fromAccount).awaitAsOneOrNull() } override fun observerCurrentAccount(): Flow = @@ -317,7 +321,7 @@ internal class AccountsDAOImpl internal constructor( } override suspend fun persistentWebSocketStatus(userIDEntity: UserIDEntity): Boolean = withContext(queriesContext) { - queries.persistentWebSocketStatus(userIDEntity).executeAsOne() + queries.persistentWebSocketStatus(userIDEntity).awaitAsOne() } override suspend fun getAllValidAccountPersistentWebSocketStatus(): Flow> = @@ -327,21 +331,22 @@ internal class AccountsDAOImpl internal constructor( .flowOn(queriesContext) override suspend fun getAccountManagedBy(userIDEntity: UserIDEntity): ManagedByEntity? = withContext(queriesContext) { - queries.managedBy(userIDEntity).executeAsOneOrNull()?.managed_by + queries.managedBy(userIDEntity).awaitAsOneOrNull()?.managed_by } override suspend fun validAccountWithServerConfigId(): Map = withContext(queriesContext) { - queries.allValidAccountsWithServerConfig(mapper = mapper::fromUserIDWithServerConfig).executeAsList().toMap() + queries.allValidAccountsWithServerConfig(mapper = mapper::fromUserIDWithServerConfig).awaitAsList().toMap() } override suspend fun accountInfo(userIDEntity: UserIDEntity): AccountInfoEntity? = withContext(queriesContext) { - queries.accountInfo(userIDEntity, mapper = mapper::fromAccount).executeAsOneOrNull() + queries.accountInfo(userIDEntity, mapper = mapper::fromAccount).awaitAsOneOrNull() } - override fun fullAccountInfo(userIDEntity: UserIDEntity): FullAccountEntity? = - queries.fullAccountInfo(userIDEntity, mapper = mapper::fromFullAccountInfo).executeAsOneOrNull() + override suspend fun fullAccountInfo(userIDEntity: UserIDEntity): FullAccountEntity? = withContext(queriesContext) { + queries.fullAccountInfo(userIDEntity, mapper = mapper::fromFullAccountInfo).awaitAsOneOrNull() + } override suspend fun doesValidNomadAccountExist(): Boolean = withContext(queriesContext) { - queries.doesValidNomadAccountExist().executeAsOne() + queries.doesValidNomadAccountExist().awaitAsOne() } } diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/daokaliumdb/ServerConfigurationDAO.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/daokaliumdb/ServerConfigurationDAO.kt index bc4281b817c5..40974958cedf 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/daokaliumdb/ServerConfigurationDAO.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/daokaliumdb/ServerConfigurationDAO.kt @@ -18,6 +18,10 @@ package com.wire.kalium.persistence.daokaliumdb +import app.cash.sqldelight.async.coroutines.awaitAsList +import app.cash.sqldelight.async.coroutines.awaitAsOne +import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull + import app.cash.sqldelight.coroutines.asFlow import com.wire.kalium.persistence.ServerConfigurationQueries import com.wire.kalium.persistence.dao.QualifiedIDEntity @@ -128,7 +132,7 @@ interface ServerConfigurationDAO { suspend fun insert(insertData: InsertData) suspend fun allConfigFlow(): Flow> suspend fun allConfig(): List - fun configById(id: String): ServerConfigEntity? + suspend fun configById(id: String): ServerConfigEntity? suspend fun configByLinks(links: ServerConfigEntity.Links): ServerConfigEntity? suspend fun getCommonApiVersion(domain: String): Int suspend fun updateServerMetaData(id: String, federation: Boolean, commonApiVersion: Int) @@ -208,11 +212,12 @@ internal class ServerConfigurationDAOImpl internal constructor( .flowOn(queriesContext) override suspend fun allConfig(): List = withContext(queriesContext) { - queries.storedConfig(mapper = mapper::fromServerConfiguration).executeAsList() + queries.storedConfig(mapper = mapper::fromServerConfiguration).awaitAsList() } - override fun configById(id: String): ServerConfigEntity? = - queries.getById(id, mapper = mapper::fromServerConfiguration).executeAsOneOrNull() + override suspend fun configById(id: String): ServerConfigEntity? = withContext(queriesContext) { + queries.getById(id, mapper = mapper::fromServerConfiguration).awaitAsOneOrNull() + } override suspend fun configByLinks(links: ServerConfigEntity.Links): ServerConfigEntity? = withContext(queriesContext) { with(links) { @@ -224,7 +229,7 @@ internal class ServerConfigurationDAOImpl internal constructor( api_proxy_port = apiProxy?.port, mapper = mapper::fromServerConfiguration ) - }.executeAsOneOrNull() + }.awaitAsOneOrNull() } override suspend fun updateServerMetaData(id: String, federation: Boolean, commonApiVersion: Int) { @@ -234,7 +239,7 @@ internal class ServerConfigurationDAOImpl internal constructor( } override suspend fun getCommonApiVersion(domain: String): Int = withContext(queriesContext) { - queries.getCommonApiVersionByDomain(domain).executeAsOne() + queries.getCommonApiVersionByDomain(domain).awaitAsOne() } override suspend fun updateApiVersionAndDomain(id: String, domain: String, commonApiVersion: Int) { @@ -244,7 +249,7 @@ internal class ServerConfigurationDAOImpl internal constructor( } override suspend fun configForUser(userId: UserIDEntity): ServerConfigEntity? = withContext(queriesContext) { - queries.getByUser(userId, mapper = mapper::fromServerConfiguration).executeAsOneOrNull() + queries.getByUser(userId, mapper = mapper::fromServerConfiguration).awaitAsOneOrNull() } override suspend fun setNativePushSupportedByServer(userId: UserIDEntity, supported: Boolean) { @@ -254,7 +259,7 @@ internal class ServerConfigurationDAOImpl internal constructor( } override suspend fun isNativePushSupportedByServer(userId: UserIDEntity): Boolean? = withContext(queriesContext) { - queries.isNativePushSupportedByServer(userId).executeAsOneOrNull() + queries.isNativePushSupportedByServer(userId).awaitAsOneOrNull() } override suspend fun setFederationToTrue(id: String) { @@ -276,7 +281,7 @@ internal class ServerConfigurationDAOImpl internal constructor( } override suspend fun teamUrlForUser(userId: UserIDEntity): String? = withContext(queriesContext) { - queries.getTeamUrlByUser(userId).executeAsOneOrNull() + queries.getTeamUrlByUser(userId).awaitAsOneOrNull() } override suspend fun getServerConfigByLinksFlow(links: ServerConfigEntity.Links): Flow = diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/GlobalDatabaseBuilder.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/GlobalDatabaseBuilder.kt index 180f339779c5..95e91a082e49 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/GlobalDatabaseBuilder.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/GlobalDatabaseBuilder.kt @@ -62,7 +62,11 @@ class GlobalDatabaseBuilder internal constructor( ) init { - database.globalDatabasePropertiesQueries.enableForeignKeyContraints() + sqlDriver.execute( + identifier = null, + sql = "PRAGMA foreign_keys = 1;", + parameters = 0, + ) } val serverConfigurationDAO: ServerConfigurationDAO diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt index 435a1b2c95dd..0cce3b1d9d7e 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt @@ -18,6 +18,9 @@ package com.wire.kalium.persistence.db +import app.cash.sqldelight.async.coroutines.awaitMigrate +import app.cash.sqldelight.async.coroutines.awaitQuery +import app.cash.sqldelight.async.coroutines.await import app.cash.sqldelight.db.QueryResult import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.db.SqlSchema @@ -208,8 +211,18 @@ class UserDatabaseBuilder internal constructor( ) init { - database.databasePropertiesQueries.insertSelfUserId(userId) - database.databasePropertiesQueries.enableForeignKeyContraints() + sqlDriver.execute( + identifier = null, + sql = "INSERT OR IGNORE INTO SelfUser(id) VALUES(?);", + parameters = 1 + ) { + bindString(0, userId.toString()) + } + sqlDriver.execute( + identifier = null, + sql = "PRAGMA foreign_keys = 1;", + parameters = 0, + ) } val readDispatcher: ReadDispatcher = ReadDispatcher(dispatcher.limitedParallelism(MAX_READ_PARALLELISM)) @@ -464,16 +477,16 @@ internal expect fun createEmptyDatabaseFile( ): String? @Suppress("TooGenericExceptionCaught") -fun SqlDriver.migrate(sqlSchema: SqlSchema>): Boolean { - val oldVersion = this.executeQuery(null, "PRAGMA user_version;", { - it.next() - it.getLong(0).let { QueryResult.Value(it) } - }, 0).value ?: return false +suspend fun SqlDriver.migrate(sqlSchema: SqlSchema>): Boolean { + val oldVersion = awaitQuery(null, "PRAGMA user_version;", { + it.next().await() + it.getLong(0) + }, 0) ?: return false val newVersion = sqlSchema.version return try { if (oldVersion != newVersion) { - sqlSchema.migrate(this, oldVersion, newVersion) + sqlSchema.awaitMigrate(this, oldVersion, newVersion) } true } catch (e: CancellationException) { @@ -486,16 +499,16 @@ fun SqlDriver.migrate(sqlSchema: SqlSchema>): Boolean { /** * @return true if the database have fk violations, false otherwise */ -fun SqlDriver.checkFKViolations(): Boolean { +suspend fun SqlDriver.checkFKViolations(): Boolean { var result = false - executeQuery(null, "PRAGMA foreign_key_check;", { + awaitQuery(null, "PRAGMA foreign_key_check;", { // foreign_key_check returns the rows with the fk violations // if the cursor has a next, it means there are violations // and the backup is corrupted - if (it.next().value) { + if (it.next().await()) { result = true } - QueryResult.Unit + Unit }, 0, null) return result diff --git a/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/GlobalDatabase.kt b/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/GlobalDatabase.kt index 87a853ff4768..4e8ffbf4ffc5 100644 --- a/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/GlobalDatabase.kt +++ b/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/GlobalDatabase.kt @@ -25,10 +25,14 @@ actual fun globalDatabaseProvider( queriesContext: CoroutineDispatcher, passphrase: GlobalDatabaseSecret?, enableWAL: Boolean -): GlobalDatabaseBuilder { - TODO("Not yet implemented") -} +): GlobalDatabaseBuilder = + GlobalDatabaseBuilder( + sqlDriver = createKaliumWebWorkerDriver(), + platformDatabaseData = platformDatabaseData, + queriesContext = queriesContext + ) actual fun nuke(platformDatabaseData: PlatformDatabaseData): Boolean { - TODO("Not yet implemented") + // TODO: Implement real JS global database deletion once the worker driver uses a stable persisted storage key. + return true } diff --git a/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/SqlDelightAsyncSmoke.kt b/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/SqlDelightAsyncSmoke.kt new file mode 100644 index 000000000000..cf51dc0f7146 --- /dev/null +++ b/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/SqlDelightAsyncSmoke.kt @@ -0,0 +1,48 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +@file:OptIn(ExperimentalJsExport::class) + +package com.wire.kalium.persistence.db + +import com.wire.kalium.persistence.dao.UserIDEntity +import kotlin.js.Promise +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.promise + +@JsExport +fun runSqlDelightAsyncSmoke(): Promise = CoroutineScope(SupervisorJob() + Dispatchers.Default).promise { + val database = userDatabaseBuilder( + platformDatabaseData = PlatformDatabaseData(), + userId = UserIDEntity("js-smoke", "wire.test"), + passphrase = null, + dispatcher = Dispatchers.Default, + enableWAL = false, + ) + + try { + database.metadataDAO.insertValue("value", "async-smoke") + val stored = database.metadataDAO.valueByKey("async-smoke") + check(stored == "value") { "Unexpected metadata value: $stored" } + "ok" + } finally { + database.nuke() + } +} diff --git a/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt b/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt index c237632ed778..0e6671ee4b8a 100644 --- a/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt +++ b/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt @@ -34,26 +34,56 @@ actual fun userDatabaseBuilder( dispatcher: CoroutineDispatcher, enableWAL: Boolean, dbInvalidationControlEnabled: Boolean -): UserDatabaseBuilder = TODO("Not yet implemented") +): UserDatabaseBuilder { + val rawDriver = createKaliumWebWorkerDriver() + val invalidationController = DbInvalidationController( + enabled = dbInvalidationControlEnabled, + notifyKey = { key -> rawDriver.notifyListeners(key) } + ) + val driver: SqlDriver = MutedSqlDriver( + delegate = rawDriver, + invalidationController = invalidationController + ) + return UserDatabaseBuilder( + userId = userId, + sqlDriver = driver, + dispatcher = dispatcher, + platformDatabaseData = platformDatabaseData, + isEncrypted = false, + dbInvalidationController = invalidationController + ) +} actual fun userDatabaseDriverByPath( platformDatabaseData: PlatformDatabaseData, path: String, passphrase: UserDBSecret?, enableWAL: Boolean -): SqlDriver = TODO() +): SqlDriver { + // TODO: Honor the requested JS database identity instead of ignoring path; the current worker driver setup always opens an anonymous DB. + return createKaliumWebWorkerDriver() +} internal actual fun nuke( userId: UserIDEntity, platformDatabaseData: PlatformDatabaseData -): Boolean = TODO() +): Boolean { + // TODO: Implement real JS database deletion once the worker driver uses a stable persisted storage key. + return true +} internal actual fun getDatabaseAbsoluteFileLocation( platformDatabaseData: PlatformDatabaseData, userId: UserIDEntity -): String? = TODO() +): String? { + // TODO: Replace this file-path contract for JS with a storage-capability API; browser-backed DBs do not expose absolute paths. + return null +} internal actual fun createEmptyDatabaseFile( platformDatabaseData: PlatformDatabaseData, userId: UserIDEntity, -): String? = TODO() +): String? { + // TODO: Implement a JS-compatible export target flow instead of relying on native temporary database file creation. + return null +} diff --git a/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/WebWorkerDriverFactory.kt b/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/WebWorkerDriverFactory.kt new file mode 100644 index 000000000000..278e3ae4a1d2 --- /dev/null +++ b/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/WebWorkerDriverFactory.kt @@ -0,0 +1,73 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.persistence.db + +import app.cash.sqldelight.db.SqlDriver +import app.cash.sqldelight.driver.worker.WebWorkerDriver +import org.w3c.dom.Worker + +internal fun createKaliumWebWorkerDriver(): SqlDriver = WebWorkerDriver(createKaliumWorker()) + +@Suppress("UnsafeCastFromDynamic") +private fun createKaliumWorker(): Worker { + val hasBrowserWorker = js("typeof window !== 'undefined' && typeof Worker !== 'undefined'").unsafeCast() + if (hasBrowserWorker) { + return Worker(js("""new URL("@cashapp/sqldelight-sqljs-worker/sqljs.worker.js", import.meta.url)""")) + } + + return js( + """ + (function() { + var WorkerCtor = require('node:worker_threads').Worker; + var workerPath = require.resolve('@cashapp/sqldelight-sqljs-worker/sqljs.worker.js'); + var worker = new WorkerCtor(workerPath); + var listeners = new Map(); + + return { + postMessage: function(message) { + worker.postMessage(message); + }, + terminate: function() { + return worker.terminate(); + }, + addEventListener: function(type, listener) { + var eventName = type === 'message' ? 'message' : 'error'; + var handler = function(payload) { + if (typeof listener === 'function') { + listener({ data: payload }); + } else if (listener && typeof listener.handleEvent === 'function') { + listener.handleEvent(eventName === 'message' ? { data: payload } : payload); + } + }; + + listeners.set(listener, { eventName: eventName, handler: handler }); + worker.on(eventName, handler); + }, + removeEventListener: function(_type, listener) { + var entry = listeners.get(listener); + if (!entry) return; + + worker.off(entry.eventName, entry.handler); + listeners.delete(listener); + } + }; + })() + """ + ).unsafeCast() +} diff --git a/data/persistence/src/jsTest/kotlin/com/wire/kalium/persistence/BaseDatabaseTest.kt b/data/persistence/src/jsTest/kotlin/com/wire/kalium/persistence/BaseDatabaseTest.kt index d3198d69dd6a..9c08e32efe4f 100644 --- a/data/persistence/src/jsTest/kotlin/com/wire/kalium/persistence/BaseDatabaseTest.kt +++ b/data/persistence/src/jsTest/kotlin/com/wire/kalium/persistence/BaseDatabaseTest.kt @@ -30,7 +30,7 @@ actual open class BaseDatabaseTest actual constructor() { protected actual val dispatcher: TestDispatcher = StandardTestDispatcher() actual fun deleteDatabase(userId: UserIDEntity) { - // TODO delete test database + createDatabase(userId, null, enableWAL = false).nuke() } actual fun createDatabase( @@ -39,23 +39,30 @@ actual open class BaseDatabaseTest actual constructor() { enableWAL: Boolean, dbInvalidationControlEnabled: Boolean ): UserDatabaseBuilder { - TODO("Not yet implemented") + return com.wire.kalium.persistence.db.userDatabaseBuilder( + platformDatabaseData = PlatformDatabaseData(), + userId = userId, + passphrase = passphrase, + dispatcher = dispatcher, + enableWAL = enableWAL, + dbInvalidationControlEnabled = dbInvalidationControlEnabled + ) } actual fun databasePath( userId: UserIDEntity ): String { - TODO("Not yet implemented") + return userId.toString() } actual fun doesDatabaseExist( userId: UserIDEntity - ): Boolean = TODO("Not yet implemented") + ): Boolean = true actual val encryptedDBSecret: UserDBSecret - get() = TODO("Not yet implemented") + get() = UserDBSecret(ByteArray(0)) actual fun platformDBData(userId: UserIDEntity): PlatformDatabaseData { - TODO("Not yet implemented") + return PlatformDatabaseData() } } diff --git a/data/persistence/src/jsTest/kotlin/com/wire/kalium/persistence/GlobalDBBaseTest.kt b/data/persistence/src/jsTest/kotlin/com/wire/kalium/persistence/GlobalDBBaseTest.kt index edb29062eb1f..069440b1435e 100644 --- a/data/persistence/src/jsTest/kotlin/com/wire/kalium/persistence/GlobalDBBaseTest.kt +++ b/data/persistence/src/jsTest/kotlin/com/wire/kalium/persistence/GlobalDBBaseTest.kt @@ -19,13 +19,21 @@ package com.wire.kalium.persistence import com.wire.kalium.persistence.db.GlobalDatabaseBuilder +import com.wire.kalium.persistence.db.PlatformDatabaseData +import com.wire.kalium.persistence.db.globalDatabaseProvider +import com.wire.kalium.util.KaliumDispatcherImpl actual abstract class GlobalDBBaseTest { actual fun deleteDatabase() { - TODO("Not yet implemented") + createDatabase().nuke() } actual fun createDatabase(): GlobalDatabaseBuilder { - TODO("Not yet implemented") + return globalDatabaseProvider( + platformDatabaseData = PlatformDatabaseData(), + queriesContext = KaliumDispatcherImpl.io, + passphrase = null, + enableWAL = false + ) } } diff --git a/data/persistence/src/jsTest/kotlin/com/wire/kalium/persistence/SqlDelightAsyncSmokeTest.kt b/data/persistence/src/jsTest/kotlin/com/wire/kalium/persistence/SqlDelightAsyncSmokeTest.kt new file mode 100644 index 000000000000..61f8301a2843 --- /dev/null +++ b/data/persistence/src/jsTest/kotlin/com/wire/kalium/persistence/SqlDelightAsyncSmokeTest.kt @@ -0,0 +1,49 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.persistence + +import com.wire.kalium.persistence.dao.UserIDEntity +import com.wire.kalium.persistence.db.PlatformDatabaseData +import com.wire.kalium.persistence.db.userDatabaseBuilder +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest + +class SqlDelightAsyncSmokeTest { + @Test + fun canWriteAndReadUsingWebWorkerDriver() = runTest { + val database = userDatabaseBuilder( + platformDatabaseData = PlatformDatabaseData(), + userId = UserIDEntity("js-smoke", "wire.test"), + passphrase = null, + dispatcher = StandardTestDispatcher(testScheduler), + enableWAL = false, + ) + + try { + database.metadataDAO.insertValue("value", "async-smoke") + val stored = database.metadataDAO.valueByKey("async-smoke") + + assertEquals("value", stored) + } finally { + database.nuke() + } + } +} diff --git a/data/persistence/src/jvmMain/kotlin/com/wire/kalium/persistence/db/GlobalDatabase.kt b/data/persistence/src/jvmMain/kotlin/com/wire/kalium/persistence/db/GlobalDatabase.kt index ea7084fb42c5..3b494dfc725f 100644 --- a/data/persistence/src/jvmMain/kotlin/com/wire/kalium/persistence/db/GlobalDatabase.kt +++ b/data/persistence/src/jvmMain/kotlin/com/wire/kalium/persistence/db/GlobalDatabase.kt @@ -18,6 +18,7 @@ package com.wire.kalium.persistence.db +import app.cash.sqldelight.async.coroutines.synchronous import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver import com.wire.kalium.persistence.GlobalDatabase import com.wire.kalium.persistence.util.FileNameUtil @@ -42,7 +43,7 @@ actual fun globalDatabaseProvider( throw NotImplementedError("Encrypted DB is not supported on JVM") } - val schema = GlobalDatabase.Schema + val schema = GlobalDatabase.Schema.synchronous() val databasePath = storageData.file.resolve(FileNameUtil.globalDBName()) // Make sure all intermediate directories exist @@ -61,7 +62,7 @@ actual fun nuke(platformDatabaseData: PlatformDatabaseData): Boolean { } fun createGlobalInMemoryDatabase(dispatcher: CoroutineDispatcher): GlobalDatabaseBuilder { - val driver = databaseDriver(uri = JdbcSqliteDriver.IN_MEMORY, schema = GlobalDatabase.Schema) { + val driver = databaseDriver(uri = JdbcSqliteDriver.IN_MEMORY, schema = GlobalDatabase.Schema.synchronous()) { isWALEnabled = false areForeignKeyConstraintsEnforced = true } diff --git a/data/persistence/src/jvmMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt b/data/persistence/src/jvmMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt index 212978b7fa68..d25d85e486d5 100644 --- a/data/persistence/src/jvmMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt +++ b/data/persistence/src/jvmMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt @@ -20,6 +20,7 @@ package com.wire.kalium.persistence.db +import app.cash.sqldelight.async.coroutines.synchronous import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver import com.wire.kalium.persistence.UserDatabase @@ -49,7 +50,7 @@ actual fun userDatabaseBuilder( throw NotImplementedError("Encrypted DB is not supported on JVM") } - val schema = UserDatabase.Schema + val schema = UserDatabase.Schema.synchronous() val databasePath = storageData.file.resolve(DATABASE_NAME) // Make sure all intermediate directories exist @@ -119,7 +120,7 @@ fun inMemoryDatabase( userId: UserIDEntity, dispatcher: CoroutineDispatcher ): UserDatabaseBuilder = InMemoryDatabaseCache.getOrCreate(userId) { - val rawDriver = databaseDriver(uri = JdbcSqliteDriver.IN_MEMORY, schema = UserDatabase.Schema) { + val rawDriver = databaseDriver(uri = JdbcSqliteDriver.IN_MEMORY, schema = UserDatabase.Schema.synchronous()) { isWALEnabled = false areForeignKeyConstraintsEnforced = true } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/session/SessionRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/session/SessionRepository.kt index 09e2e7be62d4..a58d0cb51221 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/session/SessionRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/session/SessionRepository.kt @@ -59,7 +59,7 @@ internal interface SessionRepository { suspend fun allValidSessions(): Either> suspend fun allValidSessionsFlow(): Flow>> suspend fun doesValidSessionExist(userId: UserId): Either - fun fullAccountInfo(userId: UserId): Either + suspend fun fullAccountInfo(userId: UserId): Either suspend fun userAccountInfo(userId: UserId): Either suspend fun updateCurrentSession(userId: UserId?): Either suspend fun logout(userId: UserId, reason: LogoutReason): Either @@ -139,7 +139,7 @@ internal class SessionDataSource internal constructor( override suspend fun doesValidSessionExist(userId: UserId): Either = wrapStorageRequest { accountsDAO.doesValidAccountExists(userId.toDao()) } - override fun fullAccountInfo(userId: UserId): Either = + override suspend fun fullAccountInfo(userId: UserId): Either = wrapStorageRequest { accountsDAO.fullAccountInfo(userId.toDao()) } .flatMap { val accountInfo = sessionMapper.fromAccountInfoEntity(it.info) From 0f8fcd1e0b7196cf36bd66acb31690647414f290 Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Thu, 19 Mar 2026 12:26:34 +0100 Subject: [PATCH 2/8] detekt fix --- .../kotlin/com/wire/kalium/persistence/db/UserDatabase.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt b/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt index 0e6671ee4b8a..372c44c4fd39 100644 --- a/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt +++ b/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt @@ -60,7 +60,8 @@ actual fun userDatabaseDriverByPath( passphrase: UserDBSecret?, enableWAL: Boolean ): SqlDriver { - // TODO: Honor the requested JS database identity instead of ignoring path; the current worker driver setup always opens an anonymous DB. + // TODO: Honor the requested JS database identity instead of ignoring path; + // the current worker driver setup always opens an anonymous DB. return createKaliumWebWorkerDriver() } From c660f9b84f71973bbf4d4bc475e4a2c65df6199a Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Thu, 19 Mar 2026 14:46:41 +0100 Subject: [PATCH 3/8] make AuthenticationScopeForConfigIdUseCase suspend --- .../AuthenticationScopeForConfigIdUseCase.kt | 2 +- .../kotlin/com/wire/kalium/logic/network/SessionManagerImpl.kt | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/auth/autoVersioningAuth/AuthenticationScopeForConfigIdUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/auth/autoVersioningAuth/AuthenticationScopeForConfigIdUseCase.kt index b6e27b57229b..f34051ef2f10 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/auth/autoVersioningAuth/AuthenticationScopeForConfigIdUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/auth/autoVersioningAuth/AuthenticationScopeForConfigIdUseCase.kt @@ -35,7 +35,7 @@ public class AuthenticationScopeForConfigIdUseCase internal constructor( private val serverConfigMapper: ServerConfigMapper, private val authenticationScopeFactory: (ServerConfig) -> AuthenticationScope, ) { - public operator fun invoke(configId: String): AutoVersionAuthScopeUseCase.Result = + public suspend operator fun invoke(configId: String): AutoVersionAuthScopeUseCase.Result = wrapStorageRequest { serverConfigurationDAO.configById(configId) } .fold( { AutoVersionAuthScopeUseCase.Result.Failure.Generic(it) }, diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/network/SessionManagerImpl.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/network/SessionManagerImpl.kt index 9cf4d25a697a..2d974078d4eb 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/network/SessionManagerImpl.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/network/SessionManagerImpl.kt @@ -47,6 +47,7 @@ import com.wire.kalium.network.session.SessionManager import com.wire.kalium.persistence.client.AuthTokenStorage import com.wire.kalium.util.KaliumDispatcherImpl import io.ktor.http.HttpStatusCode +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import kotlin.coroutines.CoroutineContext @@ -146,7 +147,7 @@ internal class SessionManagerImpl internal constructor( sessionMapper.fromEntityToProxyCredentialsDTO(it) }) - private fun account(): Account = account ?: run { + private fun account(): Account = account ?: runBlocking { sessionRepository.fullAccountInfo(userId) .onSuccess { account = it } .fold({ error("user account is missing or an error occurred while reading local storage") }, { it }) From 1078c3dbfe3e2111b28e23ca694a620a7d326eeb Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Tue, 12 May 2026 09:32:13 +0200 Subject: [PATCH 4/8] fix: stabilize async sqldelight rebase --- data/persistence/build.gradle.kts | 3 +- .../persistence/backup/DatabaseExporter.kt | 3 +- .../dao/backup/NomadMessagesDAO.kt | 6 +- .../backup/RemoteBackupChangeLogDAOImpl.kt | 5 +- .../dao/conversation/ConversationDAOImpl.kt | 14 +-- .../dao/message/CompositeMessageDAO.kt | 3 +- .../persistence/dao/message/MessageDAOImpl.kt | 5 +- .../dao/message/MessageInsertExtension.kt | 3 +- .../dao/newclient/NewClientDAOImpl.kt | 3 +- .../kalium/persistence/db/GlobalDatabase.kt | 18 +++- .../db/SchemaInitializingSqlDriver.kt | 91 +++++++++++++++++++ .../kalium/persistence/db/UserDatabase.kt | 21 ++++- .../persistence/db/WebWorkerDriverFactory.kt | 50 +++++++++- .../VerifyDatabaseMigrationsTest.kt | 19 ++-- 14 files changed, 206 insertions(+), 38 deletions(-) create mode 100644 data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/SchemaInitializingSqlDriver.kt diff --git a/data/persistence/build.gradle.kts b/data/persistence/build.gradle.kts index f02e3644ee38..d1e3436e5631 100644 --- a/data/persistence/build.gradle.kts +++ b/data/persistence/build.gradle.kts @@ -101,7 +101,8 @@ kotlin { dependencies { implementation(libs.sqldelight.jsDriver) implementation(npm("@cashapp/sqldelight-sqljs-worker", libs.versions.sqldelight.get())) - implementation(npm("sql.js", "1.6.2")) + implementation(npm("sql.js", "1.8.0")) + implementation(devNpm("webpack", "^5.1.0")) implementation(devNpm("copy-webpack-plugin", "9.1.0")) } } diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/backup/DatabaseExporter.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/backup/DatabaseExporter.kt index 58090088700c..f3be0ed8ce05 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/backup/DatabaseExporter.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/backup/DatabaseExporter.kt @@ -18,6 +18,7 @@ package com.wire.kalium.persistence.backup +import app.cash.sqldelight.async.coroutines.await import com.wire.kalium.persistence.dao.UserIDEntity import com.wire.kalium.persistence.db.PlatformDatabaseData import com.wire.kalium.persistence.db.UserDBSecret @@ -94,7 +95,7 @@ internal class DatabaseExporterImpl internal constructor( try { // attach the plain DB to the user DB // dump the content of the user DB into the plain DB - plainDatabase.database.dumpContentQueries.dumpAllTables() + plainDatabase.database.dumpContentQueries.dumpAllTables().await() } catch (e: CancellationException) { throw e } catch (e: Exception) { diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/backup/NomadMessagesDAO.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/backup/NomadMessagesDAO.kt index 8d0197f9253a..0ff768cf06ad 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/backup/NomadMessagesDAO.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/backup/NomadMessagesDAO.kt @@ -179,7 +179,7 @@ internal class NomadMessagesDAOImpl internal constructor( return true } - private fun insertReactions(message: NomadMessageToInsert) { + private suspend fun insertReactions(message: NomadMessageToInsert) { message.reactions.forEach { reaction -> reaction.emojis.forEach { emoji -> reactionsQueries.insertReaction( @@ -193,7 +193,7 @@ internal class NomadMessagesDAOImpl internal constructor( } } - private fun insertReadReceipts(message: NomadMessageToInsert) { + private suspend fun insertReadReceipts(message: NomadMessageToInsert) { message.readReceipts.forEach { receipt -> receiptsQueries.insertReceipt( message.id, @@ -299,7 +299,7 @@ private class NomadMessageContentWriter( private val messageAttachmentsQueries: MessageAttachmentsQueries, ) { - private suspend fun insertRegularContent(message: NomadMessageToInsert): Boolean { + suspend fun insertRegularContent(message: NomadMessageToInsert): Boolean { val content = message.payload return when (content) { is SyncableMessagePayloadEntity.Text -> insertTextContent(message, content) diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/backup/RemoteBackupChangeLogDAOImpl.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/backup/RemoteBackupChangeLogDAOImpl.kt index a15bcafc0e3e..677523d04008 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/backup/RemoteBackupChangeLogDAOImpl.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/backup/RemoteBackupChangeLogDAOImpl.kt @@ -19,6 +19,7 @@ package com.wire.kalium.persistence.dao.backup import app.cash.sqldelight.async.coroutines.awaitAsList +import app.cash.sqldelight.async.coroutines.await import app.cash.sqldelight.coroutines.asFlow import com.wire.kalium.persistence.RemotebackupChangeLogQueries import com.wire.kalium.persistence.dao.QualifiedIDEntity @@ -98,7 +99,7 @@ internal class RemoteBackupChangeLogDAOImpl( conversationId = conversationId, eventType = ChangeLogEventType.CONVERSATION_DELETE, timestampMs = timestampMs - ) + ).await() } override suspend fun logConversationClear( @@ -109,7 +110,7 @@ internal class RemoteBackupChangeLogDAOImpl( conversationId = conversationId, eventType = ChangeLogEventType.CONVERSATION_CLEAR, timestampMs = timestampMs - ) + ).await() } override suspend fun logConversationMetadataSync( diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt index 203a275fe416..04640922b2fc 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt @@ -21,6 +21,7 @@ package com.wire.kalium.persistence.dao.conversation import app.cash.sqldelight.async.coroutines.awaitAsList import app.cash.sqldelight.async.coroutines.awaitAsOne import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull +import app.cash.sqldelight.async.coroutines.await import app.cash.sqldelight.coroutines.asFlow import com.wire.kalium.persistence.ConversationDetailsQueries @@ -662,7 +663,7 @@ internal class ConversationDAOImpl internal constructor( override suspend fun clearContent(conversationId: QualifiedIDEntity) { withContext(writeDispatcher.value) { - conversationQueries.clearContent(conversationId) + conversationQueries.clearContent(conversationId).await() } } @@ -685,19 +686,12 @@ internal class ConversationDAOImpl internal constructor( conversationId: QualifiedIDEntity, legalHoldStatus: ConversationEntity.LegalHoldStatus ) = withContext(writeDispatcher.value) { - conversationQueries.transactionWithResult { - conversationQueries.updateLegalHoldStatus(legalHoldStatus, conversationId) - conversationQueries.selectChanges().awaitAsOne() > 0 - } - + conversationQueries.updateLegalHoldStatus(legalHoldStatus, conversationId).await() > 0 } override suspend fun updateLegalHoldStatusChangeNotified(conversationId: QualifiedIDEntity, notified: Boolean) = withContext(writeDispatcher.value) { - conversationQueries.transactionWithResult { - conversationQueries.upsertLegalHoldStatusChangeNotified(conversationId, notified) - conversationQueries.selectChanges().awaitAsOne() > 0 - } + conversationQueries.upsertLegalHoldStatusChangeNotified(conversationId, notified).await() > 0 } override suspend fun observeLegalHoldStatus(conversationId: QualifiedIDEntity) = diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/CompositeMessageDAO.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/CompositeMessageDAO.kt index 26a692371369..5a7faa2b1bf0 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/CompositeMessageDAO.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/CompositeMessageDAO.kt @@ -17,6 +17,7 @@ */ package com.wire.kalium.persistence.dao.message +import app.cash.sqldelight.async.coroutines.await import com.wire.kalium.persistence.content.ButtonContentQueries import com.wire.kalium.persistence.dao.QualifiedIDEntity import com.wire.kalium.persistence.db.WriteDispatcher @@ -45,7 +46,7 @@ internal class CompositeMessageDAOImpl internal constructor( buttonId: String ) { withContext(writeDispatcher.value) { - buttonContentQueries.markSelected(conversation_id = conversationId, message_id = messageId, id = buttonId) + buttonContentQueries.markSelected(conversation_id = conversationId, message_id = messageId, id = buttonId).await() } } diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt index 49a1f1e3a758..bbc4ff203463 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt @@ -21,6 +21,7 @@ package com.wire.kalium.persistence.dao.message import app.cash.sqldelight.async.coroutines.awaitAsList import app.cash.sqldelight.async.coroutines.awaitAsOne import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull +import app.cash.sqldelight.async.coroutines.await import app.cash.sqldelight.coroutines.asFlow import com.wire.kalium.persistence.ConversationsQueries @@ -98,7 +99,7 @@ internal class MessageDAOImpl internal constructor( queries.markMessageAsDeleted( message_id = id, conversation_id = conversationsId - ) + ).await() unreadEventsQueries.deleteUnreadEvent(id, conversationsId) } } @@ -536,7 +537,7 @@ internal class MessageDAOImpl internal constructor( conversation_id = conversationId, message_id = messageUuid, delivery_duration = Instant.fromEpochMilliseconds(millis) - ) + ).await() } } diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageInsertExtension.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageInsertExtension.kt index 3b20d5873708..be33725c8e61 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageInsertExtension.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageInsertExtension.kt @@ -19,6 +19,7 @@ package com.wire.kalium.persistence.dao.message import app.cash.sqldelight.async.coroutines.awaitAsList import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull +import app.cash.sqldelight.async.coroutines.await import com.wire.kalium.persistence.ConversationsQueries import com.wire.kalium.persistence.MessageAttachmentsQueries @@ -95,7 +96,7 @@ internal class MessageInsertExtensionImpl( assetSha256 = assetSha256Key, assetEncryptionAlgorithm = assetEncryptionAlgorithm, assetNormalizedLoudness = assetNormalizedLoudness, - ) + ).await() } } diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/newclient/NewClientDAOImpl.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/newclient/NewClientDAOImpl.kt index 1a904129ab6d..182457908f4d 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/newclient/NewClientDAOImpl.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/newclient/NewClientDAOImpl.kt @@ -17,6 +17,7 @@ */ package com.wire.kalium.persistence.dao.newclient +import app.cash.sqldelight.async.coroutines.await import app.cash.sqldelight.coroutines.asFlow import com.wire.kalium.persistence.NewClientQueries import com.wire.kalium.persistence.dao.client.DeviceTypeEntity @@ -66,7 +67,7 @@ internal class NewClientDAOImpl( model = model, label = label, mls_public_keys = mlsPublicKeys - ) + ).await() } } } diff --git a/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/GlobalDatabase.kt b/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/GlobalDatabase.kt index 4e8ffbf4ffc5..da85b9328583 100644 --- a/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/GlobalDatabase.kt +++ b/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/GlobalDatabase.kt @@ -18,6 +18,8 @@ package com.wire.kalium.persistence.db +import app.cash.sqldelight.async.coroutines.await +import com.wire.kalium.persistence.GlobalDatabase import kotlinx.coroutines.CoroutineDispatcher actual fun globalDatabaseProvider( @@ -25,12 +27,22 @@ actual fun globalDatabaseProvider( queriesContext: CoroutineDispatcher, passphrase: GlobalDatabaseSecret?, enableWAL: Boolean -): GlobalDatabaseBuilder = - GlobalDatabaseBuilder( - sqlDriver = createKaliumWebWorkerDriver(), +): GlobalDatabaseBuilder { + val rawDriver = createKaliumWebWorkerDriver() + val initializedDriver = SchemaInitializingSqlDriver(rawDriver) { + GlobalDatabase.Schema.create(rawDriver).await() + rawDriver.execute( + identifier = null, + sql = "PRAGMA foreign_keys = 1;", + parameters = 0, + ).await() + } + return GlobalDatabaseBuilder( + sqlDriver = initializedDriver, platformDatabaseData = platformDatabaseData, queriesContext = queriesContext ) +} actual fun nuke(platformDatabaseData: PlatformDatabaseData): Boolean { // TODO: Implement real JS global database deletion once the worker driver uses a stable persisted storage key. diff --git a/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/SchemaInitializingSqlDriver.kt b/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/SchemaInitializingSqlDriver.kt new file mode 100644 index 000000000000..4b15d206031c --- /dev/null +++ b/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/SchemaInitializingSqlDriver.kt @@ -0,0 +1,91 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.persistence.db + +import app.cash.sqldelight.Query +import app.cash.sqldelight.Transacter +import app.cash.sqldelight.async.coroutines.await +import app.cash.sqldelight.db.QueryResult +import app.cash.sqldelight.db.SqlCursor +import app.cash.sqldelight.db.SqlDriver +import app.cash.sqldelight.db.SqlPreparedStatement +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.promise + +internal class SchemaInitializingSqlDriver( + private val delegate: SqlDriver, + initialize: suspend () -> Unit, +) : SqlDriver { + + private val initialized = CompletableDeferred() + + init { + GlobalScope.promise { + initialize() + initialized.complete(Unit) + }.catch { error -> + initialized.completeExceptionally(error) + } + } + + override fun addListener(vararg queryKeys: String, listener: Query.Listener) { + delegate.addListener(*queryKeys, listener = listener) + } + + override fun removeListener(vararg queryKeys: String, listener: Query.Listener) { + delegate.removeListener(*queryKeys, listener = listener) + } + + override fun notifyListeners(vararg queryKeys: String) { + delegate.notifyListeners(*queryKeys) + } + + override fun close() { + delegate.close() + } + + override fun currentTransaction(): Transacter.Transaction? = delegate.currentTransaction() + + override fun executeQuery( + identifier: Int?, + sql: String, + mapper: (SqlCursor) -> QueryResult, + parameters: Int, + binders: (SqlPreparedStatement.() -> Unit)?, + ): QueryResult = QueryResult.AsyncValue { + initialized.await() + delegate.executeQuery(identifier, sql, mapper, parameters, binders).await() + } + + override fun execute( + identifier: Int?, + sql: String, + parameters: Int, + binders: (SqlPreparedStatement.() -> Unit)?, + ): QueryResult = QueryResult.AsyncValue { + initialized.await() + delegate.execute(identifier, sql, parameters, binders).await() + } + + override fun newTransaction(): QueryResult = QueryResult.AsyncValue { + initialized.await() + delegate.newTransaction().await() + } +} diff --git a/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt b/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt index 372c44c4fd39..cae4c9ba8bab 100644 --- a/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt +++ b/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt @@ -20,7 +20,9 @@ package com.wire.kalium.persistence.db +import app.cash.sqldelight.async.coroutines.await import app.cash.sqldelight.db.SqlDriver +import com.wire.kalium.persistence.UserDatabase import com.wire.kalium.persistence.dao.UserIDEntity import kotlinx.coroutines.CoroutineDispatcher @@ -36,12 +38,27 @@ actual fun userDatabaseBuilder( dbInvalidationControlEnabled: Boolean ): UserDatabaseBuilder { val rawDriver = createKaliumWebWorkerDriver() + val initializedDriver = SchemaInitializingSqlDriver(rawDriver) { + UserDatabase.Schema.create(rawDriver).await() + rawDriver.execute( + identifier = null, + sql = "INSERT OR IGNORE INTO SelfUser(id) VALUES(?);", + parameters = 1 + ) { + bindString(0, userId.toString()) + }.await() + rawDriver.execute( + identifier = null, + sql = "PRAGMA foreign_keys = 1;", + parameters = 0, + ).await() + } val invalidationController = DbInvalidationController( enabled = dbInvalidationControlEnabled, - notifyKey = { key -> rawDriver.notifyListeners(key) } + notifyKey = { key -> initializedDriver.notifyListeners(key) } ) val driver: SqlDriver = MutedSqlDriver( - delegate = rawDriver, + delegate = initializedDriver, invalidationController = invalidationController ) return UserDatabaseBuilder( diff --git a/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/WebWorkerDriverFactory.kt b/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/WebWorkerDriverFactory.kt index 278e3ae4a1d2..d6f4c6920b86 100644 --- a/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/WebWorkerDriverFactory.kt +++ b/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/WebWorkerDriverFactory.kt @@ -28,15 +28,59 @@ internal fun createKaliumWebWorkerDriver(): SqlDriver = WebWorkerDriver(createKa private fun createKaliumWorker(): Worker { val hasBrowserWorker = js("typeof window !== 'undefined' && typeof Worker !== 'undefined'").unsafeCast() if (hasBrowserWorker) { - return Worker(js("""new URL("@cashapp/sqldelight-sqljs-worker/sqljs.worker.js", import.meta.url)""")) + return Worker(js("""new URL("@cashapp/sqldelight-sqljs-worker/sqljs.worker.js", window.location.href)""")) } return js( """ (function() { var WorkerCtor = require('node:worker_threads').Worker; - var workerPath = require.resolve('@cashapp/sqldelight-sqljs-worker/sqljs.worker.js'); - var worker = new WorkerCtor(workerPath); + var sqlJsModulePath = require.resolve('sql.js'); + var sqlJsWasmPath = require.resolve('sql.js/dist/sql-wasm.wasm'); + var workerCode = [ + "const { parentPort, workerData } = require('node:worker_threads');", + "const initSqlJs = require(workerData.sqlJsModulePath);", + "let dbPromise = initSqlJs({ locateFile: () => workerData.sqlJsWasmPath }).then(SQL => new SQL.Database());", + "function resultFor(db, sql, params) {", + " const result = db.exec(sql, params)[0];", + " if (result) return result;", + " return { values: [[db.getRowsModified()]] };", + "}", + "parentPort.on('message', async (data) => {", + " try {", + " const db = await dbPromise;", + " switch (data && data.action) {", + " case 'exec':", + " if (!data.sql) throw new Error('exec: Missing query string');", + " parentPort.postMessage({ id: data.id, results: resultFor(db, data.sql, data.params) });", + " break;", + " case 'begin_transaction':", + " parentPort.postMessage({ id: data.id, results: resultFor(db, 'BEGIN TRANSACTION;', []) });", + " break;", + " case 'end_transaction':", + " parentPort.postMessage({ id: data.id, results: resultFor(db, 'END TRANSACTION;', []) });", + " break;", + " case 'rollback_transaction':", + " parentPort.postMessage({ id: data.id, results: resultFor(db, 'ROLLBACK TRANSACTION;', []) });", + " break;", + " default:", + " throw new Error('Unsupported action: ' + (data && data.action));", + " }", + " } catch (error) {", + " parentPort.postMessage({ id: data && data.id, error: { message: error && error.message, name: error && error.name } });", + " }", + "});" + ].join('\n'); + var worker = new WorkerCtor(workerCode, { + eval: true, + workerData: { + sqlJsModulePath: sqlJsModulePath, + sqlJsWasmPath: sqlJsWasmPath + } + }); + if (typeof worker.unref === 'function') { + worker.unref(); + } var listeners = new Map(); return { diff --git a/data/persistence/src/jvmTest/kotlin/com/wire/kalium/persistence/migrations/VerifyDatabaseMigrationsTest.kt b/data/persistence/src/jvmTest/kotlin/com/wire/kalium/persistence/migrations/VerifyDatabaseMigrationsTest.kt index d94c8055381e..cb6aaa5f8a12 100644 --- a/data/persistence/src/jvmTest/kotlin/com/wire/kalium/persistence/migrations/VerifyDatabaseMigrationsTest.kt +++ b/data/persistence/src/jvmTest/kotlin/com/wire/kalium/persistence/migrations/VerifyDatabaseMigrationsTest.kt @@ -17,6 +17,8 @@ */ package com.wire.kalium.persistence.migrations +import app.cash.sqldelight.async.coroutines.await +import app.cash.sqldelight.async.coroutines.awaitMigrate import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver import com.wire.kalium.persistence.GlobalDatabase import com.wire.kalium.persistence.UserDatabase @@ -24,13 +26,14 @@ import com.wire.kalium.persistence.migrations.dump.SchemaDump import com.wire.kalium.persistence.migrations.dump.SqliteSchemaDumper import dev.andrewbailey.diff.differenceOf import java.io.File +import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals class VerifyDatabaseMigrationsTest { @Test - fun verifyUserDatabaseMigrations() { + fun verifyUserDatabaseMigrations() = runTest { // given // clean up and prepare base schema for derivation File("build/user-schema-dumps").deleteRecursively() @@ -61,7 +64,7 @@ class VerifyDatabaseMigrationsTest { } @Test - fun verifyGlobalDatabaseMigrations() { + fun verifyGlobalDatabaseMigrations() = runTest { // given // clean up and prepare base schema for derivation File("build/global-schema-dumps").deleteRecursively() @@ -112,32 +115,32 @@ class VerifyDatabaseMigrationsTest { private class Arrangement(private val databaseSchemaSource: DatabaseSchemaSource) { - fun withFreshDatabaseSchemaFromDefinitions() = apply { + suspend fun withFreshDatabaseSchemaFromDefinitions() = apply { val dbFile = File(databaseSchemaSource.freshSchemaDbPath) dbFile.parentFile.mkdirs() val driver = JdbcSqliteDriver("jdbc:sqlite:${dbFile.absolutePath}") when (databaseSchemaSource) { - is DatabaseSchemaSource.UserDatabaseDefaults -> UserDatabase.Companion.Schema.create(driver) - is DatabaseSchemaSource.GlobalDatabaseDefaults -> GlobalDatabase.Companion.Schema.create(driver) + is DatabaseSchemaSource.UserDatabaseDefaults -> UserDatabase.Companion.Schema.create(driver).await() + is DatabaseSchemaSource.GlobalDatabaseDefaults -> GlobalDatabase.Companion.Schema.create(driver).await() } driver.close() println("Fresh-schema DB created at: ${dbFile.absolutePath}") } - fun withDerivedUserDatabaseFromMigrations() = apply { + suspend fun withDerivedUserDatabaseFromMigrations() = apply { val dbFile = File(databaseSchemaSource.derivedSchemaDbPath) dbFile.parentFile.mkdirs() val driver = JdbcSqliteDriver("jdbc:sqlite:${dbFile.absolutePath}") when (databaseSchemaSource) { - is DatabaseSchemaSource.UserDatabaseDefaults -> UserDatabase.Companion.Schema.migrate( + is DatabaseSchemaSource.UserDatabaseDefaults -> UserDatabase.Companion.Schema.awaitMigrate( driver, 34, // Starting from 34 since 33.sqm has errors. UserDatabase.Companion.Schema.version ) - is DatabaseSchemaSource.GlobalDatabaseDefaults -> GlobalDatabase.Companion.Schema.migrate( + is DatabaseSchemaSource.GlobalDatabaseDefaults -> GlobalDatabase.Companion.Schema.awaitMigrate( driver, 5, // Starting from 5 since 4.sqm has errors. GlobalDatabase.Companion.Schema.version From 7a821b1d96955853f3e40b80cca9023f5fa77af4 Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Tue, 12 May 2026 11:19:55 +0200 Subject: [PATCH 5/8] fix: stabilize sqldelight async persistence tests --- .../wire/kalium/persistence/utils/IgnoreJS.kt | 21 ++++ .../wire/kalium/persistence/utils/IgnoreJS.kt | 21 ++++ .../wire/kalium/persistence/utils/IgnoreJS.kt | 21 ++++ .../src/commonMain/db_user/migrations/1.sqm | 2 +- .../src/commonMain/db_user/migrations/10.sqm | 4 +- .../src/commonMain/db_user/migrations/11.sqm | 4 +- .../src/commonMain/db_user/migrations/12.sqm | 6 +- .../src/commonMain/db_user/migrations/14.sqm | 2 +- .../src/commonMain/db_user/migrations/15.sqm | 4 +- .../src/commonMain/db_user/migrations/16.sqm | 2 +- .../src/commonMain/db_user/migrations/17.sqm | 2 +- .../src/commonMain/db_user/migrations/19.sqm | 2 +- .../src/commonMain/db_user/migrations/2.sqm | 4 +- .../src/commonMain/db_user/migrations/20.sqm | 2 +- .../src/commonMain/db_user/migrations/21.sqm | 2 +- .../src/commonMain/db_user/migrations/22.sqm | 2 +- .../src/commonMain/db_user/migrations/23.sqm | 2 +- .../src/commonMain/db_user/migrations/4.sqm | 2 +- .../src/commonMain/db_user/migrations/6.sqm | 2 +- .../src/commonMain/db_user/migrations/7.sqm | 4 +- .../com/wire/kalium/persistence/dao/AppDAO.kt | 3 +- .../dao/conversation/ConversationDAOImpl.kt | 6 +- .../conversation/ConversationExtensions.kt | 9 +- .../dao/message/AsyncQueryPagingSource.kt | 100 ++++++++++++++++++ .../persistence/dao/message/MessageDAOImpl.kt | 2 +- .../dao/message/MessageExtensions.kt | 17 ++- .../dao/message/MessageInsertExtension.kt | 2 +- .../dao/pendingaction/PendingActionDAOImpl.kt | 3 +- .../backup/DatabaseExporterTest.kt | 2 + .../backup/DatabaseImporterTest.kt | 2 + .../kalium/persistence/backup/NukeDBTest.kt | 2 + .../dao/event/EventMigration109Test.kt | 7 +- .../wire/kalium/persistence/utils/IgnoreJS.kt | 21 ++++ .../persistence/db/WebWorkerDriverFactory.kt | 25 ++++- .../wire/kalium/persistence/utils/IgnoreJS.kt | 23 ++++ .../wire/kalium/persistence/utils/IgnoreJS.kt | 21 ++++ 36 files changed, 304 insertions(+), 52 deletions(-) create mode 100644 data/persistence/src/androidDeviceTest/kotlin/com/wire/kalium/persistence/utils/IgnoreJS.kt create mode 100644 data/persistence/src/androidHostTest/kotlin/com/wire/kalium/persistence/utils/IgnoreJS.kt create mode 100644 data/persistence/src/appleTest/kotlin/com/wire/kalium/persistence/utils/IgnoreJS.kt create mode 100644 data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/AsyncQueryPagingSource.kt create mode 100644 data/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/utils/IgnoreJS.kt create mode 100644 data/persistence/src/jsTest/kotlin/com/wire/kalium/persistence/utils/IgnoreJS.kt create mode 100644 data/persistence/src/jvmTest/kotlin/com/wire/kalium/persistence/utils/IgnoreJS.kt diff --git a/data/persistence/src/androidDeviceTest/kotlin/com/wire/kalium/persistence/utils/IgnoreJS.kt b/data/persistence/src/androidDeviceTest/kotlin/com/wire/kalium/persistence/utils/IgnoreJS.kt new file mode 100644 index 000000000000..3c3760d8ace9 --- /dev/null +++ b/data/persistence/src/androidDeviceTest/kotlin/com/wire/kalium/persistence/utils/IgnoreJS.kt @@ -0,0 +1,21 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.persistence.utils + +actual annotation class IgnoreJS diff --git a/data/persistence/src/androidHostTest/kotlin/com/wire/kalium/persistence/utils/IgnoreJS.kt b/data/persistence/src/androidHostTest/kotlin/com/wire/kalium/persistence/utils/IgnoreJS.kt new file mode 100644 index 000000000000..3c3760d8ace9 --- /dev/null +++ b/data/persistence/src/androidHostTest/kotlin/com/wire/kalium/persistence/utils/IgnoreJS.kt @@ -0,0 +1,21 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.persistence.utils + +actual annotation class IgnoreJS diff --git a/data/persistence/src/appleTest/kotlin/com/wire/kalium/persistence/utils/IgnoreJS.kt b/data/persistence/src/appleTest/kotlin/com/wire/kalium/persistence/utils/IgnoreJS.kt new file mode 100644 index 000000000000..3c3760d8ace9 --- /dev/null +++ b/data/persistence/src/appleTest/kotlin/com/wire/kalium/persistence/utils/IgnoreJS.kt @@ -0,0 +1,21 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.persistence.utils + +actual annotation class IgnoreJS diff --git a/data/persistence/src/commonMain/db_user/migrations/1.sqm b/data/persistence/src/commonMain/db_user/migrations/1.sqm index 2924d697651e..026355f8b273 100644 --- a/data/persistence/src/commonMain/db_user/migrations/1.sqm +++ b/data/persistence/src/commonMain/db_user/migrations/1.sqm @@ -1,4 +1,4 @@ -DROP VIEW ConversatonDetails; +DROP VIEW IF EXISTS ConversatonDetails; CREATE VIEW IF NOT EXISTS ConversationDetails AS SELECT Conversation.qualified_id AS qualifiedId, diff --git a/data/persistence/src/commonMain/db_user/migrations/10.sqm b/data/persistence/src/commonMain/db_user/migrations/10.sqm index fc1181f88829..0202ed35977e 100644 --- a/data/persistence/src/commonMain/db_user/migrations/10.sqm +++ b/data/persistence/src/commonMain/db_user/migrations/10.sqm @@ -1,5 +1,5 @@ -DROP VIEW MessageDetailsView; -DROP VIEW ConversationDetails; +DROP VIEW IF EXISTS MessageDetailsView; +DROP VIEW IF EXISTS ConversationDetails; CREATE VIEW IF NOT EXISTS MessageDetailsView AS SELECT diff --git a/data/persistence/src/commonMain/db_user/migrations/11.sqm b/data/persistence/src/commonMain/db_user/migrations/11.sqm index 21041b36f6f8..12700b9a3d10 100644 --- a/data/persistence/src/commonMain/db_user/migrations/11.sqm +++ b/data/persistence/src/commonMain/db_user/migrations/11.sqm @@ -1,5 +1,5 @@ -DROP VIEW MessageDetailsView; -DROP VIEW ConversationDetails; +DROP VIEW IF EXISTS MessageDetailsView; +DROP VIEW IF EXISTS ConversationDetails; CREATE VIEW IF NOT EXISTS MessageDetailsView AS SELECT diff --git a/data/persistence/src/commonMain/db_user/migrations/12.sqm b/data/persistence/src/commonMain/db_user/migrations/12.sqm index e2724089531c..ff79b7d2bd45 100644 --- a/data/persistence/src/commonMain/db_user/migrations/12.sqm +++ b/data/persistence/src/commonMain/db_user/migrations/12.sqm @@ -1,6 +1,6 @@ -DROP VIEW MessageDetailsView; -DROP VIEW ConversationDetails; -DROP VIEW MessageQuoteDetails; +DROP VIEW IF EXISTS MessageDetailsView; +DROP VIEW IF EXISTS ConversationDetails; +DROP VIEW IF EXISTS MessageQuoteDetails; CREATE VIEW IF NOT EXISTS MessageDetailsView AS SELECT diff --git a/data/persistence/src/commonMain/db_user/migrations/14.sqm b/data/persistence/src/commonMain/db_user/migrations/14.sqm index 5cd574ac403f..3e9de8e115a8 100644 --- a/data/persistence/src/commonMain/db_user/migrations/14.sqm +++ b/data/persistence/src/commonMain/db_user/migrations/14.sqm @@ -1,4 +1,4 @@ -DROP VIEW MessagePreview; +DROP VIEW IF EXISTS MessagePreview; CREATE VIEW IF NOT EXISTS MessagePreview AS SELECT diff --git a/data/persistence/src/commonMain/db_user/migrations/15.sqm b/data/persistence/src/commonMain/db_user/migrations/15.sqm index 0e8385431146..4305b2f2a9c0 100644 --- a/data/persistence/src/commonMain/db_user/migrations/15.sqm +++ b/data/persistence/src/commonMain/db_user/migrations/15.sqm @@ -1,6 +1,6 @@ import kotlin.Boolean; -DROP VIEW MessageDetailsView; +DROP VIEW IF EXISTS MessageDetailsView; ALTER TABLE Message ADD COLUMN expects_read_confirmation INTEGER AS Boolean NOT NULL DEFAULT(0); CREATE VIEW IF NOT EXISTS MessageDetailsView @@ -112,7 +112,7 @@ LEFT JOIN MessageConversationChangedContent AS ConversationNameChangedContent ON LEFT JOIN MessageQuoteDetails AS QuotedMessage ON Message.id = QuotedMessage.messageId AND Message.conversation_id = QuotedMessage.conversationId LEFT JOIN SelfUser; -DROP VIEW ReceiptDetails; +DROP VIEW IF EXISTS ReceiptDetails; CREATE VIEW IF NOT EXISTS ReceiptDetails AS SELECT Receipt.type, diff --git a/data/persistence/src/commonMain/db_user/migrations/16.sqm b/data/persistence/src/commonMain/db_user/migrations/16.sqm index a52cbcb9ac57..516d2a1543c8 100644 --- a/data/persistence/src/commonMain/db_user/migrations/16.sqm +++ b/data/persistence/src/commonMain/db_user/migrations/16.sqm @@ -1,6 +1,6 @@ import kotlin.Boolean; -DROP VIEW MessageDetailsView; +DROP VIEW IF EXISTS MessageDetailsView; ALTER TABLE MessageFailedToDecryptContent ADD COLUMN is_decryption_resolved INTEGER AS Boolean NOT NULL DEFAULT(0); CREATE VIEW IF NOT EXISTS MessageDetailsView diff --git a/data/persistence/src/commonMain/db_user/migrations/17.sqm b/data/persistence/src/commonMain/db_user/migrations/17.sqm index 3fd64d7bc860..8f6e4f9df518 100644 --- a/data/persistence/src/commonMain/db_user/migrations/17.sqm +++ b/data/persistence/src/commonMain/db_user/migrations/17.sqm @@ -1,6 +1,6 @@ import kotlin.Boolean; -DROP VIEW MessagePreview; +DROP VIEW IF EXISTS MessagePreview; UPDATE Conversation SET muted_status = 'ONLY_MENTIONS_AND_REPLIES_ALLOWED' diff --git a/data/persistence/src/commonMain/db_user/migrations/19.sqm b/data/persistence/src/commonMain/db_user/migrations/19.sqm index 1462e27df05c..5cdaf4496907 100644 --- a/data/persistence/src/commonMain/db_user/migrations/19.sqm +++ b/data/persistence/src/commonMain/db_user/migrations/19.sqm @@ -6,7 +6,7 @@ BEGIN WHERE qualified_id = new.conversation_id AND muted_status = 'ALL_MUTED'; END; -DROP VIEW MessagePreview; +DROP VIEW IF EXISTS MessagePreview; CREATE VIEW IF NOT EXISTS MessagePreview AS SELECT Message.id AS id, diff --git a/data/persistence/src/commonMain/db_user/migrations/2.sqm b/data/persistence/src/commonMain/db_user/migrations/2.sqm index 51f6abacebb7..2c5de7deef31 100644 --- a/data/persistence/src/commonMain/db_user/migrations/2.sqm +++ b/data/persistence/src/commonMain/db_user/migrations/2.sqm @@ -1,5 +1,5 @@ -DROP VIEW MessageDetailsView; -DROP VIEW ConversationDetails; +DROP VIEW IF EXISTS MessageDetailsView; +DROP VIEW IF EXISTS ConversationDetails; CREATE VIEW IF NOT EXISTS MessageDetailsView AS SELECT diff --git a/data/persistence/src/commonMain/db_user/migrations/20.sqm b/data/persistence/src/commonMain/db_user/migrations/20.sqm index 7e09dd65dfdd..0b5fef2d1d99 100644 --- a/data/persistence/src/commonMain/db_user/migrations/20.sqm +++ b/data/persistence/src/commonMain/db_user/migrations/20.sqm @@ -1,4 +1,4 @@ -DROP VIEW MessagePreview; +DROP VIEW IF EXISTS MessagePreview; CREATE VIEW IF NOT EXISTS MessagePreview AS SELECT diff --git a/data/persistence/src/commonMain/db_user/migrations/21.sqm b/data/persistence/src/commonMain/db_user/migrations/21.sqm index 07ba52dd0141..793aa96a5be7 100644 --- a/data/persistence/src/commonMain/db_user/migrations/21.sqm +++ b/data/persistence/src/commonMain/db_user/migrations/21.sqm @@ -1,6 +1,6 @@ import com.wire.kalium.persistence.dao.conversation.ConversationEntity; -DROP VIEW ConversationDetails; +DROP VIEW IF EXISTS ConversationDetails; ALTER TABLE Conversation ADD COLUMN receipt_mode TEXT AS ConversationEntity.ReceiptMode DEFAULT "DISABLED" NOT NULL; CREATE VIEW IF NOT EXISTS ConversationDetails AS diff --git a/data/persistence/src/commonMain/db_user/migrations/22.sqm b/data/persistence/src/commonMain/db_user/migrations/22.sqm index 7a43f04755ba..703793eb6a07 100644 --- a/data/persistence/src/commonMain/db_user/migrations/22.sqm +++ b/data/persistence/src/commonMain/db_user/migrations/22.sqm @@ -1,6 +1,6 @@ DROP TRIGGER IF EXISTS updateMutedConversationNotificationDateAfterNewMessage; -DROP VIEW MessagePreview; +DROP VIEW IF EXISTS MessagePreview; CREATE VIEW IF NOT EXISTS MessagePreview AS SELECT diff --git a/data/persistence/src/commonMain/db_user/migrations/23.sqm b/data/persistence/src/commonMain/db_user/migrations/23.sqm index 3ac2d1313317..17ab2d4ab76c 100644 --- a/data/persistence/src/commonMain/db_user/migrations/23.sqm +++ b/data/persistence/src/commonMain/db_user/migrations/23.sqm @@ -2,7 +2,7 @@ import com.wire.kalium.persistence.dao.QualifiedIDEntity; import com.wire.kalium.persistence.dao.receipt.ReceiptTypeEntity; -- Delete the ReceiptDetails View -DROP VIEW ReceiptDetails; +DROP VIEW IF EXISTS ReceiptDetails; -- Create new table with ON UPDATE CASCADE CREATE TABLE ReceiptTemp ( diff --git a/data/persistence/src/commonMain/db_user/migrations/4.sqm b/data/persistence/src/commonMain/db_user/migrations/4.sqm index 7dc7ceb2187d..46e1aa98da71 100644 --- a/data/persistence/src/commonMain/db_user/migrations/4.sqm +++ b/data/persistence/src/commonMain/db_user/migrations/4.sqm @@ -1,4 +1,4 @@ -DROP VIEW ConversationDetails; +DROP VIEW IF EXISTS ConversationDetails; CREATE VIEW IF NOT EXISTS ConversationDetails AS SELECT diff --git a/data/persistence/src/commonMain/db_user/migrations/6.sqm b/data/persistence/src/commonMain/db_user/migrations/6.sqm index 3d2926239c1b..4690338a6e99 100644 --- a/data/persistence/src/commonMain/db_user/migrations/6.sqm +++ b/data/persistence/src/commonMain/db_user/migrations/6.sqm @@ -25,7 +25,7 @@ INNER JOIN SelfUser AS SelfUser LEFT JOIN MessageTextContent AS QuotedTextContent ON QuotedTextContent.message_id = QuotedMessage.id AND QuotedMessage.conversation_id = MessageTextContent.conversation_id LEFT JOIN MessageAssetContent AS QuotedAssetContent ON QuotedAssetContent.message_id = QuotedMessage.id AND QuotedMessage.conversation_id = MessageTextContent.conversation_id; -DROP VIEW MessageDetailsView; +DROP VIEW IF EXISTS MessageDetailsView; CREATE VIEW IF NOT EXISTS MessageDetailsView AS SELECT diff --git a/data/persistence/src/commonMain/db_user/migrations/7.sqm b/data/persistence/src/commonMain/db_user/migrations/7.sqm index 8a255639e662..295988df2fb2 100644 --- a/data/persistence/src/commonMain/db_user/migrations/7.sqm +++ b/data/persistence/src/commonMain/db_user/migrations/7.sqm @@ -1,7 +1,7 @@ ALTER TABLE MessageTextContent ADD COLUMN is_quote_verified INTEGER; -DROP VIEW MessageQuoteDetails; -DROP VIEW MessageDetailsView; +DROP VIEW IF EXISTS MessageQuoteDetails; +DROP VIEW IF EXISTS MessageDetailsView; CREATE VIEW IF NOT EXISTS MessageQuoteDetails diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/AppDAO.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/AppDAO.kt index f5b03d2b3b1d..be8ee6030c33 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/AppDAO.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/AppDAO.kt @@ -17,6 +17,7 @@ */ package com.wire.kalium.persistence.dao +import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull import app.cash.sqldelight.coroutines.asFlow import app.cash.sqldelight.coroutines.mapToOneOrNull import com.wire.kalium.persistence.App @@ -118,7 +119,7 @@ internal class AppDAOImpl( } override suspend fun byId(id: QualifiedIDEntity): AppEntity? = withContext(readDispatcher.value) { - appsQueries.byId(id = id, mapper = ::mapToAppEntity).executeAsOneOrNull() + appsQueries.byId(id = id, mapper = ::mapToAppEntity).awaitAsOneOrNull() } /** diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt index 04640922b2fc..ccff7582ed78 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt @@ -92,7 +92,7 @@ internal class ConversationDAOImpl internal constructor( override suspend fun getNonDeletedConversationById( qualifiedID: QualifiedIDEntity ): ConversationEntity? = withContext(readDispatcher.value) { - conversationQueries.selectByQualifiedId(qualifiedID, conversationMapper::fromViewToModel).executeAsOneOrNull() + conversationQueries.selectByQualifiedId(qualifiedID, conversationMapper::fromViewToModel).awaitAsOneOrNull() } override suspend fun observeConversationDetailsById( @@ -505,7 +505,7 @@ internal class ConversationDAOImpl internal constructor( conversationDates.forEach { (conversationId, date) -> unreadEventsQueries.deleteUnreadEvents(date, conversationId) conversationQueries.updateConversationReadDate(date, conversationId) - updatedConversations += conversationQueries.selectChanges().executeAsOne().toInt() + updatedConversations += conversationQueries.selectChanges().awaitAsOne().toInt() } updatedConversations @@ -792,6 +792,6 @@ internal class ConversationDAOImpl internal constructor( offset = offset.toLong(), query = query, mapper = conversationMapper::toConversationEntity - ).executeAsList() + ).awaitAsList() } } diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationExtensions.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationExtensions.kt index 7bb8e0b77a8b..c1bba2a9694a 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationExtensions.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationExtensions.kt @@ -17,11 +17,11 @@ */ package com.wire.kalium.persistence.dao.conversation -import androidx.paging.Pager -import androidx.paging.PagingConfig -import app.cash.sqldelight.paging3.QueryPagingSource +import app.cash.paging.Pager +import app.cash.paging.PagingConfig import com.wire.kalium.persistence.ConversationDetailsWithEventsQueries import com.wire.kalium.persistence.dao.conversation.ConversationExtensions.QueryConfig +import com.wire.kalium.persistence.dao.message.AsyncQueryPagingSource import com.wire.kalium.persistence.dao.message.KaliumPager import com.wire.kalium.persistence.db.ReadDispatcher @@ -73,7 +73,7 @@ internal class ConversationExtensionsImpl internal constructor( * - SELECT still uses full ConversationDetails rules (COUNT can be a superset) */ private fun pagingSource(queryConfig: QueryConfig, initialOffset: Long) = with(queryConfig) { - QueryPagingSource( + AsyncQueryPagingSource( countQuery = if (searchQuery.isBlank()) { queries.countConversations( @@ -90,7 +90,6 @@ internal class ConversationExtensionsImpl internal constructor( strict_mls = if (strictMlsFilter) 1 else 0, ) }, - transacter = queries, context = readDispatcher.value, initialOffset = initialOffset, queryProvider = { limit, offset -> diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/AsyncQueryPagingSource.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/AsyncQueryPagingSource.kt new file mode 100644 index 000000000000..39805eaf8830 --- /dev/null +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/AsyncQueryPagingSource.kt @@ -0,0 +1,100 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.persistence.dao.message + +import app.cash.paging.PagingSource +import app.cash.paging.PagingSourceLoadParams +import app.cash.paging.PagingSourceLoadParamsAppend +import app.cash.paging.PagingSourceLoadParamsPrepend +import app.cash.paging.PagingSourceLoadParamsRefresh +import app.cash.paging.PagingSourceLoadResult +import app.cash.paging.PagingSourceLoadResultError +import app.cash.paging.PagingSourceLoadResultInvalid +import app.cash.paging.PagingSourceLoadResultPage +import app.cash.paging.PagingState +import app.cash.sqldelight.Query +import app.cash.sqldelight.async.coroutines.awaitAsList +import app.cash.sqldelight.async.coroutines.awaitAsOne +import kotlin.coroutines.CoroutineContext +import kotlin.properties.Delegates +import kotlinx.coroutines.withContext + +internal class AsyncQueryPagingSource( + private val countQuery: Query, + private val context: CoroutineContext, + private val queryProvider: (limit: Long, offset: Long) -> Query, + private val initialOffset: Long = 0, +) : PagingSource(), Query.Listener { + private var currentQuery: Query? by Delegates.observable(null) { _, old, new -> + old?.removeListener(this) + new?.addListener(this) + } + + override val jumpingSupported: Boolean get() = true + + init { + registerInvalidatedCallback { + currentQuery?.removeListener(this) + currentQuery = null + } + } + + override fun queryResultsChanged() = invalidate() + + @Suppress("CyclomaticComplexMethod", "TooGenericExceptionCaught") + override suspend fun load(params: PagingSourceLoadParams): PagingSourceLoadResult = + withContext(context) { + try { + val key = params.key?.toLong() ?: initialOffset + val limit = when (params) { + is PagingSourceLoadParamsPrepend<*> -> minOf(key, params.loadSize.toLong()) + else -> params.loadSize.toLong() + } + val count = countQuery.awaitAsOne().toInt() + val offset = when (params) { + is PagingSourceLoadParamsPrepend<*> -> maxOf(0, key - params.loadSize).toInt() + is PagingSourceLoadParamsAppend<*> -> key.toInt() + is PagingSourceLoadParamsRefresh<*> -> + if (key >= count - params.loadSize) maxOf(0, count - params.loadSize) else key.toInt() + else -> error("Unknown PagingSourceLoadParams ${params::class}") + } + val data = queryProvider(limit, offset.toLong()) + .also { currentQuery = it } + .awaitAsList() + val nextPosition = offset + data.size + + if (invalid) { + PagingSourceLoadResultInvalid() + } else { + PagingSourceLoadResultPage( + data = data, + prevKey = offset.takeIf { it > 0 && data.isNotEmpty() }, + nextKey = nextPosition.takeIf { data.isNotEmpty() && data.size >= limit && it < count }, + itemsBefore = offset, + itemsAfter = maxOf(0, count - nextPosition), + ) + } + } catch (exception: Exception) { + PagingSourceLoadResultError(exception) + } + } + + override fun getRefreshKey(state: PagingState): Int? = + state.anchorPosition?.let { maxOf(0, it - (state.config.initialLoadSize / 2)) } +} diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt index bbc4ff203463..fd6acc365419 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt @@ -298,7 +298,7 @@ internal class MessageDAOImpl internal constructor( conversationId: ConversationIDEntity ): Long? = withContext(readDispatcher.value) { queries.selectOldestVisibleMessageTimestampByConversationId(conversationId) - .executeAsOneOrNull() + .awaitAsOneOrNull() ?.MIN?.toEpochMilliseconds() } diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageExtensions.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageExtensions.kt index a8cddef4efc1..9a93d9fa9d83 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageExtensions.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageExtensions.kt @@ -18,9 +18,8 @@ package com.wire.kalium.persistence.dao.message -import androidx.paging.Pager -import androidx.paging.PagingConfig -import app.cash.sqldelight.paging3.QueryPagingSource +import app.cash.paging.Pager +import app.cash.paging.PagingConfig import com.wire.kalium.persistence.MessageAssetViewQueries import com.wire.kalium.persistence.MessagesQueries import com.wire.kalium.persistence.dao.ConversationIDEntity @@ -124,9 +123,8 @@ internal class MessageExtensionsImpl internal constructor( conversationId: ConversationIDEntity, visibilities: Collection, initialOffset: Long - ) = QueryPagingSource( + ) = AsyncQueryPagingSource( countQuery = messagesQueries.countByConversationIdAndVisibility(conversationId, visibilities), - transacter = messagesQueries, context = readDispatcher.value, initialOffset = initialOffset, queryProvider = { limit, offset -> @@ -145,9 +143,8 @@ internal class MessageExtensionsImpl internal constructor( searchQuery: String, conversationId: ConversationIDEntity, initialOffset: Long - ) = QueryPagingSource( + ) = AsyncQueryPagingSource( countQuery = messagesQueries.countBySearchedMessageAndConversationId(searchQuery, conversationId), - transacter = messagesQueries, context = readDispatcher.value, initialOffset = initialOffset, queryProvider = { limit, offset -> @@ -165,14 +162,13 @@ internal class MessageExtensionsImpl internal constructor( conversationId: ConversationIDEntity, mimeTypes: Set, initialOffset: Long - ) = QueryPagingSource( + ) = AsyncQueryPagingSource( countQuery = messageAssetViewQueries.countAssetMessagesByConversationIdAndMimeTypes( conversationId, listOf(MessageEntity.Visibility.VISIBLE), listOf(MessageEntity.ContentType.ASSET), mimeTypes ), - transacter = messageAssetViewQueries, context = readDispatcher.value, initialOffset = initialOffset, queryProvider = { limit, offset -> @@ -192,14 +188,13 @@ internal class MessageExtensionsImpl internal constructor( conversationId: ConversationIDEntity, mimeTypes: Set, initialOffset: Long - ) = QueryPagingSource( + ) = AsyncQueryPagingSource( countQuery = messageAssetViewQueries.countImageAssetMessagesByConversationIdAndMimeTypes( conversationId, listOf(MessageEntity.Visibility.VISIBLE), listOf(MessageEntity.ContentType.ASSET), mimeTypes ), - transacter = messageAssetViewQueries, context = readDispatcher.value, initialOffset = initialOffset, queryProvider = { limit, offset -> diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageInsertExtension.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageInsertExtension.kt index be33725c8e61..2600a2e54209 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageInsertExtension.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageInsertExtension.kt @@ -110,7 +110,7 @@ internal class MessageInsertExtensionImpl( } } catch (e: CancellationException) { throw e - } catch (e: Exception) { + } catch (e: Throwable) { /* no-op */ } } diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/pendingaction/PendingActionDAOImpl.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/pendingaction/PendingActionDAOImpl.kt index 1dc0d7fd010c..2b14a169070d 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/pendingaction/PendingActionDAOImpl.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/pendingaction/PendingActionDAOImpl.kt @@ -17,6 +17,7 @@ */ package com.wire.kalium.persistence.dao.pendingaction +import app.cash.sqldelight.async.coroutines.awaitAsList import com.wire.kalium.persistence.PendingActionsQueries import com.wire.kalium.persistence.dao.QualifiedIDEntity import com.wire.kalium.persistence.db.ReadDispatcher @@ -43,7 +44,7 @@ internal class PendingActionDAOImpl( pendingActionsQueries.selectByActionType( action_type = actionType, mapper = ::PendingActionEntity - ).executeAsList() + ).awaitAsList() } override suspend fun deleteByActionTypeAndIds(actionType: PendingActionType, qualifiedIds: List) { diff --git a/data/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/backup/DatabaseExporterTest.kt b/data/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/backup/DatabaseExporterTest.kt index 49274b809d0c..90e7296376ba 100644 --- a/data/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/backup/DatabaseExporterTest.kt +++ b/data/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/backup/DatabaseExporterTest.kt @@ -22,6 +22,7 @@ import com.wire.kalium.persistence.dao.UserIDEntity import com.wire.kalium.persistence.dao.message.MessageEntity import com.wire.kalium.persistence.dao.message.MessageEntityContent import com.wire.kalium.persistence.db.UserDatabaseBuilder +import com.wire.kalium.persistence.utils.IgnoreJS import com.wire.kalium.persistence.utils.IgnoreIOS import com.wire.kalium.persistence.utils.IgnoreJvm import com.wire.kalium.persistence.utils.stubs.newConversationEntity @@ -41,6 +42,7 @@ import kotlin.test.fail @OptIn(ExperimentalCoroutinesApi::class) @IgnoreJvm @IgnoreIOS +@IgnoreJS class DatabaseExporterTest : BaseDatabaseTest() { private lateinit var localDB: UserDatabaseBuilder diff --git a/data/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/backup/DatabaseImporterTest.kt b/data/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/backup/DatabaseImporterTest.kt index aecf4b3d67be..4c0bdd11ffec 100644 --- a/data/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/backup/DatabaseImporterTest.kt +++ b/data/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/backup/DatabaseImporterTest.kt @@ -25,6 +25,7 @@ import com.wire.kalium.persistence.dao.call.CallEntity import com.wire.kalium.persistence.dao.conversation.ConversationEntity import com.wire.kalium.persistence.dao.member.MemberEntity import com.wire.kalium.persistence.db.UserDatabaseBuilder +import com.wire.kalium.persistence.utils.IgnoreJS import com.wire.kalium.persistence.utils.IgnoreIOS import com.wire.kalium.persistence.utils.IgnoreJvm import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -43,6 +44,7 @@ import kotlin.test.assertTrue @OptIn(ExperimentalCoroutinesApi::class) @IgnoreJvm @IgnoreIOS +@IgnoreJS class DatabaseImporterTest : BaseDatabaseTest() { diff --git a/data/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/backup/NukeDBTest.kt b/data/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/backup/NukeDBTest.kt index 9a2989bb0b84..5ac619577930 100644 --- a/data/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/backup/NukeDBTest.kt +++ b/data/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/backup/NukeDBTest.kt @@ -21,6 +21,7 @@ import com.wire.kalium.persistence.BaseDatabaseTest import com.wire.kalium.persistence.dao.UserIDEntity import com.wire.kalium.persistence.db.UserDatabaseBuilder import com.wire.kalium.persistence.db.nuke +import com.wire.kalium.persistence.utils.IgnoreJS import com.wire.kalium.persistence.utils.IgnoreIOS import com.wire.kalium.persistence.utils.IgnoreJvm import kotlin.test.BeforeTest @@ -30,6 +31,7 @@ import kotlin.test.assertTrue @IgnoreIOS @IgnoreJvm +@IgnoreJS class NukeDBTest : BaseDatabaseTest() { private lateinit var localDB: UserDatabaseBuilder diff --git a/data/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/event/EventMigration109Test.kt b/data/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/event/EventMigration109Test.kt index 2be1a7852b62..e8a7d55a370b 100644 --- a/data/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/event/EventMigration109Test.kt +++ b/data/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/event/EventMigration109Test.kt @@ -17,6 +17,7 @@ */ package com.wire.kalium.persistence.dao.event +import app.cash.sqldelight.async.coroutines.await import com.wire.kalium.persistence.BaseDatabaseTest import com.wire.kalium.persistence.dao.UserIDEntity import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -44,7 +45,7 @@ class EventMigration109Test : BaseDatabaseTest() { metadataDAO = userDatabase.metadataDAO } - private fun runMigration109Query() { + private suspend fun runMigration109Query() { // This is the exact query from 109/sqm.sq val updateQuery = """ UPDATE Metadata @@ -67,8 +68,8 @@ class EventMigration109Test : BaseDatabaseTest() { val deleteQuery = "DELETE FROM Events" // Execute the migration queries using the SqlDriver - userDatabase.sqlDriver.execute(null, updateQuery, 0) - userDatabase.sqlDriver.execute(null, deleteQuery, 0) + userDatabase.sqlDriver.execute(null, updateQuery, 0).await() + userDatabase.sqlDriver.execute(null, deleteQuery, 0).await() } @Test diff --git a/data/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/utils/IgnoreJS.kt b/data/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/utils/IgnoreJS.kt new file mode 100644 index 000000000000..92668d0ce0c9 --- /dev/null +++ b/data/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/utils/IgnoreJS.kt @@ -0,0 +1,21 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.persistence.utils + +expect annotation class IgnoreJS() diff --git a/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/WebWorkerDriverFactory.kt b/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/WebWorkerDriverFactory.kt index d6f4c6920b86..d3c340647da3 100644 --- a/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/WebWorkerDriverFactory.kt +++ b/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/WebWorkerDriverFactory.kt @@ -24,7 +24,7 @@ import org.w3c.dom.Worker internal fun createKaliumWebWorkerDriver(): SqlDriver = WebWorkerDriver(createKaliumWorker()) -@Suppress("UnsafeCastFromDynamic") +@Suppress("LongMethod", "UnsafeCastFromDynamic") private fun createKaliumWorker(): Worker { val hasBrowserWorker = js("typeof window !== 'undefined' && typeof Worker !== 'undefined'").unsafeCast() if (hasBrowserWorker) { @@ -41,9 +41,27 @@ private fun createKaliumWorker(): Worker { "const { parentPort, workerData } = require('node:worker_threads');", "const initSqlJs = require(workerData.sqlJsModulePath);", "let dbPromise = initSqlJs({ locateFile: () => workerData.sqlJsWasmPath }).then(SQL => new SQL.Database());", + "const booleanColumnPattern = /^(deleted|incomplete_metadata|defederated|archived|deletedLocally|isFavorite|isChannel|is_channel|userDeleted|userDefederated|isUserDeleted|senderIsDeleted|senderDefederated|hasIncompleteMetadata|lastMessageIsSelfMessage|expects_read_confirmation|expectsReadConfirmation|isSelfMessage|isSelfDelete|isEphemeral|isMentioningSelfUser|isQuotingSelfUser|isQuotingSelf|isQuoteVerified|isUnread|isConversationAppsEnabled|newConversationReceiptMode|conversationReceiptModeChanged|isDecryptionResolved|is_selected|cell_asset|is_valid|is_verified|is_mls_capable|is_async_notifications_capable|legal_hold_status_change_notified|legalHoldStatusChangeNotified|degraded_conversation_notified|degradedConversationNotified|mls_degraded_notified|is_guest_password_protected|should_notify|read_receipt|enabled|notified|hasRegisteredMLSClient|isOnPremises|federation|apiProxyNeedsAuthentication|isNativePushSupportedByServer|isPersistentWebSocketEnabled)$/i;", + "function normalizeResult(sql, result) {", + " if (!result || !Array.isArray(result.columns) || !Array.isArray(result.values)) return result;", + " const booleanIndexes = result.columns", + " .map((column, index) => booleanColumnPattern.test(String(column)) ? index : -1)", + " .filter(index => index >= 0);", + " if (result.columns.length === 1 && /^\\s*select\\s+exists\\s*\\(/i.test(sql)) booleanIndexes.push(0);", + " if (booleanIndexes.length === 0) return result;", + " return {", + " values: result.values.map(row => row.map((value, index) =>", + " booleanIndexes.includes(index) && (value === 0 || value === 1) ? value === 1 : value", + " ))", + " };", + "}", + "function isQuery(sql) {", + " return /^\\s*(select|with|pragma|explain)\\b/i.test(sql);", + "}", "function resultFor(db, sql, params) {", " const result = db.exec(sql, params)[0];", - " if (result) return result;", + " if (result) return normalizeResult(sql, result);", + " if (isQuery(sql)) return { values: [] };", " return { values: [[db.getRowsModified()]] };", "}", "parentPort.on('message', async (data) => {", @@ -78,6 +96,9 @@ private fun createKaliumWorker(): Worker { sqlJsWasmPath: sqlJsWasmPath } }); + if (typeof worker.setMaxListeners === 'function') { + worker.setMaxListeners(0); + } if (typeof worker.unref === 'function') { worker.unref(); } diff --git a/data/persistence/src/jsTest/kotlin/com/wire/kalium/persistence/utils/IgnoreJS.kt b/data/persistence/src/jsTest/kotlin/com/wire/kalium/persistence/utils/IgnoreJS.kt new file mode 100644 index 000000000000..6360c571928b --- /dev/null +++ b/data/persistence/src/jsTest/kotlin/com/wire/kalium/persistence/utils/IgnoreJS.kt @@ -0,0 +1,23 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.persistence.utils + +import kotlin.test.Ignore + +actual typealias IgnoreJS = Ignore diff --git a/data/persistence/src/jvmTest/kotlin/com/wire/kalium/persistence/utils/IgnoreJS.kt b/data/persistence/src/jvmTest/kotlin/com/wire/kalium/persistence/utils/IgnoreJS.kt new file mode 100644 index 000000000000..3c3760d8ace9 --- /dev/null +++ b/data/persistence/src/jvmTest/kotlin/com/wire/kalium/persistence/utils/IgnoreJS.kt @@ -0,0 +1,21 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.persistence.utils + +actual annotation class IgnoreJS From f0732c64281a5a361d28e0eb4cf930016b550191 Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Tue, 12 May 2026 11:50:06 +0200 Subject: [PATCH 6/8] test fixes --- .../CustomServerConfigRepositoryTest.kt | 14 +++++++------- .../configuration/ServerConfigRepositoryTest.kt | 16 ++++++++-------- .../auth/AddAuthenticatedUserUseCaseTest.kt | 16 ++++++++-------- .../feature/backup/CreateBackupUseCaseTest.kt | 8 ++++---- .../IsCurrentSessionNomadAccountUseCaseTest.kt | 2 +- .../kalium/logic/network/SessionManagerTest.kt | 4 ++-- .../AuthenticationScopeForConfigIdUseCaseTest.kt | 10 ++++++---- 7 files changed, 36 insertions(+), 34 deletions(-) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/configuration/CustomServerConfigRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/configuration/CustomServerConfigRepositoryTest.kt index 688845344cda..0d08d0870df5 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/configuration/CustomServerConfigRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/configuration/CustomServerConfigRepositoryTest.kt @@ -89,7 +89,7 @@ class CustomServerConfigRepositoryTest { verifySuspend(VerifyMode.exactly(1)) { arrangement.serverConfigurationDAO.setFederationToTrue(any()) } - verify(VerifyMode.exactly(1)) { + verifySuspend(VerifyMode.exactly(1)) { arrangement.serverConfigurationDAO.configById(any()) } } @@ -117,7 +117,7 @@ class CustomServerConfigRepositoryTest { verifySuspend(VerifyMode.not) { arrangement.serverConfigurationDAO.setFederationToTrue(any()) } - verify(VerifyMode.exactly(1)) { + verifySuspend(VerifyMode.exactly(1)) { arrangement.serverConfigurationDAO.configById(any()) } } @@ -156,7 +156,7 @@ class CustomServerConfigRepositoryTest { verifySuspend(VerifyMode.not) { arrangement.serverConfigurationDAO.setFederationToTrue(any()) } - verify(VerifyMode.exactly(1)) { + verifySuspend(VerifyMode.exactly(1)) { arrangement.serverConfigurationDAO.configById(any()) } } @@ -203,7 +203,7 @@ class CustomServerConfigRepositoryTest { CustomServerConfigDataSource(versionApi, serverConfigApi, developmentApiEnabled, serverConfigurationDAO, backendMetaDataUtil) suspend fun withConfigForNewRequest(serverConfigEntity: ServerConfigEntity): Arrangement { - every { + everySuspend { serverConfigurationDAO.configById(any()) } returns serverConfigEntity everySuspend { @@ -230,8 +230,8 @@ class CustomServerConfigRepositoryTest { return this } - fun withConfigById(serverConfig: ServerConfigEntity): Arrangement { - every { + suspend fun withConfigById(serverConfig: ServerConfigEntity): Arrangement { + everySuspend { serverConfigurationDAO.configById(any()) } returns serverConfig return this @@ -265,7 +265,7 @@ class CustomServerConfigRepositoryTest { ) everySuspend { serverConfigurationDAO.configByLinks(serverConfigEntity.links) } returns serverConfigEntity - every { + everySuspend { serverConfigurationDAO.configById(any()) } returns newServerConfigEntity diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/configuration/ServerConfigRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/configuration/ServerConfigRepositoryTest.kt index 3e4edc917f27..8e18eebbd98a 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/configuration/ServerConfigRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/configuration/ServerConfigRepositoryTest.kt @@ -94,7 +94,7 @@ class ServerConfigRepositoryTest { arrangement.versionApi.fetchApiVersion(any()) } - verify(VerifyMode.exactly(1)) { + verifySuspend(VerifyMode.exactly(1)) { arrangement.serverConfigDAO.configById(any()) } @@ -122,7 +122,7 @@ class ServerConfigRepositoryTest { arrangement.versionApi.fetchApiVersion(any()) } - verify(VerifyMode.not) { + verifySuspend(VerifyMode.not) { arrangement.serverConfigDAO.configById(any()) } @@ -157,7 +157,7 @@ class ServerConfigRepositoryTest { verifySuspend(VerifyMode.exactly(1)) { arrangement.serverConfigDAO.setFederationToTrue(any()) } - verify(VerifyMode.exactly(1)) { + verifySuspend(VerifyMode.exactly(1)) { arrangement.serverConfigDAO.configById(any()) } } @@ -185,7 +185,7 @@ class ServerConfigRepositoryTest { verifySuspend(VerifyMode.not) { arrangement.serverConfigDAO.setFederationToTrue(any()) } - verify(VerifyMode.exactly(1)) { + verifySuspend(VerifyMode.exactly(1)) { arrangement.serverConfigDAO.configById(any()) } } @@ -209,14 +209,14 @@ class ServerConfigRepositoryTest { suspend fun withConfigForNewRequest(serverConfigEntity: ServerConfigEntity): Arrangement { everySuspend { serverConfigDAO.configByLinks(serverConfigEntity.links) } returns null - every { + everySuspend { serverConfigDAO.configById(any()) } returns newServerConfigEntity(1) return this } - fun withConfigById(serverConfig: ServerConfigEntity?): Arrangement { - every { + suspend fun withConfigById(serverConfig: ServerConfigEntity?): Arrangement { + everySuspend { serverConfigDAO.configById(any()) } returns serverConfig return this @@ -248,7 +248,7 @@ class ServerConfigRepositoryTest { everySuspend { serverConfigDAO.configByLinks(serverConfigEntity.links) } returns serverConfigEntity - every { + everySuspend { serverConfigDAO.configById(any()) } returns ( newServerConfigEntity diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/auth/AddAuthenticatedUserUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/auth/AddAuthenticatedUserUseCaseTest.kt index 4dbc40911957..7a5d8ab839d8 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/auth/AddAuthenticatedUserUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/auth/AddAuthenticatedUserUseCaseTest.kt @@ -170,11 +170,11 @@ class AddAuthenticatedUserUseCaseTest { arrangement.sessionRepository.updateCurrentSession(any()) } - verify(VerifyMode.exactly(1)) { + verifySuspend(VerifyMode.exactly(1)) { arrangement.serverConfigurationDAO.configById(any()) } - verify(VerifyMode.exactly(1)) { + verifySuspend(VerifyMode.exactly(1)) { arrangement.sessionRepository.fullAccountInfo(any()) } verifySuspend(VerifyMode.exactly(1)) { @@ -240,10 +240,10 @@ class AddAuthenticatedUserUseCaseTest { verifySuspend(VerifyMode.not) { arrangement.sessionRepository.updateCurrentSession(any()) } - verify(VerifyMode.exactly(1)) { + verifySuspend(VerifyMode.exactly(1)) { arrangement.sessionRepository.fullAccountInfo(any()) } - verify(VerifyMode.exactly(1)) { + verifySuspend(VerifyMode.exactly(1)) { arrangement.serverConfigurationDAO.configById(any()) } } @@ -429,18 +429,18 @@ class AddAuthenticatedUserUseCaseTest { everySuspend { serverConfigurationDAO.configForUser(userId) } returns (result) } - fun withFullAccountInfoResult( + suspend fun withFullAccountInfoResult( userId: UserId, result: Either ) = apply { - every { sessionRepository.fullAccountInfo(userId) } returns (result) + everySuspend { sessionRepository.fullAccountInfo(userId) } returns (result) } - fun withConfigByIdSuccess( + suspend fun withConfigByIdSuccess( serverConfigId: String, result: ServerConfigEntity ) = apply { - every { serverConfigurationDAO.configById(serverConfigId) } returns (result) + everySuspend { serverConfigurationDAO.configById(serverConfigId) } returns (result) } suspend fun withStoreSessionResult( diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/backup/CreateBackupUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/backup/CreateBackupUseCaseTest.kt index eed04e46eaf7..68461d58653f 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/backup/CreateBackupUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/backup/CreateBackupUseCaseTest.kt @@ -203,18 +203,18 @@ class CreateBackupUseCaseTest { } returns (clientId?.let { Either.Right(it) } ?: Either.Left(StorageFailure.DataNotFound)) } - fun withExportedDB(dbName: String?, dbData: ByteArray) = apply { + suspend fun withExportedDB(dbName: String?, dbData: ByteArray) = apply { with(fakeFileSystem) { val exportedDBPath = dbName?.let { rootDBPath / it } ?: "null".toPath() sink(exportedDBPath).buffer().use { it.write(dbData) } - every { + everySuspend { databaseExporter.exportToPlainDB(any()) } returns (exportedDBPath.toString()) } } - fun withExportedDBError() = apply { - every { + suspend fun withExportedDBError() = apply { + everySuspend { databaseExporter.exportToPlainDB(any()) } returns (null) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/session/IsCurrentSessionNomadAccountUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/session/IsCurrentSessionNomadAccountUseCaseTest.kt index 6a4afe7591d8..7e2f624d2a23 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/session/IsCurrentSessionNomadAccountUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/session/IsCurrentSessionNomadAccountUseCaseTest.kt @@ -108,7 +108,7 @@ internal class IsCurrentSessionNomadAccountUseCaseTest { } suspend fun withFullAccountInfo(userId: UserId, result: Either) = apply { - every { sessionRepository.fullAccountInfo(eq(userId)) } returns result + everySuspend { sessionRepository.fullAccountInfo(eq(userId)) } returns result } fun arrange() = this to useCase diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/network/SessionManagerTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/network/SessionManagerTest.kt index ac92d774edee..bf0731e72c14 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/network/SessionManagerTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/network/SessionManagerTest.kt @@ -268,8 +268,8 @@ class SessionManagerTest { } calls { invocation -> block(invocation.args.toTypedArray()) } } - fun withFullAccountInfoReturning(block: (args: Array) -> Either) = apply { - every { + suspend fun withFullAccountInfoReturning(block: (args: Array) -> Either) = apply { + everySuspend { sessionRepository.fullAccountInfo(any()) } calls { invocation -> block(invocation.args.toTypedArray()) } } diff --git a/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/auth/autoVersioningAuth/AuthenticationScopeForConfigIdUseCaseTest.kt b/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/auth/autoVersioningAuth/AuthenticationScopeForConfigIdUseCaseTest.kt index 75fb6b8d8830..b7d937661443 100644 --- a/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/auth/autoVersioningAuth/AuthenticationScopeForConfigIdUseCaseTest.kt +++ b/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/auth/autoVersioningAuth/AuthenticationScopeForConfigIdUseCaseTest.kt @@ -26,8 +26,10 @@ import com.wire.kalium.logic.util.stubs.newServerConfigEntity import com.wire.kalium.persistence.daokaliumdb.ServerConfigurationDAO import dev.mokkery.answering.returns import dev.mokkery.every +import dev.mokkery.everySuspend import dev.mokkery.mock import dev.mokkery.verify +import dev.mokkery.verifySuspend import kotlinx.coroutines.test.runTest import org.junit.Test import sun.misc.Unsafe @@ -44,7 +46,7 @@ class AuthenticationScopeForConfigIdUseCaseTest { val authScope = allocateUninitializedInstance() val dao = mock { - every { configById(serverConfig.id) } returns entity + everySuspend { configById(serverConfig.id) } returns entity } val mapper = mock { every { fromEntity(entity) } returns serverConfig @@ -65,7 +67,7 @@ class AuthenticationScopeForConfigIdUseCaseTest { assertSame(authScope, result.authenticationScope) assertSame(serverConfig, factoryCalledWith) - verify { dao.configById(serverConfig.id) } + verifySuspend { dao.configById(serverConfig.id) } verify { mapper.fromEntity(entity) } } @@ -74,7 +76,7 @@ class AuthenticationScopeForConfigIdUseCaseTest { val configId = "non-existent-id" val dao = mock { - every { configById(configId) } returns null + everySuspend { configById(configId) } returns null } val mapper = mock() var factoryCalled = false @@ -93,7 +95,7 @@ class AuthenticationScopeForConfigIdUseCaseTest { assertIs(result.genericFailure) assertEquals(false, factoryCalled) - verify { dao.configById(configId) } + verifySuspend { dao.configById(configId) } } @Suppress("UNCHECKED_CAST") From e6a5cf9dc074d110e63926952de2f6f8dbea2918 Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Tue, 12 May 2026 12:43:40 +0200 Subject: [PATCH 7/8] fix: use platform-specific persistence validation queries --- .../persistence/db/DatabaseValidation.kt | 50 ++++++++++++++++ .../persistence/db/DatabaseValidation.kt | 50 ++++++++++++++++ .../persistence/db/UserDatabaseBuilder.kt | 39 +----------- .../persistence/db/DatabaseValidation.kt | 60 +++++++++++++++++++ .../persistence/db/DatabaseValidation.kt | 50 ++++++++++++++++ 5 files changed, 212 insertions(+), 37 deletions(-) create mode 100644 data/persistence/src/androidMain/kotlin/com/wire/kalium/persistence/db/DatabaseValidation.kt create mode 100644 data/persistence/src/appleMain/kotlin/com/wire/kalium/persistence/db/DatabaseValidation.kt create mode 100644 data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/DatabaseValidation.kt create mode 100644 data/persistence/src/jvmMain/kotlin/com/wire/kalium/persistence/db/DatabaseValidation.kt diff --git a/data/persistence/src/androidMain/kotlin/com/wire/kalium/persistence/db/DatabaseValidation.kt b/data/persistence/src/androidMain/kotlin/com/wire/kalium/persistence/db/DatabaseValidation.kt new file mode 100644 index 000000000000..28413f2d5cea --- /dev/null +++ b/data/persistence/src/androidMain/kotlin/com/wire/kalium/persistence/db/DatabaseValidation.kt @@ -0,0 +1,50 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.persistence.db + +import app.cash.sqldelight.async.coroutines.synchronous +import app.cash.sqldelight.db.QueryResult +import app.cash.sqldelight.db.SqlDriver +import app.cash.sqldelight.db.SqlSchema +import kotlin.coroutines.cancellation.CancellationException + +@Suppress("TooGenericExceptionCaught") +actual suspend fun SqlDriver.migrate(sqlSchema: SqlSchema>): Boolean { + val oldVersion = executeQuery(null, "PRAGMA user_version;", { + it.next().value + QueryResult.Value(it.getLong(0)) + }, 0).value ?: return false + + val newVersion = sqlSchema.version + return try { + if (oldVersion != newVersion) { + sqlSchema.synchronous().migrate(this, oldVersion, newVersion).value + } + true + } catch (exception: CancellationException) { + throw exception + } catch (_: Exception) { + false + } +} + +actual suspend fun SqlDriver.checkFKViolations(): Boolean = + executeQuery(null, "PRAGMA foreign_key_check;", { + QueryResult.Value(it.next().value) + }, 0).value diff --git a/data/persistence/src/appleMain/kotlin/com/wire/kalium/persistence/db/DatabaseValidation.kt b/data/persistence/src/appleMain/kotlin/com/wire/kalium/persistence/db/DatabaseValidation.kt new file mode 100644 index 000000000000..28413f2d5cea --- /dev/null +++ b/data/persistence/src/appleMain/kotlin/com/wire/kalium/persistence/db/DatabaseValidation.kt @@ -0,0 +1,50 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.persistence.db + +import app.cash.sqldelight.async.coroutines.synchronous +import app.cash.sqldelight.db.QueryResult +import app.cash.sqldelight.db.SqlDriver +import app.cash.sqldelight.db.SqlSchema +import kotlin.coroutines.cancellation.CancellationException + +@Suppress("TooGenericExceptionCaught") +actual suspend fun SqlDriver.migrate(sqlSchema: SqlSchema>): Boolean { + val oldVersion = executeQuery(null, "PRAGMA user_version;", { + it.next().value + QueryResult.Value(it.getLong(0)) + }, 0).value ?: return false + + val newVersion = sqlSchema.version + return try { + if (oldVersion != newVersion) { + sqlSchema.synchronous().migrate(this, oldVersion, newVersion).value + } + true + } catch (exception: CancellationException) { + throw exception + } catch (_: Exception) { + false + } +} + +actual suspend fun SqlDriver.checkFKViolations(): Boolean = + executeQuery(null, "PRAGMA foreign_key_check;", { + QueryResult.Value(it.next().value) + }, 0).value diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt index 0cce3b1d9d7e..ce609db561fa 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt @@ -18,9 +18,6 @@ package com.wire.kalium.persistence.db -import app.cash.sqldelight.async.coroutines.awaitMigrate -import app.cash.sqldelight.async.coroutines.awaitQuery -import app.cash.sqldelight.async.coroutines.await import app.cash.sqldelight.db.QueryResult import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.db.SqlSchema @@ -107,7 +104,6 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel -import kotlin.coroutines.cancellation.CancellationException import kotlin.jvm.JvmInline @JvmInline @@ -476,40 +472,9 @@ internal expect fun createEmptyDatabaseFile( userId: UserIDEntity, ): String? -@Suppress("TooGenericExceptionCaught") -suspend fun SqlDriver.migrate(sqlSchema: SqlSchema>): Boolean { - val oldVersion = awaitQuery(null, "PRAGMA user_version;", { - it.next().await() - it.getLong(0) - }, 0) ?: return false - - val newVersion = sqlSchema.version - return try { - if (oldVersion != newVersion) { - sqlSchema.awaitMigrate(this, oldVersion, newVersion) - } - true - } catch (e: CancellationException) { - throw e - } catch (e: Exception) { - false - } -} +expect suspend fun SqlDriver.migrate(sqlSchema: SqlSchema>): Boolean /** * @return true if the database have fk violations, false otherwise */ -suspend fun SqlDriver.checkFKViolations(): Boolean { - var result = false - awaitQuery(null, "PRAGMA foreign_key_check;", { - // foreign_key_check returns the rows with the fk violations - // if the cursor has a next, it means there are violations - // and the backup is corrupted - if (it.next().await()) { - result = true - } - Unit - }, 0, null) - - return result -} +expect suspend fun SqlDriver.checkFKViolations(): Boolean diff --git a/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/DatabaseValidation.kt b/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/DatabaseValidation.kt new file mode 100644 index 000000000000..a44acf961032 --- /dev/null +++ b/data/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/DatabaseValidation.kt @@ -0,0 +1,60 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.persistence.db + +import app.cash.sqldelight.async.coroutines.await +import app.cash.sqldelight.async.coroutines.awaitMigrate +import app.cash.sqldelight.async.coroutines.awaitQuery +import app.cash.sqldelight.db.QueryResult +import app.cash.sqldelight.db.SqlDriver +import app.cash.sqldelight.db.SqlSchema +import kotlin.coroutines.cancellation.CancellationException + +@Suppress("TooGenericExceptionCaught") +actual suspend fun SqlDriver.migrate(sqlSchema: SqlSchema>): Boolean { + val oldVersion = awaitQuery(null, "PRAGMA user_version;", { + it.next().await() + it.getLong(0) + }, 0) ?: return false + + val newVersion = sqlSchema.version + return try { + if (oldVersion != newVersion) { + sqlSchema.awaitMigrate(this, oldVersion, newVersion) + } + true + } catch (exception: CancellationException) { + throw exception + } catch (_: Exception) { + false + } +} + +actual suspend fun SqlDriver.checkFKViolations(): Boolean { + var result = false + awaitQuery(null, "PRAGMA foreign_key_check;", { + // foreign_key_check returns rows when the backup is corrupted. + if (it.next().await()) { + result = true + } + Unit + }, 0, null) + + return result +} diff --git a/data/persistence/src/jvmMain/kotlin/com/wire/kalium/persistence/db/DatabaseValidation.kt b/data/persistence/src/jvmMain/kotlin/com/wire/kalium/persistence/db/DatabaseValidation.kt new file mode 100644 index 000000000000..28413f2d5cea --- /dev/null +++ b/data/persistence/src/jvmMain/kotlin/com/wire/kalium/persistence/db/DatabaseValidation.kt @@ -0,0 +1,50 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.persistence.db + +import app.cash.sqldelight.async.coroutines.synchronous +import app.cash.sqldelight.db.QueryResult +import app.cash.sqldelight.db.SqlDriver +import app.cash.sqldelight.db.SqlSchema +import kotlin.coroutines.cancellation.CancellationException + +@Suppress("TooGenericExceptionCaught") +actual suspend fun SqlDriver.migrate(sqlSchema: SqlSchema>): Boolean { + val oldVersion = executeQuery(null, "PRAGMA user_version;", { + it.next().value + QueryResult.Value(it.getLong(0)) + }, 0).value ?: return false + + val newVersion = sqlSchema.version + return try { + if (oldVersion != newVersion) { + sqlSchema.synchronous().migrate(this, oldVersion, newVersion).value + } + true + } catch (exception: CancellationException) { + throw exception + } catch (_: Exception) { + false + } +} + +actual suspend fun SqlDriver.checkFKViolations(): Boolean = + executeQuery(null, "PRAGMA foreign_key_check;", { + QueryResult.Value(it.next().value) + }, 0).value From 943ca376f5280d27097debdf2a384143d486ae23 Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Thu, 14 May 2026 07:14:58 +0200 Subject: [PATCH 8/8] fix: align async persistence paging after rebase --- data/persistence/build.gradle.kts | 2 +- .../conversation/ConversationExtensions.kt | 4 +-- .../dao/message/AsyncQueryPagingSource.kt | 28 +++++++------------ .../dao/message/MessageExtensions.kt | 4 +-- gradle/libs.versions.toml | 1 + 5 files changed, 16 insertions(+), 23 deletions(-) diff --git a/data/persistence/build.gradle.kts b/data/persistence/build.gradle.kts index d1e3436e5631..a6bfa205fda7 100644 --- a/data/persistence/build.gradle.kts +++ b/data/persistence/build.gradle.kts @@ -100,7 +100,7 @@ kotlin { val jsMain by getting { dependencies { implementation(libs.sqldelight.jsDriver) - implementation(npm("@cashapp/sqldelight-sqljs-worker", libs.versions.sqldelight.get())) + implementation(npm("@cashapp/sqldelight-sqljs-worker", libs.versions.sqldelightSqljsWorker.get())) implementation(npm("sql.js", "1.8.0")) implementation(devNpm("webpack", "^5.1.0")) implementation(devNpm("copy-webpack-plugin", "9.1.0")) diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationExtensions.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationExtensions.kt index c1bba2a9694a..f5ce7de9e643 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationExtensions.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationExtensions.kt @@ -17,8 +17,8 @@ */ package com.wire.kalium.persistence.dao.conversation -import app.cash.paging.Pager -import app.cash.paging.PagingConfig +import androidx.paging.Pager +import androidx.paging.PagingConfig import com.wire.kalium.persistence.ConversationDetailsWithEventsQueries import com.wire.kalium.persistence.dao.conversation.ConversationExtensions.QueryConfig import com.wire.kalium.persistence.dao.message.AsyncQueryPagingSource diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/AsyncQueryPagingSource.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/AsyncQueryPagingSource.kt index 39805eaf8830..0b9c2fa9c8d5 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/AsyncQueryPagingSource.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/AsyncQueryPagingSource.kt @@ -18,16 +18,8 @@ package com.wire.kalium.persistence.dao.message -import app.cash.paging.PagingSource -import app.cash.paging.PagingSourceLoadParams -import app.cash.paging.PagingSourceLoadParamsAppend -import app.cash.paging.PagingSourceLoadParamsPrepend -import app.cash.paging.PagingSourceLoadParamsRefresh -import app.cash.paging.PagingSourceLoadResult -import app.cash.paging.PagingSourceLoadResultError -import app.cash.paging.PagingSourceLoadResultInvalid -import app.cash.paging.PagingSourceLoadResultPage -import app.cash.paging.PagingState +import androidx.paging.PagingSource +import androidx.paging.PagingState import app.cash.sqldelight.Query import app.cash.sqldelight.async.coroutines.awaitAsList import app.cash.sqldelight.async.coroutines.awaitAsOne @@ -58,19 +50,19 @@ internal class AsyncQueryPagingSource( override fun queryResultsChanged() = invalidate() @Suppress("CyclomaticComplexMethod", "TooGenericExceptionCaught") - override suspend fun load(params: PagingSourceLoadParams): PagingSourceLoadResult = + override suspend fun load(params: LoadParams): LoadResult = withContext(context) { try { val key = params.key?.toLong() ?: initialOffset val limit = when (params) { - is PagingSourceLoadParamsPrepend<*> -> minOf(key, params.loadSize.toLong()) + is LoadParams.Prepend<*> -> minOf(key, params.loadSize.toLong()) else -> params.loadSize.toLong() } val count = countQuery.awaitAsOne().toInt() val offset = when (params) { - is PagingSourceLoadParamsPrepend<*> -> maxOf(0, key - params.loadSize).toInt() - is PagingSourceLoadParamsAppend<*> -> key.toInt() - is PagingSourceLoadParamsRefresh<*> -> + is LoadParams.Prepend<*> -> maxOf(0, key - params.loadSize).toInt() + is LoadParams.Append<*> -> key.toInt() + is LoadParams.Refresh<*> -> if (key >= count - params.loadSize) maxOf(0, count - params.loadSize) else key.toInt() else -> error("Unknown PagingSourceLoadParams ${params::class}") } @@ -80,9 +72,9 @@ internal class AsyncQueryPagingSource( val nextPosition = offset + data.size if (invalid) { - PagingSourceLoadResultInvalid() + LoadResult.Invalid() } else { - PagingSourceLoadResultPage( + LoadResult.Page( data = data, prevKey = offset.takeIf { it > 0 && data.isNotEmpty() }, nextKey = nextPosition.takeIf { data.isNotEmpty() && data.size >= limit && it < count }, @@ -91,7 +83,7 @@ internal class AsyncQueryPagingSource( ) } } catch (exception: Exception) { - PagingSourceLoadResultError(exception) + LoadResult.Error(exception) } } diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageExtensions.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageExtensions.kt index 9a93d9fa9d83..5f62f45cb53e 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageExtensions.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageExtensions.kt @@ -18,8 +18,8 @@ package com.wire.kalium.persistence.dao.message -import app.cash.paging.Pager -import app.cash.paging.PagingConfig +import androidx.paging.Pager +import androidx.paging.PagingConfig import com.wire.kalium.persistence.MessageAssetViewQueries import com.wire.kalium.persistence.MessagesQueries import com.wire.kalium.persistence.dao.ConversationIDEntity diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 34f7d1554ef7..d1febd7b1e98 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,6 +32,7 @@ kover = "0.9.8" multiplatform-settings = "1.3.0" moduleGraph = "0.13.0" sqldelight = "0.0.6-2.3.2" +sqldelightSqljsWorker = "2.2.1" sqlcipher-android = "4.13.0" pbandk = "0.16.0" turbine = "1.1.0"