Skip to content

Commit 46256c9

Browse files
arnaud-lbondrejmirtes
authored andcommitted
Aggregate functions affect nullability only when the query is not grouped
1 parent 938350f commit 46256c9

File tree

2 files changed

+34
-14
lines changed

2 files changed

+34
-14
lines changed

src/Type/Doctrine/Query/QueryResultTypeWalker.php

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,10 @@ class QueryResultTypeWalker extends SqlWalker
9797
private $descriptorRegistry;
9898

9999
/** @var bool */
100-
private $isAggregated;
100+
private $hasAggregateFunction;
101+
102+
/** @var bool */
103+
private $hasGroupByClause;
101104

102105
/**
103106
* @param Query<mixed> $query
@@ -125,7 +128,8 @@ public function __construct($query, $parserResult, array $queryComponents)
125128
$this->em = $query->getEntityManager();
126129
$this->queryComponents = $queryComponents;
127130
$this->nullableQueryComponents = [];
128-
$this->isAggregated = false;
131+
$this->hasAggregateFunction = false;
132+
$this->hasGroupByClause = false;
129133

130134
// The object is instantiated by Doctrine\ORM\Query\Parser, so receiving
131135
// dependencies through the constructor is not an option. Instead, we
@@ -166,7 +170,8 @@ public function __construct($query, $parserResult, array $queryComponents)
166170
public function walkSelectStatement(AST\SelectStatement $AST)
167171
{
168172
$this->typeBuilder->setSelectQuery();
169-
$this->isAggregated = $this->isAggregated($AST);
173+
$this->hasAggregateFunction = $this->hasAggregateFunction($AST);
174+
$this->hasGroupByClause = $AST->groupByClause !== null;
170175

171176
$this->walkFromClause($AST->fromClause);
172177

@@ -231,7 +236,9 @@ public function walkPathExpression($pathExpr)
231236

232237
assert(is_string($typeName));
233238

234-
$nullable = $this->isQueryComponentNullable($dqlAlias) || $class->isNullable($fieldName) || $this->isAggregated;
239+
$nullable = $this->isQueryComponentNullable($dqlAlias)
240+
|| $class->isNullable($fieldName)
241+
|| $this->hasAggregateWithoutGroupBy();
235242

236243
$fieldType = $this->resolveDatabaseInternalType($typeName, $nullable);
237244

@@ -267,7 +274,8 @@ public function walkPathExpression($pathExpr)
267274

268275
assert(is_string($typeName));
269276

270-
$nullable = (bool) ($joinColumn['nullable'] ?? true) || $this->isAggregated;
277+
$nullable = (bool) ($joinColumn['nullable'] ?? true)
278+
|| $this->hasAggregateWithoutGroupBy();
271279

272280
$fieldType = $this->resolveDatabaseInternalType($typeName, $nullable);
273281

@@ -699,7 +707,7 @@ public function walkSelectExpression($selectExpression)
699707

700708
$type = new ObjectType($class->name);
701709

702-
if ($this->isQueryComponentNullable($dqlAlias) || $this->isAggregated) {
710+
if ($this->isQueryComponentNullable($dqlAlias) || $this->hasAggregateWithoutGroupBy()) {
703711
$type = TypeCombinator::addNull($type);
704712
}
705713

@@ -725,7 +733,9 @@ public function walkSelectExpression($selectExpression)
725733

726734
assert(is_string($typeName));
727735

728-
$nullable = $this->isQueryComponentNullable($dqlAlias) || $class->isNullable($fieldName) || $this->isAggregated;
736+
$nullable = $this->isQueryComponentNullable($dqlAlias)
737+
|| $class->isNullable($fieldName)
738+
|| $this->hasAggregateWithoutGroupBy();
729739

730740
$type = $this->resolveDoctrineType($typeName, $nullable);
731741

@@ -1288,12 +1298,22 @@ private function toNumericOrNull(Type $type): Type
12881298
});
12891299
}
12901300

1291-
private function isAggregated(AST\SelectStatement $AST): bool
1301+
/**
1302+
* Returns whether the query has aggregate function and no group by clause
1303+
*
1304+
* Queries with aggregate functions and no group by clause always have
1305+
* exactly 1 group. This implies that they return exactly 1 row, and that
1306+
* all column can have a null value.
1307+
*
1308+
* c.f. SQL92, section 7.9, General Rules
1309+
*/
1310+
private function hasAggregateWithoutGroupBy(): bool
12921311
{
1293-
if ($AST->groupByClause !== null) {
1294-
return true;
1295-
}
1312+
return $this->hasAggregateFunction && !$this->hasGroupByClause;
1313+
}
12961314

1315+
private function hasAggregateFunction(AST\SelectStatement $AST): bool
1316+
{
12971317
foreach ($AST->selectClause->selectExpressions as $selectExpression) {
12981318
if (!$selectExpression instanceof AST\SelectExpression) {
12991319
continue;

tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -538,19 +538,19 @@ public function getTestData(): array
538538
$this->constantArray([
539539
[
540540
new ConstantStringType('intColumn'),
541-
TypeCombinator::addNull(new IntegerType()),
541+
new IntegerType(),
542542
],
543543
[
544544
new ConstantStringType('max'),
545545
TypeCombinator::addNull($this->intStringified()),
546546
],
547547
[
548548
new ConstantStringType('arithmetic'),
549-
TypeCombinator::addNull($this->intStringified()),
549+
$this->intStringified(),
550550
],
551551
[
552552
new ConstantStringType('coalesce'),
553-
TypeCombinator::addNull($this->intStringified()),
553+
$this->intStringified(),
554554
],
555555
[
556556
new ConstantStringType('count'),

0 commit comments

Comments
 (0)