mirror of
https://github.com/mihonapp/mihon.git
synced 2026-10-16 13:48:09 -07:00
Rework database queries for the new schema (#4022)
* query - category Key every single-row statement on :id and list columns on one line. A lookup by primary key no longer needs LIMIT 1, and resetting every category's flags takes a value rather than a nullable nobody passed. Assisted-by: Claude:claude-opus-5 * query - chapter List columns in the table's order and name bind and mapper parameters after the columns they fill. Inserts pass their arguments by name, since reordering the columns put the upload and fetch dates side by side with the same type. With (manga_id, remote_url) unique, looking a chapter up by them no longer needs LIMIT 1. Available scanlators come back distinct instead of one row per chapter, which is all the only caller wanted. insert was insertReturningId without the RETURNING, so restore uses the latter and the former goes, along with the unused getChapterByUrl. Inserting skips a chapter that a sync running alongside stored first, rather than failing the whole batch and losing every new chapter of that update. Assisted-by: Claude:claude-opus-5 * query - excluded_scanlator An entry excludes a scanlator at most once now. Inserting one that is already excluded does nothing, so restoring a backup that lists one twice drops the repeat instead of failing the entry, and restore no longer reads the stored exclusions first to skip them itself. Saving an unchanged selection no longer runs an empty delete, which still made every chapter query run again. Assisted-by: Claude:claude-opus-5 * query - extension_store Take the upsert's new values from excluded and drop a WHERE its conflict target already implies. That also retires a `signing_key =: signingKey` that only parsed by accident. Assisted-by: Claude:claude-opus-5 * query - history Restoring an entry's history looked up the history row and, failing that, the chapter for every item in the backup, one query each. It now reads the entry's chapters and history once and matches them in memory, which leaves getHistoryByChapterUrlAndMangaId without callers. Creating a backup looked up the chapter of every history row one at a time and failed on a row whose chapter was gone. It reads the entry's chapters once and leaves such a row out. The upsert takes its new values from excluded, drops a trailing WHERE its conflict target already pins, and names its parameters after the columns they fill. Assisted-by: Claude:claude-opus-5 * query - manga List columns in the table's order and name bind and mapper parameters after the columns they fill, forwarding mapper arguments by name. With (source_id, remote_url) unique, looking an entry up by them no longer needs LIMIT 1, and fetching a network entry becomes a single upsert that refreshes an existing row only while it is outside the library, instead of an insert guarded by NOT EXISTS followed by an update of the same row. "Is anything read" checks use EXISTS rather than IN over a DISTINCT subquery, category membership drops a coalesce on a column that is never null, and the unused getAllManga goes. Applying the default chapter flags to the library is one transaction instead of one per entry, and restore checks backup entries against the stored urls as a set rather than a list. Assisted-by: Claude:claude-opus-5 * query - manga_category An entry belongs to a category at most once now. Inserting a pairing that already exists does nothing, so setting an entry's categories from a list that names one twice no longer fails, which restore could produce when two backup categories resolve to the same local one. Assisted-by: Claude:claude-opus-5 * query - manga_track List columns in the table's order and name bind and mapper parameters after the columns they fill. That retires the sync-era :syncId, :mediaId, :totalChapter and :trackingUrl. Assisted-by: Claude:claude-opus-5 * query - source List columns in the table's order and name the upsert's parameters after its columns. The upsert takes the conflicting row's values from excluded instead of rebinding them, and drops a WHERE its conflict target already implies. The call passes its two string arguments by name, since swapping them would still compile. Assisted-by: Claude:claude-opus-5 * query - historyView The latest read entry is read from the tables through the index on history's read time instead of aggregating the whole view for one row: ~50ms to under 1ms on a 114k chapter library. Assisted-by: Claude:claude-opus-5 * query - libraryView List the columns like the other queries. Assisted-by: Claude:claude-opus-5 * query - updatesView Drop a coalesce on category_id, which is never null, from the queries over updatesView, and the unused getRecentUpdates. The view definitions themselves are unchanged, so no migration is needed. The read and unread lookups become separate queries with the status written into each. SQLite only uses a partial index when the query's own condition matches it, which a bound parameter never does, so the unread lookup the widget follows walked nearly the whole fetch date index, ~60ms each time on a 114k chapter library and after every chapter or history write. With the status written in, it's served by the partial index over unread chapters, ~8ms. Assisted-by: Claude:claude-opus-5 * Query browse entries only while they are shown Each loaded entry kept a live query for as long as the screen existed, so every write to manga re-ran all of them. An entry now starts from the row the listing stored and queries only while shown. Assisted-by: Claude:claude-opus-5 * Read library selection categories from memory The change category dialog and category updates read each selected entry's categories from the loaded library instead of querying them one at a time. Assisted-by: Claude:claude-opus-5
This commit is contained in:
+5
-3
@@ -62,9 +62,11 @@ class MangaBackupCreator(
|
||||
if (options.history) {
|
||||
val historyByMangaId = getHistory.await(manga.id)
|
||||
if (historyByMangaId.isNotEmpty()) {
|
||||
val history = historyByMangaId.map { history ->
|
||||
val chapter = checkNotNull(chapterRepository.getChapterById(history.chapterId))
|
||||
BackupHistory(chapter.url, history.readAt?.time ?: 0L, history.readDuration)
|
||||
val chapterUrlsById = chapterRepository.getChapterByMangaId(manga.id).associate { it.id to it.url }
|
||||
val history = historyByMangaId.mapNotNull { history ->
|
||||
// A chapter removed since its history was read takes that history with it
|
||||
val url = chapterUrlsById[history.chapterId] ?: return@mapNotNull null
|
||||
BackupHistory(url, history.readAt?.time ?: 0L, history.readDuration)
|
||||
}
|
||||
if (history.isNotEmpty()) {
|
||||
mangaObject.history = history
|
||||
|
||||
+3
-1
@@ -32,6 +32,7 @@ import eu.kanade.tachiyomi.util.removeCovers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.WhileSubscribed
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
@@ -56,6 +57,7 @@ import tachiyomi.domain.manga.model.MangaWithChapterCount
|
||||
import tachiyomi.domain.source.interactor.GetRemoteManga
|
||||
import tachiyomi.domain.source.service.SourceManager
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import eu.kanade.tachiyomi.source.model.Filter as SourceModelFilter
|
||||
|
||||
@AssistedInject
|
||||
@@ -133,7 +135,7 @@ class BrowseSourceViewModel(
|
||||
pagingData.map { manga ->
|
||||
getManga.subscribe(manga.url, manga.source)
|
||||
.map { it ?: manga }
|
||||
.stateIn(viewModelScope)
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5.seconds), manga)
|
||||
}
|
||||
.filter { !hideInLibraryItems || !it.value.favorite }
|
||||
}
|
||||
|
||||
@@ -458,34 +458,19 @@ class LibraryViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the common categories for the given list of manga.
|
||||
*
|
||||
* @param mangas the list of manga.
|
||||
*/
|
||||
private suspend fun getCommonCategories(mangas: List<Manga>): Collection<Category> {
|
||||
if (mangas.isEmpty()) return emptyList()
|
||||
return mangas
|
||||
.map { getCategories.await(it.id).toSet() }
|
||||
.reduce { set1, set2 -> set1.intersect(set2) }
|
||||
private suspend fun getCategoryIds(manga: Manga): Set<Long> {
|
||||
return state.value.libraryData.favoritesById[manga.id]
|
||||
?.libraryManga
|
||||
?.categories
|
||||
// The library reports no category as 0
|
||||
?.filterTo(mutableSetOf()) { it != 0L }
|
||||
?: getCategories.await(manga.id).mapTo(mutableSetOf()) { it.id }
|
||||
}
|
||||
|
||||
suspend fun getNextUnreadChapter(manga: Manga): Chapter? {
|
||||
return getChaptersByMangaId.await(manga.id, applyScanlatorFilter = true).getNextUnread(manga, downloadManager)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the mix (non-common) categories for the given list of manga.
|
||||
*
|
||||
* @param mangas the list of manga.
|
||||
*/
|
||||
private suspend fun getMixCategories(mangas: List<Manga>): Collection<Category> {
|
||||
if (mangas.isEmpty()) return emptyList()
|
||||
val mangaCategories = mangas.map { getCategories.await(it.id).toSet() }
|
||||
val common = mangaCategories.reduce { set1, set2 -> set1.intersect(set2) }
|
||||
return mangaCategories.flatten().distinct().subtract(common)
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues the amount specified of unread chapters from the list of selected manga
|
||||
*/
|
||||
@@ -599,8 +584,7 @@ class LibraryViewModel(
|
||||
fun setMangaCategories(mangaList: List<Manga>, addCategories: List<Long>, removeCategories: List<Long>) {
|
||||
viewModelScope.launchNonCancellable {
|
||||
mangaList.forEach { manga ->
|
||||
val categoryIds = getCategories.await(manga.id)
|
||||
.map { it.id }
|
||||
val categoryIds = getCategoryIds(manga)
|
||||
.subtract(removeCategories.toSet())
|
||||
.plus(addCategories)
|
||||
.toList()
|
||||
@@ -729,13 +713,12 @@ class LibraryViewModel(
|
||||
// Hide the default category because it has a different behavior than the ones from db.
|
||||
val categories = state.value.displayedCategories.filter { it.id != 0L }
|
||||
|
||||
// Get indexes of the common categories to preselect.
|
||||
val common = getCommonCategories(mangaList)
|
||||
// Get indexes of the mix categories to preselect.
|
||||
val mix = getMixCategories(mangaList)
|
||||
val mangaCategoryIds = mangaList.map { getCategoryIds(it) }
|
||||
val common = mangaCategoryIds.reduceOrNull { set1, set2 -> set1 intersect set2 }.orEmpty()
|
||||
val mix = mangaCategoryIds.flatten().toSet() - common
|
||||
val preselected = categories
|
||||
.map {
|
||||
when (it) {
|
||||
when (it.id) {
|
||||
in common -> CheckboxState.State.Checked(it)
|
||||
in mix -> CheckboxState.TriState.Exclude(it)
|
||||
else -> CheckboxState.State.None(it)
|
||||
|
||||
Reference in New Issue
Block a user