@,KlL/.`oIk%_;|G{赉oD@HVsJHjL{q]ۇFvǻ9oN9[0F㝡ZNN%AѢOaهR9/$M 13iHGK_3e*'CA\3uDJkXK:|ROZSB#='O?'):[k/=τX;9aʸ8)5

DTy8-x(\8n`Q=+3eō13õK{q6*[oϔX]S_1ՙxb?Aۊ!>]EK9'}G1`]!R:mpWk3sd--	/DX;co%":a`7"2>1w>F-A%Bxgg/兆͇*3O(%^,n/!FQPT>ds9sD%w"v3ST6\:&ߌ:0y%{MX腭j9.A'<}@<(u}I.f04f-J1@Ò5R<ўa'4yB~/z `axHVF=0M36kWyem/}.Q86]Qe}.KC`wwm6#7::QuoYb<: [Az鐈H×NV:?h%FMƏP ]211!N6<^2~
r5d=_jbdSPȳ1!
  ZdW,cĿGd}Eau_cֱ#h8;FÈQo;CH)~+QA)GK>,D$0nU0 L@b4#T!HuOvUǻU>wx{eYj!tr}$=K2JwTSBiEzf@g@A?D9<p}N
l;(k6
 5x߁٫4O?#7.؄j>p\HG
кC1LKk7>xTiAa&[)Խ9"aV'ħͦ:K~LǱh^ۃߕŖfX]#mNĕ1"nRX01LVJ&}6Gw8sX^!VK$0=^tڀ|5W3K~A6^sƹ`M}yudrt{03*\QTx$ެvtdp.?P7QUy]+m#yWzd)7kuP'j۱{FZyZqk93EI5zlBK<_Ʉ;yK":
D-mB7>ڞkKJ*HScdr~znNqs_V <6(+(:^$spAyVfG!"|2@`8s&7ZٺVVƿ|YVW]ef6"9k_U~R8sX^!VK$Eݴȫ~t*p5Vxģ0v
ӓQv9LLTl> |31Y6@x{1$zUR[֓U}W%oJBJ2<#GQv5%lkXj0;n.@[2LTl> |3߫偎3]^T?-r)΢7ub̠1|CĶk5S,.(}!Ù$OV<{[H-d?!՜庢]Sjޅudy66-VJ쓷?P"
OH/>
@HjY)d7w5DBvqO]̫/eVpNn0!5QY!_U=7|1gV7XHL?P+}rYΐ7aԄÒFt5lqеl2Pf#XsʀQb4Ѐ<`3[`|?	"͙2;o"J_Y>tpq	C,/&2"V 8C	d#-ly{UPc˺ &g#yqK	jZO.(z&!t7+XLS+鱊S	uSxݲ?787wYL[^g~8SCfTV<j@h/+̔-ng805Quʡ;B5ΖNdr[|-Ա&QUP14CN.핾QB5<`LF&@
C{gR%"RԺӺO}vr>#w\S_p١I~A	ibglf=/q!3} G5Ql
LȘB&vBKArˍjwIdRQa]8.a ȉi ~m8FO!
V0!k<vE)qۄes4eU#.{ZxnO5Il-Tfnju[ΊeCb8E荑Ar<RD>spز=ܰ`ի2ݳtjz>wI	РnUX7(^p5MW]hSSo˿8(S^e&YN^B}(_reI7cX"9VyRqف6*ob#r KD6Jn&9MK'mQQ<F+kЦ$h<'M%Ym5|>^:/ 1>}BF ފҤ7f4`ZS?691^TҪ,Vc(rmw$u{&bZnzMcQNG-J1@Ò@4wXƢpĺOjebR9@&2#NCTyNMer=?؂~YJqASnl^ӗwSƩ;EIu!*F2WTwQfRUS1x<$i17[QR\sR)+6P<Pm$̚vͲ7E=PÕQblbpy2dA3_K`Cb5Z4%35BpB;^^#g-1BE@hsv-높 @%)sSOT(OJP C{gR%F&Vd A݋#Eb# aow5nF&xrsUXfOa>+8eoIw,?lhDR(x	$A4Z	G'zuSZ`%c솽q_:?6:-Q1j^P_	/jPCW8Aiz*8+ZD3X\ʀ)n<QOc=0ĘqS?vʀ[&	_3Ϊ-rXDá/Pon|F4p@xôV$fa&bJ6Bgn#8s	sQS\1s ;@P85bᾔn܀D>Qj+NіwH+ꮥ5޼$xq?nrhUK="
s#ChUd?WHHe
     * @param bool             $strict    whether strict mode should be
     *                                    enabled or not
     * @param string           $delimiter the delimiter to be used
     *
     * @return TokensList
     */
    public static function getTokens($str, $strict = false, $delimiter = null)
    {
        $lexer = new self($str, $strict, $delimiter);

        return $lexer->list;
    }

    /**
     * Constructor.
     *
     * @param string|UtfString $str       the query to be lexed
     * @param bool             $strict    whether strict mode should be
     *                                    enabled or not
     * @param string           $delimiter the delimiter to be used
     */
    public function __construct($str, $strict = false, $delimiter = null)
    {
        // `strlen` is used instead of `mb_strlen` because the lexer needs to
        // parse each byte of the input.
        $len = $str instanceof UtfString ? $str->length() : strlen($str);

        // For multi-byte strings, a new instance of `UtfString` is
        // initialized (only if `UtfString` usage is forced.
        if (!$str instanceof UtfString && USE_UTF_STRINGS && $len !== mb_strlen($str, 'UTF-8')) {
            $str = new UtfString($str);
        }

        $this->str = $str;
        $this->len = $str instanceof UtfString ? $str->length() : $len;

        $this->strict = $strict;

        // Setting the delimiter.
        $this->setDelimiter(
            !empty($delimiter) ? $delimiter : static::$DEFAULT_DELIMITER
        );

        $this->lex();
    }

    /**
     * Sets the delimiter.
     *
     * @param string $delimiter the new delimiter
     */
    public function setDelimiter($delimiter)
    {
        $this->delimiter = $delimiter;
        $this->delimiterLen = strlen($delimiter);
    }

    /**
     * Parses the string and extracts lexemes.
     */
    public function lex()
    {
        // TODO: Sometimes, static::parse* functions make unnecessary calls to
        // is* functions. For a better performance, some rules can be deduced
        // from context.
        // For example, in `parseBool` there is no need to compare the token
        // every time with `true` and `false`. The first step would be to
        // compare with 'true' only and just after that add another letter from
        // context and compare again with `false`.
        // Another example is `parseComment`.

        $list = new TokensList();

        /**
         * Last processed token.
         *
         * @var Token
         */
        $lastToken = null;

        for ($this->last = 0, $lastIdx = 0; $this->last < $this->len; $lastIdx = ++$this->last) {
            /**
             * The new token.
             *
             * @var Token
             */
            $token = null;

            foreach (static::$PARSER_METHODS as $method) {
                if ($token = $this->$method()) {
                    break;
                }
            }

            if ($token === null) {
                // @assert($this->last === $lastIdx);
                $token = new Token($this->str[$this->last]);
                $this->error(
                    __('Unexpected character.'),
                    $this->str[$this->last],
                    $this->last
                );
            } elseif ($lastToken !== null
                && $token->type === Token::TYPE_SYMBOL
                && $token->flags & Token::FLAG_SYMBOL_VARIABLE
                && (
                    $lastToken->type === Token::TYPE_STRING
                    || (
                        $lastToken->type === Token::TYPE_SYMBOL
                        && $lastToken->flags & Token::FLAG_SYMBOL_BACKTICK
                    )
                )
            ) {
                // Handles ```... FROM 'user'@'%' ...```.
                $lastToken->token .= $token->token;
                $lastToken->type = Token::TYPE_SYMBOL;
                $lastToken->flags = Token::FLAG_SYMBOL_USER;
                $lastToken->value .= '@' . $token->value;
                continue;
            } elseif ($lastToken !== null
                && $token->type === Token::TYPE_KEYWORD
                && $lastToken->type === Token::TYPE_OPERATOR
                && $lastToken->value === '.'
            ) {
                // Handles ```... tbl.FROM ...```. In this case, FROM is not
                // a reserved word.
                $token->type = Token::TYPE_NONE;
                $token->flags = 0;
                $token->value = $token->token;
            }

            $token->position = $lastIdx;

            $list->tokens[$list->count++] = $token;

            // Handling delimiters.
            if ($token->type === Token::TYPE_NONE && $token->value === 'DELIMITER') {
                if ($this->last + 1 >= $this->len) {
                    $this->error(
                        __('Expected whitespace(s) before delimiter.'),
                        '',
                        $this->last + 1
                    );
                    continue;
                }

                // Skipping last R (from `delimiteR`) and whitespaces between
                // the keyword `DELIMITER` and the actual delimiter.
                $pos = ++$this->last;
                if (($token = $this->parseWhitespace()) !== null) {
                    $token->position = $pos;
                    $list->tokens[$list->count++] = $token;
                }

                // Preparing the token that holds the new delimiter.
                if ($this->last + 1 >= $this->len) {
                    $this->error(
                        __('Expected delimiter.'),
                        '',
                        $this->last + 1
                    );
                    continue;
                }
                $pos = $this->last + 1;

                // Parsing the delimiter.
                $this->delimiter = null;
                while (++$this->last < $this->len && !Context::isWhitespace($this->str[$this->last])) {
                    $this->delimiter .= $this->str[$this->last];
                }

                if (empty($this->delimiter)) {
                    $this->error(
                        __('Expected delimiter.'),
                        '',
                        $this->last
                    );
                    $this->delimiter = ';';
                }

                --$this->last;

                // Saving the delimiter and its token.
                $this->delimiterLen = strlen($this->delimiter);
                $token = new Token($this->delimiter, Token::TYPE_DELIMITER);
                $token->position = $pos;
                $list->tokens[$list->count++] = $token;
            }

            $lastToken = $token;
        }

        // Adding a final delimiter to mark the ending.
        $list->tokens[$list->count++] = new Token(null, Token::TYPE_DELIMITER);

        // Saving the tokens list.
        $this->list = $list;
    }

    /**
     * Creates a new error log.
     *
     * @param string $msg  the error message
     * @param string $str  the character that produced the error
     * @param int    $pos  the position of the character
     * @param int    $code the code of the error
     *
     * @throws LexerException throws the exception, if strict mode is enabled
     */
    public function error($msg = '', $str = '', $pos = 0, $code = 0)
    {
        $error = new LexerException($msg, $str, $pos, $code);
        if ($this->strict) {
            throw $error;
        }
        $this->errors[] = $error;
    }

    /**
     * Parses a keyword.
     *
     * @return Token
     */
    public function parseKeyword()
    {
        $token = '';

        /**
         * Value to be returned.
         *
         * @var Token
         */
        $ret = null;

        /**
         * The value of `$this->last` where `$token` ends in `$this->str`.
         *
         * @var int
         */
        $iEnd = $this->last;

        /**
         * Whether last parsed character is a whitespace.
         *
         * @var bool
         */
        $lastSpace = false;

        for ($j = 1; $j < Context::KEYWORD_MAX_LENGTH && $this->last < $this->len; ++$j, ++$this->last) {
            // Composed keywords shouldn't have more than one whitespace between
            // keywords.
            if (Context::isWhitespace($this->str[$this->last])) {
                if ($lastSpace) {
                    --$j; // The size of the keyword didn't increase.
                    continue;
                } else {
                    $lastSpace = true;
                }
            } else {
                $lastSpace = false;
            }

            $token .= $this->str[$this->last];
            if (($this->last + 1 === $this->len || Context::isSeparator($this->str[$this->last + 1]))
                && $flags = Context::isKeyword($token)
            ) {
                $ret = new Token($token, Token::TYPE_KEYWORD, $flags);
                $iEnd = $this->last;

                // We don't break so we find longest keyword.
                // For example, `OR` and `ORDER` have a common prefix `OR`.
                // If we stopped at `OR`, the parsing would be invalid.
            }
        }

        $this->last = $iEnd;

        return $ret;
    }

    /**
     * Parses a label.
     *
     * @return Token
     */
    public function parseLabel()
    {
        $token = '';

        /**
         * Value to be returned.
         *
         * @var Token
         */
        $ret = null;

        /**
         * The value of `$this->last` where `$token` ends in `$this->str`.
         *
         * @var int
         */
        $iEnd = $this->last;

        /**
         * Whether last parsed character is a whitespace.
         *
         * @var bool
         */
        $lastSpace = false;

        for ($j = 1; $j < Context::LABEL_MAX_LENGTH && $this->last < $this->len; ++$j, ++$this->last) {
            // Composed keywords shouldn't have more than one whitespace between
            // keywords.
            if (Context::isWhitespace($this->str[$this->last])) {
                if ($lastSpace) {
                    --$j; // The size of the keyword didn't increase.
                    continue;
                } else {
                    $lastSpace = true;
                }
            } elseif ($this->str[$this->last] === ':') {
                $token .= $this->str[$this->last];
                $ret = new Token($token, Token::TYPE_LABEL);
                $iEnd = $this->last;
                break;
            } else {
                $lastSpace = false;
            }
            $token .= $this->str[$this->last];
        }

        $this->last = $iEnd;

        return $ret;
    }

    /**
     * Parses an operator.
     *
     * @return Token
     */
    public function parseOperator()
    {
        $token = '';

        /**
         * Value to be returned.
         *
         * @var Token
         */
        $ret = null;

        /**
         * The value of `$this->last` where `$token` ends in `$this->str`.
         *
         * @var int
         */
        $iEnd = $this->last;

        for ($j = 1; $j < Context::OPERATOR_MAX_LENGTH && $this->last < $this->len; ++$j, ++$this->last) {
            $token .= $this->str[$this->last];
            if ($flags = Context::isOperator($token)) {
                $ret = new Token($token, Token::TYPE_OPERATOR, $flags);
                $iEnd = $this->last;
            }
        }

        $this->last = $iEnd;

        return $ret;
    }

    /**
     * Parses a whitespace.
     *
     * @return Token
     */
    public function parseWhitespace()
    {
        $token = $this->str[$this->last];

        if (!Context::isWhitespace($token)) {
            return null;
        }

        while (++$this->last < $this->len && Context::isWhitespace($this->str[$this->last])) {
            $token .= $this->str[$this->last];
        }

        --$this->last;

        return new Token($token, Token::TYPE_WHITESPACE);
    }

    /**
     * Parses a comment.
     *
     * @return Token
     */
    public function parseComment()
    {
        $iBak = $this->last;
        $token = $this->str[$this->last];

        // Bash style comments. (#comment\n)
        if (Context::isComment($token)) {
            while (
                ++$this->last < $this->len
                && $this->str[$this->last] !== "\n"
            ) {
                $token .= $this->str[$this->last];
            }
            return new Token($token, Token::TYPE_COMMENT, Token::FLAG_COMMENT_BASH);
        }

        // C style comments. (/*comment*\/)
        if (++$this->last < $this->len) {
            $token .= $this->str[$this->last];
            if (Context::isComment($token)) {
                $flags = Token::FLAG_COMMENT_C;

                // This comment already ended. It may be a part of a
                // previous MySQL specific command.
                if ($token === '*/') {
                    return new Token($token, Token::TYPE_COMMENT, $flags);
                }

                // Checking if this is a MySQL-specific command.
                if ($this->last + 1 < $this->len
                    && $this->str[$this->last + 1] === '!'
                ) {
                    $flags |= Token::FLAG_COMMENT_MYSQL_CMD;
                    $token .= $this->str[++$this->last];

                    while (
                        ++$this->last < $this->len
                        && '0' <= $this->str[$this->last]
                        && $this->str[$this->last] <= '9'
                    ) {
                        $token .= $this->str[$this->last];
                    }
                    --$this->last;

                    // We split this comment and parse only its beginning
                    // here.
                    return new Token($token, Token::TYPE_COMMENT, $flags);
                }

                // Parsing the comment.
                while (
                    ++$this->last < $this->len
                    && (
                        $this->str[$this->last - 1] !== '*'
                        || $this->str[$this->last] !== '/'
                    )
                ) {
                    $token .= $this->str[$this->last];
                }

                // Adding the ending.
                if ($this->last < $this->len) {
                    $token .= $this->str[$this->last];
                }

                return new Token($token, Token::TYPE_COMMENT, $flags);
            }
        }

        // SQL style comments. (-- comment\n)
        if (++$this->last < $this->len) {
            $token .= $this->str[$this->last];
            if (Context::isComment($token)) {
                // Checking if this comment did not end already (```--\n```).
                if ($this->str[$this->last] !== "\n") {
                    while (
                        ++$this->last < $this->len
                        && $this->str[$this->last] !== "\n"
                    ) {
                        $token .= $this->str[$this->last];
                    }
                }

                return new Token($token, Token::TYPE_COMMENT, Token::FLAG_COMMENT_SQL);
            }
        }

        $this->last = $iBak;

        return null;
    }

    /**
     * Parses a boolean.
     *
     * @return Token
     */
    public function parseBool()
    {
        if ($this->last + 3 >= $this->len) {
            // At least `min(strlen('TRUE'), strlen('FALSE'))` characters are
            // required.
            return null;
        }

        $iBak = $this->last;
        $token = $this->str[$this->last] . $this->str[++$this->last]
        . $this->str[++$this->last] . $this->str[++$this->last]; // _TRUE_ or _FALS_e

        if (Context::isBool($token)) {
            return new Token($token, Token::TYPE_BOOL);
        } elseif (++$this->last < $this->len) {
            $token .= $this->str[$this->last]; // fals_E_
            if (Context::isBool($token)) {
                return new Token($token, Token::TYPE_BOOL, 1);
            }
        }

        $this->last = $iBak;

        return null;
    }

    /**
     * Parses a number.
     *
     * @return Token
     */
    public function parseNumber()
    {
        // A rudimentary state machine is being used to parse numbers due to
        // the various forms of their notation.
        //
        // Below are the states of the machines and the conditions to change
        // the state.
        //
        //      1 --------------------[ + or - ]-------------------> 1
        //      1 -------------------[ 0x or 0X ]------------------> 2
        //      1 --------------------[ 0 to 9 ]-------------------> 3
        //      1 -----------------------[ . ]---------------------> 4
        //      1 -----------------------[ b ]---------------------> 7
        //
        //      2 --------------------[ 0 to F ]-------------------> 2
        //
        //      3 --------------------[ 0 to 9 ]-------------------> 3
        //      3 -----------------------[ . ]---------------------> 4
        //      3 --------------------[ e or E ]-------------------> 5
        //
        //      4 --------------------[ 0 to 9 ]-------------------> 4
        //      4 --------------------[ e or E ]-------------------> 5
        //
        //      5 ---------------[ + or - or 0 to 9 ]--------------> 6
        //
        //      7 -----------------------[ ' ]---------------------> 8
        //
        //      8 --------------------[ 0 or 1 ]-------------------> 8
        //      8 -----------------------[ ' ]---------------------> 9
        //
        // State 1 may be reached by negative numbers.
        // State 2 is reached only by hex numbers.
        // State 4 is reached only by float numbers.
        // State 5 is reached only by numbers in approximate form.
        // State 7 is reached only by numbers in bit representation.
        //
        // Valid final states are: 2, 3, 4 and 6. Any parsing that finished in a
        // state other than these is invalid.
        $iBak = $this->last;
        $token = '';
        $flags = 0;
        $state = 1;
        for (; $this->last < $this->len; ++$this->last) {
            if ($state === 1) {
                if ($this->str[$this->last] === '-') {
                    $flags |= Token::FLAG_NUMBER_NEGATIVE;
                } elseif ($this->last + 1 < $this->len
                    && $this->str[$this->last] === '0'
                    && (
                        $this->str[$this->last + 1] === 'x'
                        || $this->str[$this->last + 1] === 'X'
                    )
                ) {
                    $token .= $this->str[$this->last++];
                    $state = 2;
                } elseif ($this->str[$this->last] >= '0' && $this->str[$this->last] <= '9') {
                    $state = 3;
                } elseif ($this->str[$this->last] === '.') {
                    $state = 4;
                } elseif ($this->str[$this->last] === 'b') {
                    $state = 7;
                } elseif ($this->str[$this->last] !== '+') {
                    // `+` is a valid character in a number.
                    break;
                }
            } elseif ($state === 2) {
                $flags |= Token::FLAG_NUMBER_HEX;
                if (
                    !(
                        ($this->str[$this->last] >= '0' && $this->str[$this->last] <= '9')
                        || ($this->str[$this->last] >= 'A' && $this->str[$this->last] <= 'F')
                        || ($this->str[$this->last] >= 'a' && $this->str[$this->last] <= 'f')
                    )
                ) {
                    break;
                }
            } elseif ($state === 3) {
                if ($this->str[$this->last] === '.') {
                    $state = 4;
                } elseif ($this->str[$this->last] === 'e' || $this->str[$this->last] === 'E') {
                    $state = 5;
                } elseif ($this->str[$this->last] < '0' || $this->str[$this->last] > '9') {
                    // Just digits and `.`, `e` and `E` are valid characters.
                    break;
                }
            } elseif ($state === 4) {
                $flags |= Token::FLAG_NUMBER_FLOAT;
                if ($this->str[$this->last] === 'e' || $this->str[$this->last] === 'E') {
                    $state = 5;
                } elseif ($this->str[$this->last] < '0' || $this->str[$this->last] > '9') {
                    // Just digits, `e` and `E` are valid characters.
                    break;
                }
            } elseif ($state === 5) {
                $flags |= Token::FLAG_NUMBER_APPROXIMATE;
                if ($this->str[$this->last] === '+' || $this->str[$this->last] === '-'
                    || ($this->str[$this->last] >= '0' && $this->str[$this->last] <= '9')
                ) {
                    $state = 6;
                } else {
                    break;
                }
            } elseif ($state === 6) {
                if ($this->str[$this->last] < '0' || $this->str[$this->last] > '9') {
                    // Just digits are valid characters.
                    break;
                }
            } elseif ($state === 7) {
                $flags |= Token::FLAG_NUMBER_BINARY;
                if ($this->str[$this->last] === '\'') {
                    $state = 8;
                } else {
                    break;
                }
            } elseif ($state === 8) {
                if ($this->str[$this->last] === '\'') {
                    $state = 9;
                } elseif ($this->str[$this->last] !== '0'
                    && $this->str[$this->last] !== '1'
                ) {
                    break;
                }
            } elseif ($state === 9) {
                break;
            }
            $token .= $this->str[$this->last];
        }
        if ($state === 2 || $state === 3
            || ($token !== '.' && $state === 4)
            || $state === 6 || $state === 9
        ) {
            --$this->last;

            return new Token($token, Token::TYPE_NUMBER, $flags);
        }
        $this->last = $iBak;

        return null;
    }

    /**
     * Parses a string.
     *
     * @param string $quote additional starting symbol
     *
     * @return Token
     */
    public function parseString($quote = '')
    {
        $token = $this->str[$this->last];
        if (!($flags = Context::isString($token)) && $token !== $quote) {
            return null;
        }
        $quote = $token;

        while (++$this->last < $this->len) {
            if ($this->last + 1 < $this->len
                && (
                    ($this->str[$this->last] === $quote && $this->str[$this->last + 1] === $quote)
                    || ($this->str[$this->last] === '\\' && $quote !== '`')
                )
            ) {
                $token .= $this->str[$this->last] . $this->str[++$this->last];
            } else {
                if ($this->str[$this->last] === $quote) {
                    break;
                }
                $token .= $this->str[$this->last];
            }
        }

        if ($this->last >= $this->len || $this->str[$this->last] !== $quote) {
            $this->error(
                sprintf(
                    __('Ending quote %1$s was expected.'),
                    $quote
                ),
                '',
                $this->last
            );
        } else {
            $token .= $this->str[$this->last];
        }

        return new Token($token, Token::TYPE_STRING, $flags);
    }

    /**
     * Parses a symbol.
     *
     * @return Token
     */
    public function parseSymbol()
    {
        $token = $this->str[$this->last];
        if (!($flags = Context::isSymbol($token))) {
            return null;
        }

        if ($flags & Token::FLAG_SYMBOL_VARIABLE) {
            if ($this->last + 1 < $this->len && $this->str[++$this->last] === '@') {
                // This is a system variable (e.g. `@@hostname`).
                $token .= $this->str[$this->last++];
                $flags |= Token::FLAG_SYMBOL_SYSTEM;
            }
        } else {
            $token = '';
        }

        $str = null;

        if ($this->last < $this->len) {
            if (($str = $this->parseString('`')) === null) {
                if (($str = static::parseUnknown()) === null) {
                    $this->error(
                        __('Variable name was expected.'),
                        $this->str[$this->last],
                        $this->last
                    );
                }
            }
        }

        if ($str !== null) {
            $token .= $str->token;
        }

        return new Token($token, Token::TYPE_SYMBOL, $flags);
    }

    /**
     * Parses unknown parts of the query.
     *
     * @return Token
     */
    public function parseUnknown()
    {
        $token = $this->str[$this->last];
        if (Context::isSeparator($token)) {
            return null;
        }

        while (++$this->last < $this->len && !Context::isSeparator($this->str[$this->last])) {
            $token .= $this->str[$this->last];
        }
        --$this->last;

        return new Token($token);
    }

    /**
     * Parses the delimiter of the query.
     *
     * @return Token
     */
    public function parseDelimiter()
    {
        $idx = 0;

        while ($idx < $this->delimiterLen && $this->last + $idx < $this->len) {
            if ($this->delimiter[$idx] !== $this->str[$this->last + $idx]) {
                return null;
            }
            ++$idx;
        }

        $this->last += $this->delimiterLen - 1;

        return new Token($this->delimiter, Token::TYPE_DELIMITER);
    }
}
