CtrlK
BlogDocsLog inGet started
Tessl Logo

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

SQLDelight multiplatform runtime library providing Kotlin APIs for type-safe database operations with compile-time SQL verification

Pending
Overview
Eval results
Files

logging-debugging.mddocs/

Logging and Debugging

Comprehensive logging support for debugging database operations with query and transaction visibility, parameter inspection, and connection lifecycle monitoring.

Capabilities

LogSqliteDriver Class

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

StatementParameterInterceptor Class

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

Log Message Formats

The LogSqliteDriver produces structured log messages in the following formats:

  • Query Execution: QUERY\n [SQL statement] followed by parameter list if present
  • Statement Execution: EXECUTE\n [SQL statement] followed by parameter list if present
  • Transaction Events: TRANSACTION BEGIN, TRANSACTION COMMIT, TRANSACTION ROLLBACK
  • Connection Events: CLOSE CONNECTION
  • Listener Events: BEGIN [listener] LISTENING TO [table1, table2], END [listener] LISTENING TO [table1, table2], NOTIFYING LISTENERS OF [table1, table2]
  • Parameters: [param1, param2, param3] when parameters are bound to statements

Performance Considerations

  • Logging Overhead: Each database operation incurs additional logging overhead
  • String Formatting: Parameter serialization and message formatting add CPU cost
  • I/O Operations: File or network logging can impact database performance
  • Memory Usage: Parameter capture temporarily holds values in memory
  • Thread Safety: Logger functions should be thread-safe for concurrent database access

Security Considerations

  • Sensitive Data: Be careful not to log sensitive information like passwords or tokens
  • Parameter Sanitization: Consider sanitizing or redacting sensitive parameters
  • Log Storage: Ensure log files are properly secured and access-controlled
  • Data Retention: Implement appropriate log rotation and retention policies

Install with Tessl CLI

npx tessl i tessl/maven-app-cash-sqldelight--runtime-iossimulatorarm64

docs

column-adapters.md

database-driver.md

index.md

logging-debugging.md

query-execution.md

schema-management.md

transaction-management.md

tile.json