CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-app-cash-sqldelight--runtime

SQLDelight multiplatform runtime library providing typesafe Kotlin APIs from SQL statements with compile-time schema verification

Pending
Overview
Eval results
Files

logging-utilities.mddocs/

Logging and Utilities

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.

Capabilities

SQL Logging Driver

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())
}

Statement Parameter Interceptor

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)
    }
}

Transaction Lifecycle Logging

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")
}

Query Listener Logging

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]

Custom Logging Integration

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()
    ))
}

Performance Monitoring

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

docs

column-adapters.md

database-driver.md

index.md

logging-utilities.md

query-system.md

schema-management.md

transaction-management.md

tile.json