From c1fec32f58d6450c6711ed58ce74ee3f02471e4e Mon Sep 17 00:00:00 2001 From: Guilherme Blanco Date: Fri, 30 Jul 2010 01:30:02 -0300 Subject: [PATCH] [2.0][DDC-482] Added support to INSTANCE OF in DQL. --- .../ORM/Mapping/ClassMetadataFactory.php | 25 +++++++- .../ORM/Query/AST/InstanceOfExpression.php | 49 +++++++++++++++ lib/Doctrine/ORM/Query/Lexer.php | 55 ++++++++-------- lib/Doctrine/ORM/Query/Parser.php | 41 +++++++++++- lib/Doctrine/ORM/Query/QueryException.php | 5 ++ lib/Doctrine/ORM/Query/SqlWalker.php | 63 ++++++++++++++++++- lib/Doctrine/ORM/Query/TreeWalker.php | 8 +++ lib/Doctrine/ORM/Query/TreeWalkerAdapter.php | 8 +++ lib/Doctrine/ORM/Query/TreeWalkerChain.php | 13 ++++ .../ORM/Query/LanguageRecognitionTest.php | 15 +++++ .../ORM/Query/SelectSqlGenerationTest.php | 41 +++++++++++- 11 files changed, 290 insertions(+), 33 deletions(-) create mode 100644 lib/Doctrine/ORM/Query/AST/InstanceOfExpression.php diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php index d4790f62b..107f3cdcf 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php @@ -240,13 +240,13 @@ class ClassMetadataFactory if ($parent) { $class->setInheritanceType($parent->inheritanceType); - $class->setDiscriminatorColumn($parent->discriminatorColumn); $class->setIdGeneratorType($parent->generatorType); $this->_addInheritedFields($class, $parent); $this->_addInheritedRelations($class, $parent); $class->setIdentifier($parent->identifier); $class->setVersioned($parent->isVersioned); $class->setVersionField($parent->versionField); + $this->_addInheritedDiscriminatorColumn($class, $parent); $class->setDiscriminatorMap($parent->discriminatorMap); $class->setLifecycleCallbacks($parent->lifecycleCallbacks); } @@ -313,6 +313,29 @@ class ClassMetadataFactory { return new ClassMetadata($className); } + + /** + * Adds inherited discriminator column to the subclass mapping. + * + * @param Doctrine\ORM\Mapping\ClassMetadata $subClass + * @param Doctrine\ORM\Mapping\ClassMetadata $parentClass + */ + private function _addInheritedDiscriminatorColumn(ClassMetadata $subClass, ClassMetadata $parentClass) + { + if ($parentClass->discriminatorColumn) { + $columnDef = $parentClass->discriminatorColumn; + + if ( ! isset($columnDef['inherited'])) { + $columnDef['inherited'] = $parentClass->name; + } + + if ( ! isset($columnDef['declared'])) { + $columnDef['declared'] = $parentClass->name; + } + + $subClass->setDiscriminatorColumn($columnDef); + } + } /** * Adds inherited fields to the subclass mapping. diff --git a/lib/Doctrine/ORM/Query/AST/InstanceOfExpression.php b/lib/Doctrine/ORM/Query/AST/InstanceOfExpression.php new file mode 100644 index 000000000..3aefd61d9 --- /dev/null +++ b/lib/Doctrine/ORM/Query/AST/InstanceOfExpression.php @@ -0,0 +1,49 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * InstanceOfExpression ::= IdentificationVariable ["NOT"] "INSTANCE" ["OF"] (AbstractSchemaName | InputParameter) + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class InstanceOfExpression extends Node +{ + public $not; + public $identificationVariable; + public $value; + + public function __construct($identVariable) + { + $this->identificationVariable = $identVariable; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkInstanceOfExpression($this); + } +} + diff --git a/lib/Doctrine/ORM/Query/Lexer.php b/lib/Doctrine/ORM/Query/Lexer.php index ec8067dc0..480756f01 100644 --- a/lib/Doctrine/ORM/Query/Lexer.php +++ b/lib/Doctrine/ORM/Query/Lexer.php @@ -75,33 +75,34 @@ class Lexer extends \Doctrine\Common\Lexer const T_IN = 121; const T_INDEX = 122; const T_INNER = 123; - const T_IS = 124; - const T_JOIN = 125; - const T_LEADING = 126; - const T_LEFT = 127; - const T_LIKE = 128; - const T_MAX = 129; - const T_MEMBER = 130; - const T_MIN = 131; - const T_NOT = 132; - const T_NULL = 133; - const T_OF = 134; - const T_OR = 135; - const T_ORDER = 136; - const T_OUTER = 137; - const T_SELECT = 138; - const T_SET = 139; - const T_SIZE = 140; - const T_SOME = 141; - const T_SUM = 142; - const T_TRAILING = 143; - const T_TRUE = 144; - const T_UPDATE = 145; - const T_WHERE = 146; - const T_WITH = 147; - const T_PARTIAL = 148; - const T_MOD = 149; - + const T_INSTANCE = 124; + const T_IS = 125; + const T_JOIN = 126; + const T_LEADING = 127; + const T_LEFT = 128; + const T_LIKE = 129; + const T_MAX = 130; + const T_MEMBER = 131; + const T_MIN = 132; + const T_NOT = 133; + const T_NULL = 134; + const T_OF = 135; + const T_OR = 136; + const T_ORDER = 137; + const T_OUTER = 138; + const T_SELECT = 139; + const T_SET = 140; + const T_SIZE = 141; + const T_SOME = 142; + const T_SUM = 143; + const T_TRAILING = 144; + const T_TRUE = 145; + const T_UPDATE = 146; + const T_WHERE = 147; + const T_WITH = 148; + const T_PARTIAL = 149; + const T_MOD = 150; + /** * Creates a new query scanner object. * diff --git a/lib/Doctrine/ORM/Query/Parser.php b/lib/Doctrine/ORM/Query/Parser.php index a01162453..21a86862d 100644 --- a/lib/Doctrine/ORM/Query/Parser.php +++ b/lib/Doctrine/ORM/Query/Parser.php @@ -1205,13 +1205,13 @@ class Parser } /** - * UpdateItem ::= IdentificationVariable "." {StateField | SingleValuedAssociationField} "=" NewValue + * UpdateItem ::= SingleValuedPathExpression "=" NewValue * * @return \Doctrine\ORM\Query\AST\UpdateItem */ public function UpdateItem() { - $pathExpr = $this->PathExpression(AST\PathExpression::TYPE_STATE_FIELD | AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION); + $pathExpr = $this->SingleValuedPathExpression(); $this->match(Lexer::T_EQUALS); @@ -1797,7 +1797,8 @@ class Parser * SimpleConditionalExpression ::= * ComparisonExpression | BetweenExpression | LikeExpression | * InExpression | NullComparisonExpression | ExistsExpression | - * EmptyCollectionComparisonExpression | CollectionMemberExpression + * EmptyCollectionComparisonExpression | CollectionMemberExpression | + * InstanceOfExpression */ public function SimpleConditionalExpression() { @@ -1853,6 +1854,8 @@ class Parser return $this->LikeExpression(); case Lexer::T_IN: return $this->InExpression(); + case Lexer::T_INSTANCE: + return $this->InstanceOfExpression(); case Lexer::T_IS: if ($lookahead['type'] == Lexer::T_NULL) { return $this->NullComparisonExpression(); @@ -2386,6 +2389,38 @@ class Parser return $inExpression; } + /** + * InstanceOfExpression ::= IdentificationVariable ["NOT"] "INSTANCE" ["OF"] (AbstractSchemaName | InputParameter) + * + * @return \Doctrine\ORM\Query\AST\InstanceOfExpression + */ + public function InstanceOfExpression() + { + $instanceOfExpression = new AST\InstanceOfExpression($this->IdentificationVariable()); + + if ($this->_lexer->isNextToken(Lexer::T_NOT)) { + $this->match(Lexer::T_NOT); + $instanceOfExpression->not = true; + } + + $this->match(Lexer::T_INSTANCE); + + if ($this->_lexer->isNextToken(Lexer::T_OF)) { + $this->match(Lexer::T_OF); + } + + if ($this->_lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) { + $this->match(Lexer::T_INPUT_PARAMETER); + $exprValue = new AST\InputParameter($this->_lexer->token['value']); + } else { + $exprValue = $this->AliasIdentificationVariable(); + } + + $instanceOfExpression->value = $exprValue; + + return $instanceOfExpression; + } + /** * LikeExpression ::= StringExpression ["NOT"] "LIKE" (string | input_parameter) ["ESCAPE" char] * diff --git a/lib/Doctrine/ORM/Query/QueryException.php b/lib/Doctrine/ORM/Query/QueryException.php index 2685d3555..9969e28b5 100644 --- a/lib/Doctrine/ORM/Query/QueryException.php +++ b/lib/Doctrine/ORM/Query/QueryException.php @@ -47,6 +47,11 @@ class QueryException extends \Doctrine\ORM\ORMException return new self('[Semantical Error] ' . $message); } + public static function invalidParameterType($expected, $received) + { + return new self('Invalid parameter type, ' . $received . ' given, but ' . $expected . ' expected.'); + } + public static function invalidParameterPosition($pos) { return new self('Invalid parameter position: ' . $pos); diff --git a/lib/Doctrine/ORM/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index 34be0a1be..3b48d3961 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -436,9 +436,10 @@ class SqlWalker implements TreeWalker * Walks down an IdentificationVariable (no AST node associated), thereby generating the SQL. * * @param string $identificationVariable + * @param string $fieldName * @return string The SQL. */ - public function walkIdentificationVariable($identificationVariable, $fieldName) + public function walkIdentificationVariable($identificationVariable, $fieldName = null) { $class = $this->_queryComponents[$identificationVariable]['metadata']; @@ -1510,6 +1511,66 @@ class SqlWalker implements TreeWalker return $sql; } + /** + * Walks down an InstanceOfExpression AST node, thereby generating the appropriate SQL. + * + * @param InstanceOfExpression + * @return string The SQL. + */ + function walkInstanceOfExpression($instanceOfExpr) + { + $sql = ''; + + $dqlAlias = $instanceOfExpr->identificationVariable; + $discrClass = $class = $this->_queryComponents[$dqlAlias]['metadata']; + $fieldName = null; + + if ($class->discriminatorColumn && isset($class->discriminatorColumn['inherited'])) { + $discrClass = $this->_em->getClassMetadata($class->discriminatorColumn['inherited']); + } + + if ($this->_useSqlTableAliases) { + $sql .= $this->getSQLTableAlias($discrClass->table['name'], $dqlAlias) . '.'; + } + + $sql .= $class->discriminatorColumn['name'] . ($instanceOfExpr->not ? ' != ' : ' = '); + + if ($instanceOfExpr->value instanceof AST\InputParameter) { + // We need to modify the parameter value to be its correspondent mapped value + $dqlParamKey = $instanceOfExpr->value->name; + $paramValue = $this->_query->getParameter($dqlParamKey); + + if ( ! ($paramValue instanceof \Doctrine\ORM\Mapping\ClassMetadata)) { + throw QueryException::invalidParameterType('ClassMetadata', get_class($paramValue)); + } + + $entityClassName = $paramValue->name; + } else { + $entityClassName = $instanceOfExpr->value; + } + + $conn = $this->_em->getConnection(); + + if ($entityClassName == $class->name) { + $sql .= $conn->quote($class->discriminatorValue); + } else { + foreach ($class->subClasses as $subclassName) { + if ($entityClassName === $subclassName) { + $sql .= $conn->quote($this->_em->getClassMetadata($subclassName)->discriminatorValue); + break; + } + } + } + + return $sql; + } + + /** + * Walks down an InParameter AST node, thereby generating the appropriate SQL. + * + * @param InParameter + * @return string The SQL. + */ public function walkInParameter($inParam) { return $inParam instanceof AST\InputParameter ? diff --git a/lib/Doctrine/ORM/Query/TreeWalker.php b/lib/Doctrine/ORM/Query/TreeWalker.php index 1654f2a1c..4bbe963a6 100644 --- a/lib/Doctrine/ORM/Query/TreeWalker.php +++ b/lib/Doctrine/ORM/Query/TreeWalker.php @@ -290,6 +290,14 @@ interface TreeWalker */ function walkInExpression($inExpr); + /** + * Walks down an InstanceOfExpression AST node, thereby generating the appropriate SQL. + * + * @param InstanceOfExpression + * @return string The SQL. + */ + function walkInstanceOfExpression($instanceOfExpr); + /** * Walks down a literal that represents an AST node, thereby generating the appropriate SQL. * diff --git a/lib/Doctrine/ORM/Query/TreeWalkerAdapter.php b/lib/Doctrine/ORM/Query/TreeWalkerAdapter.php index 7f5f33f3c..ca2a49520 100644 --- a/lib/Doctrine/ORM/Query/TreeWalkerAdapter.php +++ b/lib/Doctrine/ORM/Query/TreeWalkerAdapter.php @@ -324,6 +324,14 @@ abstract class TreeWalkerAdapter implements TreeWalker */ public function walkInExpression($inExpr) {} + /** + * Walks down an InstanceOfExpression AST node, thereby generating the appropriate SQL. + * + * @param InstanceOfExpression + * @return string The SQL. + */ + function walkInstanceOfExpression($instanceOfExpr) {} + /** * Walks down a literal that represents an AST node, thereby generating the appropriate SQL. * diff --git a/lib/Doctrine/ORM/Query/TreeWalkerChain.php b/lib/Doctrine/ORM/Query/TreeWalkerChain.php index 28ff54b95..1fc197783 100644 --- a/lib/Doctrine/ORM/Query/TreeWalkerChain.php +++ b/lib/Doctrine/ORM/Query/TreeWalkerChain.php @@ -472,6 +472,19 @@ class TreeWalkerChain implements TreeWalker } } + /** + * Walks down an InstanceOfExpression AST node, thereby generating the appropriate SQL. + * + * @param InstanceOfExpression + * @return string The SQL. + */ + function walkInstanceOfExpression($instanceOfExpr) + { + foreach ($this->_walkers as $walker) { + $walker->walkInstanceOfExpression($instanceOfExpr); + } + } + /** * Walks down a literal that represents an AST node, thereby generating the appropriate SQL. * diff --git a/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php b/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php index 26e7cd0ba..757d542ad 100644 --- a/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php +++ b/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php @@ -134,6 +134,21 @@ class LanguageRecognitionTest extends \Doctrine\Tests\OrmTestCase $this->assertValidDql('SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id NOT IN (1)'); } + public function testInstanceOfExpressionSupportedInWherePart() + { + $this->assertValidDql('SELECT u FROM Doctrine\Tests\Models\Company\CompanyPerson u WHERE u INSTANCE OF Doctrine\Tests\Models\Company\CompanyEmployee'); + } + + public function testInstanceOfExpressionWithInputParamSupportedInWherePart() + { + $this->assertValidDql('SELECT u FROM Doctrine\Tests\Models\Company\CompanyPerson u WHERE u INSTANCE OF ?1'); + } + + public function testNotInstanceOfExpressionSupportedInWherePart() + { + $this->assertValidDql('SELECT u FROM Doctrine\Tests\Models\Company\CompanyPerson u WHERE u NOT INSTANCE OF ?1'); + } + public function testExistsExpressionSupportedInWherePart() { $this->assertValidDql('SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE EXISTS (SELECT p.phonenumber FROM Doctrine\Tests\Models\CMS\CmsPhonenumber p WHERE p.phonenumber = 1234)'); diff --git a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php index 35a51a42e..7a664b400 100644 --- a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php +++ b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php @@ -15,16 +15,22 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase $this->_em = $this->_getTestEntityManager(); } - public function assertSqlGeneration($dqlToBeTested, $sqlToBeConfirmed, array $queryHints = array()) + public function assertSqlGeneration($dqlToBeTested, $sqlToBeConfirmed, array $queryHints = array(), array $queryParams = array()) { try { $query = $this->_em->createQuery($dqlToBeTested); + + foreach ($queryParams AS $name => $value) { + $query->setParameter($name, $value); + } + $query->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true) ->useQueryCache(false); foreach ($queryHints AS $name => $value) { $query->setHint($name, $value); } + parent::assertEquals($sqlToBeConfirmed, $query->getSql()); $query->free(); } catch (\Exception $e) { @@ -283,6 +289,39 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase ); } + public function testSupportsInstanceOfExpressionsInWherePart() + { + $this->assertSqlGeneration( + "SELECT u FROM Doctrine\Tests\Models\Company\CompanyPerson u WHERE u INSTANCE OF Doctrine\Tests\Models\Company\CompanyEmployee", + "SELECT c0_.id AS id0, c0_.name AS name1, c0_.discr AS discr2 FROM company_persons c0_ WHERE c0_.discr = 'employee'" + ); + } + + public function testSupportsInstanceOfExpressionsInWherePartInDeeperLevel() + { + $this->assertSqlGeneration( + "SELECT u FROM Doctrine\Tests\Models\Company\CompanyEmployee u WHERE u INSTANCE OF Doctrine\Tests\Models\Company\CompanyManager", + "SELECT c0_.id AS id0, c0_.name AS name1, c1_.salary AS salary2, c1_.department AS department3, c0_.discr AS discr4 FROM company_employees c1_ INNER JOIN company_persons c0_ ON c1_.id = c0_.id WHERE c0_.discr = 'manager'" + ); + } + + public function testSupportsInstanceOfExpressionsInWherePartInDeepestLevel() + { + $this->assertSqlGeneration( + "SELECT u FROM Doctrine\Tests\Models\Company\CompanyManager u WHERE u INSTANCE OF Doctrine\Tests\Models\Company\CompanyManager", + "SELECT c0_.id AS id0, c0_.name AS name1, c1_.salary AS salary2, c1_.department AS department3, c2_.title AS title4, c0_.discr AS discr5 FROM company_managers c2_ INNER JOIN company_employees c1_ ON c2_.id = c1_.id INNER JOIN company_persons c0_ ON c2_.id = c0_.id WHERE c0_.discr = 'manager'" + ); + } + + public function testSupportsInstanceOfExpressionsUsingInputParameterInWherePart() + { + $this->assertSqlGeneration( + "SELECT u FROM Doctrine\Tests\Models\Company\CompanyPerson u WHERE u INSTANCE OF ?1", + "SELECT c0_.id AS id0, c0_.name AS name1, c0_.discr AS discr2 FROM company_persons c0_ WHERE c0_.discr = 'employee'", + array(), array(1 => $this->_em->getClassMetadata('Doctrine\Tests\Models\Company\CompanyEmployee')) + ); + } + // Ticket #973 public function testSupportsSingleValuedInExpressionWithoutSpacesInWherePart() {