mirror of
https://github.com/mihonapp/mihon.git
synced 2026-10-16 13:48:09 -07:00
Tweak database table and view schema (#3805)
* Select explicit columns and name same-typed arguments SQLDelight expands SELECT * using the table's column order and binds mappers to it by position, so reordering columns would silently swap mapper arguments. Queries list their columns instead. Calls whose arguments share a type pass them by name, so swapping two still fails to compile. The generated code is unchanged apart from the SQL text. * table - manga Rebuild `mangas` as `manga` with a UNIQUE(source_id, remote_url) constraint, `remote_`/`user_`/`state_` column prefixes and `_id` -> `id`. `remote_` is what the source reported, `user_` is what the user set, `state_` is local bookkeeping that is neither; `source_id` keeps its name because it identifies the source rather than carrying its data. Entries sharing a source and url are merged rather than dropped. The copies are ranked with the ones in the library first, since that is the entry the user has, then by age. The first one stays, takes each detail it is missing from the first copy that has it, and absorbs the children of the rest. The duplicate chapters that repointing creates are merged too, read state and history included. The child tables keep pointing at the table this replaces until their own step rebuilds them. Foreign keys are off for the whole migration, so nothing reads those clauses in between, and copying every child table here just to rewrite one clause would double the work the migration does. For the same reason the children resolve the surviving entry while they are copied rather than being rewritten here first. Merging entries is also what creates duplicate chapters and duplicate tracks, but each of those is resolved in the step that rebuilds its own table, where the constraint that makes the collision matter is introduced. Queries are only retargeted at the new names; using the new structure is left to later changes. Assisted-by: Claude:claude-opus-5 * table - chapter Rebuild `chapters` as `chapter` with a UNIQUE(manga_id, remote_url) constraint, `remote_`/`user_`/`state_` column prefixes and `_id` -> `id`. The entries that merged in the previous step brought their chapters with them, so the chapters move onto the surviving entry here and the duplicates that creates are merged: the copy with reading history is kept, the oldest otherwise, and the read state and history of the others are folded into it. It happens here rather than with the merge because this is where the constraint that rejects the duplicates is introduced. `chapters_manga_id_index`, `chapters_unread_by_manga_index` and `idx_chapters_url` go away: the new unique constraint indexes `manga_id`, and the only url-only lookup has no callers. The unread index served the updates view's read filter, but that view sorts by fetch date and stops at a limit, which an index on `state_date_fetch` serves better with or without the read filter, so that index takes its place. A second, partial one covers only the unread chapters' fetch dates: most chapters in a library are read, so finding the unread ones through the full index means walking nearly all of it. Queries are only retargeted at the new names; using the new structure is left to later changes. Assisted-by: Claude:claude-opus-5 * table - category Rebuild `categories` as `category` with `_id` -> `id` and `sort` -> `order`, the name it already had everywhere above the database. `order` gains a UNIQUE constraint, since two categories sharing a position was never meaningful. `sort` was never unique, and reordering wrote colliding values often enough that it cannot simply be copied under the constraint. The migration renumbers the rows from 0 in the order they were displayed, ties broken by id, and keeps the system category at -1. `mangas_categories` is rebuilt in the migration only to point its foreign key at the new table; it gets its real rebuild in its own step. Queries are only retargeted at the new names; using the new structure is left to later changes. Assisted-by: Claude:claude-opus-5 * table - manga_category Rebuild `mangas_categories` as `manga_category`. The surrogate `_id` went unused, so the pair becomes the identity with a UNIQUE(manga_id, category_id) constraint; duplicate memberships were never meaningful. `idx_mangas_categories_manga_id` goes away because the unique constraint already indexes `manga_id` as its leading column. Entries merged in the manga step bring their memberships along. When the merged entry is in the library, only the copies that were in it decide its categories, since a copy outside the library can still hold the categories it had before it was removed. Queries are only retargeted at the new names; using the new structure is left to later changes. Assisted-by: Claude:claude-opus-5 * table - history Rebuild `history` with `_id` -> `id`, `last_read` -> `read_at` and `time_read` -> `read_duration`; the old names read as booleans rather than the timestamp and duration they hold. `history_history_chapter_id_index` goes away because `chapter_id` is already UNIQUE and therefore indexed. A row also names the entry it belongs to now, not just the chapter. Reaching the entry meant joining through the chapter every time, and the entry is the thing history is read by. It is filled from the chapter it hangs off, and keyed to both: the entry first, then the chapter. A row whose chapter is already gone cannot name an entry and is dropped, which is what the foreign key would do to it once it is enforced again. The table is the only one here that keeps its name, so the original is renamed aside and the new one built under the real name. Building it beside the original and renaming it into place makes SQLite rewrite the stored definition, which would leave this table spelled differently from the same table on a fresh install. Queries are only retargeted at the new names; using the new structure is left to later changes. Assisted-by: Claude:claude-opus-5 * table - excluded_scanlator Rebuild `excluded_scanlators` as `excluded_scanlator`, singular like the other tables. The pair becomes the identity with a UNIQUE(manga_id, scanlator) constraint; excluding the same scanlator twice for one entry was never meaningful. `excluded_scanlators_manga_id_index` goes away because the unique constraint already indexes `manga_id` as its leading column. Queries are only retargeted at the new names; using the new structure is left to later changes. Assisted-by: Claude:claude-opus-5 * table - manga_track Rebuild `manga_sync` as `manga_track`: the table holds tracker entries, and nothing about it is a sync. `_id` -> `id` and `sync_id` -> `tracker_id`, which is what it identifies. `idx_manga_sync_manga_id` goes away because UNIQUE(manga_id, tracker_id) already indexes `manga_id` as its leading column. The constraint drops ON CONFLICT REPLACE and aborts like every other one. Replacing deleted the row and inserted a new one under a new id, which lost anything keyed to the old id, such as a queued tracking update, and it hid any other write that collided. The insert now says what a conflict means: an existing track for the entry and tracker is updated in place. When entries merge, two rows for the same tracker can end up pointing at the survivor, so the row to keep is chosen first: one on a copy in the library, then the one furthest read, then the one on the copy ranked first, then the oldest. A track is taken whole rather than merged field by field, because two rows on the same tracker can point at different remote entries. Queries are only retargeted at the new names; using the new structure is left to later changes. Assisted-by: Claude:claude-opus-5 * table - source Rebuild `sources` as `source` with `_id` -> `id` and `lang` -> `language`, spelled out like every other column here. Queries are only retargeted at the new names; using the new structure is left to later changes. Assisted-by: Claude:claude-opus-5 * view - updates The view is the Kotlin-facing shape of a row, so its columns are named the way the callers read them; `last_page_read` and `datefetch` were the only two that weren't. The migration builds every view once at its end, so this updates that definition rather than dropping and recreating it again. Queries are only retargeted at the new names; using the new structure is left to later changes. Assisted-by: Claude:claude-opus-5 * view - library Bound the chapter and category aggregates to the library. Both subqueries group over every chapter and every category membership in the database, and only the outer query drops the entries that are not in the library, so an entry that was merely browsed still pays for its chapters every time the library is read. The entries the view keeps are the only ones whose aggregates it uses, so the same filter belongs inside them, where `manga_user_favorite_at_index` can serve it. The view returns the same rows either way. On a 560k chapter database holding 400 entries among 4400, reading the library goes from ~156ms to ~23ms. Assisted-by: Claude:claude-opus-5 * view - history The view is the Kotlin-facing shape of a row, so its columns are named the way the callers read them: `source` becomes `sourceId` and `state_cover_last_modified` loses the prefix it only carried because the column it reads has one. The migration builds every view once at its end, so this updates that definition rather than dropping and recreating it again. Queries are only retargeted at the new names; using the new structure is left to later changes. Assisted-by: Claude:claude-opus-5
This commit is contained in:
@@ -36,15 +36,15 @@ object DatabaseBindings {
|
||||
return Database(
|
||||
driver = driver,
|
||||
historyAdapter = History.Adapter(
|
||||
last_readAdapter = DateColumnAdapter,
|
||||
read_atAdapter = DateColumnAdapter,
|
||||
),
|
||||
mangasAdapter = Mangas.Adapter(
|
||||
genreAdapter = StringListColumnAdapter,
|
||||
update_strategyAdapter = UpdateStrategyColumnAdapter,
|
||||
memoAdapter = MemoColumnAdapter,
|
||||
mangaAdapter = Manga.Adapter(
|
||||
remote_genreAdapter = StringListColumnAdapter,
|
||||
remote_update_strategyAdapter = UpdateStrategyColumnAdapter,
|
||||
remote_memoAdapter = MemoColumnAdapter,
|
||||
),
|
||||
chaptersAdapter = Chapters.Adapter(
|
||||
memoAdapter = MemoColumnAdapter,
|
||||
chapterAdapter = Chapter.Adapter(
|
||||
remote_memoAdapter = MemoColumnAdapter,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -35,10 +35,10 @@ class RestoreRepositoryImpl(
|
||||
) : RestoreRepository {
|
||||
|
||||
override suspend fun getMangaUrlsBySourceId(): Map<Long, List<String>> {
|
||||
return database.mangasQueries
|
||||
return database.mangaQueries
|
||||
.getAllMangaSourceAndUrl()
|
||||
.awaitAsList()
|
||||
.groupBy({ it.source }, { it.url })
|
||||
.groupBy({ it.source_id }, { it.remote_url })
|
||||
}
|
||||
|
||||
override suspend fun restoreManga(entries: List<RestoredManga>, update: suspend (Manga) -> MangaUpdate) {
|
||||
@@ -92,7 +92,7 @@ class RestoreRepositoryImpl(
|
||||
}
|
||||
|
||||
private suspend fun updateManga(manga: Manga): Manga {
|
||||
database.mangasQueries.updateFromBackup(
|
||||
database.mangaQueries.updateFromBackup(
|
||||
favoriteAt = manga.favoriteAt,
|
||||
artist = manga.artist,
|
||||
author = manga.author,
|
||||
@@ -115,7 +115,7 @@ class RestoreRepositoryImpl(
|
||||
}
|
||||
|
||||
private suspend fun insertManga(manga: Manga): Long {
|
||||
return database.mangasQueries.insertReturningId(
|
||||
return database.mangaQueries.insertReturningId(
|
||||
favoriteAt = manga.favoriteAt,
|
||||
source = manga.source,
|
||||
url = manga.url,
|
||||
@@ -183,23 +183,23 @@ class RestoreRepositoryImpl(
|
||||
.partition { it.id > 0 }
|
||||
|
||||
newChapters.forEach { chapter ->
|
||||
database.chaptersQueries.insert(
|
||||
chapter.mangaId,
|
||||
chapter.url,
|
||||
chapter.name,
|
||||
chapter.scanlator,
|
||||
chapter.read,
|
||||
chapter.bookmark,
|
||||
chapter.lastPageRead,
|
||||
chapter.chapterNumber,
|
||||
chapter.sourceOrder,
|
||||
chapter.dateFetch,
|
||||
chapter.dateUpload,
|
||||
chapter.memo,
|
||||
database.chapterQueries.insert(
|
||||
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,
|
||||
)
|
||||
}
|
||||
existingChapters.forEach { chapter ->
|
||||
database.chaptersQueries.updateFromBackup(
|
||||
database.chapterQueries.updateFromBackup(
|
||||
read = chapter.read,
|
||||
bookmark = chapter.bookmark,
|
||||
lastPageRead = chapter.lastPageRead,
|
||||
@@ -223,25 +223,25 @@ class RestoreRepositoryImpl(
|
||||
.awaitAsOneOrNull()
|
||||
|
||||
if (dbHistory == null) {
|
||||
val chapter = database.chaptersQueries
|
||||
val chapter = database.chapterQueries
|
||||
.getChapterByUrlAndMangaId(chapterUrl, manga.id)
|
||||
.awaitAsOneOrNull()
|
||||
// Chapter doesn't exist; skip
|
||||
?: return@mapNotNull null
|
||||
// New history entry
|
||||
return@mapNotNull Triple(chapter._id, Date(readAt), readDuration)
|
||||
return@mapNotNull Triple(chapter.id, Date(readAt), readDuration)
|
||||
}
|
||||
|
||||
// Update history entry
|
||||
Triple(
|
||||
dbHistory.chapter_id,
|
||||
Date(max(readAt, dbHistory.last_read?.time ?: 0L)),
|
||||
max(readDuration, dbHistory.time_read) - dbHistory.time_read,
|
||||
Date(max(readAt, dbHistory.read_at?.time ?: 0L)),
|
||||
max(readDuration, dbHistory.read_duration) - dbHistory.read_duration,
|
||||
)
|
||||
}
|
||||
|
||||
toUpdate.forEach { (chapterId, readAt, readDuration) ->
|
||||
database.historyQueries.upsert(chapterId, readAt, readDuration)
|
||||
database.historyQueries.upsert(chapterId = chapterId, readAt = readAt, readDuration = readDuration)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,31 +269,31 @@ class RestoreRepositoryImpl(
|
||||
}
|
||||
|
||||
existingTracks.forEach { track ->
|
||||
database.manga_syncQueries.update(
|
||||
track.mangaId,
|
||||
track.trackerId,
|
||||
track.remoteId,
|
||||
track.libraryId,
|
||||
track.title,
|
||||
track.lastChapterRead,
|
||||
track.totalChapters,
|
||||
track.status,
|
||||
track.score,
|
||||
track.remoteUrl,
|
||||
track.startDate,
|
||||
track.finishDate,
|
||||
track.private,
|
||||
track.id,
|
||||
database.manga_trackQueries.update(
|
||||
mangaId = track.mangaId,
|
||||
syncId = track.trackerId,
|
||||
mediaId = track.remoteId,
|
||||
libraryId = track.libraryId,
|
||||
title = track.title,
|
||||
lastChapterRead = track.lastChapterRead,
|
||||
totalChapter = track.totalChapters,
|
||||
status = track.status,
|
||||
score = track.score,
|
||||
trackingUrl = track.remoteUrl,
|
||||
startDate = track.startDate,
|
||||
finishDate = track.finishDate,
|
||||
`private` = track.private,
|
||||
id = track.id,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun restoreExcludedScanlators(manga: Manga, excludedScanlators: List<String>) {
|
||||
if (excludedScanlators.isEmpty()) return
|
||||
val existingExcludedScanlators = database.excluded_scanlatorsQueries
|
||||
val existingExcludedScanlators = database.excluded_scanlatorQueries
|
||||
.getExcludedScanlatorsByMangaId(manga.id)
|
||||
.awaitAsList()
|
||||
val toInsert = excludedScanlators.filter { it !in existingExcludedScanlators }
|
||||
toInsert.forEach { database.excluded_scanlatorsQueries.insert(manga.id, it) }
|
||||
toInsert.forEach { database.excluded_scanlatorQueries.insert(manga.id, it) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,37 +21,37 @@ class CategoryRepositoryImpl(
|
||||
) : CategoryRepository {
|
||||
|
||||
override suspend fun get(id: Long): Category? {
|
||||
return database.categoriesQueries
|
||||
return database.categoryQueries
|
||||
.getCategory(id, ::mapCategory)
|
||||
.awaitAsOneOrNull()
|
||||
}
|
||||
|
||||
override suspend fun getAll(): List<Category> {
|
||||
return database.categoriesQueries
|
||||
return database.categoryQueries
|
||||
.getCategories(::mapCategory)
|
||||
.awaitAsList()
|
||||
}
|
||||
|
||||
override fun getAllAsFlow(): Flow<List<Category>> {
|
||||
return database.categoriesQueries
|
||||
return database.categoryQueries
|
||||
.getCategories(::mapCategory)
|
||||
.subscribeToList()
|
||||
}
|
||||
|
||||
override suspend fun getCategoriesByMangaId(mangaId: Long): List<Category> {
|
||||
return database.categoriesQueries
|
||||
return database.categoryQueries
|
||||
.getCategoriesByMangaId(mangaId, ::mapCategory)
|
||||
.awaitAsList()
|
||||
}
|
||||
|
||||
override fun getCategoriesByMangaIdAsFlow(mangaId: Long): Flow<List<Category>> {
|
||||
return database.categoriesQueries
|
||||
return database.categoryQueries
|
||||
.getCategoriesByMangaId(mangaId, ::mapCategory)
|
||||
.subscribeToList()
|
||||
}
|
||||
|
||||
override suspend fun insert(category: NewCategory) {
|
||||
database.categoriesQueries.insert(name = category.name, flags = category.flags)
|
||||
database.categoryQueries.insert(name = category.name, flags = category.flags)
|
||||
}
|
||||
|
||||
override suspend fun insertAll(categories: List<NewCategory>) {
|
||||
@@ -61,32 +61,32 @@ class CategoryRepositoryImpl(
|
||||
}
|
||||
|
||||
override suspend fun updateName(categoryId: Long, name: String) {
|
||||
database.categoriesQueries.updateName(name = name, categoryId = categoryId)
|
||||
database.categoryQueries.updateName(name = name, categoryId = categoryId)
|
||||
}
|
||||
|
||||
override suspend fun updateFlags(categoryId: Long, flags: Long) {
|
||||
database.categoriesQueries.updateFlags(flags = flags, categoryId = categoryId)
|
||||
database.categoryQueries.updateFlags(flags = flags, categoryId = categoryId)
|
||||
}
|
||||
|
||||
override suspend fun updateAllFlags(flags: Long?) {
|
||||
database.categoriesQueries.updateAllFlags(flags = flags)
|
||||
database.categoryQueries.updateAllFlags(flags = flags)
|
||||
}
|
||||
|
||||
override suspend fun updateAllOrders(orderedIds: List<Long>) {
|
||||
database.transaction {
|
||||
val current = database.categoriesQueries.getUserCategoryIds().awaitAsList()
|
||||
val current = database.categoryQueries.getUserCategoryIds().awaitAsList()
|
||||
val ids = orderedIds.filter { it in current } + current.filterNot { it in orderedIds }
|
||||
ids.forEachIndexed { index, categoryId ->
|
||||
database.categoriesQueries.updateOrder(order = -index - 2L, categoryId = categoryId)
|
||||
database.categoryQueries.updateOrder(order = -index - 2L, categoryId = categoryId)
|
||||
}
|
||||
ids.forEachIndexed { index, categoryId ->
|
||||
database.categoriesQueries.updateOrder(order = index.toLong(), categoryId = categoryId)
|
||||
database.categoryQueries.updateOrder(order = index.toLong(), categoryId = categoryId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun delete(categoryId: Long) {
|
||||
database.categoriesQueries.delete(categoryId = categoryId)
|
||||
database.categoryQueries.delete(categoryId = categoryId)
|
||||
}
|
||||
|
||||
private fun mapCategory(
|
||||
|
||||
@@ -39,37 +39,41 @@ class ChapterRepositoryImpl(
|
||||
): List<Chapter> {
|
||||
return database.transactionWithResult {
|
||||
if (removedIds.isNotEmpty()) {
|
||||
database.chaptersQueries.removeChaptersWithIds(removedIds)
|
||||
database.chapterQueries.removeChaptersWithIds(removedIds)
|
||||
}
|
||||
val existing = added.map { it.mangaId }
|
||||
.distinct()
|
||||
.flatMap { mangaId ->
|
||||
database.chaptersQueries
|
||||
.getChaptersByMangaId(mangaId, applyScanlatorFilter = false.toLong(), ::mapChapter)
|
||||
database.chapterQueries
|
||||
.getChaptersByMangaId(
|
||||
mangaId = mangaId,
|
||||
applyScanlatorFilter = false.toLong(),
|
||||
mapper = ::mapChapter,
|
||||
)
|
||||
.awaitAsList()
|
||||
.map { mangaId to it.url }
|
||||
}
|
||||
.toMutableSet()
|
||||
val stored = added.filter { existing.add(it.mangaId to it.url) }.map { chapter ->
|
||||
val chapterId = database.chaptersQueries.insertReturningId(
|
||||
chapter.mangaId,
|
||||
chapter.url,
|
||||
chapter.name,
|
||||
chapter.scanlator,
|
||||
chapter.read,
|
||||
chapter.bookmark,
|
||||
chapter.lastPageRead,
|
||||
chapter.chapterNumber,
|
||||
chapter.sourceOrder,
|
||||
chapter.dateFetch,
|
||||
chapter.dateUpload,
|
||||
chapter.memo,
|
||||
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,
|
||||
)
|
||||
.awaitAsOne()
|
||||
chapter.copy(id = chapterId)
|
||||
}
|
||||
updated.forEach { chapterUpdate ->
|
||||
database.chaptersQueries.updateRemote(
|
||||
database.chapterQueries.updateRemote(
|
||||
name = chapterUpdate.name,
|
||||
scanlator = chapterUpdate.scanlator,
|
||||
chapterNumber = chapterUpdate.chapterNumber,
|
||||
@@ -86,7 +90,7 @@ class ChapterRepositoryImpl(
|
||||
private suspend fun partialUpdate(vararg chapterUpdates: ChapterUpdate) {
|
||||
database.transaction {
|
||||
chapterUpdates.forEach { chapterUpdate ->
|
||||
database.chaptersQueries.update(
|
||||
database.chapterQueries.update(
|
||||
read = chapterUpdate.read,
|
||||
bookmark = chapterUpdate.bookmark,
|
||||
lastPageRead = chapterUpdate.lastPageRead,
|
||||
@@ -98,43 +102,51 @@ class ChapterRepositoryImpl(
|
||||
}
|
||||
|
||||
override suspend fun getChapterByMangaId(mangaId: Long, applyScanlatorFilter: Boolean): List<Chapter> {
|
||||
return database.chaptersQueries
|
||||
.getChaptersByMangaId(mangaId, applyScanlatorFilter.toLong(), ::mapChapter)
|
||||
return database.chapterQueries
|
||||
.getChaptersByMangaId(
|
||||
mangaId = mangaId,
|
||||
applyScanlatorFilter = applyScanlatorFilter.toLong(),
|
||||
mapper = ::mapChapter,
|
||||
)
|
||||
.awaitAsList()
|
||||
}
|
||||
|
||||
override suspend fun getScanlatorsByMangaId(mangaId: Long): List<String> {
|
||||
return database.chaptersQueries
|
||||
return database.chapterQueries
|
||||
.getScanlatorsByMangaId(mangaId) { it.orEmpty() }
|
||||
.awaitAsList()
|
||||
}
|
||||
|
||||
override fun getScanlatorsByMangaIdAsFlow(mangaId: Long): Flow<List<String>> {
|
||||
return database.chaptersQueries
|
||||
return database.chapterQueries
|
||||
.getScanlatorsByMangaId(mangaId) { it.orEmpty() }
|
||||
.subscribeToList()
|
||||
}
|
||||
|
||||
override suspend fun getBookmarkedChaptersByMangaId(mangaId: Long): List<Chapter> {
|
||||
return database.chaptersQueries
|
||||
return database.chapterQueries
|
||||
.getBookmarkedChaptersByMangaId(mangaId, ::mapChapter)
|
||||
.awaitAsList()
|
||||
}
|
||||
|
||||
override suspend fun getChapterById(id: Long): Chapter? {
|
||||
return database.chaptersQueries
|
||||
return database.chapterQueries
|
||||
.getChapterById(id, ::mapChapter)
|
||||
.awaitAsOneOrNull()
|
||||
}
|
||||
|
||||
override suspend fun getChapterByMangaIdAsFlow(mangaId: Long, applyScanlatorFilter: Boolean): Flow<List<Chapter>> {
|
||||
return database.chaptersQueries
|
||||
.getChaptersByMangaId(mangaId, applyScanlatorFilter.toLong(), ::mapChapter)
|
||||
return database.chapterQueries
|
||||
.getChaptersByMangaId(
|
||||
mangaId = mangaId,
|
||||
applyScanlatorFilter = applyScanlatorFilter.toLong(),
|
||||
mapper = ::mapChapter,
|
||||
)
|
||||
.subscribeToList()
|
||||
}
|
||||
|
||||
override suspend fun getChapterByUrlAndMangaId(url: String, mangaId: Long): Chapter? {
|
||||
return database.chaptersQueries
|
||||
return database.chapterQueries
|
||||
.getChapterByUrlAndMangaId(url, mangaId, ::mapChapter)
|
||||
.awaitAsOneOrNull()
|
||||
}
|
||||
|
||||
@@ -81,9 +81,9 @@ class HistoryRepositoryImpl(
|
||||
override suspend fun upsertHistory(historyUpdate: HistoryUpdate) {
|
||||
try {
|
||||
database.historyQueries.upsert(
|
||||
historyUpdate.chapterId,
|
||||
historyUpdate.readAt,
|
||||
historyUpdate.sessionReadDuration,
|
||||
chapterId = historyUpdate.chapterId,
|
||||
readAt = historyUpdate.readAt,
|
||||
readDuration = historyUpdate.sessionReadDuration,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, throwable = e)
|
||||
|
||||
@@ -84,27 +84,27 @@ object MangaMapper {
|
||||
categories: String,
|
||||
): LibraryManga = LibraryManga(
|
||||
manga = mapManga(
|
||||
id,
|
||||
source,
|
||||
url,
|
||||
artist,
|
||||
author,
|
||||
description,
|
||||
genre,
|
||||
title,
|
||||
status,
|
||||
thumbnailUrl,
|
||||
lastUpdate,
|
||||
nextUpdate,
|
||||
initialized,
|
||||
viewerFlags,
|
||||
chapterFlags,
|
||||
coverLastModified,
|
||||
updateStrategy,
|
||||
calculateInterval,
|
||||
notes,
|
||||
memo,
|
||||
favoriteAt,
|
||||
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,
|
||||
),
|
||||
categories = categories.split(",").map { it.toLong() },
|
||||
totalChapters = totalCount,
|
||||
@@ -140,27 +140,27 @@ object MangaMapper {
|
||||
totalCount: Long,
|
||||
): MangaWithChapterCount = MangaWithChapterCount(
|
||||
manga = mapManga(
|
||||
id,
|
||||
source,
|
||||
url,
|
||||
artist,
|
||||
author,
|
||||
description,
|
||||
genre,
|
||||
title,
|
||||
status,
|
||||
thumbnailUrl,
|
||||
lastUpdate,
|
||||
nextUpdate,
|
||||
initialized,
|
||||
viewerFlags,
|
||||
chapterFlags,
|
||||
coverLastModified,
|
||||
updateStrategy,
|
||||
calculateInterval,
|
||||
notes,
|
||||
memo,
|
||||
favoriteAt,
|
||||
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,
|
||||
),
|
||||
chapterCount = totalCount,
|
||||
)
|
||||
|
||||
@@ -35,37 +35,37 @@ class MangaRepositoryImpl(
|
||||
) : MangaRepository {
|
||||
|
||||
override suspend fun getMangaById(id: Long): Manga {
|
||||
return database.mangasQueries
|
||||
return database.mangaQueries
|
||||
.getMangaById(id, MangaMapper::mapManga)
|
||||
.awaitAsOne()
|
||||
}
|
||||
|
||||
override fun getMangaByIdAsFlow(id: Long): Flow<Manga> {
|
||||
return database.mangasQueries
|
||||
return database.mangaQueries
|
||||
.getMangaById(id, MangaMapper::mapManga)
|
||||
.subscribeToOne()
|
||||
}
|
||||
|
||||
override suspend fun getMangaByUrlAndSourceId(url: String, sourceId: Long): Manga? {
|
||||
return database.mangasQueries
|
||||
return database.mangaQueries
|
||||
.getMangaByUrlAndSource(url, sourceId, MangaMapper::mapManga)
|
||||
.awaitAsOneOrNull()
|
||||
}
|
||||
|
||||
override fun getMangaByUrlAndSourceIdAsFlow(url: String, sourceId: Long): Flow<Manga?> {
|
||||
return database.mangasQueries
|
||||
return database.mangaQueries
|
||||
.getMangaByUrlAndSource(url, sourceId, MangaMapper::mapManga)
|
||||
.subscribeToOneOrNull()
|
||||
}
|
||||
|
||||
override suspend fun getFavorites(): List<Manga> {
|
||||
return database.mangasQueries
|
||||
return database.mangaQueries
|
||||
.getFavorites(MangaMapper::mapManga)
|
||||
.awaitAsList()
|
||||
}
|
||||
|
||||
override suspend fun getReadMangaNotInLibrary(): List<Manga> {
|
||||
return database.mangasQueries
|
||||
return database.mangaQueries
|
||||
.getReadMangaNotInLibrary(MangaMapper::mapManga)
|
||||
.awaitAsList()
|
||||
}
|
||||
@@ -83,13 +83,13 @@ class MangaRepositoryImpl(
|
||||
}
|
||||
|
||||
override fun getFavoritesBySourceId(sourceId: Long): Flow<List<Manga>> {
|
||||
return database.mangasQueries
|
||||
return database.mangaQueries
|
||||
.getFavoriteBySourceId(sourceId, MangaMapper::mapManga)
|
||||
.subscribeToList()
|
||||
}
|
||||
|
||||
override suspend fun getDuplicateLibraryManga(id: Long, title: String): List<MangaWithChapterCount> {
|
||||
return database.mangasQueries
|
||||
return database.mangaQueries
|
||||
.getDuplicateLibraryManga(id, title, MangaMapper::mapMangaWithChapterCount)
|
||||
.awaitAsList()
|
||||
}
|
||||
@@ -102,7 +102,7 @@ class MangaRepositoryImpl(
|
||||
val timeZone = TimeZone.currentSystemDefault()
|
||||
val epochMillis =
|
||||
Clock.System.now().toLocalDateTime(timeZone).date.atStartOfDayIn(timeZone).toEpochMilliseconds()
|
||||
return database.mangasQueries
|
||||
return database.mangaQueries
|
||||
.getUpcomingManga(
|
||||
startOfDay = epochMillis,
|
||||
statuses = statuses,
|
||||
@@ -117,7 +117,7 @@ class MangaRepositoryImpl(
|
||||
|
||||
override suspend fun resetViewerFlags(): Boolean {
|
||||
return try {
|
||||
database.mangasQueries.resetViewerFlags()
|
||||
database.mangaQueries.resetViewerFlags()
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
@@ -126,27 +126,27 @@ class MangaRepositoryImpl(
|
||||
}
|
||||
|
||||
override suspend fun deleteNonLibraryManga(sourceIds: List<Long>, keepReadManga: Boolean) {
|
||||
database.mangasQueries.deleteNonLibraryManga(sourceIds, keepReadManga.toLong())
|
||||
database.mangaQueries.deleteNonLibraryManga(sourceIds, keepReadManga.toLong())
|
||||
}
|
||||
|
||||
override suspend fun setMangaCategories(mangaId: Long, categoryIds: List<Long>) {
|
||||
database.transaction {
|
||||
database.mangas_categoriesQueries.deleteMangaCategoryByMangaId(mangaId)
|
||||
database.manga_categoryQueries.deleteMangaCategoryByMangaId(mangaId)
|
||||
categoryIds.forEach { categoryId ->
|
||||
database.mangas_categoriesQueries.insert(mangaId, categoryId)
|
||||
database.manga_categoryQueries.insert(mangaId = mangaId, categoryId = categoryId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getExcludedScanlators(mangaId: Long): Set<String> {
|
||||
return database.excluded_scanlatorsQueries
|
||||
return database.excluded_scanlatorQueries
|
||||
.getExcludedScanlatorsByMangaId(mangaId)
|
||||
.awaitAsList()
|
||||
.toSet()
|
||||
}
|
||||
|
||||
override fun getExcludedScanlatorsAsFlow(mangaId: Long): Flow<Set<String>> {
|
||||
return database.excluded_scanlatorsQueries
|
||||
return database.excluded_scanlatorQueries
|
||||
.getExcludedScanlatorsByMangaId(mangaId)
|
||||
.subscribeToList()
|
||||
.map { it.toSet() }
|
||||
@@ -154,14 +154,14 @@ class MangaRepositoryImpl(
|
||||
|
||||
override suspend fun setExcludedScanlators(mangaId: Long, excludedScanlators: Set<String>) {
|
||||
database.transaction {
|
||||
val current = database.excluded_scanlatorsQueries
|
||||
val current = database.excluded_scanlatorQueries
|
||||
.getExcludedScanlatorsByMangaId(mangaId)
|
||||
.awaitAsList()
|
||||
.toSet()
|
||||
excludedScanlators.minus(current).forEach { scanlator ->
|
||||
database.excluded_scanlatorsQueries.insert(mangaId, scanlator)
|
||||
database.excluded_scanlatorQueries.insert(mangaId, scanlator)
|
||||
}
|
||||
database.excluded_scanlatorsQueries.remove(mangaId, current.minus(excludedScanlators))
|
||||
database.excluded_scanlatorQueries.remove(mangaId, current.minus(excludedScanlators))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ class MangaRepositoryImpl(
|
||||
override suspend fun insertNetworkManga(manga: List<Manga>): List<Manga> {
|
||||
return database.transactionWithResult {
|
||||
manga.map {
|
||||
database.mangasQueries.insertNetworkManga(
|
||||
database.mangaQueries.insertNetworkManga(
|
||||
source = it.source,
|
||||
url = it.url,
|
||||
artist = it.artist,
|
||||
@@ -220,7 +220,7 @@ class MangaRepositoryImpl(
|
||||
|
||||
override suspend fun updateRemote(update: MangaRemoteUpdate): Boolean {
|
||||
return try {
|
||||
database.mangasQueries.updateRemote(
|
||||
database.mangaQueries.updateRemote(
|
||||
artist = update.artist,
|
||||
author = update.author,
|
||||
description = update.description,
|
||||
@@ -245,7 +245,7 @@ class MangaRepositoryImpl(
|
||||
database.transaction {
|
||||
mangaUpdates.forEach { value ->
|
||||
with(value) {
|
||||
database.mangasQueries.update(
|
||||
database.mangaQueries.update(
|
||||
favoriteAtSet = isSet(::favoriteAt),
|
||||
favoriteAt = favoriteAt,
|
||||
lastUpdate = lastUpdate,
|
||||
|
||||
@@ -48,7 +48,7 @@ class SourceRepositoryImpl(
|
||||
}
|
||||
|
||||
override fun getSourcesWithFavoriteCount(): Flow<List<Pair<DomainSource, Long>>> {
|
||||
val sourceIdWithFavoriteCountFlow = database.mangasQueries
|
||||
val sourceIdWithFavoriteCountFlow = database.mangaQueries
|
||||
.getSourceIdWithFavoriteCount()
|
||||
.subscribeToList()
|
||||
return combine(sourceIdWithFavoriteCountFlow, sourceManager.sources) { sourceIdWithFavoriteCount, _ ->
|
||||
@@ -66,7 +66,7 @@ class SourceRepositoryImpl(
|
||||
}
|
||||
|
||||
override fun getSourcesWithNonLibraryManga(): Flow<List<SourceWithCount>> {
|
||||
return database.mangasQueries
|
||||
return database.mangaQueries
|
||||
.getSourceIdsWithNonLibraryManga()
|
||||
.subscribeToList()
|
||||
.map { sourceId ->
|
||||
|
||||
@@ -19,19 +19,19 @@ class StubSourceRepositoryImpl(
|
||||
) : StubSourceRepository {
|
||||
|
||||
override fun subscribeAll(): Flow<List<StubSource>> {
|
||||
return database.sourcesQueries
|
||||
return database.sourceQueries
|
||||
.findAll(::mapStubSource)
|
||||
.subscribeToList()
|
||||
}
|
||||
|
||||
override suspend fun getStubSource(id: Long): StubSource? {
|
||||
return database.sourcesQueries
|
||||
return database.sourceQueries
|
||||
.findOne(id, ::mapStubSource)
|
||||
.awaitAsOneOrNull()
|
||||
}
|
||||
|
||||
override suspend fun upsertStubSource(id: Long, lang: String, name: String) {
|
||||
database.sourcesQueries.upsert(id, lang, name)
|
||||
database.sourceQueries.upsert(id = id, lang = lang, name = name)
|
||||
}
|
||||
|
||||
private fun mapStubSource(
|
||||
|
||||
@@ -20,31 +20,31 @@ class TrackRepositoryImpl(
|
||||
) : TrackRepository {
|
||||
|
||||
override suspend fun getTrackById(id: Long): Track? {
|
||||
return database.manga_syncQueries
|
||||
return database.manga_trackQueries
|
||||
.getTrackById(id, TrackMapper::mapTrack)
|
||||
.awaitAsOneOrNull()
|
||||
}
|
||||
|
||||
override suspend fun getTracksByMangaId(mangaId: Long): List<Track> {
|
||||
return database.manga_syncQueries
|
||||
return database.manga_trackQueries
|
||||
.getTracksByMangaId(mangaId, TrackMapper::mapTrack)
|
||||
.awaitAsList()
|
||||
}
|
||||
|
||||
override fun getTracksAsFlow(): Flow<List<Track>> {
|
||||
return database.manga_syncQueries
|
||||
return database.manga_trackQueries
|
||||
.getTracks(TrackMapper::mapTrack)
|
||||
.subscribeToList()
|
||||
}
|
||||
|
||||
override fun getTracksByMangaIdAsFlow(mangaId: Long): Flow<List<Track>> {
|
||||
return database.manga_syncQueries
|
||||
return database.manga_trackQueries
|
||||
.getTracksByMangaId(mangaId, TrackMapper::mapTrack)
|
||||
.subscribeToList()
|
||||
}
|
||||
|
||||
override suspend fun delete(mangaId: Long, trackerId: Long) {
|
||||
database.manga_syncQueries.delete(
|
||||
database.manga_trackQueries.delete(
|
||||
mangaId = mangaId,
|
||||
syncId = trackerId,
|
||||
)
|
||||
@@ -61,7 +61,7 @@ class TrackRepositoryImpl(
|
||||
private suspend fun upsertValues(vararg tracks: Track) {
|
||||
database.transaction {
|
||||
tracks.forEach { mangaTrack ->
|
||||
database.manga_syncQueries.upsert(
|
||||
database.manga_trackQueries.upsert(
|
||||
mangaId = mangaTrack.mangaId,
|
||||
syncId = mangaTrack.trackerId,
|
||||
remoteId = mangaTrack.remoteId,
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
CREATE TABLE categories(
|
||||
_id INTEGER NOT NULL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
sort INTEGER NOT NULL,
|
||||
flags INTEGER NOT NULL
|
||||
);
|
||||
|
||||
-- Insert system category
|
||||
INSERT OR IGNORE INTO categories(_id, name, sort, flags) VALUES (0, "", -1, 0);
|
||||
-- Disallow deletion of default category
|
||||
CREATE TRIGGER IF NOT EXISTS system_category_delete_trigger BEFORE DELETE
|
||||
ON categories
|
||||
BEGIN SELECT CASE
|
||||
WHEN old._id <= 0 THEN
|
||||
RAISE(ABORT, "System category can't be deleted")
|
||||
END;
|
||||
END;
|
||||
|
||||
getCategory:
|
||||
SELECT *
|
||||
FROM categories
|
||||
WHERE _id = :id
|
||||
LIMIT 1;
|
||||
|
||||
getCategories:
|
||||
SELECT
|
||||
_id AS id,
|
||||
name,
|
||||
sort AS `order`,
|
||||
flags
|
||||
FROM categories
|
||||
ORDER BY sort;
|
||||
|
||||
getCategoriesByMangaId:
|
||||
SELECT
|
||||
C._id AS id,
|
||||
C.name,
|
||||
C.sort AS `order`,
|
||||
C.flags
|
||||
FROM categories C
|
||||
JOIN mangas_categories MC
|
||||
ON C._id = MC.category_id
|
||||
WHERE MC.manga_id = :mangaId
|
||||
ORDER BY C.sort;
|
||||
|
||||
insert:
|
||||
INSERT INTO categories(name, sort, flags)
|
||||
SELECT :name, coalesce(max(sort), -1) + 1, :flags
|
||||
FROM categories;
|
||||
|
||||
delete:
|
||||
DELETE FROM categories
|
||||
WHERE _id = :categoryId;
|
||||
|
||||
updateName:
|
||||
UPDATE categories
|
||||
SET name = :name
|
||||
WHERE _id = :categoryId;
|
||||
|
||||
getUserCategoryIds:
|
||||
SELECT _id
|
||||
FROM categories
|
||||
WHERE _id > 0
|
||||
ORDER BY sort;
|
||||
|
||||
updateOrder:
|
||||
UPDATE categories
|
||||
SET sort = :order
|
||||
WHERE _id = :categoryId;
|
||||
|
||||
updateFlags:
|
||||
UPDATE categories
|
||||
SET flags = :flags
|
||||
WHERE _id = :categoryId;
|
||||
|
||||
updateAllFlags:
|
||||
UPDATE categories SET
|
||||
flags = coalesce(:flags, flags);
|
||||
@@ -0,0 +1,78 @@
|
||||
CREATE TABLE category(
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
`order` INTEGER NOT NULL UNIQUE,
|
||||
flags INTEGER NOT NULL
|
||||
);
|
||||
|
||||
-- Insert system category
|
||||
INSERT OR IGNORE INTO category(id, name, `order`, flags) VALUES (0, "", -1, 0);
|
||||
-- Disallow deletion of default category
|
||||
CREATE TRIGGER IF NOT EXISTS system_category_delete_trigger BEFORE DELETE
|
||||
ON category
|
||||
BEGIN SELECT CASE
|
||||
WHEN old.id <= 0 THEN
|
||||
RAISE(ABORT, "System category can't be deleted")
|
||||
END;
|
||||
END;
|
||||
|
||||
getCategory:
|
||||
SELECT id, name, `order`, flags
|
||||
FROM category
|
||||
WHERE id = :id
|
||||
LIMIT 1;
|
||||
|
||||
getCategories:
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
`order`,
|
||||
flags
|
||||
FROM category
|
||||
ORDER BY `order`;
|
||||
|
||||
getCategoriesByMangaId:
|
||||
SELECT
|
||||
C.id,
|
||||
C.name,
|
||||
C.`order`,
|
||||
C.flags
|
||||
FROM category C
|
||||
JOIN manga_category MC
|
||||
ON C.id = MC.category_id
|
||||
WHERE MC.manga_id = :mangaId
|
||||
ORDER BY C.`order`;
|
||||
|
||||
insert:
|
||||
INSERT INTO category(name, `order`, flags)
|
||||
SELECT :name, coalesce(max(`order`), -1) + 1, :flags
|
||||
FROM category;
|
||||
|
||||
delete:
|
||||
DELETE FROM category
|
||||
WHERE id = :categoryId;
|
||||
|
||||
updateName:
|
||||
UPDATE category
|
||||
SET name = :name
|
||||
WHERE id = :categoryId;
|
||||
|
||||
getUserCategoryIds:
|
||||
SELECT id
|
||||
FROM category
|
||||
WHERE id > 0
|
||||
ORDER BY `order`;
|
||||
|
||||
updateOrder:
|
||||
UPDATE category
|
||||
SET `order` = :order
|
||||
WHERE id = :categoryId;
|
||||
|
||||
updateFlags:
|
||||
UPDATE category
|
||||
SET flags = :flags
|
||||
WHERE id = :categoryId;
|
||||
|
||||
updateAllFlags:
|
||||
UPDATE category SET
|
||||
flags = coalesce(:flags, flags);
|
||||
@@ -0,0 +1,123 @@
|
||||
import kotlin.Boolean;
|
||||
import kotlinx.serialization.json.JsonObject;
|
||||
|
||||
CREATE TABLE chapter(
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
manga_id INTEGER NOT NULL,
|
||||
remote_url TEXT NOT NULL,
|
||||
remote_name TEXT NOT NULL,
|
||||
remote_scanlator TEXT,
|
||||
remote_chapter_number REAL NOT NULL,
|
||||
remote_date_upload INTEGER NOT NULL,
|
||||
remote_order INTEGER NOT NULL,
|
||||
remote_memo BLOB AS JsonObject NOT NULL,
|
||||
user_read INTEGER AS Boolean NOT NULL,
|
||||
user_bookmark INTEGER AS Boolean NOT NULL,
|
||||
user_last_page_read INTEGER NOT NULL,
|
||||
state_date_fetch INTEGER NOT NULL,
|
||||
UNIQUE(manga_id, remote_url),
|
||||
FOREIGN KEY(manga_id) REFERENCES manga (id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX chapter_state_date_fetch_index ON chapter(state_date_fetch);
|
||||
CREATE INDEX chapter_unread_state_date_fetch_index ON chapter(state_date_fetch) WHERE user_read = 0;
|
||||
|
||||
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
|
||||
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
|
||||
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 = :mangaId
|
||||
AND (
|
||||
:applyScanlatorFilter = 0
|
||||
OR ES.scanlator IS NULL
|
||||
);
|
||||
|
||||
getScanlatorsByMangaId:
|
||||
SELECT 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
|
||||
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;
|
||||
|
||||
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
|
||||
FROM chapter
|
||||
WHERE remote_url = :chapterUrl
|
||||
AND manga_id = :mangaId
|
||||
LIMIT 1;
|
||||
|
||||
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)
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
@@ -1,104 +0,0 @@
|
||||
import kotlin.Boolean;
|
||||
import kotlinx.serialization.json.JsonObject;
|
||||
|
||||
CREATE TABLE chapters(
|
||||
_id INTEGER NOT NULL PRIMARY KEY,
|
||||
manga_id INTEGER NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
scanlator TEXT,
|
||||
read INTEGER AS Boolean NOT NULL,
|
||||
bookmark INTEGER AS Boolean NOT NULL,
|
||||
last_page_read INTEGER NOT NULL,
|
||||
chapter_number REAL NOT NULL,
|
||||
source_order INTEGER NOT NULL,
|
||||
date_fetch INTEGER NOT NULL,
|
||||
date_upload INTEGER NOT NULL,
|
||||
memo BLOB AS JsonObject NOT NULL DEFAULT '{}',
|
||||
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX chapters_manga_id_index ON chapters(manga_id);
|
||||
CREATE INDEX chapters_unread_by_manga_index ON chapters(manga_id, read) WHERE read = 0;
|
||||
CREATE INDEX idx_chapters_url ON chapters(url);
|
||||
|
||||
|
||||
getChapterById:
|
||||
SELECT *
|
||||
FROM chapters
|
||||
WHERE _id = :id;
|
||||
|
||||
getChaptersByMangaId:
|
||||
SELECT C.*
|
||||
FROM chapters C
|
||||
LEFT JOIN excluded_scanlators ES
|
||||
ON C.manga_id = ES.manga_id
|
||||
AND C.scanlator = ES.scanlator
|
||||
WHERE C.manga_id = :mangaId
|
||||
AND (
|
||||
:applyScanlatorFilter = 0
|
||||
OR ES.scanlator IS NULL
|
||||
);
|
||||
|
||||
getScanlatorsByMangaId:
|
||||
SELECT scanlator
|
||||
FROM chapters
|
||||
WHERE manga_id = :mangaId;
|
||||
|
||||
getBookmarkedChaptersByMangaId:
|
||||
SELECT *
|
||||
FROM chapters
|
||||
WHERE bookmark
|
||||
AND manga_id = :mangaId;
|
||||
|
||||
getChapterByUrl:
|
||||
SELECT *
|
||||
FROM chapters
|
||||
WHERE url = :chapterUrl;
|
||||
|
||||
getChapterByUrlAndMangaId:
|
||||
SELECT *
|
||||
FROM chapters
|
||||
WHERE url = :chapterUrl
|
||||
AND manga_id = :mangaId
|
||||
LIMIT 1;
|
||||
|
||||
removeChaptersWithIds:
|
||||
DELETE FROM chapters
|
||||
WHERE _id IN :chapterIds;
|
||||
|
||||
insert:
|
||||
INSERT INTO chapters(manga_id, url, name, scanlator, read, bookmark, last_page_read, chapter_number, source_order, date_fetch, date_upload, memo)
|
||||
VALUES (:mangaId, :url, :name, :scanlator, :read, :bookmark, :lastPageRead, :chapterNumber, :sourceOrder, :dateFetch, :dateUpload, :memo);
|
||||
|
||||
insertReturningId:
|
||||
INSERT INTO chapters(manga_id, url, name, scanlator, read, bookmark, last_page_read, chapter_number, source_order, date_fetch, date_upload, memo)
|
||||
VALUES (:mangaId, :url, :name, :scanlator, :read, :bookmark, :lastPageRead, :chapterNumber, :sourceOrder, :dateFetch, :dateUpload, :memo)
|
||||
RETURNING _id;
|
||||
|
||||
update:
|
||||
UPDATE chapters
|
||||
SET read = coalesce(:read, read),
|
||||
bookmark = coalesce(:bookmark, bookmark),
|
||||
last_page_read = coalesce(:lastPageRead, last_page_read),
|
||||
date_fetch = coalesce(:dateFetch, date_fetch)
|
||||
WHERE _id = :chapterId;
|
||||
|
||||
updateFromBackup:
|
||||
UPDATE chapters
|
||||
SET read = coalesce(:read, read),
|
||||
bookmark = coalesce(:bookmark, bookmark),
|
||||
last_page_read = coalesce(:lastPageRead, last_page_read),
|
||||
memo = coalesce(:memo, memo)
|
||||
WHERE _id = :chapterId;
|
||||
|
||||
updateRemote:
|
||||
UPDATE chapters
|
||||
SET name = :name,
|
||||
scanlator = coalesce(:scanlator, scanlator),
|
||||
chapter_number = :chapterNumber,
|
||||
source_order = :sourceOrder,
|
||||
date_upload = coalesce(:dateUpload, date_upload),
|
||||
memo = :memo
|
||||
WHERE _id = :chapterId;
|
||||
@@ -0,0 +1,22 @@
|
||||
CREATE TABLE excluded_scanlator(
|
||||
manga_id INTEGER NOT NULL,
|
||||
scanlator TEXT NOT NULL,
|
||||
UNIQUE(manga_id, scanlator),
|
||||
FOREIGN KEY(manga_id) REFERENCES manga (id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
|
||||
insert:
|
||||
INSERT INTO excluded_scanlator(manga_id, scanlator)
|
||||
VALUES (:mangaId, :scanlator);
|
||||
|
||||
remove:
|
||||
DELETE FROM excluded_scanlator
|
||||
WHERE manga_id = :mangaId
|
||||
AND scanlator IN :scanlators;
|
||||
|
||||
getExcludedScanlatorsByMangaId:
|
||||
SELECT scanlator
|
||||
FROM excluded_scanlator
|
||||
WHERE manga_id = :mangaId;
|
||||
@@ -1,23 +0,0 @@
|
||||
CREATE TABLE excluded_scanlators(
|
||||
manga_id INTEGER NOT NULL,
|
||||
scanlator TEXT NOT NULL,
|
||||
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX excluded_scanlators_manga_id_index ON excluded_scanlators(manga_id);
|
||||
CREATE INDEX idx_excluded_scanlators_scanlator ON excluded_scanlators(scanlator);
|
||||
|
||||
insert:
|
||||
INSERT INTO excluded_scanlators(manga_id, scanlator)
|
||||
VALUES (:mangaId, :scanlator);
|
||||
|
||||
remove:
|
||||
DELETE FROM excluded_scanlators
|
||||
WHERE manga_id = :mangaId
|
||||
AND scanlator IN :scanlators;
|
||||
|
||||
getExcludedScanlatorsByMangaId:
|
||||
SELECT scanlator
|
||||
FROM excluded_scanlators
|
||||
WHERE manga_id = :mangaId;
|
||||
@@ -15,7 +15,9 @@ contains:
|
||||
SELECT EXISTS(SELECT 0 FROM extension_store WHERE index_url = :indexUrl);
|
||||
|
||||
getAll:
|
||||
SELECT *
|
||||
SELECT
|
||||
index_url, name, badge_label, signing_key,
|
||||
contact_website, contact_discord, is_legacy, extension_list_url
|
||||
FROM extension_store;
|
||||
|
||||
getCount:
|
||||
|
||||
@@ -1,75 +1,70 @@
|
||||
import java.util.Date;
|
||||
|
||||
CREATE TABLE history(
|
||||
_id INTEGER NOT NULL PRIMARY KEY,
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
manga_id INTEGER NOT NULL,
|
||||
chapter_id INTEGER NOT NULL UNIQUE,
|
||||
last_read INTEGER AS Date,
|
||||
time_read INTEGER NOT NULL,
|
||||
FOREIGN KEY(chapter_id) REFERENCES chapters (_id)
|
||||
read_at INTEGER AS Date,
|
||||
read_duration INTEGER NOT NULL,
|
||||
FOREIGN KEY(manga_id) REFERENCES manga (id)
|
||||
ON DELETE CASCADE,
|
||||
FOREIGN KEY(chapter_id) REFERENCES chapter (id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX history_history_chapter_id_index ON history(chapter_id);
|
||||
CREATE INDEX idx_history_last_read ON history(last_read);
|
||||
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.id,
|
||||
H.chapter_id,
|
||||
H.last_read,
|
||||
H.time_read
|
||||
H.read_at,
|
||||
H.read_duration
|
||||
FROM history H
|
||||
JOIN chapters C
|
||||
ON H.chapter_id = C._id
|
||||
WHERE C.manga_id = :mangaId AND C._id = H.chapter_id;
|
||||
WHERE H.manga_id = :mangaId;
|
||||
|
||||
getHistoryByChapterUrlAndMangaId:
|
||||
SELECT
|
||||
H._id,
|
||||
H.id,
|
||||
H.chapter_id,
|
||||
H.last_read,
|
||||
H.time_read
|
||||
H.read_at,
|
||||
H.read_duration
|
||||
FROM history H
|
||||
JOIN chapters C
|
||||
ON H.chapter_id = C._id
|
||||
WHERE C.url = :chapterUrl AND C.manga_id = :mangaId AND C._id = H.chapter_id
|
||||
JOIN chapter C
|
||||
ON H.chapter_id = C.id
|
||||
WHERE C.remote_url = :chapterUrl AND C.manga_id = :mangaId
|
||||
LIMIT 1;
|
||||
|
||||
resetHistoryById:
|
||||
UPDATE history
|
||||
SET last_read = 0
|
||||
WHERE _id = :historyId;
|
||||
SET read_at = 0
|
||||
WHERE id = :historyId;
|
||||
|
||||
resetHistoryByMangaId:
|
||||
UPDATE history
|
||||
SET last_read = 0
|
||||
WHERE _id IN (
|
||||
SELECT H._id
|
||||
FROM mangas M
|
||||
INNER JOIN chapters C
|
||||
ON M._id = C.manga_id
|
||||
INNER JOIN history H
|
||||
ON C._id = H.chapter_id
|
||||
WHERE M._id = :mangaId
|
||||
);
|
||||
SET read_at = 0
|
||||
WHERE manga_id = :mangaId;
|
||||
|
||||
removeAllHistory:
|
||||
DELETE FROM history;
|
||||
|
||||
removeResettedHistory:
|
||||
DELETE FROM history
|
||||
WHERE last_read = 0;
|
||||
WHERE read_at = 0;
|
||||
|
||||
upsert:
|
||||
INSERT INTO history(chapter_id, last_read, time_read)
|
||||
VALUES (:chapterId, :readAt, :time_read)
|
||||
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
|
||||
last_read = :readAt,
|
||||
time_read = time_read + :time_read
|
||||
read_at = :readAt,
|
||||
read_duration = read_duration + :readDuration
|
||||
WHERE chapter_id = :chapterId;
|
||||
|
||||
getReadDuration:
|
||||
SELECT coalesce(sum(time_read), 0)
|
||||
SELECT coalesce(sum(read_duration), 0)
|
||||
FROM history;
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy;
|
||||
import kotlin.Boolean;
|
||||
import kotlin.String;
|
||||
import kotlin.collections.List;
|
||||
import kotlinx.serialization.json.JsonObject;
|
||||
|
||||
CREATE TABLE manga(
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
source_id INTEGER NOT NULL,
|
||||
remote_url TEXT NOT NULL,
|
||||
remote_title TEXT NOT NULL,
|
||||
remote_author TEXT,
|
||||
remote_artist TEXT,
|
||||
remote_description TEXT,
|
||||
remote_genre TEXT AS List<String>,
|
||||
remote_status INTEGER NOT NULL,
|
||||
remote_cover TEXT,
|
||||
remote_update_strategy INTEGER AS UpdateStrategy NOT NULL,
|
||||
remote_memo BLOB AS JsonObject NOT NULL,
|
||||
user_favorite_at INTEGER,
|
||||
user_notes TEXT NOT NULL,
|
||||
user_reader_flags INTEGER NOT NULL,
|
||||
user_chapter_flags INTEGER NOT NULL,
|
||||
state_chapter_last_update INTEGER,
|
||||
state_chapter_next_update INTEGER,
|
||||
state_chapter_fetch_interval INTEGER NOT NULL,
|
||||
state_cover_last_modified INTEGER NOT NULL,
|
||||
state_initialized INTEGER AS Boolean NOT NULL,
|
||||
UNIQUE(source_id, remote_url)
|
||||
);
|
||||
|
||||
CREATE INDEX manga_user_favorite_at_index ON manga(user_favorite_at) WHERE user_favorite_at IS NOT NULL;
|
||||
|
||||
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
|
||||
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
|
||||
FROM manga
|
||||
WHERE remote_url = :url
|
||||
AND source_id = :source
|
||||
LIMIT 1;
|
||||
|
||||
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
|
||||
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
|
||||
FROM manga
|
||||
WHERE user_favorite_at IS NULL AND id IN (
|
||||
SELECT DISTINCT chapter.manga_id
|
||||
FROM chapter
|
||||
WHERE 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(*)
|
||||
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
|
||||
FROM manga
|
||||
WHERE user_favorite_at IS NOT NULL
|
||||
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
|
||||
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;
|
||||
|
||||
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
|
||||
FROM manga
|
||||
WHERE state_chapter_next_update >= :startOfDay
|
||||
AND user_favorite_at IS NOT NULL
|
||||
AND remote_status IN :statuses
|
||||
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 = manga.id
|
||||
AND COALESCE(manga_category.category_id, 0) IN :includedCategories
|
||||
)
|
||||
OR (0 IN :includedCategories AND NOT EXISTS (
|
||||
SELECT 1 FROM manga_category WHERE manga_category.manga_id = manga.id)
|
||||
)
|
||||
)
|
||||
AND (
|
||||
-- excludedEmpty being true expresses "don't care" state and bypasses the membership filter
|
||||
:excludedEmpty
|
||||
OR (
|
||||
NOT EXISTS (
|
||||
SELECT 1 FROM manga_category
|
||||
WHERE manga_category.manga_id = manga.id
|
||||
AND COALESCE(manga_category.category_id, 0) IN :excludedCategories
|
||||
)
|
||||
AND (
|
||||
0 NOT IN :excludedCategories
|
||||
OR EXISTS (SELECT 1 FROM manga_category WHERE manga_category.manga_id = manga.id)
|
||||
)
|
||||
)
|
||||
)
|
||||
ORDER BY state_chapter_next_update ASC;
|
||||
|
||||
resetViewerFlags:
|
||||
UPDATE manga
|
||||
SET user_reader_flags = 0;
|
||||
|
||||
getSourceIdsWithNonLibraryManga:
|
||||
SELECT source_id, COUNT(*) AS manga_count
|
||||
FROM manga
|
||||
WHERE user_favorite_at IS NULL
|
||||
GROUP BY source_id;
|
||||
|
||||
deleteNonLibraryManga:
|
||||
DELETE FROM manga
|
||||
WHERE user_favorite_at IS NULL
|
||||
AND source_id IN :sourceIds
|
||||
AND (
|
||||
:keepReadManga = 0
|
||||
OR id NOT IN (
|
||||
SELECT DISTINCT manga_id
|
||||
FROM chapter
|
||||
WHERE 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)
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
insertNetworkManga {
|
||||
-- Insert the manga if it doesn't exist already
|
||||
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
|
||||
)
|
||||
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);
|
||||
|
||||
-- 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
|
||||
FROM manga
|
||||
WHERE source_id = :source
|
||||
AND remote_url = :url
|
||||
LIMIT 1;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
CREATE TABLE manga_category(
|
||||
manga_id INTEGER NOT NULL,
|
||||
category_id INTEGER NOT NULL,
|
||||
UNIQUE(manga_id, category_id),
|
||||
FOREIGN KEY(category_id) REFERENCES category (id)
|
||||
ON DELETE CASCADE,
|
||||
FOREIGN KEY(manga_id) REFERENCES manga (id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX manga_category_category_id_index ON manga_category(category_id);
|
||||
|
||||
insert:
|
||||
INSERT INTO manga_category(manga_id, category_id)
|
||||
VALUES (:mangaId, :categoryId);
|
||||
|
||||
deleteMangaCategoryByMangaId:
|
||||
DELETE FROM manga_category
|
||||
WHERE manga_id = :mangaId;
|
||||
+32
-23
@@ -1,49 +1,58 @@
|
||||
import kotlin.Boolean;
|
||||
|
||||
CREATE TABLE manga_sync(
|
||||
_id INTEGER NOT NULL PRIMARY KEY,
|
||||
CREATE TABLE manga_track(
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
manga_id INTEGER NOT NULL,
|
||||
sync_id INTEGER NOT NULL,
|
||||
tracker_id INTEGER NOT NULL,
|
||||
remote_id INTEGER NOT NULL,
|
||||
remote_url TEXT NOT NULL,
|
||||
library_id INTEGER,
|
||||
title TEXT NOT NULL,
|
||||
last_chapter_read REAL NOT NULL,
|
||||
total_chapters INTEGER NOT NULL,
|
||||
status INTEGER NOT NULL,
|
||||
score REAL NOT NULL,
|
||||
remote_url TEXT NOT NULL,
|
||||
start_date INTEGER NOT NULL,
|
||||
finish_date INTEGER NOT NULL,
|
||||
private INTEGER AS Boolean DEFAULT 0 NOT NULL,
|
||||
UNIQUE (manga_id, sync_id) ON CONFLICT REPLACE,
|
||||
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
|
||||
private INTEGER AS Boolean NOT NULL DEFAULT 0,
|
||||
UNIQUE (manga_id, tracker_id),
|
||||
FOREIGN KEY(manga_id) REFERENCES manga (id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_manga_sync_manga_id ON manga_sync(manga_id);
|
||||
CREATE INDEX manga_track_tracker_id_remote_id_index ON manga_track(tracker_id, remote_id);
|
||||
|
||||
delete:
|
||||
DELETE FROM manga_sync
|
||||
WHERE manga_id = :mangaId AND sync_id = :syncId;
|
||||
DELETE FROM manga_track
|
||||
WHERE manga_id = :mangaId AND tracker_id = :syncId;
|
||||
|
||||
getTracks:
|
||||
SELECT *
|
||||
FROM manga_sync;
|
||||
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
|
||||
FROM manga_track;
|
||||
|
||||
getTrackById:
|
||||
SELECT *
|
||||
FROM manga_sync
|
||||
WHERE _id = :id;
|
||||
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
|
||||
FROM manga_track
|
||||
WHERE id = :id;
|
||||
|
||||
getTracksByMangaId:
|
||||
SELECT *
|
||||
FROM manga_sync
|
||||
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
|
||||
FROM manga_track
|
||||
WHERE manga_id = :mangaId;
|
||||
|
||||
upsert:
|
||||
INSERT INTO manga_sync(manga_id,sync_id,remote_id,library_id,title,last_chapter_read,total_chapters,status,score,remote_url,start_date,finish_date,private)
|
||||
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, sync_id)
|
||||
ON CONFLICT(manga_id, tracker_id)
|
||||
DO UPDATE
|
||||
SET
|
||||
remote_id = :remoteId,
|
||||
@@ -57,13 +66,13 @@ SET
|
||||
start_date = :startDate,
|
||||
finish_date = :finishDate,
|
||||
private = :private
|
||||
WHERE manga_id = :mangaId AND sync_id = :syncId;
|
||||
WHERE manga_id = :mangaId AND tracker_id = :syncId;
|
||||
|
||||
update:
|
||||
UPDATE manga_sync
|
||||
UPDATE manga_track
|
||||
SET
|
||||
manga_id = coalesce(:mangaId, manga_id),
|
||||
sync_id = coalesce(:syncId, sync_id),
|
||||
tracker_id = coalesce(:syncId, tracker_id),
|
||||
remote_id = coalesce(:mediaId, remote_id),
|
||||
library_id = coalesce(:libraryId, library_id),
|
||||
title = coalesce(:title, title),
|
||||
@@ -75,4 +84,4 @@ SET
|
||||
start_date = coalesce(:startDate, start_date),
|
||||
finish_date = coalesce(:finishDate, finish_date),
|
||||
private = coalesce(:private, private)
|
||||
WHERE _id = :id;
|
||||
WHERE id = :id;
|
||||
@@ -1,273 +0,0 @@
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy;
|
||||
import kotlin.Boolean;
|
||||
import kotlin.String;
|
||||
import kotlin.collections.List;
|
||||
import kotlinx.serialization.json.JsonObject;
|
||||
|
||||
CREATE TABLE mangas(
|
||||
_id INTEGER NOT NULL PRIMARY KEY,
|
||||
source INTEGER NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
artist TEXT,
|
||||
author TEXT,
|
||||
description TEXT,
|
||||
genre TEXT AS List<String>,
|
||||
title TEXT NOT NULL,
|
||||
status INTEGER NOT NULL,
|
||||
thumbnail_url TEXT,
|
||||
last_update INTEGER,
|
||||
next_update INTEGER,
|
||||
initialized INTEGER AS Boolean NOT NULL,
|
||||
viewer INTEGER NOT NULL,
|
||||
chapter_flags INTEGER NOT NULL,
|
||||
cover_last_modified INTEGER NOT NULL,
|
||||
update_strategy INTEGER AS UpdateStrategy NOT NULL DEFAULT 0,
|
||||
calculate_interval INTEGER DEFAULT 0 NOT NULL,
|
||||
notes TEXT NOT NULL DEFAULT "",
|
||||
memo BLOB AS JsonObject NOT NULL DEFAULT '{}',
|
||||
favorite_at INTEGER
|
||||
);
|
||||
|
||||
CREATE INDEX library_favorite_at_index ON mangas(favorite_at) WHERE favorite_at IS NOT NULL;
|
||||
CREATE INDEX mangas_url_index ON mangas(url);
|
||||
CREATE INDEX idx_mangas_source ON mangas(source);
|
||||
|
||||
getMangaById:
|
||||
SELECT *
|
||||
FROM mangas
|
||||
WHERE _id = :id;
|
||||
|
||||
-- TODO: this should ideally never really have more than 1 result
|
||||
getMangaByUrlAndSource:
|
||||
SELECT *
|
||||
FROM mangas
|
||||
WHERE url = :url
|
||||
AND source = :source
|
||||
LIMIT 1;
|
||||
|
||||
getFavorites:
|
||||
SELECT *
|
||||
FROM mangas
|
||||
WHERE favorite_at IS NOT NULL;
|
||||
|
||||
getReadMangaNotInLibrary:
|
||||
SELECT *
|
||||
FROM mangas
|
||||
WHERE favorite_at IS NULL AND _id IN (
|
||||
SELECT DISTINCT chapters.manga_id
|
||||
FROM chapters
|
||||
WHERE read = 1 OR last_page_read != 0
|
||||
);
|
||||
|
||||
getAllManga:
|
||||
SELECT *
|
||||
FROM mangas;
|
||||
|
||||
getAllMangaSourceAndUrl:
|
||||
SELECT source, url
|
||||
FROM mangas;
|
||||
|
||||
getSourceIdWithFavoriteCount:
|
||||
SELECT
|
||||
source,
|
||||
count(*)
|
||||
FROM mangas
|
||||
WHERE favorite_at IS NOT NULL
|
||||
GROUP BY source;
|
||||
|
||||
getFavoriteBySourceId:
|
||||
SELECT *
|
||||
FROM mangas
|
||||
WHERE favorite_at IS NOT NULL
|
||||
AND source = :sourceId;
|
||||
|
||||
getDuplicateLibraryManga:
|
||||
WITH
|
||||
track_dupes AS (
|
||||
SELECT DISTINCT S2.manga_id
|
||||
FROM manga_sync S1
|
||||
INNER JOIN manga_sync S2
|
||||
ON S1.sync_id = S2.sync_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.*
|
||||
FROM mangas M
|
||||
LEFT JOIN track_dupes D
|
||||
ON D.manga_id = _id
|
||||
WHERE favorite_at IS NOT NULL
|
||||
AND _id != :id
|
||||
AND (lower(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 chapters C
|
||||
ON M._id = C.manga_id
|
||||
LEFT JOIN excluded_scanlators ES
|
||||
ON C.manga_id = ES.manga_id
|
||||
AND C.scanlator = ES.scanlator
|
||||
WHERE ES.scanlator IS NULL
|
||||
GROUP BY M._id
|
||||
)
|
||||
SELECT
|
||||
M.*,
|
||||
coalesce(CC.chapter_count, 0) AS chapter_count
|
||||
FROM duplicates M
|
||||
LEFT JOIN chapter_counts CC
|
||||
ON M._id = CC.manga_id;
|
||||
|
||||
getUpcomingManga:
|
||||
SELECT *
|
||||
FROM mangas
|
||||
WHERE next_update >= :startOfDay
|
||||
AND favorite_at IS NOT NULL
|
||||
AND status IN :statuses
|
||||
AND (
|
||||
-- includedEmpty being true expresses "don't care" state and bypasses the membership filter
|
||||
:includedEmpty
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM mangas_categories
|
||||
WHERE mangas_categories.manga_id = mangas._id
|
||||
AND COALESCE(mangas_categories.category_id, 0) IN :includedCategories
|
||||
)
|
||||
OR (0 IN :includedCategories AND NOT EXISTS (
|
||||
SELECT 1 FROM mangas_categories WHERE mangas_categories.manga_id = mangas._id)
|
||||
)
|
||||
)
|
||||
AND (
|
||||
-- excludedEmpty being true expresses "don't care" state and bypasses the membership filter
|
||||
:excludedEmpty
|
||||
OR (
|
||||
NOT EXISTS (
|
||||
SELECT 1 FROM mangas_categories
|
||||
WHERE mangas_categories.manga_id = mangas._id
|
||||
AND COALESCE(mangas_categories.category_id, 0) IN :excludedCategories
|
||||
)
|
||||
AND (
|
||||
0 NOT IN :excludedCategories
|
||||
OR EXISTS (SELECT 1 FROM mangas_categories WHERE mangas_categories.manga_id = mangas._id)
|
||||
)
|
||||
)
|
||||
)
|
||||
ORDER BY next_update ASC;
|
||||
|
||||
resetViewerFlags:
|
||||
UPDATE mangas
|
||||
SET viewer = 0;
|
||||
|
||||
getSourceIdsWithNonLibraryManga:
|
||||
SELECT source, COUNT(*) AS manga_count
|
||||
FROM mangas
|
||||
WHERE favorite_at IS NULL
|
||||
GROUP BY source;
|
||||
|
||||
deleteNonLibraryManga:
|
||||
DELETE FROM mangas
|
||||
WHERE favorite_at IS NULL
|
||||
AND source IN :sourceIds
|
||||
AND (
|
||||
:keepReadManga = 0
|
||||
OR _id NOT IN (
|
||||
SELECT DISTINCT manga_id
|
||||
FROM chapters
|
||||
WHERE read = 1
|
||||
OR last_page_read != 0
|
||||
)
|
||||
);
|
||||
|
||||
insertReturningId:
|
||||
INSERT INTO mangas(source, url, artist, author, description, genre, title, status, thumbnail_url, favorite_at, last_update, next_update, initialized, viewer, chapter_flags, cover_last_modified, update_strategy, calculate_interval, notes, memo)
|
||||
VALUES (:source, :url, :artist, :author, :description, :genre, :title, :status, :thumbnailUrl, :favoriteAt, :lastUpdate, :nextUpdate, :initialized, :viewerFlags, :chapterFlags, :coverLastModified, :updateStrategy, :calculateInterval, :notes, :memo)
|
||||
RETURNING _id;
|
||||
|
||||
update:
|
||||
UPDATE mangas SET
|
||||
-- NULL is a real value here (not in the library), which coalesce cannot write
|
||||
favorite_at = CASE WHEN :favoriteAtSet THEN :favoriteAt ELSE favorite_at END,
|
||||
last_update = coalesce(:lastUpdate, last_update),
|
||||
next_update = coalesce(:nextUpdate, next_update),
|
||||
viewer = coalesce(:viewer, viewer),
|
||||
chapter_flags = coalesce(:chapterFlags, chapter_flags),
|
||||
cover_last_modified = coalesce(:coverLastModified, cover_last_modified),
|
||||
calculate_interval = coalesce(:calculateInterval, calculate_interval),
|
||||
notes = coalesce(:notes, notes)
|
||||
WHERE _id = :mangaId;
|
||||
|
||||
updateFromBackup:
|
||||
UPDATE mangas SET
|
||||
artist = coalesce(:artist, artist),
|
||||
author = coalesce(:author, author),
|
||||
description = coalesce(:description, description),
|
||||
genre = coalesce(:genre, genre),
|
||||
title = coalesce(:title, title),
|
||||
status = coalesce(:status, status),
|
||||
thumbnail_url = coalesce(:thumbnailUrl, thumbnail_url),
|
||||
favorite_at = :favoriteAt,
|
||||
last_update = coalesce(:lastUpdate, last_update),
|
||||
initialized = coalesce(:initialized, initialized),
|
||||
viewer = coalesce(:viewer, viewer),
|
||||
chapter_flags = coalesce(:chapterFlags, chapter_flags),
|
||||
cover_last_modified = coalesce(:coverLastModified, cover_last_modified),
|
||||
update_strategy = coalesce(:updateStrategy, update_strategy),
|
||||
notes = coalesce(:notes, notes),
|
||||
memo = coalesce(:memo, memo)
|
||||
WHERE _id = :mangaId;
|
||||
|
||||
updateRemote:
|
||||
UPDATE mangas SET
|
||||
artist = coalesce(:artist, artist),
|
||||
author = coalesce(:author, author),
|
||||
description = coalesce(:description, description),
|
||||
genre = coalesce(:genre, genre),
|
||||
title = coalesce(:title, title),
|
||||
status = :status,
|
||||
thumbnail_url = coalesce(:thumbnailUrl, thumbnail_url),
|
||||
initialized = :initialized,
|
||||
cover_last_modified = coalesce(:coverLastModified, cover_last_modified),
|
||||
update_strategy = :updateStrategy,
|
||||
memo = :memo
|
||||
WHERE _id = :mangaId;
|
||||
|
||||
insertNetworkManga {
|
||||
-- Insert the manga if it doesn't exist already
|
||||
INSERT INTO mangas(
|
||||
source, url, artist, author, description, genre, title, status, thumbnail_url, favorite_at,
|
||||
last_update, next_update, initialized, viewer, chapter_flags, cover_last_modified,
|
||||
update_strategy, calculate_interval, memo
|
||||
)
|
||||
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 mangas WHERE source = :source AND url = :url);
|
||||
|
||||
-- Update the relevant details if applicable and not favorite
|
||||
UPDATE mangas
|
||||
SET
|
||||
title = CASE WHEN :updateTitle THEN :title ELSE title END,
|
||||
thumbnail_url = CASE WHEN :updateCover THEN :thumbnailUrl ELSE thumbnail_url END,
|
||||
author = CASE WHEN :updateDetails THEN :author ELSE author END,
|
||||
artist = CASE WHEN :updateDetails THEN :artist ELSE artist END,
|
||||
description = CASE WHEN :updateDetails THEN :description ELSE description END,
|
||||
genre = CASE WHEN :updateDetails THEN :genre ELSE genre END,
|
||||
status = CASE WHEN :updateDetails THEN :status ELSE status END,
|
||||
update_strategy = CASE WHEN :updateDetails THEN :updateStrategy ELSE update_strategy END,
|
||||
initialized = :updateDetails,
|
||||
memo = :memo
|
||||
WHERE source = :source
|
||||
AND url = :url
|
||||
AND favorite_at IS NULL;
|
||||
|
||||
-- Finally return the manga
|
||||
SELECT *
|
||||
FROM mangas
|
||||
WHERE source = :source
|
||||
AND url = :url
|
||||
LIMIT 1;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
CREATE TABLE mangas_categories(
|
||||
_id INTEGER NOT NULL PRIMARY KEY,
|
||||
manga_id INTEGER NOT NULL,
|
||||
category_id INTEGER NOT NULL,
|
||||
FOREIGN KEY(category_id) REFERENCES categories (_id)
|
||||
ON DELETE CASCADE,
|
||||
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_mangas_categories_manga_id ON mangas_categories(manga_id);
|
||||
CREATE INDEX idx_mangas_categories_category_id ON mangas_categories(category_id);
|
||||
|
||||
insert:
|
||||
INSERT INTO mangas_categories(manga_id, category_id)
|
||||
VALUES (:mangaId, :categoryId);
|
||||
|
||||
deleteMangaCategoryByMangaId:
|
||||
DELETE FROM mangas_categories
|
||||
WHERE manga_id = :mangaId;
|
||||
@@ -0,0 +1,24 @@
|
||||
CREATE TABLE source(
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
language TEXT NOT NULL
|
||||
);
|
||||
|
||||
findAll:
|
||||
SELECT id, language, name
|
||||
FROM source;
|
||||
|
||||
findOne:
|
||||
SELECT id, language, name
|
||||
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;
|
||||
@@ -1,24 +0,0 @@
|
||||
CREATE TABLE sources(
|
||||
_id INTEGER NOT NULL PRIMARY KEY,
|
||||
lang TEXT NOT NULL,
|
||||
name TEXT NOT NULL
|
||||
);
|
||||
|
||||
findAll:
|
||||
SELECT *
|
||||
FROM sources;
|
||||
|
||||
findOne:
|
||||
SELECT *
|
||||
FROM sources
|
||||
WHERE _id = :id;
|
||||
|
||||
upsert:
|
||||
INSERT INTO sources(_id, lang, name)
|
||||
VALUES (:id, :lang, :name)
|
||||
ON CONFLICT(_id)
|
||||
DO UPDATE
|
||||
SET
|
||||
lang = :lang,
|
||||
name = :name
|
||||
WHERE _id = :id;
|
||||
@@ -0,0 +1,545 @@
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy;
|
||||
import kotlin.Boolean;
|
||||
import kotlin.String;
|
||||
import kotlin.collections.List;
|
||||
import kotlinx.serialization.json.JsonObject;
|
||||
|
||||
-- ---------------------------------------------------------------------------------------------------------
|
||||
-- table - manga
|
||||
-- ---------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- Every view reads `mangas`, so they are dropped here and recreated against the new table below
|
||||
DROP VIEW historyView;
|
||||
DROP VIEW libraryView;
|
||||
DROP VIEW updatesView;
|
||||
|
||||
-- Entries sharing a source and url are merged into one, since the new table won't hold both. The copies are
|
||||
-- ranked with the ones in the library first, since that is the entry the user has, then by age. The first
|
||||
-- one stays, takes each detail it is missing from the first copy that has it, and takes over everything
|
||||
-- attached to the others.
|
||||
CREATE TEMP TABLE duplicate_mangas AS
|
||||
SELECT
|
||||
_id,
|
||||
first_value(_id) OVER (PARTITION BY source, url ORDER BY favorite_at IS NOT NULL DESC, _id ASC) AS keep_id,
|
||||
row_number() OVER (PARTITION BY source, url ORDER BY favorite_at IS NOT NULL DESC, _id ASC) AS precedence,
|
||||
favorite_at IS NOT NULL AS favorite
|
||||
FROM mangas
|
||||
WHERE (source, url) IN (
|
||||
SELECT source, url
|
||||
FROM mangas
|
||||
GROUP BY source, url
|
||||
HAVING count(*) > 1
|
||||
);
|
||||
|
||||
UPDATE mangas
|
||||
SET favorite_at = merged.favorite_at,
|
||||
initialized = merged.initialized,
|
||||
artist = merged.artist,
|
||||
author = merged.author,
|
||||
description = merged.description,
|
||||
genre = merged.genre,
|
||||
thumbnail_url = merged.thumbnail_url,
|
||||
status = merged.status,
|
||||
viewer = merged.viewer,
|
||||
chapter_flags = merged.chapter_flags,
|
||||
notes = merged.notes,
|
||||
memo = merged.memo,
|
||||
last_update = merged.last_update,
|
||||
cover_last_modified = merged.cover_last_modified
|
||||
FROM (
|
||||
SELECT DISTINCT
|
||||
D.keep_id,
|
||||
-- The earliest real date if any copy has one, else 0 if any copy is in the library, else NULL
|
||||
coalesce(
|
||||
min(nullif(M.favorite_at, 0)) OVER (PARTITION BY D.keep_id),
|
||||
min(M.favorite_at) OVER (PARTITION BY D.keep_id)
|
||||
) AS favorite_at,
|
||||
max(M.initialized) OVER (PARTITION BY D.keep_id) AS initialized,
|
||||
first_value(M.artist) OVER (PARTITION BY D.keep_id ORDER BY M.artist IS NULL, D.precedence) AS artist,
|
||||
first_value(M.author) OVER (PARTITION BY D.keep_id ORDER BY M.author IS NULL, D.precedence) AS author,
|
||||
first_value(M.description) OVER (PARTITION BY D.keep_id ORDER BY M.description IS NULL, D.precedence) AS description,
|
||||
first_value(M.genre) OVER (PARTITION BY D.keep_id ORDER BY M.genre IS NULL, D.precedence) AS genre,
|
||||
first_value(M.thumbnail_url) OVER (PARTITION BY D.keep_id ORDER BY M.thumbnail_url IS NULL, D.precedence) AS thumbnail_url,
|
||||
first_value(M.status) OVER (PARTITION BY D.keep_id ORDER BY M.status = 0, D.precedence) AS status,
|
||||
first_value(M.viewer) OVER (PARTITION BY D.keep_id ORDER BY M.viewer = 0, D.precedence) AS viewer,
|
||||
first_value(M.chapter_flags) OVER (PARTITION BY D.keep_id ORDER BY M.chapter_flags = 0, D.precedence) AS chapter_flags,
|
||||
first_value(M.notes) OVER (PARTITION BY D.keep_id ORDER BY M.notes = '', D.precedence) AS notes,
|
||||
first_value(M.memo) OVER (PARTITION BY D.keep_id ORDER BY M.memo = '{}', D.precedence) AS memo,
|
||||
max(M.last_update) OVER (PARTITION BY D.keep_id) AS last_update,
|
||||
max(M.cover_last_modified) OVER (PARTITION BY D.keep_id) AS cover_last_modified
|
||||
FROM duplicate_mangas D
|
||||
JOIN mangas M
|
||||
ON M._id = D._id
|
||||
) AS merged
|
||||
WHERE mangas._id = merged.keep_id;
|
||||
|
||||
-- Every child table is rebuilt further down and resolves the surviving entry there, so nothing
|
||||
-- else has to be rewritten here first.
|
||||
DELETE FROM mangas
|
||||
WHERE _id IN (SELECT _id FROM duplicate_mangas WHERE _id != keep_id);
|
||||
|
||||
CREATE TABLE manga(
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
source_id INTEGER NOT NULL,
|
||||
remote_url TEXT NOT NULL,
|
||||
remote_title TEXT NOT NULL,
|
||||
remote_author TEXT,
|
||||
remote_artist TEXT,
|
||||
remote_description TEXT,
|
||||
remote_genre TEXT AS List<String>,
|
||||
remote_status INTEGER NOT NULL,
|
||||
remote_cover TEXT,
|
||||
remote_update_strategy INTEGER AS UpdateStrategy NOT NULL,
|
||||
remote_memo BLOB AS JsonObject NOT NULL,
|
||||
user_favorite_at INTEGER,
|
||||
user_notes TEXT NOT NULL,
|
||||
user_reader_flags INTEGER NOT NULL,
|
||||
user_chapter_flags INTEGER NOT NULL,
|
||||
state_chapter_last_update INTEGER,
|
||||
state_chapter_next_update INTEGER,
|
||||
state_chapter_fetch_interval INTEGER NOT NULL,
|
||||
state_cover_last_modified INTEGER NOT NULL,
|
||||
state_initialized INTEGER AS Boolean NOT NULL,
|
||||
UNIQUE(source_id, remote_url)
|
||||
);
|
||||
|
||||
INSERT INTO manga(
|
||||
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
|
||||
)
|
||||
SELECT
|
||||
_id, source, url, title, author, artist, description, genre,
|
||||
status, thumbnail_url, update_strategy, memo,
|
||||
favorite_at, notes,
|
||||
viewer, chapter_flags, last_update, next_update, calculate_interval,
|
||||
cover_last_modified, initialized
|
||||
FROM mangas;
|
||||
|
||||
DROP TABLE mangas;
|
||||
|
||||
CREATE INDEX manga_user_favorite_at_index ON manga(user_favorite_at) WHERE user_favorite_at IS NOT NULL;
|
||||
|
||||
-- ---------------------------------------------------------------------------------------------------------
|
||||
-- table - chapter
|
||||
-- ---------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- The entries that merged in the previous group brought their chapters with them
|
||||
UPDATE chapters
|
||||
SET manga_id = (SELECT keep_id FROM duplicate_mangas WHERE _id = chapters.manga_id)
|
||||
WHERE manga_id IN (SELECT _id FROM duplicate_mangas WHERE _id != keep_id);
|
||||
|
||||
-- Moving the chapters over brings rows for the same chapter together, and duplicates like those are
|
||||
-- what issue #647 is about in the first place. The one with reading history is kept, the oldest
|
||||
-- otherwise, and the read state and history of the others are merged into it.
|
||||
CREATE TEMP TABLE duplicate_chapters AS
|
||||
SELECT
|
||||
_id,
|
||||
first_value(_id) OVER (PARTITION BY manga_id, url ORDER BY has_history DESC, _id ASC) AS keep_id
|
||||
FROM (
|
||||
SELECT
|
||||
_id,
|
||||
manga_id,
|
||||
url,
|
||||
EXISTS(SELECT 1 FROM history WHERE history.chapter_id = chapters._id) AS has_history
|
||||
FROM chapters
|
||||
WHERE (manga_id, url) IN (
|
||||
SELECT manga_id, url
|
||||
FROM chapters
|
||||
GROUP BY manga_id, url
|
||||
HAVING count(*) > 1
|
||||
)
|
||||
);
|
||||
|
||||
UPDATE chapters
|
||||
SET read = merged.read,
|
||||
bookmark = merged.bookmark,
|
||||
last_page_read = merged.last_page_read,
|
||||
date_fetch = merged.date_fetch,
|
||||
date_upload = merged.date_upload
|
||||
FROM (
|
||||
SELECT
|
||||
D.keep_id,
|
||||
max(C.read) AS read,
|
||||
max(C.bookmark) AS bookmark,
|
||||
max(C.last_page_read) AS last_page_read,
|
||||
-- 0 means the fetch date was never recorded rather than "the beginning of time", so it only
|
||||
-- wins when no copy recorded one. Updates only lists chapters fetched after the entry joined
|
||||
-- the library, so letting a 0 through would hide the merged chapter from it.
|
||||
coalesce(min(nullif(C.date_fetch, 0)), 0) AS date_fetch,
|
||||
max(C.date_upload) AS date_upload
|
||||
FROM duplicate_chapters D
|
||||
JOIN chapters C
|
||||
ON C._id = D._id
|
||||
GROUP BY D.keep_id
|
||||
) AS merged
|
||||
WHERE chapters._id = merged.keep_id;
|
||||
|
||||
-- The kept chapter has a history entry whenever any of its duplicates has one
|
||||
UPDATE history
|
||||
SET last_read = merged.last_read,
|
||||
time_read = merged.time_read
|
||||
FROM (
|
||||
SELECT
|
||||
D.keep_id,
|
||||
max(H.last_read) AS last_read,
|
||||
-- Each duplicate accumulated its own reading time, so the durations add up; only the
|
||||
-- last-read timestamp is a point in time and takes the latest.
|
||||
sum(H.time_read) AS time_read
|
||||
FROM duplicate_chapters D
|
||||
JOIN history H
|
||||
ON H.chapter_id = D._id
|
||||
GROUP BY D.keep_id
|
||||
) AS merged
|
||||
WHERE history.chapter_id = merged.keep_id;
|
||||
|
||||
-- Foreign keys are off during migrations, so the history of a discarded chapter goes explicitly
|
||||
DELETE FROM history
|
||||
WHERE chapter_id IN (SELECT _id FROM duplicate_chapters WHERE _id != keep_id);
|
||||
|
||||
DELETE FROM chapters
|
||||
WHERE _id IN (SELECT _id FROM duplicate_chapters WHERE _id != keep_id);
|
||||
|
||||
DROP TABLE duplicate_chapters;
|
||||
|
||||
CREATE TABLE chapter(
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
manga_id INTEGER NOT NULL,
|
||||
remote_url TEXT NOT NULL,
|
||||
remote_name TEXT NOT NULL,
|
||||
remote_scanlator TEXT,
|
||||
remote_chapter_number REAL NOT NULL,
|
||||
remote_date_upload INTEGER NOT NULL,
|
||||
remote_order INTEGER NOT NULL,
|
||||
remote_memo BLOB NOT NULL,
|
||||
user_read INTEGER NOT NULL,
|
||||
user_bookmark INTEGER NOT NULL,
|
||||
user_last_page_read INTEGER NOT NULL,
|
||||
state_date_fetch INTEGER NOT NULL,
|
||||
UNIQUE(manga_id, remote_url),
|
||||
FOREIGN KEY(manga_id) REFERENCES manga (id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO chapter(
|
||||
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
|
||||
)
|
||||
SELECT
|
||||
_id, manga_id, url, name, scanlator, chapter_number, date_upload,
|
||||
source_order, memo, read, bookmark, last_page_read, date_fetch
|
||||
FROM chapters;
|
||||
|
||||
DROP TABLE chapters;
|
||||
|
||||
CREATE INDEX chapter_state_date_fetch_index ON chapter(state_date_fetch);
|
||||
CREATE INDEX chapter_unread_state_date_fetch_index ON chapter(state_date_fetch) WHERE user_read = 0;
|
||||
|
||||
-- ---------------------------------------------------------------------------------------------------------
|
||||
-- table - category
|
||||
-- ---------------------------------------------------------------------------------------------------------
|
||||
|
||||
CREATE TABLE category(
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
`order` INTEGER NOT NULL UNIQUE,
|
||||
flags INTEGER NOT NULL
|
||||
);
|
||||
|
||||
-- `sort` was never unique, and reordering wrote colliding values often enough that it cannot simply be
|
||||
-- copied under the new constraint. The rows keep the order they were displayed in and are renumbered from
|
||||
-- 0, with ties broken by id. The system category is pinned to -1 rather than renumbered, because it is
|
||||
-- identified by id 0 everywhere else and sorts ahead of the categories the user made.
|
||||
INSERT INTO category(id, name, `order`, flags)
|
||||
SELECT
|
||||
_id,
|
||||
name,
|
||||
CASE
|
||||
WHEN _id <= 0 THEN -1
|
||||
ELSE row_number() OVER (PARTITION BY (_id <= 0) ORDER BY sort, _id) - 1
|
||||
END,
|
||||
flags
|
||||
FROM categories;
|
||||
|
||||
-- Dropping the table takes `system_category_delete_trigger` with it
|
||||
DROP TABLE categories;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS system_category_delete_trigger BEFORE DELETE
|
||||
ON category
|
||||
BEGIN SELECT CASE
|
||||
WHEN old.id <= 0 THEN
|
||||
RAISE(ABORT, "System category can't be deleted")
|
||||
END;
|
||||
END;
|
||||
|
||||
-- ---------------------------------------------------------------------------------------------------------
|
||||
-- table - manga_category
|
||||
-- ---------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- The surrogate key is gone: a manga belongs to a category once, and that pair is the identity of the row
|
||||
CREATE TABLE manga_category(
|
||||
manga_id INTEGER NOT NULL,
|
||||
category_id INTEGER NOT NULL,
|
||||
UNIQUE(manga_id, category_id),
|
||||
FOREIGN KEY(category_id) REFERENCES category (id)
|
||||
ON DELETE CASCADE,
|
||||
FOREIGN KEY(manga_id) REFERENCES manga (id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Entries merged in the manga group brought their category rows with them. When the entry is in the library,
|
||||
-- only the copies in it say where it belongs; a copy outside it can still hold the categories it had before
|
||||
-- it was removed. The same pair can appear twice by now, and membership carries nothing else, so the copies
|
||||
-- simply collapse.
|
||||
INSERT INTO manga_category(manga_id, category_id)
|
||||
SELECT DISTINCT coalesce(D.keep_id, MC.manga_id), MC.category_id
|
||||
FROM mangas_categories MC
|
||||
LEFT JOIN duplicate_mangas D
|
||||
ON D._id = MC.manga_id
|
||||
LEFT JOIN manga K
|
||||
ON K.id = D.keep_id
|
||||
WHERE D._id IS NULL OR D.favorite OR K.user_favorite_at IS NULL;
|
||||
|
||||
DROP TABLE mangas_categories;
|
||||
|
||||
CREATE INDEX manga_category_category_id_index ON manga_category(category_id);
|
||||
|
||||
-- ---------------------------------------------------------------------------------------------------------
|
||||
-- table - history
|
||||
-- ---------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- `last_read` is when the chapter was last read and `time_read` is how long it has been read for, so
|
||||
-- both are renamed to say so. The chapter it belongs to is already unique, which leaves
|
||||
-- `history_history_chapter_id_index` a prefix of that constraint.
|
||||
-- The table keeps its name, so the original is renamed aside and the new one built under
|
||||
-- the real name; renaming a table into place makes SQLite rewrite its stored definition,
|
||||
-- leaving it spelled differently from the same table on a fresh install. The views were
|
||||
-- dropped at the top of this migration, so nothing refers to the name being moved.
|
||||
ALTER TABLE history RENAME TO history_temp;
|
||||
|
||||
CREATE TABLE history(
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
manga_id INTEGER NOT NULL,
|
||||
chapter_id INTEGER NOT NULL UNIQUE,
|
||||
read_at INTEGER,
|
||||
read_duration INTEGER NOT NULL,
|
||||
FOREIGN KEY(manga_id) REFERENCES manga (id)
|
||||
ON DELETE CASCADE,
|
||||
FOREIGN KEY(chapter_id) REFERENCES chapter (id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Reaching the entry a row belongs to meant going through its chapter; it names the entry
|
||||
-- itself now, taken from that same chapter. A row whose chapter is already gone cannot name
|
||||
-- one and is dropped, which is what the foreign key would do to it once it is enforced again.
|
||||
INSERT INTO history(id, manga_id, chapter_id, read_at, read_duration)
|
||||
SELECT H._id, C.manga_id, H.chapter_id, H.last_read, H.time_read
|
||||
FROM history_temp H
|
||||
JOIN chapter C
|
||||
ON C.id = H.chapter_id;
|
||||
|
||||
DROP TABLE history_temp;
|
||||
|
||||
CREATE INDEX history_manga_id_index ON history(manga_id);
|
||||
CREATE INDEX history_read_at_index ON history(read_at);
|
||||
|
||||
-- ---------------------------------------------------------------------------------------------------------
|
||||
-- table - excluded_scanlator
|
||||
-- ---------------------------------------------------------------------------------------------------------
|
||||
|
||||
CREATE TABLE excluded_scanlator(
|
||||
manga_id INTEGER NOT NULL,
|
||||
scanlator TEXT NOT NULL,
|
||||
UNIQUE(manga_id, scanlator),
|
||||
FOREIGN KEY(manga_id) REFERENCES manga (id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Excluding a scanlator twice excludes it once, and merged entries brought their exclusions along, so
|
||||
-- the copies collapse. `excluded_scanlators_manga_id_index` is a prefix of the new constraint.
|
||||
INSERT INTO excluded_scanlator(manga_id, scanlator)
|
||||
SELECT DISTINCT coalesce(D.keep_id, ES.manga_id), ES.scanlator
|
||||
FROM excluded_scanlators ES
|
||||
LEFT JOIN duplicate_mangas D
|
||||
ON D._id = ES.manga_id;
|
||||
|
||||
DROP TABLE excluded_scanlators;
|
||||
|
||||
|
||||
-- ---------------------------------------------------------------------------------------------------------
|
||||
-- table - manga_track
|
||||
-- ---------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- `manga_sync` is what tracking was called when it was still called syncing; `sync_id` is the tracker.
|
||||
-- An entry that merged can leave two rows for the same tracker on the survivor, which the unique constraint
|
||||
-- rejects, so the one to keep is chosen first: one on a copy in the library, then the one furthest read, then
|
||||
-- the one on the copy ranked first, then the oldest. A track is taken whole rather than merged field by field,
|
||||
-- because two rows on the same tracker can point at different remote entries.
|
||||
CREATE TEMP TABLE duplicate_tracks AS
|
||||
SELECT
|
||||
T._id,
|
||||
first_value(T._id) OVER (
|
||||
PARTITION BY D.keep_id, T.sync_id
|
||||
ORDER BY D.favorite DESC, T.last_chapter_read DESC, D.precedence ASC, T._id ASC
|
||||
) AS keep_track_id
|
||||
FROM manga_sync T
|
||||
JOIN duplicate_mangas D
|
||||
ON D._id = T.manga_id;
|
||||
|
||||
DELETE FROM manga_sync
|
||||
WHERE _id IN (SELECT _id FROM duplicate_tracks WHERE _id != keep_track_id);
|
||||
|
||||
DROP TABLE duplicate_tracks;
|
||||
|
||||
CREATE TABLE manga_track(
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
manga_id INTEGER NOT NULL,
|
||||
tracker_id INTEGER NOT NULL,
|
||||
remote_id INTEGER NOT NULL,
|
||||
remote_url TEXT NOT NULL,
|
||||
library_id INTEGER,
|
||||
title TEXT NOT NULL,
|
||||
last_chapter_read REAL NOT NULL,
|
||||
total_chapters INTEGER NOT NULL,
|
||||
status INTEGER NOT NULL,
|
||||
score REAL NOT NULL,
|
||||
start_date INTEGER NOT NULL,
|
||||
finish_date INTEGER NOT NULL,
|
||||
private INTEGER NOT NULL DEFAULT 0,
|
||||
UNIQUE (manga_id, tracker_id),
|
||||
FOREIGN KEY(manga_id) REFERENCES manga (id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO manga_track(
|
||||
id, manga_id, tracker_id, remote_id, remote_url, library_id, title, last_chapter_read,
|
||||
total_chapters, status, score, start_date, finish_date, private
|
||||
)
|
||||
SELECT
|
||||
T._id, coalesce(D.keep_id, T.manga_id), T.sync_id, T.remote_id, T.remote_url, T.library_id,
|
||||
T.title, T.last_chapter_read, T.total_chapters, T.status, T.score, T.start_date,
|
||||
T.finish_date, T.private
|
||||
FROM manga_sync T
|
||||
LEFT JOIN duplicate_mangas D
|
||||
ON D._id = T.manga_id;
|
||||
|
||||
DROP TABLE duplicate_mangas;
|
||||
|
||||
-- `idx_manga_sync_manga_id` is a prefix of the unique constraint
|
||||
DROP TABLE manga_sync;
|
||||
|
||||
CREATE INDEX manga_track_tracker_id_remote_id_index ON manga_track(tracker_id, remote_id);
|
||||
|
||||
-- ---------------------------------------------------------------------------------------------------------
|
||||
-- table - source
|
||||
-- ---------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- The last of the plural table names, and `lang` spelled out. Nothing references this table, so it is a
|
||||
-- straight copy; name comes before language to read the way the row does.
|
||||
CREATE TABLE source(
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
language TEXT NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO source(id, name, language)
|
||||
SELECT _id, name, lang
|
||||
FROM sources;
|
||||
|
||||
DROP TABLE sources;
|
||||
|
||||
-- The views come back reading the new table
|
||||
CREATE VIEW historyView AS
|
||||
SELECT
|
||||
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,
|
||||
max_read.read_at AS maxReadAt,
|
||||
max_read.chapter_id AS maxReadAtChapterId
|
||||
FROM manga
|
||||
JOIN chapter
|
||||
ON manga.id = chapter.manga_id
|
||||
JOIN history
|
||||
ON chapter.id = history.chapter_id
|
||||
JOIN (
|
||||
SELECT manga_id, chapter_id, MAX(read_at) AS read_at
|
||||
FROM history
|
||||
GROUP BY manga_id
|
||||
) AS max_read
|
||||
ON chapter.manga_id = max_read.manga_id;
|
||||
|
||||
CREATE VIEW libraryView AS
|
||||
SELECT
|
||||
M.*,
|
||||
coalesce(C.total, 0) AS totalCount,
|
||||
coalesce(C.readCount, 0) AS readCount,
|
||||
coalesce(C.latestUpload, 0) AS latestUpload,
|
||||
coalesce(C.fetchedAt, 0) AS chapterFetchedAt,
|
||||
coalesce(C.lastRead, 0) AS lastRead,
|
||||
coalesce(C.bookmarkCount, 0) AS bookmarkCount,
|
||||
coalesce(MC.categories, '0') AS categories
|
||||
FROM manga M
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
chapter.manga_id,
|
||||
count(*) AS total,
|
||||
sum(user_read) AS readCount,
|
||||
coalesce(max(chapter.remote_date_upload), 0) AS latestUpload,
|
||||
coalesce(max(history.read_at), 0) AS lastRead,
|
||||
coalesce(max(chapter.state_date_fetch), 0) AS fetchedAt,
|
||||
sum(chapter.user_bookmark) AS bookmarkCount
|
||||
FROM chapter
|
||||
LEFT JOIN excluded_scanlator
|
||||
ON chapter.manga_id = excluded_scanlator.manga_id
|
||||
AND chapter.remote_scanlator = excluded_scanlator.scanlator
|
||||
LEFT JOIN history
|
||||
ON chapter.id = history.chapter_id
|
||||
WHERE excluded_scanlator.scanlator IS NULL
|
||||
AND chapter.manga_id IN (SELECT id FROM manga WHERE user_favorite_at IS NOT NULL)
|
||||
GROUP BY chapter.manga_id
|
||||
) AS C
|
||||
ON M.id = C.manga_id
|
||||
LEFT JOIN (
|
||||
SELECT manga_id, group_concat(category_id) AS categories
|
||||
FROM manga_category
|
||||
WHERE manga_id IN (SELECT id FROM manga WHERE user_favorite_at IS NOT NULL)
|
||||
GROUP BY manga_id
|
||||
) AS MC
|
||||
ON MC.manga_id = M.id
|
||||
WHERE M.user_favorite_at IS NOT NULL;
|
||||
|
||||
CREATE VIEW updatesView AS
|
||||
SELECT
|
||||
manga.id AS mangaId,
|
||||
manga.remote_title AS mangaTitle,
|
||||
chapter.id AS chapterId,
|
||||
chapter.remote_name AS chapterName,
|
||||
chapter.remote_scanlator AS scanlator,
|
||||
chapter.remote_url AS chapterUrl,
|
||||
chapter.user_read AS read,
|
||||
chapter.user_bookmark AS bookmark,
|
||||
chapter.user_last_page_read AS lastPageRead,
|
||||
manga.source_id AS sourceId,
|
||||
manga.user_favorite_at IS NOT NULL AS favorite,
|
||||
manga.remote_cover AS thumbnailUrl,
|
||||
manga.state_cover_last_modified AS coverLastModified,
|
||||
chapter.remote_date_upload AS dateUpload,
|
||||
chapter.state_date_fetch AS dateFetch,
|
||||
excluded_scanlator.scanlator AS excludedScanlator
|
||||
FROM manga JOIN chapter
|
||||
ON manga.id = chapter.manga_id
|
||||
LEFT JOIN excluded_scanlator
|
||||
ON manga.id = excluded_scanlator.manga_id
|
||||
AND chapter.remote_scanlator = excluded_scanlator.scanlator
|
||||
WHERE user_favorite_at IS NOT NULL
|
||||
AND state_date_fetch > user_favorite_at
|
||||
ORDER BY state_date_fetch DESC;
|
||||
@@ -1,30 +1,29 @@
|
||||
CREATE VIEW historyView AS
|
||||
SELECT
|
||||
history._id AS id,
|
||||
mangas._id AS mangaId,
|
||||
chapters._id AS chapterId,
|
||||
mangas.title,
|
||||
mangas.thumbnail_url AS thumbnailUrl,
|
||||
mangas.source,
|
||||
mangas.favorite_at IS NOT NULL AS favorite,
|
||||
mangas.cover_last_modified,
|
||||
chapters.chapter_number AS chapterNumber,
|
||||
history.last_read AS readAt,
|
||||
history.time_read AS readDuration,
|
||||
max_last_read.last_read AS maxReadAt,
|
||||
max_last_read.chapter_id AS maxReadAtChapterId
|
||||
FROM mangas
|
||||
JOIN chapters
|
||||
ON mangas._id = chapters.manga_id
|
||||
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,
|
||||
max_read.read_at AS maxReadAt,
|
||||
max_read.chapter_id AS maxReadAtChapterId
|
||||
FROM manga
|
||||
JOIN chapter
|
||||
ON manga.id = chapter.manga_id
|
||||
JOIN history
|
||||
ON chapters._id = history.chapter_id
|
||||
ON chapter.id = history.chapter_id
|
||||
JOIN (
|
||||
SELECT chapters.manga_id,chapters._id AS chapter_id, MAX(history.last_read) AS last_read
|
||||
FROM chapters JOIN history
|
||||
ON chapters._id = history.chapter_id
|
||||
GROUP BY chapters.manga_id
|
||||
) AS max_last_read
|
||||
ON chapters.manga_id = max_last_read.manga_id;
|
||||
SELECT manga_id, chapter_id, MAX(read_at) AS read_at
|
||||
FROM history
|
||||
GROUP BY manga_id
|
||||
) AS max_read
|
||||
ON chapter.manga_id = max_read.manga_id;
|
||||
|
||||
history:
|
||||
SELECT
|
||||
@@ -33,9 +32,9 @@ mangaId,
|
||||
chapterId,
|
||||
title,
|
||||
thumbnailUrl,
|
||||
source,
|
||||
sourceId,
|
||||
favorite,
|
||||
cover_last_modified,
|
||||
coverLastModified,
|
||||
chapterNumber,
|
||||
readAt,
|
||||
readDuration
|
||||
@@ -52,9 +51,9 @@ mangaId,
|
||||
chapterId,
|
||||
title,
|
||||
thumbnailUrl,
|
||||
source,
|
||||
sourceId,
|
||||
favorite,
|
||||
cover_last_modified,
|
||||
coverLastModified,
|
||||
chapterNumber,
|
||||
readAt,
|
||||
readDuration
|
||||
|
||||
@@ -8,34 +8,43 @@ SELECT
|
||||
coalesce(C.lastRead, 0) AS lastRead,
|
||||
coalesce(C.bookmarkCount, 0) AS bookmarkCount,
|
||||
coalesce(MC.categories, '0') AS categories
|
||||
FROM mangas M
|
||||
FROM manga M
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
chapters.manga_id,
|
||||
chapter.manga_id,
|
||||
count(*) AS total,
|
||||
sum(read) AS readCount,
|
||||
coalesce(max(chapters.date_upload), 0) AS latestUpload,
|
||||
coalesce(max(history.last_read), 0) AS lastRead,
|
||||
coalesce(max(chapters.date_fetch), 0) AS fetchedAt,
|
||||
sum(chapters.bookmark) AS bookmarkCount
|
||||
FROM chapters
|
||||
LEFT JOIN excluded_scanlators
|
||||
ON chapters.manga_id = excluded_scanlators.manga_id
|
||||
AND chapters.scanlator = excluded_scanlators.scanlator
|
||||
sum(user_read) AS readCount,
|
||||
coalesce(max(chapter.remote_date_upload), 0) AS latestUpload,
|
||||
coalesce(max(history.read_at), 0) AS lastRead,
|
||||
coalesce(max(chapter.state_date_fetch), 0) AS fetchedAt,
|
||||
sum(chapter.user_bookmark) AS bookmarkCount
|
||||
FROM chapter
|
||||
LEFT JOIN excluded_scanlator
|
||||
ON chapter.manga_id = excluded_scanlator.manga_id
|
||||
AND chapter.remote_scanlator = excluded_scanlator.scanlator
|
||||
LEFT JOIN history
|
||||
ON chapters._id = history.chapter_id
|
||||
WHERE excluded_scanlators.scanlator IS NULL
|
||||
GROUP BY chapters.manga_id
|
||||
ON chapter.id = history.chapter_id
|
||||
WHERE excluded_scanlator.scanlator IS NULL
|
||||
AND chapter.manga_id IN (SELECT id FROM manga WHERE user_favorite_at IS NOT NULL)
|
||||
GROUP BY chapter.manga_id
|
||||
) AS C
|
||||
ON M._id = C.manga_id
|
||||
ON M.id = C.manga_id
|
||||
LEFT JOIN (
|
||||
SELECT manga_id, group_concat(category_id) AS categories
|
||||
FROM mangas_categories
|
||||
FROM manga_category
|
||||
WHERE manga_id IN (SELECT id FROM manga WHERE user_favorite_at IS NOT NULL)
|
||||
GROUP BY manga_id
|
||||
) AS MC
|
||||
ON MC.manga_id = M._id
|
||||
WHERE M.favorite_at IS NOT NULL;
|
||||
ON MC.manga_id = M.id
|
||||
WHERE M.user_favorite_at IS NOT NULL;
|
||||
|
||||
library:
|
||||
SELECT *
|
||||
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
|
||||
FROM libraryView;
|
||||
|
||||
@@ -1,47 +1,55 @@
|
||||
CREATE VIEW updatesView AS
|
||||
SELECT
|
||||
mangas._id AS mangaId,
|
||||
mangas.title AS mangaTitle,
|
||||
chapters._id AS chapterId,
|
||||
chapters.name AS chapterName,
|
||||
chapters.scanlator,
|
||||
chapters.url AS chapterUrl,
|
||||
chapters.read,
|
||||
chapters.bookmark,
|
||||
chapters.last_page_read,
|
||||
mangas.source,
|
||||
mangas.favorite_at IS NOT NULL AS favorite,
|
||||
mangas.thumbnail_url AS thumbnailUrl,
|
||||
mangas.cover_last_modified AS coverLastModified,
|
||||
chapters.date_upload AS dateUpload,
|
||||
chapters.date_fetch AS datefetch,
|
||||
excluded_scanlators.scanlator AS excludedScanlator
|
||||
FROM mangas JOIN chapters
|
||||
ON mangas._id = chapters.manga_id
|
||||
LEFT JOIN excluded_scanlators
|
||||
ON mangas._id = excluded_scanlators.manga_id
|
||||
AND chapters.scanlator = excluded_scanlators.scanlator
|
||||
WHERE favorite_at IS NOT NULL
|
||||
AND date_fetch > favorite_at
|
||||
ORDER BY date_fetch DESC;
|
||||
manga.id AS mangaId,
|
||||
manga.remote_title AS mangaTitle,
|
||||
chapter.id AS chapterId,
|
||||
chapter.remote_name AS chapterName,
|
||||
chapter.remote_scanlator AS scanlator,
|
||||
chapter.remote_url AS chapterUrl,
|
||||
chapter.user_read AS read,
|
||||
chapter.user_bookmark AS bookmark,
|
||||
chapter.user_last_page_read AS lastPageRead,
|
||||
manga.source_id AS sourceId,
|
||||
manga.user_favorite_at IS NOT NULL AS favorite,
|
||||
manga.remote_cover AS thumbnailUrl,
|
||||
manga.state_cover_last_modified AS coverLastModified,
|
||||
chapter.remote_date_upload AS dateUpload,
|
||||
chapter.state_date_fetch AS dateFetch,
|
||||
excluded_scanlator.scanlator AS excludedScanlator
|
||||
FROM manga JOIN chapter
|
||||
ON manga.id = chapter.manga_id
|
||||
LEFT JOIN excluded_scanlator
|
||||
ON manga.id = excluded_scanlator.manga_id
|
||||
AND chapter.remote_scanlator = excluded_scanlator.scanlator
|
||||
WHERE user_favorite_at IS NOT NULL
|
||||
AND state_date_fetch > user_favorite_at
|
||||
ORDER BY state_date_fetch DESC;
|
||||
|
||||
getRecentUpdates:
|
||||
SELECT *
|
||||
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
|
||||
ORDER BY dateFetch DESC
|
||||
LIMIT :limit;
|
||||
|
||||
getRecentUpdatesWithFilters:
|
||||
SELECT *
|
||||
SELECT
|
||||
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)
|
||||
-- Started means some progress but not finished, Read means finished chapter, thus:
|
||||
AND (
|
||||
:started IS NULL
|
||||
OR (:started = 1 AND last_page_read > 0 AND read = 0)
|
||||
OR (:started = 0 AND last_page_read = 0 AND read = 0)
|
||||
OR (:started = 1 AND lastPageRead > 0 AND read = 0)
|
||||
OR (:started = 0 AND lastPageRead = 0 AND read = 0)
|
||||
)
|
||||
AND (:bookmarked IS NULL OR bookmark = :bookmarked)
|
||||
AND (
|
||||
@@ -51,12 +59,12 @@ AND (
|
||||
-- includedEmpty being true expresses "don't care" state and bypasses the membership filter
|
||||
:includedEmpty
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM mangas_categories
|
||||
WHERE mangas_categories.manga_id = mangaId
|
||||
AND COALESCE(mangas_categories.category_id, 0) IN :includedCategories
|
||||
SELECT 1 FROM manga_category
|
||||
WHERE manga_category.manga_id = mangaId
|
||||
AND COALESCE(manga_category.category_id, 0) IN :includedCategories
|
||||
)
|
||||
OR (0 IN :includedCategories AND NOT EXISTS (
|
||||
SELECT 1 FROM mangas_categories mc WHERE mc.manga_id = mangaId)
|
||||
SELECT 1 FROM manga_category mc WHERE mc.manga_id = mangaId)
|
||||
)
|
||||
)
|
||||
AND (
|
||||
@@ -64,23 +72,27 @@ AND (
|
||||
:excludedEmpty
|
||||
OR (
|
||||
NOT EXISTS (
|
||||
SELECT 1 FROM mangas_categories
|
||||
WHERE mangas_categories.manga_id = mangaId
|
||||
AND COALESCE(mangas_categories.category_id, 0) IN :excludedCategories
|
||||
SELECT 1 FROM manga_category
|
||||
WHERE manga_category.manga_id = mangaId
|
||||
AND COALESCE(manga_category.category_id, 0) IN :excludedCategories
|
||||
)
|
||||
AND (
|
||||
0 NOT IN :excludedCategories
|
||||
OR EXISTS (SELECT 1 FROM mangas_categories WHERE mangas_categories.manga_id = mangaId)
|
||||
OR EXISTS (SELECT 1 FROM manga_category WHERE manga_category.manga_id = mangaId)
|
||||
)
|
||||
)
|
||||
)
|
||||
ORDER BY datefetch DESC
|
||||
ORDER BY dateFetch DESC
|
||||
LIMIT :limit;
|
||||
|
||||
getUpdatesByReadStatus:
|
||||
SELECT *
|
||||
SELECT
|
||||
mangaId, mangaTitle, chapterId, chapterName,
|
||||
scanlator, chapterUrl, read, bookmark,
|
||||
lastPageRead, sourceId, favorite, thumbnailUrl,
|
||||
coverLastModified, dateUpload, dateFetch, excludedScanlator
|
||||
FROM updatesView
|
||||
WHERE read = :read
|
||||
AND dateUpload > :after
|
||||
ORDER BY datefetch DESC
|
||||
ORDER BY dateFetch DESC
|
||||
LIMIT :limit;
|
||||
|
||||
Reference in New Issue
Block a user