Skip to content

Commit ec0cac1

Browse files
committed
JSON support for other JDBC databases
1 parent 0e4f358 commit ec0cac1

File tree

8 files changed

+131
-10
lines changed

8 files changed

+131
-10
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 + JsonObjectEncoding.encoders,
108+
additionalDecoders = encodingConfig.additionalDecoders + JsonObjectEncoding.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) }
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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.postgres
15+
val ctx by lazy {
16+
JdbcControllers.Postgres(ds)
17+
}
18+
19+
beforeEach {
20+
ds.run("DELETE FROM JsonbExample")
21+
ds.run("DELETE FROM JsonbExample2")
22+
ds.run("DELETE FROM JsonbExample3")
23+
ds.run("DELETE FROM JsonExample")
24+
}
25+
26+
"SqlJsonValue annotation works on" - {
27+
"inner data class" - {
28+
@SqlJsonValue
29+
@Serializable
30+
data class MyPerson(val name: String, val age: Int)
31+
32+
@Serializable
33+
data class Example(val id: Int, val value: MyPerson)
34+
35+
val je = Example(1, MyPerson("Alice", 30))
36+
37+
"should encode in json (with explicit serializer) and decode" {
38+
Sql("INSERT INTO JsonExample (id, value) VALUES (1, ${Param.withSer(je.value, MyPerson.serializer())})").action().runOn(ctx)
39+
Sql("SELECT id, value FROM JsonExample").queryOf<Example>().runOn(ctx) shouldBe listOf(je)
40+
}
41+
}
42+
43+
"annotated field" {
44+
@Serializable
45+
data class MyPerson(val name: String, val age: Int)
46+
47+
@Serializable
48+
data class Example(val id: Int, @SqlJsonValue val value: MyPerson)
49+
50+
val je = Example(1, MyPerson("Joe", 123))
51+
Sql("""INSERT INTO JsonbExample (id, value) VALUES (1, '{"name":"Joe", "age":123}')""").action().runOn(ctx)
52+
val customers = Sql("SELECT id, value FROM JsonbExample").queryOf<Example>().runOn(ctx)
53+
customers shouldBe listOf(je)
54+
}
55+
}
56+
57+
})

terpal-sql-jdbc/src/test/resources/db/h2-schema.sql

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,8 @@ CREATE TABLE IF NOT EXISTS JavaTestEntity(
8181
javaUtilDateOpt TIMESTAMP,
8282
uuidOpt UUID
8383
);
84+
85+
CREATE TABLE IF NOT EXISTS JsonExample(
86+
id IDENTITY,
87+
"value" VARCHAR(255)
88+
);

terpal-sql-jdbc/src/test/resources/db/mysql-schema.sql

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,8 @@ CREATE TABLE JavaTestEntity(
8585
javaUtilDateOpt DATETIME,
8686
uuidOpt VARCHAR(255)
8787
);
88+
89+
CREATE TABLE JsonExample(
90+
id INT PRIMARY KEY,
91+
value JSON
92+
);

terpal-sql-jdbc/src/test/resources/db/oracle-schema.sql

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,8 @@ CREATE TABLE JavaTestEntity(
8080
javaUtilDateOpt TIMESTAMP,
8181
uuidOpt VARCHAR(36)
8282
);
83+
84+
CREATE TABLE JsonExample(
85+
id INT PRIMARY KEY,
86+
value VARCHAR(255)
87+
);

terpal-sql-jdbc/src/test/resources/db/sqlite-schema.sql

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,8 @@ CREATE TABLE IF NOT EXISTS JavaTestEntity(
8181
javaUtilDateOpt BIGINT,
8282
uuidOpt VARCHAR(36)
8383
);
84+
85+
CREATE TABLE JsonExample(
86+
id INT PRIMARY KEY,
87+
value VARCHAR
88+
);

terpal-sql-jdbc/src/test/resources/db/sqlserver-schema.sql

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,8 @@ CREATE TABLE JavaTestEntity(
8181
javaUtilDateOpt DATETIMEOFFSET, -- java.util.Date i.e. legacy date with time zone
8282
uuidOpt VARCHAR(255)
8383
);
84+
85+
CREATE TABLE JsonExample(
86+
id INT PRIMARY KEY,
87+
value VARCHAR
88+
);

0 commit comments

Comments
 (0)