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..be71f4646 100644 --- a/lib/Doctrine/ORM/Query/Lexer.php +++ b/lib/Doctrine/ORM/Query/Lexer.php @@ -30,85 +30,89 @@ 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; - // 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. @@ -126,10 +130,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,8 +168,8 @@ class Lexer extends \Doctrine\Common\Lexer return self::T_STRING; - // Recognize identifiers - case (ctype_alpha($value[0]) || $value[0] === '_'): + // Recognize identifiers, aliased or qualified names + case (ctype_alpha($value[0]) || $value[0] === '_' || $value[0] === '\\'): $name = 'Doctrine\ORM\Query\Lexer::T_' . strtoupper($value); if (defined($name)) { @@ -175,6 +180,14 @@ 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 diff --git a/lib/Doctrine/ORM/Query/Parser.php b/lib/Doctrine/ORM/Query/Parser.php index deacc8088..c5c31d6a5 100644 --- a/lib/Doctrine/ORM/Query/Parser.php +++ b/lib/Doctrine/ORM/Query/Parser.php @@ -311,9 +311,22 @@ 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) { - $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(); @@ -949,29 +962,45 @@ class Parser } /** - * AbstractSchemaName ::= identifier + * AbstractSchemaName ::= fully_qualified_name | aliased_name | identifier * * @return string */ public function AbstractSchemaName() { - $this->match(Lexer::T_IDENTIFIER); + if ($this->lexer->isNextToken(Lexer::T_FULLY_QUALIFIED_NAME)) { + $this->match(Lexer::T_FULLY_QUALIFIED_NAME); - $schemaName = ltrim($this->lexer->token['value'], '\\'); + $schemaName = $this->lexer->token['value']; + } else if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER)) { + $this->match(Lexer::T_IDENTIFIER); - if (strrpos($schemaName, ':') !== false) { - list($namespaceAlias, $simpleClassName) = explode(':', $schemaName); + $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; } /** @@ -1199,9 +1228,12 @@ 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)) { $this->match(Lexer::T_AS); } @@ -1253,7 +1285,11 @@ 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 +1714,6 @@ class Parser // Describe non-root join declaration if ($joinDeclaration instanceof AST\RangeVariableDeclaration) { $joinDeclaration->isRoot = false; - - $adhocConditions = true; } // Check for ad-hoc Join conditions @@ -1701,6 +1735,8 @@ class Parser { $abstractSchemaName = $this->AbstractSchemaName(); + $this->validateAbstractSchemaName($abstractSchemaName); + if ($this->lexer->isNextToken(Lexer::T_AS)) { $this->match(Lexer::T_AS); } @@ -1811,24 +1847,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); @@ -2231,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; @@ -2246,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(); @@ -3138,7 +3169,11 @@ 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/Query/LanguageRecognitionTest.php b/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php index 85a5a810c..b90047e19 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( + + 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\\'), + 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\ 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 +266,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..1bc7f5dfb --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Query/ParserTest.php @@ -0,0 +1,143 @@ +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 testAbstractSchemaNameSupportsClassnamesWithLeadingBackslash() + { + $parser = $this->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 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..6166a81ad 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,6 +489,7 @@ 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 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')"