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

column-adapters.mddocs/

Column Type Adapters

SQLDelight's column adapter system provides bidirectional type conversion between Kotlin types and database column types. It enables custom serialization/deserialization and provides built-in adapters for common use cases like enums and complex types.

Capabilities

Column Adapter Interface

Core interface for bidirectional type conversion between Kotlin and database types.

/**
 * Marshal and map the type T to and from a database type S which is one of
 * Long, Double, String, ByteArray
 * @param T The Kotlin type to convert to/from
 * @param S The database type (Long, Double, String, or ByteArray)
 */
interface ColumnAdapter<T : Any, S> {
    /**
     * Convert database value to Kotlin type
     * @param databaseValue The value from the database column
     * @returns databaseValue decoded as type T
     */
    fun decode(databaseValue: S): T
    
    /**
     * Convert Kotlin type to database value  
     * @param value The Kotlin value to store
     * @returns value encoded as database type S
     */
    fun encode(value: T): S
}

Usage Examples:

import app.cash.sqldelight.ColumnAdapter

// Custom date adapter using Long for timestamp storage
class DateAdapter : ColumnAdapter<Date, Long> {
    override fun decode(databaseValue: Long): Date {
        return Date(databaseValue)
    }
    
    override fun encode(value: Date): Long {
        return value.time
    }
}

// JSON adapter for complex objects using String storage
class JsonAdapter<T>(
    private val serializer: KSerializer<T>
) : ColumnAdapter<T, String> {
    override fun decode(databaseValue: String): T {
        return Json.decodeFromString(serializer, databaseValue)
    }
    
    override fun encode(value: T): String {
        return Json.encodeToString(serializer, value)
    }
}

// UUID adapter using String storage
class UuidAdapter : ColumnAdapter<UUID, String> {
    override fun decode(databaseValue: String): UUID {
        return UUID.fromString(databaseValue)
    }
    
    override fun encode(value: UUID): String {
        return value.toString()
    }
}

// Usage in generated code context
val database = Database(
    driver = driver,
    userAdapter = User.Adapter(
        id = UuidAdapter(),
        createdAt = DateAdapter(),
        preferences = JsonAdapter(UserPreferences.serializer())
    )
)

Built-in Enum Column Adapter

Pre-built adapter for mapping enum classes to database strings using enum names.

/**
 * A ColumnAdapter which maps the enum class T to a string in the database
 * @param enumValues Array of all enum values for the type T
 */
class EnumColumnAdapter<T : Enum<T>>(
    private val enumValues: Array<out T>
) : ColumnAdapter<T, String> {
    /**
     * Convert database string to enum value by matching enum name
     * @param databaseValue The enum name string from database
     * @returns The enum value with matching name
     * @throws NoSuchElementException if no enum value matches the database string
     */
    override fun decode(databaseValue: String): T
    
    /**
     * Convert enum value to database string using enum name
     * @param value The enum value to convert
     * @returns The name property of the enum value
     */
    override fun encode(value: T): String
}

/**
 * Factory function to create EnumColumnAdapter for reified enum type
 * A ColumnAdapter which maps the enum class T to a string in the database
 * @returns EnumColumnAdapter instance for type T
 */
inline fun <reified T : Enum<T>> EnumColumnAdapter(): EnumColumnAdapter<T>

Usage Examples:

import app.cash.sqldelight.EnumColumnAdapter

// Define enum type
enum class UserStatus {
    ACTIVE, INACTIVE, SUSPENDED, DELETED
}

enum class Priority {
    LOW, MEDIUM, HIGH, CRITICAL
}

// Create adapters using factory function (preferred)
val statusAdapter = EnumColumnAdapter<UserStatus>()
val priorityAdapter = EnumColumnAdapter<Priority>()

// Create adapters manually
val manualStatusAdapter = EnumColumnAdapter(UserStatus.values())

// Usage in database schema
val database = Database(
    driver = driver,
    userAdapter = User.Adapter(
        status = statusAdapter
    ),
    taskAdapter = Task.Adapter(
        priority = priorityAdapter
    )
)

// The adapter handles conversion automatically
val activeUsers = userQueries.selectByStatus(UserStatus.ACTIVE).executeAsList()
// Database query: SELECT * FROM users WHERE status = 'ACTIVE'

