diff --git a/modules/core/src/main/scala/doobie/util/analysis.scala b/modules/core/src/main/scala/doobie/util/analysis.scala index de07c8b07..97e8733d3 100644 --- a/modules/core/src/main/scala/doobie/util/analysis.scala +++ b/modules/core/src/main/scala/doobie/util/analysis.scala @@ -135,11 +135,12 @@ object analysis { columnAlignment: List[(Get[?], NullabilityKnown) `Ior` ColumnMeta] ) { - def parameterMisalignments: List[ParameterMisalignment] = + 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 = { !put.jdbcTargets.contains_(paramMeta.jdbcType) || diff --git a/modules/core/src/test/scala/doobie/util/ReadSuite.scala b/modules/core/src/test/scala/doobie/util/ReadSuite.scala index 1fc61055e..327fedefc 100644 --- a/modules/core/src/test/scala/doobie/util/ReadSuite.scala +++ b/modules/core/src/test/scala/doobie/util/ReadSuite.scala @@ -9,8 +9,11 @@ import doobie.util.TestTypes.* import doobie.util.transactor.Transactor import doobie.testutils.VoidExtensions import doobie.syntax.all.* -import doobie.Query +import doobie.{ConnectionIO, Query} +import doobie.util.analysis.{Analysis, ColumnMisalignment, ColumnTypeError, ColumnTypeWarning, NullabilityMisalignment} +import doobie.util.fragment.Fragment import munit.Location + import scala.annotation.nowarn class ReadSuite extends munit.FunSuite with ReadSuitePlatform { @@ -57,54 +60,54 @@ class ReadSuite extends munit.FunSuite with ReadSuitePlatform { test("Semiauto derivation selects custom Read instances when available") { implicit val i0: Read[HasCustomReadWrite0] = Read.derived[HasCustomReadWrite0] assertEquals(i0.length, 2) - insertTupleAndCheckRead(("x", "y"), HasCustomReadWrite0(CustomReadWrite("x_R"), "y")) + insertTuple2AndCheckRead(("x", "y"), HasCustomReadWrite0(CustomReadWrite("x_R"), "y")) implicit val i1: Read[HasCustomReadWrite1] = Read.derived[HasCustomReadWrite1] assertEquals(i1.length, 2) - insertTupleAndCheckRead(("x", "y"), HasCustomReadWrite1("x", CustomReadWrite("y_R"))) + insertTuple2AndCheckRead(("x", "y"), HasCustomReadWrite1("x", CustomReadWrite("y_R"))) implicit val iOpt0: Read[HasOptCustomReadWrite0] = Read.derived[HasOptCustomReadWrite0] assertEquals(iOpt0.length, 2) - insertTupleAndCheckRead(("x", "y"), HasOptCustomReadWrite0(Some(CustomReadWrite("x_R")), "y")) + insertTuple2AndCheckRead(("x", "y"), HasOptCustomReadWrite0(Some(CustomReadWrite("x_R")), "y")) implicit val iOpt1: Read[HasOptCustomReadWrite1] = Read.derived[HasOptCustomReadWrite1] assertEquals(iOpt1.length, 2) - insertTupleAndCheckRead(("x", "y"), HasOptCustomReadWrite1("x", Some(CustomReadWrite("y_R")))) + insertTuple2AndCheckRead(("x", "y"), HasOptCustomReadWrite1("x", Some(CustomReadWrite("y_R")))) } test("Semiauto derivation selects custom Get instances to use for Read when available") { implicit val i0: Read[HasCustomGetPut0] = Read.derived[HasCustomGetPut0] assertEquals(i0.length, 2) - insertTupleAndCheckRead(("x", "y"), HasCustomGetPut0(CustomGetPut("x_G"), "y")) + insertTuple2AndCheckRead(("x", "y"), HasCustomGetPut0(CustomGetPut("x_G"), "y")) implicit val i1: Read[HasCustomGetPut1] = Read.derived[HasCustomGetPut1] assertEquals(i1.length, 2) - insertTupleAndCheckRead(("x", "y"), HasCustomGetPut1("x", CustomGetPut("y_G"))) + insertTuple2AndCheckRead(("x", "y"), HasCustomGetPut1("x", CustomGetPut("y_G"))) implicit val iOpt0: Read[HasOptCustomGetPut0] = Read.derived[HasOptCustomGetPut0] assertEquals(iOpt0.length, 2) - insertTupleAndCheckRead(("x", "y"), HasOptCustomGetPut0(Some(CustomGetPut("x_G")), "y")) + insertTuple2AndCheckRead(("x", "y"), HasOptCustomGetPut0(Some(CustomGetPut("x_G")), "y")) implicit val iOpt1: Read[HasOptCustomGetPut1] = Read.derived[HasOptCustomGetPut1] assertEquals(iOpt1.length, 2) - insertTupleAndCheckRead(("x", "y"), HasOptCustomGetPut1("x", Some(CustomGetPut("y_G")))) + insertTuple2AndCheckRead(("x", "y"), HasOptCustomGetPut1("x", Some(CustomGetPut("y_G")))) } test("Automatic derivation selects custom Read instances when available") { import doobie.implicits.* - insertTupleAndCheckRead(("x", "y"), HasCustomReadWrite0(CustomReadWrite("x_R"), "y")) - insertTupleAndCheckRead(("x", "y"), HasCustomReadWrite1("x", CustomReadWrite("y_R"))) - insertTupleAndCheckRead(("x", "y"), HasOptCustomReadWrite0(Some(CustomReadWrite("x_R")), "y")) - insertTupleAndCheckRead(("x", "y"), HasOptCustomReadWrite1("x", Some(CustomReadWrite("y_R")))) + insertTuple2AndCheckRead(("x", "y"), HasCustomReadWrite0(CustomReadWrite("x_R"), "y")) + insertTuple2AndCheckRead(("x", "y"), HasCustomReadWrite1("x", CustomReadWrite("y_R"))) + insertTuple2AndCheckRead(("x", "y"), HasOptCustomReadWrite0(Some(CustomReadWrite("x_R")), "y")) + insertTuple2AndCheckRead(("x", "y"), HasOptCustomReadWrite1("x", Some(CustomReadWrite("y_R")))) } test("Automatic derivation selects custom Get instances to use for Read when available") { import doobie.implicits.* - insertTupleAndCheckRead(("x", "y"), HasCustomGetPut0(CustomGetPut("x_G"), "y")) - insertTupleAndCheckRead(("x", "y"), HasCustomGetPut1("x", CustomGetPut("y_G"))) - insertTupleAndCheckRead(("x", "y"), HasOptCustomGetPut0(Some(CustomGetPut("x_G")), "y")) - insertTupleAndCheckRead(("x", "y"), HasOptCustomGetPut1("x", Some(CustomGetPut("y_G")))) + insertTuple2AndCheckRead(("x", "y"), HasCustomGetPut0(CustomGetPut("x_G"), "y")) + insertTuple2AndCheckRead(("x", "y"), HasCustomGetPut1("x", CustomGetPut("y_G"))) + insertTuple2AndCheckRead(("x", "y"), HasOptCustomGetPut0(Some(CustomGetPut("x_G")), "y")) + insertTuple2AndCheckRead(("x", "y"), HasOptCustomGetPut1("x", Some(CustomGetPut("y_G")))) } test("Read should not be derivable for case objects") { @@ -138,6 +141,16 @@ class ReadSuite extends munit.FunSuite with ReadSuitePlatform { assertEquals(p.gets, (readInt.gets ++ readString.gets)) } + test(".map should correctly transform the value") { + import doobie.implicits.* + implicit val r: Read[WrappedSimpleCaseClass] = Read[SimpleCaseClass].map(s => + WrappedSimpleCaseClass( + s.copy(s = "custom") + )) + + insertTuple3AndCheckRead((1, "s1", "s2"), WrappedSimpleCaseClass(SimpleCaseClass(Some(1), "custom", Some("s2")))) + } + /* case class with nested Option case class field */ @@ -195,10 +208,125 @@ class ReadSuite extends munit.FunSuite with ReadSuitePlatform { assertEquals(o, List((1, (2, 3)))) } - private def insertTupleAndCheckRead[Tup: Write, A: Read](in: Tup, expectedOut: A)(implicit loc: Location): Unit = { + test("Read typechecking should work for Tuples") { + val frag = sql"SELECT 1, 's', 3.0 :: DOUBLE" + + assertSuccessTypecheckRead[(Int, String, Double)](frag) + assertSuccessTypecheckRead[(Int, (String, Double))](frag) + assertSuccessTypecheckRead[((Int, String), Double)](frag) + + assertSuccessTypecheckRead[(Int, Option[String], Double)](frag) + assertSuccessTypecheckRead[(Option[Int], Option[(String, Double)])](frag) + assertSuccessTypecheckRead[Option[((Int, String), Double)]](frag) + + assertWarnedTypecheckRead[(Boolean, String, Double)](frag) + + assertMisalignedTypecheckRead[(Int, String)](frag) + assertMisalignedTypecheckRead[(Int, String, Double, Int)](frag) + + } + + test("Read typechecking should work for case classes") { + implicit val rscc: Read[SimpleCaseClass] = Read.derived[SimpleCaseClass] + implicit val rccc: Read[ComplexCaseClass] = Read.derived[ComplexCaseClass] + implicit val rwscc: Read[WrappedSimpleCaseClass] = + rscc.map(WrappedSimpleCaseClass.apply) // Test map doesn't break typechecking + + assertSuccessTypecheckRead( + sql"create table tab(c1 int, c2 varchar not null, c3 varchar)".update.run.flatMap(_ => + sql"SELECT c1,c2,c3 from tab".query[SimpleCaseClass].analysis) + ) + assertSuccessTypecheckRead( + sql"create table tab(c1 int, c2 varchar not null, c3 varchar)".update.run.flatMap(_ => + sql"SELECT c1,c2,c3 from tab".query[WrappedSimpleCaseClass].analysis) + ) + + assertSuccessTypecheckRead( + sql"create table tab(c1 int, c2 varchar, c3 varchar)".update.run.flatMap(_ => + sql"SELECT c1,c2,c3 from tab".query[Option[SimpleCaseClass]].analysis) + ) + assertSuccessTypecheckRead( + sql"create table tab(c1 int, c2 varchar, c3 varchar)".update.run.flatMap(_ => + sql"SELECT c1,c2,c3 from tab".query[Option[WrappedSimpleCaseClass]].analysis) + ) + + assertTypeErrorTypecheckRead( + sql"create table tab(c1 binary, c2 varchar not null, c3 varchar)".update.run.flatMap(_ => + sql"SELECT c1,c2,c3 from tab".query[SimpleCaseClass].analysis) + ) + + assertMisalignedNullabilityTypecheckRead( + sql"create table tab(c1 int, c2 varchar, c3 varchar)".update.run.flatMap(_ => + sql"SELECT c1,c2,c3 from tab".query[SimpleCaseClass].analysis) + ) + + assertSuccessTypecheckRead( + sql"create table tab(c1 int, c2 varchar not null, c3 varchar, c4 int, c5 varchar, c6 varchar, c7 int, c8 varchar not null)" + .update.run.flatMap(_ => + sql"SELECT c1,c2,c3,c4,c5,c6,c7,c8 from tab".query[ComplexCaseClass].analysis) + ) + + assertTypeErrorTypecheckRead( + sql"create table tab(c1 binary, c2 varchar not null, c3 varchar, c4 int, c5 varchar, c6 varchar, c7 int, c8 varchar not null)" + .update.run.flatMap(_ => + sql"SELECT c1,c2,c3,c4,c5,c6,c7,c8 from tab".query[ComplexCaseClass].analysis) + ) + + assertMisalignedNullabilityTypecheckRead( + sql"create table tab(c1 int, c2 varchar, c3 varchar, c4 int, c5 varchar, c6 varchar, c7 int, c8 varchar not null)" + .update.run.flatMap(_ => + sql"SELECT c1,c2,c3,c4,c5,c6,c7,c8 from tab".query[ComplexCaseClass].analysis) + ) + + } + + private def insertTuple3AndCheckRead[Tup <: (?, ?, ?): Write, A: Read](in: Tup, expectedOut: A)(implicit + loc: Location + ): Unit = { + val res = Query[Tup, A]("SELECT ?, ?, ?").unique(in).transact(xa) + .unsafeRunSync() + assertEquals(res, expectedOut) + } + + private def insertTuple2AndCheckRead[Tup <: (?, ?): Write, A: Read](in: Tup, expectedOut: A)(implicit + loc: Location + ): Unit = { val res = Query[Tup, A]("SELECT ?, ?").unique(in).transact(xa) .unsafeRunSync() assertEquals(res, expectedOut) } + private def assertSuccessTypecheckRead(connio: ConnectionIO[Analysis])(implicit loc: Location): Unit = { + val analysisResult = connio.transact(xa).unsafeRunSync() + assertEquals(analysisResult.columnAlignmentErrors, Nil) + } + + private def assertSuccessTypecheckRead[A: Read](frag: Fragment)(implicit loc: Location): Unit = { + assertSuccessTypecheckRead(frag.query[A].analysis) + } + + private def assertWarnedTypecheckRead[A: Read](frag: Fragment)(implicit loc: Location): Unit = { + val analysisResult = frag.query[A].analysis.transact(xa).unsafeRunSync() + val errorClasses = analysisResult.columnAlignmentErrors.map(_.getClass) + assertEquals(errorClasses, List(classOf[ColumnTypeWarning])) + } + + private def assertTypeErrorTypecheckRead(connio: ConnectionIO[Analysis])(implicit loc: Location): Unit = { + val analysisResult = connio.transact(xa).unsafeRunSync() + val errorClasses = analysisResult.columnAlignmentErrors.map(_.getClass) + assertEquals(errorClasses, List(classOf[ColumnTypeError])) + } + + private def assertMisalignedNullabilityTypecheckRead(connio: ConnectionIO[Analysis])(implicit loc: Location): Unit = { + val analysisResult = connio.transact(xa).unsafeRunSync() + val errorClasses = analysisResult.columnAlignmentErrors.map(_.getClass) + assertEquals(errorClasses, List(classOf[NullabilityMisalignment])) + } + + private def assertMisalignedTypecheckRead[A: Read](frag: Fragment)(implicit loc: Location): Unit = { + val analysisResult = frag.query[A].analysis.transact(xa).unsafeRunSync() + val errorClasses = analysisResult.columnAlignmentErrors.map(_.getClass) + assertEquals(errorClasses, List(classOf[ColumnMisalignment])) + } + } diff --git a/modules/core/src/test/scala/doobie/util/TestTypes.scala b/modules/core/src/test/scala/doobie/util/TestTypes.scala index c2a1ca397..d0217cd98 100644 --- a/modules/core/src/test/scala/doobie/util/TestTypes.scala +++ b/modules/core/src/test/scala/doobie/util/TestTypes.scala @@ -10,6 +10,7 @@ object TestTypes { case class TrivialCaseClass(i: Int) case class SimpleCaseClass(i: Option[Int], s: String, os: Option[String]) case class ComplexCaseClass(sc: SimpleCaseClass, osc: Option[SimpleCaseClass], i: Option[Int], s: String) + case class WrappedSimpleCaseClass(sc: SimpleCaseClass) case class HasCustomReadWrite0(c: CustomReadWrite, s: String) case class HasCustomReadWrite1(s: String, c: CustomReadWrite) diff --git a/modules/core/src/test/scala/doobie/util/WriteSuite.scala b/modules/core/src/test/scala/doobie/util/WriteSuite.scala index 11dee1273..564dd6ff4 100644 --- a/modules/core/src/test/scala/doobie/util/WriteSuite.scala +++ b/modules/core/src/test/scala/doobie/util/WriteSuite.scala @@ -4,13 +4,15 @@ package doobie.util -import doobie.{Query, Transactor, Update} +import doobie.{ConnectionIO, Query, Transactor, Update} import doobie.util.TestTypes.* import cats.effect.IO import cats.effect.unsafe.implicits.global import doobie.testutils.VoidExtensions import doobie.syntax.all.* +import doobie.util.analysis.{Analysis, ParameterMisalignment, ParameterTypeError} import munit.Location + import scala.annotation.nowarn class WriteSuite extends munit.FunSuite with WriteSuitePlatform { @@ -55,54 +57,54 @@ class WriteSuite extends munit.FunSuite with WriteSuitePlatform { test("Semiauto derivation selects custom Write instances when available") { implicit val i0: Write[HasCustomReadWrite0] = Write.derived[HasCustomReadWrite0] assertEquals(i0.length, 2) - writeAndCheckTuple(HasCustomReadWrite0(CustomReadWrite("x"), "y"), ("x_W", "y")) + writeAndCheckTuple2(HasCustomReadWrite0(CustomReadWrite("x"), "y"), ("x_W", "y")) implicit val i1: Write[HasCustomReadWrite1] = Write.derived[HasCustomReadWrite1] assertEquals(i1.length, 2) - writeAndCheckTuple(HasCustomReadWrite1("x", CustomReadWrite("y")), ("x", "y_W")) + writeAndCheckTuple2(HasCustomReadWrite1("x", CustomReadWrite("y")), ("x", "y_W")) implicit val iOpt0: Write[HasOptCustomReadWrite0] = Write.derived[HasOptCustomReadWrite0] assertEquals(iOpt0.length, 2) - writeAndCheckTuple(HasOptCustomReadWrite0(Some(CustomReadWrite("x")), "y"), ("x_W", "y")) + writeAndCheckTuple2(HasOptCustomReadWrite0(Some(CustomReadWrite("x")), "y"), ("x_W", "y")) implicit val iOpt1: Write[HasOptCustomReadWrite1] = Write.derived[HasOptCustomReadWrite1] assertEquals(iOpt1.length, 2) - writeAndCheckTuple(HasOptCustomReadWrite1("x", Some(CustomReadWrite("y"))), ("x", "y_W")) + writeAndCheckTuple2(HasOptCustomReadWrite1("x", Some(CustomReadWrite("y"))), ("x", "y_W")) } test("Semiauto derivation selects custom Put instances to use for Write when available") { implicit val i0: Write[HasCustomGetPut0] = Write.derived[HasCustomGetPut0] assertEquals(i0.length, 2) - writeAndCheckTuple(HasCustomGetPut0(CustomGetPut("x"), "y"), ("x_P", "y")) + writeAndCheckTuple2(HasCustomGetPut0(CustomGetPut("x"), "y"), ("x_P", "y")) implicit val i1: Write[HasCustomGetPut1] = Write.derived[HasCustomGetPut1] assertEquals(i1.length, 2) - writeAndCheckTuple(HasCustomGetPut1("x", CustomGetPut("y")), ("x", "y_P")) + writeAndCheckTuple2(HasCustomGetPut1("x", CustomGetPut("y")), ("x", "y_P")) implicit val iOpt0: Write[HasOptCustomGetPut0] = Write.derived[HasOptCustomGetPut0] assertEquals(iOpt0.length, 2) - writeAndCheckTuple(HasOptCustomGetPut0(Some(CustomGetPut("x")), "y"), ("x_P", "y")) + writeAndCheckTuple2(HasOptCustomGetPut0(Some(CustomGetPut("x")), "y"), ("x_P", "y")) implicit val iOpt1: Write[HasOptCustomGetPut1] = Write.derived[HasOptCustomGetPut1] assertEquals(iOpt1.length, 2) - writeAndCheckTuple(HasOptCustomGetPut1("x", Some(CustomGetPut("y"))), ("x", "y_P")) + writeAndCheckTuple2(HasOptCustomGetPut1("x", Some(CustomGetPut("y"))), ("x", "y_P")) } test("Automatic derivation selects custom Write instances when available") { import doobie.implicits.* - writeAndCheckTuple(HasCustomReadWrite0(CustomReadWrite("x"), "y"), ("x_W", "y")) - writeAndCheckTuple(HasCustomReadWrite1("x", CustomReadWrite("y")), ("x", "y_W")) - writeAndCheckTuple(HasOptCustomReadWrite0(Some(CustomReadWrite("x")), "y"), ("x_W", "y")) - writeAndCheckTuple(HasOptCustomReadWrite1("x", Some(CustomReadWrite("y"))), ("x", "y_W")) + writeAndCheckTuple2(HasCustomReadWrite0(CustomReadWrite("x"), "y"), ("x_W", "y")) + writeAndCheckTuple2(HasCustomReadWrite1("x", CustomReadWrite("y")), ("x", "y_W")) + writeAndCheckTuple2(HasOptCustomReadWrite0(Some(CustomReadWrite("x")), "y"), ("x_W", "y")) + writeAndCheckTuple2(HasOptCustomReadWrite1("x", Some(CustomReadWrite("y"))), ("x", "y_W")) } test("Automatic derivation selects custom Put instances to use for Write when available") { import doobie.implicits.* - writeAndCheckTuple(HasCustomGetPut0(CustomGetPut("x"), "y"), ("x_P", "y")) - writeAndCheckTuple(HasCustomGetPut1("x", CustomGetPut("y")), ("x", "y_P")) - writeAndCheckTuple(HasOptCustomGetPut0(Some(CustomGetPut("x")), "y"), ("x_P", "y")) - writeAndCheckTuple(HasOptCustomGetPut1("x", Some(CustomGetPut("y"))), ("x", "y_P")) + writeAndCheckTuple2(HasCustomGetPut0(CustomGetPut("x"), "y"), ("x_P", "y")) + writeAndCheckTuple2(HasCustomGetPut1("x", CustomGetPut("y")), ("x", "y_P")) + writeAndCheckTuple2(HasOptCustomGetPut0(Some(CustomGetPut("x")), "y"), ("x_P", "y")) + writeAndCheckTuple2(HasOptCustomGetPut1("x", Some(CustomGetPut("y"))), ("x", "y_P")) } test("Write should not be derivable for case objects") { @@ -162,6 +164,110 @@ class WriteSuite extends munit.FunSuite with WriteSuitePlatform { } } + test(".contramap correctly transformers the input value") { + import doobie.implicits.* + implicit val w: Write[WrappedSimpleCaseClass] = Write[SimpleCaseClass].contramap(v => + v.sc.copy( + s = "custom" + )) + + writeAndCheckTuple3(WrappedSimpleCaseClass(SimpleCaseClass(Some(1), "s1", Some("s2"))), (1, "custom", "s2")) + } + + test("Write typechecking should work for tuples") { + val createTable = sql"create temp table tab(c1 int, c2 varchar not null, c3 double)".update.run + val createAllNullableTable = sql"create temp table tab(c1 int, c2 varchar, c3 double)".update.run + val insertSql = "INSERT INTO tab VALUES (?,?,?)" + + assertSuccessTypecheckWrite( + createTable.flatMap(_ => Update[(Option[Int], String, Double)](insertSql).analysis)) + assertSuccessTypecheckWrite( + createTable.flatMap(_ => Update[((Option[Int], String), Double)](insertSql).analysis)) + assertSuccessTypecheckWrite( + createTable.flatMap(_ => Update[(Option[Int], String, Option[Double])](insertSql).analysis)) + assertSuccessTypecheckWrite( + createAllNullableTable.flatMap(_ => Update[(Option[Int], Option[String], Option[Double])](insertSql).analysis)) + assertSuccessTypecheckWrite( + createAllNullableTable.flatMap(_ => Update[Option[(Option[Int], String, Double)]](insertSql).analysis)) + assertSuccessTypecheckWrite( + createAllNullableTable.flatMap(_ => Update[Option[(Int, Option[(String, Double)])]](insertSql).analysis)) + + assertMisalignedTypecheckWrite(createTable.flatMap(_ => Update[(Option[Int], String)](insertSql).analysis)) + assertMisalignedTypecheckWrite(createTable.flatMap(_ => + Update[(Option[Int], String, Double, Int)](insertSql).analysis)) + + assertTypeErrorTypecheckWrite( + sql"create temp table tab(c1 binary not null, c2 varchar not null, c3 int)".update.run.flatMap(_ => + Update[(Int, String, Option[Int])](insertSql).analysis) + ) + } + + test("Write typechecking should work for case classes") { + implicit val wscc: Write[SimpleCaseClass] = Write.derived[SimpleCaseClass] + implicit val wccc: Write[ComplexCaseClass] = Write.derived[ComplexCaseClass] + implicit val wwscc: Write[WrappedSimpleCaseClass] = + wscc.contramap(_.sc) // Testing contramap doesn't break typechecking + + val createTable = sql"create temp table tab(c1 int, c2 varchar not null, c3 varchar)".update.run + + val insertSimpleSql = "INSERT INTO tab VALUES (?,?,?)" + + assertSuccessTypecheckWrite(createTable.flatMap(_ => Update[SimpleCaseClass](insertSimpleSql).analysis)) + assertSuccessTypecheckWrite(createTable.flatMap(_ => Update[WrappedSimpleCaseClass](insertSimpleSql).analysis)) + + // This shouldn't pass but JDBC driver (at least for h2) doesn't tell us when a parameter should be not-nullable + assertSuccessTypecheckWrite(createTable.flatMap(_ => Update[Option[SimpleCaseClass]](insertSimpleSql).analysis)) + assertSuccessTypecheckWrite(createTable.flatMap(_ => + Update[Option[WrappedSimpleCaseClass]](insertSimpleSql).analysis)) + + val insertComplexSql = "INSERT INTO tab VALUES (?,?,?,?,?,?,?,?)" + + assertSuccessTypecheckWrite( + sql"create temp table tab(c1 int, c2 varchar, c3 varchar, c4 int, c5 varchar, c6 varchar, c7 int, c8 varchar not null)" + .update.run + .flatMap(_ => Update[ComplexCaseClass](insertComplexSql).analysis) + ) + + assertTypeErrorTypecheckWrite( + sql"create temp table tab(c1 int, c2 varchar, c3 varchar, c4 BINARY, c5 varchar, c6 varchar, c7 int, c8 varchar not null)" + .update.run + .flatMap(_ => Update[ComplexCaseClass](insertComplexSql).analysis) + ) + } + + private def assertSuccessTypecheckWrite(connio: ConnectionIO[Analysis])(implicit loc: Location): Unit = { + val analysisResult = connio.transact(xa).unsafeRunSync() + assertEquals(analysisResult.parameterAlignmentErrors, Nil) + } + + private def assertMisalignedTypecheckWrite(connio: ConnectionIO[Analysis])(implicit loc: Location): Unit = { + val analysisResult = connio.transact(xa).unsafeRunSync() + val errorClasses = analysisResult.parameterAlignmentErrors.map(_.getClass) + assertEquals(errorClasses, List(classOf[ParameterMisalignment])) + } + + private def assertTypeErrorTypecheckWrite(connio: ConnectionIO[Analysis])(implicit loc: Location): Unit = { + val analysisResult = connio.transact(xa).unsafeRunSync() + val errorClasses = analysisResult.parameterAlignmentErrors.map(_.getClass) + assertEquals(errorClasses, List(classOf[ParameterTypeError])) + } + + private def writeAndCheckTuple2[A: Write, Tup <: (?, ?): Read](in: A, expectedOut: Tup)(implicit + loc: Location + ): Unit = { + val res = Query[A, Tup]("SELECT ?, ?").unique(in).transact(xa) + .unsafeRunSync() + assertEquals(res, expectedOut) + } + + private def writeAndCheckTuple3[A: Write, Tup <: (?, ?, ?): Read](in: A, expectedOut: Tup)(implicit + loc: Location + ): Unit = { + val res = Query[A, Tup]("SELECT ?, ?, ?").unique(in).transact(xa) + .unsafeRunSync() + assertEquals(res, expectedOut) + } + private def testNullPut(input: (String, Option[String])): Int = { import doobie.implicits.* @@ -173,12 +279,6 @@ class WriteSuite extends munit.FunSuite with WriteSuitePlatform { .unsafeRunSync() } - private def writeAndCheckTuple[A: Write, Tup: Read](in: A, expectedOut: Tup)(implicit loc: Location): Unit = { - val res = Query[A, Tup]("SELECT ?, ?").unique(in).transact(xa) - .unsafeRunSync() - assertEquals(res, expectedOut) - } - } object WriteSuite {}