diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c6c36209c4..cc1dd45c37 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -7,6 +7,7 @@ plugins { kotlin("plugin.serialization") id("kotlin-parcelize") id("com.google.android.gms.oss-licenses-plugin") + id("app.cash.sqldelight") id("com.google.gms.google-services") apply false id("com.google.firebase.crashlytics") apply false } @@ -142,6 +143,16 @@ android { jvmTarget = "17" } namespace = "eu.kanade.tachiyomi" + + sqldelight { + databases { + create("Database") { + packageName.set("tachiyomi.data") + dialect(libs.sqldelight.dialects.sql) + schemaOutputDirectory.set(project.file("./src/main/sqldelight")) + } + } + } } dependencies { @@ -208,7 +219,9 @@ dependencies { implementation(libs.play.services.gcm) // Database + implementation(libs.bundles.db) implementation(libs.sqlite.android) + implementation(libs.bundles.sqlite) //noinspection UseTomlInstead implementation("com.github.inorichi.storio:storio-common:8be19de@aar") //noinspection UseTomlInstead diff --git a/app/src/main/java/dev/yokai/data/AndroidDatabaseHandler.kt b/app/src/main/java/dev/yokai/data/AndroidDatabaseHandler.kt new file mode 100644 index 0000000000..8ed92f3e92 --- /dev/null +++ b/app/src/main/java/dev/yokai/data/AndroidDatabaseHandler.kt @@ -0,0 +1,93 @@ +package dev.yokai.data + +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 +import tachiyomi.data.Database + +class AndroidDatabaseHandler( + val db: Database, + private val driver: SqlDriver, + val queryDispatcher: CoroutineDispatcher = Dispatchers.IO, + val transactionDispatcher: CoroutineDispatcher = queryDispatcher +) : DatabaseHandler { + + val suspendingTransactionId = ThreadLocal() + + override suspend fun await(inTransaction: Boolean, block: suspend Database.() -> T): T { + return dispatch(inTransaction, block) + } + + override suspend fun awaitList( + inTransaction: Boolean, + block: suspend Database.() -> Query + ): List { + return dispatch(inTransaction) { block(db).executeAsList() } + } + + override suspend fun awaitOne( + inTransaction: Boolean, + block: suspend Database.() -> Query + ): T { + return dispatch(inTransaction) { block(db).executeAsOne() } + } + + override suspend fun awaitOneOrNull( + inTransaction: Boolean, + block: suspend Database.() -> Query + ): T? { + return dispatch(inTransaction) { block(db).executeAsOneOrNull() } + } + + override fun subscribeToList(block: Database.() -> Query): Flow> { + return block(db).asFlow().mapToList(queryDispatcher) + } + + override fun subscribeToOne(block: Database.() -> Query): Flow { + return block(db).asFlow().mapToOne(queryDispatcher) + } + + override fun subscribeToOneOrNull(block: Database.() -> Query): Flow { + return block(db).asFlow().mapToOneOrNull(queryDispatcher) + } + + /* + override fun subscribeToPagingSource( + countQuery: Database.() -> Query, + transacter: Database.() -> Transacter, + queryProvider: Database.(Long, Long) -> Query + ): PagingSource { + return QueryPagingSource( + countQuery = countQuery(db), + transacter = transacter(db), + dispatcher = queryDispatcher, + queryProvider = { limit, offset -> + queryProvider.invoke(db, limit, offset) + } + ) + } + */ + + private suspend fun 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) } + } +} diff --git a/app/src/main/java/dev/yokai/data/DatabaseAdapter.kt b/app/src/main/java/dev/yokai/data/DatabaseAdapter.kt new file mode 100644 index 0000000000..d26141845e --- /dev/null +++ b/app/src/main/java/dev/yokai/data/DatabaseAdapter.kt @@ -0,0 +1,20 @@ +package dev.yokai.data + +import app.cash.sqldelight.ColumnAdapter +import java.util.Date + +val dateAdapter = object : ColumnAdapter { + override fun decode(databaseValue: Long): Date = Date(databaseValue) + override fun encode(value: Date): Long = value.time +} + +private const val listOfStringsSeparator = ", " +val listOfStringsAdapter = object : ColumnAdapter, String> { + override fun decode(databaseValue: String) = + if (databaseValue.isEmpty()) { + listOf() + } else { + databaseValue.split(listOfStringsSeparator) + } + override fun encode(value: List) = value.joinToString(separator = listOfStringsSeparator) +} diff --git a/app/src/main/java/dev/yokai/data/DatabaseHandler.kt b/app/src/main/java/dev/yokai/data/DatabaseHandler.kt new file mode 100644 index 0000000000..5232f8cb2e --- /dev/null +++ b/app/src/main/java/dev/yokai/data/DatabaseHandler.kt @@ -0,0 +1,39 @@ +package dev.yokai.data + +import app.cash.sqldelight.Query +import app.cash.sqldelight.Transacter +import kotlinx.coroutines.flow.Flow +import tachiyomi.data.Database + +interface DatabaseHandler { + suspend fun await(inTransaction: Boolean = false, block: suspend Database.() -> T): T + + suspend fun awaitList( + inTransaction: Boolean = false, + block: suspend Database.() -> Query + ): List + + suspend fun awaitOne( + inTransaction: Boolean = false, + block: suspend Database.() -> Query + ): T + + suspend fun awaitOneOrNull( + inTransaction: Boolean = false, + block: suspend Database.() -> Query + ): T? + + fun subscribeToList(block: Database.() -> Query): Flow> + + fun subscribeToOne(block: Database.() -> Query): Flow + + fun subscribeToOneOrNull(block: Database.() -> Query): Flow + + /* + fun subscribeToPagingSource( + countQuery: Database.() -> Query, + transacter: Database.() -> Transacter, + queryProvider: Database.(Long, Long) -> Query + ): PagingSource + */ +} diff --git a/app/src/main/java/dev/yokai/data/TransactionContext.kt b/app/src/main/java/dev/yokai/data/TransactionContext.kt new file mode 100644 index 0000000000..53e0086c1b --- /dev/null +++ b/app/src/main/java/dev/yokai/data/TransactionContext.kt @@ -0,0 +1,161 @@ +package dev.yokai.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 java.util.concurrent.atomic.AtomicInteger +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 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. + */ +private class TransactionElement( + private val transactionThreadControlJob: Job, + val transactionDispatcher: ContinuationInterceptor +) : CoroutineContext.Element { + + companion object Key : CoroutineContext.Key + + override val key: CoroutineContext.Key + 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 = AtomicInteger(0) + + fun acquire() { + referenceCount.incrementAndGet() + } + + fun release() { + val count = referenceCount.decrementAndGet() + 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() + } + } +} diff --git a/app/src/main/java/dev/yokai/data/manga/MangaMapper.kt b/app/src/main/java/dev/yokai/data/manga/MangaMapper.kt new file mode 100644 index 0000000000..fb9cf0a4a8 --- /dev/null +++ b/app/src/main/java/dev/yokai/data/manga/MangaMapper.kt @@ -0,0 +1,28 @@ +package dev.yokai.data.manga + +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.updateStrategyAdapter + +val mangaMapper: (Long, Long, String, String?, String?, String?, String?, String, Int, String?, Boolean, Long, Boolean, Int, Int, Boolean, Long, String?, Int) -> Manga = + { id, source, url, artist, author, description, genre, title, status, thumbnailUrl, favorite, lastUpdate, initialized, viewerFlags, chapterFlags, hideTitle, dateAdded, filteredScanlators, updateStrategy -> + Manga.create(source).apply { + this.id = id + this.url = url + this.artist = artist + this.author = author + this.description = description + this.genre = genre + this.title = title + this.status = status + this.thumbnail_url = thumbnailUrl + this.favorite = favorite + this.last_update = lastUpdate + this.initialized = initialized + this.viewer_flags = viewerFlags + this.chapter_flags = chapterFlags + this.hide_title = hideTitle + this.date_added = dateAdded + this.filtered_scanlators = filteredScanlators + this.update_strategy = updateStrategy.let(updateStrategyAdapter::decode) + } + } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseAdapter.kt index f5f4d3a90c..ba5fe18e82 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseAdapter.kt @@ -3,21 +3,7 @@ package eu.kanade.tachiyomi.data.database import eu.kanade.tachiyomi.source.model.UpdateStrategy import java.util.Date -val dateAdapter = object : ColumnAdapter { - override fun decode(databaseValue: Long): Date = Date(databaseValue) - override fun encode(value: Date): Long = value.time -} - -private const val listOfStringsSeparator = ", " -val listOfStringsAdapter = object : ColumnAdapter, String> { - override fun decode(databaseValue: String) = - if (databaseValue.isEmpty()) { - emptyList() - } else { - databaseValue.split(listOfStringsSeparator) - } - override fun encode(value: List) = value.joinToString(separator = listOfStringsSeparator) -} +// TODO: Move to dev.yokai.data.DatabaseAdapter val updateStrategyAdapter = object : ColumnAdapter { private val enumValues by lazy { UpdateStrategy.entries } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt index af1f84dfd4..4932dc2ca9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt @@ -29,7 +29,10 @@ import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory /** * This class provides operations to manage the database through its interfaces. */ -open class DatabaseHelper(context: Context) : +open class DatabaseHelper( + context: Context, + openHelper: SupportSQLiteOpenHelper, +) : MangaQueries, ChapterQueries, TrackQueries, @@ -38,13 +41,8 @@ open class DatabaseHelper(context: Context) : HistoryQueries, SearchMetadataQueries { - private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context) - .name(DbOpenCallback.DATABASE_NAME) - .callback(DbOpenCallback()) - .build() - override val db = DefaultStorIOSQLite.builder() - .sqliteOpenHelper(RequerySQLiteOpenHelperFactory().create(configuration)) + .sqliteOpenHelper(openHelper) .addTypeMapping(Manga::class.java, MangaTypeMapping()) .addTypeMapping(Chapter::class.java, ChapterTypeMapping()) .addTypeMapping(Track::class.java, TrackTypeMapping()) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt index 1d5ed74f3e..fc5ab39315 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt @@ -1,26 +1,17 @@ package eu.kanade.tachiyomi.data.database import androidx.sqlite.db.SupportSQLiteDatabase -import androidx.sqlite.db.SupportSQLiteOpenHelper -import eu.kanade.tachiyomi.data.database.tables.CategoryTable -import eu.kanade.tachiyomi.data.database.tables.ChapterTable -import eu.kanade.tachiyomi.data.database.tables.HistoryTable -import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable -import eu.kanade.tachiyomi.data.database.tables.MangaTable -import eu.kanade.tachiyomi.data.database.tables.TrackTable +import app.cash.sqldelight.driver.android.AndroidSqliteDriver +import tachiyomi.data.Database +import timber.log.Timber -class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) { +class DbOpenCallback : AndroidSqliteDriver.Callback(Database.Schema) { companion object { /** * Name of the database file. */ const val DATABASE_NAME = "tachiyomi.db" - - /** - * Version of the database. - */ - const val DATABASE_VERSION = 17 } override fun onOpen(db: SupportSQLiteDatabase) { @@ -36,84 +27,15 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) { cursor.close() } - override fun onCreate(db: SupportSQLiteDatabase) = with(db) { - execSQL(MangaTable.createTableQuery) - execSQL(ChapterTable.createTableQuery) - execSQL(TrackTable.createTableQuery) - execSQL(CategoryTable.createTableQuery) - execSQL(MangaCategoryTable.createTableQuery) - execSQL(HistoryTable.createTableQuery) - - // DB indexes - execSQL(MangaTable.createUrlIndexQuery) - execSQL(MangaTable.createLibraryIndexQuery) - execSQL(ChapterTable.createMangaIdIndexQuery) - execSQL(ChapterTable.createUnreadChaptersIndexQuery) - execSQL(HistoryTable.createChapterIdIndexQuery) + override fun onCreate(db: SupportSQLiteDatabase) { + Timber.d("Creating new database...") + super.onCreate(db) } override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) { - if (oldVersion < 2) { - db.execSQL(ChapterTable.sourceOrderUpdateQuery) - - // Fix kissmanga covers after supporting cloudflare - db.execSQL( - """UPDATE mangas SET thumbnail_url = - REPLACE(thumbnail_url, '93.174.95.110', 'kissmanga.com') WHERE source = 4""", - ) - } - if (oldVersion < 3) { - // Initialize history tables - db.execSQL(HistoryTable.createTableQuery) - db.execSQL(HistoryTable.createChapterIdIndexQuery) - } - if (oldVersion < 4) { - db.execSQL(ChapterTable.bookmarkUpdateQuery) - } - if (oldVersion < 5) { - db.execSQL(ChapterTable.addScanlator) - } - if (oldVersion < 6) { - db.execSQL(TrackTable.addTrackingUrl) - } - if (oldVersion < 7) { - db.execSQL(TrackTable.addLibraryId) - } - if (oldVersion < 8) { - db.execSQL("DROP INDEX IF EXISTS mangas_favorite_index") - db.execSQL(MangaTable.createLibraryIndexQuery) - db.execSQL(ChapterTable.createUnreadChaptersIndexQuery) - } - if (oldVersion < 9) { - db.execSQL(MangaTable.addHideTitle) - } - if (oldVersion < 10) { - db.execSQL(CategoryTable.addMangaOrder) - } - if (oldVersion < 11) { - db.execSQL(ChapterTable.pagesLeftQuery) - } - if (oldVersion < 12) { - db.execSQL(MangaTable.addDateAddedCol) - } - if (oldVersion < 13) { - db.execSQL(TrackTable.addStartDate) - db.execSQL(TrackTable.addFinishDate) - } - if (oldVersion < 14) { - db.execSQL(MangaTable.addFilteredScanlators) - } - if (oldVersion < 15) { - db.execSQL(TrackTable.renameTableToTemp) - db.execSQL(TrackTable.createTableQuery) - db.execSQL(TrackTable.insertFromTempTable) - db.execSQL(TrackTable.dropTempTable) - } - if (oldVersion < 16) { - db.execSQL(MangaTable.addUpdateStrategy) - } - if (oldVersion < 17) { - db.execSQL(TrackTable.updateMangaUpdatesScore) + if (oldVersion < newVersion) { + Timber.d("Upgrading database from $oldVersion to $newVersion") + super.onUpgrade(db, oldVersion, newVersion) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/CategoryTable.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/CategoryTable.kt index d3b9477b8a..7d515bc53f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/CategoryTable.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/CategoryTable.kt @@ -14,16 +14,4 @@ object CategoryTable { const val COL_MANGA_ORDER = "manga_order" - val createTableQuery: String - get() = - """CREATE TABLE $TABLE( - $COL_ID INTEGER NOT NULL PRIMARY KEY, - $COL_NAME TEXT NOT NULL, - $COL_ORDER INTEGER NOT NULL, - $COL_FLAGS INTEGER NOT NULL, - $COL_MANGA_ORDER TEXT NOT NULL - )""" - - val addMangaOrder: String - get() = "ALTER TABLE $TABLE ADD COLUMN $COL_MANGA_ORDER TEXT" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/ChapterTable.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/ChapterTable.kt index 5448ff7ef7..36a472fdfc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/ChapterTable.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/ChapterTable.kt @@ -30,42 +30,4 @@ object ChapterTable { const val COL_SOURCE_ORDER = "source_order" - val createTableQuery: String - get() = - """CREATE TABLE $TABLE( - $COL_ID INTEGER NOT NULL PRIMARY KEY, - $COL_MANGA_ID INTEGER NOT NULL, - $COL_URL TEXT NOT NULL, - $COL_NAME TEXT NOT NULL, - $COL_SCANLATOR TEXT, - $COL_READ BOOLEAN NOT NULL, - $COL_BOOKMARK BOOLEAN NOT NULL, - $COL_LAST_PAGE_READ INT NOT NULL, - $COL_PAGES_LEFT INT NOT NULL, - $COL_CHAPTER_NUMBER FLOAT NOT NULL, - $COL_SOURCE_ORDER INTEGER NOT NULL, - $COL_DATE_FETCH LONG NOT NULL, - $COL_DATE_UPLOAD LONG NOT NULL, - FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID}) - ON DELETE CASCADE - )""" - - val createMangaIdIndexQuery: String - get() = "CREATE INDEX ${TABLE}_${COL_MANGA_ID}_index ON $TABLE($COL_MANGA_ID)" - - val createUnreadChaptersIndexQuery: String - get() = "CREATE INDEX ${TABLE}_unread_by_manga_index ON $TABLE($COL_MANGA_ID, $COL_READ) " + - "WHERE $COL_READ = 0" - - val sourceOrderUpdateQuery: String - get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SOURCE_ORDER INTEGER DEFAULT 0" - - val bookmarkUpdateQuery: String - get() = "ALTER TABLE $TABLE ADD COLUMN $COL_BOOKMARK BOOLEAN DEFAULT FALSE" - - val addScanlator: String - get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SCANLATOR TEXT DEFAULT NULL" - - val pagesLeftQuery: String - get() = "ALTER TABLE $TABLE ADD COLUMN $COL_PAGES_LEFT INTEGER DEFAULT 0" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaCategoryTable.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaCategoryTable.kt index 578a85bbc9..79aa1cb955 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaCategoryTable.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaCategoryTable.kt @@ -10,15 +10,4 @@ object MangaCategoryTable { const val COL_CATEGORY_ID = "category_id" - val createTableQuery: String - get() = - """CREATE TABLE $TABLE( - $COL_ID INTEGER NOT NULL PRIMARY KEY, - $COL_MANGA_ID INTEGER NOT NULL, - $COL_CATEGORY_ID INTEGER NOT NULL, - FOREIGN KEY($COL_CATEGORY_ID) REFERENCES ${CategoryTable.TABLE} (${CategoryTable.COL_ID}) - ON DELETE CASCADE, - FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID}) - ON DELETE CASCADE - )""" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaTable.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaTable.kt index 71e7f5f5c8..e43989dab6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaTable.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaTable.kt @@ -50,47 +50,4 @@ object MangaTable { const val COL_UPDATE_STRATEGY = "update_strategy" - val createTableQuery: String - get() = - """CREATE TABLE $TABLE( - $COL_ID INTEGER NOT NULL PRIMARY KEY, - $COL_SOURCE INTEGER NOT NULL, - $COL_URL TEXT NOT NULL, - $COL_ARTIST TEXT, - $COL_AUTHOR TEXT, - $COL_DESCRIPTION TEXT, - $COL_GENRE TEXT, - $COL_TITLE TEXT NOT NULL, - $COL_STATUS INTEGER NOT NULL, - $COL_THUMBNAIL_URL TEXT, - $COL_FAVORITE INTEGER NOT NULL, - $COL_LAST_UPDATE LONG, - $COL_INITIALIZED BOOLEAN NOT NULL, - $COL_VIEWER INTEGER NOT NULL, - $COL_HIDE_TITLE INTEGER NOT NULL, - $COL_CHAPTER_FLAGS INTEGER NOT NULL, - $COL_DATE_ADDED LONG, - $COL_FILTERED_SCANLATORS TEXT, - $COL_UPDATE_STRATEGY INTEGER NOT NULL DEFAULT 0 - - )""" - - val createUrlIndexQuery: String - get() = "CREATE INDEX ${TABLE}_${COL_URL}_index ON $TABLE($COL_URL)" - - val createLibraryIndexQuery: String - get() = "CREATE INDEX library_${COL_FAVORITE}_index ON $TABLE($COL_FAVORITE) " + - "WHERE $COL_FAVORITE = 1" - - val addHideTitle: String - get() = "ALTER TABLE $TABLE ADD COLUMN $COL_HIDE_TITLE INTEGER DEFAULT 0" - - val addDateAddedCol: String - get() = "ALTER TABLE $TABLE ADD COLUMN $COL_DATE_ADDED LONG DEFAULT 0" - - val addFilteredScanlators: String - get() = "ALTER TABLE $TABLE ADD COLUMN $COL_FILTERED_SCANLATORS TEXT" - - val addUpdateStrategy: String - get() = "ALTER TABLE $TABLE ADD COLUMN $COL_UPDATE_STRATEGY INTEGER NOT NULL DEFAULT 0" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/TrackTable.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/TrackTable.kt index 6a82f1600f..fb62651a09 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/TrackTable.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/TrackTable.kt @@ -1,7 +1,5 @@ package eu.kanade.tachiyomi.data.database.tables -import eu.kanade.tachiyomi.data.track.TrackManager - object TrackTable { const val TABLE = "manga_sync" @@ -32,59 +30,4 @@ object TrackTable { const val COL_FINISH_DATE = "finish_date" - val createTableQuery: String - get() = - """CREATE TABLE $TABLE( - $COL_ID INTEGER NOT NULL PRIMARY KEY, - $COL_MANGA_ID INTEGER NOT NULL, - $COL_SYNC_ID INTEGER NOT NULL, - $COL_MEDIA_ID INTEGER NOT NULL, - $COL_LIBRARY_ID INTEGER, - $COL_TITLE TEXT NOT NULL, - $COL_LAST_CHAPTER_READ REAL NOT NULL, - $COL_TOTAL_CHAPTERS INTEGER NOT NULL, - $COL_STATUS INTEGER NOT NULL, - $COL_SCORE FLOAT NOT NULL, - $COL_TRACKING_URL TEXT NOT NULL, - $COL_START_DATE LONG NOT NULL, - $COL_FINISH_DATE LONG NOT NULL, - UNIQUE ($COL_MANGA_ID, $COL_SYNC_ID) ON CONFLICT REPLACE, - FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID}) - ON DELETE CASCADE - )""" - - val addTrackingUrl: String - get() = "ALTER TABLE $TABLE ADD COLUMN $COL_TRACKING_URL TEXT DEFAULT ''" - - val addLibraryId: String - get() = "ALTER TABLE $TABLE ADD COLUMN $COL_LIBRARY_ID INTEGER NULL" - - val addStartDate: String - get() = "ALTER TABLE $TABLE ADD COLUMN $COL_START_DATE LONG NOT NULL DEFAULT 0" - - val addFinishDate: String - get() = "ALTER TABLE $TABLE ADD COLUMN $COL_FINISH_DATE LONG NOT NULL DEFAULT 0" - - val renameTableToTemp: String - get() = - "ALTER TABLE $TABLE RENAME TO ${TABLE}_tmp" - - val insertFromTempTable: String - get() = - """ - |INSERT INTO $TABLE($COL_ID,$COL_MANGA_ID,$COL_SYNC_ID,$COL_MEDIA_ID,$COL_LIBRARY_ID,$COL_TITLE,$COL_LAST_CHAPTER_READ,$COL_TOTAL_CHAPTERS,$COL_STATUS,$COL_SCORE,$COL_TRACKING_URL,$COL_START_DATE,$COL_FINISH_DATE) - |SELECT $COL_ID,$COL_MANGA_ID,$COL_SYNC_ID,$COL_MEDIA_ID,$COL_LIBRARY_ID,$COL_TITLE,$COL_LAST_CHAPTER_READ,$COL_TOTAL_CHAPTERS,$COL_STATUS,$COL_SCORE,$COL_TRACKING_URL,$COL_START_DATE,$COL_FINISH_DATE - |FROM ${TABLE}_tmp - """.trimMargin() - - val dropTempTable: String - get() = "DROP TABLE ${TABLE}_tmp" - - val updateMangaUpdatesScore: String - get() = - """ - UPDATE $TABLE - SET $COL_SCORE = max($COL_SCORE, 0) - WHERE $COL_SYNC_ID = ${TrackManager.MANGA_UPDATES}; - """.trimIndent() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt b/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt index 6d3841192d..d12fc94d1c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt @@ -1,14 +1,23 @@ package eu.kanade.tachiyomi.di import android.app.Application +import android.os.Build import androidx.core.content.ContextCompat +import androidx.sqlite.db.SupportSQLiteOpenHelper +import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory +import app.cash.sqldelight.db.SqlDriver +import app.cash.sqldelight.driver.android.AndroidSqliteDriver +import dev.yokai.data.AndroidDatabaseHandler +import dev.yokai.data.DatabaseHandler import dev.yokai.domain.SplashState import dev.yokai.domain.extension.TrustExtension import dev.yokai.domain.storage.StorageManager +import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.core.storage.AndroidStorageFolderProvider import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.DbOpenCallback import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.library.CustomMangaManager import eu.kanade.tachiyomi.data.track.TrackManager @@ -18,10 +27,12 @@ import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.util.chapter.ChapterFilter import eu.kanade.tachiyomi.util.manga.MangaShortcutManager +import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory import kotlinx.serialization.json.Json import nl.adaptivity.xmlutil.XmlDeclMode import nl.adaptivity.xmlutil.core.XmlVersion import nl.adaptivity.xmlutil.serialization.XML +import tachiyomi.data.Database import uy.kohesive.injekt.api.InjektModule import uy.kohesive.injekt.api.InjektRegistrar import uy.kohesive.injekt.api.addSingleton @@ -33,7 +44,49 @@ class AppModule(val app: Application) : InjektModule { override fun InjektRegistrar.registerInjectables() { addSingleton(app) - addSingletonFactory { DatabaseHelper(app) } + addSingletonFactory { + val configuration = SupportSQLiteOpenHelper.Configuration.builder(app) + .callback(DbOpenCallback()) + .name(DbOpenCallback.DATABASE_NAME) + .noBackupDirectory(false) + .build() + + /* + if (BuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // Support database inspector in Android Studio + FrameworkSQLiteOpenHelperFactory().create(configuration) + } else { + RequerySQLiteOpenHelperFactory().create(configuration) + } + */ + RequerySQLiteOpenHelperFactory().create(configuration) + } + + addSingletonFactory { + AndroidSqliteDriver(openHelper = get()) + /* + AndroidSqliteDriver( + schema = Database.Schema, + context = app, + name = "tachiyomi.db", + factory = if (BuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // Support database inspector in Android Studio + FrameworkSQLiteOpenHelperFactory() + } else { + RequerySQLiteOpenHelperFactory() + }, + callback = get(), + ) + */ + } + addSingletonFactory { + Database( + driver = get(), + ) + } + addSingletonFactory { AndroidDatabaseHandler(get(), get()) } + + addSingletonFactory { DatabaseHelper(app, get()) } addSingletonFactory { ChapterCache(app) } @@ -87,6 +140,8 @@ class AppModule(val app: Application) : InjektModule { get() + get() + get() get() diff --git a/app/src/main/sqldelight/tachiyomi/data/categories.sq b/app/src/main/sqldelight/tachiyomi/data/categories.sq new file mode 100644 index 0000000000..a108b81891 --- /dev/null +++ b/app/src/main/sqldelight/tachiyomi/data/categories.sq @@ -0,0 +1,7 @@ +CREATE TABLE categories( + _id INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + sort INTEGER NOT NULL, + flags INTEGER NOT NULL, + manga_order TEXT NOT NULL +); diff --git a/app/src/main/sqldelight/tachiyomi/data/chapters.sq b/app/src/main/sqldelight/tachiyomi/data/chapters.sq new file mode 100644 index 0000000000..9664191dd9 --- /dev/null +++ b/app/src/main/sqldelight/tachiyomi/data/chapters.sq @@ -0,0 +1,24 @@ +import kotlin.Boolean; +import kotlin.Float; +import kotlin.Long; + +CREATE TABLE chapters( + _id INTEGER NOT NULL PRIMARY KEY, + manga_id INTEGER NOT NULL, + url TEXT NOT NULL, + name TEXT NOT NULL, + scanlator TEXT, + read INTEGER AS Boolean NOT NULL, + bookmark INTEGER AS Boolean NOT NULL, + last_page_read INTEGER NOT NULL, + pages_left INTEGER NOT NULL, + chapter_number REAL AS Float NOT NULL, + source_order INTEGER NOT NULL, + date_fetch INTEGER AS Long NOT NULL, + date_upload INTEGER AS Long NOT NULL, + FOREIGN KEY(manga_id) REFERENCES mangas (_id) + ON DELETE CASCADE +); + +CREATE INDEX chapters_manga_id_index ON chapters(manga_id); +CREATE INDEX chapters_unread_by_manga_index ON chapters(manga_id, read) WHERE read = 0; diff --git a/app/src/main/sqldelight/tachiyomi/data/history.sq b/app/src/main/sqldelight/tachiyomi/data/history.sq new file mode 100644 index 0000000000..e702010df0 --- /dev/null +++ b/app/src/main/sqldelight/tachiyomi/data/history.sq @@ -0,0 +1,12 @@ +import kotlin.Long; + +CREATE TABLE history( + history_id INTEGER NOT NULL PRIMARY KEY, + history_chapter_id INTEGER NOT NULL UNIQUE, + history_last_read INTEGER AS Long, + history_time_read INTEGER AS Long, + FOREIGN KEY(history_chapter_id) REFERENCES chapters (_id) + ON DELETE CASCADE +); + +CREATE INDEX history_history_chapter_id_index ON history(history_chapter_id); diff --git a/app/src/main/sqldelight/tachiyomi/data/manga_categories.sq b/app/src/main/sqldelight/tachiyomi/data/manga_categories.sq new file mode 100644 index 0000000000..2984135757 --- /dev/null +++ b/app/src/main/sqldelight/tachiyomi/data/manga_categories.sq @@ -0,0 +1,9 @@ +CREATE TABLE mangas_categories( + _id INTEGER NOT NULL PRIMARY KEY, + manga_id INTEGER NOT NULL, + category_id INTEGER NOT NULL, + FOREIGN KEY(category_id) REFERENCES categories (_id) + ON DELETE CASCADE, + FOREIGN KEY(manga_id) REFERENCES mangas (_id) + ON DELETE CASCADE +); diff --git a/app/src/main/sqldelight/tachiyomi/data/manga_sync.sq b/app/src/main/sqldelight/tachiyomi/data/manga_sync.sq new file mode 100644 index 0000000000..7e95fbb802 --- /dev/null +++ b/app/src/main/sqldelight/tachiyomi/data/manga_sync.sq @@ -0,0 +1,21 @@ +import kotlin.Float; +import kotlin.Long; + +CREATE TABLE manga_sync( + _id INTEGER NOT NULL PRIMARY KEY, + manga_id INTEGER NOT NULL, + sync_id INTEGER NOT NULL, + remote_id INTEGER NOT NULL, + library_id INTEGER, + title TEXT NOT NULL, + last_chapter_read REAL NOT NULL, + total_chapters INTEGER NOT NULL, + status INTEGER NOT NULL, + score REAL AS Float NOT NULL, + remote_url TEXT NOT NULL, + start_date INTEGER AS Long NOT NULL, + finish_date INTEGER AS Long NOT NULL, + UNIQUE (manga_id, sync_id) ON CONFLICT REPLACE, + FOREIGN KEY(manga_id) REFERENCES mangas (_id) + ON DELETE CASCADE +); diff --git a/app/src/main/sqldelight/tachiyomi/data/mangas.sq b/app/src/main/sqldelight/tachiyomi/data/mangas.sq new file mode 100644 index 0000000000..48f2b9b6dc --- /dev/null +++ b/app/src/main/sqldelight/tachiyomi/data/mangas.sq @@ -0,0 +1,27 @@ +import kotlin.Boolean; +import kotlin.Long; + +CREATE TABLE mangas( + _id INTEGER NOT NULL PRIMARY KEY, + source INTEGER NOT NULL, + url TEXT NOT NULL, + artist TEXT, + author TEXT, + description TEXT, + genre TEXT, + title TEXT NOT NULL, + status INTEGER NOT NULL, + thumbnail_url TEXT, + favorite INTEGER NOT NULL, + last_update INTEGER AS Long, + initialized INTEGER AS Boolean NOT NULL, + viewer INTEGER NOT NULL, + hideTitle INTEGER NOT NULL, + chapter_flags INTEGER NOT NULL, + date_added INTEGER AS Long, + filtered_scanlators TEXT, + update_strategy INTEGER NOT NULL DEFAULT 0 +); + +CREATE INDEX mangas_url_index ON mangas(url); +CREATE INDEX library_favorite_index ON mangas(favorite) WHERE favorite = 1; diff --git a/app/src/main/sqldelight/tachiyomi/migrations/1.sqm b/app/src/main/sqldelight/tachiyomi/migrations/1.sqm new file mode 100644 index 0000000000..69cce44697 --- /dev/null +++ b/app/src/main/sqldelight/tachiyomi/migrations/1.sqm @@ -0,0 +1,3 @@ +ALTER TABLE chapters ADD COLUMN source_order INTEGER DEFAULT 0; + +UPDATE mangas SET thumbnail_url = REPLACE(thumbnail_url, '93.174.95.110', 'kissmanga.com') WHERE source = 4; diff --git a/app/src/main/sqldelight/tachiyomi/migrations/10.sqm b/app/src/main/sqldelight/tachiyomi/migrations/10.sqm new file mode 100644 index 0000000000..b0442c62fe --- /dev/null +++ b/app/src/main/sqldelight/tachiyomi/migrations/10.sqm @@ -0,0 +1 @@ +ALTER TABLE chapters ADD COLUMN pages_left INTEGER DEFAULT 0; diff --git a/app/src/main/sqldelight/tachiyomi/migrations/11.sqm b/app/src/main/sqldelight/tachiyomi/migrations/11.sqm new file mode 100644 index 0000000000..166b7ecb13 --- /dev/null +++ b/app/src/main/sqldelight/tachiyomi/migrations/11.sqm @@ -0,0 +1 @@ +ALTER TABLE mangas ADD COLUMN date_added INTEGER NOT NULL DEFAULT 0; diff --git a/app/src/main/sqldelight/tachiyomi/migrations/12.sqm b/app/src/main/sqldelight/tachiyomi/migrations/12.sqm new file mode 100644 index 0000000000..ef57eff6c5 --- /dev/null +++ b/app/src/main/sqldelight/tachiyomi/migrations/12.sqm @@ -0,0 +1,2 @@ +ALTER TABLE manga_sync ADD COLUMN start_date INTEGER NOT NULL DEFAULT 0; +ALTER TABLE manga_sync ADD COLUMN finish_date INTEGER NOT NULL DEFAULT 0; diff --git a/app/src/main/sqldelight/tachiyomi/migrations/13.sqm b/app/src/main/sqldelight/tachiyomi/migrations/13.sqm new file mode 100644 index 0000000000..4370d8972c --- /dev/null +++ b/app/src/main/sqldelight/tachiyomi/migrations/13.sqm @@ -0,0 +1 @@ +ALTER TABLE mangas ADD COLUMN filtered_scanlators TEXT; diff --git a/app/src/main/sqldelight/tachiyomi/migrations/14.sqm b/app/src/main/sqldelight/tachiyomi/migrations/14.sqm new file mode 100644 index 0000000000..8ab67e2c6f --- /dev/null +++ b/app/src/main/sqldelight/tachiyomi/migrations/14.sqm @@ -0,0 +1,29 @@ +import kotlin.Float; +import kotlin.Long; + +ALTER TABLE manga_sync RENAME TO manga_sync_tmp; + +CREATE TABLE manga_sync( + _id INTEGER NOT NULL PRIMARY KEY, + manga_id INTEGER NOT NULL, + sync_id INTEGER NOT NULL, + remote_id INTEGER NOT NULL, + library_id INTEGER, + title TEXT NOT NULL, + last_chapter_read REAL NOT NULL, + total_chapters INTEGER NOT NULL, + status INTEGER NOT NULL, + score REAL AS Float NOT NULL, + remote_url TEXT NOT NULL, + start_date INTEGER AS Long NOT NULL, + finish_date INTEGER AS Long NOT NULL, + UNIQUE (manga_id, sync_id) ON CONFLICT REPLACE, + FOREIGN KEY(manga_id) REFERENCES mangas (_id) + ON DELETE CASCADE +); + +INSERT INTO manga_sync(_id, manga_id, sync_id, remote_id, library_id, title, last_chapter_read, total_chapters, status, score, remote_url, start_date, finish_date) +SELECT _id,manga_id, sync_id, remote_id, library_id, title, last_chapter_read, total_chapters, status, score, remote_url, start_date, finish_date +FROM manga_sync_tmp; + +DROP TABLE manga_sync_tmp; diff --git a/app/src/main/sqldelight/tachiyomi/migrations/15.sqm b/app/src/main/sqldelight/tachiyomi/migrations/15.sqm new file mode 100644 index 0000000000..8c4cc983a6 --- /dev/null +++ b/app/src/main/sqldelight/tachiyomi/migrations/15.sqm @@ -0,0 +1 @@ +ALTER TABLE mangas ADD COLUMN update_strategy INTEGER NOT NULL DEFAULT 0; diff --git a/app/src/main/sqldelight/tachiyomi/migrations/16.sqm b/app/src/main/sqldelight/tachiyomi/migrations/16.sqm new file mode 100644 index 0000000000..52c2dbdd1d --- /dev/null +++ b/app/src/main/sqldelight/tachiyomi/migrations/16.sqm @@ -0,0 +1,3 @@ +UPDATE manga_sync +SET score = max(score, 0) +WHERE sync_id = 7; diff --git a/app/src/main/sqldelight/tachiyomi/migrations/2.sqm b/app/src/main/sqldelight/tachiyomi/migrations/2.sqm new file mode 100644 index 0000000000..0e4efe018c --- /dev/null +++ b/app/src/main/sqldelight/tachiyomi/migrations/2.sqm @@ -0,0 +1,10 @@ +CREATE TABLE history( + history_id INTEGER NOT NULL PRIMARY KEY, + history_chapter_id INTEGER NOT NULL UNIQUE, + history_last_read INTEGER, + history_time_read INTEGER, + FOREIGN KEY(history_chapter_id) REFERENCES chapters (_id) + ON DELETE CASCADE +); + +CREATE INDEX history_history_chapter_id_index ON history(history_chapter_id); diff --git a/app/src/main/sqldelight/tachiyomi/migrations/3.sqm b/app/src/main/sqldelight/tachiyomi/migrations/3.sqm new file mode 100644 index 0000000000..aae646b644 --- /dev/null +++ b/app/src/main/sqldelight/tachiyomi/migrations/3.sqm @@ -0,0 +1 @@ +ALTER TABLE chapters ADD COLUMN bookmark INTEGER DEFAULT 0; diff --git a/app/src/main/sqldelight/tachiyomi/migrations/4.sqm b/app/src/main/sqldelight/tachiyomi/migrations/4.sqm new file mode 100644 index 0000000000..ef71e3b29d --- /dev/null +++ b/app/src/main/sqldelight/tachiyomi/migrations/4.sqm @@ -0,0 +1 @@ +ALTER TABLE chapters ADD COLUMN scanlator TEXT DEFAULT NULL; diff --git a/app/src/main/sqldelight/tachiyomi/migrations/5.sqm b/app/src/main/sqldelight/tachiyomi/migrations/5.sqm new file mode 100644 index 0000000000..b2bfaaf950 --- /dev/null +++ b/app/src/main/sqldelight/tachiyomi/migrations/5.sqm @@ -0,0 +1 @@ +ALTER TABLE manga_sync ADD COLUMN remote_url TEXT DEFAULT ''; diff --git a/app/src/main/sqldelight/tachiyomi/migrations/6.sqm b/app/src/main/sqldelight/tachiyomi/migrations/6.sqm new file mode 100644 index 0000000000..55725c73b2 --- /dev/null +++ b/app/src/main/sqldelight/tachiyomi/migrations/6.sqm @@ -0,0 +1 @@ +ALTER TABLE manga_sync ADD COLUMN library_id INTEGER; diff --git a/app/src/main/sqldelight/tachiyomi/migrations/7.sqm b/app/src/main/sqldelight/tachiyomi/migrations/7.sqm new file mode 100644 index 0000000000..3ec8ec9ba8 --- /dev/null +++ b/app/src/main/sqldelight/tachiyomi/migrations/7.sqm @@ -0,0 +1,5 @@ +DROP INDEX IF EXISTS mangas_favorite_index; + +CREATE INDEX library_favorite_index ON mangas(favorite) WHERE favorite = 1; + +CREATE INDEX chapters_unread_by_manga_index ON chapters(manga_id, read) WHERE read = 0; diff --git a/app/src/main/sqldelight/tachiyomi/migrations/8.sqm b/app/src/main/sqldelight/tachiyomi/migrations/8.sqm new file mode 100644 index 0000000000..259688bdd6 --- /dev/null +++ b/app/src/main/sqldelight/tachiyomi/migrations/8.sqm @@ -0,0 +1 @@ +ALTER TABLE mangas ADD COLUMN hideTitle INTEGER DEFAULT 0; diff --git a/app/src/main/sqldelight/tachiyomi/migrations/9.sqm b/app/src/main/sqldelight/tachiyomi/migrations/9.sqm new file mode 100644 index 0000000000..7b96f4a829 --- /dev/null +++ b/app/src/main/sqldelight/tachiyomi/migrations/9.sqm @@ -0,0 +1 @@ +ALTER TABLE categories ADD COLUMN manga_order TEXT; diff --git a/build.gradle.kts b/build.gradle.kts index 1a87930dbe..d97f392324 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,6 +14,7 @@ buildscript { classpath(libs.oss.licenses.plugin) classpath(kotlinx.serialization.gradle) classpath(libs.firebase.crashlytics.gradle) + classpath(libs.sqldelight.gradle) } repositories { gradlePluginPortal() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 281c1d589d..7228f36a17 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,6 +6,8 @@ fast_adapter = "5.6.0" nucleus = "3.0.0" okhttp = "5.0.0-alpha.11" shizuku = "12.1.0" +sqlite = "2.4.0" +sqldelight = "2.0.2" junit = "5.8.2" [libraries] @@ -65,7 +67,17 @@ rxrelay = { module = "com.jakewharton.rxrelay:rxrelay", version = "1.2.0" } rxjava = { module = "io.reactivex:rxjava", version = "1.3.8" } rxandroid = { module = "io.reactivex:rxandroid", version = "1.2.1" } slice = { module = "com.github.mthli:Slice", version = "v1.2" } -sqlite-android = { module = "com.github.requery:sqlite-android", version = "3.39.2" } + +sqlite-framework = { module = "androidx.sqlite:sqlite-framework", version.ref = "sqlite" } +sqlite-ktx = { module = "androidx.sqlite:sqlite-ktx", version.ref = "sqlite" } +sqlite-android = { module = "com.github.requery:sqlite-android", version = "3.45.0" } + +sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions-jvm", version.ref = "sqldelight" } +sqldelight-android-driver = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" } +sqldelight-android-paging = { module = "app.cash.sqldelight:androidx-paging3-extensions", version.ref = "sqldelight" } +sqldelight-dialects-sql = { module = "app.cash.sqldelight:sqlite-3-38-dialect", version.ref = "sqldelight" } +sqldelight-gradle = { module = "app.cash.sqldelight:gradle-plugin", version.ref = "sqldelight" } + subsamplingscaleimageview = { module = "com.github.null2264:subsampling-scale-image-view", version = "338caedb5f" } shizuku-api = { module = "dev.rikka.shizuku:api", version.ref = "shizuku" } shizuku-provider = { module = "dev.rikka.shizuku:provider", version.ref = "shizuku" } @@ -82,7 +94,9 @@ gradle-versions = { id = "com.github.ben-manes.versions", version = "0.42.0" } [bundles] archive = [ "common-compress", "junrar" ] +db = [ "sqldelight-android-driver", "sqldelight-android-paging", "sqldelight-coroutines" ] coil = [ "coil3", "coil3-svg", "coil3-gif", "coil3-okhttp" ] +sqlite = [ "sqlite-framework", "sqlite-ktx" ] test = [ "junit-api", "mockk" ] test-android = [ "junit-android" ] test-runtime = [ "junit-engine" ]