Move SQLDelight to data module (#8954)

And use tachiyomi instead of eu.kanade.tachiyomi for package names in the module
This commit is contained in:
Andreas
2023-01-21 16:37:07 +01:00
committed by GitHub
parent 2b5d9fd76b
commit 823749fc1e
60 changed files with 90 additions and 48 deletions

1
data/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

27
data/build.gradle.kts Normal file
View File

@ -0,0 +1,27 @@
plugins {
id("com.android.library")
kotlin("android")
id("com.squareup.sqldelight")
}
android {
namespace = "tachiyomi.data"
defaultConfig {
consumerProguardFiles("consumer-rules.pro")
}
sqldelight {
database("Database") {
packageName = "tachiyomi.data"
dialect = "sqlite:3.24"
}
}
}
dependencies {
implementation(project(":source-api"))
api(libs.sqldelight.android.driver)
api(libs.sqldelight.coroutines)
api(libs.sqldelight.android.paging)
}

0
data/consumer-rules.pro Normal file
View File

21
data/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View File

@ -0,0 +1,89 @@
package tachiyomi.data
import androidx.paging.PagingSource
import com.squareup.sqldelight.Query
import com.squareup.sqldelight.db.SqlDriver
import com.squareup.sqldelight.runtime.coroutines.asFlow
import com.squareup.sqldelight.runtime.coroutines.mapToList
import com.squareup.sqldelight.runtime.coroutines.mapToOne
import com.squareup.sqldelight.runtime.coroutines.mapToOneOrNull
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> awaitOneOrNull(
inTransaction: Boolean,
block: suspend Database.() -> Query<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) }
}
}

View File

@ -0,0 +1,30 @@
package tachiyomi.data
import com.squareup.sqldelight.ColumnAdapter
import eu.kanade.tachiyomi.source.model.UpdateStrategy
import java.util.Date
val dateAdapter = object : ColumnAdapter<Date, Long> {
override fun decode(databaseValue: Long): Date = Date(databaseValue)
override fun encode(value: Date): Long = value.time
}
private const val listOfStringsSeparator = ", "
val listOfStringsAdapter = object : ColumnAdapter<List<String>, String> {
override fun decode(databaseValue: String) =
if (databaseValue.isEmpty()) {
emptyList()
} else {
databaseValue.split(listOfStringsSeparator)
}
override fun encode(value: List<String>) = value.joinToString(separator = listOfStringsSeparator)
}
val updateStrategyAdapter = object : ColumnAdapter<UpdateStrategy, Long> {
private val enumValues by lazy { UpdateStrategy.values() }
override fun decode(databaseValue: Long): UpdateStrategy =
enumValues.getOrElse(databaseValue.toInt()) { UpdateStrategy.ALWAYS_UPDATE }
override fun encode(value: UpdateStrategy): Long = value.ordinal.toLong()
}

View File

@ -0,0 +1,36 @@
package tachiyomi.data
import androidx.paging.PagingSource
import com.squareup.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> awaitOneOrNull(
inTransaction: Boolean = false,
block: suspend Database.() -> Query<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>
}

View File

