@@ -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 ;
0 commit comments