From b33069f107dcdacfc952e7d1b8515cb3606dea28 Mon Sep 17 00:00:00 2001 From: Zeedif Date: Wed, 17 Jun 2026 20:39:30 -0600 Subject: [PATCH] fix(opds): resolve sql group by syntax error when filtering library (#2118) --- .../opds/repository/MangaRepository.kt | 87 ++++++++++++------- .../opds/repository/NavigationRepository.kt | 45 +++++++--- 2 files changed, 86 insertions(+), 46 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/opds/repository/MangaRepository.kt b/server/src/main/kotlin/suwayomi/tachidesk/opds/repository/MangaRepository.kt index 2e8245fcb..a5ae49560 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/opds/repository/MangaRepository.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/opds/repository/MangaRepository.kt @@ -11,6 +11,7 @@ import org.jetbrains.exposed.v1.core.and import org.jetbrains.exposed.v1.core.eq import org.jetbrains.exposed.v1.core.greater import org.jetbrains.exposed.v1.core.inList +import org.jetbrains.exposed.v1.core.inSubQuery import org.jetbrains.exposed.v1.core.intLiteral import org.jetbrains.exposed.v1.core.like import org.jetbrains.exposed.v1.core.lowerCase @@ -75,13 +76,28 @@ fun Query.applyOpdsMangaFilter( } if (excludeField != "filter") { criteria.filter?.let { filterVal -> - val unreadCountExpr = Case().When(ChapterTable.isRead eq false, intLiteral(1)).Else(intLiteral(0)).sum() - val downloadedCountExpr = Case().When(ChapterTable.isDownloaded eq true, intLiteral(1)).Else(intLiteral(0)).sum() when (filterVal) { - "unread" -> having { unreadCountExpr greater 0 } - "downloaded" -> having { downloadedCountExpr greater 0 } - "ongoing" -> andWhere { MangaTable.status eq MangaStatus.ONGOING.value } - "completed" -> andWhere { MangaTable.status eq MangaStatus.COMPLETED.value } + "unread" -> { + andWhere { + MangaTable.id inSubQuery + ChapterTable.select(ChapterTable.manga).where { ChapterTable.isRead eq false } + } + } + + "downloaded" -> { + andWhere { + MangaTable.id inSubQuery + ChapterTable.select(ChapterTable.manga).where { ChapterTable.isDownloaded eq true } + } + } + + "ongoing" -> { + andWhere { MangaTable.status eq MangaStatus.ONGOING.value } + } + + "completed" -> { + andWhere { MangaTable.status eq MangaStatus.COMPLETED.value } + } } } } @@ -133,11 +149,17 @@ object MangaRepository { val unreadCount = unreadCountExpr.alias("unread_count") // Base query with necessary joins for filtering and sorting - val query = + var baseJoin = MangaTable .join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id) .join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga) - .join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga) + + if (criteria.categoryId != null) { + baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga) + } + + val query = + baseJoin .select(MangaTable.columns + SourceTable.lang + SourceTable.name + unreadCount) .where { MangaTable.inLibrary eq true } @@ -301,7 +323,6 @@ object MangaRepository { * Applies sorting and filtering logic to a manga library query. * @param query The Exposed SQL query to modify. * @param sort The sorting parameter. - * @param filter The filtering parameter. */ private fun applyMangaLibrarySort( query: Query, @@ -330,36 +351,38 @@ object MangaRepository { */ fun getLibraryFilterCounts(activeFilters: OpdsMangaFilter): Map = transaction { - val unreadCountExpr = Case().When(ChapterTable.isRead eq false, intLiteral(1)).Else(intLiteral(0)).sum() - val downloadedCountExpr = Case().When(ChapterTable.isDownloaded eq true, intLiteral(1)).Else(intLiteral(0)).sum() + var baseJoin = + MangaTable + .join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id) + + if (activeFilters.categoryId != null) { + baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga) + } val baseQuery = - MangaTable - .join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga) - .join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id) - .join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga) + baseJoin .select(MangaTable.id) .where { MangaTable.inLibrary eq true } + .withDistinct() baseQuery.applyOpdsMangaFilter(activeFilters, excludeField = "filter") - baseQuery.groupBy(MangaTable.id) - val unreadCount = baseQuery.copy().having { unreadCountExpr greater 0 }.count() - val downloadedCount = baseQuery.copy().having { downloadedCountExpr greater 0 }.count() - - val statusBaseQuery = - MangaTable - .join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id) - .join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga) - .join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga) - .select(MangaTable.id) - .where { MangaTable.inLibrary eq true } - - statusBaseQuery.applyOpdsMangaFilter(activeFilters, excludeField = "filter") - statusBaseQuery.groupBy(MangaTable.id) - - val ongoingCount = statusBaseQuery.copy().andWhere { MangaTable.status eq MangaStatus.ONGOING.value }.count() - val completedCount = statusBaseQuery.copy().andWhere { MangaTable.status eq MangaStatus.COMPLETED.value }.count() + val unreadCount = + baseQuery + .copy() + .andWhere { + MangaTable.id inSubQuery + ChapterTable.select(ChapterTable.manga).where { ChapterTable.isRead eq false } + }.count() + val downloadedCount = + baseQuery + .copy() + .andWhere { + MangaTable.id inSubQuery + ChapterTable.select(ChapterTable.manga).where { ChapterTable.isDownloaded eq true } + }.count() + val ongoingCount = baseQuery.copy().andWhere { MangaTable.status eq MangaStatus.ONGOING.value }.count() + val completedCount = baseQuery.copy().andWhere { MangaTable.status eq MangaStatus.COMPLETED.value }.count() mapOf( "unread" to unreadCount, diff --git a/server/src/main/kotlin/suwayomi/tachidesk/opds/repository/NavigationRepository.kt b/server/src/main/kotlin/suwayomi/tachidesk/opds/repository/NavigationRepository.kt index 4bed7a9a9..97eb08f73 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/opds/repository/NavigationRepository.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/opds/repository/NavigationRepository.kt @@ -13,7 +13,6 @@ import suwayomi.tachidesk.i18n.MR import suwayomi.tachidesk.manga.impl.extension.Extension import suwayomi.tachidesk.manga.model.table.CategoryMangaTable import suwayomi.tachidesk.manga.model.table.CategoryTable -import suwayomi.tachidesk.manga.model.table.ChapterTable import suwayomi.tachidesk.manga.model.table.ExtensionTable import suwayomi.tachidesk.manga.model.table.MangaStatus import suwayomi.tachidesk.manga.model.table.MangaTable @@ -167,12 +166,17 @@ object NavigationRepository { transaction { val mangaCount = MangaTable.id.countDistinct().alias("manga_count") - val query = + var baseJoin = SourceTable .join(MangaTable, JoinType.INNER, SourceTable.id, MangaTable.sourceReference) .join(ExtensionTable, JoinType.LEFT, onColumn = SourceTable.extension, otherColumn = ExtensionTable.id) - .join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga) - .join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga) + + if (activeFilters.categoryId != null) { + baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga) + } + + val query = + baseJoin .select(SourceTable.id, SourceTable.name, SourceTable.lang, ExtensionTable.apkName, mangaCount) .where { MangaTable.inLibrary eq true } @@ -228,7 +232,6 @@ object NavigationRepository { .join(CategoryMangaTable, JoinType.INNER, CategoryTable.id, CategoryMangaTable.category) .join(MangaTable, JoinType.INNER, CategoryMangaTable.manga, MangaTable.id) .join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id) - .join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga) .select(CategoryTable.id, CategoryTable.name, mangaCount) .where { MangaTable.inLibrary eq true } @@ -263,11 +266,15 @@ object NavigationRepository { activeFilters: OpdsMangaFilter = OpdsMangaFilter(), ): Pair, Long> = transaction { - val query = + var baseJoin = MangaTable .join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id) - .join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga) - .join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga) + if (activeFilters.categoryId != null) { + baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga) + } + + val query = + baseJoin .select(MangaTable.genre) .where { MangaTable.inLibrary eq true } @@ -322,11 +329,16 @@ object NavigationRepository { val statusCounts = transaction { val countExpr = MangaTable.id.countDistinct().alias("manga_count") - val query = + + var baseJoin = MangaTable .join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id) - .join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga) - .join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga) + if (activeFilters.categoryId != null) { + baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga) + } + + val query = + baseJoin .select(MangaTable.status, countExpr) .where { MangaTable.inLibrary eq true } @@ -369,11 +381,16 @@ object NavigationRepository { ): Pair, Long> = transaction { val mangaCount = MangaTable.id.countDistinct().alias("manga_count") - val query = + + var baseJoin = SourceTable .join(MangaTable, JoinType.INNER, SourceTable.id, MangaTable.sourceReference) - .join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga) - .join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga) + if (activeFilters.categoryId != null) { + baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga) + } + + val query = + baseJoin .select(SourceTable.lang, mangaCount) .where { MangaTable.inLibrary eq true }