fix(Query): keep all occurrences of a repeated identification variable
Parser::$identVariableExpressions was keyed by alias and held a single SelectExpression per alias, so listing the same identification variable twice in a SELECT clause made the second node overwrite the first. fixIdentificationVariableOrder() only unsets and re-appends the nodes it knows about, so the forgotten first node stayed at the head of the select clause, ahead of its own parent. The hydrator then reached the child before the parent existed in resultPointers, took the "parent object of relation not found" branch in ObjectHydrator and unset the 'fetched' hint for the association. Because the hints are only rebuilt in prepare(), the association stayed marked as not fetched for the rest of the hydration run: the fetch join was silently dropped and every parent's collection lazy loaded, while the query still returned correct data. Track every select expression per identification variable and move all of them when reordering, so duplicates are re-appended together behind their parent, in declaration order. With a single occurrence per alias the inner loop runs once and the behaviour is unchanged.
This commit is contained in:
1 parent
8703a8e5de
commit
2c75209ce3
2 files changed
+36
-7
No files matched your search
+15
-7
@@ -19,6 +19,7 @@ use ReflectionClass;
|
||||
|
||||
use function array_intersect;
|
||||
use function array_key_exists;
|
||||
use function array_keys;
|
||||
use function array_search;
|
||||
use function assert;
|
||||
use function class_exists;
|
||||
@@ -167,7 +168,13 @@ final class Parser
|
||||
*/
|
||||
private $customOutputWalker;
|
||||
|
||||
/** @phpstan-var array<string, AST\SelectExpression> */
|
||||
/**
|
||||
* All select expressions that refer to an identification variable, grouped
|
||||
* by that variable. An alias may be listed more than once in the select
|
||||
* clause, so every occurrence has to be tracked, not just the last one.
|
||||
*
|
||||
* @phpstan-var array<string, non-empty-list<AST\SelectExpression>>
|
||||
*/
|
||||
private array $identVariableExpressions = [];
|
||||
|
||||
/**
|
||||
@@ -405,12 +412,13 @@ final class Parser
|
||||
continue;
|
||||
}
|
||||
|
||||
$expr = $this->identVariableExpressions[$dqlAlias];
|
||||
$key = array_search($expr, $AST->selectClause->selectExpressions, true);
|
||||
foreach ($this->identVariableExpressions[$dqlAlias] as $expr) {
|
||||
$key = array_search($expr, $AST->selectClause->selectExpressions, true);
|
||||
|
||||
unset($AST->selectClause->selectExpressions[$key]);
|
||||
unset($AST->selectClause->selectExpressions[$key]);
|
||||
|
||||
$AST->selectClause->selectExpressions[] = $expr;
|
||||
$AST->selectClause->selectExpressions[] = $expr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -796,7 +804,7 @@ final class Parser
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->identVariableExpressions as $dqlAlias => $expr) {
|
||||
foreach (array_keys($this->identVariableExpressions) as $dqlAlias) {
|
||||
if (isset($this->queryComponents[$dqlAlias]) && ! isset($this->queryComponents[$dqlAlias]['parent'])) {
|
||||
return;
|
||||
}
|
||||
@@ -2290,7 +2298,7 @@ final class Parser
|
||||
$expr = new AST\SelectExpression($expression, $aliasResultVariable, $hiddenAliasResultVariable);
|
||||
|
||||
if ($identVariable) {
|
||||
$this->identVariableExpressions[$identVariable] = $expr;
|
||||
$this->identVariableExpressions[$identVariable][] = $expr;
|
||||
}
|
||||
|
||||
return $expr;
|
||||
|
||||
@@ -222,6 +222,27 @@ class SelectSqlGenerationTest extends OrmTestCase
|
||||
);
|
||||
}
|
||||
|
||||
#[Group('GH12585')]
|
||||
public function testRepeatedIdentificationVariableKeepsSelectClauseInDeclarationOrder(): void
|
||||
{
|
||||
// The repeated collection alias must not be hoisted in front of its own parent,
|
||||
// otherwise the hydrator sees the child before the parent and stops fetch joining.
|
||||
$this->assertSqlGeneration(
|
||||
'SELECT u, p, p FROM Doctrine\Tests\Models\CMS\CmsUser u JOIN u.phonenumbers p',
|
||||
'SELECT c0_.id AS id_0, c0_.status AS status_1, c0_.username AS username_2, c0_.name AS name_3, c1_.phonenumber AS phonenumber_4, c1_.phonenumber AS phonenumber_5, c0_.email_id AS email_id_6, c1_.user_id AS user_id_7 FROM cms_users c0_ INNER JOIN cms_phonenumbers c1_ ON c0_.id = c1_.user_id',
|
||||
);
|
||||
|
||||
$this->assertSqlGeneration(
|
||||
'SELECT u, p, u FROM Doctrine\Tests\Models\CMS\CmsUser u JOIN u.phonenumbers p',
|
||||
'SELECT c0_.id AS id_0, c0_.status AS status_1, c0_.username AS username_2, c0_.name AS name_3, c0_.id AS id_4, c0_.status AS status_5, c0_.username AS username_6, c0_.name AS name_7, c1_.phonenumber AS phonenumber_8, c0_.email_id AS email_id_9, c1_.user_id AS user_id_10 FROM cms_users c0_ INNER JOIN cms_phonenumbers c1_ ON c0_.id = c1_.user_id',
|
||||
);
|
||||
|
||||
$this->assertSqlGeneration(
|
||||
'SELECT u, p, a, p FROM Doctrine\Tests\Models\CMS\CmsUser u JOIN u.phonenumbers p JOIN u.articles a',
|
||||
'SELECT c0_.id AS id_0, c0_.status AS status_1, c0_.username AS username_2, c0_.name AS name_3, c1_.phonenumber AS phonenumber_4, c1_.phonenumber AS phonenumber_5, c2_.id AS id_6, c2_.topic AS topic_7, c2_.text AS text_8, c2_.version AS version_9, c0_.email_id AS email_id_10, c1_.user_id AS user_id_11, c2_.user_id AS user_id_12 FROM cms_users c0_ INNER JOIN cms_phonenumbers c1_ ON c0_.id = c1_.user_id INNER JOIN cms_articles c2_ ON c0_.id = c2_.user_id',
|
||||
);
|
||||
}
|
||||
|
||||
public function testSupportsSelectWithSingleValuedAssociationJoin(): void
|
||||
{
|
||||
$this->assertSqlGeneration(
|
||||
|
||||
Reference in new issue
Block a user