@ -0,0 +1,71 @@
package tachiyomi.data
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.squareup.sqldelight.Query
import kotlin.properties.Delegates
class QueryPagingSource<RowType : Any>(
val handler: DatabaseHandler,
val countQuery: Database.() -> Query<Long>,
val queryProvider: Database.(Long, Long) -> Query<RowType>,
) : PagingSource<Long, RowType>(), Query.Listener {
override val jumpingSupported: Boolean = true
private var currentQuery: Query<RowType>? by Delegates.observable(null) { _, old, new ->
old?.removeListener(this)
new?.addListener(this)
}
init {
registerInvalidatedCallback {
currentQuery?.removeListener(this)
currentQuery = null
}
}
override suspend fun load(params: LoadParams<Long>): LoadResult<Long, RowType> {
try {
val key = params.key ?: 0L
val loadSize = params.loadSize
val count = handler.awaitOne { countQuery() }
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 (prevKey, nextKey) = when (params) {
is LoadParams.Append -> { offset - loadSize to offset + loadSize }
else -> { offset to offset + loadSize }
}
return LoadResult.Page(
data = data,
prevKey = if (offset <= 0L || prevKey < 0L) null else prevKey,
nextKey = if (offset + loadSize >= count) null else nextKey,
itemsBefore = maxOf(0L, offset).toInt(),
itemsAfter = maxOf(0L, count - (offset + loadSize)).toInt(),
)
} catch (e: Exception) {
return LoadResult.Error(throwable = e)
}
}
override fun getRefreshKey(state: PagingState<Long, RowType>): Long? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey ?: anchorPage?.nextKey
}
}
override fun queryResultsChanged() {
invalidate()
}
}

View File

@ -0,0 +1,161 @@
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 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 <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.
*/
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 = 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()
}
}
}

View File

@ -0,0 +1,65 @@
CREATE TABLE categories(
_id INTEGER NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
sort INTEGER NOT NULL,
flags INTEGER NOT NULL
);
-- Insert system category
INSERT OR IGNORE INTO categories(_id, name, sort, flags) VALUES (0, "", -1, 0);
-- Disallow deletion of default category
CREATE TRIGGER IF NOT EXISTS system_category_delete_trigger BEFORE DELETE
ON categories
BEGIN SELECT CASE
WHEN old._id <= 0 THEN
RAISE(ABORT, "System category can't be deleted")
END;
END;
getCategory:
SELECT *
FROM categories
WHERE _id = :id
LIMIT 1;
getCategories:
SELECT
_id AS id,
name,
sort AS `order`,
flags
FROM categories
ORDER BY sort;
getCategoriesByMangaId:
SELECT
C._id AS id,
C.name,
C.sort AS `order`,
C.flags
FROM categories C
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);
delete:
DELETE FROM categories
WHERE _id = :categoryId;
update:
UPDATE categories
SET name = coalesce(:name, name),
sort = coalesce(:order, sort),
flags = coalesce(:flags, flags)
WHERE _id = :categoryId;
updateAllFlags:
UPDATE categories SET
flags = coalesce(?, flags);
selectLastInsertedRowId:
SELECT last_insert_rowid();

View File

@ -0,0 +1,72 @@
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,
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;
getChapterById:
SELECT *
FROM chapters
WHERE _id = :id;
getChaptersByMangaId:
SELECT *
FROM chapters
WHERE manga_id = :mangaId;
getBookmarkedChaptersByMangaId:
SELECT *
FROM chapters
WHERE bookmark
AND manga_id = :mangaId;
getChapterByUrl:
SELECT *
FROM chapters
WHERE url = :chapterUrl;
getChapterByUrlAndMangaId:
SELECT *
FROM chapters
WHERE url = :chapterUrl
AND manga_id = :mangaId;
removeChaptersWithIds:
DELETE FROM chapters
WHERE _id IN :chapterIds;
insert:
INSERT INTO chapters(manga_id,url,name,scanlator,read,bookmark,last_page_read,chapter_number,source_order,date_fetch,date_upload)
VALUES (:mangaId,:url,:name,:scanlator,:read,:bookmark,:lastPageRead,:chapterNumber,:sourceOrder,:dateFetch,:dateUpload);
update:
UPDATE chapters
SET manga_id = coalesce(:mangaId, manga_id),
url = coalesce(:url, url),
name = coalesce(:name, name),
scanlator = coalesce(:scanlator, scanlator),
read = coalesce(:read, read),
bookmark = coalesce(:bookmark, bookmark),
last_page_read = coalesce(:lastPageRead, last_page_read),
chapter_number = coalesce(:chapterNumber, chapter_number),
source_order = coalesce(:sourceOrder, source_order),
date_fetch = coalesce(:dateFetch, date_fetch),
date_upload = coalesce(:dateUpload, date_upload)
WHERE _id = :chapterId;
selectLastInsertedRowId:
SELECT last_insert_rowid();

