1
votes

I have the following simplified code to reflect the issue I'm facing. It's using slick 3.1.1, with scala 2.10.4 and mysql.

I have a User table, with one column that has Option[Seq[String]] and the other column is Seq[String].

There are 2 MappedColumnType; that will convert the Seq[String] to String, and Option[Seq[String]] to String

Below is the simplified code:

package models

import slick.driver.MySQLDriver.api._

case class User(id: Long,
                name: Option[String],
                cities: Option[Seq[String]] = None,
                countries: Seq[String])

class UserMapping(tag: Tag) extends Table[User](tag, "USERS") {

  implicit val stringListMapper = MappedColumnType.base[Seq[String], String](
    list => list.mkString(","),
    string => string.split(',').toSeq
  )

  implicit val stringOptionalListMapper = MappedColumnType.base[Option[Seq[String]], String](
    list => list.get.mkString(","),
    string => Some(string.split(',').toSeq)
  )

  def id: Rep[Long] = column[Long]("ID", O.PrimaryKey, O.AutoInc)
  def name: Rep[Option[String]] = column[Option[String]]("NAME")
  def cities: Rep[Option[Seq[String]]] = column[Option[Seq[String]]]("CITIES")(stringOptionalListMapper)
  def countries: Rep[Seq[String]] = column[Seq[String]]("COUNTRIES")(stringListMapper)

  // scalastyle:off method.name public.methods.have.type
  def * = (id, name, cities, countries) <> (User.tupled, User.unapply)
  // scalastyle:on method.name public.methods.have.type

}

The compiler is failing at the projection:

[error] Slick does not know how to map the given types.
[error] Possible causes: T in Table[T] does not match your *     projection. Or you use an unsupported type in a Query (e.g. scala List).
[error]   Required level: slick.lifted.FlatShapeLevel
[error]      Source type: (slick.lifted.Rep[Long], slick.lifted.Rep[Option[String]], slick.lifted.Rep[Option[Seq[String]]], slick.lifted.Rep[Seq[String]])
[error]    Unpacked type: (Long, Option[String], Option[Seq[String]], Seq[String])
[error]      Packed type: Any
[error]   def * = (id, name, cities, countries) <> (User.tupled, User.unapply)
[error]                                         ^
[error] one error found
[error] (compile:compileIncremental) Compilation failed
[error] Total time: 2 s, completed Jan 23, 2018 3:24:23 PM

It works fine if I have only one MappedColumnType defined in here; unfortunately, my data model needs one to be optional and the other to be required. Any ideas of what's happening in here? Thanks!

1

1 Answers

1
votes

It looks like that the really important thing is not that there are two mapped column but the fact that they are of the same shape except for Option. This is bad because it makes you introduce two implicit vals for mappings and this makes them ambiguous for conversion of (id, name, cities, countries) into a ProvenShape

If the logic for such shape is actually the same as in your example, then Slick seems to be able to add Option wrapper on its own so you are OK with just one (non-Option) implicit such as:

class UserMapping(tag: Tag) extends Table[User](tag, "USERS") {

  implicit val stringListMapper = MappedColumnType.base[Seq[String], String](
    list => list.mkString(","),
    string => string.split(',').toSeq
  )

  def id: Rep[Long] = column[Long]("ID", O.PrimaryKey, O.AutoInc)

  def name: Rep[Option[String]] = column[Option[String]]("NAME")

  def cities: Rep[Option[Seq[String]]] = column[Option[Seq[String]]]("CITIES")   // share stringListMapper

  def countries: Rep[Seq[String]] = column[Seq[String]]("COUNTRIES")        // share stringListMapper

  // scalastyle:off method.name public.methods.have.type
  def * = (id, name, cities, countries) <> (User.tupled, User.unapply)
  // scalastyle:on method.name public.methods.have.type

}

However if you are unlucky and mappings for the same shape are actually different so you need to pass them both explicitly to column (for example if the separator chars are different), then you will have to explicitly provide an implicit evidence of the proper Shape such as:

class UserMapping(tag: Tag) extends Table[User](tag, "USERS") {

  val stringListMapper = MappedColumnType.base[Seq[String], String](
    list => list.mkString(","),
    string => string.split(',').toSeq
  )

  val stringOptionalListMapper = MappedColumnType.base[Option[Seq[String]], String](
    list => list.get.mkString(","),
    string => Some(string.split(',').toSeq)
  )

  def id: Rep[Long] = column[Long]("ID", O.PrimaryKey, O.AutoInc)

  def name: Rep[Option[String]] = column[Option[String]]("NAME")

  def cities: Rep[Option[Seq[String]]] = column[Option[Seq[String]]]("CITIES")(stringOptionalListMapper)

  def countries: Rep[Seq[String]] = column[Seq[String]]("COUNTRIES")(stringListMapper)


  // explicitly provide proper Shape evidence
  import slick.lifted.Shape
  implicit val shape = Shape.tuple4Shape(
    Shape.repColumnShape(longColumnType),
    Shape.optionShape(Shape.repColumnShape(stringColumnType)),
    Shape.repColumnShape(stringOptionalListMapper),
    Shape.repColumnShape(stringListMapper))

  // scalastyle:off method.name public.methods.have.type
  def * = (id, name, cities, countries) <> (User.tupled, User.unapply)
  // scalastyle:on method.name public.methods.have.type

}