Skip to content

Commit d830e6e

Browse files
authored
980 having clause (#1347)
* add `having` clause * add a test * add docs * enable the test for sqlite
1 parent 46b982b commit d830e6e

File tree

5 files changed

+100
-1
lines changed

5 files changed

+100
-1
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
.. _having:
2+
3+
having
4+
======
5+
6+
You can use the ``having`` clause with the following queries:
7+
8+
* :ref:`Select`
9+
10+
It is used in combination with the :ref:`group_by` clause, to filter the
11+
grouped rows.
12+
13+
The syntax is identical to the :ref:`where` clause.
14+
15+
-------------------------------------------------------------------------------
16+
17+
Example
18+
-------
19+
20+
In the following example, we get the number of albums per band (filtering out
21+
any bands with less than 2 albums).
22+
23+
.. hint:: You can run this query in the :ref:`playground`.
24+
25+
.. code-block:: python
26+
27+
>>> from piccolo.query.functions.aggregate import Count
28+
29+
>>> await Album.select(
30+
... Album.band.name.as_alias('band_name'),
31+
... Count()
32+
... ).group_by(
33+
... Album.band
34+
... ).having(
35+
... Count() >= 1
36+
... )
37+
38+
[
39+
{"band_name": "Pythonistas", "count": 2},
40+
]

docs/src/piccolo/query_clauses/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ by modifying the return values.
2525
./distinct
2626
./freeze
2727
./group_by
28+
./having
2829
./lock_rows
2930
./offset
3031
./on_conflict

piccolo/apps/playground/commands/run.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,15 @@ def populate():
299299
Album(
300300
{
301301
Album.name: "Awesome album 2",
302+
Album.recorded_at: recording_studio_1,
303+
Album.band: pythonistas,
304+
Album.release_date: datetime.date(year=2025, month=1, day=1),
305+
Album.awards: ["Grammy Award 2025"],
306+
}
307+
),
308+
Album(
309+
{
310+
Album.name: "Awesome album 3",
302311
Album.recorded_at: recording_studio_2,
303312
Album.band: rustaceans,
304313
Album.release_date: datetime.date(year=2022, month=2, day=2),

piccolo/query/methods/select.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ class Select(Query[TableInstance, list[dict[str, Any]]]):
161161
"output_delegate",
162162
"callback_delegate",
163163
"where_delegate",
164+
"having_delegate",
164165
"lock_rows_delegate",
165166
)
166167

@@ -186,6 +187,7 @@ def __init__(
186187
self.output_delegate = OutputDelegate()
187188
self.callback_delegate = CallbackDelegate()
188189
self.where_delegate = WhereDelegate()
190+
self.having_delegate = WhereDelegate()
189191
self.lock_rows_delegate = LockRowsDelegate()
190192

191193
self.columns(*columns_list)
@@ -477,6 +479,10 @@ def where(self: Self, *where: Union[Combinable, QueryString]) -> Self:
477479
self.where_delegate.where(*where)
478480
return self
479481

482+
def having(self: Self, *where: Union[Combinable, QueryString]) -> Self:
483+
self.having_delegate.where(*where)
484+
return self
485+
480486
async def batch(
481487
self,
482488
batch_size: Optional[int] = None,
@@ -569,13 +575,18 @@ def default_querystrings(self) -> Sequence[QueryString]:
569575

570576
select_joins = self._get_joins(self.columns_delegate.selected_columns)
571577
where_joins = self._get_joins(self.where_delegate.get_where_columns())
578+
having_joins = self._get_joins(
579+
self.having_delegate.get_where_columns()
580+
)
572581
order_by_joins = self._get_joins(
573582
self.order_by_delegate.get_order_by_columns()
574583
)
575584

576585
# Combine all joins, and remove duplicates
577586
joins: list[str] = list(
578-
OrderedDict.fromkeys(select_joins + where_joins + order_by_joins)
587+
OrderedDict.fromkeys(
588+
select_joins + where_joins + having_joins + order_by_joins
589+
)
579590
)
580591

581592
#######################################################################
@@ -627,6 +638,10 @@ def default_querystrings(self) -> Sequence[QueryString]:
627638
query += "{}"
628639
args.append(self.group_by_delegate._group_by.querystring)
629640

641+
if self.having_delegate._where:
642+
query += " HAVING {}"
643+
args.append(self.having_delegate._where.querystring)
644+
630645
if self.order_by_delegate._order_by.order_by_items:
631646
query += "{}"
632647
args.append(self.order_by_delegate._order_by.querystring)

tests/table/test_select.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from piccolo.query.methods.select import SelectRaw
1212
from piccolo.query.mixins import DistinctOnError
1313
from piccolo.table import Table, create_db_tables_sync, drop_db_tables_sync
14+
from piccolo.testing.test_case import AsyncTableTest
1415
from tests.base import (
1516
DBTestCase,
1617
engine_is,
@@ -1507,3 +1508,36 @@ def test_distinct_on_order_by_error(self):
15071508
Album.select().distinct(on=[Album.band]).order_by(
15081509
Album.release_date
15091510
).run_sync()
1511+
1512+
1513+
class TestHaving(AsyncTableTest):
1514+
tables = [Album]
1515+
1516+
async def test_having(self):
1517+
await Album.insert(
1518+
Album(
1519+
{
1520+
Album.band: "Pythonistas",
1521+
}
1522+
),
1523+
Album(
1524+
{
1525+
Album.band: "Pythonistas",
1526+
}
1527+
),
1528+
Album(
1529+
{
1530+
Album.band: "Rustaceans",
1531+
}
1532+
),
1533+
)
1534+
1535+
response = (
1536+
await Album.select(Album.band)
1537+
.group_by(Album.band)
1538+
.having(Count() >= 2)
1539+
.output(as_list=True)
1540+
)
1541+
1542+
self.assertIn("Pythonistas", response)
1543+
self.assertNotIn("Rustaceans", response)

0 commit comments

Comments
 (0)