View File

@ -0,0 +1,73 @@
import java.util.Date;
CREATE TABLE history(
_id INTEGER NOT NULL PRIMARY KEY,
chapter_id INTEGER NOT NULL UNIQUE,
last_read INTEGER AS Date,
time_read INTEGER NOT NULL,
FOREIGN KEY(chapter_id) REFERENCES chapters (_id)
ON DELETE CASCADE
);
CREATE INDEX history_history_chapter_id_index ON history(chapter_id);
getHistoryByMangaId:
SELECT
H._id,
H.chapter_id,
H.last_read,
H.time_read
FROM history H
JOIN chapters C
ON H.chapter_id = C._id
WHERE C.manga_id = :mangaId AND C._id = H.chapter_id;
getHistoryByChapterUrl:
SELECT
H._id,
H.chapter_id,
H.last_read,
H.time_read
FROM history H
JOIN chapters C
ON H.chapter_id = C._id
WHERE C.url = :chapterUrl AND C._id = H.chapter_id;
resetHistoryById:
UPDATE history
SET last_read = 0
WHERE _id = :historyId;
resetHistoryByMangaId:
UPDATE history
SET last_read = 0
WHERE _id IN (
SELECT H._id
FROM mangas M
INNER JOIN chapters C
ON M._id = C.manga_id
INNER JOIN history H
ON C._id = H.chapter_id
WHERE M._id = :mangaId
);
removeAllHistory:
DELETE FROM history;
removeResettedHistory:
DELETE FROM history
WHERE last_read = 0;
upsert:
INSERT INTO history(chapter_id, last_read, time_read)
VALUES (:chapterId, :readAt, :time_read)
ON CONFLICT(chapter_id)
DO UPDATE
SET
last_read = :readAt,
time_read = time_read + :time_read
WHERE chapter_id = :chapterId;
getReadDuration:
SELECT coalesce(sum(time_read), 0)
FROM history;

View File

@ -0,0 +1,57 @@
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
);
delete:
DELETE FROM manga_sync
WHERE manga_id = :mangaId AND sync_id = :syncId;
getTracks:
SELECT *
FROM manga_sync;
getTrackById:
SELECT *
FROM manga_sync
WHERE _id = :id;
getTracksByMangaId:
SELECT *
FROM manga_sync
WHERE manga_id = :mangaId;
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(:mediaId, remote_id),
library_id = coalesce(:libraryId, library_id),
title = coalesce(:title, title),
last_chapter_read = coalesce(:lastChapterRead, last_chapter_read),
total_chapters = coalesce(:totalChapter, total_chapters),
status = coalesce(:status, status),
score = coalesce(:score, score),
remote_url = coalesce(:trackingUrl, remote_url),
start_date = coalesce(:startDate, start_date),
finish_date = coalesce(:finishDate, finish_date)
WHERE _id = :id;

View File

