Permalink
Browse files

Add a macro-based operator for mapping projections to case classes

The new `mapTo` operator requires less boilerplate than a traditional
case class mapping defined with `<>` and it can support case classes of
more than 22 elements by using an HList instead of a tuple on the
left-hand side. Mappings of up to 22 elements may use either one.
  • Loading branch information...
szeiger committed Oct 2, 2015
1 parent 4f22634 commit d980dbb26ee2c35c02bf4543d92f527665f9daab
@@ -147,7 +147,7 @@ class AggregateTest extends AsyncTest[RelationalTestDB] {
class T4(tag: Tag) extends Table[Pair](tag, "t4") {
def a = column[Int]("a")
def b = column[Option[Int]]("b")
def * = (a, b) <> (Pair.tupled,Pair.unapply)
def * = (a, b).mapTo[Pair]
}
val t4s = TableQuery[T4]
db.run(t4s.schema.create >>
@@ -198,7 +198,7 @@ class AggregateTest extends AsyncTest[RelationalTestDB] {
def col4 = column[Int]("COL4")
def col5 = column[Int]("COL5")
def * = (col1, col2, col3, col4, col5) <> (Tab.tupled, Tab.unapply)
def * = (col1, col2, col3, col4, col5).mapTo[Tab]
}
val Tabs = TableQuery[Tabs]
@@ -60,7 +60,7 @@ class JdbcMapperTest extends AsyncTest[JdbcTestDB] {
class T(tag: Tag) extends Table[Data](tag, "T") {
def a = column[Int]("A")
def b = column[Int]("B")
def * = (a, b) <> (Data.tupled, Data.unapply _)
def * = (a, b).mapTo[Data]
}
val ts = TableQuery[T]
@@ -77,8 +77,16 @@ class JdbcMapperTest extends AsyncTest[JdbcTestDB] {
}
def testWideMappedEntity = {
import slick.collection.heterogeneous._
import slick.collection.heterogeneous.syntax._
case class Part(i1: Int, i2: Int, i3: Int, i4: Int, i5: Int, i6: Int)
case class Whole(id: Int, p1: Part, p2: Part, p3: Part, p4: Part)
case class BigCase(id: Int,
p1i1: Int, p1i2: Int, p1i3: Int, p1i4: Int, p1i5: Int, p1i6: Int,
p2i1: Int, p2i2: Int, p2i3: Int, p2i4: Int, p2i5: Int, p2i6: Int,
p3i1: Int, p3i2: Int, p3i3: Int, p3i4: Int, p3i5: Int, p3i6: Int,
p4i1: Int, p4i2: Int, p4i3: Int, p4i4: Int, p4i5: Int, p4i6: Int)
class T(tag: Tag) extends Table[Whole](tag, "t_wide") {
def id = column[Int]("id", O.PrimaryKey)
@@ -106,19 +114,37 @@ class JdbcMapperTest extends AsyncTest[JdbcTestDB] {
def p4i4 = column[Int]("p4i4")
def p4i5 = column[Int]("p4i5")
def p4i6 = column[Int]("p4i6")
def * = (
// Composable bidirectional mappings
def m1 = (
id,
(p1i1, p1i2, p1i3, p1i4, p1i5, p1i6).mapTo[Part],
(p2i1, p2i2, p2i3, p2i4, p2i5, p2i6) <> (Part.tupled, Part.unapply _),
(p3i1, p3i2, p3i3, p3i4, p3i5, p3i6).mapTo[Part],
(p4i1, p4i2, p4i3, p4i4, p4i5, p4i6).mapTo[Part]
).mapTo[Whole]
// Manually composed mapping functions
def m2 = (
id,
(p1i1, p1i2, p1i3, p1i4, p1i5, p1i6),
(p2i1, p2i2, p2i3, p2i4, p2i5, p2i6),
(p3i1, p3i2, p3i3, p3i4, p3i5, p3i6),
(p4i1, p4i2, p4i3, p4i4, p4i5, p4i6)
).shaped <> ({ case (id, p1, p2, p3, p4) =>
).shaped <> ({ case (id, p1, p2, p3, p4) =>
// We could do this without .shaped but then we'd have to write a type annotation for the parameters
Whole(id, Part.tupled.apply(p1), Part.tupled.apply(p2), Part.tupled.apply(p3), Part.tupled.apply(p4))
}, { w: Whole =>
def f(p: Part) = Part.unapply(p).get
Some((w.id, f(w.p1), f(w.p2), f(w.p3), f(w.p4)))
})
// HList-based wide case class mapping
def m3 = (
id ::
p1i1 :: p1i2 :: p1i3 :: p1i4 :: p1i5 :: p1i6 ::
p2i1 :: p2i2 :: p2i3 :: p2i4 :: p2i5 :: p2i6 ::
p3i1 :: p3i2 :: p3i3 :: p3i4 :: p3i5 :: p3i6 ::
p4i1 :: p4i2 :: p4i3 :: p4i4 :: p4i5 :: p4i6 :: HNil
).mapTo[BigCase]
def * = m1
}
val ts = TableQuery[T]
@@ -132,7 +158,9 @@ class JdbcMapperTest extends AsyncTest[JdbcTestDB] {
seq(
ts.schema.create,
ts += oData,
ts.result.head.map(_ shouldBe oData)
ts.result.head.map(_ shouldBe oData),
ts.map(_.m2).result.head.map(_ shouldBe oData),
ts.map(_.m3).result.head.map(_ shouldBe BigCase(0, 11, 12, 13, 14, 15, 16, 21, 22, 23, 24, 25, 26, 31, 32, 33, 34, 35, 36, 41, 42, 43, 44, 45, 46))
)
}
@@ -147,7 +175,7 @@ class JdbcMapperTest extends AsyncTest[JdbcTestDB] {
def p3 = column[String]("p3")
def p4 = column[Int]("p4")
def part1 = (p1,p2) <> (Part1.tupled,Part1.unapply)
def part2 = (p3,p4) <> (Part2.tupled,Part2.unapply)
def part2 = (p3,p4).mapTo[Part2]
def * = (part1, part2) <> (Whole.tupled,Whole.unapply)
}
val T = TableQuery[T]
@@ -169,14 +197,14 @@ class JdbcMapperTest extends AsyncTest[JdbcTestDB] {
class ARow(tag: Tag) extends Table[A](tag, "t4_a") {
def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
def data = column[Int]("data")
def * = (id, data) <> (A.tupled, A.unapply _)
def * = (id, data).mapTo[A]
}
val as = TableQuery[ARow]
class BRow(tag: Tag) extends Table[B](tag, "t5_b") {
def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
def data = column[String]("data")
def * = (id, Rep.Some(data)) <> (B.tupled, B.unapply _)
def * = (id, Rep.Some(data)).mapTo[B]
}
val bs = TableQuery[BRow]
@@ -297,11 +325,14 @@ class JdbcMapperTest extends AsyncTest[JdbcTestDB] {
import slick.collection.heterogeneous._
import slick.collection.heterogeneous.syntax._
case class Data(id: Int, b: Boolean, s: String)
class B(tag: Tag) extends Table[Int :: Boolean :: String :: HNil](tag, "hlist_b") {
def id = column[Int]("id", O.PrimaryKey)
def b = column[Boolean]("b")
def s = column[String]("s")
def * = id :: b :: s :: HNil
def mapped = *.mapTo[Data]
}
val bs = TableQuery[B]
@@ -320,9 +351,10 @@ class JdbcMapperTest extends AsyncTest[JdbcTestDB] {
bs.schema.create,
bs += (1 :: true :: "a" :: HNil),
bs += (2 :: false :: "c" :: HNil),
bs += (3 :: false :: "b" :: HNil),
bs.map(_.mapped) += Data(3, false, "b"),
q1.result.map(_ shouldBe Vector(3 :: "bb" :: (42 :: HNil) :: HNil, 2 :: "cc" :: (42 :: HNil) :: HNil)),
q1.result.map(_ shouldBe Vector(3 :: "bb" :: (42 :: HNil) :: HNil, 2 :: "cc" :: (42 :: HNil) :: HNil))
q2.result.map(_ shouldBe Vector(3 :: "bb" :: (42 :: HNil) :: HNil, 2 :: "cc" :: (42 :: HNil) :: HNil)),
bs.map(_.mapped).result.map(_.toSet shouldBe Set(Data(1, true, "a"), Data(2, false, "c"), Data(3, false, "b")))
)
}
@@ -374,7 +406,7 @@ class JdbcMapperTest extends AsyncTest[JdbcTestDB] {
class T(tag: Tag) extends Table[Data](tag, "T_fastpath") {
def a = column[Int]("A")
def b = column[Int]("B")
def * = (a, b) <> (Data.tupled, Data.unapply _) fastPath(new FastPath(_) {
def * = (a, b).mapTo[Data].fastPath(new FastPath(_) {
val (a, b) = (next[Int], next[Int])
override def read(r: Reader) = Data(a.read(r), b.read(r))
})
@@ -218,7 +218,7 @@ class NestingTest extends AsyncTest[RelationalTestDB] {
def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
def name = column[Option[String]]("name")
def popularOptions = column[Option[String]]("popularOptions")
def * = (name.getOrElse(""), popularOptions.getOrElse(""), id) <> (Chord.tupled, Chord.unapply)
def * = (name.getOrElse(""), popularOptions.getOrElse(""), id).mapTo[Chord]
}
val chords = TableQuery[Chords]
val allChords = Set(Chord("maj7", "9 #11"), Chord("m7", "9 11"), Chord("7", "9 13"), Chord("m7b5", "11"), Chord("aug7", "9"), Chord("dim7", ""))
@@ -162,7 +162,7 @@ class RelationalMiscTest extends AsyncTest[RelationalTestDB] {
class A(tag: Tag) extends Table[Customer](tag, "INIT_A") {
def id = column[Id]("ID", O.PrimaryKey, O.AutoInc)(Tables.idMapper)
import Tables.idMapper
def * = id.<>(Customer.apply, Customer.unapply)
def * = id.mapTo[Customer]
}
Tables.as.schema
@@ -1,8 +1,10 @@
package slick.lifted
import scala.language.{existentials, implicitConversions, higherKinds}
import scala.language.experimental.macros
import scala.annotation.implicitNotFound
import scala.annotation.unchecked.uncheckedVariance
import scala.reflect.macros.blackbox.Context
import slick.SlickException
import slick.util.{ConstArray, ProductWrapper, TupleSupport}
import slick.ast._
@@ -269,12 +271,53 @@ case class ShapedValue[T, U](value: T, shape: Shape[_ <: FlatShapeLevel, T, U, _
def toNode = shape.toNode(value)
def packedValue[R](implicit ev: Shape[_ <: FlatShapeLevel, T, _, R]): ShapedValue[R, U] = ShapedValue(shape.pack(value).asInstanceOf[R], shape.packedShape.asInstanceOf[Shape[FlatShapeLevel, R, U, _]])
def zip[T2, U2](s2: ShapedValue[T2, U2]) = new ShapedValue[(T, T2), (U, U2)]((value, s2.value), Shape.tuple2Shape(shape, s2.shape))
@inline def <>[R : ClassTag](f: (U => R), g: (R => Option[U])) = new MappedProjection[R, U](shape.toNode(value), MappedScalaType.Mapper(g.andThen(_.get).asInstanceOf[Any => Any], f.asInstanceOf[Any => Any], None), implicitly[ClassTag[R]])
def <>[R : ClassTag](f: (U => R), g: (R => Option[U])) = new MappedProjection[R, U](shape.toNode(value), MappedScalaType.Mapper(g.andThen(_.get).asInstanceOf[Any => Any], f.asInstanceOf[Any => Any], None), implicitly[ClassTag[R]])
@inline def shaped: ShapedValue[T, U] = this
def mapTo[R <: Product with Serializable](implicit rCT: ClassTag[R]): MappedProjection[R, U] = macro ShapedValue.mapToImpl[R, U]
}
object ShapedValue {
@inline implicit def shapedValueShape[T, U, Level <: ShapeLevel] = RepShape[Level, ShapedValue[T, U], U]
def mapToImpl[R <: Product with Serializable, U](c: Context { type PrefixType = ShapedValue[_, U] })(rCT: c.Expr[ClassTag[R]])(implicit rTag: c.WeakTypeTag[R], uTag: c.WeakTypeTag[U]): c.Tree = {
import c.universe._
val rSym = symbolOf[R]
if(!rSym.isClass || !rSym.asClass.isCaseClass)
c.abort(c.enclosingPosition, s"${rSym.fullName} must be a case class")
val rModule = rSym.companion match {
case NoSymbol => q"${rSym.name.toTermName}" // This can happen for case classes defined inside of methods
case s => q"$s"
}
val caseFields = rTag.tpe.decls.collect {
case s: TermSymbol if s.isVal && s.isCaseAccessor => (TermName(s.name.toString.trim), s.typeSignature)
}.toIndexedSeq
val (f, g) = if(uTag.tpe <:< c.typeOf[slick.collection.heterogeneous.HList]) { // Map from HList
val rTypeAsHList = caseFields.foldRight[Tree](tq"_root_.slick.collection.heterogeneous.HNil.type") {
case ((_, t), z) => tq"_root_.slick.collection.heterogeneous.HCons[$t, $z]"
}
val matchNames = caseFields.map(_ => TermName(c.freshName()))
val pat = matchNames.foldRight[Tree](pq"_root_.slick.collection.heterogeneous.HNil") {
case (n, z) => pq"_root_.slick.collection.heterogeneous.HCons($n, $z)"
}
val cons = caseFields.foldRight[Tree](q"_root_.slick.collection.heterogeneous.HNil") {
case ((n, _), z) => q"v.$n :: $z"
}
(q"({ case $pat => new $rTag(..$matchNames) } : ($rTypeAsHList => $rTag)): ($uTag => $rTag)",
q"{ case v => $cons }: ($rTag => $uTag)")
} else if(caseFields.length == 1) { // Map from single value
(q"($rModule.apply _) : ($uTag => $rTag)",
q"(($rModule.unapply _) : $rTag => Option[$uTag]).andThen(_.get)")
} else { // Map from tuple
(q"($rModule.tupled) : ($uTag => $rTag)",
q"(($rModule.unapply _) : $rTag => Option[$uTag]).andThen(_.get)")
}
q"""val ff = $f // Resolving f first creates more useful type errors
new _root_.slick.lifted.MappedProjection[$rTag, $uTag](${c.prefix}.toNode,
_root_.slick.ast.MappedScalaType.Mapper($g.asInstanceOf[Any => Any], ff.asInstanceOf[Any => Any], _root_.scala.None),
$rCT
)"""
}
}
/** A limited version of ShapedValue which can be constructed for every type

2 comments on commit d980dbb

@ikhoon

This comment has been minimized.

Show comment
Hide comment
@ikhoon

ikhoon Aug 7, 2016

Wow, awesome!! 👍

ikhoon replied Aug 7, 2016

Wow, awesome!! 👍

@phderome

This comment has been minimized.

Show comment
Hide comment
@phderome

phderome Oct 29, 2016

Does this commit address some complaints raised on slow compile time when using Shapeless as solution to function +22 params as per this discussion milessabin/shapeless#619 ?
Said differently is the compile time good with case classes of about 100 columns using this build of Slick?

phderome replied Oct 29, 2016

Does this commit address some complaints raised on slow compile time when using Shapeless as solution to function +22 params as per this discussion milessabin/shapeless#619 ?
Said differently is the compile time good with case classes of about 100 columns using this build of Slick?

Please sign in to comment.