From 0e24654cb77e8b9c35dfdeac8be2a2e78e4fb56f Mon Sep 17 00:00:00 2001 From: Sam Guymer Date: Wed, 21 Dec 2022 21:15:17 +1000 Subject: [PATCH] Improve Java time meta experience Provide Java time meta instances without requiring any imports. These instances follow the JDBC specification and map a single column type to a java.time class. The Postgres specific instances have been updated to match exactly what the driver allows but the default instances should really be used instead. Any instances for `ZonedDateTime` have been removed. Storing one requires the `LocalDateTime` and `ZoneId` to be stored, it cannot safely be put into an `OffsetDateTime`. --- .../core/src/main/scala/doobie/package.scala | 3 - .../scala/doobie/util/meta/legacymeta.scala | 37 ------ .../main/scala/doobie/util/meta/meta.scala | 19 ++- .../main/scala/doobie/util/meta/sqlmeta.scala | 10 +- .../scala/doobie/util/meta/timemeta.scala | 89 +++++++------- .../src/main/mdoc/docs/12-Custom-Mappings.md | 2 +- .../mdoc/docs/15-Extensions-PostgreSQL.md | 8 +- modules/docs/src/main/mdoc/docs/17-FAQ.md | 19 --- .../main/scala/example/CustomReadWrite.scala | 1 - .../h2/src/test/scala/doobie/h2/h2types.scala | 110 ++++++++++++++---- .../doobie/postgres/JavaTimeInstances.scala | 60 ++++------ .../scala/doobie/postgres/TypesSuite.scala | 6 +- .../util/arbitraries/TimeArbitraries.scala | 7 -- 13 files changed, 172 insertions(+), 199 deletions(-) delete mode 100644 modules/core/src/main/scala/doobie/util/meta/legacymeta.scala diff --git a/modules/core/src/main/scala/doobie/package.scala b/modules/core/src/main/scala/doobie/package.scala index 0e5fe1c08..a6a8511a4 100644 --- a/modules/core/src/main/scala/doobie/package.scala +++ b/modules/core/src/main/scala/doobie/package.scala @@ -21,8 +21,5 @@ package object doobie object implicits extends free.Instances with syntax.AllSyntax - with util.meta.SqlMeta - with util.meta.TimeMeta - with util.meta.LegacyMeta } diff --git a/modules/core/src/main/scala/doobie/util/meta/legacymeta.scala b/modules/core/src/main/scala/doobie/util/meta/legacymeta.scala deleted file mode 100644 index 415b65c79..000000000 --- a/modules/core/src/main/scala/doobie/util/meta/legacymeta.scala +++ /dev/null @@ -1,37 +0,0 @@ -// 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.util.meta - -trait LegacyMeta { - - object legacy { - - object instant extends LegacyInstantMetaInstance - - object localdate extends LegacyLocalDateMetaInstance - - } - -} - -trait LegacyLocalDateMetaInstance { - - import doobie.implicits.javasql.DateMeta - - /** @group Instances */ - implicit val JavaTimeLocalDateMeta: Meta[java.time.LocalDate] = - DateMeta.imap(_.toLocalDate)(java.sql.Date.valueOf) - -} - -trait LegacyInstantMetaInstance { - - import doobie.implicits.javasql.TimestampMeta - - /** @group Instances */ - implicit val JavaTimeInstantMeta: Meta[java.time.Instant] = - TimestampMeta.imap(_.toInstant)(java.sql.Timestamp.from) - -} diff --git a/modules/core/src/main/scala/doobie/util/meta/meta.scala b/modules/core/src/main/scala/doobie/util/meta/meta.scala index ddb6bd896..d1ee01152 100644 --- a/modules/core/src/main/scala/doobie/util/meta/meta.scala +++ b/modules/core/src/main/scala/doobie/util/meta/meta.scala @@ -38,6 +38,8 @@ final class Meta[A](val get: Get[A], val put: Put[A]) { /** Module of constructors and instances for `Meta`. */ object Meta extends MetaConstructors with MetaInstances + with SqlMetaInstances + with TimeMetaInstances { /** Summon the `Meta` instance if possible. */ @@ -85,6 +87,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(_, _) + ) } /** @@ -239,10 +252,4 @@ trait MetaInstances { this: MetaConstructors => implicit val ScalaBigDecimalMeta: Meta[BigDecimal] = BigDecimalMeta.imap(BigDecimal.apply)(_.bigDecimal) - import doobie.implicits.javasql.DateMeta - - /** @group Instances */ - implicit val JavaUtilDateMeta: Meta[java.util.Date] = - DateMeta.imap[java.util.Date](a => a)(d => new java.sql.Date(d.getTime)) - } diff --git a/modules/core/src/main/scala/doobie/util/meta/sqlmeta.scala b/modules/core/src/main/scala/doobie/util/meta/sqlmeta.scala index 0a13278b5..0969144dc 100644 --- a/modules/core/src/main/scala/doobie/util/meta/sqlmeta.scala +++ b/modules/core/src/main/scala/doobie/util/meta/sqlmeta.scala @@ -6,12 +6,6 @@ package doobie.util.meta import doobie.enumerated.JdbcType._ -trait SqlMeta { - - object javasql extends MetaConstructors with SqlMetaInstances - -} - trait SqlMetaInstances { this: MetaConstructors => /** @group Instances */ @@ -35,4 +29,8 @@ trait SqlMetaInstances { this: MetaConstructors => List(Char, VarChar, LongVarChar, Date, Time), _.getTimestamp(_), _.setTimestamp(_, _), _.updateTimestamp(_, _)) + /** @group Instances */ + implicit val JavaUtilDateMeta: Meta[java.util.Date] = + DateMeta.imap[java.util.Date](a => a)(d => new java.sql.Date(d.getTime)) + } diff --git a/modules/core/src/main/scala/doobie/util/meta/timemeta.scala b/modules/core/src/main/scala/doobie/util/meta/timemeta.scala index 259728b7a..c21c19b4c 100644 --- a/modules/core/src/main/scala/doobie/util/meta/timemeta.scala +++ b/modules/core/src/main/scala/doobie/util/meta/timemeta.scala @@ -5,75 +5,68 @@ package doobie.util.meta import doobie.enumerated.JdbcType._ -import Predef._ - -trait TimeMeta { - - @deprecated("Use doobie.implicits.javatimedrivernative instead. If you are using a database which doobie directly integrates with, " + - "You won't need this import anymore as datetime instances are provided in the DB-specific implicit import. " + - "e.g. for PostgreSQL: `import doobie.postgres.implicits._`. ", - since = "0.11.0" - ) - object javatime extends MetaConstructors with TimeMetaInstances - - /** - * Use this import if you want to use the driver-native support for java.time.* types. - * This means that the java.time value is passed straight to the JDBC driver you're using - * without being converted to java.sql.* types (Unlike doobie.implicits.legacy.instant/localdate) - */ - object javatimedrivernative extends MetaConstructors with TimeMetaInstances - -} +/** + * Instances for Java time classes that follow the JDBC specification. + */ trait TimeMetaInstances { this: MetaConstructors => + import Predef.classOf /** @group Instances */ - implicit val JavaTimeInstantMeta: Meta[java.time.Instant] = - Basic.one[java.time.Instant]( - Timestamp, - List(Char, VarChar, LongVarChar, Date, Time), - _.getObject(_, classOf[java.time.Instant]), _.setObject(_, _), _.updateObject(_, _)) + implicit val JavaOffsetDateTimeMeta: Meta[java.time.OffsetDateTime] = + Basic.oneObject( + TimestampWithTimezone, + List(TimeWithTimezone), + classOf[java.time.OffsetDateTime] + ) /** @group Instances */ - implicit val JavaTimeLocalDateMeta: Meta[java.time.LocalDate] = - Basic.one[java.time.LocalDate]( + implicit val JavaLocalDateMeta: Meta[java.time.LocalDate] = + Basic.oneObject( Date, - List(Char, VarChar, LongVarChar, Timestamp), - _.getObject(_, classOf[java.time.LocalDate]), _.setObject(_, _), _.updateObject(_, _)) + Nil, + classOf[java.time.LocalDate] + ) /** @group Instances */ implicit val JavaLocalTimeMeta: Meta[java.time.LocalTime] = - Basic.one[java.time.LocalTime]( + Basic.oneObject( Time, - List(Char, VarChar, LongVarChar, Timestamp), - _.getObject(_, classOf[java.time.LocalTime]), _.setObject(_, _), _.updateObject(_, _)) + Nil, + classOf[java.time.LocalTime] + ) /** @group Instances */ - implicit val JavaTimeLocalDateTimeMeta: Meta[java.time.LocalDateTime] = - Basic.one[java.time.LocalDateTime]( + implicit val JavaLocalDateTimeMeta: Meta[java.time.LocalDateTime] = + Basic.oneObject( Timestamp, - List(Char, VarChar, LongVarChar, Date, Time), - _.getObject(_, classOf[java.time.LocalDateTime]), _.setObject(_, _), _.updateObject(_, _)) + Nil, + classOf[java.time.LocalDateTime] + ) /** @group Instances */ implicit val JavaOffsetTimeMeta: Meta[java.time.OffsetTime] = - Basic.one[java.time.OffsetTime]( + Basic.oneObject( TimeWithTimezone, - List(Char, VarChar, LongVarChar, Timestamp, Time), - _.getObject(_, classOf[java.time.OffsetTime]), _.setObject(_, _), _.updateObject(_, _)) + Nil, + classOf[java.time.OffsetTime] + ) + + // extra instances not in the spec /** @group Instances */ - implicit val JavaOffsetDateTimeMeta: Meta[java.time.OffsetDateTime] = - Basic.one[java.time.OffsetDateTime]( - TimestampWithTimezone, - List(Char, VarChar, LongVarChar, Date, Time, Timestamp), - _.getObject(_, classOf[java.time.OffsetDateTime]), _.setObject(_, _), _.updateObject(_, _)) + implicit val JavaInstantMeta: Meta[java.time.Instant] = + JavaOffsetDateTimeMeta.imap(_.toInstant)(_.atOffset(java.time.ZoneOffset.UTC)) /** @group Instances */ - implicit val JavaZonedDateTimeMeta: Meta[java.time.ZonedDateTime] = - Basic.one[java.time.ZonedDateTime]( - TimestampWithTimezone, - List(Char, VarChar, LongVarChar, Date, Time, Timestamp), - _.getObject(_, classOf[java.time.ZonedDateTime]), _.setObject(_, _), _.updateObject(_, _)) + implicit val JavaTimeZoneId: Meta[java.time.ZoneId] = { + def parse(str: String) = try { + Right(java.time.ZoneId.of(str)) + } catch { + case e: java.time.DateTimeException => Left(e.getMessage) + } + + Meta[String].tiemap(parse(_))(_.getId) + } } diff --git a/modules/docs/src/main/mdoc/docs/12-Custom-Mappings.md b/modules/docs/src/main/mdoc/docs/12-Custom-Mappings.md index ffa00819a..b46e840ef 100644 --- a/modules/docs/src/main/mdoc/docs/12-Custom-Mappings.md +++ b/modules/docs/src/main/mdoc/docs/12-Custom-Mappings.md @@ -53,7 +53,7 @@ Instances are provided for the following Scala types: - `Boolean`, `String`, and `Array[Byte]`; - `Date`, `Time`, and `Timestamp` from the `java.sql` package; - `Date` from the `java.util` package; -- `Instant`, `LocalDate`, `LocalTime`, `LocalDateTime`, `OffsetTime`, `OffsetDateTime` and `ZonedDateTime` from the `java.time` package; and +- `Instant`, `LocalDate`, `LocalTime`, `LocalDateTime`, `OffsetTime` and `OffsetDateTime` from the `java.time` package; and - single-element case classes wrapping one of the above types. The `java.time` instances may require a separate import , dependent on your Database Driver . See the [doobie-faq](https://tpolecat.github.io/doobie/docs/17-FAQ.html#how-do-i-use-java-time-types-with-doobie-) for details diff --git a/modules/docs/src/main/mdoc/docs/15-Extensions-PostgreSQL.md b/modules/docs/src/main/mdoc/docs/15-Extensions-PostgreSQL.md index f57d01688..86a87fac0 100644 --- a/modules/docs/src/main/mdoc/docs/15-Extensions-PostgreSQL.md +++ b/modules/docs/src/main/mdoc/docs/15-Extensions-PostgreSQL.md @@ -76,17 +76,11 @@ import doobie.postgres.implicits._ ### Java 8 Time Types (JSR310) -An explicit import is required to bring in mappings for `java.time.OffsetDateTime` / `java.time.Instant` / `java.time.ZonedDateTime` / `java.time.LocalDateTime` / `java.time.LocalDate` / `java.time.LocalTime` - -```scala mdoc:silent -import doobie.postgres.implicits._ -``` - To ensure **doobie** performs the conversion correctly between Java 8 time types and PostgreSQL Date/Time types when handling timezones or the lack thereof. The correct combination of date/time types should be used: - `TIMESTAMP` maps to `java.time.LocalDateTime` -- `TIMESTAMPTZ` maps to `java.time.Instant`, `java.time.ZonedDateTime` or `java.time.OffsetDateTime` +- `TIMESTAMPTZ` maps to `java.time.Instant` or `java.time.OffsetDateTime` - `DATE` maps to `java.time.LocalDate` - `TIME` maps to `java.time.LocalTime` diff --git a/modules/docs/src/main/mdoc/docs/17-FAQ.md b/modules/docs/src/main/mdoc/docs/17-FAQ.md index f93c186c3..37357d870 100644 --- a/modules/docs/src/main/mdoc/docs/17-FAQ.md +++ b/modules/docs/src/main/mdoc/docs/17-FAQ.md @@ -201,22 +201,3 @@ implicit val nesMeta: Meta[NonEmptyString] = { distinct.string("nes").imap(NonEmptyString.apply)(_.value) } ``` - -## How do I use `java.time` types with Doobie? - -This depends on whether the underlying JDBC driver you're using supports `java.time.*` types natively. -("native support" means that you can hand the driver e.g. a value of java.time.Instant and it will know how to convert -that to a value on-the-wire that the actual database can understand) - -If you're using PostgreSQL, you can import that instances via `import doobie.postgres.implicits._` - -If your JDBC driver supports the java.time types you're using natively, use `import doobie.implicits.javatimedrivernative._`. - -| Database driver | java.time.Instant | java.time.LocalDate | -| --- | --- | --- | -| Postgres (org.postgresql.Driver) | `doobie.postgres.implicits._` | `doobie.postgres.implicits._` | -| MySQL (com.mysql.jdbc.Driver) | `doobie.implicits.legacy.instant._` | `doobie.implicits.legacy.localdate._` | - -References: - -- [Postgres JDBC - Using Java 8 Date and Time classes](https://jdbc.postgresql.org/documentation/head/8-date-time.html) diff --git a/modules/example/src/main/scala/example/CustomReadWrite.scala b/modules/example/src/main/scala/example/CustomReadWrite.scala index 1164fcbd8..de27aff39 100644 --- a/modules/example/src/main/scala/example/CustomReadWrite.scala +++ b/modules/example/src/main/scala/example/CustomReadWrite.scala @@ -7,7 +7,6 @@ package example import doobie._, doobie.implicits._ import java.sql.Date - import doobie.implicits.javasql._ object CustomReadWrite { diff --git a/modules/h2/src/test/scala/doobie/h2/h2types.scala b/modules/h2/src/test/scala/doobie/h2/h2types.scala index d4adbfdc9..eaad318f0 100644 --- a/modules/h2/src/test/scala/doobie/h2/h2types.scala +++ b/modules/h2/src/test/scala/doobie/h2/h2types.scala @@ -8,15 +8,13 @@ import java.util.UUID import cats.effect.IO import doobie._ -import doobie.h2.implicits._ import doobie.implicits._ +import doobie.h2.implicits._ +import doobie.util.analysis.{Analysis, ColumnTypeError, ColumnTypeWarning} import doobie.util.arbitraries.SQLArbitraries._ import doobie.util.arbitraries.StringArbitraries._ import org.scalacheck.Prop.forAll import org.scalacheck.{Arbitrary, Gen} -import doobie.implicits.javasql._ -import doobie.implicits.javatimedrivernative.{JavaTimeInstantMeta => NewJavaTimeInstantMeta, _} -import doobie.implicits.legacy.instant.{JavaTimeInstantMeta => LegacyJavaTimeInstantMeta} // Establish that we can read various types. It's not very comprehensive as a test, bit it's a start. class h2typesspec extends munit.ScalaCheckSuite { @@ -59,18 +57,6 @@ class h2typesspec extends munit.ScalaCheckSuite { } } - def testInOutWithCustomTransform[A](col: String)(f: A => A)(implicit m: Get[A], p: Put[A], arbitrary: Arbitrary[A]) = { - test(s"Mapping for $col as ${m.typeStack} - write+read $col as ${m.typeStack}") { - forAll { (t: A) => assertEquals( inOut(col, f(t)).transact(xa).attempt.unsafeRunSync(), Right(f(t))) } - } - test(s"Mapping for $col as ${m.typeStack} - write+read $col as Option[${m.typeStack}] (Some)") { - forAll { (t: A) => assertEquals( inOutOpt[A](col, Some(f(t))).transact(xa).attempt.unsafeRunSync(), Right(Some(f(t)))) } - } - 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) {} @@ -97,12 +83,6 @@ class h2typesspec extends munit.ScalaCheckSuite { */ testInOut[java.sql.Timestamp]("TIMESTAMP(9)") testInOut[java.time.LocalDateTime]("TIMESTAMP(9)") - testInOut[java.time.Instant]("TIMESTAMP(9)")( - LegacyJavaTimeInstantMeta.get, LegacyJavaTimeInstantMeta.put, Arbitrary(arbitraryTimestamp.arbitrary.map(_.toInstant)) - ) - testInOut[java.time.Instant]("TIMESTAMP(9) WITH TIME ZONE")( - NewJavaTimeInstantMeta.get, NewJavaTimeInstantMeta.put, implicitly - ) /* TIME WITH TIMEZONE @@ -115,7 +95,9 @@ class h2typesspec extends munit.ScalaCheckSuite { If fractional seconds precision is specified it should be from 0 to 9, 6 is default. */ testInOut[java.time.OffsetDateTime]("TIMESTAMP(9) WITH TIME ZONE") - testInOutWithCustomTransform[java.time.ZonedDateTime]("TIMESTAMP WITH TIME ZONE")(_.withFixedOffsetZone().withNano(0)) + testInOut[java.time.Instant]("TIMESTAMP(9) WITH TIME ZONE") + + testInOut[java.time.ZoneId]("VARCHAR") testInOut[List[Byte]]("BINARY") skip("OTHER") @@ -150,4 +132,86 @@ class h2typesspec extends munit.ScalaCheckSuite { assertEquals(a.alignmentErrors, Nil) } + test("Mapping for LocalDate should pass query analysis for DATE") { + val a = analyzeDate[java.time.LocalDate] + assertEquals(a.alignmentErrors, Nil) + } + + test("Mapping for LocalDate should fail query analysis for TIMESTAMP") { + val a = analyzeTimestamp[java.time.LocalDate] + assertAnalyzeColumnError(a) + } + + test("Mapping for LocalTime should pass query analysis for TIME") { + val a = analyzeTime[java.time.LocalTime] + assertEquals(a.alignmentErrors, Nil) + } + + test("Mapping for LocalTime should fail query analysis for TIME WITH TIME ZONE") { + val a = analyzeTimeWithTimeZone[java.time.LocalTime] + assertAnalyzeColumnError(a) + } + + test("Mapping for OffsetTime should pass query analysis for TIME WITH TIME ZONE") { + val a = analyzeTimeWithTimeZone[java.time.OffsetTime] + assertEquals(a.alignmentErrors, Nil) + } + + test("Mapping for OffsetTime should fail query analysis for TIME") { + val a = analyzeTime[java.time.OffsetTime] + assertAnalyzeColumnError(a) + } + + test("Mapping for LocalDateTime should pass query analysis for TIMESTAMP") { + val a = analyzeTimestamp[java.time.LocalDateTime] + assertEquals(a.alignmentErrors, Nil) + } + + test("Mapping for LocalDateTime should fail query analysis for DATE") { + val a = analyzeDate[java.time.LocalDateTime] + assertAnalyzeColumnError(a) + } + + test("Mapping for LocalDateTime should fail query analysis for TIME") { + val a = analyzeTime[java.time.LocalDateTime] + assertAnalyzeColumnError(a) + } + + test("Mapping for LocalDateTime should fail query analysis for TIMESTAMP WITH TIME ZONE") { + val a = analyzeTimestampWithTimeZone[java.time.LocalDateTime] + assertAnalyzeColumnError(a) + } + + test("Mapping for OffsetDateTime should pass query analysis for TIMESTAMP WITH TIME ZONE") { + val a = analyzeTimestampWithTimeZone[java.time.OffsetDateTime] + assertEquals(a.alignmentErrors, Nil) + } + + test("Mapping for OffsetDateTime should warn query analysis for TIME WITH TIME ZONE") { + val a = analyzeTimeWithTimeZone[java.time.OffsetDateTime] + assertAnalyzeColumnWarning(a) + } + + test("Mapping for OffsetDateTime should fail query analysis for TIMESTAMP") { + val a = analyzeTimestamp[java.time.OffsetDateTime] + assertAnalyzeColumnError(a) + } + + private def analyzeDate[R: Read] = analyze(sql"select '2000-01-02'::DATE".query[R]) + private def analyzeTime[R: Read] = analyze(sql"select '01:02:03'::TIME".query[R]) + private def analyzeTimeWithTimeZone[R: Read] = analyze(sql"select '01:02:03+04:05'::TIME WITH TIME ZONE".query[R]) + private def analyzeTimestamp[R: Read] = analyze(sql"select '2000-01-02T01:02:03'::TIMESTAMP".query[R]) + private def analyzeTimestampWithTimeZone[R: Read] = analyze(sql"select '2000-01-02T01:02:03+04:05'::TIMESTAMP WITH TIME ZONE".query[R]) + + private def analyze[R](q: Query0[R]) = q.analysis.transact(xa).unsafeRunSync() + + private def assertAnalyzeColumnWarning(result: Analysis): Unit = { + val errorClasses = result.alignmentErrors.map(_.getClass) + assertEquals(errorClasses, List(classOf[ColumnTypeWarning])) + } + private def assertAnalyzeColumnError(result: Analysis): Unit = { + val errorClasses = result.alignmentErrors.map(_.getClass) + assertEquals(errorClasses, List(classOf[ColumnTypeError])) + } + } diff --git a/modules/postgres/src/main/scala/doobie/postgres/JavaTimeInstances.scala b/modules/postgres/src/main/scala/doobie/postgres/JavaTimeInstances.scala index 71b5b0d27..aae210c02 100644 --- a/modules/postgres/src/main/scala/doobie/postgres/JavaTimeInstances.scala +++ b/modules/postgres/src/main/scala/doobie/postgres/JavaTimeInstances.scala @@ -8,7 +8,7 @@ import doobie.Meta import doobie.enumerated.{JdbcType => JT} import doobie.util.meta.MetaConstructors -import java.time.{OffsetDateTime, ZoneOffset} // Using database JDBC driver native support +import java.time.{OffsetDateTime, ZoneOffset} /** * Instances for JSR-310 date time types. @@ -19,57 +19,39 @@ import java.time.{OffsetDateTime, ZoneOffset} // Using database JDBC driver nati trait JavaTimeInstances extends MetaConstructors { /** - * This type should map to TIMESTAMP WITH TIMEZONE (TIMESTAMPTZ) - * When writing to the database, the same instant is preserved if your target column is of type TIMESTAMPTZ - * (The JDBC driver works out the timezone conversion for you). Note that since offset information is not stored in - * the database column, retrieving the same value will yield the same instant in time, but with offset = 0 (UTC) + * This type should map to TIMESTAMP WITH TIMEZONE (TIMESTAMPTZ). + * + * Allows `TimeWithTimezone` and `Timestamp` reluctantly. See comment in the driver: + * https://github.com/pgjdbc/pgjdbc/blob/REL42.4.1/pgjdbc/src/main/java/org/postgresql/jdbc/PgResultSet.java#L645 */ - implicit val JavaTimeOffsetDateTimeMeta: Meta[java.time.OffsetDateTime] = - Basic.one[java.time.OffsetDateTime]( + implicit val JavaOffsetDateTimeMeta: Meta[java.time.OffsetDateTime] = + Basic.oneObject( JT.TimestampWithTimezone, List(JT.Timestamp, JT.TimeWithTimezone), - _.getObject(_, classOf[java.time.OffsetDateTime]), _.setObject(_, _), _.updateObject(_, _)) + classOf[java.time.OffsetDateTime] + ) /** * This type should map to TIMESTAMP WITH TIMEZONE (TIMESTAMPTZ) */ - implicit val JavaTimeInstantMeta: Meta[java.time.Instant] = - JavaTimeOffsetDateTimeMeta.timap(_.toInstant)(OffsetDateTime.ofInstant(_, ZoneOffset.UTC)) + implicit val JavaInstantMeta: Meta[java.time.Instant] = + JavaOffsetDateTimeMeta.timap(_.toInstant)(OffsetDateTime.ofInstant(_, ZoneOffset.UTC)) /** - * This type should map to TIMESTAMP + * Allows `Timestamp`: + * https://github.com/pgjdbc/pgjdbc/blob/REL42.4.1/pgjdbc/src/main/java/org/postgresql/jdbc/PgResultSet.java#L732 */ - implicit val JavaTimeLocalDateTimeMeta: Meta[java.time.LocalDateTime] = - Basic.one[java.time.LocalDateTime]( - JT.Timestamp, - Nil, - _.getObject(_, classOf[java.time.LocalDateTime]), _.setObject(_, _), _.updateObject(_, _)) - - /** - * This type should map to DATE - */ - implicit val JavaTimeLocalDateMeta: Meta[java.time.LocalDate] = - Basic.one[java.time.LocalDate]( + implicit val JavaLocalDateMeta: Meta[java.time.LocalDate] = + Basic.oneObject( JT.Date, List(JT.Timestamp), - _.getObject(_, classOf[java.time.LocalDate]), _.setObject(_, _), _.updateObject(_, _)) + classOf[java.time.LocalDate] + ) - /** - * This type should map to TIME - */ - implicit val JavaTimeLocalTimeMeta: Meta[java.time.LocalTime] = - Basic.one[java.time.LocalTime]( - JT.Time, - Nil, - _.getObject(_, classOf[java.time.LocalTime]), _.setObject(_, _), _.updateObject(_, _)) + implicit val JavaLocalTimeMeta: Meta[java.time.LocalTime] = Meta.JavaLocalTimeMeta - /** - * This type should map to TIME WITH TIMEZONE (TIMETZ) - */ - implicit val JavaTimeOffsetTimeMeta: Meta[java.time.OffsetTime] = - Basic.one[java.time.OffsetTime]( - JT.TimeWithTimezone, - Nil, - _.getObject(_, classOf[java.time.OffsetTime]), _.setObject(_, _), _.updateObject(_, _)) + implicit val JavaLocalDateTimeMeta: Meta[java.time.LocalDateTime] = Meta.JavaLocalDateTimeMeta + + implicit val JavaOffsetTimeMeta: Meta[java.time.OffsetTime] = Meta.JavaOffsetTimeMeta } diff --git a/modules/postgres/src/test/scala/doobie/postgres/TypesSuite.scala b/modules/postgres/src/test/scala/doobie/postgres/TypesSuite.scala index 5cafb93b8..df7b74af7 100644 --- a/modules/postgres/src/test/scala/doobie/postgres/TypesSuite.scala +++ b/modules/postgres/src/test/scala/doobie/postgres/TypesSuite.scala @@ -11,7 +11,6 @@ import java.util.UUID import cats.effect.IO import doobie._ import doobie.implicits._ -import doobie.implicits.javasql._ import doobie.postgres.enums._ import doobie.postgres.implicits._ import doobie.postgres.pgisimplicits._ @@ -131,9 +130,12 @@ class TypesSuite extends munit.ScalaCheckSuite { testInOut[java.sql.Time]("time") testInOut[java.time.LocalTime]("time") - skip("time with time zone") + testInOut[java.time.OffsetTime]("time with time zone") + testInOut("interval", new PGInterval(1, 2, 3, 4, 5, 6.7)) + testInOut[java.time.ZoneId]("text") + // 8.6 Boolean Type testInOut[Boolean]("boolean") diff --git a/modules/postgres/src/test/scala/doobie/postgres/util/arbitraries/TimeArbitraries.scala b/modules/postgres/src/test/scala/doobie/postgres/util/arbitraries/TimeArbitraries.scala index 6de2bc6ee..c2254612b 100644 --- a/modules/postgres/src/test/scala/doobie/postgres/util/arbitraries/TimeArbitraries.scala +++ b/modules/postgres/src/test/scala/doobie/postgres/util/arbitraries/TimeArbitraries.scala @@ -79,11 +79,4 @@ object TimeArbitraries { } yield dateTime.atOffset(offset) } - implicit val arbitraryZonedDateTime: Arbitrary[ZonedDateTime] = Arbitrary { - for { - dateTime <- arbitraryLocalDateTime.arbitrary - zone <- arbitraryZoneOffset.arbitrary - } yield ZonedDateTime.of(dateTime, zone) - } - }