Skip to content

Commit 403c69b

Browse files
committed
Allow constructing Read from a list of underlying Read instances
1 parent 3729572 commit 403c69b

File tree

2 files changed

+77
-1
lines changed

2 files changed

+77
-1
lines changed

modules/core/src/main/scala/doobie/util/read.scala

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
package doobie.util
66

7+
import cats.syntax.all.*
78
import cats.Applicative
89
import doobie.ResultSetIO
910
import doobie.enumerated.Nullability
@@ -12,6 +13,7 @@ import doobie.free.resultset as IFRS
1213

1314
import java.sql.ResultSet
1415
import scala.annotation.implicitNotFound
16+
import scala.collection.immutable.ArraySeq
1517

1618
@implicitNotFound("""
1719
Cannot find or construct a Read instance for type:
@@ -119,7 +121,7 @@ object Read extends LowerPriority1Read {
119121
override lazy val length: Int = underlyingRead.length
120122
}
121123

122-
/** A Read instance consists of multiple underlying Read instances */
124+
/** A Read instance consists of two underlying Read instances */
123125
class Composite[A, S0, S1](read0: Read[S0], read1: Read[S1], f: (S0, S1) => A) extends Read[A] {
124126
override def unsafeGet(rs: ResultSet, startIdx: Int): A = {
125127
val r0 = read0.unsafeGet(rs, startIdx)
@@ -145,6 +147,30 @@ object Read extends LowerPriority1Read {
145147
override lazy val length: Int = read0.length + read1.length
146148
}
147149

150+
/** A Composite made up of a list of underlying Read instances. This class but is intended to provide a simpler
151+
* interface for other libraries that uses Doobie. This isn't used by Doobie itself for its derived instances.
152+
*
153+
* For large number of columns, this class may be more performant than chain of Read.Composite.
154+
*/
155+
class CompositeOfInstances(readInstances: ArraySeq[Read[Any]]) extends Read[ArraySeq[Any]] {
156+
override def unsafeGet(rs: ResultSet, startIdx: Int): ArraySeq[Any] = {
157+
var columnIdx = startIdx
158+
readInstances.map { r =>
159+
val res = r.unsafeGet(rs, columnIdx)
160+
columnIdx += r.length // This Read instance "consumed" x number of columns
161+
res
162+
}
163+
}
164+
165+
override def gets: List[(Get[?], NullabilityKnown)] = readInstances.flatMap(_.gets).toList
166+
167+
override def toOpt: Read[Option[ArraySeq[Any]]] = {
168+
readInstances.traverse(_.toOpt).map(arrayOfOptions => arrayOfOptions.sequence)
169+
}
170+
171+
override def length: Int = readInstances.map(_.length).sum
172+
}
173+
148174
}
149175

150176
trait LowerPriority1Read extends LowerPriority2Read {

modules/core/src/test/scala/doobie/util/ReadSuite.scala

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ import doobie.util.TestTypes.*
99
import doobie.util.transactor.Transactor
1010
import doobie.testutils.VoidExtensions
1111
import doobie.syntax.all.*
12+
import doobie.util.Read.CompositeOfInstances
1213
import doobie.{ConnectionIO, Query}
1314
import doobie.util.analysis.{Analysis, ColumnMisalignment, ColumnTypeError, ColumnTypeWarning, NullabilityMisalignment}
1415
import doobie.util.fragment.Fragment
1516
import munit.Location
1617

1718
import scala.annotation.nowarn
19+
import scala.collection.immutable.ArraySeq
1820

1921
class ReadSuite extends munit.CatsEffectSuite with ReadSuitePlatform {
2022

@@ -201,6 +203,54 @@ class ReadSuite extends munit.CatsEffectSuite with ReadSuitePlatform {
201203
q2.transact(xa).assertEquals(List(Some((1, 2, 3, 4))))
202204
}
203205

206+
test("Read.CompositeOfInstances reads columns correctly") {
207+
import cats.syntax.functor.*
208+
val rscc: Read[SimpleCaseClass] = Read.derived[SimpleCaseClass]
209+
210+
implicit val read: Read[ComplexCaseClass] =
211+
new Read.CompositeOfInstances(ArraySeq(
212+
rscc.widen[Any],
213+
rscc.toOpt.widen[Any],
214+
Read[Option[Int]].widen[Any],
215+
Read[String].widen[Any]))
216+
.map { arr =>
217+
ComplexCaseClass(
218+
arr(0).asInstanceOf[SimpleCaseClass],
219+
arr(1).asInstanceOf[Option[SimpleCaseClass]],
220+
arr(2).asInstanceOf[Option[Int]],
221+
arr(3).asInstanceOf[String])
222+
}
223+
224+
assertEquals(read.length, 8)
225+
226+
sql"SELECT 1, '2', '3', 4, '5', '6', 7, '8'"
227+
.query[ComplexCaseClass].unique.transact(xa)
228+
.assertEquals(
229+
ComplexCaseClass(
230+
SimpleCaseClass(Some(1), "2", Some("3")),
231+
Some(SimpleCaseClass(Some(4), "5", Some("6"))),
232+
Some(7),
233+
"8"
234+
)
235+
)
236+
237+
// The 's' field in Option[SimpleCaseClass] is NULL, so whole case class value is None
238+
sql"SELECT NULL, '2', '3', 4, NULL, '6', 7, '8'"
239+
.query[ComplexCaseClass].unique.transact(xa)
240+
.assertEquals(
241+
ComplexCaseClass(
242+
SimpleCaseClass(None, "2", Some("3")),
243+
None,
244+
Some(7),
245+
"8"
246+
)
247+
)
248+
249+
sql"SELECT 1, NULL, '3', 4, '5', '6', 7, '8'"
250+
.query[Option[ComplexCaseClass]].unique.transact(xa)
251+
.assertEquals(None)
252+
}
253+
204254
test("Read should select correct columns when combined with `ap`") {
205255
import cats.syntax.all.*
206256
import doobie.implicits.*

0 commit comments

Comments
 (0)