diff options
Diffstat (limited to 'src')
35 files changed, 563 insertions, 243 deletions
diff --git a/src/Builder.php b/src/Builder.php new file mode 100644 index 0000000..b4559e9 --- /dev/null +++ b/src/Builder.php @@ -0,0 +1,84 @@ +<?php + +/** + * This is one of the most important components, along with the lexer and the + * parser. + * + * @package SqlParser + */ +namespace SqlParser; + +use SqlParser\Exceptions\ParserException; + +/** + * Builds the string representation of a Statement. + * + * @category Parser + * @package SqlParser + * @author Dan Ungureanu <udan1107@gmail.com> + * @license http://opensource.org/licenses/GPL-2.0 GNU Public License + */ +class Builder +{ + + /** + * Statement to be build. + * + * @var Statement + */ + public $statement; + + /** + * Built query. + * + * @var string + */ + public $query; + + /** + * Constructor. + * + * @param Statement $statement + */ + public function __construct($statement = null) + { + $this->statement = $statement; + + if ($this->statement != null) { + $this->build(); + } + } + + /** + * Builds the statement. + * + * @return void + */ + public function build() + { + $statement = $this->statement; + foreach ($statement::$CLAUSES as $clause) { + $name = $clause[0]; + $type = $clause[1]; + + if (empty(Parser::$KEYWORD_PARSERS[$name])) { + continue; + } + + $class = Parser::$KEYWORD_PARSERS[$name]['class']; + $field = Parser::$KEYWORD_PARSERS[$name]['field']; + + if (empty($statement->$field)) { + continue; + } + + if ($type & 2) { + $this->query .= $name . ' '; + } + + if ($type & 1) { + $this->query .= $class::build($statement->$field) . ' '; + } + } + } +} diff --git a/src/Fragment.php b/src/Fragment.php index a9a349a..6ae85da 100644 --- a/src/Fragment.php +++ b/src/Fragment.php @@ -38,4 +38,15 @@ abstract class Fragment return null; } + /** + * Builds the string representation of a fragment of this type. + * + * @param Fragment $fragment The fragment to be built. + * + * @return string + */ + public static function build(Fragment $fragment) + { + return null; + } } diff --git a/src/Fragments/ArrayFragment.php b/src/Fragments/ArrayFragment.php index 7ed22ad..db12622 100644 --- a/src/Fragments/ArrayFragment.php +++ b/src/Fragments/ArrayFragment.php @@ -26,6 +26,13 @@ class ArrayFragment extends Fragment { /** + * The array that contains the unprocessed value of each token. + * + * @var array + */ + public $raw = array(); + + /** * The array that contains the processed value of each token. * * @var array @@ -33,11 +40,16 @@ class ArrayFragment extends Fragment public $values = array(); /** - * The array that contains the unprocessed value of each token. + * Constructor. * - * @var array + * @param array $raw The unprocessed values. + * @param array $values The processed values. */ - public $raw = array(); + public function __construct(array $raw = array(), array $values = array()) + { + $this->raw = $raw; + $this->values = $values; + } /** * @param Parser $parser The parser that serves as context. @@ -67,7 +79,6 @@ class ArrayFragment extends Fragment $state = 0; for (; $list->idx < $list->count; ++$list->idx) { - /** * Token parsed at this moment. * @var Token @@ -114,4 +125,22 @@ class ArrayFragment extends Fragment return $ret; } + + /** + * @param ArrayFragment $fragment The fragment to be built. + * + * @return string + */ + public static function build(ArrayFragment $fragment) + { + $values = array(); + if (!empty($fragment->raw)) { + $values = $fragment->raw; + } else { + foreach ($fragment->values as $value) { + $values[] = $value; + } + } + return '(' . implode(', ', $values) . ')'; + } } diff --git a/src/Fragments/CallKeyword.php b/src/Fragments/CallKeyword.php index ae3c467..3b8cc80 100644 --- a/src/Fragments/CallKeyword.php +++ b/src/Fragments/CallKeyword.php @@ -35,9 +35,25 @@ class CallKeyword extends Fragment /** * The list of parameters * - * @var array + * @var ArrayFragment */ - public $parameters = array(); + public $parameters; + + /** + * Constructor. + * + * @param string $name The name of the function to be called. + * @param array|ArrayFragment $parameters The parameters of this function. + */ + public function __construct($name = null, $parameters = null) + { + $this->name = $name; + if (is_array($parameters)) { + $this->parameters = new ArrayFragment($parameters); + } elseif ($parameters instanceof ArrayFragment) { + $this->parameters = $parameters; + } + } /** * @param Parser $parser The parser that serves as context. @@ -64,7 +80,6 @@ class CallKeyword extends Fragment $state = 0; for (; $list->idx < $list->count; ++$list->idx) { - /** * Token parsed at this moment. * @var Token @@ -86,7 +101,7 @@ class CallKeyword extends Fragment $state = 1; } elseif ($state === 1) { if (($token->type === Token::TYPE_OPERATOR) && ($token->value === '(')) { - $ret->parameters = ArrayFragment::parse($parser, $list)->values; + $ret->parameters = ArrayFragment::parse($parser, $list); } break; } @@ -95,4 +110,14 @@ class CallKeyword extends Fragment return $ret; } + + /** + * @param CallKeyword $fragment The fragment to be built. + * + * @return string + */ + public static function build(CallKeyword $fragment) + { + return $fragment->name . ArrayFragment::build($fragment->parameters); + } } diff --git a/src/Fragments/CreateDefFragment.php b/src/Fragments/CreateDefFragment.php index 403426c..933ea5e 100644 --- a/src/Fragments/CreateDefFragment.php +++ b/src/Fragments/CreateDefFragment.php @@ -92,7 +92,6 @@ class CreateDefFragment extends Fragment $ret = new CreateDefFragment(); for (; $list->idx < $list->count; ++$list->idx) { - /** * Token parsed at this moment. * @var Token diff --git a/src/Fragments/DataTypeFragment.php b/src/Fragments/DataTypeFragment.php index 2ad700a..0b9e1a8 100644 --- a/src/Fragments/DataTypeFragment.php +++ b/src/Fragments/DataTypeFragment.php @@ -94,7 +94,6 @@ class DataTypeFragment extends Fragment $state = 0; for (; $list->idx < $list->count; ++$list->idx) { - /** * Token parsed at this moment. * @var Token diff --git a/src/Fragments/FieldDefFragment.php b/src/Fragments/FieldDefFragment.php index d38eeda..6c5771e 100644 --- a/src/Fragments/FieldDefFragment.php +++ b/src/Fragments/FieldDefFragment.php @@ -129,7 +129,6 @@ class FieldDefFragment extends Fragment $state = 0; for (; $list->idx < $list->count; ++$list->idx) { - /** * Token parsed at this moment. * @var Token @@ -176,7 +175,7 @@ class FieldDefFragment extends Fragment --$list->idx; } $state = 5; - } else if ($state === 5) { + } elseif ($state === 5) { if ((!empty($expr->type)) || (!empty($expr->key))) { $ret[] = $expr; } diff --git a/src/Fragments/FieldFragment.php b/src/Fragments/FieldFragment.php index 5140324..8b0f0ef 100644 --- a/src/Fragments/FieldFragment.php +++ b/src/Fragments/FieldFragment.php @@ -75,6 +75,38 @@ class FieldFragment extends Fragment public $subquery; /** + * Constructor. + * + * Syntax: + * new FieldFragment('expr') + * new FieldFragment('expr', 'alias') + * new FieldFragment('database', 'table', 'column') + * new FieldFragment('database', 'table', 'column', 'alias') + * + * If the database, table or column name is not required, pass an empty + * string. + * + * @param string $database The name of the database or the the expression. + * the the expression. + * @param string $table The name of the table or the alias of the expression. + * the alias of the expression. + * @param string $column The name of the column. + * @param string $alias The name of the alias. + */ + public function __construct($database = null, $table = null, $column = null, $alias = null) + { + if (($column === null) && ($alias === null)) { + $this->expr = $database; // case 1 + $this->alias = $table; // case 2 + } else { + $this->database = $database; // case 3 + $this->table = $table; // case 3 + $this->column = $column; // case 3 + $this->alias = $alias; // case 4 + } + } + + /** * @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. @@ -120,7 +152,6 @@ class FieldFragment extends Fragment $prev = null; for (; $list->idx < $list->count; ++$list->idx) { - /** * Token parsed at this moment. * @var Token @@ -150,8 +181,10 @@ class FieldFragment extends Fragment $alias = 2; continue; } - break; - } else if ($prev === true) { + if (!($token->flags & Token::FLAG_KEYWORD_FUNCTION)) { + break; + } + } elseif ($prev === true) { if ((empty($ret->subquery) && (!empty(Parser::$STATEMENT_PARSERS[$token->value])))) { // A `(` was previously found and this keyword is the // beginning of a statement, so this is a subquery. @@ -236,7 +269,7 @@ class FieldFragment extends Fragment if (($token->type === Token::TYPE_KEYWORD) && ($token->flags & Token::FLAG_KEYWORD_FUNCTION)) { $prev = strtoupper($token->value); - } else if (($token->type === Token::TYPE_OPERATOR) || ($token->value === '(')) { + } elseif (($token->type === Token::TYPE_OPERATOR) || ($token->value === '(')) { $prev = true; } else { $prev = null; @@ -258,4 +291,36 @@ class FieldFragment extends Fragment --$list->idx; return $ret; } + + /** + * @param FieldFragment $fragment The fragment to be built. + * + * @return string + */ + public static function build($fragment) + { + $ret = ''; + + if (!empty($fragment->expr)) { + $ret = $fragment->expr; + } else { + $fields = array(); + if (!empty($fragment->database)) { + $fields[] = $fragment->database; + } + if (!empty($fragment->table)) { + $fields[] = $fragment->table; + } + if (!empty($fragment->column)) { + $fields[] = $fragment->column; + } + $ret = implode('.', $fields); + } + + if (!empty($fragment->alias)) { + $ret .= ' AS ' . $fragment->alias; + } + + return $ret; + } } diff --git a/src/Fragments/SelectKeyword.php b/src/Fragments/FieldListFragment.php index 5cf4719..cc769e0 100644 --- a/src/Fragments/SelectKeyword.php +++ b/src/Fragments/FieldListFragment.php @@ -1,7 +1,7 @@ <?php /** - * `SELECT` keyword parser. + * Parses a a list of fields delimited by a single comma. * * @package SqlParser * @subpackage Fragments @@ -14,7 +14,7 @@ use SqlParser\Token; use SqlParser\TokensList; /** - * `SELECT` keyword parser. + * Parses a a list of fields delimited by a single comma. * * @category Keywords * @package SqlParser @@ -22,7 +22,7 @@ use SqlParser\TokensList; * @author Dan Ungureanu <udan1107@gmail.com> * @license http://opensource.org/licenses/GPL-2.0 GNU Public License */ -class SelectKeyword extends Fragment +class FieldListFragment extends Fragment { /** @@ -39,7 +39,6 @@ class SelectKeyword extends Fragment $expr = null; for (; $list->idx < $list->count; ++$list->idx) { - /** * Token parsed at this moment. * @var Token @@ -64,7 +63,7 @@ class SelectKeyword extends Fragment if (($token->type === Token::TYPE_OPERATOR) && ($token->value === ',')) { $ret[] = $expr; } else { - $expr = FieldFragment::parse($parser, $list); + $expr = FieldFragment::parse($parser, $list, $options); if ($expr === null) { break; } @@ -80,4 +79,13 @@ class SelectKeyword extends Fragment --$list->idx; return $ret; } + + public static function build($fragment) + { + $ret = array(); + foreach ($fragment as $frag) { + $ret[] = $frag::build($frag); + } + return implode($ret, ', '); + } } diff --git a/src/Fragments/FromKeyword.php b/src/Fragments/FromKeyword.php deleted file mode 100644 index 6fbf0ba..0000000 --- a/src/Fragments/FromKeyword.php +++ /dev/null @@ -1,83 +0,0 @@ -<?php - -/** - * `FROM` keyword parser. - * - * @package SqlParser - * @subpackage Fragments - */ -namespace SqlParser\Fragments; - -use SqlParser\Fragment; -use SqlParser\Parser; -use SqlParser\Token; -use SqlParser\TokensList; - -/** - * `FROM` keyword parser. - * - * @category Keywords - * @package SqlParser - * @subpackage Fragments - * @author Dan Ungureanu <udan1107@gmail.com> - * @license http://opensource.org/licenses/GPL-2.0 GNU Public License - */ -class FromKeyword extends Fragment -{ - - /** - * @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 FieldFragment[] - */ - public static function parse(Parser $parser, TokensList $list, array $options = array()) - { - $ret = array(); - - $expr = new FieldFragment(); - - for (; $list->idx < $list->count; ++$list->idx) { - - /** - * Token parsed at this moment. - * @var Token - */ - $token = $list->tokens[$list->idx]; - - // End of statement. - if ($token->type === Token::TYPE_DELIMITER) { - break; - } - - // Skipping whitespaces and comments. - if (($token->type === Token::TYPE_WHITESPACE) || ($token->type === Token::TYPE_COMMENT)) { - continue; - } - - // No keyword is expected. - if (($token->type === Token::TYPE_KEYWORD) && ($token->flags & Token::FLAG_KEYWORD_RESERVED)) { - break; - } - - if (($token->type === Token::TYPE_OPERATOR) && ($token->value === ',')) { - $ret[] = $expr; - } else { - $expr = FieldFragment::parse($parser, $list, array('skipColumn' => true)); - if ($expr === null) { - break; - } - } - - } - - // Last iteration was not saved. - if ($expr !== null) { - $ret[] = $expr; - } - - --$list->idx; - return $ret; - } -} diff --git a/src/Fragments/IntoKeyword.php b/src/Fragments/IntoKeyword.php index 558b3e6..0b51531 100644 --- a/src/Fragments/IntoKeyword.php +++ b/src/Fragments/IntoKeyword.php @@ -74,7 +74,6 @@ class IntoKeyword extends Fragment $state = 0; for (; $list->idx < $list->count; ++$list->idx) { - /** * Token parsed at this moment. * @var Token @@ -105,13 +104,13 @@ class IntoKeyword extends Fragment if ($state === 0) { $ret->name = $token->value; $state = 1; - } else if ($state === 1) { + } elseif ($state === 1) { if (($token->type === Token::TYPE_OPERATOR) && ($token->value === '(')) { $ret->fields = ArrayFragment::parse($parser, $list)->values; ++$list->idx; } break; - } else if ($state === 2) { + } elseif ($state === 2) { $ret->name = $token->value; ++$list->idx; break; diff --git a/src/Fragments/JoinKeyword.php b/src/Fragments/JoinKeyword.php index 011bd4d..f8e1a57 100644 --- a/src/Fragments/JoinKeyword.php +++ b/src/Fragments/JoinKeyword.php @@ -66,7 +66,6 @@ class JoinKeyword extends Fragment $state = 0; for (; $list->idx < $list->count; ++$list->idx) { - /** * Token parsed at this moment. * @var Token diff --git a/src/Fragments/KeyFragment.php b/src/Fragments/KeyFragment.php index 4a71857..d84c9f6 100644 --- a/src/Fragments/KeyFragment.php +++ b/src/Fragments/KeyFragment.php @@ -95,7 +95,6 @@ class KeyFragment extends Fragment $state = 0; for (; $list->idx < $list->count; ++$list->idx) { - /** * Token parsed at this moment. * @var Token diff --git a/src/Fragments/LimitKeyword.php b/src/Fragments/LimitKeyword.php index a23eb4c..af8d92d 100644 --- a/src/Fragments/LimitKeyword.php +++ b/src/Fragments/LimitKeyword.php @@ -40,6 +40,18 @@ class LimitKeyword extends Fragment public $rowCount; /** + * Constructor. + * + * @param int $rowCount The row count. + * @param int $offset The offset. + */ + public function __construct($rowCount = null, $offset = null) + { + $this->rowCount = $rowCount; + $this->offset = $offset; + } + + /** * @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. @@ -53,7 +65,6 @@ class LimitKeyword extends Fragment $offset = false; for (; $list->idx < $list->count; ++$list->idx) { - /** * Token parsed at this moment. * @var Token @@ -103,4 +114,18 @@ class LimitKeyword extends Fragment --$list->idx; return $ret; } + + /** + * @param LimitKeyword $fragment The fragment to be built. + * + * @return string + */ + public static function build($fragment) + { + if (empty($fragment->offset)) { + return $fragment->rowCount; + } else { + return $fragment->offset . ', ' . $fragment->rowCount; + } + } } diff --git a/src/Fragments/OptionsFragment.php b/src/Fragments/OptionsFragment.php index 54d4857..16459cc 100644 --- a/src/Fragments/OptionsFragment.php +++ b/src/Fragments/OptionsFragment.php @@ -33,6 +33,17 @@ class OptionsFragment extends Fragment public $options = array(); /** + * Constructor. + * + * @param array $options The array of options. Options that have a value + * must be an array with two keys 'name' and 'value'. + */ + public function __construct(array $options = array()) + { + $this->options = $options; + } + + /** * @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. @@ -59,7 +70,6 @@ class OptionsFragment extends Fragment $brackets = 0; for (; $list->idx < $list->count; ++$list->idx) { - /** * Token parsed at this moment. * @var Token @@ -128,6 +138,24 @@ class OptionsFragment extends Fragment } /** + * @param OptionsFragment $fragment The fragment to be built. + * + * @return string + */ + public static function build(OptionsFragment $fragment) + { + $options = array(); + foreach ($fragment->options as $option) { + if (is_array($option)) { + $options[] = $option['name'] . '=' . $option['value']; + } else { + $options[] = $option; + } + } + return implode(' ', $options); + } + + /** * Checks if it has the specified option and returns it value or true. * * @param string $key The key to be checked. @@ -158,7 +186,7 @@ class OptionsFragment extends Fragment { if (is_array($options)) { $this->options = array_merge_recursive($this->options, $options); - } else if ($options instanceof OptionsFragment) { + } elseif ($options instanceof OptionsFragment) { $this->options = array_merge_recursive($this->options, $options->options); } } diff --git a/src/Fragments/OrderKeyword.php b/src/Fragments/OrderKeyword.php index 5194208..7a76084 100644 --- a/src/Fragments/OrderKeyword.php +++ b/src/Fragments/OrderKeyword.php @@ -67,7 +67,6 @@ class OrderKeyword extends Fragment $state = 0; for (; $list->idx < $list->count; ++$list->idx) { - /** * Token parsed at this moment. * @var Token @@ -90,7 +89,7 @@ class OrderKeyword extends Fragment } elseif ($state === 1) { if (($token->type === Token::TYPE_KEYWORD) && (($token->value === 'ASC') || ($token->value === 'DESC'))) { $expr->type = $token->value; - } else if (($token->type === Token::TYPE_OPERATOR) && ($token->value === ',')) { + } elseif (($token->type === Token::TYPE_OPERATOR) && ($token->value === ',')) { if (!empty($expr->field)) { $ret[] = $expr; } diff --git a/src/Fragments/ParamDefFragment.php b/src/Fragments/ParamDefFragment.php index a512596..583c3ab 100644 --- a/src/Fragments/ParamDefFragment.php +++ b/src/Fragments/ParamDefFragment.php @@ -80,7 +80,6 @@ class ParamDefFragment extends Fragment $state = 0; for (; $list->idx < $list->count; ++$list->idx) { - /** * Token parsed at this moment. * @var Token diff --git a/src/Fragments/ReferencesKeyword.php b/src/Fragments/ReferencesKeyword.php index d4afd01..8de0233 100644 --- a/src/Fragments/ReferencesKeyword.php +++ b/src/Fragments/ReferencesKeyword.php @@ -84,7 +84,6 @@ class ReferencesKeyword extends Fragment $state = 0; for (; $list->idx < $list->count; ++$list->idx) { - /** * Token parsed at this moment. * @var Token @@ -104,10 +103,10 @@ class ReferencesKeyword extends Fragment if ($state === 0) { $ret->table = $token->value; $state = 1; - } else if ($state === 1) { + } elseif ($state === 1) { $ret->columns = ArrayFragment::parse($parser, $list)->values; $state = 2; - } else if ($state === 2) { + } elseif ($state === 2) { $ret->options = OptionsFragment::parse($parser, $list, static::$REFERENCES_OPTIONS); ++$list->idx; break; diff --git a/src/Fragments/RenameKeyword.php b/src/Fragments/RenameKeyword.php index 980ad7b..077982a 100644 --- a/src/Fragments/RenameKeyword.php +++ b/src/Fragments/RenameKeyword.php @@ -71,7 +71,6 @@ class RenameKeyword extends Fragment $state = 0; for (; $list->idx < $list->count; ++$list->idx) { - /** * Token parsed at this moment. * @var Token diff --git a/src/Fragments/SetKeyword.php b/src/Fragments/SetKeyword.php index 547768c..bced45f 100644 --- a/src/Fragments/SetKeyword.php +++ b/src/Fragments/SetKeyword.php @@ -67,7 +67,6 @@ class SetKeyword extends Fragment $state = 0; for (; $list->idx < $list->count; ++$list->idx) { - /** * Token parsed at this moment. * @var Token diff --git a/src/Fragments/ValuesKeyword.php b/src/Fragments/ValuesKeyword.php index 6977f68..c867551 100644 --- a/src/Fragments/ValuesKeyword.php +++ b/src/Fragments/ValuesKeyword.php @@ -65,7 +65,6 @@ class ValuesKeyword extends Fragment $state = 0; for (; $list->idx < $list->count; ++$list->idx) { - /** * Token parsed at this moment. * @var Token diff --git a/src/Fragments/WhereKeyword.php b/src/Fragments/WhereKeyword.php index 1b36cc8..d2143f3 100644 --- a/src/Fragments/WhereKeyword.php +++ b/src/Fragments/WhereKeyword.php @@ -47,6 +47,16 @@ class WhereKeyword extends Fragment public $condition; /** + * Constructor. + * + * @param string $condition The condition or the operator. + */ + public function __construct($condition = null) + { + $this->condition = trim($condition); + } + + /** * @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. @@ -57,10 +67,13 @@ class WhereKeyword extends Fragment { $ret = array(); - $expr = new WhereKeyword(); + /** + * The condition that was parsed so far. + * @var string + */ + $condition = ''; for (; $list->idx < $list->count; ++$list->idx) { - /** * Token parsed at this moment. * @var Token @@ -73,23 +86,23 @@ class WhereKeyword extends Fragment } // Skipping whitespaces and comments. - if (($token->type === Token::TYPE_WHITESPACE) || ($token->type === Token::TYPE_COMMENT)) { + if ($token->type === Token::TYPE_COMMENT) { continue; } // Conditions are delimited by logical operators. if (in_array($token->value, static::$OPERATORS, true)) { - if (!empty($expr->condition)) { - $ret[] = $expr; + if (!empty(trim($condition))) { + // Adding the condition that is delimited by this operator. + $ret[] = new WhereKeyword($condition); + $condition = ''; } - $expr = new WhereKeyword(); + // Adding the operator. + $expr = new WhereKeyword($token->value); $expr->isOperator = true; - $expr->condition = $token->value; $ret[] = $expr; - $expr = new WhereKeyword(); - continue; } @@ -98,16 +111,30 @@ class WhereKeyword extends Fragment break; } - $expr->condition .= $token->token; + $condition .= $token->token; } // Last iteration was not processed. - if (!empty($expr->condition)) { - $ret[] = $expr; + if (!empty(trim($condition))) { + $ret[] = new WhereKeyword($condition); } --$list->idx; return $ret; } + + /** + * @param WhereKeyword $fragment The fragment to be built. + * + * @return string + */ + public static function build($fragment) + { + $conditions = array(); + foreach ($fragment as $f) { + $conditions[] = $f->condition; + } + return implode(' ', $conditions); + } } diff --git a/src/Lexer.php b/src/Lexer.php index b91db8b..0d0862d 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -168,7 +168,6 @@ class Lexer $lastToken = null; for ($this->last = 0, $lastIdx = 0; $this->last < $this->len; $lastIdx = ++$this->last) { - /** * The new token. * @var Token diff --git a/src/Parser.php b/src/Parser.php index 2a9bd47..29e6701 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -9,6 +9,7 @@ */ namespace SqlParser; +use SqlParser\Statements\SelectStatement; use SqlParser\Exceptions\ParserException; /** @@ -37,7 +38,7 @@ class Parser 'ANALYZE' => 'SqlParser\\Statements\\AnalyzeStatement', 'BACKUP' => 'SqlParser\\Statements\\BackupStatement', 'CHECK' => 'SqlParser\\Statements\\CheckStatement', - 'CHECKSUM' => 'SqlParser\\Statements\\ChecsumStatement', + 'CHECKSUM' => 'SqlParser\\Statements\\ChecksumStatement', 'OPTIMIZE' => 'SqlParser\\Statements\\OptimizeStatement', 'REPAIR' => 'SqlParser\\Statements\\RepairStatement', 'RESTORE' => 'SqlParser\\Statements\\RestoreStatement', @@ -79,38 +80,51 @@ class Parser * @var array */ public static $KEYWORD_PARSERS = array( + + // This is not a proper keyword and was added here to help the builder. + '_OPTIONS' => array( + 'class' => 'SqlParser\\Fragments\\OptionsFragment', + 'field' => 'options', + ), + 'ALTER' => array( 'class' => 'SqlParser\\Fragments\\FieldFragment', 'field' => 'table', 'options' => array('skipColumn' => true), ), 'ANALYZE' => array( - 'class' => 'SqlParser\\Fragments\\FromKeyword', + 'class' => 'SqlParser\\Fragments\\FieldListFragment', 'field' => 'tables', + 'options' => array('skipColumn' => true), ), 'BACKUP' => array( - 'class' => 'SqlParser\\Fragments\\FromKeyword', + 'class' => 'SqlParser\\Fragments\\FieldListFragment', 'field' => 'tables', + 'options' => array('skipColumn' => true), ), 'CALL' => array( 'class' => 'SqlParser\\Fragments\\CallKeyword', 'field' => 'call', ), 'CHECK' => array( - 'class' => 'SqlParser\\Fragments\\FromKeyword', + 'class' => 'SqlParser\\Fragments\\FieldListFragment', 'field' => 'tables', + 'options' => array('skipColumn' => true), ), 'CHECKSUM' => array( - 'class' => 'SqlParser\\Fragments\\FromKeyword', + 'class' => 'SqlParser\\Fragments\\FieldListFragment', 'field' => 'tables', + 'options' => array('skipColumn' => true), ), 'DROP' => array( - 'class' => 'SqlParser\\Fragments\\FromKeyword', - 'field' => 'fields' + 'class' => 'SqlParser\\Fragments\\FieldListFragment', + 'field' => 'fields', + 'options' => array('skipColumn' => true), ), 'FROM' => array( - 'class' => 'SqlParser\\Fragments\\FromKeyword', + 'class' => 'SqlParser\\Fragments\\FieldListFragment', 'field' => 'from', + 'options' => array('skipColumn' => true), ), 'GROUP BY' => array( 'class' => 'SqlParser\\Fragments\\OrderKeyword', @@ -133,8 +147,9 @@ class Parser 'field' => 'limit', ), 'OPTIMIZE' => array( - 'class' => 'SqlParser\\Fragments\\FromKeyword', + 'class' => 'SqlParser\\Fragments\\FieldListFragment', 'field' => 'tables', + 'options' => array('skipColumn' => true), ), 'ORDER BY' => array( 'class' => 'SqlParser\\Fragments\\OrderKeyword', @@ -153,24 +168,27 @@ class Parser 'field' => 'renames', ), 'REPAIR' => array( - 'class' => 'SqlParser\\Fragments\\FromKeyword', + 'class' => 'SqlParser\\Fragments\\FieldListFragment', 'field' => 'tables', + 'options' => array('skipColumn' => true), ), 'RESTORE' => array( - 'class' => 'SqlParser\\Fragments\\FromKeyword', + 'class' => 'SqlParser\\Fragments\\FieldListFragment', 'field' => 'tables', + 'options' => array('skipColumn' => true), ), 'SET' => array( 'class' => 'SqlParser\\Fragments\\SetKeyword', 'field' => 'set', ), 'SELECT' => array( - 'class' => 'SqlParser\\Fragments\\SelectKeyword', + 'class' => 'SqlParser\\Fragments\\FieldListFragment', 'field' => 'expr', ), 'UPDATE' => array( - 'class' => 'SqlParser\\Fragments\\FromKeyword', + 'class' => 'SqlParser\\Fragments\\FieldListFragment', 'field' => 'from', + 'options' => array('skipColumn' => true), ), 'VALUE' => array( 'class' => 'SqlParser\\Fragments\\ValuesKeyword', @@ -252,6 +270,29 @@ class Parser */ public function parse() { + + /** + * Last parsed statement. + * @var Statement + */ + $lastStatement = null; + + /** + * Whether a union is parsed or not. + * @var bool + */ + $inUnion = true; + + /** + * The index of the last token from the last statement. + * @var int + */ + $prevLastIdx = -1; + + /** + * The list of tokens. + * @var TokensList + */ $list = &$this->list; for (; $list->idx < $list->count; ++$list->idx) { @@ -268,13 +309,21 @@ class Parser continue; } + if ($token->value === 'UNION') { + $inUnion = true; + continue; + } + // Checking if it is a known statement that can be parsed. if (empty(static::$STATEMENT_PARSERS[$token->value])) { $this->error( 'Unrecognized statement type "' . $token->value . '".', $token ); + // Skipping to the end of this statement. $list->getNextOfType(Token::TYPE_DELIMITER); + // + $prevLastIdx = $list->idx; continue; } @@ -290,8 +339,32 @@ class Parser */ $stmt = new $class(); + // The first token that is a part of this token is the next token + // unprocessed by the previous statement. + // There might be brackets around statements and this shouldn't + // affect the parser + $stmt->first = $prevLastIdx + 1; + + // Parsing the actual statement. $stmt->parse($this, $this->list); - $this->statements[] = $stmt; + + // Storing the index of the last token parsed and updating the old + // index. + $stmt->last = $list->idx; + $prevLastIdx = $list->idx; + + // Finally, storing the statement. + if (($inUnion) + && ($lastStatement instanceof SelectStatement) + && ($stmt instanceof SelectStatement) + ) { + $lastStatement->union[] = $stmt; + $inUnion = false; + } else { + $this->statements[] = $stmt; + $lastStatement = $stmt; + } + } } diff --git a/src/Statement.php b/src/Statement.php index f10b5ee..0cb362c 100644 --- a/src/Statement.php +++ b/src/Statement.php @@ -27,6 +27,20 @@ abstract class Statement { /** + * The clauses of this statement, in order. + * + * The value attributed to each clause is used by the builder and it may + * have one of the following values: + * + * - 1 = 01 - add the clause only + * - 2 = 10 - add the keyword + * - 3 = 11 - add both the keyword and the clause + * + * @var array + */ + public static $CLAUSES; + + /** * The options of this query. * * @var OptionsFragment @@ -59,8 +73,6 @@ abstract class Statement */ public function parse(Parser $parser, TokensList $list) { - $this->first = $list->idx; - /** * Whether options were parsed or not. * For statements that do not have any options this is set to `true` by @@ -70,7 +82,6 @@ abstract class Statement $parsedOptions = isset(static::$OPTIONS) ? false : true; for (; $list->idx < $list->count; ++$list->idx) { - /** * Token parsed at this moment. * @var Token @@ -88,6 +99,12 @@ abstract class Statement continue; } + // Unions are parsed by the parser because they represent more than + // one statement. + if ($token->value === 'UNION') { + break; + } + /** * The name of the class that is used for parsing. * @var string @@ -124,7 +141,7 @@ abstract class Statement ); $parsedOptions = true; } - } else if ($class === null) { + } elseif ($class === null) { // There is no parser for this keyword and isn't the beggining // of a statement (so no options) either. $parser->error( @@ -145,11 +162,11 @@ abstract class Statement $this->after($parser, $list, $token); } - $this->last = $list->idx--; // Go back to last used token. + --$list->idx; // Go back to last used token. } /** - * Function called before the token was processed. + * Function called before the token is processed. * * @param Parser $parser The instance that requests parsing. * @param TokensList $list The list of tokens to be parsed. @@ -175,5 +192,4 @@ abstract class Statement { } - } diff --git a/src/Statements/AlterStatement.php b/src/Statements/AlterStatement.php index b6c18ed..81bca05 100644 --- a/src/Statements/AlterStatement.php +++ b/src/Statements/AlterStatement.php @@ -97,6 +97,8 @@ class AlterStatement extends NotImplementedStatement ); /** + * Function called after the token was processed. + * * Extracts the name of affected column. * * @param Parser $parser The instance that requests parsing. @@ -124,5 +126,4 @@ class AlterStatement extends NotImplementedStatement // parent::after($parser, $list, $token); } - } diff --git a/src/Statements/ChecsumStatement.php b/src/Statements/ChecksumStatement.php index e2c6371..e2c6371 100644 --- a/src/Statements/ChecsumStatement.php +++ b/src/Statements/ChecksumStatement.php diff --git a/src/Statements/CreateStatement.php b/src/Statements/CreateStatement.php index ef11537..f13866a 100644 --- a/src/Statements/CreateStatement.php +++ b/src/Statements/CreateStatement.php @@ -100,6 +100,8 @@ class CreateStatement extends Statement public $body; /** + * Function called before the token is processed. + * * Parsing the `CREATE` statement. * * @param Parser $parser The instance that requests parsing. diff --git a/src/Statements/MaintenanceStatement.php b/src/Statements/MaintenanceStatement.php index a0df634..97c87b9 100644 --- a/src/Statements/MaintenanceStatement.php +++ b/src/Statements/MaintenanceStatement.php @@ -37,6 +37,8 @@ class MaintenanceStatement extends Statement public $tables; /** + * Function called after the token was processed. + * * Parses the additional options fragment at the end. * * @param Parser $parser The instance that requests parsing. diff --git a/src/Statements/NotImplementedStatement.php b/src/Statements/NotImplementedStatement.php index 1689df2..b96c558 100644 --- a/src/Statements/NotImplementedStatement.php +++ b/src/Statements/NotImplementedStatement.php @@ -28,6 +28,8 @@ class NotImplementedStatement extends Statement { /** + * Function called after the token was processed. + * * Jump to the end of the delimiter. * * @param Parser $parser The instance that requests parsing. diff --git a/src/Statements/RenameStatement.php b/src/Statements/RenameStatement.php index 5ebd2fa..473efe0 100644 --- a/src/Statements/RenameStatement.php +++ b/src/Statements/RenameStatement.php @@ -36,6 +36,8 @@ class RenameStatement extends Statement public $renames; /** + * Function called before the token is processed. + * * Skips the `TABLE` keyword after `RENAME`. * * @param Parser $parser The instance that requests parsing. diff --git a/src/Statements/SelectStatement.php b/src/Statements/SelectStatement.php index 0c4c354..7704e26 100644 --- a/src/Statements/SelectStatement.php +++ b/src/Statements/SelectStatement.php @@ -68,17 +68,29 @@ class SelectStatement extends Statement ); /** - * The sections of this statement, in order. + * The clauses of this statement, in order. * - * Used by the query builder to arrange the clauses. + * @see Statement::$CLAUSES * * @var array */ - public static $SECTIONS = array( - 'SELECT' => 0, '%OPTIONS' => 1,'FROM' => 2, 'PARTITION' => 3, - 'WHERE' => 4, 'GROUP BY' => 5, 'HAVING' => 6, 'ORDER BY' => 7, - 'LIMIT' => 8, 'PROCEDURE' => 9, 'INTO' => 10, 'UNION' => 11, - 'JOIN' => 12, '%OPTIONS' => 13 + public static $CLAUSES = array( + 'SELECT' => array('SELECT', 2), + // Used for options. + '_OPTIONS' => array('_OPTIONS', 1), + // Used for select expressions. + '_SELECT' => array('SELECT', 1), + 'FROM' => array('FROM', 3), + 'PARTITION' => array('PARTITION', 3), + 'WHERE' => array('WHERE', 3), + 'GROUP BY' => array('GROUP BY', 3), + 'HAVING' => array('HAVING', 3), + 'ORDER BY' => array('ORDER BY', 3), + 'LIMIT' => array('LIMIT', 3), + 'PROCEDURE' => array('PROCEDURE', 3), + 'INTO' => array('INTO', 3), + 'UNION' => array('UNION', 3), + 'JOIN' => array('JOIN', 3), ); /** @@ -86,33 +98,33 @@ class SelectStatement extends Statement * * @var FieldFragment[] */ - public $expr; + public $expr = array(); /** * Tables used as sources for this statement. * * @var FieldFragment[] */ - public $from; + public $from = array(); /** * Partitions used as source for this statement. * - * @var ArrayFragment[] + * @var ArrayFragment */ public $partition; /** * Conditions used for filtering each row of the result set. * - * @var WhereKeyword + * @var WhereKeyword[] */ public $where; /** * Conditions used for grouping the result set. * - * @var GroupKeyword + * @var OrderKeyword[] */ public $group; @@ -157,4 +169,11 @@ class SelectStatement extends Statement * @var JoinKeyword */ public $join; + + /** + * Unions. + * + * @var SelectStatement[] + */ + public $union = array(); } diff --git a/src/Token.php b/src/Token.php index fa23d28..1db0d1d 100644 --- a/src/Token.php +++ b/src/Token.php @@ -217,52 +217,52 @@ class Token public function extract() { switch ($this->type) { - case Token::TYPE_KEYWORD: - if (!($this->flags & Token::FLAG_KEYWORD_RESERVED)) { - // Unreserved keywords should stay the way they are because they - // might represent field names. - return $this->token; - } - return strtoupper($this->token); - case Token::TYPE_WHITESPACE: - return ' '; - case Token::TYPE_BOOL: - return strtoupper($this->token) === 'TRUE'; - case Token::TYPE_NUMBER: - $ret = str_replace('--', '', $this->token); // e.g. ---42 === -42 - if ($this->flags & Token::FLAG_NUMBER_HEX) { - if ($this->flags & Token::FLAG_NUMBER_NEGATIVE) { - $ret = str_replace('-', '', $this->token); - sscanf($ret, "%x", $ret); - $ret = -$ret; - } else { - sscanf($ret, "%x", $ret); + case Token::TYPE_KEYWORD: + if (!($this->flags & Token::FLAG_KEYWORD_RESERVED)) { + // Unreserved keywords should stay the way they are because they + // might represent field names. + return $this->token; } - } elseif (($this->flags & Token::FLAG_NUMBER_APPROXIMATE) + return strtoupper($this->token); + case Token::TYPE_WHITESPACE: + return ' '; + case Token::TYPE_BOOL: + return strtoupper($this->token) === 'TRUE'; + case Token::TYPE_NUMBER: + $ret = str_replace('--', '', $this->token); // e.g. ---42 === -42 + if ($this->flags & Token::FLAG_NUMBER_HEX) { + if ($this->flags & Token::FLAG_NUMBER_NEGATIVE) { + $ret = str_replace('-', '', $this->token); + sscanf($ret, "%x", $ret); + $ret = -$ret; + } else { + sscanf($ret, "%x", $ret); + } + } elseif (($this->flags & Token::FLAG_NUMBER_APPROXIMATE) || ($this->flags & Token::FLAG_NUMBER_FLOAT) - ) { - sscanf($ret, "%f", $ret); - } else { - sscanf($ret, "%d", $ret); - } - return $ret; - case Token::TYPE_STRING: - $quote = $this->token[0]; - $str = str_replace($quote . $quote, $quote, $this->token); - return mb_substr($str, 1, -1); // trims quotes - case Token::TYPE_SYMBOL: - $str = $this->token; - if ((isset($str[0])) && ($str[0] === '@')) { - $str = mb_substr($str, 1); - } - if ((isset($str[0])) && (($str[0] === '`') + ) { + sscanf($ret, "%f", $ret); + } else { + sscanf($ret, "%d", $ret); + } + return $ret; + case Token::TYPE_STRING: + $quote = $this->token[0]; + $str = str_replace($quote . $quote, $quote, $this->token); + return mb_substr($str, 1, -1); // trims quotes + case Token::TYPE_SYMBOL: + $str = $this->token; + if ((isset($str[0])) && ($str[0] === '@')) { + $str = mb_substr($str, 1); + } + if ((isset($str[0])) && (($str[0] === '`') || ($str[0] === '"') || ($str[0] === '\'')) - ) { - $quote = $str[0]; - $str = str_replace($quote . $quote, $quote, $str); - $str = mb_substr($str, 1, -1); - } - return $str; + ) { + $quote = $str[0]; + $str = str_replace($quote . $quote, $quote, $str); + $str = mb_substr($str, 1, -1); + } + return $str; } return $this->token; } diff --git a/src/Utils/Query.php b/src/Utils/Query.php index dde3304..f973744 100644 --- a/src/Utils/Query.php +++ b/src/Utils/Query.php @@ -222,32 +222,32 @@ class Query if ($statement instanceof AlterStatement) { $flags['querytype'] = 'ALTER'; $flags['reload'] = true; - } else if ($statement instanceof CreateStatement) { + } elseif ($statement instanceof CreateStatement) { $flags['querytype'] = 'CREATE'; $flags['reload'] = true; - } else if ($statement instanceof AnalyzeStatement) { + } elseif ($statement instanceof AnalyzeStatement) { $flags['querytype'] = 'ANALYZE'; $flags['is_maint'] = true; - } else if ($statement instanceof CheckStatement) { + } elseif ($statement instanceof CheckStatement) { $flags['querytype'] = 'CHECK'; $flags['is_maint'] = true; - } else if ($statement instanceof ChecksumStatement) { + } elseif ($statement instanceof ChecksumStatement) { $flags['querytype'] = 'CHECKSUM'; $flags['is_maint'] = true; - } else if ($statement instanceof OptimizeStatement) { + } elseif ($statement instanceof OptimizeStatement) { $flags['querytype'] = 'OPTIMIZE'; $flags['is_maint'] = true; - } else if ($statement instanceof RepairStatement) { + } elseif ($statement instanceof RepairStatement) { $flags['querytype'] = 'REPAIR'; $flags['is_maint'] = true; - } else if ($statement instanceof CallStatement) { + } elseif ($statement instanceof CallStatement) { $flags['querytype'] = 'CALL'; $flags['is_procedure'] = true; - } else if ($statement instanceof DeleteStatement) { + } elseif ($statement instanceof DeleteStatement) { $flags['querytype'] = 'DELETE'; $flags['is_delete'] = true; $flags['is_affected'] = true; - } else if ($statement instanceof DropStatement) { + } elseif ($statement instanceof DropStatement) { $flags['querytype'] = 'DROP'; $flags['reload'] = true; @@ -256,19 +256,19 @@ class Query ) { $flags['drop_database'] = true; } - } else if ($statement instanceof ExplainStatement) { + } elseif ($statement instanceof ExplainStatement) { $flags['querytype'] = 'EXPLAIN'; $flags['is_explain'] = true; - } else if ($statement instanceof InsertStatement) { + } elseif ($statement instanceof InsertStatement) { $flags['querytype'] = 'INSERT'; $flags['is_affected'] = true; $flags['is_insert'] = true; - } else if ($statement instanceof ReplaceStatement) { + } elseif ($statement instanceof ReplaceStatement) { $flags['querytype'] = 'REPLACE'; $flags['is_affected'] = true; $flags['is_replace'] = true; $flags['is_insert'] = true; - } else if ($statement instanceof SelectStatement) { + } elseif ($statement instanceof SelectStatement) { $flags['querytype'] = 'SELECT'; $flags['is_select'] = true; @@ -294,7 +294,7 @@ class Query if (!empty($expr->function)) { if ($expr->function === 'COUNT') { $flags['is_count'] = true; - } else if (in_array($expr->function, static::$FUNCTIONS)) { + } elseif (in_array($expr->function, static::$FUNCTIONS)) { $flags['is_func'] = true; } } @@ -325,10 +325,10 @@ class Query $flags['join'] = true; } - } else if ($statement instanceof ShowStatement) { + } elseif ($statement instanceof ShowStatement) { $flags['querytype'] = 'SHOW'; $flags['is_show'] = true; - } else if ($statement instanceof UpdateStatement) { + } elseif ($statement instanceof UpdateStatement) { $flags['querytype'] = 'UPDATE'; $flags['is_affected'] = true; } @@ -409,7 +409,7 @@ class Query /** * Gets the type of clause. * - * @param string $clause The clause. + * @param string $clause The clause. * * @return string */ @@ -450,7 +450,7 @@ class Query { /** - * Current location. + * The index of the current clause. * @var int */ $currIdx = 0; @@ -469,19 +469,20 @@ class Query $ret = ''; /** - * The place where the clause should be added. + * The clauses of this type of statement and their index. + * @var array + */ + $clauses = array_flip(array_keys($statement::$CLAUSES)); + + /** + * The index of this clause. * @var int */ - $clauseIdx = $statement::$SECTIONS[static::getClauseType($clause)]; + $clauseIdx = $clauses[static::getClauseType($clause)]; for ($i = $statement->first; $i <= $statement->last; ++$i) { - $token = $list->tokens[$i]; - if ($token->type === Token::TYPE_DELIMITER) { - break; - } - if ($token->type === Token::TYPE_OPERATOR) { if ($token->value === '(') { ++$brackets; @@ -493,9 +494,9 @@ class Query if ($brackets == 0) { // Checking if we changed sections. if ($token->type === Token::TYPE_KEYWORD) { - if (isset($statement::$SECTIONS[$token->value])) { - if ($statement::$SECTIONS[$token->value] >= $currIdx) { - $currIdx = $statement::$SECTIONS[$token->value]; + if (isset($clauses[$token->value])) { + if ($clauses[$token->value] >= $currIdx) { + $currIdx = $clauses[$token->value]; if (($skipFirst) && ($currIdx == $clauseIdx)) { // This token is skipped (not added to the old // clause) because it will be replaced. @@ -546,5 +547,4 @@ class Query $clause . ' ' . static::getClause($statement, $list, $clause, 1, false); } - } diff --git a/src/Utils/Table.php b/src/Utils/Table.php index 95b6850..86ccbfb 100644 --- a/src/Utils/Table.php +++ b/src/Utils/Table.php @@ -43,7 +43,6 @@ class Table $ret = array(); foreach ($statement->fields as $field) { - if ((empty($field->key)) || ($field->key->type !== 'FOREIGN KEY')) { continue; } @@ -93,7 +92,6 @@ class Table $ret = array(); foreach ($statement->fields as $field) { - // Skipping keys. if (empty($field->type)) { continue; @@ -105,7 +103,6 @@ class Table ); if ($field->options) { - if ($field->type->name === 'TIMESTAMP') { if ($field->options->has('NOT NULL')) { $ret[$field->name]['timestamp_not_null'] = true; |