mirror of
https://github.com/mihonapp/mihon.git
synced 2025-10-28 21:07:57 +01:00
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:
89
data/src/main/java/tachiyomi/data/AndroidDatabaseHandler.kt
Normal file
89
data/src/main/java/tachiyomi/data/AndroidDatabaseHandler.kt
Normal 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) }
|
||||
}
|
||||
}
|
||||
30
data/src/main/java/tachiyomi/data/DatabaseAdapter.kt
Normal file
30
data/src/main/java/tachiyomi/data/DatabaseAdapter.kt
Normal 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()
|
||||
}
|
||||
36
data/src/main/java/tachiyomi/data/DatabaseHandler.kt
Normal file
36
data/src/main/java/tachiyomi/data/DatabaseHandler.kt
Normal 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>
|
||||
}
|
||||
71
data/src/main/java/tachiyomi/data/QueryPagingSource.kt
Normal file
71
data/src/main/java/tachiyomi/data/QueryPagingSource.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
161
data/src/main/java/tachiyomi/data/TransactionContext.kt
Normal file
161
data/src/main/java/tachiyomi/data/TransactionContext.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user