From b7bd42638dcf8aa049698f01466688ff7762953b Mon Sep 17 00:00:00 2001 From: Matthias Pigulla Date: Sun, 26 Apr 2015 23:10:51 +0200 Subject: [PATCH 1/2] Fix for DDC-3697 and DDC-3701 Also fix Lexer::match() so it does not accept T_WHERE when T_WITH is supposed to be matched. (DDC-3701) --- .../reference/dql-doctrine-query-language.rst | 10 +- lib/Doctrine/ORM/Query/Lexer.php | 60 +++++--- lib/Doctrine/ORM/Query/Parser.php | 70 +++++---- lib/Doctrine/ORM/Query/SqlWalker.php | 14 +- .../ORM/Functional/BasicFunctionalTest.php | 2 +- .../Tests/ORM/Functional/NewOperatorTest.php | 8 +- .../ORM/Query/LanguageRecognitionTest.php | 71 ++++++++- tests/Doctrine/Tests/ORM/Query/LexerTest.php | 59 ++++---- tests/Doctrine/Tests/ORM/Query/ParserTest.php | 138 ++++++++++++++++++ .../ORM/Query/SelectSqlGenerationTest.php | 42 ++++-- 10 files changed, 369 insertions(+), 105 deletions(-) create mode 100644 tests/Doctrine/Tests/ORM/Query/ParserTest.php diff --git a/docs/en/reference/dql-doctrine-query-language.rst b/docs/en/reference/dql-doctrine-query-language.rst index ea902cb6b..eaf2d4610 100644 --- a/docs/en/reference/dql-doctrine-query-language.rst +++ b/docs/en/reference/dql-doctrine-query-language.rst @@ -1409,7 +1409,9 @@ Terminals ~~~~~~~~~ -- identifier (name, email, ...) +- identifier (name, email, ...) must match ``[a-z_][a-z0-9_]*`` +- fully_qualified_name (Doctrine\Tests\Models\CMS\CmsUser) matches PHP's fully qualified class names +- aliased_name (CMS:CmsUser) uses two identifiers, one for the namespace alias and one for the class inside it - string ('foo', 'bar''s house', '%ninja%', ...) - char ('/', '\\', ' ', ...) - integer (-1, 0, 1, 34, ...) @@ -1443,8 +1445,8 @@ Identifiers /* Alias Identification declaration (the "u" of "FROM User u") */ AliasIdentificationVariable :: = identifier - /* identifier that must be a class name (the "User" of "FROM User u") */ - AbstractSchemaName ::= identifier + /* identifier that must be a class name (the "User" of "FROM User u"), possibly as a fully qualified class name or namespace-aliased */ + AbstractSchemaName ::= fully_qualified_name | aliased_name | identifier /* Alias ResultVariable declaration (the "total" of "COUNT(*) AS total") */ AliasResultVariable = identifier @@ -1543,7 +1545,7 @@ Select Expressions SimpleSelectExpression ::= (StateFieldPathExpression | IdentificationVariable | FunctionDeclaration | AggregateExpression | "(" Subselect ")" | ScalarExpression) [["AS"] AliasResultVariable] PartialObjectExpression ::= "PARTIAL" IdentificationVariable "." PartialFieldSet PartialFieldSet ::= "{" SimpleStateField {"," SimpleStateField}* "}" - NewObjectExpression ::= "NEW" IdentificationVariable "(" NewObjectArg {"," NewObjectArg}* ")" + NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")" NewObjectArg ::= ScalarExpression | "(" Subselect ")" Conditional Expressions diff --git a/lib/Doctrine/ORM/Query/Lexer.php b/lib/Doctrine/ORM/Query/Lexer.php index d5721a735..3985a36b4 100644 --- a/lib/Doctrine/ORM/Query/Lexer.php +++ b/lib/Doctrine/ORM/Query/Lexer.php @@ -30,25 +30,27 @@ namespace Doctrine\ORM\Query; class Lexer extends \Doctrine\Common\Lexer { // All tokens that are not valid identifiers must be < 100 - const T_NONE = 1; - const T_INTEGER = 2; - const T_STRING = 3; - const T_INPUT_PARAMETER = 4; - const T_FLOAT = 5; - const T_CLOSE_PARENTHESIS = 6; - const T_OPEN_PARENTHESIS = 7; - const T_COMMA = 8; - const T_DIVIDE = 9; - const T_DOT = 10; - const T_EQUALS = 11; - const T_GREATER_THAN = 12; - const T_LOWER_THAN = 13; - const T_MINUS = 14; - const T_MULTIPLY = 15; - const T_NEGATE = 16; - const T_PLUS = 17; - const T_OPEN_CURLY_BRACE = 18; - const T_CLOSE_CURLY_BRACE = 19; + const T_NONE = 1; + const T_INTEGER = 2; + const T_STRING = 3; + const T_INPUT_PARAMETER = 4; + const T_FLOAT = 5; + const T_CLOSE_PARENTHESIS = 6; + const T_OPEN_PARENTHESIS = 7; + const T_COMMA = 8; + const T_DIVIDE = 9; + const T_DOT = 10; + const T_EQUALS = 11; + const T_GREATER_THAN = 12; + const T_LOWER_THAN = 13; + const T_MINUS = 14; + const T_MULTIPLY = 15; + const T_NEGATE = 16; + const T_PLUS = 17; + const T_OPEN_CURLY_BRACE = 18; + const T_CLOSE_CURLY_BRACE = 19; + const T_ALIASED_NAME = 20; + const T_FULLY_QUALIFIED_NAME = 21; // All tokens that are also identifiers should be >= 100 const T_IDENTIFIER = 100; @@ -126,10 +128,11 @@ class Lexer extends \Doctrine\Common\Lexer protected function getCatchablePatterns() { return array( - '[a-z_\\\][a-z0-9_\:\\\]*[a-z0-9_]{1}', - '(?:[0-9]+(?:[\.][0-9]+)*)(?:e[+-]?[0-9]+)?', - "'(?:[^']|'')*'", - '\?[0-9]*|:[a-z_][a-z0-9_]*' + '[a-z_][a-z0-9_]*\:[a-z_][a-z0-9_]*(?:\\\[a-z_][a-z0-9_]*)*', // aliased name + '[a-z_][a-z0-9_]*(?:\\\[a-z_][a-z0-9_]*)*', // identifier or qualified name + '(?:[0-9]+(?:[\.][0-9]+)*)(?:e[+-]?[0-9]+)?', // numbers + "'(?:[^']|'')*'", // quoted strings + '\?[0-9]*|:[a-z_][a-z0-9_]*' // parameters ); } @@ -163,7 +166,7 @@ class Lexer extends \Doctrine\Common\Lexer return self::T_STRING; - // Recognize identifiers + // Recognize identifiers, aliased or qualified names case (ctype_alpha($value[0]) || $value[0] === '_'): $name = 'Doctrine\ORM\Query\Lexer::T_' . strtoupper($value); @@ -175,6 +178,12 @@ class Lexer extends \Doctrine\Common\Lexer } } + if (strpos($value, ':') !== false) { + return self::T_ALIASED_NAME; + } + if (strpos($value, '\\') !== false) { + return self::T_FULLY_QUALIFIED_NAME; + } return self::T_IDENTIFIER; // Recognize input parameters @@ -197,6 +206,9 @@ class Lexer extends \Doctrine\Common\Lexer case ($value === '{'): return self::T_OPEN_CURLY_BRACE; case ($value === '}'): return self::T_CLOSE_CURLY_BRACE; + case (strrpos($value, ':') !== false): + return self::T_ALIASED_NAME; + // Default default: // Do nothing diff --git a/lib/Doctrine/ORM/Query/Parser.php b/lib/Doctrine/ORM/Query/Parser.php index deacc8088..6478b14b8 100644 --- a/lib/Doctrine/ORM/Query/Parser.php +++ b/lib/Doctrine/ORM/Query/Parser.php @@ -311,8 +311,13 @@ class Parser { $lookaheadType = $this->lexer->lookahead['type']; - // short-circuit on first condition, usually types match - if ($lookaheadType !== $token && $token !== Lexer::T_IDENTIFIER && $lookaheadType <= Lexer::T_IDENTIFIER) { + /* + * The following condition means: + * - If next token matches expectation -> ok. + * - If expectation is to get T_IDENTIFIER and we find one of the tokens that are reserved words *but* would work as identifiers as well (e. g. "FROM", DDC-505) -> ok. + * - Else fail. + */ + if ($lookaheadType !== $token && ($token !== Lexer::T_IDENTIFIER || $lookaheadType <= Lexer::T_IDENTIFIER)) { $this->syntaxError($this->lexer->getLiteral($token)); } @@ -949,29 +954,41 @@ class Parser } /** - * AbstractSchemaName ::= identifier + * AbstractSchemaName ::= fully_qualified_name | aliased_name | identifier * * @return string */ public function AbstractSchemaName() { - $this->match(Lexer::T_IDENTIFIER); - - $schemaName = ltrim($this->lexer->token['value'], '\\'); - - if (strrpos($schemaName, ':') !== false) { - list($namespaceAlias, $simpleClassName) = explode(':', $schemaName); - + if ($this->lexer->isNextToken(Lexer::T_FULLY_QUALIFIED_NAME)) { + $this->match(Lexer::T_FULLY_QUALIFIED_NAME); + $schemaName = $this->lexer->token['value']; + } else if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER)) { + $this->match(Lexer::T_IDENTIFIER); + $schemaName = $this->lexer->token['value']; + } else { + $this->match(Lexer::T_ALIASED_NAME); + list($namespaceAlias, $simpleClassName) = explode(':', $this->lexer->token['value']); $schemaName = $this->em->getConfiguration()->getEntityNamespace($namespaceAlias) . '\\' . $simpleClassName; } + return $schemaName; + } + + /** + * Validates an AbstractSchemaName, making sure the class exists. + * + * @param string $schemaName The name to validate. + * + * @throws QueryException if the name does not exist. + */ + private function validateAbstractSchemaName($schemaName) + { $exists = class_exists($schemaName, true); - if ( ! $exists) { + if (! $exists) { $this->semanticalError("Class '$schemaName' is not defined.", $this->lexer->token); } - - return $schemaName; } /** @@ -1201,6 +1218,7 @@ class Parser $this->match(Lexer::T_UPDATE); $token = $this->lexer->lookahead; $abstractSchemaName = $this->AbstractSchemaName(); + $this->validateAbstractSchemaName($abstractSchemaName); if ($this->lexer->isNextToken(Lexer::T_AS)) { $this->match(Lexer::T_AS); @@ -1253,7 +1271,9 @@ class Parser } $token = $this->lexer->lookahead; - $deleteClause = new AST\DeleteClause($this->AbstractSchemaName()); + $abstractSchemaName = $this->AbstractSchemaName(); + $this->validateAbstractSchemaName($abstractSchemaName); + $deleteClause = new AST\DeleteClause($abstractSchemaName); if ($this->lexer->isNextToken(Lexer::T_AS)) { $this->match(Lexer::T_AS); @@ -1678,8 +1698,6 @@ class Parser // Describe non-root join declaration if ($joinDeclaration instanceof AST\RangeVariableDeclaration) { $joinDeclaration->isRoot = false; - - $adhocConditions = true; } // Check for ad-hoc Join conditions @@ -1700,6 +1718,7 @@ class Parser public function RangeVariableDeclaration() { $abstractSchemaName = $this->AbstractSchemaName(); + $this->validateAbstractSchemaName($abstractSchemaName); if ($this->lexer->isNextToken(Lexer::T_AS)) { $this->match(Lexer::T_AS); @@ -1811,24 +1830,16 @@ class Parser } /** - * NewObjectExpression ::= "NEW" IdentificationVariable "(" NewObjectArg {"," NewObjectArg}* ")" + * NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")" * * @return \Doctrine\ORM\Query\AST\NewObjectExpression */ public function NewObjectExpression() { $this->match(Lexer::T_NEW); - $this->match(Lexer::T_IDENTIFIER); - $token = $this->lexer->token; - $className = $token['value']; - - if (strrpos($className, ':') !== false) { - list($namespaceAlias, $simpleClassName) = explode(':', $className); - - $className = $this->em->getConfiguration() - ->getEntityNamespace($namespaceAlias) . '\\' . $simpleClassName; - } + $className = $this->AbstractSchemaName(); // note that this is not yet validated + $token = $this->lexer->token; $this->match(Lexer::T_OPEN_PARENTHESIS); @@ -3138,7 +3149,10 @@ class Parser return new AST\InputParameter($this->lexer->token['value']); } - return $this->AliasIdentificationVariable(); + $abstractSchemaName = $this->AbstractSchemaName(); + $this->validateAbstractSchemaName($abstractSchemaName); + + return $abstractSchemaName; } /** diff --git a/lib/Doctrine/ORM/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index 4f1932ea8..b631ba6ae 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -1129,15 +1129,18 @@ class SqlWalker implements TreeWalker $class = $this->em->getClassMetadata($joinDeclaration->abstractSchemaName); $dqlAlias = $joinDeclaration->aliasIdentificationVariable; $tableAlias = $this->getSQLTableAlias($class->table['name'], $dqlAlias); - $condition = '(' . $this->walkConditionalExpression($join->conditionalExpression) . ')'; + $conditions = []; + + if ($join->conditionalExpression) { + $conditions[] = '(' . $this->walkConditionalExpression($join->conditionalExpression) . ')'; + } + $condExprConjunction = ($class->isInheritanceTypeJoined() && $joinType != AST\Join::JOIN_TYPE_LEFT && $joinType != AST\Join::JOIN_TYPE_LEFTOUTER) ? ' AND ' : ' ON '; $sql .= $this->walkRangeVariableDeclaration($joinDeclaration); - $conditions = array($condition); - // Apply remaining inheritance restrictions $discrSql = $this->_generateDiscriminatorColumnConditionSQL(array($dqlAlias)); @@ -1152,7 +1155,10 @@ class SqlWalker implements TreeWalker $conditions[] = $filterExpr; } - $sql .= $condExprConjunction . implode(' AND ', $conditions); + if ($conditions) { + $sql .= $condExprConjunction . implode(' AND ', $conditions); + } + break; case ($joinDeclaration instanceof \Doctrine\ORM\Query\AST\JoinAssociationDeclaration): diff --git a/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php b/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php index 29f8fab85..569200361 100644 --- a/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php @@ -129,7 +129,7 @@ class BasicFunctionalTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->_em->clear(); - $user2 = $this->_em->createQuery('select u from \Doctrine\Tests\Models\CMS\CmsUser u where u.id=?1') + $user2 = $this->_em->createQuery('select u from Doctrine\Tests\Models\CMS\CmsUser u where u.id=?1') ->setParameter(1, $userId) ->getSingleResult(); diff --git a/tests/Doctrine/Tests/ORM/Functional/NewOperatorTest.php b/tests/Doctrine/Tests/ORM/Functional/NewOperatorTest.php index e8c0e300f..7cde64b43 100644 --- a/tests/Doctrine/Tests/ORM/Functional/NewOperatorTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/NewOperatorTest.php @@ -1046,21 +1046,21 @@ class NewOperatorTest extends \Doctrine\Tests\OrmFunctionalTestCase /** * @expectedException Doctrine\ORM\Query\QueryException - * @expectedExceptionMessage [Semantical Error] line 0, col 11 near '\InvalidClass(u.name)': Error: Class "\InvalidClass" is not defined. + * @expectedExceptionMessage [Semantical Error] line 0, col 11 near 'InvalidClass(u.name)': Error: Class "InvalidClass" is not defined. */ public function testInvalidClassException() { - $dql = "SELECT new \InvalidClass(u.name) FROM Doctrine\Tests\Models\CMS\CmsUser u"; + $dql = "SELECT new InvalidClass(u.name) FROM Doctrine\Tests\Models\CMS\CmsUser u"; $this->_em->createQuery($dql)->getResult(); } /** * @expectedException Doctrine\ORM\Query\QueryException - * @expectedExceptionMessage [Semantical Error] line 0, col 11 near '\stdClass(u.name)': Error: Class "\stdClass" has not a valid constructor. + * @expectedExceptionMessage [Semantical Error] line 0, col 11 near 'stdClass(u.name)': Error: Class "stdClass" has not a valid constructor. */ public function testInvalidClassConstructorException() { - $dql = "SELECT new \stdClass(u.name) FROM Doctrine\Tests\Models\CMS\CmsUser u"; + $dql = "SELECT new stdClass(u.name) FROM Doctrine\Tests\Models\CMS\CmsUser u"; $this->_em->createQuery($dql)->getResult(); } diff --git a/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php b/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php index 85a5a810c..a0888d94e 100644 --- a/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php +++ b/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php @@ -1,11 +1,15 @@ assertValidDQL('SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u'); } + /** + * @dataProvider invalidDQL + */ + public function testRejectsInvalidDQL($dql) + { + $this->setExpectedException('\Doctrine\ORM\Query\QueryException'); + + $this->_em->getConfiguration()->setEntityNamespaces(array('Unknown' => 'Unknown', 'CMS' => 'Doctrine\Tests\Models\CMS')); + $this->parseDql($dql); + } + + public function invalidDQL() + { + return array( + + /* Checks for invalid IdentificationVariables and AliasIdentificationVariables */ + array('SELECT \foo FROM Doctrine\Tests\Models\CMS\CmsUser \foo'), + array('SELECT foo\ FROM Doctrine\Tests\Models\CMS\CmsUser foo\\'), + array('SELECT foo\bar FROM Doctrine\Tests\Models\CMS\CmsUser foo\bar'), + array('SELECT foo:bar FROM Doctrine\Tests\Models\CMS\CmsUser foo:bar'), + array('SELECT foo: FROM Doctrine\Tests\Models\CMS\CmsUser foo:'), + + /* Checks for invalid AbstractSchemaName */ + array('SELECT u FROM UnknownClass u'), // unknown + array('SELECT u FROM Unknown\Class u'), // unknown with namespace + array('SELECT u FROM \Unknown\Class u'), // unknown, leading backslash + array('SELECT u FROM Unknown\\\\Class u'), // unknown, syntactically bogus (duplicate \\) + array('SELECT u FROM Unknown\Class\ u'), // unknown, syntactically bogus (trailing \) + array('SELECT u FROM Unknown:Class u'), // unknown, with namespace alias + array('SELECT u FROM Unknown::Class u'), // unknown, with PAAMAYIM_NEKUDOTAYIM + array('SELECT u FROM Unknown:Class:Name u'), // unknown, with invalid namespace alias + array('SELECT u FROM UnknownClass: u'), // unknown, with invalid namespace alias + array('SELECT u FROM Unknown:Class: u'), // unknown, with invalid namespace alias + array('SELECT u FROM Doctrine\Tests\Models\CMS\\\\CmsUser u'), // syntactically bogus (duplicate \\)array('SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser\ u'), // syntactically bogus (trailing \) + array('SELECT u FROM CMS::User u'), + array('SELECT u FROM CMS:User: u'), + array('SELECT u FROM CMS:User:Foo u'), + + /* Checks for invalid AliasResultVariable */ + array('SELECT \'foo\' AS \foo FROM Doctrine\Tests\Models\CMS\CmsUser u'), + array('SELECT \'foo\' AS \foo\bar FROM Doctrine\Tests\Models\CMS\CmsUser u'), + array('SELECT \'foo\' AS foo\bar FROM Doctrine\Tests\Models\CMS\CmsUser u'), + array('SELECT \'foo\' AS foo\ FROM Doctrine\Tests\Models\CMS\CmsUser u'), + array('SELECT \'foo\' AS foo\\\\bar FROM Doctrine\Tests\Models\CMS\CmsUser u'), + array('SELECT \'foo\' AS foo: FROM Doctrine\Tests\Models\CMS\CmsUser u'), + array('SELECT \'foo\' AS foo:bar FROM Doctrine\Tests\Models\CMS\CmsUser u'), + ); + } + public function testSelectSingleComponentWithMultipleColumns() { $this->assertValidDQL('SELECT u.name, u.username FROM Doctrine\Tests\Models\CMS\CmsUser u'); @@ -209,11 +262,27 @@ class LanguageRecognitionTest extends \Doctrine\Tests\OrmTestCase $this->assertValidDQL('SELECT u.name, a.topic, p.phonenumber FROM Doctrine\Tests\Models\CMS\CmsUser u INNER JOIN u.articles a LEFT JOIN u.phonenumbers p'); } - public function testJoinClassPath() + public function testJoinClassPathUsingWITH() { $this->assertValidDQL('SELECT u.name FROM Doctrine\Tests\Models\CMS\CmsUser u JOIN Doctrine\Tests\Models\CMS\CmsArticle a WITH a.user = u.id'); } + /** + * @group DDC-3701 + */ + public function testJoinClassPathUsingWHERE() + { + $this->assertValidDQL('SELECT u.name FROM Doctrine\Tests\Models\CMS\CmsUser u JOIN Doctrine\Tests\Models\CMS\CmsArticle a WHERE a.user = u.id'); + } + + /** + * @group DDC-3701 + */ + public function testDDC3701WHEREIsNotWITH() + { + $this->assertInvalidDQL('SELECT c FROM Doctrine\Tests\Models\Company\CompanyContract c JOIN Doctrine\Tests\Models\Company\CompanyEmployee e WHERE e.id = c.salesPerson WHERE c.completed = true'); + } + public function testOrderBySingleColumn() { $this->assertValidDQL('SELECT u.name FROM Doctrine\Tests\Models\CMS\CmsUser u ORDER BY u.name'); diff --git a/tests/Doctrine/Tests/ORM/Query/LexerTest.php b/tests/Doctrine/Tests/ORM/Query/LexerTest.php index 7e0b4b8fb..5c9828731 100644 --- a/tests/Doctrine/Tests/ORM/Query/LexerTest.php +++ b/tests/Doctrine/Tests/ORM/Query/LexerTest.php @@ -11,44 +11,34 @@ class LexerTest extends \Doctrine\Tests\OrmTestCase protected function setUp() { } - public function testScannerRecognizesIdentifierWithLengthOfOneCharacter() + /** + * @dataProvider provideTokens + */ + public function testScannerRecognizesTokens($type, $value) { - $lexer = new Lexer('u'); + $lexer = new Lexer($value); $lexer->moveNext(); $token = $lexer->lookahead; - $this->assertEquals(Lexer::T_IDENTIFIER, $token['type']); - $this->assertEquals('u', $token['value']); + $this->assertEquals($type, $token['type']); + $this->assertEquals($value, $token['value']); } - public function testScannerRecognizesIdentifierConsistingOfLetters() + public function testScannerRecognizesTerminalString() { - $lexer = new Lexer('someIdentifier'); + /* + * "all" looks like an identifier, but in fact it's a reserved word + * (a terminal string). It's up to the parser to accept it as an identifier + * (with its literal value) when appropriate. + */ + + $lexer = new Lexer('all'); $lexer->moveNext(); $token = $lexer->lookahead; - $this->assertEquals(Lexer::T_IDENTIFIER, $token['type']); - $this->assertEquals('someIdentifier', $token['value']); - } - public function testScannerRecognizesIdentifierIncludingDigits() - { - $lexer = new Lexer('s0m31d3nt1f13r'); - - $lexer->moveNext(); - $token = $lexer->lookahead; - $this->assertEquals(Lexer::T_IDENTIFIER, $token['type']); - $this->assertEquals('s0m31d3nt1f13r', $token['value']); - } - - public function testScannerRecognizesIdentifierIncludingUnderscore() - { - $lexer = new Lexer('some_identifier'); - $lexer->moveNext(); - $token = $lexer->lookahead; - $this->assertEquals(Lexer::T_IDENTIFIER, $token['type']); - $this->assertEquals('some_identifier', $token['value']); + $this->assertEquals(Lexer::T_ALL, $token['type']); } public function testScannerRecognizesDecimalInteger() @@ -188,7 +178,7 @@ class LexerTest extends \Doctrine\Tests\OrmTestCase ), array( 'value' => 'My\Namespace\User', - 'type' => Lexer::T_IDENTIFIER, + 'type' => Lexer::T_FULLY_QUALIFIED_NAME, 'position' => 14 ), array( @@ -238,4 +228,19 @@ class LexerTest extends \Doctrine\Tests\OrmTestCase $this->assertFalse($lexer->moveNext()); } + + public function provideTokens() + { + return array( + array(Lexer::T_IDENTIFIER, 'u'), // one char + array(Lexer::T_IDENTIFIER, 'someIdentifier'), + array(Lexer::T_IDENTIFIER, 's0m31d3nt1f13r'), // including digits + array(Lexer::T_IDENTIFIER, 'some_identifier'), // including underscore + array(Lexer::T_IDENTIFIER, '_some_identifier'), // starts with underscore + array(Lexer::T_IDENTIFIER, 'comma'), // name of a token class with value < 100 (whitebox test) + array(Lexer::T_FULLY_QUALIFIED_NAME, 'Some\Class'), // DQL class reference + array(Lexer::T_ALIASED_NAME, 'Some:Name'), + array(Lexer::T_ALIASED_NAME, 'Some:Subclassed\Name') + ); + } } diff --git a/tests/Doctrine/Tests/ORM/Query/ParserTest.php b/tests/Doctrine/Tests/ORM/Query/ParserTest.php new file mode 100644 index 000000000..6cfc449c6 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Query/ParserTest.php @@ -0,0 +1,138 @@ +createParser('Doctrine\Tests\Models\CMS\CmsUser'); + + $this->assertEquals('Doctrine\Tests\Models\CMS\CmsUser', $parser->AbstractSchemaName()); + } + + /** + * @covers Doctrine\ORM\Query\Parser::AbstractSchemaName + * @group DDC-3715 + */ + public function testAbstractSchemaNameFailsOnClassnamesWithLeadingBackslash() + { + $this->setExpectedException('\Doctrine\ORM\Query\QueryException'); + + $parser = $this->createParser('\Doctrine\Tests\Models\CMS\CmsUser'); + $parser->AbstractSchemaName(); + } + + /** + * @covers \Doctrine\ORM\Query\Parser::AbstractSchemaName + * @group DDC-3715 + */ + public function testAbstractSchemaNameSupportsIdentifier() + { + $parser = $this->createParser('stdClass'); + $this->assertEquals('stdClass', $parser->AbstractSchemaName()); + } + + /** + * @covers \Doctrine\ORM\Query\Parser::AbstractSchemaName + * @group DDC-3715 + */ + public function testAbstractSchemaNameSupportsNamespaceAlias() + { + $parser = $this->createParser('CMS:CmsUser'); + $parser->getEntityManager()->getConfiguration()->addEntityNamespace('CMS', 'Doctrine\Tests\Models\CMS'); + + $this->assertEquals('Doctrine\Tests\Models\CMS\CmsUser', $parser->AbstractSchemaName()); + } + + /** + * @covers \Doctrine\ORM\Query\Parser::AbstractSchemaName + * @group DDC-3715 + */ + public function testAbstractSchemaNameSupportsNamespaceAliasWithRelativeClassname() + { + $parser = $this->createParser('Model:CMS\CmsUser'); + $parser->getEntityManager()->getConfiguration()->addEntityNamespace('Model', 'Doctrine\Tests\Models'); + + $this->assertEquals('Doctrine\Tests\Models\CMS\CmsUser', $parser->AbstractSchemaName()); + } + + /** + * @dataProvider validMatches + * @covers Doctrine\ORM\Query\Parser::match + * @group DDC-3701 + */ + public function testMatch($expectedToken, $inputString) + { + $parser = $this->createParser($inputString); + $parser->match($expectedToken); // throws exception if not matched + $this->addToAssertionCount(1); + } + + /** + * @dataProvider invalidMatches + * @covers Doctrine\ORM\Query\Parser::match + * @group DDC-3701 + */ + public function testMatchFailure($expectedToken, $inputString) + { + $this->setExpectedException('\Doctrine\ORM\Query\QueryException'); + + $parser = $this->createParser($inputString); + $parser->match($expectedToken); + } + + public function validMatches() + { + /* + * This only covers the special case handling in the Parser that some + * tokens that are *not* T_IDENTIFIER are accepted as well when matching + * identifiers. + * + * The basic checks that tokens are classified correctly do not belong here + * but in LexerTest. + */ + return array( + array(Lexer::T_WHERE, 'where'), // keyword + array(Lexer::T_DOT, '.'), // token that cannot be an identifier + array(Lexer::T_IDENTIFIER, 'someIdentifier'), + array(Lexer::T_IDENTIFIER, 'from'), // also a terminal string (the "FROM" keyword) as in DDC-505 + array(Lexer::T_IDENTIFIER, 'comma') // not even a terminal string, but the name of a constant in the Lexer (whitebox test) + ); + } + + public function invalidMatches() + { + return array( + array(Lexer::T_DOT, 'ALL'), // ALL is a terminal string (reserved keyword) and also possibly an identifier + array(Lexer::T_DOT, ','), // "," is a token on its own, but cannot be used as identifier + array(Lexer::T_WHERE, 'WITH'), // as in DDC-3697 + array(Lexer::T_WHERE, '.'), + + // The following are qualified or aliased names and must not be accepted where only an Identifier is expected + array(Lexer::T_IDENTIFIER, '\\Some\\Class'), + array(Lexer::T_IDENTIFIER, 'Some\\Class'), + array(Lexer::T_IDENTIFIER, 'Some:Name'), + ); + } + + private function createParser($dql) + { + $query = new Query($this->_getTestEntityManager()); + $query->setDQL($dql); + + $parser = new Parser($query); + $parser->getLexer()->moveNext(); + + return $parser; + } +} diff --git a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php index 0eb7b3bbe..bd5c4d598 100644 --- a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php +++ b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php @@ -84,6 +84,34 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase $this->fail($sql); } + /** + * @group DDC-3697 + */ + public function testJoinWithRangeVariablePutsConditionIntoSqlWhereClause() + { + $this->assertSqlGeneration( + 'SELECT c.id FROM Doctrine\Tests\Models\Company\CompanyPerson c JOIN Doctrine\Tests\Models\Company\CompanyPerson r WHERE c.spouse = r AND r.id = 42', + 'SELECT c0_.id AS id_0 FROM company_persons c0_ INNER JOIN company_persons c1_ WHERE c0_.spouse_id = c1_.id AND c1_.id = 42', + array(Query::HINT_FORCE_PARTIAL_LOAD => true) + ); + } + + /** + * @group DDC-3697 + */ + public function testJoinWithRangeVariableAndInheritancePutsConditionIntoSqlWhereClause() + { + /* + * Basically like the previous test, but this time load data for the inherited objects as well. + * The important thing is that the ON clauses in LEFT JOINs only contain the conditions necessary to join the appropriate inheritance table + * whereas the filtering condition must remain in the SQL WHERE clause. + */ + $this->assertSqlGeneration( + 'SELECT c.id FROM Doctrine\Tests\Models\Company\CompanyPerson c JOIN Doctrine\Tests\Models\Company\CompanyPerson r WHERE c.spouse = r AND r.id = 42', + 'SELECT c0_.id AS id_0 FROM company_persons c0_ LEFT JOIN company_managers c1_ ON c0_.id = c1_.id LEFT JOIN company_employees c2_ ON c0_.id = c2_.id INNER JOIN company_persons c3_ LEFT JOIN company_managers c4_ ON c3_.id = c4_.id LEFT JOIN company_employees c5_ ON c3_.id = c5_.id WHERE c0_.spouse_id = c3_.id AND c3_.id = 42', + array(Query::HINT_FORCE_PARTIAL_LOAD => false) + ); + } public function testSupportsSelectForAllFields() { @@ -461,23 +489,13 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase public function testSupportsInstanceOfExpressionInWherePartWithMultipleValues() { + // This also uses FQCNs starting with or without a backslash in the INSTANCE OF parameter $this->assertSqlGeneration( - "SELECT u FROM Doctrine\Tests\Models\Company\CompanyPerson u WHERE u INSTANCE OF (Doctrine\Tests\Models\Company\CompanyEmployee, \Doctrine\Tests\Models\Company\CompanyManager)", + "SELECT u FROM Doctrine\Tests\Models\Company\CompanyPerson u WHERE u INSTANCE OF (Doctrine\Tests\Models\Company\CompanyEmployee, Doctrine\Tests\Models\Company\CompanyManager)", "SELECT c0_.id AS id_0, c0_.name AS name_1, c0_.discr AS discr_2 FROM company_persons c0_ WHERE c0_.discr IN ('employee', 'manager')" ); } - /** - * @group DDC-1194 - */ - public function testSupportsInstanceOfExpressionsInWherePartPrefixedSlash() - { - $this->assertSqlGeneration( - "SELECT u FROM Doctrine\Tests\Models\Company\CompanyPerson u WHERE u INSTANCE OF \Doctrine\Tests\Models\Company\CompanyEmployee", - "SELECT c0_.id AS id_0, c0_.name AS name_1, c0_.discr AS discr_2 FROM company_persons c0_ WHERE c0_.discr IN ('employee')" - ); - } - /** * @group DDC-1194 */ From 443259f62964a69ce77272243696543444743e67 Mon Sep 17 00:00:00 2001 From: Guilherme Blanco Date: Sun, 8 Nov 2015 16:05:18 +0000 Subject: [PATCH 2/2] Some extra refinement over patch --- lib/Doctrine/ORM/Query/Lexer.php | 133 +++++++++--------- lib/Doctrine/ORM/Query/Parser.php | 39 +++-- .../ORM/Functional/BasicFunctionalTest.php | 2 +- .../Tests/ORM/Functional/NewOperatorTest.php | 8 +- .../ORM/Query/LanguageRecognitionTest.php | 8 +- tests/Doctrine/Tests/ORM/Query/ParserTest.php | 13 +- .../ORM/Query/SelectSqlGenerationTest.php | 13 +- 7 files changed, 129 insertions(+), 87 deletions(-) diff --git a/lib/Doctrine/ORM/Query/Lexer.php b/lib/Doctrine/ORM/Query/Lexer.php index 3985a36b4..be71f4646 100644 --- a/lib/Doctrine/ORM/Query/Lexer.php +++ b/lib/Doctrine/ORM/Query/Lexer.php @@ -49,68 +49,70 @@ class Lexer extends \Doctrine\Common\Lexer const T_PLUS = 17; const T_OPEN_CURLY_BRACE = 18; const T_CLOSE_CURLY_BRACE = 19; - const T_ALIASED_NAME = 20; - const T_FULLY_QUALIFIED_NAME = 21; - // All tokens that are also identifiers should be >= 100 - const T_IDENTIFIER = 100; - const T_ALL = 101; - const T_AND = 102; - const T_ANY = 103; - const T_AS = 104; - const T_ASC = 105; - const T_AVG = 106; - const T_BETWEEN = 107; - const T_BOTH = 108; - const T_BY = 109; - const T_CASE = 110; - const T_COALESCE = 111; - const T_COUNT = 112; - const T_DELETE = 113; - const T_DESC = 114; - const T_DISTINCT = 115; - const T_ELSE = 116; - const T_EMPTY = 117; - const T_END = 118; - const T_ESCAPE = 119; - const T_EXISTS = 120; - const T_FALSE = 121; - const T_FROM = 122; - const T_GROUP = 123; - const T_HAVING = 124; - const T_HIDDEN = 125; - const T_IN = 126; - const T_INDEX = 127; - const T_INNER = 128; - const T_INSTANCE = 129; - const T_IS = 130; - const T_JOIN = 131; - const T_LEADING = 132; - const T_LEFT = 133; - const T_LIKE = 134; - const T_MAX = 135; - const T_MEMBER = 136; - const T_MIN = 137; - const T_NOT = 138; - const T_NULL = 139; - const T_NULLIF = 140; - const T_OF = 141; - const T_OR = 142; - const T_ORDER = 143; - const T_OUTER = 144; - const T_SELECT = 145; - const T_SET = 146; - const T_SOME = 147; - const T_SUM = 148; - const T_THEN = 149; - const T_TRAILING = 150; - const T_TRUE = 151; - const T_UPDATE = 152; - const T_WHEN = 153; - const T_WHERE = 154; - const T_WITH = 155; - const T_PARTIAL = 156; - const T_NEW = 157; + // All tokens that are identifiers or keywords that could be considered as identifiers should be >= 100 + const T_ALIASED_NAME = 100; + const T_FULLY_QUALIFIED_NAME = 101; + const T_IDENTIFIER = 102; + + // All keyword tokens should be >= 200 + const T_ALL = 200; + const T_AND = 201; + const T_ANY = 202; + const T_AS = 203; + const T_ASC = 204; + const T_AVG = 205; + const T_BETWEEN = 206; + const T_BOTH = 207; + const T_BY = 208; + const T_CASE = 209; + const T_COALESCE = 210; + const T_COUNT = 211; + const T_DELETE = 212; + const T_DESC = 213; + const T_DISTINCT = 214; + const T_ELSE = 215; + const T_EMPTY = 216; + const T_END = 217; + const T_ESCAPE = 218; + const T_EXISTS = 219; + const T_FALSE = 220; + const T_FROM = 221; + const T_GROUP = 222; + const T_HAVING = 223; + const T_HIDDEN = 224; + const T_IN = 225; + const T_INDEX = 226; + const T_INNER = 227; + const T_INSTANCE = 228; + const T_IS = 229; + const T_JOIN = 230; + const T_LEADING = 231; + const T_LEFT = 232; + const T_LIKE = 233; + const T_MAX = 234; + const T_MEMBER = 235; + const T_MIN = 236; + const T_NEW = 237; + const T_NOT = 238; + const T_NULL = 239; + const T_NULLIF = 240; + const T_OF = 241; + const T_OR = 242; + const T_ORDER = 243; + const T_OUTER = 244; + const T_PARTIAL = 245; + const T_SELECT = 246; + const T_SET = 247; + const T_SOME = 248; + const T_SUM = 249; + const T_THEN = 250; + const T_TRAILING = 251; + const T_TRUE = 252; + const T_UPDATE = 253; + const T_WHEN = 254; + const T_WHERE = 255; + const T_WITH = 256; /** * Creates a new query scanner object. @@ -129,7 +131,7 @@ class Lexer extends \Doctrine\Common\Lexer { return array( '[a-z_][a-z0-9_]*\:[a-z_][a-z0-9_]*(?:\\\[a-z_][a-z0-9_]*)*', // aliased name - '[a-z_][a-z0-9_]*(?:\\\[a-z_][a-z0-9_]*)*', // identifier or qualified name + '[a-z_\\\][a-z0-9_]*(?:\\\[a-z_][a-z0-9_]*)*', // identifier or qualified name '(?:[0-9]+(?:[\.][0-9]+)*)(?:e[+-]?[0-9]+)?', // numbers "'(?:[^']|'')*'", // quoted strings '\?[0-9]*|:[a-z_][a-z0-9_]*' // parameters @@ -167,7 +169,7 @@ class Lexer extends \Doctrine\Common\Lexer return self::T_STRING; // Recognize identifiers, aliased or qualified names - case (ctype_alpha($value[0]) || $value[0] === '_'): + case (ctype_alpha($value[0]) || $value[0] === '_' || $value[0] === '\\'): $name = 'Doctrine\ORM\Query\Lexer::T_' . strtoupper($value); if (defined($name)) { @@ -181,9 +183,11 @@ class Lexer extends \Doctrine\Common\Lexer if (strpos($value, ':') !== false) { return self::T_ALIASED_NAME; } + if (strpos($value, '\\') !== false) { return self::T_FULLY_QUALIFIED_NAME; } + return self::T_IDENTIFIER; // Recognize input parameters @@ -206,9 +210,6 @@ class Lexer extends \Doctrine\Common\Lexer case ($value === '{'): return self::T_OPEN_CURLY_BRACE; case ($value === '}'): return self::T_CLOSE_CURLY_BRACE; - case (strrpos($value, ':') !== false): - return self::T_ALIASED_NAME; - // Default default: // Do nothing diff --git a/lib/Doctrine/ORM/Query/Parser.php b/lib/Doctrine/ORM/Query/Parser.php index 6478b14b8..c5c31d6a5 100644 --- a/lib/Doctrine/ORM/Query/Parser.php +++ b/lib/Doctrine/ORM/Query/Parser.php @@ -311,14 +311,22 @@ class Parser { $lookaheadType = $this->lexer->lookahead['type']; - /* - * The following condition means: - * - If next token matches expectation -> ok. - * - If expectation is to get T_IDENTIFIER and we find one of the tokens that are reserved words *but* would work as identifiers as well (e. g. "FROM", DDC-505) -> ok. - * - Else fail. - */ - if ($lookaheadType !== $token && ($token !== Lexer::T_IDENTIFIER || $lookaheadType <= Lexer::T_IDENTIFIER)) { - $this->syntaxError($this->lexer->getLiteral($token)); + // Short-circuit on first condition, usually types match + if ($lookaheadType !== $token) { + // If parameter is not identifier (1-99) must be exact match + if ($token < Lexer::T_IDENTIFIER) { + $this->syntaxError($this->lexer->getLiteral($token)); + } + + // If parameter is keyword (200+) must be exact match + if ($token > Lexer::T_IDENTIFIER) { + $this->syntaxError($this->lexer->getLiteral($token)); + } + + // If parameter is T_IDENTIFIER, then matches T_IDENTIFIER (100) and keywords (200+) + if ($token === Lexer::T_IDENTIFIER && $lookaheadType < Lexer::T_IDENTIFIER) { + $this->syntaxError($this->lexer->getLiteral($token)); + } } $this->lexer->moveNext(); @@ -962,13 +970,17 @@ class Parser { if ($this->lexer->isNextToken(Lexer::T_FULLY_QUALIFIED_NAME)) { $this->match(Lexer::T_FULLY_QUALIFIED_NAME); + $schemaName = $this->lexer->token['value']; } else if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER)) { $this->match(Lexer::T_IDENTIFIER); + $schemaName = $this->lexer->token['value']; } else { $this->match(Lexer::T_ALIASED_NAME); + list($namespaceAlias, $simpleClassName) = explode(':', $this->lexer->token['value']); + $schemaName = $this->em->getConfiguration()->getEntityNamespace($namespaceAlias) . '\\' . $simpleClassName; } @@ -1216,8 +1228,10 @@ class Parser public function UpdateClause() { $this->match(Lexer::T_UPDATE); + $token = $this->lexer->lookahead; $abstractSchemaName = $this->AbstractSchemaName(); + $this->validateAbstractSchemaName($abstractSchemaName); if ($this->lexer->isNextToken(Lexer::T_AS)) { @@ -1272,7 +1286,9 @@ class Parser $token = $this->lexer->lookahead; $abstractSchemaName = $this->AbstractSchemaName(); + $this->validateAbstractSchemaName($abstractSchemaName); + $deleteClause = new AST\DeleteClause($abstractSchemaName); if ($this->lexer->isNextToken(Lexer::T_AS)) { @@ -1718,6 +1734,7 @@ class Parser public function RangeVariableDeclaration() { $abstractSchemaName = $this->AbstractSchemaName(); + $this->validateAbstractSchemaName($abstractSchemaName); if ($this->lexer->isNextToken(Lexer::T_AS)) { @@ -2242,9 +2259,12 @@ class Parser } // [["AS"] ["HIDDEN"] AliasResultVariable] + $mustHaveAliasResultVariable = false; if ($this->lexer->isNextToken(Lexer::T_AS)) { $this->match(Lexer::T_AS); + + $mustHaveAliasResultVariable = true; } $hiddenAliasResultVariable = false; @@ -2257,7 +2277,7 @@ class Parser $aliasResultVariable = null; - if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER)) { + if ($mustHaveAliasResultVariable || $this->lexer->isNextToken(Lexer::T_IDENTIFIER)) { $token = $this->lexer->lookahead; $aliasResultVariable = $this->AliasResultVariable(); @@ -3150,6 +3170,7 @@ class Parser } $abstractSchemaName = $this->AbstractSchemaName(); + $this->validateAbstractSchemaName($abstractSchemaName); return $abstractSchemaName; diff --git a/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php b/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php index 569200361..29f8fab85 100644 --- a/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php @@ -129,7 +129,7 @@ class BasicFunctionalTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->_em->clear(); - $user2 = $this->_em->createQuery('select u from Doctrine\Tests\Models\CMS\CmsUser u where u.id=?1') + $user2 = $this->_em->createQuery('select u from \Doctrine\Tests\Models\CMS\CmsUser u where u.id=?1') ->setParameter(1, $userId) ->getSingleResult(); diff --git a/tests/Doctrine/Tests/ORM/Functional/NewOperatorTest.php b/tests/Doctrine/Tests/ORM/Functional/NewOperatorTest.php index 7cde64b43..e8c0e300f 100644 --- a/tests/Doctrine/Tests/ORM/Functional/NewOperatorTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/NewOperatorTest.php @@ -1046,21 +1046,21 @@ class NewOperatorTest extends \Doctrine\Tests\OrmFunctionalTestCase /** * @expectedException Doctrine\ORM\Query\QueryException - * @expectedExceptionMessage [Semantical Error] line 0, col 11 near 'InvalidClass(u.name)': Error: Class "InvalidClass" is not defined. + * @expectedExceptionMessage [Semantical Error] line 0, col 11 near '\InvalidClass(u.name)': Error: Class "\InvalidClass" is not defined. */ public function testInvalidClassException() { - $dql = "SELECT new InvalidClass(u.name) FROM Doctrine\Tests\Models\CMS\CmsUser u"; + $dql = "SELECT new \InvalidClass(u.name) FROM Doctrine\Tests\Models\CMS\CmsUser u"; $this->_em->createQuery($dql)->getResult(); } /** * @expectedException Doctrine\ORM\Query\QueryException - * @expectedExceptionMessage [Semantical Error] line 0, col 11 near 'stdClass(u.name)': Error: Class "stdClass" has not a valid constructor. + * @expectedExceptionMessage [Semantical Error] line 0, col 11 near '\stdClass(u.name)': Error: Class "\stdClass" has not a valid constructor. */ public function testInvalidClassConstructorException() { - $dql = "SELECT new stdClass(u.name) FROM Doctrine\Tests\Models\CMS\CmsUser u"; + $dql = "SELECT new \stdClass(u.name) FROM Doctrine\Tests\Models\CMS\CmsUser u"; $this->_em->createQuery($dql)->getResult(); } diff --git a/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php b/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php index a0888d94e..b90047e19 100644 --- a/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php +++ b/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php @@ -84,7 +84,11 @@ class LanguageRecognitionTest extends \Doctrine\Tests\OrmTestCase { $this->setExpectedException('\Doctrine\ORM\Query\QueryException'); - $this->_em->getConfiguration()->setEntityNamespaces(array('Unknown' => 'Unknown', 'CMS' => 'Doctrine\Tests\Models\CMS')); + $this->_em->getConfiguration()->setEntityNamespaces(array( + 'Unknown' => 'Unknown', + 'CMS' => 'Doctrine\Tests\Models\CMS' + )); + $this->parseDql($dql); } @@ -92,6 +96,7 @@ class LanguageRecognitionTest extends \Doctrine\Tests\OrmTestCase { return array( + array('SELECT \'foo\' AS foo\bar FROM Doctrine\Tests\Models\CMS\CmsUser u'), /* Checks for invalid IdentificationVariables and AliasIdentificationVariables */ array('SELECT \foo FROM Doctrine\Tests\Models\CMS\CmsUser \foo'), array('SELECT foo\ FROM Doctrine\Tests\Models\CMS\CmsUser foo\\'), @@ -118,7 +123,6 @@ class LanguageRecognitionTest extends \Doctrine\Tests\OrmTestCase /* Checks for invalid AliasResultVariable */ array('SELECT \'foo\' AS \foo FROM Doctrine\Tests\Models\CMS\CmsUser u'), array('SELECT \'foo\' AS \foo\bar FROM Doctrine\Tests\Models\CMS\CmsUser u'), - array('SELECT \'foo\' AS foo\bar FROM Doctrine\Tests\Models\CMS\CmsUser u'), array('SELECT \'foo\' AS foo\ FROM Doctrine\Tests\Models\CMS\CmsUser u'), array('SELECT \'foo\' AS foo\\\\bar FROM Doctrine\Tests\Models\CMS\CmsUser u'), array('SELECT \'foo\' AS foo: FROM Doctrine\Tests\Models\CMS\CmsUser u'), diff --git a/tests/Doctrine/Tests/ORM/Query/ParserTest.php b/tests/Doctrine/Tests/ORM/Query/ParserTest.php index 6cfc449c6..1bc7f5dfb 100644 --- a/tests/Doctrine/Tests/ORM/Query/ParserTest.php +++ b/tests/Doctrine/Tests/ORM/Query/ParserTest.php @@ -24,12 +24,11 @@ class ParserTest extends \Doctrine\Tests\OrmTestCase * @covers Doctrine\ORM\Query\Parser::AbstractSchemaName * @group DDC-3715 */ - public function testAbstractSchemaNameFailsOnClassnamesWithLeadingBackslash() + public function testAbstractSchemaNameSupportsClassnamesWithLeadingBackslash() { - $this->setExpectedException('\Doctrine\ORM\Query\QueryException'); - $parser = $this->createParser('\Doctrine\Tests\Models\CMS\CmsUser'); - $parser->AbstractSchemaName(); + + $this->assertEquals('\Doctrine\Tests\Models\CMS\CmsUser', $parser->AbstractSchemaName()); } /** @@ -39,6 +38,7 @@ class ParserTest extends \Doctrine\Tests\OrmTestCase public function testAbstractSchemaNameSupportsIdentifier() { $parser = $this->createParser('stdClass'); + $this->assertEquals('stdClass', $parser->AbstractSchemaName()); } @@ -49,6 +49,7 @@ class ParserTest extends \Doctrine\Tests\OrmTestCase public function testAbstractSchemaNameSupportsNamespaceAlias() { $parser = $this->createParser('CMS:CmsUser'); + $parser->getEntityManager()->getConfiguration()->addEntityNamespace('CMS', 'Doctrine\Tests\Models\CMS'); $this->assertEquals('Doctrine\Tests\Models\CMS\CmsUser', $parser->AbstractSchemaName()); @@ -61,6 +62,7 @@ class ParserTest extends \Doctrine\Tests\OrmTestCase public function testAbstractSchemaNameSupportsNamespaceAliasWithRelativeClassname() { $parser = $this->createParser('Model:CMS\CmsUser'); + $parser->getEntityManager()->getConfiguration()->addEntityNamespace('Model', 'Doctrine\Tests\Models'); $this->assertEquals('Doctrine\Tests\Models\CMS\CmsUser', $parser->AbstractSchemaName()); @@ -74,7 +76,9 @@ class ParserTest extends \Doctrine\Tests\OrmTestCase public function testMatch($expectedToken, $inputString) { $parser = $this->createParser($inputString); + $parser->match($expectedToken); // throws exception if not matched + $this->addToAssertionCount(1); } @@ -88,6 +92,7 @@ class ParserTest extends \Doctrine\Tests\OrmTestCase $this->setExpectedException('\Doctrine\ORM\Query\QueryException'); $parser = $this->createParser($inputString); + $parser->match($expectedToken); } diff --git a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php index bd5c4d598..6166a81ad 100644 --- a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php +++ b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php @@ -491,11 +491,22 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase { // This also uses FQCNs starting with or without a backslash in the INSTANCE OF parameter $this->assertSqlGeneration( - "SELECT u FROM Doctrine\Tests\Models\Company\CompanyPerson u WHERE u INSTANCE OF (Doctrine\Tests\Models\Company\CompanyEmployee, Doctrine\Tests\Models\Company\CompanyManager)", + "SELECT u FROM Doctrine\Tests\Models\Company\CompanyPerson u WHERE u INSTANCE OF (Doctrine\Tests\Models\Company\CompanyEmployee, \Doctrine\Tests\Models\Company\CompanyManager)", "SELECT c0_.id AS id_0, c0_.name AS name_1, c0_.discr AS discr_2 FROM company_persons c0_ WHERE c0_.discr IN ('employee', 'manager')" ); } + /** + * @group DDC-1194 + */ + public function testSupportsInstanceOfExpressionsInWherePartPrefixedSlash() + { + $this->assertSqlGeneration( + "SELECT u FROM Doctrine\Tests\Models\Company\CompanyPerson u WHERE u INSTANCE OF \Doctrine\Tests\Models\Company\CompanyEmployee", + "SELECT c0_.id AS id_0, c0_.name AS name_1, c0_.discr AS discr_2 FROM company_persons c0_ WHERE c0_.discr IN ('employee')" + ); + } + /** * @group DDC-1194 */