Why can't I use Options inside of a slick query - scala

In order to save me having to create so many methods, I tried passing in Option's into my method and then checking if the Option is defined, if so, then apply the filter.
def getUsers(locationId: Option[Int], companyId: Int, salary: Option[Int]): List[User] = {
val query = for {
u <- users if u.companyId === companyId && (locationId.isDefined && u.locationId === locationId.get) && (salary.isDefined && u.salary >= salary.get)
}
query.list()
}
I am getting errors saying:
polymorphic expression cannot be instantiated to expected type;
IntelliJ errors are expected Boolean actual Column[Boolean].
Is this type of clause just not possible in a slick query or I'm just doing it wrong?

I can't tell you why but this compiles for me:
def getUsers(locationId: Option[Int], companyId: Int, salary: Option[Int]): List[User] = {
val query = for {
u <- users if u.companyId === companyId && locationId.isDefined && u.locationId === locationId.get && salary.isDefined && u.salary >= salary.get
} yield(u)
query.list()
}
Note that there are no parenthesis and that you have to yield something otherwise the return type for query would be Unit.

Sure, don't see any issue here, just use filter (or withFilter) and map over the options.
def getUsers(locationId: Option[Int], companyId: Int, salary: Option[Int]): List[User] = (for {
u <- users filter(u=>
if (u.companyId === companyId.bind) &&
(locationId.map(_.bind === u.locationId).getOrElse(true)) &&
(salary.map(_.bind <= u.salary).getOrElse(true))
)
} yield u).list()
Using filter allows you to drop down to Scala for the map or true fallback expressions. If you start with u < users if... then there's no way to use Scala conditionals. The bind calls just escape potential malicious input (i.e. if params are coming from outside the application).

Why it doesn't work
As cvot has noted in his comment, the reason this doesn't work is because:
Slick translates the None as SQL NULL including SQLs 3-valued-logic NULL propagation, so (None === a) is None regardless of the value of a ... basically if anything is None in the expression, the whole expression will be None, so the filter expression will be treated as false and the query result will be empty.
That said, there is a way to get the same behavior you want (filtering only if an optional value is provided).
A way to arrive at the desired behavior
The key thing to note is that for comprehensions get compiled down by Scala to a combination of map / flatMap / withFilter / filter calls. Slick, if I understand it correctly, works with the resulting structure when it compiles the Scala comprehension into a SQL query.
This lets us build up a query in parts:
val baseQuery = for {
u <- users if u.companyId === companyId
} yield u
val possiblyFilteredByLocation = if (locationId.isDefined) {
baseQuery.withFilter(u => u.locationId === locationId.get
} else baseQuery
val possiblyFilteredBySalaryAndOrLocation = if (salary.isDefined) {
possiblyFilteredByLocation.withFilter(u => u.salary >= salary.get)
} else possiblyFilteredByLocation
possiblyFilteredBySalaryAndOrLocation.list()
We can simplify this by using a var and fold:
var query = for {
u <- users if u.companyId === companyId
} yield u
query = locationId.fold(query)(id => query.withFilter(u => u.locationId === id))
query = salary.fold(query)(salary => query.withFilter(u => u.salary >= salary))
query.list()
If we do this frequently, we can generalize this pattern of filtering on an Option into something like this:
// Untested, probably does not compile
implicit class ConditionalFilter(query: Query) {
def ifPresent[T](value: Option[T], predicate: (Query, T) => Query) = {
value.fold(query)(predicate(query, _))
}
}
Then we can simplify our whole filter chain to:
query
.ifPresent[Int](locationId, (q, id) => q.withFilter(u => u.locationId === id))
.ifPresent[Int](salary, (q, s) => q.withFilter(u => u.salary >= s))
.list()

You can use the following solution (with Slick 3.3.x):
def getUsers(locationId: Option[Int], companyId: Int, minSalary: Option[Int]) =
users.
.filter(_.company === companyId)
.filterOpt(locationId)(_.locationId === _)
.filterOpt(minSalary)(_.salary >= _)

Because the Slick query gets translated into SQL, which has no notion of the isDefined and get methods of the Option class.
But you can fix this by calling the methods outside the query and passing the results (via the map function on the options).
The following code should fix it:
def getUsers(locationId: Option[Int], companyId: Int, salary: Option[Int]): List[User] = {
val locationAndSalary = for {
locId <- locationId;
sal <- salary
} yield (locId, sal)
locationAndSalary.map(locAndSal => {
val query = for {
u <- users if u.companyId === companyId && u.locationId === locAndSal._1 && u.salary >= locAndSal._2)
} yield u
query.list()
}).getOrElse(List[User]()) //If the locationID or salary is None, return empty list.
}
The locationAndSalary may seem strange, but we are using for comprehensions to give use a value only when both locationId and salary has a value and storing the result in a tuple, with the locationId in the first position and salary at the second. The following links explains it: Scala: for comprehensions with Options.
Edit: According to #Ende Neu answer the code compiles if you add the yield-statement, but I still think my solution is more the "Scala way".

