Slick: Weird case statement when creating a query with left join - postgresql

In one of my queries in the application, I just realized that Slick adds this add case statement in select.
Here is how I build the query:
def search(searchCriteria: SearchCriteria,
drop: Long = 0,
take: Long = 100): Future[Seq[(School, Option[Address])]] = {
val query = Schools.schools joinLeft
Addresses.addresses on ((s, a) => s.addressId === a.id && a.deletedAt.isEmpty)
val q = for {
(school, address) <- query.filter{ case (s, a) =>
List(
Some(s.deletedAt.isEmpty),
searchCriteria.school.name.map(n => s.name.toLowerCase like s"%${n.toLowerCase}%"),
searchCriteria.school.ready.map(r => s.ready === r)
).collect({ case Some(criteria) => criteria }).reduceLeftOption(_ && _).getOrElse(true: Rep[Boolean])
}
} yield (school, address)
db.run(q.drop(drop).take(take).result)
}
It is a really simple join with some filtering. And in fact, everything works fine. But, when I inspect the executed query I see this:
SELECT
x2."id",
x2."address_id",
x2."name",
x2."about",
x2."number_of_students",
x2."website_url",
x2."media_id",
x2."slug",
x2."short_description",
x2."ready",
x2."classrooms",
x2."year_established",
x2."display_copyright",
x2."createdat",
x2."updatedat",
x2."deletedat",
x2."createdby",
x2."updatedby",
x2."dli_number",
(CASE WHEN (x3."id" IS NULL)
THEN NULL
ELSE 1 END),
x3."id",
x3."country_id",
x3."continent_id",
x3."state_id",
x3."address1",
x3."address2",
x3."city",
x3."zip",
x3."createdat",
x3."updatedat",
x3."deletedat",
x3."createdby",
x3."updatedby"
FROM "school" x2 LEFT OUTER JOIN "address" x3 ON (x2."address_id" = x3."id") AND (x3."deletedat" IS NULL)
WHERE ((x2."deletedat" IS NULL) AND (lower(x2."name") LIKE '%a%')) AND (x2."ready" = TRUE)
LIMIT 100
OFFSET 0
Why Slick adds this:
(CASE WHEN (x3."id" IS NULL) THEN NULL ELSE 1 END)
I can't see how this is useful in any way.
Thanks,

Related

Slick how to sort by a option column got from left join

At first, get all the children comments in a subquery
val getChildCommentStatQuery =
Tbcomments
.filter(row =>
row.itargetid === commentTargetId && row.iparentid =!= 0 && row.istatus === statusNormal)
.groupBy(_.iparentid)
.map { case (parentId, group) =>
// return parent commentId,the number of all the children,and the sum of like of all the children
(parentId, group.size, group.map(_.ilikecount).sum)
}
Secondly, use Table Tbcomments to left join with subquery table got in the first step.
val query =
Tbcomments
.joinLeft(getChildCommentStatQuery)
.on(_.iid == _._1)
.filter { case (comments, _) =>
// got the first level comment whose parentid equals to 0
comments.itargetid === commentTargetId && comments.iparentid === 0 && comments.istatus === statusNormal
}
.map { case (comments, childComments) =>
val replyCount = childComments.map(_._2)
val likeCountSum = comments.ilikecount + childComments.map(_._3.getOrElse(0))
val hot = replyCount + likeCountSum
val support = comments.ilikecount - comments.inotlikecount
(comments.iid, replyCount, likeCountSum, hot, support, comments.dtcreated)
}
.sortBy(row =>
sortOrder match {
case LATEST_COMMENT_ORDER => row._6.desc
case EARLIEST_COMMENT_ORDER => row._6.asc
case SUPPORT_COMMENT_ORDER => row._5.desc
// case HEAT_COMMENT_ORDER => row._4.get
})
The problem is, the row._4 is a field aggregate based on right table, and in the left join case it's an option. It seems can't sortBy the option field directly.
Thanks!

Slick Combining query conditions effeciently

