1
0
mirror of synced 2025-02-02 21:41:45 +03:00

Merge pull request #1549 from doctrine/DDC-3697

[RFC] DDC-3697
This commit is contained in:
Guilherme Blanco 2015-11-08 22:30:50 -05:00
commit bad0f17c10
8 changed files with 453 additions and 147 deletions

View File

@ -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

View File

@ -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

View File

@ -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;
}
/**

View File

@ -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):

View File

@ -1,11 +1,15 @@
<?php
namespace Doctrine\Tests\ORM\Query;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query,
Doctrine\ORM\Query\QueryException;
class LanguageRecognitionTest extends \Doctrine\Tests\OrmTestCase
{
/**
* @var EntityManagerInterface
*/
private $_em;
protected function setUp()
@ -73,6 +77,59 @@ class LanguageRecognitionTest extends \Doctrine\Tests\OrmTestCase
$this->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');

View File

@ -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')
);
}
}

View File

@ -0,0 +1,143 @@
<?php
namespace Doctrine\Tests\ORM\Query;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\Parser;
class ParserTest extends \Doctrine\Tests\OrmTestCase
{
/**
* @covers \Doctrine\ORM\Query\Parser::AbstractSchemaName
* @group DDC-3715
*/
public function testAbstractSchemaNameSupportsFQCN()
{
$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 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;
}
}

View File

@ -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')"