Related

Making a Slick query that filters from an optional id?

I would like to query an object by id, but if a fooId is also provided then I'd like to include it in the query.
def getById(id: Long, fooIdOpt: Option[Long]): Future[Option[Bar]] = {
val query = for {
b <- bars if b.id === id && fooIdOpt.fold(true)(b.fooId === _)
} yield { // Compiler error here ^
b
}
db.run(query.result.headOption)
}
The issue here is that fooIdOpt.fold(true)(b.fooId === _) needs to return a Rep[Boolean], but I'm initializing the fold with a Boolean - a clear type violation of fold's method signature.
However I can't seem to find a way to pass a Rep[Boolean] that evaluates to true in as the fold initializer. Shooting in the dark, I've tried Rep(true), and LiftedLiteral[Boolean](true), but neither quite work.
I could abandon fold entirely and go with:
b <- bars if {
val rep1 = b.id === id
fooIdOpt.map(fooId => rep1 && b.fooId === fooId).getOrElse(rep1)
}
But that just seems so overly complicated, and a fooIdOpt match { case Some ... wouldn't look much better.
Is there a way to instantiate a Rep[Boolean] literal that always evaluates to true?
If not, is there an alternative to my attempts at fold, map, or match above that will allow me to build a query that optionally compares a fooId value?
As you've already figured out, the ifEmpty value type for fold needs to match that of b.fooId === _, which is Rep[Boolean]. One approach would be to apply bind to the ifEmpty value as shown below:
val query = for {
b <- bars if b.id === id && fooIdOpt.fold(true.bind)(b.fooId === _)
} yield b
One alternative that isn't quite as bad as your other options:
(fooIdOpt.toList.map(b.fooId === _) :+ (b.id === id)).reduceLeft(_ && _)

Slick one to many and grouping

I'm trying to model the following with Slick 3.1.0;
case class Review(txt: String, userId: Long, id: Long)
case class User(name: String, id: Long)
case class ReviewEvent(event: String, reviewId: Long)
I need to populate a class called a FullReview, which looks like;
case class FullReview(r: Review, user: User, evts: Seq[ReviewEvent])
Assuming I have the right tables for each of the models, I'm trying to fetch a FullReview using a combination of join and group by, like so:
val withUser = for {
(r, u) <- RTable join UTable on (_.userId === _.id)
}
val withUAndEvts = (for {
((r, user), evts) <- withUser joinLeft ETable on {
case ((r, _), ev) => r.id === ev.reviewId
}
} yield (r, user, events)).groupBy(_._1._id)
This seems to yield, when a nested Query type, from what I can see. What am I doing wrong here?
If I understand you correctly, you can use following example:
val users = TableQuery[Users]
val reviews = TableQuery[Reviews]
val events = TableQuery[ReviewEvents]
override def findAllReviews(): Future[Seq[FullReview]] = {
val query = reviews
.join(users).on(_.userId === _.id)
.joinLeft(events).on(_._1.id === _.reviewId)
db.run(query.result).map { a =>
a.groupBy(_._1._1.id).map { case (_, tuples) =>
val ((review, user), _) = tuples.head
val reviewEvents = tuples.flatMap(_._2)
FullReview(review, user, reviewEvents)
}.toSeq
}
}
If you want to add pagination to this request, I've already answered here and here is full example.
From some tinkering around, I figured it would just be better to do the aggregation on the client. What that would mean, indirectly, is that if 100 rows on the table ETable would match a single row on the RTable, you would get multiple rows on the client. The client then has to implement its own aggregation to group all the ReviewEvent by Review.
As far as pagination is concerned, you may do something like;
def withUser(page: Int, pageSize: Int) = for {
(r, u) <- RTable.drop(page * pageSize).take(pageSize) join UTable on (_.userId === _.id)
}
I guess this is elegant enough for now. If someone has a better answer, I'd be happy to hear it.

Conditionally map a nullable field to None value in Slick

In Slick 2.1, I need to perform a query/map operation where I convert a nullable field to None if it contains a certain non-null value. Not sure whether it matters or not, but in my case the column type in question is a mapped column type. Here is a code snippet which tries to illustrate what I'm trying to do. It won't compile, as the compiler doesn't like the None.
case class Record(field1: Int, field2: Int, field3: MyEnum)
sealed trait MyEnum
val MyValue: MyEnum = new MyEnum { }
// table is a TableQuery[Record]
table.map { r => (
r.field1,
r.field2,
Case If (r.field3 === MyValue) Then MyValue Else None // compile error on 'None'
)
}
The error is something like this:
type mismatch; found : None.type required: scala.slick.lifted.Column[MyEnum]
Actually, the reason I want to do this is that I want to perform a groupBy in which I count the number of records whose field3 contains a given value. I couldn't get the more complicated groupBy expression working, so I backed off to this simpler example which I still can't get working. If there's a more direct way to show me the groupBy expression, that would be fine too. Thanks!
Update
I tried the code suggested by #cvogt but this produces a compile error. Here is a SSCCE in case anyone can spot what I'm doing wrong here. Compile fails with "value ? is not a member of Int":
import scala.slick.jdbc.JdbcBackend.Database
import scala.slick.driver.H2Driver
object ExpMain extends App {
val dbName = "mydb"
val db = Database.forURL(s"jdbc:h2:mem:${dbName};DB_CLOSE_DELAY=-1", driver = "org.h2.Driver")
val driver = H2Driver
import driver.simple._
class Exp(tag: Tag) extends Table[(Int, Option[Int])](tag, "EXP") {
def id = column[Int]("ID", O.PrimaryKey)
def value = column[Option[Int]]("VALUE")
def * = (id, value)
}
val exp = TableQuery[Exp]
db withSession { implicit session =>
exp.ddl.create
exp += (1, (Some(1)))
exp += (2, None)
exp += (3, (Some(4)))
exp.map { record =>
Case If (record.value === 1) Then 1.? Else None // this will NOT compile
//Case If (record.value === 1) Then Some(1) Else None // this will NOT compile
//Case If (record.value === 1) Then 1 Else 0 // this will compile
}.foreach {
println
}
}
}
I need to perform a query/map operation where I convert a nullable field to None if it contains a certain non-null value
Given the example data you have in the update, and pretending that 1 is the "certain" value you care about, I believe this is the output you expect:
None, None, Some(4)
(for rows with IDs 1, 2 and 3)
If I've understood the problem correctly, is this what you need...?
val q: Query[Column[Option[Int]], Option[Int], Seq] = exp.map { record =>
Case If (record.value === 1) Then (None: Option[Int]) Else (record.value)
}
...which equates to:
select (case when ("VALUE" = 1) then null else "VALUE" end) from "EXP"
You need to wrap MyValue in an Option, so that both outcomes of the conditional are options. In Slick 2.1 you use the .? operator for that. In Slick 3.0 it will likely be Rep.Some(...).
Try
Case If (r.field3 === MyValue) Then MyValue.? Else None
or
Case If (r.field3 === MyValue) Then MyValue.? Else (None: Option[MyEnum])

Slick 1.0.0 | Return Expression From For Comprehension

The code below compiles and works fine as shown.
However, if I try to yield Some("SomeConstant"), I get the runtime error shown below.
Why is this happening, and how can I return expressions (e.g. Some(...)) from my query?
def cannotUnpack(db: Database) {
db.withSession {
val data = (for {
rw1 <- TableOne
rw2 <- TableTwo if rw1.cl1 === rw2.cl1 && rw1.cl2 === rw2.cl2 && rw1.cl1 === "0"
now = new Timestamp(System.currentTimeMillis())
six = 6
} yield (uuid, rw1.cl3, "SomeConstant", six, now) ).list // Works
// } yield (uuid, rw1.cl3, Some("SomeConstant"), six, now) ).list // Runtime error
}
}
Runtime error:
Don't know how to unpack (String, scala.slick.lifted.Column[Option[String]], Some[String], scala.slick.lifted.Column[Int], scala.slick.lifted.Column[java.sql.Timestamp]) to T and pack to G
rw2 <- TableTwo if rw1.cl1 === rw2.cl1 && rw1.cl2 === rw2.cl2 && rw1.cl1 === "0"
^
Environment:
scala 2.10 on Ubuntu, Java 7
Slick 1.0.0, SQL Server, JTDS driver
The short answer: It works, if you write
Some("SomeConstant") : Option[String].
The long answer: If you supply a constant as part of your Slick query, Slick has to put the value into the SQL query and later read it back from the results. This preserves composability, i.e. allows you to use the Slick query as a component in another Slick query. In order to encode the value into the SQL query, the method you are calling (in case of a comprehension: map or flatMap) needs to find an implicit value of type TypeMapper[T] where T is the type of the value. Slick does define a TyperMapper[Option[String]] but the problem is that it does not apply in your case, because Some("SomeConstant") is of type Some[String] and there is no TypeMapper[Some[String]] defined in Slick (and TypeMapper[T] is invariant in T). By explicitly supplying :Option[String], you loosen the type information, so a matching TypeMapper can be found.
We will think about if we can add support for constants of type Some into Slick. I added a ticket (https://www.assembla.com/spaces/typesafe-slick/tickets/268) and will bring it up in our next team meeting.
Well, I wouldn't involve constants in select but use them only when managing results loaded from DB.
Try it like:
def cannotUnpack(db: Database) {
db.withSession {
val data = (for {
rw1 <- TableOne
rw2 <- TableTwo if rw1.cl1 === rw2.cl1 && rw1.cl2 === rw2.cl2 && rw1.cl1 === "0"
} yield (uuid, rw1.cl3)
}
}
after that get your data ready for what you need:
for (
(uuid, rw1_cl3) <- data.list
) yield (uuid, rw1_cl3, Some("constant"), 6, new Timestamp(System.currentTimeMillis()))
I usually use an output case class when preparing final data, for instance:
case class Export(uuid: String, rw1: String, constant: Option[String], six: String, now: Timestamp)
for (
(uuid, rw1_cl3) <- data.list
) yield Export(
uuid,
rw1_cl3,
Some("constant"),
6,
new Timestamp(System.currentTimeMillis()))

How do you change lifted types back to Scala types when using Slick lifted embedding?

How do you 'un-lift' a value inside a query in Slick when using lifted embedding? I was hoping a 'get', 'toLong' or something like that may do the trick, but no such luck.
The following code does not compile:
val userById = for {
uid <- Parameters[Long]
u <- Users if u.id === uid
} yield u
val userFirstNameById = for {
uid <- Parameters[Long]
u <- userById(uid)
---------------^
// type mismatch; found : scala.slick.lifted.Column[Long] required: Long
} yield u.name
You can't, for 2 reasons:
1) with val this is happening at compile time, there is no Long
value uid. userById(uid) binds a Long uid to the compile time
generated prepared statement, and then .list, .first, etc. invoke
the query.
2) the other issue is as soon as you Parameterize a query,
composition is no longer possible -- it's a limitation dating back to
ScalaQuery.
Your best bet is to delay Parameterization until the final composed query:
val forFooBars = for{
f <- Foos
b <- Bars if f.id is b.fooID
} yield(f,b)
val allByStatus = for{ id ~ active <- Parameters[(Long,Boolean)]
(f,b) <- forFooBars if (f.id is id) && (b.active is active)
} yield(f,b)
def findAllByActive(id: Long, isActive: Boolean) = allByStatus(id, isActive).list
At any rate, in your example you could just as well do:
val byID = Users.createFinderBy(_.id)
The only way that I know to get this kind of thing to work is wrap the query val in a def and pass in a runtime variable, which means Slick has to re-generate the sql on every request, and no prepared statement is sent to underlying DBMS. In some cases you have to do this, like passing in a List(1,2,3) for inList.
def whenNothingElseWorks(id: Long) = {
val userFirstNameById = for {u <- userById(id.bind)} yield u.name
}