@@ -97,7 +97,10 @@ class QueryResultTypeWalker extends SqlWalker
97
97
private $ descriptorRegistry ;
98
98
99
99
/** @var bool */
100
- private $ isAggregated ;
100
+ private $ hasAggregateFunction ;
101
+
102
+ /** @var bool */
103
+ private $ hasGroupByClause ;
101
104
102
105
/**
103
106
* @param Query<mixed> $query
@@ -125,7 +128,8 @@ public function __construct($query, $parserResult, array $queryComponents)
125
128
$ this ->em = $ query ->getEntityManager ();
126
129
$ this ->queryComponents = $ queryComponents ;
127
130
$ this ->nullableQueryComponents = [];
128
- $ this ->isAggregated = false ;
131
+ $ this ->hasAggregateFunction = false ;
132
+ $ this ->hasGroupByClause = false ;
129
133
130
134
// The object is instantiated by Doctrine\ORM\Query\Parser, so receiving
131
135
// dependencies through the constructor is not an option. Instead, we
@@ -166,7 +170,8 @@ public function __construct($query, $parserResult, array $queryComponents)
166
170
public function walkSelectStatement (AST \SelectStatement $ AST )
167
171
{
168
172
$ this ->typeBuilder ->setSelectQuery ();
169
- $ this ->isAggregated = $ this ->isAggregated ($ AST );
173
+ $ this ->hasAggregateFunction = $ this ->hasAggregateFunction ($ AST );
174
+ $ this ->hasGroupByClause = $ AST ->groupByClause !== null ;
170
175
171
176
$ this ->walkFromClause ($ AST ->fromClause );
172
177
@@ -231,7 +236,9 @@ public function walkPathExpression($pathExpr)
231
236
232
237
assert (is_string ($ typeName ));
233
238
234
- $ nullable = $ this ->isQueryComponentNullable ($ dqlAlias ) || $ class ->isNullable ($ fieldName ) || $ this ->isAggregated ;
239
+ $ nullable = $ this ->isQueryComponentNullable ($ dqlAlias )
240
+ || $ class ->isNullable ($ fieldName )
241
+ || $ this ->hasAggregateWithoutGroupBy ();
235
242
236
243
$ fieldType = $ this ->resolveDatabaseInternalType ($ typeName , $ nullable );
237
244
@@ -267,7 +274,8 @@ public function walkPathExpression($pathExpr)
267
274
268
275
assert (is_string ($ typeName ));
269
276
270
- $ nullable = (bool ) ($ joinColumn ['nullable ' ] ?? true ) || $ this ->isAggregated ;
277
+ $ nullable = (bool ) ($ joinColumn ['nullable ' ] ?? true )
278
+ || $ this ->hasAggregateWithoutGroupBy ();
271
279
272
280
$ fieldType = $ this ->resolveDatabaseInternalType ($ typeName , $ nullable );
273
281
@@ -699,7 +707,7 @@ public function walkSelectExpression($selectExpression)
699
707
700
708
$ type = new ObjectType ($ class ->name );
701
709
702
- if ($ this ->isQueryComponentNullable ($ dqlAlias ) || $ this ->isAggregated ) {
710
+ if ($ this ->isQueryComponentNullable ($ dqlAlias ) || $ this ->hasAggregateWithoutGroupBy () ) {
703
711
$ type = TypeCombinator::addNull ($ type );
704
712
}
705
713
@@ -725,7 +733,9 @@ public function walkSelectExpression($selectExpression)
725
733
726
734
assert (is_string ($ typeName ));
727
735
728
- $ nullable = $ this ->isQueryComponentNullable ($ dqlAlias ) || $ class ->isNullable ($ fieldName ) || $ this ->isAggregated ;
736
+ $ nullable = $ this ->isQueryComponentNullable ($ dqlAlias )
737
+ || $ class ->isNullable ($ fieldName )
738
+ || $ this ->hasAggregateWithoutGroupBy ();
729
739
730
740
$ type = $ this ->resolveDoctrineType ($ typeName , $ nullable );
731
741
@@ -1288,12 +1298,22 @@ private function toNumericOrNull(Type $type): Type
1288
1298
});
1289
1299
}
1290
1300
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
1292
1311
{
1293
- if ($ AST ->groupByClause !== null ) {
1294
- return true ;
1295
- }
1312
+ return $ this ->hasAggregateFunction && !$ this ->hasGroupByClause ;
1313
+ }
1296
1314
1315
+ private function hasAggregateFunction (AST \SelectStatement $ AST ): bool
1316
+ {
1297
1317
foreach ($ AST ->selectClause ->selectExpressions as $ selectExpression ) {
1298
1318
if (!$ selectExpression instanceof AST \SelectExpression) {
1299
1319
continue ;
0 commit comments