Skip to content

Commit d592970

Browse files
committed
Add GROUP BY and HAVING
1 parent 55ed243 commit d592970

File tree

4 files changed

+128
-25
lines changed

4 files changed

+128
-25
lines changed

README.md

+15-6
Original file line numberDiff line numberDiff line change
@@ -406,9 +406,6 @@ messages in your log.
406406
**Notice:** Don't worry when you were already using the v1.2 `Criteria` class. It is kept for compatibility
407407
in the 1.x versions (`Criteria` now inherits from `Query`). Starting with v2.0, this interface will be removed.
408408

409-
**Disclaimer:** The Query API cannot yet produce `GROUP BY` clauses as they are more complex to build.
410-
It will be added later.
411-
412409
## Creating a Query
413410
Two ways exist: Creating a `Query` object from the `Database` object, or alternatively from the `DAO`
414411
object:
@@ -603,6 +600,20 @@ $restrictions = Restrictions::eq('name', 'Jane Doe');
603600
$dao->createQuery()->add($restrictions)->delete();
604601
```
605602

603+
## GROUP BY and HAVING clauses
604+
The Query API allows to define grouping result sets and restricting the returned result with the HAVING clause:
605+
606+
```
607+
// List the number of books that authors published whose names begin with 'John'
608+
$bookQuery
609+
->setColumns(Projections::property('author'), Projections::rowCount('cnt'))
610+
->addGroupBy(Projections::property('author'))
611+
->addHaving(Restrictions::like('author', 'John%'))
612+
->list();
613+
```
614+
615+
Please notice that using `->count()` on such a query might produce unexpected results. This is still an unresolved issue.
616+
606617
## Useful methods
607618
You might want to make use of some methods that will ease your code writing:
608619

@@ -626,10 +637,8 @@ created the `Query` object. It is self-contained.
626637

627638
However, some limitations exist:
628639

629-
' Query API supports basic use cases so far (searching objects with basic restrictions).
640+
* Query API supports basic use cases so far (searching objects with basic restrictions).
630641
* Only MySQL / MariaDB SQL dialect is produced (but can be extended to other dialects easily when you stick to the API).
631-
* GROUP BY clauses are not implemented yet
632-
* Multiple projections are not yet supported (such as `SELECT MAX(name), MIN(name) FROM #__users`) - will be extended.
633642
* A few of the limitations may be ovecome by using the `SqlExpression` and `SqlProjection` classes:
634643

