Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package io.github.openflocon.flocon.database.room3

import androidx.sqlite.SQLiteDriver

fun SQLiteDriver.floconLogs(id: String): SQLiteDriver = this // no op
4 changes: 4 additions & 0 deletions FloconAndroid/database/room3/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ kotlin {
}

sourceSets {
// the SQLite driver methods are not suspend on these targets, they are on the web
listOf("androidMain", "jvmMain", "iosMain").forEach {
named(it) { kotlin.srcDir("src/nonWebMain/kotlin") }
}
commonMain {
kotlin.srcDir("src/roomMain/kotlin")
dependencies {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package io.github.openflocon.flocon.database.room3

import androidx.sqlite.SQLiteConnection
import androidx.sqlite.SQLiteDriver
import androidx.sqlite.SQLiteStatement
import kotlin.test.Test
import kotlin.test.assertEquals

class FloconLoggingSQLiteDriverTest {

private data class Log(val dbId: String, val sql: String, val bindArgs: List<Any?>)

private val logs = mutableListOf<Log>()
private val statement = FakeStatement()
private val driver = FloconLoggingSQLiteDriver(
delegate = object : SQLiteDriver {
override fun open(fileName: String): SQLiteConnection = object : SQLiteConnection {
override fun prepare(sql: String): SQLiteStatement = statement
override fun close() = Unit
}
},
dbId = "dogs",
logger = { dbId, sql, bindArgs -> logs += Log(dbId, sql, bindArgs) },
)

private fun prepare(sql: String) = driver.open("dogs.db").prepare(sql)

@Test
fun `the query is logged with its id and its arguments when it runs`() {
val prepared = prepare("INSERT INTO dog (name, age) VALUES (?, ?)")
prepared.bindText(1, "Rex")
prepared.bindLong(2, 4)
assertEquals(emptyList(), logs)

prepared.step()

assertEquals(listOf(Log("dogs", "INSERT INTO dog (name, age) VALUES (?, ?)", listOf("Rex", 4L))), logs)
}

@Test
fun `the arguments are in the order of their index`() {
val prepared = prepare("SELECT * FROM dog WHERE age > ? AND name = ?")
prepared.bindText(2, "Rex")
prepared.bindNull(1)

prepared.step()

assertEquals(listOf<Any?>(null, "Rex"), logs.single().bindArgs)
}

@Test
fun `a query is logged once per run, not once per row`() {
val prepared = prepare("SELECT * FROM dog")

prepared.step()
prepared.step()
prepared.step()
assertEquals(1, logs.size)

prepared.reset()
prepared.step()
assertEquals(2, logs.size)
}

@Test
fun `the statement still runs on the wrapped driver`() {
val prepared = prepare("SELECT 1")
prepared.bindLong(1, 1)

prepared.step()

assertEquals(listOf("bindLong 1", "step"), statement.calls)
}

private class FakeStatement : SQLiteStatement {
val calls = mutableListOf<String>()
override fun bindBlob(index: Int, value: ByteArray) { calls += "bindBlob $index" }
override fun bindDouble(index: Int, value: Double) { calls += "bindDouble $index" }
override fun bindLong(index: Int, value: Long) { calls += "bindLong $index" }
override fun bindText(index: Int, value: String) { calls += "bindText $index" }
override fun bindNull(index: Int) { calls += "bindNull $index" }
override fun getBlob(index: Int): ByteArray = ByteArray(0)
override fun getDouble(index: Int): Double = 0.0
override fun getLong(index: Int): Long = 0
override fun getText(index: Int): String = ""
override fun isNull(index: Int): Boolean = false
override fun getColumnCount(): Int = 0
override fun getColumnName(index: Int): String = ""
override fun getColumnType(index: Int): Int = 0
override fun step(): Boolean { calls += "step"; return false }
override fun reset() { calls += "reset" }
override fun clearBindings() { calls += "clearBindings" }
override fun close() = Unit
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package io.github.openflocon.flocon.database.room3

import androidx.sqlite.SQLiteConnection
import androidx.sqlite.SQLiteDriver
import androidx.sqlite.SQLiteStatement

actual fun SQLiteDriver.floconLogs(id: String): SQLiteDriver = FloconLoggingSQLiteDriver(delegate = this, dbId = id, logger = DatabasePluginQueryLogger)

internal class FloconLoggingSQLiteDriver(
private val delegate: SQLiteDriver,
private val dbId: String,
private val logger: QueryLogger,
) : SQLiteDriver {

override val hasConnectionPool: Boolean
get() = delegate.hasConnectionPool

override fun open(fileName: String): SQLiteConnection = FloconLoggingConnection(delegate.open(fileName), dbId, logger)
}

private class FloconLoggingConnection(
private val delegate: SQLiteConnection,
private val dbId: String,
private val logger: QueryLogger,
) : SQLiteConnection by delegate {

override fun prepare(sql: String): SQLiteStatement = FloconLoggingStatement(delegate.prepare(sql), dbId, sql, logger)
}

/**
* Logs the query when it starts running: the first `step`, and the first one after each `reset`.
*/
private class FloconLoggingStatement(
private val delegate: SQLiteStatement,
private val dbId: String,
private val sql: String,
private val logger: QueryLogger,
) : SQLiteStatement by delegate {

private val bindArgs = mutableMapOf<Int, Any?>()
private var logged = false

override fun bindBlob(index: Int, value: ByteArray) {
bindArgs[index] = "[blob ${value.size} bytes]"
delegate.bindBlob(index, value)
}

override fun bindDouble(index: Int, value: Double) {
bindArgs[index] = value
delegate.bindDouble(index, value)
}

override fun bindLong(index: Int, value: Long) {
bindArgs[index] = value
delegate.bindLong(index, value)
}

override fun bindText(index: Int, value: String) {
bindArgs[index] = value
delegate.bindText(index, value)
}

override fun bindNull(index: Int) {
bindArgs[index] = null
delegate.bindNull(index)
}

override fun step(): Boolean {
if (!logged) {
logged = true
logger.log(dbId = dbId, sql = sql, bindArgs = bindArgs.entries.sortedBy { it.key }.map { it.value })
}
return delegate.step()
}

override fun reset() {
logged = false
delegate.reset()
}

override fun clearBindings() {
bindArgs.clear()
delegate.clearBindings()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.github.openflocon.flocon.database.room3

import androidx.sqlite.SQLiteDriver

/**
* Shows the queries run by a Room 3 database in the desktop app: Room 3 has no query callback, so the
* driver of the database is wrapped.
*
* ```kotlin
* Room.databaseBuilder<DogDatabase>(context, "dogs_database")
* .setDriver(BundledSQLiteDriver().floconLogs(id = "dogs"))
* .build()
* .also { Flocon.databasePlugin.register(id = "dogs", displayName = "Dogs", database = it) }
* ```
*
* Meant for the drivers that run the queries themselves (bundled, framework, native), not for the web worker driver.
*
* @param id the id the database is registered with, see [register]
*/
expect fun SQLiteDriver.floconLogs(id: String): SQLiteDriver
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.github.openflocon.flocon.database.room3

import io.github.openflocon.flocon.Flocon
import io.github.openflocon.flocon.database.core.databasePlugin

internal fun interface QueryLogger {
fun log(dbId: String, sql: String, bindArgs: List<Any?>)
}

internal object DatabasePluginQueryLogger : QueryLogger {
override fun log(dbId: String, sql: String, bindArgs: List<Any?>) {
Flocon.databasePlugin.logQuery(dbId = dbId, sqlQuery = sql, bindArgs = bindArgs)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package io.github.openflocon.flocon.database.room3

import androidx.sqlite.SQLiteConnection
import androidx.sqlite.SQLiteDriver
import androidx.sqlite.SQLiteStatement

actual fun SQLiteDriver.floconLogs(id: String): SQLiteDriver = FloconLoggingSQLiteDriver(delegate = this, dbId = id, logger = DatabasePluginQueryLogger)

internal class FloconLoggingSQLiteDriver(
private val delegate: SQLiteDriver,
private val dbId: String,
private val logger: QueryLogger,
) : SQLiteDriver {

override val hasConnectionPool: Boolean
get() = delegate.hasConnectionPool

override suspend fun open(fileName: String): SQLiteConnection = FloconLoggingConnection(delegate.open(fileName), dbId, logger)
}

private class FloconLoggingConnection(
private val delegate: SQLiteConnection,
private val dbId: String,
private val logger: QueryLogger,
) : SQLiteConnection by delegate {

override suspend fun prepare(sql: String): SQLiteStatement = FloconLoggingStatement(delegate.prepare(sql), dbId, sql, logger)
}

/**
* Logs the query when it starts running: the first `step`, and the first one after each `reset`.
*/
private class FloconLoggingStatement(
private val delegate: SQLiteStatement,
private val dbId: String,
private val sql: String,
private val logger: QueryLogger,
) : SQLiteStatement by delegate {

private val bindArgs = mutableMapOf<Int, Any?>()
private var logged = false

override fun bindBlob(index: Int, value: ByteArray) {
bindArgs[index] = "[blob ${value.size} bytes]"
delegate.bindBlob(index, value)
}

override fun bindDouble(index: Int, value: Double) {
bindArgs[index] = value
delegate.bindDouble(index, value)
}

override fun bindLong(index: Int, value: Long) {
bindArgs[index] = value
delegate.bindLong(index, value)
}

override fun bindText(index: Int, value: String) {
bindArgs[index] = value
delegate.bindText(index, value)
}

override fun bindNull(index: Int) {
bindArgs[index] = null
delegate.bindNull(index)
}

override suspend fun step(): Boolean {
if (!logged) {
logged = true
logger.log(dbId = dbId, sql = sql, bindArgs = bindArgs.entries.sortedBy { it.key }.map { it.value })
}
return delegate.step()
}

override fun reset() {
logged = false
delegate.reset()
}

override fun clearBindings() {
bindArgs.clear()
delegate.clearBindings()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package io.github.openflocon.flocon.myapplication.multi

import android.content.Context
import androidx.room3.Room
import androidx.sqlite.driver.AndroidSQLiteDriver
import io.github.openflocon.flocon.database.room3.floconLogs
import io.github.openflocon.flocon.Flocon
import io.github.openflocon.flocon.database.core.databasePlugin
import io.github.openflocon.flocon.database.room3.register
Expand All @@ -19,6 +21,7 @@ object Databases {
klass = DogDatabase::class.java,
name = "dogs_database"
)
.setDriver(AndroidSQLiteDriver().floconLogs(id = "dogs_room3"))
.fallbackToDestructiveMigration(dropAllTables = true)
.build()
dogDatabase = instance
Expand All @@ -39,7 +42,9 @@ object Databases {
context = context.applicationContext,
klass = FoodDatabase::class.java,
name = "food_database"
).build()
)
.setDriver(AndroidSQLiteDriver().floconLogs(id = "food_room3"))
.build()
foodDatabase = instance
Flocon.databasePlugin.register(
id = "food_room3",
Expand All @@ -58,7 +63,9 @@ object Databases {
val instance = Room.inMemoryDatabaseBuilder(
context.applicationContext,
DogDatabase::class.java,
).build()
)
.setDriver(AndroidSQLiteDriver().floconLogs(id = "dogs_room3_memory"))
.build()
inMemoryDogDatabase = instance
Flocon.databasePlugin.register(
id = "dogs_room3_memory",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.room3.Room
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import io.github.openflocon.flocon.database.room3.floconLogs
import coil3.ImageLoader
import coil3.PlatformContext
import coil3.SingletonImageLoader
Expand Down Expand Up @@ -108,7 +109,7 @@ private fun dogDatabase(): DogDatabase {
val dbFile = File(System.getProperty("java.io.tmpdir"), "flocon_dogs_database.db")
return Room.databaseBuilder<DogDatabase>(name = dbFile.absolutePath)
.fallbackToDestructiveMigration(dropAllTables = true)
.setDriver(BundledSQLiteDriver())
.setDriver(BundledSQLiteDriver().floconLogs(id = "dogs_jvm"))
.setQueryCoroutineContext(Dispatchers.IO)
.build()
}
Expand All @@ -117,7 +118,7 @@ private fun foodDatabase(): FoodDatabase {
val dbFile = File(System.getProperty("java.io.tmpdir"), "flocon_food_database.db")
return Room.databaseBuilder<FoodDatabase>(name = dbFile.absolutePath)
.fallbackToDestructiveMigration(dropAllTables = true)
.setDriver(BundledSQLiteDriver())
.setDriver(BundledSQLiteDriver().floconLogs(id = "food_jvm"))
.setQueryCoroutineContext(Dispatchers.IO)
.build()
}
Expand Down
Loading