Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for JSON in H2 using circe #1599

Merged
merged 1 commit into from
Jan 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ lazy val doobie = project.in(file("."))
example,
free,
h2,
`h2-circe`,
hikari,
postgres,
`postgres-circe`,
Expand Down Expand Up @@ -344,6 +345,21 @@ lazy val h2 = project
scalacOptions -= "-Xfatal-warnings" // we need to do deprecated things
)

lazy val `h2-circe` = project
.in(file("modules/h2-circe"))
.enablePlugins(AutomateHeaderPlugin)
.dependsOn(core, h2)
.settings(doobieSettings)
.settings(publishSettings)
.settings(
name := "doobie-h2-circe",
description := "h2 circe support for doobie.",
libraryDependencies ++= Seq(
"io.circe" %% "circe-core" % circeVersion,
"io.circe" %% "circe-parser" % circeVersion
)
)

lazy val hikari = project
.in(file("modules/hikari"))
.enablePlugins(AutomateHeaderPlugin)
Expand Down
20 changes: 20 additions & 0 deletions modules/docs/src/main/mdoc/docs/16-Extensions-H2.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,26 @@ libraryDependencies += "org.tpolecat" %% "doobie-h2" % "$version$"

This library pulls in H2 as a transitive dependency.

There are extensions available for dealing with JSON by using Circe, if you like to use those, include this dependency:

@@@ vars

```scala
libraryDependencies += "org.tpolecat" %% "doobie-h2-circe" % "$version$"
```

@@@

Then, you will be able to import the implicits for dealing with JSON:

@@@ vars

```scala
import doobie.h2.circe.json.implicits
Copy link
Collaborator

@jatcwang jatcwang Dec 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean

Suggested change
import doobie.h2.circe.json.implicits
import doobie.h2.circe.json.implicits._

?

Also wondering whether we should mention that instances need to be created explicitly with h2DecoderGetT h2EncoderGetT?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest I was thinking about adding ._, but I haven't saw that in PostgreSQL implementation that I based my code on.

Regarding h2DecoderGetT and h2EncoderGetT, from what I understand they're used mostly for testing.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding h2DecoderGetT and h2EncoderGetT, from what I understand they're used mostly for testing.

They are used for creating instances of Put[A]/Get[A] provided that you have Encoder[A]/Decoder[A]. This isn't automatically derived (i.e. not implicit def) because that can often lead to the wrong behaviour. (Just because you have an Encoder[A] does not mean you want to write A as a JSON column in the database).

Anyway I can do the doc improvements in another PR. Thanks for your contribution :)

```

@@@

### Array Types

**doobie** supports H2 arrays of the following types:
Expand Down
55 changes: 55 additions & 0 deletions modules/h2-circe/src/main/scala/doobie/h2/circe/Instances.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright (c) 2013-2020 Rob Norris and Contributors
// This software is licensed under the MIT License (MIT).
// For more information see LICENSE or https://opensource.org/licenses/MIT

package doobie.h2.circe

import cats.Show
import cats.data.NonEmptyList
import cats.syntax.all._
import doobie.enumerated.JdbcType
import io.circe._
import io.circe.jawn._
import io.circe.syntax._
import doobie.util._

import java.nio.charset.StandardCharsets.UTF_8

