From ca41e02fe1e6cf00e196734412de5dceffe97056 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Fri, 29 Nov 2024 11:28:38 +0700 Subject: [PATCH] refactor(db): Migrate some more category queries to SQLDelight --- .../data/backup/restore/BackupRestorer.kt | 1 - .../restorers/CategoriesBackupRestorer.kt | 15 +++-- .../data/database/models/Category.kt | 3 + .../ui/category/CategoryPresenter.kt | 50 ++++++++++----- .../main/java/yokai/core/di/DomainModule.kt | 6 ++ .../data/category/CategoryRepositoryImpl.kt | 64 +++++++++++++++++++ .../domain/category/CategoryRepository.kt | 6 ++ .../category/interactor/DeleteCategories.kt | 12 ++++ .../category/interactor/InsertCategories.kt | 11 ++++ .../category/interactor/UpdateCategories.kt | 12 ++++ .../sqldelight/tachiyomi/data/categories.sq | 19 ++++++ .../domain/category/models/CategoryUpdate.kt | 9 +++ 12 files changed, 187 insertions(+), 21 deletions(-) create mode 100644 app/src/main/java/yokai/domain/category/interactor/DeleteCategories.kt create mode 100644 app/src/main/java/yokai/domain/category/interactor/InsertCategories.kt create mode 100644 app/src/main/java/yokai/domain/category/interactor/UpdateCategories.kt create mode 100644 domain/src/commonMain/kotlin/yokai/domain/category/models/CategoryUpdate.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt index a72743999b..1dc0a634dc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt @@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.data.backup.restore import android.content.Context import android.net.Uri import eu.kanade.tachiyomi.data.backup.BackupNotifier -import eu.kanade.tachiyomi.data.backup.models.BackupSource import eu.kanade.tachiyomi.data.backup.restore.restorers.CategoriesBackupRestorer import eu.kanade.tachiyomi.data.backup.restore.restorers.MangaBackupRestorer import eu.kanade.tachiyomi.data.backup.restore.restorers.PreferenceBackupRestorer diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/CategoriesBackupRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/CategoriesBackupRestorer.kt index 0412046a7c..6582aa746e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/CategoriesBackupRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/CategoriesBackupRestorer.kt @@ -1,20 +1,20 @@ package eu.kanade.tachiyomi.data.backup.restore.restorers import eu.kanade.tachiyomi.data.backup.models.BackupCategory -import eu.kanade.tachiyomi.data.database.DatabaseHelper import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import yokai.data.DatabaseHandler import yokai.domain.category.interactor.GetCategories class CategoriesBackupRestorer( - private val db: DatabaseHelper = Injekt.get(), private val getCategories: GetCategories = Injekt.get(), + private val handler: DatabaseHandler = Injekt.get(), ) { suspend fun restoreCategories(backupCategories: List, onComplete: () -> Unit) { // Get categories from file and from db // Do it outside of transaction because StorIO might hang because we're using SQLDelight val dbCategories = getCategories.await() - db.inTransaction { + handler.await(true) { // Iterate over them backupCategories.map { it.getCategoryImpl() }.forEach { category -> // Used to know if the category is already in the db @@ -33,8 +33,13 @@ class CategoriesBackupRestorer( if (!found) { // Let the db assign the id category.id = null - val result = db.insertCategory(category).executeAsBlocking() - category.id = result.insertedId()?.toInt() + categoriesQueries.insert( + name = category.name, + mangaOrder = category.mangaOrderToString(), + sort = category.order.toLong(), + flags = category.flags.toLong(), + ) + category.id = categoriesQueries.selectLastInsertedRowId().executeAsOneOrNull()?.toInt() } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Category.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Category.kt index 0e5eb31de8..a0022e5729 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Category.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Category.kt @@ -53,6 +53,9 @@ interface Category : Serializable { mangaSort = (LibrarySort.valueOf(sort) ?: LibrarySort.Title).categoryValue } + fun mangaOrderToString(): String = + if (mangaSort != null) mangaSort.toString() else mangaOrder.joinToString("/") + companion object { var lastCategoriesAddedTo = emptySet() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryPresenter.kt index 4f13946af6..3ff00eb7ca 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryPresenter.kt @@ -1,19 +1,19 @@ package eu.kanade.tachiyomi.ui.category -import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.ui.library.LibrarySort -import eu.kanade.tachiyomi.util.system.executeOnIO import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy +import yokai.domain.category.interactor.DeleteCategories import yokai.domain.category.interactor.GetCategories +import yokai.domain.category.interactor.InsertCategories +import yokai.domain.category.interactor.UpdateCategories +import yokai.domain.category.models.CategoryUpdate import yokai.i18n.MR import yokai.util.lang.getString @@ -22,9 +22,11 @@ import yokai.util.lang.getString */ class CategoryPresenter( private val controller: CategoryController, - private val db: DatabaseHelper = Injekt.get(), ) { + private val deleteCategories: DeleteCategories by injectLazy() private val getCategories: GetCategories by injectLazy() + private val insertCategories: InsertCategories by injectLazy() + private val updateCategories: UpdateCategories by injectLazy() private var scope = CoroutineScope(Job() + Dispatchers.Default) @@ -79,8 +81,8 @@ class CategoryPresenter( // Insert into database. cat.mangaSort = LibrarySort.Title.categoryValue - db.insertCategory(cat).executeAsBlocking() // FIXME: Don't do blocking + runBlocking { insertCategories.awaitOne(cat) } val cats = runBlocking { getCategories.await() } val newCat = cats.find { it.name == name } ?: return false categories.add(1, newCat) @@ -94,10 +96,12 @@ class CategoryPresenter( * @param category The category to delete. */ fun deleteCategory(category: Category?) { - val safeCategory = category ?: return - db.deleteCategory(safeCategory).executeAsBlocking() - categories.remove(safeCategory) - controller.setCategories(categories.map(::CategoryItem)) + val safeCategory = category?.id ?: return + scope.launch { + deleteCategories.awaitOne(safeCategory.toLong()) + categories.remove(category) + controller.setCategories(categories.map(::CategoryItem)) + } } /** @@ -107,10 +111,19 @@ class CategoryPresenter( */ fun reorderCategories(categories: List) { scope.launch { - categories.forEachIndexed { i, category -> - if (category.order != CREATE_CATEGORY_ORDER) category.order = i - 1 - } - db.insertCategories(categories.filter { it.order != CREATE_CATEGORY_ORDER }).executeOnIO() + val updates: MutableList = mutableListOf() + categories + .filter { it.order != CREATE_CATEGORY_ORDER } + .forEachIndexed { i, category -> + category.order = i - 1 + updates.add( + CategoryUpdate( + id = category.id!!.toLong(), + order = category.order.toLong(), + ) + ) + } + updateCategories.await(updates) this@CategoryPresenter.categories = categories.sortedBy { it.order }.toMutableList() withContext(Dispatchers.Main) { controller.setCategories(this@CategoryPresenter.categories.map(::CategoryItem)) @@ -135,7 +148,14 @@ class CategoryPresenter( } category.name = name - db.insertCategory(category).executeAsBlocking() + runBlocking { + updateCategories.awaitOne( + CategoryUpdate( + id = category.id!!.toLong(), + name = category.name, + ) + ) + } categories.find { it.id == category.id }?.name = name controller.setCategories(categories.map(::CategoryItem)) return true diff --git a/app/src/main/java/yokai/core/di/DomainModule.kt b/app/src/main/java/yokai/core/di/DomainModule.kt index a6ce9c2a8e..09bec29885 100644 --- a/app/src/main/java/yokai/core/di/DomainModule.kt +++ b/app/src/main/java/yokai/core/di/DomainModule.kt @@ -9,7 +9,10 @@ import yokai.data.library.custom.CustomMangaRepositoryImpl import yokai.data.manga.MangaRepositoryImpl import yokai.data.track.TrackRepositoryImpl import yokai.domain.category.CategoryRepository +import yokai.domain.category.interactor.DeleteCategories import yokai.domain.category.interactor.GetCategories +import yokai.domain.category.interactor.InsertCategories +import yokai.domain.category.interactor.UpdateCategories import yokai.domain.chapter.ChapterRepository import yokai.domain.chapter.interactor.DeleteChapter import yokai.domain.chapter.interactor.GetAvailableScanlators @@ -45,7 +48,10 @@ fun domainModule() = module { factory { TrustExtension(get(), get()) } single { CategoryRepositoryImpl(get()) } + factory { DeleteCategories(get()) } factory { GetCategories(get()) } + factory { InsertCategories(get()) } + factory { UpdateCategories(get()) } single { ExtensionRepoRepositoryImpl(get()) } factory { CreateExtensionRepo(get()) } diff --git a/app/src/main/java/yokai/data/category/CategoryRepositoryImpl.kt b/app/src/main/java/yokai/data/category/CategoryRepositoryImpl.kt index f910a258c9..5363a77a3b 100644 --- a/app/src/main/java/yokai/data/category/CategoryRepositoryImpl.kt +++ b/app/src/main/java/yokai/data/category/CategoryRepositoryImpl.kt @@ -1,9 +1,13 @@ package yokai.data.category +import co.touchlab.kermit.Logger import eu.kanade.tachiyomi.data.database.models.Category +import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_MANGA_ORDER import kotlinx.coroutines.flow.Flow import yokai.data.DatabaseHandler +import yokai.data.updateStrategyAdapter import yokai.domain.category.CategoryRepository +import yokai.domain.category.models.CategoryUpdate class CategoryRepositoryImpl(private val handler: DatabaseHandler) : CategoryRepository { override suspend fun getAll(): List = @@ -14,4 +18,64 @@ class CategoryRepositoryImpl(private val handler: DatabaseHandler) : CategoryRep override fun getAllAsFlow(): Flow> = handler.subscribeToList { categoriesQueries.findAll(Category::mapper) } + + override suspend fun insert(category: Category): Long? = + handler.awaitOneOrNullExecutable { + categoriesQueries.insert( + name = category.name, + mangaOrder = category.mangaOrderToString(), + sort = category.order.toLong(), + flags = category.flags.toLong(), + ) + categoriesQueries.selectLastInsertedRowId() + } + + override suspend fun insertBulk(categories: List) = + handler.await(true) { + categories.forEach { category -> + categoriesQueries.insert( + name = category.name, + mangaOrder = category.mangaOrderToString(), + sort = category.order.toLong(), + flags = category.flags.toLong(), + ) + } + } + + override suspend fun update(update: CategoryUpdate): Boolean { + return try { + partialUpdate(update) + true + } catch (e: Exception) { + Logger.e { "Failed to update category with id '${update.id}'" } + false + } + } + + override suspend fun updateAll(updates: List): Boolean { + return try { + partialUpdate(*updates.toTypedArray()) + true + } catch (e: Exception) { + Logger.e(e) { "Failed to bulk update categories" } + false + } + } + + private suspend fun partialUpdate(vararg updates: CategoryUpdate) { + handler.await(inTransaction = true) { + updates.forEach { update -> + categoriesQueries.update( + id = update.id, + name = update.name, + mangaOrder = update.mangaOrder, + sort = update.order, + flags = update.flags, + ) + } + } + } + + override suspend fun delete(id: Long) = + handler.await { categoriesQueries.delete(id) } } diff --git a/app/src/main/java/yokai/domain/category/CategoryRepository.kt b/app/src/main/java/yokai/domain/category/CategoryRepository.kt index 11a9ace577..02f0aed75c 100644 --- a/app/src/main/java/yokai/domain/category/CategoryRepository.kt +++ b/app/src/main/java/yokai/domain/category/CategoryRepository.kt @@ -2,9 +2,15 @@ package yokai.domain.category import eu.kanade.tachiyomi.data.database.models.Category import kotlinx.coroutines.flow.Flow +import yokai.domain.category.models.CategoryUpdate interface CategoryRepository { suspend fun getAll(): List suspend fun getAllByMangaId(mangaId: Long): List fun getAllAsFlow(): Flow> + suspend fun insert(category: Category): Long? + suspend fun insertBulk(categories: List) + suspend fun update(update: CategoryUpdate): Boolean + suspend fun updateAll(updates: List): Boolean + suspend fun delete(id: Long) } diff --git a/app/src/main/java/yokai/domain/category/interactor/DeleteCategories.kt b/app/src/main/java/yokai/domain/category/interactor/DeleteCategories.kt new file mode 100644 index 0000000000..708ab0420f --- /dev/null +++ b/app/src/main/java/yokai/domain/category/interactor/DeleteCategories.kt @@ -0,0 +1,12 @@ +package yokai.domain.category.interactor + +import eu.kanade.tachiyomi.data.database.models.Category +import yokai.domain.category.CategoryRepository +import yokai.domain.category.models.CategoryUpdate + +class DeleteCategories( + private val categoryRepository: CategoryRepository, +) { +// suspend fun await(updates: List) = + suspend fun awaitOne(id: Long) = categoryRepository.delete(id) +} diff --git a/app/src/main/java/yokai/domain/category/interactor/InsertCategories.kt b/app/src/main/java/yokai/domain/category/interactor/InsertCategories.kt new file mode 100644 index 0000000000..11ca296bd3 --- /dev/null +++ b/app/src/main/java/yokai/domain/category/interactor/InsertCategories.kt @@ -0,0 +1,11 @@ +package yokai.domain.category.interactor + +import eu.kanade.tachiyomi.data.database.models.Category +import yokai.domain.category.CategoryRepository + +class InsertCategories( + private val categoryRepository: CategoryRepository, +) { + suspend fun await(categories: List) = categoryRepository.insertBulk(categories) + suspend fun awaitOne(category: Category) = categoryRepository.insert(category) +} diff --git a/app/src/main/java/yokai/domain/category/interactor/UpdateCategories.kt b/app/src/main/java/yokai/domain/category/interactor/UpdateCategories.kt new file mode 100644 index 0000000000..9178928049 --- /dev/null +++ b/app/src/main/java/yokai/domain/category/interactor/UpdateCategories.kt @@ -0,0 +1,12 @@ +package yokai.domain.category.interactor + +import eu.kanade.tachiyomi.data.database.models.Category +import yokai.domain.category.CategoryRepository +import yokai.domain.category.models.CategoryUpdate + +class UpdateCategories( + private val categoryRepository: CategoryRepository, +) { + suspend fun await(updates: List) = categoryRepository.updateAll(updates) + suspend fun awaitOne(update: CategoryUpdate) = categoryRepository.update(update) +} diff --git a/data/src/commonMain/sqldelight/tachiyomi/data/categories.sq b/data/src/commonMain/sqldelight/tachiyomi/data/categories.sq index d8a03606b7..d53bdcdb40 100644 --- a/data/src/commonMain/sqldelight/tachiyomi/data/categories.sq +++ b/data/src/commonMain/sqldelight/tachiyomi/data/categories.sq @@ -15,3 +15,22 @@ findAllByMangaId: SELECT categories.* FROM categories JOIN mangas_categories ON categories._id = mangas_categories.category_id WHERE mangas_categories.manga_id = :mangaId; + +insert: +INSERT INTO categories (name, sort, flags, manga_order) +VALUES (:name, :sort, :flags, :mangaOrder); + +selectLastInsertedRowId: +SELECT last_insert_rowid(); + +update: +UPDATE categories SET + name = coalesce(:name, name), + sort = coalesce(:sort, sort), + flags = coalesce(:flags, flags), + manga_order = coalesce(:mangaOrder, manga_order) +WHERE _id = :id; + +delete: +DELETE FROM categories +WHERE _id = :id; diff --git a/domain/src/commonMain/kotlin/yokai/domain/category/models/CategoryUpdate.kt b/domain/src/commonMain/kotlin/yokai/domain/category/models/CategoryUpdate.kt new file mode 100644 index 0000000000..2395ab5eed --- /dev/null +++ b/domain/src/commonMain/kotlin/yokai/domain/category/models/CategoryUpdate.kt @@ -0,0 +1,9 @@ +package yokai.domain.category.models + +data class CategoryUpdate( + val id: Long, + val name: String? = null, + val mangaOrder: String? = null, + val order: Long? = null, + val flags: Long? = null, +)