Commit 3928ba9d authored by guilhermeblanco's avatar guilhermeblanco

[2.0] Added support to complex PathExpression in DQL queries

parent 20c84166
...@@ -94,6 +94,13 @@ class Parser ...@@ -94,6 +94,13 @@ class Parser
*/ */
private $_em; private $_em;
/**
* The Abtract Syntax Tree of processed DQL query.
*
* @var SelectStatement | UpdateStatement | DeleteStatement
*/
private $_ast;
/** /**
* The Query to parse. * The Query to parse.
* *
...@@ -140,6 +147,7 @@ class Parser ...@@ -140,6 +147,7 @@ class Parser
$this->_em = $query->getEntityManager(); $this->_em = $query->getEntityManager();
$this->_lexer = new Lexer($query->getDql()); $this->_lexer = new Lexer($query->getDql());
$this->_parserResult = new ParserResult(); $this->_parserResult = new ParserResult();
$this->_ast = null;
} }
/** /**
...@@ -193,6 +201,16 @@ class Parser ...@@ -193,6 +201,16 @@ class Parser
return $this->_em; return $this->_em;
} }
/**
* Gets the Abstract Syntax Tree generated by the parser.
*
* @return SelectStatement | UpdateStatement | DeleteStatement
*/
public function getAST()
{
return $this->_ast;
}
/** /**
* Registers a custom function that returns strings. * Registers a custom function that returns strings.
* *
...@@ -274,10 +292,8 @@ class Parser ...@@ -274,10 +292,8 @@ class Parser
// Parse & build AST // Parse & build AST
$AST = $this->QueryLanguage(); $AST = $this->QueryLanguage();
// Check for end of string // Activate semantical checks after this point. Process all deferred checks in pipeline
if ($this->_lexer->lookahead !== null) { $this->_processDeferredExpressionsStack($AST);
$this->syntaxError('end of string');
}
if ($customWalkers = $this->_query->getHint(Query::HINT_CUSTOM_TREE_WALKERS)) { if ($customWalkers = $this->_query->getHint(Query::HINT_CUSTOM_TREE_WALKERS)) {
$this->_customTreeWalkers = $customWalkers; $this->_customTreeWalkers = $customWalkers;
...@@ -286,9 +302,11 @@ class Parser ...@@ -286,9 +302,11 @@ class Parser
// Run any custom tree walkers over the AST // Run any custom tree walkers over the AST
if ($this->_customTreeWalkers) { if ($this->_customTreeWalkers) {
$treeWalkerChain = new TreeWalkerChain($this->_query, $this->_parserResult, $this->_queryComponents); $treeWalkerChain = new TreeWalkerChain($this->_query, $this->_parserResult, $this->_queryComponents);
foreach ($this->_customTreeWalkers as $walker) { foreach ($this->_customTreeWalkers as $walker) {
$treeWalkerChain->addTreeWalker($walker); $treeWalkerChain->addTreeWalker($walker);
} }
if ($AST instanceof AST\SelectStatement) { if ($AST instanceof AST\SelectStatement) {
$treeWalkerChain->walkSelectStatement($AST); $treeWalkerChain->walkSelectStatement($AST);
} else if ($AST instanceof AST\UpdateStatement) { } else if ($AST instanceof AST\UpdateStatement) {
...@@ -492,11 +510,11 @@ class Parser ...@@ -492,11 +510,11 @@ class Parser
* @param array $token * @param array $token
* @param integer $nestingLevel * @param integer $nestingLevel
*/ */
private function _subscribeExpression($expression, $method, $token, $nextingLevel = null) private function _subscribeExpression($expression, $method, $token, $nestingLevel = null)
{ {
$nestingLevel = ($nestingLevel !== null) ?: $this->_nestingLevel; $nestingLevel = ($nestingLevel !== null) ?: $this->_nestingLevel;
$exprStack[] = array( $exprStack = array(
'method' => $method, 'method' => $method,
'expression' => $expression, 'expression' => $expression,
'nestingLevel' => $nestingLevel, 'nestingLevel' => $nestingLevel,
...@@ -508,13 +526,17 @@ class Parser ...@@ -508,13 +526,17 @@ class Parser
/** /**
* Processes the topmost stack of deferred path expressions. * Processes the topmost stack of deferred path expressions.
*
* @param mixed $AST
*/ */
private function _processDeferredExpressionsStack() private function _processDeferredExpressionsStack($AST)
{ {
foreach ($this->_deferredExpressionsStack as $item) { foreach ($this->_deferredExpressionsStack as $item) {
if (!isset($item['method'])) var_dump($item);
$method = '_validate' . $item['method']; $method = '_validate' . $item['method'];
$this->$method($item['expression'], $item['token'], $item['nestingLevel']); $this->$method($item, $AST);
} }
} }
...@@ -523,30 +545,35 @@ class Parser ...@@ -523,30 +545,35 @@ class Parser
* Validates that the given <tt>IdentificationVariable</tt> is a semantically correct. * Validates that the given <tt>IdentificationVariable</tt> is a semantically correct.
* It must exist in query components list. * It must exist in query components list.
* *
* @param string $identVariable * @param array $deferredItem
* @param array $token * @param mixed $AST
* @param integer $nestingLevel
* *
* @return array Query Component * @return array Query Component
*/ */
private function _validateIdentificationVariable($identVariable, $token, $nestingLevel) private function _validateIdentificationVariable($deferredItem, $AST)
{ {
$identVariable = $deferredItem['expression'];
// Check if IdentificationVariable exists in queryComponents // Check if IdentificationVariable exists in queryComponents
if ( ! isset($this->_queryComponents[$identVariable])) { if ( ! isset($this->_queryComponents[$identVariable])) {
$this->semanticalError("'$identVariable' is not defined.", $token); $this->semanticalError(
"'$identVariable' is not defined.", $deferredItem['token']
);
} }
$qComp = $this->_queryComponents[$identVariable]; $qComp = $this->_queryComponents[$identVariable];
// Check if queryComponent points to an AbstractSchemaName or a ResultVariable // Check if queryComponent points to an AbstractSchemaName or a ResultVariable
if ( ! isset($qComp['metadata'])) { if ( ! isset($qComp['metadata'])) {
$this->semanticalError("'$identVariable' does not point to a Class.", $token); $this->semanticalError(
"'$identVariable' does not point to a Class.", $deferredItem['token']
);
} }
// Validate if identification variable nesting level is lower or equal than the current one // Validate if identification variable nesting level is lower or equal than the current one
if ($qComp['nestingLevel'] > $nestingLevel) { if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) {
$this->semanticalError( $this->semanticalError(
"'$identVariable' is used outside the scope of its declaration.", $token "'$identVariable' is used outside the scope of its declaration.", $deferredItem['token']
); );
} }
...@@ -554,75 +581,38 @@ class Parser ...@@ -554,75 +581,38 @@ class Parser
} }
/** /**
* Validates that the given <tt>AliasIdentificationVariable</tt> is a semantically correct. * Validates that the given <tt>ResultVariable</tt> is a semantically correct.
* It must not exist in query components list.
*
* @param string $aliasIdentVariable
* @param array $token
* @param integer $nestingLevel
*
* @return boolean
*/
private function _validateAliasIdentificationVariable($aliasIdentVariable, $token, $nestingLevel)
{
$exists = isset($this->_queryComponents[$aliasIdentVariable]);
if ($exists) {
$this->semanticalError("'$aliasIdentVariable' is already defined.", $token);
}
return $exists;
}
/**
* Validates that the given <tt>AbstractSchemaName</tt> is a semantically correct.
* It must be defined in the scope of Application.
*
* @param string $schemaName
* @param array $token
* @param integer $nestingLevel
*
* @return boolean
*/
private function _validateAbstractSchemaName($schemaName, $token, $nestingLevel)
{
$exists = class_exists($schemaName, true);
if ( ! $exists) {
$this->semanticalError("Class '$schemaName' is not defined.", $token);
}
return $exists;
}
/**
* Validates that the given <tt>AliasIdentificationVariable</tt> is a semantically correct.
* It must exist in query components list. * It must exist in query components list.
* *
* @param string $resultVariable * @param array $deferredItem
* @param array $token * @param mixed $AST
* @param integer $nestingLevel
* *
* @return array Query Component * @return array Query Component
*/ */
private function _validateResultVariable($resultVariable, $token, $nestingLevel) private function _validateResultVariable($deferredItem, $AST)
{ {
$resultVariable = $deferredItem['expression'];
// Check if ResultVariable exists in queryComponents // Check if ResultVariable exists in queryComponents
if ( ! isset($this->_queryComponents[$resultVariable])) { if ( ! isset($this->_queryComponents[$resultVariable])) {
$this->semanticalError("'$resultVariable' is not defined.", $token); $this->semanticalError(
"'$resultVariable' is not defined.", $deferredItem['token']
);
} }
$qComp = $this->_queryComponents[$resultVariable]; $qComp = $this->_queryComponents[$resultVariable];
// Check if queryComponent points to an AbstractSchemaName or a ResultVariable // Check if queryComponent points to an AbstractSchemaName or a ResultVariable
if ( ! isset($qComp['resultVariable'])) { if ( ! isset($qComp['resultVariable'])) {
$this->semanticalError("'$identVariable' does not point to a ResultVariable.", $token); $this->semanticalError(
"'$identVariable' does not point to a ResultVariable.", $deferredItem['token']
);
} }
// Validate if identification variable nesting level is lower or equal than the current one // Validate if identification variable nesting level is lower or equal than the current one
if ($qComp['nestingLevel'] > $nestingLevel) { if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) {
$this->semanticalError( $this->semanticalError(
"'$resultVariable' is used outside the scope of its declaration.", $token "'$resultVariable' is used outside the scope of its declaration.", $deferredItem['token']
); );
} }
...@@ -632,14 +622,14 @@ class Parser ...@@ -632,14 +622,14 @@ class Parser
/** /**
* Validates that the given <tt>JoinAssociationPathExpression</tt> is a semantically correct. * Validates that the given <tt>JoinAssociationPathExpression</tt> is a semantically correct.
* *
* @param JoinAssociationPathExpression $pathExpression * @param array $deferredItem
* @param array $token * @param mixed $AST
* @param integer $nestingLevel
* *
* @return array Query Component * @return array Query Component
*/ */
private function _validateJoinAssociationPathExpression(AST\JoinAssociationPathExpression $pathExpression, $token, $nestingLevel) private function _validateJoinAssociationPathExpression($deferredItem, $AST)
{ {
$pathExpression = $deferredItem['expression'];
$qComp = $this->_queryComponents[$pathExpression->identificationVariable];; $qComp = $this->_queryComponents[$pathExpression->identificationVariable];;
// Validating association field (*-to-one or *-to-many) // Validating association field (*-to-one or *-to-many)
...@@ -662,70 +652,106 @@ class Parser ...@@ -662,70 +652,106 @@ class Parser
* SingleValuedAssociationPathExpression ::= IdentificationVariable "." {SingleValuedAssociationField "."}* SingleValuedAssociationField * SingleValuedAssociationPathExpression ::= IdentificationVariable "." {SingleValuedAssociationField "."}* SingleValuedAssociationField
* CollectionValuedPathExpression ::= IdentificationVariable "." {SingleValuedAssociationField "."}* CollectionValuedAssociationField * CollectionValuedPathExpression ::= IdentificationVariable "." {SingleValuedAssociationField "."}* CollectionValuedAssociationField
* *
* @param PathExpression $pathExpression * @param array $deferredItem
* @param array $token * @param mixed $AST
* @param integer $nestingLevel
* *
* @return integer * @return integer
*/ */
private function _validatePathExpression(AST\PathExpression $pathExpression, $token, $nestingLevel) private function _validatePathExpression($deferredItem, $AST)
{ {
$pathExpression = $deferredItem['expression'];
$parts = $pathExpression->parts;
$numParts = count($parts);
$qComp = $this->_queryComponents[$pathExpression->identificationVariable]; $qComp = $this->_queryComponents[$pathExpression->identificationVariable];
$aliasIdentificationVariable = $pathExpression->identificationVariable;
$parentField = $pathExpression->identificationVariable;
$class = $qComp['metadata']; $class = $qComp['metadata'];
$stateField = $collectionField = null; $fieldType = null;
$curIndex = 0;
foreach ($pathExpression->parts as $field) { foreach ($parts as $field) {
// Check if it is not in a state field // Check if it is not in a state field
if ($stateField !== null) { if ($fieldType & AST\PathExpression::TYPE_STATE_FIELD) {
$this->semanticalError( $this->semanticalError(
'Cannot navigate through state field named ' . $stateField, $token 'Cannot navigate through state field named ' . $field . ' on ' . $parentField,
$deferredItem['token']
); );
} }
// Check if it is not a collection field // Check if it is not a collection field
if ($collectionField !== null) { if ($fieldType & AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION) {
$this->semanticalError( $this->semanticalError(
'Cannot navigate through collection-valued field named ' . $collectionField, 'Cannot navigate through collection field named ' . $field . ' on ' . $parentField,
$token $deferredItem['token']
); );
} }
// Check if field exists // Check if field or association exists
if ( ! isset($class->associationMappings[$field]) && ! isset($class->fieldMappings[$field])) { if ( ! isset($class->associationMappings[$field]) && ! isset($class->fieldMappings[$field])) {
$this->semanticalError( $this->semanticalError(
'Class ' . $class->name . ' has no field or association named ' . $field, $token 'Class ' . $class->name . ' has no field or association named ' . $field,
$deferredItem['token']
); );
} }
$parentField = $field;
if (isset($class->fieldMappings[$field])) { if (isset($class->fieldMappings[$field])) {
$stateField = $field; $fieldType = AST\PathExpression::TYPE_STATE_FIELD;
} else if ($class->associationMappings[$field]->isOneToOne()) {
$class = $this->_em->getClassMetadata($class->associationMappings[$field]->targetEntityName);
} else { } else {
$collectionField = $field; $assoc = $class->associationMappings[$field];
} $class = $this->_em->getClassMetadata($assoc->targetEntityName);
if (
($curIndex != $numParts - 1) &&
! isset($this->_queryComponents[$aliasIdentificationVariable . '.' . $field])
) {
// Building queryComponent
$joinQueryComponent = array(
'metadata' => $class,
'parent' => $aliasIdentificationVariable,
'relation' => $assoc,
'map' => null,
'nestingLevel' => $this->_nestingLevel,
'token' => $deferredItem['token'],
);
// Create AST node
$joinVariableDeclaration = new AST\JoinVariableDeclaration(
new AST\Join(
AST\Join::JOIN_TYPE_INNER,
new AST\JoinAssociationPathExpression($aliasIdentificationVariable, $field),
$aliasIdentificationVariable . '.' . $field
),
null
);
$AST->fromClause->identificationVariableDeclarations[0]->joinVariableDeclarations[] = $joinVariableDeclaration;
$this->_queryComponents[$aliasIdentificationVariable . '.' . $field] = $joinQueryComponent;
} }
// Recognize correct expression type $aliasIdentificationVariable .= '.' . $field;
$expressionType = null;
if ($stateField !== null) { if ($assoc->isOneToOne()) {
$expressionType = AST\PathExpression::TYPE_STATE_FIELD; $fieldType = AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION;
} else if ($collectionField !== null) {
$expressionType = AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION;
} else { } else {
$expressionType = AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION; $fieldType = AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION;
}
}
$curIndex++;
} }
// Validate if PathExpression is one of the expected types // Validate if PathExpression is one of the expected types
$expectedType = $pathExpression->expectedType; $expectedType = $pathExpression->expectedType;
if ( ! ($expectedType & $expressionType)) { if ( ! ($expectedType & $fieldType)) {
// We need to recognize which was expected type(s) // We need to recognize which was expected type(s)
$expectedStringTypes = array(); $expectedStringTypes = array();
// Validate state field type (field/column) // Validate state field type
if ($expectedType & AST\PathExpression::TYPE_STATE_FIELD) { if ($expectedType & AST\PathExpression::TYPE_STATE_FIELD) {
$expectedStringTypes[] = 'StateFieldPathExpression'; $expectedStringTypes[] = 'StateFieldPathExpression';
} }
...@@ -749,16 +775,15 @@ class Parser ...@@ -749,16 +775,15 @@ class Parser
$semanticalError .= implode(' or ', $expectedStringTypes) . ' expected.'; $semanticalError .= implode(' or ', $expectedStringTypes) . ' expected.';
} }
$this->semanticalError($semanticalError, $token); $this->semanticalError($semanticalError, $deferredItem['token']);
} }
// We need to force the type in PathExpression // We need to force the type in PathExpression
$pathExpression->type = $expressionType; $pathExpression->type = $fieldType;
return $expressionType; return $fieldType;
} }
/** /**
* QueryLanguage ::= SelectStatement | UpdateStatement | DeleteStatement * QueryLanguage ::= SelectStatement | UpdateStatement | DeleteStatement
* *
...@@ -784,6 +809,11 @@ class Parser ...@@ -784,6 +809,11 @@ class Parser
$this->syntaxError('SELECT, UPDATE or DELETE'); $this->syntaxError('SELECT, UPDATE or DELETE');
break; break;
} }
// Check for end of string
if ($this->_lexer->lookahead !== null) {
$this->syntaxError('end of string');
}
} }
...@@ -808,9 +838,6 @@ class Parser ...@@ -808,9 +838,6 @@ class Parser
$selectStatement->orderByClause = $this->_lexer->isNextToken(Lexer::T_ORDER) $selectStatement->orderByClause = $this->_lexer->isNextToken(Lexer::T_ORDER)
? $this->OrderByClause() : null; ? $this->OrderByClause() : null;
// Activate semantical checks after this point. Process all deferred checks in pipeline
$this->_processDeferredExpressionsStack();
return $selectStatement; return $selectStatement;
} }
...@@ -825,9 +852,6 @@ class Parser ...@@ -825,9 +852,6 @@ class Parser
$updateStatement->whereClause = $this->_lexer->isNextToken(Lexer::T_WHERE) $updateStatement->whereClause = $this->_lexer->isNextToken(Lexer::T_WHERE)
? $this->WhereClause() : null; ? $this->WhereClause() : null;
// Activate semantical checks after this point. Process all deferred checks in pipeline
$this->_processDeferredExpressionsStack();
return $updateStatement; return $updateStatement;
} }
...@@ -842,9 +866,6 @@ class Parser ...@@ -842,9 +866,6 @@ class Parser
$deleteStatement->whereClause = $this->_lexer->isNextToken(Lexer::T_WHERE) $deleteStatement->whereClause = $this->_lexer->isNextToken(Lexer::T_WHERE)
? $this->WhereClause() : null; ? $this->WhereClause() : null;
// Activate semantical checks after this point. Process all deferred checks in pipeline
$this->_processDeferredExpressionsStack();
return $deleteStatement; return $deleteStatement;
} }
...@@ -883,11 +904,13 @@ class Parser ...@@ -883,11 +904,13 @@ class Parser
$this->match(Lexer::T_IDENTIFIER); $this->match(Lexer::T_IDENTIFIER);
$aliasIdentVariable = $this->_lexer->token['value']; $aliasIdentVariable = $this->_lexer->token['value'];
$exists = isset($this->_queryComponents[$aliasIdentVariable]);
// Apply AliasIdentificationVariable validation if ($exists) {
$this->_validateAliasIdentificationVariable( $this->semanticalError(
$aliasIdentVariable, $this->_lexer->token, $this->_nestingLevel "'$aliasIdentVariable' is already defined.", $this->_lexer->token
); );
}
return $aliasIdentVariable; return $aliasIdentVariable;
} }
...@@ -902,15 +925,36 @@ class Parser ...@@ -902,15 +925,36 @@ class Parser
$this->match(Lexer::T_IDENTIFIER); $this->match(Lexer::T_IDENTIFIER);
$schemaName = $this->_lexer->token['value']; $schemaName = $this->_lexer->token['value'];
$exists = class_exists($schemaName, true);
// Apply AbstractSchemaName validation if ( ! $exists) {
$this->_validateAbstractSchemaName( $this->semanticalError("Class '$schemaName' is not defined.", $this->_lexer->token);
$schemaName, $this->_lexer->token, $this->_nestingLevel }
);
return $schemaName; return $schemaName;
} }
/**
* AliasResultVariable ::= identifier
*
* @return string
*/
public function AliasResultVariable()
{
$this->match(Lexer::T_IDENTIFIER);
$resultVariable = $this->_lexer->token['value'];
$exists = isset($this->_queryComponents[$resultVariable]);
if ($exists) {
$this->semanticalError(
"'$resultVariable' is already defined.", $this->_lexer->token
);
}
return $resultVariable;
}
/** /**
* ResultVariable ::= identifier * ResultVariable ::= identifier
* *
...@@ -923,15 +967,11 @@ class Parser ...@@ -923,15 +967,11 @@ class Parser
$resultVariable = $this->_lexer->token['value']; $resultVariable = $this->_lexer->token['value'];
// Defer ResultVariable validation // Defer ResultVariable validation
$exprStack = array( $this->_subscribeExpression(
'method' => 'ResultVariable', $resultVariable, 'ResultVariable',
'expression' => $resultVariable, $this->_lexer->token, $this->_nestingLevel
'nestingLevel' => $this->_nestingLevel,
'token' => $this->_lexer->token,
); );
array_push($this->_deferredExpressionsStack, $exprStack);
return $resultVariable; return $resultVariable;
} }
...@@ -953,15 +993,11 @@ class Parser ...@@ -953,15 +993,11 @@ class Parser
$pathExpr = new AST\JoinAssociationPathExpression($identVariable, $field); $pathExpr = new AST\JoinAssociationPathExpression($identVariable, $field);
// Defer JoinAssociationPathExpression validation // Defer JoinAssociationPathExpression validation
$exprStack = array( $this->_subscribeExpression(
'method' => 'JoinAssociationPathExpression', $pathExpr, 'JoinAssociationPathExpression',
'expression' => $pathExpr, $token, $this->_nestingLevel
'nestingLevel' => $this->_nestingLevel,
'token' => $token,
); );
array_push($this->_deferredExpressionsStack, $exprStack);
return $pathExpr; return $pathExpr;
} }
...@@ -969,7 +1005,7 @@ class Parser ...@@ -969,7 +1005,7 @@ class Parser
* Parses an arbitrary path expression and defers semantical validation * Parses an arbitrary path expression and defers semantical validation
* based on expected types. * based on expected types.
* *
* PathExpression ::= IdentificationVariable "." {identifier "."}* identifier * PathExpression ::= IdentificationVariable {"." identifier}* "." identifier
* *
* @param integer $expectedTypes * @param integer $expectedTypes
* @return \Doctrine\ORM\Query\AST\PathExpression * @return \Doctrine\ORM\Query\AST\PathExpression
...@@ -991,15 +1027,11 @@ class Parser ...@@ -991,15 +1027,11 @@ class Parser
$pathExpr = new AST\PathExpression($expectedTypes, $identVariable, $parts); $pathExpr = new AST\PathExpression($expectedTypes, $identVariable, $parts);
// Defer PathExpression validation if requested to be defered // Defer PathExpression validation if requested to be defered
$exprStack = array( $this->_subscribeExpression(
'method' => 'PathExpression', $pathExpr, 'PathExpression',
'expression' => $pathExpr, $token, $this->_nestingLevel
'nestingLevel' => $this->_nestingLevel,
'token' => $token,
); );
array_push($this->_deferredExpressionsStack, $exprStack);
return $pathExpr; return $pathExpr;
} }
...@@ -1598,7 +1630,7 @@ class Parser ...@@ -1598,7 +1630,7 @@ class Parser
); );
} }
$targetClassName = $parentClass->getAssociationMapping($assocField)->getTargetEntityName(); $targetClassName = $parentClass->getAssociationMapping($assocField)->targetEntityName;
// Building queryComponent // Building queryComponent
$joinQueryComponent = array( $joinQueryComponent = array(
...@@ -1651,7 +1683,7 @@ class Parser ...@@ -1651,7 +1683,7 @@ class Parser
/** /**
* SelectExpression ::= * SelectExpression ::=
* IdentificationVariable | StateFieldPathExpression | * IdentificationVariable | StateFieldPathExpression |
* (AggregateExpression | "(" Subselect ")" | FunctionDeclaration) [["AS"] ResultVariable] * (AggregateExpression | "(" Subselect ")" | FunctionDeclaration) [["AS"] AliasResultVariable]
* *
* @return \Doctrine\ORM\Query\AST\SelectExpression * @return \Doctrine\ORM\Query\AST\SelectExpression
*/ */
...@@ -1683,9 +1715,9 @@ class Parser ...@@ -1683,9 +1715,9 @@ class Parser
if ($this->_lexer->isNextToken(Lexer::T_IDENTIFIER)) { if ($this->_lexer->isNextToken(Lexer::T_IDENTIFIER)) {
$token = $this->_lexer->lookahead; $token = $this->_lexer->lookahead;
$fieldAliasIdentificationVariable = $this->ResultVariable(); $fieldAliasIdentificationVariable = $this->AliasResultVariable();
// Include ResultVariable in query components. // Include AliasResultVariable in query components.
$this->_queryComponents[$fieldAliasIdentificationVariable] = array( $this->_queryComponents[$fieldAliasIdentificationVariable] = array(
'resultVariable' => $expression, 'resultVariable' => $expression,
'nestingLevel' => $this->_nestingLevel, 'nestingLevel' => $this->_nestingLevel,
...@@ -1708,7 +1740,7 @@ class Parser ...@@ -1708,7 +1740,7 @@ class Parser
} }
/** /**
* SimpleSelectExpression ::= StateFieldPathExpression | IdentificationVariable | (AggregateExpression [["AS"] ResultVariable]) * SimpleSelectExpression ::= StateFieldPathExpression | IdentificationVariable | (AggregateExpression [["AS"] AliasResultVariable])
* *
* @return \Doctrine\ORM\Query\AST\SimpleSelectExpression * @return \Doctrine\ORM\Query\AST\SimpleSelectExpression
*/ */
...@@ -1735,10 +1767,10 @@ class Parser ...@@ -1735,10 +1767,10 @@ class Parser
if ($this->_lexer->isNextToken(Lexer::T_IDENTIFIER)) { if ($this->_lexer->isNextToken(Lexer::T_IDENTIFIER)) {
$token = $this->_lexer->lookahead; $token = $this->_lexer->lookahead;
$resultVariable = $this->ResultVariable(); $resultVariable = $this->AliasResultVariable();
$expr->fieldIdentificationVariable = $resultVariable; $expr->fieldIdentificationVariable = $resultVariable;
// Include ResultVariable in query components. // Include AliasResultVariable in query components.
$this->_queryComponents[$resultVariable] = array( $this->_queryComponents[$resultVariable] = array(
'resultvariable' => $expr, 'resultvariable' => $expr,
'nestingLevel' => $this->_nestingLevel, 'nestingLevel' => $this->_nestingLevel,
......
...@@ -414,9 +414,8 @@ class SqlWalker implements TreeWalker ...@@ -414,9 +414,8 @@ class SqlWalker implements TreeWalker
switch ($pathExpr->type) { switch ($pathExpr->type) {
case AST\PathExpression::TYPE_STATE_FIELD: case AST\PathExpression::TYPE_STATE_FIELD:
$parts = $pathExpr->parts; $parts = $pathExpr->parts;
$numParts = count($parts); $fieldName = array_pop($parts);
$dqlAlias = $pathExpr->identificationVariable; $dqlAlias = $pathExpr->identificationVariable . (( ! empty($parts)) ? '.' . implode('.', $parts) : '');
$fieldName = $parts[$numParts - 1];
$qComp = $this->_queryComponents[$dqlAlias]; $qComp = $this->_queryComponents[$dqlAlias];
$class = $qComp['metadata']; $class = $qComp['metadata'];
...@@ -609,20 +608,18 @@ class SqlWalker implements TreeWalker ...@@ -609,20 +608,18 @@ class SqlWalker implements TreeWalker
*/ */
public function walkOrderByItem($orderByItem) public function walkOrderByItem($orderByItem)
{ {
$sql = '';
$expr = $orderByItem->expression; $expr = $orderByItem->expression;
if ($expr instanceof AST\PathExpression) {
$parts = $expr->parts;
$dqlAlias = $expr->identificationVariable;
$class = $this->_queryComponents[$dqlAlias]['metadata'];
$columnName = $class->getQuotedColumnName($parts[0], $this->_platform);
return $this->getSqlTableAlias($class->getTableName(), $dqlAlias) . '.' if ($expr instanceof AST\PathExpression) {
. $columnName . ' ' . strtoupper($orderByItem->type); $sql = $this->walkPathExpression($expr);
} else { } else {
$columnName = $this->_queryComponents[$expr]['token']['value']; $columnName = $this->_queryComponents[$expr]['token']['value'];
return $this->_scalarResultAliasMap[$columnName] . ' ' . strtoupper($orderByItem->type); $sql = $this->_scalarResultAliasMap[$columnName];
} }
return $sql . ' ' . strtoupper($orderByItem->type);;
} }
/** /**
...@@ -771,9 +768,8 @@ class SqlWalker implements TreeWalker ...@@ -771,9 +768,8 @@ class SqlWalker implements TreeWalker
if ($expr instanceof AST\PathExpression) { if ($expr instanceof AST\PathExpression) {
if ($expr->type == AST\PathExpression::TYPE_STATE_FIELD) { if ($expr->type == AST\PathExpression::TYPE_STATE_FIELD) {
$parts = $expr->parts; $parts = $expr->parts;
$numParts = count($parts); $fieldName = array_pop($parts);
$dqlAlias = $expr->identificationVariable; $dqlAlias = $expr->identificationVariable . (( ! empty($parts)) ? '.' . implode('.', $parts) : '');
$fieldName = $parts[$numParts - 1];
$qComp = $this->_queryComponents[$dqlAlias]; $qComp = $this->_queryComponents[$dqlAlias];
$class = $qComp['metadata']; $class = $qComp['metadata'];
...@@ -1004,17 +1000,8 @@ class SqlWalker implements TreeWalker ...@@ -1004,17 +1000,8 @@ class SqlWalker implements TreeWalker
*/ */
public function walkAggregateExpression($aggExpression) public function walkAggregateExpression($aggExpression)
{ {
$sql = '';
$parts = $aggExpression->pathExpression->parts;
$dqlAlias = $aggExpression->pathExpression->identificationVariable;
$fieldName = $parts[0];
$qComp = $this->_queryComponents[$dqlAlias];
$columnName = $qComp['metadata']->getQuotedColumnName($fieldName, $this->_platform);
return $aggExpression->functionName . '(' . ($aggExpression->isDistinct ? 'DISTINCT ' : '') return $aggExpression->functionName . '(' . ($aggExpression->isDistinct ? 'DISTINCT ' : '')
. $this->getSqlTableAlias($qComp['metadata']->getTableName(), $dqlAlias) . '.' . $this->walkPathExpression($aggExpression->pathExpression) . ')';
. $columnName . ')';
} }
/** /**
...@@ -1038,12 +1025,7 @@ class SqlWalker implements TreeWalker ...@@ -1038,12 +1025,7 @@ class SqlWalker implements TreeWalker
*/ */
public function walkGroupByItem(AST\PathExpression $pathExpr) public function walkGroupByItem(AST\PathExpression $pathExpr)
{ {
$parts = $pathExpr->parts; return $this->walkPathExpression($pathExpr);
$dqlAlias = $pathExpr->identificationVariable;
$qComp = $this->_queryComponents[$dqlAlias];
$columnName = $qComp['metadata']->getQuotedColumnName($parts[0], $this->_platform);
return $this->getSqlTableAlias($qComp['metadata']->getTableName(), $dqlAlias) . '.' . $columnName;
} }
/** /**
...@@ -1219,8 +1201,10 @@ class SqlWalker implements TreeWalker ...@@ -1219,8 +1201,10 @@ class SqlWalker implements TreeWalker
$sql .= 'EXISTS (SELECT 1 FROM '; $sql .= 'EXISTS (SELECT 1 FROM ';
$entityExpr = $collMemberExpr->entityExpression; $entityExpr = $collMemberExpr->entityExpression;
$collPathExpr = $collMemberExpr->collectionValuedPathExpression; $collPathExpr = $collMemberExpr->collectionValuedPathExpression;
$parts = $collPathExpr->parts; $parts = $collPathExpr->parts;
$dqlAlias = $collPathExpr->identificationVariable; $fieldName = array_pop($parts);
$dqlAlias = $collPathExpr->identificationVariable . (( ! empty($parts)) ? '.' . implode('.', $parts) : '');
$class = $this->_queryComponents[$dqlAlias]['metadata']; $class = $this->_queryComponents[$dqlAlias]['metadata'];
...@@ -1232,7 +1216,7 @@ class SqlWalker implements TreeWalker ...@@ -1232,7 +1216,7 @@ class SqlWalker implements TreeWalker
throw DoctrineException::notImplemented(); throw DoctrineException::notImplemented();
} }
$assoc = $class->associationMappings[$parts[0]]; $assoc = $class->associationMappings[$fieldName];
if ($assoc->isOneToMany()) { if ($assoc->isOneToMany()) {
$targetClass = $this->_em->getClassMetadata($assoc->targetEntityName); $targetClass = $this->_em->getClassMetadata($assoc->targetEntityName);
......
...@@ -59,6 +59,25 @@ class LanguageRecognitionTest extends \Doctrine\Tests\OrmTestCase ...@@ -59,6 +59,25 @@ class LanguageRecognitionTest extends \Doctrine\Tests\OrmTestCase
return $parser->parse(); return $parser->parse();
} }
public function parseDqlForAST($dql, $hints = array())
{
$query = $this->_em->createQuery($dql);
$query->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true);
$query->setDql($dql);
foreach ($hints as $key => $value) {
$query->setHint($key, $value);
}
$parser = new \Doctrine\ORM\Query\Parser($query);
// We do NOT test SQL output here. That only unnecessarily slows down the tests!
$parser->setCustomOutputTreeWalker('Doctrine\Tests\Mocks\MockTreeWalker');
$parser->parse();
return $parser->getAST();
}
public function testEmptyQueryString() public function testEmptyQueryString()
{ {
$this->assertInvalidDql(''); $this->assertInvalidDql('');
...@@ -84,6 +103,11 @@ class LanguageRecognitionTest extends \Doctrine\Tests\OrmTestCase ...@@ -84,6 +103,11 @@ class LanguageRecognitionTest extends \Doctrine\Tests\OrmTestCase
$this->assertValidDql('SELECT u, p FROM Doctrine\Tests\Models\CMS\CmsUser u JOIN u.phonenumbers p'); $this->assertValidDql('SELECT u, p FROM Doctrine\Tests\Models\CMS\CmsUser u JOIN u.phonenumbers p');
} }
public function testSelectMultipleComponentsWithAsterisk2()
{
$this->assertValidDql('SELECT a.user.name FROM Doctrine\Tests\Models\CMS\CmsArticle a');
}
public function testSelectDistinctIsSupported() public function testSelectDistinctIsSupported()
{ {
$this->assertValidDql('SELECT DISTINCT u.name FROM Doctrine\Tests\Models\CMS\CmsUser u'); $this->assertValidDql('SELECT DISTINCT u.name FROM Doctrine\Tests\Models\CMS\CmsUser u');
...@@ -94,6 +118,11 @@ class LanguageRecognitionTest extends \Doctrine\Tests\OrmTestCase ...@@ -94,6 +118,11 @@ class LanguageRecognitionTest extends \Doctrine\Tests\OrmTestCase
$this->assertValidDql('SELECT COUNT(u.id) FROM Doctrine\Tests\Models\CMS\CmsUser u'); $this->assertValidDql('SELECT COUNT(u.id) FROM Doctrine\Tests\Models\CMS\CmsUser u');
} }
public function testDuplicatedAliasInAggregateFunction()
{
$this->assertInvalidDql('SELECT COUNT(u.id) AS num, SUM(u.id) AS num FROM Doctrine\Tests\Models\CMS\CmsUser u');
}
public function testAggregateFunctionWithDistinctInSelect() public function testAggregateFunctionWithDistinctInSelect()
{ {
$this->assertValidDql('SELECT COUNT(DISTINCT u.name) FROM Doctrine\Tests\Models\CMS\CmsUser u'); $this->assertValidDql('SELECT COUNT(DISTINCT u.name) FROM Doctrine\Tests\Models\CMS\CmsUser u');
......
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
namespace Doctrine\Tests\ORM\Query; namespace Doctrine\Tests\ORM\Query;
use Doctrine\ORM\Query; use Doctrine\ORM\Query,
Doctrine\Common\DoctrineException;
require_once __DIR__ . '/../../TestInit.php'; require_once __DIR__ . '/../../TestInit.php';
...@@ -22,7 +23,7 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase ...@@ -22,7 +23,7 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase
$query->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true); $query->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true);
parent::assertEquals($sqlToBeConfirmed, $query->getSql()); parent::assertEquals($sqlToBeConfirmed, $query->getSql());
$query->free(); $query->free();
} catch (Doctrine_Exception $e) { } catch (DoctrineException $e) {
$this->fail($e->getMessage()); $this->fail($e->getMessage());
} }
} }
...@@ -43,6 +44,22 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase ...@@ -43,6 +44,22 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase
); );
} }
public function testSupportsSelectForOneNestedField()
{
$this->assertSqlGeneration(
'SELECT a.user.id FROM Doctrine\Tests\Models\CMS\CmsArticle a',
'SELECT c0_.id AS id0 FROM cms_articles c1_ INNER JOIN cms_users c0_ ON c1_.user_id = c0_.id'
);
}
public function testSupportsSelectForAllNestedField()
{
$this->assertSqlGeneration(
'SELECT a FROM Doctrine\Tests\Models\CMS\CmsArticle a ORDER BY a.user.name ASC',
'SELECT c0_.id AS id0, c0_.topic AS topic1, c0_.text AS text2 FROM cms_articles c0_ INNER JOIN cms_users c1_ ON c0_.user_id = c1_.id ORDER BY c1_.name ASC'
);
}
public function testSupportsSelectForMultipleColumnsOfASingleComponent() public function testSupportsSelectForMultipleColumnsOfASingleComponent()
{ {
$this->assertSqlGeneration( $this->assertSqlGeneration(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment