commit
bad0f17c10
@ -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
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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):
|
||||
|
@ -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');
|
||||
|
@ -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')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
143
tests/Doctrine/Tests/ORM/Query/ParserTest.php
Normal file
143
tests/Doctrine/Tests/ORM/Query/ParserTest.php
Normal 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;
|
||||
}
|
||||
}
|
@ -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')"
|
||||
|
Loading…
x
Reference in New Issue
Block a user