userQueries.updateStatus(userId = 1, status = UserStatus.SUSPENDED)
// Database query: UPDATE users SET status = 'SUSPENDED' WHERE id = 1

// Error handling for invalid database values
try {
    val invalidUser = userQueries.selectById(invalidId).executeAsOne()
} catch (e: NoSuchElementException) {
    // Thrown if database contains enum name not in current enum definition
    println("Invalid enum value in database: ${e.message}")
}

Complex Type Adapters

Examples of adapters for handling complex data types and serialization scenarios.

Usage Examples:

import app.cash.sqldelight.ColumnAdapter
import kotlinx.serialization.*
import kotlinx.serialization.json.*

// List adapter using JSON serialization
class ListAdapter<T>(
    private val elementSerializer: KSerializer<T>
) : ColumnAdapter<List<T>, String> {
    override fun decode(databaseValue: String): List<T> {
        return Json.decodeFromString(ListSerializer(elementSerializer), databaseValue)
    }
    
    override fun encode(value: List<T>): String {
        return Json.encodeToString(ListSerializer(elementSerializer), value)
    }
}

// Map adapter for key-value storage
class MapAdapter<K, V>(
    private val keySerializer: KSerializer<K>,
    private val valueSerializer: KSerializer<V>
) : ColumnAdapter<Map<K, V>, String> {
    override fun decode(databaseValue: String): Map<K, V> {
        return Json.decodeFromString(
            MapSerializer(keySerializer, valueSerializer), 
            databaseValue
        )
    }
    
    override fun encode(value: Map<K, V>): String {
        return Json.encodeToString(
            MapSerializer(keySerializer, valueSerializer), 
            value
        )
    }
}

// BigDecimal adapter using String for precision
class BigDecimalAdapter : ColumnAdapter<BigDecimal, String> {
    override fun decode(databaseValue: String): BigDecimal {
        return BigDecimal(databaseValue)
    }
    
    override fun encode(value: BigDecimal): String {
        return value.toPlainString()
    }
}

// Instant adapter using Long for epoch milliseconds
class InstantAdapter : ColumnAdapter<Instant, Long> {
    override fun decode(databaseValue: Long): Instant {
        return Instant.fromEpochMilliseconds(databaseValue)
    }
    
    override fun encode(value: Instant): Long {
        return value.toEpochMilliseconds()
    }
}

// Usage with complex types
@Serializable
data class UserPreferences(
    val theme: String,
    val notifications: Boolean,
    val language: String
)

@Serializable
data class ContactInfo(
    val email: String,
    val phone: String?,
    val socialLinks: Map<String, String>
)

val database = Database(
    driver = driver,
    userAdapter = User.Adapter(
        tags = ListAdapter(String.serializer()),
        preferences = JsonAdapter(UserPreferences.serializer()),
        contactInfo = JsonAdapter(ContactInfo.serializer()),
        metadata = MapAdapter(String.serializer(), String.serializer()),
        balance = BigDecimalAdapter(),
        lastSeen = InstantAdapter()
    )
)

Error Handling in Adapters

Implement robust error handling for data conversion failures.

Usage Examples:

import app.cash.sqldelight.ColumnAdapter

// Adapter with comprehensive error handling
class SafeJsonAdapter<T>(
    private val serializer: KSerializer<T>,
    private val defaultValue: T
) : ColumnAdapter<T, String> {
    override fun decode(databaseValue: String): T {
        return try {
            Json.decodeFromString(serializer, databaseValue)
        } catch (e: SerializationException) {
            println("Failed to decode JSON: $databaseValue, using default: $defaultValue")
            defaultValue
        } catch (e: IllegalArgumentException) {
            println("Invalid JSON format: $databaseValue, using default: $defaultValue")
            defaultValue
        }
    }
    
    override fun encode(value: T): String {
        return try {
            Json.encodeToString(serializer, value)
        } catch (e: SerializationException) {
            println("Failed to encode value: $value, using default JSON")
            Json.encodeToString(serializer, defaultValue)
        }
    }
}

