SQLDelight multiplatform runtime library providing typesafe Kotlin APIs from SQL statements with compile-time schema verification
—
SQLDelight Runtime provides logging and debugging utilities to help monitor database operations and troubleshoot issues. These utilities include SQL statement logging and parameter inspection capabilities.
A decorator that wraps any SqlDriver to provide comprehensive logging of all database operations.
/**
* SqlDriver decorator that logs all database operations for debugging and monitoring
* @param sqlDriver The underlying SqlDriver to wrap
* @param logger Function to receive log messages
*/
class LogSqliteDriver(
private val sqlDriver: SqlDriver,
private val logger: (String) -> Unit
) : SqlDriver {
override fun <R> executeQuery(
identifier: Int?,
sql: String,
mapper: (SqlCursor) -> QueryResult<R>,
parameters: Int,
binders: (SqlPreparedStatement.() -> Unit)?
): QueryResult<R>
override fun execute(
identifier: Int?,
sql: String,
parameters: Int,
binders: (SqlPreparedStatement.() -> Unit)?
): QueryResult<Long>
override fun newTransaction(): QueryResult<Transacter.Transaction>
override fun currentTransaction(): Transacter.Transaction?
override fun addListener(vararg queryKeys: String, listener: Query.Listener)
override fun removeListener(vararg queryKeys: String, listener: Query.Listener)
override fun notifyListeners(vararg queryKeys: String)
override fun close()
}Usage Examples:
import app.cash.sqldelight.logs.LogSqliteDriver
// Create a logging wrapper around existing driver
val loggingDriver = LogSqliteDriver(
sqlDriver = actualDriver,
logger = { message ->
println("[SQLDelight] $message")
}
)
// Use with custom logger (e.g., Android Log)
val androidLoggingDriver = LogSqliteDriver(
sqlDriver = actualDriver,
logger = { message ->
Log.d("Database", message)
}
)
// Use with structured logging
val structuredLoggingDriver = LogSqliteDriver(
sqlDriver = actualDriver,
logger = { message ->
logger.debug("database_operation") {
put("sql_operation", message)
put("timestamp", System.currentTimeMillis())
}
}
)
// Create database with logging
val database = Database(loggingDriver)
// All operations will now be logged:
// [SQLDelight] QUERY
// SELECT * FROM users WHERE active = ?
// [true]
val users = database.userQueries.selectActiveUsers(true).executeAsList()
// [SQLDelight] EXECUTE
// INSERT INTO users (name, email) VALUES (?, ?)
// [Alice, alice@example.com]
database.userQueries.insertUser("Alice", "alice@example.com")
// [SQLDelight] TRANSACTION BEGIN
// [SQLDelight] EXECUTE
// UPDATE users SET last_login = ? WHERE id = ?
// [1609459200000, 1]
// [SQLDelight] TRANSACTION COMMIT
database.transaction {
database.userQueries.updateLastLogin(1, System.currentTimeMillis())
}Utility class for intercepting and inspecting prepared statement parameters during SQL execution.
/**
* SqlPreparedStatement implementation that intercepts and stores parameter values for logging
*/
class StatementParameterInterceptor : SqlPreparedStatement {
override fun bindBytes(index: Int, bytes: ByteArray?)
override fun bindLong(index: Int, long: Long?)
override fun bindDouble(index: Int, double: Double?)
override fun bindString(index: Int, string: String?)
override fun bindBoolean(index: Int, boolean: Boolean?)
/**
* Get all bound parameters and clear the internal parameter list
* @returns List of all parameter values in binding order
*/
fun getAndClearParameters(): List<Any?>
}Usage Examples:
import app.cash.sqldelight.logs.StatementParameterInterceptor
// Manual parameter inspection
val interceptor = StatementParameterInterceptor()
// Simulate parameter binding
interceptor.bindString(1, "Alice")
interceptor.bindLong(2, 25L)
interceptor.bindBoolean(3, true)
// Retrieve bound parameters
val parameters = interceptor.getAndClearParameters()
println("Bound parameters: $parameters")
// Output: Bound parameters: [Alice, 25, true]
// Use in custom driver implementation
class CustomLoggingDriver(private val delegate: SqlDriver) : SqlDriver by delegate {
override fun execute(
identifier: Int?,
sql: String,
parameters: Int,
binders: (SqlPreparedStatement.() -> Unit)?
): QueryResult<Long> {
if (binders != null) {
val interceptor = StatementParameterInterceptor()
interceptor.binders()
val params = interceptor.getAndClearParameters()
println("Executing: $sql with parameters: $params")
} else {
println("Executing: $sql (no parameters)")
}
return delegate.execute(identifier, sql, parameters, binders)
}
}LogSqliteDriver automatically attaches logging hooks to transactions for comprehensive transaction lifecycle monitoring.
Usage Examples:
import app.cash.sqldelight.logs.LogSqliteDriver
val loggingDriver = LogSqliteDriver(actualDriver) { message ->
println("[${System.currentTimeMillis()}] $message")
}
val database = Database(loggingDriver)
// Transaction logging shows complete lifecycle
database.transaction {
// [1609459200000] TRANSACTION BEGIN
database.userQueries.insertUser("Alice", "alice@example.com")
// [1609459200001] EXECUTE
// INSERT INTO users (name, email) VALUES (?, ?)
// [Alice, alice@example.com]
database.userQueries.insertUser("Bob", "bob@example.com")
// [1609459200002] EXECUTE
// INSERT INTO users (name, email) VALUES (?, ?)
// [Bob, bob@example.com]
// [1609459200003] TRANSACTION COMMIT
}
// Failed transaction logging
try {
database.transaction {
// [1609459200100] TRANSACTION BEGIN
database.userQueries.insertUser("Charlie", "invalid-email-format")
// [1609459200101] EXECUTE
// INSERT INTO users (name, email) VALUES (?, ?)
// [Charlie, invalid-email-format]
throw RuntimeException("Simulated failure")
// [1609459200102] TRANSACTION ROLLBACK
}
} catch (e: Exception) {
println("Transaction failed as expected")
}Monitor query listener registration and notification events.
Usage Examples:
import app.cash.sqldelight.Query
import app.cash.sqldelight.logs.LogSqliteDriver
val loggingDriver = LogSqliteDriver(actualDriver) { message ->
println("[Listener] $message")
}
val database = Database(loggingDriver)
val userQuery = database.userQueries.selectAll()
val listener = Query.Listener {
println("User data changed!")
}
// Register listener
userQuery.addListener(listener)
// [Listener] BEGIN Query.Listener@123456 LISTENING TO [users]
// Make changes that trigger notifications
database.userQueries.insertUser("New User", "new@example.com")
// [Listener] NOTIFYING LISTENERS OF [users]
// User data changed!
// Remove listener
userQuery.removeListener(listener)
// [Listener] END Query.Listener@123456 LISTENING TO [users]Integrate with various logging frameworks and monitoring systems.
Usage Examples:
import app.cash.sqldelight.logs.LogSqliteDriver
// SLF4J integration
import org.slf4j.LoggerFactory
val slf4jLogger = LoggerFactory.getLogger("SqlDelight")
val slf4jLoggingDriver = LogSqliteDriver(actualDriver) { message ->
slf4jLogger.debug(message)
}
// Logback with structured logging
val structuredDriver = LogSqliteDriver(actualDriver) { message ->
slf4jLogger.atDebug()
.addKeyValue("component", "sqldelight")
.addKeyValue("operation", extractOperation(message))
.log(message)
}
// Metrics collection
class MetricsLoggingDriver(
delegate: SqlDriver,
private val metrics: MetricsCollector
) : LogSqliteDriver(delegate, { message ->
when {
message.startsWith("QUERY") -> metrics.incrementCounter("sql.queries")
message.startsWith("EXECUTE") -> metrics.incrementCounter("sql.executions")
message.startsWith("TRANSACTION BEGIN") -> metrics.incrementCounter("sql.transactions.begin")
message.startsWith("TRANSACTION COMMIT") -> metrics.incrementCounter("sql.transactions.commit")
message.startsWith("TRANSACTION ROLLBACK") -> metrics.incrementCounter("sql.transactions.rollback")
}
// Also log to console for debugging
println(message)
})
// Conditional logging (e.g., only in debug builds)
val conditionalDriver = LogSqliteDriver(actualDriver) { message ->
if (BuildConfig.DEBUG) {
println("[SQL Debug] $message")
}
}
// File-based logging
val fileLoggingDriver = LogSqliteDriver(actualDriver) { message ->
File("sql_operations.log").appendText("${Date()}: $message\n")
}
// Network logging for remote monitoring
val networkLoggingDriver = LogSqliteDriver(actualDriver) { message ->
// Send to remote logging service
loggingService.send(LogEntry(
timestamp = System.currentTimeMillis(),
component = "sqldelight",
message = message,
deviceId = getDeviceId()
))
}Use logging to monitor database performance and identify bottlenecks.
Usage Examples:
import app.cash.sqldelight.logs.LogSqliteDriver
class PerformanceLoggingDriver(
delegate: SqlDriver
) : LogSqliteDriver(delegate, { message -> /* no-op */ }) {
private val queryTimes = mutableMapOf<String, Long>()
override fun <R> executeQuery(
identifier: Int?,
sql: String,
mapper: (SqlCursor) -> QueryResult<R>,
parameters: Int,
binders: (SqlPreparedStatement.() -> Unit)?
): QueryResult<R> {
val startTime = System.nanoTime()
return try {
super.executeQuery(identifier, sql, mapper, parameters, binders)
} finally {
val duration = (System.nanoTime() - startTime) / 1_000_000 // Convert to milliseconds
if (duration > 100) { // Log slow queries (>100ms)
println("SLOW QUERY ($duration ms): $sql")
}
// Track average query times
val avgKey = sql.take(50) // Use first 50 chars as key
queryTimes[avgKey] = ((queryTimes[avgKey] ?: 0L) + duration) / 2
}
}
fun printPerformanceStats() {
println("Query Performance Statistics:")
queryTimes.entries.sortedByDescending { it.value }.forEach { (sql, avgTime) ->
println("$avgTime ms avg: $sql...")
}
}
}
// Usage
val perfDriver = PerformanceLoggingDriver(actualDriver)
val database = Database(perfDriver)
// Perform operations...
database.userQueries.selectAll().executeAsList()
database.userQueries.selectById(1).executeAsOne()
// Print performance report
perfDriver.printPerformanceStats()Install with Tessl CLI
npx tessl i tessl/maven-app-cash-sqldelight--runtime