fix(opds): resolve sql group by syntax error when filtering library (#2118)

This commit is contained in:
Zeedif
2026-06-17 20:39:30 -06:00
committed by GitHub
parent 14ab3aa9f4
commit b33069f107
2 changed files with 86 additions and 46 deletions

View File

@@ -11,6 +11,7 @@ import org.jetbrains.exposed.v1.core.and
import org.jetbrains.exposed.v1.core.eq import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.greater import org.jetbrains.exposed.v1.core.greater
import org.jetbrains.exposed.v1.core.inList 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.intLiteral
import org.jetbrains.exposed.v1.core.like import org.jetbrains.exposed.v1.core.like
import org.jetbrains.exposed.v1.core.lowerCase import org.jetbrains.exposed.v1.core.lowerCase
@@ -75,13 +76,28 @@ fun Query.applyOpdsMangaFilter(
} }
if (excludeField != "filter") { if (excludeField != "filter") {
criteria.filter?.let { filterVal -> 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) { when (filterVal) {
"unread" -> having { unreadCountExpr greater 0 } "unread" -> {
"downloaded" -> having { downloadedCountExpr greater 0 } andWhere {
"ongoing" -> andWhere { MangaTable.status eq MangaStatus.ONGOING.value } MangaTable.id inSubQuery
"completed" -> andWhere { MangaTable.status eq MangaStatus.COMPLETED.value } 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") val unreadCount = unreadCountExpr.alias("unread_count")
// Base query with necessary joins for filtering and sorting // Base query with necessary joins for filtering and sorting
val query = var baseJoin =
MangaTable MangaTable
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id) .join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga) .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) .select(MangaTable.columns + SourceTable.lang + SourceTable.name + unreadCount)
.where { MangaTable.inLibrary eq true } .where { MangaTable.inLibrary eq true }
@@ -301,7 +323,6 @@ object MangaRepository {
* Applies sorting and filtering logic to a manga library query. * Applies sorting and filtering logic to a manga library query.
* @param query The Exposed SQL query to modify. * @param query The Exposed SQL query to modify.
* @param sort The sorting parameter. * @param sort The sorting parameter.
* @param filter The filtering parameter.
*/ */
private fun applyMangaLibrarySort( private fun applyMangaLibrarySort(
query: Query, query: Query,
@@ -330,36 +351,38 @@ object MangaRepository {
*/ */
fun getLibraryFilterCounts(activeFilters: OpdsMangaFilter): Map<String, Long> = fun getLibraryFilterCounts(activeFilters: OpdsMangaFilter): Map<String, Long> =
transaction { transaction {
val unreadCountExpr = Case().When(ChapterTable.isRead eq false, intLiteral(1)).Else(intLiteral(0)).sum() var baseJoin =
val downloadedCountExpr = Case().When(ChapterTable.isDownloaded eq true, intLiteral(1)).Else(intLiteral(0)).sum() 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 = val baseQuery =
MangaTable baseJoin
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
.select(MangaTable.id) .select(MangaTable.id)
.where { MangaTable.inLibrary eq true } .where { MangaTable.inLibrary eq true }
.withDistinct()
baseQuery.applyOpdsMangaFilter(activeFilters, excludeField = "filter") baseQuery.applyOpdsMangaFilter(activeFilters, excludeField = "filter")
baseQuery.groupBy(MangaTable.id)
val unreadCount = baseQuery.copy().having { unreadCountExpr greater 0 }.count() val unreadCount =
val downloadedCount = baseQuery.copy().having { downloadedCountExpr greater 0 }.count() baseQuery
.copy()
val statusBaseQuery = .andWhere {
MangaTable MangaTable.id inSubQuery
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id) ChapterTable.select(ChapterTable.manga).where { ChapterTable.isRead eq false }
.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga) }.count()
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga) val downloadedCount =
.select(MangaTable.id) baseQuery
.where { MangaTable.inLibrary eq true } .copy()
.andWhere {
statusBaseQuery.applyOpdsMangaFilter(activeFilters, excludeField = "filter") MangaTable.id inSubQuery
statusBaseQuery.groupBy(MangaTable.id) ChapterTable.select(ChapterTable.manga).where { ChapterTable.isDownloaded eq true }
}.count()
val ongoingCount = statusBaseQuery.copy().andWhere { MangaTable.status eq MangaStatus.ONGOING.value }.count() val ongoingCount = baseQuery.copy().andWhere { MangaTable.status eq MangaStatus.ONGOING.value }.count()
val completedCount = statusBaseQuery.copy().andWhere { MangaTable.status eq MangaStatus.COMPLETED.value }.count() val completedCount = baseQuery.copy().andWhere { MangaTable.status eq MangaStatus.COMPLETED.value }.count()
mapOf( mapOf(
"unread" to unreadCount, "unread" to unreadCount,

View File

@@ -13,7 +13,6 @@ import suwayomi.tachidesk.i18n.MR
import suwayomi.tachidesk.manga.impl.extension.Extension import suwayomi.tachidesk.manga.impl.extension.Extension
import suwayomi.tachidesk.manga.model.table.CategoryMangaTable import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
import suwayomi.tachidesk.manga.model.table.CategoryTable 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.ExtensionTable
import suwayomi.tachidesk.manga.model.table.MangaStatus import suwayomi.tachidesk.manga.model.table.MangaStatus
import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.manga.model.table.MangaTable
@@ -167,12 +166,17 @@ object NavigationRepository {
transaction { transaction {
val mangaCount = MangaTable.id.countDistinct().alias("manga_count") val mangaCount = MangaTable.id.countDistinct().alias("manga_count")
val query = var baseJoin =
SourceTable SourceTable
.join(MangaTable, JoinType.INNER, SourceTable.id, MangaTable.sourceReference) .join(MangaTable, JoinType.INNER, SourceTable.id, MangaTable.sourceReference)
.join(ExtensionTable, JoinType.LEFT, onColumn = SourceTable.extension, otherColumn = ExtensionTable.id) .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) .select(SourceTable.id, SourceTable.name, SourceTable.lang, ExtensionTable.apkName, mangaCount)
.where { MangaTable.inLibrary eq true } .where { MangaTable.inLibrary eq true }
@@ -228,7 +232,6 @@ object NavigationRepository {
.join(CategoryMangaTable, JoinType.INNER, CategoryTable.id, CategoryMangaTable.category) .join(CategoryMangaTable, JoinType.INNER, CategoryTable.id, CategoryMangaTable.category)
.join(MangaTable, JoinType.INNER, CategoryMangaTable.manga, MangaTable.id) .join(MangaTable, JoinType.INNER, CategoryMangaTable.manga, MangaTable.id)
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id) .join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga)
.select(CategoryTable.id, CategoryTable.name, mangaCount) .select(CategoryTable.id, CategoryTable.name, mangaCount)
.where { MangaTable.inLibrary eq true } .where { MangaTable.inLibrary eq true }
@@ -263,11 +266,15 @@ object NavigationRepository {
activeFilters: OpdsMangaFilter = OpdsMangaFilter(), activeFilters: OpdsMangaFilter = OpdsMangaFilter(),
): Pair<List<OpdsGenreNavEntry>, Long> = ): Pair<List<OpdsGenreNavEntry>, Long> =
transaction { transaction {
val query = var baseJoin =
MangaTable MangaTable
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id) .join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga) if (activeFilters.categoryId != null) {
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga) baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
}
val query =
baseJoin
.select(MangaTable.genre) .select(MangaTable.genre)
.where { MangaTable.inLibrary eq true } .where { MangaTable.inLibrary eq true }
@@ -322,11 +329,16 @@ object NavigationRepository {
val statusCounts = val statusCounts =
transaction { transaction {
val countExpr = MangaTable.id.countDistinct().alias("manga_count") val countExpr = MangaTable.id.countDistinct().alias("manga_count")
val query =
var baseJoin =
MangaTable MangaTable
.join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id) .join(SourceTable, JoinType.INNER, MangaTable.sourceReference, SourceTable.id)
.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga) if (activeFilters.categoryId != null) {
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga) baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
}
val query =
baseJoin
.select(MangaTable.status, countExpr) .select(MangaTable.status, countExpr)
.where { MangaTable.inLibrary eq true } .where { MangaTable.inLibrary eq true }
@@ -369,11 +381,16 @@ object NavigationRepository {
): Pair<List<OpdsLanguageNavEntry>, Long> = ): Pair<List<OpdsLanguageNavEntry>, Long> =
transaction { transaction {
val mangaCount = MangaTable.id.countDistinct().alias("manga_count") val mangaCount = MangaTable.id.countDistinct().alias("manga_count")
val query =
var baseJoin =
SourceTable SourceTable
.join(MangaTable, JoinType.INNER, SourceTable.id, MangaTable.sourceReference) .join(MangaTable, JoinType.INNER, SourceTable.id, MangaTable.sourceReference)
.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga) if (activeFilters.categoryId != null) {
.join(ChapterTable, JoinType.LEFT, MangaTable.id, ChapterTable.manga) baseJoin = baseJoin.join(CategoryMangaTable, JoinType.LEFT, MangaTable.id, CategoryMangaTable.manga)
}
val query =
baseJoin
.select(SourceTable.lang, mangaCount) .select(SourceTable.lang, mangaCount)
.where { MangaTable.inLibrary eq true } .where { MangaTable.inLibrary eq true }