Skip to content

Commit 09d261b

Browse files
committed
Allow set GROUP BY with query builder
1 parent 2808e78 commit 09d261b

5 files changed

Lines changed: 216 additions & 0 deletions

File tree

src/Manipulation/Select.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
class Select extends Statement
2525
{
2626
use Traits\Join;
27+
use Traits\GroupBy;
2728
use Traits\Having;
2829
use Traits\OrderBy;
2930

@@ -527,6 +528,11 @@ public function sql() : string
527528
$this->hasFrom('WHERE');
528529
$sql .= $part . \PHP_EOL;
529530
}
531+
$part = $this->renderGroupBy();
532+
if ($part) {
533+
$this->hasFrom('GROUP BY');
534+
$sql .= $part . \PHP_EOL;
535+
}
530536
$part = $this->renderHaving();
531537
if ($part) {
532538
$this->hasFrom('HAVING');
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php declare(strict_types=1);
2+
/*
3+
* This file is part of Aplus Framework Database Library.
4+
*
5+
* (c) Natan Felles <natanfelles@gmail.com>
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
namespace Framework\Database\Manipulation\Traits;
11+
12+
use Closure;
13+
14+
/**
15+
* Trait GroupBy.
16+
*
17+
* @see https://mariadb.com/kb/en/group-by/
18+
*
19+
* @package database
20+
*/
21+
trait GroupBy
22+
{
23+
/**
24+
* Appends columns to the GROUP BY clause.
25+
*
26+
* @param Closure|string $column The column name or a subquery
27+
* @param Closure|string ...$columns Extra column names and/or subqueries
28+
*
29+
* @return static
30+
*/
31+
public function groupBy(Closure | string $column, Closure | string ...$columns) : static
32+
{
33+
return $this->addGroupBy($column, $columns, null);
34+
}
35+
36+
/**
37+
* Appends columns with the ASC direction to the GROUP BY clause.
38+
*
39+
* @param Closure|string $column The column name or a subquery
40+
* @param Closure|string ...$columns Extra column names and/or subqueries
41+
*
42+
* @return static
43+
*/
44+
public function groupByAsc(Closure | string $column, Closure | string ...$columns) : static
45+
{
46+
return $this->addGroupBy($column, $columns, 'ASC');
47+
}
48+
49+
/**
50+
* Appends columns with the DESC direction to the GROUP BY clause.
51+
*
52+
* @param Closure|string $column The column name or a subquery
53+
* @param Closure|string ...$columns Extra column names and/or subqueries
54+
*
55+
* @return static
56+
*/
57+
public function groupByDesc(Closure | string $column, Closure | string ...$columns) : static
58+
{
59+
return $this->addGroupBy($column, $columns, 'DESC');
60+
}
61+
62+
/**
63+
* Adds a GROUP BY expression.
64+
*
65+
* @param Closure|string $column The column name or a subquery
66+
* @param array<Closure|string> $columns Extra column names and/or subqueries
67+
* @param string|null $direction `ASC`, `DESC` or null for none
68+
*
69+
* @return static
70+
*/
71+
private function addGroupBy(Closure | string $column, array $columns, ?string $direction) : static
72+
{
73+
foreach ([$column, ...$columns] as $column) {
74+
$this->sql['group_by'][] = [
75+
'column' => $column,
76+
'direction' => $direction,
77+
];
78+
}
79+
return $this;
80+
}
81+
82+
/**
83+
* Renders the GROUP BY clause.
84+
*
85+
* @return string|null The GROUP BY clause or null if it was not set
86+
*/
87+
protected function renderGroupBy() : ?string
88+
{
89+
if ( ! isset($this->sql['group_by'])) {
90+
return null;
91+
}
92+
$expressions = [];
93+
foreach ($this->sql['group_by'] as $part) {
94+
$expression = $this->renderIdentifier($part['column']);
95+
if ($part['direction']) {
96+
$expression .= " {$part['direction']}";
97+
}
98+
$expressions[] = $expression;
99+
}
100+
$expressions = \implode(', ', $expressions);
101+
return " GROUP BY {$expressions}";
102+
}
103+
}

tests/Manipulation/SelectTest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,15 @@ public function testWhere() : void
272272
);
273273
}
274274

