How to select max, min in same query in slick - scala

I want to do this SELECT MAX(age), MIN(age) FROM users WHERE name = 'Rick'. The best I came up with involves 2 queries: Users.filter(_.name === 'Rick').map(_.age).max

This is supported, but first, you'll need to group it. Since you're treating the entire set as a group, group it by true and slick will ignore it when generating the SQL:
val q = Users.filter(_.name === 'Rick').groupBy { _ => true }.map {
case (_, group) =>
(group.map(_.age).max, group.map(_.age).min)
}
It should yield you something like this:
q.selectStatement
# => select max(x2."age"), min(x2."age") from "Users" x2
Another approach you might try would be a union.

Related

How can I run inter-dependent queries alongside a non-DB operation in the same transaction using slick

Given the data model (<- indicating a foreign key dependency)
TableA <- TableB <- TableC
^ v
-----------------
I need to execute an api DELETE operation that soft-deletes a row in TableC. This delete must also trigger a call to another service (requiring values from TableA and TableB) if there are no more undeleted TableC entries that reference that row's parent in TableB. If the external call fails, I want to rollback the soft-delete. I want to do all of this in an idiomatic fashion (I'm effectively brand new to scala/slick), and use transactions for the rollback
Based on what I've read, I need to be using for comprehension to assemble the queries, but I'm having issues getting the database operations to gel nicely with the external service call. My original plan was:
val select = for {
tableCRow <- tableBDao.TableQueryC.filter(_.id === idParam)
tableBRow <- tableBDao.TableQueryB if tableCRow.tableBForeignKey === tableBRow.id
tableARow <- TableADao.TableQueryA if tableCRow.tableAForeignKey === tableARow.id
count <- tableBDao.TableQueryC.filter(_.tableBForeignKey === tableBRow.id).map(_.id).countDefined
_ <- tableBDao.softDeleteRow(idParam)
_ <- if (count > 1) DBIO.successful(httpRequestService.deleteOtherResource(tableARow.someValue, tableBRow.someValue))
} yield ()
db.run(select.result)
But this had problems because I couldn't pass Slick's Rep[T] values to my httpRequestService method. I then tried to break it down into two portions - SELECT first, then DELETE, like so:
val select = for {
tableCRow <- tableBDao.TableQueryC.filter(_.id === idParam)
tableBRow <- tableBDao.TableQueryB if tableCRow.tableBForeignKey === tableBRow.id
tableARow <- TableADao.TableQueryA if tableCRow.tableAForeignKey === tableARow.id
count <- tableBDao.TableQueryC.filter(_.tableBForeignKey === tableBRow.id).map(_.id).countDefined
} yield (tableBRow.date.formatted("yyyy-MM-DD"), tableARow.externalServiceId, count)
val result: Future[Option[(String, Long, Integer)]] = db.run(select.result.headOption)
result.map {
case None => throw new IllegalArgumentException("exception message")
case Some(data) =>
val delete = for {
_ <- tableBDao.softDeleteRow(idParam)
_ <- if (data._3 > 1) DBIO.successful(httpRequestService.cancelSchedulerJob(data._2, data._1))
} yield numRows
db.run(delete.transactionally)
}
But, despite this actually passing IntelliJ IDEA checks, it won't compile as my count query returns a Rep[Int], which lacks a map function. Additionally, each of my table(A|B|C)Row maps raises an error because they're expecting slick.lifted.Query[Nothing,Nothing,Seq] and they're getting slick.lifted.Query[Nothing,T,Seq]. Finally, the db.run statement doesn't want to use headOption, and apparently returns Any which doesn't support map
halp
Solved this by finally understanding how slick puts things together in a for-comprehension. I had to pull the count out of the query and into a followup groupby->map function, which accumulated my list of things I wanted to count and THEN counted them as opposed to counting as part of the query. This fixed all the rest of the problems too, as the count query was throwing off the expected return types of everything else.
basically, the solution looked like (thing1 was for a join):
val select = (for {
thing1 <- query1
thing2 <- query2
thing3 <- query3
listToCount <- query4 selecting everything I wanted to count
} yield (thing2, thing3, listToCount))
.groupBy({
case (thing2, thing3, listToCount) =>
(thing2, thing3)
})
.map({
case ((thing2, thing3), list) =>
(thing2.deliveryDate, thing3.schedulerJobId, list.map(_._3).length)
})

