Skip to content

Commit 7c64742

Browse files
feat: add support for ORDER BY clause for array_agg() (#267)
1 parent 3be452a commit 7c64742

File tree

8 files changed

+115
-122
lines changed

8 files changed

+115
-122
lines changed

src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ArrayAgg.php

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,36 @@
44

55
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
66

7+
use Doctrine\ORM\Query\Parser;
8+
use Doctrine\ORM\Query\SqlWalker;
9+
710
/**
811
* Implementation of PostgreSql ARRAY_AGG().
912
*
10-
* @see https://www.postgresql.org/docs/9.5/functions-aggregate.html
13+
* @see https://www.postgresql.org/docs/17/functions-aggregate.html
1114
* @since 1.4
1215
*
1316
* @author Martin Georgiev <martin.georgiev@gmail.com>
1417
*/
15-
class ArrayAgg extends BaseFunction
18+
class ArrayAgg extends BaseOrderableFunction
1619
{
1720
protected function customiseFunction(): void
1821
{
19-
$this->setFunctionPrototype('array_agg(%s)');
20-
$this->addNodeMapping('StringPrimary');
22+
$this->setFunctionPrototype('array_agg(%s%s)');
23+
}
24+
25+
protected function parseFunction(Parser $parser): void
26+
{
27+
$this->expression = $parser->StringPrimary();
28+
}
29+
30+
public function getSql(SqlWalker $sqlWalker): string
31+
{
32+
$dispatched = [
33+
$this->expression->dispatch($sqlWalker),
34+
$this->getOptionalOrderByClause($sqlWalker),
35+
];
36+
37+
return \vsprintf($this->functionPrototype, $dispatched);
2138
}
2239
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
6+
7+
use Doctrine\ORM\Query\AST\Node;
8+
use Doctrine\ORM\Query\AST\OrderByClause;
9+
use Doctrine\ORM\Query\Lexer;
10+
use Doctrine\ORM\Query\Parser;
11+
use Doctrine\ORM\Query\SqlWalker;
12+
use Doctrine\ORM\Query\TokenType;
13+
use MartinGeorgiev\Utils\DoctrineOrm;
14+
15+
abstract class BaseOrderableFunction extends BaseFunction
16+
{
17+
protected Node $expression;
18+
protected ?OrderByClause $orderByClause = null;
19+
20+
public function parse(Parser $parser): void
21+
{
22+
$shouldUseLexer = DoctrineOrm::isPre219();
23+
24+
$this->customiseFunction();
25+
26+
$parser->match($shouldUseLexer ? Lexer::T_IDENTIFIER : TokenType::T_IDENTIFIER);
27+
$parser->match($shouldUseLexer ? Lexer::T_OPEN_PARENTHESIS : TokenType::T_OPEN_PARENTHESIS);
28+
29+
$this->parseFunction($parser);
30+
31+
$lexer = $parser->getLexer();
32+
if ($lexer->isNextToken($shouldUseLexer ? Lexer::T_ORDER : TokenType::T_ORDER)) {
33+
$this->orderByClause = $parser->OrderByClause();
34+
}
35+
36+
$parser->match($shouldUseLexer ? Lexer::T_CLOSE_PARENTHESIS : TokenType::T_CLOSE_PARENTHESIS);
37+
}
38+
39+
abstract protected function parseFunction(Parser $parser): void;
40+
41+
protected function getOptionalOrderByClause(SqlWalker $sqlWalker): string
42+
{
43+
return $this->orderByClause instanceof OrderByClause ? $this->orderByClause->dispatch($sqlWalker) : '';
44+
}
45+
}

src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StringAgg.php

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
66

77
use Doctrine\ORM\Query\AST\Node;
8-
use Doctrine\ORM\Query\AST\OrderByClause;
98
use Doctrine\ORM\Query\Lexer;
109
use Doctrine\ORM\Query\Parser;
1110
use Doctrine\ORM\Query\SqlWalker;
@@ -20,54 +19,40 @@
2019
*
2120
* @author Martin Georgiev <martin.georgiev@gmail.com>
2221
*/
23-
class StringAgg extends BaseFunction
22+
class StringAgg extends BaseOrderableFunction
2423
{
2524
private bool $isDistinct = false;
26-
27-
private Node $expression;
28-
2925
private Node $delimiter;
3026

31-
private OrderByClause $orderByClause;
32-
3327
protected function customiseFunction(): void
3428
{
3529
$this->setFunctionPrototype('string_agg(%s%s, %s%s)');
3630
}
3731

38-
public function parse(Parser $parser): void
32+
protected function parseFunction(Parser $parser): void
3933
{
4034
$shouldUseLexer = DoctrineOrm::isPre219();
41-
42-
$this->customiseFunction();
43-
44-
$parser->match($shouldUseLexer ? Lexer::T_IDENTIFIER : TokenType::T_IDENTIFIER);
45-
$parser->match($shouldUseLexer ? Lexer::T_OPEN_PARENTHESIS : TokenType::T_OPEN_PARENTHESIS);
46-
4735
$lexer = $parser->getLexer();
36+
4837
if ($lexer->isNextToken($shouldUseLexer ? Lexer::T_DISTINCT : TokenType::T_DISTINCT)) {
4938
$parser->match($shouldUseLexer ? Lexer::T_DISTINCT : TokenType::T_DISTINCT);
5039
$this->isDistinct = true;
5140
}
5241

5342
$this->expression = $parser->StringPrimary();
54-
$parser->match($shouldUseLexer ? Lexer::T_COMMA : TokenType::T_COMMA);
55-
$this->delimiter = $parser->StringPrimary();
5643

57-
if ($lexer->isNextToken($shouldUseLexer ? Lexer::T_ORDER : TokenType::T_ORDER)) {
58-
$this->orderByClause = $parser->OrderByClause();
59-
}
44+
$parser->match($shouldUseLexer ? Lexer::T_COMMA : TokenType::T_COMMA);
6045

61-
$parser->match($shouldUseLexer ? Lexer::T_CLOSE_PARENTHESIS : TokenType::T_CLOSE_PARENTHESIS);
46+
$this->delimiter = $parser->StringPrimary();
6247
}
6348

6449
public function getSql(SqlWalker $sqlWalker): string
6550
{
6651
$dispatched = [
67-
$this->isDistinct ? 'distinct ' : '',
52+
$this->isDistinct ? 'DISTINCT ' : '',
6853
$this->expression->dispatch($sqlWalker),
6954
$this->delimiter->dispatch($sqlWalker),
70-
isset($this->orderByClause) ? $this->orderByClause->dispatch($sqlWalker) : '',
55+
$this->getOptionalOrderByClause($sqlWalker),
7156
];
7257

7358
return \vsprintf($this->functionPrototype, $dispatched);

tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ArrayAggTest.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,29 @@ protected function getStringFunctions(): array
1919
protected function getExpectedSqlStatements(): array
2020
{
2121
return [
22+
// Basic usage
23+
'SELECT array_agg(c0_.text1) AS sclr_0 FROM ContainsTexts c0_',
24+
// With concatenation
2225
'SELECT array_agg(c0_.text1 || c0_.text2) AS sclr_0 FROM ContainsTexts c0_',
26+
// With ORDER BY
27+
'SELECT array_agg(c0_.text1 ORDER BY c0_.text1 ASC) AS sclr_0 FROM ContainsTexts c0_',
28+
'SELECT array_agg(c0_.text1 ORDER BY c0_.text1 DESC) AS sclr_0 FROM ContainsTexts c0_',
29+
// With concatenation and ORDER BY
30+
'SELECT array_agg(c0_.text1 || c0_.text2 ORDER BY c0_.text1 ASC) AS sclr_0 FROM ContainsTexts c0_',
31+
// With multiple ORDER BY columns
32+
'SELECT array_agg(c0_.text1 ORDER BY c0_.text1 ASC, c0_.text2 DESC) AS sclr_0 FROM ContainsTexts c0_',
2333
];
2434
}
2535

2636
protected function getDqlStatements(): array
2737
{
2838
return [
39+
\sprintf('SELECT ARRAY_AGG(e.text1) FROM %s e', ContainsTexts::class),
2940
\sprintf('SELECT ARRAY_AGG(CONCAT(e.text1, e.text2)) FROM %s e', ContainsTexts::class),
41+
\sprintf('SELECT ARRAY_AGG(e.text1 ORDER BY e.text1) FROM %s e', ContainsTexts::class),
42+
\sprintf('SELECT ARRAY_AGG(e.text1 ORDER BY e.text1 DESC) FROM %s e', ContainsTexts::class),
43+
\sprintf('SELECT ARRAY_AGG(CONCAT(e.text1, e.text2) ORDER BY e.text1) FROM %s e', ContainsTexts::class),
44+
\sprintf('SELECT ARRAY_AGG(e.text1 ORDER BY e.text1 ASC, e.text2 DESC) FROM %s e', ContainsTexts::class),
3045
];
3146
}
3247
}

tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StringAggTest.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,41 @@ protected function getStringFunctions(): array
1919
protected function getExpectedSqlStatements(): array
2020
{
2121
return [
22+
// Basic usage
23+
"SELECT string_agg(c0_.text1, ',') AS sclr_0 FROM ContainsTexts c0_",
24+
// With concatenation
2225
"SELECT string_agg(c0_.text1 || c0_.text2, ',') AS sclr_0 FROM ContainsTexts c0_",
26+
// With DISTINCT
27+
"SELECT string_agg(DISTINCT c0_.text1, ',') AS sclr_0 FROM ContainsTexts c0_",
28+
// With DISTINCT and concatenation
29+
"SELECT string_agg(DISTINCT c0_.text1 || c0_.text2, ',') AS sclr_0 FROM ContainsTexts c0_",
30+
// With ORDER BY
31+
"SELECT string_agg(c0_.text1, ',' ORDER BY c0_.text1 ASC) AS sclr_0 FROM ContainsTexts c0_",
32+
"SELECT string_agg(c0_.text1, ',' ORDER BY c0_.text1 DESC) AS sclr_0 FROM ContainsTexts c0_",
33+
// With DISTINCT and ORDER BY
34+
"SELECT string_agg(DISTINCT c0_.text1, ',' ORDER BY c0_.text1 ASC) AS sclr_0 FROM ContainsTexts c0_",
35+
// With concatenation, DISTINCT and ORDER BY
36+
"SELECT string_agg(DISTINCT c0_.text1 || c0_.text2, ',' ORDER BY c0_.text1 ASC) AS sclr_0 FROM ContainsTexts c0_",
37+
// With multiple ORDER BY columns
38+
"SELECT string_agg(c0_.text1, ',' ORDER BY c0_.text1 ASC, c0_.text2 DESC) AS sclr_0 FROM ContainsTexts c0_",
39+
// With different delimiter
40+
"SELECT string_agg(c0_.text1, ' | ') AS sclr_0 FROM ContainsTexts c0_",
2341
];
2442
}
2543

2644
protected function getDqlStatements(): array
2745
{
2846
return [
47+
\sprintf("SELECT STRING_AGG(e.text1, ',') FROM %s e", ContainsTexts::class),
2948
\sprintf("SELECT STRING_AGG(CONCAT(e.text1, e.text2), ',') FROM %s e", ContainsTexts::class),
49+
\sprintf("SELECT STRING_AGG(DISTINCT e.text1, ',') FROM %s e", ContainsTexts::class),
50+
\sprintf("SELECT STRING_AGG(DISTINCT CONCAT(e.text1, e.text2), ',') FROM %s e", ContainsTexts::class),
51+
\sprintf("SELECT STRING_AGG(e.text1, ',' ORDER BY e.text1) FROM %s e", ContainsTexts::class),
52+
\sprintf("SELECT STRING_AGG(e.text1, ',' ORDER BY e.text1 DESC) FROM %s e", ContainsTexts::class),
53+
\sprintf("SELECT STRING_AGG(DISTINCT e.text1, ',' ORDER BY e.text1) FROM %s e", ContainsTexts::class),
54+
\sprintf("SELECT STRING_AGG(DISTINCT CONCAT(e.text1, e.text2), ',' ORDER BY e.text1) FROM %s e", ContainsTexts::class),
55+
\sprintf("SELECT STRING_AGG(e.text1, ',' ORDER BY e.text1 ASC, e.text2 DESC) FROM %s e", ContainsTexts::class),
56+
\sprintf("SELECT STRING_AGG(e.text1, ' | ') FROM %s e", ContainsTexts::class),
3057
];
3158
}
3259
}

tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StringAggWithDistinctAndOrderByTest.php

Lines changed: 0 additions & 32 deletions
This file was deleted.

tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StringAggWithDistinctTest.php

Lines changed: 0 additions & 32 deletions
This file was deleted.

tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/StringAggWithOrderByTest.php

Lines changed: 0 additions & 32 deletions
This file was deleted.

0 commit comments

Comments
 (0)