Skip to content

Commit

Permalink
MySQL Java time instances
Browse files Browse the repository at this point in the history
  • Loading branch information
guymers committed Dec 28, 2022
1 parent 7e876b1 commit 3537fc9
Show file tree
Hide file tree
Showing 11 changed files with 387 additions and 2 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,11 @@ jobs:

- name: Make target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
run: mkdir -p modules/weaver/target modules/scalatest/target modules/refined/target target modules/postgres/target modules/docs/target modules/postgres-circe/target modules/h2/target modules/hikari/target modules/munit/target modules/h2-circe/target modules/core/target modules/example/target modules/specs2/target modules/free/target modules/bench/target project/target
run: mkdir -p modules/weaver/target modules/scalatest/target modules/refined/target target modules/postgres/target modules/docs/target modules/postgres-circe/target modules/h2/target modules/hikari/target modules/munit/target modules/h2-circe/target modules/mysql/target modules/core/target modules/example/target modules/specs2/target modules/free/target modules/bench/target project/target

- name: Compress target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
run: tar cf targets.tar modules/weaver/target modules/scalatest/target modules/refined/target target modules/postgres/target modules/docs/target modules/postgres-circe/target modules/h2/target modules/hikari/target modules/munit/target modules/h2-circe/target modules/core/target modules/example/target modules/specs2/target modules/free/target modules/bench/target project/target
run: tar cf targets.tar modules/weaver/target modules/scalatest/target modules/refined/target target modules/postgres/target modules/docs/target modules/postgres-circe/target modules/h2/target modules/hikari/target modules/munit/target modules/h2-circe/target modules/mysql/target modules/core/target modules/example/target modules/specs2/target modules/free/target modules/bench/target project/target

- name: Upload target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
Expand Down
14 changes: 14 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ lazy val fs2Version = "3.4.0"
lazy val h2Version = "1.4.200"
lazy val hikariVersion = "4.0.3" // N.B. Hikari v4 introduces a breaking change via slf4j v2
lazy val kindProjectorVersion = "0.11.2"
lazy val mysqlVersion = "8.0.31"
lazy val postGisVersion = "2.5.1"
lazy val postgresVersion = "42.5.1"
lazy val refinedVersion = "0.10.1"
Expand Down Expand Up @@ -120,6 +121,7 @@ lazy val doobie = project.in(file("."))
h2,
`h2-circe`,
hikari,
mysql,
postgres,
`postgres-circe`,
refined,
Expand Down Expand Up @@ -226,6 +228,18 @@ lazy val example = project
)
)

lazy val mysql = project
.in(file("modules/mysql"))
.enablePlugins(AutomateHeaderPlugin)
.dependsOn(core % "compile->compile;test->test")
.settings(doobieSettings)
.settings(
name := "doobie-mysql",
libraryDependencies ++= Seq(
"com.mysql" % "mysql-connector-j" % mysqlVersion,
),
)

lazy val postgres = project
.in(file("modules/postgres"))
.enablePlugins(AutomateHeaderPlugin)
Expand Down
8 changes: 8 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,11 @@ services:
- 5432:5432
volumes:
- ./init/:/docker-entrypoint-initdb.d/

mysql:
image: mysql:5.7-debian
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: world
ports:
- 3306:3306
11 changes: 11 additions & 0 deletions modules/core/src/main/scala/doobie/util/meta/meta.scala
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,17 @@ trait MetaConstructors {
Put.Basic.one(jdbcType, put, update)
)

def oneObject[A: TypeName](
jdbcType: JdbcType,
jdbcSourceSecondary: List[JdbcType],
clazz: Class[A]
): Meta[A] = one(
jdbcType = jdbcType,
jdbcSourceSecondary = jdbcSourceSecondary,
_.getObject(_, clazz),
_.setObject(_, _),
_.updateObject(_, _)
)
}

