diff --git a/lib/Doctrine/Common/DoctrineException.php b/lib/Doctrine/Common/DoctrineException.php index 994d79fe9..725638650 100644 --- a/lib/Doctrine/Common/DoctrineException.php +++ b/lib/Doctrine/Common/DoctrineException.php @@ -124,10 +124,6 @@ class DoctrineException extends \Exception if ( ! self::$_messages) { // Lazy-init messages self::$_messages = array( - 'DoctrineException#partialObjectsAreDangerous' => - "Loading partial objects is dangerous. Fetch full objects or consider " . - "using a different fetch mode. If you really want partial objects, " . - "set the doctrine.forcePartialLoad query hint to TRUE.", 'QueryException#nonUniqueResult' => "The query contains more than one result." ); diff --git a/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php index 2fe8acf22..8ba3b18ac 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php @@ -79,6 +79,9 @@ class ArrayHydrator extends AbstractHydrator if (isset($rowData['scalars'])) { $scalars = $rowData['scalars']; unset($rowData['scalars']); + if (empty($rowData)) { + ++$this->_resultCounter; + } } // 2) Now hydrate the data found in the current row. @@ -129,7 +132,7 @@ class ArrayHydrator extends AbstractHydrator } end($baseElement[$relationAlias]); $this->_identifierMap[$path][$id[$parent]][$id[$dqlAlias]] = - key($baseElement[$relationAlias]); + key($baseElement[$relationAlias]); } } else if ( ! isset($baseElement[$relationAlias])) { $baseElement[$relationAlias] = array(); @@ -177,6 +180,10 @@ class ArrayHydrator extends AbstractHydrator $this->_identifierMap[$dqlAlias][$id[$dqlAlias]] = key($result); } else { $index = $this->_identifierMap[$dqlAlias][$id[$dqlAlias]]; + /*if ($this->_rsm->isMixed) { + $result[] =& $result[$index]; + ++$this->_resultCounter; + }*/ } $this->updateResultPointer($result, $index, $dqlAlias, false); } diff --git a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php index 1eee52e6e..7a364bb25 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php @@ -251,6 +251,9 @@ class ObjectHydrator extends AbstractHydrator if (isset($rowData['scalars'])) { $scalars = $rowData['scalars']; unset($rowData['scalars']); + if (empty($rowData)) { + ++$this->_resultCounter; + } } // Hydrate the data chunks @@ -409,14 +412,18 @@ class ObjectHydrator extends AbstractHydrator $this->_identifierMap[$dqlAlias][$id[$dqlAlias]] = $this->_resultCounter; ++$this->_resultCounter; } - + // Update result pointer $this->_resultPointers[$dqlAlias] = $element; - + } else { // Update result pointer $index = $this->_identifierMap[$dqlAlias][$id[$dqlAlias]]; $this->_resultPointers[$dqlAlias] = $result[$index]; + /*if ($this->_rsm->isMixed) { + $result[] = $result[$index]; + ++$this->_resultCounter; + }*/ } } } diff --git a/lib/Doctrine/ORM/Mapping/OneToOneMapping.php b/lib/Doctrine/ORM/Mapping/OneToOneMapping.php index 32f96ab4a..4a0cf3480 100644 --- a/lib/Doctrine/ORM/Mapping/OneToOneMapping.php +++ b/lib/Doctrine/ORM/Mapping/OneToOneMapping.php @@ -278,7 +278,7 @@ class OneToOneMapping extends AssociationMapping /** * @internal Experimental. For MetaModel API, Doctrine 2.1 or later. */ - public static function __set_state(array $state) + /*public static function __set_state(array $state) { $assoc = new self(array()); $assoc->isOptional = $state['isOptional']; @@ -302,5 +302,5 @@ class OneToOneMapping extends AssociationMapping $assoc->sourceFieldName = $state['sourceFieldName']; return $assoc; - } + }*/ } diff --git a/lib/Doctrine/ORM/Query/AST/ASTException.php b/lib/Doctrine/ORM/Query/AST/ASTException.php new file mode 100644 index 000000000..7472572e4 --- /dev/null +++ b/lib/Doctrine/ORM/Query/AST/ASTException.php @@ -0,0 +1,13 @@ +identificationVariable = $identificationVariable; + $this->partialFieldSet = $partialFieldSet; + } +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Query/Lexer.php b/lib/Doctrine/ORM/Query/Lexer.php index 3fff56fc3..b956291d5 100644 --- a/lib/Doctrine/ORM/Query/Lexer.php +++ b/lib/Doctrine/ORM/Query/Lexer.php @@ -88,7 +88,6 @@ class Lexer extends \Doctrine\Common\Lexer const T_NULL = 145; const T_OF = 146; const T_OFFSET = 147; - const T_ON = 148; const T_OPEN_PARENTHESIS = 149; const T_OR = 150; const T_ORDER = 151; @@ -104,8 +103,9 @@ class Lexer extends \Doctrine\Common\Lexer const T_UPDATE = 161; const T_WHERE = 162; const T_WITH = 163; - - private $_keywordsTable; + const T_PARTIAL = 164; + const T_OPEN_CURLY_BRACE = 165; + const T_CLOSE_CURLY_BRACE = 166; /** * Creates a new query scanner object. @@ -151,7 +151,7 @@ class Lexer extends \Doctrine\Common\Lexer $value = $newVal; return (strpos($value, '.') !== false || stripos($value, 'e') !== false) - ? self::T_FLOAT : self::T_INTEGER; + ? self::T_FLOAT : self::T_INTEGER; } if ($value[0] === "'") { @@ -176,6 +176,8 @@ class Lexer extends \Doctrine\Common\Lexer case '*': return self::T_MULTIPLY; case '/': return self::T_DIVIDE; case '!': return self::T_NEGATE; + case '{': return self::T_OPEN_CURLY_BRACE; + case '}': return self::T_CLOSE_CURLY_BRACE; default: // Do nothing break; @@ -186,7 +188,7 @@ class Lexer extends \Doctrine\Common\Lexer } /** - * @todo Doc + * @todo Inline this method. */ private function _getNumeric($value) { @@ -206,6 +208,7 @@ class Lexer extends \Doctrine\Common\Lexer * * @param string $identifier identifier name * @return int token type + * @todo Inline this method. */ private function _checkLiteral($identifier) { diff --git a/lib/Doctrine/ORM/Query/Parser.php b/lib/Doctrine/ORM/Query/Parser.php index 864848248..cc4faffc1 100644 --- a/lib/Doctrine/ORM/Query/Parser.php +++ b/lib/Doctrine/ORM/Query/Parser.php @@ -21,11 +21,11 @@ namespace Doctrine\ORM\Query; -use Doctrine\Common\DoctrineException, +use Doctrine\Common\DoctrineException, //TODO: Remove Doctrine\ORM\Query; /** - * An LL(*) parser for the context-free grammar of the Doctrine Query Language. + * An LL(*) recursive-descent parser for the context-free grammar of the Doctrine Query Language. * Parses a DQL query, reports any errors in it, and generates an AST. * * @license http://www.opensource.org/licenses/lgpl-license.php LGPL @@ -68,10 +68,11 @@ class Parser /** * Expressions that were encountered during parsing of identifiers and expressions * and still need to be validated. - * - * @var array */ - private $_deferredExpressionsStack = array(); + private $_deferredIdentificationVariables = array(); + private $_deferredPartialObjectExpressions = array(); + private $_deferredPathExpressions = array(); + private $_deferredResultVariables = array(); /** * The lexer. @@ -233,7 +234,8 @@ class Parser * error. * * @param int|string token type or value - * @return bool True, if tokens match; false otherwise. + * @return void + * @throws QueryException If the tokens dont match. */ public function match($token) { @@ -274,9 +276,19 @@ class Parser // Parse & build AST $AST = $this->QueryLanguage(); - // Activate semantical checks after this point. Process all deferred checks in pipeline - $this->_processDeferredExpressionsStack($AST); - + // Process any deferred validations of some nodes in the AST. + // This also allows post-processing of the AST for modification purposes. + $this->_processDeferredIdentificationVariables(); + if ($this->_deferredPartialObjectExpressions) { + $this->_processDeferredPartialObjectExpressions(); + } + if ($this->_deferredPathExpressions) { + $this->_processDeferredPathExpressions($AST); + } + if ($this->_deferredResultVariables) { + $this->_processDeferredResultVariables(); + } + if ($customWalkers = $this->_query->getHint(Query::HINT_CUSTOM_TREE_WALKERS)) { $this->_customTreeWalkers = $customWalkers; } @@ -317,8 +329,8 @@ class Parser /** * Generates a new syntax error. * - * @param string $expected Optional expected string. - * @param array $token Optional token. + * @param string $expected Expected string. + * @param array $token Got token. * * @throws \Doctrine\ORM\Query\QueryException */ @@ -483,146 +495,101 @@ class Parser return ($la['value'] === '(' && $next['type'] === Lexer::T_SELECT); } - - /** - * Subscribe expression to be validated. - * - * @param mixed $expression - * @param string $method - * @param array $token - * @param integer $nestingLevel - */ - private function _subscribeExpression($expression, $method, $token, $nestingLevel = null) - { - $nestingLevel = ($nestingLevel !== null) ?: $this->_nestingLevel; - - $exprStack = array( - 'method' => $method, - 'expression' => $expression, - 'nestingLevel' => $nestingLevel, - 'token' => $token, - ); - - array_push($this->_deferredExpressionsStack, $exprStack); - } - /** - * Processes the topmost stack of deferred path expressions. - * - * @param mixed $AST - */ - private function _processDeferredExpressionsStack($AST) - { - foreach ($this->_deferredExpressionsStack as $item) { - $method = '_validate' . $item['method']; - - $this->$method($item, $AST); - } - } - - /** * Validates that the given IdentificationVariable is a semantically correct. * It must exist in query components list. * - * @param array $deferredItem - * @param mixed $AST - * - * @return array Query Component + * @return void */ - private function _validateIdentificationVariable($deferredItem, $AST) + private function _processDeferredIdentificationVariables() { - $identVariable = $deferredItem['expression']; - - // Check if IdentificationVariable exists in queryComponents - if ( ! isset($this->_queryComponents[$identVariable])) { - $this->semanticalError( + foreach ($this->_deferredIdentificationVariables as $deferredItem) { + $identVariable = $deferredItem['expression']; + + // Check if IdentificationVariable exists in queryComponents + if ( ! isset($this->_queryComponents[$identVariable])) { + $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( + ); + } + + $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.", $deferredItem['token'] - ); - } - - // Validate if identification variable nesting level is lower or equal than the current one - if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) { - $this->semanticalError( + ); + } + + // Validate if identification variable nesting level is lower or equal than the current one + if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) { + $this->semanticalError( "'$identVariable' is used outside the scope of its declaration.", $deferredItem['token'] - ); + ); + } + } + } + + private function _processDeferredPartialObjectExpressions() + { + foreach ($this->_deferredPartialObjectExpressions as $deferredItem) { + $expr = $deferredItem['expression']; + $class = $this->_queryComponents[$expr->identificationVariable]['metadata']; + foreach ($expr->partialFieldSet as $field) { + if ( ! isset($class->fieldMappings[$field])) { + $this->semanticalError( + "There is no mapped field named '$field' on class " . $class->name . ".", + $deferredItem['token'] + ); + } + } + if (array_intersect($class->identifier, $expr->partialFieldSet) != $class->identifier) { + $this->semanticalError( + "The partial field selection of class " . $class->name . " must contain the identifier.", + $deferredItem['token'] + ); + } } - - return $qComp; } /** * Validates that the given ResultVariable is a semantically correct. * It must exist in query components list. * - * @param array $deferredItem - * @param mixed $AST - * - * @return array Query Component + * @return void */ - private function _validateResultVariable($deferredItem, $AST) + private function _processDeferredResultVariables() { - $resultVariable = $deferredItem['expression']; - - // Check if ResultVariable exists in queryComponents - if ( ! isset($this->_queryComponents[$resultVariable])) { - $this->semanticalError( + foreach ($this->_deferredResultVariables as $deferredItem) { + $resultVariable = $deferredItem['expression']; + + // Check if ResultVariable exists in queryComponents + if ( ! isset($this->_queryComponents[$resultVariable])) { + $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( + ); + } + + $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.", $deferredItem['token'] - ); - } - - // Validate if identification variable nesting level is lower or equal than the current one - if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) { - $this->semanticalError( + ); + } + + // Validate if identification variable nesting level is lower or equal than the current one + if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) { + $this->semanticalError( "'$resultVariable' is used outside the scope of its declaration.", $deferredItem['token'] - ); + ); + } } - - return $qComp; } - - /** - * Validates that the given JoinAssociationPathExpression is a semantically correct. - * - * @param array $deferredItem - * @param mixed $AST - * - * @return array Query Component - */ - private function _validateJoinAssociationPathExpression($deferredItem, $AST) - { - $pathExpression = $deferredItem['expression']; - $qComp = $this->_queryComponents[$pathExpression->identificationVariable];; - - // Validating association field (*-to-one or *-to-many) - $field = $pathExpression->associationField; - $class = $qComp['metadata']; - - if ( ! isset($class->associationMappings[$field])) { - $this->semanticalError('Class ' . $class->name . ' has no association named ' . $field); - } - - return $qComp; - } - + /** * Validates that the given PathExpression is a semantically correct for grammar rules: * @@ -634,136 +601,135 @@ class Parser * * @param array $deferredItem * @param mixed $AST - * - * @return integer */ - private function _validatePathExpression($deferredItem, $AST) + private function _processDeferredPathExpressions($AST) { - $pathExpression = $deferredItem['expression']; - $parts = $pathExpression->parts; - $numParts = count($parts); - - $qComp = $this->_queryComponents[$pathExpression->identificationVariable]; - - $aliasIdentificationVariable = $pathExpression->identificationVariable; - $parentField = $pathExpression->identificationVariable; - $class = $qComp['metadata']; - $fieldType = null; - $curIndex = 0; - - foreach ($parts as $field) { - // Check if it is not in a state field - if ($fieldType & AST\PathExpression::TYPE_STATE_FIELD) { - $this->semanticalError( + foreach ($this->_deferredPathExpressions as $deferredItem) { + $pathExpression = $deferredItem['expression']; + $parts = $pathExpression->parts; + $numParts = count($parts); + + $qComp = $this->_queryComponents[$pathExpression->identificationVariable]; + + $aliasIdentificationVariable = $pathExpression->identificationVariable; + $parentField = $pathExpression->identificationVariable; + $class = $qComp['metadata']; + $fieldType = null; + $curIndex = 0; + + foreach ($parts as $field) { + // Check if it is not in a state field + if ($fieldType & AST\PathExpression::TYPE_STATE_FIELD) { + $this->semanticalError( 'Cannot navigate through state field named ' . $field . ' on ' . $parentField, $deferredItem['token'] - ); - } - - // Check if it is not a collection field - if ($fieldType & AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION) { - $this->semanticalError( + ); + } + + // Check if it is not a collection field + if ($fieldType & AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION) { + $this->semanticalError( 'Cannot navigate through collection field named ' . $field . ' on ' . $parentField, $deferredItem['token'] - ); - } - - // Check if field or association exists - if ( ! isset($class->associationMappings[$field]) && ! isset($class->fieldMappings[$field])) { - $this->semanticalError( + ); + } + + // 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, $deferredItem['token'] - ); - } - - $parentField = $field; - - if (isset($class->fieldMappings[$field])) { - $fieldType = AST\PathExpression::TYPE_STATE_FIELD; - } else { - $assoc = $class->associationMappings[$field]; - $class = $this->_em->getClassMetadata($assoc->targetEntityName); + ); + } - if ( + $parentField = $field; + + if (isset($class->fieldMappings[$field])) { + $fieldType = AST\PathExpression::TYPE_STATE_FIELD; + } else { + $assoc = $class->associationMappings[$field]; + $class = $this->_em->getClassMetadata($assoc->targetEntityName); + + if ( ($curIndex != $numParts - 1) && ! isset($this->_queryComponents[$aliasIdentificationVariable . '.' . $field]) - ) { - // Building queryComponent - $joinQueryComponent = array( + ) { + // 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( + // Create AST node + $joinVariableDeclaration = new AST\JoinVariableDeclaration( new AST\Join( - AST\Join::JOIN_TYPE_INNER, - new AST\JoinAssociationPathExpression($aliasIdentificationVariable, $field), - $aliasIdentificationVariable . '.' . $field + AST\Join::JOIN_TYPE_INNER, + new AST\JoinAssociationPathExpression($aliasIdentificationVariable, $field), + $aliasIdentificationVariable . '.' . $field, + false ), 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++; - } - - // Validate if PathExpression is one of the expected types - $expectedType = $pathExpression->expectedType; + ); + $AST->fromClause->identificationVariableDeclarations[0]->joinVariableDeclarations[] = $joinVariableDeclaration; - if ( ! ($expectedType & $fieldType)) { - // We need to recognize which was expected type(s) - $expectedStringTypes = array(); - - // Validate state field type - if ($expectedType & AST\PathExpression::TYPE_STATE_FIELD) { - $expectedStringTypes[] = 'StateFieldPathExpression'; + $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; } - - // Validate single valued association (*-to-one) - if ($expectedType & AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION) { - $expectedStringTypes[] = 'SingleValuedAssociationField'; + + // Validate if PathExpression is one of the expected types + $expectedType = $pathExpression->expectedType; + + if ( ! ($expectedType & $fieldType)) { + // We need to recognize which was expected type(s) + $expectedStringTypes = array(); + + // Validate state field type + if ($expectedType & AST\PathExpression::TYPE_STATE_FIELD) { + $expectedStringTypes[] = 'StateFieldPathExpression'; + } + + // Validate single valued association (*-to-one) + if ($expectedType & AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION) { + $expectedStringTypes[] = 'SingleValuedAssociationField'; + } + + // Validate single valued association (*-to-many) + if ($expectedType & AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION) { + $expectedStringTypes[] = 'CollectionValuedAssociationField'; + } + + // Build the error message + $semanticalError = 'Invalid PathExpression. '; + + if (count($expectedStringTypes) == 1) { + $semanticalError .= 'Must be a ' . $expectedStringTypes[0] . '.'; + } else { + $semanticalError .= implode(' or ', $expectedStringTypes) . ' expected.'; + } + + $this->semanticalError($semanticalError, $deferredItem['token']); } - - // Validate single valued association (*-to-many) - if ($expectedType & AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION) { - $expectedStringTypes[] = 'CollectionValuedAssociationField'; - } - - // Build the error message - $semanticalError = 'Invalid PathExpression. '; - - if (count($expectedStringTypes) == 1) { - $semanticalError .= 'Must be a ' . $expectedStringTypes[0] . '.'; - } else { - $semanticalError .= implode(' or ', $expectedStringTypes) . ' expected.'; - } - - $this->semanticalError($semanticalError, $deferredItem['token']); + + // We need to force the type in PathExpression + $pathExpression->type = $fieldType; } - - // We need to force the type in PathExpression - $pathExpression->type = $fieldType; - - return $fieldType; } - + /** * QueryLanguage ::= SelectStatement | UpdateStatement | DeleteStatement * @@ -777,25 +743,26 @@ class Parser switch ($this->_lexer->lookahead['type']) { case Lexer::T_SELECT: - return $this->SelectStatement(); - + $statement = $this->SelectStatement(); + break; case Lexer::T_UPDATE: - return $this->UpdateStatement(); - + $statement = $this->UpdateStatement(); + break; case Lexer::T_DELETE: - return $this->DeleteStatement(); - + $statement = $this->DeleteStatement(); + break; default: $this->syntaxError('SELECT, UPDATE or DELETE'); break; } - + // Check for end of string if ($this->_lexer->lookahead !== null) { $this->syntaxError('end of string'); } + + return $statement; } - /** * SelectStatement ::= SelectClause FromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause] @@ -805,7 +772,7 @@ class Parser public function SelectStatement() { $selectStatement = new AST\SelectStatement($this->SelectClause(), $this->FromClause()); - + $selectStatement->whereClause = $this->_lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null; @@ -830,11 +797,11 @@ class Parser { $updateStatement = new AST\UpdateStatement($this->UpdateClause()); $updateStatement->whereClause = $this->_lexer->isNextToken(Lexer::T_WHERE) - ? $this->WhereClause() : null; + ? $this->WhereClause() : null; return $updateStatement; } - + /** * DeleteStatement ::= DeleteClause [WhereClause] * @@ -844,12 +811,11 @@ class Parser { $deleteStatement = new AST\DeleteStatement($this->DeleteClause()); $deleteStatement->whereClause = $this->_lexer->isNextToken(Lexer::T_WHERE) - ? $this->WhereClause() : null; + ? $this->WhereClause() : null; return $deleteStatement; } - - + /** * IdentificationVariable ::= identifier * @@ -860,20 +826,16 @@ class Parser $this->match(Lexer::T_IDENTIFIER); $identVariable = $this->_lexer->token['value']; - - // Defer IdentificationVariable validation - $exprStack = array( - 'method' => 'IdentificationVariable', + + $this->_deferredIdentificationVariables[] = array( 'expression' => $identVariable, 'nestingLevel' => $this->_nestingLevel, 'token' => $this->_lexer->token, ); - array_push($this->_deferredExpressionsStack, $exprStack); - return $identVariable; } - + /** * AliasIdentificationVariable = identifier * @@ -894,7 +856,7 @@ class Parser return $aliasIdentVariable; } - + /** * AbstractSchemaName ::= identifier * @@ -913,7 +875,7 @@ class Parser return $schemaName; } - + /** * AliasResultVariable ::= identifier * @@ -934,7 +896,7 @@ class Parser return $resultVariable; } - + /** * ResultVariable ::= identifier * @@ -947,14 +909,14 @@ class Parser $resultVariable = $this->_lexer->token['value']; // Defer ResultVariable validation - $this->_subscribeExpression( - $resultVariable, 'ResultVariable', - $this->_lexer->token, $this->_nestingLevel + $this->_deferredResultVariables[] = array( + 'expression' => $resultVariable, + 'nestingLevel' => $this->_nestingLevel, + 'token' => $this->_lexer->token, ); return $resultVariable; } - /** * JoinAssociationPathExpression ::= IdentificationVariable "." (CollectionValuedAssociationField | SingleValuedAssociationField) @@ -967,18 +929,19 @@ class Parser $identVariable = $this->IdentificationVariable(); $this->match(Lexer::T_DOT); + //TODO: $this->match($this->_lexer->lookahead['value']); $this->match(Lexer::T_IDENTIFIER); $field = $this->_lexer->token['value']; - $pathExpr = new AST\JoinAssociationPathExpression($identVariable, $field); + // Validate association field + $qComp = $this->_queryComponents[$identVariable]; + $class = $qComp['metadata']; - // Defer JoinAssociationPathExpression validation - $this->_subscribeExpression( - $pathExpr, 'JoinAssociationPathExpression', - $token, $this->_nestingLevel - ); - - return $pathExpr; + if ( ! isset($class->associationMappings[$field])) { + $this->semanticalError('Class ' . $class->name . ' has no association named ' . $field); + } + + return new AST\JoinAssociationPathExpression($identVariable, $field); } /** @@ -1007,14 +970,15 @@ class Parser $pathExpr = new AST\PathExpression($expectedTypes, $identVariable, $parts); // Defer PathExpression validation if requested to be defered - $this->_subscribeExpression( - $pathExpr, 'PathExpression', - $token, $this->_nestingLevel + $this->_deferredPathExpressions[] = array( + 'expression' => $pathExpr, + 'nestingLevel' => $this->_nestingLevel, + 'token' => $this->_lexer->token, ); return $pathExpr; } - + /** * AssociationPathExpression ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression * @@ -1027,7 +991,7 @@ class Parser AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION ); } - + /** * SingleValuedPathExpression ::= StateFieldPathExpression | SingleValuedAssociationPathExpression * @@ -1040,7 +1004,7 @@ class Parser AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION ); } - + /** * StateFieldPathExpression ::= SimpleStateFieldPathExpression | SimpleStateFieldAssociationPathExpression * @@ -1050,7 +1014,7 @@ class Parser { return $this->PathExpression(AST\PathExpression::TYPE_STATE_FIELD); } - + /** * SingleValuedAssociationPathExpression ::= IdentificationVariable "." {SingleValuedAssociationField "."}* SingleValuedAssociationField * @@ -1060,7 +1024,7 @@ class Parser { return $this->PathExpression(AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION); } - + /** * CollectionValuedPathExpression ::= IdentificationVariable "." {SingleValuedAssociationField "."}* CollectionValuedAssociationField * @@ -1070,7 +1034,7 @@ class Parser { return $this->PathExpression(AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION); } - + /** * SimpleStateFieldPathExpression ::= IdentificationVariable "." StateField * @@ -1091,7 +1055,6 @@ class Parser return $pathExpression; } - /** * SelectClause ::= "SELECT" ["DISTINCT"] SelectExpression {"," SelectExpression} * @@ -1304,7 +1267,7 @@ class Parser return new AST\GroupByClause($groupByItems); } - + /** * OrderByClause ::= "ORDER" "BY" OrderByItem {"," OrderByItem}* * @@ -1356,7 +1319,6 @@ class Parser return $subselect; } - /** * UpdateItem ::= IdentificationVariable "." {StateField | SingleValuedAssociationField} "=" NewValue * @@ -1403,13 +1365,10 @@ class Parser if ($glimpse['value'] != '.') { $token = $this->_lexer->lookahead; $identVariable = $this->IdentificationVariable(); - - // Validate if IdentificationVariable is defined - $this->_validateIdentificationVariable($identVariable, null, $token); - + return $identVariable; } - + return $this->SingleValuedPathExpression(); } @@ -1472,7 +1431,6 @@ class Parser return $this->SimpleArithmeticExpression(); } - /** * IdentificationVariableDeclaration ::= RangeVariableDeclaration [IndexBy] {JoinVariableDeclaration}* * @@ -1528,7 +1486,7 @@ class Parser { $join = $this->Join(); $indexBy = $this->_lexer->isNextToken(Lexer::T_INDEX) - ? $this->IndexBy() : null; + ? $this->IndexBy() : null; return new AST\JoinVariableDeclaration($join, $indexBy); } @@ -1536,12 +1494,12 @@ class Parser /** * RangeVariableDeclaration ::= AbstractSchemaName ["AS"] AliasIdentificationVariable * - * @return \Doctrine\ORM\Query\AST\RangeVariableDeclaration + * @return Doctrine\ORM\Query\AST\RangeVariableDeclaration */ public function RangeVariableDeclaration() { $abstractSchemaName = $this->AbstractSchemaName(); - + if ($this->_lexer->isNextToken(Lexer::T_AS)) { $this->match(Lexer::T_AS); } @@ -1557,18 +1515,55 @@ class Parser 'relation' => null, 'map' => null, 'nestingLevel' => $this->_nestingLevel, - 'token' => $token, + 'token' => $token ); $this->_queryComponents[$aliasIdentificationVariable] = $queryComponent; return new AST\RangeVariableDeclaration($abstractSchemaName, $aliasIdentificationVariable); } + /** + * PartialObjectExpression ::= "PARTIAL" IdentificationVariable "." PartialFieldSet + * PartialFieldSet ::= "{" SimpleStateField {"," SimpleStateField}* "}" + * + * @return array + */ + public function PartialObjectExpression() + { + $this->match(Lexer::T_PARTIAL); + + $partialFieldSet = array(); + + $identificationVariable = $this->IdentificationVariable(); + $this->match(Lexer::T_DOT); + + $this->match(Lexer::T_OPEN_CURLY_BRACE); + $this->match(Lexer::T_IDENTIFIER); + $partialFieldSet[] = $this->_lexer->token['value']; + while ($this->_lexer->isNextToken(Lexer::T_COMMA)) { + $this->match(Lexer::T_COMMA); + $this->match(Lexer::T_IDENTIFIER); + $partialFieldSet[] = $this->_lexer->token['value']; + } + $this->match(Lexer::T_CLOSE_CURLY_BRACE); + + $partialObjectExpression = new AST\PartialObjectExpression($identificationVariable, $partialFieldSet); + + // Defer PartialObjectExpression validation + $this->_deferredPartialObjectExpressions[] = array( + 'expression' => $partialObjectExpression, + 'nestingLevel' => $this->_nestingLevel, + 'token' => $this->_lexer->token, + ); + + return $partialObjectExpression; + } + /** * Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN" JoinAssociationPathExpression - * ["AS"] AliasIdentificationVariable [("ON" | "WITH") ConditionalExpression] + * ["AS"] AliasIdentificationVariable ["WITH" ConditionalExpression] * - * @return \Doctrine\ORM\Query\AST\Join + * @return Doctrine\ORM\Query\AST\Join */ public function Join() { @@ -1590,7 +1585,7 @@ class Parser } $this->match(Lexer::T_JOIN); - + $joinPathExpression = $this->JoinAssociationPathExpression(); if ($this->_lexer->isNextToken(Lexer::T_AS)) { @@ -1619,7 +1614,7 @@ class Parser 'relation' => $parentClass->getAssociationMapping($assocField), 'map' => null, 'nestingLevel' => $this->_nestingLevel, - 'token' => $token, + 'token' => $token ); $this->_queryComponents[$aliasIdentificationVariable] = $joinQueryComponent; @@ -1627,14 +1622,8 @@ class Parser $join = new AST\Join($joinType, $joinPathExpression, $aliasIdentificationVariable); // Check for ad-hoc Join conditions - if ($this->_lexer->isNextToken(Lexer::T_ON) || $this->_lexer->isNextToken(Lexer::T_WITH)) { - if ($this->_lexer->isNextToken(Lexer::T_ON)) { - $this->match(Lexer::T_ON); - $join->whereType = AST\Join::JOIN_WHERE_ON; - } else { - $this->match(Lexer::T_WITH); - } - + if ($this->_lexer->isNextToken(Lexer::T_WITH)) { + $this->match(Lexer::T_WITH); $join->conditionalExpression = $this->ConditionalExpression(); } @@ -1644,7 +1633,7 @@ class Parser /** * IndexBy ::= "INDEX" "BY" SimpleStateFieldPathExpression * - * @return \Doctrine\ORM\Query\AST\IndexBy + * @return Doctrine\ORM\Query\AST\IndexBy */ public function IndexBy() { @@ -1659,13 +1648,60 @@ class Parser return $pathExp; } - + /** + * ScalarExpression ::= SimpleArithmeticExpression | StringPrimary | DateTimePrimary | + * StateFieldPathExpression | BooleanPrimary | CaseExpression | + * EntityTypeExpression + * + * @return mixed One of the possible expressions or subexpressions. + */ + public function ScalarExpression() + { + $lookahead = $this->_lexer->lookahead['type']; + if ($lookahead === Lexer::T_IDENTIFIER) { + $this->_lexer->peek(); // lookahead => '.' + $this->_lexer->peek(); // lookahead => token after '.' + $peek = $this->_lexer->peek(); // lookahead => token after the token after the '.' + $this->_lexer->resetPeek(); + + if ($peek['value'] == '+' || $peek['value'] == '-' || $peek['value'] == '/' || $peek['value'] == '*') { + return $this->SimpleArithmeticExpression(); + } + + return $this->StateFieldPathExpression(); + } else if ($lookahead == Lexer::T_INTEGER || $lookahead == Lexer::T_FLOAT) { + return $this->SimpleArithmeticExpression(); + } else if ($this->_isFunction()) { + return $this->FunctionDeclaration(); + } else if ($lookahead == Lexer::T_STRING) { + return $this->StringPrimary(); + } else if ($lookahead == Lexer::T_INPUT_PARAMETER) { + return $this->InputParameter(); + } else if ($lookahead == Lexer::T_TRUE || $lookahead == Lexer::T_FALSE) { + $this->match($lookahead); + return new AST\Literal(AST\Literal::BOOLEAN, $this->_lexer->token['value']); + } else if ($lookahead == Lexer::T_CASE || $lookahead == Lexer::T_COALESCE || $lookahead == Lexer::T_NULLIF) { + return $this->CaseExpression(); + } else { + $this->syntaxError(); + } + } + + public function CaseExpression() + { + // if "CASE" "WHEN" => GeneralCaseExpression + // else if "CASE" => SimpleCaseExpression + // else if "COALESCE" => CoalesceExpression + // else if "NULLIF" => NullifExpression + $this->semanticalError('CaseExpression not yet supported.'); + } + /** * SelectExpression ::= * IdentificationVariable | StateFieldPathExpression | - * (AggregateExpression | "(" Subselect ")" | FunctionDeclaration) [["AS"] AliasResultVariable] + * (AggregateExpression | "(" Subselect ")" | ScalarExpression) [["AS"] AliasResultVariable] * - * @return \Doctrine\ORM\Query\AST\SelectExpression + * @return Doctrine\ORM\Query\AST\SelectExpression */ public function SelectExpression() { @@ -1673,22 +1709,46 @@ class Parser $fieldAliasIdentificationVariable = null; $peek = $this->_lexer->glimpse(); - // First we recognize for an IdentificationVariable (DQL class alias) - if ($peek['value'] != '.' && $peek['value'] != '(' && $this->_lexer->lookahead['type'] === Lexer::T_IDENTIFIER) { - $expression = $this->IdentificationVariable(); - } else if (($isFunction = $this->_isFunction()) !== false || $this->_isSubselect()) { - if ($isFunction) { - if ($this->_isAggregateFunction($this->_lexer->lookahead['type'])) { - $expression = $this->AggregateExpression(); - } else { - $expression = $this->FunctionDeclaration(); - } + $supportsAlias = true; + if ($peek['value'] != '(' && $this->_lexer->lookahead['type'] === Lexer::T_IDENTIFIER) { + if ($peek['value'] == '.') { + // ScalarExpression + $expression = $this->ScalarExpression(); } else { + $supportsAlias = false; + $expression = $this->IdentificationVariable(); + } + } else if ($this->_lexer->lookahead['value'] == '(') { + if ($peek['type'] == Lexer::T_SELECT) { + // Subselect $this->match(Lexer::T_OPEN_PARENTHESIS); $expression = $this->Subselect(); $this->match(Lexer::T_CLOSE_PARENTHESIS); + } else { + // Shortcut: ScalarExpression => SimpleArithmeticExpression + $expression = $this->SimpleArithmeticExpression(); } - + } else if ($this->_isFunction()) { + if ($this->_isAggregateFunction($this->_lexer->lookahead['type'])) { + $expression = $this->AggregateExpression(); + } else { + // Shortcut: ScalarExpression => Function + $expression = $this->FunctionDeclaration(); + } + } else if ($this->_lexer->lookahead['type'] == Lexer::T_PARTIAL) { + $supportsAlias = false; + $expression = $this->PartialObjectExpression(); + } else if ($this->_lexer->lookahead['type'] == Lexer::T_INTEGER || + $this->_lexer->lookahead['type'] == Lexer::T_FLOAT) { + // Shortcut: ScalarExpression => SimpleArithmeticExpression + $expression = $this->SimpleArithmeticExpression(); + } else { + $this->syntaxError('IdentificationVariable | StateFieldPathExpression' + . ' | AggregateExpression | "(" Subselect ")" | ScalarExpression', + $this->_lexer->lookahead); + } + + if ($supportsAlias) { if ($this->_lexer->isNextToken(Lexer::T_AS)) { $this->match(Lexer::T_AS); } @@ -1704,16 +1764,6 @@ class Parser 'token' => $token, ); } - } else { - // Deny hydration of partial objects if doctrine.forcePartialLoad query hint not defined - if ( - $this->_query->getHydrationMode() == Query::HYDRATE_OBJECT && - ! $this->_query->getHint(Query::HINT_FORCE_PARTIAL_LOAD) - ) { - throw DoctrineException::partialObjectsAreDangerous(); - } - - $expression = $this->StateFieldPathExpression(); } return new AST\SelectExpression($expression, $fieldAliasIdentificationVariable); @@ -1761,7 +1811,6 @@ class Parser return $expr; } - /** * ConditionalExpression ::= ConditionalTerm {"OR" ConditionalTerm}* * @@ -1944,8 +1993,7 @@ class Parser return $this->ComparisonExpression(); } - - + /** * EmptyCollectionComparisonExpression ::= CollectionValuedPathExpression "IS" ["NOT"] "EMPTY" * @@ -1967,7 +2015,7 @@ class Parser return $emptyColletionCompExpr; } - + /** * CollectionMemberExpression ::= EntityExpression ["NOT"] "MEMBER" ["OF"] CollectionValuedPathExpression * @@ -2001,7 +2049,6 @@ class Parser return $collMemberExpr; } - /** * Literal ::= string | char | integer | float | boolean * @@ -2032,7 +2079,7 @@ class Parser $this->syntaxError('Literal'); } } - + /** * InParameter ::= Literal | InputParameter * @@ -2046,8 +2093,7 @@ class Parser return $this->Literal(); } - - + /** * InputParameter ::= PositionalParameter | NamedParameter * @@ -2059,8 +2105,7 @@ class Parser return new AST\InputParameter($this->_lexer->token['value']); } - - + /** * ArithmeticExpression ::= SimpleArithmeticExpression | "(" Subselect ")" * @@ -2103,7 +2148,7 @@ class Parser $terms[] = $this->_lexer->token['value']; $terms[] = $this->ArithmeticTerm(); } - + return new AST\SimpleArithmeticExpression($terms); } @@ -2191,7 +2236,7 @@ class Parser } } } - + /** * StringExpression ::= StringPrimary | "(" Subselect ")" * @@ -2259,7 +2304,7 @@ class Parser return $this->SimpleEntityExpression(); } - + /** * SimpleEntityExpression ::= IdentificationVariable | InputParameter * @@ -2274,7 +2319,6 @@ class Parser return $this->IdentificationVariable(); } - /** * AggregateExpression ::= * ("AVG" | "MAX" | "MIN" | "SUM") "(" ["DISTINCT"] StateFieldPathExpression ")" | @@ -2321,7 +2365,6 @@ class Parser return new AST\AggregateExpression($functionName, $pathExp, $isDistinct); } - /** * QuantifiedExpression ::= ("ALL" | "ANY" | "SOME") "(" Subselect ")" * @@ -2573,7 +2616,6 @@ class Parser } } - /** * FunctionDeclaration ::= FunctionsReturningStrings | FunctionsReturningNumerics | FunctionsReturningDatetime */ @@ -2623,7 +2665,7 @@ class Parser return $function; } - + /** * FunctionsReturningStrings ::= * "CONCAT" "(" StringPrimary "," StringPrimary ")" | diff --git a/lib/Doctrine/ORM/Query/QueryException.php b/lib/Doctrine/ORM/Query/QueryException.php index ead7acf6d..3584b9119 100644 --- a/lib/Doctrine/ORM/Query/QueryException.php +++ b/lib/Doctrine/ORM/Query/QueryException.php @@ -85,6 +85,15 @@ class QueryException extends \Doctrine\Common\DoctrineException "in class ".$assoc->sourceEntityName." assocation ".$assoc->sourceFieldName ); } + + public static function partialObjectsAreDangerous() + { + return new self( + "Loading partial objects is dangerous. Fetch full objects or consider " . + "using a different fetch mode. If you really want partial objects, " . + "set the doctrine.forcePartialLoad query hint to TRUE." + ); + } public static function overwritingJoinConditionsNotYetSupported($assoc) { @@ -94,4 +103,21 @@ class QueryException extends \Doctrine\Common\DoctrineException "Use WITH to append additional join conditions to the association." ); } + + public static function associationPathInverseSideNotSupported() + { + return new self( + "A single-valued association path expression to an inverse side is not supported". + " in DQL queries. Use an explicit join instead." + ); + } + + public static function associationPathCompositeKeyNotSupported() + { + return new self( + "A single-valued association path expression to an entity with a composite primary ". + "key is not supported. Explicitly name the components of the composite primary key ". + "in the query." + ); + } } \ No newline at end of file diff --git a/lib/Doctrine/ORM/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index 80edbb51c..86f6e4dc4 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -32,6 +32,7 @@ use Doctrine\ORM\Query, * @author Roman Borschel * @author Benjamin Eberlei * @since 2.0 + * @todo Rename: SQLWalker */ class SqlWalker implements TreeWalker { @@ -40,14 +41,10 @@ class SqlWalker implements TreeWalker */ private $_rsm; - /** Counter for generating unique column aliases. */ + /** Counters for generating unique column aliases, table aliases and parameter indexes. */ private $_aliasCounter = 0; - - /** Counter for generating unique table aliases. */ private $_tableAliasCounter = 0; private $_scalarResultCounter = 1; - - /** Counter for SQL parameter positions. */ private $_sqlParamIndex = 1; /** @@ -70,7 +67,7 @@ class SqlWalker implements TreeWalker */ private $_query; - private $_dqlToSqlAliasMap = array(); + private $_tableAliasMap = array(); /** Map from result variable names to their SQL column alias names. */ private $_scalarResultAliasMap = array(); @@ -89,7 +86,7 @@ class SqlWalker implements TreeWalker /** * Flag that indicates whether to generate SQL table aliases in the SQL. - * These should only be generated for SELECT queries. + * These should only be generated for SELECT queries, not for UPDATE/DELETE. */ private $_useSqlTableAliases = true; @@ -101,17 +98,17 @@ class SqlWalker implements TreeWalker private $_platform; /** - * @inheritdoc + * {@inheritDoc} */ public function __construct($query, $parserResult, array $queryComponents) { - $this->_rsm = $parserResult->getResultSetMapping(); $this->_query = $query; + $this->_parserResult = $parserResult; + $this->_queryComponents = $queryComponents; + $this->_rsm = $parserResult->getResultSetMapping(); $this->_em = $query->getEntityManager(); $this->_conn = $this->_em->getConnection(); $this->_platform = $this->_conn->getDatabasePlatform(); - $this->_parserResult = $parserResult; - $this->_queryComponents = $queryComponents; } /** @@ -200,11 +197,11 @@ class SqlWalker implements TreeWalker { $tableName .= $dqlAlias; - if ( ! isset($this->_dqlToSqlAliasMap[$tableName])) { - $this->_dqlToSqlAliasMap[$tableName] = strtolower(substr($tableName, 0, 1)) . $this->_tableAliasCounter++ . '_'; + if ( ! isset($this->_tableAliasMap[$tableName])) { + $this->_tableAliasMap[$tableName] = strtolower(substr($tableName, 0, 1)) . $this->_tableAliasCounter++ . '_'; } - return $this->_dqlToSqlAliasMap[$tableName]; + return $this->_tableAliasMap[$tableName]; } /** @@ -216,7 +213,7 @@ class SqlWalker implements TreeWalker */ public function setSqlTableAlias($tableName, $alias) { - $this->_dqlToSqlAliasMap[$tableName] = $alias; + $this->_tableAliasMap[$tableName] = $alias; return $alias; } @@ -270,16 +267,16 @@ class SqlWalker implements TreeWalker $subClass = $this->_em->getClassMetadata($subClassName); $tableAlias = $this->getSqlTableAlias($subClass->primaryTable['name'], $dqlAlias); $sql .= ' LEFT JOIN ' . $subClass->getQuotedTableName($this->_platform) - . ' ' . $tableAlias . ' ON '; - $first = true; + . ' ' . $tableAlias . ' ON '; + $first = true; foreach ($class->identifier as $idField) { if ($first) $first = false; else $sql .= ' AND '; $columnName = $class->getQuotedColumnName($idField, $this->_platform); $sql .= $baseTableAlias . '.' . $columnName - . ' = ' - . $tableAlias . '.' . $columnName; + . ' = ' + . $tableAlias . '.' . $columnName; } } } @@ -339,9 +336,8 @@ class SqlWalker implements TreeWalker $sql .= $AST->havingClause ? $this->walkHavingClause($AST->havingClause) : ''; $sql .= $AST->orderByClause ? $this->walkOrderByClause($AST->orderByClause) : ''; - $q = $this->getQuery(); $sql = $this->_platform->modifyLimitQuery( - $sql, $q->getMaxResults(), $q->getFirstResult() + $sql, $this->_query->getMaxResults(), $this->_query->getFirstResult() ); return $sql; @@ -407,7 +403,6 @@ class SqlWalker implements TreeWalker return $this->getSqlTableAlias($class->primaryTable['name'], $identificationVariable); } - /** * Walks down a PathExpression AST node, thereby generating the appropriate SQL. @@ -418,15 +413,13 @@ class SqlWalker implements TreeWalker public function walkPathExpression($pathExpr) { $sql = ''; - $pathExprType = $pathExpr->type; - + switch ($pathExpr->type) { case AST\PathExpression::TYPE_STATE_FIELD: $parts = $pathExpr->parts; $fieldName = array_pop($parts); - $dqlAlias = $pathExpr->identificationVariable . (( ! empty($parts)) ? '.' . implode('.', $parts) : ''); - $qComp = $this->_queryComponents[$dqlAlias]; - $class = $qComp['metadata']; + $dqlAlias = $pathExpr->identificationVariable . ( ! empty($parts) ? '.' . implode('.', $parts) : ''); + $class = $this->_queryComponents[$dqlAlias]['metadata']; if ($this->_useSqlTableAliases) { $sql .= $this->walkIdentificationVariable($dqlAlias, $fieldName) . '.'; @@ -434,33 +427,27 @@ class SqlWalker implements TreeWalker $sql .= $class->getQuotedColumnName($fieldName, $this->_platform); break; - case AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION: - // "u.Group" should be converted to: - // 1- IdentificationVariable is the owning side: - // Just append the condition: u.group_id = ? - /*$parts = $pathExpr->parts; - $numParts = count($parts); + // 1- the owning side: + // Just use the foreign key, i.e. u.group_id + $parts = $pathExpr->parts; + $fieldName = array_pop($parts); $dqlAlias = $pathExpr->identificationVariable; - $fieldName = $parts[$numParts - 1]; - $qComp = $this->_queryComponents[$dqlAlias]; - $class = $qComp['metadata']; + $class = $this->_queryComponents[$dqlAlias]['metadata']; $assoc = $class->associationMappings[$fieldName]; if ($assoc->isOwningSide) { - foreach ($assoc->) - $sql .= $this->walkIdentificationVariable($dqlAlias, $fieldName) . '.'; - + // COMPOSITE KEYS NOT (YET?) SUPPORTED + if (count($assoc->sourceToTargetKeyColumns) > 1) { + throw QueryException::associationPathCompositeKeyNotSupported(); + } + $sql .= $this->walkIdentificationVariable($dqlAlias) . '.' + . $assoc->getQuotedJoinColumnName(key($assoc->sourceToTargetKeyColumns), $this->_platform); + } else { + // 2- Inverse side: NOT (YET?) SUPPORTED + throw QueryException::associationPathInverseSideNotSupported(); } - - // 2- IdentificationVariable is the inverse side: - // Join required: INNER JOIN u.Group g - // Append condition: g.id = ? - break;*/ - - case AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION: - throw DoctrineException::notImplemented(); - + break; default: throw QueryException::invalidPathExpression($pathExpr); } @@ -468,7 +455,6 @@ class SqlWalker implements TreeWalker return $sql; } - /** * Walks down a SelectClause AST node, thereby generating the appropriate SQL. * @@ -639,10 +625,8 @@ class SqlWalker implements TreeWalker */ public function walkHavingClause($havingClause) { - $condExpr = $havingClause->conditionalExpression; - return ' HAVING ' . implode( - ' OR ', array_map(array($this, 'walkConditionalTerm'), $condExpr->conditionalTerms) + ' OR ', array_map(array($this, 'walkConditionalTerm'), $havingClause->conditionalExpression->conditionalTerms) ); } @@ -750,15 +734,11 @@ class SqlWalker implements TreeWalker } } - // Handle ON / WITH clause + // Handle WITH clause if ($join->conditionalExpression !== null) { - if ($join->whereType == AST\Join::JOIN_WHERE_ON) { - throw QueryException::overwritingJoinConditionsNotYetSupported($assoc); - } else { - $sql .= ' AND (' . implode(' OR ', - array_map(array($this, 'walkConditionalTerm'), $join->conditionalExpression->conditionalTerms) - ). ')'; - } + $sql .= ' AND (' . implode(' OR ', + array_map(array($this, 'walkConditionalTerm'), $join->conditionalExpression->conditionalTerms) + ). ')'; } $discrSql = $this->_generateDiscriminatorColumnConditionSql($joinedDqlAlias); @@ -792,22 +772,25 @@ class SqlWalker implements TreeWalker $dqlAlias = $expr->identificationVariable . (( ! empty($parts)) ? '.' . implode('.', $parts) : ''); $qComp = $this->_queryComponents[$dqlAlias]; $class = $qComp['metadata']; - - if ( ! isset($this->_selectedClasses[$dqlAlias])) { - $this->_selectedClasses[$dqlAlias] = $class; + + if ( ! $selectExpression->fieldIdentificationVariable) { + $resultAlias = $fieldName; + } else { + $resultAlias = $selectExpression->fieldIdentificationVariable; } $sqlTableAlias = $this->getSqlTableAlias($class->getTableName(), $dqlAlias); $columnName = $class->getQuotedColumnName($fieldName, $this->_platform); - $columnAlias = $this->getSqlColumnAlias($class->columnNames[$fieldName]); - $sql .= $sqlTableAlias . '.' . $columnName . ' AS ' . $columnAlias; + $columnAlias = $this->getSqlColumnAlias($columnName); + $sql .= $sqlTableAlias . '.' . $columnName . ' AS ' . $columnAlias; $columnAlias = $this->_platform->getSqlResultCasing($columnAlias); - $this->_rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $class->name); + $this->_rsm->addScalarResult($columnAlias, $resultAlias); } else { - throw DoctrineException::invalidPathExpression($expr->type); + throw QueryException::invalidPathExpression($expr->type); } - } else if ($expr instanceof AST\AggregateExpression) { + } + else if ($expr instanceof AST\AggregateExpression) { if ( ! $selectExpression->fieldIdentificationVariable) { $resultAlias = $this->_scalarResultCounter++; } else { @@ -820,9 +803,11 @@ class SqlWalker implements TreeWalker $columnAlias = $this->_platform->getSqlResultCasing($columnAlias); $this->_rsm->addScalarResult($columnAlias, $resultAlias); - } else if ($expr instanceof AST\Subselect) { + } + else if ($expr instanceof AST\Subselect) { $sql .= $this->walkSubselect($expr); - } else if ($expr instanceof AST\Functions\FunctionNode) { + } + else if ($expr instanceof AST\Functions\FunctionNode) { if ( ! $selectExpression->fieldIdentificationVariable) { $resultAlias = $this->_scalarResultCounter++; } else { @@ -833,11 +818,32 @@ class SqlWalker implements TreeWalker $sql .= $this->walkFunction($expr) . ' AS ' . $columnAlias; $this->_scalarResultAliasMap[$resultAlias] = $columnAlias; + $columnAlias = $this->_platform->getSqlResultCasing($columnAlias); + $this->_rsm->addScalarResult($columnAlias, $resultAlias); + } + else if ($expr instanceof AST\SimpleArithmeticExpression) { + if ( ! $selectExpression->fieldIdentificationVariable) { + $resultAlias = $this->_scalarResultCounter++; + } else { + $resultAlias = $selectExpression->fieldIdentificationVariable; + } + + $columnAlias = 'sclr' . $this->_aliasCounter++; + $sql .= $this->walkSimpleArithmeticExpression($expr) . ' AS ' . $columnAlias; + $this->_scalarResultAliasMap[$resultAlias] = $columnAlias; + $columnAlias = $this->_platform->getSqlResultCasing($columnAlias); $this->_rsm->addScalarResult($columnAlias, $resultAlias); } else { - // $expr == IdentificationVariable - $dqlAlias = $expr; + // IdentificationVariable or PartialObjectExpression + if ($expr instanceof AST\PartialObjectExpression) { + $dqlAlias = $expr->identificationVariable; + $partialFieldSet = $expr->partialFieldSet; + } else { + $dqlAlias = $expr; + $partialFieldSet = array(); + } + $queryComp = $this->_queryComponents[$dqlAlias]; $class = $queryComp['metadata']; @@ -848,6 +854,10 @@ class SqlWalker implements TreeWalker $beginning = true; // Select all fields from the queried class foreach ($class->fieldMappings as $fieldName => $mapping) { + if ($partialFieldSet && !in_array($fieldName, $partialFieldSet)) { + continue; + } + if (isset($mapping['inherited'])) { $tableName = $this->_em->getClassMetadata($mapping['inherited'])->primaryTable['name']; } else { @@ -874,7 +884,7 @@ class SqlWalker implements TreeWalker $subClass = $this->_em->getClassMetadata($subClassName); $sqlTableAlias = $this->getSqlTableAlias($subClass->primaryTable['name'], $dqlAlias); foreach ($subClass->fieldMappings as $fieldName => $mapping) { - if (isset($mapping['inherited'])) { + if (isset($mapping['inherited']) || $partialFieldSet && !in_array($fieldName, $partialFieldSet)) { continue; } diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index d008d0e7e..0e68b419b 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -317,7 +317,7 @@ class UnitOfWork implements PropertyChangedListener } catch (\Exception $e) { $conn->setRollbackOnly(); $conn->rollback(); - $this->clear(); + $this->_em->close(); throw $e; } diff --git a/tests/Doctrine/Tests/Models/CMS/CmsUser.php b/tests/Doctrine/Tests/Models/CMS/CmsUser.php index 9c21c7836..148fa6674 100644 --- a/tests/Doctrine/Tests/Models/CMS/CmsUser.php +++ b/tests/Doctrine/Tests/Models/CMS/CmsUser.php @@ -116,5 +116,4 @@ class CmsUser $address->setUser($this); } } - } diff --git a/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php b/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php index 5e49f86c1..6a0c6d937 100644 --- a/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php @@ -286,6 +286,7 @@ class BasicFunctionalTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertEquals('gblanco', $users[0]->username); $this->assertEquals('developer', $users[0]->status); $this->assertTrue($users[0]->phonenumbers instanceof \Doctrine\ORM\PersistentCollection); + $this->assertTrue($users[0]->phonenumbers->isInitialized()); $this->assertEquals(0, $users[0]->phonenumbers->count()); //$this->assertNull($users[0]->articles); } @@ -332,7 +333,7 @@ class BasicFunctionalTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->_em->flush(); $this->_em->clear(); - $query = $this->_em->createQuery("select u, g from Doctrine\Tests\Models\CMS\CmsUser u inner join u.groups g"); + $query = $this->_em->createQuery("select u, g from Doctrine\Tests\Models\CMS\CmsUser u join u.groups g"); $this->assertEquals(0, count($query->getResult())); } diff --git a/tests/Doctrine/Tests/ORM/Functional/DetachedEntityTest.php b/tests/Doctrine/Tests/ORM/Functional/DetachedEntityTest.php index 6a3db8936..22f1d2072 100644 --- a/tests/Doctrine/Tests/ORM/Functional/DetachedEntityTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/DetachedEntityTest.php @@ -4,6 +4,7 @@ namespace Doctrine\Tests\ORM\Functional; use Doctrine\Tests\Models\CMS\CmsUser; use Doctrine\Tests\Models\CMS\CmsPhonenumber; +use Doctrine\Tests\Models\CMS\CmsAddress; use Doctrine\ORM\UnitOfWork; require_once __DIR__ . '/../../TestInit.php'; @@ -83,7 +84,6 @@ class DetachedEntityTest extends \Doctrine\Tests\OrmFunctionalTestCase $phonenumbers = $user->getPhonenumbers(); $this->assertTrue($this->_em->contains($phonenumbers[0])); $this->assertTrue($this->_em->contains($phonenumbers[1])); - } - + } } diff --git a/tests/Doctrine/Tests/ORM/Functional/QueryDqlFunctionTest.php b/tests/Doctrine/Tests/ORM/Functional/QueryDqlFunctionTest.php index d6ea041d0..09ddaf0a3 100644 --- a/tests/Doctrine/Tests/ORM/Functional/QueryDqlFunctionTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/QueryDqlFunctionTest.php @@ -189,8 +189,7 @@ class QueryDqlFunctionTest extends \Doctrine\Tests\OrmFunctionalTestCase " TRIM(LEADING '.' FROM m.name) AS str2, TRIM(CONCAT(' ', CONCAT(m.name, ' '))) AS str3 ". "FROM Doctrine\Tests\Models\Company\CompanyManager m"; - $result = $this->_em->createQuery($dql) - ->getArrayResult(); + $result = $this->_em->createQuery($dql)->getArrayResult(); $this->assertEquals(4, count($result)); $this->assertEquals('Roman B', $result[0]['str1']); @@ -207,34 +206,34 @@ class QueryDqlFunctionTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertEquals('Jonathan W.', $result[3]['str3']); } - /*public function testOperatorAdd() + public function testOperatorAdd() { $result = $this->_em->createQuery('SELECT m, m.salary+2500 AS add FROM Doctrine\Tests\Models\Company\CompanyManager m') - ->getResult(); - + ->getResult(); + $this->assertEquals(4, count($result)); - $this->assertEquals(102500, $result[0]['op']); - $this->assertEquals(202500, $result[1]['op']); - $this->assertEquals(402500, $result[2]['op']); - $this->assertEquals(802500, $result[3]['op']); + $this->assertEquals(102500, $result[0]['add']); + $this->assertEquals(202500, $result[1]['add']); + $this->assertEquals(402500, $result[2]['add']); + $this->assertEquals(802500, $result[3]['add']); } public function testOperatorSub() { - $result = $this->_em->createQuery('SELECT m, m.salary-2500 AS add FROM Doctrine\Tests\Models\Company\CompanyManager m') - ->getResult(); + $result = $this->_em->createQuery('SELECT m, m.salary-2500 AS sub FROM Doctrine\Tests\Models\Company\CompanyManager m') + ->getResult(); $this->assertEquals(4, count($result)); - $this->assertEquals(102500, $result[0]['op']); - $this->assertEquals(202500, $result[1]['op']); - $this->assertEquals(402500, $result[2]['op']); - $this->assertEquals(802500, $result[3]['op']); + $this->assertEquals(97500, $result[0]['sub']); + $this->assertEquals(197500, $result[1]['sub']); + $this->assertEquals(397500, $result[2]['sub']); + $this->assertEquals(797500, $result[3]['sub']); } public function testOperatorMultiply() { $result = $this->_em->createQuery('SELECT m, m.salary*2 AS op FROM Doctrine\Tests\Models\Company\CompanyManager m') - ->getResult(); + ->getResult(); $this->assertEquals(4, count($result)); $this->assertEquals(200000, $result[0]['op']); @@ -243,17 +242,33 @@ class QueryDqlFunctionTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertEquals(1600000, $result[3]['op']); } + /** + * @group test + */ public function testOperatorDiv() { $result = $this->_em->createQuery('SELECT m, (m.salary/0.5) AS op FROM Doctrine\Tests\Models\Company\CompanyManager m') - ->getResult(); + ->getResult(); $this->assertEquals(4, count($result)); $this->assertEquals(200000, $result[0]['op']); $this->assertEquals(400000, $result[1]['op']); $this->assertEquals(800000, $result[2]['op']); $this->assertEquals(1600000, $result[3]['op']); - }*/ + } + + public function testConcatFunction() + { + $arg = $this->_em->createQuery('SELECT CONCAT(m.name, m.department) AS namedep FROM Doctrine\Tests\Models\Company\CompanyManager m order by namedep desc') + ->getArrayResult(); + + $this->assertEquals(4, count($arg)); + $this->assertEquals('Roman B.IT', $arg[0]['namedep']); + $this->assertEquals('Jonathan W.Administration', $arg[1]['namedep']); + $this->assertEquals('Guilherme B.Complaint Department', $arg[2]['namedep']); + $this->assertEquals('Benjamin E.HR', $arg[3]['namedep']); + } + protected function generateFixture() { diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC163Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC163Test.php index e18346c20..3126d5491 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC163Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC163Test.php @@ -45,7 +45,7 @@ class DDC163Test extends \Doctrine\Tests\OrmFunctionalTestCase $this->_em->flush(); $this->_em->clear(); - $dql = 'SELECT person.name, spouse.name, friend.name + $dql = 'SELECT PARTIAL person.{id,name}, PARTIAL spouse.{id,name}, PARTIAL friend.{id,name} FROM Doctrine\Tests\Models\Company\CompanyPerson person LEFT JOIN person.spouse spouse LEFT JOIN person.friends friend diff --git a/tests/Doctrine/Tests/ORM/Hydration/ArrayHydratorTest.php b/tests/Doctrine/Tests/ORM/Hydration/ArrayHydratorTest.php index 1fe79610c..d81a02ced 100644 --- a/tests/Doctrine/Tests/ORM/Hydration/ArrayHydratorTest.php +++ b/tests/Doctrine/Tests/ORM/Hydration/ArrayHydratorTest.php @@ -626,6 +626,76 @@ class ArrayHydratorTest extends HydrationTestCase $this->assertTrue(isset($result[1]['boards'])); $this->assertEquals(1, count($result[1]['boards'])); } + + /** + * DQL: select partial u.{id,status}, a.id, a.topic, c.id as cid, c.topic as ctopic from CmsUser u left join u.articles a left join a.comments c + * + */ + /*public function testChainedJoinWithScalars() + { + $rsm = new ResultSetMapping; + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addFieldResult('u', 'u__id', 'id'); + $rsm->addFieldResult('u', 'u__status', 'status'); + $rsm->addScalarResult('a__id', 'id'); + $rsm->addScalarResult('a__topic', 'topic'); + $rsm->addScalarResult('c__id', 'cid'); + $rsm->addScalarResult('c__topic', 'ctopic'); + + // Faked result set + $resultSet = array( + //row1 + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'a__id' => '1', + 'a__topic' => 'The First', + 'c__id' => '1', + 'c__topic' => 'First Comment' + ), + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'a__id' => '1', + 'a__topic' => 'The First', + 'c__id' => '2', + 'c__topic' => 'Second Comment' + ), + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'a__id' => '42', + 'a__topic' => 'The Answer', + 'c__id' => null, + 'c__topic' => null + ), + ); + + $stmt = new HydratorMockStatement($resultSet); + $hydrator = new \Doctrine\ORM\Internal\Hydration\ArrayHydrator($this->_em); + + $result = $hydrator->hydrateAll($stmt, $rsm); + + $this->assertEquals(3, count($result)); + + $this->assertEquals(2, count($result[0][0])); // User array + $this->assertEquals(1, $result[0]['id']); + $this->assertEquals('The First', $result[0]['topic']); + $this->assertEquals(1, $result[0]['cid']); + $this->assertEquals('First Comment', $result[0]['ctopic']); + + $this->assertEquals(2, count($result[1][0])); // User array, duplicated + $this->assertEquals(1, $result[1]['id']); // duplicated + $this->assertEquals('The First', $result[1]['topic']); // duplicated + $this->assertEquals(2, $result[1]['cid']); + $this->assertEquals('Second Comment', $result[1]['ctopic']); + + $this->assertEquals(2, count($result[2][0])); // User array, duplicated + $this->assertEquals(42, $result[2]['id']); + $this->assertEquals('The Answer', $result[2]['topic']); + $this->assertNull($result[2]['cid']); + $this->assertNull($result[2]['ctopic']); + }*/ public function testResultIteration() { diff --git a/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php b/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php index 8cfb8d14f..f0b6ee50e 100644 --- a/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php +++ b/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php @@ -743,6 +743,81 @@ class ObjectHydratorTest extends HydrationTestCase $this->assertEquals(0, $result[0]->articles->count()); $this->assertEquals(0, $result[1]->articles->count()); } + + /** + * DQL: select partial u.{id,status}, a.id, a.topic, c.id as cid, c.topic as ctopic from CmsUser u left join u.articles a left join a.comments c + * + * @group bubu + */ + /*public function testChainedJoinWithScalars() + { + $rsm = new ResultSetMapping; + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addFieldResult('u', 'u__id', 'id'); + $rsm->addFieldResult('u', 'u__status', 'status'); + $rsm->addScalarResult('a__id', 'id'); + $rsm->addScalarResult('a__topic', 'topic'); + $rsm->addScalarResult('c__id', 'cid'); + $rsm->addScalarResult('c__topic', 'ctopic'); + + // Faked result set + $resultSet = array( + //row1 + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'a__id' => '1', + 'a__topic' => 'The First', + 'c__id' => '1', + 'c__topic' => 'First Comment' + ), + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'a__id' => '1', + 'a__topic' => 'The First', + 'c__id' => '2', + 'c__topic' => 'Second Comment' + ), + array( + 'u__id' => '1', + 'u__status' => 'developer', + 'a__id' => '42', + 'a__topic' => 'The Answer', + 'c__id' => null, + 'c__topic' => null + ), + ); + + $stmt = new HydratorMockStatement($resultSet); + $hydrator = new \Doctrine\ORM\Internal\Hydration\ObjectHydrator($this->_em); + + $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); + + $this->assertEquals(3, count($result)); + + $this->assertTrue($result[0][0] instanceof CmsUser); // User object + $this->assertEquals(1, $result[0]['id']); + $this->assertEquals('The First', $result[0]['topic']); + $this->assertEquals(1, $result[0]['cid']); + $this->assertEquals('First Comment', $result[0]['ctopic']); + + $this->assertTrue($result[1][0] instanceof CmsUser); // Same User object + $this->assertEquals(1, $result[1]['id']); // duplicated + $this->assertEquals('The First', $result[1]['topic']); // duplicated + $this->assertEquals(2, $result[1]['cid']); + $this->assertEquals('Second Comment', $result[1]['ctopic']); + + $this->assertTrue($result[2][0] instanceof CmsUser); // Same User object + $this->assertEquals(42, $result[2]['id']); + $this->assertEquals('The Answer', $result[2]['topic']); + $this->assertNull($result[2]['cid']); + $this->assertNull($result[2]['ctopic']); + + $this->assertTrue($result[0][0] === $result[1][0]); + $this->assertTrue($result[1][0] === $result[2][0]); + $this->assertTrue($result[0][0] === $result[2][0]); + }*/ public function testResultIteration() { diff --git a/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php b/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php index 1204e1f02..59a1ca65f 100644 --- a/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php +++ b/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php @@ -215,9 +215,9 @@ class LanguageRecognitionTest extends \Doctrine\Tests\OrmTestCase $this->assertValidDql('SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id = :id'); } - public function testJoinConditionsSupported() + public function testJoinConditionOverrideNotSupported() { - $this->assertValidDql("SELECT u.name, p FROM Doctrine\Tests\Models\CMS\CmsUser u LEFT JOIN u.phonenumbers p ON p.phonenumber = '123 123'"); + $this->assertInvalidDql("SELECT u.name, p FROM Doctrine\Tests\Models\CMS\CmsUser u LEFT JOIN u.phonenumbers p ON p.phonenumber = '123 123'"); } public function testIndexByClauseWithOneComponent() @@ -270,12 +270,12 @@ class LanguageRecognitionTest extends \Doctrine\Tests\OrmTestCase $this->assertInvalidDql("SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u INNER JOIN u.articles u WHERE u.id = 1"); } - /*public function testImplicitJoinInWhereOnSingleValuedAssociationPathExpression() + public function testImplicitJoinInWhereOnSingleValuedAssociationPathExpression() { // This should be allowed because avatar is a single-value association. // SQL: SELECT ... FROM forum_user fu INNER JOIN forum_avatar fa ON fu.avatar_id = fa.id WHERE fa.id = ? - $this->assertValidDql("SELECT u FROM Doctrine\Tests\Models\Forum\ForumUser u WHERE u.avatar.id = ?"); - }*/ + $this->assertValidDql("SELECT u FROM Doctrine\Tests\Models\Forum\ForumUser u WHERE u.avatar.id = ?1"); + } public function testImplicitJoinInWhereOnCollectionValuedPathExpression() { @@ -319,10 +319,6 @@ class LanguageRecognitionTest extends \Doctrine\Tests\OrmTestCase } /** - * TODO: Hydration can't deal with this currently but it should be allowed. - * Also, generated SQL looks like: "... FROM cms_user, cms_article ..." which - * may not work on all dbms. - * * The main use case for this generalized style of join is when a join condition * does not involve a foreign key relationship that is mapped to an entity relationship. */ @@ -392,15 +388,30 @@ class LanguageRecognitionTest extends \Doctrine\Tests\OrmTestCase $this->assertInvalidDql('SELECT u FROM UnknownClassName u'); } - /** - * This checks for invalid attempt to hydrate a proxy. It should throw an exception - * - * @expectedException \Doctrine\Common\DoctrineException - */ - public function testPartialObjectLoad() + public function testCorrectPartialObjectLoad() { - $this->parseDql('SELECT u.name FROM Doctrine\Tests\Models\CMS\CmsUser u', array( - \Doctrine\ORM\Query::HINT_FORCE_PARTIAL_LOAD => false - )); + $this->assertValidDql('SELECT PARTIAL u.{id,name} FROM Doctrine\Tests\Models\CMS\CmsUser u'); } + + public function testIncorrectPartialObjectLoadBecauseOfMissingIdentifier() + { + $this->assertInvalidDql('SELECT PARTIAL u.{name} FROM Doctrine\Tests\Models\CMS\CmsUser u'); + } + + public function testScalarExpressionInSelect() + { + $this->assertValidDql('SELECT u, 42 + u.id AS someNumber FROM Doctrine\Tests\Models\CMS\CmsUser u'); + } + + public function testInputParameterInSelect() + { + $this->assertValidDql('SELECT u, u.id + ?1 AS someNumber FROM Doctrine\Tests\Models\CMS\CmsUser u'); + } + + /* The exception is currently thrown in the SQLWalker, not earlier. + public function testInverseSideSingleValuedAssociationPathNotAllowed() + { + $this->assertInvalidDql('SELECT u.id FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.address = ?1'); + } + */ } \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php index d56e985d9..e69e7bffa 100644 --- a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php +++ b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php @@ -23,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 (DoctrineException $e) { + } catch (\Exception $e) { $this->fail($e->getMessage()); } } @@ -534,4 +534,24 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase "SELECT c0_.id AS id0, c0_.status AS status1, c0_.username AS username2, c0_.name AS name3 FROM cms_users c0_ WHERE EXISTS (SELECT 1 FROM cms_addresses c1_ WHERE c1_.user_id = c0_.id AND c1_.id = ?)" );*/ } + + + public function testSingleValuedAssociationNullCheckOnOwningSide() + { + $this->assertSqlGeneration( + "SELECT a FROM Doctrine\Tests\Models\CMS\CmsAddress a WHERE a.user IS NULL", + "SELECT c0_.id AS id0, c0_.country AS country1, c0_.zip AS zip2, c0_.city AS city3 FROM cms_addresses c0_ WHERE c0_.user_id IS NULL" + ); + } + + // Null check on inverse side has to happen through explicit JOIN. + // "SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.address IS NULL" + // where the CmsUser is the inverse side is not supported. + public function testSingleValuedAssociationNullCheckOnInverseSide() + { + $this->assertSqlGeneration( + "SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u LEFT JOIN u.address a WHERE a.id IS NULL", + "SELECT c0_.id AS id0, c0_.status AS status1, c0_.username AS username2, c0_.name AS name3 FROM cms_users c0_ LEFT JOIN cms_addresses c1_ ON c0_.id = c1_.user_id WHERE c1_.id IS NULL" + ); + } }