275+
public function testGroupBy() : void
276+
{
277+
$part = $this->selectAllFrom('t1');
278+
self::assertSame(
279+
$part . " GROUP BY `name` ASC, `id`\n",
280+
$this->select->groupByAsc('name')->groupBy('id')->sql()
281+
);
282+
}
283+
275284
public function testHaving() : void
276285
{
277286
$part = $this->selectAllFrom('t1');
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
/*
3+
* This file is part of Aplus Framework Database Library.
4+
*
5+
* (c) Natan Felles <natanfelles@gmail.com>
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
namespace Tests\Database\Manipulation\Traits;
11+
12+
use Framework\Database\Manipulation\Traits\GroupBy;
13+
use Tests\Database\Manipulation\StatementMock;
14+
15+
class GroupByMock extends StatementMock
16+
{
17+
use GroupBy {
18+
renderGroupBy as public;
19+
}
20+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
/*
3+
* This file is part of Aplus Framework Database Library.
4+
*
5+
* (c) Natan Felles <natanfelles@gmail.com>
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
namespace Tests\Database\Manipulation\Traits;
11+
12+
use Tests\Database\TestCase;
13+
14+
final class GroupByTest extends TestCase
15+
{
16+
protected GroupByMock $statement;
17+
18+
public function setup() : void
19+
{
20+
$this->statement = new GroupByMock(static::$database);
21+
}
22+
23+
public function testGroupBy() : void
24+
{
25+
self::assertNull($this->statement->renderGroupBy());
26+
$this->statement->groupBy('c1');
27+
self::assertSame(' GROUP BY `c1`', $this->statement->renderGroupBy());
28+
$this->statement->groupBy(static fn () => 'select c2');
29+
self::assertSame(' GROUP BY `c1`, (select c2)', $this->statement->renderGroupBy());
30+
$this->statement->groupBy(static fn () => 'select c3', 'c4');
31+
self::assertSame(
32+
' GROUP BY `c1`, (select c2), (select c3), `c4`',
33+
$this->statement->renderGroupBy()
34+
);
35+
}
36+
37+
public function testGroupByAsc() : void
38+
{
39+
$this->statement->groupByAsc('c1');
40+
self::assertSame(' GROUP BY `c1` ASC', $this->statement->renderGroupBy());
41+
$this->statement->groupByAsc('c2', 'c3');
42+
self::assertSame(
43+
' GROUP BY `c1` ASC, `c2` ASC, `c3` ASC',
44+
$this->statement->renderGroupBy()
45+
);
46+
}
47+
48+
public function testGroupByDesc() : void
49+
{
50+
$this->statement->groupByDesc('c1');
51+
self::assertSame(' GROUP BY `c1` DESC', $this->statement->renderGroupBy());
52+
$this->statement->groupByDesc('c2', 'c3');
53+
self::assertSame(
54+
' GROUP BY `c1` DESC, `c2` DESC, `c3` DESC',
55+
$this->statement->renderGroupBy()
56+
);
57+
}
58+
59+
public function testGroupByMixed() : void
60+
{
61+
$this->statement->groupBy('c1');
62+
$this->statement->groupByAsc('c2');
63+
$this->statement->groupByDesc('c3');
64+
$this->statement->groupBy('a', 'b');
65+
$this->statement->groupByAsc('c', 'D');
66+
$this->statement->groupByDesc('e', static fn () => 'select "f"');
67+
self::assertSame(
68+
' GROUP BY `c1`, `c2` ASC, `c3` DESC, `a`, `b`, `c` ASC, `D` ASC, `e` DESC, (select "f") DESC',
69+
$this->statement->renderGroupBy()
70+
);
71+
}
72+
73+
public function testInvalidExpressionDataType() : void
74+
{
75+
$this->expectException(\TypeError::class);
76+
$this->statement->groupBy([]); // @phpstan-ignore-line
77+
}
78+
}

0 commit comments

Comments
 (0)