@ -0,0 +1,108 @@
import eu.kanade.tachiyomi.source.model.UpdateStrategy;
import java.lang.String;
import kotlin.collections.List;
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 AS List<String>,
title TEXT NOT NULL,
status INTEGER NOT NULL,
thumbnail_url TEXT,
favorite INTEGER AS Boolean NOT NULL,
last_update INTEGER AS Long,
next_update INTEGER AS Long,
initialized INTEGER AS Boolean NOT NULL,
viewer INTEGER NOT NULL,
chapter_flags INTEGER NOT NULL,
cover_last_modified INTEGER AS Long NOT NULL,
date_added INTEGER AS Long NOT NULL,
update_strategy INTEGER AS UpdateStrategy NOT NULL DEFAULT 0
);
CREATE INDEX library_favorite_index ON mangas(favorite) WHERE favorite = 1;
CREATE INDEX mangas_url_index ON mangas(url);
getMangaById:
SELECT *
FROM mangas
WHERE _id = :id;
-- TODO: this should ideally never really have more than 1 result
getMangaByUrlAndSource:
SELECT *
FROM mangas
WHERE url = :url AND source = :source
LIMIT 1;
getFavorites:
SELECT *
FROM mangas
WHERE favorite = 1;
getSourceIdWithFavoriteCount:
SELECT
source,
count(*)
FROM mangas
WHERE favorite = 1
GROUP BY source;
getFavoriteBySourceId:
SELECT *
FROM mangas
WHERE favorite = 1
AND source = :sourceId;
getDuplicateLibraryManga:
SELECT *
FROM mangas
WHERE favorite = 1
AND LOWER(title) = :title
LIMIT 1;
resetViewerFlags:
UPDATE mangas
SET viewer = 0;
getSourceIdsWithNonLibraryManga:
SELECT source, COUNT(*) AS manga_count
FROM mangas
WHERE favorite = 0
GROUP BY source;
deleteMangasNotInLibraryBySourceIds:
DELETE FROM mangas
WHERE favorite = 0 AND source IN :sourceIds;
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)
VALUES (:source,:url,:artist,:author,:description,:genre,:title,:status,:thumbnailUrl,:favorite,:lastUpdate,:nextUpdate,:initialized,:viewerFlags,:chapterFlags,:coverLastModified,:dateAdded,:updateStrategy);
update:
UPDATE mangas SET
source = coalesce(:source, source),
url = coalesce(:url, url),
artist = coalesce(:artist, artist),
author = coalesce(:author, author),
description = coalesce(:description, description),
genre = coalesce(:genre, genre),
title = coalesce(:title, title),
status = coalesce(:status, status),
thumbnail_url = coalesce(:thumbnailUrl, thumbnail_url),
favorite = coalesce(:favorite, favorite),
last_update = coalesce(:lastUpdate, last_update),
initialized = coalesce(:initialized, initialized),
viewer = coalesce(:viewer, viewer),
chapter_flags = coalesce(:chapterFlags, chapter_flags),
cover_last_modified = coalesce(:coverLastModified, cover_last_modified),
date_added = coalesce(:dateAdded, date_added),
update_strategy = coalesce(:updateStrategy, update_strategy)
WHERE _id = :mangaId;
selectLastInsertedRowId:
SELECT last_insert_rowid();

View File

@ -0,0 +1,17 @@
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
);
insert:
INSERT INTO mangas_categories(manga_id, category_id)
VALUES (:mangaId, :categoryId);
deleteMangaCategoryByMangaId:
DELETE FROM mangas_categories
WHERE manga_id = :mangaId;

View File

@ -0,0 +1,24 @@
CREATE TABLE sources(
_id INTEGER NOT NULL PRIMARY KEY,
lang TEXT NOT NULL,
name TEXT NOT NULL
);
findAll:
SELECT *
FROM sources;
findOne:
SELECT *
FROM sources
WHERE _id = :id;
upsert:
INSERT INTO sources(_id, lang, name)
VALUES (:id, :lang, :name)
ON CONFLICT(_id)
DO UPDATE
SET
lang = :lang,
name = :name
WHERE _id = :id;

View File

@ -0,0 +1,6 @@
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;

View File

@ -0,0 +1,11 @@
ALTER TABLE mangas
ADD COLUMN date_added INTEGER NOT NULL DEFAULT 0;
UPDATE mangas
SET date_added = (
SELECT MIN(date_fetch)
FROM mangas M
INNER JOIN chapters C
ON M._id = C.manga_id
GROUP BY M._id
);

View File

@ -0,0 +1,2 @@
ALTER TABLE mangas
ADD COLUMN next_update INTEGER DEFAULT 0;

View File

@ -0,0 +1,27 @@
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;

View File

@ -0,0 +1,3 @@
UPDATE chapters
SET date_upload = date_fetch
WHERE date_upload = 0;

