From f68db612db90d2d3616c68e5c99a101216254755 Mon Sep 17 00:00:00 2001 From: Dan Ungureanu Date: Sat, 25 Jul 2015 14:27:42 +0300 Subject: [PATCH] Updated sql-parser library to udan11/sql-parser@7861701. Signed-off-by: Dan Ungureanu --- libraries/sql-parser/src/Component.php | 132 ++++++++++-------- .../sql-parser/src/Components/Expression.php | 5 +- .../src/Components/ExpressionArray.php | 5 +- .../src/Components/FieldDefinition.php | 7 + .../src/Components/OrderKeyword.php | 32 ++++- .../src/Components/SetOperation.php | 18 +++ .../src/Components/UnionKeyword.php | 41 ++++++ .../src/Contexts/ContextMySql50000.php | 11 +- .../src/Contexts/ContextMySql50100.php | 11 +- .../src/Contexts/ContextMySql50500.php | 11 +- .../src/Contexts/ContextMySql50600.php | 11 +- .../src/Contexts/ContextMySql50700.php | 11 +- libraries/sql-parser/src/Parser.php | 121 ++++++++++++---- libraries/sql-parser/src/Statement.php | 34 +++-- .../src/Statements/SelectStatement.php | 3 + .../src/Statements/TransactionStatement.php | 119 ++++++++++++++++ libraries/sql-parser/src/Utils/Query.php | 11 +- 17 files changed, 464 insertions(+), 119 deletions(-) create mode 100644 libraries/sql-parser/src/Components/UnionKeyword.php create mode 100644 libraries/sql-parser/src/Statements/TransactionStatement.php diff --git a/libraries/sql-parser/src/Component.php b/libraries/sql-parser/src/Component.php index 296ce77ade..7291ca5e31 100644 --- a/libraries/sql-parser/src/Component.php +++ b/libraries/sql-parser/src/Component.php @@ -10,64 +10,84 @@ * * @package SqlParser */ -namespace SqlParser; -/** - * A component (of a statement) is a part of a statement that is common to - * multiple query types. - * - * @category Components - * @package SqlParser - * @author Dan Ungureanu - * @license http://opensource.org/licenses/GPL-2.0 GNU Public License - */ -abstract class Component -{ +namespace { - /** - * Parses the tokens contained in the given list in the context of the given - * parser. - * - * @param Parser $parser The parser that serves as context. - * @param TokensList $list The list of tokens that are being parsed. - * @param array $options Parameters for parsing. - * - * @return mixed - */ - public static function parse( - Parser $parser, TokensList $list, array $options = array() - ) { - // This method should be abstract, but it can't be both static and - // abstract. - return null; - } + if (!function_exists('__')) { - /** - * Builds the string representation of a component of this type. - * - * In other words, this function represents the inverse function of - * `static::parse`. - * - * @param mixed $component The component to be built. - * - * @return string - */ - public static function build($component) - { - // This method should be abstract, but it can't be both static and - // abstract. - return null; - } - - /** - * Builds the string representation of a component of this type. - * - * @see static::build - * - * @return string - */ - public function __toString() - { - return static::build($this); + /** + * Translates the given string. + * + * @param string $str String to be translated. + * + * @return string + */ + function __($str) + { + return $str; + } + } +} + +namespace SqlParser { + + /** + * A component (of a statement) is a part of a statement that is common to + * multiple query types. + * + * @category Components + * @package SqlParser + * @author Dan Ungureanu + * @license http://opensource.org/licenses/GPL-2.0 GNU Public License + */ + abstract class Component + { + + /** + * Parses the tokens contained in the given list in the context of the given + * parser. + * + * @param Parser $parser The parser that serves as context. + * @param TokensList $list The list of tokens that are being parsed. + * @param array $options Parameters for parsing. + * + * @return mixed + */ + public static function parse( + Parser $parser, TokensList $list, array $options = array() + ) { + // This method should be abstract, but it can't be both static and + // abstract. + throw new \Exception(\__('Not implemented yet.')); + } + + /** + * Builds the string representation of a component of this type. + * + * In other words, this function represents the inverse function of + * `static::parse`. + * + * @param mixed $component The component to be built. + * + * @return string + */ + public static function build($component) + { + // This method should be abstract, but it can't be both static and + // abstract. + throw new \Exception(\__('Not implemented yet.')); + } + + /** + * Builds the string representation of a component of this type. + * + * @see static::build + * + * @return string + */ + public function __toString() + { + return static::build($this); + } } } diff --git a/libraries/sql-parser/src/Components/Expression.php b/libraries/sql-parser/src/Components/Expression.php index b9f257c7ed..d7d0d4a91c 100644 --- a/libraries/sql-parser/src/Components/Expression.php +++ b/libraries/sql-parser/src/Components/Expression.php @@ -175,7 +175,10 @@ class Expression extends Component continue; } - if (($token->type === Token::TYPE_KEYWORD) && ($token->flags & Token::FLAG_KEYWORD_RESERVED)) { + if (($token->type === Token::TYPE_KEYWORD) + && ($token->flags & Token::FLAG_KEYWORD_RESERVED) + && ($token->value !== 'DUAL') + ) { // Keywords may be found only between brackets. if ($brackets === 0) { if ((empty($options['noAlias'])) && ($token->value === 'AS')) { diff --git a/libraries/sql-parser/src/Components/ExpressionArray.php b/libraries/sql-parser/src/Components/ExpressionArray.php index e006aafeda..2e082dc48e 100644 --- a/libraries/sql-parser/src/Components/ExpressionArray.php +++ b/libraries/sql-parser/src/Components/ExpressionArray.php @@ -67,7 +67,10 @@ class ExpressionArray extends Component continue; } - if (($token->type === Token::TYPE_KEYWORD) && ($token->flags & Token::FLAG_KEYWORD_RESERVED)) { + if (($token->type === Token::TYPE_KEYWORD) + && ($token->flags & Token::FLAG_KEYWORD_RESERVED) + && ($token->value !== 'DUAL') + ) { // No keyword is expected. break; } diff --git a/libraries/sql-parser/src/Components/FieldDefinition.php b/libraries/sql-parser/src/Components/FieldDefinition.php index 32a426ba00..351e8b551b 100644 --- a/libraries/sql-parser/src/Components/FieldDefinition.php +++ b/libraries/sql-parser/src/Components/FieldDefinition.php @@ -241,6 +241,13 @@ class FieldDefinition extends Component $state = 6; ++$list->idx; break; + } else { + $parser->error( + __('A comma or a closing bracket was expected.'), + $token + ); + $state = 0; + break; } } } diff --git a/libraries/sql-parser/src/Components/OrderKeyword.php b/libraries/sql-parser/src/Components/OrderKeyword.php index 26d0f20d80..7f1879c519 100644 --- a/libraries/sql-parser/src/Components/OrderKeyword.php +++ b/libraries/sql-parser/src/Components/OrderKeyword.php @@ -37,7 +37,19 @@ class OrderKeyword extends Component * * @var string */ - public $type = 'ASC'; + public $type; + + /** + * Constructor. + * + * @param Expression $field The field that we are sorting by. + * @param string $type The sorting type. + */ + public function __construct($field = null, $type = 'ASC') + { + $this->field = $field; + $this->type = $type; + } /** * @param Parser $parser The parser that serves as context. @@ -110,4 +122,22 @@ class OrderKeyword extends Component --$list->idx; return $ret; } + + /** + * @param OrderKeyword $component The component to be built. + * + * @return string + */ + public static function build($component) + { + if (is_array($component)) { + $ret = array(); + foreach ($component as $c) { + $ret[] = static::build($c); + } + return implode(", ", $ret); + } else { + return Expression::build($component->field) . ' ' . $component->type; + } + } } diff --git a/libraries/sql-parser/src/Components/SetOperation.php b/libraries/sql-parser/src/Components/SetOperation.php index a9e8ba1624..fc05cf7404 100644 --- a/libraries/sql-parser/src/Components/SetOperation.php +++ b/libraries/sql-parser/src/Components/SetOperation.php @@ -117,4 +117,22 @@ class SetOperation extends Component --$list->idx; return $ret; } + + /** + * @param SetOperation|SetOperation[] $component The component to be built. + * + * @return string + */ + public static function build($component) + { + if (is_array($component)) { + $ret = array(); + foreach ($component as $c) { + $ret[] = static::build($c); + } + return implode(", ", $ret); + } else { + return $component->column . ' = ' . $component->value; + } + } } diff --git a/libraries/sql-parser/src/Components/UnionKeyword.php b/libraries/sql-parser/src/Components/UnionKeyword.php new file mode 100644 index 0000000000..5a247bc46f --- /dev/null +++ b/libraries/sql-parser/src/Components/UnionKeyword.php @@ -0,0 +1,41 @@ + + * @license http://opensource.org/licenses/GPL-2.0 GNU Public License + */ +class UnionKeyword extends Component +{ + + /** + * @param SelectStatement[] $component The component to be built. + * + * @return string + */ + public static function build($component) + { + $ret = array(); + foreach ($component as $c) { + $ret[] = $c->build(); + } + return implode(" UNION ", $ret); + } +} diff --git a/libraries/sql-parser/src/Contexts/ContextMySql50000.php b/libraries/sql-parser/src/Contexts/ContextMySql50000.php index 5069b917ab..ed86320ac4 100644 --- a/libraries/sql-parser/src/Contexts/ContextMySql50000.php +++ b/libraries/sql-parser/src/Contexts/ContextMySql50000.php @@ -144,15 +144,18 @@ class ContextMySql50000 extends Context 'SQL_CALC_FOUND_ROWS' => 3, 'GROUP BY' => 7, 'NOT NULL' => 7, 'ORDER BY' => 7, 'SET NULL' => 7, - 'FULL JOIN' => 7, 'IF EXISTS' => 7, 'LEFT JOIN' => 7, 'NO ACTION' => 7, - 'ON DELETE' => 7, 'ON UPDATE' => 7, - 'INNER JOIN' => 7, 'OR REPLACE' => 7, 'RIGHT JOIN' => 7, - 'FOR EACH ROW' => 7, 'SQL SECURITY' => 7, + 'AND CHAIN' => 7, 'FULL JOIN' => 7, 'IF EXISTS' => 7, 'LEFT JOIN' => 7, + 'NO ACTION' => 7, 'ON DELETE' => 7, 'ON UPDATE' => 7, + 'INNER JOIN' => 7, 'NO RELEASE' => 7, 'OR REPLACE' => 7, 'RIGHT JOIN' => 7, + 'AND NO CHAIN' => 7, 'FOR EACH ROW' => 7, 'SQL SECURITY' => 7, 'CHARACTER SET' => 7, 'IF NOT EXISTS' => 7, 'DATA DIRECTORY' => 7, 'DEFAULT CHARSET' => 7, 'DEFAULT COLLATE' => 7, 'INDEX DIRECTORY' => 7, 'GENERATED ALWAYS' => 7, + 'START TRANSACTION' => 7, + 'SELECT TRANSACTION' => 7, 'DEFAULT CHARACTER SET' => 7, + 'WITH CONSISTENT SNAPSHOT' => 7, 'XML' => 9, 'ENUM' => 9, 'TEXT' => 9, diff --git a/libraries/sql-parser/src/Contexts/ContextMySql50100.php b/libraries/sql-parser/src/Contexts/ContextMySql50100.php index ad39eb7917..c71ff6cc22 100644 --- a/libraries/sql-parser/src/Contexts/ContextMySql50100.php +++ b/libraries/sql-parser/src/Contexts/ContextMySql50100.php @@ -156,15 +156,18 @@ class ContextMySql50100 extends Context 'MASTER_SSL_VERIFY_SERVER_CERT' => 3, 'GROUP BY' => 7, 'NOT NULL' => 7, 'ORDER BY' => 7, 'SET NULL' => 7, - 'FULL JOIN' => 7, 'IF EXISTS' => 7, 'LEFT JOIN' => 7, 'NO ACTION' => 7, - 'ON DELETE' => 7, 'ON UPDATE' => 7, - 'INNER JOIN' => 7, 'OR REPLACE' => 7, 'RIGHT JOIN' => 7, - 'FOR EACH ROW' => 7, 'SQL SECURITY' => 7, + 'AND CHAIN' => 7, 'FULL JOIN' => 7, 'IF EXISTS' => 7, 'LEFT JOIN' => 7, + 'NO ACTION' => 7, 'ON DELETE' => 7, 'ON UPDATE' => 7, + 'INNER JOIN' => 7, 'NO RELEASE' => 7, 'OR REPLACE' => 7, 'RIGHT JOIN' => 7, + 'AND NO CHAIN' => 7, 'FOR EACH ROW' => 7, 'SQL SECURITY' => 7, 'CHARACTER SET' => 7, 'IF NOT EXISTS' => 7, 'DATA DIRECTORY' => 7, 'DEFAULT CHARSET' => 7, 'DEFAULT COLLATE' => 7, 'INDEX DIRECTORY' => 7, 'GENERATED ALWAYS' => 7, + 'START TRANSACTION' => 7, + 'SELECT TRANSACTION' => 7, 'DEFAULT CHARACTER SET' => 7, + 'WITH CONSISTENT SNAPSHOT' => 7, 'XML' => 9, 'ENUM' => 9, 'TEXT' => 9, diff --git a/libraries/sql-parser/src/Contexts/ContextMySql50500.php b/libraries/sql-parser/src/Contexts/ContextMySql50500.php index c0074c709b..6f6bb66af6 100644 --- a/libraries/sql-parser/src/Contexts/ContextMySql50500.php +++ b/libraries/sql-parser/src/Contexts/ContextMySql50500.php @@ -161,15 +161,18 @@ class ContextMySql50500 extends Context 'MASTER_SSL_VERIFY_SERVER_CERT' => 3, 'GROUP BY' => 7, 'NOT NULL' => 7, 'ORDER BY' => 7, 'SET NULL' => 7, - 'FULL JOIN' => 7, 'IF EXISTS' => 7, 'LEFT JOIN' => 7, 'NO ACTION' => 7, - 'ON DELETE' => 7, 'ON UPDATE' => 7, - 'INNER JOIN' => 7, 'OR REPLACE' => 7, 'RIGHT JOIN' => 7, - 'FOR EACH ROW' => 7, 'SQL SECURITY' => 7, + 'AND CHAIN' => 7, 'FULL JOIN' => 7, 'IF EXISTS' => 7, 'LEFT JOIN' => 7, + 'NO ACTION' => 7, 'ON DELETE' => 7, 'ON UPDATE' => 7, + 'INNER JOIN' => 7, 'NO RELEASE' => 7, 'OR REPLACE' => 7, 'RIGHT JOIN' => 7, + 'AND NO CHAIN' => 7, 'FOR EACH ROW' => 7, 'SQL SECURITY' => 7, 'CHARACTER SET' => 7, 'IF NOT EXISTS' => 7, 'DATA DIRECTORY' => 7, 'DEFAULT CHARSET' => 7, 'DEFAULT COLLATE' => 7, 'INDEX DIRECTORY' => 7, 'GENERATED ALWAYS' => 7, + 'START TRANSACTION' => 7, + 'SELECT TRANSACTION' => 7, 'DEFAULT CHARACTER SET' => 7, + 'WITH CONSISTENT SNAPSHOT' => 7, 'XML' => 9, 'ENUM' => 9, 'TEXT' => 9, diff --git a/libraries/sql-parser/src/Contexts/ContextMySql50600.php b/libraries/sql-parser/src/Contexts/ContextMySql50600.php index 453d9e2629..62b75abae1 100644 --- a/libraries/sql-parser/src/Contexts/ContextMySql50600.php +++ b/libraries/sql-parser/src/Contexts/ContextMySql50600.php @@ -166,15 +166,18 @@ class ContextMySql50600 extends Context 'MASTER_SSL_VERIFY_SERVER_CERT' => 3, 'GROUP BY' => 7, 'NOT NULL' => 7, 'ORDER BY' => 7, 'SET NULL' => 7, - 'FULL JOIN' => 7, 'IF EXISTS' => 7, 'LEFT JOIN' => 7, 'NO ACTION' => 7, - 'ON DELETE' => 7, 'ON UPDATE' => 7, - 'INNER JOIN' => 7, 'OR REPLACE' => 7, 'RIGHT JOIN' => 7, - 'FOR EACH ROW' => 7, 'SQL SECURITY' => 7, + 'AND CHAIN' => 7, 'FULL JOIN' => 7, 'IF EXISTS' => 7, 'LEFT JOIN' => 7, + 'NO ACTION' => 7, 'ON DELETE' => 7, 'ON UPDATE' => 7, + 'INNER JOIN' => 7, 'NO RELEASE' => 7, 'OR REPLACE' => 7, 'RIGHT JOIN' => 7, + 'AND NO CHAIN' => 7, 'FOR EACH ROW' => 7, 'SQL SECURITY' => 7, 'CHARACTER SET' => 7, 'IF NOT EXISTS' => 7, 'DATA DIRECTORY' => 7, 'DEFAULT CHARSET' => 7, 'DEFAULT COLLATE' => 7, 'INDEX DIRECTORY' => 7, 'GENERATED ALWAYS' => 7, + 'START TRANSACTION' => 7, + 'SELECT TRANSACTION' => 7, 'DEFAULT CHARACTER SET' => 7, + 'WITH CONSISTENT SNAPSHOT' => 7, 'XML' => 9, 'ENUM' => 9, 'TEXT' => 9, diff --git a/libraries/sql-parser/src/Contexts/ContextMySql50700.php b/libraries/sql-parser/src/Contexts/ContextMySql50700.php index 9946a26361..8b0568b0cd 100644 --- a/libraries/sql-parser/src/Contexts/ContextMySql50700.php +++ b/libraries/sql-parser/src/Contexts/ContextMySql50700.php @@ -174,15 +174,18 @@ class ContextMySql50700 extends Context 'MASTER_SSL_VERIFY_SERVER_CERT' => 3, 'GROUP BY' => 7, 'NOT NULL' => 7, 'ORDER BY' => 7, 'SET NULL' => 7, - 'FULL JOIN' => 7, 'IF EXISTS' => 7, 'LEFT JOIN' => 7, 'NO ACTION' => 7, - 'ON DELETE' => 7, 'ON UPDATE' => 7, - 'INNER JOIN' => 7, 'OR REPLACE' => 7, 'RIGHT JOIN' => 7, - 'FOR EACH ROW' => 7, 'SQL SECURITY' => 7, + 'AND CHAIN' => 7, 'FULL JOIN' => 7, 'IF EXISTS' => 7, 'LEFT JOIN' => 7, + 'NO ACTION' => 7, 'ON DELETE' => 7, 'ON UPDATE' => 7, + 'INNER JOIN' => 7, 'NO RELEASE' => 7, 'OR REPLACE' => 7, 'RIGHT JOIN' => 7, + 'AND NO CHAIN' => 7, 'FOR EACH ROW' => 7, 'SQL SECURITY' => 7, 'CHARACTER SET' => 7, 'IF NOT EXISTS' => 7, 'DATA DIRECTORY' => 7, 'DEFAULT CHARSET' => 7, 'DEFAULT COLLATE' => 7, 'INDEX DIRECTORY' => 7, 'GENERATED ALWAYS' => 7, + 'START TRANSACTION' => 7, + 'SELECT TRANSACTION' => 7, 'DEFAULT CHARACTER SET' => 7, + 'WITH CONSISTENT SNAPSHOT' => 7, 'XML' => 9, 'ENUM' => 9, 'TEXT' => 9, diff --git a/libraries/sql-parser/src/Parser.php b/libraries/sql-parser/src/Parser.php index f00e442d95..911e71be4b 100644 --- a/libraries/sql-parser/src/Parser.php +++ b/libraries/sql-parser/src/Parser.php @@ -28,8 +28,9 @@ namespace { namespace SqlParser { - use SqlParser\Statements\SelectStatement; use SqlParser\Exceptions\ParserException; + use SqlParser\Statements\SelectStatement; + use SqlParser\Statements\TransactionStatement; /** * Takes multiple tokens (contained in a Lexer instance) as input and builds a @@ -50,47 +51,54 @@ namespace SqlParser { */ public static $STATEMENT_PARSERS = array( - 'EXPLAIN' => 'SqlParser\\Statements\\ExplainStatement', + 'EXPLAIN' => 'SqlParser\\Statements\\ExplainStatement', // Table Maintenance Statements // https://dev.mysql.com/doc/refman/5.7/en/table-maintenance-sql.html - 'ANALYZE' => 'SqlParser\\Statements\\AnalyzeStatement', - 'BACKUP' => 'SqlParser\\Statements\\BackupStatement', - 'CHECK' => 'SqlParser\\Statements\\CheckStatement', - 'CHECKSUM' => 'SqlParser\\Statements\\ChecksumStatement', - 'OPTIMIZE' => 'SqlParser\\Statements\\OptimizeStatement', - 'REPAIR' => 'SqlParser\\Statements\\RepairStatement', - 'RESTORE' => 'SqlParser\\Statements\\RestoreStatement', + 'ANALYZE' => 'SqlParser\\Statements\\AnalyzeStatement', + 'BACKUP' => 'SqlParser\\Statements\\BackupStatement', + 'CHECK' => 'SqlParser\\Statements\\CheckStatement', + 'CHECKSUM' => 'SqlParser\\Statements\\ChecksumStatement', + 'OPTIMIZE' => 'SqlParser\\Statements\\OptimizeStatement', + 'REPAIR' => 'SqlParser\\Statements\\RepairStatement', + 'RESTORE' => 'SqlParser\\Statements\\RestoreStatement', // Database Administration Statements // https://dev.mysql.com/doc/refman/5.7/en/sql-syntax-server-administration.html - 'SET' => '', - 'SHOW' => 'SqlParser\\Statements\\ShowStatement', + 'SET' => '', + 'SHOW' => 'SqlParser\\Statements\\ShowStatement', // Data Definition Statements. // https://dev.mysql.com/doc/refman/5.7/en/sql-syntax-data-definition.html - 'ALTER' => 'SqlParser\\Statements\\AlterStatement', - 'CREATE' => 'SqlParser\\Statements\\CreateStatement', - 'DROP' => 'SqlParser\\Statements\\DropStatement', - 'RENAME' => 'SqlParser\\Statements\\RenameStatement', - 'TRUNCATE' => 'SqlParser\\Statements\\TruncateStatement', + 'ALTER' => 'SqlParser\\Statements\\AlterStatement', + 'CREATE' => 'SqlParser\\Statements\\CreateStatement', + 'DROP' => 'SqlParser\\Statements\\DropStatement', + 'RENAME' => 'SqlParser\\Statements\\RenameStatement', + 'TRUNCATE' => 'SqlParser\\Statements\\TruncateStatement', // Data Manipulation Statements. // https://dev.mysql.com/doc/refman/5.7/en/sql-syntax-data-manipulation.html - 'CALL' => 'SqlParser\\Statements\\CallStatement', - 'DELETE' => 'SqlParser\\Statements\\DeleteStatement', - 'DO' => '', - 'HANDLER' => '', - 'INSERT' => 'SqlParser\\Statements\\InsertStatement', - 'LOAD' => '', - 'REPLACE' => 'SqlParser\\Statements\\ReplaceStatement', - 'SELECT' => 'SqlParser\\Statements\\SelectStatement', - 'UPDATE' => 'SqlParser\\Statements\\UpdateStatement', + 'CALL' => 'SqlParser\\Statements\\CallStatement', + 'DELETE' => 'SqlParser\\Statements\\DeleteStatement', + 'DO' => '', + 'HANDLER' => '', + 'INSERT' => 'SqlParser\\Statements\\InsertStatement', + 'LOAD' => '', + 'REPLACE' => 'SqlParser\\Statements\\ReplaceStatement', + 'SELECT' => 'SqlParser\\Statements\\SelectStatement', + 'UPDATE' => 'SqlParser\\Statements\\UpdateStatement', // Prepared Statements. // https://dev.mysql.com/doc/refman/5.7/en/sql-syntax-prepared-statements.html - 'PREPARE' => '', - 'EXECUTE' => '', + 'PREPARE' => '', + 'EXECUTE' => '', + + // Transactional and Locking Statements + // https://dev.mysql.com/doc/refman/5.7/en/commit.html + 'START TRANSACTION' => 'SqlParser\\Statements\\TransactionStatement', + 'BEGIN' => 'SqlParser\\Statements\\TransactionStatement', + 'COMMIT' => 'SqlParser\\Statements\\TransactionStatement', + 'ROLLBACK' => 'SqlParser\\Statements\\TransactionStatement', ); /** @@ -106,6 +114,12 @@ namespace SqlParser { 'field' => 'options', ), + // This is used only for building. + 'UNION' => array( + 'class' => 'SqlParser\\Components\\UnionKeyword', + 'field' => 'union', + ), + 'ALTER' => array( 'class' => 'SqlParser\\Components\\Expression', 'field' => 'table', @@ -311,6 +325,12 @@ namespace SqlParser { public function parse() { + /** + * Last transaction. + * @var TransactionStatement + */ + $lastTransaction = null; + /** * Last parsed statement. * @var Statement $lastStatement @@ -321,7 +341,7 @@ namespace SqlParser { * Whether a union is parsed or not. * @var bool $inUnion */ - $inUnion = true; + $inUnion = false; /** * The index of the last token from the last statement. @@ -400,21 +420,62 @@ namespace SqlParser { $statement->last = $list->idx; $prevLastIdx = $list->idx; - // Finally, storing the statement. + // Handles unions. if (($inUnion) && ($lastStatement instanceof SelectStatement) && ($statement instanceof SelectStatement) ) { + /** * Last SELECT statement. * @var SelectStatement $lastStatement */ $lastStatement->union[] = $statement; + + // if there are no no delimiting brackets, the `ORDER` and + // `LIMIT` keywords actually belong to the first statement. + $lastStatement->order = $statement->order; + $lastStatement->limit = $statement->limit; + $statement->order = array(); + $statement->limit = null; + + // The statement actually ends where the last statement in + // union ends. + $lastStatement->last = $statement->last; + $inUnion = false; + continue; + } + + // Handles transactions. + if ($statement instanceof TransactionStatement) { + if ($statement->type === TransactionStatement::TYPE_BEGIN) { + $lastTransaction = $statement; + $this->statements[] = $statement; + } elseif ($statement->type === TransactionStatement::TYPE_END) { + if ($lastTransaction === null) { + // Even though an error occurred, the query is being + // saved. + $this->statements[] = $statement; + $this->error( + __('No transaction was previously started.'), + $token + ); + } else { + $lastTransaction->end = $statement; + } + $lastTransaction = null; + } + continue; + } + + // Finally, storing the statement. + if ($lastTransaction !== null) { + $lastTransaction->statements[] = $statement; } else { $this->statements[] = $statement; - $lastStatement = $statement; } + $lastStatement = $statement; } } diff --git a/libraries/sql-parser/src/Statement.php b/libraries/sql-parser/src/Statement.php index 1494a0dd52..17a6002ad4 100644 --- a/libraries/sql-parser/src/Statement.php +++ b/libraries/sql-parser/src/Statement.php @@ -122,11 +122,6 @@ abstract class Statement */ $type = $clause[1]; - // Checking if there is any parser (builder) for this clause. - if (empty(Parser::$KEYWORD_PARSERS[$name])) { - continue; - } - /** * The builder (parser) of this clause. * @var string $class @@ -169,6 +164,16 @@ abstract class Statement */ public function parse(Parser $parser, TokensList $list) { + /** + * Whether the beginning of this statement was previously parsed. + * + * This is used to delimit statements that don't use the usual + * delimiters. + * + * @var bool + */ + $parsedBeginning = false; + // This may be corrected by the parser. $this->first = $list->idx; @@ -178,7 +183,7 @@ abstract class Statement * default. * @var bool $parsedOptions */ - $parsedOptions = !empty(static::$OPTIONS) ? false : true; + $parsedOptions = empty(static::$OPTIONS); for (; $list->idx < $list->count; ++$list->idx) { /** @@ -236,8 +241,21 @@ abstract class Statement } if (!empty(Parser::$STATEMENT_PARSERS[$token->value])) { + if ($parsedBeginning) { + // New statement has started. We let the parser construct a + // new statement and parse that one + $parser->error( + __('A new statement was found, but no delimiter between them.'), + $token + ); + break; + } + $parsedBeginning = true; if (!$parsedOptions) { - ++$list->idx; // Skipping keyword. + if (empty(static::$OPTIONS[$token->value])) { + // Skipping keyword because if it is not a option. + ++$list->idx; + } $this->options = OptionsArray::parse( $parser, $list, @@ -256,7 +274,7 @@ abstract class Statement // Parsing this keyword. if ($class !== null) { - ++$list->idx; // Skipping keyword. + ++$list->idx; // Skipping keyword or last option. $this->$field = $class::parse($parser, $list, $options); } diff --git a/libraries/sql-parser/src/Statements/SelectStatement.php b/libraries/sql-parser/src/Statements/SelectStatement.php index 954c2202ec..a19bd8e143 100644 --- a/libraries/sql-parser/src/Statements/SelectStatement.php +++ b/libraries/sql-parser/src/Statements/SelectStatement.php @@ -99,6 +99,9 @@ class SelectStatement extends Statement 'PROCEDURE' => array('PROCEDURE', 3), 'INTO' => array('INTO', 3), 'UNION' => array('UNION', 3), + // These are available only when `UNION` is present. + // 'ORDER BY' => array('ORDER BY', 3), + // 'LIMIT' => array('LIMIT', 3), ); /** diff --git a/libraries/sql-parser/src/Statements/TransactionStatement.php b/libraries/sql-parser/src/Statements/TransactionStatement.php new file mode 100644 index 0000000000..d7c9dbfedf --- /dev/null +++ b/libraries/sql-parser/src/Statements/TransactionStatement.php @@ -0,0 +1,119 @@ + + * @license http://opensource.org/licenses/GPL-2.0 GNU Public License + */ +class TransactionStatement extends Statement +{ + + /** + * START TRANSACTION and BEGIN + * + * @var int + */ + const TYPE_BEGIN = 1; + + /** + * COMMIT and ROLLBACK + * + * @var int + */ + const TYPE_END = 2; + + /** + * The type of this query. + * + * @var int + */ + public $type; + + /** + * The list of statements in this transaction. + * + * @var Statements[] + */ + public $statements; + + /** + * The ending transaction statement which may be a `COMMIT` or a `ROLLBACK`. + * + * @var TransactionStatement + */ + public $end; + + /** + * Options for this query. + * + * @var array + */ + public static $OPTIONS = array( + 'START TRANSACTION' => 1, + 'BEGIN' => 1, + 'COMMIT' => 1, + 'ROLLBACK' => 1, + 'WITH CONSISTENT SNAPSHOT' => 2, + 'WORK' => 2, + 'AND NO CHAIN' => 3, + 'AND CHAIN' => 3, + 'RELEASE' => 4, + 'NO RELEASE' => 4, + ); + + /** + * @param Parser $parser The instance that requests parsing. + * @param TokensList $list The list of tokens to be parsed. + * + * @return void + */ + public function parse(Parser $parser, TokensList $list) + { + parent::parse($parser, $list); + + // Checks the type of this query. + if (($this->options->has('START TRANSACTION')) + || ($this->options->has('BEGIN')) + ) { + $this->type = TransactionStatement::TYPE_BEGIN; + } elseif (($this->options->has('COMMIT')) + || ($this->options->has('ROLLBACK')) + ) { + $this->type = TransactionStatement::TYPE_END; + } + } + + /** + * @return string + */ + public function build() + { + $ret = OptionsArray::build($this->options); + if ($this->type === TransactionStatement::TYPE_BEGIN) { + foreach ($this->statements as $statement) { + $ret .= ';' . $statement->build(); + } + $ret .= ';' . $this->end->build(); + } + return $ret; + } +} diff --git a/libraries/sql-parser/src/Utils/Query.php b/libraries/sql-parser/src/Utils/Query.php index bec402ade8..c11e858bb7 100644 --- a/libraries/sql-parser/src/Utils/Query.php +++ b/libraries/sql-parser/src/Utils/Query.php @@ -533,6 +533,14 @@ class Query */ $clauses = array_flip(array_keys($statement::$CLAUSES)); + // This is a cheap fix for `SELECT` statements that contain `UNION`. + // Replacing the `ORDER BY` or `LIMIT` clauses should replace the last + // clause. + if (($statement instanceof SelectStatement) && (!empty($statement->union))) { + $clauses['ORDER BY'] = count($clauses) + 1; + $clauses['LIMIT'] = count($clauses) + 2; + } + /** * Lexer used for lexing the clause. * @var Lexer $lexer @@ -552,10 +560,9 @@ class Query $clauseIdx = $clauses[$clauseType]; $firstClauseIdx = $clauseIdx; - $lastClauseIdx = $clauseIdx + 1; - // Determining the behaviour of this function. + // Determining the behavior of this function. if ($type === -1) { $firstClauseIdx = -1; // Something small enough. $lastClauseIdx = $clauseIdx - 1;