object Instances {

private implicit val byteArrayShow: Show[Array[Byte]] = Show.show(new String(_, UTF_8))

trait JsonInstances {
implicit val jsonPut: Put[Json] =
Put.Advanced.one[Array[Byte]](
JdbcType.VarChar,
NonEmptyList.of("JSON"),
(ps, n, a) => ps.setObject(n, a),
(rs, n, a) => rs.updateObject(n, a)
)
.tcontramap { a =>
a.noSpaces.getBytes(UTF_8)
}

implicit val jsonGet: Get[Json] =
Get.Advanced.other[Array[Byte]](
NonEmptyList.of("JSON")
).temap(a =>
parse(a.show).leftMap(_.show)
)

def h2EncoderPutT[A: Encoder]: Put[A] =
Put[Json].tcontramap(_.asJson)

def h2EncoderPut[A: Encoder]: Put[A] =
Put[Json].contramap(_.asJson)

def h2DecoderGetT[A: Decoder]: Get[A] =
Get[Json].temap(json => json.as[A].leftMap(_.show))

@SuppressWarnings(Array("org.wartremover.warts.Throw"))
def h2DecoderGet[A: Decoder]: Get[A] =
Get[Json].map(json => json.as[A].fold(throw _, identity))
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright (c) 2013-2020 Rob Norris and Contributors
// This software is licensed under the MIT License (MIT).
// For more information see LICENSE or https://opensource.org/licenses/MIT

package doobie.h2.circe

package object json {
object implicits extends doobie.h2.circe.Instances.JsonInstances
}
81 changes: 81 additions & 0 deletions modules/h2-circe/src/test/scala/doobie/h2/circe/H2JsonSuite.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright (c) 2013-2020 Rob Norris and Contributors
// This software is licensed under the MIT License (MIT).
// For more information see LICENSE or https://opensource.org/licenses/MIT

package doobie.h2.circe

import cats.effect.IO
import doobie._
import doobie.implicits._
import io.circe.{Decoder, Encoder, Json}

class H2JsonSuite extends munit.FunSuite {

import cats.effect.unsafe.implicits.global

val xa = Transactor.fromDriverManager[IO](
"org.h2.Driver",
"jdbc:h2:mem:testdb",
"sa", ""
)

def inOut[A: Write: Read](col: String, a: A) =
for {
_ <- Update0(s"CREATE TEMPORARY TABLE TEST (value $col)", None).run
a0 <- Update[A](s"INSERT INTO TEST VALUES (?)", None).withUniqueGeneratedKeys[A]("value")(a)
} yield a0

@SuppressWarnings(Array("org.wartremover.warts.StringPlusAny"))
def testInOut[A](col: String, a: A, t: Transactor[IO])(implicit m: Get[A], p: Put[A]) = {
test(s"Mapping for $col as ${m.typeStack} - write+read $col as ${m.typeStack}") {
assertEquals(inOut(col, a).transact(t).attempt.unsafeRunSync(), Right(a))
}
test(s"Mapping for $col as ${m.typeStack} - write+read $col as Option[${m.typeStack}] (Some)") {
assertEquals(inOut[Option[A]](col, Some(a)).transact(t).attempt.unsafeRunSync(), Right(Some(a)))
}
test(s"Mapping for $col as ${m.typeStack} - write+read $col as Option[${m.typeStack}] (None)") {
assertEquals(inOut[Option[A]](col, None).transact(t).attempt.unsafeRunSync(), Right(None))
}
}

{
import doobie.h2.circe.json.implicits._
testInOut("json", Json.obj("something" -> Json.fromString("Yellow")), xa)
}


// Explicit Type Checks

test("json should check ok for read") {
import doobie.h2.circe.json.implicits._

val a = sql"SELECT '{}' FORMAT JSON".query[Json].analysis.transact(xa).unsafeRunSync()
assertEquals(a.columnTypeErrors, Nil)
}
test("json should check ok for write") {
import doobie.h2.circe.json.implicits._

val a = sql"SELECT ${Json.obj()} FORMAT JSON".query[Json].analysis.transact(xa).unsafeRunSync()
assertEquals(a.parameterTypeErrors, Nil)
}

// Encoder / Decoders
private case class Foo(x: Json)
private object Foo{
import doobie.h2.circe.json.implicits._
implicit val fooEncoder: Encoder[Foo] = Encoder[Json].contramap(_.x)
implicit val fooDecoder: Decoder[Foo] = Decoder[Json].map(Foo(_))
implicit val fooGet : Get[Foo] = h2DecoderGetT[Foo]
implicit val fooPut : Put[Foo] = h2EncoderPutT[Foo]
}

test("fooGet should check ok for read") {
val a = sql"SELECT '{}' FORMAT JSON".query[Foo].analysis.transact(xa).unsafeRunSync()
assertEquals(a.columnTypeErrors, Nil)
}
test("fooPut check ok for write") {
val a = sql"SELECT ${Foo(Json.obj())} FORMAT JSON".query[Foo].analysis.transact(xa).unsafeRunSync()
assertEquals(a.parameterTypeErrors, Nil)
}

}