From e7dfa08756f97ac788fc7dfbf5b8898020e67689 Mon Sep 17 00:00:00 2001 From: Alexander Date: Sat, 9 Jun 2012 15:50:34 +0200 Subject: [PATCH 1/4] [PoC] Arbitrary join support --- lib/Doctrine/ORM/Query/AST/Join.php | 8 +- .../AST/JoinAssociationPathExpression.php | 4 +- .../ORM/Query/AST/JoinClassPathExpression.php | 47 ++++ lib/Doctrine/ORM/Query/Parser.php | 95 ++++--- lib/Doctrine/ORM/Query/SqlWalker.php | 256 +++++++++--------- .../ORM/Query/SelectSqlGenerationTest.php | 8 + 6 files changed, 253 insertions(+), 165 deletions(-) create mode 100644 lib/Doctrine/ORM/Query/AST/JoinClassPathExpression.php diff --git a/lib/Doctrine/ORM/Query/AST/Join.php b/lib/Doctrine/ORM/Query/AST/Join.php index 05eb19781..eb561a653 100644 --- a/lib/Doctrine/ORM/Query/AST/Join.php +++ b/lib/Doctrine/ORM/Query/AST/Join.php @@ -40,15 +40,13 @@ class Join extends Node const JOIN_TYPE_INNER = 3; public $joinType = self::JOIN_TYPE_INNER; - public $joinAssociationPathExpression = null; - public $aliasIdentificationVariable = null; + public $joinPathExpression = null; public $conditionalExpression = null; - public function __construct($joinType, $joinAssocPathExpr, $aliasIdentVar) + public function __construct($joinType, $joinPathExpr) { $this->joinType = $joinType; - $this->joinAssociationPathExpression = $joinAssocPathExpr; - $this->aliasIdentificationVariable = $aliasIdentVar; + $this->joinAssociationPathExpression = $joinPathExpr; } public function dispatch($sqlWalker) diff --git a/lib/Doctrine/ORM/Query/AST/JoinAssociationPathExpression.php b/lib/Doctrine/ORM/Query/AST/JoinAssociationPathExpression.php index ccc9c7fd5..6943de9d5 100644 --- a/lib/Doctrine/ORM/Query/AST/JoinAssociationPathExpression.php +++ b/lib/Doctrine/ORM/Query/AST/JoinAssociationPathExpression.php @@ -36,11 +36,13 @@ class JoinAssociationPathExpression extends Node { public $identificationVariable; public $associationField; + public $aliasIdentificationVariable = null; - public function __construct($identificationVariable, $associationField) + public function __construct($identificationVariable, $associationField, $aliasIdentVar) { $this->identificationVariable = $identificationVariable; $this->associationField = $associationField; + $this->aliasIdentificationVariable = $aliasIdentVar; } public function dispatch($sqlWalker) diff --git a/lib/Doctrine/ORM/Query/AST/JoinClassPathExpression.php b/lib/Doctrine/ORM/Query/AST/JoinClassPathExpression.php new file mode 100644 index 000000000..da7b74b23 --- /dev/null +++ b/lib/Doctrine/ORM/Query/AST/JoinClassPathExpression.php @@ -0,0 +1,47 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * JoinClassPathExpression ::= AbstractSchemaName ["AS"] AliasIdentificationVariable + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.3 + * @author Alexander + */ +class JoinClassPathExpression extends Node +{ + public $abstractSchemaName; + public $aliasIdentificationVariable; + + public function __construct($abstractSchemaName, $aliasIdentificationVar) + { + $this->abstractSchemaName = $abstractSchemaName; + $this->aliasIdentificationVariable = $aliasIdentificationVar; + } + + public function dispatch($walker) + { + return $sqlWalker->walkJoinPathExpression($this); + } +} diff --git a/lib/Doctrine/ORM/Query/Parser.php b/lib/Doctrine/ORM/Query/Parser.php index 1af7a021b..0d8b82e83 100644 --- a/lib/Doctrine/ORM/Query/Parser.php +++ b/lib/Doctrine/ORM/Query/Parser.php @@ -905,11 +905,62 @@ class Parser $qComp = $this->_queryComponents[$identVariable]; $class = $qComp['metadata']; - if ( ! isset($class->associationMappings[$field])) { + if ( ! $class->hasAssociation($field)) { $this->semanticalError('Class ' . $class->name . ' has no association named ' . $field); } - return new AST\JoinAssociationPathExpression($identVariable, $field); + if ($this->_lexer->isNextToken(Lexer::T_AS)) { + $this->match(Lexer::T_AS); + } + + $token = $this->_lexer->lookahead; + $aliasIdentificationVariable = $this->AliasIdentificationVariable(); + + // Building queryComponent + $joinQueryComponent = array( + 'metadata' => $this->_em->getClassMetadata($class->associationMappings[$field]['targetEntity']), + 'parent' => $identVariable, + 'relation' => $class->getAssociationMapping($field), + 'map' => null, + 'nestingLevel' => $this->_nestingLevel, + 'token' => $this->_lexer->lookahead + ); + + $this->_queryComponents[$aliasIdentificationVariable] = $joinQueryComponent; + + return new AST\JoinAssociationPathExpression($identVariable, $field, $aliasIdentificationVariable); + } + + /** + * JoinClassPathExpression ::= Class alias + * + * @return \Doctrine\ORM\Query\AST\JoinClassPathExpression + */ + public function JoinClassPathExpression() + { + $abstractSchemaName = $this->AbstractSchemaName(); + + if ($this->_lexer->isNextToken(Lexer::T_AS)) { + $this->match(Lexer::T_AS); + } + + $token = $this->_lexer->lookahead; + $aliasIdentificationVariable = $this->AliasIdentificationVariable(); + $classMetadata = $this->_em->getClassMetadata($abstractSchemaName); + + // Building queryComponent + $queryComponent = array( + 'metadata' => $classMetadata, + 'parent' => null, + 'relation' => null, + 'map' => null, + 'nestingLevel' => $this->_nestingLevel, + 'token' => $token + ); + + $this->_queryComponents[$aliasIdentificationVariable] = $queryComponent; + + return new AST\JoinClassPathExpression($abstractSchemaName, $aliasIdentificationVariable); } /** @@ -1570,41 +1621,19 @@ class Parser $this->match(Lexer::T_JOIN); - $joinPathExpression = $this->JoinAssociationPathExpression(); + $next = $this->_lexer->glimpse(); + if ($next['type'] === Lexer::T_DOT) { + $joinPathExpression = $this->JoinAssociationPathExpression(); + } else { + $joinPathExpression = $this->JoinClassPathExpression(); - if ($this->_lexer->isNextToken(Lexer::T_AS)) { - $this->match(Lexer::T_AS); + if (!$this->_lexer->isNextToken(Lexer::T_WITH)) { + $this->syntaxError('WITH'); + } } - $token = $this->_lexer->lookahead; - $aliasIdentificationVariable = $this->AliasIdentificationVariable(); - - // Verify that the association exists. - $parentClass = $this->_queryComponents[$joinPathExpression->identificationVariable]['metadata']; - $assocField = $joinPathExpression->associationField; - - if ( ! $parentClass->hasAssociation($assocField)) { - $this->semanticalError( - "Class " . $parentClass->name . " has no association named '$assocField'." - ); - } - - $targetClassName = $parentClass->associationMappings[$assocField]['targetEntity']; - - // Building queryComponent - $joinQueryComponent = array( - 'metadata' => $this->_em->getClassMetadata($targetClassName), - 'parent' => $joinPathExpression->identificationVariable, - 'relation' => $parentClass->getAssociationMapping($assocField), - 'map' => null, - 'nestingLevel' => $this->_nestingLevel, - 'token' => $token - ); - - $this->_queryComponents[$aliasIdentificationVariable] = $joinQueryComponent; - // Create AST node - $join = new AST\Join($joinType, $joinPathExpression, $aliasIdentificationVariable); + $join = new AST\Join($joinType, $joinPathExpression); // Check for ad-hoc Join conditions if ($this->_lexer->isNextToken(Lexer::T_WITH)) { diff --git a/lib/Doctrine/ORM/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index 8412025fa..979af059c 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -812,31 +812,138 @@ class SqlWalker implements TreeWalker ? ' LEFT JOIN ' : ' INNER JOIN '; - if ($joinVarDecl->indexBy) { - // For Many-To-One or One-To-One associations this obviously makes no sense, but is ignored silently. - $this->_rsm->addIndexBy( - $joinVarDecl->indexBy->simpleStateFieldPathExpression->identificationVariable, - $joinVarDecl->indexBy->simpleStateFieldPathExpression->field - ); + $joinPathExpr = $join->joinAssociationPathExpression; + $joinedDqlAlias = $joinPathExpr->aliasIdentificationVariable; + + if ($joinPathExpr instanceof \Doctrine\ORM\Query\AST\JoinClassPathExpression) { + $targetClass = $this->_queryComponents[$joinedDqlAlias]['metadata']; + + $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName(), $joinedDqlAlias); + $sql .= $targetClass->getQuotedTableName($this->_platform) . ' ' + . $targetTableAlias; + + $sql .= ' ON (' . $this->walkConditionalExpression($join->conditionalExpression) . ')'; + } else { + + $relation = $this->_queryComponents[$joinedDqlAlias]['relation']; + $targetClass = $this->_em->getClassMetadata($relation['targetEntity']); + $sourceClass = $this->_em->getClassMetadata($relation['sourceEntity']); + $targetTableName = $targetClass->getQuotedTableName($this->_platform); + + $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName(), $joinedDqlAlias); + $sourceTableAlias = $this->getSQLTableAlias($sourceClass->getTableName(), $joinPathExpr->identificationVariable); + + // Ensure we got the owning side, since it has all mapping info + $assoc = ( ! $relation['isOwningSide']) ? $targetClass->associationMappings[$relation['mappedBy']] : $relation; + if ($this->_query->getHint(Query::HINT_INTERNAL_ITERATION) == true && (!$this->_query->getHint(self::HINT_DISTINCT) || isset($this->_selectedClasses[$joinedDqlAlias]))) { + if ($relation['type'] == ClassMetadata::ONE_TO_MANY || $relation['type'] == ClassMetadata::MANY_TO_MANY) { + throw QueryException::iterateWithFetchJoinNotAllowed($assoc); + } + } + + // This condition is not checking ClassMetadata::MANY_TO_ONE, because by definition it cannot + // be the owning side and previously we ensured that $assoc is always the owning side of the associations. + // The owning side is necessary at this point because only it contains the JoinColumn information. + if ($assoc['type'] & ClassMetadata::TO_ONE) { + $sql .= $targetTableName . ' ' . $targetTableAlias . ' ON '; + $first = true; + + foreach ($assoc['sourceToTargetKeyColumns'] as $sourceColumn => $targetColumn) { + if ( ! $first) $sql .= ' AND '; else $first = false; + + if ($relation['isOwningSide']) { + if ($targetClass->containsForeignIdentifier && !isset($targetClass->fieldNames[$targetColumn])) { + $quotedTargetColumn = $targetColumn; // Join columns cannot be quoted. + } else { + $quotedTargetColumn = $targetClass->getQuotedColumnName($targetClass->fieldNames[$targetColumn], $this->_platform); + } + $sql .= $sourceTableAlias . '.' . $sourceColumn . ' = ' . $targetTableAlias . '.' . $quotedTargetColumn; + } else { + if ($sourceClass->containsForeignIdentifier && !isset($sourceClass->fieldNames[$targetColumn])) { + $quotedTargetColumn = $targetColumn; // Join columns cannot be quoted. + } else { + $quotedTargetColumn = $sourceClass->getQuotedColumnName($sourceClass->fieldNames[$targetColumn], $this->_platform); + } + $sql .= $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $targetTableAlias . '.' . $sourceColumn; + } + } + + } else if ($assoc['type'] == ClassMetadata::MANY_TO_MANY) { + // Join relation table + $joinTable = $assoc['joinTable']; + $joinTableAlias = $this->getSQLTableAlias($joinTable['name'], $joinedDqlAlias); + $sql .= $sourceClass->getQuotedJoinTableName($assoc, $this->_platform) . ' ' . $joinTableAlias . ' ON '; + + $first = true; + if ($relation['isOwningSide']) { + foreach ($assoc['relationToSourceKeyColumns'] as $relationColumn => $sourceColumn) { + if ( ! $first) $sql .= ' AND '; else $first = false; + + if ($sourceClass->containsForeignIdentifier && !isset($sourceClass->fieldNames[$sourceColumn])) { + $quotedTargetColumn = $sourceColumn; // Join columns cannot be quoted. + } else { + $quotedTargetColumn = $sourceClass->getQuotedColumnName($sourceClass->fieldNames[$sourceColumn], $this->_platform); + } + + $sql .= $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $relationColumn; + } + } else { + foreach ($assoc['relationToTargetKeyColumns'] as $relationColumn => $targetColumn) { + if ( ! $first) $sql .= ' AND '; else $first = false; + + if ($sourceClass->containsForeignIdentifier && !isset($sourceClass->fieldNames[$targetColumn])) { + $quotedTargetColumn = $targetColumn; // Join columns cannot be quoted. + } else { + $quotedTargetColumn = $sourceClass->getQuotedColumnName($sourceClass->fieldNames[$targetColumn], $this->_platform); + } + + $sql .= $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $relationColumn; + } + } + + // Join target table + $sql .= ($joinType == AST\Join::JOIN_TYPE_LEFT || $joinType == AST\Join::JOIN_TYPE_LEFTOUTER) ? ' LEFT JOIN ' : ' INNER JOIN '; + $sql .= $targetTableName . ' ' . $targetTableAlias . ' ON '; + + $first = true; + if ($relation['isOwningSide']) { + foreach ($assoc['relationToTargetKeyColumns'] as $relationColumn => $targetColumn) { + if ( ! $first) $sql .= ' AND '; else $first = false; + + if ($targetClass->containsForeignIdentifier && !isset($targetClass->fieldNames[$targetColumn])) { + $quotedTargetColumn = $targetColumn; // Join columns cannot be quoted. + } else { + $quotedTargetColumn = $targetClass->getQuotedColumnName($targetClass->fieldNames[$targetColumn], $this->_platform); + } + + $sql .= $targetTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $relationColumn; + } + } else { + foreach ($assoc['relationToSourceKeyColumns'] as $relationColumn => $sourceColumn) { + if ( ! $first) $sql .= ' AND '; else $first = false; + + if ($targetClass->containsForeignIdentifier && !isset($targetClass->fieldNames[$sourceColumn])) { + $quotedTargetColumn = $sourceColumn; // Join columns cannot be quoted. + } else { + $quotedTargetColumn = $targetClass->getQuotedColumnName($targetClass->fieldNames[$sourceColumn], $this->_platform); + } + + $sql .= $targetTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $relationColumn; + } + } + } + + // Handle WITH clause + if (($condExpr = $join->conditionalExpression) !== null) { + // Phase 2 AST optimization: Skip processment of ConditionalExpression + // if only one ConditionalTerm is defined + $sql .= ' AND (' . $this->walkConditionalExpression($condExpr) . ')'; + } } - $joinAssocPathExpr = $join->joinAssociationPathExpression; - $joinedDqlAlias = $join->aliasIdentificationVariable; - - $relation = $this->_queryComponents[$joinedDqlAlias]['relation']; - $targetClass = $this->_em->getClassMetadata($relation['targetEntity']); - $sourceClass = $this->_em->getClassMetadata($relation['sourceEntity']); - $targetTableName = $targetClass->getQuotedTableName($this->_platform); - - $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName(), $joinedDqlAlias); - $sourceTableAlias = $this->getSQLTableAlias($sourceClass->getTableName(), $joinAssocPathExpr->identificationVariable); - - // Ensure we got the owning side, since it has all mapping info - $assoc = ( ! $relation['isOwningSide']) ? $targetClass->associationMappings[$relation['mappedBy']] : $relation; - if ($this->_query->getHint(Query::HINT_INTERNAL_ITERATION) == true && (!$this->_query->getHint(self::HINT_DISTINCT) || isset($this->_selectedClasses[$joinedDqlAlias]))) { - if ($relation['type'] == ClassMetadata::ONE_TO_MANY || $relation['type'] == ClassMetadata::MANY_TO_MANY) { - throw QueryException::iterateWithFetchJoinNotAllowed($assoc); - } + // Apply the filters + if ($filterExpr = $this->generateFilterConditionSQL($targetClass, $targetTableAlias)) { + $sql .= ' AND ' . $filterExpr; } if ($joinVarDecl->indexBy) { @@ -849,109 +956,6 @@ class SqlWalker implements TreeWalker $this->_rsm->addIndexBy($joinedDqlAlias, $relation['indexBy']); } - // This condition is not checking ClassMetadata::MANY_TO_ONE, because by definition it cannot - // be the owning side and previously we ensured that $assoc is always the owning side of the associations. - // The owning side is necessary at this point because only it contains the JoinColumn information. - if ($assoc['type'] & ClassMetadata::TO_ONE) { - $sql .= $targetTableName . ' ' . $targetTableAlias . ' ON '; - $first = true; - - foreach ($assoc['sourceToTargetKeyColumns'] as $sourceColumn => $targetColumn) { - if ( ! $first) $sql .= ' AND '; else $first = false; - - if ($relation['isOwningSide']) { - if ($targetClass->containsForeignIdentifier && !isset($targetClass->fieldNames[$targetColumn])) { - $quotedTargetColumn = $targetColumn; // Join columns cannot be quoted. - } else { - $quotedTargetColumn = $targetClass->getQuotedColumnName($targetClass->fieldNames[$targetColumn], $this->_platform); - } - $sql .= $sourceTableAlias . '.' . $sourceColumn . ' = ' . $targetTableAlias . '.' . $quotedTargetColumn; - } else { - if ($sourceClass->containsForeignIdentifier && !isset($sourceClass->fieldNames[$targetColumn])) { - $quotedTargetColumn = $targetColumn; // Join columns cannot be quoted. - } else { - $quotedTargetColumn = $sourceClass->getQuotedColumnName($sourceClass->fieldNames[$targetColumn], $this->_platform); - } - $sql .= $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $targetTableAlias . '.' . $sourceColumn; - } - } - - } else if ($assoc['type'] == ClassMetadata::MANY_TO_MANY) { - // Join relation table - $joinTable = $assoc['joinTable']; - $joinTableAlias = $this->getSQLTableAlias($joinTable['name'], $joinedDqlAlias); - $sql .= $sourceClass->getQuotedJoinTableName($assoc, $this->_platform) . ' ' . $joinTableAlias . ' ON '; - - $first = true; - if ($relation['isOwningSide']) { - foreach ($assoc['relationToSourceKeyColumns'] as $relationColumn => $sourceColumn) { - if ( ! $first) $sql .= ' AND '; else $first = false; - - if ($sourceClass->containsForeignIdentifier && !isset($sourceClass->fieldNames[$sourceColumn])) { - $quotedTargetColumn = $sourceColumn; // Join columns cannot be quoted. - } else { - $quotedTargetColumn = $sourceClass->getQuotedColumnName($sourceClass->fieldNames[$sourceColumn], $this->_platform); - } - - $sql .= $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $relationColumn; - } - } else { - foreach ($assoc['relationToTargetKeyColumns'] as $relationColumn => $targetColumn) { - if ( ! $first) $sql .= ' AND '; else $first = false; - - if ($sourceClass->containsForeignIdentifier && !isset($sourceClass->fieldNames[$targetColumn])) { - $quotedTargetColumn = $targetColumn; // Join columns cannot be quoted. - } else { - $quotedTargetColumn = $sourceClass->getQuotedColumnName($sourceClass->fieldNames[$targetColumn], $this->_platform); - } - - $sql .= $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $relationColumn; - } - } - - // Join target table - $sql .= ($joinType == AST\Join::JOIN_TYPE_LEFT || $joinType == AST\Join::JOIN_TYPE_LEFTOUTER) ? ' LEFT JOIN ' : ' INNER JOIN '; - $sql .= $targetTableName . ' ' . $targetTableAlias . ' ON '; - - $first = true; - if ($relation['isOwningSide']) { - foreach ($assoc['relationToTargetKeyColumns'] as $relationColumn => $targetColumn) { - if ( ! $first) $sql .= ' AND '; else $first = false; - - if ($targetClass->containsForeignIdentifier && !isset($targetClass->fieldNames[$targetColumn])) { - $quotedTargetColumn = $targetColumn; // Join columns cannot be quoted. - } else { - $quotedTargetColumn = $targetClass->getQuotedColumnName($targetClass->fieldNames[$targetColumn], $this->_platform); - } - - $sql .= $targetTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $relationColumn; - } - } else { - foreach ($assoc['relationToSourceKeyColumns'] as $relationColumn => $sourceColumn) { - if ( ! $first) $sql .= ' AND '; else $first = false; - - if ($targetClass->containsForeignIdentifier && !isset($targetClass->fieldNames[$sourceColumn])) { - $quotedTargetColumn = $sourceColumn; // Join columns cannot be quoted. - } else { - $quotedTargetColumn = $targetClass->getQuotedColumnName($targetClass->fieldNames[$sourceColumn], $this->_platform); - } - - $sql .= $targetTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $relationColumn; - } - } - } - - // Apply the filters - if ($filterExpr = $this->generateFilterConditionSQL($targetClass, $targetTableAlias)) { - $sql .= ' AND ' . $filterExpr; - } - - // Handle WITH clause - if (($condExpr = $join->conditionalExpression) !== null) { - // Phase 2 AST optimization: Skip processment of ConditionalExpression - // if only one ConditionalTerm is defined - $sql .= ' AND (' . $this->walkConditionalExpression($condExpr) . ')'; - } $discrSql = $this->_generateDiscriminatorColumnConditionSQL(array($joinedDqlAlias)); diff --git a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php index 9a998580b..09fc39b9d 100644 --- a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php +++ b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php @@ -135,6 +135,14 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase ); } + public function testSupportsJoinOnMultipleComponents() + { + $this->assertSqlGeneration( + 'SELECT u, p FROM Doctrine\Tests\Models\CMS\CmsUser u JOIN Doctrine\Tests\Models\CMS\CmsPhonenumber p WITH u = p.user', + 'SELECT c0_.id AS id0, c0_.status AS status1, c0_.username AS username2, c0_.name AS name3, c1_.phonenumber AS phonenumber4 FROM cms_users c0_ INNER JOIN cms_phonenumbers c1_ ON (c0_.id = c1_.user_id)' + ); + } + public function testSupportsSelectWithCollectionAssociationJoin() { $this->assertSqlGeneration( From 41a650b699f2761b7b43c51d47d2c060c6004b0a Mon Sep 17 00:00:00 2001 From: Guilherme Blanco Date: Sun, 10 Jun 2012 22:14:25 +0200 Subject: [PATCH 2/4] Updated PoC for multiple components DQL support. --- UPGRADE_TO_2_3 | 7 +- .../AST/IdentificationVariableDeclaration.php | 6 +- lib/Doctrine/ORM/Query/AST/Join.php | 12 +- ...ion.php => JoinAssociationDeclaration.php} | 29 +- .../AST/JoinAssociationPathExpression.php | 10 +- lib/Doctrine/ORM/Query/Parser.php | 210 ++++------ lib/Doctrine/ORM/Query/SqlWalker.php | 382 +++++++++--------- lib/Doctrine/ORM/Query/TreeWalker.php | 6 +- lib/Doctrine/ORM/Query/TreeWalkerAdapter.php | 6 +- lib/Doctrine/ORM/Query/TreeWalkerChain.php | 8 +- .../ORM/Query/LanguageRecognitionTest.php | 5 + 11 files changed, 330 insertions(+), 351 deletions(-) rename lib/Doctrine/ORM/Query/AST/{JoinVariableDeclaration.php => JoinAssociationDeclaration.php} (56%) diff --git a/UPGRADE_TO_2_3 b/UPGRADE_TO_2_3 index 28e734a84..39b0d1f9d 100644 --- a/UPGRADE_TO_2_3 +++ b/UPGRADE_TO_2_3 @@ -24,4 +24,9 @@ Also, related functions were affected: * iterate($parameters, $hydrationMode) the argument $parameters can be either an key=>value array or an ArrayCollection instance * setParameters($parameters) the argument $parameters can be either an key=>value array or an ArrayCollection instance * getParameters() now returns ArrayCollection instead of array -* getParameter($key) now returns Parameter instance instead of parameter value \ No newline at end of file +* getParameter($key) now returns Parameter instance instead of parameter value + +# Query TreeWalker method renamed + +Internal changes were made to DQL and SQL generation. If you have implemented your own TreeWalker, +you probably need to update it. The method walkJoinVariableDeclaration is now named walkJoin. diff --git a/lib/Doctrine/ORM/Query/AST/IdentificationVariableDeclaration.php b/lib/Doctrine/ORM/Query/AST/IdentificationVariableDeclaration.php index 962afbf95..48961ee2a 100644 --- a/lib/Doctrine/ORM/Query/AST/IdentificationVariableDeclaration.php +++ b/lib/Doctrine/ORM/Query/AST/IdentificationVariableDeclaration.php @@ -36,13 +36,13 @@ class IdentificationVariableDeclaration extends Node { public $rangeVariableDeclaration = null; public $indexBy = null; - public $joinVariableDeclarations = array(); + public $joins = array(); - public function __construct($rangeVariableDecl, $indexBy, array $joinVariableDecls) + public function __construct($rangeVariableDecl, $indexBy, array $joins) { $this->rangeVariableDeclaration = $rangeVariableDecl; $this->indexBy = $indexBy; - $this->joinVariableDeclarations = $joinVariableDecls; + $this->joins = $joins; } public function dispatch($sqlWalker) diff --git a/lib/Doctrine/ORM/Query/AST/Join.php b/lib/Doctrine/ORM/Query/AST/Join.php index eb561a653..7b8601d9b 100644 --- a/lib/Doctrine/ORM/Query/AST/Join.php +++ b/lib/Doctrine/ORM/Query/AST/Join.php @@ -25,28 +25,26 @@ namespace Doctrine\ORM\Query\AST; * Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN" JoinAssociationPathExpression * ["AS"] AliasIdentificationVariable [("ON" | "WITH") ConditionalExpression] * - * * @link www.doctrine-project.org * @since 2.0 - * @version $Revision: 3938 $ * @author Guilherme Blanco * @author Jonathan Wage * @author Roman Borschel */ class Join extends Node { - const JOIN_TYPE_LEFT = 1; + const JOIN_TYPE_LEFT = 1; const JOIN_TYPE_LEFTOUTER = 2; - const JOIN_TYPE_INNER = 3; + const JOIN_TYPE_INNER = 3; public $joinType = self::JOIN_TYPE_INNER; - public $joinPathExpression = null; + public $joinAssociationDeclaration = null; public $conditionalExpression = null; - public function __construct($joinType, $joinPathExpr) + public function __construct($joinType, $joinAssociationDeclaration) { $this->joinType = $joinType; - $this->joinAssociationPathExpression = $joinPathExpr; + $this->joinAssociationDeclaration = $joinAssociationDeclaration; } public function dispatch($sqlWalker) diff --git a/lib/Doctrine/ORM/Query/AST/JoinVariableDeclaration.php b/lib/Doctrine/ORM/Query/AST/JoinAssociationDeclaration.php similarity index 56% rename from lib/Doctrine/ORM/Query/AST/JoinVariableDeclaration.php rename to lib/Doctrine/ORM/Query/AST/JoinAssociationDeclaration.php index 203d314d5..41cdc03d0 100644 --- a/lib/Doctrine/ORM/Query/AST/JoinVariableDeclaration.php +++ b/lib/Doctrine/ORM/Query/AST/JoinAssociationDeclaration.php @@ -15,36 +15,39 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * This software consists of voluntary contributions made by many individuals +<<<<<<< HEAD:lib/Doctrine/ORM/Query/AST/JoinVariableDeclaration.php * and is licensed under the MIT license. For more information, see * . +======= + * and is licensed under the LGPL. For more information, see + * . +>>>>>>> Updated PoC for multiple components DQL support.:lib/Doctrine/ORM/Query/AST/JoinAssociationDeclaration.php */ namespace Doctrine\ORM\Query\AST; /** - * JoinVariableDeclaration ::= Join [IndexBy] + * JoinAssociationDeclaration ::= JoinAssociationPathExpression ["AS"] AliasIdentificationVariable * - * * @link www.doctrine-project.org - * @since 2.0 - * @version $Revision: 3938 $ + * @since 2.3 * @author Guilherme Blanco - * @author Jonathan Wage - * @author Roman Borschel */ -class JoinVariableDeclaration extends Node +class JoinAssociationDeclaration extends Node { - public $join = null; - public $indexBy = null; + public $joinAssociationPathExpression; + public $aliasIdentificationVariable; + public $indexBy; - public function __construct($join, $indexBy) + public function __construct($joinAssociationPathExpression, $aliasIdentificationVariable, $indexBy) { - $this->join = $join; - $this->indexBy = $indexBy; + $this->joinAssociationPathExpression = $joinAssociationPathExpression; + $this->aliasIdentificationVariable = $aliasIdentificationVariable; + $this->indexBy = $indexBy; } public function dispatch($sqlWalker) { - return $sqlWalker->walkJoinVariableDeclaration($this); + return $sqlWalker->walkJoinAssociationDeclaration($this); } } diff --git a/lib/Doctrine/ORM/Query/AST/JoinAssociationPathExpression.php b/lib/Doctrine/ORM/Query/AST/JoinAssociationPathExpression.php index 6943de9d5..9307a7cf4 100644 --- a/lib/Doctrine/ORM/Query/AST/JoinAssociationPathExpression.php +++ b/lib/Doctrine/ORM/Query/AST/JoinAssociationPathExpression.php @@ -24,10 +24,8 @@ namespace Doctrine\ORM\Query\AST; /** * JoinAssociationPathExpression ::= IdentificationVariable "." (SingleValuedAssociationField | CollectionValuedAssociationField) * - * * @link www.doctrine-project.org * @since 2.0 - * @version $Revision: 3938 $ * @author Guilherme Blanco * @author Jonathan Wage * @author Roman Borschel @@ -36,17 +34,15 @@ class JoinAssociationPathExpression extends Node { public $identificationVariable; public $associationField; - public $aliasIdentificationVariable = null; - public function __construct($identificationVariable, $associationField, $aliasIdentVar) + public function __construct($identificationVariable, $associationField) { $this->identificationVariable = $identificationVariable; - $this->associationField = $associationField; - $this->aliasIdentificationVariable = $aliasIdentVar; + $this->associationField = $associationField; } public function dispatch($sqlWalker) { - return $sqlWalker->walkJoinPathExpression($this); + return $sqlWalker->walkPathExpression($this); } } diff --git a/lib/Doctrine/ORM/Query/Parser.php b/lib/Doctrine/ORM/Query/Parser.php index 0d8b82e83..fcf021cc0 100644 --- a/lib/Doctrine/ORM/Query/Parser.php +++ b/lib/Doctrine/ORM/Query/Parser.php @@ -909,58 +909,7 @@ class Parser $this->semanticalError('Class ' . $class->name . ' has no association named ' . $field); } - if ($this->_lexer->isNextToken(Lexer::T_AS)) { - $this->match(Lexer::T_AS); - } - - $token = $this->_lexer->lookahead; - $aliasIdentificationVariable = $this->AliasIdentificationVariable(); - - // Building queryComponent - $joinQueryComponent = array( - 'metadata' => $this->_em->getClassMetadata($class->associationMappings[$field]['targetEntity']), - 'parent' => $identVariable, - 'relation' => $class->getAssociationMapping($field), - 'map' => null, - 'nestingLevel' => $this->_nestingLevel, - 'token' => $this->_lexer->lookahead - ); - - $this->_queryComponents[$aliasIdentificationVariable] = $joinQueryComponent; - - return new AST\JoinAssociationPathExpression($identVariable, $field, $aliasIdentificationVariable); - } - - /** - * JoinClassPathExpression ::= Class alias - * - * @return \Doctrine\ORM\Query\AST\JoinClassPathExpression - */ - public function JoinClassPathExpression() - { - $abstractSchemaName = $this->AbstractSchemaName(); - - if ($this->_lexer->isNextToken(Lexer::T_AS)) { - $this->match(Lexer::T_AS); - } - - $token = $this->_lexer->lookahead; - $aliasIdentificationVariable = $this->AliasIdentificationVariable(); - $classMetadata = $this->_em->getClassMetadata($abstractSchemaName); - - // Building queryComponent - $queryComponent = array( - 'metadata' => $classMetadata, - 'parent' => null, - 'relation' => null, - 'map' => null, - 'nestingLevel' => $this->_nestingLevel, - 'token' => $token - ); - - $this->_queryComponents[$aliasIdentificationVariable] = $queryComponent; - - return new AST\JoinClassPathExpression($abstractSchemaName, $aliasIdentificationVariable); + return new AST\JoinAssociationPathExpression($identVariable, $field); } /** @@ -1452,7 +1401,7 @@ class Parser } /** - * IdentificationVariableDeclaration ::= RangeVariableDeclaration [IndexBy] {JoinVariableDeclaration}* + * IdentificationVariableDeclaration ::= RangeVariableDeclaration [IndexBy] {Join}* * * @return \Doctrine\ORM\Query\AST\IdentificationVariableDeclaration */ @@ -1460,18 +1409,18 @@ class Parser { $rangeVariableDeclaration = $this->RangeVariableDeclaration(); $indexBy = $this->_lexer->isNextToken(Lexer::T_INDEX) ? $this->IndexBy() : null; - $joinVariableDeclarations = array(); + $joins = array(); while ( $this->_lexer->isNextToken(Lexer::T_LEFT) || $this->_lexer->isNextToken(Lexer::T_INNER) || $this->_lexer->isNextToken(Lexer::T_JOIN) ) { - $joinVariableDeclarations[] = $this->JoinVariableDeclaration(); + $joins[] = $this->Join(); } return new AST\IdentificationVariableDeclaration( - $rangeVariableDeclaration, $indexBy, $joinVariableDeclarations + $rangeVariableDeclaration, $indexBy, $joins ); } @@ -1501,16 +1450,57 @@ class Parser } /** - * JoinVariableDeclaration ::= Join [IndexBy] + * Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN" + * (JoinAssociationDeclaration | RangeVariableDeclaration) + * ["WITH" ConditionalExpression] * - * @return \Doctrine\ORM\Query\AST\JoinVariableDeclaration + * @return \Doctrine\ORM\Query\AST\Join */ - public function JoinVariableDeclaration() + public function Join() { - $join = $this->Join(); - $indexBy = $this->_lexer->isNextToken(Lexer::T_INDEX) ? $this->IndexBy() : null; + // Check Join type + $joinType = AST\Join::JOIN_TYPE_INNER; - return new AST\JoinVariableDeclaration($join, $indexBy); + switch (true) { + case ($this->_lexer->isNextToken(Lexer::T_LEFT)): + $this->match(Lexer::T_LEFT); + + $joinType = AST\Join::JOIN_TYPE_LEFT; + + // Possible LEFT OUTER join + if ($this->_lexer->isNextToken(Lexer::T_OUTER)) { + $this->match(Lexer::T_OUTER); + + $joinType = AST\Join::JOIN_TYPE_LEFTOUTER; + } + break; + + case ($this->_lexer->isNextToken(Lexer::T_INNER)): + $this->match(Lexer::T_INNER); + break; + + default: + // Do nothing + } + + $this->match(Lexer::T_JOIN); + + $next = $this->_lexer->glimpse(); + $joinDeclaration = ($next['type'] === Lexer::T_DOT) + ? $this->JoinAssociationDeclaration() + : $this->RangeVariableDeclaration(); + + // Create AST node + $join = new AST\Join($joinType, $joinDeclaration); + + // Check for ad-hoc Join conditions + if ($this->_lexer->isNextToken(Lexer::T_WITH) || $joinDeclaration instanceof AST\RangeVariableDeclaration) { + $this->match(Lexer::T_WITH); + + $join->conditionalExpression = $this->ConditionalExpression(); + } + + return $join; } /** @@ -1545,6 +1535,43 @@ class Parser return new AST\RangeVariableDeclaration($abstractSchemaName, $aliasIdentificationVariable); } + /** + * JoinAssociationDeclaration ::= JoinAssociationPathExpression ["AS"] AliasIdentificationVariable [IndexBy] + * + * @return \Doctrine\ORM\Query\AST\JoinAssociationPathExpression + */ + public function JoinAssociationDeclaration() + { + $joinAssociationPathExpression = $this->JoinAssociationPathExpression(); + + if ($this->_lexer->isNextToken(Lexer::T_AS)) { + $this->match(Lexer::T_AS); + } + + $aliasIdentificationVariable = $this->AliasIdentificationVariable(); + $indexBy = $this->_lexer->isNextToken(Lexer::T_INDEX) ? $this->IndexBy() : null; + + $identificationVariable = $joinAssociationPathExpression->identificationVariable; + $field = $joinAssociationPathExpression->associationField; + + $class = $this->_queryComponents[$identificationVariable]['metadata']; + $targetClass = $this->_em->getClassMetadata($class->associationMappings[$field]['targetEntity']); + + // Building queryComponent + $joinQueryComponent = array( + 'metadata' => $targetClass, + 'parent' => $joinAssociationPathExpression->identificationVariable, + 'relation' => $class->getAssociationMapping($field), + 'map' => null, + 'nestingLevel' => $this->_nestingLevel, + 'token' => $this->_lexer->lookahead + ); + + $this->_queryComponents[$aliasIdentificationVariable] = $joinQueryComponent; + + return new AST\JoinAssociationDeclaration($joinAssociationPathExpression, $aliasIdentificationVariable, $indexBy); + } + /** * PartialObjectExpression ::= "PARTIAL" IdentificationVariable "." PartialFieldSet * PartialFieldSet ::= "{" SimpleStateField {"," SimpleStateField}* "}" @@ -1586,65 +1613,6 @@ class Parser return $partialObjectExpression; } - /** - * Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN" JoinAssociationPathExpression - * ["AS"] AliasIdentificationVariable ["WITH" ConditionalExpression] - * - * @return \Doctrine\ORM\Query\AST\Join - */ - public function Join() - { - // Check Join type - $joinType = AST\Join::JOIN_TYPE_INNER; - - switch (true) { - case ($this->_lexer->isNextToken(Lexer::T_LEFT)): - $this->match(Lexer::T_LEFT); - - $joinType = AST\Join::JOIN_TYPE_LEFT; - - // Possible LEFT OUTER join - if ($this->_lexer->isNextToken(Lexer::T_OUTER)) { - $this->match(Lexer::T_OUTER); - - $joinType = AST\Join::JOIN_TYPE_LEFTOUTER; - } - break; - - case ($this->_lexer->isNextToken(Lexer::T_INNER)): - $this->match(Lexer::T_INNER); - break; - - default: - // Do nothing - } - - $this->match(Lexer::T_JOIN); - - $next = $this->_lexer->glimpse(); - if ($next['type'] === Lexer::T_DOT) { - $joinPathExpression = $this->JoinAssociationPathExpression(); - } else { - $joinPathExpression = $this->JoinClassPathExpression(); - - if (!$this->_lexer->isNextToken(Lexer::T_WITH)) { - $this->syntaxError('WITH'); - } - } - - // Create AST node - $join = new AST\Join($joinType, $joinPathExpression); - - // Check for ad-hoc Join conditions - if ($this->_lexer->isNextToken(Lexer::T_WITH)) { - $this->match(Lexer::T_WITH); - - $join->conditionalExpression = $this->ConditionalExpression(); - } - - return $join; - } - /** * IndexBy ::= "INDEX" "BY" StateFieldPathExpression * diff --git a/lib/Doctrine/ORM/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index 979af059c..2d034ddcd 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -705,23 +705,10 @@ class SqlWalker implements TreeWalker $sqlParts = array(); foreach ($identificationVarDecls as $identificationVariableDecl) { - $sql = ''; + $sql = $this->walkRangeVariableDeclaration($identificationVariableDecl->rangeVariableDeclaration); - $rangeDecl = $identificationVariableDecl->rangeVariableDeclaration; - $dqlAlias = $rangeDecl->aliasIdentificationVariable; - - $this->_rootAliases[] = $dqlAlias; - - $class = $this->_em->getClassMetadata($rangeDecl->abstractSchemaName); - $sql .= $class->getQuotedTableName($this->_platform) . ' ' - . $this->getSQLTableAlias($class->getTableName(), $dqlAlias); - - if ($class->isInheritanceTypeJoined()) { - $sql .= $this->_generateClassTableInheritanceJoins($class, $dqlAlias); - } - - foreach ($identificationVariableDecl->joinVariableDeclarations as $joinVarDecl) { - $sql .= $this->walkJoinVariableDeclaration($joinVarDecl); + foreach ($identificationVariableDecl->joins as $join) { + $sql .= $this->walkJoin($join); } if ($identificationVariableDecl->indexBy) { @@ -744,6 +731,174 @@ class SqlWalker implements TreeWalker return ' FROM ' . implode(', ', $sqlParts); } + /** + * Walks down a RangeVariableDeclaration AST node, thereby generating the appropriate SQL. + * + * @return string + */ + public function walkRangeVariableDeclaration($rangeVariableDeclaration) + { + $class = $this->_em->getClassMetadata($rangeVariableDeclaration->abstractSchemaName); + $dqlAlias = $rangeVariableDeclaration->aliasIdentificationVariable; + + $this->_rootAliases[] = $dqlAlias; + + $sql = $class->getQuotedTableName($this->_platform) . ' ' + . $this->getSQLTableAlias($class->getTableName(), $dqlAlias); + + if ($class->isInheritanceTypeJoined()) { + $sql .= $this->_generateClassTableInheritanceJoins($class, $dqlAlias); + } + + return $sql; + } + + /** + * Walks down a JoinAssociationDeclaration AST node, thereby generating the appropriate SQL. + * + * @return string + */ + public function walkJoinAssociationDeclaration($joinAssociationDeclaration, $joinType = AST\Join::JOIN_TYPE_INNER) + { + $sql = ''; + + $associationPathExpression = $joinAssociationDeclaration->joinAssociationPathExpression; + $joinedDqlAlias = $joinAssociationDeclaration->aliasIdentificationVariable; + $indexBy = $joinAssociationDeclaration->indexBy; + + $relation = $this->_queryComponents[$joinedDqlAlias]['relation']; + $targetClass = $this->_em->getClassMetadata($relation['targetEntity']); + $sourceClass = $this->_em->getClassMetadata($relation['sourceEntity']); + $targetTableName = $targetClass->getQuotedTableName($this->_platform); + + $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName(), $joinedDqlAlias); + $sourceTableAlias = $this->getSQLTableAlias($sourceClass->getTableName(), $associationPathExpression->identificationVariable); + + // Ensure we got the owning side, since it has all mapping info + $assoc = ( ! $relation['isOwningSide']) ? $targetClass->associationMappings[$relation['mappedBy']] : $relation; + + if ($this->_query->getHint(Query::HINT_INTERNAL_ITERATION) == true && (!$this->_query->getHint(self::HINT_DISTINCT) || isset($this->_selectedClasses[$joinedDqlAlias]))) { + if ($relation['type'] == ClassMetadata::ONE_TO_MANY || $relation['type'] == ClassMetadata::MANY_TO_MANY) { + throw QueryException::iterateWithFetchJoinNotAllowed($assoc); + } + } + + // This condition is not checking ClassMetadata::MANY_TO_ONE, because by definition it cannot + // be the owning side and previously we ensured that $assoc is always the owning side of the associations. + // The owning side is necessary at this point because only it contains the JoinColumn information. + switch (true) { + case ($assoc['type'] & ClassMetadata::TO_ONE): + $conditions = array(); + + foreach ($assoc['sourceToTargetKeyColumns'] as $sourceColumn => $targetColumn) { + if ($relation['isOwningSide']) { + $quotedTargetColumn = ($targetClass->containsForeignIdentifier && !isset($targetClass->fieldNames[$targetColumn])) + ? $targetColumn // Join columns cannot be quoted. + : $targetClass->getQuotedColumnName($targetClass->fieldNames[$targetColumn], $this->_platform); + + $conditions[] = $sourceTableAlias . '.' . $sourceColumn . ' = ' . $targetTableAlias . '.' . $quotedTargetColumn; + + continue; + } + + $quotedTargetColumn = ($sourceClass->containsForeignIdentifier && !isset($sourceClass->fieldNames[$targetColumn])) + ? $targetColumn // Join columns cannot be quoted. + : $sourceClass->getQuotedColumnName($sourceClass->fieldNames[$targetColumn], $this->_platform); + + $conditions[] = $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $targetTableAlias . '.' . $sourceColumn; + } + + // Apply remaining inheritance restrictions + $discrSql = $this->_generateDiscriminatorColumnConditionSQL(array($joinedDqlAlias)); + + if ($discrSql) { + $conditions[] = $discrSql; + } + + // Apply the filters + $filterExpr = $this->generateFilterConditionSQL($targetClass, $targetTableAlias); + + if ($filterExpr) { + $conditions[] = $filterExpr; + } + + $sql .= $targetTableName . ' ' . $targetTableAlias . ' ON ' . implode(' AND ', $conditions); + break; + + case ($assoc['type'] == ClassMetadata::MANY_TO_MANY): + // Join relation table + $joinTable = $assoc['joinTable']; + $joinTableAlias = $this->getSQLTableAlias($joinTable['name'], $joinedDqlAlias); + $joinTableName = $sourceClass->getQuotedJoinTableName($assoc, $this->_platform); + + $conditions = array(); + $relationColumns = ($relation['isOwningSide']) + ? $assoc['relationToSourceKeyColumns'] + : $assoc['relationToTargetKeyColumns']; + + foreach ($relationColumns as $relationColumn => $sourceColumn) { + $quotedTargetColumn = ($sourceClass->containsForeignIdentifier && !isset($sourceClass->fieldNames[$sourceColumn])) + ? $sourceColumn // Join columns cannot be quoted. + : $sourceClass->getQuotedColumnName($sourceClass->fieldNames[$sourceColumn], $this->_platform); + + $conditions[] = $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $relationColumn; + } + + $sql .= $joinTableName . ' ' . $joinTableAlias . ' ON ' . implode(' AND ', $conditions); + + // Join target table + $sql .= ($joinType == AST\Join::JOIN_TYPE_LEFT || $joinType == AST\Join::JOIN_TYPE_LEFTOUTER) ? ' LEFT JOIN ' : ' INNER JOIN '; + + $conditions = array(); + $relationColumns = ($relation['isOwningSide']) + ? $assoc['relationToTargetKeyColumns'] + : $assoc['relationToSourceKeyColumns']; + + foreach ($relationColumns as $relationColumn => $targetColumn) { + $quotedTargetColumn = ($targetClass->containsForeignIdentifier && !isset($targetClass->fieldNames[$targetColumn])) + ? $targetColumn // Join columns cannot be quoted. + : $targetClass->getQuotedColumnName($targetClass->fieldNames[$targetColumn], $this->_platform); + + $conditions[] = $targetTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $relationColumn; + } + + // Apply remaining inheritance restrictions + $discrSql = $this->_generateDiscriminatorColumnConditionSQL(array($joinedDqlAlias)); + + if ($discrSql) { + $conditions[] = $discrSql; + } + + // Apply the filters + $filterExpr = $this->generateFilterConditionSQL($targetClass, $targetTableAlias); + + if ($filterExpr) { + $conditions[] = $filterExpr; + } + + $sql .= $targetTableName . ' ' . $targetTableAlias . ' ON ' . implode(' AND ', $conditions); + break; + } + + // FIXME: these should either be nested or all forced to be left joins (DDC-XXX) + if ($targetClass->isInheritanceTypeJoined()) { + $sql .= $this->_generateClassTableInheritanceJoins($targetClass, $joinedDqlAlias); + } + + // Apply the indexes + if ($indexBy) { + // For Many-To-One or One-To-One associations this obviously makes no sense, but is ignored silently. + $this->_rsm->addIndexBy( + $indexBy->simpleStateFieldPathExpression->identificationVariable, + $indexBy->simpleStateFieldPathExpression->field + ); + } else if (isset($relation['indexBy'])) { + $this->_rsm->addIndexBy($joinedDqlAlias, $relation['indexBy']); + } + + return $sql; + } + /** * Walks down a FunctionNode AST node, thereby generating the appropriate SQL. * @@ -799,173 +954,35 @@ class SqlWalker implements TreeWalker } /** - * Walks down a JoinVariableDeclaration AST node and creates the corresponding SQL. + * Walks down a Join AST node and creates the corresponding SQL. * - * @param JoinVariableDeclaration $joinVarDecl * @return string The SQL. */ - public function walkJoinVariableDeclaration($joinVarDecl) + public function walkJoin($join) { - $join = $joinVarDecl->join; - $joinType = $join->joinType; - $sql = ($joinType == AST\Join::JOIN_TYPE_LEFT || $joinType == AST\Join::JOIN_TYPE_LEFTOUTER) + $joinType = $join->joinType; + $joinDeclaration = $join->joinAssociationDeclaration; + + $sql = ($joinType == AST\Join::JOIN_TYPE_LEFT || $joinType == AST\Join::JOIN_TYPE_LEFTOUTER) ? ' LEFT JOIN ' : ' INNER JOIN '; - $joinPathExpr = $join->joinAssociationPathExpression; - $joinedDqlAlias = $joinPathExpr->aliasIdentificationVariable; + switch (true) { + case ($joinDeclaration instanceof \Doctrine\ORM\Query\AST\RangeVariableDeclaration): + $sql .= $this->walkRangeVariableDeclaration($joinDeclaration) + . ' ON (' . $this->walkConditionalExpression($join->conditionalExpression) . ')'; + break; - if ($joinPathExpr instanceof \Doctrine\ORM\Query\AST\JoinClassPathExpression) { - $targetClass = $this->_queryComponents[$joinedDqlAlias]['metadata']; + case ($joinDeclaration instanceof \Doctrine\ORM\Query\AST\JoinAssociationDeclaration): + $sql .= $this->walkJoinAssociationDeclaration($joinDeclaration, $joinType); - $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName(), $joinedDqlAlias); - $sql .= $targetClass->getQuotedTableName($this->_platform) . ' ' - . $targetTableAlias; - - $sql .= ' ON (' . $this->walkConditionalExpression($join->conditionalExpression) . ')'; - } else { - - $relation = $this->_queryComponents[$joinedDqlAlias]['relation']; - $targetClass = $this->_em->getClassMetadata($relation['targetEntity']); - $sourceClass = $this->_em->getClassMetadata($relation['sourceEntity']); - $targetTableName = $targetClass->getQuotedTableName($this->_platform); - - $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName(), $joinedDqlAlias); - $sourceTableAlias = $this->getSQLTableAlias($sourceClass->getTableName(), $joinPathExpr->identificationVariable); - - // Ensure we got the owning side, since it has all mapping info - $assoc = ( ! $relation['isOwningSide']) ? $targetClass->associationMappings[$relation['mappedBy']] : $relation; - if ($this->_query->getHint(Query::HINT_INTERNAL_ITERATION) == true && (!$this->_query->getHint(self::HINT_DISTINCT) || isset($this->_selectedClasses[$joinedDqlAlias]))) { - if ($relation['type'] == ClassMetadata::ONE_TO_MANY || $relation['type'] == ClassMetadata::MANY_TO_MANY) { - throw QueryException::iterateWithFetchJoinNotAllowed($assoc); + // Handle WITH clause + if (($condExpr = $join->conditionalExpression) !== null) { + // Phase 2 AST optimization: Skip processment of ConditionalExpression + // if only one ConditionalTerm is defined + $sql .= ' AND (' . $this->walkConditionalExpression($condExpr) . ')'; } - } - - // This condition is not checking ClassMetadata::MANY_TO_ONE, because by definition it cannot - // be the owning side and previously we ensured that $assoc is always the owning side of the associations. - // The owning side is necessary at this point because only it contains the JoinColumn information. - if ($assoc['type'] & ClassMetadata::TO_ONE) { - $sql .= $targetTableName . ' ' . $targetTableAlias . ' ON '; - $first = true; - - foreach ($assoc['sourceToTargetKeyColumns'] as $sourceColumn => $targetColumn) { - if ( ! $first) $sql .= ' AND '; else $first = false; - - if ($relation['isOwningSide']) { - if ($targetClass->containsForeignIdentifier && !isset($targetClass->fieldNames[$targetColumn])) { - $quotedTargetColumn = $targetColumn; // Join columns cannot be quoted. - } else { - $quotedTargetColumn = $targetClass->getQuotedColumnName($targetClass->fieldNames[$targetColumn], $this->_platform); - } - $sql .= $sourceTableAlias . '.' . $sourceColumn . ' = ' . $targetTableAlias . '.' . $quotedTargetColumn; - } else { - if ($sourceClass->containsForeignIdentifier && !isset($sourceClass->fieldNames[$targetColumn])) { - $quotedTargetColumn = $targetColumn; // Join columns cannot be quoted. - } else { - $quotedTargetColumn = $sourceClass->getQuotedColumnName($sourceClass->fieldNames[$targetColumn], $this->_platform); - } - $sql .= $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $targetTableAlias . '.' . $sourceColumn; - } - } - - } else if ($assoc['type'] == ClassMetadata::MANY_TO_MANY) { - // Join relation table - $joinTable = $assoc['joinTable']; - $joinTableAlias = $this->getSQLTableAlias($joinTable['name'], $joinedDqlAlias); - $sql .= $sourceClass->getQuotedJoinTableName($assoc, $this->_platform) . ' ' . $joinTableAlias . ' ON '; - - $first = true; - if ($relation['isOwningSide']) { - foreach ($assoc['relationToSourceKeyColumns'] as $relationColumn => $sourceColumn) { - if ( ! $first) $sql .= ' AND '; else $first = false; - - if ($sourceClass->containsForeignIdentifier && !isset($sourceClass->fieldNames[$sourceColumn])) { - $quotedTargetColumn = $sourceColumn; // Join columns cannot be quoted. - } else { - $quotedTargetColumn = $sourceClass->getQuotedColumnName($sourceClass->fieldNames[$sourceColumn], $this->_platform); - } - - $sql .= $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $relationColumn; - } - } else { - foreach ($assoc['relationToTargetKeyColumns'] as $relationColumn => $targetColumn) { - if ( ! $first) $sql .= ' AND '; else $first = false; - - if ($sourceClass->containsForeignIdentifier && !isset($sourceClass->fieldNames[$targetColumn])) { - $quotedTargetColumn = $targetColumn; // Join columns cannot be quoted. - } else { - $quotedTargetColumn = $sourceClass->getQuotedColumnName($sourceClass->fieldNames[$targetColumn], $this->_platform); - } - - $sql .= $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $relationColumn; - } - } - - // Join target table - $sql .= ($joinType == AST\Join::JOIN_TYPE_LEFT || $joinType == AST\Join::JOIN_TYPE_LEFTOUTER) ? ' LEFT JOIN ' : ' INNER JOIN '; - $sql .= $targetTableName . ' ' . $targetTableAlias . ' ON '; - - $first = true; - if ($relation['isOwningSide']) { - foreach ($assoc['relationToTargetKeyColumns'] as $relationColumn => $targetColumn) { - if ( ! $first) $sql .= ' AND '; else $first = false; - - if ($targetClass->containsForeignIdentifier && !isset($targetClass->fieldNames[$targetColumn])) { - $quotedTargetColumn = $targetColumn; // Join columns cannot be quoted. - } else { - $quotedTargetColumn = $targetClass->getQuotedColumnName($targetClass->fieldNames[$targetColumn], $this->_platform); - } - - $sql .= $targetTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $relationColumn; - } - } else { - foreach ($assoc['relationToSourceKeyColumns'] as $relationColumn => $sourceColumn) { - if ( ! $first) $sql .= ' AND '; else $first = false; - - if ($targetClass->containsForeignIdentifier && !isset($targetClass->fieldNames[$sourceColumn])) { - $quotedTargetColumn = $sourceColumn; // Join columns cannot be quoted. - } else { - $quotedTargetColumn = $targetClass->getQuotedColumnName($targetClass->fieldNames[$sourceColumn], $this->_platform); - } - - $sql .= $targetTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $relationColumn; - } - } - } - - // Handle WITH clause - if (($condExpr = $join->conditionalExpression) !== null) { - // Phase 2 AST optimization: Skip processment of ConditionalExpression - // if only one ConditionalTerm is defined - $sql .= ' AND (' . $this->walkConditionalExpression($condExpr) . ')'; - } - } - - // Apply the filters - if ($filterExpr = $this->generateFilterConditionSQL($targetClass, $targetTableAlias)) { - $sql .= ' AND ' . $filterExpr; - } - - if ($joinVarDecl->indexBy) { - // For Many-To-One or One-To-One associations this obviously makes no sense, but is ignored silently. - $this->_rsm->addIndexBy( - $joinVarDecl->indexBy->simpleStateFieldPathExpression->identificationVariable, - $joinVarDecl->indexBy->simpleStateFieldPathExpression->field - ); - } else if (isset($relation['indexBy'])) { - $this->_rsm->addIndexBy($joinedDqlAlias, $relation['indexBy']); - } - - - $discrSql = $this->_generateDiscriminatorColumnConditionSQL(array($joinedDqlAlias)); - - if ($discrSql) { - $sql .= ' AND ' . $discrSql; - } - - // FIXME: these should either be nested or all forced to be left joins (DDC-XXX) - if ($targetClass->isInheritanceTypeJoined()) { - $sql .= $this->_generateClassTableInheritanceJoins($targetClass, $joinedDqlAlias); + break; } return $sql; @@ -1308,23 +1325,10 @@ class SqlWalker implements TreeWalker $sqlParts = array (); foreach ($identificationVarDecls as $subselectIdVarDecl) { - $sql = ''; + $sql = $this->walkRangeVariableDeclaration($subselectIdVarDecl->rangeVariableDeclaration); - $rangeDecl = $subselectIdVarDecl->rangeVariableDeclaration; - $dqlAlias = $rangeDecl->aliasIdentificationVariable; - - $class = $this->_em->getClassMetadata($rangeDecl->abstractSchemaName); - $sql .= $class->getQuotedTableName($this->_platform) . ' ' - . $this->getSQLTableAlias($class->getTableName(), $dqlAlias); - - $this->_rootAliases[] = $dqlAlias; - - if ($class->isInheritanceTypeJoined()) { - $sql .= $this->_generateClassTableInheritanceJoins($class, $dqlAlias); - } - - foreach ($subselectIdVarDecl->joinVariableDeclarations as $joinVarDecl) { - $sql .= $this->walkJoinVariableDeclaration($joinVarDecl); + foreach ($subselectIdVarDecl->joins as $join) { + $sql .= $this->walkJoin($join); } $sqlParts[] = $this->_platform->appendLockHint($sql, $this->_query->getHint(Query::HINT_LOCK_MODE)); diff --git a/lib/Doctrine/ORM/Query/TreeWalker.php b/lib/Doctrine/ORM/Query/TreeWalker.php index 2000fe85a..96897ec1b 100644 --- a/lib/Doctrine/ORM/Query/TreeWalker.php +++ b/lib/Doctrine/ORM/Query/TreeWalker.php @@ -91,12 +91,12 @@ interface TreeWalker function walkHavingClause($havingClause); /** - * Walks down a JoinVariableDeclaration AST node and creates the corresponding SQL. + * Walks down a Join AST node and creates the corresponding SQL. * - * @param JoinVariableDeclaration $joinVarDecl + * @param Join $joinVarDecl * @return string The SQL. */ - function walkJoinVariableDeclaration($joinVarDecl); + function walkJoin($join); /** * Walks down a SelectExpression AST node and generates the corresponding SQL. diff --git a/lib/Doctrine/ORM/Query/TreeWalkerAdapter.php b/lib/Doctrine/ORM/Query/TreeWalkerAdapter.php index b6c30f96f..b26bd431f 100644 --- a/lib/Doctrine/ORM/Query/TreeWalkerAdapter.php +++ b/lib/Doctrine/ORM/Query/TreeWalkerAdapter.php @@ -125,12 +125,12 @@ abstract class TreeWalkerAdapter implements TreeWalker public function walkHavingClause($havingClause) {} /** - * Walks down a JoinVariableDeclaration AST node and creates the corresponding SQL. + * Walks down a Join AST node and creates the corresponding SQL. * - * @param JoinVariableDeclaration $joinVarDecl + * @param Join $join * @return string The SQL. */ - public function walkJoinVariableDeclaration($joinVarDecl) {} + public function walkJoin($join) {} /** * Walks down a SelectExpression AST node and generates the corresponding SQL. diff --git a/lib/Doctrine/ORM/Query/TreeWalkerChain.php b/lib/Doctrine/ORM/Query/TreeWalkerChain.php index 921f4b5cf..4e105a12a 100644 --- a/lib/Doctrine/ORM/Query/TreeWalkerChain.php +++ b/lib/Doctrine/ORM/Query/TreeWalkerChain.php @@ -148,15 +148,15 @@ class TreeWalkerChain implements TreeWalker } /** - * Walks down a JoinVariableDeclaration AST node and creates the corresponding SQL. + * Walks down a Join AST node and creates the corresponding SQL. * - * @param JoinVariableDeclaration $joinVarDecl + * @param Join $join * @return string The SQL. */ - public function walkJoinVariableDeclaration($joinVarDecl) + public function walkJoin($join) { foreach ($this->_walkers as $walker) { - $walker->walkJoinVariableDeclaration($joinVarDecl); + $walker->walkJoin($join); } } diff --git a/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php b/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php index 2305287a4..1f0445c08 100644 --- a/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php +++ b/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php @@ -206,6 +206,11 @@ class LanguageRecognitionTest extends \Doctrine\Tests\OrmTestCase $this->assertValidDQL('SELECT u.name, a.topic, p.phonenumber FROM Doctrine\Tests\Models\CMS\CmsUser u INNER JOIN u.articles a LEFT JOIN u.phonenumbers p'); } + public function testJoinClassPath() + { + $this->assertValidDQL('SELECT u.name FROM Doctrine\Tests\Models\CMS\CmsUser u JOIN Doctrine\Tests\Models\CMS\CmsArticle a WITH a.user = u.id'); + } + public function testOrderBySingleColumn() { $this->assertValidDQL('SELECT u.name FROM Doctrine\Tests\Models\CMS\CmsUser u ORDER BY u.name'); From cba4e55ac47b0ed1e9b326fdf336752447346956 Mon Sep 17 00:00:00 2001 From: Guilherme Blanco Date: Sun, 10 Jun 2012 23:05:21 +0200 Subject: [PATCH 3/4] Added more coverage tests. Required result confirmation. --- .../Tests/ORM/Functional/QueryTest.php | 72 ++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/tests/Doctrine/Tests/ORM/Functional/QueryTest.php b/tests/Doctrine/Tests/ORM/Functional/QueryTest.php index fd1e14703..8d32811c2 100644 --- a/tests/Doctrine/Tests/ORM/Functional/QueryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/QueryTest.php @@ -5,7 +5,9 @@ namespace Doctrine\Tests\ORM\Functional; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\DBAL\Connection; - +use Doctrine\Tests\Models\CMS\CmsUser, + Doctrine\Tests\Models\CMS\CmsArticle, + Doctrine\Tests\Models\CMS\CmsPhonenumber; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Query; use Doctrine\ORM\Query\Parameter; @@ -713,4 +715,72 @@ class QueryTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertInstanceOf('\Doctrine\ORM\NonUniqueResultException', $exc); } } + + public function testMultipleJoinComponentsUsingInnerJoin() + { + $userA = new CmsUser; + $userA->name = 'Benjamin'; + $userA->username = 'beberlei'; + $userA->status = 'developer'; + + $phonenumberA = new CmsPhonenumber; + $phonenumberA->phonenumber = '111111'; + $userA->addPhonenumber($phonenumberA); + + $userB = new CmsUser; + $userB->name = 'Alexander'; + $userB->username = 'asm89'; + $userB->status = 'developer'; + + $this->_em->persist($userA); + $this->_em->persist($userB); + $this->_em->flush(); + $this->_em->clear(); + + $query = $this->_em->createQuery(" + SELECT u, p + FROM Doctrine\Tests\Models\CMS\CmsUser u + INNER JOIN Doctrine\Tests\Models\CMS\CmsPhonenumber p WITH u = p.user + "); + $users = $query->execute(); + + $this->assertEquals(2, count($users)); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $users[0]); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsPhonenumber', $users[1]); + } + + public function testMultipleJoinComponentsUsingLeftJoin() + { + $userA = new CmsUser; + $userA->name = 'Benjamin'; + $userA->username = 'beberlei'; + $userA->status = 'developer'; + + $phonenumberA = new CmsPhonenumber; + $phonenumberA->phonenumber = '111111'; + $userA->addPhonenumber($phonenumberA); + + $userB = new CmsUser; + $userB->name = 'Alexander'; + $userB->username = 'asm89'; + $userB->status = 'developer'; + + $this->_em->persist($userA); + $this->_em->persist($userB); + $this->_em->flush(); + $this->_em->clear(); + + $query = $this->_em->createQuery(" + SELECT u, p + FROM Doctrine\Tests\Models\CMS\CmsUser u + LEFT JOIN Doctrine\Tests\Models\CMS\CmsPhonenumber p WITH u = p.user + "); + $users = $query->execute(); + + $this->assertEquals(4, count($users)); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $users[0]); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsPhonenumber', $users[1]); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $users[2]); + $this->assertNull($users[3]); + } } From 6f13e9543b9a4cfbd932cce437dae6b8f3e042cf Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Mon, 18 Jun 2012 17:03:08 +0200 Subject: [PATCH 4/4] Fix QueryTest --- tests/Doctrine/Tests/ORM/Functional/QueryTest.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/Doctrine/Tests/ORM/Functional/QueryTest.php b/tests/Doctrine/Tests/ORM/Functional/QueryTest.php index 8d32811c2..906b5e68f 100644 --- a/tests/Doctrine/Tests/ORM/Functional/QueryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/QueryTest.php @@ -12,9 +12,6 @@ use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Query; use Doctrine\ORM\Query\Parameter; -use Doctrine\Tests\Models\CMS\CmsUser; -use Doctrine\Tests\Models\CMS\CmsArticle; - require_once __DIR__ . '/../../TestInit.php'; /**