1
0
mirror of synced 2025-01-29 19:41:45 +03:00

Merge pull request #368 from doctrine/join-poc

Join poc
This commit is contained in:
Benjamin Eberlei 2012-06-18 08:18:13 -07:00
commit a3210e78aa
14 changed files with 459 additions and 325 deletions

View File

@ -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
* 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.

View File

@ -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)

View File

@ -25,30 +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 <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com>
* @author Roman Borschel <roman@code-factory.org>
*/
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 $joinAssociationPathExpression = null;
public $aliasIdentificationVariable = null;
public $joinAssociationDeclaration = null;
public $conditionalExpression = null;
public function __construct($joinType, $joinAssocPathExpr, $aliasIdentVar)
public function __construct($joinType, $joinAssociationDeclaration)
{
$this->joinType = $joinType;
$this->joinAssociationPathExpression = $joinAssocPathExpr;
$this->aliasIdentificationVariable = $aliasIdentVar;
$this->joinAssociationDeclaration = $joinAssociationDeclaration;
}
public function dispatch($sqlWalker)

View File

@ -0,0 +1,53 @@
<?php
/*
* $Id$
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* 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
* <http://www.phpdoctrine.org>.
=======
* and is licensed under the LGPL. For more information, see
* <http://www.doctrine-project.org>.
>>>>>>> Updated PoC for multiple components DQL support.:lib/Doctrine/ORM/Query/AST/JoinAssociationDeclaration.php
*/
namespace Doctrine\ORM\Query\AST;
/**
* JoinAssociationDeclaration ::= JoinAssociationPathExpression ["AS"] AliasIdentificationVariable
*
* @link www.doctrine-project.org
* @since 2.3
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
*/
class JoinAssociationDeclaration extends Node
{
public $joinAssociationPathExpression;
public $aliasIdentificationVariable;
public $indexBy;
public function __construct($joinAssociationPathExpression, $aliasIdentificationVariable, $indexBy)
{
$this->joinAssociationPathExpression = $joinAssociationPathExpression;
$this->aliasIdentificationVariable = $aliasIdentificationVariable;
$this->indexBy = $indexBy;
}
public function dispatch($sqlWalker)
{
return $sqlWalker->walkJoinAssociationDeclaration($this);
}
}

View File

@ -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 <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com>
* @author Roman Borschel <roman@code-factory.org>
@ -40,11 +38,11 @@ class JoinAssociationPathExpression extends Node
public function __construct($identificationVariable, $associationField)
{
$this->identificationVariable = $identificationVariable;
$this->associationField = $associationField;
$this->associationField = $associationField;
}
public function dispatch($sqlWalker)
{
return $sqlWalker->walkJoinPathExpression($this);
return $sqlWalker->walkPathExpression($this);
}
}

View File

@ -15,36 +15,33 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.phpdoctrine.org>.
* and is licensed under the LGPL. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Query\AST;
/**
* JoinVariableDeclaration ::= Join [IndexBy]
* JoinClassPathExpression ::= AbstractSchemaName ["AS"] AliasIdentificationVariable
*
*
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
* @link www.doctrine-project.org
* @since 2.0
* @version $Revision: 3938 $
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com>
* @author Roman Borschel <roman@code-factory.org>
* @since 2.3
* @author Alexander <iam.asm89@gmail.com>
*/
class JoinVariableDeclaration extends Node
class JoinClassPathExpression extends Node
{
public $join = null;
public $indexBy = null;
public $abstractSchemaName;
public $aliasIdentificationVariable;
public function __construct($join, $indexBy)
public function __construct($abstractSchemaName, $aliasIdentificationVar)
{
$this->join = $join;
$this->indexBy = $indexBy;
$this->abstractSchemaName = $abstractSchemaName;
$this->aliasIdentificationVariable = $aliasIdentificationVar;
}
public function dispatch($sqlWalker)
public function dispatch($walker)
{
return $sqlWalker->walkJoinVariableDeclaration($this);
return $sqlWalker->walkJoinPathExpression($this);
}
}

View File

@ -905,7 +905,7 @@ 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);
}
@ -1401,7 +1401,7 @@ class Parser
}
/**
* IdentificationVariableDeclaration ::= RangeVariableDeclaration [IndexBy] {JoinVariableDeclaration}*
* IdentificationVariableDeclaration ::= RangeVariableDeclaration [IndexBy] {Join}*
*
* @return \Doctrine\ORM\Query\AST\IdentificationVariableDeclaration
*/
@ -1409,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
);
}
@ -1450,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;
}
/**
@ -1494,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}* "}"
@ -1535,87 +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);
$joinPathExpression = $this->JoinAssociationPathExpression();
if ($this->_lexer->isNextToken(Lexer::T_AS)) {
$this->match(Lexer::T_AS);
}
$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);
// 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
*

View File

@ -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,169 +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 ';
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
);
}
switch (true) {
case ($joinDeclaration instanceof \Doctrine\ORM\Query\AST\RangeVariableDeclaration):
$sql .= $this->walkRangeVariableDeclaration($joinDeclaration)
. ' ON (' . $this->walkConditionalExpression($join->conditionalExpression) . ')';
break;
$joinAssocPathExpr = $join->joinAssociationPathExpression;
$joinedDqlAlias = $join->aliasIdentificationVariable;
case ($joinDeclaration instanceof \Doctrine\ORM\Query\AST\JoinAssociationDeclaration):
$sql .= $this->walkJoinAssociationDeclaration($joinDeclaration, $joinType);
$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);
}
}
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']);
}
// 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;
// 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) . ')';
}
}
} 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));
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;
@ -1304,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));

View File

@ -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.

View File

@ -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.

View File

@ -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);
}
}

View File

@ -5,14 +5,13 @@ 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;
use Doctrine\Tests\Models\CMS\CmsUser;
use Doctrine\Tests\Models\CMS\CmsArticle;
require_once __DIR__ . '/../../TestInit.php';
/**
@ -713,4 +712,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]);
}
}

View File

@ -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');

View File

@ -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(