Skip to content

Commit 3c46021

Browse files
feat: add support for DISTINCT clause to array_agg() (#316)
1 parent 556df87 commit 3c46021

File tree

6 files changed

+117
-73
lines changed

6 files changed

+117
-73
lines changed

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

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,13 @@
44

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

7+
use Doctrine\ORM\Query\Lexer;
78
use Doctrine\ORM\Query\Parser;
89
use Doctrine\ORM\Query\SqlWalker;
10+
use Doctrine\ORM\Query\TokenType;
11+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Traits\DistinctableTrait;
12+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Traits\OrderableTrait;
13+
use MartinGeorgiev\Utils\DoctrineOrm;
914

1015
/**
1116
* Implementation of PostgreSQL ARRAY_AGG().
@@ -15,21 +20,38 @@
1520
*
1621
* @author Martin Georgiev <martin.georgiev@gmail.com>
1722
*/
18-
class ArrayAgg extends BaseOrderableFunction
23+
class ArrayAgg extends BaseFunction
1924
{
25+
use OrderableTrait;
26+
use DistinctableTrait;
27+
2028
protected function customizeFunction(): void
2129
{
22-
$this->setFunctionPrototype('array_agg(%s%s)');
30+
$this->setFunctionPrototype('array_agg(%s%s%s)');
31+
$this->addNodeMapping('StringPrimary');
2332
}
2433

25-
protected function parseFunction(Parser $parser): void
34+
public function parse(Parser $parser): void
2635
{
36+
$shouldUseLexer = DoctrineOrm::isPre219();
37+
38+
$this->customizeFunction();
39+
40+
$parser->match($shouldUseLexer ? Lexer::T_IDENTIFIER : TokenType::T_IDENTIFIER);
41+
$parser->match($shouldUseLexer ? Lexer::T_OPEN_PARENTHESIS : TokenType::T_OPEN_PARENTHESIS);
42+
43+
$this->parseDistinctClause($parser);
2744
$this->expression = $parser->StringPrimary();
45+
46+
$this->parseOrderByClause($parser);
47+
48+
$parser->match($shouldUseLexer ? Lexer::T_CLOSE_PARENTHESIS : TokenType::T_CLOSE_PARENTHESIS);
2849
}
2950

3051
public function getSql(SqlWalker $sqlWalker): string
3152
{
3253
$dispatched = [
54+
$this->getOptionalDistinctClause(),
3355
$this->expression->dispatch($sqlWalker),
3456
$this->getOptionalOrderByClause($sqlWalker),
3557
];

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

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
use Doctrine\ORM\Query\Parser;
1010
use Doctrine\ORM\Query\SqlWalker;
1111
use Doctrine\ORM\Query\TokenType;
12+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Traits\DistinctableTrait;
13+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Traits\OrderableTrait;
1214
use MartinGeorgiev\Utils\DoctrineOrm;
1315

1416
/**
@@ -19,38 +21,44 @@
1921
*
2022
* @author Martin Georgiev <martin.georgiev@gmail.com>
2123
*/
22-
class StringAgg extends BaseOrderableFunction
24+
class StringAgg extends BaseFunction
2325
{
24-
private bool $isDistinct = false;
26+
use OrderableTrait;
27+
use DistinctableTrait;
2528

2629
private Node $delimiter;
2730

2831
protected function customizeFunction(): void
2932
{
3033
$this->setFunctionPrototype('string_agg(%s%s, %s%s)');
34+
$this->addNodeMapping('StringPrimary');
35+
$this->addNodeMapping('StringPrimary');
3136
}
3237

33-
protected function parseFunction(Parser $parser): void
38+
public function parse(Parser $parser): void
3439
{
3540
$shouldUseLexer = DoctrineOrm::isPre219();
36-
$lexer = $parser->getLexer();
3741

38-
if ($lexer->isNextToken($shouldUseLexer ? Lexer::T_DISTINCT : TokenType::T_DISTINCT)) {
39-
$parser->match($shouldUseLexer ? Lexer::T_DISTINCT : TokenType::T_DISTINCT);
40-
$this->isDistinct = true;
41-
}
42+
$this->customizeFunction();
4243

44+
$parser->match($shouldUseLexer ? Lexer::T_IDENTIFIER : TokenType::T_IDENTIFIER);
45+
$parser->match($shouldUseLexer ? Lexer::T_OPEN_PARENTHESIS : TokenType::T_OPEN_PARENTHESIS);
46+
47+
$this->parseDistinctClause($parser);
4348
$this->expression = $parser->StringPrimary();
4449

4550
$parser->match($shouldUseLexer ? Lexer::T_COMMA : TokenType::T_COMMA);
4651

4752
$this->delimiter = $parser->StringPrimary();
53+
$this->parseOrderByClause($parser);
54+
55+
$parser->match($shouldUseLexer ? Lexer::T_CLOSE_PARENTHESIS : TokenType::T_CLOSE_PARENTHESIS);
4856
}
4957

5058
public function getSql(SqlWalker $sqlWalker): string
5159
{
5260
$dispatched = [
53-
$this->isDistinct ? 'DISTINCT ' : '',
61+
$this->getOptionalDistinctClause(),
5462
$this->expression->dispatch($sqlWalker),
5563
$this->delimiter->dispatch($sqlWalker),
5664
$this->getOptionalOrderByClause($sqlWalker),
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Traits;
6+
7+
use Doctrine\ORM\Query\Lexer;
8+
use Doctrine\ORM\Query\Parser;
9+
use Doctrine\ORM\Query\TokenType;
10+
use MartinGeorgiev\Utils\DoctrineOrm;
11+
12+
trait DistinctableTrait
13+
{
14+
protected bool $isDistinct = false;
15+
16+
protected function parseDistinctClause(Parser $parser): void
17+
{
18+
$shouldUseLexer = DoctrineOrm::isPre219();
19+
$lexer = $parser->getLexer();
20+
21+
if ($lexer->isNextToken($shouldUseLexer ? Lexer::T_DISTINCT : TokenType::T_DISTINCT)) {
22+
$parser->match($shouldUseLexer ? Lexer::T_DISTINCT : TokenType::T_DISTINCT);
23+
$this->isDistinct = true;
24+
}
25+
}
26+
27+
protected function getOptionalDistinctClause(): string
28+
{
29+
return $this->isDistinct ? 'DISTINCT ' : '';
30+
}
31+
}

src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/BaseOrderableFunction.php renamed to src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/Traits/OrderableTrait.php

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
declare(strict_types=1);
44

5-
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
5+
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Traits;
66

77
use Doctrine\ORM\Query\AST\Node;
88
use Doctrine\ORM\Query\AST\OrderByClause;
@@ -12,33 +12,22 @@
1212
use Doctrine\ORM\Query\TokenType;
1313
use MartinGeorgiev\Utils\DoctrineOrm;
1414

15-
abstract class BaseOrderableFunction extends BaseFunction
15+
trait OrderableTrait
1616
{
1717
protected Node $expression;
1818

1919
protected ?OrderByClause $orderByClause = null;
2020

21-
public function parse(Parser $parser): void
21+
protected function parseOrderByClause(Parser $parser): void
2222
{
2323
$shouldUseLexer = DoctrineOrm::isPre219();
24-
25-
$this->customizeFunction();
26-
27-
$parser->match($shouldUseLexer ? Lexer::T_IDENTIFIER : TokenType::T_IDENTIFIER);
28-
$parser->match($shouldUseLexer ? Lexer::T_OPEN_PARENTHESIS : TokenType::T_OPEN_PARENTHESIS);
29-
30-
$this->parseFunction($parser);
31-
3224
$lexer = $parser->getLexer();
25+
3326
if ($lexer->isNextToken($shouldUseLexer ? Lexer::T_ORDER : TokenType::T_ORDER)) {
3427
$this->orderByClause = $parser->OrderByClause();
3528
}
36-
37-
$parser->match($shouldUseLexer ? Lexer::T_CLOSE_PARENTHESIS : TokenType::T_CLOSE_PARENTHESIS);
3829
}
3930

40-
abstract protected function parseFunction(Parser $parser): void;
41-
4231
protected function getOptionalOrderByClause(SqlWalker $sqlWalker): string
4332
{
4433
return $this->orderByClause instanceof OrderByClause ? $this->orderByClause->dispatch($sqlWalker) : '';

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

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,29 +19,32 @@ 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
25-
'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_',
22+
'basic usage' => 'SELECT array_agg(c0_.text1) AS sclr_0 FROM ContainsTexts c0_',
23+
'with concatenation' => 'SELECT array_agg(c0_.text1 || c0_.text2) AS sclr_0 FROM ContainsTexts c0_',
24+
'with DISTINCT' => 'SELECT array_agg(DISTINCT c0_.text1) AS sclr_0 FROM ContainsTexts c0_',
25+
'with DISTINCT and concatenation' => 'SELECT array_agg(DISTINCT c0_.text1 || c0_.text2) AS sclr_0 FROM ContainsTexts c0_',
26+
'with ORDER BY' => 'SELECT array_agg(c0_.text1 ORDER BY c0_.text1 ASC) AS sclr_0 FROM ContainsTexts c0_',
27+
'with ORDER BY DESC' => 'SELECT array_agg(c0_.text1 ORDER BY c0_.text1 DESC) AS sclr_0 FROM ContainsTexts c0_',
28+
'with DISTINCT and ORDER BY' => 'SELECT array_agg(DISTINCT c0_.text1 ORDER BY c0_.text1 ASC) AS sclr_0 FROM ContainsTexts c0_',
29+
'with concatenation and ORDER BY' => 'SELECT array_agg(c0_.text1 || c0_.text2 ORDER BY c0_.text1 ASC) AS sclr_0 FROM ContainsTexts c0_',
30+
'with DISTINCT, concatenation and ORDER BY' => 'SELECT array_agg(DISTINCT c0_.text1 || c0_.text2 ORDER BY c0_.text1 ASC) AS sclr_0 FROM ContainsTexts c0_',
31+
'with multiple ORDER BY columns' => 'SELECT array_agg(c0_.text1 ORDER BY c0_.text1 ASC, c0_.text2 DESC) AS sclr_0 FROM ContainsTexts c0_',
3332
];
3433
}
3534

3635
protected function getDqlStatements(): array
3736
{
3837
return [
39-
\sprintf('SELECT ARRAY_AGG(e.text1) FROM %s e', ContainsTexts::class),
40-
\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),
38+
'basic usage' => \sprintf('SELECT ARRAY_AGG(e.text1) FROM %s e', ContainsTexts::class),
39+
'with concatenation' => \sprintf('SELECT ARRAY_AGG(CONCAT(e.text1, e.text2)) FROM %s e', ContainsTexts::class),
40+
'with DISTINCT' => \sprintf('SELECT ARRAY_AGG(DISTINCT e.text1) FROM %s e', ContainsTexts::class),
41+
'with DISTINCT and concatenation' => \sprintf('SELECT ARRAY_AGG(DISTINCT CONCAT(e.text1, e.text2)) FROM %s e', ContainsTexts::class),
42+
'with ORDER BY' => \sprintf('SELECT ARRAY_AGG(e.text1 ORDER BY e.text1) FROM %s e', ContainsTexts::class),
43+
'with ORDER BY DESC' => \sprintf('SELECT ARRAY_AGG(e.text1 ORDER BY e.text1 DESC) FROM %s e', ContainsTexts::class),
44+
'with DISTINCT and ORDER BY' => \sprintf('SELECT ARRAY_AGG(DISTINCT e.text1 ORDER BY e.text1) FROM %s e', ContainsTexts::class),
45+
'with concatenation and ORDER BY' => \sprintf('SELECT ARRAY_AGG(CONCAT(e.text1, e.text2) ORDER BY e.text1) FROM %s e', ContainsTexts::class),
46+
'with DISTINCT, concatenation and ORDER BY' => \sprintf('SELECT ARRAY_AGG(DISTINCT CONCAT(e.text1, e.text2) ORDER BY e.text1) FROM %s e', ContainsTexts::class),
47+
'with multiple ORDER BY columns' => \sprintf('SELECT ARRAY_AGG(e.text1 ORDER BY e.text1 ASC, e.text2 DESC) FROM %s e', ContainsTexts::class),
4548
];
4649
}
4750
}

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

Lines changed: 20 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -19,41 +19,32 @@ 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
25-
"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_",
22+
'basic usage' => "SELECT string_agg(c0_.text1, ',') AS sclr_0 FROM ContainsTexts c0_",
23+
'with concatenation' => "SELECT string_agg(c0_.text1 || c0_.text2, ',') AS sclr_0 FROM ContainsTexts c0_",
24+
'with DISTINCT' => "SELECT string_agg(DISTINCT c0_.text1, ',') AS sclr_0 FROM ContainsTexts c0_",
25+
'with DISTINCT and concatenation' => "SELECT string_agg(DISTINCT c0_.text1 || c0_.text2, ',') AS sclr_0 FROM ContainsTexts c0_",
26+
'with ORDER BY' => "SELECT string_agg(c0_.text1, ',' ORDER BY c0_.text1 ASC) AS sclr_0 FROM ContainsTexts c0_",
27+
'with ORDER BY DESC' => "SELECT string_agg(c0_.text1, ',' ORDER BY c0_.text1 DESC) AS sclr_0 FROM ContainsTexts c0_",
28+
'with DISTINCT and ORDER BY' => "SELECT string_agg(DISTINCT c0_.text1, ',' ORDER BY c0_.text1 ASC) AS sclr_0 FROM ContainsTexts c0_",
29+
'with concatenation, DISTINCT and ORDER BY' => "SELECT string_agg(DISTINCT c0_.text1 || c0_.text2, ',' ORDER BY c0_.text1 ASC) AS sclr_0 FROM ContainsTexts c0_",
30+
'with multiple ORDER BY columns' => "SELECT string_agg(c0_.text1, ',' ORDER BY c0_.text1 ASC, c0_.text2 DESC) AS sclr_0 FROM ContainsTexts c0_",
31+
'with different delimiter' => "SELECT string_agg(c0_.text1, ' | ') AS sclr_0 FROM ContainsTexts c0_",
4132
];
4233
}
4334

4435
protected function getDqlStatements(): array
4536
{
4637
return [
47-
\sprintf("SELECT STRING_AGG(e.text1, ',') FROM %s e", ContainsTexts::class),
48-
\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),
38+
'basic usage' => \sprintf("SELECT STRING_AGG(e.text1, ',') FROM %s e", ContainsTexts::class),
39+
'with concatenation' => \sprintf("SELECT STRING_AGG(CONCAT(e.text1, e.text2), ',') FROM %s e", ContainsTexts::class),
40+
'with DISTINCT' => \sprintf("SELECT STRING_AGG(DISTINCT e.text1, ',') FROM %s e", ContainsTexts::class),
41+
'with DISTINCT and concatenation' => \sprintf("SELECT STRING_AGG(DISTINCT CONCAT(e.text1, e.text2), ',') FROM %s e", ContainsTexts::class),
42+
'with ORDER BY' => \sprintf("SELECT STRING_AGG(e.text1, ',' ORDER BY e.text1) FROM %s e", ContainsTexts::class),
43+
'with ORDER BY DESC' => \sprintf("SELECT STRING_AGG(e.text1, ',' ORDER BY e.text1 DESC) FROM %s e", ContainsTexts::class),
44+
'with DISTINCT and ORDER BY' => \sprintf("SELECT STRING_AGG(DISTINCT e.text1, ',' ORDER BY e.text1) FROM %s e", ContainsTexts::class),
45+
'with concatenation, DISTINCT and ORDER BY' => \sprintf("SELECT STRING_AGG(DISTINCT CONCAT(e.text1, e.text2), ',' ORDER BY e.text1) FROM %s e", ContainsTexts::class),
46+
'with multiple ORDER BY columns' => \sprintf("SELECT STRING_AGG(e.text1, ',' ORDER BY e.text1 ASC, e.text2 DESC) FROM %s e", ContainsTexts::class),
47+
'with different delimiter' => \sprintf("SELECT STRING_AGG(e.text1, ' | ') FROM %s e", ContainsTexts::class),
5748
];
5849
}
5950
}

0 commit comments

Comments
 (0)