Skip to content

Commit 949a950

Browse files
committed
Implement Json columns for R2dbc
1 parent 278e27c commit 949a950

7 files changed

Lines changed: 278 additions & 8 deletions

File tree

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: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package io.exoquery.r2dbc.h2
2+
3+
import io.exoquery.controller.SqlJsonValue
4+
import io.exoquery.controller.TerpalSqlUnsafe
5+
import io.exoquery.controller.r2dbc.R2dbcController
6+
import io.exoquery.controller.r2dbc.R2dbcControllers
7+
import io.exoquery.controller.runActionsUnsafe
8+
import io.exoquery.controller.runOn
9+
import io.exoquery.r2dbc.TestDatabasesR2dbc
10+
import io.exoquery.sql.Param
11+
import io.exoquery.sql.Sql
12+
import io.kotest.core.spec.style.FreeSpec
13+
import io.kotest.matchers.shouldBe
14+
import kotlinx.serialization.Serializable
15+
16+
class JsonSpec: FreeSpec({
17+
val cf = TestDatabasesR2dbc.h2
18+
val ctx: R2dbcController by lazy { R2dbcControllers.H2(connectionFactory = cf) }
19+
20+
@OptIn(TerpalSqlUnsafe::class)
21+
suspend fun runActions(actions: String) = ctx.runActionsUnsafe(actions)
22+
23+
beforeEach {
24+
runActions("DELETE FROM JsonExample")
25+
}
26+
27+
"SqlJsonValue annotation works on" - {
28+
"inner data class" - {
29+
@SqlJsonValue
30+
@Serializable
31+
data class MyPerson(val name: String, val age: Int)
32+
33+
@Serializable
34+
data class Example(val id: Int, val value: MyPerson)
35+
36+
val je = Example(1, MyPerson("Alice", 30))
37+
38+
"should encode in json (with explicit serializer) and decode" {
39+
Sql("INSERT INTO JsonExample (id, \"value\") VALUES (1, ${Param.withSer(je.value, MyPerson.serializer())})").action().runOn(ctx)
40+
Sql("SELECT id, \"value\" FROM JsonExample").queryOf<Example>().runOn(ctx) shouldBe listOf(je)
41+
}
42+
}
43+
44+
"annotated field" {
45+
@Serializable
46+
data class MyPerson(val name: String, val age: Int)
47+
48+
@Serializable
49+
data class Example(val id: Int, @SqlJsonValue val value: MyPerson)
50+
51+
val je = Example(1, MyPerson("Joe", 123))
52+
Sql("""INSERT INTO JsonExample (id, "value") VALUES (1, '{"name":"Joe", "age":123}')""").action().runOn(ctx)
53+
val customers = Sql("SELECT id, \"value\" FROM JsonExample").queryOf<Example>().runOn(ctx)
54+
customers shouldBe listOf(je)
55+
}
56+
}
57+
58+
})
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package io.exoquery.r2dbc.mysql
2+
3+
import io.exoquery.controller.SqlJsonValue
4+
import io.exoquery.controller.TerpalSqlUnsafe
5+
import io.exoquery.controller.r2dbc.R2dbcController
6+
import io.exoquery.controller.r2dbc.R2dbcControllers
7+
import io.exoquery.controller.runActionsUnsafe
8+
import io.exoquery.controller.runOn
9+
import io.exoquery.r2dbc.TestDatabasesR2dbc
10+
import io.exoquery.sql.Param
11+
import io.exoquery.sql.Sql
12+
import io.kotest.core.spec.style.FreeSpec
13+
import io.kotest.matchers.shouldBe
14+
import kotlinx.serialization.Serializable
15+
16+
class JsonSpec: FreeSpec({
17+
val cf = TestDatabasesR2dbc.mysql
18+
val ctx: R2dbcController by lazy { R2dbcControllers.Mysql(connectionFactory = cf) }
19+
20+
@OptIn(TerpalSqlUnsafe::class)
21+
suspend fun runActions(actions: String) = ctx.runActionsUnsafe(actions)
22+
23+
beforeEach {
24+
runActions("DELETE FROM JsonExample")
25+
}
26+
27+
"SqlJsonValue annotation works on" - {
28+
"inner data class" - {
29+
@SqlJsonValue
30+
@Serializable
31+
data class MyPerson(val name: String, val age: Int)
32+
33+
@Serializable
34+
data class Example(val id: Int, val value: MyPerson)
35+
36+
val je = Example(1, MyPerson("Alice", 30))
37+
38+
"should encode in json (with explicit serializer) and decode" {
39+
Sql("INSERT INTO JsonExample (id, value) VALUES (1, ${Param.withSer(je.value, MyPerson.serializer())})").action().runOn(ctx)
40+
Sql("SELECT id, value FROM JsonExample").queryOf<Example>().runOn(ctx) shouldBe listOf(je)
41+
}
42+
}
43+
44+
"annotated field" {
45+
@Serializable
46+
data class MyPerson(val name: String, val age: Int)
47+
48+
@Serializable
49+
data class Example(val id: Int, @SqlJsonValue val value: MyPerson)
50+
51+
val je = Example(1, MyPerson("Joe", 123))
52+
Sql("""INSERT INTO JsonExample (id, value) VALUES (1, '{"name":"Joe", "age":123}')""").action().runOn(ctx)
53+
val customers = Sql("SELECT id, value FROM JsonExample").queryOf<Example>().runOn(ctx)
54+
customers shouldBe listOf(je)
55+
}
56+
}
57+
58+
})
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package io.exoquery.r2dbc.oracle
2+
3+
import io.exoquery.controller.SqlJsonValue
4+
import io.exoquery.controller.TerpalSqlUnsafe
5+
import io.exoquery.controller.r2dbc.R2dbcController
6+
import io.exoquery.controller.r2dbc.R2dbcControllers
7+
import io.exoquery.controller.runActionsUnsafe
8+
import io.exoquery.controller.runOn
9+
import io.exoquery.r2dbc.TestDatabasesR2dbc
10+
import io.exoquery.sql.Param
11+
import io.exoquery.sql.Sql
12+
import io.kotest.core.spec.style.FreeSpec
13+
import io.kotest.matchers.shouldBe
14+
import kotlinx.serialization.Serializable
15+
16+
class JsonSpec: FreeSpec({
17+
val cf = TestDatabasesR2dbc.oracle
18+
val ctx: R2dbcController by lazy { R2dbcControllers.Oracle(connectionFactory = cf) }
19+
20+
@OptIn(TerpalSqlUnsafe::class)
21+
suspend fun runActions(actions: String) = ctx.runActionsUnsafe(actions)
22+
23+
beforeEach {
24+
runActions("DELETE FROM JsonExample")
25+
}
26+
27+
"SqlJsonValue annotation works on" - {
28+
"inner data class" - {
29+
@SqlJsonValue
30+
@Serializable
31+
data class MyPerson(val name: String, val age: Int)
32+
33+
@Serializable
34+
data class Example(val id: Int, val value: MyPerson)
35+
36+
val je = Example(1, MyPerson("Alice", 30))
37+
38+
"should encode in json (with explicit serializer) and decode" {
39+
Sql("INSERT INTO JsonExample (id, value) VALUES (1, ${Param.withSer(je.value, MyPerson.serializer())})").action().runOn(ctx)
40+
Sql("SELECT id, value FROM JsonExample").queryOf<Example>().runOn(ctx) shouldBe listOf(je)
41+
}
42+
}
43+
44+
"annotated field" {
45+
@Serializable
46+
data class MyPerson(val name: String, val age: Int)
47+
48+
@Serializable
49+
data class Example(val id: Int, @SqlJsonValue val value: MyPerson)
50+
51+
val je = Example(1, MyPerson("Joe", 123))
52+
Sql("""INSERT INTO JsonExample (id, value) VALUES (1, '{"name":"Joe", "age":123}')""").action().runOn(ctx)
53+
val customers = Sql("SELECT id, value FROM JsonExample").queryOf<Example>().runOn(ctx)
54+
customers shouldBe listOf(je)
55+
}
56+
}
57+
58+
})
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package io.exoquery.r2dbc.sqlserver
2+
3+
import io.exoquery.controller.SqlJsonValue
4+
import io.exoquery.controller.TerpalSqlUnsafe
5+
import io.exoquery.controller.r2dbc.R2dbcController
6+
import io.exoquery.controller.r2dbc.R2dbcControllers
7+
import io.exoquery.controller.runActionsUnsafe
8+
import io.exoquery.controller.runOn
9+
import io.exoquery.r2dbc.TestDatabasesR2dbc
10+
import io.exoquery.sql.Param
11+
import io.exoquery.sql.Sql
12+
import io.kotest.core.spec.style.FreeSpec
13+
import io.kotest.matchers.shouldBe
14+
import kotlinx.serialization.Serializable
15+
16+
class JsonSpec: FreeSpec({
17+
val cf = TestDatabasesR2dbc.sqlServer
18+
val ctx: R2dbcController by lazy { R2dbcControllers.SqlServer(connectionFactory = cf) }
19+
20+
@OptIn(TerpalSqlUnsafe::class)
21+
suspend fun runActions(actions: String) = ctx.runActionsUnsafe(actions)
22+
23+
beforeEach {
24+
runActions("DELETE FROM JsonExample")
25+
}
26+
27+
"SqlJsonValue annotation works on" - {
28+
"inner data class" - {
29+
@SqlJsonValue
30+
@Serializable
31+
data class MyPerson(val name: String, val age: Int)
32+
33+
@Serializable
34+
data class Example(val id: Int, val value: MyPerson)
35+
36+
val je = Example(1, MyPerson("Alice", 30))
37+
38+
"should encode in json (with explicit serializer) and decode" {
39+
Sql("INSERT INTO JsonExample (id, \"value\") VALUES (1, ${Param.withSer(je.value, MyPerson.serializer())})").action().runOn(ctx)
40+
Sql("SELECT id, \"value\" FROM JsonExample").queryOf<Example>().runOn(ctx) shouldBe listOf(je)
41+
}
42+
}
43+
44+
"annotated field" {
45+
@Serializable
46+
data class MyPerson(val name: String, val age: Int)
47+
48+
@Serializable
49+
data class Example(val id: Int, @SqlJsonValue val value: MyPerson)
50+
51+
val je = Example(1, MyPerson("Joe", 123))
52+
Sql("""INSERT INTO JsonExample (id, "value") VALUES (1, '{"name":"Joe", "age":123}')""").action().runOn(ctx)
53+
val customers = Sql("SELECT id, \"value\" FROM JsonExample").queryOf<Example>().runOn(ctx)
54+
customers shouldBe listOf(je)
55+
}
56+
}
57+
58+
})

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,8 @@ CREATE TABLE IF NOT EXISTS JavaTestEntity(
7474
javaUtilDateOpt TIMESTAMP,
7575
uuidOpt UUID
7676
);
77+
78+
CREATE TABLE IF NOT EXISTS JsonExample(
79+
id IDENTITY,
80+
"value" VARCHAR(255)
81+
);

0 commit comments

Comments
 (0)