Skip to content

Commit 18b2106

Browse files
authored
json array implicits for doobie (#132)
* json array implicits for doobie * jsonOrJsonbArrayGet * jsonOrJsonbArrayGet * make default implicits available via the same package
1 parent 17ab40d commit 18b2106

File tree

8 files changed

+278
-4
lines changed

8 files changed

+278
-4
lines changed

.github/workflows/jacoco_report.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ jobs:
6969
token: ${{ secrets.GITHUB_TOKEN }}
7070
min-coverage-overall: ${{ env.coverage-overall }}
7171
min-coverage-changed-files: ${{ env.coverage-changed-files }}
72-
title: JaCoCo `model` module code coverage report - scala ${{ env.scalaLong }}
72+
title: JaCoCo `core` module code coverage report - scala ${{ env.scalaLong }}
7373
update-comment: true
7474
- name: Add coverage to PR (doobie)
7575
if: steps.jacocorun.outcome == 'success'
@@ -80,7 +80,7 @@ jobs:
8080
token: ${{ secrets.GITHUB_TOKEN }}
8181
min-coverage-overall: ${{ env.coverage-overall }}
8282
min-coverage-changed-files: ${{ env.coverage-changed-files }}
83-
title: JaCoCo `agent` module code coverage report - scala ${{ env.scalaLong }}
83+
title: JaCoCo `doobie` module code coverage report - scala ${{ env.scalaLong }}
8484
update-comment: true
8585
- name: Add coverage to PR (slick)
8686
if: steps.jacocorun.outcome == 'success'
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright 2022 ABSA Group Limited
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
CREATE TABLE IF NOT EXISTS integration.actors_json_seq (
18+
id SERIAL PRIMARY KEY,
19+
actors_json JSON[],
20+
actors_jsonb JSONB[]
21+
);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright 2022 ABSA Group Limited
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
CREATE OR REPLACE FUNCTION integration.insert_actors_json(actorsJson JSON[], actorsJsonb JSONB[])
18+
RETURNS void AS $$
19+
BEGIN
20+
INSERT INTO integration.actors_json_seq (actors_json, actors_jsonb)
21+
VALUES (actorsJson, actorsJsonb);
22+
END;
23+
$$ LANGUAGE plpgsql;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright 2022 ABSA Group Limited
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
CREATE OR REPLACE FUNCTION integration.retrieve_actors_json(idUntil INT)
18+
RETURNS TABLE(actors_json JSON[]) AS $$
19+
BEGIN
20+
RETURN QUERY SELECT a.actors_json FROM integration.actors_json_seq AS a WHERE id <= idUntil;
21+
END;
22+
$$ LANGUAGE plpgsql;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright 2022 ABSA Group Limited
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
CREATE OR REPLACE FUNCTION integration.retrieve_actors_jsonb(idUntil INT)
18+
RETURNS TABLE(actors_jsonb JSONB[]) AS $$
19+
BEGIN
20+
RETURN QUERY SELECT a.actors_jsonb FROM integration.actors_json_seq AS a WHERE id <= idUntil;
21+
END;
22+
$$ LANGUAGE plpgsql;
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Copyright 2022 ABSA Group Limited
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package za.co.absa.db.fadb.doobie.postgres.circe
18+
19+
import cats.Show
20+
import cats.data.NonEmptyList
21+
import doobie.{Get, Put}
22+
import io.circe.Json
23+
import org.postgresql.jdbc.PgArray
24+
import org.postgresql.util.PGobject
25+
import io.circe.parser._
26+
27+
import scala.util.{Failure, Success, Try}
28+
29+
package object implicits {
30+
31+
private implicit val showPgArray: Show[PgArray] = Show.fromToString
32+
33+
implicit val jsonPut: Put[Json] = doobie.postgres.circe.json.implicits.jsonPut
34+
implicit val jsonbPut: Put[Json] = doobie.postgres.circe.jsonb.implicits.jsonbPut
35+
36+
implicit val jsonGet: Get[Json] = doobie.postgres.circe.json.implicits.jsonGet
37+
implicit val jsonbGet: Get[Json] = doobie.postgres.circe.jsonb.implicits.jsonbGet
38+
39+
implicit val jsonArrayPut: Put[List[Json]] = {
40+
Put.Advanced
41+
.other[PGobject](
42+
NonEmptyList.of("json[]")
43+
)
44+
.tcontramap { a =>
45+
val o = new PGobject
46+
o.setType("json[]")
47+
o.setValue(jsonListToPGJsonArrayString(a))
48+
o
49+
}
50+
}
51+
52+
implicit val jsonbArrayPut: Put[List[Json]] = {
53+
Put.Advanced
54+
.other[PGobject](
55+
NonEmptyList.of("jsonb[]")
56+
)
57+
.tcontramap { a =>
58+
val o = new PGobject
59+
o.setType("jsonb[]")
60+
o.setValue(jsonListToPGJsonArrayString(a))
61+
o
62+
}
63+
}
64+
65+
// to be used for both json[] and jsonb[] as it handles well both
66+
// and we want to avoid collision when resolving implicits
67+
implicit val jsonOrJsonbArrayGet: Get[List[Json]] = {
68+
Get.Advanced
69+
.other[PgArray](
70+
NonEmptyList.of("json[]")
71+
)
72+
.temap(pgArray => pgArrayToListOfJson(pgArray))
73+
}
74+
75+
private def jsonListToPGJsonArrayString(jsonList: List[Json]): String = {
76+
val arrayElements = jsonList.map { x =>
77+
// Convert to compact JSON string and escape inner quotes
78+
val escapedJsonString = x.noSpaces.replace("\"", "\\\"")
79+
// Wrap in double quotes for the array element
80+
s""""$escapedJsonString""""
81+
}
82+
83+
arrayElements.mkString("{", ",", "}")
84+
}
85+
86+
private def pgArrayToListOfJson(pgArray: PgArray): Either[String, List[Json]] = {
87+
Try(Option(pgArray.getArray)) match {
88+
case Success(Some(array: Array[_])) =>
89+
val results = array.toList.map {
90+
case str: String => parse(str).left.map(_.getMessage)
91+
case other => parse(other.toString).left.map(_.getMessage)
92+
}
93+
results.partition(_.isLeft) match {
94+
case (Nil, rights) => Right(rights.collect { case Right(json) => json })
95+
case (lefts, _) => Left("Failed to parse JSON: " + lefts.collect { case Left(err) => err }.mkString(", "))
96+
}
97+
case Success(Some(_)) => Left("Unexpected type encountered. Expected an Array.")
98+
case Success(None) => Right(Nil)
99+
case Failure(exception) => Left(exception.getMessage)
100+
}
101+
}
102+
103+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Copyright 2022 ABSA Group Limited
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package za.co.absa.db.fadb.doobie
18+
19+
import cats.effect.IO
20+
import cats.effect.unsafe.implicits.global
21+
import doobie.implicits.toSqlInterpolator
22+
import io.circe.Json
23+
import io.circe.syntax.EncoderOps
24+
import org.scalatest.funsuite.AnyFunSuite
25+
import za.co.absa.db.fadb.DBSchema
26+
import za.co.absa.db.fadb.doobie.DoobieFunction.{DoobieMultipleResultFunction, DoobieSingleResultFunction}
27+
import za.co.absa.db.fadb.testing.classes.DoobieTest
28+
import io.circe.generic.auto._
29+
30+
import za.co.absa.db.fadb.doobie.postgres.circe.implicits.jsonOrJsonbArrayGet
31+
32+
class JsonArrayIntegrationTests extends AnyFunSuite with DoobieTest {
33+
34+
class InsertActorsJson(implicit schema: DBSchema, dbEngine: DoobieEngine[IO])
35+
extends DoobieSingleResultFunction[List[Actor], Unit, IO] (
36+
values => {
37+
val actorsAsJsonList = values.map(_.asJson)
38+
Seq(
39+
{
40+
// has to be imported inside separate scope to avoid conflicts with the import below
41+
// as both implicits are of the same type and this would cause ambiguity
42+
import za.co.absa.db.fadb.doobie.postgres.circe.implicits.jsonArrayPut
43+
fr"$actorsAsJsonList"
44+
},
45+
{
46+
// has to be imported inside separate scope to avoid conflicts with the import above
47+
// as both implicits are of the same type and this would cause ambiguity
48+
import za.co.absa.db.fadb.doobie.postgres.circe.implicits.jsonbArrayPut
49+
fr"$actorsAsJsonList"
50+
}
51+
)
52+
}
53+
)
54+
55+
class RetrieveActorsJson(implicit schema: DBSchema, dbEngine: DoobieEngine[IO])
56+
extends DoobieMultipleResultFunction[Int, List[Json], IO] (
57+
values => Seq(fr"$values")
58+
)
59+
60+
class RetrieveActorsJsonb(implicit schema: DBSchema, dbEngine: DoobieEngine[IO])
61+
extends DoobieMultipleResultFunction[Int, List[Json], IO] (
62+
values => Seq(fr"$values")
63+
)
64+
65+
private val insertActorsJson = new InsertActorsJson()(Integration, new DoobieEngine(transactor))
66+
67+
test("Retrieve Actors from json[] and jsonb[] columns"){
68+
val expectedActors = List(Actor(1, "John", "Doe"), Actor(2, "Jane", "Doe"))
69+
insertActorsJson(expectedActors).unsafeRunSync()
70+
71+
val retrieveActorsJson = new RetrieveActorsJson()(Integration, new DoobieEngine(transactor))
72+
val actualActorsJson = retrieveActorsJson(2).unsafeRunSync()
73+
assert(expectedActors == actualActorsJson.head.map(_.as[Actor]).map(_.toTry.get))
74+
75+
val retrieveActorsJsonb = new RetrieveActorsJsonb()(Integration, new DoobieEngine(transactor))
76+
val actualActorsJsonb = retrieveActorsJsonb(2).unsafeRunSync()
77+
assert(expectedActors == actualActorsJsonb.head.map(_.as[Actor]).map(_.toTry.get))
78+
}
79+
80+
}

project/Dependencies.scala

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import sbt._
17+
import sbt.*
1818

1919
object Dependencies {
2020

@@ -47,7 +47,9 @@ object Dependencies {
4747
commonDependencies(scalaVersion) ++ Seq(
4848
"org.tpolecat" %% "doobie-core" % "1.0.0-RC2",
4949
"org.tpolecat" %% "doobie-hikari" % "1.0.0-RC2",
50-
"org.tpolecat" %% "doobie-postgres" % "1.0.0-RC2"
50+
"org.tpolecat" %% "doobie-postgres" % "1.0.0-RC2",
51+
"org.tpolecat" %% "doobie-postgres-circe" % "1.0.0-RC2",
52+
"io.circe" %% "circe-generic" % "0.14.9" % Test
5153
)
5254
}
5355

@@ -56,4 +58,5 @@ object Dependencies {
5658

5759
Seq(postgresql)
5860
}
61+
5962
}

0 commit comments

Comments
 (0)