diff --git a/docker-compose.yml b/docker-compose.yml index c7533c61e..998912a54 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,8 @@ version: '3.1' services: - postgres: - image: postgis/postgis:11-3.3 + image: postgis/postgis:16-3.4 environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: password @@ -11,4 +10,23 @@ services: ports: - 5432:5432 volumes: - - ./init/:/docker-entrypoint-initdb.d/ + - ./init/postgres/:/docker-entrypoint-initdb.d/ + deploy: + resources: + limits: + memory: 500M + + + mysql: + image: mysql:8.0-debian + environment: + MYSQL_ROOT_PASSWORD: password + MYSQL_DATABASE: world + ports: + - 3306:3306 + volumes: + - ./init/mysql/:/docker-entrypoint-initdb.d/ + deploy: + resources: + limits: + memory: 500M diff --git a/init/mysql/test-table.sql b/init/mysql/test-table.sql new file mode 100644 index 000000000..8852905cf --- /dev/null +++ b/init/mysql/test-table.sql @@ -0,0 +1,11 @@ + +CREATE TABLE IF NOT EXISTS test ( + c_integer INTEGER NOT NULL, + c_varchar VARCHAR(1024) NOT NULL, + c_date DATE NOT NULL, + c_datetime DATETIME(6) NOT NULL, + c_time TIME(6) NOT NULL, + c_timestamp TIMESTAMP(6) NOT NULL +); +INSERT INTO test(c_integer, c_varchar, c_date, c_datetime, c_time, c_timestamp) +VALUES (123, 'str', '2019-02-13', '2019-02-13 22:03:21.051', '22:03:21.051', '2019-02-13 22:03:21.051'); diff --git a/init/test-db.sql b/init/postgres/test-db.sql similarity index 100% rename from init/test-db.sql rename to init/postgres/test-db.sql diff --git a/modules/core/src/main/scala/doobie/hi/connection.scala b/modules/core/src/main/scala/doobie/hi/connection.scala index 688e224c8..d4e2d5f5f 100644 --- a/modules/core/src/main/scala/doobie/hi/connection.scala +++ b/modules/core/src/main/scala/doobie/hi/connection.scala @@ -4,26 +4,28 @@ package doobie.hi -import doobie.util.compat.propertiesToScala +import cats.Foldable +import cats.data.Ior +import cats.effect.kernel.syntax.monadCancel._ +import cats.syntax.all._ +import doobie.enumerated.AutoGeneratedKeys import doobie.enumerated.Holdability -import doobie.enumerated.ResultSetType +import doobie.enumerated.Nullability import doobie.enumerated.ResultSetConcurrency +import doobie.enumerated.ResultSetType import doobie.enumerated.TransactionIsolation -import doobie.enumerated.AutoGeneratedKeys -import doobie.util.{ Read, Write } import doobie.util.analysis.Analysis +import doobie.util.analysis.ColumnMeta +import doobie.util.analysis.ParameterMeta +import doobie.util.compat.propertiesToScala import doobie.util.stream.repeatEvalChunks +import doobie.util.{ Get, Put, Read, Write } +import fs2.Stream +import fs2.Stream.{ eval, bracket } import java.sql.{ Savepoint, PreparedStatement, ResultSet } - import scala.collection.immutable.Map -import cats.Foldable -import cats.syntax.all._ -import cats.effect.kernel.syntax.monadCancel._ -import fs2.Stream -import fs2.Stream.{ eval, bracket } - /** * Module of high-level constructors for `ConnectionIO` actions. * @group Modules @@ -92,24 +94,29 @@ object connection { * readable resultset row type `B`. */ def prepareQueryAnalysis[A: Write, B: Read](sql: String): ConnectionIO[Analysis] = - prepareStatement(sql) { - (HPS.getParameterMappings[A], HPS.getColumnMappings[B]) mapN (Analysis(sql, _, _)) - } + prepareAnalysis(sql, HPS.getParameterMappings[A], HPS.getColumnMappings[B]) def prepareQueryAnalysis0[B: Read](sql: String): ConnectionIO[Analysis] = - prepareStatement(sql) { - HPS.getColumnMappings[B] map (cm => Analysis(sql, Nil, cm)) - } + prepareAnalysis(sql, FPS.pure(Nil), HPS.getColumnMappings[B]) def prepareUpdateAnalysis[A: Write](sql: String): ConnectionIO[Analysis] = - prepareStatement(sql) { - HPS.getParameterMappings[A] map (pm => Analysis(sql, pm, Nil)) - } + prepareAnalysis(sql, HPS.getParameterMappings[A], FPS.pure(Nil)) def prepareUpdateAnalysis0(sql: String): ConnectionIO[Analysis] = - prepareStatement(sql) { - Analysis(sql, Nil, Nil).pure[PreparedStatementIO] + prepareAnalysis(sql, FPS.pure(Nil), FPS.pure(Nil)) + + private def prepareAnalysis( + sql: String, + params: PreparedStatementIO[List[(Put[_], Nullability.NullabilityKnown) Ior ParameterMeta]], + columns: PreparedStatementIO[List[(Get[_], Nullability.NullabilityKnown) Ior ColumnMeta]], + ) = { + val mappings = prepareStatement(sql) { + (params, columns).tupled } + (HC.getMetaData(FDMD.getDriverName), mappings).mapN { case (driver, (p, c)) => + Analysis(driver, sql, p, c) + } + } /** @group Statements */ diff --git a/modules/core/src/main/scala/doobie/package.scala b/modules/core/src/main/scala/doobie/package.scala index 1be36f01c..78f1cfe8d 100644 --- a/modules/core/src/main/scala/doobie/package.scala +++ b/modules/core/src/main/scala/doobie/package.scala @@ -2,6 +2,11 @@ // This software is licensed under the MIT License (MIT). // For more information see LICENSE or https://opensource.org/licenses/MIT +import doobie.util.meta.{LegacyMeta, TimeMetaInstances} +// 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 + /** * Top-level import, providing aliases for the most commonly used types and modules from * doobie-free and doobie-core. A typical starting set of imports would be something like this. @@ -21,6 +26,7 @@ package object doobie object implicits extends free.Instances with generic.AutoDerivation + with LegacyMeta with syntax.AllSyntax { // re-export these instances so `Meta` takes priority, must be in the object @@ -28,6 +34,16 @@ package object doobie implicit def metaProjectionPut[A](implicit m: Meta[A]): Put[A] = Put.metaProjectionWrite implicit def fromGetRead[A](implicit G: Get[A]): Read[A] = Read.fromGet implicit def fromPutWrite[A](implicit P: Put[A]): Write[A] = Write.fromPut + + /** + * Only use this import if: + * 1. You're NOT using one of the database doobie has direct java.time isntances for + * (PostgreSQL / MySQL). (They have more accurate column type checks) + * 2. Your driver natively supports java.time.* types + * + * If your driver doesn't support java.time.* types, use [[doobie.implicits.legacy.instant/localdate]] instead + */ + object javatimedrivernative extends TimeMetaInstances } } diff --git a/modules/core/src/main/scala/doobie/util/analysis.scala b/modules/core/src/main/scala/doobie/util/analysis.scala index 89ad2ba3c..188468dec 100644 --- a/modules/core/src/main/scala/doobie/util/analysis.scala +++ b/modules/core/src/main/scala/doobie/util/analysis.scala @@ -20,28 +20,9 @@ object analysis { /** Metadata for the JDBC end of a column/parameter mapping. */ final case class ColumnMeta(jdbcType: JdbcType, vendorTypeName: String, nullability: Nullability, name: String) - object ColumnMeta { - def apply(jdbcType: JdbcType, vendorTypeName: String, nullability: Nullability, name: String): ColumnMeta = { - new ColumnMeta(tweakJdbcType(jdbcType, vendorTypeName), vendorTypeName, nullability, name) - } - } /** Metadata for the JDBC end of a column/parameter mapping. */ final case class ParameterMeta(jdbcType: JdbcType, vendorTypeName: String, nullability: Nullability, mode: ParameterMode) - object ParameterMeta { - def apply(jdbcType: JdbcType, vendorTypeName: String, nullability: Nullability, mode: ParameterMode): ParameterMeta = { - new ParameterMeta(tweakJdbcType(jdbcType, vendorTypeName), vendorTypeName, nullability, mode) - } - } - - private def tweakJdbcType(jdbcType: JdbcType, vendorTypeName: String) = jdbcType match { - // the Postgres driver does not return *WithTimezone types but they are pretty much required for proper analysis - // https://github.com/pgjdbc/pgjdbc/issues/2485 - // https://github.com/pgjdbc/pgjdbc/issues/1766 - case JdbcType.Time if vendorTypeName.compareToIgnoreCase("timetz") == 0 => JdbcType.TimeWithTimezone - case JdbcType.Timestamp if vendorTypeName.compareToIgnoreCase("timestamptz") == 0 => JdbcType.TimestampWithTimezone - case t => t - } sealed trait AlignmentError extends Product with Serializable { def tag: String @@ -100,10 +81,10 @@ object analysis { override val tag = "C" override def msg = s"""|${schema.jdbcType.show.toUpperCase} (${schema.vendorTypeName}) is not - |coercible to ${typeName(get.typeStack.last, n)} according to the JDBC specification or any defined + |coercible to ${typeName(get.typeStack.last, n)} (${get.vendorTypeNames.mkString(",")}) according to the JDBC specification or any defined |mapping. |Fix this by changing the schema type to - |${get.jdbcSources.toList.map(_.show.toUpperCase).toList.mkString(" or ") }; or the + |${get.jdbcSources.toList.map(_.show.toUpperCase).mkString(" or ") }; or the |Scala type to an appropriate ${if (schema.jdbcType === JdbcType.Array) "array" else "object"} |type. |""".stripMargin.linesIterator.mkString(" ") @@ -122,20 +103,29 @@ object analysis { /** Compatibility analysis for the given statement and aligned mappings. */ final case class Analysis( + driver: String, sql: String, parameterAlignment: List[(Put[_], NullabilityKnown) Ior ParameterMeta], - columnAlignment: List[(Get[_], NullabilityKnown) Ior ColumnMeta]) { + columnAlignment: List[(Get[_], NullabilityKnown) Ior ColumnMeta] + ) { def parameterMisalignments: List[ParameterMisalignment] = parameterAlignment.zipWithIndex.collect { case (Ior.Left(_), n) => ParameterMisalignment(n + 1, None) case (Ior.Right(p), n) => ParameterMisalignment(n + 1, Some(p)) } + + private def hasParameterTypeErrors[A](put: Put[A], paramMeta: ParameterMeta): Boolean = { + val jdbcTypeMatches = put.jdbcTargets.contains_(paramMeta.jdbcType) + val vendorTypeMatches = put.vendorTypeNames.isEmpty || put.vendorTypeNames.contains_(paramMeta.vendorTypeName) + + !jdbcTypeMatches || !vendorTypeMatches + } def parameterTypeErrors: List[ParameterTypeError] = parameterAlignment.zipWithIndex.collect { - case (Ior.Both((j, n1), p), n) if !j.jdbcTargets.contains_(p.jdbcType) => - ParameterTypeError(n + 1, j, n1, p.jdbcType, p.vendorTypeName) + case (Ior.Both((put, n1), paramMeta), n) if hasParameterTypeErrors(put, paramMeta)=> + ParameterTypeError(n + 1, put, n1, paramMeta.jdbcType, paramMeta.vendorTypeName) } def columnMisalignments: List[ColumnMisalignment] = @@ -143,13 +133,16 @@ object analysis { case (Ior.Left(j), n) => ColumnMisalignment(n + 1, Left(j)) case (Ior.Right(p), n) => ColumnMisalignment(n + 1, Right(p)) } - + + private def hasColumnTypeError[A](get: Get[A], columnMeta: ColumnMeta): Boolean = { + val jdbcTypeMatches = (get.jdbcSources.toList ++ get.jdbcSourceSecondary).contains_(columnMeta.jdbcType) + val vendorTypeMatches = get.vendorTypeNames.isEmpty || get.vendorTypeNames.contains_(columnMeta.vendorTypeName) + !jdbcTypeMatches || !vendorTypeMatches + } def columnTypeErrors: List[ColumnTypeError] = columnAlignment.zipWithIndex.collect { - case (Ior.Both((j, n1), p), n) if !(j.jdbcSources.toList ++ j.jdbcSourceSecondary).contains_(p.jdbcType) => - ColumnTypeError(n + 1, j, n1, p) - case (Ior.Both((j, n1), p), n) if (p.jdbcType === JdbcType.JavaObject || p.jdbcType === JdbcType.Other) && !j.schemaTypes.headOption.contains_(p.vendorTypeName) => - ColumnTypeError(n + 1, j, n1, p) + case (Ior.Both((get, n1), p), n) if hasColumnTypeError(get, p) => + ColumnTypeError(n + 1, get, n1, p) } def columnTypeWarnings: List[ColumnTypeWarning] = @@ -224,6 +217,4 @@ object analysis { case Nullable => "NULL" case NullableUnknown => "NULL?" } - - } diff --git a/modules/core/src/main/scala/doobie/util/get.scala b/modules/core/src/main/scala/doobie/util/get.scala index 168ed5428..3ddb8da4e 100644 --- a/modules/core/src/main/scala/doobie/util/get.scala +++ b/modules/core/src/main/scala/doobie/util/get.scala @@ -14,12 +14,21 @@ import scala.reflect.ClassTag import org.tpolecat.typename._ import doobie.util.meta.Meta +/** + * + * @param typeStack List of types which provides the lineage of this Get instance + * @param jdbcSources Allowed JDBC types for query typechecking purposes + * @param jdbcSourceSecondary Alternative allowed JDBC types for query typechecking (warns) + * @param vendorTypeNames If non-empty, the column/parameter type reported + * by the database will be checked to match this list + * during typechecking against the database. + */ sealed abstract class Get[A]( val typeStack: NonEmptyList[Option[String]], val jdbcSources: NonEmptyList[JdbcType], val jdbcSourceSecondary: List[JdbcType], - val schemaTypes: List[String], - val get: Coyoneda[(ResultSet, Int) => *, A] + val vendorTypeNames: List[String], + val get: Coyoneda[(ResultSet, Int) => *, A], ) { @SuppressWarnings(Array("org.wartremover.warts.Throw")) @@ -55,8 +64,8 @@ sealed abstract class Get[A]( typeStack = typ :: typeStack, jdbcSources = jdbcSources, jdbcSourceSecondary = jdbcSourceSecondary, - schemaTypes = schemaTypes, - get = get.map(f) + vendorTypeNames = vendorTypeNames, + get = get.map(f), ) {} /** @@ -78,7 +87,7 @@ object Get extends GetInstances { def apply[A](implicit ev: Get[A]): ev.type = ev def derived[A](implicit ev: MkGet[A]): Get[A] = ev - + trait Auto { implicit def deriveGet[A](implicit ev: MkGet[A]): Get[A] = ev } @@ -90,28 +99,31 @@ object Get extends GetInstances { typeStack: NonEmptyList[Option[String]], jdbcSources: NonEmptyList[JdbcType], jdbcSourceSecondary: List[JdbcType], - get: Coyoneda[(ResultSet, Int) => *, A] + get: Coyoneda[(ResultSet, Int) => *, A], + checkedVendorType: Option[String], ): Get[A] = new Get[A]( typeStack, jdbcSources = jdbcSources, jdbcSourceSecondary = jdbcSourceSecondary, - schemaTypes = Nil, - get + vendorTypeNames = checkedVendorType.toList, + get = get, ) {} def many[A]( jdbcSources: NonEmptyList[JdbcType], jdbcSourceSecondary: List[JdbcType], - get: (ResultSet, Int) => A + get: (ResultSet, Int) => A, + checkedVendorType: Option[String], )(implicit ev: TypeName[A]): Get[A] = - Basic(NonEmptyList.of(Some(ev.value)), jdbcSources, jdbcSourceSecondary, Coyoneda.lift(get)) + Basic(NonEmptyList.of(Some(ev.value)), jdbcSources, jdbcSourceSecondary, Coyoneda.lift(get), checkedVendorType) def one[A: TypeName]( jdbcSources: JdbcType, jdbcSourceSecondary: List[JdbcType], - get: (ResultSet, Int) => A + get: (ResultSet, Int) => A, + checkedVendorType: Option[String], ): Get[A] = - many(NonEmptyList.of(jdbcSources), jdbcSourceSecondary, get) + many(NonEmptyList.of(jdbcSources), jdbcSourceSecondary, get, checkedVendorType) } @@ -121,45 +133,45 @@ object Get extends GetInstances { def apply[A]( typeStack: NonEmptyList[Option[String]], jdbcSources: NonEmptyList[JdbcType], - schemaTypes: NonEmptyList[String], - get: Coyoneda[(ResultSet, Int) => *, A] + vendorTypeNames: NonEmptyList[String], + get: Coyoneda[(ResultSet, Int) => *, A], ): Get[A] = new Get[A]( typeStack, jdbcSources = jdbcSources, jdbcSourceSecondary = Nil, - schemaTypes = schemaTypes.toList, - get + vendorTypeNames = vendorTypeNames.toList, + get = get, ) {} def many[A]( jdbcSources: NonEmptyList[JdbcType], - schemaTypes: NonEmptyList[String], - get: (ResultSet, Int) => A + vendorTypeNames: NonEmptyList[String], + get: (ResultSet, Int) => A, )(implicit ev: TypeName[A]): Get[A] = - Advanced(NonEmptyList.of(Some(ev.value)), jdbcSources, schemaTypes, Coyoneda.lift(get)) + Advanced(NonEmptyList.of(Some(ev.value)), jdbcSources, vendorTypeNames, Coyoneda.lift(get)) def one[A]( jdbcSource: JdbcType, - schemaTypes: NonEmptyList[String], - get: (ResultSet, Int) => A + vendorTypeNames: NonEmptyList[String], + get: (ResultSet, Int) => A, )(implicit ev: TypeName[A]): Get[A] = - Advanced(NonEmptyList.of(Some(ev.value)), NonEmptyList.of(jdbcSource), schemaTypes, Coyoneda.lift(get)) + Advanced(NonEmptyList.of(Some(ev.value)), NonEmptyList.of(jdbcSource), vendorTypeNames, Coyoneda.lift(get)) @SuppressWarnings(Array("org.wartremover.warts.Equals", "org.wartremover.warts.AsInstanceOf")) - def array[A >: Null <: AnyRef](schemaTypes: NonEmptyList[String]): Get[Array[A]] = - one(JdbcType.Array, schemaTypes, (r, n) => { + def array[A >: Null <: AnyRef](vendorTypeNames: NonEmptyList[String]): Get[Array[A]] = + one(JdbcType.Array, vendorTypeNames, (r, n) => { val a = r.getArray(n) (if (a == null) null else a.getArray).asInstanceOf[Array[A]] } ) @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf", "org.wartremover.warts.Throw")) - def other[A >: Null <: AnyRef: TypeName](schemaTypes: NonEmptyList[String])( + def other[A >: Null <: AnyRef: TypeName](vendorTypeNames: NonEmptyList[String])( implicit A: ClassTag[A] ): Get[A] = many( NonEmptyList.of(JdbcType.Other, JdbcType.JavaObject), - schemaTypes, + vendorTypeNames, (rs, n) => { rs.getObject(n) match { case null => null @@ -207,11 +219,17 @@ sealed abstract class MkGet[A]( override val typeStack: NonEmptyList[Option[String]], override val jdbcSources: NonEmptyList[JdbcType], override val jdbcSourceSecondary: List[JdbcType], - override val schemaTypes: List[String], - override val get: Coyoneda[(ResultSet, Int) => *, A] -) extends Get[A](typeStack, jdbcSources, jdbcSourceSecondary, schemaTypes, get) + override val vendorTypeNames: List[String], + override val get: Coyoneda[(ResultSet, Int) => *, A], +) extends Get[A](typeStack, jdbcSources, jdbcSourceSecondary, vendorTypeNames, get) object MkGet extends GetPlatform { def lift[A](g: Get[A]): MkGet[A] = - new MkGet[A](g.typeStack, g.jdbcSources, g.jdbcSourceSecondary, g.schemaTypes, g.get) {} + new MkGet[A]( + typeStack = g.typeStack, + jdbcSources = g.jdbcSources, + jdbcSourceSecondary = g.jdbcSourceSecondary, + vendorTypeNames = g.vendorTypeNames, + get = g.get, + ) {} } diff --git a/modules/core/src/main/scala/doobie/util/meta/legacymeta.scala b/modules/core/src/main/scala/doobie/util/meta/legacymeta.scala new file mode 100644 index 000000000..e751674bb --- /dev/null +++ b/modules/core/src/main/scala/doobie/util/meta/legacymeta.scala @@ -0,0 +1,33 @@ +// 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 { + + /** @group Instances */ + implicit val JavaTimeLocalDateMeta: Meta[java.time.LocalDate] = + Meta.DateMeta.imap(_.toLocalDate)(java.sql.Date.valueOf) + +} + +trait LegacyInstantMetaInstance { + + /** @group Instances */ + implicit val JavaTimeInstantMeta: Meta[java.time.Instant] = + Meta.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 d1ee01152..7aa9d83f8 100644 --- a/modules/core/src/main/scala/doobie/util/meta/meta.scala +++ b/modules/core/src/main/scala/doobie/util/meta/meta.scala @@ -39,7 +39,6 @@ final class Meta[A](val get: Get[A], val put: Put[A]) { object Meta extends MetaConstructors with MetaInstances with SqlMetaInstances - with TimeMetaInstances { /** Summon the `Meta` instance if possible. */ @@ -68,11 +67,12 @@ trait MetaConstructors { jdbcSourceSecondary: List[JdbcType], get: (ResultSet, Int) => A, put: (PreparedStatement, Int, A) => Unit, - update: (ResultSet, Int, A) => Unit + update: (ResultSet, Int, A) => Unit, + checkedVendorType: Option[String] = None, ): Meta[A] = new Meta( - Get.Basic.many(jdbcSource, jdbcSourceSecondary, get), - Put.Basic.many(jdbcTarget, put, update) + Get.Basic.many(jdbcSource, jdbcSourceSecondary, get, checkedVendorType), + Put.Basic.many(jdbcTarget, put, update, checkedVendorType) ) def one[A: TypeName]( @@ -80,24 +80,27 @@ trait MetaConstructors { jdbcSourceSecondary: List[JdbcType], get: (ResultSet, Int) => A, put: (PreparedStatement, Int, A) => Unit, - update: (ResultSet, Int, A) => Unit + update: (ResultSet, Int, A) => Unit, + checkedVendorType: Option[String] = None, ): Meta[A] = new Meta( - Get.Basic.one(jdbcType, jdbcSourceSecondary, get), - Put.Basic.one(jdbcType, put, update) + Get.Basic.one(jdbcType, jdbcSourceSecondary, get, checkedVendorType), + Put.Basic.one(jdbcType, put, update, checkedVendorType) ) def oneObject[A: TypeName]( jdbcType: JdbcType, - jdbcSourceSecondary: List[JdbcType], - clazz: Class[A] - ): Meta[A] = one( - jdbcType = jdbcType, - jdbcSourceSecondary = jdbcSourceSecondary, - _.getObject(_, clazz), - _.setObject(_, _), - _.updateObject(_, _) - ) + checkedVendorType: Option[String], + clazz: Class[A], + ): Meta[A] = + one( + jdbcType = jdbcType, + jdbcSourceSecondary = Nil, + get = _.getObject(_, clazz), + put = _.setObject(_, _), + update = _.updateObject(_, _), + checkedVendorType = checkedVendorType, + ) } /** @@ -108,26 +111,26 @@ trait MetaConstructors { def many[A: TypeName]( jdbcTypes: NonEmptyList[JdbcType], - schemaTypes: NonEmptyList[String], + vendorTypeNames: NonEmptyList[String], get: (ResultSet, Int) => A, put: (PreparedStatement, Int, A) => Unit, - update: (ResultSet, Int, A) => Unit + update: (ResultSet, Int, A) => Unit, ): Meta[A] = new Meta( - Get.Advanced.many(jdbcTypes, schemaTypes, get), - Put.Advanced.many(jdbcTypes, schemaTypes, put, update) + Get.Advanced.many(jdbcTypes, vendorTypeNames, get), + Put.Advanced.many(jdbcTypes, vendorTypeNames, put, update) ) def one[A: TypeName]( jdbcTypes: JdbcType, - schemaTypes: NonEmptyList[String], + vendorTypeNames: NonEmptyList[String], get: (ResultSet, Int) => A, put: (PreparedStatement, Int, A) => Unit, - update: (ResultSet, Int, A) => Unit + update: (ResultSet, Int, A) => Unit, ): Meta[A] = new Meta( - Get.Advanced.one(jdbcTypes, schemaTypes, get), - Put.Advanced.one(jdbcTypes, schemaTypes, put, update) + Get.Advanced.one(jdbcTypes, vendorTypeNames, get), + Put.Advanced.one(jdbcTypes, vendorTypeNames, put, update) ) def array[A >: Null <: AnyRef]( @@ -153,8 +156,11 @@ trait MetaConstructors { } -trait MetaInstances { this: MetaConstructors => +object MetaConstructors extends MetaConstructors + +trait MetaInstances { import doobie.enumerated.JdbcType.{Boolean => JdbcBoolean, _} + import doobie.util.meta.MetaConstructors.Basic /** @group Instances */ implicit val GetPutInvariant: Invariant[Meta] = 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 0969144dc..83e2b243b 100644 --- a/modules/core/src/main/scala/doobie/util/meta/sqlmeta.scala +++ b/modules/core/src/main/scala/doobie/util/meta/sqlmeta.scala @@ -5,8 +5,9 @@ package doobie.util.meta import doobie.enumerated.JdbcType._ +import MetaConstructors.Basic -trait SqlMetaInstances { this: MetaConstructors => +trait SqlMetaInstances { /** @group Instances */ implicit val DateMeta: Meta[java.sql.Date] = 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 ba30c4a1b..71bb3ada0 100644 --- a/modules/core/src/main/scala/doobie/util/meta/timemeta.scala +++ b/modules/core/src/main/scala/doobie/util/meta/timemeta.scala @@ -5,32 +5,37 @@ package doobie.util.meta import doobie.enumerated.JdbcType._ +import doobie.util.meta.MetaConstructors.Basic /** - * Instances for Java time classes that follow the JDBC specification. + * Basic instances for Java time classes that follow the JDBC specification. + * These instances lack more precise type checking (by checking against the + * vendor type name for columns and parameters) so this should only be used if + * you're not using one of the databases which doobie has more precise + * Meta instances for. (e.g. PostgreSQL / MySQL) */ -trait TimeMetaInstances { this: MetaConstructors => +trait TimeMetaInstances { import Predef.classOf /** @group Instances */ implicit val JavaOffsetDateTimeMeta: Meta[java.time.OffsetDateTime] = - Basic.oneObject(TimestampWithTimezone, Nil, classOf[java.time.OffsetDateTime]) + Basic.oneObject( + jdbcType = Timestamp, + checkedVendorType = None, + clazz = classOf[java.time.OffsetDateTime], + ) /** @group Instances */ implicit val JavaLocalDateMeta: Meta[java.time.LocalDate] = - Basic.oneObject(Date, Nil, classOf[java.time.LocalDate]) + Basic.oneObject(jdbcType = Date, checkedVendorType = None, clazz = classOf[java.time.LocalDate]) /** @group Instances */ implicit val JavaLocalTimeMeta: Meta[java.time.LocalTime] = - Basic.oneObject(Time, Nil, classOf[java.time.LocalTime]) + Basic.oneObject(jdbcType = Time, checkedVendorType = None, clazz = classOf[java.time.LocalTime]) /** @group Instances */ implicit val JavaLocalDateTimeMeta: Meta[java.time.LocalDateTime] = - Basic.oneObject(Timestamp, Nil, classOf[java.time.LocalDateTime]) - - /** @group Instances */ - implicit val JavaOffsetTimeMeta: Meta[java.time.OffsetTime] = - Basic.oneObject(TimeWithTimezone, Nil, classOf[java.time.OffsetTime]) + Basic.oneObject(jdbcType = Timestamp, checkedVendorType = None, clazz = classOf[java.time.LocalDateTime]) // extra instances not in the spec diff --git a/modules/core/src/main/scala/doobie/util/package.scala b/modules/core/src/main/scala/doobie/util/package.scala index f1cf8cc3e..7ea3bcdcb 100644 --- a/modules/core/src/main/scala/doobie/util/package.scala +++ b/modules/core/src/main/scala/doobie/util/package.scala @@ -11,5 +11,4 @@ package object util { private[util] def void(a: Any*): Unit = (a, ())._2 - } diff --git a/modules/core/src/main/scala/doobie/util/put.scala b/modules/core/src/main/scala/doobie/util/put.scala index eefb8d30c..ce067d284 100644 --- a/modules/core/src/main/scala/doobie/util/put.scala +++ b/modules/core/src/main/scala/doobie/util/put.scala @@ -11,23 +11,32 @@ import doobie.enumerated.JdbcType import java.sql.{PreparedStatement, ResultSet} import org.tpolecat.typename._ import doobie.util.meta.Meta +import cats.syntax.foldable._ import scala.reflect.ClassTag +/** + * + * @param typeStack List of types which provides the lineage of this Put instance + * @param jdbcTargets Allowed JDBC types for parameter-setting and query typechecking purposes + * @param vendorTypeNames If non-empty, the column/parameter type reported + * by the database will be checked to match this list + * during typechecking against the database. + */ sealed abstract class Put[A]( val typeStack: NonEmptyList[Option[String]], val jdbcTargets: NonEmptyList[JdbcType], - val schemaTypes: List[String], + val vendorTypeNames: List[String], val put: ContravariantCoyoneda[(PreparedStatement, Int, *) => Unit, A], - val update: ContravariantCoyoneda[(ResultSet, Int, *) => Unit, A] + val update: ContravariantCoyoneda[(ResultSet, Int, *) => Unit, A], ) { def unsafeSetNull(ps: PreparedStatement, n: Int): Unit = { val sqlType = jdbcTargets.head.toInt - schemaTypes.headOption match { + vendorTypeNames.headOption match { case None => ps.setNull(n, sqlType) - case Some(schemaType) => ps.setNull(n, sqlType, schemaType) + case Some(vendorTypeName) => ps.setNull(n, sqlType, vendorTypeName) } } @@ -41,9 +50,9 @@ sealed abstract class Put[A]( new Put[B]( typeStack = typ :: typeStack, jdbcTargets = jdbcTargets, - schemaTypes = schemaTypes, + vendorTypeNames = vendorTypeNames, put = put.contramap(f), - update = update.contramap(f) + update = update.contramap(f), ) {} @SuppressWarnings(Array("org.wartremover.warts.Equals")) @@ -67,6 +76,10 @@ sealed abstract class Put[A]( case Some(a) => unsafeUpdateNonNullable(rs, n, a) case None => rs.updateNull(n) } + + override def toString(): String = { + s"Put(typeStack=${typeStack.mkString_(",")}, jdbcTargets=${jdbcTargets.mkString_(",")}, vendorTypeNames=${vendorTypeNames.mkString_(",")})" + } } @@ -86,27 +99,37 @@ object Put extends PutInstances { typeStack: NonEmptyList[Option[String]], jdbcTargets: NonEmptyList[JdbcType], put: ContravariantCoyoneda[(PreparedStatement, Int, *) => Unit, A], - update: ContravariantCoyoneda[(ResultSet, Int, *) => Unit, A] - ): Put[A] = new Put[A](typeStack, jdbcTargets, schemaTypes = Nil, put, update) {} + update: ContravariantCoyoneda[(ResultSet, Int, *) => Unit, A], + checkedVendorType: Option[String], + ): Put[A] = new Put[A]( + typeStack = typeStack, + jdbcTargets = jdbcTargets, + vendorTypeNames = checkedVendorType.toList, + put = put, + update = update, + ) {} def many[A]( jdbcTargets: NonEmptyList[JdbcType], put: (PreparedStatement, Int, A) => Unit, - update: (ResultSet, Int, A) => Unit + update: (ResultSet, Int, A) => Unit, + checkedVendorType: Option[String], )(implicit ev: TypeName[A]): Put[A] = Basic( NonEmptyList.of(Some(ev.value)), jdbcTargets, ContravariantCoyoneda.lift[(PreparedStatement, Int, *) => Unit, A](put), - ContravariantCoyoneda.lift[(ResultSet, Int, *) => Unit, A](update) + ContravariantCoyoneda.lift[(ResultSet, Int, *) => Unit, A](update), + checkedVendorType ) def one[A]( jdbcTarget: JdbcType, put: (PreparedStatement, Int, A) => Unit, - update: (ResultSet, Int, A) => Unit + update: (ResultSet, Int, A) => Unit, + checkedVendorType: Option[String], )(implicit ev: TypeName[A]): Put[A] = - many(NonEmptyList.of(jdbcTarget), put, update) + many(NonEmptyList.of(jdbcTarget), put, update, checkedVendorType) } @@ -115,41 +138,47 @@ object Put extends PutInstances { def apply[A]( typeStack: NonEmptyList[Option[String]], jdbcTargets: NonEmptyList[JdbcType], - schemaTypes: NonEmptyList[String], + vendorTypeNames: NonEmptyList[String], put: ContravariantCoyoneda[(PreparedStatement, Int, *) => Unit, A], - update: ContravariantCoyoneda[(ResultSet, Int, *) => Unit, A] - ): Put[A] = new Put[A](typeStack, jdbcTargets, schemaTypes.toList, put, update) {} + update: ContravariantCoyoneda[(ResultSet, Int, *) => Unit, A], + ): Put[A] = new Put[A]( + typeStack = typeStack, + jdbcTargets = jdbcTargets, + vendorTypeNames = vendorTypeNames.toList, + put = put, + update = update, + ) {} def many[A]( jdbcTargets: NonEmptyList[JdbcType], - schemaTypes: NonEmptyList[String], + vendorTypeNames: NonEmptyList[String], put: (PreparedStatement, Int, A) => Unit, - update: (ResultSet, Int, A) => Unit + update: (ResultSet, Int, A) => Unit, )(implicit ev: TypeName[A]): Put[A] = Advanced( NonEmptyList.of(Some(ev.value)), jdbcTargets, - schemaTypes, + vendorTypeNames, ContravariantCoyoneda.lift[(PreparedStatement, Int, *) => Unit, A](put), - ContravariantCoyoneda.lift[(ResultSet, Int, *) => Unit, A](update) + ContravariantCoyoneda.lift[(ResultSet, Int, *) => Unit, A](update), ) def one[A: TypeName]( jdbcTarget: JdbcType, - schemaTypes: NonEmptyList[String], + vendorTypeNames: NonEmptyList[String], put: (PreparedStatement, Int, A) => Unit, - update: (ResultSet, Int, A) => Unit + update: (ResultSet, Int, A) => Unit, ): Put[A] = - many(NonEmptyList.of(jdbcTarget), schemaTypes, put, update) + many(NonEmptyList.of(jdbcTarget), vendorTypeNames, put, update) @SuppressWarnings(Array("org.wartremover.warts.Equals", "org.wartremover.warts.AsInstanceOf")) def array[A >: Null <: AnyRef]( - schemaTypes: NonEmptyList[String], + vendorTypeNames: NonEmptyList[String], elementType: String ): Put[Array[A]] = one( JdbcType.Array, - schemaTypes, + vendorTypeNames, (ps, n, a) => { val conn = ps.getConnection val arr = conn.createArrayOf(elementType, a.asInstanceOf[Array[AnyRef]]) @@ -163,10 +192,10 @@ object Put extends PutInstances { } ) - def other[A >: Null <: AnyRef: TypeName](schemaTypes: NonEmptyList[String]): Put[A] = + def other[A >: Null <: AnyRef: TypeName](vendorTypeNames: NonEmptyList[String]): Put[A] = many( NonEmptyList.of(JdbcType.Other, JdbcType.JavaObject), - schemaTypes, + vendorTypeNames, (ps, n, a) => ps.setObject(n, a), (rs, n, a) => rs.updateObject(n, a) ) @@ -203,12 +232,18 @@ trait PutInstances { sealed abstract class MkPut[A]( override val typeStack: NonEmptyList[Option[String]], override val jdbcTargets: NonEmptyList[JdbcType], - override val schemaTypes: List[String], + override val vendorTypeNames: List[String], override val put: ContravariantCoyoneda[(PreparedStatement, Int, *) => Unit, A], - override val update: ContravariantCoyoneda[(ResultSet, Int, *) => Unit, A] -) extends Put[A](typeStack, jdbcTargets, schemaTypes, put, update) + override val update: ContravariantCoyoneda[(ResultSet, Int, *) => Unit, A], +) extends Put[A](typeStack, jdbcTargets, vendorTypeNames, put, update) object MkPut extends PutPlatform { def lift[A](g: Put[A]): MkPut[A] = - new MkPut[A](g.typeStack, g.jdbcTargets, g.schemaTypes, g.put, g.update) {} + new MkPut[A]( + typeStack = g.typeStack, + jdbcTargets = g.jdbcTargets, + vendorTypeNames = g.vendorTypeNames, + put = g.put, + update = g.update, + ) {} } diff --git a/modules/docs/src/main/mdoc/docs/17-FAQ.md b/modules/docs/src/main/mdoc/docs/17-FAQ.md index 7062ab7c1..279584421 100644 --- a/modules/docs/src/main/mdoc/docs/17-FAQ.md +++ b/modules/docs/src/main/mdoc/docs/17-FAQ.md @@ -202,3 +202,19 @@ implicit val nesMeta: Meta[NonEmptyString] = { distinct.string("nes").imap(NonEmptyString.apply)(_.value) } ``` + +## How do I use `java.time` types with Doobie? + +The following imports will provide `Meta` instances for common java.time.* types: + +| Database driver | Import | +| --- |-------------------------------| +| Postgres (org.postgresql.Driver) | `doobie.postgres.implicits.*` | +| MySQL (com.mysql.jdbc.Driver) | `doobie.mysql.implicits.*` | + +For other databases, if your JDBC driver supports the java.time types natively, +you can use `import doobie.implicits.javatimedrivernative._`. + +References: + +- [Postgres JDBC - Using Java 8 Date and Time classes](https://jdbc.postgresql.org/documentation/query/#using-java-8-date-and-time-classes) diff --git a/modules/example/src/main/scala/example/AnalysisTest.scala b/modules/example/src/main/scala/example/AnalysisTest.scala index 081d54d51..d4c38ac3f 100644 --- a/modules/example/src/main/scala/example/AnalysisTest.scala +++ b/modules/example/src/main/scala/example/AnalysisTest.scala @@ -30,12 +30,7 @@ object AnalysisTest { val arrayTest = sql""" SELECT ARRAY[1, 2, NULL] test - """.query[Option[List[String]]] - - val arrayTest2 = - sql""" - SELECT ARRAY[1, 2, NULL] test - """.query[String] + """.query[Option[List[Option[Int]]]] val pointTest = sql""" diff --git a/modules/example/src/main/scala/example/OtherSchema.scala b/modules/example/src/main/scala/example/OtherSchema.scala index e3faf77b7..225b9f720 100644 --- a/modules/example/src/main/scala/example/OtherSchema.scala +++ b/modules/example/src/main/scala/example/OtherSchema.scala @@ -24,22 +24,23 @@ import org.postgresql.util._ object OtherSchema extends IOApp.Simple { // Ok this mapping goes via String when reading and PGObject when writing, and it understands - // when the type is reported as OTHER (schemaType). - def wackyPostgresMapping(schemaName: String): Meta[String] = + // when the JDBC type is reported as OTHER and uses the venderTypeName for setting parameters + // and typechecking. + def wackyPostgresMapping(venderTypeName: String): Meta[String] = Meta.Advanced.many[String]( NonEmptyList.of(JdbcType.Other, JdbcType.VarChar), - NonEmptyList.of(schemaName), + NonEmptyList.of(venderTypeName), (rs, n) => rs.getString(n), (ps, n, a) => { val o = new PGobject o.setValue(a.toString) - o.setType(schemaName) + o.setType(venderTypeName) ps.setObject(n, o) }, (rs, n, a) => { val o = new PGobject o.setValue(a.toString) - o.setType(schemaName) + o.setType(venderTypeName) rs.updateObject(n, o) } ) diff --git a/modules/example/src/test/scala-2/example/AnalysisTestScalaTest.scala b/modules/example/src/test/scala-2/example/AnalysisTestScalaTest.scala index 8b7f4b5d6..ae7c61c27 100644 --- a/modules/example/src/test/scala-2/example/AnalysisTestScalaTest.scala +++ b/modules/example/src/test/scala-2/example/AnalysisTestScalaTest.scala @@ -9,7 +9,7 @@ import doobie.scalatest.IOChecker import doobie.util.transactor.Transactor import org.scalatest._ -class AnalysisTestScalaCheck extends funsuite.AnyFunSuite with matchers.must.Matchers with IOChecker { +class AnalysisTestScalaTest extends funsuite.AnyFunSuite with matchers.must.Matchers with IOChecker { val transactor = Transactor.fromDriverManager[IO]( driver = "org.postgresql.Driver", url = "jdbc:postgresql:world", user = "postgres", password = "password", logHandler = None @@ -19,10 +19,9 @@ class AnalysisTestScalaCheck extends funsuite.AnyFunSuite with matchers.must.Mat //test("speakerQuery") { check(AnalysisTest.speakerQuery(null, 0)) } test("speakerQuery2") { check(AnalysisTest.speakerQuery2) } test("arrayTest") { check(AnalysisTest.arrayTest) } -//test("arrayTest2") { check(AnalysisTest.arrayTest2) } test("pointTest") { check(AnalysisTest.pointTest) } //test("pointTest2") { check(AnalysisTest.pointTest2) } -//test("update") { check(AnalysisTest.update("foo", 42)) } + test("update") { check(AnalysisTest.update) } test("update2") { check(AnalysisTest.update0_2) } } diff --git a/modules/example/src/test/scala-2/example/AnalysisTestSpecs2.scala b/modules/example/src/test/scala-2/example/AnalysisTestSpecs2.scala index cf0f78507..55ddeb2be 100644 --- a/modules/example/src/test/scala-2/example/AnalysisTestSpecs2.scala +++ b/modules/example/src/test/scala-2/example/AnalysisTestSpecs2.scala @@ -19,7 +19,6 @@ class AnalysisTestSpecs2 extends Specification with IOChecker { // check(AnalysisTest.speakerQuery(null, 0)) check(AnalysisTest.speakerQuery2) check(AnalysisTest.arrayTest) - // check(AnalysisTest.arrayTest2) check(AnalysisTest.pointTest) // check(AnalysisTest.pointTest2) checkOutput(AnalysisTest.update) diff --git a/modules/h2-circe/src/main/scala/doobie/h2/circe/Instances.scala b/modules/h2-circe/src/main/scala/doobie/h2/circe/Instances.scala index 3122fc3a6..55701c667 100644 --- a/modules/h2-circe/src/main/scala/doobie/h2/circe/Instances.scala +++ b/modules/h2-circe/src/main/scala/doobie/h2/circe/Instances.scala @@ -5,7 +5,6 @@ package doobie.h2.circe import cats.Show -import cats.data.NonEmptyList import cats.syntax.all._ import doobie.enumerated.JdbcType import io.circe._ @@ -21,22 +20,26 @@ object Instances { 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) + Put.Basic.one[Array[Byte]]( + jdbcTarget = JdbcType.VarChar, + put = (ps, n, a) => ps.setBytes(n, a), + update = (rs, n, a) => rs.updateBytes(n, a), + checkedVendorType = Some("VARCHAR") ) .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) + Get.Basic.one[Array[Byte]]( + jdbcSources = JdbcType.Other, + jdbcSourceSecondary = List.empty, + get = (rs, n) => rs.getBytes(n), + checkedVendorType = Some("JSON") ) + .temap(a => + parse(a.show).leftMap(_.show) + ) def h2EncoderPutT[A: Encoder]: Put[A] = Put[Json].tcontramap(_.asJson) diff --git a/modules/h2/src/main/scala/doobie/h2/H2JavaTimeMetaInstances.scala b/modules/h2/src/main/scala/doobie/h2/H2JavaTimeMetaInstances.scala new file mode 100644 index 000000000..5cd40d042 --- /dev/null +++ b/modules/h2/src/main/scala/doobie/h2/H2JavaTimeMetaInstances.scala @@ -0,0 +1,46 @@ +// 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 + +import doobie.enumerated.JdbcType._ +import doobie.util.meta.Meta +import doobie.util.meta.MetaConstructors.Basic + +/** + * Instances for Java time classes that follow the JDBC specification. + */ +trait H2JavaTimeMetaInstances { + import Predef.classOf + + implicit val JavaOffsetDateTimeMeta: Meta[java.time.OffsetDateTime] = + Basic.oneObject( + jdbcType = TimestampWithTimezone, + checkedVendorType = Some("TIMESTAMP WITH TIME ZONE"), + clazz = classOf[java.time.OffsetDateTime], + ) + + implicit val JavaLocalDateMeta: Meta[java.time.LocalDate] = + Basic.oneObject(jdbcType = Date, checkedVendorType = None, clazz = classOf[java.time.LocalDate]) + + implicit val JavaLocalTimeMeta: Meta[java.time.LocalTime] = + Basic.oneObject(jdbcType = Time, checkedVendorType = Some("TIME"), clazz = classOf[java.time.LocalTime]) + + implicit val JavaLocalDateTimeMeta: Meta[java.time.LocalDateTime] = + Basic.oneObject(jdbcType = Timestamp, checkedVendorType = Some("TIMESTAMP"), clazz = classOf[java.time.LocalDateTime]) + + implicit val JavaOffsetTimeMeta: Meta[java.time.OffsetTime] = + Basic.oneObject( + jdbcType = TimeWithTimezone, + checkedVendorType = Some("TIME WITH TIME ZONE"), + clazz = classOf[java.time.OffsetTime] + ) + + implicit val JavaInstantMeta: Meta[java.time.Instant] = + JavaOffsetDateTimeMeta.timap(_.toInstant)(_.atOffset(java.time.ZoneOffset.UTC)) + + implicit val JavaTimeZoneId: Meta[java.time.ZoneId] = + doobie.implicits.javatimedrivernative.JavaTimeZoneId + +} diff --git a/modules/h2/src/main/scala/doobie/h2/Metas.scala b/modules/h2/src/main/scala/doobie/h2/Metas.scala index d3b5503ab..127b88276 100644 --- a/modules/h2/src/main/scala/doobie/h2/Metas.scala +++ b/modules/h2/src/main/scala/doobie/h2/Metas.scala @@ -14,7 +14,7 @@ import cats.data.NonEmptyList.{ of => NonEmptyListOf } import doobie.util.meta.Meta trait Instances { - + implicit val UuidType: Meta[UUID] = Meta.Advanced.many[UUID]( NonEmptyListOf(JdbcType.Binary), diff --git a/modules/h2/src/main/scala/doobie/h2/package.scala b/modules/h2/src/main/scala/doobie/h2/package.scala index 766c088e3..de78afa1e 100644 --- a/modules/h2/src/main/scala/doobie/h2/package.scala +++ b/modules/h2/src/main/scala/doobie/h2/package.scala @@ -13,5 +13,6 @@ package object h2 { object implicits extends Instances with syntax.ToH2TransactorOps + with H2JavaTimeMetaInstances } diff --git a/modules/h2/src/test/scala/doobie/h2/h2types.scala b/modules/h2/src/test/scala/doobie/h2/h2types.scala index f5b636f6a..4c5c0c745 100644 --- a/modules/h2/src/test/scala/doobie/h2/h2types.scala +++ b/modules/h2/src/test/scala/doobie/h2/h2types.scala @@ -10,7 +10,7 @@ import cats.effect.IO import doobie._ import doobie.implicits._ import doobie.h2.implicits._ -import doobie.util.analysis.{Analysis, ColumnTypeError, ColumnTypeWarning} +import doobie.util.analysis.{Analysis, ColumnTypeError} import doobie.util.arbitraries.SQLArbitraries._ import doobie.util.arbitraries.StringArbitraries._ import org.scalacheck.Prop.forAll @@ -207,10 +207,6 @@ class h2typesspec extends munit.ScalaCheckSuite { 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/PostgresJavaTimeMetaInstances.scala b/modules/postgres/src/main/scala/doobie/postgres/PostgresJavaTimeMetaInstances.scala new file mode 100644 index 000000000..c8ebd241b --- /dev/null +++ b/modules/postgres/src/main/scala/doobie/postgres/PostgresJavaTimeMetaInstances.scala @@ -0,0 +1,62 @@ +// 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.postgres + +import doobie.enumerated.JdbcType._ +import doobie.util.meta.Meta +import doobie.util.meta.MetaConstructors.Basic + +/** + * Instances for Java time classes that follow the JDBC specification. + */ +trait PostgresJavaTimeMetaInstances { + import Predef.classOf + + /** @group Instances */ + implicit val JavaOffsetDateTimeMeta: Meta[java.time.OffsetDateTime] = + Basic.oneObject( + jdbcType = Timestamp, + checkedVendorType = Some("timestamptz"), + clazz = classOf[java.time.OffsetDateTime], + ) + + /** @group Instances */ + implicit val JavaLocalDateMeta: Meta[java.time.LocalDate] = + Basic.oneObject(Date, None, classOf[java.time.LocalDate]) + + /** @group Instances */ + implicit val JavaLocalTimeMeta: Meta[java.time.LocalTime] = + Basic.oneObject(Time, Some("time"), classOf[java.time.LocalTime]) + + /** @group Instances */ + implicit val JavaLocalDateTimeMeta: Meta[java.time.LocalDateTime] = + Basic.oneObject(Timestamp, Some("timestamp"), classOf[java.time.LocalDateTime]) + + /** @group Instances */ + implicit val JavaOffsetTimeMeta: Meta[java.time.OffsetTime] = + Basic.oneObject( + Time, + Some("timetz"), + classOf[java.time.OffsetTime] + ) + + // extra instances not in the spec + + /** @group Instances */ + implicit val JavaInstantMeta: Meta[java.time.Instant] = + JavaOffsetDateTimeMeta.timap(_.toInstant)(_.atOffset(java.time.ZoneOffset.UTC)) + + /** @group Instances */ + 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/postgres/src/main/scala/doobie/postgres/package.scala b/modules/postgres/src/main/scala/doobie/postgres/package.scala index bfdba2b77..314b9665b 100644 --- a/modules/postgres/src/main/scala/doobie/postgres/package.scala +++ b/modules/postgres/src/main/scala/doobie/postgres/package.scala @@ -4,6 +4,8 @@ package doobie +import doobie.postgres.PostgresJavaTimeMetaInstances + package object postgres extends postgres.free.Types with postgres.free.Modules @@ -15,6 +17,7 @@ package object postgres with syntax.ToPostgresMonadErrorOps with syntax.ToFragmentOps with syntax.ToPostgresExplainOps + with PostgresJavaTimeMetaInstances object pgisimplicits extends PgisInstances diff --git a/modules/postgres/src/test/scala/doobie/postgres/CheckSuite.scala b/modules/postgres/src/test/scala/doobie/postgres/CheckSuite.scala index 276f8a770..01c5dc870 100644 --- a/modules/postgres/src/test/scala/doobie/postgres/CheckSuite.scala +++ b/modules/postgres/src/test/scala/doobie/postgres/CheckSuite.scala @@ -6,6 +6,7 @@ package doobie.postgres import doobie._ import doobie.implicits._ +import doobie.postgres.implicits._ import doobie.postgres.enums._ import doobie.util.analysis.{ColumnTypeError, ParameterTypeError} import java.time.{Instant, LocalDate, LocalDateTime, LocalTime, OffsetDateTime, OffsetTime} diff --git a/modules/postgres/src/test/scala/doobie/postgres/LOSuite.scala b/modules/postgres/src/test/scala/doobie/postgres/LOSuite.scala index 84a188a25..7ce763574 100644 --- a/modules/postgres/src/test/scala/doobie/postgres/LOSuite.scala +++ b/modules/postgres/src/test/scala/doobie/postgres/LOSuite.scala @@ -14,7 +14,7 @@ class LOSuite extends munit.FunSuite with FileEquality { import PostgresTestTransactor.xa // A big file. Contents are irrelevant. - val in = new File("init/test-db.sql") + val in = new File("init/postgres/test-db.sql") test("large object support should allow round-trip from file to large object and back") { val out = File.createTempFile("doobie", "tst")