Skip to content

Commit 4213f04

Browse files
authored
Allow multiple keys in GroupBy (#56)
This pull request extends the GroupBy class so it can use multiple fields as keys. I've also added two tests to make sure it works fine and that I did not break filtering (HAVING) feature. Fixes #52
1 parent df1b6ff commit 4213f04

File tree

4 files changed

+134
-6
lines changed

4 files changed

+134
-6
lines changed

docs/reference.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1108,6 +1108,81 @@ Purchase.select
11081108
11091109
11101110
1111+
### Select.groupBy.multipleKeys
1112+
1113+
1114+
1115+
```scala
1116+
Purchase.select.groupBy(x => (x.shippingInfoId, x.productId))(_.sumBy(_.total))
1117+
```
1118+
1119+
1120+
*
1121+
```sql
1122+
SELECT
1123+
purchase0.shipping_info_id AS res_0_0,
1124+
purchase0.product_id AS res_0_1,
1125+
SUM(purchase0.total) AS res_1
1126+
FROM
1127+
purchase purchase0
1128+
GROUP BY
1129+
purchase0.shipping_info_id,
1130+
purchase0.product_id
1131+
```
1132+
1133+
1134+
1135+
*
1136+
```scala
1137+
Seq(
1138+
((1, 1), 888.0),
1139+
((1, 2), 900.0),
1140+
((1, 3), 15.7),
1141+
((2, 4), 493.8),
1142+
((2, 5), 10000.0),
1143+
((3, 1), 44.4),
1144+
((3, 6), 1.3)
1145+
)
1146+
```
1147+
1148+
1149+
1150+
### Select.groupBy.multipleKeysHaving
1151+
1152+
1153+
1154+
```scala
1155+
Purchase.select
1156+
.groupBy(x => (x.shippingInfoId, x.productId))(_.sumBy(_.total))
1157+
.filter(_._2 > 10)
1158+
.filter(_._2 < 100)
1159+
```
1160+
1161+
1162+
*
1163+
```sql
1164+
SELECT
1165+
purchase0.shipping_info_id AS res_0_0,
1166+
purchase0.product_id AS res_0_1,
1167+
SUM(purchase0.total) AS res_1
1168+
FROM
1169+
purchase purchase0
1170+
GROUP BY
1171+
purchase0.shipping_info_id,
1172+
purchase0.product_id
1173+
HAVING
1174+
(SUM(purchase0.total) > ?) AND (SUM(purchase0.total) < ?)
1175+
```
1176+
1177+
1178+
1179+
*
1180+
```scala
1181+
Seq(((1, 3), 15.7), ((3, 1), 44.4))
1182+
```
1183+
1184+
1185+
11111186
### Select.distinct.nondistinct
11121187
11131188
Normal queries can allow duplicates in the returned row values, as seen below.

scalasql/query/src/Model.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ object Nulls {
4040
/**
4141
* Models a SQL `GROUP BY` clause
4242
*/
43-
case class GroupBy(key: Expr[?], select: () => Select[?, ?], having: Seq[Expr[?]])
43+
case class GroupBy(keys: Seq[Expr[?]], select: () => Select[?, ?], having: Seq[Expr[?]])
4444

4545
/**
4646
* Models a SQL `JOIN` clause

scalasql/query/src/SimpleSelect.scala

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -170,14 +170,14 @@ class SimpleSelect[Q, R](
170170
def groupBy[K, V, R1, R2](groupKey: Q => K)(
171171
groupAggregate: Aggregatable.Proxy[Q] => V
172172
)(implicit qrk: Queryable.Row[K, R1], qrv: Queryable.Row[V, R2]): Select[(K, V), (R1, R2)] = {
173-
val groupKeyValue = groupKey(expr)
174-
val Seq(groupKeyExpr) = qrk.walkExprs(groupKeyValue)
175-
val newExpr = (groupKeyValue, groupAggregate(new Aggregatable.Proxy[Q](this.expr)))
173+
val groupKeysValue = groupKey(expr)
174+
val groupKeysExpr = qrk.walkExprs(groupKeysValue)
175+
val newExpr = (groupKeysValue, groupAggregate(new Aggregatable.Proxy[Q](this.expr)))
176176

177177
// Weird hack to store the post-groupby `Select` as part of the `GroupBy`
178178
// object, because `.flatMap` sometimes need us to roll back any subsequent
179179
// `.map`s (???)
180-
lazy val groupByOpt: Option[GroupBy] = Some(GroupBy(groupKeyExpr, () => res, Nil))
180+
lazy val groupByOpt: Option[GroupBy] = Some(GroupBy(groupKeysExpr, () => res, Nil))
181181
lazy val res =
182182
if (groupBy0.isEmpty) this.copy(expr = newExpr, groupBy0 = groupByOpt)
183183
else
@@ -265,7 +265,9 @@ object SimpleSelect {
265265

266266
lazy val groupByOpt = SqlStr.flatten(SqlStr.opt(query.groupBy0) { groupBy =>
267267
val havingOpt = ExprsToSql.booleanExprs(sql" HAVING ", groupBy.having)
268-
sql" GROUP BY ${groupBy.key}${havingOpt}"
268+
val groupByJoined =
269+
SqlStr.join(groupBy.keys.map(x => Renderable.renderSql(x)(context)), SqlStr.commaSep)
270+
sql" GROUP BY ${groupByJoined}${havingOpt}"
269271
})
270272

271273
def render(liveExprs0: LiveExprs) = {

scalasql/test/src/query/SelectTests.scala

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,57 @@ trait SelectTests extends ScalaSqlSuite {
352352
value = Seq((1, 888.0), (5, 10000.0)),
353353
normalize = (x: Seq[(Int, Double)]) => x.sorted
354354
)
355+
356+
test("multipleKeys") - checker(
357+
query = Text {
358+
Purchase.select.groupBy(x => (x.shippingInfoId, x.productId))(_.sumBy(_.total))
359+
},
360+
sql = """
361+
SELECT
362+
purchase0.shipping_info_id AS res_0_0,
363+
purchase0.product_id AS res_0_1,
364+
SUM(purchase0.total) AS res_1
365+
FROM
366+
purchase purchase0
367+
GROUP BY
368+
purchase0.shipping_info_id,
369+
purchase0.product_id
370+
""",
371+
value = Seq(
372+
((1, 1), 888.0),
373+
((1, 2), 900.0),
374+
((1, 3), 15.7),
375+
((2, 4), 493.8),
376+
((2, 5), 10000.0),
377+
((3, 1), 44.4),
378+
((3, 6), 1.3)
379+
),
380+
normalize = (x: Seq[((Int, Int), Double)]) => x.sorted
381+
)
382+
383+
test("multipleKeysHaving") - checker(
384+
query = Text {
385+
Purchase.select
386+
.groupBy(x => (x.shippingInfoId, x.productId))(_.sumBy(_.total))
387+
.filter(_._2 > 10)
388+
.filter(_._2 < 100)
389+
},
390+
sql = """
391+
SELECT
392+
purchase0.shipping_info_id AS res_0_0,
393+
purchase0.product_id AS res_0_1,
394+
SUM(purchase0.total) AS res_1
395+
FROM
396+
purchase purchase0
397+
GROUP BY
398+
purchase0.shipping_info_id,
399+
purchase0.product_id
400+
HAVING
401+
(SUM(purchase0.total) > ?) AND (SUM(purchase0.total) < ?)
402+
""",
403+
value = Seq(((1, 3), 15.7), ((3, 1), 44.4)),
404+
normalize = (x: Seq[((Int, Int), Double)]) => x.sorted
405+
)
355406
}
356407

357408
test("distinct") {

0 commit comments

Comments
 (0)