Skip to content

Commit 6339dff

Browse files
authored
Fix detection of aggregate functions inside custom functions
1 parent d453424 commit 6339dff

File tree

4 files changed

+97
-273
lines changed

4 files changed

+97
-273
lines changed

src/Type/Doctrine/Query/QueryAggregateFunctionDetectorTreeWalker.php

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

55
use Doctrine\ORM\Query;
66
use Doctrine\ORM\Query\AST;
7-
use function is_string;
7+
use function is_array;
88

99
class QueryAggregateFunctionDetectorTreeWalker extends Query\TreeWalkerAdapter
1010
{
@@ -13,294 +13,38 @@ class QueryAggregateFunctionDetectorTreeWalker extends Query\TreeWalkerAdapter
1313

1414
public function walkSelectStatement(AST\SelectStatement $selectStatement): void
1515
{
16-
$this->doWalkSelectClause($selectStatement->selectClause);
16+
$this->walkNode($selectStatement->selectClause);
1717
}
1818

1919
/**
20-
* @param AST\SelectClause $selectClause
20+
* @param mixed $node
2121
*/
22-
public function doWalkSelectClause($selectClause): void
22+
public function walkNode($node): void
2323
{
24-
foreach ($selectClause->selectExpressions as $selectExpression) {
25-
$this->doWalkSelectExpression($selectExpression);
26-
}
27-
}
28-
29-
/**
30-
* @param AST\SelectExpression $selectExpression
31-
*/
32-
public function doWalkSelectExpression($selectExpression): void
33-
{
34-
$this->doWalkNode($selectExpression->expression);
35-
}
36-
37-
/**
38-
* @param mixed $expr
39-
*/
40-
private function doWalkNode($expr): void
41-
{
42-
if ($expr instanceof AST\AggregateExpression) {
43-
$this->markAggregateFunctionFound();
44-
45-
} elseif ($expr instanceof AST\Functions\FunctionNode) {
46-
if ($this->isAggregateFunction($expr)) {
47-
$this->markAggregateFunctionFound();
48-
}
49-
50-
} elseif ($expr instanceof AST\SimpleArithmeticExpression) {
51-
foreach ($expr->arithmeticTerms as $term) {
52-
$this->doWalkArithmeticTerm($term);
53-
}
54-
55-
} elseif ($expr instanceof AST\ArithmeticTerm) {
56-
$this->doWalkArithmeticTerm($expr);
57-
58-
} elseif ($expr instanceof AST\ArithmeticFactor) {
59-
$this->doWalkArithmeticFactor($expr);
60-
61-
} elseif ($expr instanceof AST\ParenthesisExpression) {
62-
$this->doWalkArithmeticPrimary($expr->expression);
63-
64-
} elseif ($expr instanceof AST\NullIfExpression) {
65-
$this->doWalkNullIfExpression($expr);
66-
67-
} elseif ($expr instanceof AST\CoalesceExpression) {
68-
$this->doWalkCoalesceExpression($expr);
69-
70-
} elseif ($expr instanceof AST\GeneralCaseExpression) {
71-
$this->doWalkGeneralCaseExpression($expr);
72-
73-
} elseif ($expr instanceof AST\SimpleCaseExpression) {
74-
$this->doWalkSimpleCaseExpression($expr);
75-
76-
} elseif ($expr instanceof AST\ArithmeticExpression) {
77-
$this->doWalkArithmeticExpression($expr);
78-
79-
} elseif ($expr instanceof AST\ComparisonExpression) {
80-
$this->doWalkComparisonExpression($expr);
81-
82-
} elseif ($expr instanceof AST\BetweenExpression) {
83-
$this->doWalkBetweenExpression($expr);
84-
}
85-
}
86-
87-
public function doWalkCoalesceExpression(AST\CoalesceExpression $coalesceExpression): void
88-
{
89-
foreach ($coalesceExpression->scalarExpressions as $scalarExpression) {
90-
$this->doWalkSimpleArithmeticExpression($scalarExpression);
91-
}
92-
}
93-
94-
public function doWalkNullIfExpression(AST\NullIfExpression $nullIfExpression): void
95-
{
96-
if (!is_string($nullIfExpression->firstExpression)) {
97-
$this->doWalkSimpleArithmeticExpression($nullIfExpression->firstExpression);
98-
}
99-
100-
if (is_string($nullIfExpression->secondExpression)) {
24+
if (!$node instanceof AST\Node) {
10125
return;
10226
}
10327

104-
$this->doWalkSimpleArithmeticExpression($nullIfExpression->secondExpression);
105-
}
106-
107-
public function doWalkGeneralCaseExpression(AST\GeneralCaseExpression $generalCaseExpression): void
108-
{
109-
foreach ($generalCaseExpression->whenClauses as $whenClause) {
110-
$this->doWalkConditionalExpression($whenClause->caseConditionExpression);
111-
$this->doWalkSimpleArithmeticExpression($whenClause->thenScalarExpression);
112-
}
113-
114-
$this->doWalkSimpleArithmeticExpression($generalCaseExpression->elseScalarExpression);
115-
}
116-
117-
public function doWalkSimpleCaseExpression(AST\SimpleCaseExpression $simpleCaseExpression): void
118-
{
119-
foreach ($simpleCaseExpression->simpleWhenClauses as $simpleWhenClause) {
120-
$this->doWalkSimpleArithmeticExpression($simpleWhenClause->caseScalarExpression);
121-
$this->doWalkSimpleArithmeticExpression($simpleWhenClause->thenScalarExpression);
122-
}
123-
124-
$this->doWalkSimpleArithmeticExpression($simpleCaseExpression->elseScalarExpression);
125-
}
126-
127-
/**
128-
* @param AST\ConditionalExpression|AST\Phase2OptimizableConditional $condExpr
129-
*/
130-
public function doWalkConditionalExpression($condExpr): void
131-
{
132-
if (!$condExpr instanceof AST\ConditionalExpression) {
133-
$this->doWalkConditionalTerm($condExpr); // @phpstan-ignore-line PHPStan do not read @psalm-inheritors of Phase2OptimizableConditional
134-
return;
135-
}
136-
137-
foreach ($condExpr->conditionalTerms as $conditionalTerm) {
138-
$this->doWalkConditionalTerm($conditionalTerm);
139-
}
140-
}
141-
142-
/**
143-
* @param AST\ConditionalTerm|AST\ConditionalPrimary|AST\ConditionalFactor $condTerm
144-
*/
145-
public function doWalkConditionalTerm($condTerm): void
146-
{
147-
if (!$condTerm instanceof AST\ConditionalTerm) {
148-
$this->doWalkConditionalFactor($condTerm);
28+
if ($this->isAggregateFunction($node)) {
29+
$this->markAggregateFunctionFound();
14930
return;
15031
}
15132

152-
foreach ($condTerm->conditionalFactors as $conditionalFactor) {
153-
$this->doWalkConditionalFactor($conditionalFactor);
154-
}
155-
}
33+
foreach ((array) $node as $property) {
34+
if ($property instanceof AST\Node) {
35+
$this->walkNode($property);
36+
}
15637

157-
/**
158-
* @param AST\ConditionalFactor|AST\ConditionalPrimary $factor
159-
*/
160-
public function doWalkConditionalFactor($factor): void
161-
{
162-
if (!$factor instanceof AST\ConditionalFactor) {
163-
$this->doWalkConditionalPrimary($factor);
164-
} else {
165-
$this->doWalkConditionalPrimary($factor->conditionalPrimary);
166-
}
167-
}
38+
if (is_array($property)) {
39+
foreach ($property as $propertyValue) {
40+
$this->walkNode($propertyValue);
41+
}
42+
}
16843

169-
/**
170-
* @param AST\ConditionalPrimary $primary
171-
*/
172-
public function doWalkConditionalPrimary($primary): void
173-
{
174-
if ($primary->isSimpleConditionalExpression()) {
175-
if ($primary->simpleConditionalExpression instanceof AST\ComparisonExpression) {
176-
$this->doWalkComparisonExpression($primary->simpleConditionalExpression);
44+
if ($this->wasAggregateFunctionFound()) {
17745
return;
17846
}
179-
$this->doWalkNode($primary->simpleConditionalExpression);
18047
}
181-
182-
if (!$primary->isConditionalExpression()) {
183-
return;
184-
}
185-
186-
if ($primary->conditionalExpression === null) {
187-
return;
188-
}
189-
190-
$this->doWalkConditionalExpression($primary->conditionalExpression);
191-
}
192-
193-
/**
194-
* @param AST\BetweenExpression $betweenExpr
195-
*/
196-
public function doWalkBetweenExpression($betweenExpr): void
197-
{
198-
$this->doWalkArithmeticExpression($betweenExpr->expression);
199-
$this->doWalkArithmeticExpression($betweenExpr->leftBetweenExpression);
200-
$this->doWalkArithmeticExpression($betweenExpr->rightBetweenExpression);
201-
}
202-
203-
/**
204-
* @param AST\ComparisonExpression $compExpr
205-
*/
206-
public function doWalkComparisonExpression($compExpr): void
207-
{
208-
$leftExpr = $compExpr->leftExpression;
209-
$rightExpr = $compExpr->rightExpression;
210-
211-
if ($leftExpr instanceof AST\Node) {
212-
$this->doWalkNode($leftExpr);
213-
}
214-
215-
if (!($rightExpr instanceof AST\Node)) {
216-
return;
217-
}
218-
219-
$this->doWalkNode($rightExpr);
220-
}
221-
222-
/**
223-
* @param AST\ArithmeticExpression $arithmeticExpr
224-
*/
225-
public function doWalkArithmeticExpression($arithmeticExpr): void
226-
{
227-
if (!$arithmeticExpr->isSimpleArithmeticExpression()) {
228-
return;
229-
}
230-
231-
if ($arithmeticExpr->simpleArithmeticExpression === null) {
232-
return;
233-
}
234-
235-
$this->doWalkSimpleArithmeticExpression($arithmeticExpr->simpleArithmeticExpression);
236-
}
237-
238-
/**
239-
* @param AST\Node|string $simpleArithmeticExpr
240-
*/
241-
public function doWalkSimpleArithmeticExpression($simpleArithmeticExpr): void
242-
{
243-
if (!$simpleArithmeticExpr instanceof AST\SimpleArithmeticExpression) {
244-
$this->doWalkArithmeticTerm($simpleArithmeticExpr);
245-
return;
246-
}
247-
248-
foreach ($simpleArithmeticExpr->arithmeticTerms as $term) {
249-
$this->doWalkArithmeticTerm($term);
250-
}
251-
}
252-
253-
/**
254-
* @param AST\Node|string $term
255-
*/
256-
public function doWalkArithmeticTerm($term): void
257-
{
258-
if (is_string($term)) {
259-
return;
260-
}
261-
262-
if (!$term instanceof AST\ArithmeticTerm) {
263-
$this->doWalkArithmeticFactor($term);
264-
return;
265-
}
266-
267-
foreach ($term->arithmeticFactors as $factor) {
268-
$this->doWalkArithmeticFactor($factor);
269-
}
270-
}
271-
272-
/**
273-
* @param AST\Node|string $factor
274-
*/
275-
public function doWalkArithmeticFactor($factor): void
276-
{
277-
if (is_string($factor)) {
278-
return;
279-
}
280-
281-
if (!$factor instanceof AST\ArithmeticFactor) {
282-
$this->doWalkArithmeticPrimary($factor);
283-
return;
284-
}
285-
286-
$this->doWalkArithmeticPrimary($factor->arithmeticPrimary);
287-
}
288-
289-
/**
290-
* @param AST\Node|string $primary
291-
*/
292-
public function doWalkArithmeticPrimary($primary): void
293-
{
294-
if ($primary instanceof AST\SimpleArithmeticExpression) {
295-
$this->doWalkSimpleArithmeticExpression($primary);
296-
return;
297-
}
298-
299-
if (!($primary instanceof AST\Node)) {
300-
return;
301-
}
302-
303-
$this->doWalkNode($primary);
30448
}
30549

30650
private function isAggregateFunction(AST\Node $node): bool
@@ -318,4 +62,9 @@ private function markAggregateFunctionFound(): void
31862
$this->_getQuery()->setHint(self::HINT_HAS_AGGREGATE_FUNCTION, true);
31963
}
32064

65+
private function wasAggregateFunctionFound(): bool
66+
{
67+
return $this->_getQuery()->hasHint(self::HINT_HAS_AGGREGATE_FUNCTION);
68+
}
69+
32170
}

src/Type/Doctrine/Query/QueryResultTypeWalker.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1226,6 +1226,10 @@ public function walkSelectExpression($selectExpression): string
12261226
$this->resolveDoctrineType($dbalTypeName, null, TypeCombinator::containsNull($type))
12271227
);
12281228

1229+
if ($this->hasAggregateWithoutGroupBy() && !$expr instanceof AST\Functions\CountFunction) {
1230+
$type = TypeCombinator::addNull($type);
1231+
}
1232+
12291233
} else {
12301234
// Expressions default to Doctrine's StringType, whose
12311235
// convertToPHPValue() is a no-op. So the actual type depends on

tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3961,6 +3961,38 @@ public static function provideCases(): iterable
39613961
'stringify' => self::STRINGIFY_DEFAULT,
39623962
];
39633963

3964+
yield 'INT_WRAP(MIN(t.col_float)) + no data' => [
3965+
'data' => self::dataNone(),
3966+
'select' => 'SELECT INT_WRAP(MIN(t.col_float)) FROM %s t',
3967+
'mysql' => self::intOrNull(),
3968+
'sqlite' => self::intOrNull(),
3969+
'pdo_pgsql' => self::intOrNull(),
3970+
'pgsql' => self::intOrNull(),
3971+
'mssql' => self::intOrNull(),
3972+
'mysqlResult' => null,
3973+
'sqliteResult' => null,
3974+
'pdoPgsqlResult' => null,
3975+
'pgsqlResult' => null,
3976+
'mssqlResult' => null,
3977+
'stringify' => self::STRINGIFY_NONE,
3978+
];
3979+
3980+
yield 'INT_WRAP(MIN(t.col_float))' => [
3981+
'data' => self::dataDefault(),
3982+
'select' => 'SELECT INT_WRAP(MIN(t.col_float)) FROM %s t',
3983+
'mysql' => self::intOrNull(),
3984+
'sqlite' => self::intOrNull(),
3985+
'pdo_pgsql' => self::intOrNull(),
3986+
'pgsql' => self::intOrNull(),
3987+
'mssql' => self::intOrNull(),
3988+
'mysqlResult' => 0,
3989+
'sqliteResult' => 0,
3990+
'pdoPgsqlResult' => 0,
3991+
'pgsqlResult' => 0,
3992+
'mssqlResult' => 0,
3993+
'stringify' => self::STRINGIFY_NONE,
3994+
];
3995+
39643996
yield 'COALESCE(t.col_datetime, t.col_datetime)' => [
39653997
'data' => self::dataDefault(),
39663998
'select' => 'SELECT COALESCE(t.col_datetime, t.col_datetime) FROM %s t',
@@ -5018,6 +5050,7 @@ private function createOrmConfig(): Configuration
50185050
$config->addCustomStringFunction('INT_PI', TypedExpressionIntegerPiFunction::class);
50195051
$config->addCustomStringFunction('BOOL_PI', TypedExpressionBooleanPiFunction::class);
50205052
$config->addCustomStringFunction('STRING_PI', TypedExpressionStringPiFunction::class);
5053+
$config->addCustomStringFunction('INT_WRAP', TypedExpressionIntegerWrapFunction::class);
50215054

50225055
return $config;
50235056
}

0 commit comments

Comments
 (0)