Skip to content

Commit 25dfdd3

Browse files
committed
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
1 parent cc635ff commit 25dfdd3

36 files changed

+760
-1128
lines changed

build.sbt

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ lazy val weaverVersion = "0.8.4"
3333
ThisBuild / tlBaseVersion := "1.0"
3434
ThisBuild / tlCiReleaseBranches := Seq("main") // publish snapshots on `main`
3535
ThisBuild / tlCiScalafmtCheck := true
36+
//ThisBuild / scalaVersion := scala212Version
3637
ThisBuild / scalaVersion := scala213Version
3738
//ThisBuild / scalaVersion := scala3Version
3839
ThisBuild / crossScalaVersions := Seq(scala212Version, scala213Version, scala3Version)
@@ -98,9 +99,12 @@ lazy val compilerFlags = Seq(
9899
Compile / doc / scalacOptions --= Seq(
99100
"-Xfatal-warnings"
100101
),
101-
// Test / scalacOptions --= Seq(
102-
// "-Xfatal-warnings"
103-
// ),
102+
// Disable warning when @nowarn annotation isn't suppressing a warning
103+
// to simplify cross-building
104+
// because 2.12 @nowarn doesn't actually do anything.. https://github.com/scala/bug/issues/12313
105+
scalacOptions ++= Seq(
106+
"-Wconf:cat=unused-nowarn:s"
107+
),
104108
scalacOptions ++= (if (tlIsScala3.value)
105109
// Handle irrefutable patterns in for comprehensions
106110
Seq("-source:future", "-language:adhocExtensions")
@@ -249,8 +253,7 @@ lazy val core = project
249253
).filterNot(_ => tlIsScala3.value) ++ Seq(
250254
"org.tpolecat" %% "typename" % "1.1.0",
251255
"com.h2database" % "h2" % h2Version % "test",
252-
"org.postgresql" % "postgresql" % postgresVersion % "test",
253-
"org.mockito" % "mockito-core" % "5.12.0" % Test
256+
"org.postgresql" % "postgresql" % postgresVersion % "test"
254257
),
255258
Compile / unmanagedSourceDirectories += {
256259
val sourceDir = (Compile / sourceDirectory).value

modules/core/src/main/scala-2/doobie/util/GetPlatform.scala

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@ trait GetPlatform {
1111
import doobie.util.compat.=:=
1212

1313
/** @group Instances */
14-
@deprecated("Use Get.derived instead to derive instances explicitly", "1.0.0-RC6")
1514
def unaryProductGet[A, L <: HList, H, T <: HList](
1615
implicit
1716
G: Generic.Aux[A, L],
1817
C: IsHCons.Aux[L, H, T],
1918
H: Lazy[Get[H]],
2019
E: (H :: HNil) =:= L
21-
): MkGet[A] = MkGet.unaryProductGet
20+
): Get[A] = {
21+
void(C) // C drives inference but is not used directly
22+
H.value.tmap[A](h => G.from(h :: HNil))
23+
}
2224

2325
}

modules/core/src/main/scala-2/doobie/util/MkGetPlatform.scala

Lines changed: 0 additions & 26 deletions
This file was deleted.

modules/core/src/main/scala-2/doobie/util/MkPutPlatform.scala

Lines changed: 0 additions & 26 deletions
This file was deleted.

modules/core/src/main/scala-2/doobie/util/MkReadPlatform.scala

Lines changed: 58 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -4,142 +4,92 @@
44

55
package doobie.util
66

7-
import shapeless.{HList, HNil, ::, Generic, Lazy, <:!<, OrElse}
8-
import shapeless.labelled.{field, FieldType}
7+
import shapeless.{HList, HNil, ::, Generic, Lazy, OrElse}
8+
import shapeless.labelled.FieldType
99

10-
trait MkReadPlatform extends LowerPriorityRead {
10+
trait MkReadPlatform extends LowerPriorityMkRead {
1111

1212
// Derivation base case for product types (1-element)
1313
implicit def productBase[H](
14-
implicit H: Read[H] OrElse MkRead[H]
15-
): MkRead[H :: HNil] = {
16-
val head = H.unify
17-
18-
new MkRead[H :: HNil](
19-
head.gets,
20-
(rs, n) => head.unsafeGet(rs, n) :: HNil
14+
implicit H: Read[H] OrElse Derived[MkRead[H]]
15+
): Derived[MkRead[H :: HNil]] = {
16+
val headInstance = H.fold(identity, _.instance)
17+
18+
new Derived(
19+
new MkRead(
20+
new Read.Composite(
21+
List(headInstance),
22+
_.head.asInstanceOf[H] :: HNil
23+
)
24+
)
2125
)
2226
}
2327

2428
// Derivation base case for shapeless record (1-element)
2529
implicit def recordBase[K <: Symbol, H](
26-
implicit H: Read[H] OrElse MkRead[H]
27-
): MkRead[FieldType[K, H] :: HNil] = {
28-
val head = H.unify
29-
30-
new MkRead[FieldType[K, H] :: HNil](
31-
head.gets,
32-
(rs, n) => field[K](head.unsafeGet(rs, n)) :: HNil
30+
implicit H: Read[H] OrElse Derived[MkRead[H]]
31+
): Derived[MkRead[FieldType[K, H] :: HNil]] = {
32+
val headInstance = H.fold(identity, _.instance)
33+
34+
new Derived(
35+
new MkRead(
36+
new Read.Composite(
37+
List(headInstance),
38+
_.head.asInstanceOf[FieldType[K, H]] :: HNil
39+
)
40+
)
3341
)
3442
}
3543
}
3644

37-
trait LowerPriorityRead extends EvenLowerPriorityRead {
45+
trait LowerPriorityMkRead {
3846

3947
// Derivation inductive case for product types
4048
implicit def product[H, T <: HList](
4149
implicit
42-
H: Read[H] OrElse MkRead[H],
43-
T: MkRead[T]
44-
): MkRead[H :: T] = {
45-
val head = H.unify
46-
47-
new MkRead[H :: T](
48-
head.gets ++ T.gets,
49-
(rs, n) => head.unsafeGet(rs, n) :: T.unsafeGet(rs, n + head.length)
50+
H: Read[H] OrElse Derived[MkRead[H]],
51+
T: Read[T] OrElse Derived[MkRead[T]]
52+
): Derived[MkRead[H :: T]] = {
53+
val headInstance = H.fold(identity, _.instance)
54+
val tailInstance = T.fold(identity, _.instance)
55+
56+
new Derived(
57+
new MkRead(
58+
new Read.Composite(
59+
List(headInstance, tailInstance),
60+
list => list(0).asInstanceOf[H] :: list(1).asInstanceOf[T]
61+
)
62+
)
5063
)
5164
}
5265

5366
// Derivation inductive case for shapeless records
5467
implicit def record[K <: Symbol, H, T <: HList](
5568
implicit
56-
H: Read[H] OrElse MkRead[H],
57-
T: MkRead[T]
58-
): MkRead[FieldType[K, H] :: T] = {
59-
val head = H.unify
60-
61-
new MkRead[FieldType[K, H] :: T](
62-
head.gets ++ T.gets,
63-
(rs, n) => field[K](head.unsafeGet(rs, n)) :: T.unsafeGet(rs, n + head.length)
69+
H: Read[H] OrElse Derived[MkRead[H]],
70+
T: Read[T] OrElse Derived[MkRead[T]]
71+
): Derived[MkRead[FieldType[K, H] :: T]] = {
72+
val headInstance = H.fold(identity, _.instance)
73+
val tailInstance = T.fold(identity, _.instance)
74+
75+
new Derived(
76+
new MkRead(
77+
new Read.Composite(
78+
List(headInstance, tailInstance),
79+
list => list(0).asInstanceOf[FieldType[K, H]] :: list(1).asInstanceOf[T]
80+
)
81+
)
6482
)
6583
}
6684

6785
// Derivation for product types (i.e. case class)
68-
implicit def generic[T, Repr](implicit gen: Generic.Aux[T, Repr], G: Lazy[MkRead[Repr]]): MkRead[T] =
69-
new MkRead[T](G.value.gets, (rs, n) => gen.from(G.value.unsafeGet(rs, n)))
70-
71-
// Derivation base case for Option of product types (1-element)
72-
implicit def optProductBase[H](
73-
implicit
74-
H: Read[Option[H]] OrElse MkRead[Option[H]],
75-
N: H <:!< Option[α] forSome { type α }
76-
): MkRead[Option[H :: HNil]] = {
77-
void(N)
78-
val head = H.unify
79-
80-
new MkRead[Option[H :: HNil]](
81-
head.gets,
82-
(rs, n) =>
83-
head.unsafeGet(rs, n).map(_ :: HNil)
84-
)
85-
}
86-
87-
// Derivation base case for Option of product types (where the head element is Option)
88-
implicit def optProductOptBase[H](
89-
implicit H: Read[Option[H]] OrElse MkRead[Option[H]]
90-
): MkRead[Option[Option[H] :: HNil]] = {
91-
val head = H.unify
92-
93-
new MkRead[Option[Option[H] :: HNil]](
94-
head.gets,
95-
(rs, n) => head.unsafeGet(rs, n).map(h => Some(h) :: HNil)
96-
)
97-
}
98-
99-
}
100-
101-
trait EvenLowerPriorityRead {
102-
103-
// Read[Option[H]], Read[Option[T]] implies Read[Option[H *: T]]
104-
implicit def optProduct[H, T <: HList](
86+
implicit def genericRead[T, Repr](
10587
implicit
106-
H: Read[Option[H]] OrElse MkRead[Option[H]],
107-
T: MkRead[Option[T]],
108-
N: H <:!< Option[α] forSome { type α }
109-
): MkRead[Option[H :: T]] = {
110-
void(N)
111-
val head = H.unify
112-
113-
new MkRead[Option[H :: T]](
114-
head.gets ++ T.gets,
115-
(rs, n) =>
116-
for {
117-
h <- head.unsafeGet(rs, n)
118-
t <- T.unsafeGet(rs, n + head.length)
119-
} yield h :: t
120-
)
88+
gen: Generic.Aux[T, Repr],
89+
hlistRead: Lazy[Read[Repr] OrElse Derived[MkRead[Repr]]]
90+
): Derived[MkRead[T]] = {
91+
val hlistInstance: Read[Repr] = hlistRead.value.fold(identity, _.instance)
92+
new Derived(new MkRead(hlistInstance.map(gen.from)))
12193
}
12294

123-
// Read[Option[H]], Read[Option[T]] implies Read[Option[Option[H] *: T]]
124-
implicit def optProductOpt[H, T <: HList](
125-
implicit
126-
H: Read[Option[H]] OrElse MkRead[Option[H]],
127-
T: MkRead[Option[T]]
128-
): MkRead[Option[Option[H] :: T]] = {
129-
val head = H.unify
130-
131-
new MkRead[Option[Option[H] :: T]](
132-
head.gets ++ T.gets,
133-
(rs, n) => T.unsafeGet(rs, n + head.length).map(head.unsafeGet(rs, n) :: _)
134-
)
135-
}
136-
137-
// Derivation for optional of product types (i.e. case class)
138-
implicit def ogeneric[A, Repr <: HList](
139-
implicit
140-
G: Generic.Aux[A, Repr],
141-
B: Lazy[MkRead[Option[Repr]]]
142-
): MkRead[Option[A]] =
143-
new MkRead[Option[A]](B.value.gets, B.value.unsafeGet(_, _).map(G.from))
144-
14595
}

0 commit comments

Comments
 (0)