Skip to content

Commit

Permalink
Initial intrinsic approach towards suspendable database transactions.
Browse files Browse the repository at this point in the history
Signed-off-by: Kenneth J. Shackleton <[email protected]>
  • Loading branch information
kennethshackleton committed Jul 21, 2021
1 parent 3608e83 commit 005bb7b
Show file tree
Hide file tree
Showing 11 changed files with 451 additions and 27 deletions.
2 changes: 2 additions & 0 deletions AndroidCLI/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,6 @@ dependencies {
implementation(androidX("core", "ktx", version = "1.0.2"))
implementation(androidX("paging", "runtime", "2.1.0"))
implementation(androidX("constraintlayout", version = "1.1.3"))
implementation(kotlinX("coroutines-core", Versions.KOTLIN_COROUTINES.version))
implementation(kotlinX("coroutines-android", Versions.KOTLIN_COROUTINES.version))
}
37 changes: 26 additions & 11 deletions AndroidCLI/src/main/kotlin/com/bloomberg/selekt/cli/CLIActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,28 @@ import com.bloomberg.selekt.SQLiteTraceEventMode
import com.bloomberg.selekt.android.SQLiteDatabase
import com.bloomberg.selekt.android.Selekt
import com.bloomberg.selekt.cli.databinding.ActivityMainBinding
import java.lang.StringBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class CLIActivity : AppCompatActivity() {
private lateinit var database: SQLiteDatabase
private lateinit var binding: ActivityMainBinding
private val scope = CoroutineScope(Dispatchers.IO)

private val keyListener = View.OnKeyListener { _, keyCode, keyEvent ->
if (KeyEvent.KEYCODE_ENTER == keyCode) {
if (KeyEvent.ACTION_UP == keyEvent.action) {
binding.input.text.apply {
takeUnless { it.isBlank() }?.toString()?.let {
runCatching { executeSQL(it) }.exceptionOrNull()?.let { e -> log(e) }
scope.launch {
runCatching { executeSQL(it) }.getOrElse {
launch(Dispatchers.Main) {
log(it.message)
}
}
}
}
clear()
}
Expand Down Expand Up @@ -85,18 +95,21 @@ class CLIActivity : AppCompatActivity() {

private fun log(any: Any?) = binding.console.append("\n$any")

private fun executeSQL(sql: String) {
@OptIn(Experimental::class)
private suspend fun executeSQL(sql: String) = withContext(scope.coroutineContext) {
log("> $sql")
if (sql.startsWith("SELECT", ignoreCase = true) || sql.startsWith("PRAGMA", ignoreCase = true)) {
query(sql)
} else {
database.exec(sql)
database.withTransaction {
delayTransaction(100L) // Very important work!
if (sql.startsWith("SELECT", ignoreCase = true) || sql.startsWith("PRAGMA", ignoreCase = true)) {
query(sql)
} else {
exec(sql)
null
}
}
}

private fun query(sql: String) = database.query(sql, emptyArray()).use {
}?.use {
if (it.columnCount < 1) {
return
return@use
}
val builder = StringBuilder()
while (it.moveToNext()) {
Expand All @@ -109,4 +122,6 @@ class CLIActivity : AppCompatActivity() {
builder.clear()
}
}

private fun query(sql: String) = database.query(sql, emptyArray())
}
2 changes: 2 additions & 0 deletions AndroidLib/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ android {
defaultPublishConfig = "release"

sourceSets["main"].java.srcDir("src/main/kotlin")
sourceSets["test"].java.srcDir("src/test/kotlin")
sourceSets["test"].resources.srcDir("$buildDir/intermediates/libs")

buildTypes {
Expand All @@ -73,6 +74,7 @@ dependencies {
api(selekt("api", selektVersionName))
compileOnly(selekt("annotations", selektVersionName))
compileOnly(androidX("room", "runtime", Versions.ANDROIDX_ROOM.version))
compileOnly(kotlinX("coroutines-core", Versions.KOTLIN_COROUTINES.version))
implementation(selekt("android-sqlcipher", sqlcipherVersionName))
implementation(selekt("java", selektVersionName))
implementation(selekt("sqlite3", selektVersionName))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import com.bloomberg.selekt.SQLiteJournalMode
import com.bloomberg.selekt.SQLiteTraceEventMode
import com.bloomberg.selekt.SQLiteTransactionMode
import com.bloomberg.selekt.pools.Priority
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import org.intellij.lang.annotations.Language
import java.io.Closeable
import java.io.File
Expand Down Expand Up @@ -172,7 +174,7 @@ class SQLiteDatabase private constructor(
fun batch(@Language("RoomSql") sql: String, bindArgs: Sequence<Array<out Any?>>): Int = database.batch(sql, bindArgs)

/**
* Begins a transaction in exclusive mode.
* Begins a transaction in exclusive mode. Prefer [transact] or [withTransaction] whenever possible.
*
* Transactions can be nested. When the outer transaction is ended all of the work done in that transaction and all of
* the nested transactions will be committed or rolled back. The changes will be rolled back if any transaction is ended
Expand All @@ -182,13 +184,15 @@ class SQLiteDatabase private constructor(
*
* @link [SQLite's transaction](https://www.sqlite.org/lang_transaction.html)
*/
@Throws(InterruptedException::class)
internal fun beginExclusiveTransaction() = database.beginExclusiveTransaction()

@Throws(InterruptedException::class)
internal fun beginExclusiveTransactionWithListener(listener: SQLTransactionListener) =
database.beginExclusiveTransactionWithListener(listener)

/**
* Begins a transaction in immediate mode. Prefer [transact] whenever possible.
* Begins a transaction in immediate mode. Prefer [transact] or [withTransaction] whenever possible.
*
* Transactions can be nested. When the outer transaction is ended all of the work done in that transaction and all of
* the nested transactions will be committed or rolled back. The changes will be rolled back if any transaction is ended
Expand All @@ -198,13 +202,26 @@ class SQLiteDatabase private constructor(
*
* @link [SQLite's transaction](https://www.sqlite.org/lang_transaction.html)
*/
@Throws(InterruptedException::class)
internal fun beginImmediateTransaction() = database.beginImmediateTransaction()

@Throws(InterruptedException::class)
internal fun beginImmediateTransactionWithListener(listener: SQLTransactionListener) =
database.beginImmediateTransactionWithListener(listener)

fun compileStatement(@Language("RoomSql") sql: String) = database.compileStatement(sql)

/**
* Temporarily end the transaction to allow other threads to make progress. The transaction is assumed to be successful
* thus far and committed, do not call [setTransactionSuccessful]. When this method returns a new transaction will have
* been created but not yet marked as successful.
*
* The delaying transaction can be nested.
*
* @since 0.14.0
*/
suspend fun delayTransaction(pauseMillis: Long = 0L) = database.delayTransaction(pauseMillis)

fun delete(table: String, whereClause: String?, whereArgs: Array<out Any?>?) =
database.delete(
table,
Expand Down Expand Up @@ -415,6 +432,24 @@ class SQLiteDatabase private constructor(
*/
fun vacuum() = exec("VACUUM")

/**
* Calls the suspending [block] inside a database transaction. The transaction will be marked as successful unless an
* exception is thrown in the suspending [block] or the coroutine is cancelled.
*
* Performing blocking database operations is not permitted in a coroutine scope other than the one received by the
* suspending block, doing so results in undefined behaviour. Calls to [withTransaction] can be nested though.
*
* This is an optional method that requires the Kotlin Coroutines library at runtime.
*
* @since 0.14.0
*/
@Experimental
suspend fun <T> withTransaction(
transactionMode: SQLiteTransactionMode = SQLiteTransactionMode.EXCLUSIVE,
dispatcher: CoroutineDispatcher = Dispatchers.IO,
block: suspend SQLiteDatabase.() -> T
) = database.withTransaction(this, transactionMode, dispatcher, block)

/**
* @link [SQLite's blob_write](https://www.sqlite.org/c3ref/blob_write.html)
* @since 0.7.4
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import com.bloomberg.selekt.SQLiteJournalMode
import com.bloomberg.selekt.android.SQLiteDatabase
import org.intellij.lang.annotations.Language
import java.util.Locale
import kotlin.jvm.Throws

internal fun SQLiteDatabase.asSupportSQLiteDatabase(): SupportSQLiteDatabase = SupportSQLiteDatabase(this)

Expand Down Expand Up @@ -169,7 +170,9 @@ private class SupportSQLiteDatabase constructor(
whereArgs: Array<out Any>?
) = database.update(table, values, whereClause, whereArgs, conflictAlgorithm.toConflictAlgorithm())

@Throws(InterruptedException::class)
override fun yieldIfContendedSafely() = yieldIfContendedSafely(0L)

@Throws(InterruptedException::class)
override fun yieldIfContendedSafely(sleepAfterYieldDelay: Long) = database.yieldTransaction(sleepAfterYieldDelay)
}
Loading

0 comments on commit 005bb7b

Please sign in to comment.