SQLDelight multiplatform runtime library providing Kotlin APIs for type-safe database operations with compile-time SQL verification
—
Comprehensive logging support for debugging database operations with query and transaction visibility, parameter inspection, and connection lifecycle monitoring.
Decorating SqlDriver implementation that logs all database operations for debugging purposes.
/**
* A SqlDriver decorator that logs all database operations using a provided logger function
* @param sqlDriver The underlying SqlDriver to wrap with logging
* @param logger Function to handle log messages
*/
class LogSqliteDriver(
private val sqlDriver: SqlDriver,
private val logger: (String) -> Unit
) : SqlDriver {
/**
* Get the currently open Transaction on the database
* @return Current transaction or null if no transaction is active
*/
override fun currentTransaction(): Transacter.Transaction?
/**
* Execute a SQL statement with logging
* @param identifier Opaque identifier for prepared statement caching
* @param sql SQL string to execute
* @param parameters Number of bindable parameters
* @param binders Lambda to bind parameters to the statement
* @return Number of rows affected
*/
override fun execute(
identifier: Int?,
sql: String,
parameters: Int,
binders: (SqlPreparedStatement.() -> Unit)?
): QueryResult<Long>
/**
* Execute a SQL query with logging
* @param identifier Opaque identifier for prepared statement caching
* @param sql SQL string to execute
* @param mapper Function to process the result cursor
* @param parameters Number of bindable parameters
* @param binders Lambda to bind parameters to the statement
* @return Query result from the mapper function
*/
override fun <R> executeQuery(
identifier: Int?,
sql: String,
mapper: (SqlCursor) -> QueryResult<R>,
parameters: Int,
binders: (SqlPreparedStatement.() -> Unit)?
): QueryResult<R>
/**
* Start a new Transaction with logging
* @return QueryResult containing the new Transaction with attached log hooks
*/
override fun newTransaction(): QueryResult<Transacter.Transaction>
/**
* Close the connection with logging
*/
override fun close()
/**
* Add a listener with logging
* @param queryKeys Table/view names to listen for changes
* @param listener Listener to notify when changes occur
*/
override fun addListener(vararg queryKeys: String, listener: Query.Listener)
/**
* Remove a listener with logging
* @param queryKeys Table/view names to stop listening for changes
* @param listener Listener to remove
*/
override fun removeListener(vararg queryKeys: String, listener: Query.Listener)
/**
* Notify listeners with logging
* @param queryKeys Table/view names that have changed
*/
override fun notifyListeners(vararg queryKeys: String)
}Helper class for intercepting and logging prepared statement parameters.
/**
* Helper class that intercepts SqlPreparedStatement parameter binding
* to capture values for logging purposes
*/
class StatementParameterInterceptor : SqlPreparedStatement {
/**
* Bind bytes parameter and capture for logging
* @param index Parameter index
* @param bytes ByteArray value to bind
*/
override fun bindBytes(index: Int, bytes: ByteArray?)
/**
* Bind double parameter and capture for logging
* @param index Parameter index
* @param double Double value to bind
*/
override fun bindDouble(index: Int, double: Double?)
/**
* Bind long parameter and capture for logging
* @param index Parameter index
* @param long Long value to bind
*/
override fun bindLong(index: Int, long: Long?)
/**
* Bind string parameter and capture for logging
* @param index Parameter index
* @param string String value to bind
*/
override fun bindString(index: Int, string: String?)
/**
* Bind boolean parameter and capture for logging
* @param index Parameter index
* @param boolean Boolean value to bind
*/
override fun bindBoolean(index: Int, boolean: Boolean?)
/**
* Get captured parameters and clear the internal list
* @return List of all captured parameter values
*/
fun getAndClearParameters(): List<Any?>
}Usage Examples:
import app.cash.sqldelight.logs.LogSqliteDriver
import app.cash.sqldelight.db.SqlDriver
// Basic logging setup
class DatabaseManager {
private val baseDriver: SqlDriver = createSqliteDriver()
// Create logging driver with simple console output
private val loggingDriver = LogSqliteDriver(baseDriver) { message ->
println("[DB] $message")
}
// Use the logging driver like any other SqlDriver
fun setupDatabase() {
val database = MyDatabase(loggingDriver)
// All operations will be logged
database.userQueries.insertUser("John Doe", "john@example.com")
val users = database.userQueries.selectAllUsers().executeAsList()
}
}
// Structured logging with different log levels
class StructuredDatabaseLogger {
private val logger = LoggerFactory.getLogger(StructuredDatabaseLogger::class.java)
fun createLoggingDriver(baseDriver: SqlDriver): SqlDriver {
return LogSqliteDriver(baseDriver) { message ->
when {
message.startsWith("EXECUTE") -> logger.debug("SQL Execute: {}", message)
message.startsWith("QUERY") -> logger.debug("SQL Query: {}", message)
message.startsWith("TRANSACTION") -> logger.info("SQL Transaction: {}", message)
message.startsWith("CLOSE") -> logger.info("SQL Connection: {}", message)
message.contains("LISTENING") -> logger.trace("SQL Listener: {}", message)
else -> logger.debug("SQL: {}", message)
}
}
}
}
// File-based logging
class FileDatabaseLogger {
private val logFile = File("database.log")
private val logWriter = logFile.bufferedWriter()
fun createLoggingDriver(baseDriver: SqlDriver): SqlDriver {
return LogSqliteDriver(baseDriver) { message ->
synchronized(logWriter) {
val timestamp = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
logWriter.write("[$timestamp] $message")
logWriter.newLine()
logWriter.flush()
}
}
}
fun close() {
logWriter.close()
}
}
// Conditional logging based on configuration
class ConfigurableLogging {
private val isDebugEnabled = System.getProperty("sqldelight.debug", "false").toBoolean()
private val logQueries = System.getProperty("sqldelight.log.queries", "true").toBoolean()
private val logTransactions = System.getProperty("sqldelight.log.transactions", "true").toBoolean()
fun createLoggingDriver(baseDriver: SqlDriver): SqlDriver {
return if (isDebugEnabled) {
LogSqliteDriver(baseDriver) { message ->
val shouldLog = when {
message.startsWith("QUERY") || message.startsWith("EXECUTE") -> logQueries
message.startsWith("TRANSACTION") -> logTransactions
else -> true
}
if (shouldLog) {
System.err.println("[SQL] $message")
}
}
} else {
baseDriver
}
}
}
// Performance monitoring with logging
class PerformanceLoggingDriver(
baseDriver: SqlDriver
) : SqlDriver by LogSqliteDriver(baseDriver, ::logWithTiming) {
companion object {
private fun logWithTiming(message: String) {
val timestamp = System.currentTimeMillis()
val threadName = Thread.currentThread().name
println("[$timestamp][$threadName] $message")
}
}
}
// Custom logging with metrics collection
class MetricsCollectingLogger {
private val queryCount = AtomicLong(0)
private val transactionCount = AtomicLong(0)
private val errorCount = AtomicLong(0)
fun createLoggingDriver(baseDriver: SqlDriver): SqlDriver {
return LogSqliteDriver(baseDriver) { message ->
// Collect metrics
when {
message.startsWith("QUERY") || message.startsWith("EXECUTE") ->
queryCount.incrementAndGet()
message.startsWith("TRANSACTION BEGIN") ->
transactionCount.incrementAndGet()
message.contains("ERROR") || message.contains("ROLLBACK") ->
errorCount.incrementAndGet()
}
// Log the message
println("[METRICS] Queries: ${queryCount.get()}, Transactions: ${transactionCount.get()}, Errors: ${errorCount.get()}")
println("[SQL] $message")
}
}
fun getMetrics(): DatabaseMetrics {
return DatabaseMetrics(
totalQueries = queryCount.get(),
totalTransactions = transactionCount.get(),
totalErrors = errorCount.get()
)
}
}
// Log filtering and formatting
class FilteredLoggingDriver {
private val sensitivePatterns = listOf(
Regex("password\\s*=\\s*'[^']*'", RegexOption.IGNORE_CASE),
Regex("token\\s*=\\s*'[^']*'", RegexOption.IGNORE_CASE)
)
fun createLoggingDriver(baseDriver: SqlDriver): SqlDriver {
return LogSqliteDriver(baseDriver) { message ->
val sanitized = sanitizeMessage(message)
val formatted = formatMessage(sanitized)
println(formatted)
}
}
private fun sanitizeMessage(message: String): String {
var sanitized = message
sensitivePatterns.forEach { pattern ->
sanitized = pattern.replace(sanitized, "[REDACTED]")
}
return sanitized
}
private fun formatMessage(message: String): String {
val timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss.SSS"))
return "[$timestamp] $message"
}
}
// Testing with captured logs
class TestLoggingDriver {
private val capturedLogs = mutableListOf<String>()
fun createLoggingDriver(baseDriver: SqlDriver): SqlDriver {
return LogSqliteDriver(baseDriver) { message ->
synchronized(capturedLogs) {
capturedLogs.add(message)
}
}
}
fun getCapturedLogs(): List<String> {
return synchronized(capturedLogs) {
capturedLogs.toList()
}
}
fun clearLogs() {
synchronized(capturedLogs) {
capturedLogs.clear()
}
}
fun assertQueryLogged(expectedSql: String) {
val found = capturedLogs.any { it.contains(expectedSql) }
assert(found) { "Expected SQL not found in logs: $expectedSql" }
}
}
// Usage in test
@Test
fun testUserInsertion() {
val testLogger = TestLoggingDriver()
val database = MyDatabase(testLogger.createLoggingDriver(testDriver))
database.userQueries.insertUser("Alice", "alice@example.com")
testLogger.assertQueryLogged("INSERT INTO users")
val logs = testLogger.getCapturedLogs()
assert(logs.any { it.contains("alice@example.com") })
}The LogSqliteDriver produces structured log messages in the following formats:
QUERY\n [SQL statement] followed by parameter list if presentEXECUTE\n [SQL statement] followed by parameter list if presentTRANSACTION BEGIN, TRANSACTION COMMIT, TRANSACTION ROLLBACKCLOSE CONNECTIONBEGIN [listener] LISTENING TO [table1, table2], END [listener] LISTENING TO [table1, table2], NOTIFYING LISTENERS OF [table1, table2] [param1, param2, param3] when parameters are bound to statementsInstall with Tessl CLI
npx tessl i tessl/maven-app-cash-sqldelight--runtime-iossimulatorarm64