// Enum adapter with fallback for unknown values
class SafeEnumAdapter<T : Enum<T>>(
    private val enumValues: Array<out T>,
    private val defaultValue: T
) : ColumnAdapter<T, String> {
    override fun decode(databaseValue: String): T {
        return enumValues.firstOrNull { it.name == databaseValue } 
            ?: run {
                println("Unknown enum value: $databaseValue, using default: $defaultValue")
                defaultValue
            }
    }
    
    override fun encode(value: T): String {
        return value.name
    }
}

// Numeric adapter with validation
class ValidatedIntAdapter(
    private val min: Int = Int.MIN_VALUE,
    private val max: Int = Int.MAX_VALUE
) : ColumnAdapter<Int, Long> {
    override fun decode(databaseValue: Long): Int {
        val intValue = databaseValue.toInt()
        return when {
            intValue < min -> {
                println("Value $intValue below minimum $min, clamping")
                min
            }
            intValue > max -> {
                println("Value $intValue above maximum $max, clamping")
                max
            }
            else -> intValue
        }
    }
    
    override fun encode(value: Int): Long {
        val clampedValue = value.coerceIn(min, max)
        if (clampedValue != value) {
            println("Value $value outside range [$min, $max], clamped to $clampedValue")
        }
        return clampedValue.toLong()
    }
}

// Usage with error handling
val database = Database(
    driver = driver,
    userAdapter = User.Adapter(
        status = SafeEnumAdapter(UserStatus.values(), UserStatus.ACTIVE),
        preferences = SafeJsonAdapter(
            UserPreferences.serializer(), 
            UserPreferences(theme = "default", notifications = true, language = "en")
        ),
        score = ValidatedIntAdapter(min = 0, max = 100)
    )
)

Migration and Schema Evolution

Handle schema changes and data migration with column adapters.

Usage Examples:

import app.cash.sqldelight.ColumnAdapter

// Versioned adapter for handling schema evolution
class VersionedUserPreferencesAdapter : ColumnAdapter<UserPreferences, String> {
    override fun decode(databaseValue: String): UserPreferences {
        return try {
            // Try current version first
            Json.decodeFromString<UserPreferences>(databaseValue)
        } catch (e: SerializationException) {
            try {
                // Fallback to previous version
                val oldPrefs = Json.decodeFromString<OldUserPreferences>(databaseValue)
                migrateFromOldVersion(oldPrefs)
            } catch (e: SerializationException) {
                // Final fallback to defaults
                UserPreferences.default()
            }
        }
    }
    
    override fun encode(value: UserPreferences): String {
        return Json.encodeToString(value)
    }
    
    private fun migrateFromOldVersion(old: OldUserPreferences): UserPreferences {
        return UserPreferences(
            theme = old.theme,
            notifications = old.notifications,
            language = old.language ?: "en", // New field with default
            // New fields get default values
            darkMode = false,
            fontSize = FontSize.MEDIUM
        )
    }
}

// Backward-compatible enum adapter
class BackwardCompatibleStatusAdapter : ColumnAdapter<UserStatus, String> {
    override fun decode(databaseValue: String): UserStatus {
        return when (databaseValue) {
            "ACTIVE" -> UserStatus.ACTIVE
            "INACTIVE" -> UserStatus.INACTIVE
            "SUSPENDED" -> UserStatus.SUSPENDED
            "DELETED" -> UserStatus.DELETED
            // Handle old enum values that no longer exist
            "BANNED" -> UserStatus.SUSPENDED // Map old value to new equivalent
            "PENDING" -> UserStatus.INACTIVE // Map old value to new equivalent
            else -> {
                println("Unknown status: $databaseValue, defaulting to INACTIVE")
                UserStatus.INACTIVE
            }
        }
    }
    
    override fun encode(value: UserStatus): String {
        return value.name
    }
}

// Usage during database migration
val legacyDatabase = Database(
    driver = driver,
    userAdapter = User.Adapter(
        status = BackwardCompatibleStatusAdapter(),
        preferences = VersionedUserPreferencesAdapter()
    )
)

// Migration script example
fun migrateUserData() {
    val users = legacyDatabase.userQueries.selectAll().executeAsList()
    users.forEach { user ->
        // Adapters handle the conversion automatically
        val updatedUser = user.copy(
            // Any necessary transformations
        )
        legacyDatabase.userQueries.update(updatedUser)
    }
}

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