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")
@@ -19,24 +19,15 @@ END;
getCategory:
SELECT id, name, `order`, flags
FROM category
WHERE id = :id
LIMIT 1;
WHERE id = :id;
getCategories:
SELECT
id,
name,
`order`,
flags
SELECT id, name, `order`, flags
FROM category
ORDER BY `order`;
getCategoriesByMangaId:
SELECT
C.id,
C.name,
C.`order`,
C.flags
SELECT C.id, C.name, C.`order`, C.flags
FROM category C
JOIN manga_category MC
ON C.id = MC.category_id
@@ -44,18 +35,19 @@ WHERE MC.manga_id = :mangaId
ORDER BY C.`order`;
insert:
-- The position is taken inside the insert, so two categories created at once cannot claim the same one
INSERT INTO category(name, `order`, flags)
SELECT :name, coalesce(max(`order`), -1) + 1, :flags
FROM category;
delete:
DELETE FROM category
WHERE id = :categoryId;
WHERE id = :id;
updateName:
UPDATE category
SET name = :name
WHERE id = :categoryId;
WHERE id = :id;
getUserCategoryIds:
SELECT id
@@ -66,13 +58,13 @@ ORDER BY `order`;
updateOrder:
UPDATE category
SET `order` = :order
WHERE id = :categoryId;
WHERE id = :id;
updateFlags:
UPDATE category
SET flags = :flags
WHERE id = :categoryId;
WHERE id = :id;
updateAllFlags:
UPDATE category SET
flags = coalesce(:flags, flags);
UPDATE category
SET flags = :flags;
@@ -25,19 +25,17 @@ CREATE INDEX chapter_unread_state_date_fetch_index ON chapter(state_date_fetch)
getChapterById:
SELECT
id, manga_id, remote_url, remote_name, remote_scanlator,
user_read, user_bookmark, user_last_page_read, remote_chapter_number, remote_order,
state_date_fetch, remote_date_upload,
remote_memo
id, manga_id, remote_url, remote_name, remote_scanlator, remote_chapter_number,
remote_date_upload, remote_order, remote_memo, user_read, user_bookmark,
user_last_page_read, state_date_fetch
FROM chapter
WHERE id = :id;
getChaptersByMangaId:
SELECT
C.id, C.manga_id, C.remote_url, C.remote_name, C.remote_scanlator,
C.user_read, C.user_bookmark, C.user_last_page_read, C.remote_chapter_number, C.remote_order,
C.state_date_fetch, C.remote_date_upload,
C.remote_memo
C.id, C.manga_id, C.remote_url, C.remote_name, C.remote_scanlator, C.remote_chapter_number,
C.remote_date_upload, C.remote_order, C.remote_memo, C.user_read, C.user_bookmark,
C.user_last_page_read, C.state_date_fetch
FROM chapter C
LEFT JOIN excluded_scanlator ES
ON C.manga_id = ES.manga_id
@@ -49,75 +47,70 @@ AND (
);
getScanlatorsByMangaId:
SELECT remote_scanlator
SELECT DISTINCT remote_scanlator
FROM chapter
WHERE manga_id = :mangaId;
getBookmarkedChaptersByMangaId:
SELECT
id, manga_id, remote_url, remote_name, remote_scanlator,
user_read, user_bookmark, user_last_page_read, remote_chapter_number, remote_order,
state_date_fetch, remote_date_upload,
remote_memo
id, manga_id, remote_url, remote_name, remote_scanlator, remote_chapter_number,
remote_date_upload, remote_order, remote_memo, user_read, user_bookmark,
user_last_page_read, state_date_fetch
FROM chapter
WHERE user_bookmark
AND manga_id = :mangaId;
getChapterByUrl:
SELECT
id, manga_id, remote_url, remote_name, remote_scanlator,
user_read, user_bookmark, user_last_page_read, remote_chapter_number, remote_order,
state_date_fetch, remote_date_upload,
remote_memo
FROM chapter
WHERE remote_url = :chapterUrl;
WHERE manga_id = :mangaId
AND user_bookmark;
getChapterByUrlAndMangaId:
SELECT
id, manga_id, remote_url, remote_name, remote_scanlator,
user_read, user_bookmark, user_last_page_read, remote_chapter_number, remote_order,
state_date_fetch, remote_date_upload,
remote_memo
id, manga_id, remote_url, remote_name, remote_scanlator, remote_chapter_number,
remote_date_upload, remote_order, remote_memo, user_read, user_bookmark,
user_last_page_read, state_date_fetch
FROM chapter
WHERE remote_url = :chapterUrl
AND manga_id = :mangaId
LIMIT 1;
WHERE manga_id = :mangaId
AND remote_url = :remoteUrl;
removeChaptersWithIds:
DELETE FROM chapter
WHERE id IN :chapterIds;
insert:
INSERT INTO chapter(manga_id, remote_url, remote_name, remote_scanlator, user_read, user_bookmark, user_last_page_read, remote_chapter_number, remote_order, state_date_fetch, remote_date_upload, remote_memo)
VALUES (:mangaId, :url, :name, :scanlator, :read, :bookmark, :lastPageRead, :chapterNumber, :sourceOrder, :dateFetch, :dateUpload, :memo);
insertReturningId:
INSERT INTO chapter(manga_id, remote_url, remote_name, remote_scanlator, user_read, user_bookmark, user_last_page_read, remote_chapter_number, remote_order, state_date_fetch, remote_date_upload, remote_memo)
VALUES (:mangaId, :url, :name, :scanlator, :read, :bookmark, :lastPageRead, :chapterNumber, :sourceOrder, :dateFetch, :dateUpload, :memo)
INSERT INTO chapter(
manga_id, remote_url, remote_name, remote_scanlator, remote_chapter_number,
remote_date_upload, remote_order, remote_memo, user_read, user_bookmark,
user_last_page_read, state_date_fetch
)
VALUES (
:mangaId, :remoteUrl, :remoteName, :remoteScanlator, :remoteChapterNumber,
:remoteDateUpload, :remoteOrder, :remoteMemo, :userRead, :userBookmark,
:userLastPageRead, :stateDateFetch
)
-- Two syncs of the same entry can race; the chapter the other one already stored is skipped rather
-- than failing the whole batch
ON CONFLICT(manga_id, remote_url) DO NOTHING
RETURNING id;
update:
UPDATE chapter
SET user_read = coalesce(:read, user_read),
user_bookmark = coalesce(:bookmark, user_bookmark),
user_last_page_read = coalesce(:lastPageRead, user_last_page_read),
state_date_fetch = coalesce(:dateFetch, state_date_fetch)
WHERE id = :chapterId;
SET user_read = coalesce(:userRead, user_read),
user_bookmark = coalesce(:userBookmark, user_bookmark),
user_last_page_read = coalesce(:userLastPageRead, user_last_page_read),
state_date_fetch = coalesce(:stateDateFetch, state_date_fetch)
WHERE id = :id;
updateFromBackup:
UPDATE chapter
SET user_read = coalesce(:read, user_read),
user_bookmark = coalesce(:bookmark, user_bookmark),
user_last_page_read = coalesce(:lastPageRead, user_last_page_read),
remote_memo = coalesce(:memo, remote_memo)
WHERE id = :chapterId;
SET user_read = coalesce(:userRead, user_read),
user_bookmark = coalesce(:userBookmark, user_bookmark),
user_last_page_read = coalesce(:userLastPageRead, user_last_page_read),
remote_memo = coalesce(:remoteMemo, remote_memo)
WHERE id = :id;
updateRemote:
UPDATE chapter
SET remote_name = :name,
remote_scanlator = coalesce(:scanlator, remote_scanlator),
remote_chapter_number = :chapterNumber,
remote_order = :sourceOrder,
remote_date_upload = coalesce(:dateUpload, remote_date_upload),
remote_memo = :memo
WHERE id = :chapterId;
SET remote_name = :remoteName,
remote_scanlator = coalesce(:remoteScanlator, remote_scanlator),
remote_chapter_number = :remoteChapterNumber,
remote_order = :remoteOrder,
remote_date_upload = coalesce(:remoteDateUpload, remote_date_upload),
remote_memo = :remoteMemo
WHERE id = :id;
@@ -9,7 +9,8 @@ CREATE TABLE excluded_scanlator(
insert:
INSERT INTO excluded_scanlator(manga_id, scanlator)
VALUES (:mangaId, :scanlator);
VALUES (:mangaId, :scanlator)
ON CONFLICT(manga_id, scanlator) DO NOTHING;
remove:
DELETE FROM excluded_scanlator
@@ -21,23 +21,26 @@ SELECT
FROM extension_store;
getCount:
SELECT COUNT(*)
SELECT count(*)
FROM extension_store;
upsert:
INSERT INTO extension_store(index_url, name, badge_label, signing_key, contact_website, contact_discord, is_legacy, extension_list_url)
VALUES (:indexUrl, :name, :badgeLabel, :signingKey, :contactWebsite, :contactDiscord, :isLegacy, :extensionListUrl)
ON CONFLICT(index_url)
DO UPDATE
SET
name = :name,
badge_label = :badgeLabel,
signing_key =: signingKey,
contact_website = :contactWebsite,
contact_discord = :contactDiscord,
is_legacy = :isLegacy,
extension_list_url = :extensionListUrl
WHERE index_url = :indexUrl;
INSERT INTO extension_store(
index_url, name, badge_label, signing_key, contact_website, contact_discord, is_legacy,
extension_list_url
)
VALUES (
:indexUrl, :name, :badgeLabel, :signingKey, :contactWebsite, :contactDiscord, :isLegacy,
:extensionListUrl
)
ON CONFLICT(index_url) DO UPDATE SET
name = excluded.name,
badge_label = excluded.badge_label,
signing_key = excluded.signing_key,
contact_website = excluded.contact_website,
contact_discord = excluded.contact_discord,
is_legacy = excluded.is_legacy,
extension_list_url = excluded.extension_list_url;
delete:
DELETE FROM extension_store
@@ -16,30 +16,14 @@ CREATE INDEX history_manga_id_index ON history(manga_id);
CREATE INDEX history_read_at_index ON history(read_at);
getHistoryByMangaId:
SELECT
H.id,
H.chapter_id,
H.read_at,
H.read_duration
FROM history H
WHERE H.manga_id = :mangaId;
getHistoryByChapterUrlAndMangaId:
SELECT
H.id,
H.chapter_id,
H.read_at,
H.read_duration
FROM history H
JOIN chapter C
ON H.chapter_id = C.id
WHERE C.remote_url = :chapterUrl AND C.manga_id = :mangaId
LIMIT 1;
SELECT id, chapter_id, read_at, read_duration
FROM history
WHERE manga_id = :mangaId;
resetHistoryById:
UPDATE history
SET read_at = 0
WHERE id = :historyId;
WHERE id = :id;
resetHistoryByMangaId:
UPDATE history
@@ -55,15 +39,12 @@ WHERE read_at = 0;
upsert:
INSERT INTO history(manga_id, chapter_id, read_at, read_duration)
SELECT C.manga_id, :chapterId, :readAt, :readDuration
FROM chapter C
WHERE C.id = :chapterId
ON CONFLICT(chapter_id)
DO UPDATE
SET
read_at = :readAt,
read_duration = read_duration + :readDuration
WHERE chapter_id = :chapterId;
SELECT manga_id, id, :readAt, :readDuration
FROM chapter
WHERE id = :chapterId
ON CONFLICT(chapter_id) DO UPDATE SET
read_at = excluded.read_at,
read_duration = read_duration + excluded.read_duration;
getReadDuration:
SELECT coalesce(sum(read_duration), 0)
+162 -166
View File
@@ -33,73 +33,68 @@ CREATE INDEX manga_user_favorite_at_index ON manga(user_favorite_at) WHERE user_
getMangaById:
SELECT
id, source_id, remote_url, remote_artist, remote_author, remote_description, remote_genre, remote_title,
remote_status, remote_cover, state_chapter_last_update, state_chapter_next_update, state_initialized,
user_reader_flags, user_chapter_flags, state_cover_last_modified, remote_update_strategy,
state_chapter_fetch_interval, user_notes, remote_memo, user_favorite_at
id, source_id, remote_url, remote_title, remote_author, remote_artist, remote_description,
remote_genre, remote_status, remote_cover, remote_update_strategy, remote_memo,
user_favorite_at, user_notes, user_reader_flags, user_chapter_flags,
state_chapter_last_update, state_chapter_next_update, state_chapter_fetch_interval,
state_cover_last_modified, state_initialized
FROM manga
WHERE id = :id;
-- TODO: this should ideally never really have more than 1 result
getMangaByUrlAndSource:
SELECT
id, source_id, remote_url, remote_artist, remote_author, remote_description, remote_genre, remote_title,
remote_status, remote_cover, state_chapter_last_update, state_chapter_next_update, state_initialized,
user_reader_flags, user_chapter_flags, state_cover_last_modified, remote_update_strategy,
state_chapter_fetch_interval, user_notes, remote_memo, user_favorite_at
id, source_id, remote_url, remote_title, remote_author, remote_artist, remote_description,
remote_genre, remote_status, remote_cover, remote_update_strategy, remote_memo,
user_favorite_at, user_notes, user_reader_flags, user_chapter_flags,
state_chapter_last_update, state_chapter_next_update, state_chapter_fetch_interval,
state_cover_last_modified, state_initialized
FROM manga
WHERE remote_url = :url
AND source_id = :source
LIMIT 1;
WHERE source_id = :sourceId
AND remote_url = :remoteUrl;
getFavorites:
SELECT
id, source_id, remote_url, remote_artist, remote_author, remote_description, remote_genre, remote_title,
remote_status, remote_cover, state_chapter_last_update, state_chapter_next_update, state_initialized,
user_reader_flags, user_chapter_flags, state_cover_last_modified, remote_update_strategy,
state_chapter_fetch_interval, user_notes, remote_memo, user_favorite_at
id, source_id, remote_url, remote_title, remote_author, remote_artist, remote_description,
remote_genre, remote_status, remote_cover, remote_update_strategy, remote_memo,
user_favorite_at, user_notes, user_reader_flags, user_chapter_flags,
state_chapter_last_update, state_chapter_next_update, state_chapter_fetch_interval,
state_cover_last_modified, state_initialized
FROM manga
WHERE user_favorite_at IS NOT NULL;
getReadMangaNotInLibrary:
SELECT
id, source_id, remote_url, remote_artist, remote_author, remote_description, remote_genre, remote_title,
remote_status, remote_cover, state_chapter_last_update, state_chapter_next_update, state_initialized,
user_reader_flags, user_chapter_flags, state_cover_last_modified, remote_update_strategy,
state_chapter_fetch_interval, user_notes, remote_memo, user_favorite_at
id, source_id, remote_url, remote_title, remote_author, remote_artist, remote_description,
remote_genre, remote_status, remote_cover, remote_update_strategy, remote_memo,
user_favorite_at, user_notes, user_reader_flags, user_chapter_flags,
state_chapter_last_update, state_chapter_next_update, state_chapter_fetch_interval,
state_cover_last_modified, state_initialized
FROM manga
WHERE user_favorite_at IS NULL AND id IN (
SELECT DISTINCT chapter.manga_id
WHERE user_favorite_at IS NULL
AND EXISTS (
SELECT 1
FROM chapter
WHERE user_read = 1 OR user_last_page_read != 0
WHERE chapter.manga_id = manga.id
AND (user_read = 1 OR user_last_page_read != 0)
);
getAllManga:
SELECT
id, source_id, remote_url, remote_artist, remote_author, remote_description, remote_genre, remote_title,
remote_status, remote_cover, state_chapter_last_update, state_chapter_next_update, state_initialized,
user_reader_flags, user_chapter_flags, state_cover_last_modified, remote_update_strategy,
state_chapter_fetch_interval, user_notes, remote_memo, user_favorite_at
FROM manga;
getAllMangaSourceAndUrl:
SELECT source_id, remote_url
FROM manga;
getSourceIdWithFavoriteCount:
SELECT
source_id,
count(*)
SELECT source_id, count(*)
FROM manga
WHERE user_favorite_at IS NOT NULL
GROUP BY source_id;
getFavoriteBySourceId:
SELECT
id, source_id, remote_url, remote_artist, remote_author, remote_description, remote_genre, remote_title,
remote_status, remote_cover, state_chapter_last_update, state_chapter_next_update, state_initialized,
user_reader_flags, user_chapter_flags, state_cover_last_modified, remote_update_strategy,
state_chapter_fetch_interval, user_notes, remote_memo, user_favorite_at
id, source_id, remote_url, remote_title, remote_author, remote_artist, remote_description,
remote_genre, remote_status, remote_cover, remote_update_strategy, remote_memo,
user_favorite_at, user_notes, user_reader_flags, user_chapter_flags,
state_chapter_last_update, state_chapter_next_update, state_chapter_fetch_interval,
state_cover_last_modified, state_initialized
FROM manga
WHERE user_favorite_at IS NOT NULL
AND source_id = :sourceId;
@@ -107,57 +102,43 @@ AND source_id = :sourceId;
getDuplicateLibraryManga:
WITH
track_dupes AS (
SELECT DISTINCT S2.manga_id
FROM manga_track S1
INNER JOIN manga_track S2
ON S1.tracker_id = S2.tracker_id
AND S1.remote_id = S2.remote_id
AND S1.remote_id != 0
AND S1.manga_id != S2.manga_id
WHERE S1.manga_id = :id
),
duplicates AS (
SELECT
M.id, M.source_id, M.remote_url, M.remote_artist, M.remote_author, M.remote_description, M.remote_genre, M.remote_title,
M.remote_status, M.remote_cover, M.state_chapter_last_update, M.state_chapter_next_update, M.state_initialized,
M.user_reader_flags, M.user_chapter_flags, M.state_cover_last_modified, M.remote_update_strategy,
M.state_chapter_fetch_interval, M.user_notes, M.remote_memo, M.user_favorite_at
FROM manga M
LEFT JOIN track_dupes D
ON D.manga_id = id
WHERE user_favorite_at IS NOT NULL
AND id != :id
AND (lower(remote_title) LIKE '%' || lower(:title) || '%' OR D.manga_id IS NOT NULL)
),
chapter_counts AS (
SELECT
M.id AS manga_id,
count(*) AS chapter_count
FROM duplicates M
JOIN chapter C
ON M.id = C.manga_id
LEFT JOIN excluded_scanlator ES
ON C.manga_id = ES.manga_id
AND C.remote_scanlator = ES.scanlator
WHERE ES.scanlator IS NULL
GROUP BY M.id
SELECT DISTINCT T2.manga_id
FROM manga_track T1
JOIN manga_track T2
ON T1.tracker_id = T2.tracker_id
AND T1.remote_id = T2.remote_id
AND T1.remote_id != 0
AND T1.manga_id != T2.manga_id
WHERE T1.manga_id = :id
)
SELECT
M.id, M.source_id, M.remote_url, M.remote_artist, M.remote_author, M.remote_description, M.remote_genre, M.remote_title,
M.remote_status, M.remote_cover, M.state_chapter_last_update, M.state_chapter_next_update, M.state_initialized,
M.user_reader_flags, M.user_chapter_flags, M.state_cover_last_modified, M.remote_update_strategy,
M.state_chapter_fetch_interval, M.user_notes, M.remote_memo, M.user_favorite_at,
coalesce(CC.chapter_count, 0) AS chapter_count
FROM duplicates M
LEFT JOIN chapter_counts CC
ON M.id = CC.manga_id;
M.id, M.source_id, M.remote_url, M.remote_title, M.remote_author, M.remote_artist,
M.remote_description, M.remote_genre, M.remote_status, M.remote_cover,
M.remote_update_strategy, M.remote_memo, M.user_favorite_at, M.user_notes,
M.user_reader_flags, M.user_chapter_flags, M.state_chapter_last_update,
M.state_chapter_next_update, M.state_chapter_fetch_interval,
M.state_cover_last_modified, M.state_initialized,
(
SELECT count(*)
FROM chapter C
LEFT JOIN excluded_scanlator ES
ON C.manga_id = ES.manga_id
AND C.remote_scanlator = ES.scanlator
WHERE C.manga_id = M.id
AND ES.scanlator IS NULL
) AS chapter_count
FROM manga M
WHERE M.user_favorite_at IS NOT NULL
AND M.id != :id
AND (lower(M.remote_title) LIKE '%' || lower(:remoteTitle) || '%' OR M.id IN (SELECT manga_id FROM track_dupes));
getUpcomingManga:
SELECT
id, source_id, remote_url, remote_artist, remote_author, remote_description, remote_genre, remote_title,
remote_status, remote_cover, state_chapter_last_update, state_chapter_next_update, state_initialized,
user_reader_flags, user_chapter_flags, state_cover_last_modified, remote_update_strategy,
state_chapter_fetch_interval, user_notes, remote_memo, user_favorite_at
id, source_id, remote_url, remote_title, remote_author, remote_artist, remote_description,
remote_genre, remote_status, remote_cover, remote_update_strategy, remote_memo,
user_favorite_at, user_notes, user_reader_flags, user_chapter_flags,
state_chapter_last_update, state_chapter_next_update, state_chapter_fetch_interval,
state_cover_last_modified, state_initialized
FROM manga
WHERE state_chapter_next_update >= :startOfDay
AND user_favorite_at IS NOT NULL
@@ -168,7 +149,7 @@ AND (
OR EXISTS (
SELECT 1 FROM manga_category
WHERE manga_category.manga_id = manga.id
AND COALESCE(manga_category.category_id, 0) IN :includedCategories
AND manga_category.category_id IN :includedCategories
)
OR (0 IN :includedCategories AND NOT EXISTS (
SELECT 1 FROM manga_category WHERE manga_category.manga_id = manga.id)
@@ -181,7 +162,7 @@ AND (
NOT EXISTS (
SELECT 1 FROM manga_category
WHERE manga_category.manga_id = manga.id
AND COALESCE(manga_category.category_id, 0) IN :excludedCategories
AND manga_category.category_id IN :excludedCategories
)
AND (
0 NOT IN :excludedCategories
@@ -193,10 +174,11 @@ ORDER BY state_chapter_next_update ASC;
resetViewerFlags:
UPDATE manga
SET user_reader_flags = 0;
SET user_reader_flags = 0
WHERE user_reader_flags != 0;
getSourceIdsWithNonLibraryManga:
SELECT source_id, COUNT(*) AS manga_count
SELECT source_id, count(*) AS manga_count
FROM manga
WHERE user_favorite_at IS NULL
GROUP BY source_id;
@@ -207,105 +189,119 @@ WHERE user_favorite_at IS NULL
AND source_id IN :sourceIds
AND (
:keepReadManga = 0
OR id NOT IN (
SELECT DISTINCT manga_id
OR NOT EXISTS (
SELECT 1
FROM chapter
WHERE user_read = 1
OR user_last_page_read != 0
WHERE chapter.manga_id = manga.id
AND (user_read = 1 OR user_last_page_read != 0)
)
);
insertReturningId:
INSERT INTO manga(source_id, remote_url, remote_artist, remote_author, remote_description, remote_genre, remote_title, remote_status, remote_cover, user_favorite_at, state_chapter_last_update, state_chapter_next_update, state_initialized, user_reader_flags, user_chapter_flags, state_cover_last_modified, remote_update_strategy, state_chapter_fetch_interval, user_notes, remote_memo)
VALUES (:source, :url, :artist, :author, :description, :genre, :title, :status, :thumbnailUrl, :favoriteAt, :lastUpdate, :nextUpdate, :initialized, :viewerFlags, :chapterFlags, :coverLastModified, :updateStrategy, :calculateInterval, :notes, :memo)
INSERT INTO manga(
source_id, remote_url, remote_title, remote_author, remote_artist, remote_description,
remote_genre, remote_status, remote_cover, remote_update_strategy, remote_memo,
user_favorite_at, user_notes, user_reader_flags, user_chapter_flags,
state_chapter_last_update, state_chapter_next_update, state_chapter_fetch_interval,
state_cover_last_modified, state_initialized
)
VALUES (
:sourceId, :remoteUrl, :remoteTitle, :remoteAuthor, :remoteArtist, :remoteDescription,
:remoteGenre, :remoteStatus, :remoteCover, :remoteUpdateStrategy, :remoteMemo,
:userFavoriteAt, :userNotes, :userReaderFlags, :userChapterFlags,
:stateChapterLastUpdate, :stateChapterNextUpdate, :stateChapterFetchInterval,
:stateCoverLastModified, :stateInitialized
)
RETURNING id;
update:
UPDATE manga SET
-- NULL is a real value here (not in the library), which coalesce cannot write
user_favorite_at = CASE WHEN :favoriteAtSet THEN :favoriteAt ELSE user_favorite_at END,
state_chapter_last_update = coalesce(:lastUpdate, state_chapter_last_update),
state_chapter_next_update = coalesce(:nextUpdate, state_chapter_next_update),
user_reader_flags = coalesce(:viewer, user_reader_flags),
user_chapter_flags = coalesce(:chapterFlags, user_chapter_flags),
state_cover_last_modified = coalesce(:coverLastModified, state_cover_last_modified),
state_chapter_fetch_interval = coalesce(:calculateInterval, state_chapter_fetch_interval),
user_notes = coalesce(:notes, user_notes)
WHERE id = :mangaId;
user_favorite_at = CASE WHEN :userFavoriteAtSet THEN :userFavoriteAt ELSE user_favorite_at END,
user_notes = coalesce(:userNotes, user_notes),
user_reader_flags = coalesce(:userReaderFlags, user_reader_flags),
user_chapter_flags = coalesce(:userChapterFlags, user_chapter_flags),
state_chapter_last_update = coalesce(:stateChapterLastUpdate, state_chapter_last_update),
state_chapter_next_update = coalesce(:stateChapterNextUpdate, state_chapter_next_update),
state_chapter_fetch_interval = coalesce(:stateChapterFetchInterval, state_chapter_fetch_interval),
state_cover_last_modified = coalesce(:stateCoverLastModified, state_cover_last_modified)
WHERE id = :id;
updateFromBackup:
UPDATE manga SET
remote_artist = coalesce(:artist, remote_artist),
remote_author = coalesce(:author, remote_author),
remote_description = coalesce(:description, remote_description),
remote_genre = coalesce(:genre, remote_genre),
remote_title = coalesce(:title, remote_title),
remote_status = coalesce(:status, remote_status),
remote_cover = coalesce(:thumbnailUrl, remote_cover),
user_favorite_at = :favoriteAt,
state_chapter_last_update = coalesce(:lastUpdate, state_chapter_last_update),
state_initialized = coalesce(:initialized, state_initialized),
user_reader_flags = coalesce(:viewer, user_reader_flags),
user_chapter_flags = coalesce(:chapterFlags, user_chapter_flags),
state_cover_last_modified = coalesce(:coverLastModified, state_cover_last_modified),
remote_update_strategy = coalesce(:updateStrategy, remote_update_strategy),
user_notes = coalesce(:notes, user_notes),
remote_memo = coalesce(:memo, remote_memo)
WHERE id = :mangaId;
remote_title = coalesce(:remoteTitle, remote_title),
remote_author = coalesce(:remoteAuthor, remote_author),
remote_artist = coalesce(:remoteArtist, remote_artist),
remote_description = coalesce(:remoteDescription, remote_description),
remote_genre = coalesce(:remoteGenre, remote_genre),
remote_status = coalesce(:remoteStatus, remote_status),
remote_cover = coalesce(:remoteCover, remote_cover),
remote_update_strategy = coalesce(:remoteUpdateStrategy, remote_update_strategy),
remote_memo = coalesce(:remoteMemo, remote_memo),
user_favorite_at = :userFavoriteAt,
user_notes = coalesce(:userNotes, user_notes),
user_reader_flags = coalesce(:userReaderFlags, user_reader_flags),
user_chapter_flags = coalesce(:userChapterFlags, user_chapter_flags),
state_chapter_last_update = coalesce(:stateChapterLastUpdate, state_chapter_last_update),
state_cover_last_modified = coalesce(:stateCoverLastModified, state_cover_last_modified),
state_initialized = coalesce(:stateInitialized, state_initialized)
WHERE id = :id;
updateRemote:
UPDATE manga SET
remote_artist = coalesce(:artist, remote_artist),
remote_author = coalesce(:author, remote_author),
remote_description = coalesce(:description, remote_description),
remote_genre = coalesce(:genre, remote_genre),
remote_title = coalesce(:title, remote_title),
remote_status = :status,
remote_cover = coalesce(:thumbnailUrl, remote_cover),
state_initialized = :initialized,
state_cover_last_modified = coalesce(:coverLastModified, state_cover_last_modified),
remote_update_strategy = :updateStrategy,
remote_memo = :memo
WHERE id = :mangaId;
remote_title = coalesce(:remoteTitle, remote_title),
remote_author = coalesce(:remoteAuthor, remote_author),
remote_artist = coalesce(:remoteArtist, remote_artist),
remote_description = coalesce(:remoteDescription, remote_description),
remote_genre = coalesce(:remoteGenre, remote_genre),
remote_status = :remoteStatus,
remote_cover = coalesce(:remoteCover, remote_cover),
remote_update_strategy = :remoteUpdateStrategy,
remote_memo = :remoteMemo,
state_cover_last_modified = coalesce(:stateCoverLastModified, state_cover_last_modified),
state_initialized = :stateInitialized
WHERE id = :id;
insertNetworkManga {
-- Insert the manga if it doesn't exist already
-- A new entry is inserted as the source reported it. An existing one takes the fresh details
-- only while it is outside the library, so a library entry keeps what the user already has.
INSERT INTO manga(
source_id, remote_url, remote_artist, remote_author, remote_description, remote_genre, remote_title, remote_status, remote_cover, user_favorite_at,
state_chapter_last_update, state_chapter_next_update, state_initialized, user_reader_flags, user_chapter_flags, state_cover_last_modified,
remote_update_strategy, state_chapter_fetch_interval, user_notes, remote_memo
source_id, remote_url, remote_title, remote_author, remote_artist, remote_description,
remote_genre, remote_status, remote_cover, remote_update_strategy, remote_memo,
user_favorite_at, user_notes, user_reader_flags, user_chapter_flags,
state_chapter_last_update, state_chapter_next_update, state_chapter_fetch_interval,
state_cover_last_modified, state_initialized
)
SELECT
:source, :url, :artist, :author, :description, :genre, :title, :status, :thumbnailUrl, :favoriteAt,
:lastUpdate, :nextUpdate, :initialized, :viewerFlags, :chapterFlags, :coverLastModified,
:updateStrategy, :calculateInterval, '', :memo
WHERE NOT EXISTS(SELECT 0 FROM manga WHERE source_id = :source AND remote_url = :url);
VALUES (
:sourceId, :remoteUrl, :remoteTitle, :remoteAuthor, :remoteArtist, :remoteDescription,
:remoteGenre, :remoteStatus, :remoteCover, :remoteUpdateStrategy, :remoteMemo,
:userFavoriteAt, '', :userReaderFlags, :userChapterFlags,
:stateChapterLastUpdate, :stateChapterNextUpdate, :stateChapterFetchInterval,
:stateCoverLastModified, :stateInitialized
)
ON CONFLICT(source_id, remote_url) DO UPDATE SET
remote_title = CASE WHEN :updateTitle THEN excluded.remote_title ELSE remote_title END,
remote_cover = CASE WHEN :updateCover THEN excluded.remote_cover ELSE remote_cover END,
remote_author = CASE WHEN :updateDetails THEN excluded.remote_author ELSE remote_author END,
remote_artist = CASE WHEN :updateDetails THEN excluded.remote_artist ELSE remote_artist END,
remote_description = CASE WHEN :updateDetails THEN excluded.remote_description ELSE remote_description END,
remote_genre = CASE WHEN :updateDetails THEN excluded.remote_genre ELSE remote_genre END,
remote_status = CASE WHEN :updateDetails THEN excluded.remote_status ELSE remote_status END,
remote_update_strategy = CASE
WHEN :updateDetails THEN excluded.remote_update_strategy
ELSE remote_update_strategy
END,
remote_memo = excluded.remote_memo,
state_initialized = :updateDetails
WHERE user_favorite_at IS NULL;
-- Update the relevant details if applicable and not favorite
UPDATE manga
SET
remote_title = CASE WHEN :updateTitle THEN :title ELSE remote_title END,
remote_cover = CASE WHEN :updateCover THEN :thumbnailUrl ELSE remote_cover END,
remote_author = CASE WHEN :updateDetails THEN :author ELSE remote_author END,
remote_artist = CASE WHEN :updateDetails THEN :artist ELSE remote_artist END,
remote_description = CASE WHEN :updateDetails THEN :description ELSE remote_description END,
remote_genre = CASE WHEN :updateDetails THEN :genre ELSE remote_genre END,
remote_status = CASE WHEN :updateDetails THEN :status ELSE remote_status END,
remote_update_strategy = CASE WHEN :updateDetails THEN :updateStrategy ELSE remote_update_strategy END,
state_initialized = :updateDetails,
remote_memo = :memo
WHERE source_id = :source
AND remote_url = :url
AND user_favorite_at IS NULL;
-- Finally return the manga
SELECT
id, source_id, remote_url, remote_artist, remote_author, remote_description, remote_genre, remote_title,
remote_status, remote_cover, state_chapter_last_update, state_chapter_next_update, state_initialized,
user_reader_flags, user_chapter_flags, state_cover_last_modified, remote_update_strategy,
state_chapter_fetch_interval, user_notes, remote_memo, user_favorite_at
id, source_id, remote_url, remote_title, remote_author, remote_artist, remote_description,
remote_genre, remote_status, remote_cover, remote_update_strategy, remote_memo,
user_favorite_at, user_notes, user_reader_flags, user_chapter_flags,
state_chapter_last_update, state_chapter_next_update, state_chapter_fetch_interval,
state_cover_last_modified, state_initialized
FROM manga
WHERE source_id = :source
AND remote_url = :url
LIMIT 1;
WHERE source_id = :sourceId
AND remote_url = :remoteUrl;
}
@@ -12,7 +12,8 @@ CREATE INDEX manga_category_category_id_index ON manga_category(category_id);
insert:
INSERT INTO manga_category(manga_id, category_id)
VALUES (:mangaId, :categoryId);
VALUES (:mangaId, :categoryId)
ON CONFLICT(manga_id, category_id) DO NOTHING;
deleteMangaCategoryByMangaId:
DELETE FROM manga_category
@@ -24,63 +24,64 @@ CREATE INDEX manga_track_tracker_id_remote_id_index ON manga_track(tracker_id, r
delete:
DELETE FROM manga_track
WHERE manga_id = :mangaId AND tracker_id = :syncId;
WHERE manga_id = :mangaId
AND tracker_id = :trackerId;
getTracks:
SELECT
id, manga_id, tracker_id, remote_id, library_id,
title, last_chapter_read, total_chapters, status, score,
remote_url, start_date, finish_date, private
id, manga_id, tracker_id, remote_id, remote_url, library_id, title, last_chapter_read,
total_chapters, status, score, start_date, finish_date, private
FROM manga_track;
getTrackById:
SELECT
id, manga_id, tracker_id, remote_id, library_id,
title, last_chapter_read, total_chapters, status, score,
remote_url, start_date, finish_date, private
id, manga_id, tracker_id, remote_id, remote_url, library_id, title, last_chapter_read,
total_chapters, status, score, start_date, finish_date, private
FROM manga_track
WHERE id = :id;
getTracksByMangaId:
SELECT
id, manga_id, tracker_id, remote_id, library_id,
title, last_chapter_read, total_chapters, status, score,
remote_url, start_date, finish_date, private
id, manga_id, tracker_id, remote_id, remote_url, library_id, title, last_chapter_read,
total_chapters, status, score, start_date, finish_date, private
FROM manga_track
WHERE manga_id = :mangaId;
upsert:
INSERT INTO manga_track(manga_id,tracker_id,remote_id,library_id,title,last_chapter_read,total_chapters,status,score,remote_url,start_date,finish_date,private)
VALUES (:mangaId,:syncId,:remoteId,:libraryId,:title,:lastChapterRead,:totalChapters,:status,:score,:remoteUrl,:startDate,:finishDate,:private)
ON CONFLICT(manga_id, tracker_id)
DO UPDATE
SET
remote_id = :remoteId,
library_id = :libraryId,
title = :title,
last_chapter_read = :lastChapterRead,
total_chapters = :totalChapters,
status = :status,
score = :score,
remote_url = :remoteUrl,
start_date = :startDate,
finish_date = :finishDate,
private = :private
WHERE manga_id = :mangaId AND tracker_id = :syncId;
INSERT INTO manga_track(
manga_id, tracker_id, remote_id, remote_url, library_id, title, last_chapter_read,
total_chapters, status, score, start_date, finish_date, private
)
VALUES (
:mangaId, :trackerId, :remoteId, :remoteUrl, :libraryId, :title, :lastChapterRead,
:totalChapters, :status, :score, :startDate, :finishDate, :private
)
ON CONFLICT(manga_id, tracker_id) DO UPDATE SET
remote_id = excluded.remote_id,
remote_url = excluded.remote_url,
library_id = excluded.library_id,
title = excluded.title,
last_chapter_read = excluded.last_chapter_read,
total_chapters = excluded.total_chapters,
status = excluded.status,
score = excluded.score,
start_date = excluded.start_date,
finish_date = excluded.finish_date,
private = excluded.private;
update:
UPDATE manga_track
SET
manga_id = coalesce(:mangaId, manga_id),
tracker_id = coalesce(:syncId, tracker_id),
remote_id = coalesce(:mediaId, remote_id),
tracker_id = coalesce(:trackerId, tracker_id),
remote_id = coalesce(:remoteId, remote_id),
remote_url = coalesce(:remoteUrl, remote_url),
library_id = coalesce(:libraryId, library_id),
title = coalesce(:title, title),
last_chapter_read = coalesce(:lastChapterRead, last_chapter_read),
total_chapters = coalesce(:totalChapter, total_chapters),
total_chapters = coalesce(:totalChapters, total_chapters),
status = coalesce(:status, status),
score = coalesce(:score, score),
remote_url = coalesce(:trackingUrl, remote_url),
start_date = coalesce(:startDate, start_date),
finish_date = coalesce(:finishDate, finish_date),
private = coalesce(:private, private)
@@ -5,20 +5,17 @@ CREATE TABLE source(
);
findAll:
SELECT id, language, name
SELECT id, name, language
FROM source;
findOne:
SELECT id, language, name
SELECT id, name, language
FROM source
WHERE id = :id;
upsert:
INSERT INTO source(id, language, name)
VALUES (:id, :lang, :name)
ON CONFLICT(id)
DO UPDATE
SET
language = :lang,
name = :name
WHERE id = :id;
INSERT INTO source(id, name, language)
VALUES (:id, :name, :language)
ON CONFLICT(id) DO UPDATE SET
name = excluded.name,
language = excluded.language;
@@ -46,18 +46,22 @@ ORDER BY readAt DESC;
getLatestHistory:
SELECT
id,
mangaId,
chapterId,
title,
thumbnailUrl,
sourceId,
favorite,
coverLastModified,
chapterNumber,
readAt,
readDuration
FROM historyView
WHERE historyView.readAt > 0
ORDER BY readAt DESC
history.id AS id,
manga.id AS mangaId,
chapter.id AS chapterId,
manga.remote_title AS title,
manga.remote_cover AS thumbnailUrl,
manga.source_id AS sourceId,
manga.user_favorite_at IS NOT NULL AS favorite,
manga.state_cover_last_modified AS coverLastModified,
chapter.remote_chapter_number AS chapterNumber,
history.read_at AS readAt,
history.read_duration AS readDuration
FROM history
JOIN manga
ON manga.id = history.manga_id
JOIN chapter
ON chapter.id = history.chapter_id
WHERE history.read_at > 0
ORDER BY history.read_at DESC
LIMIT 1;
@@ -40,11 +40,10 @@ WHERE M.user_favorite_at IS NOT NULL;
library:
SELECT
id, source_id, remote_url, remote_artist, remote_author,
remote_description, remote_genre, remote_title, remote_status, remote_cover,
state_chapter_last_update, state_chapter_next_update, state_initialized, user_reader_flags,
user_chapter_flags, state_cover_last_modified, remote_update_strategy, state_chapter_fetch_interval,
user_notes, remote_memo,
user_favorite_at, totalCount, readCount, latestUpload, chapterFetchedAt,
lastRead, bookmarkCount, categories
id, source_id, remote_url, remote_title, remote_author, remote_artist, remote_description,
remote_genre, remote_status, remote_cover, remote_update_strategy, remote_memo,
user_favorite_at, user_notes, user_reader_flags, user_chapter_flags,
state_chapter_last_update, state_chapter_next_update, state_chapter_fetch_interval,
state_cover_last_modified, state_initialized, totalCount, readCount, latestUpload,
chapterFetchedAt, lastRead, bookmarkCount, categories
FROM libraryView;
@@ -25,23 +25,11 @@ WHERE user_favorite_at IS NOT NULL
AND state_date_fetch > user_favorite_at
ORDER BY state_date_fetch DESC;
getRecentUpdates:
SELECT
mangaId, mangaTitle, chapterId, chapterName,
scanlator, chapterUrl, read, bookmark,
lastPageRead, sourceId, favorite, thumbnailUrl,
coverLastModified, dateUpload, dateFetch, excludedScanlator
FROM updatesView
WHERE dateUpload > :after
ORDER BY dateFetch DESC
LIMIT :limit;
getRecentUpdatesWithFilters:
SELECT
mangaId, mangaTitle, chapterId, chapterName,
scanlator, chapterUrl, read, bookmark,
lastPageRead, sourceId, favorite, thumbnailUrl,
coverLastModified, dateUpload, dateFetch, excludedScanlator
mangaId, mangaTitle, chapterId, chapterName, scanlator, chapterUrl, read, bookmark,
lastPageRead, sourceId, favorite, thumbnailUrl, coverLastModified, dateUpload, dateFetch,
excludedScanlator
FROM updatesView
WHERE dateUpload > :after
AND (:read IS NULL OR read = :read)
@@ -52,19 +40,17 @@ AND (
OR (:started = 0 AND lastPageRead = 0 AND read = 0)
)
AND (:bookmarked IS NULL OR bookmark = :bookmarked)
AND (
(excludedScanlator IS NULL OR :hideExcludedScanlators = 0)
)
AND (excludedScanlator IS NULL OR :hideExcludedScanlators = 0)
AND (
-- includedEmpty being true expresses "don't care" state and bypasses the membership filter
:includedEmpty
OR EXISTS (
SELECT 1 FROM manga_category
WHERE manga_category.manga_id = mangaId
AND COALESCE(manga_category.category_id, 0) IN :includedCategories
AND manga_category.category_id IN :includedCategories
)
OR (0 IN :includedCategories AND NOT EXISTS (
SELECT 1 FROM manga_category mc WHERE mc.manga_id = mangaId)
SELECT 1 FROM manga_category WHERE manga_category.manga_id = mangaId)
)
)
AND (
@@ -74,7 +60,7 @@ AND (
NOT EXISTS (
SELECT 1 FROM manga_category
WHERE manga_category.manga_id = mangaId
AND COALESCE(manga_category.category_id, 0) IN :excludedCategories
AND manga_category.category_id IN :excludedCategories
)
AND (
0 NOT IN :excludedCategories
@@ -85,14 +71,24 @@ AND (
ORDER BY dateFetch DESC
LIMIT :limit;
getUpdatesByReadStatus:
getReadUpdates:
SELECT
mangaId, mangaTitle, chapterId, chapterName,
scanlator, chapterUrl, read, bookmark,
lastPageRead, sourceId, favorite, thumbnailUrl,
coverLastModified, dateUpload, dateFetch, excludedScanlator
mangaId, mangaTitle, chapterId, chapterName, scanlator, chapterUrl, read, bookmark,
lastPageRead, sourceId, favorite, thumbnailUrl, coverLastModified, dateUpload, dateFetch,
excludedScanlator
FROM updatesView
WHERE read = :read
WHERE read = 1
AND dateUpload > :after
ORDER BY dateFetch DESC
LIMIT :limit;
getUnreadUpdates:
SELECT
mangaId, mangaTitle, chapterId, chapterName, scanlator, chapterUrl, read, bookmark,
lastPageRead, sourceId, favorite, thumbnailUrl, coverLastModified, dateUpload, dateFetch,
excludedScanlator
FROM updatesView
WHERE read = 0
AND dateUpload > :after
ORDER BY dateFetch DESC
LIMIT :limit;