0
# Column Adapters
1
2
Type marshaling system for converting between Kotlin types and database-supported primitives, with built-in enum support and extensible custom type handling.
3
4
## Capabilities
5
6
### ColumnAdapter Interface
7
8
Core interface for marshaling custom types to and from database-supported types.
9
10
```kotlin { .api }
11
/**
12
* Marshal and map the type T to and from a database type S which is one of
13
* Long, Double, String, byte[]
14
* @param T The Kotlin type to marshal
15
* @param S The database-supported type (Long, Double, String, ByteArray)
16
*/
17
interface ColumnAdapter<T : Any, S> {
18
/**
19
* Decode a database value to the Kotlin type
20
* @param databaseValue Raw value from the database
21
* @return Decoded value as type T
22
*/
23
fun decode(databaseValue: S): T
24
25
/**
26
* Encode a Kotlin value to the database type
27
* @param value Value to encode for database storage
28
* @return Encoded value as database type S
29
*/
30
fun encode(value: T): S
31
}
32
```
33
34
### EnumColumnAdapter Class
35
36
Built-in column adapter for mapping enum classes to strings in the database.
37
38
```kotlin { .api }
39
/**
40
* A ColumnAdapter which maps the enum class T to a string in the database
41
* @param T The enum type to adapt
42
*/
43
class EnumColumnAdapter<T : Enum<T>> internal constructor(
44
private val enumValues: Array<out T>
45
) : ColumnAdapter<T, String> {
46
/**
47
* Decode a string database value to the enum instance
48
* @param databaseValue String representation of the enum name
49
* @return Enum instance matching the name
50
*/
51
override fun decode(databaseValue: String): T
52
53
/**
54
* Encode an enum instance to its string name
55
* @param value Enum instance to encode
56
* @return String name of the enum
57
*/
58
override fun encode(value: T): String
59
}
60
61
/**
62
* Factory function for creating EnumColumnAdapter instances
63
* @return EnumColumnAdapter instance for the reified enum type T
64
*/
65
inline fun <reified T : Enum<T>> EnumColumnAdapter(): EnumColumnAdapter<T>
66
```
67
68
**Usage Examples:**
69
70
```kotlin
71
import app.cash.sqldelight.ColumnAdapter
72
import app.cash.sqldelight.EnumColumnAdapter
73
import java.time.LocalDate
74
import java.time.LocalDateTime
75
import java.time.format.DateTimeFormatter
76
import java.util.UUID
77
78
// Basic enum adapter usage
79
enum class UserStatus {
80
ACTIVE, INACTIVE, SUSPENDED, DELETED
81
}
82
83
val statusAdapter = EnumColumnAdapter<UserStatus>()
84
85
// Using the adapter
86
val activeStatus = statusAdapter.decode("ACTIVE") // UserStatus.ACTIVE
87
val statusString = statusAdapter.encode(UserStatus.SUSPENDED) // "SUSPENDED"
88
89
// Custom date adapter
90
class LocalDateAdapter : ColumnAdapter<LocalDate, String> {
91
override fun decode(databaseValue: String): LocalDate {
92
return LocalDate.parse(databaseValue)
93
}
94
95
override fun encode(value: LocalDate): String {
96
return value.toString()
97
}
98
}
99
100
// Custom datetime adapter with formatting
101
class LocalDateTimeAdapter : ColumnAdapter<LocalDateTime, String> {
102
private val formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME
103
104
override fun decode(databaseValue: String): LocalDateTime {
105
return LocalDateTime.parse(databaseValue, formatter)
106
}
107
108
override fun encode(value: LocalDateTime): String {
109
return value.format(formatter)
110
}
111
}
112
113
// UUID adapter
114
class UuidAdapter : ColumnAdapter<UUID, String> {
115
override fun decode(databaseValue: String): UUID {
116
return UUID.fromString(databaseValue)
117
}
118
119
override fun encode(value: UUID): String {
120
return value.toString()
121
}
122
}
123
124
// JSON adapter for complex objects
125
class JsonAdapter<T>(
126
private val serializer: (T) -> String,
127
private val deserializer: (String) -> T
128
) : ColumnAdapter<T, String> {
129
override fun decode(databaseValue: String): T {
130
return deserializer(databaseValue)
131
}
132
133
override fun encode(value: T): String {
134
return serializer(value)
135
}
136
}
137
138
// Using JSON adapter with kotlinx.serialization
139
@Serializable
140
data class UserPreferences(
141
val theme: String,
142
val language: String,
143
val notifications: Boolean
144
)
145
146
val preferencesAdapter = JsonAdapter<UserPreferences>(
147
serializer = { Json.encodeToString(it) },
148
deserializer = { Json.decodeFromString(it) }
149
)
150
151
// Numeric type adapters
152
class BigDecimalAdapter : ColumnAdapter<BigDecimal, String> {
153
override fun decode(databaseValue: String): BigDecimal {
154
return BigDecimal(databaseValue)
155
}
156
157
override fun encode(value: BigDecimal): String {
158
return value.toString()
159
}
160
}
161
162
// Boolean to integer adapter
163
class BooleanIntAdapter : ColumnAdapter<Boolean, Long> {
164
override fun decode(databaseValue: Long): Boolean {
165
return databaseValue != 0L
166
}
167
168
override fun encode(value: Boolean): Long {
169
return if (value) 1L else 0L
170
}
171
}
172
173
// List adapter for comma-separated values
174
class StringListAdapter : ColumnAdapter<List<String>, String> {
175
override fun decode(databaseValue: String): List<String> {
176
return if (databaseValue.isEmpty()) {
177
emptyList()
178
} else {
179
databaseValue.split(",")
180
}
181
}
182
183
override fun encode(value: List<String>): String {
184
return value.joinToString(",")
185
}
186
}
187
188
// Using adapters in generated SQLDelight code
189
// In your .sq file:
190
// CREATE TABLE users (
191
// id INTEGER PRIMARY KEY,
192
// uuid TEXT NOT NULL,
193
// status TEXT NOT NULL,
194
// created_at TEXT NOT NULL,
195
// preferences TEXT NOT NULL,
196
// tags TEXT NOT NULL
197
// );
198
199
// In your database setup:
200
class UserDatabase(driver: SqlDriver) : Database(
201
driver = driver,
202
userAdapter = User.Adapter(
203
uuidAdapter = UuidAdapter(),
204
statusAdapter = EnumColumnAdapter<UserStatus>(),
205
created_atAdapter = LocalDateTimeAdapter(),
206
preferencesAdapter = preferencesAdapter,
207
tagsAdapter = StringListAdapter()
208
)
209
)
210
211
// Nullable adapters
212
class NullableLocalDateAdapter : ColumnAdapter<LocalDate?, String?> {
213
override fun decode(databaseValue: String?): LocalDate? {
214
return databaseValue?.let { LocalDate.parse(it) }
215
}
216
217
override fun encode(value: LocalDate?): String? {
218
return value?.toString()
219
}
220
}
221
222
// Binary data adapter
223
class ByteArrayAdapter : ColumnAdapter<ByteArray, ByteArray> {
224
override fun decode(databaseValue: ByteArray): ByteArray {
225
return databaseValue
226
}
227
228
override fun encode(value: ByteArray): ByteArray {
229
return value
230
}
231
}
232
233
// Encrypted string adapter
234
class EncryptedStringAdapter(
235
private val encryptor: (String) -> String,
236
private val decryptor: (String) -> String
237
) : ColumnAdapter<String, String> {
238
override fun decode(databaseValue: String): String {
239
return decryptor(databaseValue)
240
}
241
242
override fun encode(value: String): String {
243
return encryptor(value)
244
}
245
}
246
```
247
248
### Adapter Integration Patterns
249
250
Column adapters are typically used in three main contexts:
251
252
1. **Generated Database Classes**: SQLDelight generates adapter parameters for custom types
253
2. **Manual Query Construction**: When building queries manually with custom types
254
3. **Migration Scenarios**: Converting between different type representations during schema migrations
255
256
### Performance Considerations
257
258
- **Encoding/Decoding Cost**: Adapters add serialization overhead on every database operation
259
- **Caching**: Consider caching expensive conversions when the same values are frequently accessed
260
- **Bulk Operations**: For large datasets, consider batch processing optimizations
261
- **Memory Usage**: Be mindful of memory allocation in adapters, especially for collections and complex objects
262
263
### Error Handling
264
265
Column adapters should handle malformed data gracefully:
266
267
```kotlin
268
class SafeLocalDateAdapter : ColumnAdapter<LocalDate?, String?> {
269
override fun decode(databaseValue: String?): LocalDate? {
270
return try {
271
databaseValue?.let { LocalDate.parse(it) }
272
} catch (e: DateTimeParseException) {
273
null // or throw a more specific exception
274
}
275
}
276
277
override fun encode(value: LocalDate?): String? {
278
return value?.toString()
279
}
280
}
281
```