SQLDelight multiplatform runtime library providing typesafe Kotlin APIs from SQL statements with compile-time schema verification
—
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.
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())
)
)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}")
}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()
)
)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)
)
)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