From 25dfdd3855c95dc1290dba4924b1ecff0063a776 Mon Sep 17 00:00:00 2001 From: Jacob Wang Date: Tue, 5 Nov 2024 21:11:52 +0000 Subject: [PATCH] Rework Read/Write & Fix derivation to use custom instances Fix both semiauto and automatic derivation to use custom defined Read/Write instances (e.g. in companion objects). A complete rework of Read and Write is unfortunately necessary because with the previous implementation, we cannot simply derive a `Read[Option[A]]` given a `Read[A]` - we'd need to derive `Option[A]` from scratch by resolving `Read[Option[X]]` instances for each of `A`'s columns. After the rework, both `Read` and `Write` are now sealed traits, each with 3 subclasses: - Single: Wrapper over a `Get/Put` - SingleOpt: Wrapper over a `Get/Put`, but is nullable i.e. `Read[Option[A]]`, `Write[Option[A]]` - Composite: A composite of `Read/Write` instances Apart from enabling proper semiauto and automatic derivation, the rework also brings these benefits: - Derivation rules are much simpler (which also translates to faster compile times). In particular, given a `Read/Write[A]` we can trivially derive `Read/Write[Option[A]]`. - We now correctly handle optionality for `Option[CaseClassWithOptionalAndNonOptionalFields]`. More tests will be added for this in a follow up PR to demonstrate Other notes: - Semiauto and Auto derivation of unary product type (e.g. 1 element case class) are removed due to it causing auto derivation to pick the wrong path. It seems unnecessary since Write/Read derivation will yield the same behaviour anyway? Fixes #1054, #2104 --- build.sbt | 13 +- .../scala-2/doobie/util/GetPlatform.scala | 6 +- .../scala-2/doobie/util/MkGetPlatform.scala | 26 --- .../scala-2/doobie/util/MkPutPlatform.scala | 26 --- .../scala-2/doobie/util/MkReadPlatform.scala | 166 +++++--------- .../scala-2/doobie/util/MkWritePlatform.scala | 211 +++++------------ .../scala-2/doobie/util/PutPlatform.scala | 6 +- .../scala-2/doobie/util/ReadPlatform.scala | 53 +++-- .../scala-2/doobie/util/WritePlatform.scala | 56 +++-- .../scala-3/doobie/util/MkGetPlatform.scala | 20 -- .../scala-3/doobie/util/MkPutPlatform.scala | 20 -- .../scala-3/doobie/util/MkReadPlatform.scala | 134 ++++------- .../scala-3/doobie/util/MkWritePlatform.scala | 148 +++--------- .../scala-3/doobie/util/PutPlatform.scala | 2 +- .../scala-3/doobie/util/ReadPlatform.scala | 33 ++- .../scala-3/doobie/util/WritePlatform.scala | 38 ++-- .../src/main/scala/doobie/generic/auto.scala | 16 +- .../core/src/main/scala/doobie/package.scala | 6 - .../scala/doobie/util/Derived.scala} | 9 +- .../src/main/scala/doobie/util/fragment.scala | 48 +--- .../core/src/main/scala/doobie/util/get.scala | 26 --- .../core/src/main/scala/doobie/util/put.scala | 30 +-- .../src/main/scala/doobie/util/read.scala | 145 ++++++++---- .../src/main/scala/doobie/util/write.scala | 214 +++++++++++------- .../doobie/util/GetSuitePlatform.scala | 29 --- .../doobie/util/PutSuitePlatform.scala | 28 --- .../doobie/util/QueryLogSuitePlatform.scala | 1 - .../doobie/util/PutSuitePlatform.scala | 23 -- .../doobie/util/QueryLogSuitePlatform.scala | 1 - .../src/test/scala/doobie/util/GetSuite.scala | 44 +--- .../src/test/scala/doobie/util/PutSuite.scala | 18 +- .../test/scala/doobie/util/ReadSuite.scala | 129 +++++++---- .../test/scala/doobie/util/TestTypes.scala | 35 ++- .../test/scala/doobie/util/WriteSuite.scala | 124 ++++++---- .../scala/doobie/munit/CheckerTests.scala | 2 - .../scala/doobie/weaver/CheckerTests.scala | 2 - 36 files changed, 760 insertions(+), 1128 deletions(-) delete mode 100644 modules/core/src/main/scala-2/doobie/util/MkGetPlatform.scala delete mode 100644 modules/core/src/main/scala-2/doobie/util/MkPutPlatform.scala delete mode 100644 modules/core/src/main/scala-3/doobie/util/MkGetPlatform.scala delete mode 100644 modules/core/src/main/scala-3/doobie/util/MkPutPlatform.scala rename modules/core/src/{test/scala-3/doobie/util/GetSuitePlatform.scala => main/scala/doobie/util/Derived.scala} (55%) delete mode 100644 modules/core/src/test/scala-2/doobie/util/GetSuitePlatform.scala delete mode 100644 modules/core/src/test/scala-2/doobie/util/PutSuitePlatform.scala delete mode 100644 modules/core/src/test/scala-3/doobie/util/PutSuitePlatform.scala diff --git a/build.sbt b/build.sbt index 1af30437f..8451f3aa6 100644 --- a/build.sbt +++ b/build.sbt @@ -33,6 +33,7 @@ lazy val weaverVersion = "0.8.4" ThisBuild / tlBaseVersion := "1.0" ThisBuild / tlCiReleaseBranches := Seq("main") // publish snapshots on `main` ThisBuild / tlCiScalafmtCheck := true +//ThisBuild / scalaVersion := scala212Version ThisBuild / scalaVersion := scala213Version //ThisBuild / scalaVersion := scala3Version ThisBuild / crossScalaVersions := Seq(scala212Version, scala213Version, scala3Version) @@ -98,9 +99,12 @@ lazy val compilerFlags = Seq( Compile / doc / scalacOptions --= Seq( "-Xfatal-warnings" ), -// Test / scalacOptions --= Seq( -// "-Xfatal-warnings" -// ), + // Disable warning when @nowarn annotation isn't suppressing a warning + // to simplify cross-building + // because 2.12 @nowarn doesn't actually do anything.. https://github.com/scala/bug/issues/12313 + scalacOptions ++= Seq( + "-Wconf:cat=unused-nowarn:s" + ), scalacOptions ++= (if (tlIsScala3.value) // Handle irrefutable patterns in for comprehensions Seq("-source:future", "-language:adhocExtensions") @@ -249,8 +253,7 @@ lazy val core = project ).filterNot(_ => tlIsScala3.value) ++ Seq( "org.tpolecat" %% "typename" % "1.1.0", "com.h2database" % "h2" % h2Version % "test", - "org.postgresql" % "postgresql" % postgresVersion % "test", - "org.mockito" % "mockito-core" % "5.12.0" % Test + "org.postgresql" % "postgresql" % postgresVersion % "test" ), Compile / unmanagedSourceDirectories += { val sourceDir = (Compile / sourceDirectory).value diff --git a/modules/core/src/main/scala-2/doobie/util/GetPlatform.scala b/modules/core/src/main/scala-2/doobie/util/GetPlatform.scala index 0967c24f0..0d0568f8d 100644 --- a/modules/core/src/main/scala-2/doobie/util/GetPlatform.scala +++ b/modules/core/src/main/scala-2/doobie/util/GetPlatform.scala @@ -11,13 +11,15 @@ trait GetPlatform { import doobie.util.compat.=:= /** @group Instances */ - @deprecated("Use Get.derived instead to derive instances explicitly", "1.0.0-RC6") def unaryProductGet[A, L <: HList, H, T <: HList]( implicit G: Generic.Aux[A, L], C: IsHCons.Aux[L, H, T], H: Lazy[Get[H]], E: (H :: HNil) =:= L - ): MkGet[A] = MkGet.unaryProductGet + ): Get[A] = { + void(C) // C drives inference but is not used directly + H.value.tmap[A](h => G.from(h :: HNil)) + } } diff --git a/modules/core/src/main/scala-2/doobie/util/MkGetPlatform.scala b/modules/core/src/main/scala-2/doobie/util/MkGetPlatform.scala deleted file mode 100644 index 1a805d0e1..000000000 --- a/modules/core/src/main/scala-2/doobie/util/MkGetPlatform.scala +++ /dev/null @@ -1,26 +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 - -import shapeless._ -import shapeless.ops.hlist.IsHCons - -trait MkGetPlatform { - import doobie.util.compat.=:= - - /** @group Instances */ - implicit def unaryProductGet[A, L <: HList, H, T <: HList]( - implicit - G: Generic.Aux[A, L], - C: IsHCons.Aux[L, H, T], - H: Lazy[Get[H]], - E: (H :: HNil) =:= L - ): MkGet[A] = { - void(C) // C drives inference but is not used directly - val get = H.value.tmap[A](h => G.from(h :: HNil)) - MkGet.lift(get) - } - -} diff --git a/modules/core/src/main/scala-2/doobie/util/MkPutPlatform.scala b/modules/core/src/main/scala-2/doobie/util/MkPutPlatform.scala deleted file mode 100644 index 2a4d32c3b..000000000 --- a/modules/core/src/main/scala-2/doobie/util/MkPutPlatform.scala +++ /dev/null @@ -1,26 +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 - -import shapeless._ -import shapeless.ops.hlist.IsHCons - -trait MkPutPlatform { - import doobie.util.compat.=:= - - /** @group Instances */ - implicit def unaryProductPut[A, L <: HList, H, T <: HList]( - implicit - G: Generic.Aux[A, L], - C: IsHCons.Aux[L, H, T], - H: Lazy[Put[H]], - E: (H :: HNil) =:= L - ): MkPut[A] = { - void(E) // E is a necessary constraint but isn't used directly - val put = H.value.contramap[A](a => G.to(a).head) - MkPut.lift(put) - } - -} diff --git a/modules/core/src/main/scala-2/doobie/util/MkReadPlatform.scala b/modules/core/src/main/scala-2/doobie/util/MkReadPlatform.scala index eb2d14074..9ff5b7897 100644 --- a/modules/core/src/main/scala-2/doobie/util/MkReadPlatform.scala +++ b/modules/core/src/main/scala-2/doobie/util/MkReadPlatform.scala @@ -4,142 +4,92 @@ package doobie.util -import shapeless.{HList, HNil, ::, Generic, Lazy, <:!<, OrElse} -import shapeless.labelled.{field, FieldType} +import shapeless.{HList, HNil, ::, Generic, Lazy, OrElse} +import shapeless.labelled.FieldType -trait MkReadPlatform extends LowerPriorityRead { +trait MkReadPlatform extends LowerPriorityMkRead { // Derivation base case for product types (1-element) implicit def productBase[H]( - implicit H: Read[H] OrElse MkRead[H] - ): MkRead[H :: HNil] = { - val head = H.unify - - new MkRead[H :: HNil]( - head.gets, - (rs, n) => head.unsafeGet(rs, n) :: HNil + implicit H: Read[H] OrElse Derived[MkRead[H]] + ): Derived[MkRead[H :: HNil]] = { + val headInstance = H.fold(identity, _.instance) + + new Derived( + new MkRead( + new Read.Composite( + List(headInstance), + _.head.asInstanceOf[H] :: HNil + ) + ) ) } // Derivation base case for shapeless record (1-element) implicit def recordBase[K <: Symbol, H]( - implicit H: Read[H] OrElse MkRead[H] - ): MkRead[FieldType[K, H] :: HNil] = { - val head = H.unify - - new MkRead[FieldType[K, H] :: HNil]( - head.gets, - (rs, n) => field[K](head.unsafeGet(rs, n)) :: HNil + implicit H: Read[H] OrElse Derived[MkRead[H]] + ): Derived[MkRead[FieldType[K, H] :: HNil]] = { + val headInstance = H.fold(identity, _.instance) + + new Derived( + new MkRead( + new Read.Composite( + List(headInstance), + _.head.asInstanceOf[FieldType[K, H]] :: HNil + ) + ) ) } } -trait LowerPriorityRead extends EvenLowerPriorityRead { +trait LowerPriorityMkRead { // Derivation inductive case for product types implicit def product[H, T <: HList]( implicit - H: Read[H] OrElse MkRead[H], - T: MkRead[T] - ): MkRead[H :: T] = { - val head = H.unify - - new MkRead[H :: T]( - head.gets ++ T.gets, - (rs, n) => head.unsafeGet(rs, n) :: T.unsafeGet(rs, n + head.length) + H: Read[H] OrElse Derived[MkRead[H]], + T: Read[T] OrElse Derived[MkRead[T]] + ): Derived[MkRead[H :: T]] = { + val headInstance = H.fold(identity, _.instance) + val tailInstance = T.fold(identity, _.instance) + + new Derived( + new MkRead( + new Read.Composite( + List(headInstance, tailInstance), + list => list(0).asInstanceOf[H] :: list(1).asInstanceOf[T] + ) + ) ) } // Derivation inductive case for shapeless records implicit def record[K <: Symbol, H, T <: HList]( implicit - H: Read[H] OrElse MkRead[H], - T: MkRead[T] - ): MkRead[FieldType[K, H] :: T] = { - val head = H.unify - - new MkRead[FieldType[K, H] :: T]( - head.gets ++ T.gets, - (rs, n) => field[K](head.unsafeGet(rs, n)) :: T.unsafeGet(rs, n + head.length) + H: Read[H] OrElse Derived[MkRead[H]], + T: Read[T] OrElse Derived[MkRead[T]] + ): Derived[MkRead[FieldType[K, H] :: T]] = { + val headInstance = H.fold(identity, _.instance) + val tailInstance = T.fold(identity, _.instance) + + new Derived( + new MkRead( + new Read.Composite( + List(headInstance, tailInstance), + list => list(0).asInstanceOf[FieldType[K, H]] :: list(1).asInstanceOf[T] + ) + ) ) } // Derivation for product types (i.e. case class) - implicit def generic[T, Repr](implicit gen: Generic.Aux[T, Repr], G: Lazy[MkRead[Repr]]): MkRead[T] = - new MkRead[T](G.value.gets, (rs, n) => gen.from(G.value.unsafeGet(rs, n))) - - // Derivation base case for Option of product types (1-element) - implicit def optProductBase[H]( - implicit - H: Read[Option[H]] OrElse MkRead[Option[H]], - N: H <:!< Option[α] forSome { type α } - ): MkRead[Option[H :: HNil]] = { - void(N) - val head = H.unify - - new MkRead[Option[H :: HNil]]( - head.gets, - (rs, n) => - head.unsafeGet(rs, n).map(_ :: HNil) - ) - } - - // Derivation base case for Option of product types (where the head element is Option) - implicit def optProductOptBase[H]( - implicit H: Read[Option[H]] OrElse MkRead[Option[H]] - ): MkRead[Option[Option[H] :: HNil]] = { - val head = H.unify - - new MkRead[Option[Option[H] :: HNil]]( - head.gets, - (rs, n) => head.unsafeGet(rs, n).map(h => Some(h) :: HNil) - ) - } - -} - -trait EvenLowerPriorityRead { - - // Read[Option[H]], Read[Option[T]] implies Read[Option[H *: T]] - implicit def optProduct[H, T <: HList]( + implicit def genericRead[T, Repr]( implicit - H: Read[Option[H]] OrElse MkRead[Option[H]], - T: MkRead[Option[T]], - N: H <:!< Option[α] forSome { type α } - ): MkRead[Option[H :: T]] = { - void(N) - val head = H.unify - - new MkRead[Option[H :: T]]( - head.gets ++ T.gets, - (rs, n) => - for { - h <- head.unsafeGet(rs, n) - t <- T.unsafeGet(rs, n + head.length) - } yield h :: t - ) + gen: Generic.Aux[T, Repr], + hlistRead: Lazy[Read[Repr] OrElse Derived[MkRead[Repr]]] + ): Derived[MkRead[T]] = { + val hlistInstance: Read[Repr] = hlistRead.value.fold(identity, _.instance) + new Derived(new MkRead(hlistInstance.map(gen.from))) } - // Read[Option[H]], Read[Option[T]] implies Read[Option[Option[H] *: T]] - implicit def optProductOpt[H, T <: HList]( - implicit - H: Read[Option[H]] OrElse MkRead[Option[H]], - T: MkRead[Option[T]] - ): MkRead[Option[Option[H] :: T]] = { - val head = H.unify - - new MkRead[Option[Option[H] :: T]]( - head.gets ++ T.gets, - (rs, n) => T.unsafeGet(rs, n + head.length).map(head.unsafeGet(rs, n) :: _) - ) - } - - // Derivation for optional of product types (i.e. case class) - implicit def ogeneric[A, Repr <: HList]( - implicit - G: Generic.Aux[A, Repr], - B: Lazy[MkRead[Option[Repr]]] - ): MkRead[Option[A]] = - new MkRead[Option[A]](B.value.gets, B.value.unsafeGet(_, _).map(G.from)) - } diff --git a/modules/core/src/main/scala-2/doobie/util/MkWritePlatform.scala b/modules/core/src/main/scala-2/doobie/util/MkWritePlatform.scala index b6572ad41..bb82bd57d 100644 --- a/modules/core/src/main/scala-2/doobie/util/MkWritePlatform.scala +++ b/modules/core/src/main/scala-2/doobie/util/MkWritePlatform.scala @@ -4,185 +4,96 @@ package doobie.util -import shapeless.{HList, HNil, ::, Generic, Lazy, <:!<, OrElse} -import shapeless.labelled.{FieldType} +import shapeless.{::, Generic, HList, HNil, Lazy, OrElse} +import shapeless.labelled.FieldType -trait MkWritePlatform extends LowerPriorityWrite { +import scala.annotation.nowarn + +trait MkWritePlatform extends LowerPriorityMkWrite { // Derivation base case for product types (1-element) implicit def productBase[H]( - implicit H: Write[H] OrElse MkWrite[H] - ): MkWrite[H :: HNil] = { - val head = H.unify - - new MkWrite[H :: HNil]( - head.puts, - { case h :: HNil => head.toList(h) }, - { case (ps, n, h :: HNil) => head.unsafeSet(ps, n, h); }, - { case (rs, n, h :: HNil) => head.unsafeUpdate(rs, n, h); } + implicit H: Write[H] OrElse Derived[MkWrite[H]] + ): Derived[MkWrite[H :: HNil]] = { + val head = H.fold(identity, _.instance) + + new Derived( + new MkWrite[H :: HNil]( + new Write.Composite(List(head), { case h :: HNil => List(h) }) + ) ) } // Derivation base case for shapelss record (1-element) implicit def recordBase[K <: Symbol, H]( - implicit H: Write[H] OrElse MkWrite[H] - ): MkWrite[FieldType[K, H] :: HNil] = { - val head = H.unify - - new MkWrite( - head.puts, - { case h :: HNil => head.toList(h) }, - { case (ps, n, h :: HNil) => head.unsafeSet(ps, n, h) }, - { case (rs, n, h :: HNil) => head.unsafeUpdate(rs, n, h) } + implicit H: Write[H] OrElse Derived[MkWrite[H]] + ): Derived[MkWrite[FieldType[K, H] :: HNil]] = { + val head = H.fold(identity, _.instance) + + new Derived( + new MkWrite( + new Write.Composite(List(head), { case h :: HNil => List(h) }) + ) ) } } -trait LowerPriorityWrite extends EvenLowerPriorityWrite { +trait LowerPriorityMkWrite { // Derivation inductive case for product types implicit def product[H, T <: HList]( implicit - H: Write[H] OrElse MkWrite[H], - T: MkWrite[T] - ): MkWrite[H :: T] = { - val head = H.unify - - new MkWrite( - head.puts ++ T.puts, - { case h :: t => head.toList(h) ++ T.toList(t) }, - { case (ps, n, h :: t) => head.unsafeSet(ps, n, h); T.unsafeSet(ps, n + head.length, t) }, - { case (rs, n, h :: t) => head.unsafeUpdate(rs, n, h); T.unsafeUpdate(rs, n + head.length, t) } + H: Write[H] OrElse Derived[MkWrite[H]], + T: Write[T] OrElse Derived[MkWrite[T]] + ): Derived[MkWrite[H :: T]] = { + val head = H.fold(identity, _.instance) + val tail = T.fold(identity, _.instance) + + new Derived( + new MkWrite[H :: T]( + new Write.Composite( + List(head, tail), + { case h :: t => List(h, t) } + ) + ) ) } - // Derivation for product types (i.e. case class) - implicit def generic[B, A](implicit gen: Generic.Aux[B, A], A: Lazy[MkWrite[A]]): MkWrite[B] = - new MkWrite[B]( - A.value.puts, - b => A.value.toList(gen.to(b)), - (ps, n, b) => A.value.unsafeSet(ps, n, gen.to(b)), - (rs, n, b) => A.value.unsafeUpdate(rs, n, gen.to(b)) - ) - // Derivation inductive case for shapeless records implicit def record[K <: Symbol, H, T <: HList]( implicit - H: Write[H] OrElse MkWrite[H], - T: MkWrite[T] - ): MkWrite[FieldType[K, H] :: T] = { - val head = H.unify - - new MkWrite( - head.puts ++ T.puts, - { case h :: t => head.toList(h) ++ T.toList(t) }, - { case (ps, n, h :: t) => head.unsafeSet(ps, n, h); T.unsafeSet(ps, n + head.length, t) }, - { case (rs, n, h :: t) => head.unsafeUpdate(rs, n, h); T.unsafeUpdate(rs, n + head.length, t) } - ) - } - - // Derivation base case for Option of product types (1-element) - implicit def optProductBase[H]( - implicit - H: Write[Option[H]] OrElse MkWrite[Option[H]], - N: H <:!< Option[α] forSome { type α } - ): MkWrite[Option[H :: HNil]] = { - void(N) - val head = H.unify - - def withHead[A](opt: Option[H :: HNil])(f: Option[H] => A): A = { - f(opt.map(_.head)) - } - - new MkWrite( - head.puts, - withHead(_)(head.toList(_)), - (ps, n, i) => withHead(i)(h => head.unsafeSet(ps, n, h)), - (rs, n, i) => withHead(i)(h => head.unsafeUpdate(rs, n, h)) - ) - - } - - // Derivation base case for Option of product types (where the head element is Option) - implicit def optProductOptBase[H]( - implicit H: Write[Option[H]] OrElse MkWrite[Option[H]] - ): MkWrite[Option[Option[H] :: HNil]] = { - val head = H.unify - - def withHead[A](opt: Option[Option[H] :: HNil])(f: Option[H] => A): A = { - opt match { - case Some(h :: _) => f(h) - case None => f(None) - } - } - - new MkWrite( - head.puts, - withHead(_) { h => head.toList(h) }, - (ps, n, i) => withHead(i) { h => head.unsafeSet(ps, n, h) }, - (rs, n, i) => withHead(i) { h => head.unsafeUpdate(rs, n, h) } - ) - - } - -} - -trait EvenLowerPriorityWrite { - - // Write[Option[H]], Write[Option[T]] implies Write[Option[H *: T]] - implicit def optPorduct[H, T <: HList]( - implicit - H: Write[Option[H]] OrElse MkWrite[Option[H]], - T: MkWrite[Option[T]], - N: H <:!< Option[α] forSome { type α } - ): MkWrite[Option[H :: T]] = { - void(N) - val head = H.unify - - def split[A](i: Option[H :: T])(f: (Option[H], Option[T]) => A): A = - i.fold(f(None, None)) { case h :: t => f(Some(h), Some(t)) } - - new MkWrite( - head.puts ++ T.puts, - split(_) { (h, t) => head.toList(h) ++ T.toList(t) }, - (ps, n, i) => split(i) { (h, t) => head.unsafeSet(ps, n, h); T.unsafeSet(ps, n + head.length, t) }, - (rs, n, i) => split(i) { (h, t) => head.unsafeUpdate(rs, n, h); T.unsafeUpdate(rs, n + head.length, t) } + H: Write[H] OrElse Derived[MkWrite[H]], + T: Write[T] OrElse Derived[MkWrite[T]] + ): Derived[MkWrite[FieldType[K, H] :: T]] = { + val head = H.fold(identity, _.instance) + val tail = T.fold(identity, _.instance) + + new Derived( + new MkWrite( + new Write.Composite( + List(head, tail), + { + case h :: t => List(h, t) + } + ) + ) ) - } - // Write[Option[H]], Write[Option[T]] implies Write[Option[Option[H] *: T]] - implicit def optProductOpt[H, T <: HList]( + // Derivation for product types (i.e. case class) + implicit def genericWrite[A, Repr <: HList]( implicit - H: Write[Option[H]] OrElse MkWrite[Option[H]], - T: MkWrite[Option[T]] - ): MkWrite[Option[Option[H] :: T]] = { - val head = H.unify - - def split[A](i: Option[Option[H] :: T])(f: (Option[H], Option[T]) => A): A = - i.fold(f(None, None)) { case oh :: t => f(oh, Some(t)) } - - new MkWrite( - head.puts ++ T.puts, - split(_) { (h, t) => head.toList(h) ++ T.toList(t) }, - (ps, n, i) => split(i) { (h, t) => head.unsafeSet(ps, n, h); T.unsafeSet(ps, n + head.length, t) }, - (rs, n, i) => split(i) { (h, t) => head.unsafeUpdate(rs, n, h); T.unsafeUpdate(rs, n + head.length, t) } + gen: Generic.Aux[A, Repr], + hlistWrite: Lazy[Write[Repr] OrElse Derived[MkWrite[Repr]]] + ): Derived[MkWrite[A]] = { + val g = hlistWrite.value.fold(identity, _.instance) + + new Derived( + new MkWrite[A]( + new Write.Composite(List(g), a => List(gen.to(a))) + ) ) - } - // Derivation for optional of product types (i.e. case class) - implicit def ogeneric[B, A <: HList]( - implicit - G: Generic.Aux[B, A], - A: Lazy[MkWrite[Option[A]]] - ): MkWrite[Option[B]] = - new MkWrite( - A.value.puts, - b => A.value.toList(b.map(G.to)), - (rs, n, a) => A.value.unsafeSet(rs, n, a.map(G.to)), - (rs, n, a) => A.value.unsafeUpdate(rs, n, a.map(G.to)) - ) - } diff --git a/modules/core/src/main/scala-2/doobie/util/PutPlatform.scala b/modules/core/src/main/scala-2/doobie/util/PutPlatform.scala index 26e3c5cf5..c237bc6fb 100644 --- a/modules/core/src/main/scala-2/doobie/util/PutPlatform.scala +++ b/modules/core/src/main/scala-2/doobie/util/PutPlatform.scala @@ -11,13 +11,15 @@ trait PutPlatform { import doobie.util.compat.=:= /** @group Instances */ - @deprecated("Use Put.derived instead to derive instances explicitly", "1.0.0-RC6") def unaryProductPut[A, L <: HList, H, T <: HList]( implicit G: Generic.Aux[A, L], C: IsHCons.Aux[L, H, T], H: Lazy[Put[H]], E: (H :: HNil) =:= L - ): MkPut[A] = MkPut.unaryProductPut + ): Put[A] = { + void(E) // E is a necessary constraint but isn't used directly + H.value.contramap[A](a => G.to(a).head) + } } diff --git a/modules/core/src/main/scala-2/doobie/util/ReadPlatform.scala b/modules/core/src/main/scala-2/doobie/util/ReadPlatform.scala index 64a5e7371..d3789e119 100644 --- a/modules/core/src/main/scala-2/doobie/util/ReadPlatform.scala +++ b/modules/core/src/main/scala-2/doobie/util/ReadPlatform.scala @@ -4,32 +4,51 @@ package doobie.util -import shapeless.{Generic, HList, IsTuple, Lazy} +import shapeless.labelled.FieldType +import shapeless.{Generic, HList, IsTuple, Lazy, OrElse} +import shapeless.{::, HNil} -trait ReadPlatform { +trait ReadPlatform extends LowerPriority1ReadPlatform { // Derivation for product types (i.e. case class) implicit def genericTuple[A, Repr <: HList](implicit gen: Generic.Aux[A, Repr], - G: Lazy[MkRead[Repr]], + G: Lazy[Read[Repr]], isTuple: IsTuple[A] - ): MkRead[A] = { + ): Read[A] = { val _ = isTuple - MkRead.generic[A, Repr] + implicit val r: Lazy[Read[Repr] OrElse Derived[MkRead[Repr]]] = G.map(OrElse.primary(_)) + MkRead.genericRead[A, Repr].instance } - // Derivation for optional of product types (i.e. case class) - implicit def ogenericTuple[A, Repr <: HList]( + @deprecated("Use Read.derived instead to derive instances explicitly", "1.0.0-RC6") + def generic[T, Repr <: HList]( implicit - G: Generic.Aux[A, Repr], - B: Lazy[MkRead[Option[Repr]]], - isTuple: IsTuple[A] - ): MkRead[Option[A]] = { - val _ = isTuple - MkRead.ogeneric[A, Repr] - } + gen: Generic.Aux[T, Repr], + G: Lazy[Read[Repr] OrElse Derived[MkRead[Repr]]] + ): Read[T] = + MkRead.genericRead[T, Repr].instance - @deprecated("Use Read.derived instead to derive instances explicitly", "1.0.0-RC6") - def generic[T, Repr](implicit gen: Generic.Aux[T, Repr], G: Lazy[MkRead[Repr]]): MkRead[T] = - MkRead.generic[T, Repr] + implicit def recordBase[K <: Symbol, H]( + implicit H: Read[H] + ): Read[FieldType[K, H] :: HNil] = MkRead.recordBase[K, H].instance + + implicit def productBase[H]( + implicit H: Read[H] + ): Read[H :: HNil] = MkRead.productBase[H].instance +} + +trait LowerPriority1ReadPlatform extends LowestPriorityRead { + + implicit def product[H, T <: HList]( + implicit + H: Read[H], + T: Read[T] + ): Read[H :: T] = MkRead.product[H, T].instance + + implicit def record[K <: Symbol, H, T <: HList]( + implicit + H: Read[H], + T: Read[T] + ): Read[FieldType[K, H] :: T] = MkRead.record[K, H, T].instance } diff --git a/modules/core/src/main/scala-2/doobie/util/WritePlatform.scala b/modules/core/src/main/scala-2/doobie/util/WritePlatform.scala index 0553067d2..dbe7287ba 100644 --- a/modules/core/src/main/scala-2/doobie/util/WritePlatform.scala +++ b/modules/core/src/main/scala-2/doobie/util/WritePlatform.scala @@ -4,31 +4,53 @@ package doobie.util -import shapeless.{Generic, HList, IsTuple, Lazy} +import shapeless.* +import shapeless.labelled.FieldType -trait WritePlatform { +trait WritePlatform extends LowerPriority1WritePlatform { - implicit def genericTuple[A, Repr]( + implicit def genericTuple[A, Repr <: HList]( implicit gen: Generic.Aux[A, Repr], - A: Lazy[MkWrite[Repr]], + G: Lazy[Write[Repr]], isTuple: IsTuple[A] - ): MkWrite[A] = { + ): Write[A] = { val _ = isTuple - MkWrite.generic[A, Repr] + implicit val hlistWrite: Lazy[Write[Repr] OrElse Derived[MkWrite[Repr]]] = G.map(OrElse.primary(_)) + MkWrite.genericWrite[A, Repr].instance } - implicit def ogenericTuple[A, Repr <: HList]( - implicit - G: Generic.Aux[A, Repr], - A: Lazy[MkWrite[Option[Repr]]], - isTuple: IsTuple[A] - ): MkWrite[Option[A]] = { - val _ = isTuple - MkWrite.ogeneric[A, Repr] + @deprecated("Use Write.derived instead to derive instances explicitly", "1.0.0-RC6") + def generic[T, Repr <: HList](implicit + gen: Generic.Aux[T, Repr], + A: Write[Repr] OrElse Derived[MkWrite[Repr]] + ): Write[T] = { + implicit val hlistWrite: Lazy[Write[Repr] OrElse Derived[MkWrite[Repr]]] = A + MkWrite.genericWrite[T, Repr].instance } - @deprecated("Use Write.derived instead to derive instances explicitly", "1.0.0-RC6") - def generic[T, Repr](implicit gen: Generic.Aux[T, Repr], A: Lazy[MkWrite[Repr]]): MkWrite[T] = - MkWrite.generic[T, Repr] + implicit def recordBase[K <: Symbol, H]( + implicit H: Write[H] + ): Write[FieldType[K, H] :: HNil] = MkWrite.recordBase[K, H].instance + + implicit def productBase[H]( + implicit H: Write[H] + ): Write[H :: HNil] = MkWrite.productBase[H].instance + +} + +trait LowerPriority1WritePlatform extends LowestPriorityWrite { + + implicit def product[H, T <: HList]( + implicit + H: Write[H], + T: Write[T] + ): Write[H :: T] = MkWrite.product[H, T].instance + + implicit def record[K <: Symbol, H, T <: HList]( + implicit + H: Write[H], + T: Write[T] + ): Write[FieldType[K, H] :: T] = MkWrite.record[K, H, T].instance + } diff --git a/modules/core/src/main/scala-3/doobie/util/MkGetPlatform.scala b/modules/core/src/main/scala-3/doobie/util/MkGetPlatform.scala deleted file mode 100644 index 833dbcabc..000000000 --- a/modules/core/src/main/scala-3/doobie/util/MkGetPlatform.scala +++ /dev/null @@ -1,20 +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 - -import scala.deriving.Mirror - -trait MkGetPlatform: - - // Get is available for single-element products. - given unaryProductGet[P <: Product, A]( - using - p: Mirror.ProductOf[P], - i: p.MirroredElemTypes =:= (A *: EmptyTuple), - g: Get[A] - ): MkGet[P] = { - val get = g.map(a => p.fromProduct(a *: EmptyTuple)) - MkGet.lift(get) - } diff --git a/modules/core/src/main/scala-3/doobie/util/MkPutPlatform.scala b/modules/core/src/main/scala-3/doobie/util/MkPutPlatform.scala deleted file mode 100644 index 9c9afcacb..000000000 --- a/modules/core/src/main/scala-3/doobie/util/MkPutPlatform.scala +++ /dev/null @@ -1,20 +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 - -import scala.deriving.Mirror - -trait MkPutPlatform: - - // Put is available for single-element products. - given unaryProductPut[P <: Product, A]( - using - m: Mirror.ProductOf[P], - i: m.MirroredElemTypes =:= (A *: EmptyTuple), - p: Put[A] - ): MkPut[P] = { - val put: Put[P] = p.contramap(p => i(Tuple.fromProductTyped(p)).head) - MkPut.lift(put) - } diff --git a/modules/core/src/main/scala-3/doobie/util/MkReadPlatform.scala b/modules/core/src/main/scala-3/doobie/util/MkReadPlatform.scala index 5a002a261..e77390817 100644 --- a/modules/core/src/main/scala-3/doobie/util/MkReadPlatform.scala +++ b/modules/core/src/main/scala-3/doobie/util/MkReadPlatform.scala @@ -6,103 +6,53 @@ package doobie.util import scala.deriving.Mirror import doobie.util.shapeless.OrElse +import scala.util.NotGiven trait MkReadPlatform: - // Generic Read for products. - given derived[P <: Product, A]( - using + // Derivation for product types (i.e. case class) + implicit def derived[P <: Product, A]( + implicit m: Mirror.ProductOf[P], i: A =:= m.MirroredElemTypes, - w: MkRead[A] - ): MkRead[P] = { - val read = w.map(a => m.fromProduct(i(a))) - MkRead.lift(read) - } - - // Generic Read for option of products. - given derivedOption[P <: Product, A]( - using - m: Mirror.ProductOf[P], - i: A =:= m.MirroredElemTypes, - w: MkRead[Option[A]] - ): MkRead[Option[P]] = { - val read = w.map(a => a.map(a => m.fromProduct(i(a)))) - MkRead.lift(read) - } - - // Derivation base case for product types (1-element) - given productBase[H]( - using H: Read[H] `OrElse` MkRead[H] - ): MkRead[H *: EmptyTuple] = { - val head = H.unify - new MkRead( - head.gets, - (rs, n) => head.unsafeGet(rs, n) *: EmptyTuple - ) - } - - // Read for head and tail. - given product[H, T <: Tuple]( - using - H: Read[H] `OrElse` MkRead[H], - T: MkRead[T] - ): MkRead[H *: T] = { - val head = H.unify - - new MkRead[H *: T]( - head.gets ++ T.gets, - (rs, n) => head.unsafeGet(rs, n) *: T.unsafeGet(rs, n + head.length) - ) - } - - given optProductBase[H]( - using H: Read[Option[H]] `OrElse` MkRead[Option[H]] - ): MkRead[Option[H *: EmptyTuple]] = { - val head = H.unify - MkRead[Option[H *: EmptyTuple]]( - head.gets, - (rs, n) => head.unsafeGet(rs, n).map(_ *: EmptyTuple) - ) - } - - given optProduct[H, T <: Tuple]( - using - H: Read[Option[H]] `OrElse` MkRead[Option[H]], - T: MkRead[Option[T]] - ): MkRead[Option[H *: T]] = { - val head = H.unify - - new MkRead[Option[H *: T]]( - head.gets ++ T.gets, - (rs, n) => - for { - h <- head.unsafeGet(rs, n) - t <- T.unsafeGet(rs, n + head.length) - } yield h *: t - ) - } - - given optProductOptBase[H]( - using H: Read[Option[H]] `OrElse` MkRead[Option[H]] - ): MkRead[Option[Option[H] *: EmptyTuple]] = { - val head = H.unify - - MkRead[Option[Option[H] *: EmptyTuple]]( - head.gets, - (rs, n) => head.unsafeGet(rs, n).map(h => Some(h) *: EmptyTuple) + r: Read[A] `OrElse` Derived[MkRead[A]], + isNotCaseObj: NotGiven[m.MirroredElemTypes =:= EmptyTuple] + ): Derived[MkRead[P]] = { + val _ = isNotCaseObj + val read = r.fold(identity, _.instance).map(a => m.fromProduct(i(a))) + new Derived(new MkRead(read)) + } + + // Derivation base case for tuple (1-element) + implicit def productBase[H]( + implicit H: Read[H] `OrElse` Derived[MkRead[H]] + ): Derived[MkRead[H *: EmptyTuple]] = { + val headInstance = H.fold(identity, _.instance) + new Derived( + new MkRead( + Read.Composite( + List(headInstance), + list => list(0).asInstanceOf[H] *: EmptyTuple + ) + )) + } + + // Derivation inductive case for tuples + implicit def product[H, T <: Tuple]( + implicit + H: Read[H] `OrElse` Derived[MkRead[H]], + T: Read[T] `OrElse` Derived[MkRead[T]] + ): Derived[MkRead[H *: T]] = { + val headInstance = H.fold(identity, _.instance) + val tailInstance = T.fold(identity, _.instance) + + new Derived( + new MkRead( + Read.Composite( + List(headInstance, tailInstance), + list => list(0).asInstanceOf[H] *: list(1).asInstanceOf[T] + ) + ) ) - } - - given optProductOpt[H, T <: Tuple]( - using - H: Read[Option[H]] `OrElse` MkRead[Option[H]], - T: MkRead[Option[T]] - ): MkRead[Option[Option[H] *: T]] = { - val head = H.unify - new MkRead[Option[Option[H] *: T]]( - head.gets ++ T.gets, - (rs, n) => T.unsafeGet(rs, n + head.length).map(head.unsafeGet(rs, n) *: _) - ) } diff --git a/modules/core/src/main/scala-3/doobie/util/MkWritePlatform.scala b/modules/core/src/main/scala-3/doobie/util/MkWritePlatform.scala index 78d200e60..91f393017 100644 --- a/modules/core/src/main/scala-3/doobie/util/MkWritePlatform.scala +++ b/modules/core/src/main/scala-3/doobie/util/MkWritePlatform.scala @@ -6,130 +6,50 @@ package doobie.util import scala.deriving.Mirror import doobie.util.shapeless.OrElse +import scala.util.NotGiven trait MkWritePlatform: // Derivation for product types (i.e. case class) - given derived[P <: Product, A]( - using + implicit def derived[P <: Product, A]( + implicit m: Mirror.ProductOf[P], i: m.MirroredElemTypes =:= A, - w: MkWrite[A] - ): MkWrite[P] = - val write: Write[P] = w.contramap(p => i(Tuple.fromProductTyped(p))) - MkWrite.lift(write) - - // Derivation for optional product types - given derivedOption[P <: Product, A]( - using - m: Mirror.ProductOf[P], - i: m.MirroredElemTypes =:= A, - w: MkWrite[Option[A]] - ): MkWrite[Option[P]] = - val write: Write[Option[P]] = w.contramap(op => op.map(p => i(Tuple.fromProductTyped(p)))) - MkWrite.lift(write) - - // Derivation base case for product types (1-element) - given productBase[H]( - using H: Write[H] `OrElse` MkWrite[H] - ): MkWrite[H *: EmptyTuple] = { - val head = H.unify - MkWrite( - head.puts, - { case h *: t => head.toList(h) }, - { case (ps, n, h *: t) => head.unsafeSet(ps, n, h) }, - { case (rs, n, h *: t) => head.unsafeUpdate(rs, n, h) } + w: Write[A] `OrElse` Derived[MkWrite[A]], + isNotCaseObj: NotGiven[m.MirroredElemTypes =:= EmptyTuple] + ): Derived[MkWrite[P]] = + val _ = isNotCaseObj + val write: Write[P] = w.fold(identity, _.instance).contramap(p => i(Tuple.fromProductTyped(p))) + new Derived( + new MkWrite(write) ) - } - // Derivation inductive case for product types - given product[H, T <: Tuple]( - using - H: Write[H] `OrElse` MkWrite[H], - T: MkWrite[T] - ): MkWrite[H *: T] = { - val head = H.unify - - MkWrite( - head.puts ++ T.puts, - { case h *: t => head.toList(h) ++ T.toList(t) }, - { case (ps, n, h *: t) => head.unsafeSet(ps, n, h); T.unsafeSet(ps, n + head.length, t) }, - { case (rs, n, h *: t) => head.unsafeUpdate(rs, n, h); T.unsafeUpdate(rs, n + head.length, t) } + // Derivation base case for tuple (1-element) + implicit def productBase[H]( + implicit H: Write[H] `OrElse` Derived[MkWrite[H]] + ): Derived[MkWrite[H *: EmptyTuple]] = { + val headInstance = H.fold(identity, _.instance) + new Derived( + new MkWrite(Write.Composite( + List(headInstance), + { case h *: EmptyTuple => List(h) } + )) ) } - // Derivation base case for Option of product types (1-element) - given optProductBase[H]( - using H: Write[Option[H]] `OrElse` MkWrite[Option[H]] - ): MkWrite[Option[H *: EmptyTuple]] = { - val head = H.unify - - MkWrite[Option[H *: EmptyTuple]]( - head.puts, - i => head.toList(i.map { case h *: EmptyTuple => h }), - (ps, n, i) => head.unsafeSet(ps, n, i.map { case h *: EmptyTuple => h }), - (rs, n, i) => head.unsafeUpdate(rs, n, i.map { case h *: EmptyTuple => h }) + // Derivation inductive case for tuples + implicit def product[H, T <: Tuple]( + implicit + H: Write[H] `OrElse` Derived[MkWrite[H]], + T: Write[T] `OrElse` Derived[MkWrite[T]] + ): Derived[MkWrite[H *: T]] = { + val headWrite = H.fold(identity, _.instance) + val tailWrite = T.fold(identity, _.instance) + + new Derived( + new MkWrite(Write.Composite( + List(headWrite, tailWrite), + { case h *: t => List(h, t) } + )) ) } - - // Write[Option[H]], Write[Option[T]] implies Write[Option[H *: T]] - given optProduct[H, T <: Tuple]( - using - H: Write[Option[H]] `OrElse` MkWrite[Option[H]], - T: MkWrite[Option[T]] - ): MkWrite[Option[H *: T]] = - val head = H.unify - - def split[A](i: Option[H *: T])(f: (Option[H], Option[T]) => A): A = - i.fold(f(None, None)) { case h *: t => f(Some(h), Some(t)) } - - MkWrite( - head.puts ++ T.puts, - split(_) { (h, t) => head.toList(h) ++ T.toList(t) }, - (ps, n, i) => - split(i) { (h, t) => - head.unsafeSet(ps, n, h); T.unsafeSet(ps, n + head.length, t) - }, - (rs, n, i) => - split(i) { (h, t) => - head.unsafeUpdate(rs, n, h); T.unsafeUpdate(rs, n + head.length, t) - } - ) - - // Derivation base case for Option of product types (where the head element is Option) - given optProductOptBase[H]( - using H: Write[Option[H]] `OrElse` MkWrite[Option[H]] - ): MkWrite[Option[Option[H] *: EmptyTuple]] = { - val head = H.unify - - MkWrite[Option[Option[H] *: EmptyTuple]]( - head.puts, - i => head.toList(i.flatMap { case ho *: EmptyTuple => ho }), - (ps, n, i) => head.unsafeSet(ps, n, i.flatMap { case ho *: EmptyTuple => ho }), - (rs, n, i) => head.unsafeUpdate(rs, n, i.flatMap { case ho *: EmptyTuple => ho }) - ) - } - - // Write[Option[H]], Write[Option[T]] implies Write[Option[Option[H] *: T]] - given optProductOpt[H, T <: Tuple]( - using - H: Write[Option[H]] `OrElse` MkWrite[Option[H]], - T: MkWrite[Option[T]] - ): MkWrite[Option[Option[H] *: T]] = - val head = H.unify - - def split[A](i: Option[Option[H] *: T])(f: (Option[H], Option[T]) => A): A = - i.fold(f(None, None)) { case oh *: t => f(oh, Some(t)) } - - MkWrite( - head.puts ++ T.puts, - split(_) { (h, t) => head.toList(h) ++ T.toList(t) }, - (ps, n, i) => - split(i) { (h, t) => - head.unsafeSet(ps, n, h); T.unsafeSet(ps, n + head.length, t) - }, - (rs, n, i) => - split(i) { (h, t) => - head.unsafeUpdate(rs, n, h); T.unsafeUpdate(rs, n + head.length, t) - } - ) diff --git a/modules/core/src/main/scala-3/doobie/util/PutPlatform.scala b/modules/core/src/main/scala-3/doobie/util/PutPlatform.scala index 9a7151ebe..f39f8a136 100644 --- a/modules/core/src/main/scala-3/doobie/util/PutPlatform.scala +++ b/modules/core/src/main/scala-3/doobie/util/PutPlatform.scala @@ -4,4 +4,4 @@ package doobie.util -trait PutPlatform {} +trait PutPlatform diff --git a/modules/core/src/main/scala-3/doobie/util/ReadPlatform.scala b/modules/core/src/main/scala-3/doobie/util/ReadPlatform.scala index f6e0fa795..c0254cb64 100644 --- a/modules/core/src/main/scala-3/doobie/util/ReadPlatform.scala +++ b/modules/core/src/main/scala-3/doobie/util/ReadPlatform.scala @@ -4,23 +4,22 @@ package doobie.util -import scala.deriving.Mirror +trait ReadPlatform extends LowestPriorityRead: -trait ReadPlatform: - // Generic Read for products. - given derivedTuple[P <: Tuple, A]( - using - m: Mirror.ProductOf[P], - i: A =:= m.MirroredElemTypes, - w: MkRead[A] - ): MkRead[P] = - MkRead.derived[P, A] + given tupleBase[H]( + using H: Read[H] + ): Read[H *: EmptyTuple] = + Read.Composite( + List(H), + list => list(0).asInstanceOf[H] *: EmptyTuple + ) - // Generic Read for option of products. - given derivedOptionTuple[P <: Tuple, A]( + given tuple[H, T <: Tuple]( using - m: Mirror.ProductOf[P], - i: A =:= m.MirroredElemTypes, - w: MkRead[Option[A]] - ): MkRead[Option[P]] = - MkRead.derivedOption[P, A] + H: Read[H], + T: Read[T] + ): Read[H *: T] = + Read.Composite( + List(H, T), + list => list(0).asInstanceOf[H] *: list(1).asInstanceOf[T] + ) diff --git a/modules/core/src/main/scala-3/doobie/util/WritePlatform.scala b/modules/core/src/main/scala-3/doobie/util/WritePlatform.scala index 3e6989fe5..71864ba7b 100644 --- a/modules/core/src/main/scala-3/doobie/util/WritePlatform.scala +++ b/modules/core/src/main/scala-3/doobie/util/WritePlatform.scala @@ -4,24 +4,26 @@ package doobie.util -import scala.deriving.Mirror +trait WritePlatform extends LowestPriorityWrite: -trait WritePlatform: + given tupleBase[H]( + using H: Write[H] + ): Write[H *: EmptyTuple] = + Write.Composite[H *: EmptyTuple]( + List(H), + { + case h *: EmptyTuple => List(h) + } + ) - // Derivation for product types (i.e. case class) - given derivedTuple[P <: Tuple, A]( + given tuple[H, T <: Tuple]( using - m: Mirror.ProductOf[P], - i: m.MirroredElemTypes =:= A, - w: MkWrite[A] - ): MkWrite[P] = - MkWrite.derived[P, A] - - // Derivation for optional product types - given derivedOptionTuple[P <: Tuple, A]( - using - m: Mirror.ProductOf[P], - i: m.MirroredElemTypes =:= A, - w: MkWrite[Option[A]] - ): MkWrite[Option[P]] = - MkWrite.derivedOption[P, A] + H: Write[H], + T: Write[T] + ): Write[H *: T] = + Write.Composite( + List(H, T), + { + case h *: t => List(h, t) + } + ) diff --git a/modules/core/src/main/scala/doobie/generic/auto.scala b/modules/core/src/main/scala/doobie/generic/auto.scala index e8c73c6cf..c78ac237f 100644 --- a/modules/core/src/main/scala/doobie/generic/auto.scala +++ b/modules/core/src/main/scala/doobie/generic/auto.scala @@ -4,20 +4,10 @@ package doobie.generic -import doobie.util.meta.Meta -import doobie.util.{Get, Put, Read, Write} +import doobie.util.{Read, Write} trait AutoDerivation - extends Get.Auto - with Put.Auto - with Read.Auto + extends Read.Auto with Write.Auto -object auto extends AutoDerivation { - - // re-export these instances so `Meta` takes priority, must be in the object - implicit def metaProjectionGet[A](implicit m: Meta[A]): Get[A] = Get.metaProjection - 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 -} +object auto extends AutoDerivation diff --git a/modules/core/src/main/scala/doobie/package.scala b/modules/core/src/main/scala/doobie/package.scala index db89940ab..1ea5f99f6 100644 --- a/modules/core/src/main/scala/doobie/package.scala +++ b/modules/core/src/main/scala/doobie/package.scala @@ -29,12 +29,6 @@ package object doobie with LegacyMeta with syntax.AllSyntax { - // re-export these instances so `Meta` takes priority, must be in the object - implicit def metaProjectionGet[A](implicit m: Meta[A]): Get[A] = Get.metaProjection - 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 diff --git a/modules/core/src/test/scala-3/doobie/util/GetSuitePlatform.scala b/modules/core/src/main/scala/doobie/util/Derived.scala similarity index 55% rename from modules/core/src/test/scala-3/doobie/util/GetSuitePlatform.scala rename to modules/core/src/main/scala/doobie/util/Derived.scala index c82893dca..07c40fbc2 100644 --- a/modules/core/src/test/scala-3/doobie/util/GetSuitePlatform.scala +++ b/modules/core/src/main/scala/doobie/util/Derived.scala @@ -2,11 +2,6 @@ // This software is licensed under the MIT License (MIT). // For more information see LICENSE or https://opensource.org/licenses/MIT -package doobie -package util +package doobie.util -trait GetSuitePlatform { self: munit.FunSuite => - - test("Get should be derived for unary products (AnyVal)".ignore) {} - -} +class Derived[+I](val instance: I) extends AnyVal diff --git a/modules/core/src/main/scala/doobie/util/fragment.scala b/modules/core/src/main/scala/doobie/util/fragment.scala index ad7f732cb..13bf95720 100644 --- a/modules/core/src/main/scala/doobie/util/fragment.scala +++ b/modules/core/src/main/scala/doobie/util/fragment.scala @@ -6,7 +6,6 @@ package doobie.util import cats.* import cats.data.Chain -import doobie.enumerated.Nullability.* import doobie.free.connection.ConnectionIO import doobie.free.preparedstatement.PreparedStatementIO import doobie.util.pos.Pos @@ -14,9 +13,6 @@ import doobie.hi.connection as IHC import doobie.util.query.{Query, Query0} import doobie.util.update.{Update, Update0} -import java.sql.{PreparedStatement, ResultSet} -import scala.Predef.{augmentString, implicitly} - /** Module defining the `Fragment` data type. */ object fragment { @@ -35,42 +31,20 @@ object fragment { private implicit lazy val write: Write[elems.type] = { import Elem.* - val puts: List[(Put[?], NullabilityKnown)] = + val writes: List[Write[?]] = elems.map { - case Arg(_, p) => (p, NoNulls) - case Opt(_, p) => (p, Nullable) + case Arg(_, p) => new Write.Single(p) + case Opt(_, p) => new Write.SingleOpt(p) }.toList - val toList: elems.type => List[Any] = elems => - elems.map { - case Arg(a, _) => a - case Opt(a, _) => a - }.toList - - val unsafeSet: (PreparedStatement, Int, elems.type) => Unit = { (ps, n, elems) => - var index = n - elems.iterator.foreach { e => - e match { - case Arg(a, p) => p.unsafeSetNonNullable(ps, index, a) - case Opt(a, p) => p.unsafeSetNullable(ps, index, a) - } - index += 1 - } - } - - val unsafeUpdate: (ResultSet, Int, elems.type) => Unit = { (ps, n, elems) => - var index = n - elems.iterator.foreach { e => - e match { - case Arg(a, p) => p.unsafeUpdateNonNullable(ps, index, a) - case Opt(a, p) => p.unsafeUpdateNullable(ps, index, a) - } - index += 1 - } - } - - Write(puts, toList, unsafeSet, unsafeUpdate) - + new Write.Composite( + writes, + elems => + elems.map { + case Arg(a, _) => a + case Opt(aOpt, _) => aOpt + }.toList + ) } /** Construct a program in ConnectionIO that constructs and prepares a PreparedStatement, with further handling diff --git a/modules/core/src/main/scala/doobie/util/get.scala b/modules/core/src/main/scala/doobie/util/get.scala index e1a9557b1..44dca67e7 100644 --- a/modules/core/src/main/scala/doobie/util/get.scala +++ b/modules/core/src/main/scala/doobie/util/get.scala @@ -84,12 +84,6 @@ object Get extends GetInstances with GetPlatform { 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 - } - /** Get instance for a basic JDBC type. */ object Basic { @@ -213,23 +207,3 @@ trait GetInstances { ev.tmap(_.toVector) } - -sealed abstract class MkGet[A]( - override val typeStack: NonEmptyList[Option[String]], - override val jdbcSources: NonEmptyList[JdbcType], - override val jdbcSourceSecondary: List[JdbcType], - override val vendorTypeNames: List[String], - override val get: Coyoneda[(ResultSet, Int) => *, A] -) extends Get[A](typeStack, jdbcSources, jdbcSourceSecondary, vendorTypeNames, get) - -object MkGet extends MkGetPlatform { - - def lift[A](g: Get[A]): MkGet[A] = - 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/put.scala b/modules/core/src/main/scala/doobie/util/put.scala index 2588c8ce9..68d7ae13b 100644 --- a/modules/core/src/main/scala/doobie/util/put.scala +++ b/modules/core/src/main/scala/doobie/util/put.scala @@ -82,16 +82,10 @@ sealed abstract class Put[A]( } -object Put extends PutInstances with PutPlatform { +object Put extends PutInstances { def apply[A](implicit ev: Put[A]): ev.type = ev - def derived[A](implicit ev: MkPut[A]): Put[A] = ev - - trait Auto { - implicit def derivePut[A](implicit ev: MkPut[A]): Put[A] = ev - } - object Basic { def apply[A]( @@ -208,7 +202,7 @@ object Put extends PutInstances with PutPlatform { } -trait PutInstances { +trait PutInstances extends PutPlatform { /** @group Instances */ implicit val ContravariantPut: Contravariant[Put] = @@ -226,23 +220,3 @@ trait PutInstances { ev.tcontramap(_.toArray) } - -sealed abstract class MkPut[A]( - override val typeStack: NonEmptyList[Option[String]], - override val jdbcTargets: NonEmptyList[JdbcType], - 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, vendorTypeNames, put, update) - -object MkPut extends MkPutPlatform { - - def lift[A](g: Put[A]): MkPut[A] = - new MkPut[A]( - typeStack = g.typeStack, - jdbcTargets = g.jdbcTargets, - vendorTypeNames = g.vendorTypeNames, - put = g.put, - update = g.update - ) {} -} diff --git a/modules/core/src/main/scala/doobie/util/read.scala b/modules/core/src/main/scala/doobie/util/read.scala index a8f3e4bdb..3aa9c71a0 100644 --- a/modules/core/src/main/scala/doobie/util/read.scala +++ b/modules/core/src/main/scala/doobie/util/read.scala @@ -4,13 +4,15 @@ package doobie.util -import cats.* -import doobie.free.ResultSetIO -import doobie.enumerated.Nullability.* +import cats.Applicative +import cats.implicits.* +import doobie.ResultSetIO +import doobie.enumerated.Nullability +import doobie.enumerated.Nullability.{NoNulls, NullabilityKnown} +import doobie.free.resultset as IFRS import java.sql.ResultSet import scala.annotation.implicitNotFound -import doobie.free.resultset as IFRS @implicitNotFound(""" Cannot find or construct a Read instance for type: @@ -27,7 +29,7 @@ some debugging hints: version. - For types you expect to map to a single column ensure that a Get instance is in scope. -- For case classes, HLists, and shapeless records ensure that each element +- For case classes, shapeless HLists/records ensure that each element has a Read instance in scope. - Lather, rinse, repeat, recursively until you find the problematic bit. @@ -42,67 +44,120 @@ and similarly with Get: And find the missing instance and construct it as needed. Refer to Chapter 12 of the book of doobie for more information. """) -sealed abstract class Read[A]( - val gets: List[(Get[?], NullabilityKnown)], - val unsafeGet: (ResultSet, Int) => A -) { - - final lazy val length: Int = gets.length - - def map[B](f: A => B): Read[B] = - new Read(gets, (rs, n) => f(unsafeGet(rs, n))) {} - - def ap[B](ff: Read[A => B]): Read[B] = - new Read(ff.gets ++ gets, (rs, n) => ff.unsafeGet(rs, n)(unsafeGet(rs, n + ff.length))) {} +sealed trait Read[A] { + def unsafeGet(rs: ResultSet, startIdx: Int): A + def gets: List[(Get[?], NullabilityKnown)] + def toOpt: Read[Option[A]] + def length: Int - def get(n: Int): ResultSetIO[A] = + final def get(n: Int): ResultSetIO[A] = IFRS.raw(unsafeGet(_, n)) -} + final def map[B](f: A => B): Read[B] = new Read.Composite[B](List(this), l => f(l.head.asInstanceOf[A])) -object Read extends ReadPlatform { + final def ap[B](ff: Read[A => B]): Read[B] = + new Read.Composite[B](List(ff, this), l => l(0).asInstanceOf[A => B](l(1).asInstanceOf[A])) +} - def apply[A]( - gets: List[(Get[?], NullabilityKnown)], - unsafeGet: (ResultSet, Int) => A - ): Read[A] = new Read(gets, unsafeGet) {} +object Read extends LowerPriorityRead { - def apply[A](implicit ev: Read[A]): ev.type = ev + def apply[A](implicit ev: Read[A]): Read[A] = ev - def derived[A](implicit ev: MkRead[A]): Read[A] = ev + def derived[A](implicit + @implicitNotFound( + "Cannot derive Read instance. Please check that each field in the case class has a Read instance or can derive one") + ev: Derived[MkRead[A]] + ): Read[A] = ev.instance.underlying - trait Auto { - implicit def deriveRead[A](implicit ev: MkRead[A]): Read[A] = ev - } + trait Auto extends MkReadInstances implicit val ReadApply: Applicative[Read] = new Applicative[Read] { def ap[A, B](ff: Read[A => B])(fa: Read[A]): Read[B] = fa.ap(ff) - def pure[A](x: A): Read[A] = new Read(Nil, (_, _) => x) {} + def pure[A](x: A): Read[A] = new Read.Composite(Nil, _ => x) override def map[A, B](fa: Read[A])(f: A => B): Read[B] = fa.map(f) } - implicit val unit: Read[Unit] = - Read(Nil, (_, _) => ()) + implicit val unit: Read[Unit] = new Read.Composite(Nil, _ => ()) + + implicit val optionUnit: Read[Option[Unit]] = new Read.Composite(Nil, _ => Some(())) - implicit val optionUnit: Read[Option[Unit]] = - Read(Nil, (_, _) => Some(())) + implicit def fromReadOption[A](implicit read: Read[A]): Read[Option[A]] = read.toOpt - implicit def fromGet[A](implicit ev: Get[A]): Read[A] = - new Read(List((ev, NoNulls)), ev.unsafeGetNonNullable) {} + /** Simple instance wrapping a Get. i.e. single column non-null value */ + class Single[A](get: Get[A]) extends Read[A] { + def unsafeGet(rs: ResultSet, startIdx: Int): A = + get.unsafeGetNonNullable(rs, startIdx) + + override def toOpt: Read[Option[A]] = new SingleOpt(get) + + override def gets: List[(Get[?], NullabilityKnown)] = List(get -> NoNulls) + + override val length: Int = 1 + + } + + /** Simple instance wrapping a Get. i.e. single column nullable value */ + class SingleOpt[A](get: Get[A]) extends Read[Option[A]] { + def unsafeGet(rs: ResultSet, startIdx: Int): Option[A] = + get.unsafeGetNullable(rs, startIdx) + + override def toOpt: Read[Option[Option[A]]] = new Composite(List(this), l => Some(l.head.asInstanceOf[Option[A]])) + override def gets: List[(Get[?], NullabilityKnown)] = List(get -> Nullability.Nullable) + + override val length: Int = 1 + } - implicit def fromGetOption[A](implicit ev: Get[A]): Read[Option[A]] = - new Read(List((ev, Nullable)), ev.unsafeGetNullable) {} + /** A Read instance consists of multiple underlying Read instances */ + class Composite[A](readInstances: List[Read[?]], construct: List[Any] => A) extends Read[A] { + + override val length: Int = readInstances.map(_.length).sum + + override lazy val gets: List[(Get[?], NullabilityKnown)] = readInstances.flatMap(_.gets) + + override def unsafeGet(rs: ResultSet, startIdx: Int): A = { + import scala.collection.mutable + val accum = mutable.ListBuffer.empty[Any] + var idx = startIdx + readInstances.foreach { rr => + accum += rr.unsafeGet(rs, idx) + idx += rr.length + } + construct(accum.toList) + } + + override def toOpt: Read[Option[A]] = { + val readOptInstances = readInstances.map(_.toOpt) + + val constructFromOptions: List[Option[Any]] => Option[A] = l => + l.sequence.map(construct) + + new Composite[Option[A]](readOptInstances, constructFromOptions.asInstanceOf[List[Any] => Option[A]]) + } + + } } -final class MkRead[A]( - override val gets: List[(Get[?], NullabilityKnown)], - override val unsafeGet: (ResultSet, Int) => A -) extends Read[A](gets, unsafeGet) +trait LowerPriorityRead extends ReadPlatform { + + implicit def fromGet[A](implicit get: Get[A]): Read[A] = new Read.Single(get) + + implicit def fromGetOption[A](implicit get: Get[A]): Read[Option[A]] = new Read.SingleOpt(get) + +} -object MkRead extends MkReadPlatform { +trait LowestPriorityRead { + implicit def fromDerived[A](implicit ev: Derived[Read[A]]): Read[A] = ev.instance +} - def lift[A](r: Read[A]): MkRead[A] = - new MkRead[A](r.gets, r.unsafeGet) +final class MkRead[A](val underlying: Read[A]) extends Read[A] { + override def unsafeGet(rs: ResultSet, startIdx: Int): A = underlying.unsafeGet(rs, startIdx) + override def gets: List[(Get[?], NullabilityKnown)] = underlying.gets + override def toOpt: Read[Option[A]] = underlying.toOpt + override def length: Int = underlying.length } + +object MkRead extends MkReadInstances + +trait MkReadInstances extends MkReadPlatform diff --git a/modules/core/src/main/scala/doobie/util/write.scala b/modules/core/src/main/scala/doobie/util/write.scala index eb06bb09e..d47914c78 100644 --- a/modules/core/src/main/scala/doobie/util/write.scala +++ b/modules/core/src/main/scala/doobie/util/write.scala @@ -5,6 +5,7 @@ package doobie.util import cats.ContravariantSemigroupal +import doobie.enumerated.Nullability import doobie.enumerated.Nullability.* import doobie.free.{PreparedStatementIO, ResultSetIO} @@ -28,7 +29,7 @@ some debugging hints: version. - For types you expect to map to a single column ensure that a Put instance is in scope. -- For case classes, HLists, and shapeless records ensure that each element +- For case classes, shapeless HLists/records ensure that each element has a Write instance in scope. - Lather, rinse, repeat, recursively until you find the problematic bit. @@ -43,40 +44,26 @@ and similarly with Put: And find the missing instance and construct it as needed. Refer to Chapter 12 of the book of doobie for more information. """) -sealed abstract class Write[A]( - val puts: List[(Put[?], NullabilityKnown)], - val toList: A => List[Any], - val unsafeSet: (PreparedStatement, Int, A) => Unit, - val unsafeUpdate: (ResultSet, Int, A) => Unit -) { - - lazy val length = puts.length - - def set(n: Int, a: A): PreparedStatementIO[Unit] = +sealed trait Write[A] { + def puts: List[(Put[?], NullabilityKnown)] + def toList(a: A): List[Any] + def unsafeSet(ps: PreparedStatement, startIdx: Int, a: A): Unit + def unsafeUpdate(rs: ResultSet, startIdx: Int, a: A): Unit + def toOpt: Write[Option[A]] + def length: Int + + final def set(n: Int, a: A): PreparedStatementIO[Unit] = IFPS.raw(unsafeSet(_, n, a)) - def update(n: Int, a: A): ResultSetIO[Unit] = + final def update(n: Int, a: A): ResultSetIO[Unit] = IFRS.raw(unsafeUpdate(_, n, a)) - def contramap[B](f: B => A): Write[B] = - new Write[B]( - puts, - b => toList(f(b)), - (ps, n, a) => unsafeSet(ps, n, f(a)), - (rs, n, a) => unsafeUpdate(rs, n, f(a)) - ) {} - - def product[B](fb: Write[B]): Write[(A, B)] = - new Write[(A, B)]( - puts ++ fb.puts, - { case (a, b) => toList(a) ++ fb.toList(b) }, - { case (ps, n, (a, b)) => unsafeSet(ps, n, a); fb.unsafeSet(ps, n + length, b) }, - { case (rs, n, (a, b)) => unsafeUpdate(rs, n, a); fb.unsafeUpdate(rs, n + length, b) } - ) {} - - /** Given a value of type `A` and an appropriately parameterized SQL string we can construct a `Fragment`. If `sql` is - * unspecified a comma-separated list of `length` placeholders will be used. - */ + final def contramap[B](f: B => A): Write[B] = new Write.Composite[B](List(this), b => List(f(b))) + + final def product[B](fb: Write[B]): Write[(A, B)] = { + new Write.Composite[(A, B)](List(this, fb), tuple => List(tuple._1, tuple._2)) + } + def toFragment(a: A, sql: String = List.fill(length)("?").mkString(",")): Fragment = { val elems: List[Elem] = (puts zip toList(a)).map { case ((p: Put[a], NoNulls), a) => Elem.Arg(a.asInstanceOf[a], p) @@ -84,77 +71,134 @@ sealed abstract class Write[A]( } Fragment(sql, elems, None) } - } -object Write extends WritePlatform { - - def apply[A]( - puts: List[(Put[?], NullabilityKnown)], - toList: A => List[Any], - unsafeSet: (PreparedStatement, Int, A) => Unit, - unsafeUpdate: (ResultSet, Int, A) => Unit - ): Write[A] = new Write(puts, toList, unsafeSet, unsafeUpdate) {} - +object Write extends LowerPriorityWrite { def apply[A](implicit A: Write[A]): Write[A] = A - def derived[A](implicit ev: MkWrite[A]): Write[A] = ev + def derived[A](implicit + @implicitNotFound( + "Cannot derive Write instance. Please check that each field in the case class has a Write instance or can derive one") + ev: Derived[MkWrite[A]] + ): Write[A] = ev.instance - trait Auto { - implicit def deriveWrite[A](implicit ev: MkWrite[A]): Write[A] = ev - } + trait Auto extends MkWriteInstances implicit val WriteContravariantSemigroupal: ContravariantSemigroupal[Write] = new ContravariantSemigroupal[Write] { - def contramap[A, B](fa: Write[A])(f: B => A) = fa.contramap(f) - def product[A, B](fa: Write[A], fb: Write[B]) = fa.product(fb) + def contramap[A, B](fa: Write[A])(f: B => A): Write[B] = fa.contramap(f) + def product[A, B](fa: Write[A], fb: Write[B]): Write[(A, B)] = fa.product(fb) } - private def doNothing[P, A](p: P, i: Int, a: A): Unit = { - void(p, i, a) + implicit val unitComposite: Write[Unit] = + new Composite[Unit](Nil, _ => List.empty) + + implicit val optionUnit: Write[Option[Unit]] = + new Composite[Option[Unit]](Nil, _ => List.empty) + + implicit def optionalFromWrite[A](implicit write: Write[A]): Write[Option[A]] = + write.toOpt + + /** Simple instance wrapping a Put. i.e. single column non-null value */ + class Single[A](put: Put[A]) extends Write[A] { + override val length: Int = 1 + + override def unsafeSet(ps: PreparedStatement, startIdx: Int, a: A): Unit = + put.unsafeSetNonNullable(ps, startIdx, a) + + override def unsafeUpdate(rs: ResultSet, startIdx: Int, a: A): Unit = + put.unsafeUpdateNonNullable(rs, startIdx, a) + + override def puts: List[(Put[?], NullabilityKnown)] = List(put -> Nullability.NoNulls) + + override def toList(a: A): List[Any] = List(a) + + override def toOpt: Write[Option[A]] = new SingleOpt(put) } - private def empty[A](a: A): List[Any] = { - void(a) - List.empty + /** Simple instance wrapping a Put. i.e. single column nullable value */ + class SingleOpt[A](put: Put[A]) extends Write[Option[A]] { + override val length: Int = 1 + + override def unsafeSet(ps: PreparedStatement, startIdx: Int, a: Option[A]): Unit = + put.unsafeSetNullable(ps, startIdx, a) + + override def unsafeUpdate(rs: ResultSet, startIdx: Int, a: Option[A]): Unit = + put.unsafeUpdateNullable(rs, startIdx, a) + + override def puts: List[(Put[?], NullabilityKnown)] = List(put -> Nullability.Nullable) + + override def toList(a: Option[A]): List[Any] = List(a) + + override def toOpt: Write[Option[Option[A]]] = new Composite[Option[Option[A]]](List(this), x => List(x.flatten)) } - implicit val unitComposite: Write[Unit] = - Write[Unit](Nil, empty[Unit](_), doNothing[PreparedStatement, Unit](_, _, _), doNothing[ResultSet, Unit](_, _, _)) + /** A Write instance consists of multiple underlying Write instances */ + class Composite[A]( + writeInstances: List[Write[?]], + deconstruct: A => List[Any] + ) extends Write[A] { + override lazy val length: Int = writeInstances.map(_.length).sum + + // Make the types match up with deconstruct + private val anyWrites: List[Write[Any]] = writeInstances.asInstanceOf[List[Write[Any]]] + + override def unsafeSet(ps: PreparedStatement, startIdx: Int, a: A): Unit = { + val parts = deconstruct(a) + var idx = startIdx + anyWrites.zip(parts).foreach { case (w, p) => + w.unsafeSet(ps, idx, p) + idx += w.length + } + } - implicit val optionUnit: Write[Option[Unit]] = - Write[Option[Unit]]( - Nil, - empty[Option[Unit]](_), - doNothing[PreparedStatement, Option[Unit]](_, _, _), - doNothing[ResultSet, Option[Unit]](_, _, _)) - - implicit def fromPut[A](implicit P: Put[A]): Write[A] = - new Write[A]( - List((P, NoNulls)), - a => List(a), - (ps, n, a) => P.unsafeSetNonNullable(ps, n, a), - (rs, n, a) => P.unsafeUpdateNonNullable(rs, n, a) - ) {} - - implicit def fromPutOption[A](implicit P: Put[A]): Write[Option[A]] = - new Write[Option[A]]( - List((P, Nullable)), - a => List(a), - (ps, n, a) => P.unsafeSetNullable(ps, n, a), - (rs, n, a) => P.unsafeUpdateNullable(rs, n, a) - ) {} + override def unsafeUpdate(rs: ResultSet, startIdx: Int, a: A): Unit = { + val parts = deconstruct(a) + var idx = startIdx + anyWrites.zip(parts).foreach { case (w, p) => + w.unsafeUpdate(rs, idx, p) + idx += w.length + } + } + + override lazy val puts: List[(Put[?], NullabilityKnown)] = writeInstances.flatMap(_.puts) + + override def toList(a: A): List[Any] = + anyWrites.zip(deconstruct(a)).flatMap { case (w, p) => w.toList(p) } + override def toOpt: Write[Option[A]] = new Composite[Option[A]]( + writeInstances.map(_.toOpt), + { + case Some(a) => deconstruct(a).map(Some(_)) + case None => List.fill(writeInstances.length)(None) // All Nones + } + ) + } } -final class MkWrite[A]( - override val puts: List[(Put[?], NullabilityKnown)], - override val toList: A => List[Any], - override val unsafeSet: (PreparedStatement, Int, A) => Unit, - override val unsafeUpdate: (ResultSet, Int, A) => Unit -) extends Write[A](puts, toList, unsafeSet, unsafeUpdate) -object MkWrite extends MkWritePlatform { +trait LowerPriorityWrite extends WritePlatform { + + implicit def fromPut[A](implicit put: Put[A]): Write[A] = + new Write.Single(put) + + implicit def fromPutOption[A](implicit put: Put[A]): Write[Option[A]] = + new Write.SingleOpt(put) + +} - def lift[A](w: Write[A]): MkWrite[A] = - new MkWrite[A](w.puts, w.toList, w.unsafeSet, w.unsafeUpdate) +trait LowestPriorityWrite { + implicit def fromDerived[A](implicit ev: Derived[Write[A]]): Write[A] = ev.instance } + +final class MkWrite[A](val instance: Write[A]) extends Write[A] { + override def puts: List[(Put[?], NullabilityKnown)] = instance.puts + override def toList(a: A): List[Any] = instance.toList(a) + override def unsafeSet(ps: PreparedStatement, startIdx: Int, a: A): Unit = instance.unsafeSet(ps, startIdx, a) + override def unsafeUpdate(rs: ResultSet, startIdx: Int, a: A): Unit = instance.unsafeUpdate(rs, startIdx, a) + override def toOpt: Write[Option[A]] = instance.toOpt + override def length: Int = instance.length +} + +object MkWrite extends MkWriteInstances + +trait MkWriteInstances extends MkWritePlatform diff --git a/modules/core/src/test/scala-2/doobie/util/GetSuitePlatform.scala b/modules/core/src/test/scala-2/doobie/util/GetSuitePlatform.scala deleted file mode 100644 index a6daa5f06..000000000 --- a/modules/core/src/test/scala-2/doobie/util/GetSuitePlatform.scala +++ /dev/null @@ -1,29 +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 -import doobie.testutils.{VoidExtensions, assertContains} -import doobie.testutils.TestClasses.{CCIntString, PlainObj, CCAnyVal} - -trait GetSuitePlatform { self: munit.FunSuite => - - test("Get can be auto derived for unary products (AnyVal)") { - import doobie.generic.auto.* - - Get[CCAnyVal].void - } - - test("Get can be explicitly derived for unary products (AnyVal)") { - Get.derived[CCAnyVal].void - } - - test("Get should not be derived for non-unary products") { - import doobie.generic.auto.* - - assertContains(compileErrors("Get[CCIntString]"), "implicit value") - assertContains(compileErrors("Get[(Int, Int)]"), "implicit value") - assertContains(compileErrors("Get[PlainObj.type]"), "implicit value") - } - -} diff --git a/modules/core/src/test/scala-2/doobie/util/PutSuitePlatform.scala b/modules/core/src/test/scala-2/doobie/util/PutSuitePlatform.scala deleted file mode 100644 index a7eda5bed..000000000 --- a/modules/core/src/test/scala-2/doobie/util/PutSuitePlatform.scala +++ /dev/null @@ -1,28 +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 -import doobie.testutils.{VoidExtensions, assertContains} -import doobie.testutils.TestClasses.{CCIntString, PlainObj, CCAnyVal} - -trait PutSuitePlatform { self: munit.FunSuite => - test("Put can be auto derived for unary products (AnyVal)") { - import doobie.generic.auto.* - - Put[CCAnyVal].void - } - - test("Put can be explicitly derived for unary products (AnyVal)") { - Put.derived[CCAnyVal].void - } - - test("Put should not be derived for non-unary products") { - import doobie.generic.auto.* - - assertContains(compileErrors("Put[CCIntString]"), "implicit value") - assertContains(compileErrors("Put[(Int, Int)]"), "implicit value") - assertContains(compileErrors("Put[PlainObj.type]"), "implicit value") - } - -} diff --git a/modules/core/src/test/scala-2/doobie/util/QueryLogSuitePlatform.scala b/modules/core/src/test/scala-2/doobie/util/QueryLogSuitePlatform.scala index b82855515..539e82b4e 100644 --- a/modules/core/src/test/scala-2/doobie/util/QueryLogSuitePlatform.scala +++ b/modules/core/src/test/scala-2/doobie/util/QueryLogSuitePlatform.scala @@ -8,7 +8,6 @@ import doobie.util.log.{Parameters, ProcessingFailure, Success} import shapeless._ trait QueryLogSuitePlatform { self: QueryLogSuite => - import doobie.generic.auto._ test("[Query] n-arg success") { val Sql = "select 1 where ? = ?" diff --git a/modules/core/src/test/scala-3/doobie/util/PutSuitePlatform.scala b/modules/core/src/test/scala-3/doobie/util/PutSuitePlatform.scala deleted file mode 100644 index 96305ccf2..000000000 --- a/modules/core/src/test/scala-3/doobie/util/PutSuitePlatform.scala +++ /dev/null @@ -1,23 +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 -import doobie.testutils.assertContains - -import scala.annotation.nowarn - -trait PutSuitePlatform { self: munit.FunSuite => - - test("Put should be derived for unary products (AnyVal)".ignore) {} - - test("Put should not be derived for non-unary products") { - import doobie.generic.auto.* - import doobie.testutils.TestClasses.{CCIntString, PlainObj} - - assertContains(compileErrors("Put[CCIntString]"), "No given instance") - assertContains(compileErrors("Put[(Int, Int)]"), "No given instance") - assertContains(compileErrors("Put[PlainObj.type]"), "No given instance") - }: @nowarn("msg=.*unused.*") - -} diff --git a/modules/core/src/test/scala-3/doobie/util/QueryLogSuitePlatform.scala b/modules/core/src/test/scala-3/doobie/util/QueryLogSuitePlatform.scala index 6943cfcfe..81aa8532c 100644 --- a/modules/core/src/test/scala-3/doobie/util/QueryLogSuitePlatform.scala +++ b/modules/core/src/test/scala-3/doobie/util/QueryLogSuitePlatform.scala @@ -7,7 +7,6 @@ package doobie.util import doobie.util.log.{Parameters, Success, ProcessingFailure} trait QueryLogSuitePlatform { self: QueryLogSuite => - import doobie.generic.auto.* test("[Query] n-arg success") { val Sql = "select 1 where ? = ?" diff --git a/modules/core/src/test/scala/doobie/util/GetSuite.scala b/modules/core/src/test/scala/doobie/util/GetSuite.scala index fa9b74b2e..35b985b44 100644 --- a/modules/core/src/test/scala/doobie/util/GetSuite.scala +++ b/modules/core/src/test/scala/doobie/util/GetSuite.scala @@ -9,7 +9,7 @@ import doobie.enumerated.JdbcType import doobie.testutils.VoidExtensions import doobie.util.transactor.Transactor -class GetSuite extends munit.FunSuite with GetSuitePlatform { +class GetSuite extends munit.FunSuite { case class X(x: Int) case class Q(x: String) @@ -22,29 +22,6 @@ class GetSuite extends munit.FunSuite with GetSuitePlatform { Get[String].void } - test("Get should be auto derived for unary products") { - import doobie.generic.auto.* - - Get[X].void - Get[Q].void - } - - test("Get is not auto derived without an import") { - compileErrors("Get[X]").void - compileErrors("Get[Q]").void - } - - test("Get can be manually derived for unary products") { - Get.derived[X].void - Get.derived[Q].void - } - - test("Get should not be derived for non-unary products") { - compileErrors("Get[Z]").void - compileErrors("Get[(Int, Int)]").void - compileErrors("Get[S.type]").void - } - } final case class Foo(s: String) @@ -64,8 +41,8 @@ class GetDBSuite extends munit.FunSuite { // Both of these will fail at runtime if called with a null value, we check that this is // avoided below. - implicit def FooMeta: Get[Foo] = Get[String].map(s => Foo(s.toUpperCase)) - implicit def barMeta: Get[Bar] = Get[Int].temap(n => if (n == 0) Left("cannot be 0") else Right(Bar(n))) + implicit val FooMeta: Get[Foo] = Get[String].map(s => Foo(s.toUpperCase)) + implicit val barMeta: Get[Bar] = Get[Int].temap(n => if (n == 0) Left("cannot be 0") else Right(Bar(n))) test("Get should not allow map to observe null on the read side (AnyRef)") { val x = sql"select null".query[Option[Foo]].unique.transact(xa).unsafeRunSync() @@ -82,21 +59,6 @@ class GetDBSuite extends munit.FunSuite { assertEquals(x, Left(doobie.util.invariant.NonNullableColumnRead(1, JdbcType.Char))) } - test("Get should not allow map to observe null on the read side (AnyVal)") { - val x = sql"select null".query[Option[Bar]].unique.transact(xa).unsafeRunSync() - assertEquals(x, None) - } - - test("Get should read non-null value (AnyVal)") { - val x = sql"select 1".query[Bar].unique.transact(xa).unsafeRunSync() - assertEquals(x, Bar(1)) - } - - test("Get should error when reading a NULL into an unlifted Scala type (AnyVal)") { - def x = sql"select null".query[Bar].unique.transact(xa).attempt.unsafeRunSync() - assertEquals(x, Left(doobie.util.invariant.NonNullableColumnRead(1, JdbcType.Integer))) - } - test("Get should error when reading an incorrect value") { def x = sql"select 0".query[Bar].unique.transact(xa).attempt.unsafeRunSync() assertEquals(x, Left(doobie.util.invariant.InvalidValue[Int, Bar](0, "cannot be 0"))) diff --git a/modules/core/src/test/scala/doobie/util/PutSuite.scala b/modules/core/src/test/scala/doobie/util/PutSuite.scala index a8548e211..fd2f4b0c4 100644 --- a/modules/core/src/test/scala/doobie/util/PutSuite.scala +++ b/modules/core/src/test/scala/doobie/util/PutSuite.scala @@ -8,7 +8,7 @@ import cats.effect.IO import doobie.testutils.VoidExtensions import doobie.util.transactor.Transactor -class PutSuite extends munit.FunSuite with PutSuitePlatform { +class PutSuite extends munit.FunSuite { case class X(x: Int) case class Q(x: String) @@ -34,20 +34,4 @@ class PutSuite extends munit.FunSuite with PutSuitePlatform { Put[String].void } - test("Put should be auto derived for unary products") { - import doobie.generic.auto.* - - Put[X].void - Put[Q].void - } - - test("Put is not auto derived without an import") { - compileErrors("Put[X]").void - compileErrors("Put[Q]").void - } - - test("Put can be manually derived for unary products") { - Put.derived[X].void - Put.derived[Q].void - } } diff --git a/modules/core/src/test/scala/doobie/util/ReadSuite.scala b/modules/core/src/test/scala/doobie/util/ReadSuite.scala index da8fd1b6e..1fc61055e 100644 --- a/modules/core/src/test/scala/doobie/util/ReadSuite.scala +++ b/modules/core/src/test/scala/doobie/util/ReadSuite.scala @@ -8,6 +8,10 @@ import cats.effect.IO import doobie.util.TestTypes.* import doobie.util.transactor.Transactor import doobie.testutils.VoidExtensions +import doobie.syntax.all.* +import doobie.Query +import munit.Location +import scala.annotation.nowarn class ReadSuite extends munit.FunSuite with ReadSuitePlatform { @@ -21,34 +25,19 @@ class ReadSuite extends munit.FunSuite with ReadSuitePlatform { logHandler = None ) - test("Read should exist for some fancy types") { - import doobie.generic.auto.* - - Read[Int].void - Read[(Int, Int)].void - Read[(Int, Int, String)].void - Read[(Int, (Int, String))].void - } - - test("Read is not auto derived for case classes without importing auto derive import") { - assert(compileErrors("Read[LenStr1]").contains("Cannot find or construct")) - } - - test("Read should not be derivable for case objects") { - assert(compileErrors("Read[CaseObj.type]").contains("Cannot find or construct")) - assert(compileErrors("Read[Option[CaseObj.type]]").contains("Cannot find or construct")) - } - - test("Read is auto derived for tuples without an import") { + test("Read is available for tuples without an import when all elements have a Write instance") { Read[(Int, Int)].void Read[(Int, Int, String)].void Read[(Int, (Int, String))].void Read[Option[(Int, Int)]].void Read[Option[(Int, Option[(String, Int)])]].void + + // But shouldn't automatically derive anything that doesn't already have a Read instance + assert(compileErrors("Read[(Int, TrivialCaseClass)]").contains("Cannot find or construct")) } - test("Read is still auto derived for tuples when import is present (no ambiguous implicits)") { + test("Read is still auto derived for tuples when import is present (no ambiguous implicits) ") { import doobie.generic.auto.* Read[(Int, Int)].void Read[(Int, Int, String)].void @@ -56,45 +45,86 @@ class ReadSuite extends munit.FunSuite with ReadSuitePlatform { Read[Option[(Int, Int)]].void Read[Option[(Int, Option[(String, Int)])]].void + + Read[(ComplexCaseClass, Int)].void + Read[(Int, ComplexCaseClass)].void } - test("Read can be manually derived") { - Read.derived[LenStr1] + test("Read is not auto derived for case classes without importing auto derive import") { + assert(compileErrors("Read[TrivialCaseClass]").contains("Cannot find or construct")) } - test("Read should exist for Unit") { - import doobie.generic.auto.* + 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")) - Read[Unit] - assertEquals(Read[(Int, Unit)].length, 1) - } + implicit val i1: Read[HasCustomReadWrite1] = Read.derived[HasCustomReadWrite1] + assertEquals(i1.length, 2) + insertTupleAndCheckRead(("x", "y"), HasCustomReadWrite1("x", CustomReadWrite("y_R"))) - test("Read should exist for option of some fancy types") { - import doobie.generic.auto.* + implicit val iOpt0: Read[HasOptCustomReadWrite0] = Read.derived[HasOptCustomReadWrite0] + assertEquals(iOpt0.length, 2) + insertTupleAndCheckRead(("x", "y"), HasOptCustomReadWrite0(Some(CustomReadWrite("x_R")), "y")) - Read[Option[Int]].void - Read[Option[(Int, Int)]].void - Read[Option[(Int, Int, String)]].void - Read[Option[(Int, (Int, String))]].void - Read[Option[(Int, Option[(Int, String)])]].void - Read[ComplexCaseClass].void + implicit val iOpt1: Read[HasOptCustomReadWrite1] = Read.derived[HasOptCustomReadWrite1] + assertEquals(iOpt1.length, 2) + insertTupleAndCheckRead(("x", "y"), HasOptCustomReadWrite1("x", Some(CustomReadWrite("y_R")))) } - test("Read should exist for option of Unit") { - import doobie.generic.auto.* + 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")) + + implicit val i1: Read[HasCustomGetPut1] = Read.derived[HasCustomGetPut1] + assertEquals(i1.length, 2) + insertTupleAndCheckRead(("x", "y"), HasCustomGetPut1("x", CustomGetPut("y_G"))) - Read[Option[Unit]].void - assertEquals(Read[Option[(Int, Unit)]].length, 1).void + implicit val iOpt0: Read[HasOptCustomGetPut0] = Read.derived[HasOptCustomGetPut0] + assertEquals(iOpt0.length, 2) + insertTupleAndCheckRead(("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")))) } - test("Read should select multi-column instance by default") { - import doobie.generic.auto.* + 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")))) + } - assertEquals(Read[LenStr1].length, 2).void + 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")))) } - test("Read should select 1-column instance when available") { - assertEquals(Read[LenStr2].length, 1).void + test("Read should not be derivable for case objects") { + val expectedDeriveError = + if (util.Properties.versionString.startsWith("version 2.12")) + "could not find implicit" + else + "Cannot derive" + assert(compileErrors("Read.derived[CaseObj.type]").contains(expectedDeriveError)) + assert(compileErrors("Read.derived[Option[CaseObj.type]]").contains(expectedDeriveError)) + + import doobie.implicits.* + assert(compileErrors("Read[CaseObj.type]").contains("not find or construct")) + assert(compileErrors("Read[Option[CaseObj.type]]").contains("not find or construct")) + }: @nowarn("msg=.*(u|U)nused import.*") + + test("Read should exist for Unit/Option[Unit]") { + assertEquals(Read[Unit].length, 0) + assertEquals(Read[Option[Unit]].length, 0) + assertEquals(Read[(Int, Unit)].length, 1) } test(".product should product the correct ordering of gets") { @@ -118,10 +148,7 @@ class ReadSuite extends munit.FunSuite with ReadSuitePlatform { val frag = sql"SELECT 1, NULL, 3, NULL" val q1 = frag.query[Option[(Int, Option[Int], Int, Option[Int])]].to[List] val o1 = q1.transact(xa).unsafeRunSync() - // This result doesn't seem ideal, because we should know that Int isn't - // nullable, so the correct result is Some((1, None, 3, None)) - // But with how things are wired at the moment this isn't possible - assertEquals(o1, List(None)) + assertEquals(o1, List(Some((1, None, 3, None)))) val q2 = frag.query[Option[(Int, Int, Int, Int)]].to[List] val o2 = q2.transact(xa).unsafeRunSync() @@ -168,4 +195,10 @@ 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 = { + val res = Query[Tup, A]("SELECT ?, ?").unique(in).transact(xa) + .unsafeRunSync() + assertEquals(res, expectedOut) + } + } diff --git a/modules/core/src/test/scala/doobie/util/TestTypes.scala b/modules/core/src/test/scala/doobie/util/TestTypes.scala index 8607103f4..c2a1ca397 100644 --- a/modules/core/src/test/scala/doobie/util/TestTypes.scala +++ b/modules/core/src/test/scala/doobie/util/TestTypes.scala @@ -4,20 +4,35 @@ package doobie.util -import doobie.util.meta.Meta - object TestTypes { - case class LenStr1(n: Int, s: String) - - case class LenStr2(n: Int, s: String) - object LenStr2 { - implicit val LenStrMeta: Meta[LenStr2] = - Meta[String].timap(s => LenStr2(s.length, s))(_.s) - } - case object CaseObj + 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 HasCustomReadWrite0(c: CustomReadWrite, s: String) + case class HasCustomReadWrite1(s: String, c: CustomReadWrite) + case class HasOptCustomReadWrite0(c: Option[CustomReadWrite], s: String) + case class HasOptCustomReadWrite1(s: String, c: Option[CustomReadWrite]) + + case class CustomReadWrite(s: String) + + object CustomReadWrite { + implicit val write: Write[CustomReadWrite] = Write.fromPut[String].contramap(a => a.s.concat("_W")) + implicit val read: Read[CustomReadWrite] = Read.fromGet[String].map(str => CustomReadWrite(str.concat("_R"))) + } + + case class HasCustomGetPut0(c: CustomGetPut, s: String) + case class HasCustomGetPut1(s: String, c: CustomGetPut) + case class HasOptCustomGetPut0(c: Option[CustomGetPut], s: String) + case class HasOptCustomGetPut1(s: String, c: Option[CustomGetPut]) + + case class CustomGetPut(s: String) + + object CustomGetPut { + implicit val put: Put[CustomGetPut] = Put[String].contramap(a => a.s.concat("_P")) + implicit val get: Get[CustomGetPut] = Get[String].tmap(a => CustomGetPut(a.concat("_G"))) + } + } diff --git a/modules/core/src/test/scala/doobie/util/WriteSuite.scala b/modules/core/src/test/scala/doobie/util/WriteSuite.scala index f90cdf885..57cc83c67 100644 --- a/modules/core/src/test/scala/doobie/util/WriteSuite.scala +++ b/modules/core/src/test/scala/doobie/util/WriteSuite.scala @@ -4,12 +4,14 @@ package doobie.util -import doobie.Transactor -import doobie.Update +import doobie.{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 munit.Location +import scala.annotation.nowarn class WriteSuite extends munit.FunSuite with WriteSuitePlatform { @@ -21,83 +23,109 @@ class WriteSuite extends munit.FunSuite with WriteSuitePlatform { logHandler = None ) - test("Write should exist for some fancy types") { - import doobie.generic.auto.* - - Write[Int].void - Write[(Int, Int)].void - Write[(Int, Int, String)].void - Write[(Int, (Int, String))].void - Write[ComplexCaseClass].void - } - - test("Write is auto derived for tuples without an import") { + test("Write is available for tuples without an import when all elements have a Write instance") { Write[(Int, Int)].void Write[(Int, Int, String)].void Write[(Int, (Int, String))].void Write[Option[(Int, Int)]].void Write[Option[(Int, Option[(String, Int)])]].void + + // But shouldn't automatically derive anything that doesn't already have a Read instance + assert(compileErrors("Write[(Int, TrivialCaseClass)]").contains("Cannot find or construct")) } test("Write is still auto derived for tuples when import is present (no ambiguous implicits) ") { - import doobie.generic.auto.* + import doobie.implicits.* Write[(Int, Int)].void Write[(Int, Int, String)].void Write[(Int, (Int, String))].void Write[Option[(Int, Int)]].void Write[Option[(Int, Option[(String, Int)])]].void + + Write[(ComplexCaseClass, Int)].void + Write[(Int, ComplexCaseClass)].void } test("Write is not auto derived for case classes") { - assert(compileErrors("Write[LenStr1]").contains("Cannot find or construct")) + assert(compileErrors("Write[TrivialCaseClass]").contains("Cannot find or construct")) } - test("Write should not be derivable for case objects") { - assert(compileErrors("Write[CaseObj.type]").contains("Cannot find or construct")) - assert(compileErrors("Write[Option[CaseObj.type]]").contains("Cannot find or construct")) - } + 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")) - test("Write can be manually derived") { - Write.derived[LenStr1].void - } + implicit val i1: Write[HasCustomReadWrite1] = Write.derived[HasCustomReadWrite1] + assertEquals(i1.length, 2) + writeAndCheckTuple(HasCustomReadWrite1("x", CustomReadWrite("y")), ("x", "y_W")) - test("Write should exist for Unit") { - import doobie.generic.auto.* + implicit val iOpt0: Write[HasOptCustomReadWrite0] = Write.derived[HasOptCustomReadWrite0] + assertEquals(iOpt0.length, 2) + writeAndCheckTuple(HasOptCustomReadWrite0(Some(CustomReadWrite("x")), "y"), ("x_W", "y")) - Write[Unit].void - assertEquals(Write[(Int, Unit)].length, 1) + implicit val iOpt1: Write[HasOptCustomReadWrite1] = Write.derived[HasOptCustomReadWrite1] + assertEquals(iOpt1.length, 2) + writeAndCheckTuple(HasOptCustomReadWrite1("x", Some(CustomReadWrite("y"))), ("x", "y_W")) } - test("Write should exist for option of some fancy types") { - import doobie.generic.auto.* + 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")) - Write[Option[Int]].void - Write[Option[(Int, Int)]].void - Write[Option[(Int, Int, String)]].void - Write[Option[(Int, (Int, String))]].void - Write[Option[(Int, Option[(Int, String)])]].void - } + implicit val i1: Write[HasCustomGetPut1] = Write.derived[HasCustomGetPut1] + assertEquals(i1.length, 2) + writeAndCheckTuple(HasCustomGetPut1("x", CustomGetPut("y")), ("x", "y_P")) - test("Write should exist for option of Unit") { - import doobie.generic.auto.* + implicit val iOpt0: Write[HasOptCustomGetPut0] = Write.derived[HasOptCustomGetPut0] + assertEquals(iOpt0.length, 2) + writeAndCheckTuple(HasOptCustomGetPut0(Some(CustomGetPut("x")), "y"), ("x_P", "y")) - Write[Option[Unit]].void - assertEquals(Write[Option[(Int, Unit)]].length, 1) + implicit val iOpt1: Write[HasOptCustomGetPut1] = Write.derived[HasOptCustomGetPut1] + assertEquals(iOpt1.length, 2) + writeAndCheckTuple(HasOptCustomGetPut1("x", Some(CustomGetPut("y"))), ("x", "y_P")) } - test("Write should select multi-column instance by default") { - import doobie.generic.auto.* + test("Automatic derivation selects custom Write instances when available") { + import doobie.implicits.* - assertEquals(Write[LenStr1].length, 2) + 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")) } - test("Write should select 1-column instance when available") { - assertEquals(Write[LenStr2].length, 1) + 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")) + } + + test("Write should not be derivable for case objects") { + val expectedDeriveError = + if (util.Properties.versionString.startsWith("version 2.12")) + "could not find implicit" + else + "Cannot derive" + assert(compileErrors("Write.derived[CaseObj.type]").contains(expectedDeriveError)) + assert(compileErrors("Write.derived[Option[CaseObj.type]]").contains(expectedDeriveError)) + + import doobie.implicits.* + assert(compileErrors("Write[Option[CaseObj.type]]").contains("not find or construct")) + assert(compileErrors("Write[CaseObj.type]").contains("not find or construct")) + }: @nowarn("msg=.*(u|U)nused import.*") + + test("Write should exist for Unit/Option[Unit]") { + assertEquals(Write[Unit].length, 0) + assertEquals(Write[Option[Unit]].length, 0) + assertEquals(Write[(Int, Unit)].length, 1) } - test("Write should correct set parameters for Option instances ") { + test("Write should correctly set parameters for Option instances ") { import doobie.implicits.* (for { _ <- sql"create temp table t1 (a int, b int)".update.run @@ -122,4 +150,12 @@ 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 {} diff --git a/modules/munit/src/test/scala/doobie/munit/CheckerTests.scala b/modules/munit/src/test/scala/doobie/munit/CheckerTests.scala index 296211fac..c31cf40b0 100644 --- a/modules/munit/src/test/scala/doobie/munit/CheckerTests.scala +++ b/modules/munit/src/test/scala/doobie/munit/CheckerTests.scala @@ -47,8 +47,6 @@ trait CheckerChecks[M[_]] extends FunSuite with Checker[M] { } test("Read should select correct columns for checking when combined with `ap`") { - import doobie.generic.auto.* - val readInt = Read[(Int, Int)] val readIntToInt: Read[Tuple2[Int, Int] => String] = Read[(String, String)].map(i => k => s"$i,$k") diff --git a/modules/weaver/src/test/scala/doobie/weaver/CheckerTests.scala b/modules/weaver/src/test/scala/doobie/weaver/CheckerTests.scala index 97a703c4d..43989044a 100644 --- a/modules/weaver/src/test/scala/doobie/weaver/CheckerTests.scala +++ b/modules/weaver/src/test/scala/doobie/weaver/CheckerTests.scala @@ -54,8 +54,6 @@ object CheckerTests extends IOSuite with IOChecker { } test("Read should select correct columns for checking when combined with `ap`") { implicit transactor => - import doobie.generic.auto.* - val readInt = Read[(Int, Int)] val readIntToInt: Read[Tuple2[Int, Int] => String] = Read[(String, String)].map(i => k => s"$i,$k")