Skip to content

Commit fa65c32

Browse files
authored
Support for JSON columns in other databases (#25)
* JSON support for other JDBC databases * Fix up support for other DBs * Implement Json columns for R2dbc
1 parent 0e4f358 commit fa65c32

File tree

19 files changed

+622
-18
lines changed

19 files changed

+622
-18
lines changed

controller-jdbc/src/main/kotlin/io/exoquery/controller/jdbc/JdbcControllers.kt

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ object JdbcControllers {
3232
// Postgrees comes with its own default encoders, to exclude them need to override this property direct
3333
override val encodingConfig =
3434
encodingConfig.copy(
35-
additionalEncoders = encodingConfig.additionalEncoders + AdditionalPostgresEncoding.encoders,
36-
additionalDecoders = encodingConfig.additionalDecoders + AdditionalPostgresEncoding.decoders
35+
additionalEncoders = encodingConfig.additionalEncoders + JsonObjectEncoding.encoders,
36+
additionalDecoders = encodingConfig.additionalDecoders + JsonObjectEncoding.decoders
3737
)
3838

3939
// Postgres does not support Types.TIME_WITH_TIMEZONE as a JDBC type but does have a `TIME WITH TIMEZONE` datatype this is puzzling.
@@ -59,29 +59,35 @@ object JdbcControllers {
5959

6060
override val encodingConfig =
6161
encodingConfig.copy(
62-
additionalEncoders = encodingConfig.additionalEncoders + AdditionalPostgresEncoding.encoders,
63-
additionalDecoders = encodingConfig.additionalDecoders + AdditionalPostgresEncoding.decoders
62+
additionalEncoders = encodingConfig.additionalEncoders + JsonObjectEncoding.encoders,
63+
additionalDecoders = encodingConfig.additionalDecoders + JsonObjectEncoding.decoders
6464
)
6565

6666
companion object { }
6767
}
6868

6969
open class H2(
7070
override val database: DataSource,
71-
override val encodingConfig: JdbcEncodingConfig = JdbcEncodingConfig.Default
71+
encodingConfig: JdbcEncodingConfig = JdbcEncodingConfig.Default
7272
): JdbcController(database) {
7373
override val encodingApi: JdbcSqlEncoding =
7474
object: JavaSqlEncoding<Connection, PreparedStatement, ResultSet>,
7575
BasicEncoding<Connection, PreparedStatement, ResultSet> by JdbcBasicEncoding,
7676
JavaTimeEncoding<Connection, PreparedStatement, ResultSet> by JdbcTimeEncoding(),
7777
JavaUuidEncoding<Connection, PreparedStatement, ResultSet> by JdbcUuidObjectEncoding {}
7878

79+
override val encodingConfig =
80+
encodingConfig.copy(
81+
additionalEncoders = encodingConfig.additionalEncoders + JsonTextEncoding.encoders,
82+
additionalDecoders = encodingConfig.additionalDecoders + JsonTextEncoding.decoders
83+
)
84+
7985
companion object { }
8086
}
8187

8288
open class Mysql(
8389
override val database: DataSource,
84-
override val encodingConfig: JdbcEncodingConfig = JdbcEncodingConfig.Default
90+
encodingConfig: JdbcEncodingConfig = JdbcEncodingConfig.Default
8591
): JdbcController(database) {
8692
override val encodingApi: JdbcSqlEncoding =
8793
object : JavaSqlEncoding<Connection, PreparedStatement, ResultSet>,
@@ -95,19 +101,32 @@ object JdbcControllers {
95101
override val jdbcTypeOfOffsetTime = Types.TIME
96102
override val jdbcTypeOfOffsetDateTime = Types.TIMESTAMP
97103
}
104+
105+
override val encodingConfig =
106+
encodingConfig.copy(
107+
additionalEncoders = encodingConfig.additionalEncoders + JsonTextEncoding.encoders,
108+
additionalDecoders = encodingConfig.additionalDecoders + JsonTextEncoding.decoders
109+
)
110+
98111
companion object { }
99112
}
100113

101114
open class Sqlite(
102115
override val database: DataSource,
103-
override val encodingConfig: JdbcEncodingConfig = JdbcEncodingConfig.Default
116+
encodingConfig: JdbcEncodingConfig = JdbcEncodingConfig.Default
104117
): JdbcController(database) {
105118
override val encodingApi: JdbcSqlEncoding =
106119
object : JavaSqlEncoding<Connection, PreparedStatement, ResultSet>,
107120
BasicEncoding<Connection, PreparedStatement, ResultSet> by JdbcBasicEncoding,
108121
JavaTimeEncoding<Connection, PreparedStatement, ResultSet> by JdbcTimeEncodingLegacy,
109122
JavaUuidEncoding<Connection, PreparedStatement, ResultSet> by JdbcUuidStringEncoding {}
110123

124+
override val encodingConfig =
125+
encodingConfig.copy(
126+
additionalEncoders = encodingConfig.additionalEncoders + JsonTextEncoding.encoders,
127+
additionalDecoders = encodingConfig.additionalDecoders + JsonTextEncoding.decoders
128+
)
129+
111130
protected override open suspend fun <T> runActionReturningScoped(act: ControllerActionReturning<T>, options: JdbcExecutionOptions): Flow<T> =
112131
flowWithConnection(options) {
113132
val conn = localConnection()
@@ -159,14 +178,20 @@ object JdbcControllers {
159178

160179
open class Oracle(
161180
override val database: DataSource,
162-
override val encodingConfig: JdbcEncodingConfig = JdbcEncodingConfig.Default
181+
encodingConfig: JdbcEncodingConfig = JdbcEncodingConfig.Default
163182
): JdbcController(database) {
164183
override val encodingApi: JdbcSqlEncoding =
165184
object : JavaSqlEncoding<Connection, PreparedStatement, ResultSet>,
166185
BasicEncoding<Connection, PreparedStatement, ResultSet> by JdbcEncodingOracle,
167186
JavaTimeEncoding<Connection, PreparedStatement, ResultSet> by OracleTimeEncoding,
168187
JavaUuidEncoding<Connection, PreparedStatement, ResultSet> by JdbcUuidStringEncoding {}
169188

189+
override val encodingConfig =
190+
encodingConfig.copy(
191+
additionalEncoders = encodingConfig.additionalEncoders + JsonTextEncoding.encoders,
192+
additionalDecoders = encodingConfig.additionalDecoders + JsonTextEncoding.decoders
193+
)
194+
170195
object OracleTimeEncoding: JdbcTimeEncoding() {
171196
// Normally it is Types.TIME by in that case Oracle truncates the milliseconds
172197
override val jdbcTypeOfLocalTime = Types.TIMESTAMP
@@ -193,14 +218,20 @@ object JdbcControllers {
193218

194219
open class SqlServer(
195220
override val database: DataSource,
196-
override val encodingConfig: JdbcEncodingConfig = JdbcEncodingConfig.Default
221+
encodingConfig: JdbcEncodingConfig = JdbcEncodingConfig.Default
197222
): JdbcController(database) {
198223
override val encodingApi: JdbcSqlEncoding =
199224
object : JavaSqlEncoding<Connection, PreparedStatement, ResultSet>,
200225
BasicEncoding<Connection, PreparedStatement, ResultSet> by JdbcBasicEncoding,
201226
JavaTimeEncoding<Connection, PreparedStatement, ResultSet> by SqlServerTimeEncoding,
202227
JavaUuidEncoding<Connection, PreparedStatement, ResultSet> by JdbcUuidStringEncoding {}
203228

229+
override val encodingConfig =
230+
encodingConfig.copy(
231+
additionalEncoders = encodingConfig.additionalEncoders + JsonTextEncoding.encoders,
232+
additionalDecoders = encodingConfig.additionalDecoders + JsonTextEncoding.decoders
233+
)
234+
204235
object SqlServerTimeEncoding: JdbcTimeEncoding() {
205236
// Override java.util.Date encoding to use TIMESTAMP_WITH_TIMEZONE for DATETIMEOFFSET columns
206237
override val JDateEncoder: JdbcEncoderAny<java.util.Date> = JdbcEncoderAny(Types.TIMESTAMP_WITH_TIMEZONE, java.util.Date::class) { ctx, v, i ->

controller-jdbc/src/main/kotlin/io/exoquery/controller/jdbc/JdbcEncoders.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,22 @@ open class JdbcBasicEncoding:
6464
override val ByteArrayDecoder: JdbcDecoderAny<ByteArray> = JdbcDecoderAny(ByteArray::class) { ctx, i -> ctx.row.getBytes(i) }
6565
}
6666

67-
object AdditionalPostgresEncoding {
67+
object JsonObjectEncoding {
6868
val SqlJsonEncoder: JdbcEncoder<SqlJson> = JdbcEncoderAny(Types.OTHER, SqlJson::class) { ctx, v, i -> ctx.stmt.setObject(i, v.value, Types.OTHER) }
6969
val SqlJsonDecoder: JdbcDecoder<SqlJson> = JdbcDecoderAny(SqlJson::class) { ctx, i -> SqlJson(ctx.row.getString(i)) }
7070

7171
val encoders = setOf(SqlJsonEncoder)
7272
val decoders = setOf(SqlJsonDecoder)
7373
}
7474

75+
object JsonTextEncoding {
76+
val SqlJsonEncoder: JdbcEncoder<SqlJson> = JdbcEncoderAny(Types.VARCHAR, SqlJson::class) { ctx, v, i -> ctx.stmt.setObject(i, v.value, Types.VARCHAR) }
77+
val SqlJsonDecoder: JdbcDecoder<SqlJson> = JdbcDecoderAny(SqlJson::class) { ctx, i -> SqlJson(ctx.row.getString(i)) }
78+
79+
val encoders = setOf(SqlJsonEncoder)
80+
val decoders = setOf(SqlJsonDecoder)
81+
}
82+
7583
object AdditionalJdbcEncoding {
7684
val BigDecimalEncoder: JdbcEncoderAny<BigDecimal> = JdbcEncoderAny(Types.NUMERIC, BigDecimal::class) { ctx, v, i -> ctx.stmt.setBigDecimal(i, v) }
7785
val BigDecimalDecoder: JdbcDecoderAny<BigDecimal> = JdbcDecoderAny(BigDecimal::class) { ctx, i -> ctx.row.getBigDecimal(i) }

controller-r2dbc/src/main/kotlin/io/exoquery/controller/r2dbc/R2dbcControllers.kt

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@ import io.r2dbc.spi.Row
1111
import io.r2dbc.spi.Statement
1212

1313
object R2dbcControllers {
14-
class Postgres(
14+
open class Postgres(
1515
encodingConfig: R2dbcEncodingConfig = R2dbcEncodingConfig.Default(),
1616
override val connectionFactory: ConnectionFactory
1717
): R2dbcController(encodingConfig,connectionFactory) {
1818

1919
override val encodingConfig =
2020
encodingConfig.copy(
21-
additionalEncoders = encodingConfig.additionalEncoders + R2dbcPostgresAdditionalEncoding.encoders,
22-
additionalDecoders = encodingConfig.additionalDecoders + R2dbcPostgresAdditionalEncoding.decoders
21+
additionalEncoders = encodingConfig.additionalEncoders + R2dbcJsonObjectEncoding.encoders,
22+
additionalDecoders = encodingConfig.additionalDecoders + R2dbcJsonObjectEncoding.decoders
2323
)
2424

2525
override val encodingApi: R2dbcSqlEncoding =
@@ -32,11 +32,17 @@ object R2dbcControllers {
3232
changePlaceholdersIn(sql) { index -> "$${index + 1}" }
3333
}
3434

35-
class SqlServer(
35+
open class SqlServer(
3636
encodingConfig: R2dbcEncodingConfig = R2dbcEncodingConfig.Default(),
3737
override val connectionFactory: ConnectionFactory
3838
): R2dbcController(encodingConfig,connectionFactory) {
3939

40+
override val encodingConfig =
41+
encodingConfig.copy(
42+
additionalEncoders = encodingConfig.additionalEncoders + R2dbcJsonTextEncoding.encoders,
43+
additionalDecoders = encodingConfig.additionalDecoders + R2dbcJsonTextEncoding.decoders
44+
)
45+
4046
override val encodingApi: R2dbcSqlEncoding =
4147
object: JavaSqlEncoding<Connection, Statement, Row>,
4248
BasicEncoding<Connection, Statement, Row> by R2dbcBasicEncoding,
@@ -54,11 +60,17 @@ object R2dbcControllers {
5460
changePlaceholdersIn(sql) { index -> "@Param${index + startingStatementIndex.value}" }
5561
}
5662

57-
class Mysql(
63+
open class Mysql(
5864
encodingConfig: R2dbcEncodingConfig = R2dbcEncodingConfig.Default(),
5965
override val connectionFactory: ConnectionFactory
6066
): R2dbcController(encodingConfig, connectionFactory) {
6167

68+
override val encodingConfig =
69+
encodingConfig.copy(
70+
additionalEncoders = encodingConfig.additionalEncoders + R2dbcJsonTextEncoding.encoders,
71+
additionalDecoders = encodingConfig.additionalDecoders + R2dbcJsonTextEncoding.decoders
72+
)
73+
6274
override val encodingApi: R2dbcSqlEncoding =
6375
object: JavaSqlEncoding<Connection, Statement, Row>,
6476
BasicEncoding<Connection, Statement, Row> by R2dbcBasicEncoding,
@@ -69,11 +81,17 @@ object R2dbcControllers {
6981
override fun changePlaceholders(sql: String): String = sql
7082
}
7183

72-
class H2(
84+
open class H2(
7385
encodingConfig: R2dbcEncodingConfig = R2dbcEncodingConfig.Default(),
7486
override val connectionFactory: ConnectionFactory
7587
): R2dbcController(encodingConfig, connectionFactory) {
7688

89+
override val encodingConfig =
90+
encodingConfig.copy(
91+
additionalEncoders = encodingConfig.additionalEncoders + R2dbcJsonTextEncoding.encoders,
92+
additionalDecoders = encodingConfig.additionalDecoders + R2dbcJsonTextEncoding.decoders
93+
)
94+
7795
override val startingResultRowIndex: StartingIndex get() = StartingIndex.Zero
7896

7997
override val encodingApi: R2dbcSqlEncoding =
@@ -86,11 +104,17 @@ object R2dbcControllers {
86104
changePlaceholdersIn(sql) { index -> "$${index + 1}" }
87105
}
88106

89-
class Oracle(
107+
open class Oracle(
90108
encodingConfig: R2dbcEncodingConfig = R2dbcEncodingConfig.Default(),
91109
override val connectionFactory: ConnectionFactory
92110
): R2dbcController(encodingConfig, connectionFactory) {
93111

112+
override val encodingConfig =
113+
encodingConfig.copy(
114+
additionalEncoders = encodingConfig.additionalEncoders + R2dbcJsonTextEncoding.encoders,
115+
additionalDecoders = encodingConfig.additionalDecoders + R2dbcJsonTextEncoding.decoders
116+
)
117+
94118
override val encodingApi: R2dbcSqlEncoding =
95119
object: JavaSqlEncoding<Connection, Statement, Row>,
96120
BasicEncoding<Connection, Statement, Row> by R2dbcBasicEncodingOracle,

controller-r2dbc/src/main/kotlin/io/exoquery/controller/r2dbc/R2dbcPostgresAdditionalEncoding.kt renamed to controller-r2dbc/src/main/kotlin/io/exoquery/controller/r2dbc/R2dbcJsonObjectEncoding.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,20 @@ package io.exoquery.controller.r2dbc
22

33
import io.exoquery.controller.SqlJson
44

5-
object R2dbcPostgresAdditionalEncoding {
5+
object R2dbcJsonObjectEncoding {
66
private const val NA = 0
77
val SqlJsonEncoder: R2dbcEncoderAny<SqlJson> = R2dbcEncoderAny(NA, SqlJson::class) { ctx, v, i -> ctx.stmt.bind(i, io.r2dbc.postgresql.codec.Json.of(v.value)) }
88
val SqlJsonDecoder: R2dbcDecoderAny<SqlJson> = R2dbcDecoderAny(SqlJson::class) { ctx, i -> SqlJson(ctx.row.get(i, io.r2dbc.postgresql.codec.Json::class.java).asString()) }
99

1010
val encoders: Set<R2dbcEncoderAny<*>> = setOf(SqlJsonEncoder)
1111
val decoders: Set<R2dbcDecoderAny<*>> = setOf(SqlJsonDecoder)
1212
}
13+
14+
object R2dbcJsonTextEncoding {
15+
private const val NA = 0
16+
val SqlJsonEncoder: R2dbcEncoderAny<SqlJson> = R2dbcEncoderAny(NA, SqlJson::class) { ctx, v, i -> ctx.stmt.bind(i, v.value) }
17+
val SqlJsonDecoder: R2dbcDecoderAny<SqlJson> = R2dbcDecoderAny(SqlJson::class) { ctx, i -> SqlJson(ctx.row.get(i, String::class.java)) }
18+
19+
val encoders: Set<R2dbcEncoderAny<*>> = setOf(SqlJsonEncoder)
20+
val decoders: Set<R2dbcDecoderAny<*>> = setOf(SqlJsonDecoder)
21+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package io.exoquery.sql.h2
2+
3+
import io.exoquery.controller.JsonValue
4+
import io.exoquery.controller.SqlJsonValue
5+
import io.exoquery.sql.*
6+
import io.exoquery.sql.Sql
7+
import io.exoquery.controller.jdbc.JdbcControllers
8+
import io.exoquery.controller.runOn
9+
import io.kotest.core.spec.style.FreeSpec
10+
import io.kotest.matchers.shouldBe
11+
import kotlinx.serialization.Serializable
12+
13+
class JsonSpec: FreeSpec({
14+
val ds = TestDatabases.h2
15+
val ctx by lazy {
16+
JdbcControllers.H2(ds)
17+
}
18+
19+
beforeEach {
20+
ds.run("DELETE FROM JsonExample")
21+
}
22+
23+
"SqlJsonValue annotation works on" - {
24+
"inner data class" - {
25+
@SqlJsonValue
26+
@Serializable
27+
data class MyPerson(val name: String, val age: Int)
28+
29+
@Serializable
30+
data class Example(val id: Int, val value: MyPerson)
31+
32+
val je = Example(1, MyPerson("Alice", 30))
33+
34+
"should encode in json (with explicit serializer) and decode" {
35+
Sql("INSERT INTO JsonExample (id, \"value\") VALUES (1, ${Param.withSer(je.value, MyPerson.serializer())})").action().runOn(ctx)
36+
Sql("SELECT id, \"value\" FROM JsonExample").queryOf<Example>().runOn(ctx) shouldBe listOf(je)
37+
}
38+
}
39+
40+
"annotated field" {
41+
@Serializable
42+
data class MyPerson(val name: String, val age: Int)
43+
44+
@Serializable
45+
data class Example(val id: Int, @SqlJsonValue val value: MyPerson)
46+
47+
val je = Example(1, MyPerson("Joe", 123))
48+
Sql("""INSERT INTO JsonExample (id, "value") VALUES (1, '{"name":"Joe", "age":123}')""").action().runOn(ctx)
49+
val customers = Sql("SELECT id, \"value\" FROM JsonExample").queryOf<Example>().runOn(ctx)
50+
customers shouldBe listOf(je)
51+
}
52+
}
53+
54+
})
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package io.exoquery.sql.mysql
2+
3+
import io.exoquery.controller.JsonValue
4+
import io.exoquery.controller.SqlJsonValue
5+
import io.exoquery.sql.*
6+
import io.exoquery.sql.Sql
7+
import io.exoquery.controller.jdbc.JdbcControllers
8+
import io.exoquery.controller.runOn
9+
import io.kotest.core.spec.style.FreeSpec
10+
import io.kotest.matchers.shouldBe
11+
import kotlinx.serialization.Serializable
12+
13+
class JsonSpec: FreeSpec({
14+
val ds = TestDatabases.mysql
15+
val ctx by lazy {
16+
JdbcControllers.Mysql(ds)
17+
}
18+
19+
beforeEach {
20+
ds.run("DELETE FROM JsonExample")
21+
}
22+
23+
"SqlJsonValue annotation works on" - {
24+
"inner data class" - {
25+
@SqlJsonValue
26+
@Serializable
27+
data class MyPerson(val name: String, val age: Int)
28+
29+
@Serializable
30+
data class Example(val id: Int, val value: MyPerson)
31+
32+
val je = Example(1, MyPerson("Alice", 30))
33+
34+
"should encode in json (with explicit serializer) and decode" {
35+
Sql("INSERT INTO JsonExample (id, value) VALUES (1, ${Param.withSer(je.value, MyPerson.serializer())})").action().runOn(ctx)
36+
Sql("SELECT id, value FROM JsonExample").queryOf<Example>().runOn(ctx) shouldBe listOf(je)
37+
}
38+
}
39+
40+
"annotated field" {
41+
@Serializable
42+
data class MyPerson(val name: String, val age: Int)
43+
44+
@Serializable
45+
data class Example(val id: Int, @SqlJsonValue val value: MyPerson)
46+
47+
val je = Example(1, MyPerson("Joe", 123))
48+
Sql("""INSERT INTO JsonExample (id, value) VALUES (1, '{"name":"Joe", "age":123}')""").action().runOn(ctx)
49+
val customers = Sql("SELECT id, value FROM JsonExample").queryOf<Example>().runOn(ctx)
50+
customers shouldBe listOf(je)
51+
}
52+
}
53+
54+
})

0 commit comments

Comments
 (0)