Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ interface FloconDatabasePlugin : FloconPlugin {

fun register(floconDatabaseModel: FloconDatabaseModel)

fun logQuery(dbName: String, sqlQuery: String, bindArgs: List<Any?>)
fun logQuery(dbId: String, sqlQuery: String, bindArgs: List<Any?>)

}

internal object FloconDatabasePluginNoOp : FloconDatabasePlugin {
override val key: String = "database"
override val providers: List<FloconDatabaseProvider> = emptyList()
override fun register(floconDatabaseModel: FloconDatabaseModel) = Unit
override fun logQuery(dbName: String, sqlQuery: String, bindArgs: List<Any?>) = Unit
override fun logQuery(dbId: String, sqlQuery: String, bindArgs: List<Any?>) = Unit
override suspend fun onMessageReceived(method: String, body: String) = Unit
override suspend fun onConnectedToServer() = Unit
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,15 @@ interface FloconDatabasePlugin : FloconPlugin {

val providers: List<FloconDatabaseProvider>

/**
* @throws IllegalArgumentException if another database is already registered with the same [FloconDatabaseModel.id]
*/
fun register(floconDatabaseModel: FloconDatabaseModel)

fun logQuery(dbName: String, sqlQuery: String, bindArgs: List<Any?>)
/**
* @param dbId the id the database was registered with (see [register]), the queries are shown with this database in the desktop app
*/
fun logQuery(dbId: String, sqlQuery: String, bindArgs: List<Any?>)

}

Expand All @@ -23,7 +29,7 @@ private class NoOpDatabasePlugin : FloconDatabasePlugin {
override val key: String = ""
override val providers: List<FloconDatabaseProvider> = emptyList()
override fun register(floconDatabaseModel: FloconDatabaseModel) = Unit
override fun logQuery(dbName: String, sqlQuery: String, bindArgs: List<Any?>) = Unit
override fun logQuery(dbId: String, sqlQuery: String, bindArgs: List<Any?>) = Unit
override suspend fun onMessageReceived(method: String, body: String) = Unit
override suspend fun onConnectedToServer() = Unit
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,11 @@ internal class FloconDatabasePluginImpl(
dbMap.values.toList()
}

val all = updatedDatabases.map { DeviceDataBaseDataModel(id = it.id, name = it.displayName) }
sendDatabases(updatedDatabases)
}

private fun sendDatabases(databases: List<FloconDatabaseModel>) {
val all = databases.map { DeviceDataBaseDataModel(id = it.id, name = it.displayName) }

try {
sender.send(
Expand All @@ -99,21 +103,27 @@ internal class FloconDatabasePluginImpl(

override fun register(floconDatabaseModel: FloconDatabaseModel) {
registeredDatabases.update { currentList ->
val existing = currentList.firstOrNull { it.id == floconDatabaseModel.id }
require(existing == null || existing == floconDatabaseModel) {
"A database is already registered with the id \"${floconDatabaseModel.id}\", the ids must be unique"
}
val dbMap = currentList.associateBy { it.id }.toMutableMap()
dbMap[floconDatabaseModel.id] = floconDatabaseModel
dbMap.values.toList()
}
// the desktop shows the database, and the name of its logs, as soon as it is registered
sendDatabases(registeredDatabases.value)
}

override fun logQuery(dbName: String, sqlQuery: String, bindArgs: List<Any?>) {
override fun logQuery(dbId: String, sqlQuery: String, bindArgs: List<Any?>) {
scope.launch {
try {
sender.send(
plugin = Protocol.FromDevice.Database.Plugin,
method = Protocol.FromDevice.Database.Method.LogQuery,
body = encoder.encode(
DatabaseQueryLogModel(
dbName = dbName,
dbId = dbId,
sqlQuery = sqlQuery,
bindArgs = bindArgs.map { it.toString() },
timestamp = currentTimeMillis()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import kotlinx.serialization.Serializable

@Serializable
internal data class DatabaseQueryLogModel(
val dbName: String,
val dbId: String,
val sqlQuery: String,
val bindArgs: List<String>?,
val timestamp: Long,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ package io.github.openflocon.flocon.database.core
import io.github.openflocon.flocon.Protocol
import io.github.openflocon.flocon.core.FloconEncoder
import io.github.openflocon.flocon.core.FloconMessageSender
import io.github.openflocon.flocon.database.core.model.FloconFileDatabaseModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlin.test.Test
import kotlin.test.assertContains
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith

class FloconDatabasePluginTest {

Expand All @@ -29,15 +31,42 @@ class FloconDatabasePluginTest {

@Test
fun `logQuery sends the query`() {
plugin.logQuery(dbName = "dogs", sqlQuery = "SELECT * FROM dog WHERE id = ?", bindArgs = listOf(1))
plugin.logQuery(dbId = "dogs", sqlQuery = "SELECT * FROM dog WHERE id = ?", bindArgs = listOf(1))

val (pluginName, method, body) = sender.messages.single()
assertEquals(Protocol.FromDevice.Database.Plugin, pluginName)
assertEquals(Protocol.FromDevice.Database.Method.LogQuery, method)
assertContains(body, "\"dbName\":\"dogs\"")
assertContains(body, "\"dbId\":\"dogs\"")
assertContains(body, "\"bindArgs\":[\"1\"]")
}

@Test
fun `register refuses a second database with the same id`() {
plugin.register(FloconFileDatabaseModel(id = "dogs", displayName = "dogs", absolutePath = "/a"))

assertFailsWith<IllegalArgumentException> {
plugin.register(FloconFileDatabaseModel(id = "dogs", displayName = "other", absolutePath = "/b"))
}
}

@Test
fun `register sends the databases to the desktop with their id and name`() {
plugin.register(FloconFileDatabaseModel(id = "dogs_room3", displayName = "dogs", absolutePath = "/a"))

val (_, method, body) = sender.messages.single()
assertEquals(Protocol.FromDevice.Database.Method.GetDatabases, method)
assertContains(body, "\"id\":\"dogs_room3\"")
assertContains(body, "\"name\":\"dogs\"")
}

@Test
fun `register accepts the same database twice`() {
val database = FloconFileDatabaseModel(id = "dogs", displayName = "dogs", absolutePath = "/a")

plugin.register(database)
plugin.register(database)
}

@Test
fun `key is the plugin name sent by the desktop`() {
assertEquals(Protocol.ToDevice.Database.Plugin, plugin.key)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import java.util.concurrent.Executor
import java.util.concurrent.Executors

inline fun <reified T : RoomDatabase> RoomDatabase.Builder<T>.floconLogs(
name: String? = T::class.simpleName,
id: String,
executor: Executor = Executors.newSingleThreadExecutor(),
queryCallback: QueryCallback? = null
): RoomDatabase.Builder<T> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ package io.github.openflocon.flocon.database.room
import androidx.room.RoomDatabase
import io.github.openflocon.flocon.database.core.FloconDatabasePlugin

fun FloconDatabasePlugin.register(displayName: String, database: RoomDatabase) {
fun FloconDatabasePlugin.register(id: String, displayName: String, database: RoomDatabase) {
// no op
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ import java.util.concurrent.Executor
import java.util.concurrent.Executors

inline fun <reified T : RoomDatabase> RoomDatabase.Builder<T>.floconLogs(
name: String? = T::class.simpleName,
id: String,
executor: Executor = Executors.newSingleThreadExecutor(),
queryCallback: QueryCallback? = null
): RoomDatabase.Builder<T> {
return setQueryCallback(
queryCallback = { sqlQuery, bindArgs ->
Flocon.databasePlugin.logQuery(
dbName = name ?: "database",
dbId = id,
sqlQuery = sqlQuery,
bindArgs = bindArgs
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ import io.github.openflocon.flocon.database.core.model.fromdevice.DatabaseExecut

/**
* Registers a Room database, to browse it from the desktop app
*
* @param id unique and stable, also the one to give to the query logs of this database
*/
fun FloconDatabasePlugin.register(displayName: String, database: RoomDatabase) {
fun FloconDatabasePlugin.register(id: String, displayName: String, database: RoomDatabase) {
register(
FloconRoomDatabaseModel(
id = displayName,
id = id,
displayName = displayName,
database = database
)
Expand Down
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ package io.github.openflocon.flocon.database.room3
import androidx.room3.RoomDatabase
import io.github.openflocon.flocon.database.core.FloconDatabasePlugin

fun FloconDatabasePlugin.register(displayName: String, database: RoomDatabase) {
fun FloconDatabasePlugin.register(id: String, displayName: String, database: RoomDatabase) {
// 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
}
}
Loading
Loading