mirror of
https://github.com/null2264/yokai.git
synced 2025-06-21 10:44:42 +00:00
refactor: Move database related stuff to data module
This commit is contained in:
parent
66354205f1
commit
fc171c1e0a
44 changed files with 36 additions and 49 deletions
|
@ -0,0 +1,92 @@
|
|||
package 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
|
||||
|
||||
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>,
|
||||
transacter: Database.() -> Transacter,
|
||||
queryProvider: Database.(Long, Long) -> Query<T>
|
||||
): PagingSource<Long, T> {
|
||||
return QueryPagingSource(
|
||||
countQuery = countQuery(db),
|
||||
transacter = transacter(db),
|
||||
dispatcher = queryDispatcher,
|
||||
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) }
|
||||
}
|
||||
}
|
161
data/src/androidMain/kotlin/yokai/data/TransactionContext.kt
Normal file
161
data/src/androidMain/kotlin/yokai/data/TransactionContext.kt
Normal file
|
@ -0,0 +1,161 @@
|
|||
package 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.*
|
||||
import java.util.concurrent.atomic.*
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
package eu.kanade.tachiyomi.data.database
|
||||
|
||||
import app.cash.sqldelight.ColumnAdapter
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
import java.util.*
|
||||
|
||||
// TODO: Move to yokai.data.DatabaseAdapter
|
||||
|
||||
|
@ -13,14 +15,18 @@ val updateStrategyAdapter = object : ColumnAdapter<UpdateStrategy, Int> {
|
|||
override fun encode(value: UpdateStrategy): Int = value.ordinal
|
||||
}
|
||||
|
||||
interface ColumnAdapter<T : Any, S> {
|
||||
/**
|
||||
* @return [databaseValue] decoded as type [T].
|
||||
*/
|
||||
fun decode(databaseValue: S): T
|
||||
|
||||
/**
|
||||
* @return [value] encoded as database type [S].
|
||||
*/
|
||||
fun encode(value: T): S
|
||||
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()) {
|
||||
listOf()
|
||||
} else {
|
||||
databaseValue.split(listOfStringsSeparator)
|
||||
}
|
||||
override fun encode(value: List<String>) = value.joinToString(separator = listOfStringsSeparator)
|
||||
}
|
||||
|
|
37
data/src/commonMain/kotlin/yokai/data/DatabaseHandler.kt
Normal file
37
data/src/commonMain/kotlin/yokai/data/DatabaseHandler.kt
Normal file
|
@ -0,0 +1,37 @@
|
|||
package yokai.data
|
||||
|
||||
import app.cash.sqldelight.Query
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface DatabaseHandler {
|
||||
suspend fun <T> await(inTransaction: Boolean = false, block: suspend Database.() -> T): T
|
||||
|
||||
suspend fun <T : Any> awaitList(
|
||||
inTransaction: Boolean = false,
|
||||
block: suspend Database.() -> Query<T>
|
||||
): List<T>
|
||||
|
||||
suspend fun <T : Any> awaitOne(
|
||||
inTransaction: Boolean = false,
|
||||
block: suspend Database.() -> Query<T>
|
||||
): T
|
||||
|
||||
suspend fun <T : Any> 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>,
|
||||
transacter: Database.() -> Transacter,
|
||||
queryProvider: Database.(Long, Long) -> Query<T>
|
||||
): PagingSource<Long, T>
|
||||
*/
|
||||
}
|
|
@ -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
|
||||
);
|
38
data/src/commonMain/sqldelight/tachiyomi/data/chapters.sq
Normal file
38
data/src/commonMain/sqldelight/tachiyomi/data/chapters.sq
Normal file
|
@ -0,0 +1,38 @@
|
|||
import kotlin.Boolean;
|
||||
|
||||
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 NOT NULL,
|
||||
source_order INTEGER NOT NULL,
|
||||
date_fetch INTEGER NOT NULL,
|
||||
date_upload INTEGER 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;
|
||||
|
||||
getChaptersByMangaId:
|
||||
SELECT C.*
|
||||
FROM chapters AS C
|
||||
LEFT JOIN scanlators_view AS S
|
||||
ON C.manga_id = S.manga_id
|
||||
AND ifnull(C.scanlator, 'N/A') = ifnull(S.name, '/<INVALID>/') -- I assume if it's N/A it shouldn't be filtered
|
||||
WHERE C.manga_id = :manga_id
|
||||
AND (
|
||||
:apply_filter = 0 OR S.name IS NULL
|
||||
);
|
||||
|
||||
getScanlatorsByMangaId:
|
||||
SELECT scanlator
|
||||
FROM chapters
|
||||
WHERE manga_id = :mangaId;
|
|
@ -0,0 +1,39 @@
|
|||
CREATE TABLE custom_manga_info (
|
||||
manga_id INTEGER NOT NULL PRIMARY KEY,
|
||||
title TEXT,
|
||||
author TEXT,
|
||||
artist TEXT,
|
||||
description TEXT,
|
||||
genre TEXT,
|
||||
status INTEGER,
|
||||
UNIQUE (manga_id) ON CONFLICT REPLACE,
|
||||
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
findAll:
|
||||
SELECT *
|
||||
FROM custom_manga_info;
|
||||
|
||||
insert:
|
||||
INSERT INTO custom_manga_info(manga_id, title, author, artist, description, genre, status)
|
||||
VALUES (:manga_id, :title, :author, :artist, :description, :genre, :status)
|
||||
ON CONFLICT (manga_id)
|
||||
DO UPDATE
|
||||
SET
|
||||
title = :title,
|
||||
author = :author,
|
||||
artist = :artist,
|
||||
description = :description,
|
||||
genre = :genre,
|
||||
status = :status
|
||||
WHERE manga_id = :manga_id;
|
||||
|
||||
delete:
|
||||
DELETE FROM custom_manga_info
|
||||
WHERE manga_id = :manga_id;
|
||||
|
||||
relink:
|
||||
UPDATE custom_manga_info
|
||||
SET manga_id = :new_id
|
||||
WHERE manga_id = :old_id;
|
|
@ -0,0 +1,57 @@
|
|||
CREATE TABLE extension_repos (
|
||||
base_url TEXT NOT NULL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
short_name TEXT,
|
||||
website TEXT NOT NULL,
|
||||
signing_key_fingerprint TEXT UNIQUE NOT NULL
|
||||
);
|
||||
|
||||
findOne:
|
||||
SELECT *
|
||||
FROM extension_repos
|
||||
WHERE base_url = :base_url;
|
||||
|
||||
findOneBySigningKeyFingerprint:
|
||||
SELECT *
|
||||
FROM extension_repos
|
||||
WHERE signing_key_fingerprint = :fingerprint;
|
||||
|
||||
findAll:
|
||||
SELECT *
|
||||
FROM extension_repos;
|
||||
|
||||
count:
|
||||
SELECT COUNT(*)
|
||||
FROM extension_repos;
|
||||
|
||||
insert:
|
||||
INSERT INTO extension_repos(base_url, name, short_name, website, signing_key_fingerprint)
|
||||
VALUES (:base_url, :name, :short_name, :website, :fingerprint);
|
||||
|
||||
upsert:
|
||||
INSERT INTO extension_repos(base_url, name, short_name, website, signing_key_fingerprint)
|
||||
VALUES (:base_url, :name, :short_name, :website, :fingerprint)
|
||||
ON CONFLICT(base_url)
|
||||
DO UPDATE
|
||||
SET
|
||||
name = :name,
|
||||
short_name = :short_name,
|
||||
website =: website,
|
||||
signing_key_fingerprint = :fingerprint
|
||||
WHERE base_url = base_url;
|
||||
|
||||
replace:
|
||||
INSERT INTO extension_repos(base_url, name, short_name, website, signing_key_fingerprint)
|
||||
VALUES (:base_url, :name, :short_name, :website, :fingerprint)
|
||||
ON CONFLICT(signing_key_fingerprint)
|
||||
DO UPDATE
|
||||
SET
|
||||
base_url = :base_url,
|
||||
name = :name,
|
||||
short_name = :short_name,
|
||||
website =: website
|
||||
WHERE signing_key_fingerprint = signing_key_fingerprint;
|
||||
|
||||
delete:
|
||||
DELETE FROM extension_repos
|
||||
WHERE base_url = :base_url;
|
12
data/src/commonMain/sqldelight/tachiyomi/data/history.sq
Normal file
12
data/src/commonMain/sqldelight/tachiyomi/data/history.sq
Normal file
|
@ -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);
|
|
@ -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
|
||||
);
|
21
data/src/commonMain/sqldelight/tachiyomi/data/manga_sync.sq
Normal file
21
data/src/commonMain/sqldelight/tachiyomi/data/manga_sync.sq
Normal file
|
@ -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
|
||||
);
|
31
data/src/commonMain/sqldelight/tachiyomi/data/mangas.sq
Normal file
31
data/src/commonMain/sqldelight/tachiyomi/data/mangas.sq
Normal file
|
@ -0,0 +1,31 @@
|
|||
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,
|
||||
hide_title 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;
|
||||
|
||||
findAll:
|
||||
SELECT *
|
||||
FROM mangas;
|
|
@ -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;
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE chapters ADD COLUMN pages_left INTEGER DEFAULT 0;
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE mangas ADD COLUMN date_added INTEGER NOT NULL DEFAULT 0;
|
|
@ -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;
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE mangas ADD COLUMN filtered_scanlators TEXT;
|
33
data/src/commonMain/sqldelight/tachiyomi/migrations/14.sqm
Normal file
33
data/src/commonMain/sqldelight/tachiyomi/migrations/14.sqm
Normal file
|
@ -0,0 +1,33 @@
|
|||
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;
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE mangas ADD COLUMN update_strategy INTEGER NOT NULL DEFAULT 0;
|
|
@ -0,0 +1,3 @@
|
|||
UPDATE manga_sync
|
||||
SET score = max(score, 0)
|
||||
WHERE sync_id = 7;
|
48
data/src/commonMain/sqldelight/tachiyomi/migrations/17.sqm
Normal file
48
data/src/commonMain/sqldelight/tachiyomi/migrations/17.sqm
Normal file
|
@ -0,0 +1,48 @@
|
|||
import kotlin.Boolean;
|
||||
import kotlin.Long;
|
||||
|
||||
CREATE TABLE extension_repos (
|
||||
base_url TEXT NOT NULL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
short_name TEXT,
|
||||
website TEXT NOT NULL,
|
||||
signing_key_fingerprint TEXT UNIQUE NOT NULL
|
||||
);
|
||||
|
||||
--- >> Rename hideTitle to hide_title
|
||||
DROP INDEX IF EXISTS mangas_url_index;
|
||||
DROP INDEX IF EXISTS library_favorite_index;
|
||||
|
||||
ALTER TABLE mangas RENAME TO mangas_tmp;
|
||||
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,
|
||||
hide_title INTEGER NOT NULL,
|
||||
chapter_flags INTEGER NOT NULL,
|
||||
date_added INTEGER AS Long,
|
||||
filtered_scanlators TEXT,
|
||||
update_strategy INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
INSERT INTO mangas
|
||||
(_id, source, url, artist, author, description, genre, title, status, thumbnail_url, favorite, last_update,
|
||||
initialized, viewer, hide_title, chapter_flags, date_added, filtered_scanlators, update_strategy)
|
||||
SELECT
|
||||
_id, source, url, artist, author, description, genre, title, status, thumbnail_url, favorite, last_update,
|
||||
initialized, viewer, hideTitle, chapter_flags, date_added, filtered_scanlators, update_strategy
|
||||
FROM mangas_tmp;
|
||||
|
||||
CREATE INDEX mangas_url_index ON mangas(url);
|
||||
CREATE INDEX library_favorite_index ON mangas(favorite) WHERE favorite = 1;
|
||||
--- << Rename hideTitle to hide_title
|
104
data/src/commonMain/sqldelight/tachiyomi/migrations/18.sqm
Normal file
104
data/src/commonMain/sqldelight/tachiyomi/migrations/18.sqm
Normal file
|
@ -0,0 +1,104 @@
|
|||
import kotlin.Boolean;
|
||||
import kotlin.Float;
|
||||
import kotlin.Long;
|
||||
|
||||
--- >> Fix migration 17 mistake
|
||||
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;
|
||||
|
||||
ALTER TABLE chapters RENAME TO chapters_tmp;
|
||||
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
|
||||
);
|
||||
INSERT INTO chapters
|
||||
(_id, manga_id, url, name, scanlator, read, bookmark, last_page_read, pages_left, chapter_number, source_order,
|
||||
date_fetch, date_upload)
|
||||
SELECT
|
||||
_id, manga_id, url, name, scanlator, read, bookmark, last_page_read, pages_left, chapter_number, source_order,
|
||||
date_fetch, date_upload
|
||||
FROM chapters_tmp;
|
||||
|
||||
ALTER TABLE history RENAME TO history_tmp;
|
||||
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
|
||||
(history_id, history_chapter_id, history_last_read, history_time_read)
|
||||
SELECT
|
||||
history_id, history_chapter_id, history_last_read, history_time_read
|
||||
FROM history_tmp;
|
||||
|
||||
ALTER TABLE mangas_categories RENAME TO mangas_categories_tmp;
|
||||
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
|
||||
(_id, manga_id, category_id)
|
||||
SELECT
|
||||
_id, manga_id, category_id
|
||||
FROM mangas_categories_tmp;
|
||||
|
||||
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;
|
||||
|
||||
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);
|
||||
|
||||
DROP TABLE IF EXISTS chapters_tmp;
|
||||
DROP TABLE IF EXISTS history_tmp;
|
||||
DROP TABLE IF EXISTS mangas_categories_tmp;
|
||||
DROP TABLE IF EXISTS manga_sync_tmp;
|
||||
DROP TABLE IF EXISTS mangas_tmp;
|
||||
--- << Fix migration 17 mistake
|
12
data/src/commonMain/sqldelight/tachiyomi/migrations/19.sqm
Normal file
12
data/src/commonMain/sqldelight/tachiyomi/migrations/19.sqm
Normal file
|
@ -0,0 +1,12 @@
|
|||
CREATE TABLE custom_manga_info (
|
||||
manga_id INTEGER NOT NULL PRIMARY KEY,
|
||||
title TEXT,
|
||||
author TEXT,
|
||||
artist TEXT,
|
||||
description TEXT,
|
||||
genre TEXT,
|
||||
status INTEGER,
|
||||
UNIQUE (manga_id) ON CONFLICT REPLACE,
|
||||
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
|
||||
ON DELETE CASCADE
|
||||
);
|
10
data/src/commonMain/sqldelight/tachiyomi/migrations/2.sqm
Normal file
10
data/src/commonMain/sqldelight/tachiyomi/migrations/2.sqm
Normal 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);
|
37
data/src/commonMain/sqldelight/tachiyomi/migrations/20.sqm
Normal file
37
data/src/commonMain/sqldelight/tachiyomi/migrations/20.sqm
Normal file
|
@ -0,0 +1,37 @@
|
|||
CREATE VIEW library_view AS
|
||||
SELECT
|
||||
M.*,
|
||||
coalesce(C.total, 0) AS total,
|
||||
coalesce(C.read_count, 0) AS has_read,
|
||||
coalesce(C.bookmark_count, 0) AS bookmark_count,
|
||||
coalesce(MC.category_id, 0) AS category
|
||||
FROM mangas AS M
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
chapters.manga_id,
|
||||
count(*) AS total,
|
||||
sum(read) AS read_count,
|
||||
sum(bookmark) AS bookmark_count
|
||||
FROM chapters
|
||||
LEFT JOIN (
|
||||
WITH RECURSIVE split(seq, _id, name, str) AS (
|
||||
SELECT 0, mangas._id, NULL, replace(ifnull(mangas.filtered_scanlators, ''), ' & ', '[.]')||'[.]' FROM mangas
|
||||
UNION ALL SELECT
|
||||
seq+1,
|
||||
_id,
|
||||
substr(str, 0, instr(str, '[.]')),
|
||||
substr(str, instr(str, '[.]')+3)
|
||||
FROM split WHERE str != ''
|
||||
) SELECT _id, name FROM split WHERE split.seq != 0 ORDER BY split.seq ASC
|
||||
) AS filtered_scanlators
|
||||
ON chapters.manga_id = filtered_scanlators._id
|
||||
AND ifnull(chapters.scanlator, 'N/A') = ifnull(filtered_scanlators.name, '/<INVALID>/')
|
||||
WHERE filtered_scanlators.name IS NULL
|
||||
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
|
||||
GROUP BY M._id
|
||||
ORDER BY M.title;
|
37
data/src/commonMain/sqldelight/tachiyomi/migrations/21.sqm
Normal file
37
data/src/commonMain/sqldelight/tachiyomi/migrations/21.sqm
Normal file
|
@ -0,0 +1,37 @@
|
|||
DROP VIEW IF EXISTS library_view;
|
||||
CREATE VIEW library_view AS
|
||||
SELECT
|
||||
M.*,
|
||||
coalesce(C.total, 0) AS total,
|
||||
coalesce(C.read_count, 0) AS has_read,
|
||||
coalesce(C.bookmark_count, 0) AS bookmark_count,
|
||||
coalesce(MC.category_id, 0) AS category
|
||||
FROM mangas AS M
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
chapters.manga_id,
|
||||
count(*) AS total,
|
||||
sum(read) AS read_count,
|
||||
sum(bookmark) AS bookmark_count
|
||||
FROM chapters
|
||||
LEFT JOIN (
|
||||
WITH RECURSIVE split(seq, _id, name, str) AS ( -- Probably should migrate this to its own table someday
|
||||
SELECT 0, mangas._id, NULL, replace(ifnull(mangas.filtered_scanlators, ''), ' & ', '[.]')||'[.]' FROM mangas
|
||||
UNION ALL SELECT
|
||||
seq+1,
|
||||
_id,
|
||||
substr(str, 0, instr(str, '[.]')),
|
||||
substr(str, instr(str, '[.]')+3)
|
||||
FROM split WHERE str != ''
|
||||
) SELECT _id, name FROM split WHERE split.seq != 0 ORDER BY split.seq ASC
|
||||
) AS filtered_scanlators
|
||||
ON chapters.manga_id = filtered_scanlators._id
|
||||
AND ifnull(chapters.scanlator, 'N/A') = ifnull(filtered_scanlators.name, '/<INVALID>/') -- I assume if it's N/A it shouldn't be filtered
|
||||
WHERE filtered_scanlators.name IS NULL
|
||||
GROUP BY chapters.manga_id
|
||||
) AS C
|
||||
ON M._id = C.manga_id
|
||||
LEFT JOIN (SELECT * FROM mangas_categories) AS MC
|
||||
ON MC.manga_id = M._id
|
||||
WHERE M.favorite = 1
|
||||
ORDER BY M.title;
|
41
data/src/commonMain/sqldelight/tachiyomi/migrations/22.sqm
Normal file
41
data/src/commonMain/sqldelight/tachiyomi/migrations/22.sqm
Normal file
|
@ -0,0 +1,41 @@
|
|||
CREATE VIEW scanlators_view AS
|
||||
SELECT S.* FROM (
|
||||
WITH RECURSIVE split(seq, _id, name, str) AS ( -- Probably should migrate this to its own table someday
|
||||
SELECT 0, mangas._id, NULL, replace(ifnull(mangas.filtered_scanlators, ''), ' & ', '[.]')||'[.]' FROM mangas WHERE mangas._id
|
||||
UNION ALL SELECT
|
||||
seq+1,
|
||||
_id,
|
||||
substr(str, 0, instr(str, '[.]')),
|
||||
substr(str, instr(str, '[.]')+3)
|
||||
FROM split WHERE str != ''
|
||||
)
|
||||
SELECT _id AS manga_id, name FROM split WHERE split.seq != 0 ORDER BY split.seq ASC
|
||||
) AS S;
|
||||
|
||||
DROP VIEW IF EXISTS library_view;
|
||||
CREATE VIEW library_view AS
|
||||
SELECT
|
||||
M.*,
|
||||
coalesce(C.total, 0) AS total,
|
||||
coalesce(C.read_count, 0) AS has_read,
|
||||
coalesce(C.bookmark_count, 0) AS bookmark_count,
|
||||
coalesce(MC.category_id, 0) AS category
|
||||
FROM mangas AS M
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
chapters.manga_id,
|
||||
count(*) AS total,
|
||||
sum(read) AS read_count,
|
||||
sum(bookmark) AS bookmark_count
|
||||
FROM chapters
|
||||
LEFT JOIN scanlators_view AS filtered_scanlators
|
||||
ON chapters.manga_id = filtered_scanlators.manga_id
|
||||
AND ifnull(chapters.scanlator, 'N/A') = ifnull(filtered_scanlators.name, '/<INVALID>/')
|
||||
WHERE filtered_scanlators.name IS NULL
|
||||
GROUP BY chapters.manga_id
|
||||
) AS C
|
||||
ON M._id = C.manga_id
|
||||
LEFT JOIN (SELECT * FROM mangas_categories) AS MC
|
||||
ON MC.manga_id = M._id
|
||||
WHERE M.favorite = 1
|
||||
ORDER BY M.title;
|
35
data/src/commonMain/sqldelight/tachiyomi/migrations/23.sqm
Normal file
35
data/src/commonMain/sqldelight/tachiyomi/migrations/23.sqm
Normal file
|
@ -0,0 +1,35 @@
|
|||
DROP VIEW IF EXISTS library_view;
|
||||
CREATE VIEW library_view AS
|
||||
SELECT
|
||||
M.*,
|
||||
coalesce(C.total, 0) AS total,
|
||||
coalesce(C.read_count, 0) AS has_read,
|
||||
coalesce(C.bookmark_count, 0) AS bookmark_count,
|
||||
coalesce(MC.category_id, 0) AS category,
|
||||
coalesce(C.latestUpload, 0) AS latestUpload,
|
||||
coalesce(C.lastRead, 0) AS lastRead,
|
||||
coalesce(C.lastFetch, 0) AS lastFetch
|
||||
FROM mangas AS M
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
chapters.manga_id,
|
||||
count(*) AS total,
|
||||
sum(read) AS read_count,
|
||||
sum(bookmark) AS bookmark_count,
|
||||
coalesce(max(chapters.date_upload), 0) AS latestUpload,
|
||||
coalesce(max(history.history_last_read), 0) AS lastRead,
|
||||
coalesce(max(chapters.date_fetch), 0) AS lastFetch
|
||||
FROM chapters
|
||||
LEFT JOIN scanlators_view AS filtered_scanlators
|
||||
ON chapters.manga_id = filtered_scanlators.manga_id
|
||||
AND ifnull(chapters.scanlator, 'N/A') = ifnull(filtered_scanlators.name, '/<INVALID>/')
|
||||
LEFT JOIN history
|
||||
ON chapters._id = history.history_chapter_id
|
||||
WHERE filtered_scanlators.name IS NULL
|
||||
GROUP BY chapters.manga_id
|
||||
) AS C
|
||||
ON M._id = C.manga_id
|
||||
LEFT JOIN (SELECT * FROM mangas_categories) AS MC
|
||||
ON MC.manga_id = M._id
|
||||
WHERE M.favorite = 1
|
||||
ORDER BY M.title;
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE chapters ADD COLUMN bookmark INTEGER DEFAULT 0;
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE chapters ADD COLUMN scanlator TEXT DEFAULT NULL;
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE manga_sync ADD COLUMN remote_url TEXT DEFAULT '';
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE manga_sync ADD COLUMN library_id INTEGER;
|
|
@ -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;
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE mangas ADD COLUMN hideTitle INTEGER DEFAULT 0;
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE categories ADD COLUMN manga_order TEXT;
|
|
@ -0,0 +1,38 @@
|
|||
CREATE VIEW library_view AS
|
||||
SELECT
|
||||
M.*,
|
||||
coalesce(C.total, 0) AS total,
|
||||
coalesce(C.read_count, 0) AS has_read,
|
||||
coalesce(C.bookmark_count, 0) AS bookmark_count,
|
||||
coalesce(MC.category_id, 0) AS category,
|
||||
coalesce(C.latestUpload, 0) AS latestUpload,
|
||||
coalesce(C.lastRead, 0) AS lastRead,
|
||||
coalesce(C.lastFetch, 0) AS lastFetch
|
||||
FROM mangas AS M
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
chapters.manga_id,
|
||||
count(*) AS total,
|
||||
sum(read) AS read_count,
|
||||
sum(bookmark) AS bookmark_count,
|
||||
coalesce(max(chapters.date_upload), 0) AS latestUpload,
|
||||
coalesce(max(history.history_last_read), 0) AS lastRead,
|
||||
coalesce(max(chapters.date_fetch), 0) AS lastFetch
|
||||
FROM chapters
|
||||
LEFT JOIN scanlators_view AS filtered_scanlators
|
||||
ON chapters.manga_id = filtered_scanlators.manga_id
|
||||
AND ifnull(chapters.scanlator, 'N/A') = ifnull(filtered_scanlators.name, '/<INVALID>/') -- I assume if it's N/A it shouldn't be filtered
|
||||
LEFT JOIN history
|
||||
ON chapters._id = history.history_chapter_id
|
||||
WHERE filtered_scanlators.name IS NULL
|
||||
GROUP BY chapters.manga_id
|
||||
) AS C
|
||||
ON M._id = C.manga_id
|
||||
LEFT JOIN (SELECT * FROM mangas_categories) AS MC
|
||||
ON MC.manga_id = M._id
|
||||
WHERE M.favorite = 1
|
||||
ORDER BY M.title;
|
||||
|
||||
findAll:
|
||||
SELECT *
|
||||
FROM library_view;
|
|
@ -0,0 +1,13 @@
|
|||
CREATE VIEW scanlators_view AS
|
||||
SELECT S.* FROM (
|
||||
WITH RECURSIVE split(seq, _id, name, str) AS ( -- Probably should migrate this to its own table someday
|
||||
SELECT 0, mangas._id, NULL, replace(ifnull(mangas.filtered_scanlators, ''), ' & ', '[.]')||'[.]' FROM mangas WHERE mangas._id
|
||||
UNION ALL SELECT
|
||||
seq+1,
|
||||
_id,
|
||||
substr(str, 0, instr(str, '[.]')),
|
||||
substr(str, instr(str, '[.]')+3)
|
||||
FROM split WHERE str != ''
|
||||
)
|
||||
SELECT _id AS manga_id, name FROM split WHERE split.seq != 0 ORDER BY split.seq ASC
|
||||
) AS S;
|
Loading…
Add table
Add a link
Reference in a new issue