diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/MangaQueryBuilder.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/MangaQueryBuilder.kt new file mode 100644 index 000000000..16c99b563 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/MangaQueryBuilder.kt @@ -0,0 +1,109 @@ +package org.koitharu.kotatsu.core.db + +import androidx.sqlite.db.SimpleSQLiteQuery +import org.koitharu.kotatsu.list.domain.ListFilterOption +import java.util.LinkedList + +class MangaQueryBuilder( + private val table: String, + private val conditionCallback: ConditionCallback +) { + + private var filterOptions: Collection = emptyList() + private var whereConditions = LinkedList() + private var orderBy: String? = null + private var groupBy: String? = null + private var extraJoins: String? = null + private var limit: Int = 0 + + fun filters(options: Collection) = apply { + filterOptions = options + } + + fun where(condition: String) = apply { + whereConditions.add(condition) + } + + fun orderBy(orderBy: String?) = apply { + this@MangaQueryBuilder.orderBy = orderBy + } + + fun groupBy(groupBy: String?) = apply { + this@MangaQueryBuilder.groupBy = groupBy + } + + fun limit(limit: Int) = apply { + this@MangaQueryBuilder.limit = limit + } + + fun join(join: String?) = apply { + extraJoins = join + } + + fun build() = buildString { + append("SELECT * FROM ") + append(table) + extraJoins?.let { + append(' ') + append(it) + } + if (whereConditions.isNotEmpty()) { + whereConditions.joinTo( + buffer = this, + prefix = " WHERE ", + separator = " AND ", + ) + } + if (filterOptions.isNotEmpty()) { + if (whereConditions.isEmpty()) { + append(" WHERE") + } + var isFirst = true + val groupedOptions = filterOptions.groupBy { it.groupKey } + for ((_, group) in groupedOptions) { + if (group.isEmpty()) { + continue + } + if (isFirst) { + isFirst = false + append(' ') + } else { + append(" AND ") + } + if (group.size > 1) { + group.joinTo( + buffer = this, + separator = " OR ", + prefix = "(", + postfix = ")", + transform = ::getConditionOrThrow, + ) + } else { + append(getConditionOrThrow(group.single())) + } + } + } + groupBy?.let { + append(" GROUP BY ") + append(it) + } + orderBy?.let { + append(" ORDER BY ") + append(it) + } + if (limit > 0) { + append(" LIMIT ") + append(limit) + } + }.let { SimpleSQLiteQuery(it) } + + private fun getConditionOrThrow(option: ListFilterOption): String = + requireNotNull(conditionCallback.getCondition(option)) { + "Unsupported filter option $option" + } + + fun interface ConditionCallback { + + fun getCondition(option: ListFilterOption): String? + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/TrackLogsDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/TrackLogsDao.kt index 43f830849..d121ce5be 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/TrackLogsDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/TrackLogsDao.kt @@ -6,51 +6,26 @@ import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.RawQuery import androidx.room.Transaction -import androidx.sqlite.db.SimpleSQLiteQuery import androidx.sqlite.db.SupportSQLiteQuery import kotlinx.coroutines.flow.Flow -import org.koitharu.kotatsu.core.db.entity.toEntity +import org.koitharu.kotatsu.core.db.MangaQueryBuilder import org.koitharu.kotatsu.list.domain.ListFilterOption import org.koitharu.kotatsu.tracker.data.TrackLogEntity import org.koitharu.kotatsu.tracker.data.TrackLogWithManga @Dao -abstract class TrackLogsDao { - - fun observeAll(limit: Int, filterOptions: Set): Flow> { - val query = buildString { - append("SELECT * FROM track_logs") - if (filterOptions.isNotEmpty()) { - append(" WHERE") - var isFirst = true - val groupedOptions = filterOptions.groupBy { it.groupKey } - for ((_, group) in groupedOptions) { - if (group.isEmpty()) { - continue - } - if (isFirst) { - isFirst = false - append(' ') - } else { - append(" AND ") - } - if (group.size > 1) { - group.joinTo(this, separator = " OR ", prefix = "(", postfix = ")") { - it.getCondition() - } - } else { - append(group.single().getCondition()) - } - } - } - append(" ORDER BY created_at DESC") - if (limit > 0) { - append(" LIMIT ") - append(limit) - } - } - return observeAllImpl(SimpleSQLiteQuery(query)) - } +abstract class TrackLogsDao : MangaQueryBuilder.ConditionCallback { + + fun observeAll( + limit: Int, + filterOptions: Set, + ): Flow> = observeAllImpl( + MangaQueryBuilder("track_logs", this) + .filters(filterOptions) + .limit(limit) + .orderBy("created_at DESC") + .build(), + ) @Query("SELECT COUNT(*) FROM track_logs WHERE unread = 1") abstract fun observeUnreadCount(): Flow @@ -77,10 +52,10 @@ abstract class TrackLogsDao { @RawQuery(observedEntities = [TrackLogEntity::class]) protected abstract fun observeAllImpl(query: SupportSQLiteQuery): Flow> - private fun ListFilterOption.getCondition(): String = when (this) { + override fun getCondition(option: ListFilterOption): String? = when (option) { ListFilterOption.Macro.FAVORITE -> "EXISTS(SELECT * FROM favourites WHERE favourites.manga_id = track_logs.manga_id)" - is ListFilterOption.Favorite -> "EXISTS(SELECT * FROM favourites WHERE favourites.manga_id = track_logs.manga_id AND favourites.category_id = ${category.id})" - is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE manga_tags.manga_id = track_logs.manga_id AND tag_id = ${tag.toEntity().id})" - else -> throw IllegalArgumentException("Unsupported option $this") + is ListFilterOption.Favorite -> "EXISTS(SELECT * FROM favourites WHERE favourites.manga_id = track_logs.manga_id AND favourites.category_id = ${option.category.id})" + is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE manga_tags.manga_id = track_logs.manga_id AND tag_id = ${option.tagId})" + else -> null } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt index 6f28a466d..8ab368661 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt @@ -11,14 +11,15 @@ import androidx.sqlite.db.SimpleSQLiteQuery import androidx.sqlite.db.SupportSQLiteQuery import kotlinx.coroutines.flow.Flow import org.intellij.lang.annotations.Language -import org.koitharu.kotatsu.core.db.entity.toEntity +import org.koitharu.kotatsu.core.db.MangaQueryBuilder +import org.koitharu.kotatsu.core.db.TABLE_FAVOURITES import org.koitharu.kotatsu.favourites.domain.model.Cover import org.koitharu.kotatsu.list.domain.ListFilterOption import org.koitharu.kotatsu.list.domain.ListSortOrder import org.koitharu.kotatsu.list.domain.ReadingProgress.Companion.PROGRESS_COMPLETED @Dao -abstract class FavouritesDao { +abstract class FavouritesDao : MangaQueryBuilder.ConditionCallback { /** SELECT **/ @@ -55,41 +56,17 @@ abstract class FavouritesDao { order: ListSortOrder, filterOptions: Set, limit: Int - ): Flow> { - val orderBy = getOrderBy(order) - val query = buildString { - append( - "SELECT * FROM favourites LEFT JOIN manga ON favourites.manga_id = manga.manga_id " + - "WHERE deleted_at = 0", - ) - if (categoryId != 0L) { - append(" AND category_id = ") - append(categoryId) - } - val groupedOptions = filterOptions.groupBy { it.groupKey } - for ((_, group) in groupedOptions) { - if (group.isEmpty()) { - continue - } - append(" AND ") - if (group.size > 1) { - group.joinTo(this, separator = " OR ", prefix = "(", postfix = ")") { - it.getCondition() - } - } else { - append(group.single().getCondition()) - } - } - append(" GROUP BY favourites.manga_id ORDER BY ") - append(orderBy) - if (limit > 0) { - append(" LIMIT ") - append(limit) - } - } - - return observeAllImpl(SimpleSQLiteQuery(query)) - } + ): Flow> = observeAllImpl( + MangaQueryBuilder(TABLE_FAVOURITES, this) + .join("LEFT JOIN manga ON favourites.manga_id = manga.manga_id") + .where("deleted_at = 0") + .run { if (categoryId != 0L) where("category_id = $categoryId") else this } + .filters(filterOptions) + .groupBy("favourites.manga_id") + .orderBy(getOrderBy(order)) + .limit(limit) + .build(), + ) suspend fun findCovers(categoryId: Long, order: ListSortOrder): List { val orderBy = getOrderBy(order) @@ -213,13 +190,13 @@ abstract class FavouritesDao { else -> throw IllegalArgumentException("Sort order $sortOrder is not supported") } - private fun ListFilterOption.getCondition(): String = when (this) { + override fun getCondition(option: ListFilterOption): String? = when (option) { ListFilterOption.Macro.COMPLETED -> "EXISTS(SELECT * FROM history WHERE history.manga_id = favourites.manga_id AND history.percent >= $PROGRESS_COMPLETED)" ListFilterOption.Macro.NEW_CHAPTERS -> "(SELECT chapters_new FROM tracks WHERE tracks.manga_id = favourites.manga_id) > 0" ListFilterOption.Macro.NSFW -> "manga.nsfw = 1" - is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE favourites.manga_id = manga_tags.manga_id AND tag_id = ${tag.toEntity().id})" + is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE favourites.manga_id = manga_tags.manga_id AND tag_id = ${option.tagId})" ListFilterOption.Downloaded, is ListFilterOption.Favorite, - ListFilterOption.Macro.FAVORITE -> throw IllegalArgumentException("Unsupported option $this") + ListFilterOption.Macro.FAVORITE -> null } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt index 2cfbca8a8..b8d335a64 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt @@ -6,17 +6,17 @@ import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.RawQuery import androidx.room.Transaction -import androidx.sqlite.db.SimpleSQLiteQuery import androidx.sqlite.db.SupportSQLiteQuery import kotlinx.coroutines.flow.Flow +import org.koitharu.kotatsu.core.db.MangaQueryBuilder +import org.koitharu.kotatsu.core.db.TABLE_HISTORY import org.koitharu.kotatsu.core.db.entity.TagEntity -import org.koitharu.kotatsu.core.db.entity.toEntity import org.koitharu.kotatsu.list.domain.ListFilterOption import org.koitharu.kotatsu.list.domain.ListSortOrder import org.koitharu.kotatsu.list.domain.ReadingProgress.Companion.PROGRESS_COMPLETED @Dao -abstract class HistoryDao { +abstract class HistoryDao : MangaQueryBuilder.ConditionCallback { @Transaction @Query("SELECT * FROM history WHERE deleted_at = 0 ORDER BY updated_at DESC LIMIT :limit OFFSET :offset") @@ -34,48 +34,30 @@ abstract class HistoryDao { order: ListSortOrder, filterOptions: Set, limit: Int - ): Flow> { - val orderBy = when (order) { - ListSortOrder.LAST_READ -> "history.updated_at DESC" - ListSortOrder.LONG_AGO_READ -> "history.updated_at ASC" - ListSortOrder.NEWEST -> "history.created_at DESC" - ListSortOrder.OLDEST -> "history.created_at ASC" - ListSortOrder.PROGRESS -> "history.percent DESC" - ListSortOrder.UNREAD -> "history.percent ASC" - ListSortOrder.ALPHABETIC -> "manga.title" - ListSortOrder.ALPHABETIC_REVERSE -> "manga.title DESC" - ListSortOrder.NEW_CHAPTERS -> "IFNULL((SELECT chapters_new FROM tracks WHERE tracks.manga_id = manga.manga_id), 0) DESC" - ListSortOrder.UPDATED -> "IFNULL((SELECT last_chapter_date FROM tracks WHERE tracks.manga_id = manga.manga_id), 0) DESC" - else -> throw IllegalArgumentException("Sort order $order is not supported") - } - val query = buildString { - append( - "SELECT * FROM history LEFT JOIN manga ON history.manga_id = manga.manga_id " + - "WHERE history.deleted_at = 0", + ): Flow> = observeAllImpl( + MangaQueryBuilder(TABLE_HISTORY, this) + .join("LEFT JOIN manga ON history.manga_id = manga.manga_id") + .where("history.deleted_at = 0") + .filters(filterOptions) + .orderBy( + orderBy = when (order) { + ListSortOrder.LAST_READ -> "history.updated_at DESC" + ListSortOrder.LONG_AGO_READ -> "history.updated_at ASC" + ListSortOrder.NEWEST -> "history.created_at DESC" + ListSortOrder.OLDEST -> "history.created_at ASC" + ListSortOrder.PROGRESS -> "history.percent DESC" + ListSortOrder.UNREAD -> "history.percent ASC" + ListSortOrder.ALPHABETIC -> "manga.title" + ListSortOrder.ALPHABETIC_REVERSE -> "manga.title DESC" + ListSortOrder.NEW_CHAPTERS -> "IFNULL((SELECT chapters_new FROM tracks WHERE tracks.manga_id = manga.manga_id), 0) DESC" + ListSortOrder.UPDATED -> "IFNULL((SELECT last_chapter_date FROM tracks WHERE tracks.manga_id = manga.manga_id), 0) DESC" + else -> throw IllegalArgumentException("Sort order $order is not supported") + }, ) - val groupedOptions = filterOptions.groupBy { it.groupKey } - for ((_, group) in groupedOptions) { - if (group.isEmpty()) { - continue - } - append(" AND ") - if (group.size > 1) { - group.joinTo(this, separator = " OR ", prefix = "(", postfix = ")") { - it.getCondition() - } - } else { - append(group.single().getCondition()) - } - } - append(" GROUP BY history.manga_id ORDER BY ") - append(orderBy) - if (limit > 0) { - append(" LIMIT ") - append(limit) - } - } - return observeAllImpl(SimpleSQLiteQuery(query)) - } + .groupBy("history.manga_id") + .limit(limit) + .build(), + ) @Query("SELECT manga_id FROM history WHERE deleted_at = 0") abstract suspend fun findAllIds(): LongArray @@ -170,13 +152,13 @@ abstract class HistoryDao { @RawQuery(observedEntities = [HistoryEntity::class]) protected abstract fun observeAllImpl(query: SupportSQLiteQuery): Flow> - private fun ListFilterOption.getCondition(): String = when (this) { - ListFilterOption.Downloaded -> throw IllegalArgumentException("Unsupported option $this") - is ListFilterOption.Favorite -> "EXISTS(SELECT * FROM favourites WHERE history.manga_id = favourites.manga_id AND category_id = ${category.id})" + override fun getCondition(option: ListFilterOption): String? = when (option) { + ListFilterOption.Downloaded -> null + is ListFilterOption.Favorite -> "EXISTS(SELECT * FROM favourites WHERE history.manga_id = favourites.manga_id AND category_id = ${option.category.id})" ListFilterOption.Macro.COMPLETED -> "percent >= $PROGRESS_COMPLETED" ListFilterOption.Macro.NEW_CHAPTERS -> "(SELECT chapters_new FROM tracks WHERE tracks.manga_id = history.manga_id) > 0" ListFilterOption.Macro.FAVORITE -> "EXISTS(SELECT * FROM favourites WHERE history.manga_id = favourites.manga_id)" ListFilterOption.Macro.NSFW -> "manga.nsfw = 1" - is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE history.manga_id = manga_tags.manga_id AND tag_id = ${tag.toEntity().id})" + is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE history.manga_id = manga_tags.manga_id AND tag_id = ${option.tagId})" } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/ListFilterOption.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/ListFilterOption.kt index 463bef143..2e77f7ebf 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/ListFilterOption.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/ListFilterOption.kt @@ -3,6 +3,7 @@ package org.koitharu.kotatsu.list.domain import androidx.annotation.DrawableRes import androidx.annotation.StringRes import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.db.entity.toEntity import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.parsers.model.MangaTag @@ -55,6 +56,8 @@ sealed interface ListFilterOption { val tag: MangaTag ) : ListFilterOption { + val tagId: Long = tag.toEntity().id + override val titleResId: Int get() = 0 diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/data/SuggestionDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/data/SuggestionDao.kt index f11c94534..317f2ffff 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/data/SuggestionDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/data/SuggestionDao.kt @@ -7,54 +7,29 @@ import androidx.room.Query import androidx.room.RawQuery import androidx.room.Transaction import androidx.room.Update -import androidx.sqlite.db.SimpleSQLiteQuery import androidx.sqlite.db.SupportSQLiteQuery import kotlinx.coroutines.flow.Flow +import org.koitharu.kotatsu.core.db.MangaQueryBuilder import org.koitharu.kotatsu.core.db.entity.TagEntity -import org.koitharu.kotatsu.core.db.entity.toEntity import org.koitharu.kotatsu.list.domain.ListFilterOption @Dao -abstract class SuggestionDao { +abstract class SuggestionDao : MangaQueryBuilder.ConditionCallback { @Transaction @Query("SELECT * FROM suggestions ORDER BY relevance DESC") abstract fun observeAll(): Flow> - fun observeAll(limit: Int, filterOptions: Collection): Flow> { - val query = buildString { - append("SELECT * FROM suggestions") - if (filterOptions.isNotEmpty()) { - append(" WHERE") - var isFirst = true - val groupedOptions = filterOptions.groupBy { it.groupKey } - for ((_, group) in groupedOptions) { - if (group.isEmpty()) { - continue - } - if (isFirst) { - isFirst = false - append(' ') - } else { - append(" AND ") - } - if (group.size > 1) { - group.joinTo(this, separator = " OR ", prefix = "(", postfix = ")") { - it.getCondition() - } - } else { - append(group.single().getCondition()) - } - } - } - append(" ORDER BY relevance DESC") - if (limit > 0) { - append(" LIMIT ") - append(limit) - } - } - return observeAllImpl(SimpleSQLiteQuery(query)) - } + fun observeAll( + limit: Int, + filterOptions: Collection + ): Flow> = observeAllImpl( + MangaQueryBuilder("suggestions", this) + .filters(filterOptions) + .orderBy("relevance DESC") + .limit(limit) + .build(), + ) @Transaction @Query("SELECT * FROM suggestions ORDER BY RANDOM() LIMIT 1") @@ -93,9 +68,9 @@ abstract class SuggestionDao { @RawQuery(observedEntities = [SuggestionEntity::class]) protected abstract fun observeAllImpl(query: SupportSQLiteQuery): Flow> - private fun ListFilterOption.getCondition(): String = when (this) { + override fun getCondition(option: ListFilterOption): String? = when (option) { ListFilterOption.Macro.NSFW -> "(SELECT nsfw FROM manga WHERE manga.manga_id = suggestions.manga_id) = 1" - is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE manga_tags.manga_id = suggestions.manga_id AND tag_id = ${tag.toEntity().id})" - else -> throw IllegalArgumentException("Unsupported option $this") + is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE manga_tags.manga_id = suggestions.manga_id AND tag_id = ${option.tagId})" + else -> null } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TracksDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TracksDao.kt index 9faa9b908..69fad40bd 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TracksDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TracksDao.kt @@ -5,14 +5,13 @@ import androidx.room.Query import androidx.room.RawQuery import androidx.room.Transaction import androidx.room.Upsert -import androidx.sqlite.db.SimpleSQLiteQuery import androidx.sqlite.db.SupportSQLiteQuery import kotlinx.coroutines.flow.Flow -import org.koitharu.kotatsu.core.db.entity.toEntity +import org.koitharu.kotatsu.core.db.MangaQueryBuilder import org.koitharu.kotatsu.list.domain.ListFilterOption @Dao -abstract class TracksDao { +abstract class TracksDao : MangaQueryBuilder.ConditionCallback { @Transaction @Query("SELECT * FROM tracks ORDER BY last_check_time ASC LIMIT :limit OFFSET :offset") @@ -44,33 +43,17 @@ abstract class TracksDao { @Query("SELECT * FROM tracks WHERE chapters_new > 0 ORDER BY last_chapter_date DESC") abstract fun observeUpdatedManga(): Flow> - fun observeUpdatedManga(limit: Int, filterOptions: Set): Flow> { - val query = buildString { - append("SELECT * FROM tracks WHERE chapters_new > 0") - if (filterOptions.isNotEmpty()) { - val groupedOptions = filterOptions.groupBy { it.groupKey } - for ((_, group) in groupedOptions) { - if (group.isEmpty()) { - continue - } - append(" AND ") - if (group.size > 1) { - group.joinTo(this, separator = " OR ", prefix = "(", postfix = ")") { - it.getCondition() - } - } else { - append(group.single().getCondition()) - } - } - } - append(" ORDER BY last_chapter_date DESC") - if (limit > 0) { - append(" LIMIT ") - append(limit) - } - } - return observeMangaImpl(SimpleSQLiteQuery(query)) - } + fun observeUpdatedManga( + limit: Int, + filterOptions: Set, + ): Flow> = observeMangaImpl( + MangaQueryBuilder("tracks", this) + .where("chapters_new > 0") + .filters(filterOptions) + .limit(limit) + .orderBy("last_chapter_date DESC") + .build(), + ) @Query("DELETE FROM tracks") abstract suspend fun clear() @@ -94,10 +77,10 @@ abstract class TracksDao { @RawQuery(observedEntities = [TrackEntity::class]) protected abstract fun observeMangaImpl(query: SupportSQLiteQuery): Flow> - private fun ListFilterOption.getCondition(): String = when (this) { + override fun getCondition(option: ListFilterOption): String? = when (option) { ListFilterOption.Macro.FAVORITE -> "EXISTS(SELECT * FROM favourites WHERE favourites.manga_id = tracks.manga_id)" - is ListFilterOption.Favorite -> "EXISTS(SELECT * FROM favourites WHERE favourites.manga_id = tracks.manga_id AND favourites.category_id = ${category.id})" - is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE manga_tags.manga_id = tracks.manga_id AND tag_id = ${tag.toEntity().id})" - else -> throw IllegalArgumentException("Unsupported option $this") + is ListFilterOption.Favorite -> "EXISTS(SELECT * FROM favourites WHERE favourites.manga_id = tracks.manga_id AND favourites.category_id = ${option.category.id})" + is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE manga_tags.manga_id = tracks.manga_id AND tag_id = ${option.tagId})" + else -> null } }