View File

@ -0,0 +1,149 @@
DROP INDEX IF EXISTS chapters_manga_id_index;
DROP INDEX IF EXISTS chapters_unread_by_manga_index;
DROP INDEX IF EXISTS history_history_chapter_id_index;
DROP INDEX IF EXISTS library_favorite_index;
DROP INDEX IF EXISTS mangas_url_index;
ALTER TABLE mangas RENAME TO manga_temp;
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,
next_update INTEGER AS Long,
initialized INTEGER AS Boolean NOT NULL,
viewer INTEGER NOT NULL,
chapter_flags INTEGER NOT NULL,
cover_last_modified INTEGER AS Long NOT NULL,
date_added INTEGER AS Long NOT NULL
);
INSERT INTO mangas
SELECT _id,source,url,artist,author,description,genre,title,status,thumbnail_url,favorite,last_update,next_update,initialized,viewer,chapter_flags,cover_last_modified,date_added
FROM manga_temp;
ALTER TABLE categories RENAME TO categories_temp;
CREATE TABLE categories(
_id INTEGER NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
sort INTEGER NOT NULL,
flags INTEGER NOT NULL
);
INSERT INTO categories
SELECT _id,name,sort,flags
FROM categories_temp;
ALTER TABLE chapters RENAME TO chapters_temp;
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,
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
);
INSERT INTO chapters
SELECT _id,manga_id,url,name,scanlator,read,bookmark,last_page_read,chapter_number,source_order,date_fetch,date_upload
FROM chapters_temp;
ALTER TABLE history RENAME TO history_temp;
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
);
INSERT INTO history
SELECT history_id, history_chapter_id, history_last_read, history_time_read
FROM history_temp;
ALTER TABLE mangas_categories RENAME TO mangas_categories_temp;
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
);
INSERT INTO mangas_categories
SELECT _id, manga_id, category_id
FROM mangas_categories_temp;
ALTER TABLE manga_sync RENAME TO manga_sync_temp;
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
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_temp;
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;
CREATE INDEX history_history_chapter_id_index ON history(history_chapter_id);
CREATE INDEX library_favorite_index ON mangas(favorite) WHERE favorite = 1;
CREATE INDEX mangas_url_index ON mangas(url);
CREATE VIEW IF NOT EXISTS historyView AS
SELECT
history.history_id AS id,
mangas._id AS mangaId,
chapters._id AS chapterId,
mangas.title,
mangas.thumbnail_url AS thumnailUrl,
chapters.chapter_number AS chapterNumber,
history.history_last_read AS readAt,
max_last_read.history_last_read AS maxReadAt,
max_last_read.history_chapter_id AS maxReadAtChapterId
FROM mangas
JOIN chapters
ON mangas._id = chapters.manga_id
JOIN history
ON chapters._id = history.history_chapter_id
JOIN (
SELECT chapters.manga_id,chapters._id AS history_chapter_id, MAX(history.history_last_read) AS history_last_read
FROM chapters JOIN history
ON chapters._id = history.history_chapter_id
GROUP BY chapters.manga_id
) AS max_last_read
ON chapters.manga_id = max_last_read.manga_id;
DROP TABLE IF EXISTS manga_sync_temp;
DROP TABLE IF EXISTS mangas_categories_temp;
DROP TABLE IF EXISTS history_temp;
DROP TABLE IF EXISTS chapters_temp;
DROP TABLE IF EXISTS categories_temp;
DROP TABLE IF EXISTS manga_temp;

View File

