1
0
mirror of synced 2025-02-09 00:39:25 +03:00

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)
This commit is contained in:
Matthias Pigulla 2015-04-26 23:10:51 +02:00 committed by Guilherme Blanco
parent ada97d55ce
commit b7bd42638d
10 changed files with 369 additions and 105 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,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

View File

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

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

@ -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();

View File

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

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,55 @@ 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(
/* 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');

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,138 @@
<?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 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;
}
}

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,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
*/