/** * @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 LimitKeyword */ public static function parse(Parser $parser, TokensList $list, array $options = array()) { $ret = new LimitKeyword(); $offset = false; 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; } if ($token->type === Token::TYPE_KEYWORD && $token->flags & Token::FLAG_KEYWORD_RESERVED) { break; } if ($token->type === Token::TYPE_KEYWORD && $token->value === 'OFFSET') { if ($offset) { $parser->error('An offset was expected.'); } $offset = true; continue; } if ($token->type === Token::TYPE_OPERATOR && $token->value === ',') { $ret->offset = $ret->rowCount; $ret->rowCount = 0; continue; } if ($offset) { $ret->offset = $token->value; $offset = false; } else { $ret->rowCount = $token->value; } } if ($offset) { $parser->error('An offset was expected.'); } --$list->idx; return $ret; }
/** * Possible options: * * `field` * * First field to be filled. * If this is not specified, it takes the value of `parseField`. * * `parseField` * * Specifies the type of the field parsed. It may be `database`, * `table` or `column`. These expressions may not include * parentheses. * * `breakOnAlias` * * If not empty, breaks when the alias occurs (it is not included). * * `breakOnParentheses` * * If not empty, breaks when the first parentheses occurs. * * `parenthesesDelimited` * * If not empty, breaks after last parentheses occurred. * * @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 Expression */ public static function parse(Parser $parser, TokensList $list, array $options = array()) { $ret = new Expression(); /** * Whether current tokens make an expression or a table reference. * * @var bool $isExpr */ $isExpr = false; /** * Whether a period was previously found. * * @var bool $dot */ $dot = false; /** * Whether an alias is expected. Is 2 if `AS` keyword was found. * * @var bool $alias */ $alias = false; /** * Counts brackets. * * @var int $brackets */ $brackets = 0; /** * Keeps track of the last two previous tokens. * * @var Token[] $prev */ $prev = array(null, null); // When a field is parsed, no parentheses are expected. if (!empty($options['parseField'])) { $options['breakOnParentheses'] = true; $options['field'] = $options['parseField']; } for (; $list->idx < $list->count; ++$list->idx) { /** * Token parsed at this moment. * * @var Token $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) { if ($isExpr) { $ret->expr .= $token->token; } continue; } if ($token->type === Token::TYPE_KEYWORD) { if ($brackets > 0 && 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. $ret->subquery = $token->value; } elseif ($token->flags & Token::FLAG_KEYWORD_FUNCTION && (empty($options['parseField']) && !$alias)) { $isExpr = true; } elseif ($token->flags & Token::FLAG_KEYWORD_RESERVED && $brackets === 0) { if (empty(self::$ALLOWED_KEYWORDS[$token->value])) { // A reserved keyword that is not allowed in the // expression was found so the expression must have // ended and a new clause is starting. break; } if ($token->value === 'AS') { if (!empty($options['breakOnAlias'])) { break; } if ($alias) { $parser->error(__('An alias was expected.'), $token); break; } $alias = true; continue; } elseif ($token->value === 'CASE') { // For a use of CASE like // 'SELECT a = CASE .... END, b=1, `id`, ... FROM ...' $tempCaseExpr = CaseExpression::parse($parser, $list); $ret->expr .= CaseExpression::build($tempCaseExpr); $isExpr = true; continue; } $isExpr = true; } elseif ($brackets === 0 && count($ret->expr) > 0 && !$alias) { /* End of expression */ break; } } if ($token->type === Token::TYPE_NUMBER || $token->type === Token::TYPE_BOOL || $token->type === Token::TYPE_SYMBOL && $token->flags & Token::FLAG_SYMBOL_VARIABLE || $token->type === Token::TYPE_OPERATOR && $token->value !== '.') { if (!empty($options['parseField'])) { break; } // Numbers, booleans and operators (except dot) are usually part // of expressions. $isExpr = true; } if ($token->type === Token::TYPE_OPERATOR) { if (!empty($options['breakOnParentheses']) && ($token->value === '(' || $token->value === ')')) { // No brackets were expected. break; } if ($token->value === '(') { ++$brackets; if (empty($ret->function) && $prev[1] !== null && ($prev[1]->type === Token::TYPE_NONE || $prev[1]->type === Token::TYPE_SYMBOL || $prev[1]->type === Token::TYPE_KEYWORD && $prev[1]->flags & Token::FLAG_KEYWORD_FUNCTION)) { $ret->function = $prev[1]->value; } } elseif ($token->value === ')' && $brackets == 0) { // Not our bracket break; } elseif ($token->value === ')') { --$brackets; if ($brackets === 0) { if (!empty($options['parenthesesDelimited'])) { // The current token is the last bracket, the next // one will be outside the expression. $ret->expr .= $token->token; ++$list->idx; break; } } elseif ($brackets < 0) { // $parser->error(__('Unexpected closing bracket.'), $token); // $brackets = 0; break; } } elseif ($token->value === ',') { // Expressions are comma-delimited. if ($brackets === 0) { break; } } } // Saving the previous tokens. $prev[0] = $prev[1]; $prev[1] = $token; if ($alias) { // An alias is expected (the keyword `AS` was previously found). if (!empty($ret->alias)) { $parser->error(__('An alias was previously found.'), $token); break; } $ret->alias = $token->value; $alias = false; } elseif ($isExpr) { // Handling aliases. if ($brackets === 0 && ($prev[0] === null || ($prev[0]->type !== Token::TYPE_OPERATOR || $prev[0]->token === ')') && ($prev[0]->type !== Token::TYPE_KEYWORD || !($prev[0]->flags & Token::FLAG_KEYWORD_RESERVED))) && ($prev[1]->type === Token::TYPE_STRING || $prev[1]->type === Token::TYPE_SYMBOL && !($prev[1]->flags & Token::FLAG_SYMBOL_VARIABLE) || $prev[1]->type === Token::TYPE_NONE)) { if (!empty($ret->alias)) { $parser->error(__('An alias was previously found.'), $token); break; } $ret->alias = $prev[1]->value; } else { $ret->expr .= $token->token; } } elseif (!$isExpr) { if ($token->type === Token::TYPE_OPERATOR && $token->value === '.') { // Found a `.` which means we expect a column name and // the column name we parsed is actually the table name // and the table name is actually a database name. if (!empty($ret->database) || $dot) { $parser->error(__('Unexpected dot.'), $token); } $ret->database = $ret->table; $ret->table = $ret->column; $ret->column = null; $dot = true; $ret->expr .= $token->token; } else { $field = empty($options['field']) ? 'column' : $options['field']; if (empty($ret->{$field})) { $ret->{$field} = $token->value; $ret->expr .= $token->token; $dot = false; } else { // No alias is expected. if (!empty($options['breakOnAlias'])) { break; } if (!empty($ret->alias)) { $parser->error(__('An alias was previously found.'), $token); break; } $ret->alias = $token->value; } } } } if ($alias) { $parser->error(__('An alias was expected.'), $list->tokens[$list->idx - 1]); } // White-spaces might be added at the end. $ret->expr = trim($ret->expr); if ($ret->expr === '') { return null; } --$list->idx; return $ret; }
/** * @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 Expression */ public static function parse(Parser $parser, TokensList $list, array $options = array()) { $ret = new Expression(); /** * Whether current tokens make an expression or a table reference. * * @var bool $isExpr */ $isExpr = false; /** * Whether a period was previously found. * * @var bool $dot */ $dot = false; /** * Whether an alias is expected. Is 2 if `AS` keyword was found. * * @var int $alias */ $alias = 0; /** * Counts brackets. * * @var int $brackets */ $brackets = 0; /** * Keeps track of the previous token. * Possible values: * string, if function was previously found; * true, if opening bracket was previously found; * null, in any other case. * * @var string|bool $prev */ $prev = null; for (; $list->idx < $list->count; ++$list->idx) { /** * Token parsed at this moment. * * @var Token $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) { if ($isExpr && !$alias) { $ret->expr .= $token->token; } if ($alias === 0 && empty($options['noAlias']) && !$isExpr && !$dot && !empty($ret->expr)) { $alias = 1; } continue; } 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') { $alias = 2; continue; } 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. $ret->subquery = $token->value; } } } if ($token->type === Token::TYPE_OPERATOR) { if (!empty($options['noBrackets']) && ($token->value === '(' || $token->value === ')')) { break; } if ($token->value === '(') { ++$brackets; if (empty($ret->function) && $prev !== null && $prev !== true) { // A function name was previously found and now an open // bracket, so this is a function call. $ret->function = $prev; } $isExpr = true; } elseif ($token->value === ')') { --$brackets; if ($brackets === 0) { if (!empty($options['bracketsDelimited'])) { // The current token is the last brackets, the next // one will be outside. $ret->expr .= $token->token; ++$list->idx; break; } } elseif ($brackets < 0) { // $parser->error(__('Unexpected closing bracket.'), $token); // $brackets = 0; break; } } elseif ($token->value === ',') { if ($brackets === 0) { break; } } } if ($token->type === Token::TYPE_NUMBER || $token->type === Token::TYPE_BOOL || $token->type === Token::TYPE_SYMBOL && $token->flags & Token::FLAG_SYMBOL_VARIABLE || $token->type === Token::TYPE_OPERATOR && $token->value !== '.') { // Numbers, booleans and operators are usually part of expressions. $isExpr = true; } if ($alias) { // An alias is expected (the keyword `AS` was previously found). if (!empty($ret->alias)) { $parser->error(__('An alias was previously found.'), $token); } $ret->alias = $token->value; $alias = 0; } else { if (!$isExpr) { if ($token->type === Token::TYPE_OPERATOR && $token->value === '.') { // Found a `.` which means we expect a column name and // the column name we parsed is actually the table name // and the table name is actually a database name. if (!empty($ret->database) || $dot) { $parser->error(__('Unexpected dot.'), $token); } $ret->database = $ret->table; $ret->table = $ret->column; $ret->column = null; $dot = true; } else { // We found the name of a column (or table if column // field should be skipped; used to parse table names). $field = !empty($options['skipColumn']) ? 'table' : 'column'; if (!empty($ret->{$field})) { // No alias is expected. if (!empty($options['noAlias'])) { break; } // Parsing aliases without `AS` keyword and any // whitespace. // Example: SELECT 1`foo` if ($token->type === Token::TYPE_STRING || $token->type === Token::TYPE_SYMBOL && $token->flags & Token::FLAG_SYMBOL_BACKTICK) { if (!empty($ret->alias)) { $parser->error(__('An alias was previously found.'), $token); } $ret->alias = $token->value; } } else { $ret->{$field} = $token->value; } $dot = false; } } else { // Parsing aliases without `AS` keyword. // Example: SELECT 'foo' `bar` if ($brackets === 0 && empty($options['noAlias'])) { if ($token->type === Token::TYPE_NONE || $token->type === Token::TYPE_STRING || $token->type === Token::TYPE_SYMBOL && $token->flags & Token::FLAG_SYMBOL_BACKTICK) { if (!empty($ret->alias)) { $parser->error(__('An alias was previously found.'), $token); } $ret->alias = $token->value; continue; } } } $ret->expr .= $token->token; } if ($token->type === Token::TYPE_KEYWORD && $token->flags & Token::FLAG_KEYWORD_FUNCTION) { $prev = strtoupper($token->value); } elseif ($token->type === Token::TYPE_OPERATOR || $token->value === '(') { $prev = true; } else { $prev = null; } } if ($alias === 2) { $parser->error(__('An alias was expected.'), $list->tokens[$list->idx - 1]); } // Whitespaces might be added at the end. $ret->expr = trim($ret->expr); if (empty($ret->expr)) { return null; } --$list->idx; return $ret; }
/** * @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()) { $ret = empty($options['type']) ? new ArrayObj() : array(); /** * The state of the parser. * * Below are the states of the parser. * * 0 -----------------------[ ( ]------------------------> 1 * * 1 ------------------[ array element ]-----------------> 2 * * 2 ------------------------[ , ]-----------------------> 1 * 2 ------------------------[ ) ]-----------------------> (END) * * @var int $state */ $state = 0; for (; $list->idx < $list->count; ++$list->idx) { /** * Token parsed at this moment. * * @var Token $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; } if ($state === 0) { if ($token->type !== Token::TYPE_OPERATOR || $token->value !== '(') { $parser->error(__('An opening bracket was expected.'), $token); break; } $state = 1; } elseif ($state === 1) { if ($token->type === Token::TYPE_OPERATOR && $token->value === ')') { // Empty array. break; } if (empty($options['type'])) { $ret->values[] = $token->value; $ret->raw[] = $token->token; } else { $ret[] = $options['type']::parse($parser, $list, empty($options['typeOptions']) ? array() : $options['typeOptions']); } $state = 2; } elseif ($state === 2) { if ($token->type !== Token::TYPE_OPERATOR || $token->value !== ',' && $token->value !== ')') { $parser->error(__('A comma or a closing bracket was expected'), $token); break; } if ($token->value === ',') { $state = 1; } else { // ) break; } } } return $ret; }
/** * @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 DataTypeFragment[] */ public static function parse(Parser $parser, TokensList $list, array $options = array()) { $ret = new DataTypeFragment(); /** * The state of the parser. * * Below are the states of the parser. * * 0 -------------------[ data type ]--------------------> 1 * * 1 ----------------[ size and options ]----------------> 2 * * @var int */ $state = 0; for (; $list->idx < $list->count; ++$list->idx) { /** * Token parsed at this moment. * @var Token */ $token = $list->tokens[$list->idx]; // Skipping whitespaces and comments. if ($token->type === Token::TYPE_WHITESPACE || $token->type === Token::TYPE_COMMENT) { continue; } if ($state === 0) { $ret->name = strtoupper($token->value); if ($token->type !== Token::TYPE_KEYWORD || !($token->flags & Token::FLAG_KEYWORD_DATA_TYPE)) { $parser->error('Unrecognized data type.', $token); } $state = 1; } elseif ($state === 1) { if ($token->type === Token::TYPE_OPERATOR && $token->value === '(') { $size = ArrayFragment::parse($parser, $list); ++$list->idx; $ret->parameters = $ret->name === 'ENUM' || $ret->name === 'SET' ? $size->raw : $size->values; } $ret->options = OptionsFragment::parse($parser, $list, static::$DATA_TYPE_OPTIONS); ++$list->idx; break; } } if (empty($ret->name)) { return null; } --$list->idx; return $ret; }
/** * Parses the statements defined by the tokens list. * * @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) { /** * 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; /** * Whether options were parsed or not. * For statements that do not have any options this is set to `true` by * default. * @var bool $parsedOptions */ $parsedOptions = empty(static::$OPTIONS); for (; $list->idx < $list->count; ++$list->idx) { /** * Token parsed at this moment. * @var Token $token */ $token = $list->tokens[$list->idx]; // End of statement. if ($token->type === Token::TYPE_DELIMITER) { break; } // Only keywords are relevant here. Other parts of the query are // processed in the functions below. if ($token->type !== Token::TYPE_KEYWORD) { if ($token->type !== TOKEN::TYPE_COMMENT && $token->type !== Token::TYPE_WHITESPACE) { $parser->error(__('Unexpected token.'), $token); } 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 $class */ $class = null; /** * The name of the field where the result of the parsing is stored. * @var string $field */ $field = null; /** * Parser's options. * @var array $options */ $options = array(); if (!empty(Parser::$KEYWORD_PARSERS[$token->value])) { $class = Parser::$KEYWORD_PARSERS[$token->value]['class']; $field = Parser::$KEYWORD_PARSERS[$token->value]['field']; if (!empty(Parser::$KEYWORD_PARSERS[$token->value]['options'])) { $options = Parser::$KEYWORD_PARSERS[$token->value]['options']; } } 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) { if (empty(static::$OPTIONS[$token->value])) { // Skipping keyword because if it is not a option. ++$list->idx; } $this->options = OptionsArray::parse($parser, $list, static::$OPTIONS); $parsedOptions = true; } } elseif ($class === null) { // There is no parser for this keyword and isn't the beginning // of a statement (so no options) either. $parser->error(__('Unrecognized keyword.'), $token); continue; } $this->before($parser, $list, $token); // Parsing this keyword. if ($class !== null) { ++$list->idx; // Skipping keyword or last option. $this->{$field} = $class::parse($parser, $list, $options); } $this->after($parser, $list, $token); } // This may be corrected by the parser. $this->last = --$list->idx; // Go back to last used token. }
/** * @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 CreateDefinition[] */ public static function parse(Parser $parser, TokensList $list, array $options = array()) { $ret = array(); $expr = new CreateDefinition(); /** * The state of the parser. * * Below are the states of the parser. * * 0 -----------------------[ ( ]------------------------> 1 * * 1 --------------------[ CONSTRAINT ]------------------> 1 * 1 -----------------------[ key ]----------------------> 2 * 1 -------------[ constraint / column name ]-----------> 2 * * 2 --------------------[ data type ]-------------------> 3 * * 3 ---------------------[ options ]--------------------> 4 * * 4 --------------------[ REFERENCES ]------------------> 4 * * 5 ------------------------[ , ]-----------------------> 1 * 5 ------------------------[ ) ]-----------------------> 6 (-1) * * @var int $state */ $state = 0; for (; $list->idx < $list->count; ++$list->idx) { /** * Token parsed at this moment. * * @var Token $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; } if ($state === 0) { if ($token->type === Token::TYPE_OPERATOR && $token->value === '(') { $state = 1; } else { $parser->error(__('An opening bracket was expected.'), $token); break; } } elseif ($state === 1) { if ($token->type === Token::TYPE_KEYWORD && $token->value === 'CONSTRAINT') { $expr->isConstraint = true; } elseif ($token->type === Token::TYPE_KEYWORD && $token->flags & Token::FLAG_KEYWORD_KEY) { $expr->key = Key::parse($parser, $list); $state = 4; } else { $expr->name = $token->value; if (!$expr->isConstraint) { $state = 2; } } } elseif ($state === 2) { $expr->type = DataType::parse($parser, $list); $state = 3; } elseif ($state === 3) { $expr->options = OptionsArray::parse($parser, $list, static::$FIELD_OPTIONS); $state = 4; } elseif ($state === 4) { if ($token->type === Token::TYPE_KEYWORD && $token->value === 'REFERENCES') { ++$list->idx; // Skipping keyword 'REFERENCES'. $expr->references = Reference::parse($parser, $list); } else { --$list->idx; } $state = 5; } elseif ($state === 5) { if (!empty($expr->type) || !empty($expr->key)) { $ret[] = $expr; } $expr = new CreateDefinition(); if ($token->value === ',') { $state = 1; } elseif ($token->value === ')') { $state = 6; ++$list->idx; break; } else { $parser->error(__('A comma or a closing bracket was expected.'), $token); $state = 0; break; } } } // Last iteration was not saved. if (!empty($expr->type) || !empty($expr->key)) { $ret[] = $expr; } if ($state !== 0 && $state !== 6) { $parser->error(__('A closing bracket was expected.'), $list->tokens[$list->idx - 1]); } --$list->idx; return $ret; }
/** * @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 ArrayObj */ public static function parse(Parser $parser, TokensList $list, array $options = array()) { $ret = new ArrayObj(); /** * The state of the parser. * * Below are the states of the parser. * * 0 -----------------------[ ( ]------------------------> 1 * * 1 ------------------[ array element ]-----------------> 2 * * 2 ------------------------[ , ]-----------------------> 1 * 2 ------------------------[ ) ]-----------------------> -1 * * @var int */ $state = 0; for (; $list->idx < $list->count; ++$list->idx) { /** * Token parsed at this moment. * @var Token $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; } if ($state === 0) { if ($token->type !== Token::TYPE_OPERATOR || $token->value !== '(') { $parser->error('An open bracket was expected.', $token); break; } $state = 1; } elseif ($state === 1) { if ($token->type === Token::TYPE_OPERATOR && $token->value === ')') { // Empty array. break; } $ret->values[] = $token->value; $ret->raw[] = $token->token; $state = 2; } elseif ($state === 2) { if ($token->type !== Token::TYPE_OPERATOR || $token->value !== ',' && $token->value !== ')') { $parser->error('Symbols \')\' or \',\' were expected', $token); break; } if ($token->value === ',') { $state = 1; } else { // ) break; } } } return $ret; }
/** * @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 OptionsArray */ public static function parse(Parser $parser, TokensList $list, array $options = array()) { $ret = new OptionsArray(); /** * The ID that will be assigned to duplicate options. * * @var int $lastAssignedId */ $lastAssignedId = count($options) + 1; /** * The option that was processed last time. * * @var array $lastOption */ $lastOption = null; /** * The index of the option that was processed last time. * * @var int $lastOptionId */ $lastOptionId = 0; /** * Counts brackets. * * @var int $brackets */ $brackets = 0; /** * The state of the parser. * * Below are the states of the parser. * * 0 ---------------------[ option ]----------------------> 1 * * 1 -------------------[ = (optional) ]------------------> 2 * * 2 ----------------------[ value ]----------------------> 0 * * @var int $state */ $state = 0; for (; $list->idx < $list->count; ++$list->idx) { /** * Token parsed at this moment. * * @var Token $token */ $token = $list->tokens[$list->idx]; // End of statement. if ($token->type === Token::TYPE_DELIMITER) { break; } // Skipping comments. if ($token->type === Token::TYPE_COMMENT) { continue; } // Skipping whitespace if not parsing value. if ($token->type === Token::TYPE_WHITESPACE && $brackets === 0) { continue; } if ($lastOption === null) { $upper = strtoupper($token->token); if (isset($options[$upper])) { $lastOption = $options[$upper]; $lastOptionId = is_array($lastOption) ? $lastOption[0] : $lastOption; $state = 0; // Checking for option conflicts. // For example, in `SELECT` statements the keywords `ALL` // and `DISTINCT` conflict and if used together, they // produce an invalid query. // // Usually, tokens can be identified in the array by the // option ID, but if conflicts occur, a generated option ID // is used. // // The first pseudo duplicate ID is the maximum value of the // real options (e.g. if there are 5 options, the first // fake ID is 6). if (isset($ret->options[$lastOptionId])) { $parser->error(sprintf(__('This option conflicts with "%1$s".'), is_array($ret->options[$lastOptionId]) ? $ret->options[$lastOptionId]['name'] : $ret->options[$lastOptionId]), $token); $lastOptionId = $lastAssignedId++; } } else { // There is no option to be processed. break; } } if ($state === 0) { if (!is_array($lastOption)) { // This is a just keyword option without any value. // This is the beginning and the end of it. $ret->options[$lastOptionId] = $token->value; $lastOption = null; $state = 0; } elseif ($lastOption[1] === 'var' || $lastOption[1] === 'var=') { // This is a keyword that is followed by a value. // This is only the beginning. The value is parsed in state // 1 and 2. State 1 is used to skip the first equals sign // and state 2 to parse the actual value. $ret->options[$lastOptionId] = array('name' => $token->value, 'equals' => $lastOption[1] === 'var=', 'expr' => '', 'value' => ''); $state = 1; } elseif ($lastOption[1] === 'expr' || $lastOption[1] === 'expr=') { // This is a keyword that is followed by an expression. // The expression is used by the specialized parser. // Skipping this option in order to parse the expression. ++$list->idx; $ret->options[$lastOptionId] = array('name' => $token->value, 'equals' => $lastOption[1] === 'expr=', 'expr' => ''); $state = 1; } } elseif ($state === 1) { $state = 2; if ($token->token === '=') { $ret->options[$lastOptionId]['equals'] = true; continue; } } // This is outside the `elseif` group above because the change might // change this iteration. if ($state === 2) { if ($lastOption[1] === 'expr' || $lastOption[1] === 'expr=') { $ret->options[$lastOptionId]['expr'] = Expression::parse($parser, $list, empty($lastOption[2]) ? array() : $lastOption[2]); $ret->options[$lastOptionId]['value'] = $ret->options[$lastOptionId]['expr']->expr; $lastOption = null; $state = 0; } else { if ($token->token === '(') { ++$brackets; } elseif ($token->token === ')') { --$brackets; } $ret->options[$lastOptionId]['expr'] .= $token->token; if (!($token->token === '(' && $brackets === 1 || $token->token === ')' && $brackets === 0)) { // First pair of brackets is being skipped. $ret->options[$lastOptionId]['value'] .= $token->value; } // Checking if we finished parsing. if ($brackets === 0) { $lastOption = null; } } } } /* * We reached the end of statement without getting a value * for an option for which a value was required */ if ($state === 1 && $lastOption && ($lastOption[1] == 'expr' || $lastOption[1] == 'var' || $lastOption[1] == 'var=' || $lastOption[1] == 'expr=')) { $parser->error(sprintf(__('Value/Expression for the option %1$s was expected'), $ret->options[$lastOptionId]['name']), $list->tokens[$list->idx - 1]); } if (empty($options['_UNSORTED'])) { ksort($ret->options); } --$list->idx; return $ret; }
/** * Parses the statements defined by the tokens list. * * @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) { /** * Array containing all list of clauses parsed. * This is used to check for duplicates. * * @var array $parsedClauses */ $parsedClauses = array(); // This may be corrected by the parser. $this->first = $list->idx; /** * Whether options were parsed or not. * For statements that do not have any options this is set to `true` by * default. * * @var bool $parsedOptions */ $parsedOptions = empty(static::$OPTIONS); for (; $list->idx < $list->count; ++$list->idx) { /** * Token parsed at this moment. * * @var Token $token */ $token = $list->tokens[$list->idx]; // End of statement. if ($token->type === Token::TYPE_DELIMITER) { break; } // Only keywords are relevant here. Other parts of the query are // processed in the functions below. if ($token->type !== Token::TYPE_KEYWORD) { if ($token->type !== TOKEN::TYPE_COMMENT && $token->type !== Token::TYPE_WHITESPACE) { $parser->error(__('Unexpected token.'), $token); } 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 Component $class */ $class = null; /** * The name of the field where the result of the parsing is stored. * * @var string $field */ $field = null; /** * Parser's options. * * @var array $options */ $options = array(); // Looking for duplicated clauses. if (!empty(Parser::$KEYWORD_PARSERS[$token->value]) || !empty(Parser::$STATEMENT_PARSERS[$token->value])) { if (!empty($parsedClauses[$token->value])) { $parser->error(__('This type of clause was previously parsed.'), $token); break; } $parsedClauses[$token->value] = true; } // Checking if this is the beginning of a clause. if (!empty(Parser::$KEYWORD_PARSERS[$token->value])) { $class = Parser::$KEYWORD_PARSERS[$token->value]['class']; $field = Parser::$KEYWORD_PARSERS[$token->value]['field']; if (!empty(Parser::$KEYWORD_PARSERS[$token->value]['options'])) { $options = Parser::$KEYWORD_PARSERS[$token->value]['options']; } } // Checking if this is the beginning of the statement. if (!empty(Parser::$STATEMENT_PARSERS[$token->value])) { if (!empty(static::$CLAUSES) && empty(static::$CLAUSES[$token->value])) { // Some keywords (e.g. `SET`) may be the beginning of a // statement and a clause. // If such keyword was found and it cannot be a clause of // this statement it means it is a new statement, but no // delimiter was found between them. $parser->error(__('A new statement was found, but no delimiter between it and the previous one.'), $token); break; } if (!$parsedOptions) { if (empty(static::$OPTIONS[$token->value])) { // Skipping keyword because if it is not a option. ++$list->idx; } $this->options = OptionsArray::parse($parser, $list, static::$OPTIONS); $parsedOptions = true; } } elseif ($class === null) { // There is no parser for this keyword and isn't the beginning // of a statement (so no options) either. $parser->error(__('Unrecognized keyword.'), $token); continue; } $this->before($parser, $list, $token); // Parsing this keyword. if ($class !== null) { ++$list->idx; // Skipping keyword or last option. $this->{$field} = $class::parse($parser, $list, $options); } $this->after($parser, $list, $token); } // This may be corrected by the parser. $this->last = --$list->idx; // Go back to last used token. }
/** * @expectedException SqlParser\Exceptions\ParserException * @expectedExceptionMessage strict error * @expectedExceptionCode 3 */ public function testErrorStrict() { $parser = new Parser(new TokensList()); $parser->strict = true; $parser->error(__('strict error'), new Token('foo'), 3); }
/** * @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 ArrayObj|Component[] */ public static function parse(Parser $parser, TokensList $list, array $options = array()) { $ret = empty($options['type']) ? new ArrayObj() : array(); /** * The last raw expression. * * @var string $lastRaw */ $lastRaw = ''; /** * The last value. * * @var string $lastValue */ $lastValue = ''; /** * Counts brackets. * * @var int $brackets */ $brackets = 0; /** * Last separator (bracket or comma). * * @var boolean $isCommaLast */ $isCommaLast = false; for (; $list->idx < $list->count; ++$list->idx) { /** * Token parsed at this moment. * * @var Token $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) { $lastRaw .= $token->token; $lastValue = trim($lastValue) . ' '; continue; } if ($brackets === 0 && ($token->type !== Token::TYPE_OPERATOR || $token->value !== '(')) { $parser->error(__('An opening bracket was expected.'), $token); break; } if ($token->type === Token::TYPE_OPERATOR) { if ($token->value === '(') { if (++$brackets === 1) { // 1 is the base level. continue; } } elseif ($token->value === ')') { if (--$brackets === 0) { // Array ended. break; } } elseif ($token->value === ',') { if ($brackets === 1) { $isCommaLast = true; if (empty($options['type'])) { $ret->raw[] = trim($lastRaw); $ret->values[] = trim($lastValue); $lastRaw = $lastValue = ''; } } continue; } } if (empty($options['type'])) { $lastRaw .= $token->token; $lastValue .= $token->value; } else { $ret[] = $options['type']::parse($parser, $list, empty($options['typeOptions']) ? array() : $options['typeOptions']); } } // Handling last element. // // This is treated differently to treat the following cases: // // => array() // (,) => array('', '') // () => array() // (a,) => array('a', '') // (a) => array('a') // $lastRaw = trim($lastRaw); if (empty($options['type']) && (strlen($lastRaw) > 0 || $isCommaLast)) { $ret->raw[] = $lastRaw; $ret->values[] = trim($lastValue); } return $ret; }
/** * @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 OptionsFragment */ public static function parse(Parser $parser, TokensList $list, array $options = array()) { $ret = new OptionsFragment(); /** * The ID that will be assigned to duplicate options. * @var int */ $lastAssignedId = count($options) + 1; /** * The option that was processed last time. * @var array */ $lastOption = null; $lastOptionId = 0; $brackets = 0; 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; } if ($lastOption === null) { if (isset($options[strtoupper($token->value)])) { $lastOption = $options[strtoupper($token->value)]; $lastOptionId = is_array($lastOption) ? $lastOption[0] : $lastOption; // Checking for option conflicts. // For example, in `SELECT` statements the keywords `ALL` and `DISTINCT` // conflict and if used together, they produce an invalid query. // Usually, tokens can be identified in the array by the option ID, // but if conflicts occur, a psuedo option ID is used. // The first pseudo duplicate ID is the maximum value of the real // options (e.g. if there are 5 options, the first fake ID is 6). if (isset($ret->options[$lastOptionId])) { $parser->error('This option conflicts with \'' . $ret->options[$lastOptionId] . '\'.', $token); $lastOptionId = $lastAssignedId++; } } else { // There is no option to be processed. break; } } if (is_array($lastOption)) { if (empty($ret->options[$lastOptionId])) { $ret->options[$lastOptionId] = array('name' => $token->value, 'value' => ''); } else { if ($token->value !== '=') { if ($token->value === '(') { ++$brackets; } elseif ($token->value === ')') { --$brackets; } else { $ret->options[$lastOptionId]['value'] .= $token->value; } if ($brackets === 0) { $lastOption = null; } } } } else { $ret->options[$lastOptionId] = $token->value; $lastOption = null; } } ksort($ret->options); --$list->idx; return $ret; }
/** * Validates the order of the clauses in parsed statement * Ideally this should be called after successfully * completing the parsing of each statement * * @param Parser $parser The instance that requests parsing. * @param TokensList $list The list of tokens to be parsed. * * @return boolean */ public function validateClauseOrder($parser, $list) { $clauses = array_flip(array_keys($this->getClauses())); if (empty($clauses) || count($clauses) == 0) { return true; } $minIdx = -1; /** * For tracking JOIN clauses in a query * 0 - JOIN not found till now * 1 - JOIN has been found * 2 - A Non-JOIN clause has been found * after a previously found JOIN clause * * @var int $joinStart */ $joinStart = 0; $error = 0; foreach ($clauses as $clauseType => $index) { $clauseStartIdx = Utils\Query::getClauseStartOffset($this, $list, $clauseType); // Handle ordering of Multiple Joins in a query if ($clauseStartIdx != -1) { if ($joinStart == 0 && stripos($clauseType, 'JOIN')) { $joinStart = 1; } elseif ($joinStart == 1 && !stripos($clauseType, 'JOIN')) { $joinStart = 2; } elseif ($joinStart == 2 && stripos($clauseType, 'JOIN')) { $error = 1; } } if ($clauseStartIdx != -1 && $clauseStartIdx < $minIdx) { if ($joinStart == 0 || $joinStart == 2 && ($error = 1)) { $token = $list->tokens[$clauseStartIdx]; $parser->error(__('Unexpected ordering of clauses.'), $token); return false; } else { $minIdx = $clauseStartIdx; } } elseif ($clauseStartIdx != -1) { $minIdx = $clauseStartIdx; } } return true; }