diff --git a/draft/Doctrine/Query/Parser.php b/draft/Doctrine/Query/Parser.php index 11e609a58..739ff6cdd 100644 --- a/draft/Doctrine/Query/Parser.php +++ b/draft/Doctrine/Query/Parser.php @@ -1,4 +1,37 @@ . + */ + +/** + * An LL(k) parser for the context-free grammar of Doctrine Query Language. + * Parses a DQL query, reports any errors in it, and generates the corresponding + * SQL. + * + * @package Doctrine + * @subpackage Query + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ class Doctrine_Query_Parser { /** @@ -46,21 +79,41 @@ class Doctrine_Query_Parser protected $_errorDistance = self::MIN_ERROR_DISTANCE; /** - * A query printer object used to print a parse tree from the input string. + * A query printer object used to print a parse tree from the input string + * for debugging purposes. * * @var Doctrine_Query_Printer */ protected $_printer; + /** + * The connection object used by this query. + * + * @var Doctrine_Connection + */ + protected $_conn; + /** * Creates a new query parser object. * * @param string $input query string to be parsed + * @param Doctrine_Connection The connection object the query will use. */ - public function __construct($input) + public function __construct($input, Doctrine_Connection $conn = null) { $this->_scanner = new Doctrine_Query_Scanner($input); $this->_printer = new Doctrine_Query_Printer(true); + + if ($conn === null) { + $conn = Doctrine_Manager::getInstance()->getCurrentConnection(); + } + + $this->_conn = $conn; + } + + public function getConnection() + { + return $this->_conn; } public function getProduction($name) @@ -90,26 +143,20 @@ class Doctrine_Query_Parser } if ($isMatch) { - //$this->_printer->println($this->lookahead['value']); + $this->_printer->println($this->lookahead['value']); $this->lookahead = $this->_scanner->next(); $this->_errorDistance++; } else { - $this->syntaxError(); + $this->logError(); } } - public function syntaxError() + public function logError($message = '') { - $this->_error('Unexpected "' . $this->lookahead['value'] . '"'); - } + if ($message === '') { + $message = 'Unexpected "' . $this->lookahead['value'] . '"'; + } - public function semanticalError($message) - { - $this->_error($message); - } - - protected function _error($message) - { if ($this->_errorDistance >= self::MIN_ERROR_DISTANCE) { $message .= 'at line ' . $this->lookahead['line'] . ', column ' . $this->lookahead['column']; diff --git a/draft/Doctrine/Query/Production.php b/draft/Doctrine/Query/Production.php index e3083343a..c6382349f 100644 --- a/draft/Doctrine/Query/Production.php +++ b/draft/Doctrine/Query/Production.php @@ -1,6 +1,35 @@ . + */ + /** - * An abstract base class that all query parser productions extend. + * An abstract base class for the productions of the Doctrine Query Language + * context-free grammar. + * + * @package Doctrine + * @subpackage Query + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ */ abstract class Doctrine_Query_Production { @@ -37,7 +66,6 @@ abstract class Doctrine_Query_Production */ public function __call($method, $args) { - return $this->_parser->getProduction($method)->execute($args); $this->_parser->getPrinter()->startProduction($name); $retval = $this->_parser->getProduction($method)->execute($args); $this->_parser->getPrinter()->endProduction(); @@ -53,13 +81,4 @@ abstract class Doctrine_Query_Production * @return mixed */ abstract public function execute(array $params = array()); - - protected function _isSubquery() - { - $lookahead = $this->_parser->lookahead; - $next = $this->_parser->getScanner()->peek(); - - return $lookahead['value'] === '(' && $next['type'] === Doctrine_Query_Token::T_SELECT; - } - } diff --git a/draft/Doctrine/Query/Production/AbstractSchemaName.php b/draft/Doctrine/Query/Production/AbstractSchemaName.php new file mode 100644 index 000000000..0d2ef3eee --- /dev/null +++ b/draft/Doctrine/Query/Production/AbstractSchemaName.php @@ -0,0 +1,70 @@ +. + */ + +/** + * This class represents the AbstractSchemaName production in DQL grammar. + * + * + * AbstractSchemaName = identifier + * + * + * @package Doctrine + * @subpackage Query + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_AbstractSchemaName extends Doctrine_Query_Production +{ + /** + * Executes the AbstractSchemaName production. + * + * + * AbstractSchemaName = identifier + * + * + * @param array $params This production does not take any parameters. + * @return Doctrine_Table|null the table object corresponding the identifier + * name + */ + public function execute(array $params = array()) + { + $table = null; + $token = $this->_parser->lookahead; + + if ($token['type'] === Doctrine_Query_Token::T_IDENTIFIER) { + + $table = $this->_parser->getConnection()->getTable($token['value']); + + if ($table === null) { + $this->_parser->logError('Table named "' . $name . '" does not exist.'); + } + + $this->_parser->match(Doctrine_Query_Token::T_IDENTIFIER); + } else { + $this->_parser->logError('Identifier expected'); + } + + return $table; + } +} diff --git a/draft/Doctrine/Query/Production/IdentificationVariable.php b/draft/Doctrine/Query/Production/IdentificationVariable.php new file mode 100644 index 000000000..d19fe3cdf --- /dev/null +++ b/draft/Doctrine/Query/Production/IdentificationVariable.php @@ -0,0 +1,19 @@ +_parser->lookahead; + + $this->_parser->match(Doctrine_Query_Token::T_IDENTIFIER); + + /* + if ( ! isValidIdentificationVariable($token['value'])) { + $this->error('"' . $name . '" is not a identification variable.'); + } + */ + } +} diff --git a/draft/Doctrine/Query/Production/PathExpressionEndingWithAsterisk.php b/draft/Doctrine/Query/Production/PathExpressionEndingWithAsterisk.php new file mode 100644 index 000000000..70c4541f4 --- /dev/null +++ b/draft/Doctrine/Query/Production/PathExpressionEndingWithAsterisk.php @@ -0,0 +1,16 @@ +_isNextToken(Doctrine_Query_Token::T_IDENTIFIER)) { + $this->_parser->match(Doctrine_Query_Token::T_IDENTIFIER); + $this->_parser->match('.'); + } + + $this->_parser->match('*'); + } +} diff --git a/draft/Doctrine/Query/Production/QueryLanguage.php b/draft/Doctrine/Query/Production/QueryLanguage.php index 0f44291b4..306b14825 100644 --- a/draft/Doctrine/Query/Production/QueryLanguage.php +++ b/draft/Doctrine/Query/Production/QueryLanguage.php @@ -18,7 +18,7 @@ class Doctrine_Query_Production_QueryLanguage extends Doctrine_Query_Production $this->DeleteStatement(); break; default: - $this->_parser->syntaxError(); + $this->_parser->logError(); } } } diff --git a/draft/Doctrine/Query/Production/RangeVariableDeclaration.php b/draft/Doctrine/Query/Production/RangeVariableDeclaration.php index bdb8f9098..b40c14e5a 100644 --- a/draft/Doctrine/Query/Production/RangeVariableDeclaration.php +++ b/draft/Doctrine/Query/Production/RangeVariableDeclaration.php @@ -1,18 +1,17 @@ PathExpression(); + $this->AbstractSchemaName(); if ($this->_isNextToken(Doctrine_Query_Token::T_AS)) { $this->_parser->match(Doctrine_Query_Token::T_AS); - $this->_parser->match(Doctrine_Query_Token::T_IDENTIFIER); - } elseif ($this->_isNextToken(Doctrine_Query_Token::T_IDENTIFIER)) { - $this->_parser->match(Doctrine_Query_Token::T_IDENTIFIER); } + + $this->IdentificationVariable(); } } diff --git a/draft/Doctrine/Query/Production/SelectExpression.php b/draft/Doctrine/Query/Production/SelectExpression.php index e505f32f1..a125b40bd 100644 --- a/draft/Doctrine/Query/Production/SelectExpression.php +++ b/draft/Doctrine/Query/Production/SelectExpression.php @@ -1,12 +1,35 @@ _parser->lookahead; + $this->_parser->getScanner()->resetPeek(); + + while (($token['type'] === Doctrine_Query_Token::T_IDENTIFIER) || ($token['value'] === '.')) { + $token = $this->_parser->getScanner()->peek(); + } + + return $token['value'] === '*'; + } + + private function _isSubquery() + { + $lookahead = $this->_parser->lookahead; + $next = $this->_parser->getScanner()->peek(); + + return $lookahead['value'] === '(' && $next['type'] === Doctrine_Query_Token::T_SELECT; + } + public function execute(array $params = array()) { - if ($this->_isSubquery()) { + if ($this->_isPathExpressionEndingWithAsterisk()) { + $this->PathExpressionEndingWithAsterisk(); + } elseif ($this->_isSubquery()) { $this->_parser->match('('); $this->Subselect(); $this->_parser->match(')'); @@ -18,7 +41,7 @@ class Doctrine_Query_Production_SelectExpression extends Doctrine_Query_Producti $this->_parser->match(Doctrine_Query_Token::T_AS); $this->_parser->match(Doctrine_Query_Token::T_IDENTIFIER); } elseif ($this->_isNextToken(Doctrine_Query_Token::T_IDENTIFIER)) { - $this->_parser->match(Doctrine_Query_Token::T_IDENTIFIER); + $this->IdentificationVariable(); } } } diff --git a/draft/Doctrine/Query/Scanner.php b/draft/Doctrine/Query/Scanner.php index 68c3972dc..2937c42bd 100644 --- a/draft/Doctrine/Query/Scanner.php +++ b/draft/Doctrine/Query/Scanner.php @@ -1,4 +1,35 @@ . + */ + +/** + * ... + * + * @package Doctrine + * @subpackage Query + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ class Doctrine_Query_Scanner { /** diff --git a/draft/Doctrine/Query/Token.php b/draft/Doctrine/Query/Token.php index de05bef03..e4f6ccb7f 100644 --- a/draft/Doctrine/Query/Token.php +++ b/draft/Doctrine/Query/Token.php @@ -50,14 +50,6 @@ final class Doctrine_Query_Token const T_SQRT = 141; const T_MOD = 142; const T_SIZE = 143; - const T_CURRENT_DATE = 144; - const T_CURRENT_TIMESTAMP = 145; - const T_CURRENT_TIME = 146; - const T_SUBSTRING = 147; - const T_CONCAT = 148; - const T_TRIM = 149; - const T_LOWER = 150; - const T_UPPER = 151; private function __construct() {} } diff --git a/draft/query-language.txt b/draft/query-language.txt index d396a35af..c9244789e 100644 --- a/draft/query-language.txt +++ b/draft/query-language.txt @@ -1,3 +1,16 @@ +/* Context-free grammar for Doctrine Query Language + * + * Document syntax: + * - non-terminals begin with an upper case character + * - terminals begin with a lower case character + * - parentheses (...) are used for grouping + * - square brackets [...] are used for defining an optional part, eg. zero or + * one time time + * - curly brackets {...} are used for repetion, eg. zero or more times + * - double quotation marks "..." define a terminal string + * - a vertical bar represents an alternative + */ + QueryLanguage = SelectStatement | UpdateStatement | DeleteStatement SelectStatement = [SelectClause] FromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause] [LimitClause] @@ -20,11 +33,11 @@ OrderByItem = PathExpression ["ASC" | "DESC"] GroupByItem = PathExpression UpdateItem = PathExpression "=" (Expression | "NULL") -IdentificationVariableDeclaration = RangeVariableDeclaration {Join} -RangeVariableDeclaration = PathExpression [["AS" ] identifier] +IdentificationVariableDeclaration = RangeVariableDeclaration {Join} +RangeVariableDeclaration = AbstractSchemaName ["AS"] IdentificationVariable -Join = ["LEFT" | "INNER"] "JOIN" PathExpression "AS" identifier [["ON" | "WITH"] ConditionalExpression] IndexBy -IndexBy = "INDEXBY" identifier +Join = ["LEFT" | "INNER"] "JOIN" PathExpression ["AS"] IdentificationVariable [("ON" | "WITH") ConditionalExpression] [IndexBy] +IndexBy = "INDEXBY" PathExpression ConditionalExpression = ConditionalTerm {"OR" ConditionalTerm} ConditionalTerm = ConditionalFactor {"AND" ConditionalFactor} @@ -34,15 +47,16 @@ SimpleConditionalExpression = Expression (ComparisonExpression | BetweenExpression | LikeExpression | InExpression | NullComparisonExpression) | ExistsExpression -Atom = string-literal | numeric-constant | input-parameter +Atom = string_literal | numeric_constant | input_parameter -Expression = Expression {("+" | "-" | "*" | "/") Expression} -Expression = ("+" | "-") Expression -Expression = "(" Expression ")" -Expression = PathExpression | Atom | | Function | AggregateExpression +Expression = Term {("+" | "-") Term} +Term = Factor {("*" | "/") Factor} +Factor = [("+" | "-")] Primary +Primary = PathExpression | Atom | "(" Expression ")" | Function | AggregateExpression -SelectExpression = (Expression | "(" Subselect ")" ) [["AS"] identifier] +SelectExpression = (PathExpressionEndingWithAsterisk | Expression | "(" Subselect ")" ) [["AS"] IdentificationVariable] PathExpression = identifier {"." identifier} +PathExpressionEndingWithAsterisk = {identifier "."} "*" AggregateExpression = ("AVG" | "MAX" | "MIN" | "SUM") "(" ["DISTINCT"] Expression ")" | "COUNT" "(" ["DISTINCT"] (Expression | "*") ")" @@ -51,26 +65,8 @@ QuantifiedExpression = ("ALL" | "ANY" | "SOME") "(" Subselect ")" BetweenExpression = ["NOT"] "BETWEEN" Expression "AND" Expression ComparisonExpression = ComparisonOperator ( QuantifiedExpression | Expression | "(" Subselect ")" ) InExpression = ["NOT"] "IN" "(" (Atom {"," Atom} | Subselect) ")" -LikeExpression = ["NOT"] "LIKE" Expression ["ESCAPE" escape_character] +LikeExpression = ["NOT"] "LIKE" Expression ["ESCAPE" string_literal] NullComparisonExpression = "IS" ["NOT"] "NULL" ExistsExpression = ["NOT"] "EXISTS" "(" Subselect ")" - -Function = - "CURRENT_DATE" | - "CURRENT_TIME" | - "CURRENT_TIMESTAMP" | - "LENGTH" "(" Expression ")" | - "LOCATE" "(" Expression "," Expression ["," Expression] ")" | - "ABS" "(" Expression ")" | - "SQRT" "(" Expression ")" | - "MOD" "(" Expression "," Expression ")" | - "SIZE" "(" Expression ")" | - "CONCAT" "(" Expression "," Expression ")" | - "SUBSTRING" "(" Expression "," Expression "," "Expression" ")" | - "TRIM" "(" [[TrimSpecification] [trim_character] "FROM"] string_primary ")" | - "LOWER" "(" string_primary ")" | - "UPPER" "(" string_primary ")" | - identifier "(" [Expression {"," Expression}]")" // Custom function - -TrimSpecification = "LEADING" | "TRAILING" | "BOTH" +Function = identifier "(" [Expression {"," Expression}] ")"