635644
```

src/TgDatabase/Criterion/QueryImpl.php

+65-16
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@ public function __construct($database, $tableName, $resultClassName = NULL, $ali
1717
$this->tableName = $tableName;
1818
$this->resultClassName = $resultClassName;
1919
$this->alias = $alias;
20-
$this->projections = array();
20+
$this->columns = array();
2121
$this->subqueries = array();
22+
$this->groupBy = array();
23+
$this->having = array();
2224
$this->criterions = array();
2325
$this->orders = array();
2426
$this->firstResult = -1;
@@ -30,8 +32,10 @@ public function __construct($database, $tableName, $resultClassName = NULL, $ali
3032
*/
3133
public function clone() {
3234
$rc = new QueryImpl($this->database, $this->tableName, $this->resultClassName, $this->alias);
33-
$rc->projections = $this->projections;
35+
$rc->columns = $this->columns;
3436
$rc->subqueries = $this->subqueries;
37+
$rc->groupBy = $this->groupBy;
38+
$rc->having = $this->having;
3539
$rc->criterions = $this->criterions;
3640
$rc->orders = $this->orders;
3741
$rc->firstResult = -1;
@@ -70,29 +74,48 @@ public function addOrder(Order ...$orders) {
7074
* Attention! This class removes any result class name from the query. Use #setResultClass() after calling.
7175
* @deprecated Use #setColumns() instead
7276
*/
73-
public function setProjection(Expression ...$components) {
74-
return call_user_func_array(array($this, 'setColumns'), $components);
77+
public function setProjection(Expression ...$expressions) {
78+
return call_user_func_array(array($this, 'setColumns'), $expressions);
7579
}
7680

7781
/**
7882
* Set select columns for the query.
7983
* Attention! This class removes any result class name from the query. Use #setResultClass() after calling.
8084
*/
81-
public function setColumns(Expression ...$components) {
82-
$this->projections = array();
83-
return call_user_func_array(array($this, 'addColumns'), $components);
85+
public function setColumns(Expression ...$expressions) {
86+
$this->columns = array();
87+
return call_user_func_array(array($this, 'addColumns'), $expressions);
8488
}
8589

8690
/**
8791
* Add select columns for the query.
8892
*/
89-
public function addColumns(Expression ...$components) {
90-
foreach ($components AS $c) {
91-
if ($c != NULL) $this->projections[] = $c;
93+
public function addColumns(Expression ...$expressions) {
94+
foreach ($expressions AS $c) {
95+
if ($c != NULL) $this->columns[] = $c;
9296
}
9397
return $this;
9498
}
9599

100+
/**
101+
* Add group by columns for the query.
102+
*/
103+
public function addGroupBy(Expression ...$expressions) {
104+
foreach ($expressions AS $c) {
105+
if ($c != NULL) $this->groupBy[] = $c;
106+
}
107+
return $this;
108+
}
109+
110+
/**
111+
* Add a restriction to constrain the group by result.
112+
* @return Query - this query for method chaining.
113+
*/
114+
public function addHaving(Criterion ...$criterions) {
115+
foreach ($criterions AS $criterion) $this->having[] = $criterion;
116+
return $this;
117+
}
118+
96119
/**
97120
* Set the index of the first result to be retrieved.
98121
* @return Query - this query for method chaining.
@@ -215,7 +238,7 @@ public function delete($throwException = FALSE) {
215238
}
216239

217240
public function getSelectSql() {
218-
// SELECT projections
241+
// SELECT columns
219242
$rc = 'SELECT '.$this->getSelectClause();
220243

221244
// FROM table
@@ -227,10 +250,16 @@ public function getSelectSql() {
227250
$rc .= ' '.trim($join);
228251
}
229252

230-
// GROUP BY projection not implemented yet
253+
// GROUP BY
231254
$group = $this->getGroupByClause();
232255
if ($group != NULL) {
233-
$rc .= ' '.$group;
256+
$rc .= ' GROUP BY '.$group;
257+
258+
// HAVING
259+
$having = $this->getHavingClause();
260+
if ($having != NULL) {
261+
$rc .= ' HAVING '.$having;
262+
}
234263
}
235264

236265
// WHERE clauses
@@ -298,9 +327,9 @@ public function toSqlString() {
298327

299328
public function getSelectClause() {
300329
$rc = '';
301-
if (($this->projections != NULL) && (count($this->projections) > 0)) {
330+
if (($this->columns != NULL) && (count($this->columns) > 0)) {
302331
$sql = array();
303-
foreach ($this->projections AS $p) {
332+
foreach ($this->columns AS $p) {
304333
$sql[] = $p->toSqlString($this, $this);
305334
}
306335
$rc .= implode(', ', $sql);
@@ -330,7 +359,27 @@ public function getJoinClause() {
330359
}
331360

332361
public function getGroupByClause() {
333-
return NULL;
362+
$rc = NULL;
363+
if (($this->groupBy != NULL) && (count($this->groupBy) > 0)) {
364+
$sql = array();
365+
foreach ($this->groupBy AS $p) {
366+
$sql[] = $p->toSqlString($this, $this);
367+
}
368+
$rc = implode(', ', $sql);
369+
}
370+
return $rc;
371+
}
372+
373+
public function getHavingClause() {
374+
$rc = NULL;
375+
if (count($this->having) > 0) {
376+
foreach ($this->having AS $criterion) {
377+
if ($rc != NULL) $rc .= ' AND ';
378+
else $rc = '';
379+
$rc .= '('.$criterion->toSqlString($this, $this).')';
380+
}
381+
}
382+
return $rc;
334383
}
335384

336385
public function getWhereClause() {

src/TgDatabase/Query.php

+23-3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ interface Query {
1212
/**
1313
* Resets the result class.
1414
* Useful when using #setColumns() as this method erases the result class.
15+
* @return Query - this query for method chaining.
1516
*/
1617
public function setResultClass(string $name);
1718

@@ -23,25 +24,41 @@ public function add(Criterion ...$criterion);
2324

2425
/**
2526
* Add an ordering to the result set.
27+
* @return Query - this query for method chaining.
2628
*/
2729
public function addOrder(Order ...$order);
2830

2931
/**
3032
* Add select columns for the query.
33+
* @return Query - this query for method chaining.
3134
*/
32-
public function addColumns(Expression ...$components);
35+
public function addColumns(Expression ...$expressions);
3336

3437
/**
3538
* Set select columns for the query.
3639
* Attention! This class removes any result class name from the query. Use #setResultClass() after calling.
40+
* @return Query - this query for method chaining.
3741
*/
38-
public function setColumns(Expression ...$components);
42+
public function setColumns(Expression ...$expressions);
3943

4044
/**
4145
* Add projections for the query.
46+
* @return Query - this query for method chaining.
4247
* @deprecated Use #setColumns()
4348
*/
44-
public function setProjection(Expression ...$components);
49+
public function setProjection(Expression ...$expressions);
50+
51+
/**
52+
* Add group by column
53+
* @return Query - this query for method chaining.
54+
*/
55+
public function addGroupBy(Expression ...$expressions);
56+
57+
/**
58+
* Add a restriction to constrain the group by result.
59+
* @return Query - this query for method chaining.
60+
*/
61+
public function addHaving(Criterion ...$criterions);
4562

4663
/**
4764
* Set the index of the first result to be retrieved.
@@ -52,6 +69,7 @@ public function setFirstResult(int $firstResult);
5269
/**
5370
* Set a limit upon the number of rows to be retrieved.
5471
* @return Query - this query for method chaining.
72+
* @return Query - this query for method chaining.
5573
*/
5674
public function setMaxResults(int $maxResults);
5775

@@ -68,11 +86,13 @@ public function createCriteria($tableName, $alias, $joinCriterion);
6886

6987
/**
7088
* Add a new join query.
89+
* @return Query - this query for method chaining.
7190
*/
7291
public function addJoinedQuery(Query $query, $joinCriterion);
7392

7493
/**
7594
* Add a new join query.
95+
* @return Query - this query for method chaining.
7696
* @deprecated Use #addJoinedQuery instead
7797
*/
7898
public function addCriteria(Query $query, $joinCriterion);

tests/TgDatabase/Criterion/QueryImplTest.php

+25
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,31 @@ public function testDeleteWhereSql(): void {
156156
}
157157
}
158158

159+
public function testGroupBySql(): void {
160+
$database = TestHelper::getDatabase();
161+
if ($database != NULL) {
162+
$query = new QueryImpl($database, 'dual');
163+
$query
164+
->addColumns(Projections::property('attr1'), Projections::rowCount('cnt'))
165+
->add(Restrictions::eq('attr3', 'value3'))
166+
->addGroupBy(Projections::property('attr1'));
167+
$this->assertEquals("SELECT `attr1`, COUNT(*) AS `cnt` FROM `dual` GROUP BY `attr1` WHERE (`attr3` = 'value3')", $query->getSelectSql());
168+
}
169+
}
170+
171+
public function testHavingSql(): void {
172+
$database = TestHelper::getDatabase();
173+
if ($database != NULL) {
174+
$query = new QueryImpl($database, 'dual');
175+
$query
176+
->addColumns(Projections::property('attr1'), Projections::rowCount('cnt'))
177+
->add(Restrictions::eq('attr3', 'value3'))
178+
->addGroupBy(Projections::property('attr1'))
179+
->addHaving(Restrictions::eq('attr1', 'value1'));
180+
$this->assertEquals("SELECT `attr1`, COUNT(*) AS `cnt` FROM `dual` GROUP BY `attr1` HAVING (`attr1` = 'value1') WHERE (`attr3` = 'value3')", $query->getSelectSql());
181+
}
182+
}
183+
159184
public function testCount(): void {
160185
$database = TestHelper::getDatabase();
161186
if ($database != NULL) {

0 commit comments

Comments
 (0)