/**
Expand Down
53 changes: 53 additions & 0 deletions modules/mysql/src/main/scala/doobie/mysql/JavaTimeInstances.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// 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.mysql

import java.time.OffsetDateTime
import java.time.ZoneOffset

import doobie.Meta
import doobie.enumerated.{JdbcType => JT}
import doobie.util.meta.MetaConstructors

trait JavaTimeInstances extends MetaConstructors {

implicit val JavaTimeOffsetDateTimeMeta: Meta[java.time.OffsetDateTime] =
Basic.oneObject(
JT.Timestamp,
List(JT.VarChar, JT.Date, JT.Time),
classOf[java.time.OffsetDateTime]
)

implicit val JavaTimeInstantMeta: Meta[java.time.Instant] =
JavaTimeOffsetDateTimeMeta.timap(_.toInstant)(OffsetDateTime.ofInstant(_, ZoneOffset.UTC))

implicit val JavaTimeLocalDateTimeMeta: Meta[java.time.LocalDateTime] =
Basic.oneObject(
JT.Timestamp,
List(JT.VarChar, JT.Date, JT.Time),
classOf[java.time.LocalDateTime]
)

implicit val JavaTimeLocalDateMeta: Meta[java.time.LocalDate] =
Basic.oneObject(
JT.Date,
List(JT.VarChar, JT.Time, JT.Timestamp),
classOf[java.time.LocalDate]
)

implicit val JavaTimeLocalTimeMeta: Meta[java.time.LocalTime] =
Basic.oneObject(
JT.Time,
List(JT.Date, JT.Timestamp),
classOf[java.time.LocalTime]
)

implicit val JavaTimeOffsetTimeMeta: Meta[java.time.OffsetTime] =
Basic.oneObject(
JT.Timestamp,
List(JT.Date, JT.Time),
classOf[java.time.OffsetTime]
)
}
11 changes: 11 additions & 0 deletions modules/mysql/src/main/scala/doobie/mysql/package.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// 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

package object mysql {

object implicits
extends JavaTimeInstances
}
90 changes: 90 additions & 0 deletions modules/mysql/src/test/scala/doobie/mysql/CheckSuite.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// 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.mysql

import java.time.OffsetTime
import java.time.{LocalDate, LocalDateTime, LocalTime, OffsetDateTime}

import doobie._
import doobie.implicits._
import doobie.mysql.implicits._
import doobie.util.analysis.{ColumnTypeError, ColumnTypeWarning}

class CheckSuite extends munit.FunSuite {
import cats.effect.unsafe.implicits.global
import MySQLTestTransactor.xa

test("OffsetDateTime Read typechecks") {
successRead[Option[OffsetDateTime]](sql"SELECT CAST('2019-02-13 22:03:21.051' AS DATETIME)")

warnRead[Option[OffsetDateTime]](sql"SELECT '2019-02-13 22:03:21.051'")
warnRead[Option[OffsetDateTime]](sql"SELECT CAST('03:21' AS TIME)")
warnRead[Option[OffsetDateTime]](sql"SELECT CAST('2019-02-13' AS DATE)")
failedRead[Option[OffsetDateTime]](sql"SELECT 123")
}

test("LocalDateTime Read typechecks") {
successRead[Option[LocalDateTime]](sql"SELECT CAST('2019-02-13 22:03:21.051' AS DATETIME)")

warnRead[Option[LocalDateTime]](sql"SELECT '2019-02-13 22:03:21.051'")
warnRead[Option[LocalDateTime]](sql"SELECT CAST('03:21' AS TIME)")
warnRead[Option[LocalDateTime]](sql"SELECT CAST('2019-02-13' AS DATE)")
failedRead[Option[LocalDateTime]](sql"SELECT 123")
}

test("LocalDate Read typechecks") {
successRead[Option[LocalDate]](sql"SELECT CAST('2015-02-23' AS DATE)")

warnRead[Option[LocalDate]](sql"SELECT CAST('2019-02-13 22:03:21.051' AS DATETIME)")
warnRead[Option[LocalDate]](sql"SELECT CAST('03:21' AS TIME)")
warnRead[Option[LocalDate]](sql"SELECT '2015-02-23'")
failedRead[Option[LocalDate]](sql"SELECT 123")
}

test("LocalTime Read typechecks") {
successRead[Option[LocalTime]](sql"SELECT CAST('03:21' AS TIME)")

warnRead[Option[LocalTime]](sql"SELECT CAST('2019-02-13 22:03:21.051' AS DATETIME)")
warnRead[Option[LocalTime]](sql"SELECT CAST('2015-02-23' AS DATE)")
failedRead[Option[LocalTime]](sql"SELECT '03:21'")
failedRead[Option[LocalTime]](sql"SELECT 123")
}

test("OffsetTime Read typechecks") {
successRead[Option[OffsetTime]](sql"SELECT CAST('2019-02-13 22:03:21.051' AS DATETIME)")

warnRead[Option[OffsetTime]](sql"SELECT CAST('03:21' AS TIME)")
warnRead[Option[OffsetTime]](sql"SELECT CAST('2015-02-23' AS DATE)")
failedRead[Option[OffsetTime]](sql"SELECT '03:21'")
failedRead[Option[OffsetTime]](sql"SELECT 123")
}

private def successRead[A: Read](frag: Fragment): Unit = {
val analysisResult = frag.query[A].analysis.transact(xa).unsafeRunSync()
assertEquals(analysisResult.columnAlignmentErrors, Nil)

val result = frag.query[A].unique.transact(xa).attempt.unsafeRunSync()
assert(result.isRight)
}

private def warnRead[A: Read](frag: Fragment): Unit = {
val analysisResult = frag.query[A].analysis.transact(xa).unsafeRunSync()
val errorClasses = analysisResult.columnAlignmentErrors.map(_.getClass)
assertEquals(errorClasses, List(classOf[ColumnTypeWarning]))

val result = frag.query[A].unique.transact(xa).attempt.unsafeRunSync()
assert(result.isRight)
}

private def failedRead[A: Read](frag: Fragment): Unit = {
val analysisResult = frag.query[A].analysis.transact(xa).unsafeRunSync()
val errorClasses = analysisResult.columnAlignmentErrors.map(_.getClass)
assertEquals(errorClasses, List(classOf[ColumnTypeError]))

val result = frag.query[A].unique.transact(xa).attempt.unsafeRunSync()
assert(result.isLeft)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// 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.mysql

import cats.effect.IO
import doobie.Transactor

object MySQLTestTransactor {

val xa = Transactor.fromDriverManager[IO](
"com.mysql.cj.jdbc.Driver",
"jdbc:mysql://localhost:3306/world",
"root", "password"
)
}
83 changes: 83 additions & 0 deletions modules/mysql/src/test/scala/doobie/mysql/TypesSuite.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// 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.mysql

import java.time.ZoneOffset

import doobie._
import doobie.implicits._
import doobie.implicits.javasql._
import doobie.mysql.implicits._
import doobie.mysql.util.arbitraries.SQLArbitraries._
import doobie.mysql.util.arbitraries.TimeArbitraries._
import org.scalacheck.Arbitrary
import org.scalacheck.Gen
import org.scalacheck.Prop.forAll

class TypesSuite extends munit.ScalaCheckSuite {
import cats.effect.unsafe.implicits.global
import MySQLTestTransactor.xa

def inOut[A: Get : Put](col: String, a: A): ConnectionIO[A] = for {
_ <- Update0(s"CREATE TEMPORARY TABLE test (value $col NOT NULL)", None).run
_ <- Update[A](s"INSERT INTO test VALUES (?)", None).run(a)
a0 <- Query0[A](s"SELECT value FROM test", None).unique
} yield a0

def inOutOpt[A: Get : Put](col: String, a: Option[A]): ConnectionIO[Option[A]] =
for {
_ <- Update0(s"CREATE TEMPORARY TABLE test (value $col)", None).run
_ <- Update[Option[A]](s"INSERT INTO test VALUES (?)", None).run(a)
a0 <- Query0[Option[A]](s"SELECT value FROM test", None).unique
} yield a0

def testInOut[A](col: String)(implicit m: Get[A], p: Put[A], arbitrary: Arbitrary[A]) = {
testInOutWithCustomGen(col, arbitrary.arbitrary)
}

def testInOutNormalize[A](col: String)(f: A => A)(implicit m: Get[A], p: Put[A], arbitrary: Arbitrary[A]) = {
testInOutWithCustomGen(col, arbitrary.arbitrary, skipNone = false, f)
}

def testInOutWithCustomGen[A](col: String, gen: Gen[A], skipNone: Boolean = false, expected: A => A = identity[A](_))(implicit m: Get[A], p: Put[A]) = {
test(s"Mapping for $col as ${m.typeStack} - write+read $col as ${m.typeStack}") {
forAll(gen) { (t: A) =>
val actual = inOut(col, t).transact(xa).attempt.unsafeRunSync()
assertEquals(actual.map(expected(_)), Right(expected(t)))
}
}
test(s"Mapping for $col as ${m.typeStack} - write+read $col as Option[${m.typeStack}] (Some)") {
forAll(gen) { (t: A) =>
val actual = inOutOpt[A](col, Some(t)).transact(xa).attempt.unsafeRunSync()
assertEquals(actual.map(_.map(expected(_))), Right(Some(expected(t))))
}
}
if (!skipNone) {
test(s"Mapping for $col as ${m.typeStack} - write+read $col as Option[${m.typeStack}] (None)") {
assertEquals(inOutOpt[A](col, None).transact(xa).attempt.unsafeRunSync(), Right(None))
}
}
}

def skip(col: String, msg: String = "not yet implemented") =
test(s"Mapping for $col ($msg)".ignore) {}

testInOut[java.sql.Timestamp]("datetime(6)")
testInOutNormalize[java.time.OffsetDateTime]("datetime(6)")(_.withOffsetSameInstant(ZoneOffset.UTC))
testInOut[java.time.Instant]("datetime(6)")

testInOut[java.time.LocalDateTime]("datetime(6)")
testInOutWithCustomGen[java.time.LocalDateTime](
"timestamp(6)",
arbitraryLocalDateTimeTimestamp.arbitrary,
skipNone = true // returns the current timestamp, lol
)

testInOut[java.sql.Date]("date")
testInOut[java.time.LocalDate]("date")

testInOut[java.sql.Time]("time")
testInOut[java.time.LocalTime]("time(6)")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// 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.mysql.util.arbitraries

import java.sql.Date
import java.sql.Time
import java.sql.Timestamp

import org.scalacheck.Arbitrary

object SQLArbitraries {

implicit val arbitraryTime: Arbitrary[Time] = Arbitrary {
TimeArbitraries.arbitraryLocalTime.arbitrary.map(Time.valueOf(_))
}

implicit val arbitraryDate: Arbitrary[Date] = Arbitrary {
TimeArbitraries.arbitraryLocalDate.arbitrary.map(Date.valueOf(_))
}

implicit val arbitraryTimestamp: Arbitrary[Timestamp] = Arbitrary {
TimeArbitraries.arbitraryLocalDateTime.arbitrary.map(Timestamp.valueOf(_))
}

}
Loading

0 comments on commit 3537fc9

Please sign in to comment.