@ -0,0 +1,52 @@
import java.util.Date;
DROP INDEX IF EXISTS history_history_chapter_id_index;
DROP VIEW IF EXISTS historyView;
/**
* [last_read] was made not-null
* [time_read] was kept as long and made non-null
* `history` prefix was removed from table name
*/
ALTER TABLE history RENAME TO history_temp;
CREATE TABLE history(
_id INTEGER NOT NULL PRIMARY KEY,
chapter_id INTEGER NOT NULL UNIQUE,
last_read INTEGER AS Date NOT NULL,
time_read INTEGER NOT NULL,
FOREIGN KEY(chapter_id) REFERENCES chapters (_id)
ON DELETE CASCADE
);
INSERT INTO history
SELECT history_id, history_chapter_id, coalesce(history_last_read, 0), coalesce(history_time_read, 0)
FROM history_temp;
/**
* [history.time_read] was added as a column in [historyView]
*/
CREATE VIEW historyView AS
SELECT
history._id AS id,
mangas._id AS mangaId,
chapters._id AS chapterId,
mangas.title,
mangas.thumbnail_url AS thumbnailUrl,
chapters.chapter_number AS chapterNumber,
history.last_read AS readAt,
history.time_read AS readDuration,
max_last_read.last_read AS maxReadAt,
max_last_read.chapter_id AS maxReadAtChapterId
FROM mangas
JOIN chapters
ON mangas._id = chapters.manga_id
JOIN history
ON chapters._id = history.chapter_id
JOIN (
SELECT chapters.manga_id,chapters._id AS chapter_id, MAX(history.last_read) AS last_read
FROM chapters JOIN history
ON chapters._id = history.chapter_id
GROUP BY chapters.manga_id
) AS max_last_read
ON chapters.manga_id = max_last_read.manga_id;
CREATE INDEX history_history_chapter_id_index ON history(chapter_id);

View File

@ -0,0 +1,5 @@
CREATE TABLE sources(
_id INTEGER NOT NULL PRIMARY KEY,
lang TEXT NOT NULL,
name TEXT NOT NULL
);

View File

@ -0,0 +1,29 @@
DROP VIEW IF EXISTS historyView;
CREATE VIEW historyView AS
SELECT
history._id AS id,
mangas._id AS mangaId,
chapters._id AS chapterId,
mangas.title,
mangas.thumbnail_url AS thumbnailUrl,
mangas.source,
mangas.favorite,
mangas.cover_last_modified,
chapters.chapter_number AS chapterNumber,
history.last_read AS readAt,
history.time_read AS readDuration,
max_last_read.last_read AS maxReadAt,
max_last_read.chapter_id AS maxReadAtChapterId
FROM mangas
JOIN chapters
ON mangas._id = chapters.manga_id
JOIN history
ON chapters._id = history.chapter_id
JOIN (
SELECT chapters.manga_id,chapters._id AS chapter_id, MAX(history.last_read) AS last_read
FROM chapters JOIN history
ON chapters._id = history.chapter_id
GROUP BY chapters.manga_id
) AS max_last_read
ON chapters.manga_id = max_last_read.manga_id;

View File

@ -0,0 +1,20 @@
CREATE VIEW updatesView AS
SELECT
mangas._id AS mangaId,
mangas.title AS mangaTitle,
chapters._id AS chapterId,
chapters.name AS chapterName,
chapters.scanlator,
chapters.read,
chapters.bookmark,
mangas.source,
mangas.favorite,
mangas.thumbnail_url AS thumbnailUrl,
mangas.cover_last_modified AS coverLastModified,
chapters.date_upload AS dateUpload,
chapters.date_fetch AS datefetch
FROM mangas JOIN chapters
ON mangas._id = chapters.manga_id
WHERE favorite = 1
AND date_fetch > date_added
ORDER BY date_fetch DESC;

View File

@ -0,0 +1,10 @@
-- Insert Default category
INSERT OR IGNORE INTO categories(_id, name, sort, flags) VALUES (0, "", -1, 0);
-- Disallow deletion of default category
CREATE TRIGGER IF NOT EXISTS system_category_delete_trigger BEFORE DELETE
ON categories
BEGIN SELECT CASE
WHEN old._id <= 0 THEN
RAISE(ABORT, "System category can't be deleted")
END;
END;

View File

