refactor: Move database related stuff to data module

This commit is contained in:
Ahmad Ansori Palembani 2024-06-17 16:15:20 +07:00
parent 66354205f1
commit fc171c1e0a
Signed by: null2264
GPG key ID: BA64F8B60AF3EFB6
44 changed files with 36 additions and 49 deletions

View file

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

View 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()
}
}
}

View file

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

View 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>
*/
}

View file

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

View 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;

View file

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

View file

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

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

View file

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

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

View 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;

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1 @@
ALTER TABLE mangas ADD COLUMN filtered_scanlators TEXT;

View 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;

View file

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

View file

@ -0,0 +1,3 @@
UPDATE manga_sync
SET score = max(score, 0)
WHERE sync_id = 7;

View 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

View 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

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

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,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;

View 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;

View 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;

View 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;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1 @@
ALTER TABLE categories ADD COLUMN manga_order TEXT;

View file

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

View file

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