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.sqldelight.async)
// AndroidX libraries
implementation(libs.androidx.annotation)
implementation(libs.androidx.appCompat)
@@ -1,24 +1,26 @@
package eu.kanade.domain.manga.interactor
import app.cash.sqldelight.async.coroutines.awaitAsList
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import tachiyomi.data.DatabaseHandler
import tachiyomi.data.Database
import tachiyomi.data.subscribeToList
class GetExcludedScanlators(
private val handler: DatabaseHandler,
private val database: Database,
) {
suspend fun await(mangaId: Long): Set<String> {
return handler.awaitList {
excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId)
}
return database.excluded_scanlatorsQueries
.getExcludedScanlatorsByMangaId(mangaId)
.awaitAsList()
.toSet()
}
fun subscribe(mangaId: Long): Flow<Set<String>> {
return handler.subscribeToList {
excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId)
}
return database.excluded_scanlatorsQueries
.getExcludedScanlatorsByMangaId(mangaId)
.subscribeToList()
.map { it.toSet() }
}
}
@@ -1,22 +1,24 @@
package eu.kanade.domain.manga.interactor
import tachiyomi.data.DatabaseHandler
import app.cash.sqldelight.async.coroutines.awaitAsList
import tachiyomi.data.Database
class SetExcludedScanlators(
private val handler: DatabaseHandler,
private val database: Database,
) {
suspend fun await(mangaId: Long, excludedScanlators: Set<String>) {
handler.await(inTransaction = true) {
val currentExcluded = handler.awaitList {
excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId)
}.toSet()
database.transaction {
val currentExcluded = database.excluded_scanlatorsQueries
.getExcludedScanlatorsByMangaId(mangaId)
.awaitAsList()
.toSet()
val toAdd = excludedScanlators.minus(currentExcluded)
for (scanlator in toAdd) {
excluded_scanlatorsQueries.insert(mangaId, scanlator)
database.excluded_scanlatorsQueries.insert(mangaId, scanlator)
}
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
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.models.BackupChapter
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.backupTrackMapper
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.history.interactor.GetHistory
import tachiyomi.domain.manga.model.Manga
@@ -15,7 +17,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MangaBackupCreator(
private val handler: DatabaseHandler = Injekt.get(),
private val database: Database = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(),
private val getHistory: GetHistory = Injekt.get(),
) {
@@ -30,19 +32,19 @@ class MangaBackupCreator(
// Entry for this manga
val mangaObject = manga.toBackupManga()
mangaObject.excludedScanlators = handler.awaitList {
excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(manga.id)
}
mangaObject.excludedScanlators = database.excluded_scanlatorsQueries
.getExcludedScanlatorsByMangaId(manga.id)
.awaitAsList()
if (options.chapters) {
// Backup all the chapters
handler.awaitList {
chaptersQueries.getChaptersByMangaId(
database.chaptersQueries
.getChaptersByMangaId(
mangaId = manga.id,
applyScanlatorFilter = 0, // false
mapper = backupChapterMapper,
)
}
.awaitAsList()
.takeUnless(List<BackupChapter>::isEmpty)
?.let { mangaObject.chapters = it }
}
@@ -56,7 +58,9 @@ class MangaBackupCreator(
}
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()) {
mangaObject.tracking = tracks
}
@@ -66,7 +70,9 @@ class MangaBackupCreator(
val historyByMangaId = getHistory.await(manga.id)
if (historyByMangaId.isNotEmpty()) {
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)
}
if (history.isNotEmpty()) {
@@ -1,14 +1,15 @@
package eu.kanade.tachiyomi.data.backup.restore.restorers
import app.cash.sqldelight.async.coroutines.awaitAsOne
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.library.service.LibraryPreferences
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class CategoriesRestorer(
private val handler: DatabaseHandler = Injekt.get(),
private val database: Database = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(),
private val libraryPreferences: LibraryPreferences = Injekt.get(),
) {
@@ -25,10 +26,9 @@ class CategoriesRestorer(
val dbCategory = dbCategoriesByName[it.name]
if (dbCategory != null) return@map dbCategory
val order = nextOrder++
handler.awaitOneExecutable {
categoriesQueries.insert(it.name, order, it.flags)
categoriesQueries.selectLastInsertedRowId()
}
database.categoriesQueries
.insert(it.name, order, it.flags)
.awaitAsOne()
.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 mihon.domain.extensionrepo.interactor.GetExtensionRepo
import tachiyomi.data.DatabaseHandler
import tachiyomi.data.Database
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class ExtensionRepoRestorer(
private val handler: DatabaseHandler = Injekt.get(),
private val database: Database = Injekt.get(),
private val getExtensionRepos: GetExtensionRepo = Injekt.get(),
) {
@@ -26,15 +26,13 @@ class ExtensionRepoRestorer(
} else if (shaExists != null) {
error("${shaExists.name} has the same signing key fingerprint")
} else {
handler.await {
extension_reposQueries.insert(
backupRepo.baseUrl,
backupRepo.name,
backupRepo.shortName,
backupRepo.website,
backupRepo.signingKeyFingerprint,
)
}
database.extension_reposQueries.insert(
backupRepo.baseUrl,
backupRepo.name,
backupRepo.shortName,
backupRepo.website,
backupRepo.signingKeyFingerprint,
)
}
}
}
@@ -1,12 +1,15 @@
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.tachiyomi.data.backup.models.BackupCategory
import eu.kanade.tachiyomi.data.backup.models.BackupChapter
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
import eu.kanade.tachiyomi.data.backup.models.BackupManga
import eu.kanade.tachiyomi.data.backup.models.BackupTracking
import tachiyomi.data.DatabaseHandler
import tachiyomi.data.Database
import tachiyomi.data.UpdateStrategyColumnAdapter
import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
@@ -24,7 +27,7 @@ import java.util.Date
import kotlin.math.max
class MangaRestorer(
private val handler: DatabaseHandler = Injekt.get(),
private val database: Database = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(),
private val getMangaByUrlAndSourceId: GetMangaByUrlAndSourceId = Injekt.get(),
private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(),
@@ -43,7 +46,9 @@ class MangaRestorer(
}
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 })
return backupMangas
@@ -57,7 +62,7 @@ class MangaRestorer(
backupManga: BackupManga,
backupCategories: List<BackupCategory>,
) {
handler.await(inTransaction = true) {
database.transaction {
val dbManga = findExistingManga(backupManga)
val manga = backupManga.getMangaImpl()
val restoredManga = if (dbManga == null) {
@@ -105,33 +110,31 @@ class MangaRestorer(
}
private suspend fun updateManga(manga: Manga): Manga {
handler.await(true) {
mangasQueries.update(
source = manga.source,
url = manga.url,
artist = manga.artist,
author = manga.author,
description = manga.description,
genre = manga.genre?.joinToString(separator = ", "),
title = manga.title,
status = manga.status,
thumbnailUrl = manga.thumbnailUrl,
favorite = manga.favorite,
lastUpdate = manga.lastUpdate,
nextUpdate = null,
calculateInterval = null,
initialized = manga.initialized,
viewer = manga.viewerFlags,
chapterFlags = manga.chapterFlags,
coverLastModified = manga.coverLastModified,
dateAdded = manga.dateAdded,
mangaId = manga.id,
updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode),
version = manga.version,
isSyncing = 1,
notes = manga.notes,
)
}
database.mangasQueries.update(
source = manga.source,
url = manga.url,
artist = manga.artist,
author = manga.author,
description = manga.description,
genre = manga.genre?.joinToString(separator = ", "),
title = manga.title,
status = manga.status,
thumbnailUrl = manga.thumbnailUrl,
favorite = manga.favorite,
lastUpdate = manga.lastUpdate,
nextUpdate = null,
calculateInterval = null,
initialized = manga.initialized,
viewer = manga.viewerFlags,
chapterFlags = manga.chapterFlags,
coverLastModified = manga.coverLastModified,
dateAdded = manga.dateAdded,
mangaId = manga.id,
updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode),
version = manga.version,
isSyncing = 1,
notes = manga.notes,
)
return manga
}
@@ -189,9 +192,9 @@ class MangaRestorer(
this.copy(id = 0L, mangaId = 0L, dateFetch = 0L, dateUpload = 0L, lastModifiedAt = 0L, version = 0L)
private suspend fun insertNewChapters(chapters: List<Chapter>) {
handler.await(true) {
database.transaction {
chapters.forEach { chapter ->
chaptersQueries.insert(
database.chaptersQueries.insert(
chapter.mangaId,
chapter.url,
chapter.name,
@@ -210,9 +213,9 @@ class MangaRestorer(
}
private suspend fun updateExistingChapters(chapters: List<Chapter>) {
handler.await(true) {
database.transaction {
chapters.forEach { chapter ->
chaptersQueries.update(
database.chaptersQueries.update(
mangaId = null,
url = null,
name = null,
@@ -238,32 +241,30 @@ class MangaRestorer(
* @return id of [Manga], null if not found
*/
private suspend fun insertManga(manga: Manga): Long {
return handler.awaitOneExecutable(true) {
mangasQueries.insert(
source = manga.source,
url = manga.url,
artist = manga.artist,
author = manga.author,
description = manga.description,
genre = manga.genre,
title = manga.title,
status = manga.status,
thumbnailUrl = manga.thumbnailUrl,
favorite = manga.favorite,
lastUpdate = manga.lastUpdate,
nextUpdate = 0L,
calculateInterval = 0L,
initialized = manga.initialized,
viewerFlags = manga.viewerFlags,
chapterFlags = manga.chapterFlags,
coverLastModified = manga.coverLastModified,
dateAdded = manga.dateAdded,
updateStrategy = manga.updateStrategy,
version = manga.version,
notes = manga.notes,
)
mangasQueries.selectLastInsertedRowId()
}
return database.mangasQueries.insert(
source = manga.source,
url = manga.url,
artist = manga.artist,
author = manga.author,
description = manga.description,
genre = manga.genre,
title = manga.title,
status = manga.status,
thumbnailUrl = manga.thumbnailUrl,
favorite = manga.favorite,
lastUpdate = manga.lastUpdate,
nextUpdate = 0L,
calculateInterval = 0L,
initialized = manga.initialized,
viewerFlags = manga.viewerFlags,
chapterFlags = manga.chapterFlags,
coverLastModified = manga.coverLastModified,
dateAdded = manga.dateAdded,
updateStrategy = manga.updateStrategy,
version = manga.version,
notes = manga.notes,
)
.awaitAsOne()
}
private suspend fun restoreMangaDetails(
@@ -309,10 +310,10 @@ class MangaRestorer(
}
if (mangaCategoriesToUpdate.isNotEmpty()) {
handler.await(true) {
mangas_categoriesQueries.deleteMangaCategoryByMangaId(manga.id)
database.transaction {
database.mangas_categoriesQueries.deleteMangaCategoryByMangaId(manga.id)
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>) {
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()
if (dbHistory == null) {
val chapter = handler.awaitOneOrNull { chaptersQueries.getChapterByUrl(history.url) }
val chapter = database.chaptersQueries
.getChapterByUrl(history.url)
.awaitAsOneOrNull()
return@mapNotNull if (chapter == null) {
// Chapter doesn't exist; skip
null
@@ -345,15 +350,14 @@ class MangaRestorer(
)
}
if (toUpdate.isNotEmpty()) {
handler.await(true) {
toUpdate.forEach {
historyQueries.upsert(
it.chapterId,
it.readAt,
it.readDuration,
)
}
if (toUpdate.isEmpty()) return
database.transaction {
toUpdate.forEach {
database.historyQueries.upsert(
it.chapterId,
it.readAt,
it.readDuration,
)
}
}
}
@@ -388,26 +392,26 @@ class MangaRestorer(
if (newTracks.isNotEmpty()) {
insertTrack.awaitAll(newTracks)
}
if (existingTracks.isNotEmpty()) {
handler.await(true) {
existingTracks.forEach { track ->
manga_syncQueries.update(
track.mangaId,
track.trackerId,
track.remoteId,
track.libraryId,
track.title,
track.lastChapterRead,
track.totalChapters,
track.status,
track.score,
track.remoteUrl,
track.startDate,
track.finishDate,
track.private,
track.id,
)
}
if (existingTracks.isEmpty()) return
database.transaction {
existingTracks.forEach { track ->
database.manga_syncQueries.update(
track.mangaId,
track.trackerId,
track.remoteId,
track.libraryId,
track.title,
track.lastChapterRead,
track.totalChapters,
track.status,
track.score,
track.remoteUrl,
track.startDate,
track.finishDate,
track.private,
track.id,
)
}
}
}
@@ -422,16 +426,11 @@ class MangaRestorer(
*/
private suspend fun restoreExcludedScanlators(manga: Manga, excludedScanlators: List<String>) {
if (excludedScanlators.isEmpty()) return
val existingExcludedScanlators = handler.awaitList {
excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(manga.id)
}
val existingExcludedScanlators = database.excluded_scanlatorsQueries
.getExcludedScanlatorsByMangaId(manga.id)
.awaitAsList()
val toInsert = excludedScanlators.filter { it !in existingExcludedScanlators }
if (toInsert.isNotEmpty()) {
handler.await {
toInsert.forEach {
excluded_scanlatorsQueries.insert(manga.id, it)
}
}
}
if (toInsert.isEmpty()) return
toInsert.forEach { database.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.serialization.XML
import tachiyomi.core.common.storage.AndroidStorageFolderProvider
import tachiyomi.data.AndroidDatabaseHandler
import tachiyomi.data.Database
import tachiyomi.data.DatabaseHandler
import tachiyomi.data.DateColumnAdapter
import tachiyomi.data.History
import tachiyomi.data.Mangas
@@ -81,7 +79,6 @@ class AppModule(val app: Application) : InjektModule {
),
)
}
addSingletonFactory<DatabaseHandler> { AndroidDatabaseHandler(get(), get()) }
addSingletonFactory {
Json {
+1
View File
@@ -15,6 +15,7 @@ android {
packageName.set("tachiyomi.data")
dialect(libs.sqldelight.sqliteDialect338)
schemaOutputDirectory.set(project.file("./src/main/sqldelight"))
generateAsync.set(true)
}
}
}
@@ -1,36 +1,49 @@
package mihon.data.repository
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.map
import mihon.domain.extensionrepo.exception.SaveExtensionRepoException
import mihon.domain.extensionrepo.model.ExtensionRepo
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
import tachiyomi.data.DatabaseHandler
import tachiyomi.data.Database
import tachiyomi.data.subscribeToList
import tachiyomi.data.subscribeToOne
class ExtensionRepoRepositoryImpl(
private val handler: DatabaseHandler,
private val database: Database,
) : ExtensionRepoRepository {
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> {
return handler.awaitList { extension_reposQueries.findAll(::mapExtensionRepo) }
return database.extension_reposQueries
.findAll(::mapExtensionRepo)
.awaitAsList()
}
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? {
return handler.awaitOneOrNull {
extension_reposQueries.findOneBySigningKeyFingerprint(fingerprint, ::mapExtensionRepo)
}
return database.extension_reposQueries
.findOneBySigningKeyFingerprint(fingerprint, ::mapExtensionRepo)
.awaitAsOneOrNull()
}
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(
@@ -41,7 +54,13 @@ class ExtensionRepoRepositoryImpl(
signingKeyFingerprint: String,
) {
try {
handler.await { extension_reposQueries.insert(baseUrl, name, shortName, website, signingKeyFingerprint) }
database.extension_reposQueries.insert(
baseUrl,
name,
shortName,
website,
signingKeyFingerprint,
)
} catch (ex: SQLException) {
throw SaveExtensionRepoException(ex)
}
@@ -55,26 +74,30 @@ class ExtensionRepoRepositoryImpl(
signingKeyFingerprint: String,
) {
try {
handler.await { extension_reposQueries.upsert(baseUrl, name, shortName, website, signingKeyFingerprint) }
database.extension_reposQueries.upsert(
baseUrl,
name,
shortName,
website,
signingKeyFingerprint,
)
} catch (ex: SQLException) {
throw SaveExtensionRepoException(ex)
}
}
override suspend fun replaceRepo(newRepo: ExtensionRepo) {
handler.await {
extension_reposQueries.replace(
newRepo.baseUrl,
newRepo.name,
newRepo.shortName,
newRepo.website,
newRepo.signingKeyFingerprint,
)
}
database.extension_reposQueries.replace(
newRepo.baseUrl,
newRepo.name,
newRepo.shortName,
newRepo.website,
newRepo.signingKeyFingerprint,
)
}
override suspend fun deleteRepo(baseUrl: String) {
return handler.await { extension_reposQueries.delete(baseUrl) }
database.extension_reposQueries.delete(baseUrl)
}
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.PagingState
import app.cash.sqldelight.Query
import app.cash.sqldelight.async.coroutines.awaitAsList
import app.cash.sqldelight.async.coroutines.awaitAsOne
import kotlin.properties.Delegates
@Suppress("unused")
class QueryPagingSource<RowType : Any>(
val handler: DatabaseHandler,
val countQuery: Database.() -> Query<Long>,
val queryProvider: Database.(Long, Long) -> Query<RowType>,
val countQuery: () -> Query<Long>,
val queryProvider: (Long, Long) -> Query<RowType>,
) : PagingSource<Long, RowType>(), Query.Listener {
override val jumpingSupported: Boolean = true
@@ -29,17 +31,16 @@ class QueryPagingSource<RowType : Any>(
try {
val key = params.key ?: 0L
val loadSize = params.loadSize
val count = handler.awaitOne { countQuery() }
val count = countQuery().awaitAsOne()
val (offset, limit) = when (params) {
is LoadParams.Prepend -> key - loadSize to loadSize.toLong()
else -> key to loadSize.toLong()
}
val data = handler.awaitList {
queryProvider(limit, offset)
.also { currentQuery = it }
}
val data = queryProvider(limit, offset)
.also { currentQuery = it }
.awaitAsList()
val (prevKey, nextKey) = when (params) {
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
import app.cash.sqldelight.async.coroutines.awaitAsList
import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull
import kotlinx.coroutines.flow.Flow
import tachiyomi.data.Database
import tachiyomi.data.DatabaseHandler
import tachiyomi.data.subscribeToList
import tachiyomi.domain.category.model.Category
import tachiyomi.domain.category.model.CategoryUpdate
import tachiyomi.domain.category.repository.CategoryRepository
class CategoryRepositoryImpl(
private val handler: DatabaseHandler,
private val database: Database,
) : CategoryRepository {
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> {
return handler.awaitList { categoriesQueries.getCategories(::mapCategory) }
return database.categoriesQueries
.getCategories(::mapCategory)
.awaitAsList()
}
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> {
return handler.awaitList {
categoriesQueries.getCategoriesByMangaId(mangaId, ::mapCategory)
}
return database.categoriesQueries
.getCategoriesByMangaId(mangaId, ::mapCategory)
.awaitAsList()
}
override fun getCategoriesByMangaIdAsFlow(mangaId: Long): Flow<List<Category>> {
return handler.subscribeToList {
categoriesQueries.getCategoriesByMangaId(mangaId, ::mapCategory)
}
return database.categoriesQueries
.getCategoriesByMangaId(mangaId, ::mapCategory)
.subscribeToList()
}
override suspend fun insert(category: Category) {
handler.await {
categoriesQueries.insert(
name = category.name,
order = category.order,
flags = category.flags,
)
}
database.categoriesQueries.insert(
name = category.name,
order = category.order,
flags = category.flags,
)
}
override suspend fun updatePartial(update: CategoryUpdate) {
handler.await {
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(
database.categoriesQueries.update(
name = update.name,
order = update.order,
flags = update.flags,
@@ -68,18 +60,18 @@ class CategoryRepositoryImpl(
)
}
override suspend fun updateAllFlags(flags: Long?) {
handler.await {
categoriesQueries.updateAllFlags(flags)
override suspend fun updatePartial(updates: List<CategoryUpdate>) {
database.transaction {
updates.forEach { updatePartial(it) }
}
}
override suspend fun updateAllFlags(flags: Long?) {
database.categoriesQueries.updateAllFlags(flags)
}
override suspend fun delete(categoryId: Long) {
handler.await {
categoriesQueries.delete(
categoryId = categoryId,
)
}
database.categoriesQueries.delete(categoryId = categoryId)
}
private fun mapCategory(
@@ -1,23 +1,27 @@
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 logcat.LogPriority
import tachiyomi.core.common.util.lang.toLong
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.ChapterUpdate
import tachiyomi.domain.chapter.repository.ChapterRepository
class ChapterRepositoryImpl(
private val handler: DatabaseHandler,
private val database: Database,
) : ChapterRepository {
override suspend fun addAll(chapters: List<Chapter>): List<Chapter> {
return try {
handler.await(inTransaction = true) {
database.transactionWithResult {
chapters.map { chapter ->
chaptersQueries.insert(
val lastInsertId = database.chaptersQueries.insert(
chapter.mangaId,
chapter.url,
chapter.name,
@@ -30,8 +34,7 @@ class ChapterRepositoryImpl(
chapter.dateFetch,
chapter.dateUpload,
chapter.version,
)
val lastInsertId = chaptersQueries.selectLastInsertedRowId().executeAsOne()
).awaitAsOne()
chapter.copy(id = lastInsertId)
}
}
@@ -50,9 +53,9 @@ class ChapterRepositoryImpl(
}
private suspend fun partialUpdate(vararg chapterUpdates: ChapterUpdate) {
handler.await(inTransaction = true) {
database.transaction {
chapterUpdates.forEach { chapterUpdate ->
chaptersQueries.update(
database.chaptersQueries.update(
mangaId = chapterUpdate.mangaId,
url = chapterUpdate.url,
name = chapterUpdate.name,
@@ -74,59 +77,55 @@ class ChapterRepositoryImpl(
override suspend fun removeChaptersWithIds(chapterIds: List<Long>) {
try {
handler.await { chaptersQueries.removeChaptersWithIds(chapterIds) }
database.chaptersQueries.removeChaptersWithIds(chapterIds)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
}
}
override suspend fun getChapterByMangaId(mangaId: Long, applyScanlatorFilter: Boolean): List<Chapter> {
return handler.awaitList {
chaptersQueries.getChaptersByMangaId(mangaId, applyScanlatorFilter.toLong(), ::mapChapter)
}
return database.chaptersQueries
.getChaptersByMangaId(mangaId, applyScanlatorFilter.toLong(), ::mapChapter)
.awaitAsList()
}
override suspend fun getScanlatorsByMangaId(mangaId: Long): List<String> {
return handler.awaitList {
chaptersQueries.getScanlatorsByMangaId(mangaId) { it.orEmpty() }
}
return database.chaptersQueries
.getScanlatorsByMangaId(mangaId) { it.orEmpty() }
.awaitAsList()
}
override fun getScanlatorsByMangaIdAsFlow(mangaId: Long): Flow<List<String>> {
return handler.subscribeToList {
chaptersQueries.getScanlatorsByMangaId(mangaId) { it.orEmpty() }
}
return database.chaptersQueries
.getScanlatorsByMangaId(mangaId) { it.orEmpty() }
.subscribeToList()
}
override suspend fun getBookmarkedChaptersByMangaId(mangaId: Long): List<Chapter> {
return handler.awaitList {
chaptersQueries.getBookmarkedChaptersByMangaId(
mangaId,
::mapChapter,
)
}
return database.chaptersQueries
.getBookmarkedChaptersByMangaId(mangaId, ::mapChapter)
.awaitAsList()
}
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>> {
return handler.subscribeToList {
chaptersQueries.getChaptersByMangaId(mangaId, applyScanlatorFilter.toLong(), ::mapChapter)
}
return database.chaptersQueries
.getChaptersByMangaId(mangaId, applyScanlatorFilter.toLong(), ::mapChapter)
.subscribeToList()
}
override suspend fun getChapterByUrlAndMangaId(url: String, mangaId: Long): Chapter? {
return handler.awaitOneOrNull {
chaptersQueries.getChapterByUrlAndMangaId(
url,
mangaId,
::mapChapter,
)
}
return database.chaptersQueries
.getChapterByUrlAndMangaId(url, mangaId, ::mapChapter)
.awaitAsOneOrNull()
}
@Suppress("UNUSED_PARAMETER")
private fun mapChapter(
id: Long,
mangaId: Long,
@@ -142,7 +141,6 @@ class ChapterRepositoryImpl(
dateUpload: Long,
lastModifiedAt: Long,
version: Long,
@Suppress("UNUSED_PARAMETER")
isSyncing: Long,
): Chapter = Chapter(
id = id,
@@ -1,41 +1,49 @@
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 logcat.LogPriority
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.HistoryUpdate
import tachiyomi.domain.history.model.HistoryWithRelations
import tachiyomi.domain.history.repository.HistoryRepository
class HistoryRepositoryImpl(
private val handler: DatabaseHandler,
private val database: Database,
) : HistoryRepository {
override fun getHistory(query: String): Flow<List<HistoryWithRelations>> {
return handler.subscribeToList {
historyViewQueries.history(query, HistoryMapper::mapHistoryWithRelations)
}
return database.historyViewQueries
.history(query, HistoryMapper::mapHistoryWithRelations)
.subscribeToList()
}
override suspend fun getLastHistory(): HistoryWithRelations? {
return handler.awaitOneOrNull {
historyViewQueries.getLatestHistory(HistoryMapper::mapHistoryWithRelations)
}
return database.historyViewQueries
.getLatestHistory(HistoryMapper::mapHistoryWithRelations)
.awaitAsOneOrNull()
}
override suspend fun getTotalReadDuration(): Long {
return handler.awaitOne { historyQueries.getReadDuration() }
return database.historyQueries
.getReadDuration()
.awaitAsOne()
}
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) {
try {
handler.await { historyQueries.resetHistoryById(historyId) }
database.historyQueries.resetHistoryById(historyId)
} catch (e: Exception) {
logcat(LogPriority.ERROR, throwable = e)
}
@@ -43,7 +51,7 @@ class HistoryRepositoryImpl(
override suspend fun resetHistoryByMangaId(mangaId: Long) {
try {
handler.await { historyQueries.resetHistoryByMangaId(mangaId) }
database.historyQueries.resetHistoryByMangaId(mangaId)
} catch (e: Exception) {
logcat(LogPriority.ERROR, throwable = e)
}
@@ -51,7 +59,7 @@ class HistoryRepositoryImpl(
override suspend fun deleteAllHistory(): Boolean {
return try {
handler.await { historyQueries.removeAllHistory() }
database.historyQueries.removeAllHistory()
true
} catch (e: Exception) {
logcat(LogPriority.ERROR, throwable = e)
@@ -61,13 +69,11 @@ class HistoryRepositoryImpl(
override suspend fun upsertHistory(historyUpdate: HistoryUpdate) {
try {
handler.await {
historyQueries.upsert(
historyUpdate.chapterId,
historyUpdate.readAt,
historyUpdate.sessionReadDuration,
)
}
database.historyQueries.upsert(
historyUpdate.chapterId,
historyUpdate.readAt,
historyUpdate.sessionReadDuration,
)
} catch (e: Exception) {
logcat(LogPriority.ERROR, throwable = e)
}
@@ -6,6 +6,7 @@ import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaWithChapterCount
object MangaMapper {
@Suppress("UNUSED_PARAMETER")
fun mapManga(
id: Long,
source: Long,
@@ -30,7 +31,6 @@ object MangaMapper {
lastModifiedAt: Long,
favoriteModifiedAt: Long?,
version: Long,
@Suppress("UNUSED_PARAMETER")
isSyncing: Long,
notes: String,
): Manga = Manga(
@@ -1,11 +1,17 @@
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 logcat.LogPriority
import tachiyomi.core.common.util.system.logcat
import tachiyomi.data.DatabaseHandler
import tachiyomi.data.Database
import tachiyomi.data.StringListColumnAdapter
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.manga.model.Manga
import tachiyomi.domain.manga.model.MangaUpdate
@@ -15,73 +21,79 @@ import java.time.LocalDate
import java.time.ZoneId
class MangaRepositoryImpl(
private val handler: DatabaseHandler,
private val database: Database,
) : MangaRepository {
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> {
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? {
return handler.awaitOneOrNull {
mangasQueries.getMangaByUrlAndSource(
url,
sourceId,
MangaMapper::mapManga,
)
}
return database.mangasQueries
.getMangaByUrlAndSource(url, sourceId, MangaMapper::mapManga)
.awaitAsOneOrNull()
}
override fun getMangaByUrlAndSourceIdAsFlow(url: String, sourceId: Long): Flow<Manga?> {
return handler.subscribeToOneOrNull {
mangasQueries.getMangaByUrlAndSource(
url,
sourceId,
MangaMapper::mapManga,
)
}
return database.mangasQueries
.getMangaByUrlAndSource(url, sourceId, MangaMapper::mapManga)
.subscribeToOneOrNull()
}
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> {
return handler.awaitList { mangasQueries.getReadMangaNotInLibrary(MangaMapper::mapManga) }
return database.mangasQueries
.getReadMangaNotInLibrary(MangaMapper::mapManga)
.awaitAsList()
}
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>> {
return handler.subscribeToList { libraryViewQueries.library(MangaMapper::mapLibraryManga) }
return database.libraryViewQueries
.library(MangaMapper::mapLibraryManga)
.subscribeToList()
}
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> {
return handler.awaitList {
mangasQueries.getDuplicateLibraryManga(id, title, MangaMapper::mapMangaWithChapterCount)
}
return database.mangasQueries
.getDuplicateLibraryManga(id, title, MangaMapper::mapMangaWithChapterCount)
.awaitAsList()
}
override suspend fun getUpcomingManga(statuses: Set<Long>): Flow<List<Manga>> {
val epochMillis = LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toEpochSecond() * 1000
return handler.subscribeToList {
mangasQueries.getUpcomingManga(epochMillis, statuses, MangaMapper::mapManga)
}
return database.mangasQueries
.getUpcomingManga(epochMillis, statuses, MangaMapper::mapManga)
.subscribeToList()
}
override suspend fun resetViewerFlags(): Boolean {
return try {
handler.await { mangasQueries.resetViewerFlags() }
database.mangasQueries.resetViewerFlags()
true
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
@@ -90,10 +102,10 @@ class MangaRepositoryImpl(
}
override suspend fun setMangaCategories(mangaId: Long, categoryIds: List<Long>) {
handler.await(inTransaction = true) {
mangas_categoriesQueries.deleteMangaCategoryByMangaId(mangaId)
categoryIds.map { categoryId ->
mangas_categoriesQueries.insert(mangaId, categoryId)
database.transaction {
database.mangas_categoriesQueries.deleteMangaCategoryByMangaId(mangaId)
categoryIds.forEach { categoryId ->
database.mangas_categoriesQueries.insert(mangaId, categoryId)
}
}
}
@@ -119,9 +131,9 @@ class MangaRepositoryImpl(
}
override suspend fun insertNetworkManga(manga: List<Manga>): List<Manga> {
return handler.await(inTransaction = true) {
return database.transactionWithResult {
manga.map {
mangasQueries.insertNetworkManga(
database.mangasQueries.insertNetworkManga(
source = it.source,
url = it.url,
artist = it.artist,
@@ -147,15 +159,15 @@ class MangaRepositoryImpl(
updateDetails = it.initialized,
mapper = MangaMapper::mapManga,
)
.executeAsOne()
.awaitAsOne()
}
}
}
private suspend fun partialUpdate(vararg mangaUpdates: MangaUpdate) {
handler.await(inTransaction = true) {
database.transaction {
mangaUpdates.forEach { value ->
mangasQueries.update(
database.mangasQueries.update(
source = value.source,
url = value.url,
artist = value.artist,
@@ -7,7 +7,8 @@ import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
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.StubSource
import tachiyomi.domain.source.repository.SourcePagingSource
@@ -17,7 +18,7 @@ import tachiyomi.domain.source.model.Source as DomainSource
class SourceRepositoryImpl(
private val sourceManager: SourceManager,
private val handler: DatabaseHandler,
private val database: Database,
) : SourceRepository {
override fun getSources(): Flow<List<DomainSource>> {
@@ -39,10 +40,12 @@ class SourceRepositoryImpl(
}
override fun getSourcesWithFavoriteCount(): Flow<List<Pair<DomainSource, Long>>> {
return combine(
handler.subscribeToList { mangasQueries.getSourceIdWithFavoriteCount() },
sourceManager.catalogueSources,
) { sourceIdWithFavoriteCount, _ -> sourceIdWithFavoriteCount }
val sourceIdWithFavoriteCountFlow = database.mangasQueries
.getSourceIdWithFavoriteCount()
.subscribeToList()
return combine(sourceIdWithFavoriteCountFlow, sourceManager.catalogueSources) { sourceIdWithFavoriteCount, _ ->
sourceIdWithFavoriteCount
}
.map {
it.map { (sourceId, count) ->
val source = sourceManager.getOrStub(sourceId)
@@ -55,17 +58,18 @@ class SourceRepositoryImpl(
}
override fun getSourcesWithNonLibraryManga(): Flow<List<SourceWithCount>> {
val sourceIdWithNonLibraryManga =
handler.subscribeToList { mangasQueries.getSourceIdsWithNonLibraryManga() }
return sourceIdWithNonLibraryManga.map { sourceId ->
sourceId.map { (sourceId, count) ->
val source = sourceManager.getOrStub(sourceId)
val domainSource = mapSourceToDomainSource(source).copy(
isStub = source is StubSource,
)
SourceWithCount(domainSource, count)
return database.mangasQueries
.getSourceIdsWithNonLibraryManga()
.subscribeToList()
.map { sourceId ->
sourceId.map { (sourceId, count) ->
val source = sourceManager.getOrStub(sourceId)
val domainSource = mapSourceToDomainSource(source).copy(
isStub = source is StubSource,
)
SourceWithCount(domainSource, count)
}
}
}
}
override fun search(
@@ -1,24 +1,30 @@
package tachiyomi.data.source
import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull
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.repository.StubSourceRepository
class StubSourceRepositoryImpl(
private val handler: DatabaseHandler,
private val database: Database,
) : StubSourceRepository {
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? {
return handler.awaitOneOrNull { sourcesQueries.findOne(id, ::mapStubSource) }
return database.sourcesQueries
.findOne(id, ::mapStubSource)
.awaitAsOneOrNull()
}
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(
@@ -1,43 +1,46 @@
package tachiyomi.data.track
import app.cash.sqldelight.async.coroutines.awaitAsList
import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull
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.repository.TrackRepository
class TrackRepositoryImpl(
private val handler: DatabaseHandler,
private val database: Database,
) : TrackRepository {
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> {
return handler.awaitList {
manga_syncQueries.getTracksByMangaId(mangaId, TrackMapper::mapTrack)
}
return database.manga_syncQueries
.getTracksByMangaId(mangaId, TrackMapper::mapTrack)
.awaitAsList()
}
override fun getTracksAsFlow(): Flow<List<Track>> {
return handler.subscribeToList {
manga_syncQueries.getTracks(TrackMapper::mapTrack)
}
return database.manga_syncQueries
.getTracks(TrackMapper::mapTrack)
.subscribeToList()
}
override fun getTracksByMangaIdAsFlow(mangaId: Long): Flow<List<Track>> {
return handler.subscribeToList {
manga_syncQueries.getTracksByMangaId(mangaId, TrackMapper::mapTrack)
}
return database.manga_syncQueries
.getTracksByMangaId(mangaId, TrackMapper::mapTrack)
.subscribeToList()
}
override suspend fun delete(mangaId: Long, trackerId: Long) {
handler.await {
manga_syncQueries.delete(
mangaId = mangaId,
syncId = trackerId,
)
}
database.manga_syncQueries.delete(
mangaId = mangaId,
syncId = trackerId,
)
}
override suspend fun insert(track: Track) {
@@ -49,9 +52,9 @@ class TrackRepositoryImpl(
}
private suspend fun insertValues(vararg tracks: Track) {
handler.await(inTransaction = true) {
database.transaction {
tracks.forEach { mangaTrack ->
manga_syncQueries.insert(
database.manga_syncQueries.insert(
mangaId = mangaTrack.mangaId,
syncId = mangaTrack.trackerId,
remoteId = mangaTrack.remoteId,
@@ -1,14 +1,16 @@
package tachiyomi.data.updates
import app.cash.sqldelight.async.coroutines.awaitAsList
import kotlinx.coroutines.flow.Flow
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.updates.model.UpdatesWithRelations
import tachiyomi.domain.updates.repository.UpdatesRepository
class UpdatesRepositoryImpl(
private val databaseHandler: DatabaseHandler,
private val database: Database,
) : UpdatesRepository {
override suspend fun awaitWithRead(
@@ -16,14 +18,14 @@ class UpdatesRepositoryImpl(
after: Long,
limit: Long,
): List<UpdatesWithRelations> {
return databaseHandler.awaitList {
updatesViewQueries.getUpdatesByReadStatus(
return database.updatesViewQueries
.getUpdatesByReadStatus(
read = read,
after = after,
limit = limit,
mapper = ::mapUpdatesWithRelations,
)
}
.awaitAsList()
}
override fun subscribeAll(
@@ -34,18 +36,17 @@ class UpdatesRepositoryImpl(
bookmarked: Boolean?,
hideExcludedScanlators: Boolean,
): Flow<List<UpdatesWithRelations>> {
return databaseHandler.subscribeToList {
updatesViewQueries.getRecentUpdatesWithFilters(
return database.updatesViewQueries
.getRecentUpdatesWithFilters(
after = after,
limit = limit,
// invert because unread in Kotlin -> read column in SQL
read = unread?.let { !it },
started = started?.toLong(),
bookmarked = bookmarked,
hideExcludedScanlators = hideExcludedScanlators.toLong(),
mapper = ::mapUpdatesWithRelations,
)
}
.subscribeToList()
}
override fun subscribeWithRead(
@@ -53,16 +54,17 @@ class UpdatesRepositoryImpl(
after: Long,
limit: Long,
): Flow<List<UpdatesWithRelations>> {
return databaseHandler.subscribeToList {
updatesViewQueries.getUpdatesByReadStatus(
return database.updatesViewQueries
.getUpdatesByReadStatus(
read = read,
after = after,
limit = limit,
mapper = ::mapUpdatesWithRelations,
)
}
.subscribeToList()
}
@Suppress("UNUSED_PARAMETER")
private fun mapUpdatesWithRelations(
mangaId: Long,
mangaTitle: String,
@@ -42,9 +42,12 @@ JOIN mangas_categories MC
ON C._id = MC.category_id
WHERE MC.manga_id = :mangaId;
insert:
INSERT INTO categories(name, sort, flags)
VALUES (:name, :order, :flags);
insert {
INSERT INTO categories(name, sort, flags)
VALUES (:name, :order, :flags);
SELECT last_insert_rowid();
}
delete:
DELETE FROM categories
@@ -60,6 +63,3 @@ WHERE _id = :categoryId;
updateAllFlags:
UPDATE categories SET
flags = coalesce(?, flags);
selectLastInsertedRowId:
SELECT last_insert_rowid();
@@ -97,10 +97,12 @@ UPDATE chapters
SET is_syncing = 0
WHERE is_syncing = 1;
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)
VALUES (:mangaId, :url, :name, :scanlator, :read, :bookmark, :lastPageRead, :chapterNumber, :sourceOrder, :dateFetch, :dateUpload, 0, :version, 0);
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)
VALUES (:mangaId, :url, :name, :scanlator, :read, :bookmark, :lastPageRead, :chapterNumber, :sourceOrder, :dateFetch, :dateUpload, 0, :version, 0);
SELECT last_insert_rowid();
}
update:
UPDATE chapters
SET manga_id = coalesce(:mangaId, manga_id),
@@ -117,6 +119,3 @@ SET manga_id = coalesce(:mangaId, manga_id),
version = coalesce(:version, version),
is_syncing = coalesce(:isSyncing, is_syncing)
WHERE _id = :chapterId;
selectLastInsertedRowId:
SELECT last_insert_rowid();
@@ -193,9 +193,12 @@ AND (
)
);
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)
VALUES (:source, :url, :artist, :author, :description, :genre, :title, :status, :thumbnailUrl, :favorite, :lastUpdate, :nextUpdate, :initialized, :viewerFlags, :chapterFlags, :coverLastModified, :dateAdded, :updateStrategy, :calculateInterval, 0, :version, :notes);
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)
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 mangas SET
@@ -223,9 +226,6 @@ UPDATE mangas SET
notes = coalesce(:notes, notes)
WHERE _id = :mangaId;
selectLastInsertedRowId:
SELECT last_insert_rowid();
insertNetworkManga {
-- Insert the manga if it doesn't exist already
INSERT INTO mangas(
+4 -3
View File
@@ -63,7 +63,7 @@ rxJava = "1.3.8"
shizuku = "13.1.5"
spotless = "8.4.0"
sqldelight = "2.3.2"
sqldelight-androidx-driver = "0.0.17"
sqldelight-androidx-driver = "0.1.1"
stringSimilarity = "0.1.0"
subsamplingScaleImageView = "66e0db195d"
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" }
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-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" }
stringSimilarity = { module = "com.aallam.similarity:string-similarity-kotlin", version.ref = "stringSimilarity" }
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"]
serialization = ["kotlinx-serialization-json", "kotlinx-serialization-jsonOkio", "kotlinx-serialization-protobuf", "xmlutil-core", "xmlutil-serialization"]
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"]
voyager = ["voyager-navigator", "voyager-screenModel", "voyager-tabNavigator", "voyager-transitions"]