@ -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);

View File

@ -0,0 +1 @@
ALTER TABLE mangas ADD COLUMN update_strategy INTEGER NOT NULL DEFAULT 0;

View File

@ -0,0 +1,27 @@
CREATE VIEW libraryView AS
SELECT
M.*,
coalesce(C.total - C.readCount, 0) AS unreadCount,
coalesce(C.readCount, 0) AS readCount,
coalesce(C.latestUpload, 0) AS latestUpload,
coalesce(C.fetchedAt, 0) AS chapterFetchedAt,
coalesce(C.lastRead, 0) AS lastRead,
COALESCE(MC.category_id, 0) AS category
FROM mangas M
LEFT JOIN mangas_categories AS MC
ON MC.manga_id = M._id
LEFT JOIN(
SELECT
chapters.manga_id,
count(*) AS total,
sum(read) AS readCount,
max(chapters.date_upload) AS latestUpload,
max(history.last_read) AS lastRead,
max(chapters.date_fetch) AS fetchedAt
FROM chapters
LEFT JOIN history
ON chapters._id = history.chapter_id
GROUP BY chapters.manga_id
) AS C
ON M._id = C.manga_id
WHERE M.favorite = 1;

View File

@ -0,0 +1,31 @@
DROP VIEW libraryView;
CREATE VIEW libraryView AS
SELECT
M.*,
coalesce(C.total, 0) AS totalCount,
coalesce(C.readCount, 0) AS readCount,
coalesce(C.latestUpload, 0) AS latestUpload,
coalesce(C.fetchedAt, 0) AS chapterFetchedAt,
coalesce(C.lastRead, 0) AS lastRead,
coalesce(C.bookmarkCount, 0) AS bookmarkCount,
coalesce(MC.category_id, 0) AS category
FROM mangas M
LEFT JOIN(
SELECT
chapters.manga_id,
count(*) AS total,
sum(read) AS readCount,
coalesce(max(chapters.date_upload), 0) AS latestUpload,
coalesce(max(history.last_read), 0) AS lastRead,
coalesce(max(chapters.date_fetch), 0) AS fetchedAt,
sum(chapters.bookmark) AS bookmarkCount
FROM chapters
LEFT JOIN history
ON chapters._id = history.chapter_id
GROUP BY chapters.manga_id
) AS C
ON M._id = C.manga_id
LEFT JOIN mangas_categories AS MC
ON MC.manga_id = M._id
WHERE M.favorite = 1;

View File

@ -0,0 +1,23 @@
DROP VIEW IF EXISTS updatesView;
CREATE VIEW updatesView AS
SELECT
mangas._id AS mangaId,
mangas.title AS mangaTitle,
chapters._id AS chapterId,
chapters.name AS chapterName,
chapters.scanlator,
chapters.read,
chapters.bookmark,
chapters.last_page_read,
mangas.source,
mangas.favorite,
mangas.thumbnail_url AS thumbnailUrl,
mangas.cover_last_modified AS coverLastModified,
chapters.date_upload AS dateUpload,
chapters.date_fetch AS datefetch
FROM mangas JOIN chapters
ON mangas._id = chapters.manga_id
WHERE favorite = 1
AND date_fetch > date_added
ORDER BY date_fetch DESC;

View File

@ -0,0 +1,2 @@
ALTER TABLE chapters
ADD COLUMN bookmark INTEGER DEFAULT 0;

View File

@ -0,0 +1,2 @@
ALTER TABLE chapters
ADD COLUMN scanlator TEXT DEFAULT NULL;

View File

@ -0,0 +1,2 @@
ALTER TABLE manga_sync
ADD COLUMN remote_url TEXT DEFAULT '';

View File

@ -0,0 +1,2 @@
ALTER TABLE manga_sync
ADD COLUMN library_id INTEGER;

View File

@ -0,0 +1,9 @@
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;

View File

@ -0,0 +1,5 @@
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;

View File

