From 8091ae38bfb52d86101a61b3996afe9f32383e80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Luis=20Vergara?= <18489634+jlvertol@users.noreply.github.com> Date: Sun, 20 Oct 2024 23:32:45 -0300 Subject: [PATCH] Fix option null values failing to put (#42) This PR fixes the error described in https://github.com/com-lihaoyi/scalasql/issues/41 Additionally it fixes it for Enum values on Postgres (which I'm fairly sure had the same error) It appears keeping record of the JDBCType on TypeMappers is not necessary anymore, but nonetheless I kept the value there to touch as little as possible. I did update the DocString. --- docs/reference.md | 80 +++++++++++++++++ scalasql/core/src/TypeMapper.scala | 3 +- scalasql/src/dialects/Dialect.scala | 2 +- .../test/src/datatypes/OptionalTests.scala | 89 +++++++++++++++++++ 4 files changed, 171 insertions(+), 3 deletions(-) diff --git a/docs/reference.md b/docs/reference.md index c4f6e931..63e3193f 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -10159,6 +10159,86 @@ OptCols.select.sortBy(_.myInt).desc.nullsFirst +### Optional.sorting.roundTripOptionalValues + +This example demonstrates a range of different data types being written +as options, both with Some(v) and None values + +```scala +object MyEnum extends Enumeration { + val foo, bar, baz = Value + + implicit def make: String => Value = withName +} +case class OptDataTypes[T[_]]( + myTinyInt: T[Option[Byte]], + mySmallInt: T[Option[Short]], + myInt: T[Option[Int]], + myBigInt: T[Option[Long]], + myDouble: T[Option[Double]], + myBoolean: T[Option[Boolean]], + myLocalDate: T[Option[LocalDate]], + myLocalTime: T[Option[LocalTime]], + myLocalDateTime: T[Option[LocalDateTime]], + myUtilDate: T[Option[Date]], + myInstant: T[Option[Instant]], + myVarBinary: T[Option[geny.Bytes]], + myUUID: T[Option[java.util.UUID]], + myEnum: T[Option[MyEnum.Value]] +) + +object OptDataTypes extends Table[OptDataTypes] { + override def tableName: String = "data_types" +} + +val rowSome = OptDataTypes[Sc]( + myTinyInt = Some(123.toByte), + mySmallInt = Some(12345.toShort), + myInt = Some(12345678), + myBigInt = Some(12345678901L), + myDouble = Some(3.14), + myBoolean = Some(true), + myLocalDate = Some(LocalDate.parse("2023-12-20")), + myLocalTime = Some(LocalTime.parse("10:15:30")), + myLocalDateTime = Some(LocalDateTime.parse("2011-12-03T10:15:30")), + myUtilDate = Some( + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS").parse("2011-12-03T10:15:30.000") + ), + myInstant = Some(Instant.parse("2011-12-03T10:15:30Z")), + myVarBinary = Some(new geny.Bytes(Array[Byte](1, 2, 3, 4, 5, 6, 7, 8))), + myUUID = Some(new java.util.UUID(1234567890L, 9876543210L)), + myEnum = Some(MyEnum.bar) +) + +val rowNone = OptDataTypes[Sc]( + myTinyInt = None, + mySmallInt = None, + myInt = None, + myBigInt = None, + myDouble = None, + myBoolean = None, + myLocalDate = None, + myLocalTime = None, + myLocalDateTime = None, + myUtilDate = None, + myInstant = None, + myVarBinary = None, + myUUID = None, + myEnum = None +) + +db.run( + OptDataTypes.insert.values(rowSome, rowNone) +) ==> 2 + +db.run(OptDataTypes.select) ==> Seq(rowSome, rowNone) +``` + + + + + + ## PostgresDialect Operations specific to working with Postgres Databases ### PostgresDialect.distinctOn diff --git a/scalasql/core/src/TypeMapper.scala b/scalasql/core/src/TypeMapper.scala index 23aec39d..6cd2a65a 100644 --- a/scalasql/core/src/TypeMapper.scala +++ b/scalasql/core/src/TypeMapper.scala @@ -32,8 +32,7 @@ import java.util.UUID trait TypeMapper[T] { outer => /** - * The JDBC type of this type. Used for `setNull` which needs to know the - * `java.sql.Types` integer ID of the type to set it properly + * The JDBC type of this type. */ def jdbcType: JDBCType diff --git a/scalasql/src/dialects/Dialect.scala b/scalasql/src/dialects/Dialect.scala index 08ffeb0c..ca6157c2 100644 --- a/scalasql/src/dialects/Dialect.scala +++ b/scalasql/src/dialects/Dialect.scala @@ -263,7 +263,7 @@ trait Dialect extends DialectTypeMappers { def put(r: PreparedStatement, idx: Int, v: Option[T]): Unit = { v match { - case None => r.setNull(idx, jdbcType.getVendorTypeNumber) + case None => r.setNull(idx, JDBCType.NULL.getVendorTypeNumber) case Some(value) => inner.put(r, idx, value) } } diff --git a/scalasql/test/src/datatypes/OptionalTests.scala b/scalasql/test/src/datatypes/OptionalTests.scala index 065506b2..c94c40a5 100644 --- a/scalasql/test/src/datatypes/OptionalTests.scala +++ b/scalasql/test/src/datatypes/OptionalTests.scala @@ -6,6 +6,19 @@ import utest._ import utils.ScalaSqlSuite import sourcecode.Text +import java.time.{ + Instant, + LocalDate, + LocalDateTime, + LocalTime, + OffsetDateTime, + ZoneId, + ZonedDateTime +} +import java.util.Date +import java.text.SimpleDateFormat +import java.util.UUID + case class OptCols[T[_]](myInt: T[Option[Int]], myInt2: T[Option[Int]]) object OptCols extends Table[OptCols] @@ -516,6 +529,82 @@ trait OptionalTests extends ScalaSqlSuite { OptCols[Sc](Some(1), Some(2)) ) ) + test("roundTripOptionalValues") - checker.recorded( + """ + This example demonstrates a range of different data types being written + as options, both with Some(v) and None values + """, + Text { + object MyEnum extends Enumeration { + val foo, bar, baz = Value + + implicit def make: String => Value = withName + } + case class OptDataTypes[T[_]]( + myTinyInt: T[Option[Byte]], + mySmallInt: T[Option[Short]], + myInt: T[Option[Int]], + myBigInt: T[Option[Long]], + myDouble: T[Option[Double]], + myBoolean: T[Option[Boolean]], + myLocalDate: T[Option[LocalDate]], + myLocalTime: T[Option[LocalTime]], + myLocalDateTime: T[Option[LocalDateTime]], + myUtilDate: T[Option[Date]], + myInstant: T[Option[Instant]], + myVarBinary: T[Option[geny.Bytes]], + myUUID: T[Option[java.util.UUID]], + myEnum: T[Option[MyEnum.Value]] + ) + + object OptDataTypes extends Table[OptDataTypes] { + override def tableName: String = "data_types" + } + + val rowSome = OptDataTypes[Sc]( + myTinyInt = Some(123.toByte), + mySmallInt = Some(12345.toShort), + myInt = Some(12345678), + myBigInt = Some(12345678901L), + myDouble = Some(3.14), + myBoolean = Some(true), + myLocalDate = Some(LocalDate.parse("2023-12-20")), + myLocalTime = Some(LocalTime.parse("10:15:30")), + myLocalDateTime = Some(LocalDateTime.parse("2011-12-03T10:15:30")), + myUtilDate = Some( + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS").parse("2011-12-03T10:15:30.000") + ), + myInstant = Some(Instant.parse("2011-12-03T10:15:30Z")), + myVarBinary = Some(new geny.Bytes(Array[Byte](1, 2, 3, 4, 5, 6, 7, 8))), + myUUID = Some(new java.util.UUID(1234567890L, 9876543210L)), + myEnum = Some(MyEnum.bar) + ) + + val rowNone = OptDataTypes[Sc]( + myTinyInt = None, + mySmallInt = None, + myInt = None, + myBigInt = None, + myDouble = None, + myBoolean = None, + myLocalDate = None, + myLocalTime = None, + myLocalDateTime = None, + myUtilDate = None, + myInstant = None, + myVarBinary = None, + myUUID = None, + myEnum = None + ) + + db.run( + OptDataTypes.insert.values(rowSome, rowNone) + ) ==> 2 + + db.run(OptDataTypes.select) ==> Seq(rowSome, rowNone) + } + ) + } } }