Skip to content

Commit

Permalink
Improve 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 3537fc9 commit 1657047
Show file tree
Hide file tree
Showing 11 changed files with 173 additions and 137 deletions.
4 changes: 3 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ services:
ports:
- 5432:5432
volumes:
- ./init/:/docker-entrypoint-initdb.d/
- ./init/postgres/:/docker-entrypoint-initdb.d/

mysql:
image: mysql:5.7-debian
Expand All @@ -20,3 +20,5 @@ services:
MYSQL_DATABASE: world
ports:
- 3306:3306
volumes:
- ./init/mysql/:/docker-entrypoint-initdb.d/
11 changes: 11 additions & 0 deletions init/mysql/test-table.sql
Original file line number Diff line number Diff line change
@@ -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');
File renamed without changes.
51 changes: 29 additions & 22 deletions modules/core/src/main/scala/doobie/hi/connection.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 */
Expand Down
61 changes: 33 additions & 28 deletions modules/core/src/main/scala/doobie/util/analysis.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -122,44 +103,53 @@ 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]
) {

private val parameterAlignment_ = parameterAlignment.map(_.map { m =>
m.copy(jdbcType = tweakMetaJdbcType(driver, m.jdbcType, vendorTypeName = m.vendorTypeName))
})
private val columnAlignment_ = columnAlignment.map(_.map { m =>
m.copy(jdbcType = tweakMetaJdbcType(driver, m.jdbcType, vendorTypeName = m.vendorTypeName))
})

def parameterMisalignments: List[ParameterMisalignment] =
parameterAlignment.zipWithIndex.collect {
parameterAlignment_.zipWithIndex.collect {
case (Ior.Left(_), n) => ParameterMisalignment(n + 1, None)
case (Ior.Right(p), n) => ParameterMisalignment(n + 1, Some(p))
}

def parameterTypeErrors: List[ParameterTypeError] =
parameterAlignment.zipWithIndex.collect {
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)
}

def columnMisalignments: List[ColumnMisalignment] =
columnAlignment.zipWithIndex.collect {
columnAlignment_.zipWithIndex.collect {
case (Ior.Left(j), n) => ColumnMisalignment(n + 1, Left(j))
case (Ior.Right(p), n) => ColumnMisalignment(n + 1, Right(p))
}

def columnTypeErrors: List[ColumnTypeError] =
columnAlignment.zipWithIndex.collect {
columnAlignment_.zipWithIndex.collect {
case (Ior.Both((j, n1), p), n) if !(j.jdbcSources.toList ++ j.fold(_.jdbcSourceSecondary.toList, _ => Nil)).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.fold(_ => None, a => Some(a.schemaTypes.head)).contains_(p.vendorTypeName) =>
ColumnTypeError(n + 1, j, n1, p)
}

def columnTypeWarnings: List[ColumnTypeWarning] =
columnAlignment.zipWithIndex.collect {
columnAlignment_.zipWithIndex.collect {
case (Ior.Both((j, n1), p), n) if j.fold(_.jdbcSourceSecondary.toList, _ => Nil).contains_(p.jdbcType) =>
ColumnTypeWarning(n + 1, j, n1, p)
}

def nullabilityMisalignments: List[NullabilityMisalignment] =
columnAlignment.zipWithIndex.collect {
columnAlignment_.zipWithIndex.collect {
// We can't do anything helpful with NoNulls .. it means "might not be nullable"
// case (Ior.Both((st, Nullable), ColumnMeta(_, _, NoNulls, col)), n) => NullabilityMisalignment(n + 1, col, st, NoNulls, Nullable)
case (Ior.Both((st, NoNulls), ColumnMeta(_, _, Nullable, col)), n) => NullabilityMisalignment(n + 1, col, st.typeStack.last, Nullable, NoNulls)
Expand All @@ -179,7 +169,7 @@ object analysis {
/** Description of each parameter, paired with its errors. */
lazy val paramDescriptions: List[(String, List[AlignmentError])] = {
val params: Block =
parameterAlignment.zipWithIndex.map {
parameterAlignment_.zipWithIndex.map {
case (Ior.Both((j1, n1), ParameterMeta(j2, s2, _, _)), i) => List(f"P${i+1}%02d", show"${typeName(j1.typeStack.last, n1)}", "", j2.show.toUpperCase, show"($s2)")
case (Ior.Left((j1, n1)), i) => List(f"P${i+1}%02d", show"${typeName(j1.typeStack.last, n1)}", "", "", "")
case (Ior.Right( ParameterMeta(j2, s2, _, _)), i) => List(f"P${i+1}%02d", "", "", j2.show.toUpperCase, show"($s2)")
Expand All @@ -193,7 +183,7 @@ object analysis {
lazy val columnDescriptions: List[(String, List[AlignmentError])] = {
import pretty._
val cols: Block =
columnAlignment.zipWithIndex.map {
columnAlignment_.zipWithIndex.map {
case (Ior.Both((j1, n1), ColumnMeta(j2, s2, n2, m)), i) => List(f"C${i+1}%02d", m, j2.show.toUpperCase, show"(${s2.toString})", formatNullability(n2), "", typeName(j1.typeStack.last, n1))
case (Ior.Left((j1, n1)), i) => List(f"C${i+1}%02d", "", "", "", "", "", typeName(j1.typeStack.last, n1))
case (Ior.Right( ColumnMeta(j2, s2, n2, m)), i) => List(f"C${i+1}%02d", m, j2.show.toUpperCase, show"(${s2.toString})", formatNullability(n2), "", "")
Expand Down Expand Up @@ -225,5 +215,20 @@ object analysis {
case NullableUnknown => "NULL?"
}

private val MySQLDriverName = "MySQL Connector/J"

// tweaks to the types returned by JDBC to improve analysis
private def tweakMetaJdbcType(driver: String, jdbcType: JdbcType, vendorTypeName: String) = jdbcType match {
// the Postgres driver does not return *WithTimezone JDBC types for *tz column types
// 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

// MySQL timestamp columns are returned as Timestamp
case JdbcType.Timestamp
if vendorTypeName.compareToIgnoreCase("timestamp") == 0 && driver == MySQLDriverName => JdbcType.TimestampWithTimezone

case t => t
}
}
53 changes: 32 additions & 21 deletions modules/mysql/src/main/scala/doobie/mysql/JavaTimeInstances.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,50 +4,61 @@

package doobie.mysql

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

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

import java.time.Instant
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.OffsetDateTime
import java.time.OffsetTime
import java.time.ZoneOffset

/**
* Instances for JSR-310 date time types.
*
* Note that to ensure instants are preserved you may need to use one of the solutions described
* in [[https://docs.oracle.com/cd/E17952_01/connector-j-8.0-en/connector-j-time-instants.html]].
*/
trait JavaTimeInstances extends MetaConstructors {

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

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

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

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

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

implicit val JavaTimeOffsetTimeMeta: Meta[java.time.OffsetTime] =
implicit val JavaTimeOffsetTimeMeta: Meta[OffsetTime] =
Basic.oneObject(
JT.Timestamp,
List(JT.Date, JT.Time),
classOf[java.time.OffsetTime]
JT.TimestampWithTimezone,
List(JT.VarChar, JT.Date, JT.Time, JT.Timestamp),
classOf[OffsetTime]
)
}
Loading

0 comments on commit 1657047

Please sign in to comment.