Group by in many-to-many join with Quill

I am trying to achieve with Quill what the following PostgreSQL query does:
select books.*, array_agg(authors.name) from books
join authors_books on(books.id = authors_books.book_id)
join authors on(authors.id = authors_books.author_id)
group by books.id
For now I have this in my Quill version:
val books = quote(querySchema[Book]("books"))
val authorsBooks = quote(querySchema[AuthorBook]("authors_books"))
val authors = quote(querySchema[Author]("authors"))
val q: db.Quoted[db.Query[(db.Query[Book], Seq[String])]] = quote{
books
.join(authorsBooks).on(_.id == _.book_id)
.join(authors).on(_._2.author_id == _.id)
.groupBy(_._1._1.id)
.map {
case (bId, q) => {
(q.map(_._1._1), unquote(q.map(_._2.name).arrayAgg))
}
}
}
How can I get rid of the nested query in the result (db.Query[Book]) and get a Book instead?
I might be a little bit rusty with SQL but are you sure that your query is valid? Particularly I find suspicious that you do select books.* while group by books.id i.e. you directly return fields that you didn't group by. And attempt to translate that wrong query directly is what makes things go wrong
One way to fix it is to do group by by all fields. Assuming Book is declared as:
case class Book(id: Int, name: String)
you can do
val qq: db.Quoted[db.Query[((Index, String), Seq[String])]] = quote {
books
.join(authorsBooks).on(_.id == _.book_id)
.join(authors).on(_._2.author_id == _.id)
.groupBy(r => (r._1._1.id, r._1._1.name))
.map {
case (bId, q) => {
// (Book.tupled(bId), unquote(q.map(_._2.name).arrayAgg)) // doesn't work
(bId, unquote(q.map(_._2.name).arrayAgg))
}
}
}
val res = db.run(qq).map(r => (Book.tupled(r._1), r._2))
Unfortunately it seems that you can't apply Book.tupled inside quote because you get error
The monad composition can't be expressed using applicative joins
but you can easily do it after db.run call to get back your Book.
Another option is to do group by just Book.id and then join the Book table again to get all the fields back. This might be actually cleaner and faster if there are many fields inside Book

Slick filter when comparing value and column is Option

have a table where formDefinitionId is nullable column in Postgres DB (9.3) . Slick version is 2.1.0
we have a this filter on a query
table.filter(_.formDefinitionId === request.formDefinitionId)
request.formDefinitionId where request is nothing but a Scala case class with formDefinitionId as an Option[Int]
case class Request(formDefinitionId : Option[Int])
now when request.formDefinitionId is None slick generates following query
(x2."form_definition_id" = null)
vs
(x2."form_definition_id” IS NULL) - this is what Postgres expects
Work around is to create a different filter based on request.formDefinitionId
e.g.
request.formDefinitionId match {
case Some(fid) => table.filter(_.formDefinitionId === fid)
case None => table.filter(_.formDefinitionId.isNull)
}
however creating such condition for every property which is Option isn't feasible - there exists numerous Option properties across many tables. Is there a better/generic way to achieve this? I would imagine this is a common scenario with people using Slick
Didn't test (I don't have a Slick setup here).
But as = null returns nothing, shouldn't something like this work?
table.filter(r => r.formDefinitionId.isNull || r.formDefinitionId === fid)
You can use the following solution with Slick 3.3.x:
table
// case 1: both are empty
.filterIf(request.formDefinitionId.isEmpty)(_.formDefinitionId.isEmpty)
// case 2: both are defined and equal
.filterOpt(request.formDefinitionId)(_.formDefinitionId === _)
case 1 corresponds the following SQL:
select * from table1 when form_definition_id is null;
case 2:
select * from table1 when form_definition_id = ?;
I am a newer to slick and now also confused with this problem. Maybe it's toolate to you.I wanted to do some search with different column(Option[text]). Here is my code ,It is worked to me.
val someone1 = cou.filter(_.id === id_S)
val someone2 = cou.filter(_.first_name === first)
val someone3 = cou.filter(_.last_name === last)
val someone = (someone1) union (someone2) union (someone3)
dbConfig.db.run(someone.result).map(_.toList)