@ -0,0 +1,2 @@
ALTER TABLE mangas
ADD COLUMN cover_last_modified INTEGER NOT NULL DEFAULT 0;

View File

@ -0,0 +1,64 @@
CREATE VIEW historyView AS
SELECT
history._id AS id,
mangas._id AS mangaId,
chapters._id AS chapterId,
mangas.title,
mangas.thumbnail_url AS thumbnailUrl,
mangas.source,
mangas.favorite,
mangas.cover_last_modified,
chapters.chapter_number AS chapterNumber,
history.last_read AS readAt,
history.time_read AS readDuration,
max_last_read.last_read AS maxReadAt,
max_last_read.chapter_id AS maxReadAtChapterId
FROM mangas
JOIN chapters
ON mangas._id = chapters.manga_id
JOIN history
ON chapters._id = history.chapter_id
JOIN (
SELECT chapters.manga_id,chapters._id AS chapter_id, MAX(history.last_read) AS last_read
FROM chapters JOIN history
ON chapters._id = history.chapter_id
GROUP BY chapters.manga_id
) AS max_last_read
ON chapters.manga_id = max_last_read.manga_id;
history:
SELECT
id,
mangaId,
chapterId,
title,
thumbnailUrl,
source,
favorite,
cover_last_modified,
chapterNumber,
readAt,
readDuration
FROM historyView
WHERE historyView.readAt > 0
AND maxReadAtChapterId = historyView.chapterId
AND lower(historyView.title) LIKE ('%' || :query || '%')
ORDER BY readAt DESC;
getLatestHistory:
SELECT
id,
mangaId,
chapterId,
title,
thumbnailUrl,
source,
favorite,
cover_last_modified,
chapterNumber,
readAt,
readDuration
FROM historyView
WHERE historyView.readAt > 0
ORDER BY readAt DESC
LIMIT 1;

View File

@ -0,0 +1,33 @@
CREATE VIEW libraryView AS
SELECT
M.*,
coalesce(C.total, 0) AS totalCount,
coalesce(C.readCount, 0) AS readCount,
coalesce(C.latestUpload, 0) AS latestUpload,
coalesce(C.fetchedAt, 0) AS chapterFetchedAt,
coalesce(C.lastRead, 0) AS lastRead,
coalesce(C.bookmarkCount, 0) AS bookmarkCount,
coalesce(MC.category_id, 0) AS category
FROM mangas M
LEFT JOIN(
SELECT
chapters.manga_id,
count(*) AS total,
sum(read) AS readCount,
coalesce(max(chapters.date_upload), 0) AS latestUpload,
coalesce(max(history.last_read), 0) AS lastRead,
coalesce(max(chapters.date_fetch), 0) AS fetchedAt,
sum(chapters.bookmark) AS bookmarkCount
FROM chapters
LEFT JOIN history
ON chapters._id = history.chapter_id
GROUP BY chapters.manga_id
) AS C
ON M._id = C.manga_id
LEFT JOIN mangas_categories AS MC
ON MC.manga_id = M._id
WHERE M.favorite = 1;
library:
SELECT *
FROM libraryView;

View File

@ -0,0 +1,26 @@
CREATE VIEW updatesView AS
SELECT
mangas._id AS mangaId,
mangas.title AS mangaTitle,
chapters._id AS chapterId,
chapters.name AS chapterName,
chapters.scanlator,
chapters.read,
chapters.bookmark,
chapters.last_page_read,
mangas.source,
mangas.favorite,
mangas.thumbnail_url AS thumbnailUrl,
mangas.cover_last_modified AS coverLastModified,
chapters.date_upload AS dateUpload,
chapters.date_fetch AS datefetch
FROM mangas JOIN chapters
ON mangas._id = chapters.manga_id
WHERE favorite = 1
AND date_fetch > date_added
ORDER BY date_fetch DESC;
updates:
SELECT *
FROM updatesView
WHERE dateUpload > :after;