I have two table and i want to run this query everytime.
def query(host: String, id: String, key: String, map: Map[String, List[String]) =
{
val query = (for {
t1 <- TableQuery[TableA]
t2 <- TableQuery[TableB]
if t1.host === host &&
(t2.host === host) &&
(t1.id === t2.id)
} yield
t2.name)
.result
db.run(query)
}
When host is not my-host do execute the above function as it is.
But want to add an additional check only when t1.host == 'my-host', then check if the key is present in the map, if yes add the condition t1.class in map values.
I want something like:
t1 <- TableQuery[TableA]
t2 <- TableQuery[TableB]
if t1.host === host &&
t1.host === 'my-host'
(t2.host === host) &&
(t1.id === t2.id) &&
t1.value inSet map(key)
else if t1.host === host &&
(t2.host === host) &&
(t1.id === t2.id)
I'm not sure if you want to construct a different query for the two cases, or write a single SQL query that handles both cases.
If it's a single query, I suggest writing out the SQL you'd expect and then it should be possible to map that to a filter (for comprehension if)
But I'm guessing you want to construct a different query based on the case of host. In that case, the pattern I'd suggest is:
construct a base query (for the things you always want to do);
add on additional filters for the case you care about; and finally
map the query to the result you need (select the right columns).
(One thing to watch out for is understanding what's happening in SQL (in the database), and what's happening in Scala (in the client). For example, where you say "I want something like:" you have difficulties because inside that for-comprehension you're already in database land. That might be possible using conditional logic in SQL, but I don't think that's what you're looking for).
To give a simplified example you might construct a basic query like this:
val baseQuery = for {
t1 <- TableQuery[TableA]
t2 <- TableQuery[TableB]
if t1.host === host && t2.host === host && t1.id === t2.id
} yield (t1, t2)
Then use that to make a new query to handle the special cases:
val queryAllowingForMyHost =
baseQuery.filterIf(host == "my-host") {
case (t1, t2) => t2.host inSet Seq("x", "y", "z")
}
I've used query.filterIf there, but you could (if you prefer) write regular Scala:
val queryAllowingForMyHost =
if (host == "my-host") {
baseQuery.filter( /* extra conditions here */ )
} else {
baseQuery
}
Lastly, add any finishing touches to the query before running it:
val query = queryAllowingForMyHost.map { case (t1, t2) => t2.name }
That's the query you'd run.

Scala Slick joinLeft and combined conditions

I want to be able to create a query with Slick that let me filter left joins in a dynamic way
case class Player(
id: Long,
createdAt: DateTime,
lastModificationDate: DateTime,
name: String
)
class PlayerTable(tag: Tag) extends Table[Player](tag, "players") {
def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
def createdAt = column[DateTime]("createdAt")
def lastModificationDate = column[DateTime]("lastModificationDate")
def name = column[String]("name")
override def * : ProvenShape[Player] = (
id,
createdAt,
lastModificationDate,
updatedAt,
name
) <> (Player.tupled, Player.unapply)
}
case class PlayerGame(
id: Long,
createdAt: DateTime,
lastModificationDate: DateTime,
playerId: Long,
level: Int,
status: String
)
class PlayerGameTable(tag: Tag) extends Table[PlayerGame](tag, "player_games") {
def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
def createdAt = column[DateTime]("createdAt")
def lastModificationDate = column[DateTime]("lastModificationDate")
def playerId = column[Long]("playerId")
def level = column[Int]("level")
def status = column[String]("status")
override def * : ProvenShape[PlayerGame] = (
id,
createdAt,
lastModificationDate,
playerId,
level,
status
) <> (PlayerGame.tupled, PlayerGame.unapply)
}
I want to write a query like this with Slick, where the WHERE CLAUSE is dynamic. I wrote two examples
SELECT *
FROM players
LEFT JOIN player_games AS playerGamesOne ON players.id = playerGamesOne.playerId AND playerGamesOne.level = 1
LEFT JOIN player_games AS playerGamesTwo ON players.id = playerGamesTwo.playerId AND playerGamesTwo.level = 2
WHERE playerGamesOne.status LIKE 'gameOver'
OR playerGamesTWO.status LIKE 'gameOver'
SELECT *
FROM players
LEFT JOIN player_games AS playerGamesOne ON players.id = playerGamesOne.playerId AND playerGamesOne.level = 1
LEFT JOIN player_games AS playerGamesTwo ON players.id = playerGamesTwo.playerId AND playerGamesTwo.level = 2
WHERE playerGamesOne.status LIKE 'playing'
OR playerGamesTwo.status NOT LIKE 'gameOver'
I was trying something like this, but I get Rep[Option[PlayerGameTable]] as the parameter. Maybe there is a different way of doing something like this
val baseQuery = for {
((p, g1), g2) <- PlayerTable.playerQuery joinLeft
PlayerGameTable.playerGameQuery ON ((x, y) => x.id === y.playerId && y.level === 1) joinLeft
PlayerGameTable.playerGameQuery ON ((x, y) => x._1.id === y.playerId && y.level === 2)
} yield (p, g1, g2)
private def filterPlayerGames(gameStatus: String, playerGamesOneOpt: Option[PlayerGameTable], playerGamesTwoOpt: Option[PlayerGameTable]) = {
(gameStatus, playerGamesOneOpt, playerGamesOneOpt) match {
case (gameStatus: String, Some(playerGamesOne: PlayerGameTable), Some(playerGamesOne: PlayerGameTable)) if gameStatus == "gameOver" => playerGamesOne.status === "gameOver" || playerGamesTwo.status === "gameOver"
}
}
It is a complex question, if soemthing is not clear please let me know and I will try to clarify it
There are a couple of issues:
With multiple conditions, the underscore placeholder used within your ON clause would not work the way intended
_.level = something is an assignment, not a condition
Assuming PlayerTable.playerQuery is TableQuery[PlayerTable] and PlayerGameTable.playerGameQuery is TableQuery[PlayerGameTable], your baseQuery should look like this:
val baseQuery = for {
((p, g1), g2) <- PlayerTable.playerQuery joinLeft
PlayerGameTable.playerGameQuery on ((x, y) => x.id === y.playerId && y.level === 1) joinLeft
PlayerGameTable.playerGameQuery on ((x, y) => x._1.id === y.playerId && y.level === 2)
} yield (p, g1, g2)
It's not entirely clear to me how your filterPlayerGames method is going to handle dynamic conditions. Nor do I think any filtering wrapper method will be flexible enough to cover multiple conditions with arbitrary and/or/negation operators. I would suggest that you use the baseQuery for the necessary joins and build filtering queries on top of it, similar to something like below:
val query1 = baseQuery.filter{ case (_, g1, g2) =>
g1.filter(_.status === "gameOver").isDefined || g2.filter(_.status === "gameOver").isDefined
}
val query2 = baseQuery.filter{ case (_, g1, g2) =>
g1.filter(_.status === "playing").isDefined || g2.filter(_.status =!= "gameOver").isDefined
}
Note that with the left joins, g1 and g2 are of Option type, thus isDefined is applied for the or operation.
On a separate note, given that your filtering conditions are only on PlayerGameTable, it would probably be more efficient to perform filtering before the joins.

How to return compound types in Slick's Case-If-Then-Else

I just start dipping my toe into Slick.
In the coffee-supplier example, I try "Case If Then Else" like this:
val q = coffees.withFilter(_.price > 9.0).flatMap({ c =>
suppliers.withFilter(_.id == c.supID).map({ s =>
val t = c.name > s.name
Case If t Then { (c.name, s.name) } Else { (s.name, c.name) }
})
})
The compiler emits an error:
could not find implicit value for evidence parameter of type scala.slick.ast.TypedType[(scala.slick.lifted.Column[String], scala.slick.lifted.Column[String])]
Case If t Then { (c.name, s.name) } Else { (s.name, c.name) }
^
Much the same error apears for other compound types like List as well. I guess I'm on my own to define an implicit for the type in question but I'm not sure where to start.
Currently not support, I created a ticket: https://github.com/slick/slick/issues/866
Workaround: Write individual If Then constructs for each scalar value
Try:
val q = coffees.withFilter(_.price > 9.0).flatMap({ c =>
suppliers.withFilter(_.id == c.supID).map{ s =>
(Case If c.name > s.name Then c.name Else s.name,
Case If c.name > s.name Then s.name Else c.name)}})

Scala Slick 2 join on multiple fields?

how can do joins on multiple fields like in example beneath?
val ownerId = 1
val contactType = 1
...
val contact = for {
(t, c) <- ContactTypes leftJoin Contacts on (_.id === _.typeId && _.ownerId === ownerId)
if t.id === contactType
} yield (c.?, t)
How can I achieve this with Slick 2.0.1? Idelly I need slick to generate this kind of query
SELECT
x2."contact_id",
x2."type_id",
x2."owner_id",
x2."value",
x2."created_on",
x2."updated_on",
x3."id",
x3."type",
x3."model"
FROM
(
SELECT
x4."id" AS "id",
x4."type" AS "type",
x4."model" AS "model"
FROM
"contact_types" x4
)x3
LEFT OUTER JOIN(
SELECT
x5."created_on" AS "created_on",
x5."value" AS "value",
x5."contact_id" AS "contact_id",
x5."updated_on" AS "updated_on",
x5."type_id" AS "type_id",
x5."owner_id" AS "owner_id"
FROM
"contacts" x5
)x2 ON x3."id" = x2."type_id" AND x2.owner_id = 1
WHERE
(x3."id" = 3)
Please note ON x3."id" = x2."type_id" AND x2.owner_id = 16
Ok, so after digging through websites and source code I think I finally found the solution
leftJoin on() method accepts following parameter pred: (E1, E2) => T, so we simply can do like this
val contacts = for {
(t, c) <- ContactTypes leftJoin Contacts on ( (type, contact) => {
type.id === contact.typeId && contact.ownerId === ownerId
} )
} yield (c.?, t)
Which generated sql query as needed.