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:
AntsyLich
2026-09-28 21:57:53 +06:00
committed by GitHub
parent 93cc075116
commit 2d1d2e4cab
28 changed files with 706 additions and 749 deletions
@@ -34,11 +34,12 @@ class RestoreRepositoryImpl(
private val trackRepository: TrackRepository,
) : RestoreRepository {
override suspend fun getMangaUrlsBySourceId(): Map<Long, List<String>> {
override suspend fun getMangaUrlsBySourceId(): Map<Long, Set<String>> {
return database.mangaQueries
.getAllMangaSourceAndUrl()
.awaitAsList()
.groupBy({ it.source_id }, { it.remote_url })
.mapValues { it.value.toSet() }
}
override suspend fun restoreManga(entries: List<RestoredManga>, update: suspend (Manga) -> MangaUpdate) {
@@ -93,49 +94,49 @@ class RestoreRepositoryImpl(
private suspend fun updateManga(manga: Manga): Manga {
database.mangaQueries.updateFromBackup(
favoriteAt = manga.favoriteAt,
artist = manga.artist,
author = manga.author,
description = manga.description,
genre = manga.genre,
title = manga.title,
status = manga.status,
thumbnailUrl = manga.thumbnailUrl,
lastUpdate = manga.lastUpdate,
initialized = manga.initialized,
viewer = manga.viewerFlags,
chapterFlags = manga.chapterFlags,
coverLastModified = manga.coverLastModified,
mangaId = manga.id,
updateStrategy = manga.updateStrategy,
notes = manga.notes,
memo = manga.memo,
userFavoriteAt = manga.favoriteAt,
remoteArtist = manga.artist,
remoteAuthor = manga.author,
remoteDescription = manga.description,
remoteGenre = manga.genre,
remoteTitle = manga.title,
remoteStatus = manga.status,
remoteCover = manga.thumbnailUrl,
stateChapterLastUpdate = manga.lastUpdate,
stateInitialized = manga.initialized,
userReaderFlags = manga.viewerFlags,
userChapterFlags = manga.chapterFlags,
stateCoverLastModified = manga.coverLastModified,
id = manga.id,
remoteUpdateStrategy = manga.updateStrategy,
userNotes = manga.notes,
remoteMemo = manga.memo,
)
return manga
}
private suspend fun insertManga(manga: Manga): Long {
return database.mangaQueries.insertReturningId(
favoriteAt = manga.favoriteAt,
source = manga.source,
url = manga.url,
artist = manga.artist,
author = manga.author,
description = manga.description,
genre = manga.genre,
title = manga.title,
status = manga.status,
thumbnailUrl = manga.thumbnailUrl,
lastUpdate = manga.lastUpdate,
nextUpdate = 0L,
calculateInterval = 0L,
initialized = manga.initialized,
viewerFlags = manga.viewerFlags,
chapterFlags = manga.chapterFlags,
coverLastModified = manga.coverLastModified,
updateStrategy = manga.updateStrategy,
notes = manga.notes,
memo = manga.memo,
userFavoriteAt = manga.favoriteAt,
sourceId = manga.source,
remoteUrl = manga.url,
remoteArtist = manga.artist,
remoteAuthor = manga.author,
remoteDescription = manga.description,
remoteGenre = manga.genre,
remoteTitle = manga.title,
remoteStatus = manga.status,
remoteCover = manga.thumbnailUrl,
stateChapterLastUpdate = manga.lastUpdate,
stateChapterNextUpdate = 0L,
stateChapterFetchInterval = 0L,
stateInitialized = manga.initialized,
userReaderFlags = manga.viewerFlags,
userChapterFlags = manga.chapterFlags,
stateCoverLastModified = manga.coverLastModified,
remoteUpdateStrategy = manga.updateStrategy,
userNotes = manga.notes,
remoteMemo = manga.memo,
)
.awaitAsOne()
}
@@ -183,28 +184,29 @@ class RestoreRepositoryImpl(
.partition { it.id > 0 }
newChapters.forEach { chapter ->
database.chapterQueries.insert(
database.chapterQueries.insertReturningId(
mangaId = chapter.mangaId,
url = chapter.url,
name = chapter.name,
scanlator = chapter.scanlator,
read = chapter.read,
bookmark = chapter.bookmark,
lastPageRead = chapter.lastPageRead,
chapterNumber = chapter.chapterNumber,
sourceOrder = chapter.sourceOrder,
dateFetch = chapter.dateFetch,
dateUpload = chapter.dateUpload,
memo = chapter.memo,
remoteUrl = chapter.url,
remoteName = chapter.name,
remoteScanlator = chapter.scanlator,
userRead = chapter.read,
userBookmark = chapter.bookmark,
userLastPageRead = chapter.lastPageRead,
remoteChapterNumber = chapter.chapterNumber,
remoteOrder = chapter.sourceOrder,
stateDateFetch = chapter.dateFetch,
remoteDateUpload = chapter.dateUpload,
remoteMemo = chapter.memo,
)
.awaitAsOneOrNull()
}
existingChapters.forEach { chapter ->
database.chapterQueries.updateFromBackup(
read = chapter.read,
bookmark = chapter.bookmark,
lastPageRead = chapter.lastPageRead,
chapterId = chapter.id,
memo = chapter.memo,
userRead = chapter.read,
userBookmark = chapter.bookmark,
userLastPageRead = chapter.lastPageRead,
id = chapter.id,
remoteMemo = chapter.memo,
)
}
}
@@ -213,28 +215,30 @@ class RestoreRepositoryImpl(
this.copy(id = 0L, mangaId = 0L, dateFetch = 0L, dateUpload = 0L)
private suspend fun restoreHistory(manga: Manga, restoredHistory: List<RestoredHistory>) {
if (restoredHistory.isEmpty()) return
val chapterIdsByUrl = chapterRepository.getChapterByMangaId(manga.id).associate { it.url to it.id }
val dbHistoryByChapterId = database.historyQueries
.getHistoryByMangaId(manga.id)
.awaitAsList()
.associateBy { it.chapter_id }
val toUpdate = restoredHistory
.groupBy { it.chapterUrl }
.mapNotNull { (chapterUrl, copies) ->
// Chapter doesn't exist; skip
.filter { it.chapterUrl in chapterIdsByUrl }
// A backup of a library that still had duplicate chapters carries a history entry for
// each copy; they are one chapter now, read as late as any copy and for as long as all
.groupBy { chapterIdsByUrl.getValue(it.chapterUrl) }
.map { (chapterId, copies) ->
val readAt = copies.maxOf { it.readAt?.time ?: 0L }
val readDuration = copies.sumOf { it.readDuration }
val dbHistory = database.historyQueries
.getHistoryByChapterUrlAndMangaId(chapterUrl, manga.id)
.awaitAsOneOrNull()
if (dbHistory == null) {
val chapter = database.chapterQueries
.getChapterByUrlAndMangaId(chapterUrl, manga.id)
.awaitAsOneOrNull()
// Chapter doesn't exist; skip
?: return@mapNotNull null
val dbHistory = dbHistoryByChapterId[chapterId]
// New history entry
return@mapNotNull Triple(chapter.id, Date(readAt), readDuration)
}
?: return@map Triple(chapterId, Date(readAt), readDuration)
// Update history entry
// Update history entry. 0 is kept rather than written as NULL, since it marks history
// the user removed.
Triple(
dbHistory.chapter_id,
chapterId,
Date(max(readAt, dbHistory.read_at?.time ?: 0L)),
max(readDuration, dbHistory.read_duration) - dbHistory.read_duration,
)
@@ -271,15 +275,15 @@ class RestoreRepositoryImpl(
existingTracks.forEach { track ->
database.manga_trackQueries.update(
mangaId = track.mangaId,
syncId = track.trackerId,
mediaId = track.remoteId,
trackerId = track.trackerId,
remoteId = track.remoteId,
libraryId = track.libraryId,
title = track.title,
lastChapterRead = track.lastChapterRead,
totalChapter = track.totalChapters,
totalChapters = track.totalChapters,
status = track.status,
score = track.score,
trackingUrl = track.remoteUrl,
remoteUrl = track.remoteUrl,
startDate = track.startDate,
finishDate = track.finishDate,
`private` = track.private,
@@ -289,11 +293,6 @@ class RestoreRepositoryImpl(
}
private suspend fun restoreExcludedScanlators(manga: Manga, excludedScanlators: List<String>) {
if (excludedScanlators.isEmpty()) return
val existingExcludedScanlators = database.excluded_scanlatorQueries
.getExcludedScanlatorsByMangaId(manga.id)
.awaitAsList()
val toInsert = excludedScanlators.filter { it !in existingExcludedScanlators }
toInsert.forEach { database.excluded_scanlatorQueries.insert(manga.id, it) }
excludedScanlators.forEach { database.excluded_scanlatorQueries.insert(manga.id, it) }
}
}
@@ -61,14 +61,14 @@ class CategoryRepositoryImpl(
}
override suspend fun updateName(categoryId: Long, name: String) {
database.categoryQueries.updateName(name = name, categoryId = categoryId)
database.categoryQueries.updateName(name = name, id = categoryId)
}
override suspend fun updateFlags(categoryId: Long, flags: Long) {
database.categoryQueries.updateFlags(flags = flags, categoryId = categoryId)
database.categoryQueries.updateFlags(flags = flags, id = categoryId)
}
override suspend fun updateAllFlags(flags: Long?) {
override suspend fun updateAllFlags(flags: Long) {
database.categoryQueries.updateAllFlags(flags = flags)
}
@@ -77,16 +77,16 @@ class CategoryRepositoryImpl(
val current = database.categoryQueries.getUserCategoryIds().awaitAsList()
val ids = orderedIds.filter { it in current } + current.filterNot { it in orderedIds }
ids.forEachIndexed { index, categoryId ->
database.categoryQueries.updateOrder(order = -index - 2L, categoryId = categoryId)
database.categoryQueries.updateOrder(order = -index - 2L, id = categoryId)
}
ids.forEachIndexed { index, categoryId ->
database.categoryQueries.updateOrder(order = index.toLong(), categoryId = categoryId)
database.categoryQueries.updateOrder(order = index.toLong(), id = categoryId)
}
}
}
override suspend fun delete(categoryId: Long) {
database.categoryQueries.delete(categoryId = categoryId)
database.categoryQueries.delete(id = categoryId)
}
private fun mapCategory(
@@ -57,30 +57,30 @@ class ChapterRepositoryImpl(
val stored = added.filter { existing.add(it.mangaId to it.url) }.map { chapter ->
val chapterId = database.chapterQueries.insertReturningId(
mangaId = chapter.mangaId,
url = chapter.url,
name = chapter.name,
scanlator = chapter.scanlator,
read = chapter.read,
bookmark = chapter.bookmark,
lastPageRead = chapter.lastPageRead,
chapterNumber = chapter.chapterNumber,
sourceOrder = chapter.sourceOrder,
dateFetch = chapter.dateFetch,
dateUpload = chapter.dateUpload,
memo = chapter.memo,
remoteUrl = chapter.url,
remoteName = chapter.name,
remoteScanlator = chapter.scanlator,
userRead = chapter.read,
userBookmark = chapter.bookmark,
userLastPageRead = chapter.lastPageRead,
remoteChapterNumber = chapter.chapterNumber,
remoteOrder = chapter.sourceOrder,
stateDateFetch = chapter.dateFetch,
remoteDateUpload = chapter.dateUpload,
remoteMemo = chapter.memo,
)
.awaitAsOne()
chapter.copy(id = chapterId)
}
updated.forEach { chapterUpdate ->
database.chapterQueries.updateRemote(
name = chapterUpdate.name,
scanlator = chapterUpdate.scanlator,
chapterNumber = chapterUpdate.chapterNumber,
sourceOrder = chapterUpdate.sourceOrder,
dateUpload = chapterUpdate.dateUpload,
chapterId = chapterUpdate.id,
memo = chapterUpdate.memo,
remoteName = chapterUpdate.name,
remoteScanlator = chapterUpdate.scanlator,
remoteChapterNumber = chapterUpdate.chapterNumber,
remoteOrder = chapterUpdate.sourceOrder,
remoteDateUpload = chapterUpdate.dateUpload,
id = chapterUpdate.id,
remoteMemo = chapterUpdate.memo,
)
}
stored
@@ -91,11 +91,11 @@ class ChapterRepositoryImpl(
database.transaction {
chapterUpdates.forEach { chapterUpdate ->
database.chapterQueries.update(
read = chapterUpdate.read,
bookmark = chapterUpdate.bookmark,
lastPageRead = chapterUpdate.lastPageRead,
dateFetch = chapterUpdate.dateFetch,
chapterId = chapterUpdate.id,
userRead = chapterUpdate.read,
userBookmark = chapterUpdate.bookmark,
userLastPageRead = chapterUpdate.lastPageRead,
stateDateFetch = chapterUpdate.dateFetch,
id = chapterUpdate.id,
)
}
}
@@ -147,38 +147,37 @@ class ChapterRepositoryImpl(
override suspend fun getChapterByUrlAndMangaId(url: String, mangaId: Long): Chapter? {
return database.chapterQueries
.getChapterByUrlAndMangaId(url, mangaId, ::mapChapter)
.getChapterByUrlAndMangaId(mangaId = mangaId, remoteUrl = url, mapper = ::mapChapter)
.awaitAsOneOrNull()
}
@Suppress("UNUSED_PARAMETER")
private fun mapChapter(
id: Long,
mangaId: Long,
url: String,
name: String,
scanlator: String?,
read: Boolean,
bookmark: Boolean,
lastPageRead: Long,
chapterNumber: Double,
sourceOrder: Long,
dateFetch: Long,
dateUpload: Long,
memo: JsonObject,
remoteUrl: String,
remoteName: String,
remoteScanlator: String?,
remoteChapterNumber: Double,
remoteDateUpload: Long,
remoteOrder: Long,
remoteMemo: JsonObject,
userRead: Boolean,
userBookmark: Boolean,
userLastPageRead: Long,
stateDateFetch: Long,
): Chapter = Chapter(
id = id,
mangaId = mangaId,
read = read,
bookmark = bookmark,
lastPageRead = lastPageRead,
dateFetch = dateFetch,
sourceOrder = sourceOrder,
url = url,
name = name,
dateUpload = dateUpload,
chapterNumber = chapterNumber,
scanlator = scanlator,
memo = memo,
read = userRead,
bookmark = userBookmark,
lastPageRead = userLastPageRead,
dateFetch = stateDateFetch,
sourceOrder = remoteOrder,
url = remoteUrl,
name = remoteName,
dateUpload = remoteDateUpload,
chapterNumber = remoteChapterNumber,
scanlator = remoteScanlator,
memo = remoteMemo,
)
}
@@ -9,72 +9,72 @@ import tachiyomi.domain.manga.model.MangaWithChapterCount
object MangaMapper {
fun mapManga(
id: Long,
source: Long,
url: String,
artist: String?,
author: String?,
description: String?,
genre: List<String>?,
title: String,
status: Long,
thumbnailUrl: String?,
lastUpdate: Long?,
nextUpdate: Long?,
initialized: Boolean,
viewerFlags: Long,
chapterFlags: Long,
coverLastModified: Long,
updateStrategy: UpdateStrategy,
calculateInterval: Long,
notes: String,
memo: JsonObject,
favoriteAt: Long?,
sourceId: Long,
remoteUrl: String,
remoteTitle: String,
remoteAuthor: String?,
remoteArtist: String?,
remoteDescription: String?,
remoteGenre: List<String>?,
remoteStatus: Long,
remoteCover: String?,
remoteUpdateStrategy: UpdateStrategy,
remoteMemo: JsonObject,
userFavoriteAt: Long?,
userNotes: String,
userReaderFlags: Long,
userChapterFlags: Long,
stateChapterLastUpdate: Long?,
stateChapterNextUpdate: Long?,
stateChapterFetchInterval: Long,
stateCoverLastModified: Long,
stateInitialized: Boolean,
): Manga = Manga(
id = id,
source = source,
lastUpdate = lastUpdate ?: 0,
nextUpdate = nextUpdate ?: 0,
fetchInterval = calculateInterval.toInt(),
favoriteAt = favoriteAt,
viewerFlags = viewerFlags,
chapterFlags = chapterFlags,
coverLastModified = coverLastModified,
url = url,
title = title,
artist = artist,
author = author,
description = description,
genre = genre,
status = status,
thumbnailUrl = thumbnailUrl,
updateStrategy = updateStrategy,
initialized = initialized,
notes = notes,
memo = memo,
source = sourceId,
favoriteAt = userFavoriteAt,
lastUpdate = stateChapterLastUpdate ?: 0,
nextUpdate = stateChapterNextUpdate ?: 0,
fetchInterval = stateChapterFetchInterval.toInt(),
viewerFlags = userReaderFlags,
chapterFlags = userChapterFlags,
coverLastModified = stateCoverLastModified,
url = remoteUrl,
title = remoteTitle,
artist = remoteArtist,
author = remoteAuthor,
description = remoteDescription,
genre = remoteGenre,
status = remoteStatus,
thumbnailUrl = remoteCover,
updateStrategy = remoteUpdateStrategy,
initialized = stateInitialized,
notes = userNotes,
memo = remoteMemo,
)
fun mapLibraryManga(
id: Long,
source: Long,
url: String,
artist: String?,
author: String?,
description: String?,
genre: List<String>?,
title: String,
status: Long,
thumbnailUrl: String?,
lastUpdate: Long?,
nextUpdate: Long?,
initialized: Boolean,
viewerFlags: Long,
chapterFlags: Long,
coverLastModified: Long,
updateStrategy: UpdateStrategy,
calculateInterval: Long,
notes: String,
memo: JsonObject,
favoriteAt: Long?,
sourceId: Long,
remoteUrl: String,
remoteTitle: String,
remoteAuthor: String?,
remoteArtist: String?,
remoteDescription: String?,
remoteGenre: List<String>?,
remoteStatus: Long,
remoteCover: String?,
remoteUpdateStrategy: UpdateStrategy,
remoteMemo: JsonObject,
userFavoriteAt: Long,
userNotes: String,
userReaderFlags: Long,
userChapterFlags: Long,
stateChapterLastUpdate: Long?,
stateChapterNextUpdate: Long?,
stateChapterFetchInterval: Long,
stateCoverLastModified: Long,
stateInitialized: Boolean,
totalCount: Long,
readCount: Double,
latestUpload: Long,
@@ -85,26 +85,26 @@ object MangaMapper {
): LibraryManga = LibraryManga(
manga = mapManga(
id = id,
source = source,
url = url,
artist = artist,
author = author,
description = description,
genre = genre,
title = title,
status = status,
thumbnailUrl = thumbnailUrl,
lastUpdate = lastUpdate,
nextUpdate = nextUpdate,
initialized = initialized,
viewerFlags = viewerFlags,
chapterFlags = chapterFlags,
coverLastModified = coverLastModified,
updateStrategy = updateStrategy,
calculateInterval = calculateInterval,
notes = notes,
memo = memo,
favoriteAt = favoriteAt,
sourceId = sourceId,
remoteUrl = remoteUrl,
remoteTitle = remoteTitle,
remoteAuthor = remoteAuthor,
remoteArtist = remoteArtist,
remoteDescription = remoteDescription,
remoteGenre = remoteGenre,
remoteStatus = remoteStatus,
remoteCover = remoteCover,
remoteUpdateStrategy = remoteUpdateStrategy,
remoteMemo = remoteMemo,
userFavoriteAt = userFavoriteAt,
userNotes = userNotes,
userReaderFlags = userReaderFlags,
userChapterFlags = userChapterFlags,
stateChapterLastUpdate = stateChapterLastUpdate,
stateChapterNextUpdate = stateChapterNextUpdate,
stateChapterFetchInterval = stateChapterFetchInterval,
stateCoverLastModified = stateCoverLastModified,
stateInitialized = stateInitialized,
),
categories = categories.split(",").map { it.toLong() },
totalChapters = totalCount,
@@ -117,51 +117,51 @@ object MangaMapper {
fun mapMangaWithChapterCount(
id: Long,
source: Long,
url: String,
artist: String?,
author: String?,
description: String?,
genre: List<String>?,
title: String,
status: Long,
thumbnailUrl: String?,
lastUpdate: Long?,
nextUpdate: Long?,
initialized: Boolean,
viewerFlags: Long,
chapterFlags: Long,
coverLastModified: Long,
updateStrategy: UpdateStrategy,
calculateInterval: Long,
notes: String,
memo: JsonObject,
favoriteAt: Long?,
totalCount: Long,
sourceId: Long,
remoteUrl: String,
remoteTitle: String,
remoteAuthor: String?,
remoteArtist: String?,
remoteDescription: String?,
remoteGenre: List<String>?,
remoteStatus: Long,
remoteCover: String?,
remoteUpdateStrategy: UpdateStrategy,
remoteMemo: JsonObject,
userFavoriteAt: Long?,
userNotes: String,
userReaderFlags: Long,
userChapterFlags: Long,
stateChapterLastUpdate: Long?,
stateChapterNextUpdate: Long?,
stateChapterFetchInterval: Long,
stateCoverLastModified: Long,
stateInitialized: Boolean,
chapterCount: Long,
): MangaWithChapterCount = MangaWithChapterCount(
manga = mapManga(
id = id,
source = source,
url = url,
artist = artist,
author = author,
description = description,
genre = genre,
title = title,
status = status,
thumbnailUrl = thumbnailUrl,
lastUpdate = lastUpdate,
nextUpdate = nextUpdate,
initialized = initialized,
viewerFlags = viewerFlags,
chapterFlags = chapterFlags,
coverLastModified = coverLastModified,
updateStrategy = updateStrategy,
calculateInterval = calculateInterval,
notes = notes,
memo = memo,
favoriteAt = favoriteAt,
sourceId = sourceId,
remoteUrl = remoteUrl,
remoteTitle = remoteTitle,
remoteAuthor = remoteAuthor,
remoteArtist = remoteArtist,
remoteDescription = remoteDescription,
remoteGenre = remoteGenre,
remoteStatus = remoteStatus,
remoteCover = remoteCover,
remoteUpdateStrategy = remoteUpdateStrategy,
remoteMemo = remoteMemo,
userFavoriteAt = userFavoriteAt,
userNotes = userNotes,
userReaderFlags = userReaderFlags,
userChapterFlags = userChapterFlags,
stateChapterLastUpdate = stateChapterLastUpdate,
stateChapterNextUpdate = stateChapterNextUpdate,
stateChapterFetchInterval = stateChapterFetchInterval,
stateCoverLastModified = stateCoverLastModified,
stateInitialized = stateInitialized,
),
chapterCount = totalCount,
chapterCount = chapterCount,
)
}
@@ -48,13 +48,13 @@ class MangaRepositoryImpl(
override suspend fun getMangaByUrlAndSourceId(url: String, sourceId: Long): Manga? {
return database.mangaQueries
.getMangaByUrlAndSource(url, sourceId, MangaMapper::mapManga)
.getMangaByUrlAndSource(sourceId = sourceId, remoteUrl = url, mapper = MangaMapper::mapManga)
.awaitAsOneOrNull()
}
override fun getMangaByUrlAndSourceIdAsFlow(url: String, sourceId: Long): Flow<Manga?> {
return database.mangaQueries
.getMangaByUrlAndSource(url, sourceId, MangaMapper::mapManga)
.getMangaByUrlAndSource(sourceId = sourceId, remoteUrl = url, mapper = MangaMapper::mapManga)
.subscribeToOneOrNull()
}
@@ -161,7 +161,11 @@ class MangaRepositoryImpl(
excludedScanlators.minus(current).forEach { scanlator ->
database.excluded_scanlatorQueries.insert(mangaId, scanlator)
}
database.excluded_scanlatorQueries.remove(mangaId, current.minus(excludedScanlators))
val toRemove = current.minus(excludedScanlators)
// An empty delete still tells every chapter query to run again
if (toRemove.isNotEmpty()) {
database.excluded_scanlatorQueries.remove(mangaId, toRemove)
}
}
}
@@ -189,25 +193,25 @@ class MangaRepositoryImpl(
return database.transactionWithResult {
manga.map {
database.mangaQueries.insertNetworkManga(
source = it.source,
url = it.url,
artist = it.artist,
author = it.author,
description = it.description,
genre = it.genre,
title = it.title,
status = it.status,
thumbnailUrl = it.thumbnailUrl,
favoriteAt = it.favoriteAt,
lastUpdate = it.lastUpdate,
nextUpdate = it.nextUpdate,
calculateInterval = it.fetchInterval.toLong(),
initialized = it.initialized,
viewerFlags = it.viewerFlags,
chapterFlags = it.chapterFlags,
coverLastModified = it.coverLastModified,
updateStrategy = it.updateStrategy,
memo = it.memo,
sourceId = it.source,
remoteUrl = it.url,
remoteArtist = it.artist,
remoteAuthor = it.author,
remoteDescription = it.description,
remoteGenre = it.genre,
remoteTitle = it.title,
remoteStatus = it.status,
remoteCover = it.thumbnailUrl,
userFavoriteAt = it.favoriteAt,
stateChapterLastUpdate = it.lastUpdate,
stateChapterNextUpdate = it.nextUpdate,
stateChapterFetchInterval = it.fetchInterval.toLong(),
stateInitialized = it.initialized,
userReaderFlags = it.viewerFlags,
userChapterFlags = it.chapterFlags,
stateCoverLastModified = it.coverLastModified,
remoteUpdateStrategy = it.updateStrategy,
remoteMemo = it.memo,
updateTitle = it.title.isNotBlank(),
updateCover = !it.thumbnailUrl.isNullOrBlank(),
updateDetails = it.initialized,
@@ -221,18 +225,18 @@ class MangaRepositoryImpl(
override suspend fun updateRemote(update: MangaRemoteUpdate): Boolean {
return try {
database.mangaQueries.updateRemote(
artist = update.artist,
author = update.author,
description = update.description,
genre = update.genre,
title = update.title,
status = update.status,
thumbnailUrl = update.thumbnailUrl,
initialized = update.initialized,
coverLastModified = update.coverLastModified,
updateStrategy = update.updateStrategy,
memo = update.memo,
mangaId = update.id,
remoteArtist = update.artist,
remoteAuthor = update.author,
remoteDescription = update.description,
remoteGenre = update.genre,
remoteTitle = update.title,
remoteStatus = update.status,
remoteCover = update.thumbnailUrl,
stateInitialized = update.initialized,
stateCoverLastModified = update.coverLastModified,
remoteUpdateStrategy = update.updateStrategy,
remoteMemo = update.memo,
id = update.id,
)
true
} catch (e: Exception) {
@@ -246,16 +250,16 @@ class MangaRepositoryImpl(
mangaUpdates.forEach { value ->
with(value) {
database.mangaQueries.update(
favoriteAtSet = isSet(::favoriteAt),
favoriteAt = favoriteAt,
lastUpdate = lastUpdate,
nextUpdate = nextUpdate,
calculateInterval = fetchInterval?.toLong(),
viewer = viewerFlags,
chapterFlags = chapterFlags,
coverLastModified = coverLastModified,
mangaId = id,
notes = notes,
userFavoriteAtSet = isSet(::favoriteAt),
userFavoriteAt = favoriteAt,
stateChapterLastUpdate = lastUpdate,
stateChapterNextUpdate = nextUpdate,
stateChapterFetchInterval = fetchInterval?.toLong(),
userReaderFlags = viewerFlags,
userChapterFlags = chapterFlags,
stateCoverLastModified = coverLastModified,
id = id,
userNotes = notes,
)
}
}
@@ -31,12 +31,12 @@ class StubSourceRepositoryImpl(
}
override suspend fun upsertStubSource(id: Long, lang: String, name: String) {
database.sourceQueries.upsert(id = id, lang = lang, name = name)
database.sourceQueries.upsert(id = id, name = name, language = lang)
}
private fun mapStubSource(
id: Long,
lang: String,
name: String,
lang: String,
): StubSource = StubSource(id = id, lang = lang, name = name)
}
@@ -6,22 +6,22 @@ object TrackMapper {
fun mapTrack(
id: Long,
mangaId: Long,
syncId: Long,
trackerId: Long,
remoteId: Long,
remoteUrl: String,
libraryId: Long?,
title: String,
lastChapterRead: Double,
totalChapters: Long,
status: Long,
score: Double,
remoteUrl: String,
startDate: Long,
finishDate: Long,
private: Boolean,
): Track = Track(
id = id,
mangaId = mangaId,
trackerId = syncId,
trackerId = trackerId,
remoteId = remoteId,
libraryId = libraryId,
title = title,
@@ -46,7 +46,7 @@ class TrackRepositoryImpl(
override suspend fun delete(mangaId: Long, trackerId: Long) {
database.manga_trackQueries.delete(
mangaId = mangaId,
syncId = trackerId,
trackerId = trackerId,
)
}
@@ -63,7 +63,7 @@ class TrackRepositoryImpl(
tracks.forEach { mangaTrack ->
database.manga_trackQueries.upsert(
mangaId = mangaTrack.mangaId,
syncId = mangaTrack.trackerId,
trackerId = mangaTrack.trackerId,
remoteId = mangaTrack.remoteId,
libraryId = mangaTrack.libraryId,
title = mangaTrack.title,
@@ -1,5 +1,6 @@
package tachiyomi.data.updates
import app.cash.sqldelight.Query
import app.cash.sqldelight.async.coroutines.awaitAsList
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
@@ -25,14 +26,7 @@ class UpdatesRepositoryImpl(
after: Long,
limit: Long,
): List<UpdatesWithRelations> {
return database.updatesViewQueries
.getUpdatesByReadStatus(
read = read,
after = after,
limit = limit,
mapper = ::mapUpdatesWithRelations,
)
.awaitAsList()
return getUpdatesByReadStatus(read = read, after = after, limit = limit).awaitAsList()
}
override fun subscribeAll(
@@ -67,14 +61,23 @@ class UpdatesRepositoryImpl(
after: Long,
limit: Long,
): Flow<List<UpdatesWithRelations>> {
return database.updatesViewQueries
.getUpdatesByReadStatus(
read = read,
return getUpdatesByReadStatus(read = read, after = after, limit = limit).subscribeToList()
}
private fun getUpdatesByReadStatus(read: Boolean, after: Long, limit: Long): Query<UpdatesWithRelations> {
return if (read) {
database.updatesViewQueries.getReadUpdates(
after = after,
limit = limit,
mapper = ::mapUpdatesWithRelations,
)
.subscribeToList()
} else {
database.updatesViewQueries.getUnreadUpdates(
after = after,
limit = limit,
mapper = ::mapUpdatesWithRelations,
)
}
}
@Suppress("UNUSED_PARAMETER")