[2.0] Added support to complex PathExpression in DQL queries
This commit is contained in:
parent
20c8416607
commit
3928ba9dac
4 changed files with 254 additions and 192 deletions
|
@ -93,7 +93,14 @@ class Parser
|
|||
* @var EnityManager
|
||||
*/
|
||||
private $_em;
|
||||
|
||||
|
||||
/**
|
||||
* The Abtract Syntax Tree of processed DQL query.
|
||||
*
|
||||
* @var SelectStatement | UpdateStatement | DeleteStatement
|
||||
*/
|
||||
private $_ast;
|
||||
|
||||
/**
|
||||
* The Query to parse.
|
||||
*
|
||||
|
@ -140,6 +147,7 @@ class Parser
|
|||
$this->_em = $query->getEntityManager();
|
||||
$this->_lexer = new Lexer($query->getDql());
|
||||
$this->_parserResult = new ParserResult();
|
||||
$this->_ast = null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -193,6 +201,16 @@ class Parser
|
|||
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.
|
||||
*
|
||||
|
@ -274,10 +292,8 @@ class Parser
|
|||
// Parse & build AST
|
||||
$AST = $this->QueryLanguage();
|
||||
|
||||
// Check for end of string
|
||||
if ($this->_lexer->lookahead !== null) {
|
||||
$this->syntaxError('end of string');
|
||||
}
|
||||
// Activate semantical checks after this point. Process all deferred checks in pipeline
|
||||
$this->_processDeferredExpressionsStack($AST);
|
||||
|
||||
if ($customWalkers = $this->_query->getHint(Query::HINT_CUSTOM_TREE_WALKERS)) {
|
||||
$this->_customTreeWalkers = $customWalkers;
|
||||
|
@ -286,9 +302,11 @@ class Parser
|
|||
// Run any custom tree walkers over the AST
|
||||
if ($this->_customTreeWalkers) {
|
||||
$treeWalkerChain = new TreeWalkerChain($this->_query, $this->_parserResult, $this->_queryComponents);
|
||||
|
||||
foreach ($this->_customTreeWalkers as $walker) {
|
||||
$treeWalkerChain->addTreeWalker($walker);
|
||||
}
|
||||
|
||||
if ($AST instanceof AST\SelectStatement) {
|
||||
$treeWalkerChain->walkSelectStatement($AST);
|
||||
} else if ($AST instanceof AST\UpdateStatement) {
|
||||
|
@ -492,11 +510,11 @@ class Parser
|
|||
* @param array $token
|
||||
* @param integer $nestingLevel
|
||||
*/
|
||||
private function _subscribeExpression($expression, $method, $token, $nextingLevel = null)
|
||||
private function _subscribeExpression($expression, $method, $token, $nestingLevel = null)
|
||||
{
|
||||
$nestingLevel = ($nestingLevel !== null) ?: $this->_nestingLevel;
|
||||
|
||||
$exprStack[] = array(
|
||||
$exprStack = array(
|
||||
'method' => $method,
|
||||
'expression' => $expression,
|
||||
'nestingLevel' => $nestingLevel,
|
||||
|
@ -508,13 +526,17 @@ class Parser
|
|||
|
||||
/**
|
||||
* Processes the topmost stack of deferred path expressions.
|
||||
*
|
||||
* @param mixed $AST
|
||||
*/
|
||||
private function _processDeferredExpressionsStack()
|
||||
private function _processDeferredExpressionsStack($AST)
|
||||
{
|
||||
foreach ($this->_deferredExpressionsStack as $item) {
|
||||
if (!isset($item['method'])) var_dump($item);
|
||||
|
||||
$method = '_validate' . $item['method'];
|
||||
|
||||
$this->$method($item['expression'], $item['token'], $item['nestingLevel']);
|
||||
$this->$method($item, $AST);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -523,30 +545,35 @@ class Parser
|
|||
* Validates that the given <tt>IdentificationVariable</tt> is a semantically correct.
|
||||
* It must exist in query components list.
|
||||
*
|
||||
* @param string $identVariable
|
||||
* @param array $token
|
||||
* @param integer $nestingLevel
|
||||
* @param array $deferredItem
|
||||
* @param mixed $AST
|
||||
*
|
||||
* @return array Query Component
|
||||
*/
|
||||
private function _validateIdentificationVariable($identVariable, $token, $nestingLevel)
|
||||
private function _validateIdentificationVariable($deferredItem, $AST)
|
||||
{
|
||||
$identVariable = $deferredItem['expression'];
|
||||
|
||||
// Check if IdentificationVariable exists in queryComponents
|
||||
if ( ! isset($this->_queryComponents[$identVariable])) {
|
||||
$this->semanticalError("'$identVariable' is not defined.", $token);
|
||||
$this->semanticalError(
|
||||
"'$identVariable' is not defined.", $deferredItem['token']
|
||||
);
|
||||
}
|
||||
|
||||
$qComp = $this->_queryComponents[$identVariable];
|
||||
|
||||
// Check if queryComponent points to an AbstractSchemaName or a ResultVariable
|
||||
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
|
||||
if ($qComp['nestingLevel'] > $nestingLevel) {
|
||||
if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) {
|
||||
$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
|
|||
}
|
||||
|
||||
/**
|
||||
* Validates that the given <tt>AliasIdentificationVariable</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.
|
||||
* Validates that the given <tt>ResultVariable</tt> is a semantically correct.
|
||||
* It must exist in query components list.
|
||||
*
|
||||
* @param string $resultVariable
|
||||
* @param array $token
|
||||
* @param integer $nestingLevel
|
||||
* @param array $deferredItem
|
||||
* @param mixed $AST
|
||||
*
|
||||
* @return array Query Component
|
||||
*/
|
||||
private function _validateResultVariable($resultVariable, $token, $nestingLevel)
|
||||
private function _validateResultVariable($deferredItem, $AST)
|
||||
{
|
||||
$resultVariable = $deferredItem['expression'];
|
||||
|
||||
// Check if ResultVariable exists in queryComponents
|
||||
if ( ! isset($this->_queryComponents[$resultVariable])) {
|
||||
$this->semanticalError("'$resultVariable' is not defined.", $token);
|
||||
$this->semanticalError(
|
||||
"'$resultVariable' is not defined.", $deferredItem['token']
|
||||
);
|
||||
}
|
||||
|
||||
$qComp = $this->_queryComponents[$resultVariable];
|
||||
|
||||
// Check if queryComponent points to an AbstractSchemaName or a 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
|
||||
if ($qComp['nestingLevel'] > $nestingLevel) {
|
||||
if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) {
|
||||
$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
|
|||
/**
|
||||
* Validates that the given <tt>JoinAssociationPathExpression</tt> is a semantically correct.
|
||||
*
|
||||
* @param JoinAssociationPathExpression $pathExpression
|
||||
* @param array $token
|
||||
* @param integer $nestingLevel
|
||||
* @param array $deferredItem
|
||||
* @param mixed $AST
|
||||
*
|
||||
* @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];;
|
||||
|
||||
// Validating association field (*-to-one or *-to-many)
|
||||
|
@ -662,70 +652,106 @@ class Parser
|
|||
* SingleValuedAssociationPathExpression ::= IdentificationVariable "." {SingleValuedAssociationField "."}* SingleValuedAssociationField
|
||||
* CollectionValuedPathExpression ::= IdentificationVariable "." {SingleValuedAssociationField "."}* CollectionValuedAssociationField
|
||||
*
|
||||
* @param PathExpression $pathExpression
|
||||
* @param array $token
|
||||
* @param integer $nestingLevel
|
||||
* @param array $deferredItem
|
||||
* @param mixed $AST
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
private function _validatePathExpression(AST\PathExpression $pathExpression, $token, $nestingLevel)
|
||||
private function _validatePathExpression($deferredItem, $AST)
|
||||
{
|
||||
$qComp = $this->_queryComponents[$pathExpression->identificationVariable];
|
||||
$pathExpression = $deferredItem['expression'];
|
||||
$parts = $pathExpression->parts;
|
||||
$numParts = count($parts);
|
||||
|
||||
$qComp = $this->_queryComponents[$pathExpression->identificationVariable];
|
||||
|
||||
$aliasIdentificationVariable = $pathExpression->identificationVariable;
|
||||
$parentField = $pathExpression->identificationVariable;
|
||||
$class = $qComp['metadata'];
|
||||
$stateField = $collectionField = null;
|
||||
|
||||
foreach ($pathExpression->parts as $field) {
|
||||
$fieldType = null;
|
||||
$curIndex = 0;
|
||||
|
||||
foreach ($parts as $field) {
|
||||
// Check if it is not in a state field
|
||||
if ($stateField !== null) {
|
||||
if ($fieldType & AST\PathExpression::TYPE_STATE_FIELD) {
|
||||
$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
|
||||
if ($collectionField !== null) {
|
||||
if ($fieldType & AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION) {
|
||||
$this->semanticalError(
|
||||
'Cannot navigate through collection-valued field named ' . $collectionField,
|
||||
$token
|
||||
'Cannot navigate through collection field named ' . $field . ' on ' . $parentField,
|
||||
$deferredItem['token']
|
||||
);
|
||||
}
|
||||
|
||||
// Check if field exists
|
||||
// Check if field or association exists
|
||||
if ( ! isset($class->associationMappings[$field]) && ! isset($class->fieldMappings[$field])) {
|
||||
$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])) {
|
||||
$stateField = $field;
|
||||
} else if ($class->associationMappings[$field]->isOneToOne()) {
|
||||
$class = $this->_em->getClassMetadata($class->associationMappings[$field]->targetEntityName);
|
||||
$fieldType = AST\PathExpression::TYPE_STATE_FIELD;
|
||||
} 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;
|
||||
}
|
||||
|
||||
$aliasIdentificationVariable .= '.' . $field;
|
||||
|
||||
if ($assoc->isOneToOne()) {
|
||||
$fieldType = AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION;
|
||||
} else {
|
||||
$fieldType = AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION;
|
||||
}
|
||||
}
|
||||
|
||||
$curIndex++;
|
||||
}
|
||||
|
||||
// Recognize correct expression type
|
||||
$expressionType = null;
|
||||
|
||||
if ($stateField !== null) {
|
||||
$expressionType = AST\PathExpression::TYPE_STATE_FIELD;
|
||||
} else if ($collectionField !== null) {
|
||||
$expressionType = AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION;
|
||||
} else {
|
||||
$expressionType = AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION;
|
||||
}
|
||||
|
||||
// Validate if PathExpression is one of the expected types
|
||||
$expectedType = $pathExpression->expectedType;
|
||||
|
||||
if ( ! ($expectedType & $expressionType)) {
|
||||
if ( ! ($expectedType & $fieldType)) {
|
||||
// We need to recognize which was expected type(s)
|
||||
$expectedStringTypes = array();
|
||||
|
||||
// Validate state field type (field/column)
|
||||
// Validate state field type
|
||||
if ($expectedType & AST\PathExpression::TYPE_STATE_FIELD) {
|
||||
$expectedStringTypes[] = 'StateFieldPathExpression';
|
||||
}
|
||||
|
@ -749,15 +775,14 @@ class Parser
|
|||
$semanticalError .= implode(' or ', $expectedStringTypes) . ' expected.';
|
||||
}
|
||||
|
||||
$this->semanticalError($semanticalError, $token);
|
||||
$this->semanticalError($semanticalError, $deferredItem['token']);
|
||||
}
|
||||
|
||||
// We need to force the type in PathExpression
|
||||
$pathExpression->type = $expressionType;
|
||||
$pathExpression->type = $fieldType;
|
||||
|
||||
return $expressionType;
|
||||
return $fieldType;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* QueryLanguage ::= SelectStatement | UpdateStatement | DeleteStatement
|
||||
|
@ -769,7 +794,7 @@ class Parser
|
|||
public function QueryLanguage()
|
||||
{
|
||||
$this->_lexer->moveNext();
|
||||
|
||||
|
||||
switch ($this->_lexer->lookahead['type']) {
|
||||
case Lexer::T_SELECT:
|
||||
return $this->SelectStatement();
|
||||
|
@ -784,6 +809,11 @@ class Parser
|
|||
$this->syntaxError('SELECT, UPDATE or DELETE');
|
||||
break;
|
||||
}
|
||||
|
||||
// Check for end of string
|
||||
if ($this->_lexer->lookahead !== null) {
|
||||
$this->syntaxError('end of string');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -807,10 +837,7 @@ class Parser
|
|||
|
||||
$selectStatement->orderByClause = $this->_lexer->isNextToken(Lexer::T_ORDER)
|
||||
? $this->OrderByClause() : null;
|
||||
|
||||
// Activate semantical checks after this point. Process all deferred checks in pipeline
|
||||
$this->_processDeferredExpressionsStack();
|
||||
|
||||
|
||||
return $selectStatement;
|
||||
}
|
||||
|
||||
|
@ -824,9 +851,6 @@ class Parser
|
|||
$updateStatement = new AST\UpdateStatement($this->UpdateClause());
|
||||
$updateStatement->whereClause = $this->_lexer->isNextToken(Lexer::T_WHERE)
|
||||
? $this->WhereClause() : null;
|
||||
|
||||
// Activate semantical checks after this point. Process all deferred checks in pipeline
|
||||
$this->_processDeferredExpressionsStack();
|
||||
|
||||
return $updateStatement;
|
||||
}
|
||||
|
@ -841,9 +865,6 @@ class Parser
|
|||
$deleteStatement = new AST\DeleteStatement($this->DeleteClause());
|
||||
$deleteStatement->whereClause = $this->_lexer->isNextToken(Lexer::T_WHERE)
|
||||
? $this->WhereClause() : null;
|
||||
|
||||
// Activate semantical checks after this point. Process all deferred checks in pipeline
|
||||
$this->_processDeferredExpressionsStack();
|
||||
|
||||
return $deleteStatement;
|
||||
}
|
||||
|
@ -883,11 +904,13 @@ class Parser
|
|||
$this->match(Lexer::T_IDENTIFIER);
|
||||
|
||||
$aliasIdentVariable = $this->_lexer->token['value'];
|
||||
$exists = isset($this->_queryComponents[$aliasIdentVariable]);
|
||||
|
||||
// Apply AliasIdentificationVariable validation
|
||||
$this->_validateAliasIdentificationVariable(
|
||||
$aliasIdentVariable, $this->_lexer->token, $this->_nestingLevel
|
||||
);
|
||||
if ($exists) {
|
||||
$this->semanticalError(
|
||||
"'$aliasIdentVariable' is already defined.", $this->_lexer->token
|
||||
);
|
||||
}
|
||||
|
||||
return $aliasIdentVariable;
|
||||
}
|
||||
|
@ -902,15 +925,36 @@ class Parser
|
|||
$this->match(Lexer::T_IDENTIFIER);
|
||||
|
||||
$schemaName = $this->_lexer->token['value'];
|
||||
$exists = class_exists($schemaName, true);
|
||||
|
||||
// Apply AbstractSchemaName validation
|
||||
$this->_validateAbstractSchemaName(
|
||||
$schemaName, $this->_lexer->token, $this->_nestingLevel
|
||||
);
|
||||
if ( ! $exists) {
|
||||
$this->semanticalError("Class '$schemaName' is not defined.", $this->_lexer->token);
|
||||
}
|
||||
|
||||
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
|
||||
*
|
||||
|
@ -923,15 +967,11 @@ class Parser
|
|||
$resultVariable = $this->_lexer->token['value'];
|
||||
|
||||
// Defer ResultVariable validation
|
||||
$exprStack = array(
|
||||
'method' => 'ResultVariable',
|
||||
'expression' => $resultVariable,
|
||||
'nestingLevel' => $this->_nestingLevel,
|
||||
'token' => $this->_lexer->token,
|
||||
$this->_subscribeExpression(
|
||||
$resultVariable, 'ResultVariable',
|
||||
$this->_lexer->token, $this->_nestingLevel
|
||||
);
|
||||
|
||||
array_push($this->_deferredExpressionsStack, $exprStack);
|
||||
|
||||
|
||||
return $resultVariable;
|
||||
}
|
||||
|
||||
|
@ -953,14 +993,10 @@ class Parser
|
|||
$pathExpr = new AST\JoinAssociationPathExpression($identVariable, $field);
|
||||
|
||||
// Defer JoinAssociationPathExpression validation
|
||||
$exprStack = array(
|
||||
'method' => 'JoinAssociationPathExpression',
|
||||
'expression' => $pathExpr,
|
||||
'nestingLevel' => $this->_nestingLevel,
|
||||
'token' => $token,
|
||||
$this->_subscribeExpression(
|
||||
$pathExpr, 'JoinAssociationPathExpression',
|
||||
$token, $this->_nestingLevel
|
||||
);
|
||||
|
||||
array_push($this->_deferredExpressionsStack, $exprStack);
|
||||
|
||||
return $pathExpr;
|
||||
}
|
||||
|
@ -969,7 +1005,7 @@ class Parser
|
|||
* Parses an arbitrary path expression and defers semantical validation
|
||||
* based on expected types.
|
||||
*
|
||||
* PathExpression ::= IdentificationVariable "." {identifier "."}* identifier
|
||||
* PathExpression ::= IdentificationVariable {"." identifier}* "." identifier
|
||||
*
|
||||
* @param integer $expectedTypes
|
||||
* @return \Doctrine\ORM\Query\AST\PathExpression
|
||||
|
@ -991,14 +1027,10 @@ class Parser
|
|||
$pathExpr = new AST\PathExpression($expectedTypes, $identVariable, $parts);
|
||||
|
||||
// Defer PathExpression validation if requested to be defered
|
||||
$exprStack = array(
|
||||
'method' => 'PathExpression',
|
||||
'expression' => $pathExpr,
|
||||
'nestingLevel' => $this->_nestingLevel,
|
||||
'token' => $token,
|
||||
$this->_subscribeExpression(
|
||||
$pathExpr, 'PathExpression',
|
||||
$token, $this->_nestingLevel
|
||||
);
|
||||
|
||||
array_push($this->_deferredExpressionsStack, $exprStack);
|
||||
|
||||
return $pathExpr;
|
||||
}
|
||||
|
@ -1598,7 +1630,7 @@ class Parser
|
|||
);
|
||||
}
|
||||
|
||||
$targetClassName = $parentClass->getAssociationMapping($assocField)->getTargetEntityName();
|
||||
$targetClassName = $parentClass->getAssociationMapping($assocField)->targetEntityName;
|
||||
|
||||
// Building queryComponent
|
||||
$joinQueryComponent = array(
|
||||
|
@ -1651,7 +1683,7 @@ class Parser
|
|||
/**
|
||||
* SelectExpression ::=
|
||||
* IdentificationVariable | StateFieldPathExpression |
|
||||
* (AggregateExpression | "(" Subselect ")" | FunctionDeclaration) [["AS"] ResultVariable]
|
||||
* (AggregateExpression | "(" Subselect ")" | FunctionDeclaration) [["AS"] AliasResultVariable]
|
||||
*
|
||||
* @return \Doctrine\ORM\Query\AST\SelectExpression
|
||||
*/
|
||||
|
@ -1683,9 +1715,9 @@ class Parser
|
|||
|
||||
if ($this->_lexer->isNextToken(Lexer::T_IDENTIFIER)) {
|
||||
$token = $this->_lexer->lookahead;
|
||||
$fieldAliasIdentificationVariable = $this->ResultVariable();
|
||||
$fieldAliasIdentificationVariable = $this->AliasResultVariable();
|
||||
|
||||
// Include ResultVariable in query components.
|
||||
// Include AliasResultVariable in query components.
|
||||
$this->_queryComponents[$fieldAliasIdentificationVariable] = array(
|
||||
'resultVariable' => $expression,
|
||||
'nestingLevel' => $this->_nestingLevel,
|
||||
|
@ -1708,7 +1740,7 @@ class Parser
|
|||
}
|
||||
|
||||
/**
|
||||
* SimpleSelectExpression ::= StateFieldPathExpression | IdentificationVariable | (AggregateExpression [["AS"] ResultVariable])
|
||||
* SimpleSelectExpression ::= StateFieldPathExpression | IdentificationVariable | (AggregateExpression [["AS"] AliasResultVariable])
|
||||
*
|
||||
* @return \Doctrine\ORM\Query\AST\SimpleSelectExpression
|
||||
*/
|
||||
|
@ -1735,10 +1767,10 @@ class Parser
|
|||
|
||||
if ($this->_lexer->isNextToken(Lexer::T_IDENTIFIER)) {
|
||||
$token = $this->_lexer->lookahead;
|
||||
$resultVariable = $this->ResultVariable();
|
||||
$resultVariable = $this->AliasResultVariable();
|
||||
$expr->fieldIdentificationVariable = $resultVariable;
|
||||
|
||||
// Include ResultVariable in query components.
|
||||
// Include AliasResultVariable in query components.
|
||||
$this->_queryComponents[$resultVariable] = array(
|
||||
'resultvariable' => $expr,
|
||||
'nestingLevel' => $this->_nestingLevel,
|
||||
|
|
|
@ -414,9 +414,8 @@ class SqlWalker implements TreeWalker
|
|||
switch ($pathExpr->type) {
|
||||
case AST\PathExpression::TYPE_STATE_FIELD:
|
||||
$parts = $pathExpr->parts;
|
||||
$numParts = count($parts);
|
||||
$dqlAlias = $pathExpr->identificationVariable;
|
||||
$fieldName = $parts[$numParts - 1];
|
||||
$fieldName = array_pop($parts);
|
||||
$dqlAlias = $pathExpr->identificationVariable . (( ! empty($parts)) ? '.' . implode('.', $parts) : '');
|
||||
$qComp = $this->_queryComponents[$dqlAlias];
|
||||
$class = $qComp['metadata'];
|
||||
|
||||
|
@ -609,20 +608,18 @@ class SqlWalker implements TreeWalker
|
|||
*/
|
||||
public function walkOrderByItem($orderByItem)
|
||||
{
|
||||
$sql = '';
|
||||
$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) . '.'
|
||||
. $columnName . ' ' . strtoupper($orderByItem->type);
|
||||
$sql = $this->walkPathExpression($expr);
|
||||
} else {
|
||||
$columnName = $this->_queryComponents[$expr]['token']['value'];
|
||||
|
||||
return $this->_scalarResultAliasMap[$columnName] . ' ' . strtoupper($orderByItem->type);
|
||||
$sql = $this->_scalarResultAliasMap[$columnName];
|
||||
}
|
||||
|
||||
return $sql . ' ' . strtoupper($orderByItem->type);;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -771,16 +768,15 @@ class SqlWalker implements TreeWalker
|
|||
if ($expr instanceof AST\PathExpression) {
|
||||
if ($expr->type == AST\PathExpression::TYPE_STATE_FIELD) {
|
||||
$parts = $expr->parts;
|
||||
$numParts = count($parts);
|
||||
$dqlAlias = $expr->identificationVariable;
|
||||
$fieldName = $parts[$numParts - 1];
|
||||
$fieldName = array_pop($parts);
|
||||
$dqlAlias = $expr->identificationVariable . (( ! empty($parts)) ? '.' . implode('.', $parts) : '');
|
||||
$qComp = $this->_queryComponents[$dqlAlias];
|
||||
$class = $qComp['metadata'];
|
||||
|
||||
if ( ! isset($this->_selectedClasses[$dqlAlias])) {
|
||||
$this->_selectedClasses[$dqlAlias] = $class;
|
||||
}
|
||||
|
||||
|
||||
$sqlTableAlias = $this->getSqlTableAlias($class->getTableName(), $dqlAlias);
|
||||
$columnName = $class->getQuotedColumnName($fieldName, $this->_platform);
|
||||
$columnAlias = $this->getSqlColumnAlias($class->columnNames[$fieldName]);
|
||||
|
@ -1004,17 +1000,8 @@ class SqlWalker implements TreeWalker
|
|||
*/
|
||||
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 ' : '')
|
||||
. $this->getSqlTableAlias($qComp['metadata']->getTableName(), $dqlAlias) . '.'
|
||||
. $columnName . ')';
|
||||
. $this->walkPathExpression($aggExpression->pathExpression) . ')';
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1038,12 +1025,7 @@ class SqlWalker implements TreeWalker
|
|||
*/
|
||||
public function walkGroupByItem(AST\PathExpression $pathExpr)
|
||||
{
|
||||
$parts = $pathExpr->parts;
|
||||
$dqlAlias = $pathExpr->identificationVariable;
|
||||
$qComp = $this->_queryComponents[$dqlAlias];
|
||||
$columnName = $qComp['metadata']->getQuotedColumnName($parts[0], $this->_platform);
|
||||
|
||||
return $this->getSqlTableAlias($qComp['metadata']->getTableName(), $dqlAlias) . '.' . $columnName;
|
||||
return $this->walkPathExpression($pathExpr);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1219,8 +1201,10 @@ class SqlWalker implements TreeWalker
|
|||
$sql .= 'EXISTS (SELECT 1 FROM ';
|
||||
$entityExpr = $collMemberExpr->entityExpression;
|
||||
$collPathExpr = $collMemberExpr->collectionValuedPathExpression;
|
||||
|
||||
$parts = $collPathExpr->parts;
|
||||
$dqlAlias = $collPathExpr->identificationVariable;
|
||||
$fieldName = array_pop($parts);
|
||||
$dqlAlias = $collPathExpr->identificationVariable . (( ! empty($parts)) ? '.' . implode('.', $parts) : '');
|
||||
|
||||
$class = $this->_queryComponents[$dqlAlias]['metadata'];
|
||||
|
||||
|
@ -1232,7 +1216,7 @@ class SqlWalker implements TreeWalker
|
|||
throw DoctrineException::notImplemented();
|
||||
}
|
||||
|
||||
$assoc = $class->associationMappings[$parts[0]];
|
||||
$assoc = $class->associationMappings[$fieldName];
|
||||
|
||||
if ($assoc->isOneToMany()) {
|
||||
$targetClass = $this->_em->getClassMetadata($assoc->targetEntityName);
|
||||
|
|
|
@ -58,6 +58,25 @@ class LanguageRecognitionTest extends \Doctrine\Tests\OrmTestCase
|
|||
|
||||
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()
|
||||
{
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
public function testSelectMultipleComponentsWithAsterisk2()
|
||||
{
|
||||
$this->assertValidDql('SELECT a.user.name FROM Doctrine\Tests\Models\CMS\CmsArticle a');
|
||||
}
|
||||
|
||||
public function testSelectDistinctIsSupported()
|
||||
{
|
||||
$this->assertValidDql('SELECT DISTINCT u.name FROM Doctrine\Tests\Models\CMS\CmsUser u');
|
||||
|
@ -93,6 +117,11 @@ class LanguageRecognitionTest extends \Doctrine\Tests\OrmTestCase
|
|||
{
|
||||
$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()
|
||||
{
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
namespace Doctrine\Tests\ORM\Query;
|
||||
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Query,
|
||||
Doctrine\Common\DoctrineException;
|
||||
|
||||
require_once __DIR__ . '/../../TestInit.php';
|
||||
|
||||
|
@ -22,7 +23,7 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase
|
|||
$query->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true);
|
||||
parent::assertEquals($sqlToBeConfirmed, $query->getSql());
|
||||
$query->free();
|
||||
} catch (Doctrine_Exception $e) {
|
||||
} catch (DoctrineException $e) {
|
||||
$this->fail($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
@ -42,6 +43,22 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase
|
|||
'SELECT c0_.id AS id0 FROM cms_users c0_'
|
||||
);
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
|
|
Loading…
Add table
Reference in a new issue