Skip to content

Commit d204544

Browse files
committed
add aggregate functions with filter
Signed-off-by: Pfilsx <[email protected]>
1 parent 97d0cf2 commit d204544

File tree

8 files changed

+263
-0
lines changed

8 files changed

+263
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ Documentation
5353
-------------
5454

5555
* [ENUMS](docs/ENUMS.md)
56+
* [Functions](docs/FUNCTIONS-AND-OPERATORS.md)
5657

5758
License
5859
-------

docs/FUNCTIONS-AND-OPERATORS.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
Available functions
2+
===================
3+
4+
| PostgreSQL function | DQL function | Implementation |
5+
|---------------------|--------------|-------------------------------------------------------------------------------------------------------------|
6+
| ARRAY_AGG() | ARRAY_AGG | [Pfilsx\PostgreSQLDoctrine\ORM\Query\AST\Functions\ArrayAgg](../src/ORM/Query/AST/Functions/ArrayAgg.php) |
7+
| JSON_AGG() | JSON_AGG | [Pfilsx\PostgreSQLDoctrine\ORM\Query\AST\Functions\JsonAgg](../src/ORM/Query/AST/Functions/JsonAgg.php) |
8+
| JSONB_AGG() | JSONB_AGG | [Pfilsx\PostgreSQLDoctrine\ORM\Query\AST\Functions\JsonbAgg](../src/ORM/Query/AST/Functions/JsonbAgg.php) |
9+
| STRING_AGG() | STRING_AGG | [Pfilsx\PostgreSQLDoctrine\ORM\Query\AST\Functions\StringAgg](../src/ORM/Query/AST/Functions/StringAgg.php) |
10+
11+
Available operators
12+
===================
13+
14+
| PostgreSQL operator | DQL function | Implementation |
15+
|---------------------|-------------------------|---------------------------------------------------------------------------------------------------------------------------------|
16+
| &#124;&#124; | JSONB_CONCAT | [Pfilsx\PostgreSQLDoctrine\ORM\Query\AST\Functions\JsonbConcat](../src/ORM/Query/AST/Functions/JsonbConcat.php) |
17+
| @> | JSONB_CONTAINS | [Pfilsx\PostgreSQLDoctrine\ORM\Query\AST\Functions\JsonbContains](../src/ORM/Query/AST/Functions/JsonbContains.php) |
18+
| ? | JSONB_KEY_EXISTS | [Pfilsx\PostgreSQLDoctrine\ORM\Query\AST\Functions\JsonbKeyExists](../src/ORM/Query/AST/Functions/JsonbKeyExists.php) |
19+
| - | JSONB_REMOVE | [Pfilsx\PostgreSQLDoctrine\ORM\Query\AST\Functions\JsonbRemove](../src/ORM/Query/AST/Functions/JsonbRemove.php) |
20+
| -> | JSON_GET_FIELD | [Pfilsx\PostgreSQLDoctrine\ORM\Query\AST\Functions\JsonGetField](../src/ORM/Query/AST/Functions/JsonGetField.php) |
21+
| ->> | JSON_GET_FIELD_AS_TEXT | [Pfilsx\PostgreSQLDoctrine\ORM\Query\AST\Functions\JsonGetFieldAsText](../src/ORM/Query/AST/Functions/JsonGetFieldAsText.php) |
22+
| #> | JSON_GET_OBJECT | [Pfilsx\PostgreSQLDoctrine\ORM\Query\AST\Functions\JsonGetObject](../src/ORM/Query/AST/Functions/JsonGetObject.php) |
23+
| #>> | JSON_GET_OBJECT_AS_TEXT | [Pfilsx\PostgreSQLDoctrine\ORM\Query\AST\Functions\JsonGetObjectAsText](../src/ORM/Query/AST/Functions/JsonGetObjectAsText.php) |
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Pfilsx\PostgreSQLDoctrine\ORM\Query\AST;
6+
7+
use Doctrine\ORM\Query\AST\Node;
8+
use Doctrine\ORM\Query\AST\WhereClause;
9+
10+
final class FilterExpression extends Node
11+
{
12+
private WhereClause $whereClause;
13+
14+
public function __construct(WhereClause $whereClause)
15+
{
16+
$this->whereClause = $whereClause;
17+
}
18+
19+
public function dispatch($walker): string
20+
{
21+
return "FILTER ({$this->whereClause->dispatch($walker)})";
22+
}
23+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Pfilsx\PostgreSQLDoctrine\ORM\Query\AST\Functions;
6+
7+
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
8+
use Doctrine\ORM\Query\Lexer;
9+
use Doctrine\ORM\Query\Parser;
10+
use Doctrine\ORM\Query\SqlWalker;
11+
use Pfilsx\PostgreSQLDoctrine\ORM\Query\AST\FilterExpression;
12+
13+
abstract class AggregateWithFilterFunction extends FunctionNode
14+
{
15+
private const FILTER_IDENTIFIER = 'FILTER';
16+
private ?FilterExpression $filterExpression = null;
17+
18+
public function parse(Parser $parser): void
19+
{
20+
$this->parseFunction($parser);
21+
22+
$lexer = $parser->getLexer();
23+
24+
if (!$lexer->isNextToken(Lexer::T_IDENTIFIER)) {
25+
return;
26+
}
27+
28+
$lookaheadValue = $lexer->lookahead['value'] ?? null;
29+
30+
if (!is_string($lookaheadValue) || mb_strtoupper($lookaheadValue) !== self::FILTER_IDENTIFIER) {
31+
return;
32+
}
33+
34+
$parser->match(Lexer::T_IDENTIFIER);
35+
$parser->match(Lexer::T_OPEN_PARENTHESIS);
36+
37+
$this->filterExpression = new FilterExpression($parser->WhereClause());
38+
39+
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
40+
}
41+
42+
abstract public function parseFunction(Parser $parser): void;
43+
44+
public function getSql(SqlWalker $sqlWalker): string
45+
{
46+
$sql = $this->getFunctionSql($sqlWalker);
47+
48+
if ($this->filterExpression !== null) {
49+
$sql .= " {$this->filterExpression->dispatch($sqlWalker)}";
50+
}
51+
52+
return $sql;
53+
}
54+
55+
abstract public function getFunctionSql(SqlWalker $sqlWalker): string;
56+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Pfilsx\PostgreSQLDoctrine\ORM\Query\AST\Functions;
6+
7+
use Doctrine\ORM\Query\AST\Node;
8+
use Doctrine\ORM\Query\Lexer;
9+
use Doctrine\ORM\Query\Parser;
10+
use Doctrine\ORM\Query\SqlWalker;
11+
12+
/**
13+
* Implementation of PostgreSql ARRAY_AGG().
14+
*
15+
* @see https://www.postgresql.org/docs/current/functions-aggregate.html#FUNCTIONS-AGGREGATE-TABLE
16+
*
17+
* @example ARRAY_AGG(entity.field)
18+
* @example ARRAY_AGG(entity.field) FILTER (WHERE entity.field IS NOT NULL)
19+
*/
20+
final class ArrayAgg extends AggregateWithFilterFunction
21+
{
22+
private Node $expr;
23+
24+
public function parseFunction(Parser $parser): void
25+
{
26+
$parser->match(Lexer::T_IDENTIFIER);
27+
$parser->match(Lexer::T_OPEN_PARENTHESIS);
28+
$this->expr = $parser->StringPrimary();
29+
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
30+
}
31+
32+
public function getFunctionSql(SqlWalker $sqlWalker): string
33+
{
34+
return "ARRAY_AGG({$this->expr->dispatch($sqlWalker)})";
35+
}
36+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Pfilsx\PostgreSQLDoctrine\ORM\Query\AST\Functions;
6+
7+
use Doctrine\ORM\Query\AST\Node;
8+
use Doctrine\ORM\Query\Lexer;
9+
use Doctrine\ORM\Query\Parser;
10+
use Doctrine\ORM\Query\SqlWalker;
11+
12+
/**
13+
* Implementation of PostgreSql JSON_AGG().
14+
*
15+
* @see https://www.postgresql.org/docs/current/functions-aggregate.html#FUNCTIONS-AGGREGATE-TABLE
16+
*
17+
* @example JSON_AGG(entity.field)
18+
* @example JSON_AGG(entity.field) FILTER (WHERE entity.field IS NOT NULL)
19+
*/
20+
final class JsonAgg extends AggregateWithFilterFunction
21+
{
22+
private Node $expr;
23+
24+
public function parseFunction(Parser $parser): void
25+
{
26+
$parser->match(Lexer::T_IDENTIFIER);
27+
$parser->match(Lexer::T_OPEN_PARENTHESIS);
28+
$this->expr = $parser->StringPrimary();
29+
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
30+
}
31+
32+
public function getFunctionSql(SqlWalker $sqlWalker): string
33+
{
34+
return "JSON_AGG({$this->expr->dispatch($sqlWalker)})";
35+
}
36+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Pfilsx\PostgreSQLDoctrine\ORM\Query\AST\Functions;
6+
7+
use Doctrine\ORM\Query\AST\Node;
8+
use Doctrine\ORM\Query\Lexer;
9+
use Doctrine\ORM\Query\Parser;
10+
use Doctrine\ORM\Query\SqlWalker;
11+
12+
/**
13+
* Implementation of PostgreSql JSONB_AGG().
14+
*
15+
* @see https://www.postgresql.org/docs/current/functions-aggregate.html#FUNCTIONS-AGGREGATE-TABLE
16+
*
17+
* @example JSONB_AGG(entity.field)
18+
* @example JSONB_AGG(entity.field) FILTER (WHERE entity.field IS NOT NULL)
19+
*/
20+
final class JsonbAgg extends AggregateWithFilterFunction
21+
{
22+
private Node $expr;
23+
24+
public function parseFunction(Parser $parser): void
25+
{
26+
$parser->match(Lexer::T_IDENTIFIER);
27+
$parser->match(Lexer::T_OPEN_PARENTHESIS);
28+
$this->expr = $parser->StringPrimary();
29+
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
30+
}
31+
32+
public function getFunctionSql(SqlWalker $sqlWalker): string
33+
{
34+
return "JSON_AGG({$this->expr->dispatch($sqlWalker)})";
35+
}
36+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Pfilsx\PostgreSQLDoctrine\ORM\Query\AST\Functions;
6+
7+
use Doctrine\ORM\Query\AST\Node;
8+
use Doctrine\ORM\Query\Lexer;
9+
use Doctrine\ORM\Query\Parser;
10+
use Doctrine\ORM\Query\SqlWalker;
11+
12+
/**
13+
* Implementation of PostgreSql STRING_AGG().
14+
*
15+
* @see https://www.postgresql.org/docs/current/functions-aggregate.html#FUNCTIONS-AGGREGATE-TABLE
16+
*
17+
* @example STRING_AGG(entity.field, ', ')
18+
* @example STRING_AGG(entity.field, ', ') FILTER (WHERE entity.field IS NOT NULL)
19+
*/
20+
final class StringAgg extends AggregateWithFilterFunction
21+
{
22+
private bool $distinct = false;
23+
private Node $expr;
24+
private Node $delimiter;
25+
26+
public function parseFunction(Parser $parser): void
27+
{
28+
$parser->match(Lexer::T_IDENTIFIER);
29+
$parser->match(Lexer::T_OPEN_PARENTHESIS);
30+
31+
$lexer = $parser->getLexer();
32+
if ($lexer->isNextToken(Lexer::T_DISTINCT)) {
33+
$parser->match(Lexer::T_DISTINCT);
34+
$this->distinct = true;
35+
}
36+
37+
$this->expr = $parser->StringPrimary();
38+
$parser->match(Lexer::T_COMMA);
39+
$this->delimiter = $parser->StringPrimary();
40+
41+
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
42+
}
43+
44+
public function getFunctionSql(SqlWalker $sqlWalker): string
45+
{
46+
return sprintf('STRING_AGG(%s%s, %s)',
47+
$this->distinct ? 'DISTINCT' : '',
48+
$this->expr->dispatch($sqlWalker),
49+
$this->delimiter->dispatch($sqlWalker)
50+
);
51+
}
52+
}

0 commit comments

Comments
 (0)