Slick 3.0.0 - How to sortBy on a query with joinLeft

This question is related to another. I'm also trying to sort on a query with a joinLeft but in slick 3.0.0. And as the Option Rep are automatically lifted how would I do the exact same thing ?:
def list(filter: String, orderBy: Int):Future[Seq[(Computer, Option[Company])]] = {
val initialQuery = for {
(computer, company) <- Computer.filter(_.name like filter) leftJoin
Company on (_.companyId === _.id)
} yield (computer, company)
val sortedQuery = orderBy match {
case 2 => initialQuery.sortBy(_._1.name) //Works ok, column from a primary table
case 3 => initialQuery.sortBy(_._2.map(_.name)) //could not find implicit value for parameter ol: slick.lifted.OptionLift[slick.lifted.ColumnOrdered[String],slick.lifted.Rep[Option[QO]]]
}
db.run(sortedQuery.result)
}
Thanks,
I suppose that missing parenthesis is just a typo. I had this problem recently when I was specifying the sort direction in the wrong place, using your example:
case 3 => initialQuery.sortBy(_._2.map(_.name.asc))
It should be:
case 3 => initialQuery.sortBy(_._2.map(_.name).asc)
I had this problem too. I had a joinLeft and I want to order on a boolean column. You must be decide when Its join is empty what value you want replace it.
for example:
initialQuery.sortBy(_._2.map(_.name).getOrElse(""))
Are you sure you copied the correct code?
case 3 => data.sortBy(_._2.map(_.name) //could not find implicit value for parameter
Missing a ")"
Should be
case 3 => data.sortBy(_._2.map(_.name))
You could put the fields into the result set.
For example:
val initialQuery = for {
(computer, company) <- Computer.filter(_.name like filter) leftJoin Company on (_.companyId === _.id)
} yield (computer, company, company.map(_.name))
val sortedQuery = orderBy match {
case 2 => initialQuery.sortBy(_._1.name)
case 3 => initialQuery.sortBy(_._3)
}
db.run(sortedQuery.map(v => (v._1, v._2).result)

Dynamically created slick queries

I have three tables: users, groups and users_groups. There is a many to many relation from groups and users because one user can belong in multiple groups and a group is constituted by multiple users.
I have a GET query like /group?name=X&user=Y
From that I'm searching from groups with name like X, but the tricky part is searching for the groups which the Y user doesnt belong.
def findUserGroups(id: Long) = {
users_groups.filter(ug => ug.userID === id)
}
From that I get all the groups which the user belongs, then I do this
var queries : List[Query[GroupsTable, GroupsTable#TableElementType, Seq]]= List[Query[GroupsTable, GroupsTable#TableElementType, Seq]]()
userGroups map { userGroup =>
val query : Query[GroupsTable, GroupsTable#TableElementType, Seq] = groups.filter(_.id =!= userGroup.group.get)
queries = query :: queries
}
If I println userGroup it gives me the correct groups.
Finally I've been trying a union
def findGroupByNameSynthFunction(name: String, queries: List[Query[GroupsTable, GroupsTable#TableElementType, Seq]]) = {
val query1 = groups.filter(g => g.name like ("%" + name + "%"))
val unionQuery: Query[GroupsTable, GroupsTable#TableElementType, Seq] = query1
queries map { query =>
unionQuery ++ query
}
unionQuery
}
I execute it
val found = GroupsTable.findGroupByNameSynthFunction(name, queries).run
But I get all groups anyway.
Can Someone explained me what I'm doing very wrong!? :)
I am not sure where exactly you had the wrong expectation, but it probably revolves around the expression
queries map { query =>
unionQuery ++ query
}
This doesn't have any side-effect. Unless you are doing something with the resulting value (and you are not) this doesn't do anything. You probably want something like
def findGroupByNameSynthFunction(
name: String,
queries: List[Query[GroupsTable, GroupsTable#TableElementType, Seq]]
) = queries.map(_.filter(_.name like ("%" + name + "%")))
.reduce(_ union _)