refactor(db): Migrate track queries (that can be migrated) to SQLDelight

This commit is contained in:
Ahmad Ansori Palembani 2024-11-29 18:02:45 +07:00
parent 93ec13f324
commit 726613e6d7
Signed by: null2264
GPG key ID: BA64F8B60AF3EFB6
14 changed files with 98 additions and 40 deletions

View file

@ -20,8 +20,7 @@ interface TrackQueries : DbProvider {
) )
.prepare() .prepare()
fun insertTrack(track: Track) = db.put().`object`(track).prepare() // FIXME: Migrate to SQLDelight, on halt: in StorIO transaction
fun insertTracks(tracks: List<Track>) = db.put().objects(tracks).prepare() fun insertTracks(tracks: List<Track>) = db.put().objects(tracks).prepare()
} }

View file

@ -81,12 +81,14 @@ import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.sync.withPermit
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import yokai.domain.category.interactor.GetCategories import yokai.domain.category.interactor.GetCategories
import yokai.domain.chapter.interactor.GetChapter import yokai.domain.chapter.interactor.GetChapter
import yokai.domain.manga.interactor.GetLibraryManga import yokai.domain.manga.interactor.GetLibraryManga
import yokai.domain.manga.interactor.UpdateManga import yokai.domain.manga.interactor.UpdateManga
import yokai.domain.manga.models.cover import yokai.domain.manga.models.cover
import yokai.domain.track.interactor.GetTrack import yokai.domain.track.interactor.GetTrack
import yokai.domain.track.interactor.InsertTrack
import yokai.i18n.MR import yokai.i18n.MR
import yokai.util.lang.getString import yokai.util.lang.getString
@ -107,6 +109,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
private val getLibraryManga: GetLibraryManga = Injekt.get() private val getLibraryManga: GetLibraryManga = Injekt.get()
private val updateManga: UpdateManga = Injekt.get() private val updateManga: UpdateManga = Injekt.get()
private val getTrack: GetTrack = Injekt.get() private val getTrack: GetTrack = Injekt.get()
private val insertTrack: InsertTrack by injectLazy()
private var extraDeferredJobs = mutableListOf<Deferred<Any>>() private var extraDeferredJobs = mutableListOf<Deferred<Any>>()
@ -322,9 +325,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
if (service != null && service in loggedServices) { if (service != null && service in loggedServices) {
try { try {
val newTrack = service.refresh(track) val newTrack = service.refresh(track)
db.insertTrack(newTrack).executeAsBlocking() insertTrack.await(newTrack)
syncChaptersWithTrackServiceTwoWay(db, getChapter.awaitAll(manga.id!!, false), track, service) syncChaptersWithTrackServiceTwoWay(getChapter.awaitAll(manga.id!!, false), track, service)
} catch (e: Exception) { } catch (e: Exception) {
Logger.e(e) Logger.e(e)
} }

View file

@ -12,7 +12,6 @@ import androidx.work.WorkerParameters
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.system.e
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -21,12 +20,14 @@ import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import yokai.domain.manga.interactor.GetManga import yokai.domain.manga.interactor.GetManga
import yokai.domain.track.interactor.GetTrack import yokai.domain.track.interactor.GetTrack
import yokai.domain.track.interactor.InsertTrack
class DelayedTrackingUpdateJob(context: Context, workerParams: WorkerParameters) : class DelayedTrackingUpdateJob(context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) { CoroutineWorker(context, workerParams) {
private val getManga: GetManga by injectLazy() private val getManga: GetManga by injectLazy()
private val getTrack: GetTrack by injectLazy() private val getTrack: GetTrack by injectLazy()
private val insertTrack: InsertTrack by injectLazy()
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
val preferences = Injekt.get<PreferencesHelper>() val preferences = Injekt.get<PreferencesHelper>()
@ -56,7 +57,7 @@ class DelayedTrackingUpdateJob(context: Context, workerParams: WorkerParameters)
try { try {
track.last_chapter_read = trackChapter.second track.last_chapter_read = trackChapter.second
service.update(track, true) service.update(track, true)
db.insertTrack(track).executeAsBlocking() insertTrack.await(track)
} catch (e: Exception) { } catch (e: Exception) {
Logger.e(e) { "Unable to update tracker [tracker id ${track.sync_id}]" } Logger.e(e) { "Unable to update tracker [tracker id ${track.sync_id}]" }
} }

View file

@ -2191,7 +2191,7 @@ open class LibraryController(
*/ */
private fun showChangeMangaCategoriesSheet() { private fun showChangeMangaCategoriesSheet() {
val activity = activity ?: return val activity = activity ?: return
selectedMangas.toList().moveCategories(presenter.db, activity) { selectedMangas.toList().moveCategories(activity) {
presenter.getLibrary() presenter.getLibrary()
destroyActionModeIfNeeded() destroyActionModeIfNeeded()
} }

View file

@ -1634,7 +1634,7 @@ class MangaDetailsController :
private fun showCategoriesSheet() { private fun showCategoriesSheet() {
val adding = !presenter.manga.favorite val adding = !presenter.manga.favorite
presenter.manga.moveCategories(presenter.db, activity!!, adding) { presenter.manga.moveCategories(activity!!, adding) {
updateHeader() updateHeader()
if (adding) { if (adding) {
showAddedSnack() showAddedSnack()

View file

@ -97,6 +97,7 @@ import yokai.domain.manga.models.cover
import yokai.domain.storage.StorageManager import yokai.domain.storage.StorageManager
import yokai.domain.track.interactor.DeleteTrack import yokai.domain.track.interactor.DeleteTrack
import yokai.domain.track.interactor.GetTrack import yokai.domain.track.interactor.GetTrack
import yokai.domain.track.interactor.InsertTrack
import yokai.i18n.MR import yokai.i18n.MR
import yokai.util.lang.getString import yokai.util.lang.getString
@ -118,6 +119,7 @@ class MangaDetailsPresenter(
private val updateManga: UpdateManga by injectLazy() private val updateManga: UpdateManga by injectLazy()
private val deleteTrack: DeleteTrack by injectLazy() private val deleteTrack: DeleteTrack by injectLazy()
private val getTrack: GetTrack by injectLazy() private val getTrack: GetTrack by injectLazy()
private val insertTrack: InsertTrack by injectLazy()
private val getHistory: GetHistory by injectLazy() private val getHistory: GetHistory by injectLazy()
private val networkPreferences: NetworkPreferences by injectLazy() private val networkPreferences: NetworkPreferences by injectLazy()
@ -1020,8 +1022,8 @@ class MangaDetailsPresenter(
null null
} }
if (trackItem != null) { if (trackItem != null) {
db.insertTrack(trackItem).executeAsBlocking() insertTrack.await(trackItem)
syncChaptersWithTrackServiceTwoWay(db, chapters, trackItem, item.service) syncChaptersWithTrackServiceTwoWay(chapters, trackItem, item.service)
trackItem trackItem
} else { } else {
item.track item.track
@ -1061,10 +1063,10 @@ class MangaDetailsPresenter(
} }
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
if (binding != null) { if (binding != null) {
db.insertTrack(binding).executeAsBlocking() insertTrack.await(binding)
} }
syncChaptersWithTrackServiceTwoWay(db, chapters, item, service) syncChaptersWithTrackServiceTwoWay(chapters, item, service)
} }
fetchTracks() fetchTracks()
} }
@ -1092,7 +1094,7 @@ class MangaDetailsPresenter(
null null
} }
if (binding != null) { if (binding != null) {
withContext(Dispatchers.IO) { db.insertTrack(binding).executeAsBlocking() } withContext(Dispatchers.IO) { insertTrack.await(binding) }
fetchTracks() fetchTracks()
} else { } else {
trackRefreshDone() trackRefreshDone()

View file

@ -998,7 +998,7 @@ class ReaderViewModel(
launchIO { launchIO {
val newChapterRead = readerChapter.chapter.chapter_number val newChapterRead = readerChapter.chapter.chapter_number
val errors = updateTrackChapterRead(db, preferences, manga?.id, newChapterRead, true) val errors = updateTrackChapterRead(preferences, manga?.id, newChapterRead, true)
if (errors.isNotEmpty()) { if (errors.isNotEmpty()) {
eventChannel.send(Event.ShareTrackingError(errors)) eventChannel.send(Event.ShareTrackingError(errors))
} }

View file

@ -51,6 +51,7 @@ import yokai.domain.chapter.interactor.GetChapter
import yokai.domain.manga.interactor.GetManga import yokai.domain.manga.interactor.GetManga
import yokai.domain.manga.interactor.UpdateManga import yokai.domain.manga.interactor.UpdateManga
import yokai.domain.manga.models.MangaUpdate import yokai.domain.manga.models.MangaUpdate
import yokai.domain.track.interactor.InsertTrack
import yokai.i18n.MR import yokai.i18n.MR
import yokai.util.lang.getString import yokai.util.lang.getString
import android.R as AR import android.R as AR
@ -83,12 +84,11 @@ suspend fun Manga.shouldDownloadNewChapters(prefs: PreferencesHelper, getCategor
return categoriesForManga.any { it in includedCategories } return categoriesForManga.any { it in includedCategories }
} }
fun Manga.moveCategories(db: DatabaseHelper, activity: Activity, onMangaMoved: () -> Unit) { fun Manga.moveCategories(activity: Activity, onMangaMoved: () -> Unit) {
moveCategories(db, activity, false, onMangaMoved) moveCategories(activity, false, onMangaMoved)
} }
fun Manga.moveCategories( fun Manga.moveCategories(
db: DatabaseHelper,
activity: Activity, activity: Activity,
addingToLibrary: Boolean, addingToLibrary: Boolean,
onMangaMoved: () -> Unit, onMangaMoved: () -> Unit,
@ -110,13 +110,12 @@ fun Manga.moveCategories(
) { ) {
onMangaMoved() onMangaMoved()
if (addingToLibrary) { if (addingToLibrary) {
autoAddTrack(db, onMangaMoved) autoAddTrack(onMangaMoved)
} }
}.show() }.show()
} }
fun List<Manga>.moveCategories( fun List<Manga>.moveCategories(
db: DatabaseHelper,
activity: Activity, activity: Activity,
onMangaMoved: () -> Unit, onMangaMoved: () -> Unit,
) { ) {
@ -211,7 +210,7 @@ fun Manga.addOrRemoveToFavorites(
defaultCategory != null -> { defaultCategory != null -> {
favorite = true favorite = true
date_added = Date().time date_added = Date().time
autoAddTrack(db, onMangaMoved) autoAddTrack(onMangaMoved)
// FIXME: Don't do blocking // FIXME: Don't do blocking
runBlocking { runBlocking {
updateManga.await( updateManga.await(
@ -228,7 +227,7 @@ fun Manga.addOrRemoveToFavorites(
onMangaMoved() onMangaMoved()
return view.snack(activity.getString(MR.strings.added_to_, defaultCategory.name)) { return view.snack(activity.getString(MR.strings.added_to_, defaultCategory.name)) {
setAction(MR.strings.change) { setAction(MR.strings.change) {
moveCategories(db, activity, onMangaMoved) moveCategories(activity, onMangaMoved)
} }
} }
} }
@ -238,7 +237,7 @@ fun Manga.addOrRemoveToFavorites(
) -> { // last used category(s) ) -> { // last used category(s)
favorite = true favorite = true
date_added = Date().time date_added = Date().time
autoAddTrack(db, onMangaMoved) autoAddTrack(onMangaMoved)
// FIXME: Don't do blocking // FIXME: Don't do blocking
runBlocking { runBlocking {
updateManga.await( updateManga.await(
@ -270,14 +269,14 @@ fun Manga.addOrRemoveToFavorites(
), ),
) { ) {
setAction(MR.strings.change) { setAction(MR.strings.change) {
moveCategories(db, activity, onMangaMoved) moveCategories(activity, onMangaMoved)
} }
} }
} }
defaultCategoryId == 0 || categories.isEmpty() -> { // 'Default' or no category defaultCategoryId == 0 || categories.isEmpty() -> { // 'Default' or no category
favorite = true favorite = true
date_added = Date().time date_added = Date().time
autoAddTrack(db, onMangaMoved) autoAddTrack(onMangaMoved)
// FIXME: Don't do blocking // FIXME: Don't do blocking
runBlocking { runBlocking {
updateManga.await( updateManga.await(
@ -294,7 +293,7 @@ fun Manga.addOrRemoveToFavorites(
return if (categories.isNotEmpty()) { return if (categories.isNotEmpty()) {
view.snack(activity.getString(MR.strings.added_to_, activity.getString(MR.strings.default_value))) { view.snack(activity.getString(MR.strings.added_to_, activity.getString(MR.strings.default_value))) {
setAction(MR.strings.change) { setAction(MR.strings.change) {
moveCategories(db, activity, onMangaMoved) moveCategories(activity, onMangaMoved)
} }
} }
} else { } else {
@ -302,7 +301,7 @@ fun Manga.addOrRemoveToFavorites(
} }
} }
else -> { // Always ask else -> { // Always ask
showSetCategoriesSheet(db, activity, categories, onMangaAdded, onMangaMoved) showSetCategoriesSheet(activity, categories, onMangaAdded, onMangaMoved)
} }
} }
} else { } else {
@ -352,7 +351,6 @@ fun Manga.addOrRemoveToFavorites(
} }
private fun Manga.showSetCategoriesSheet( private fun Manga.showSetCategoriesSheet(
db: DatabaseHelper,
activity: Activity, activity: Activity,
categories: List<Category>, categories: List<Category>,
onMangaAdded: (Pair<Long, Boolean>?) -> Unit, onMangaAdded: (Pair<Long, Boolean>?) -> Unit,
@ -372,7 +370,7 @@ private fun Manga.showSetCategoriesSheet(
) { ) {
(activity as? MainActivity)?.showNotificationPermissionPrompt() (activity as? MainActivity)?.showNotificationPermissionPrompt()
onMangaAdded(null) onMangaAdded(null)
autoAddTrack(db, onMangaMoved) autoAddTrack(onMangaMoved)
}.show() }.show()
} }
@ -470,10 +468,11 @@ private fun showAddDuplicateDialog(
}.show() }.show()
} }
fun Manga.autoAddTrack(db: DatabaseHelper, onMangaMoved: () -> Unit) { fun Manga.autoAddTrack(onMangaMoved: () -> Unit) {
val loggedServices = Injekt.get<TrackManager>().services.filter { it.isLogged } val loggedServices = Injekt.get<TrackManager>().services.filter { it.isLogged }
val source = Injekt.get<SourceManager>().getOrStub(this.source) val source = Injekt.get<SourceManager>().getOrStub(this.source)
val getChapter = Injekt.get<GetChapter>() val getChapter = Injekt.get<GetChapter>()
val insertTrack = Injekt.get<InsertTrack>()
loggedServices loggedServices
.filterIsInstance<EnhancedTrackService>() .filterIsInstance<EnhancedTrackService>()
.filter { it.accept(source) } .filter { it.accept(source) }
@ -484,9 +483,13 @@ fun Manga.autoAddTrack(db: DatabaseHelper, onMangaMoved: () -> Unit) {
val mangaId = this@autoAddTrack.id!! val mangaId = this@autoAddTrack.id!!
track.manga_id = mangaId track.manga_id = mangaId
(service as TrackService).bind(track) (service as TrackService).bind(track)
db.insertTrack(track).executeAsBlocking() insertTrack.await(track)
syncChaptersWithTrackServiceTwoWay(db, getChapter.awaitAll(mangaId, false), track, service as TrackService) syncChaptersWithTrackServiceTwoWay(
getChapter.awaitAll(mangaId, false),
track,
service as TrackService
)
withUIContext { withUIContext {
onMangaMoved() onMangaMoved()
} }

View file

@ -9,7 +9,6 @@ import eu.kanade.tachiyomi.data.track.DelayedTrackingUpdateJob
import eu.kanade.tachiyomi.data.track.EnhancedTrackService import eu.kanade.tachiyomi.data.track.EnhancedTrackService
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.util.system.e
import eu.kanade.tachiyomi.util.system.isOnline import eu.kanade.tachiyomi.util.system.isOnline
import eu.kanade.tachiyomi.util.system.launchIO import eu.kanade.tachiyomi.util.system.launchIO
import eu.kanade.tachiyomi.util.system.w import eu.kanade.tachiyomi.util.system.w
@ -20,6 +19,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import yokai.domain.chapter.interactor.UpdateChapter import yokai.domain.chapter.interactor.UpdateChapter
import yokai.domain.track.interactor.GetTrack import yokai.domain.track.interactor.GetTrack
import yokai.domain.track.interactor.InsertTrack
/** /**
* Helper method for syncing a remote track with the local chapters, and back * Helper method for syncing a remote track with the local chapters, and back
@ -30,11 +30,11 @@ import yokai.domain.track.interactor.GetTrack
* @param service the tracker service. * @param service the tracker service.
*/ */
suspend fun syncChaptersWithTrackServiceTwoWay( suspend fun syncChaptersWithTrackServiceTwoWay(
db: DatabaseHelper,
chapters: List<Chapter>, chapters: List<Chapter>,
remoteTrack: Track, remoteTrack: Track,
service: TrackService, service: TrackService,
updateChapter: UpdateChapter = Injekt.get(), updateChapter: UpdateChapter = Injekt.get(),
insertTrack: InsertTrack = Injekt.get()
) = withIOContext { ) = withIOContext {
if (service !is EnhancedTrackService) { if (service !is EnhancedTrackService) {
return@withIOContext return@withIOContext
@ -54,7 +54,7 @@ suspend fun syncChaptersWithTrackServiceTwoWay(
try { try {
service.update(remoteTrack) service.update(remoteTrack)
db.insertTrack(remoteTrack).executeAsBlocking() insertTrack.await(remoteTrack)
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.w(e) Logger.w(e)
} }
@ -86,7 +86,7 @@ fun updateTrackChapterMarkedAsRead(
// We want these to execute even if the presenter is destroyed // We want these to execute even if the presenter is destroyed
trackingJobs[mangaId] = launchIO { trackingJobs[mangaId] = launchIO {
delay(delay) delay(delay)
updateTrackChapterRead(db, preferences, mangaId, newChapterRead) updateTrackChapterRead(preferences, mangaId, newChapterRead)
fetchTracks?.invoke() fetchTracks?.invoke()
trackingJobs.remove(mangaId) trackingJobs.remove(mangaId)
} to newChapterRead } to newChapterRead
@ -94,12 +94,12 @@ fun updateTrackChapterMarkedAsRead(
} }
suspend fun updateTrackChapterRead( suspend fun updateTrackChapterRead(
db: DatabaseHelper,
preferences: PreferencesHelper, preferences: PreferencesHelper,
mangaId: Long?, mangaId: Long?,
newChapterRead: Float, newChapterRead: Float,
retryWhenOnline: Boolean = false, retryWhenOnline: Boolean = false,
getTrack: GetTrack = Injekt.get() getTrack: GetTrack = Injekt.get(),
insertTrack: InsertTrack = Injekt.get(),
): List<Pair<TrackService, String?>> { ): List<Pair<TrackService, String?>> {
val trackManager = Injekt.get<TrackManager>() val trackManager = Injekt.get<TrackManager>()
val trackList = getTrack.awaitAllByMangaId(mangaId) val trackList = getTrack.awaitAllByMangaId(mangaId)
@ -113,7 +113,7 @@ suspend fun updateTrackChapterRead(
try { try {
track.last_chapter_read = newChapterRead track.last_chapter_read = newChapterRead
service.update(track, true) service.update(track, true)
db.insertTrack(track).executeAsBlocking() insertTrack.await(track)
} catch (e: Exception) { } catch (e: Exception) {
Logger.e(e) { "Unable to update tracker [tracker id ${track.sync_id}]" } Logger.e(e) { "Unable to update tracker [tracker id ${track.sync_id}]" }
failures.add(service to e.localizedMessage) failures.add(service to e.localizedMessage)

View file

@ -44,6 +44,7 @@ import yokai.domain.recents.interactor.GetRecents
import yokai.domain.track.TrackRepository import yokai.domain.track.TrackRepository
import yokai.domain.track.interactor.DeleteTrack import yokai.domain.track.interactor.DeleteTrack
import yokai.domain.track.interactor.GetTrack import yokai.domain.track.interactor.GetTrack
import yokai.domain.track.interactor.InsertTrack
fun domainModule() = module { fun domainModule() = module {
factory { TrustExtension(get(), get()) } factory { TrustExtension(get(), get()) }
@ -90,4 +91,5 @@ fun domainModule() = module {
single<TrackRepository> { TrackRepositoryImpl(get()) } single<TrackRepository> { TrackRepositoryImpl(get()) }
factory { DeleteTrack(get()) } factory { DeleteTrack(get()) }
factory { GetTrack(get()) } factory { GetTrack(get()) }
factory { InsertTrack(get()) }
} }

View file

@ -10,4 +10,22 @@ class TrackRepositoryImpl(private val handler: DatabaseHandler) : TrackRepositor
override suspend fun deleteForManga(mangaId: Long, syncId: Long) = override suspend fun deleteForManga(mangaId: Long, syncId: Long) =
handler.await { manga_syncQueries.deleteForManga(mangaId, syncId) } handler.await { manga_syncQueries.deleteForManga(mangaId, syncId) }
override suspend fun insert(track: Track) =
handler.await {
manga_syncQueries.insert(
mangaId = track.manga_id,
syncId = track.sync_id,
remoteId = track.media_id,
libraryId = track.library_id,
title = track.title,
lastChapterRead = track.last_chapter_read.toDouble(),
totalChapters = track.total_chapters,
status = track.status.toLong(),
score = track.score.toDouble(),
remoteUrl = track.tracking_url,
startDate = track.started_reading_date,
finishDate = track.finished_reading_date,
)
}
} }

View file

@ -5,4 +5,5 @@ import eu.kanade.tachiyomi.data.database.models.Track
interface TrackRepository { interface TrackRepository {
suspend fun getAllByMangaId(mangaId: Long): List<Track> suspend fun getAllByMangaId(mangaId: Long): List<Track>
suspend fun deleteForManga(mangaId: Long, syncId: Long) suspend fun deleteForManga(mangaId: Long, syncId: Long)
suspend fun insert(track: Track)
} }

View file

@ -0,0 +1,10 @@
package yokai.domain.track.interactor
import eu.kanade.tachiyomi.data.database.models.Track
import yokai.domain.track.TrackRepository
class InsertTrack(
private val trackRepository: TrackRepository,
) {
suspend fun await(track: Track) = trackRepository.insert(track)
}

View file

@ -1,5 +1,3 @@
import kotlin.Float;
CREATE TABLE manga_sync( CREATE TABLE manga_sync(
_id INTEGER NOT NULL PRIMARY KEY, _id INTEGER NOT NULL PRIMARY KEY,
manga_id INTEGER NOT NULL, manga_id INTEGER NOT NULL,
@ -27,3 +25,24 @@ WHERE manga_id = :mangaId;
deleteForManga: deleteForManga:
DELETE FROM manga_sync DELETE FROM manga_sync
WHERE manga_id = :mangaId AND sync_id = :syncId; WHERE manga_id = :mangaId AND sync_id = :syncId;
insert:
INSERT INTO manga_sync(manga_id, sync_id, remote_id, library_id, title, last_chapter_read, total_chapters, status, score, remote_url, start_date, finish_date)
VALUES(:mangaId, :syncId, :remoteId, :libraryId, :title, :lastChapterRead, :totalChapters, :status, :score, :remoteUrl, :startDate, :finishDate);
update:
UPDATE manga_sync
SET
manga_id = coalesce(:mangaId, manga_id),
sync_id = coalesce(:syncId, sync_id),
remote_id = coalesce(:remoteId, remote_id),
library_id = coalesce(:libraryId, library_id),
title = coalesce(:title, title),
last_chapter_read = coalesce(:lastChapterRead, last_chapter_read),
total_chapters = coalesce(:totalChapters, total_chapters),
status = coalesce(:status, status),
score = coalesce(:score, score),
remote_url = coalesce(:remoteUrl, remote_url),
start_date = coalesce(:startDate, start_date),
finish_date = coalesce(:finishDate, finish_date)
WHERE _id = :id;