Update sqldelight-androidx-driver and remove transaction management (#3270)

`sqldelight-androidx-driver` has it's own transaction management
This commit is contained in:
AntsyLich
2026-04-27 18:24:30 +06:00
committed by GitHub
parent a2f3ec94aa
commit 8e0c911f93
28 changed files with 481 additions and 707 deletions
+2
View File
@@ -203,6 +203,8 @@ dependencies {
implementation(libs.bundles.kotlinx.coroutines) implementation(libs.bundles.kotlinx.coroutines)
implementation(libs.sqldelight.async)
// AndroidX libraries // AndroidX libraries
implementation(libs.androidx.annotation) implementation(libs.androidx.annotation)
implementation(libs.androidx.appCompat) implementation(libs.androidx.appCompat)
@@ -1,24 +1,26 @@
package eu.kanade.domain.manga.interactor package eu.kanade.domain.manga.interactor
import app.cash.sqldelight.async.coroutines.awaitAsList
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import tachiyomi.data.DatabaseHandler import tachiyomi.data.Database
import tachiyomi.data.subscribeToList
class GetExcludedScanlators( class GetExcludedScanlators(
private val handler: DatabaseHandler, private val database: Database,
) { ) {
suspend fun await(mangaId: Long): Set<String> { suspend fun await(mangaId: Long): Set<String> {
return handler.awaitList { return database.excluded_scanlatorsQueries
excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId) .getExcludedScanlatorsByMangaId(mangaId)
} .awaitAsList()
.toSet() .toSet()
} }
fun subscribe(mangaId: Long): Flow<Set<String>> { fun subscribe(mangaId: Long): Flow<Set<String>> {
return handler.subscribeToList { return database.excluded_scanlatorsQueries
excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId) .getExcludedScanlatorsByMangaId(mangaId)
} .subscribeToList()
.map { it.toSet() } .map { it.toSet() }
} }
} }
@@ -1,22 +1,24 @@
package eu.kanade.domain.manga.interactor package eu.kanade.domain.manga.interactor
import tachiyomi.data.DatabaseHandler import app.cash.sqldelight.async.coroutines.awaitAsList
import tachiyomi.data.Database
class SetExcludedScanlators( class SetExcludedScanlators(
private val handler: DatabaseHandler, private val database: Database,
) { ) {
suspend fun await(mangaId: Long, excludedScanlators: Set<String>) { suspend fun await(mangaId: Long, excludedScanlators: Set<String>) {
handler.await(inTransaction = true) { database.transaction {
val currentExcluded = handler.awaitList { val currentExcluded = database.excluded_scanlatorsQueries
excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId) .getExcludedScanlatorsByMangaId(mangaId)
}.toSet() .awaitAsList()
.toSet()
val toAdd = excludedScanlators.minus(currentExcluded) val toAdd = excludedScanlators.minus(currentExcluded)
for (scanlator in toAdd) { for (scanlator in toAdd) {
excluded_scanlatorsQueries.insert(mangaId, scanlator) database.excluded_scanlatorsQueries.insert(mangaId, scanlator)
} }
val toRemove = currentExcluded.minus(excludedScanlators) val toRemove = currentExcluded.minus(excludedScanlators)
excluded_scanlatorsQueries.remove(mangaId, toRemove) database.excluded_scanlatorsQueries.remove(mangaId, toRemove)
} }
} }
} }
@@ -1,5 +1,7 @@
package eu.kanade.tachiyomi.data.backup.create.creators package eu.kanade.tachiyomi.data.backup.create.creators
import app.cash.sqldelight.async.coroutines.awaitAsList
import app.cash.sqldelight.async.coroutines.awaitAsOne
import eu.kanade.tachiyomi.data.backup.create.BackupOptions import eu.kanade.tachiyomi.data.backup.create.BackupOptions
import eu.kanade.tachiyomi.data.backup.models.BackupChapter import eu.kanade.tachiyomi.data.backup.models.BackupChapter
import eu.kanade.tachiyomi.data.backup.models.BackupHistory import eu.kanade.tachiyomi.data.backup.models.BackupHistory
@@ -7,7 +9,7 @@ import eu.kanade.tachiyomi.data.backup.models.BackupManga
import eu.kanade.tachiyomi.data.backup.models.backupChapterMapper import eu.kanade.tachiyomi.data.backup.models.backupChapterMapper
import eu.kanade.tachiyomi.data.backup.models.backupTrackMapper import eu.kanade.tachiyomi.data.backup.models.backupTrackMapper
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import tachiyomi.data.DatabaseHandler import tachiyomi.data.Database
import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.history.interactor.GetHistory import tachiyomi.domain.history.interactor.GetHistory
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
@@ -15,7 +17,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class MangaBackupCreator( class MangaBackupCreator(
private val handler: DatabaseHandler = Injekt.get(), private val database: Database = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(), private val getCategories: GetCategories = Injekt.get(),
private val getHistory: GetHistory = Injekt.get(), private val getHistory: GetHistory = Injekt.get(),
) { ) {
@@ -30,19 +32,19 @@ class MangaBackupCreator(
// Entry for this manga // Entry for this manga
val mangaObject = manga.toBackupManga() val mangaObject = manga.toBackupManga()
mangaObject.excludedScanlators = handler.awaitList { mangaObject.excludedScanlators = database.excluded_scanlatorsQueries
excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(manga.id) .getExcludedScanlatorsByMangaId(manga.id)
} .awaitAsList()
if (options.chapters) { if (options.chapters) {
// Backup all the chapters // Backup all the chapters
handler.awaitList { database.chaptersQueries
chaptersQueries.getChaptersByMangaId( .getChaptersByMangaId(
mangaId = manga.id, mangaId = manga.id,
applyScanlatorFilter = 0, // false applyScanlatorFilter = 0, // false
mapper = backupChapterMapper, mapper = backupChapterMapper,
) )
} .awaitAsList()
.takeUnless(List<BackupChapter>::isEmpty) .takeUnless(List<BackupChapter>::isEmpty)
?.let { mangaObject.chapters = it } ?.let { mangaObject.chapters = it }
} }
@@ -56,7 +58,9 @@ class MangaBackupCreator(
} }
if (options.tracking) { if (options.tracking) {
val tracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id, backupTrackMapper) } val tracks = database.manga_syncQueries
.getTracksByMangaId(manga.id, backupTrackMapper)
.awaitAsList()
if (tracks.isNotEmpty()) { if (tracks.isNotEmpty()) {
mangaObject.tracking = tracks mangaObject.tracking = tracks
} }
@@ -66,7 +70,9 @@ class MangaBackupCreator(
val historyByMangaId = getHistory.await(manga.id) val historyByMangaId = getHistory.await(manga.id)
if (historyByMangaId.isNotEmpty()) { if (historyByMangaId.isNotEmpty()) {
val history = historyByMangaId.map { history -> val history = historyByMangaId.map { history ->
val chapter = handler.awaitOne { chaptersQueries.getChapterById(history.chapterId) } val chapter = database.chaptersQueries
.getChapterById(history.chapterId)
.awaitAsOne()
BackupHistory(chapter.url, history.readAt?.time ?: 0L, history.readDuration) BackupHistory(chapter.url, history.readAt?.time ?: 0L, history.readDuration)
} }
if (history.isNotEmpty()) { if (history.isNotEmpty()) {
@@ -1,14 +1,15 @@
package eu.kanade.tachiyomi.data.backup.restore.restorers package eu.kanade.tachiyomi.data.backup.restore.restorers
import app.cash.sqldelight.async.coroutines.awaitAsOne
import eu.kanade.tachiyomi.data.backup.models.BackupCategory import eu.kanade.tachiyomi.data.backup.models.BackupCategory
import tachiyomi.data.DatabaseHandler import tachiyomi.data.Database
import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class CategoriesRestorer( class CategoriesRestorer(
private val handler: DatabaseHandler = Injekt.get(), private val database: Database = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(), private val getCategories: GetCategories = Injekt.get(),
private val libraryPreferences: LibraryPreferences = Injekt.get(), private val libraryPreferences: LibraryPreferences = Injekt.get(),
) { ) {
@@ -25,10 +26,9 @@ class CategoriesRestorer(
val dbCategory = dbCategoriesByName[it.name] val dbCategory = dbCategoriesByName[it.name]
if (dbCategory != null) return@map dbCategory if (dbCategory != null) return@map dbCategory
val order = nextOrder++ val order = nextOrder++
handler.awaitOneExecutable { database.categoriesQueries
categoriesQueries.insert(it.name, order, it.flags) .insert(it.name, order, it.flags)
categoriesQueries.selectLastInsertedRowId() .awaitAsOne()
}
.let { id -> it.toCategory(id).copy(order = order) } .let { id -> it.toCategory(id).copy(order = order) }
} }
@@ -2,12 +2,12 @@ package eu.kanade.tachiyomi.data.backup.restore.restorers
import eu.kanade.tachiyomi.data.backup.models.BackupExtensionRepos import eu.kanade.tachiyomi.data.backup.models.BackupExtensionRepos
import mihon.domain.extensionrepo.interactor.GetExtensionRepo import mihon.domain.extensionrepo.interactor.GetExtensionRepo
import tachiyomi.data.DatabaseHandler import tachiyomi.data.Database
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class ExtensionRepoRestorer( class ExtensionRepoRestorer(
private val handler: DatabaseHandler = Injekt.get(), private val database: Database = Injekt.get(),
private val getExtensionRepos: GetExtensionRepo = Injekt.get(), private val getExtensionRepos: GetExtensionRepo = Injekt.get(),
) { ) {
@@ -26,8 +26,7 @@ class ExtensionRepoRestorer(
} else if (shaExists != null) { } else if (shaExists != null) {
error("${shaExists.name} has the same signing key fingerprint") error("${shaExists.name} has the same signing key fingerprint")
} else { } else {
handler.await { database.extension_reposQueries.insert(
extension_reposQueries.insert(
backupRepo.baseUrl, backupRepo.baseUrl,
backupRepo.name, backupRepo.name,
backupRepo.shortName, backupRepo.shortName,
@@ -36,5 +35,4 @@ class ExtensionRepoRestorer(
) )
} }
} }
}
} }
@@ -1,12 +1,15 @@
package eu.kanade.tachiyomi.data.backup.restore.restorers package eu.kanade.tachiyomi.data.backup.restore.restorers
import app.cash.sqldelight.async.coroutines.awaitAsList
import app.cash.sqldelight.async.coroutines.awaitAsOne
import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull
import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.tachiyomi.data.backup.models.BackupCategory import eu.kanade.tachiyomi.data.backup.models.BackupCategory
import eu.kanade.tachiyomi.data.backup.models.BackupChapter import eu.kanade.tachiyomi.data.backup.models.BackupChapter
import eu.kanade.tachiyomi.data.backup.models.BackupHistory import eu.kanade.tachiyomi.data.backup.models.BackupHistory
import eu.kanade.tachiyomi.data.backup.models.BackupManga import eu.kanade.tachiyomi.data.backup.models.BackupManga
import eu.kanade.tachiyomi.data.backup.models.BackupTracking import eu.kanade.tachiyomi.data.backup.models.BackupTracking
import tachiyomi.data.DatabaseHandler import tachiyomi.data.Database
import tachiyomi.data.UpdateStrategyColumnAdapter import tachiyomi.data.UpdateStrategyColumnAdapter
import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
@@ -24,7 +27,7 @@ import java.util.Date
import kotlin.math.max import kotlin.math.max
class MangaRestorer( class MangaRestorer(
private val handler: DatabaseHandler = Injekt.get(), private val database: Database = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(), private val getCategories: GetCategories = Injekt.get(),
private val getMangaByUrlAndSourceId: GetMangaByUrlAndSourceId = Injekt.get(), private val getMangaByUrlAndSourceId: GetMangaByUrlAndSourceId = Injekt.get(),
private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(), private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(),
@@ -43,7 +46,9 @@ class MangaRestorer(
} }
suspend fun sortByNew(backupMangas: List<BackupManga>): List<BackupManga> { suspend fun sortByNew(backupMangas: List<BackupManga>): List<BackupManga> {
val urlsBySource = handler.awaitList { mangasQueries.getAllMangaSourceAndUrl() } val urlsBySource = database.mangasQueries
.getAllMangaSourceAndUrl()
.awaitAsList()
.groupBy({ it.source }, { it.url }) .groupBy({ it.source }, { it.url })
return backupMangas return backupMangas
@@ -57,7 +62,7 @@ class MangaRestorer(
backupManga: BackupManga, backupManga: BackupManga,
backupCategories: List<BackupCategory>, backupCategories: List<BackupCategory>,
) { ) {
handler.await(inTransaction = true) { database.transaction {
val dbManga = findExistingManga(backupManga) val dbManga = findExistingManga(backupManga)
val manga = backupManga.getMangaImpl() val manga = backupManga.getMangaImpl()
val restoredManga = if (dbManga == null) { val restoredManga = if (dbManga == null) {
@@ -105,8 +110,7 @@ class MangaRestorer(
} }
private suspend fun updateManga(manga: Manga): Manga { private suspend fun updateManga(manga: Manga): Manga {
handler.await(true) { database.mangasQueries.update(
mangasQueries.update(
source = manga.source, source = manga.source,
url = manga.url, url = manga.url,
artist = manga.artist, artist = manga.artist,
@@ -131,7 +135,6 @@ class MangaRestorer(
isSyncing = 1, isSyncing = 1,
notes = manga.notes, notes = manga.notes,
) )
}
return manga return manga
} }
@@ -189,9 +192,9 @@ class MangaRestorer(
this.copy(id = 0L, mangaId = 0L, dateFetch = 0L, dateUpload = 0L, lastModifiedAt = 0L, version = 0L) this.copy(id = 0L, mangaId = 0L, dateFetch = 0L, dateUpload = 0L, lastModifiedAt = 0L, version = 0L)
private suspend fun insertNewChapters(chapters: List<Chapter>) { private suspend fun insertNewChapters(chapters: List<Chapter>) {
handler.await(true) { database.transaction {
chapters.forEach { chapter -> chapters.forEach { chapter ->
chaptersQueries.insert( database.chaptersQueries.insert(
chapter.mangaId, chapter.mangaId,
chapter.url, chapter.url,
chapter.name, chapter.name,
@@ -210,9 +213,9 @@ class MangaRestorer(
} }
private suspend fun updateExistingChapters(chapters: List<Chapter>) { private suspend fun updateExistingChapters(chapters: List<Chapter>) {
handler.await(true) { database.transaction {
chapters.forEach { chapter -> chapters.forEach { chapter ->
chaptersQueries.update( database.chaptersQueries.update(
mangaId = null, mangaId = null,
url = null, url = null,
name = null, name = null,
@@ -238,8 +241,7 @@ class MangaRestorer(
* @return id of [Manga], null if not found * @return id of [Manga], null if not found
*/ */
private suspend fun insertManga(manga: Manga): Long { private suspend fun insertManga(manga: Manga): Long {
return handler.awaitOneExecutable(true) { return database.mangasQueries.insert(
mangasQueries.insert(
source = manga.source, source = manga.source,
url = manga.url, url = manga.url,
artist = manga.artist, artist = manga.artist,
@@ -262,8 +264,7 @@ class MangaRestorer(
version = manga.version, version = manga.version,
notes = manga.notes, notes = manga.notes,
) )
mangasQueries.selectLastInsertedRowId() .awaitAsOne()
}
} }
private suspend fun restoreMangaDetails( private suspend fun restoreMangaDetails(
@@ -309,10 +310,10 @@ class MangaRestorer(
} }
if (mangaCategoriesToUpdate.isNotEmpty()) { if (mangaCategoriesToUpdate.isNotEmpty()) {
handler.await(true) { database.transaction {
mangas_categoriesQueries.deleteMangaCategoryByMangaId(manga.id) database.mangas_categoriesQueries.deleteMangaCategoryByMangaId(manga.id)
mangaCategoriesToUpdate.forEach { (mangaId, categoryId) -> mangaCategoriesToUpdate.forEach { (mangaId, categoryId) ->
mangas_categoriesQueries.insert(mangaId, categoryId) database.mangas_categoriesQueries.insert(mangaId, categoryId)
} }
} }
} }
@@ -320,11 +321,15 @@ class MangaRestorer(
private suspend fun restoreHistory(backupHistory: List<BackupHistory>) { private suspend fun restoreHistory(backupHistory: List<BackupHistory>) {
val toUpdate = backupHistory.mapNotNull { history -> val toUpdate = backupHistory.mapNotNull { history ->
val dbHistory = handler.awaitOneOrNull { historyQueries.getHistoryByChapterUrl(history.url) } val dbHistory = database.historyQueries
.getHistoryByChapterUrl(history.url)
.awaitAsOneOrNull()
val item = history.getHistoryImpl() val item = history.getHistoryImpl()
if (dbHistory == null) { if (dbHistory == null) {
val chapter = handler.awaitOneOrNull { chaptersQueries.getChapterByUrl(history.url) } val chapter = database.chaptersQueries
.getChapterByUrl(history.url)
.awaitAsOneOrNull()
return@mapNotNull if (chapter == null) { return@mapNotNull if (chapter == null) {
// Chapter doesn't exist; skip // Chapter doesn't exist; skip
null null
@@ -345,10 +350,10 @@ class MangaRestorer(
) )
} }
if (toUpdate.isNotEmpty()) { if (toUpdate.isEmpty()) return
handler.await(true) { database.transaction {
toUpdate.forEach { toUpdate.forEach {
historyQueries.upsert( database.historyQueries.upsert(
it.chapterId, it.chapterId,
it.readAt, it.readAt,
it.readDuration, it.readDuration,
@@ -356,7 +361,6 @@ class MangaRestorer(
} }
} }
} }
}
private suspend fun restoreTracking(manga: Manga, backupTracks: List<BackupTracking>) { private suspend fun restoreTracking(manga: Manga, backupTracks: List<BackupTracking>) {
val dbTrackByTrackerId = getTracks.await(manga.id).associateBy { it.trackerId } val dbTrackByTrackerId = getTracks.await(manga.id).associateBy { it.trackerId }
@@ -388,10 +392,11 @@ class MangaRestorer(
if (newTracks.isNotEmpty()) { if (newTracks.isNotEmpty()) {
insertTrack.awaitAll(newTracks) insertTrack.awaitAll(newTracks)
} }
if (existingTracks.isNotEmpty()) {
handler.await(true) { if (existingTracks.isEmpty()) return
database.transaction {
existingTracks.forEach { track -> existingTracks.forEach { track ->
manga_syncQueries.update( database.manga_syncQueries.update(
track.mangaId, track.mangaId,
track.trackerId, track.trackerId,
track.remoteId, track.remoteId,
@@ -410,7 +415,6 @@ class MangaRestorer(
} }
} }
} }
}
private fun Track.forComparison() = this.copy(id = 0L, mangaId = 0L) private fun Track.forComparison() = this.copy(id = 0L, mangaId = 0L)
@@ -422,16 +426,11 @@ class MangaRestorer(
*/ */
private suspend fun restoreExcludedScanlators(manga: Manga, excludedScanlators: List<String>) { private suspend fun restoreExcludedScanlators(manga: Manga, excludedScanlators: List<String>) {
if (excludedScanlators.isEmpty()) return if (excludedScanlators.isEmpty()) return
val existingExcludedScanlators = handler.awaitList { val existingExcludedScanlators = database.excluded_scanlatorsQueries
excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(manga.id) .getExcludedScanlatorsByMangaId(manga.id)
} .awaitAsList()
val toInsert = excludedScanlators.filter { it !in existingExcludedScanlators } val toInsert = excludedScanlators.filter { it !in existingExcludedScanlators }
if (toInsert.isNotEmpty()) { if (toInsert.isEmpty()) return
handler.await { toInsert.forEach { database.excluded_scanlatorsQueries.insert(manga.id, it) }
toInsert.forEach {
excluded_scanlatorsQueries.insert(manga.id, it)
}
}
}
} }
} }
@@ -26,9 +26,7 @@ import nl.adaptivity.xmlutil.XmlDeclMode
import nl.adaptivity.xmlutil.core.XmlVersion import nl.adaptivity.xmlutil.core.XmlVersion
import nl.adaptivity.xmlutil.serialization.XML import nl.adaptivity.xmlutil.serialization.XML
import tachiyomi.core.common.storage.AndroidStorageFolderProvider import tachiyomi.core.common.storage.AndroidStorageFolderProvider
import tachiyomi.data.AndroidDatabaseHandler
import tachiyomi.data.Database import tachiyomi.data.Database
import tachiyomi.data.DatabaseHandler
import tachiyomi.data.DateColumnAdapter import tachiyomi.data.DateColumnAdapter
import tachiyomi.data.History import tachiyomi.data.History
import tachiyomi.data.Mangas import tachiyomi.data.Mangas
@@ -81,7 +79,6 @@ class AppModule(val app: Application) : InjektModule {
), ),
) )
} }
addSingletonFactory<DatabaseHandler> { AndroidDatabaseHandler(get(), get()) }
addSingletonFactory { addSingletonFactory {
Json { Json {
+1
View File
@@ -15,6 +15,7 @@ android {
packageName.set("tachiyomi.data") packageName.set("tachiyomi.data")
dialect(libs.sqldelight.sqliteDialect338) dialect(libs.sqldelight.sqliteDialect338)
schemaOutputDirectory.set(project.file("./src/main/sqldelight")) schemaOutputDirectory.set(project.file("./src/main/sqldelight"))
generateAsync.set(true)
} }
} }
} }
@@ -1,36 +1,49 @@
package mihon.data.repository package mihon.data.repository
import android.database.SQLException import android.database.SQLException
import app.cash.sqldelight.async.coroutines.awaitAsList
import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import mihon.domain.extensionrepo.exception.SaveExtensionRepoException import mihon.domain.extensionrepo.exception.SaveExtensionRepoException
import mihon.domain.extensionrepo.model.ExtensionRepo import mihon.domain.extensionrepo.model.ExtensionRepo
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
import tachiyomi.data.DatabaseHandler import tachiyomi.data.Database
import tachiyomi.data.subscribeToList
import tachiyomi.data.subscribeToOne
class ExtensionRepoRepositoryImpl( class ExtensionRepoRepositoryImpl(
private val handler: DatabaseHandler, private val database: Database,
) : ExtensionRepoRepository { ) : ExtensionRepoRepository {
override fun subscribeAll(): Flow<List<ExtensionRepo>> { override fun subscribeAll(): Flow<List<ExtensionRepo>> {
return handler.subscribeToList { extension_reposQueries.findAll(::mapExtensionRepo) } return database.extension_reposQueries
.findAll(::mapExtensionRepo)
.subscribeToList()
} }
override suspend fun getAll(): List<ExtensionRepo> { override suspend fun getAll(): List<ExtensionRepo> {
return handler.awaitList { extension_reposQueries.findAll(::mapExtensionRepo) } return database.extension_reposQueries
.findAll(::mapExtensionRepo)
.awaitAsList()
} }
override suspend fun getRepo(baseUrl: String): ExtensionRepo? { override suspend fun getRepo(baseUrl: String): ExtensionRepo? {
return handler.awaitOneOrNull { extension_reposQueries.findOne(baseUrl, ::mapExtensionRepo) } return database.extension_reposQueries
.findOne(baseUrl, ::mapExtensionRepo)
.awaitAsOneOrNull()
} }
override suspend fun getRepoBySigningKeyFingerprint(fingerprint: String): ExtensionRepo? { override suspend fun getRepoBySigningKeyFingerprint(fingerprint: String): ExtensionRepo? {
return handler.awaitOneOrNull { return database.extension_reposQueries
extension_reposQueries.findOneBySigningKeyFingerprint(fingerprint, ::mapExtensionRepo) .findOneBySigningKeyFingerprint(fingerprint, ::mapExtensionRepo)
} .awaitAsOneOrNull()
} }
override fun getCount(): Flow<Int> { override fun getCount(): Flow<Int> {
return handler.subscribeToOne { extension_reposQueries.count() }.map { it.toInt() } return database.extension_reposQueries
.count()
.subscribeToOne()
.map { it.toInt() }
} }
override suspend fun insertRepo( override suspend fun insertRepo(
@@ -41,7 +54,13 @@ class ExtensionRepoRepositoryImpl(
signingKeyFingerprint: String, signingKeyFingerprint: String,
) { ) {
try { try {
handler.await { extension_reposQueries.insert(baseUrl, name, shortName, website, signingKeyFingerprint) } database.extension_reposQueries.insert(
baseUrl,
name,
shortName,
website,
signingKeyFingerprint,
)
} catch (ex: SQLException) { } catch (ex: SQLException) {
throw SaveExtensionRepoException(ex) throw SaveExtensionRepoException(ex)
} }
@@ -55,15 +74,20 @@ class ExtensionRepoRepositoryImpl(
signingKeyFingerprint: String, signingKeyFingerprint: String,
) { ) {
try { try {
handler.await { extension_reposQueries.upsert(baseUrl, name, shortName, website, signingKeyFingerprint) } database.extension_reposQueries.upsert(
baseUrl,
name,
shortName,
website,
signingKeyFingerprint,
)
} catch (ex: SQLException) { } catch (ex: SQLException) {
throw SaveExtensionRepoException(ex) throw SaveExtensionRepoException(ex)
} }
} }
override suspend fun replaceRepo(newRepo: ExtensionRepo) { override suspend fun replaceRepo(newRepo: ExtensionRepo) {
handler.await { database.extension_reposQueries.replace(
extension_reposQueries.replace(
newRepo.baseUrl, newRepo.baseUrl,
newRepo.name, newRepo.name,
newRepo.shortName, newRepo.shortName,
@@ -71,10 +95,9 @@ class ExtensionRepoRepositoryImpl(
newRepo.signingKeyFingerprint, newRepo.signingKeyFingerprint,
) )
} }
}
override suspend fun deleteRepo(baseUrl: String) { override suspend fun deleteRepo(baseUrl: String) {
return handler.await { extension_reposQueries.delete(baseUrl) } database.extension_reposQueries.delete(baseUrl)
} }
private fun mapExtensionRepo( private fun mapExtensionRepo(
@@ -1,104 +0,0 @@
package tachiyomi.data
import androidx.paging.PagingSource
import app.cash.sqldelight.ExecutableQuery
import app.cash.sqldelight.Query
import app.cash.sqldelight.coroutines.asFlow
import app.cash.sqldelight.coroutines.mapToList
import app.cash.sqldelight.coroutines.mapToOne
import app.cash.sqldelight.coroutines.mapToOneOrNull
import app.cash.sqldelight.db.SqlDriver
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
class AndroidDatabaseHandler(
val db: Database,
private val driver: SqlDriver,
val queryDispatcher: CoroutineDispatcher = Dispatchers.IO,
val transactionDispatcher: CoroutineDispatcher = queryDispatcher,
) : DatabaseHandler {
val suspendingTransactionId = ThreadLocal<Int>()
override suspend fun <T> await(inTransaction: Boolean, block: suspend Database.() -> T): T {
return dispatch(inTransaction, block)
}
override suspend fun <T : Any> awaitList(
inTransaction: Boolean,
block: suspend Database.() -> Query<T>,
): List<T> {
return dispatch(inTransaction) { block(db).executeAsList() }
}
override suspend fun <T : Any> awaitOne(
inTransaction: Boolean,
block: suspend Database.() -> Query<T>,
): T {
return dispatch(inTransaction) { block(db).executeAsOne() }
}
override suspend fun <T : Any> awaitOneExecutable(
inTransaction: Boolean,
block: suspend Database.() -> ExecutableQuery<T>,
): T {
return dispatch(inTransaction) { block(db).executeAsOne() }
}
override suspend fun <T : Any> awaitOneOrNull(
inTransaction: Boolean,
block: suspend Database.() -> Query<T>,
): T? {
return dispatch(inTransaction) { block(db).executeAsOneOrNull() }
}
override suspend fun <T : Any> awaitOneOrNullExecutable(
inTransaction: Boolean,
block: suspend Database.() -> ExecutableQuery<T>,
): T? {
return dispatch(inTransaction) { block(db).executeAsOneOrNull() }
}
override fun <T : Any> subscribeToList(block: Database.() -> Query<T>): Flow<List<T>> {
return block(db).asFlow().mapToList(queryDispatcher)
}
override fun <T : Any> subscribeToOne(block: Database.() -> Query<T>): Flow<T> {
return block(db).asFlow().mapToOne(queryDispatcher)
}
override fun <T : Any> subscribeToOneOrNull(block: Database.() -> Query<T>): Flow<T?> {
return block(db).asFlow().mapToOneOrNull(queryDispatcher)
}
override fun <T : Any> subscribeToPagingSource(
countQuery: Database.() -> Query<Long>,
queryProvider: Database.(Long, Long) -> Query<T>,
): PagingSource<Long, T> {
return QueryPagingSource(
handler = this,
countQuery = countQuery,
queryProvider = { limit, offset ->
queryProvider.invoke(db, limit, offset)
},
)
}
private suspend fun <T> dispatch(inTransaction: Boolean, block: suspend Database.() -> T): T {
// Create a transaction if needed and run the calling block inside it.
if (inTransaction) {
return withTransaction { block(db) }
}
// If we're currently in the transaction thread, there's no need to dispatch our query.
if (driver.currentTransaction() != null) {
return block(db)
}
// Get the current database context and run the calling block.
val context = getCurrentDatabaseContext()
return withContext(context) { block(db) }
}
}
@@ -1,47 +0,0 @@
package tachiyomi.data
import androidx.paging.PagingSource
import app.cash.sqldelight.ExecutableQuery
import app.cash.sqldelight.Query
import kotlinx.coroutines.flow.Flow
interface DatabaseHandler {
suspend fun <T> await(inTransaction: Boolean = false, block: suspend Database.() -> T): T
suspend fun <T : Any> awaitList(
inTransaction: Boolean = false,
block: suspend Database.() -> Query<T>,
): List<T>
suspend fun <T : Any> awaitOne(
inTransaction: Boolean = false,
block: suspend Database.() -> Query<T>,
): T
suspend fun <T : Any> awaitOneExecutable(
inTransaction: Boolean = false,
block: suspend Database.() -> ExecutableQuery<T>,
): T
suspend fun <T : Any> awaitOneOrNull(
inTransaction: Boolean = false,
block: suspend Database.() -> Query<T>,
): T?
suspend fun <T : Any> awaitOneOrNullExecutable(
inTransaction: Boolean = false,
block: suspend Database.() -> ExecutableQuery<T>,
): T?
fun <T : Any> subscribeToList(block: Database.() -> Query<T>): Flow<List<T>>
fun <T : Any> subscribeToOne(block: Database.() -> Query<T>): Flow<T>
fun <T : Any> subscribeToOneOrNull(block: Database.() -> Query<T>): Flow<T?>
fun <T : Any> subscribeToPagingSource(
countQuery: Database.() -> Query<Long>,
queryProvider: Database.(Long, Long) -> Query<T>,
): PagingSource<Long, T>
}
@@ -0,0 +1,36 @@
package tachiyomi.data
import app.cash.sqldelight.Query
import app.cash.sqldelight.async.coroutines.awaitAsList
import app.cash.sqldelight.async.coroutines.awaitAsOne
import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull
import app.cash.sqldelight.coroutines.asFlow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
fun <T : Any> Query<T>.subscribeToList(
context: CoroutineContext = EmptyCoroutineContext,
): Flow<List<T>> = asFlow().map {
withContext(context) {
it.awaitAsList()
}
}
fun <T : Any> Query<T>.subscribeToOne(
context: CoroutineContext = EmptyCoroutineContext,
): Flow<T> = asFlow().map {
withContext(context) {
it.awaitAsOne()
}
}
fun <T : Any> Query<T>.subscribeToOneOrNull(
context: CoroutineContext = EmptyCoroutineContext,
): Flow<T?> = asFlow().map {
withContext(context) {
it.awaitAsOneOrNull()
}
}
@@ -3,12 +3,14 @@ package tachiyomi.data
import androidx.paging.PagingSource import androidx.paging.PagingSource
import androidx.paging.PagingState import androidx.paging.PagingState
import app.cash.sqldelight.Query import app.cash.sqldelight.Query
import app.cash.sqldelight.async.coroutines.awaitAsList
import app.cash.sqldelight.async.coroutines.awaitAsOne
import kotlin.properties.Delegates import kotlin.properties.Delegates
@Suppress("unused")
class QueryPagingSource<RowType : Any>( class QueryPagingSource<RowType : Any>(
val handler: DatabaseHandler, val countQuery: () -> Query<Long>,
val countQuery: Database.() -> Query<Long>, val queryProvider: (Long, Long) -> Query<RowType>,
val queryProvider: Database.(Long, Long) -> Query<RowType>,
) : PagingSource<Long, RowType>(), Query.Listener { ) : PagingSource<Long, RowType>(), Query.Listener {
override val jumpingSupported: Boolean = true override val jumpingSupported: Boolean = true
@@ -29,17 +31,16 @@ class QueryPagingSource<RowType : Any>(
try { try {
val key = params.key ?: 0L val key = params.key ?: 0L
val loadSize = params.loadSize val loadSize = params.loadSize
val count = handler.awaitOne { countQuery() } val count = countQuery().awaitAsOne()
val (offset, limit) = when (params) { val (offset, limit) = when (params) {
is LoadParams.Prepend -> key - loadSize to loadSize.toLong() is LoadParams.Prepend -> key - loadSize to loadSize.toLong()
else -> key to loadSize.toLong() else -> key to loadSize.toLong()
} }
val data = handler.awaitList { val data = queryProvider(limit, offset)
queryProvider(limit, offset)
.also { currentQuery = it } .also { currentQuery = it }
} .awaitAsList()
val (prevKey, nextKey) = when (params) { val (prevKey, nextKey) = when (params) {
is LoadParams.Append -> (offset - loadSize to offset + loadSize) is LoadParams.Append -> (offset - loadSize to offset + loadSize)
@@ -1,165 +0,0 @@
package tachiyomi.data
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Job
import kotlinx.coroutines.asContextElement
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import java.util.concurrent.RejectedExecutionException
import kotlin.concurrent.atomics.AtomicInt
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlin.concurrent.atomics.decrementAndFetch
import kotlin.concurrent.atomics.incrementAndFetch
import kotlin.coroutines.ContinuationInterceptor
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.coroutineContext
import kotlin.coroutines.resume
/**
* Returns the transaction dispatcher if we are on a transaction, or the database dispatchers.
*/
internal suspend fun AndroidDatabaseHandler.getCurrentDatabaseContext(): CoroutineContext {
return coroutineContext[TransactionElement]?.transactionDispatcher ?: queryDispatcher
}
/**
* Calls the specified suspending [block] in a database transaction. The transaction will be
* marked as successful unless an exception is thrown in the suspending [block] or the coroutine
* is cancelled.
*
* SQLDelight will only perform at most one transaction at a time, additional transactions are queued
* and executed on a first come, first serve order.
*
* Performing blocking database operations is not permitted in a coroutine scope other than the
* one received by the suspending block. It is recommended that all [Dao] function invoked within
* the [block] be suspending functions.
*
* The dispatcher used to execute the given [block] will utilize threads from SQLDelight's query executor.
*/
internal suspend fun <T> AndroidDatabaseHandler.withTransaction(block: suspend () -> T): T {
// Use inherited transaction context if available, this allows nested suspending transactions.
val transactionContext =
coroutineContext[TransactionElement]?.transactionDispatcher ?: createTransactionContext()
return withContext(transactionContext) {
val transactionElement = coroutineContext[TransactionElement]!!
transactionElement.acquire()
try {
db.transactionWithResult {
runBlocking(transactionContext) {
block()
}
}
} finally {
transactionElement.release()
}
}
}
/**
* Creates a [CoroutineContext] for performing database operations within a coroutine transaction.
*
* The context is a combination of a dispatcher, a [TransactionElement] and a thread local element.
*
* * The dispatcher will dispatch coroutines to a single thread that is taken over from the SQLDelight
* query executor. If the coroutine context is switched, suspending DAO functions will be able to
* dispatch to the transaction thread.
*
* * The [TransactionElement] serves as an indicator for inherited context, meaning, if there is a
* switch of context, suspending DAO methods will be able to use the indicator to dispatch the
* database operation to the transaction thread.
*
* * The thread local element serves as a second indicator and marks threads that are used to
* execute coroutines within the coroutine transaction, more specifically it allows us to identify
* if a blocking DAO method is invoked within the transaction coroutine. Never assign meaning to
* this value, for now all we care is if its present or not.
*/
private suspend fun AndroidDatabaseHandler.createTransactionContext(): CoroutineContext {
val controlJob = Job()
// make sure to tie the control job to this context to avoid blocking the transaction if
// context get cancelled before we can even start using this job. Otherwise, the acquired
// transaction thread will forever wait for the controlJob to be cancelled.
// see b/148181325
coroutineContext[Job]?.invokeOnCompletion {
controlJob.cancel()
}
val dispatcher = transactionDispatcher.acquireTransactionThread(controlJob)
val transactionElement = TransactionElement(controlJob, dispatcher)
val threadLocalElement =
suspendingTransactionId.asContextElement(System.identityHashCode(controlJob))
return dispatcher + transactionElement + threadLocalElement
}
/**
* Acquires a thread from the executor and returns a [ContinuationInterceptor] to dispatch
* coroutines to the acquired thread. The [controlJob] is used to control the release of the
* thread by cancelling the job.
*/
private suspend fun CoroutineDispatcher.acquireTransactionThread(
controlJob: Job,
): ContinuationInterceptor {
return suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation {
// We got cancelled while waiting to acquire a thread, we can't stop our attempt to
// acquire a thread, but we can cancel the controlling job so once it gets acquired it
// is quickly released.
controlJob.cancel()
}
try {
dispatch(EmptyCoroutineContext) {
runBlocking {
// Thread acquired, resume coroutine
continuation.resume(coroutineContext[ContinuationInterceptor]!!)
controlJob.join()
}
}
} catch (ex: RejectedExecutionException) {
// Couldn't acquire a thread, cancel coroutine
continuation.cancel(
IllegalStateException(
"Unable to acquire a thread to perform the database transaction",
ex,
),
)
}
}
}
/**
* A [CoroutineContext.Element] that indicates there is an on-going database transaction.
*/
@OptIn(ExperimentalAtomicApi::class)
private class TransactionElement(
private val transactionThreadControlJob: Job,
val transactionDispatcher: ContinuationInterceptor,
) : CoroutineContext.Element {
companion object Key : CoroutineContext.Key<TransactionElement>
override val key: CoroutineContext.Key<TransactionElement>
get() = TransactionElement
/**
* Number of transactions (including nested ones) started with this element.
* Call [acquire] to increase the count and [release] to decrease it. If the count reaches zero
* when [release] is invoked then the transaction job is cancelled and the transaction thread
* is released.
*/
private val referenceCount = AtomicInt(0)
fun acquire() {
referenceCount.incrementAndFetch()
}
fun release() {
val count = referenceCount.decrementAndFetch()
if (count < 0) {
throw IllegalStateException("Transaction was never started or was already released")
} else if (count == 0) {
// Cancel the job that controls the transaction thread, causing it to be released.
transactionThreadControlJob.cancel()
}
}
}
@@ -1,66 +1,58 @@
package tachiyomi.data.category package tachiyomi.data.category
import app.cash.sqldelight.async.coroutines.awaitAsList
import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import tachiyomi.data.Database import tachiyomi.data.Database
import tachiyomi.data.DatabaseHandler import tachiyomi.data.subscribeToList
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
import tachiyomi.domain.category.model.CategoryUpdate import tachiyomi.domain.category.model.CategoryUpdate
import tachiyomi.domain.category.repository.CategoryRepository import tachiyomi.domain.category.repository.CategoryRepository
class CategoryRepositoryImpl( class CategoryRepositoryImpl(
private val handler: DatabaseHandler, private val database: Database,
) : CategoryRepository { ) : CategoryRepository {
override suspend fun get(id: Long): Category? { override suspend fun get(id: Long): Category? {
return handler.awaitOneOrNull { categoriesQueries.getCategory(id, ::mapCategory) } return database.categoriesQueries
.getCategory(id, ::mapCategory)
.awaitAsOneOrNull()
} }
override suspend fun getAll(): List<Category> { override suspend fun getAll(): List<Category> {
return handler.awaitList { categoriesQueries.getCategories(::mapCategory) } return database.categoriesQueries
.getCategories(::mapCategory)
.awaitAsList()
} }
override fun getAllAsFlow(): Flow<List<Category>> { override fun getAllAsFlow(): Flow<List<Category>> {
return handler.subscribeToList { categoriesQueries.getCategories(::mapCategory) } return database.categoriesQueries
.getCategories(::mapCategory)
.subscribeToList()
} }
override suspend fun getCategoriesByMangaId(mangaId: Long): List<Category> { override suspend fun getCategoriesByMangaId(mangaId: Long): List<Category> {
return handler.awaitList { return database.categoriesQueries
categoriesQueries.getCategoriesByMangaId(mangaId, ::mapCategory) .getCategoriesByMangaId(mangaId, ::mapCategory)
} .awaitAsList()
} }
override fun getCategoriesByMangaIdAsFlow(mangaId: Long): Flow<List<Category>> { override fun getCategoriesByMangaIdAsFlow(mangaId: Long): Flow<List<Category>> {
return handler.subscribeToList { return database.categoriesQueries
categoriesQueries.getCategoriesByMangaId(mangaId, ::mapCategory) .getCategoriesByMangaId(mangaId, ::mapCategory)
} .subscribeToList()
} }
override suspend fun insert(category: Category) { override suspend fun insert(category: Category) {
handler.await { database.categoriesQueries.insert(
categoriesQueries.insert(
name = category.name, name = category.name,
order = category.order, order = category.order,
flags = category.flags, flags = category.flags,
) )
} }
}
override suspend fun updatePartial(update: CategoryUpdate) { override suspend fun updatePartial(update: CategoryUpdate) {
handler.await { database.categoriesQueries.update(
updatePartialBlocking(update)
}
}
override suspend fun updatePartial(updates: List<CategoryUpdate>) {
handler.await(inTransaction = true) {
for (update in updates) {
updatePartialBlocking(update)
}
}
}
private fun Database.updatePartialBlocking(update: CategoryUpdate) {
categoriesQueries.update(
name = update.name, name = update.name,
order = update.order, order = update.order,
flags = update.flags, flags = update.flags,
@@ -68,18 +60,18 @@ class CategoryRepositoryImpl(
) )
} }
override suspend fun updateAllFlags(flags: Long?) { override suspend fun updatePartial(updates: List<CategoryUpdate>) {
handler.await { database.transaction {
categoriesQueries.updateAllFlags(flags) updates.forEach { updatePartial(it) }
} }
} }
override suspend fun delete(categoryId: Long) { override suspend fun updateAllFlags(flags: Long?) {
handler.await { database.categoriesQueries.updateAllFlags(flags)
categoriesQueries.delete(
categoryId = categoryId,
)
} }
override suspend fun delete(categoryId: Long) {
database.categoriesQueries.delete(categoryId = categoryId)
} }
private fun mapCategory( private fun mapCategory(
@@ -1,23 +1,27 @@
package tachiyomi.data.chapter package tachiyomi.data.chapter
import app.cash.sqldelight.async.coroutines.awaitAsList
import app.cash.sqldelight.async.coroutines.awaitAsOne
import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.common.util.lang.toLong import tachiyomi.core.common.util.lang.toLong
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.data.DatabaseHandler import tachiyomi.data.Database
import tachiyomi.data.subscribeToList
import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.chapter.model.ChapterUpdate import tachiyomi.domain.chapter.model.ChapterUpdate
import tachiyomi.domain.chapter.repository.ChapterRepository import tachiyomi.domain.chapter.repository.ChapterRepository
class ChapterRepositoryImpl( class ChapterRepositoryImpl(
private val handler: DatabaseHandler, private val database: Database,
) : ChapterRepository { ) : ChapterRepository {
override suspend fun addAll(chapters: List<Chapter>): List<Chapter> { override suspend fun addAll(chapters: List<Chapter>): List<Chapter> {
return try { return try {
handler.await(inTransaction = true) { database.transactionWithResult {
chapters.map { chapter -> chapters.map { chapter ->
chaptersQueries.insert( val lastInsertId = database.chaptersQueries.insert(
chapter.mangaId, chapter.mangaId,
chapter.url, chapter.url,
chapter.name, chapter.name,
@@ -30,8 +34,7 @@ class ChapterRepositoryImpl(
chapter.dateFetch, chapter.dateFetch,
chapter.dateUpload, chapter.dateUpload,
chapter.version, chapter.version,
) ).awaitAsOne()
val lastInsertId = chaptersQueries.selectLastInsertedRowId().executeAsOne()
chapter.copy(id = lastInsertId) chapter.copy(id = lastInsertId)
} }
} }
@@ -50,9 +53,9 @@ class ChapterRepositoryImpl(
} }
private suspend fun partialUpdate(vararg chapterUpdates: ChapterUpdate) { private suspend fun partialUpdate(vararg chapterUpdates: ChapterUpdate) {
handler.await(inTransaction = true) { database.transaction {
chapterUpdates.forEach { chapterUpdate -> chapterUpdates.forEach { chapterUpdate ->
chaptersQueries.update( database.chaptersQueries.update(
mangaId = chapterUpdate.mangaId, mangaId = chapterUpdate.mangaId,
url = chapterUpdate.url, url = chapterUpdate.url,
name = chapterUpdate.name, name = chapterUpdate.name,
@@ -74,59 +77,55 @@ class ChapterRepositoryImpl(
override suspend fun removeChaptersWithIds(chapterIds: List<Long>) { override suspend fun removeChaptersWithIds(chapterIds: List<Long>) {
try { try {
handler.await { chaptersQueries.removeChaptersWithIds(chapterIds) } database.chaptersQueries.removeChaptersWithIds(chapterIds)
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR, e) logcat(LogPriority.ERROR, e)
} }
} }
override suspend fun getChapterByMangaId(mangaId: Long, applyScanlatorFilter: Boolean): List<Chapter> { override suspend fun getChapterByMangaId(mangaId: Long, applyScanlatorFilter: Boolean): List<Chapter> {
return handler.awaitList { return database.chaptersQueries
chaptersQueries.getChaptersByMangaId(mangaId, applyScanlatorFilter.toLong(), ::mapChapter) .getChaptersByMangaId(mangaId, applyScanlatorFilter.toLong(), ::mapChapter)
} .awaitAsList()
} }
override suspend fun getScanlatorsByMangaId(mangaId: Long): List<String> { override suspend fun getScanlatorsByMangaId(mangaId: Long): List<String> {
return handler.awaitList { return database.chaptersQueries
chaptersQueries.getScanlatorsByMangaId(mangaId) { it.orEmpty() } .getScanlatorsByMangaId(mangaId) { it.orEmpty() }
} .awaitAsList()
} }
override fun getScanlatorsByMangaIdAsFlow(mangaId: Long): Flow<List<String>> { override fun getScanlatorsByMangaIdAsFlow(mangaId: Long): Flow<List<String>> {
return handler.subscribeToList { return database.chaptersQueries
chaptersQueries.getScanlatorsByMangaId(mangaId) { it.orEmpty() } .getScanlatorsByMangaId(mangaId) { it.orEmpty() }
} .subscribeToList()
} }
override suspend fun getBookmarkedChaptersByMangaId(mangaId: Long): List<Chapter> { override suspend fun getBookmarkedChaptersByMangaId(mangaId: Long): List<Chapter> {
return handler.awaitList { return database.chaptersQueries
chaptersQueries.getBookmarkedChaptersByMangaId( .getBookmarkedChaptersByMangaId(mangaId, ::mapChapter)
mangaId, .awaitAsList()
::mapChapter,
)
}
} }
override suspend fun getChapterById(id: Long): Chapter? { override suspend fun getChapterById(id: Long): Chapter? {
return handler.awaitOneOrNull { chaptersQueries.getChapterById(id, ::mapChapter) } return database.chaptersQueries
.getChapterById(id, ::mapChapter)
.awaitAsOneOrNull()
} }
override suspend fun getChapterByMangaIdAsFlow(mangaId: Long, applyScanlatorFilter: Boolean): Flow<List<Chapter>> { override suspend fun getChapterByMangaIdAsFlow(mangaId: Long, applyScanlatorFilter: Boolean): Flow<List<Chapter>> {
return handler.subscribeToList { return database.chaptersQueries
chaptersQueries.getChaptersByMangaId(mangaId, applyScanlatorFilter.toLong(), ::mapChapter) .getChaptersByMangaId(mangaId, applyScanlatorFilter.toLong(), ::mapChapter)
} .subscribeToList()
} }
override suspend fun getChapterByUrlAndMangaId(url: String, mangaId: Long): Chapter? { override suspend fun getChapterByUrlAndMangaId(url: String, mangaId: Long): Chapter? {
return handler.awaitOneOrNull { return database.chaptersQueries
chaptersQueries.getChapterByUrlAndMangaId( .getChapterByUrlAndMangaId(url, mangaId, ::mapChapter)
url, .awaitAsOneOrNull()
mangaId,
::mapChapter,
)
}
} }
@Suppress("UNUSED_PARAMETER")
private fun mapChapter( private fun mapChapter(
id: Long, id: Long,
mangaId: Long, mangaId: Long,
@@ -142,7 +141,6 @@ class ChapterRepositoryImpl(
dateUpload: Long, dateUpload: Long,
lastModifiedAt: Long, lastModifiedAt: Long,
version: Long, version: Long,
@Suppress("UNUSED_PARAMETER")
isSyncing: Long, isSyncing: Long,
): Chapter = Chapter( ): Chapter = Chapter(
id = id, id = id,
@@ -1,41 +1,49 @@
package tachiyomi.data.history package tachiyomi.data.history
import app.cash.sqldelight.async.coroutines.awaitAsList
import app.cash.sqldelight.async.coroutines.awaitAsOne
import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.data.DatabaseHandler import tachiyomi.data.Database
import tachiyomi.data.subscribeToList
import tachiyomi.domain.history.model.History import tachiyomi.domain.history.model.History
import tachiyomi.domain.history.model.HistoryUpdate import tachiyomi.domain.history.model.HistoryUpdate
import tachiyomi.domain.history.model.HistoryWithRelations import tachiyomi.domain.history.model.HistoryWithRelations
import tachiyomi.domain.history.repository.HistoryRepository import tachiyomi.domain.history.repository.HistoryRepository
class HistoryRepositoryImpl( class HistoryRepositoryImpl(
private val handler: DatabaseHandler, private val database: Database,
) : HistoryRepository { ) : HistoryRepository {
override fun getHistory(query: String): Flow<List<HistoryWithRelations>> { override fun getHistory(query: String): Flow<List<HistoryWithRelations>> {
return handler.subscribeToList { return database.historyViewQueries
historyViewQueries.history(query, HistoryMapper::mapHistoryWithRelations) .history(query, HistoryMapper::mapHistoryWithRelations)
} .subscribeToList()
} }
override suspend fun getLastHistory(): HistoryWithRelations? { override suspend fun getLastHistory(): HistoryWithRelations? {
return handler.awaitOneOrNull { return database.historyViewQueries
historyViewQueries.getLatestHistory(HistoryMapper::mapHistoryWithRelations) .getLatestHistory(HistoryMapper::mapHistoryWithRelations)
} .awaitAsOneOrNull()
} }
override suspend fun getTotalReadDuration(): Long { override suspend fun getTotalReadDuration(): Long {
return handler.awaitOne { historyQueries.getReadDuration() } return database.historyQueries
.getReadDuration()
.awaitAsOne()
} }
override suspend fun getHistoryByMangaId(mangaId: Long): List<History> { override suspend fun getHistoryByMangaId(mangaId: Long): List<History> {
return handler.awaitList { historyQueries.getHistoryByMangaId(mangaId, HistoryMapper::mapHistory) } return database.historyQueries
.getHistoryByMangaId(mangaId, HistoryMapper::mapHistory)
.awaitAsList()
} }
override suspend fun resetHistory(historyId: Long) { override suspend fun resetHistory(historyId: Long) {
try { try {
handler.await { historyQueries.resetHistoryById(historyId) } database.historyQueries.resetHistoryById(historyId)
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR, throwable = e) logcat(LogPriority.ERROR, throwable = e)
} }
@@ -43,7 +51,7 @@ class HistoryRepositoryImpl(
override suspend fun resetHistoryByMangaId(mangaId: Long) { override suspend fun resetHistoryByMangaId(mangaId: Long) {
try { try {
handler.await { historyQueries.resetHistoryByMangaId(mangaId) } database.historyQueries.resetHistoryByMangaId(mangaId)
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR, throwable = e) logcat(LogPriority.ERROR, throwable = e)
} }
@@ -51,7 +59,7 @@ class HistoryRepositoryImpl(
override suspend fun deleteAllHistory(): Boolean { override suspend fun deleteAllHistory(): Boolean {
return try { return try {
handler.await { historyQueries.removeAllHistory() } database.historyQueries.removeAllHistory()
true true
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR, throwable = e) logcat(LogPriority.ERROR, throwable = e)
@@ -61,13 +69,11 @@ class HistoryRepositoryImpl(
override suspend fun upsertHistory(historyUpdate: HistoryUpdate) { override suspend fun upsertHistory(historyUpdate: HistoryUpdate) {
try { try {
handler.await { database.historyQueries.upsert(
historyQueries.upsert(
historyUpdate.chapterId, historyUpdate.chapterId,
historyUpdate.readAt, historyUpdate.readAt,
historyUpdate.sessionReadDuration, historyUpdate.sessionReadDuration,
) )
}
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR, throwable = e) logcat(LogPriority.ERROR, throwable = e)
} }
@@ -6,6 +6,7 @@ import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaWithChapterCount import tachiyomi.domain.manga.model.MangaWithChapterCount
object MangaMapper { object MangaMapper {
@Suppress("UNUSED_PARAMETER")
fun mapManga( fun mapManga(
id: Long, id: Long,
source: Long, source: Long,
@@ -30,7 +31,6 @@ object MangaMapper {
lastModifiedAt: Long, lastModifiedAt: Long,
favoriteModifiedAt: Long?, favoriteModifiedAt: Long?,
version: Long, version: Long,
@Suppress("UNUSED_PARAMETER")
isSyncing: Long, isSyncing: Long,
notes: String, notes: String,
): Manga = Manga( ): Manga = Manga(
@@ -1,11 +1,17 @@
package tachiyomi.data.manga package tachiyomi.data.manga
import app.cash.sqldelight.async.coroutines.awaitAsList
import app.cash.sqldelight.async.coroutines.awaitAsOne
import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.data.DatabaseHandler import tachiyomi.data.Database
import tachiyomi.data.StringListColumnAdapter import tachiyomi.data.StringListColumnAdapter
import tachiyomi.data.UpdateStrategyColumnAdapter import tachiyomi.data.UpdateStrategyColumnAdapter
import tachiyomi.data.subscribeToList
import tachiyomi.data.subscribeToOne
import tachiyomi.data.subscribeToOneOrNull
import tachiyomi.domain.library.model.LibraryManga import tachiyomi.domain.library.model.LibraryManga
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaUpdate import tachiyomi.domain.manga.model.MangaUpdate
@@ -15,73 +21,79 @@ import java.time.LocalDate
import java.time.ZoneId import java.time.ZoneId
class MangaRepositoryImpl( class MangaRepositoryImpl(
private val handler: DatabaseHandler, private val database: Database,
) : MangaRepository { ) : MangaRepository {
override suspend fun getMangaById(id: Long): Manga { override suspend fun getMangaById(id: Long): Manga {
return handler.awaitOne { mangasQueries.getMangaById(id, MangaMapper::mapManga) } return database.mangasQueries
.getMangaById(id, MangaMapper::mapManga)
.awaitAsOne()
} }
override suspend fun getMangaByIdAsFlow(id: Long): Flow<Manga> { override suspend fun getMangaByIdAsFlow(id: Long): Flow<Manga> {
return handler.subscribeToOne { mangasQueries.getMangaById(id, MangaMapper::mapManga) } return database.mangasQueries
.getMangaById(id, MangaMapper::mapManga)
.subscribeToOne()
} }
override suspend fun getMangaByUrlAndSourceId(url: String, sourceId: Long): Manga? { override suspend fun getMangaByUrlAndSourceId(url: String, sourceId: Long): Manga? {
return handler.awaitOneOrNull { return database.mangasQueries
mangasQueries.getMangaByUrlAndSource( .getMangaByUrlAndSource(url, sourceId, MangaMapper::mapManga)
url, .awaitAsOneOrNull()
sourceId,
MangaMapper::mapManga,
)
}
} }
override fun getMangaByUrlAndSourceIdAsFlow(url: String, sourceId: Long): Flow<Manga?> { override fun getMangaByUrlAndSourceIdAsFlow(url: String, sourceId: Long): Flow<Manga?> {
return handler.subscribeToOneOrNull { return database.mangasQueries
mangasQueries.getMangaByUrlAndSource( .getMangaByUrlAndSource(url, sourceId, MangaMapper::mapManga)
url, .subscribeToOneOrNull()
sourceId,
MangaMapper::mapManga,
)
}
} }
override suspend fun getFavorites(): List<Manga> { override suspend fun getFavorites(): List<Manga> {
return handler.awaitList { mangasQueries.getFavorites(MangaMapper::mapManga) } return database.mangasQueries
.getFavorites(MangaMapper::mapManga)
.awaitAsList()
} }
override suspend fun getReadMangaNotInLibrary(): List<Manga> { override suspend fun getReadMangaNotInLibrary(): List<Manga> {
return handler.awaitList { mangasQueries.getReadMangaNotInLibrary(MangaMapper::mapManga) } return database.mangasQueries
.getReadMangaNotInLibrary(MangaMapper::mapManga)
.awaitAsList()
} }
override suspend fun getLibraryManga(): List<LibraryManga> { override suspend fun getLibraryManga(): List<LibraryManga> {
return handler.awaitList { libraryViewQueries.library(MangaMapper::mapLibraryManga) } return database.libraryViewQueries
.library(MangaMapper::mapLibraryManga)
.awaitAsList()
} }
override fun getLibraryMangaAsFlow(): Flow<List<LibraryManga>> { override fun getLibraryMangaAsFlow(): Flow<List<LibraryManga>> {
return handler.subscribeToList { libraryViewQueries.library(MangaMapper::mapLibraryManga) } return database.libraryViewQueries
.library(MangaMapper::mapLibraryManga)
.subscribeToList()
} }
override fun getFavoritesBySourceId(sourceId: Long): Flow<List<Manga>> { override fun getFavoritesBySourceId(sourceId: Long): Flow<List<Manga>> {
return handler.subscribeToList { mangasQueries.getFavoriteBySourceId(sourceId, MangaMapper::mapManga) } return database.mangasQueries
.getFavoriteBySourceId(sourceId, MangaMapper::mapManga)
.subscribeToList()
} }
override suspend fun getDuplicateLibraryManga(id: Long, title: String): List<MangaWithChapterCount> { override suspend fun getDuplicateLibraryManga(id: Long, title: String): List<MangaWithChapterCount> {
return handler.awaitList { return database.mangasQueries
mangasQueries.getDuplicateLibraryManga(id, title, MangaMapper::mapMangaWithChapterCount) .getDuplicateLibraryManga(id, title, MangaMapper::mapMangaWithChapterCount)
} .awaitAsList()
} }
override suspend fun getUpcomingManga(statuses: Set<Long>): Flow<List<Manga>> { override suspend fun getUpcomingManga(statuses: Set<Long>): Flow<List<Manga>> {
val epochMillis = LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toEpochSecond() * 1000 val epochMillis = LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toEpochSecond() * 1000
return handler.subscribeToList { return database.mangasQueries
mangasQueries.getUpcomingManga(epochMillis, statuses, MangaMapper::mapManga) .getUpcomingManga(epochMillis, statuses, MangaMapper::mapManga)
} .subscribeToList()
} }
override suspend fun resetViewerFlags(): Boolean { override suspend fun resetViewerFlags(): Boolean {
return try { return try {
handler.await { mangasQueries.resetViewerFlags() } database.mangasQueries.resetViewerFlags()
true true
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR, e) logcat(LogPriority.ERROR, e)
@@ -90,10 +102,10 @@ class MangaRepositoryImpl(
} }
override suspend fun setMangaCategories(mangaId: Long, categoryIds: List<Long>) { override suspend fun setMangaCategories(mangaId: Long, categoryIds: List<Long>) {
handler.await(inTransaction = true) { database.transaction {
mangas_categoriesQueries.deleteMangaCategoryByMangaId(mangaId) database.mangas_categoriesQueries.deleteMangaCategoryByMangaId(mangaId)
categoryIds.map { categoryId -> categoryIds.forEach { categoryId ->
mangas_categoriesQueries.insert(mangaId, categoryId) database.mangas_categoriesQueries.insert(mangaId, categoryId)
} }
} }
} }
@@ -119,9 +131,9 @@ class MangaRepositoryImpl(
} }
override suspend fun insertNetworkManga(manga: List<Manga>): List<Manga> { override suspend fun insertNetworkManga(manga: List<Manga>): List<Manga> {
return handler.await(inTransaction = true) { return database.transactionWithResult {
manga.map { manga.map {
mangasQueries.insertNetworkManga( database.mangasQueries.insertNetworkManga(
source = it.source, source = it.source,
url = it.url, url = it.url,
artist = it.artist, artist = it.artist,
@@ -147,15 +159,15 @@ class MangaRepositoryImpl(
updateDetails = it.initialized, updateDetails = it.initialized,
mapper = MangaMapper::mapManga, mapper = MangaMapper::mapManga,
) )
.executeAsOne() .awaitAsOne()
} }
} }
} }
private suspend fun partialUpdate(vararg mangaUpdates: MangaUpdate) { private suspend fun partialUpdate(vararg mangaUpdates: MangaUpdate) {
handler.await(inTransaction = true) { database.transaction {
mangaUpdates.forEach { value -> mangaUpdates.forEach { value ->
mangasQueries.update( database.mangasQueries.update(
source = value.source, source = value.source,
url = value.url, url = value.url,
artist = value.artist, artist = value.artist,
@@ -7,7 +7,8 @@ import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import tachiyomi.data.DatabaseHandler import tachiyomi.data.Database
import tachiyomi.data.subscribeToList
import tachiyomi.domain.source.model.SourceWithCount import tachiyomi.domain.source.model.SourceWithCount
import tachiyomi.domain.source.model.StubSource import tachiyomi.domain.source.model.StubSource
import tachiyomi.domain.source.repository.SourcePagingSource import tachiyomi.domain.source.repository.SourcePagingSource
@@ -17,7 +18,7 @@ import tachiyomi.domain.source.model.Source as DomainSource
class SourceRepositoryImpl( class SourceRepositoryImpl(
private val sourceManager: SourceManager, private val sourceManager: SourceManager,
private val handler: DatabaseHandler, private val database: Database,
) : SourceRepository { ) : SourceRepository {
override fun getSources(): Flow<List<DomainSource>> { override fun getSources(): Flow<List<DomainSource>> {
@@ -39,10 +40,12 @@ class SourceRepositoryImpl(
} }
override fun getSourcesWithFavoriteCount(): Flow<List<Pair<DomainSource, Long>>> { override fun getSourcesWithFavoriteCount(): Flow<List<Pair<DomainSource, Long>>> {
return combine( val sourceIdWithFavoriteCountFlow = database.mangasQueries
handler.subscribeToList { mangasQueries.getSourceIdWithFavoriteCount() }, .getSourceIdWithFavoriteCount()
sourceManager.catalogueSources, .subscribeToList()
) { sourceIdWithFavoriteCount, _ -> sourceIdWithFavoriteCount } return combine(sourceIdWithFavoriteCountFlow, sourceManager.catalogueSources) { sourceIdWithFavoriteCount, _ ->
sourceIdWithFavoriteCount
}
.map { .map {
it.map { (sourceId, count) -> it.map { (sourceId, count) ->
val source = sourceManager.getOrStub(sourceId) val source = sourceManager.getOrStub(sourceId)
@@ -55,9 +58,10 @@ class SourceRepositoryImpl(
} }
override fun getSourcesWithNonLibraryManga(): Flow<List<SourceWithCount>> { override fun getSourcesWithNonLibraryManga(): Flow<List<SourceWithCount>> {
val sourceIdWithNonLibraryManga = return database.mangasQueries
handler.subscribeToList { mangasQueries.getSourceIdsWithNonLibraryManga() } .getSourceIdsWithNonLibraryManga()
return sourceIdWithNonLibraryManga.map { sourceId -> .subscribeToList()
.map { sourceId ->
sourceId.map { (sourceId, count) -> sourceId.map { (sourceId, count) ->
val source = sourceManager.getOrStub(sourceId) val source = sourceManager.getOrStub(sourceId)
val domainSource = mapSourceToDomainSource(source).copy( val domainSource = mapSourceToDomainSource(source).copy(
@@ -1,24 +1,30 @@
package tachiyomi.data.source package tachiyomi.data.source
import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import tachiyomi.data.DatabaseHandler import tachiyomi.data.Database
import tachiyomi.data.subscribeToList
import tachiyomi.domain.source.model.StubSource import tachiyomi.domain.source.model.StubSource
import tachiyomi.domain.source.repository.StubSourceRepository import tachiyomi.domain.source.repository.StubSourceRepository
class StubSourceRepositoryImpl( class StubSourceRepositoryImpl(
private val handler: DatabaseHandler, private val database: Database,
) : StubSourceRepository { ) : StubSourceRepository {
override fun subscribeAll(): Flow<List<StubSource>> { override fun subscribeAll(): Flow<List<StubSource>> {
return handler.subscribeToList { sourcesQueries.findAll(::mapStubSource) } return database.sourcesQueries
.findAll(::mapStubSource)
.subscribeToList()
} }
override suspend fun getStubSource(id: Long): StubSource? { override suspend fun getStubSource(id: Long): StubSource? {
return handler.awaitOneOrNull { sourcesQueries.findOne(id, ::mapStubSource) } return database.sourcesQueries
.findOne(id, ::mapStubSource)
.awaitAsOneOrNull()
} }
override suspend fun upsertStubSource(id: Long, lang: String, name: String) { override suspend fun upsertStubSource(id: Long, lang: String, name: String) {
handler.await { sourcesQueries.upsert(id, lang, name) } database.sourcesQueries.upsert(id, lang, name)
} }
private fun mapStubSource( private fun mapStubSource(
@@ -1,44 +1,47 @@
package tachiyomi.data.track package tachiyomi.data.track
import app.cash.sqldelight.async.coroutines.awaitAsList
import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import tachiyomi.data.DatabaseHandler import tachiyomi.data.Database
import tachiyomi.data.subscribeToList
import tachiyomi.domain.track.model.Track import tachiyomi.domain.track.model.Track
import tachiyomi.domain.track.repository.TrackRepository import tachiyomi.domain.track.repository.TrackRepository
class TrackRepositoryImpl( class TrackRepositoryImpl(
private val handler: DatabaseHandler, private val database: Database,
) : TrackRepository { ) : TrackRepository {
override suspend fun getTrackById(id: Long): Track? { override suspend fun getTrackById(id: Long): Track? {
return handler.awaitOneOrNull { manga_syncQueries.getTrackById(id, TrackMapper::mapTrack) } return database.manga_syncQueries
.getTrackById(id, TrackMapper::mapTrack)
.awaitAsOneOrNull()
} }
override suspend fun getTracksByMangaId(mangaId: Long): List<Track> { override suspend fun getTracksByMangaId(mangaId: Long): List<Track> {
return handler.awaitList { return database.manga_syncQueries
manga_syncQueries.getTracksByMangaId(mangaId, TrackMapper::mapTrack) .getTracksByMangaId(mangaId, TrackMapper::mapTrack)
} .awaitAsList()
} }
override fun getTracksAsFlow(): Flow<List<Track>> { override fun getTracksAsFlow(): Flow<List<Track>> {
return handler.subscribeToList { return database.manga_syncQueries
manga_syncQueries.getTracks(TrackMapper::mapTrack) .getTracks(TrackMapper::mapTrack)
} .subscribeToList()
} }
override fun getTracksByMangaIdAsFlow(mangaId: Long): Flow<List<Track>> { override fun getTracksByMangaIdAsFlow(mangaId: Long): Flow<List<Track>> {
return handler.subscribeToList { return database.manga_syncQueries
manga_syncQueries.getTracksByMangaId(mangaId, TrackMapper::mapTrack) .getTracksByMangaId(mangaId, TrackMapper::mapTrack)
} .subscribeToList()
} }
override suspend fun delete(mangaId: Long, trackerId: Long) { override suspend fun delete(mangaId: Long, trackerId: Long) {
handler.await { database.manga_syncQueries.delete(
manga_syncQueries.delete(
mangaId = mangaId, mangaId = mangaId,
syncId = trackerId, syncId = trackerId,
) )
} }
}
override suspend fun insert(track: Track) { override suspend fun insert(track: Track) {
insertValues(track) insertValues(track)
@@ -49,9 +52,9 @@ class TrackRepositoryImpl(
} }
private suspend fun insertValues(vararg tracks: Track) { private suspend fun insertValues(vararg tracks: Track) {
handler.await(inTransaction = true) { database.transaction {
tracks.forEach { mangaTrack -> tracks.forEach { mangaTrack ->
manga_syncQueries.insert( database.manga_syncQueries.insert(
mangaId = mangaTrack.mangaId, mangaId = mangaTrack.mangaId,
syncId = mangaTrack.trackerId, syncId = mangaTrack.trackerId,
remoteId = mangaTrack.remoteId, remoteId = mangaTrack.remoteId,
@@ -1,14 +1,16 @@
package tachiyomi.data.updates package tachiyomi.data.updates
import app.cash.sqldelight.async.coroutines.awaitAsList
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import tachiyomi.core.common.util.lang.toLong import tachiyomi.core.common.util.lang.toLong
import tachiyomi.data.DatabaseHandler import tachiyomi.data.Database
import tachiyomi.data.subscribeToList
import tachiyomi.domain.manga.model.MangaCover import tachiyomi.domain.manga.model.MangaCover
import tachiyomi.domain.updates.model.UpdatesWithRelations import tachiyomi.domain.updates.model.UpdatesWithRelations
import tachiyomi.domain.updates.repository.UpdatesRepository import tachiyomi.domain.updates.repository.UpdatesRepository
class UpdatesRepositoryImpl( class UpdatesRepositoryImpl(
private val databaseHandler: DatabaseHandler, private val database: Database,
) : UpdatesRepository { ) : UpdatesRepository {
override suspend fun awaitWithRead( override suspend fun awaitWithRead(
@@ -16,14 +18,14 @@ class UpdatesRepositoryImpl(
after: Long, after: Long,
limit: Long, limit: Long,
): List<UpdatesWithRelations> { ): List<UpdatesWithRelations> {
return databaseHandler.awaitList { return database.updatesViewQueries
updatesViewQueries.getUpdatesByReadStatus( .getUpdatesByReadStatus(
read = read, read = read,
after = after, after = after,
limit = limit, limit = limit,
mapper = ::mapUpdatesWithRelations, mapper = ::mapUpdatesWithRelations,
) )
} .awaitAsList()
} }
override fun subscribeAll( override fun subscribeAll(
@@ -34,18 +36,17 @@ class UpdatesRepositoryImpl(
bookmarked: Boolean?, bookmarked: Boolean?,
hideExcludedScanlators: Boolean, hideExcludedScanlators: Boolean,
): Flow<List<UpdatesWithRelations>> { ): Flow<List<UpdatesWithRelations>> {
return databaseHandler.subscribeToList { return database.updatesViewQueries
updatesViewQueries.getRecentUpdatesWithFilters( .getRecentUpdatesWithFilters(
after = after, after = after,
limit = limit, limit = limit,
// invert because unread in Kotlin -> read column in SQL
read = unread?.let { !it }, read = unread?.let { !it },
started = started?.toLong(), started = started?.toLong(),
bookmarked = bookmarked, bookmarked = bookmarked,
hideExcludedScanlators = hideExcludedScanlators.toLong(), hideExcludedScanlators = hideExcludedScanlators.toLong(),
mapper = ::mapUpdatesWithRelations, mapper = ::mapUpdatesWithRelations,
) )
} .subscribeToList()
} }
override fun subscribeWithRead( override fun subscribeWithRead(
@@ -53,16 +54,17 @@ class UpdatesRepositoryImpl(
after: Long, after: Long,
limit: Long, limit: Long,
): Flow<List<UpdatesWithRelations>> { ): Flow<List<UpdatesWithRelations>> {
return databaseHandler.subscribeToList { return database.updatesViewQueries
updatesViewQueries.getUpdatesByReadStatus( .getUpdatesByReadStatus(
read = read, read = read,
after = after, after = after,
limit = limit, limit = limit,
mapper = ::mapUpdatesWithRelations, mapper = ::mapUpdatesWithRelations,
) )
} .subscribeToList()
} }
@Suppress("UNUSED_PARAMETER")
private fun mapUpdatesWithRelations( private fun mapUpdatesWithRelations(
mangaId: Long, mangaId: Long,
mangaTitle: String, mangaTitle: String,
@@ -42,9 +42,12 @@ JOIN mangas_categories MC
ON C._id = MC.category_id ON C._id = MC.category_id
WHERE MC.manga_id = :mangaId; WHERE MC.manga_id = :mangaId;
insert: insert {
INSERT INTO categories(name, sort, flags) INSERT INTO categories(name, sort, flags)
VALUES (:name, :order, :flags); VALUES (:name, :order, :flags);
SELECT last_insert_rowid();
}
delete: delete:
DELETE FROM categories DELETE FROM categories
@@ -60,6 +63,3 @@ WHERE _id = :categoryId;
updateAllFlags: updateAllFlags:
UPDATE categories SET UPDATE categories SET
flags = coalesce(?, flags); flags = coalesce(?, flags);
selectLastInsertedRowId:
SELECT last_insert_rowid();
@@ -97,10 +97,12 @@ UPDATE chapters
SET is_syncing = 0 SET is_syncing = 0
WHERE is_syncing = 1; WHERE is_syncing = 1;
insert: insert {
INSERT INTO chapters(manga_id, url, name, scanlator, read, bookmark, last_page_read, chapter_number, source_order, date_fetch, date_upload, last_modified_at, version, is_syncing) INSERT INTO chapters(manga_id, url, name, scanlator, read, bookmark, last_page_read, chapter_number, source_order, date_fetch, date_upload, last_modified_at, version, is_syncing)
VALUES (:mangaId, :url, :name, :scanlator, :read, :bookmark, :lastPageRead, :chapterNumber, :sourceOrder, :dateFetch, :dateUpload, 0, :version, 0); VALUES (:mangaId, :url, :name, :scanlator, :read, :bookmark, :lastPageRead, :chapterNumber, :sourceOrder, :dateFetch, :dateUpload, 0, :version, 0);
SELECT last_insert_rowid();
}
update: update:
UPDATE chapters UPDATE chapters
SET manga_id = coalesce(:mangaId, manga_id), SET manga_id = coalesce(:mangaId, manga_id),
@@ -117,6 +119,3 @@ SET manga_id = coalesce(:mangaId, manga_id),
version = coalesce(:version, version), version = coalesce(:version, version),
is_syncing = coalesce(:isSyncing, is_syncing) is_syncing = coalesce(:isSyncing, is_syncing)
WHERE _id = :chapterId; WHERE _id = :chapterId;
selectLastInsertedRowId:
SELECT last_insert_rowid();
@@ -193,9 +193,12 @@ AND (
) )
); );
insert: insert {
INSERT INTO mangas(source, url, artist, author, description, genre, title, status, thumbnail_url, favorite, last_update, next_update, initialized, viewer, chapter_flags, cover_last_modified, date_added, update_strategy, calculate_interval, last_modified_at, version, notes) INSERT INTO mangas(source, url, artist, author, description, genre, title, status, thumbnail_url, favorite, last_update, next_update, initialized, viewer, chapter_flags, cover_last_modified, date_added, update_strategy, calculate_interval, last_modified_at, version, notes)
VALUES (:source, :url, :artist, :author, :description, :genre, :title, :status, :thumbnailUrl, :favorite, :lastUpdate, :nextUpdate, :initialized, :viewerFlags, :chapterFlags, :coverLastModified, :dateAdded, :updateStrategy, :calculateInterval, 0, :version, :notes); VALUES (:source, :url, :artist, :author, :description, :genre, :title, :status, :thumbnailUrl, :favorite, :lastUpdate, :nextUpdate, :initialized, :viewerFlags, :chapterFlags, :coverLastModified, :dateAdded, :updateStrategy, :calculateInterval, 0, :version, :notes);
SELECT last_insert_rowid();
}
update: update:
UPDATE mangas SET UPDATE mangas SET
@@ -223,9 +226,6 @@ UPDATE mangas SET
notes = coalesce(:notes, notes) notes = coalesce(:notes, notes)
WHERE _id = :mangaId; WHERE _id = :mangaId;
selectLastInsertedRowId:
SELECT last_insert_rowid();
insertNetworkManga { insertNetworkManga {
-- Insert the manga if it doesn't exist already -- Insert the manga if it doesn't exist already
INSERT INTO mangas( INSERT INTO mangas(
+4 -3
View File
@@ -63,7 +63,7 @@ rxJava = "1.3.8"
shizuku = "13.1.5" shizuku = "13.1.5"
spotless = "8.4.0" spotless = "8.4.0"
sqldelight = "2.3.2" sqldelight = "2.3.2"
sqldelight-androidx-driver = "0.0.17" sqldelight-androidx-driver = "0.1.1"
stringSimilarity = "0.1.0" stringSimilarity = "0.1.0"
subsamplingScaleImageView = "66e0db195d" subsamplingScaleImageView = "66e0db195d"
swipe = "1.3.0" swipe = "1.3.0"
@@ -169,7 +169,8 @@ shizuku-provider = { module = "dev.rikka.shizuku:provider", version.ref = "shizu
spotless-gradle = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless" } spotless-gradle = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless" }
sqldelight-androidxDriver = { module = "com.eygraber:sqldelight-androidx-driver", version.ref = "sqldelight-androidx-driver" } sqldelight-androidxDriver = { module = "com.eygraber:sqldelight-androidx-driver", version.ref = "sqldelight-androidx-driver" }
sqldelight-androidxPaging = { module = "app.cash.sqldelight:androidx-paging3-extensions", version.ref = "sqldelight" } sqldelight-androidxPaging = { module = "app.cash.sqldelight:androidx-paging3-extensions", version.ref = "sqldelight" }
sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions-jvm", version.ref = "sqldelight" } sqldelight-async = { module = "app.cash.sqldelight:async-extensions", version.ref = "sqldelight" }
sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
sqldelight-sqliteDialect338 = { module = "app.cash.sqldelight:sqlite-3-38-dialect", version.ref = "sqldelight" } sqldelight-sqliteDialect338 = { module = "app.cash.sqldelight:sqlite-3-38-dialect", version.ref = "sqldelight" }
stringSimilarity = { module = "com.aallam.similarity:string-similarity-kotlin", version.ref = "stringSimilarity" } stringSimilarity = { module = "com.aallam.similarity:string-similarity-kotlin", version.ref = "stringSimilarity" }
subsamplingScaleImageView = { module = "com.github.tachiyomiorg:subsampling-scale-image-view", version.ref = "subsamplingScaleImageView" } subsamplingScaleImageView = { module = "com.github.tachiyomiorg:subsampling-scale-image-view", version.ref = "subsamplingScaleImageView" }
@@ -208,6 +209,6 @@ markdown = ["markdown-core", "markdown-coil"]
okhttp = ["okhttp-core", "okhttp-logging", "okhttp-brotli", "okhttp-dnsOverHttps"] okhttp = ["okhttp-core", "okhttp-logging", "okhttp-brotli", "okhttp-dnsOverHttps"]
serialization = ["kotlinx-serialization-json", "kotlinx-serialization-jsonOkio", "kotlinx-serialization-protobuf", "xmlutil-core", "xmlutil-serialization"] serialization = ["kotlinx-serialization-json", "kotlinx-serialization-jsonOkio", "kotlinx-serialization-protobuf", "xmlutil-core", "xmlutil-serialization"]
shizuku = ["shizuku-api", "shizuku-provider"] shizuku = ["shizuku-api", "shizuku-provider"]
sqldelight = ["sqldelight-androidxDriver", "sqldelight-coroutines", "sqldelight-androidxPaging"] sqldelight = ["sqldelight-androidxDriver", "sqldelight-async", "sqldelight-coroutines", "sqldelight-androidxPaging"]
test = ["junit-jupiter", "kotest-assertions", "mockk"] test = ["junit-jupiter", "kotest-assertions", "mockk"]
voyager = ["voyager-navigator", "voyager-screenModel", "voyager-tabNavigator", "voyager-transitions"] voyager = ["voyager-navigator", "voyager-screenModel", "voyager-tabNavigator", "voyager-transitions"]